2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
5 * Abstract class providing common methods for XML_Feed_Parser feeds.
9 * LICENSE: This source file is subject to version 3.0 of the PHP license
10 * that is available through the world-wide-web at the following URI:
11 * http://www.php.net/license/3_0.txt. If you did not receive a copy of
12 * the PHP License and are unable to obtain it through the web, please
13 * send a note to license@php.net so we can mail you a copy immediately.
16 * @package XML_Feed_Parser
17 * @author James Stewart <james@jystewart.net>
18 * @copyright 2005 James Stewart <james@jystewart.net>
19 * @license http://www.gnu.org/copyleft/lesser.html GNU LGPL 2.1
20 * @version CVS: $Id: Type.php,v 1.25 2008/03/08 18:39:09 jystewart Exp $
21 * @link http://pear.php.net/package/XML_Feed_Parser/
25 * This abstract class provides some general methods that are likely to be
26 * implemented exactly the same way for all feed types.
28 * @package XML_Feed_Parser
29 * @author James Stewart <james@jystewart.net>
30 * @version Release: 1.0.3
32 abstract class XML_Feed_Parser_Type
35 * Where we store our DOM object for this feed
41 * For iteration we'll want a count of the number of entries
44 public $numberEntries;
47 * Where we store our entry objects once instantiated
50 public $entries = array();
53 * Store mappings between entry IDs and their position in the feed
55 public $idMappings = array();
58 * Proxy to allow use of element names as method names
60 * We are not going to provide methods for every entry type so this
61 * function will allow for a lot of mapping. We rely pretty heavily
62 * on this to handle our mappings between other feed types and atom.
64 * @param string $call - the method attempted
65 * @param array $arguments - arguments to that method
68 function __call($call, $arguments = array())
70 if (! is_array($arguments)) {
74 if (isset($this->compatMap[$call])) {
75 $tempMap = $this->compatMap;
76 $tempcall = array_pop($tempMap[$call]);
77 if (! empty($tempMap)) {
78 $arguments = array_merge($arguments, $tempMap[$call]);
83 /* To be helpful, we allow a case-insensitive search for this method */
84 if (! isset($this->map[$call])) {
85 foreach (array_keys($this->map) as $key) {
86 if (strtoupper($key) == strtoupper($call)) {
93 if (empty($this->map[$call])) {
97 $method = 'get' . $this->map[$call][0];
98 if ($method == 'getLink') {
99 $offset = empty($arguments[0]) ? 0 : $arguments[0];
100 $attribute = empty($arguments[1]) ? 'href' : $arguments[1];
101 $params = isset($arguments[2]) ? $arguments[2] : array();
102 return $this->getLink($offset, $attribute, $params);
104 if (method_exists($this, $method)) {
105 return $this->$method($call, $arguments);
112 * Proxy to allow use of element names as attribute names
114 * For many elements variable-style access will be desirable. This function
117 * @param string $value - the variable required
120 function __get($value)
122 return $this->__call($value, array());
126 * Utility function to help us resolve xml:base values
128 * We have other methods which will traverse the DOM and work out the different
129 * xml:base declarations we need to be aware of. We then need to combine them.
130 * If a declaration starts with a protocol then we restart the string. If it
131 * starts with a / then we add on to the domain name. Otherwise we simply tag
134 * @param string $base - the base to add the link to
135 * @param string $link
137 function combineBases($base, $link)
139 if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
141 } else if (preg_match('/^\//', $link)) {
142 /* Extract domain and suffix link to that */
143 preg_match('/^([A-Za-z]+:\/\/.*)?\/*/', $base, $results);
144 $firstLayer = $results[0];
145 return $firstLayer . "/" . $link;
146 } else if (preg_match('/^\.\.\//', $base)) {
147 /* Step up link to find place to be */
148 preg_match('/^((\.\.\/)+)(.*)$/', $link, $bases);
150 $count = preg_match_all('/\.\.\//', $bases[1], $steps);
151 $url = explode("/", $base);
152 for ($i = 0; $i <= $count; $i++) {
155 return implode("/", $url) . "/" . $suffix;
156 } else if (preg_match('/^(?!\/$)/', $base)) {
157 $base = preg_replace('/(.*\/).*$/', '$1', $base) ;
158 return $base . $link;
160 /* Just stick it on the end */
161 return $base . $link;
166 * Determine whether we need to apply our xml:base rules
168 * Gets us the xml:base data and then processes that with regard
169 * to our current link.
175 function addBase($link, $element)
177 if (preg_match('/^[A-Za-z]+:\/\//', $link)) {
181 return $this->combineBases($element->baseURI, $link);
185 * Get an entry by its position in the feed, starting from zero
187 * As well as allowing the items to be iterated over we want to allow
188 * users to be able to access a specific entry. This is one of two ways of
189 * doing that, the other being by ID.
192 * @return XML_Feed_Parser_RSS1Element
194 function getEntryByOffset($offset)
196 if (! isset($this->entries[$offset])) {
197 $entries = $this->model->getElementsByTagName($this->itemElement);
198 if ($entries->length > $offset) {
199 $xmlBase = $entries->item($offset)->baseURI;
200 $this->entries[$offset] = new $this->itemClass(
201 $entries->item($offset), $this, $xmlBase);
202 if ($id = $this->entries[$offset]->id) {
203 $this->idMappings[$id] = $this->entries[$offset];
206 throw new XML_Feed_Parser_Exception('No entries found');
210 return $this->entries[$offset];
214 * Return a date in seconds since epoch.
216 * Get a date construct. We use PHP's strtotime to return it as a unix datetime, which
217 * is the number of seconds since 1970-01-01 00:00:00.
219 * @link http://php.net/strtotime
220 * @param string $method The name of the date construct we want
221 * @param array $arguments Included for compatibility with our __call usage
222 * @return int|false datetime
224 protected function getDate($method, $arguments)
226 $time = $this->model->getElementsByTagName($method);
227 if ($time->length == 0 || empty($time->item(0)->nodeValue)) {
230 return strtotime($time->item(0)->nodeValue);
234 * Get a text construct.
236 * @param string $method The name of the text construct we want
237 * @param array $arguments Included for compatibility with our __call usage
240 protected function getText($method, $arguments = array())
242 $tags = $this->model->getElementsByTagName($method);
243 if ($tags->length > 0) {
244 $value = $tags->item(0)->nodeValue;
251 * Apply various rules to retrieve category data.
253 * There is no single way of declaring a category in RSS1/1.1 as there is in RSS2
254 * and Atom. Instead the usual approach is to use the dublin core namespace to
255 * declare categories. For example delicious use both:
256 * <dc:subject>PEAR</dc:subject> and: <taxo:topics><rdf:Bag>
257 * <rdf:li resource="http://del.icio.us/tag/PEAR" /></rdf:Bag></taxo:topics>
258 * to declare a categorisation of 'PEAR'.
260 * We need to be sensitive to this where possible.
262 * @param string $call for compatibility with our overloading
263 * @param array $arguments - arg 0 is the offset, arg 1 is whether to return as array
264 * @return string|array|false
266 protected function getCategory($call, $arguments)
268 $categories = $this->model->getElementsByTagName('subject');
269 $offset = empty($arguments[0]) ? 0 : $arguments[0];
270 $array = empty($arguments[1]) ? false : true;
271 if ($categories->length <= $offset) {
276 foreach ($categories as $category) {
277 array_push($list, $category->nodeValue);
281 return $categories->item($offset)->nodeValue;
285 * Count occurrences of an element
287 * This function will tell us how many times the element $type
288 * appears at this level of the feed.
290 * @param string $type the element we want to get a count of
293 protected function count($type)
295 if ($tags = $this->model->getElementsByTagName($type)) {
296 return $tags->length;
302 * Part of our xml:base processing code
304 * We need a couple of methods to access XHTML content stored in feeds.
305 * This is because we dereference all xml:base references before returning
306 * the element. This method handles the attributes.
308 * @param DOMElement $node The DOM node we are iterating over
311 function processXHTMLAttributes($node) {
313 foreach ($node->attributes as $attribute) {
314 if ($attribute->name == 'src' or $attribute->name == 'href') {
315 $attribute->value = $this->addBase(htmlentities($attribute->value, NULL, 'utf-8'), $attribute);
317 if ($attribute->name == 'base') {
320 $return .= $attribute->name . '="' . htmlentities($attribute->value, NULL, 'utf-8') .'" ';
322 if (! empty($return)) {
323 return ' ' . trim($return);
329 * Convert HTML entities based on the current character set.
334 function processEntitiesForNodeValue($node)
336 if (function_exists('iconv')) {
337 $current_encoding = $node->ownerDocument->encoding;
338 $value = iconv($current_encoding, 'UTF-8', $node->nodeValue);
339 } else if ($current_encoding == 'iso-8859-1') {
340 $value = utf8_encode($node->nodeValue);
342 $value = $node->nodeValue;
345 $decoded = html_entity_decode($value, NULL, 'UTF-8');
346 return htmlentities($decoded, NULL, 'UTF-8');
350 * Part of our xml:base processing code
352 * We need a couple of methods to access XHTML content stored in feeds.
353 * This is because we dereference all xml:base references before returning
354 * the element. This method recurs through the tree descending from the node
355 * and builds our string.
357 * @param DOMElement $node The DOM node we are processing
360 function traverseNode($node)
364 /* Add the opening of this node to the content */
365 if ($node instanceof DOMElement) {
366 $content .= '<' . $node->tagName .
367 $this->processXHTMLAttributes($node) . '>';
370 /* Process children */
371 if ($node->hasChildNodes()) {
372 foreach ($node->childNodes as $child) {
373 $content .= $this->traverseNode($child);
377 if ($node instanceof DOMText) {
378 $content .= $this->processEntitiesForNodeValue($node);
381 /* Add the closing of this node to the content */
382 if ($node instanceof DOMElement) {
383 $content .= '</' . $node->tagName . '>';
390 * Get content from RSS feeds (atom has its own implementation)
392 * The official way to include full content in an RSS1 entry is to use
393 * the content module's element 'encoded', and RSS2 feeds often duplicate that.
394 * Often, however, the 'description' element is used instead. We will offer that
395 * as a fallback. Atom uses its own approach and overrides this method.
397 * @return string|false
399 protected function getContent()
401 $options = array('encoded', 'description');
402 foreach ($options as $element) {
403 $test = $this->model->getElementsByTagName($element);
404 if ($test->length == 0) {
407 if ($test->item(0)->hasChildNodes()) {
409 foreach ($test->item(0)->childNodes as $child) {
410 if ($child instanceof DOMText) {
411 $value .= $child->nodeValue;
413 $simple = simplexml_import_dom($child);
414 $value .= $simple->asXML();
418 } else if ($test->length > 0) {
419 return $test->item(0)->nodeValue;
426 * Checks if this element has a particular child element.
432 function hasKey($name, $offset = 0)
434 $search = $this->model->getElementsByTagName($name);
435 return $search->length > $offset;
439 * Return an XML serialization of the feed, should it be required. Most
440 * users however, will already have a serialization that they used when
441 * instantiating the object.
443 * @return string XML serialization of element
445 function __toString()
447 $simple = simplexml_import_dom($this->model);
448 return $simple->asXML();
452 * Get directory holding RNG schemas. Method is based on that
453 * found in Contact_AddressBook.
455 * @return string PEAR data directory.
459 static function getSchemaDir()
461 require_once 'PEAR/Config.php';
462 $config = new PEAR_Config;
463 return $config->get('data_dir') . '/XML_Feed_Parser/schemas';