1 <?php
  2 /**
  3  * @package     Joomla.Platform
  4  * @subpackage  Feed
  5  *
  6  * @copyright   Copyright (C) 2005 - 2017 Open Source Matters, Inc. All rights reserved.
  7  * @license     GNU General Public License version 2 or later; see LICENSE
  8  */
  9 
 10 defined('JPATH_PLATFORM') or die;
 11 
 12 /**
 13  * Feed Parser class.
 14  *
 15  * @since  12.3
 16  */
 17 abstract class JFeedParser
 18 {
 19     /**
 20      * The feed element name for the entry elements.
 21      *
 22      * @var    string
 23      * @since  12.3
 24      */
 25     protected $entryElementName = 'entry';
 26 
 27     /**
 28      * Array of JFeedParserNamespace objects
 29      *
 30      * @var    array
 31      * @since  12.3
 32      */
 33     protected $namespaces = array();
 34 
 35     /**
 36      * The XMLReader stream object for the feed.
 37      *
 38      * @var    XMLReader
 39      * @since  12.3
 40      */
 41     protected $stream;
 42 
 43     /**
 44      * Constructor.
 45      *
 46      * @param   XMLReader  $stream  The XMLReader stream object for the feed.
 47      *
 48      * @since   12.3
 49      */
 50     public function __construct(XMLReader $stream)
 51     {
 52         $this->stream  = $stream;
 53     }
 54 
 55     /**
 56      * Method to parse the feed into a JFeed object.
 57      *
 58      * @return  JFeed
 59      *
 60      * @since   12.3
 61      */
 62     public function parse()
 63     {
 64         $feed = new JFeed;
 65 
 66         // Detect the feed version.
 67         $this->initialise();
 68 
 69         // Let's get this party started...
 70         do
 71         {
 72             // Expand the element for processing.
 73             $el = new SimpleXMLElement($this->stream->readOuterXml());
 74 
 75             // Get the list of namespaces used within this element.
 76             $ns = $el->getNamespaces(true);
 77 
 78             // Get an array of available namespace objects for the element.
 79             $namespaces = array();
 80 
 81             foreach ($ns as $prefix => $uri)
 82             {
 83                 // Ignore the empty namespace prefix.
 84                 if (empty($prefix))
 85                 {
 86                     continue;
 87                 }
 88 
 89                 // Get the necessary namespace objects for the element.
 90                 $namespace = $this->fetchNamespace($prefix);
 91 
 92                 if ($namespace)
 93                 {
 94                     $namespaces[] = $namespace;
 95                 }
 96             }
 97 
 98             // Process the element.
 99             $this->processElement($feed, $el, $namespaces);
100 
101             // Skip over this element's children since it has been processed.
102             $this->moveToClosingElement();
103         }
104 
105         while ($this->moveToNextElement());
106 
107         return $feed;
108     }
109 
110     /**
111      * Method to register a namespace handler object.
112      *
113      * @param   string                $prefix     The XML namespace prefix for which to register the namespace object.
114      * @param   JFeedParserNamespace  $namespace  The namespace object to register.
115      *
116      * @return  JFeed
117      *
118      * @since   12.3
119      */
120     public function registerNamespace($prefix, JFeedParserNamespace $namespace)
121     {
122         $this->namespaces[$prefix] = $namespace;
123 
124         return $this;
125     }
126 
127     /**
128      * Method to initialise the feed for parsing.  If child parsers need to detect versions or other
129      * such things this is where you'll want to implement that logic.
130      *
131      * @return  void
132      *
133      * @since   12.3
134      */
135     abstract protected function initialise();
136 
137     /**
138      * Method to parse a specific feed element.
139      *
140      * @param   JFeed             $feed        The JFeed object being built from the parsed feed.
141      * @param   SimpleXMLElement  $el          The current XML element object to handle.
142      * @param   array             $namespaces  The array of relevant namespace objects to process for the element.
143      *
144      * @return  void
145      *
146      * @since   12.3
147      */
148     protected function processElement(JFeed $feed, SimpleXMLElement $el, array $namespaces)
149     {
150         // Build the internal method name.
151         $method = 'handle' . ucfirst($el->getName());
152 
153         // If we are dealing with an item then it is feed entry time.
154         if ($el->getName() == $this->entryElementName)
155         {
156             // Create a new feed entry for the item.
157             $entry = new JFeedEntry;
158 
159             // First call the internal method.
160             $this->processFeedEntry($entry, $el);
161 
162             foreach ($namespaces as $namespace)
163             {
164                 if ($namespace instanceof JFeedParserNamespace)
165                 {
166                     $namespace->processElementForFeedEntry($entry, $el);
167                 }
168             }
169 
170             // Add the new entry to the feed.
171             $feed->addEntry($entry);
172 
173             return;
174         }
175         // Otherwise we treat it like any other element.
176 
177         // First call the internal method.
178         if (is_callable(array($this, $method)))
179         {
180             $this->$method($feed, $el);
181         }
182 
183         foreach ($namespaces as $namespace)
184         {
185             if ($namespace instanceof JFeedParserNamespace)
186             {
187                 $namespace->processElementForFeed($feed, $el);
188             }
189         }
190     }
191 
192     /**
193      * Method to get a namespace object for a given namespace prefix.
194      *
195      * @param   string  $prefix  The XML prefix for which to fetch the namespace object.
196      *
197      * @return  mixed  JFeedParserNamespace or false if none exists.
198      *
199      * @since   12.3
200      */
201     protected function fetchNamespace($prefix)
202     {
203         if (isset($this->namespaces[$prefix]))
204         {
205             return $this->namespaces[$prefix];
206         }
207 
208         $className = get_class($this) . ucfirst($prefix);
209 
210         if (class_exists($className))
211         {
212             $this->namespaces[$prefix] = new $className;
213 
214             return $this->namespaces[$prefix];
215         }
216 
217         return false;
218     }
219 
220     /**
221      * Method to move the stream parser to the next XML element node.
222      *
223      * @param   string  $name  The name of the element for which to move the stream forward until is found.
224      *
225      * @return  boolean  True if the stream parser is on an XML element node.
226      *
227      * @since   12.3
228      */
229     protected function moveToNextElement($name = null)
230     {
231         // Only keep looking until the end of the stream.
232         while ($this->stream->read())
233         {
234             // As soon as we get to the next ELEMENT node we are done.
235             if ($this->stream->nodeType == XMLReader::ELEMENT)
236             {
237                 // If we are looking for a specific name make sure we have it.
238                 if (isset($name) && ($this->stream->name != $name))
239                 {
240                     continue;
241                 }
242 
243                 return true;
244             }
245         }
246 
247         return false;
248     }
249 
250     /**
251      * Method to move the stream parser to the closing XML node of the current element.
252      *
253      * @return  void
254      *
255      * @since   12.3
256      * @throws  RuntimeException  If the closing tag cannot be found.
257      */
258     protected function moveToClosingElement()
259     {
260         // If we are on a self-closing tag then there is nothing to do.
261         if ($this->stream->isEmptyElement)
262         {
263             return;
264         }
265 
266         // Get the name and depth for the current node so that we can match the closing node.
267         $name  = $this->stream->name;
268         $depth = $this->stream->depth;
269 
270         // Only keep looking until the end of the stream.
271         while ($this->stream->read())
272         {
273             // If we have an END_ELEMENT node with the same name and depth as the node we started with we have a bingo. :-)
274             if (($this->stream->name == $name) && ($this->stream->depth == $depth) && ($this->stream->nodeType == XMLReader::END_ELEMENT))
275             {
276                 return;
277             }
278         }
279 
280         throw new RuntimeException('Unable to find the closing XML node.');
281     }
282 }
283