]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/FeedSub/extlib/XML/Feed/Parser/Atom.php
Localisation updates for !StatusNet from !translatewiki.net !sntrans
[quix0rs-gnu-social.git] / plugins / FeedSub / extlib / XML / Feed / Parser / Atom.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
3
4 /**
5  * Atom feed class for XML_Feed_Parser
6  *
7  * PHP versions 5
8  *
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.
14  *
15  * @category   XML
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: Atom.php,v 1.29 2008/03/30 22:00:36 jystewart Exp $
21  * @link       http://pear.php.net/package/XML_Feed_Parser/
22 */
23
24 /**
25  * This is the class that determines how we manage Atom 1.0 feeds
26  * 
27  * How we deal with constructs:
28  *  date - return as unix datetime for use with the 'date' function unless specified otherwise
29  *  text - return as is. optional parameter will give access to attributes
30  *  person - defaults to name, but parameter based access
31  *
32  * @author    James Stewart <james@jystewart.net>
33  * @version    Release: 1.0.3
34  * @package XML_Feed_Parser
35  */
36 class XML_Feed_Parser_Atom extends XML_Feed_Parser_Type
37 {
38     /**
39      * The URI of the RelaxNG schema used to (optionally) validate the feed 
40      * @var string
41      */
42     private $relax = 'atom.rnc';
43
44     /**
45      * We're likely to use XPath, so let's keep it global 
46      * @var DOMXPath
47      */
48     public $xpath;
49
50     /**
51      * When performing XPath queries we will use this prefix 
52      * @var string
53      */
54     private $xpathPrefix = '//';
55
56     /**
57      * The feed type we are parsing 
58      * @var string
59      */
60     public $version = 'Atom 1.0';
61
62     /** 
63      * The class used to represent individual items 
64      * @var string
65      */
66     protected $itemClass = 'XML_Feed_Parser_AtomElement';
67     
68     /** 
69      * The element containing entries 
70      * @var string
71      */
72     protected $itemElement = 'entry';
73
74     /**
75      * Here we map those elements we're not going to handle individually
76      * to the constructs they are. The optional second parameter in the array
77      * tells the parser whether to 'fall back' (not apt. at the feed level) or
78      * fail if the element is missing. If the parameter is not set, the function
79      * will simply return false and leave it to the client to decide what to do.
80      * @var array
81      */
82     protected $map = array(
83         'author' => array('Person'),
84         'contributor' => array('Person'),
85         'icon' => array('Text'),
86         'logo' => array('Text'),
87         'id' => array('Text', 'fail'),
88         'rights' => array('Text'),
89         'subtitle' => array('Text'),
90         'title' => array('Text', 'fail'),
91         'updated' => array('Date', 'fail'),
92         'link' => array('Link'),
93         'generator' => array('Text'),
94         'category' => array('Category'));
95
96     /**
97      * Here we provide a few mappings for those very special circumstances in
98      * which it makes sense to map back to the RSS2 spec. Key is RSS2 version
99      * value is an array consisting of the equivalent in atom and any attributes
100      * needed to make the mapping.
101      * @var array
102      */
103     protected $compatMap = array(
104         'guid' => array('id'),
105         'links' => array('link'),
106         'tags' => array('category'),
107         'contributors' => array('contributor'));
108
109     /**
110      * Our constructor does nothing more than its parent.
111      * 
112      * @param    DOMDocument    $xml    A DOM object representing the feed
113      * @param    bool (optional) $string    Whether or not to validate this feed
114      */
115     function __construct(DOMDocument $model, $strict = false)
116     {
117         $this->model = $model;
118
119         if ($strict) {
120             if (! $this->model->relaxNGValidateSource($this->relax)) {
121                 throw new XML_Feed_Parser_Exception('Failed required validation');
122             }
123         }
124
125         $this->xpath = new DOMXPath($this->model);
126         $this->xpath->registerNamespace('atom', 'http://www.w3.org/2005/Atom');
127         $this->numberEntries = $this->count('entry');
128     }
129
130     /**
131      * Implement retrieval of an entry based on its ID for atom feeds.
132      *
133      * This function uses XPath to get the entry based on its ID. If DOMXPath::evaluate
134      * is available, we also use that to store a reference to the entry in the array
135      * used by getEntryByOffset so that method does not have to seek out the entry
136      * if it's requested that way.
137      * 
138      * @param    string    $id    any valid Atom ID.
139      * @return    XML_Feed_Parser_AtomElement
140      */
141     function getEntryById($id)
142     {
143         if (isset($this->idMappings[$id])) {
144             return $this->entries[$this->idMappings[$id]];
145         }
146
147         $entries = $this->xpath->query("//atom:entry[atom:id='$id']");
148
149         if ($entries->length > 0) {
150             $xmlBase = $entries->item(0)->baseURI;
151             $entry = new $this->itemClass($entries->item(0), $this, $xmlBase);
152             
153             if (in_array('evaluate', get_class_methods($this->xpath))) {
154                 $offset = $this->xpath->evaluate("count(preceding-sibling::atom:entry)", $entries->item(0));
155                 $this->entries[$offset] = $entry;
156             }
157
158             $this->idMappings[$id] = $entry;
159
160             return $entry;
161         }
162         
163     }
164
165     /**
166      * Retrieves data from a person construct.
167      *
168      * Get a person construct. We default to the 'name' element but allow
169      * access to any of the elements.
170      * 
171      * @param    string    $method    The name of the person construct we want
172      * @param    array     $arguments    An array which we hope gives a 'param'
173      * @return    string|false
174      */
175     protected function getPerson($method, $arguments)
176     {
177         $offset = empty($arguments[0]) ? 0 : $arguments[0];
178         $parameter = empty($arguments[1]['param']) ? 'name' : $arguments[1]['param'];
179         $section = $this->model->getElementsByTagName($method);
180         
181         if ($parameter == 'url') {
182             $parameter = 'uri';
183         }
184
185         if ($section->length <= $offset) {
186             return false;
187         }
188
189         $param = $section->item($offset)->getElementsByTagName($parameter);
190         if ($param->length == 0) {
191             return false;
192         }
193         return $param->item(0)->nodeValue;
194     }
195
196     /**
197      * Retrieves an element's content where that content is a text construct.
198      *
199      * Get a text construct. When calling this method, the two arguments
200      * allowed are 'offset' and 'attribute', so $parser->subtitle() would
201      * return the content of the element, while $parser->subtitle(false, 'type')
202      * would return the value of the type attribute.
203      *
204      * @todo    Clarify overlap with getContent()
205      * @param    string    $method    The name of the text construct we want
206      * @param    array     $arguments    An array which we hope gives a 'param'
207      * @return    string
208      */
209     protected function getText($method, $arguments)
210     {
211         $offset = empty($arguments[0]) ? 0: $arguments[0];
212         $attribute = empty($arguments[1]) ? false : $arguments[1];
213         $tags = $this->model->getElementsByTagName($method);
214
215         if ($tags->length <= $offset) {
216             return false;
217         }
218
219         $content = $tags->item($offset);
220
221         if (! $content->hasAttribute('type')) {
222             $content->setAttribute('type', 'text');
223         }
224         $type = $content->getAttribute('type');
225
226         if (! empty($attribute) and 
227             ! ($method == 'generator' and $attribute == 'name')) {
228             if ($content->hasAttribute($attribute)) {
229                 return $content->getAttribute($attribute);
230             } else if ($attribute == 'href' and $content->hasAttribute('uri')) {
231                 return $content->getAttribute('uri');
232             }
233             return false;
234         }
235
236         return $this->parseTextConstruct($content);
237     }
238     
239     /**
240      * Extract content appropriately from atom text constructs
241      *
242      * Because of different rules applied to the content element and other text
243      * constructs, they are deployed as separate functions, but they share quite
244      * a bit of processing. This method performs the core common process, which is
245      * to apply the rules for different mime types in order to extract the content.
246      *
247      * @param   DOMNode $content    the text construct node to be parsed
248      * @return String
249      * @author James Stewart
250      **/
251     protected function parseTextConstruct(DOMNode $content)
252     {
253         if ($content->hasAttribute('type')) {
254             $type = $content->getAttribute('type');
255         } else {
256             $type = 'text';
257         }
258
259         if (strpos($type, 'text/') === 0) {
260             $type = 'text';
261         }
262
263         switch ($type) {
264             case 'text':
265             case 'html':
266                 return $content->textContent;
267                 break;
268             case 'xhtml':
269                 $container = $content->getElementsByTagName('div');
270                 if ($container->length == 0) {
271                     return false;
272                 }
273                 $contents = $container->item(0);
274                 if ($contents->hasChildNodes()) {
275                     /* Iterate through, applying xml:base and store the result */
276                     $result = '';
277                     foreach ($contents->childNodes as $node) {
278                         $result .= $this->traverseNode($node);
279                     }
280                     return $result;
281                 }
282                 break;
283             case preg_match('@^[a-zA-Z]+/[a-zA-Z+]*xml@i', $type) > 0:
284                 return $content;
285                 break;
286             case 'application/octet-stream':
287             default:
288                 return base64_decode(trim($content->nodeValue));
289                 break;
290         }
291         return false;
292     }
293     /**
294      * Get a category from the entry.
295      *
296      * A feed or entry can have any number of categories. A category can have the
297      * attributes term, scheme and label.
298      * 
299      * @param    string    $method    The name of the text construct we want
300      * @param    array     $arguments    An array which we hope gives a 'param'
301      * @return    string
302      */
303     function getCategory($method, $arguments)
304     {
305         $offset = empty($arguments[0]) ? 0: $arguments[0];
306         $attribute = empty($arguments[1]) ? 'term' : $arguments[1];
307         $categories = $this->model->getElementsByTagName('category');
308         if ($categories->length <= $offset) {
309             $category = $categories->item($offset);
310             if ($category->hasAttribute($attribute)) {
311                 return $category->getAttribute($attribute);
312             }
313         }
314         return false;
315     }
316
317     /**
318      * This element must be present at least once with rel="feed". This element may be 
319      * present any number of further times so long as there is no clash. If no 'rel' is 
320      * present and we're asked for one, we follow the example of the Universal Feed
321      * Parser and presume 'alternate'.
322      *
323      * @param    int    $offset    the position of the link within the container
324      * @param    string    $attribute    the attribute name required
325      * @param    array     an array of attributes to search by
326      * @return    string    the value of the attribute
327      */
328     function getLink($offset = 0, $attribute = 'href', $params = false)
329     {
330         if (is_array($params) and !empty($params)) {
331             $terms = array();
332             $alt_predicate = '';
333             $other_predicate = '';
334
335             foreach ($params as $key => $value) {
336                 if ($key == 'rel' && $value == 'alternate') {
337                     $alt_predicate = '[not(@rel) or @rel="alternate"]';
338                 } else {
339                     $terms[] = "@$key='$value'";
340                 }
341             }
342             if (!empty($terms)) {
343                 $other_predicate = '[' . join(' and ', $terms) . ']';
344             }
345             $query =  $this->xpathPrefix . 'atom:link' . $alt_predicate . $other_predicate;
346             $links = $this->xpath->query($query);
347         } else {
348             $links = $this->model->getElementsByTagName('link');
349         }
350         if ($links->length > $offset) {
351             if ($links->item($offset)->hasAttribute($attribute)) {
352                 $value = $links->item($offset)->getAttribute($attribute);
353                 if ($attribute == 'href') {
354                     $value = $this->addBase($value, $links->item($offset));
355                 }
356                 return $value;
357             } else if ($attribute == 'rel') {
358                 return 'alternate';
359             }
360         }
361         return false;
362     }
363 }
364
365 ?>