1<?php
2/**
3 * Atom Syndication Format PHP Library
4 *
5 * @package AtomLib
6 * @link http://code.google.com/p/phpatomlib/
7 *
8 * @author Elias Torres <elias@torrez.us>
9 * @version 0.4
10 * @since 2.3.0
11 */
12
13/**
14 * Structure that store common Atom Feed Properties
15 *
16 * @package AtomLib
17 */
18class AtomFeed {
19 /**
20 * Stores Links
21 * @var array
22 * @access public
23 */
24 var $links = array();
25 /**
26 * Stores Categories
27 * @var array
28 * @access public
29 */
30 var $categories = array();
31 /**
32 * Stores Entries
33 *
34 * @var array
35 * @access public
36 */
37 var $entries = array();
38}
39
40/**
41 * Structure that store Atom Entry Properties
42 *
43 * @package AtomLib
44 */
45class AtomEntry {
46 /**
47 * Stores Links
48 * @var array
49 * @access public
50 */
51 var $links = array();
52 /**
53 * Stores Categories
54 * @var array
55 * @access public
56 */
57 var $categories = array();
58}
59
60/**
61 * AtomLib Atom Parser API
62 *
63 * @package AtomLib
64 */
65class AtomParser {
66
67 var $NS = 'http://www.w3.org/2005/Atom';
68 var $ATOM_CONTENT_ELEMENTS = array('content','summary','title','subtitle','rights');
69 var $ATOM_SIMPLE_ELEMENTS = array('id','updated','published','draft');
70
71 var $debug = false;
72
73 var $depth = 0;
74 var $indent = 2;
75 var $in_content;
76 var $ns_contexts = array();
77 var $ns_decls = array();
78 var $content_ns_decls = array();
79 var $content_ns_contexts = array();
80 var $is_xhtml = false;
81 var $is_html = false;
82 var $is_text = true;
83 var $skipped_div = false;
84
85 var $FILE = "php://input";
86
87 var $feed;
88 var $current;
89 var $map_attrs_func;
90 var $map_xmlns_func;
91 var $error;
92 var $content;
93
94 /**
95 * PHP5 constructor.
96 */
97 function __construct() {
98
99 $this->feed = new AtomFeed();
100 $this->current = null;
101 $this->map_attrs_func = array( __CLASS__, 'map_attrs' );
102 $this->map_xmlns_func = array( __CLASS__, 'map_xmlns' );
103 }
104
105 /**
106 * PHP4 constructor.
107 */
108 public function AtomParser() {
109 self::__construct();
110 }
111
112 /**
113 * Map attributes to key="val"
114 *
115 * @param string $k Key
116 * @param string $v Value
117 * @return string
118 */
119 public static function map_attrs($k, $v) {
120 return "$k=\"$v\"";
121 }
122
123 /**
124 * Map XML namespace to string.
125 *
126 * @param indexish $p XML Namespace element index
127 * @param array $n Two-element array pair. [ 0 => {namespace}, 1 => {url} ]
128 * @return string 'xmlns="{url}"' or 'xmlns:{namespace}="{url}"'
129 */
130 public static function map_xmlns($p, $n) {
131 $xd = "xmlns";
132 if( 0 < strlen($n[0]) ) {
133 $xd .= ":{$n[0]}";
134 }
135 return "{$xd}=\"{$n[1]}\"";
136 }
137
138 function _p($msg) {
139 if($this->debug) {
140 print str_repeat(" ", $this->depth * $this->indent) . $msg ."\n";
141 }
142 }
143
144 function error_handler($log_level, $log_text, $error_file, $error_line) {
145 $this->error = $log_text;
146 }
147
148 function parse() {
149
150 set_error_handler(array(&$this, 'error_handler'));
151
152 array_unshift($this->ns_contexts, array());
153
154 if ( ! function_exists( 'xml_parser_create_ns' ) ) {
155 trigger_error( __( "PHP's XML extension is not available. Please contact your hosting provider to enable PHP's XML extension." ) );
156 return false;
157 }
158
159 $parser = xml_parser_create_ns();
160 xml_set_element_handler($parser, array($this, "start_element"), array($this, "end_element"));
161 xml_parser_set_option($parser,XML_OPTION_CASE_FOLDING,0);
162 xml_parser_set_option($parser,XML_OPTION_SKIP_WHITE,0);
163 xml_set_character_data_handler($parser, array($this, "cdata"));
164 xml_set_default_handler($parser, array($this, "_default"));
165 xml_set_start_namespace_decl_handler($parser, array($this, "start_ns"));
166 xml_set_end_namespace_decl_handler($parser, array($this, "end_ns"));
167
168 $this->content = '';
169
170 $ret = true;
171
172 $fp = fopen($this->FILE, "r");
173 while ($data = fread($fp, 4096)) {
174 if($this->debug) $this->content .= $data;
175
176 if(!xml_parse($parser, $data, feof($fp))) {
177 /* translators: 1: Error message, 2: Line number. */
178 trigger_error(sprintf(__('XML Error: %1$s at line %2$s')."\n",
179 xml_error_string(xml_get_error_code($parser)),
180 xml_get_current_line_number($parser)));
181 $ret = false;
182 break;
183 }
184 }
185 fclose($fp);
186
187 if (PHP_VERSION_ID < 80000) { // xml_parser_free() has no effect as of PHP 8.0.
188 xml_parser_free($parser);
189 }
190
191 unset($parser);
192
193 restore_error_handler();
194
195 return $ret;
196 }
197
198 function start_element($parser, $name, $attrs) {
199
200 $name_parts = explode(":", $name);
201 $tag = array_pop($name_parts);
202
203 switch($name) {
204 case $this->NS . ':feed':
205 $this->current = $this->feed;
206 break;
207 case $this->NS . ':entry':
208 $this->current = new AtomEntry();
209 break;
210 };
211
212 $this->_p("start_element('$name')");
213 #$this->_p(print_r($this->ns_contexts,true));
214 #$this->_p('current(' . $this->current . ')');
215
216 array_unshift($this->ns_contexts, $this->ns_decls);
217
218 $this->depth++;
219
220 if(!empty($this->in_content)) {
221
222 $this->content_ns_decls = array();
223
224 if($this->is_html || $this->is_text)
225 trigger_error("Invalid content in element found. Content must not be of type text or html if it contains markup.");
226
227 $attrs_prefix = array();
228
229 // resolve prefixes for attributes
230 foreach($attrs as $key => $value) {
231 $with_prefix = $this->ns_to_prefix($key, true);
232 $attrs_prefix[$with_prefix[1]] = $this->xml_escape($value);
233 }
234
235 $attrs_str = join(' ', array_map($this->map_attrs_func, array_keys($attrs_prefix), array_values($attrs_prefix)));
236 if(strlen($attrs_str) > 0) {
237 $attrs_str = " " . $attrs_str;
238 }
239
240 $with_prefix = $this->ns_to_prefix($name);
241
242 if(!$this->is_declared_content_ns($with_prefix[0])) {
243 array_push($this->content_ns_decls, $with_prefix[0]);
244 }
245
246 $xmlns_str = '';
247 if(count($this->content_ns_decls) > 0) {
248 array_unshift($this->content_ns_contexts, $this->content_ns_decls);
249 $xmlns_str .= join(' ', array_map($this->map_xmlns_func, array_keys($this->content_ns_contexts[0]), array_values($this->content_ns_contexts[0])));
250 if(strlen($xmlns_str) > 0) {
251 $xmlns_str = " " . $xmlns_str;
252 }
253 }
254
255 array_push($this->in_content, array($tag, $this->depth, "<". $with_prefix[1] ."{$xmlns_str}{$attrs_str}" . ">"));
256
257 } else if(in_array($tag, $this->ATOM_CONTENT_ELEMENTS) || in_array($tag, $this->ATOM_SIMPLE_ELEMENTS)) {
258 $this->in_content = array();
259 $this->is_xhtml = $attrs['type'] == 'xhtml';
260 $this->is_html = $attrs['type'] == 'html' || $attrs['type'] == 'text/html';
261 $this->is_text = !in_array('type',array_keys($attrs)) || $attrs['type'] == 'text';
262 $type = $this->is_xhtml ? 'XHTML' : ($this->is_html ? 'HTML' : ($this->is_text ? 'TEXT' : $attrs['type']));
263
264 if(in_array('src',array_keys($attrs))) {
265 $this->current->$tag = $attrs;
266 } else {
267 array_push($this->in_content, array($tag,$this->depth, $type));
268 }
269 } else if($tag == 'link') {
270 array_push($this->current->links, $attrs);
271 } else if($tag == 'category') {
272 array_push($this->current->categories, $attrs);
273 }
274
275 $this->ns_decls = array();
276 }
277
278 function end_element($parser, $name) {
279
280 $name_parts = explode(":", $name);
281 $tag = array_pop($name_parts);
282
283 $ccount = count($this->in_content);
284
285 # if we are *in* content, then let's proceed to serialize it
286 if(!empty($this->in_content)) {
287 # if we are ending the original content element
288 # then let's finalize the content
289 if($this->in_content[0][0] == $tag &&
290 $this->in_content[0][1] == $this->depth) {
291 $origtype = $this->in_content[0][2];
292 array_shift($this->in_content);
293 $newcontent = array();
294 foreach($this->in_content as $c) {
295 if(count($c) == 3) {
296 array_push($newcontent, $c[2]);
297 } else {
298 if($this->is_xhtml || $this->is_text) {
299 array_push($newcontent, $this->xml_escape($c));
300 } else {
301 array_push($newcontent, $c);
302 }
303 }
304 }
305 if(in_array($tag, $this->ATOM_CONTENT_ELEMENTS)) {
306 $this->current->$tag = array($origtype, join('',$newcontent));
307 } else {
308 $this->current->$tag = join('',$newcontent);
309 }
310 $this->in_content = array();
311 } else if($this->in_content[$ccount-1][0] == $tag &&
312 $this->in_content[$ccount-1][1] == $this->depth) {
313 $this->in_content[$ccount-1][2] = substr($this->in_content[$ccount-1][2],0,-1) . "/>";
314 } else {
315 # else, just finalize the current element's content
316 $endtag = $this->ns_to_prefix($name);
317 array_push($this->in_content, array($tag, $this->depth, "</$endtag[1]>"));
318 }
319 }
320
321 array_shift($this->ns_contexts);
322
323 $this->depth--;
324
325 if($name == ($this->NS . ':entry')) {
326 array_push($this->feed->entries, $this->current);
327 $this->current = null;
328 }
329
330 $this->_p("end_element('$name')");
331 }
332
333 function start_ns($parser, $prefix, $uri) {
334 $this->_p("starting: " . $prefix . ":" . $uri);
335 array_push($this->ns_decls, array($prefix,$uri));
336 }
337
338 function end_ns($parser, $prefix) {
339 $this->_p("ending: #" . $prefix . "#");
340 }
341
342 function cdata($parser, $data) {
343 $this->_p("data: #" . str_replace(array("\n"), array("\\n"), trim($data)) . "#");
344 if(!empty($this->in_content)) {
345 array_push($this->in_content, $data);
346 }
347 }
348
349 function _default($parser, $data) {
350 # when does this gets called?
351 }
352
353
354 function ns_to_prefix($qname, $attr=false) {
355 # split 'http://www.w3.org/1999/xhtml:div' into ('http','//www.w3.org/1999/xhtml','div')
356 $components = explode(":", $qname);
357
358 # grab the last one (e.g 'div')
359 $name = array_pop($components);
360
361 if(!empty($components)) {
362 # re-join back the namespace component
363 $ns = join(":",$components);
364 foreach($this->ns_contexts as $context) {
365 foreach($context as $mapping) {
366 if($mapping[1] == $ns && strlen($mapping[0]) > 0) {
367 return array($mapping, "$mapping[0]:$name");
368 }
369 }
370 }
371 }
372
373 if($attr) {
374 return array(null, $name);
375 } else {
376 foreach($this->ns_contexts as $context) {
377 foreach($context as $mapping) {
378 if(strlen($mapping[0]) == 0) {
379 return array($mapping, $name);
380 }
381 }
382 }
383 }
384 }
385
386 function is_declared_content_ns($new_mapping) {
387 foreach($this->content_ns_contexts as $context) {
388 foreach($context as $mapping) {
389 if($new_mapping == $mapping) {
390 return true;
391 }
392 }
393 }
394 return false;
395 }
396
397 function xml_escape($content)
398 {
399 return str_replace(array('&','"',"'",'<','>'),
400 array('&','"',''','<','>'),
401 $content );
402 }
403}
404