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