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('<', '<', $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 |
?>
|