XML_Feed_Parser
[ class tree: XML_Feed_Parser ] [ index: XML_Feed_Parser ] [ all elements ]

Source for file Type.php

Documentation is available at Type.php

  1. <?php
  2. /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
  3. require_once 'XML/Feed/Parser/Sanitizer.php';
  4.  
  5. /**
  6.  * Abstract class providing common methods for XML_Feed_Parser feeds.
  7.  *
  8.  * PHP versions 5
  9.  *
  10.  * LICENSE: This source file is subject to version 3.0 of the PHP license
  11.  * that is available through the world-wide-web at the following URI:
  12.  * http://www.php.net/license/3_0.txt.  If you did not receive a copy of
  13.  * the PHP License and are unable to obtain it through the web, please
  14.  * send a note to license@php.net so we can mail you a copy immediately.
  15.  *
  16.  * @category   XML
  17.  * @package    XML_Feed_Parser
  18.  * @author     James Stewart <james@jystewart.net>
  19.  * @copyright  2005 James Stewart <james@jystewart.net>
  20.  * @license    http://www.gnu.org/copyleft/lesser.html  GNU LGPL 2.1
  21.  * @version    CVS: $Id$
  22.  * @link       http://pear.php.net/package/XML_Feed_Parser/
  23.  */
  24.  
  25. /**
  26.  * This abstract class provides some general methods that are likely to be
  27.  * implemented exactly the same way for all feed types.
  28.  *
  29.  * @package XML_Feed_Parser
  30.  * @author  James Stewart <james@jystewart.net>
  31.  * @version Release: @package_version@
  32.  */
  33. abstract class XML_Feed_Parser_Type
  34. {
  35.     protected $sanitizer;
  36.     /**
  37.      * Where we store our DOM object for this feed
  38.      * @var DOMDocument 
  39.      */
  40.     public $model;
  41.  
  42.     /**
  43.      * For iteration we'll want a count of the number of entries
  44.      * @var int 
  45.      */
  46.     public $numberEntries;
  47.  
  48.     /**
  49.      * Where we store our entry objects once instantiated
  50.      * @var array 
  51.      */
  52.     public $entries = array();
  53.  
  54.     /**
  55.      * Store mappings between entry IDs and their position in the feed
  56.      */
  57.     public $idMappings = array();
  58.  
  59.     /**
  60.      * Proxy to allow use of element names as method names
  61.      *
  62.      * We are not going to provide methods for every entry type so this
  63.      * function will allow for a lot of mapping. We rely pretty heavily
  64.      * on this to handle our mappings between other feed types and atom.
  65.      *
  66.      * @param   string  $call - the method attempted
  67.      * @param   array   $arguments - arguments to that method
  68.      * @return  mixed 
  69.      */
  70.     function __call($call$arguments = array())
  71.     {
  72.         if (is_array($arguments)) {
  73.             $arguments = array();
  74.         }
  75.  
  76.         if (isset($this->compatMap[$call])) {
  77.             $tempMap $this->compatMap;
  78.             $tempcall array_pop($tempMap[$call]);
  79.             if (empty($tempMap)) {
  80.                 $arguments array_merge($arguments$tempMap[$call]);
  81.             }
  82.             $call $tempcall;
  83.         }
  84.  
  85.         /* To be helpful, we allow a case-insensitive search for this method */
  86.         if (isset($this->map[$call])) {
  87.             foreach (array_keys($this->mapas $key{
  88.                 if (strtoupper($key== strtoupper($call)) {
  89.                     $call $key;
  90.                     break;
  91.                 }
  92.             }
  93.         }
  94.  
  95.         if (empty($this->map[$call])) {
  96.             return false;
  97.         }
  98.  
  99.         $method 'get' $this->map[$call][0];
  100.         if ($method == 'getLink'{
  101.             $offset = empty($arguments[0]? 0 : $arguments[0];
  102.             $attribute = empty($arguments[1]'href' $arguments[1];
  103.             $params = isset($arguments[2]$arguments[2: array();
  104.             return $this->getLink($offset$attribute$params);
  105.         }
  106.         if (method_exists($this$method)) {
  107.             return $this->$method($call$arguments);
  108.         }
  109.  
  110.         return false;
  111.     }
  112.  
  113.     public function setSanitizer(XML_Feed_Parser_Sanitizer $sanitizer{
  114.         $this->sanitizer = $sanitizer;
  115.  
  116.         foreach ($this->entries as $entry{
  117.             $entry->setSanizier($sanitizer);
  118.         }
  119.     
  120.     public function getSanitizer({
  121.         return $this->sanitizer;
  122.     }
  123.  
  124.     /**
  125.      * Proxy to allow use of element names as attribute names
  126.      *
  127.      * For many elements variable-style access will be desirable. This function
  128.      * provides for that.
  129.      *
  130.      * @param   string  $value - the variable required
  131.      * @return  mixed 
  132.      */
  133.     function __get($value)
  134.     {
  135.         return $this->__call($valuearray());
  136.     }
  137.  
  138.     /**
  139.      * Utility function to help us resolve xml:base values
  140.      *
  141.      * We have other methods which will traverse the DOM and work out the different
  142.      * xml:base declarations we need to be aware of. We then need to combine them.
  143.      * If a declaration starts with a protocol then we restart the string. If it
  144.      * starts with a / then we add on to the domain name. Otherwise we simply tag
  145.      * it on to the end.
  146.      *
  147.      * @param   string  $base - the base to add the link to
  148.      * @param   string  $link 
  149.      */
  150.     function combineBases($base$link)
  151.     {
  152.         if (preg_match('/^[A-Za-z]+:\/\//'$link)) {
  153.             return $link;
  154.         else if (preg_match('/^\//'$link)) {
  155.             /* Extract domain and suffix link to that */
  156.             preg_match('/^([A-Za-z]+:\/\/.*)?\/*/'$base$results);
  157.             $firstLayer $results[0];
  158.             return $firstLayer "/" $link;
  159.         else if (preg_match('/^\.\.\//'$base)) {
  160.             /* Step up link to find place to be */
  161.             preg_match('/^((\.\.\/)+)(.*)$/'$link$bases);
  162.             $suffix $bases[3];
  163.             $count preg_match_all('/\.\.\//'$bases[1]$steps);
  164.             $url explode("/"$base);
  165.             for ($i = 0; $i <= $count$i++{
  166.                 array_pop($url);
  167.             }
  168.             return implode("/"$url"/" $suffix;
  169.         else if (preg_match('/^(?!\/$)/'$base)) {
  170.             $base preg_replace('/(.*\/).*$/''$1'$base)  ;
  171.             return $base $link;
  172.         else {
  173.             /* Just stick it on the end */
  174.             return $base $link;
  175.         }
  176.     }
  177.  
  178.     /**
  179.      * Determine whether we need to apply our xml:base rules
  180.      *
  181.      * Gets us the xml:base data and then processes that with regard
  182.      * to our current link.
  183.      *
  184.      * @param   string 
  185.      * @param   DOMElement 
  186.      * @return  string 
  187.      */
  188.     function addBase($link$element)
  189.     {
  190.         if (preg_match('/^[A-Za-z]+:\/\//'$link)) {
  191.             return $link;
  192.         }
  193.  
  194.         return $this->combineBases($element->baseURI$link);
  195.     }
  196.  
  197.     /**
  198.      * Get an entry by its position in the feed, starting from zero
  199.      *
  200.      * As well as allowing the items to be iterated over we want to allow
  201.      * users to be able to access a specific entry. This is one of two ways of
  202.      * doing that, the other being by ID.
  203.      * 
  204.      * @param   int $offset 
  205.      * @return  XML_Feed_Parser_RSS1Element 
  206.      */
  207.     function getEntryByOffset($offset)
  208.     {
  209.         if (isset($this->entries[$offset])) {
  210.             $entries $this->model->getElementsByTagName($this->itemElement);
  211.             if ($entries->length > $offset{
  212.                 $xmlBase $entries->item($offset)->baseURI;
  213.                 /** @todo Remove this behaviour - each driver should control this better */
  214.                 /** @todo Try to avoid new here */
  215.                 $this->entries[$offset= new $this->itemClass(
  216.                     $entries->item($offset)$this$xmlBase);
  217.                 if ($id $this->entries[$offset]->id{
  218.                     $this->idMappings[$id$this->entries[$offset];
  219.                 }
  220.             else {
  221.                 throw new XML_Feed_Parser_Exception('No entries found');
  222.             }
  223.         }
  224.  
  225.         return $this->entries[$offset];
  226.     }
  227.  
  228.     /**
  229.      * Return a date in seconds since epoch.
  230.      *
  231.      * Get a date construct. We use PHP's strtotime to return it as a unix datetime, which
  232.      * is the number of seconds since 1970-01-01 00:00:00.
  233.      * 
  234.      * @link    http://php.net/strtotime
  235.      * @param    string    $method        The name of the date construct we want
  236.      * @param    array     $arguments    Included for compatibility with our __call usage
  237.      * @return    int|falsedatetime
  238.      */
  239.     protected function getDate($method$arguments)
  240.     {
  241.         $time $this->model->getElementsByTagName($method);
  242.         if ($time->length == 0 || empty($time->item(0)->nodeValue)) {
  243.             return false;
  244.         }
  245.         return strtotime($time->item(0)->nodeValue);
  246.     }
  247.  
  248.     /**
  249.      * Get a text construct.
  250.      *
  251.      * @param    string    $method    The name of the text construct we want
  252.      * @param    array     $arguments    Included for compatibility with our __call usage
  253.      * @return    string 
  254.      */
  255.     protected function getText($method$arguments = array())
  256.     {
  257.         $tags $this->model->getElementsByTagName($method);
  258.         if ($tags->length > 0{
  259.             $value $tags->item(0)->nodeValue;
  260.             return $this->sanitizer->sanitize($value);
  261.         }
  262.         return false;
  263.     }
  264.  
  265.     /**
  266.      * Apply various rules to retrieve category data.
  267.      *
  268.      * There is no single way of declaring a category in RSS1/1.1 as there is in RSS2
  269.      * and  Atom. Instead the usual approach is to use the dublin core namespace to
  270.      * declare  categories. For example delicious use both:
  271.      * <dc:subject>PEAR</dc:subject> and: <taxo:topics><rdf:Bag>
  272.      * <rdf:li resource="http://del.icio.us/tag/PEAR" /></rdf:Bag></taxo:topics>
  273.      * to declare a categorisation of 'PEAR'.
  274.      *
  275.      * We need to be sensitive to this where possible.
  276.      *
  277.      * @param    string    $call    for compatibility with our overloading
  278.      * @param   array $arguments - arg 0 is the offset, arg 1 is whether to return as array
  279.      * @return  string|array|false
  280.      */
  281.     protected function getCategory($call$arguments)
  282.     {
  283.         $categories $this->model->getElementsByTagName('subject');
  284.         $offset = empty($arguments[0]? 0 : $arguments[0];
  285.         $array = empty($arguments[1]? false : true;
  286.         if ($categories->length <= $offset{
  287.             return false;
  288.         }
  289.         if ($array{
  290.             $list = array();
  291.             foreach ($categories as $category{
  292.                 array_push($list$this->sanitizer->sanitize($category->nodeValue));
  293.             }
  294.             return $list;
  295.         }
  296.         return $this->sanitizer->sanitize($categories->item($offset)->nodeValue);
  297.     }
  298.  
  299.     /**
  300.      * Count occurrences of an element
  301.      *
  302.      * This function will tell us how many times the element $type
  303.      * appears at this level of the feed.
  304.      * 
  305.      * @param    string    $type    the element we want to get a count of
  306.      * @return    int 
  307.      */
  308.     protected function count($type)
  309.     {
  310.         if ($tags $this->model->getElementsByTagName($type)) {
  311.             return $tags->length;
  312.         }
  313.         return 0;
  314.     }
  315.  
  316.     /**
  317.      * Part of our xml:base processing code
  318.      *
  319.      * We need a couple of methods to access XHTML content stored in feeds.
  320.      * This is because we dereference all xml:base references before returning
  321.      * the element. This method handles the attributes.
  322.      *
  323.      * @param   DOMElement $node    The DOM node we are iterating over
  324.      * @return  string 
  325.      */
  326.     function processXHTMLAttributes($node{
  327.         $return '';
  328.         foreach ($node->attributes as $attribute{
  329.             if ($attribute->name == 'src' or $attribute->name == 'href'{
  330.                 $attribute->value = $this->addBase(htmlentities($attribute->valueNULL'utf-8')$attribute);
  331.             }
  332.             if ($attribute->name == 'base'{
  333.                 continue;
  334.             }
  335.             $return .= $attribute->name . '="' htmlentities($attribute->valueNULL'utf-8'.'" ';
  336.         }
  337.         if (empty($return)) {
  338.             return ' ' trim($return);
  339.         }
  340.         return '';
  341.     }
  342.  
  343.     /**
  344.      * Convert HTML entities based on the current character set.
  345.      * 
  346.      * @param String 
  347.      * @return String 
  348.      */
  349.     function processEntitiesForNodeValue($node
  350.     {
  351.         $current_encoding $node->ownerDocument->encoding;
  352.  
  353.         // Charset left blank to trigger autodetection
  354.         $decoded html_entity_decode($node->nodeValue);
  355.         $value htmlentities($decodednullstrtoupper($current_encoding));
  356.  
  357.         if (function_exists('iconv')) {
  358.             return $this->sanitizer->sanitize(
  359.                 iconv(
  360.                     strtoupper($current_encoding),
  361.                     'UTF-8',
  362.                     $value
  363.                 )
  364.             );
  365.         else if (strtoupper($current_encoding== 'ISO-8859-1'{
  366.             return $this->sanitizer->sanitize(
  367.                 utf8_encode($value)
  368.             );
  369.         }
  370.  
  371.         return $this->sanitizer->sanitize($value);
  372.     }
  373.  
  374.     /**
  375.      * Part of our xml:base processing code
  376.      *
  377.      * We need a couple of methods to access XHTML content stored in feeds.
  378.      * This is because we dereference all xml:base references before returning
  379.      * the element. This method recurs through the tree descending from the node
  380.      * and builds our string.
  381.      *
  382.      * @param   DOMElement $node    The DOM node we are processing
  383.      * @return   string 
  384.      */
  385.     function traverseNode($node)
  386.     {
  387.         $content '';
  388.  
  389.         /* Add the opening of this node to the content */
  390.         if ($node instanceof DOMElement{
  391.             $content .= '<' $node->tagName . 
  392.                 $this->processXHTMLAttributes($node'>';
  393.         }
  394.  
  395.         /* Process children */
  396.         if ($node->hasChildNodes()) {
  397.             foreach ($node->childNodes as $child{
  398.                 $content .= $this->traverseNode($child);
  399.             }
  400.         }
  401.  
  402.         if ($node instanceof DOMText{
  403.             $content .= $this->processEntitiesForNodeValue($node);
  404.         }
  405.  
  406.         /* Add the closing of this node to the content */
  407.         if ($node instanceof DOMElement{
  408.             $content .= '</' $node->tagName . '>';
  409.         }
  410.  
  411.         return $content;
  412.     }
  413.  
  414.     /**
  415.      * Get content from RSS feeds (atom has its own implementation)
  416.      *
  417.      * The official way to include full content in an RSS1 entry is to use
  418.      * the content module's element 'encoded', and RSS2 feeds often duplicate that.
  419.      * Often, however, the 'description' element is used instead. We will offer that
  420.      * as a fallback. Atom uses its own approach and overrides this method.
  421.      *
  422.      * @return  string|false
  423.      */
  424.     protected function getContent()
  425.     {
  426.         $options = array('encoded''description');
  427.         foreach ($options as $element{
  428.             $test $this->model->getElementsByTagName($element);
  429.             if ($test->length == 0{
  430.                 continue;
  431.             }
  432.             if ($test->item(0)->hasChildNodes()) {
  433.                 $value '';
  434.                 foreach ($test->item(0)->childNodes as $child{
  435.                     if ($child instanceof DOMText{
  436.                         $value .= $this->sanitizer->sanitize($child->nodeValue);
  437.                     else {
  438.                         $simple simplexml_import_dom($child);
  439.                         $value .= $simple->asXML();
  440.                     }
  441.                 }
  442.                 return $value;
  443.             else if ($test->length > 0{
  444.                 return $this->sanitizer->sanitize($test->item(0)->nodeValue);
  445.             }
  446.         }
  447.         return false;
  448.     }
  449.  
  450.     /**
  451.      * Checks if this element has a particular child element.
  452.      *
  453.      * @param   String 
  454.      * @param   Integer 
  455.      * @return  bool 
  456.      ***/
  457.     function hasKey($name$offset = 0)
  458.     {
  459.         $search $this->model->getElementsByTagName($name);
  460.         return $search->length > $offset;
  461.     }
  462.  
  463.     /**
  464.      * Return an XML serialization of the feed, should it be required. Most
  465.      * users however, will already have a serialization that they used when
  466.      * instantiating the object.
  467.      *
  468.      * @return    string    XML serialization of element
  469.      */    
  470.     function __toString()
  471.     {
  472.         $simple simplexml_import_dom($this->model);
  473.         return $simple->asXML();
  474.     }
  475.     
  476.     /**
  477.      * Get directory holding RNG schemas. Method is based on that
  478.      * found in Contact_AddressBook.
  479.      *
  480.      * @return string PEAR data directory.
  481.      * @access public
  482.      * @static
  483.      */
  484.     static function getSchemaDir()
  485.     {
  486.         require_once 'PEAR/Config.php';
  487.         $config = new PEAR_Config;
  488.         return $config->get('data_dir''/XML_Feed_Parser/schemas';
  489.     }
  490.  
  491.     public function relaxNGValidate({
  492.         $dir = self::getSchemaDir();
  493.  
  494.         $path $dir '/' $this->relax;
  495.  
  496.         return $this->model->relaxNGValidate($path);
  497.     }
  498. }

Documentation generated on Mon, 11 Mar 2019 15:47:22 -0400 by phpDocumentor 1.4.4. PEAR Logo Copyright © PHP Group 2004.