source: branches/beta/html/test/adachi/LLReader/Lib/XML/Feed/Parser/Atom.php @ 14676

Revision 14676, 12.7 KB checked in by adati, 17 years ago (diff)

1.4.0a-betaのマージ

Line 
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.25 2007/03/26 12:49:05 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.2
34 * @package XML_Feed_Parser
35 */
36class 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->itemElement($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        return $this->parseTextConstruct($content);
236    }
237   
238    /**
239     * Extract content appropriately from atom text constructs
240     *
241     * Because of different rules applied to the content element and other text
242     * constructs, they are deployed as separate functions, but they share quite
243     * a bit of processing. This method performs the core common process, which is
244     * to apply the rules for different mime types in order to extract the content.
245     *
246     * @param   DOMNode $content    the text construct node to be parsed
247     * @return String
248     * @author James Stewart
249     **/
250    protected function parseTextConstruct(DOMNode $content)
251    {
252        if ($content->hasAttribute('type')) {
253            $type = $content->getAttribute('type');
254        } else {
255            $type = 'text';
256        }
257
258        if (strpos($type, 'text/') === 0) {
259            $type = 'text';
260        }
261        switch ($type) {
262            case 'text':
263                return $content->nodeValue;
264                break;
265            case 'html':
266                return str_replace('&lt;', '<', $content->nodeValue);
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 utf8_decode($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?>
Note: See TracBrowser for help on using the repository browser.