Subversion Repositories Applications.papyrus

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
1527 jp_milcent 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.2 2007-07-25 15:05:34 jp_milcent 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
 */
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->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
?>