magpie's modified version of Snoopy class
[moodle.git] / rss / magpie / rss_parse.inc
1 <?php
3 /**
4 * Project:     MagpieRSS: a simple RSS integration tool
5 * File:        rss_parse.inc  - parse an RSS or Atom feed
6 *               return as a simple object.
7 *
8 * Handles RSS 0.9x, RSS 2.0, RSS 1.0, and Atom 0.3
9 *
10 * The lastest version of MagpieRSS can be obtained from:
11 * http://magpierss.sourceforge.net
12 *
13 * For questions, help, comments, discussion, etc., please join the
14 * Magpie mailing list:
15 * magpierss-general@lists.sourceforge.net
16 *
17 * @author           Kellan Elliott-McCrea <kellan@protest.net>
18 * @version          0.7a
19 * @license          GPL
20 *
21 */
23 define('RSS', 'RSS');
24 define('ATOM', 'Atom');
26 require_once (MAGPIE_DIR . 'rss_utils.inc');
28 /**
29 * Hybrid parser, and object, takes RSS as a string and returns a simple object.
30 *
31 * see: rss_fetch.inc for a simpler interface with integrated caching support
32 *
33 */
34 class MagpieRSS {
35     var $parser;
36     
37     var $current_item   = array();  // item currently being parsed
38     var $items          = array();  // collection of parsed items
39     var $channel        = array();  // hash of channel fields
40     var $textinput      = array();
41     var $image          = array();
42     var $feed_type;
43     var $feed_version;
44     var $encoding       = '';       // output encoding of parsed rss
45     
46     var $_source_encoding = '';     // only set if we have to parse xml prolog
47     
48     var $ERROR = "";
49     var $WARNING = "";
50     
51     // define some constants
52     
53     var $_CONTENT_CONSTRUCTS = array('content', 'summary', 'info', 'title', 'tagline', 'copyright');
54     var $_KNOWN_ENCODINGS    = array('UTF-8', 'US-ASCII', 'ISO-8859-1');
56     // parser variables, useless if you're not a parser, treat as private
57     var $stack              = array(); // parser stack
58     var $inchannel          = false;
59     var $initem             = false;
60     var $incontent          = false; // if in Atom <content mode="xml"> field 
61     var $intextinput        = false;
62     var $inimage            = false;
63     var $current_field      = '';
64     var $current_namespace  = false;
65     
67     /**
68      *  Set up XML parser, parse source, and return populated RSS object..
69      *   
70      *  @param string $source           string containing the RSS to be parsed
71      *
72      *  NOTE:  Probably a good idea to leave the encoding options alone unless
73      *         you know what you're doing as PHP's character set support is
74      *         a little weird.
75      *
76      *  NOTE:  A lot of this is unnecessary but harmless with PHP5 
77      *
78      *
79      *  @param string $output_encoding  output the parsed RSS in this character 
80      *                                  set defaults to ISO-8859-1 as this is PHP's
81      *                                  default.
82      *
83      *                                  NOTE: might be changed to UTF-8 in future
84      *                                  versions.
85      *                               
86      *  @param string $input_encoding   the character set of the incoming RSS source. 
87      *                                  Leave blank and Magpie will try to figure it
88      *                                  out.
89      *                                  
90      *                                   
91      *  @param bool   $detect_encoding  if false Magpie won't attempt to detect
92      *                                  source encoding. (caveat emptor)
93      *
94      */
95     function MagpieRSS ($source, $output_encoding='ISO-8859-1', 
96                         $input_encoding=null, $detect_encoding=true) 
97     {   
98         # if PHP xml isn't compiled in, die
99         #
100         if (!function_exists('xml_parser_create')) {
101             $this->error( "Failed to load PHP's XML Extension. " . 
102                           "http://www.php.net/manual/en/ref.xml.php",
103                            E_USER_ERROR );
104         }
105         
106         list($parser, $source) = $this->create_parser($source, 
107                 $output_encoding, $input_encoding, $detect_encoding);
108         
109         
110         if (!is_resource($parser)) {
111             $this->error( "Failed to create an instance of PHP's XML parser. " .
112                           "http://www.php.net/manual/en/ref.xml.php",
113                           E_USER_ERROR );
114         }
116         
117         $this->parser = $parser;
118         
119         # pass in parser, and a reference to this object
120         # setup handlers
121         #
122         xml_set_object( $this->parser, $this );
123         xml_set_element_handler($this->parser, 
124                 'feed_start_element', 'feed_end_element' );
125                         
126         xml_set_character_data_handler( $this->parser, 'feed_cdata' ); 
127     
128         $status = xml_parse( $this->parser, $source );
129         
130         if (! $status ) {
131             $errorcode = xml_get_error_code( $this->parser );
132             if ( $errorcode != XML_ERROR_NONE ) {
133                 $xml_error = xml_error_string( $errorcode );
134                 $error_line = xml_get_current_line_number($this->parser);
135                 $error_col = xml_get_current_column_number($this->parser);
136                 $errormsg = "$xml_error at line $error_line, column $error_col";
138                 $this->error( $errormsg );
139             }
140         }
141         
142         xml_parser_free( $this->parser );
144         $this->normalize();
145     }
146     
147     function feed_start_element($p, $element, &$attrs) {
148         $el = $element = strtolower($element);
149         $attrs = array_change_key_case($attrs, CASE_LOWER);
150         
151         // check for a namespace, and split if found
152         $ns = false;
153         if ( strpos( $element, ':' ) ) {
154             list($ns, $el) = split( ':', $element, 2); 
155         }
156         if ( $ns and $ns != 'rdf' ) {
157             $this->current_namespace = $ns;
158         }
159             
160         # if feed type isn't set, then this is first element of feed
161         # identify feed from root element
162         #
163         if (!isset($this->feed_type) ) {
164             if ( $el == 'rdf' ) {
165                 $this->feed_type = RSS;
166                 $this->feed_version = '1.0';
167             }
168             elseif ( $el == 'rss' ) {
169                 $this->feed_type = RSS;
170                 $this->feed_version = $attrs['version'];
171             }
172             elseif ( $el == 'feed' ) {
173                 $this->feed_type = ATOM;
174                 $this->feed_version = $attrs['version'];
175                 $this->inchannel = true;
176             }
177             return;
178         }
179     
180         if ( $el == 'channel' ) 
181         {
182             $this->inchannel = true;
183         }
184         elseif ($el == 'item' or $el == 'entry' ) 
185         {
186             $this->initem = true;
187             if ( isset($attrs['rdf:about']) ) {
188                 $this->current_item['about'] = $attrs['rdf:about']; 
189             }
190         }
191         
192         // if we're in the default namespace of an RSS feed,
193         //  record textinput or image fields
194         elseif ( 
195             $this->feed_type == RSS and 
196             $this->current_namespace == '' and 
197             $el == 'textinput' ) 
198         {
199             $this->intextinput = true;
200         }
201         
202         elseif (
203             $this->feed_type == RSS and 
204             $this->current_namespace == '' and 
205             $el == 'image' ) 
206         {
207             $this->inimage = true;
208         }
209         
210         # handle atom content constructs
211         elseif ( $this->feed_type == ATOM and in_array($el, $this->_CONTENT_CONSTRUCTS) )
212         {
213             // avoid clashing w/ RSS mod_content
214             if ($el == 'content' ) {
215                 $el = 'atom_content';
216             }
217             
218             $this->incontent = $el;
219             
220             
221         }
222         
223         // if inside an Atom content construct (e.g. content or summary) field treat tags as text
224         elseif ($this->feed_type == ATOM and $this->incontent ) 
225         {
226             // if tags are inlined, then flatten
227             $attrs_str = join(' ', 
228                     array_map('map_attrs', 
229                     array_keys($attrs), 
230                     array_values($attrs) ) );
231             
232             $this->append_content( "<$element $attrs_str>"  );
233                     
234             array_unshift( $this->stack, $el );
235         }
236         
237         // Atom support many links per containging element.
238         // Magpie treats link elements of type rel='alternate'
239         // as being equivalent to RSS's simple link element.
240         //
241         elseif ($this->feed_type == ATOM and $el == 'link' ) 
242         {
243             if ( isset($attrs['rel']) and $attrs['rel'] == 'alternate' ) 
244             {
245                 $link_el = 'link';
246             }
247             else {
248                 $link_el = 'link_' . $attrs['rel'];
249             }
250             
251             $this->append($link_el, $attrs['href']);
252         }
253         // set stack[0] to current element
254         else {
255             array_unshift($this->stack, $el);
256         }
257     }
258     
260     
261     function feed_cdata ($p, $text) {
262         
263         if ($this->feed_type == ATOM and $this->incontent) 
264         {
265             $this->append_content( $text );
266         }
267         else {
268             $current_el = join('_', array_reverse($this->stack));
269             $this->append($current_el, $text);
270         }
271     }
272     
273     function feed_end_element ($p, $el) {
274         $el = strtolower($el);
275         
276         if ( $el == 'item' or $el == 'entry' ) 
277         {
278             $this->items[] = $this->current_item;
279             $this->current_item = array();
280             $this->initem = false;
281         }
282         elseif ($this->feed_type == RSS and $this->current_namespace == '' and $el == 'textinput' ) 
283         {
284             $this->intextinput = false;
285         }
286         elseif ($this->feed_type == RSS and $this->current_namespace == '' and $el == 'image' ) 
287         {
288             $this->inimage = false;
289         }
290         elseif ($this->feed_type == ATOM and in_array($el, $this->_CONTENT_CONSTRUCTS) )
291         {   
292             $this->incontent = false;
293         }
294         elseif ($el == 'channel' or $el == 'feed' ) 
295         {
296             $this->inchannel = false;
297         }
298         elseif ($this->feed_type == ATOM and $this->incontent  ) {
299             // balance tags properly
300             // note:  i don't think this is actually neccessary
301             if ( $this->stack[0] == $el ) 
302             {
303                 $this->append_content("</$el>");
304             }
305             else {
306                 $this->append_content("<$el />");
307             }
309             array_shift( $this->stack );
310         }
311         else {
312             array_shift( $this->stack );
313         }
314         
315         $this->current_namespace = false;
316     }
317     
318     function concat (&$str1, $str2="") {
319         if (!isset($str1) ) {
320             $str1="";
321         }
322         $str1 .= $str2;
323     }
324     
325     
326     
327     function append_content($text) {
328         if ( $this->initem ) {
329             $this->concat( $this->current_item[ $this->incontent ], $text );
330         }
331         elseif ( $this->inchannel ) {
332             $this->concat( $this->channel[ $this->incontent ], $text );
333         }
334     }
335     
336     // smart append - field and namespace aware
337     function append($el, $text) {
338         if (!$el) {
339             return;
340         }
341         if ( $this->current_namespace ) 
342         {
343             if ( $this->initem ) {
344                 $this->concat(
345                     $this->current_item[ $this->current_namespace ][ $el ], $text);
346             }
347             elseif ($this->inchannel) {
348                 $this->concat(
349                     $this->channel[ $this->current_namespace][ $el ], $text );
350             }
351             elseif ($this->intextinput) {
352                 $this->concat(
353                     $this->textinput[ $this->current_namespace][ $el ], $text );
354             }
355             elseif ($this->inimage) {
356                 $this->concat(
357                     $this->image[ $this->current_namespace ][ $el ], $text );
358             }
359         }
360         else {
361             if ( $this->initem ) {
362                 $this->concat(
363                     $this->current_item[ $el ], $text);
364             }
365             elseif ($this->intextinput) {
366                 $this->concat(
367                     $this->textinput[ $el ], $text );
368             }
369             elseif ($this->inimage) {
370                 $this->concat(
371                     $this->image[ $el ], $text );
372             }
373             elseif ($this->inchannel) {
374                 $this->concat(
375                     $this->channel[ $el ], $text );
376             }
377             
378         }
379     }
380     
381     function normalize () {
382         // if atom populate rss fields
383         if ( $this->is_atom() ) {
384             $this->channel['description'] = $this->channel['tagline'];
385             for ( $i = 0; $i < count($this->items); $i++) {
386                 $item = $this->items[$i];
387                 if ( isset($item['summary']) )
388                     $item['description'] = $item['summary'];
389                 if ( isset($item['atom_content']))
390                     $item['content']['encoded'] = $item['atom_content'];
391                 
392                 $atom_date = (isset($item['issued']) ) ? $item['issued'] : $item['modified'];
393                 if ( $atom_date ) {
394                     $epoch = @parse_w3cdtf($item['modified']);
395                     if ($epoch and $epoch > 0) {
396                         $item['date_timestamp'] = $epoch;
397                     }
398                 }
399                 
400                 $this->items[$i] = $item;
401             }       
402         }
403         elseif ( $this->is_rss() ) {
404             $this->channel['tagline'] = $this->channel['description'];
405             for ( $i = 0; $i < count($this->items); $i++) {
406                 $item = $this->items[$i];
407                 if ( isset($item['description']))
408                     $item['summary'] = $item['description'];
409                 if ( isset($item['content']['encoded'] ) )
410                     $item['atom_content'] = $item['content']['encoded'];
411                 
412                 if ( $this->is_rss() == '1.0' and isset($item['dc']['date']) ) {
413                     $epoch = @parse_w3cdtf($item['dc']['date']);
414                     if ($epoch and $epoch > 0) {
415                         $item['date_timestamp'] = $epoch;
416                     }
417                 }
418                 elseif ( isset($item['pubdate']) ) {
419                     $epoch = @strtotime($item['pubdate']);
420                     if ($epoch > 0) {
421                         $item['date_timestamp'] = $epoch;
422                     }
423                 }
424                 
425                 $this->items[$i] = $item;
426             }
427         }
428     }
429     
430     
431     function is_rss () {
432         if ( $this->feed_type == RSS ) {
433             return $this->feed_version; 
434         }
435         else {
436             return false;
437         }
438     }
439     
440     function is_atom() {
441         if ( $this->feed_type == ATOM ) {
442             return $this->feed_version;
443         }
444         else {
445             return false;
446         }
447     }
449     /**
450     * return XML parser, and possibly re-encoded source
451     *
452     */
453     function create_parser($source, $out_enc, $in_enc, $detect) {
454         if ( substr(phpversion(),0,1) == 5) {
455             $parser = $this->php5_create_parser($in_enc, $detect);
456         }
457         else {
458             list($parser, $source) = $this->php4_create_parser($source, $in_enc, $detect);
459         }
460         if ($out_enc) {
461             $this->encoding = $out_enc;
462             xml_parser_set_option($parser, XML_OPTION_TARGET_ENCODING, $out_enc);
463         }
464         
465         return array($parser, $source);
466     }
467     
468     /**
469     * Instantiate an XML parser under PHP5
470     *
471     * PHP5 will do a fine job of detecting input encoding
472     * if passed an empty string as the encoding. 
473     *
474     * All hail libxml2!
475     *
476     */
477     function php5_create_parser($in_enc, $detect) {
478         // by default php5 does a fine job of detecting input encodings
479         if(!$detect && $in_enc) {
480             return xml_parser_create($in_enc);
481         }
482         else {
483             return xml_parser_create('');
484         }
485     }
486     
487     /**
488     * Instaniate an XML parser under PHP4
489     *
490     * Unfortunately PHP4's support for character encodings
491     * and especially XML and character encodings sucks.  As
492     * long as the documents you parse only contain characters
493     * from the ISO-8859-1 character set (a superset of ASCII,
494     * and a subset of UTF-8) you're fine.  However once you
495     * step out of that comfy little world things get mad, bad,
496     * and dangerous to know.
497     *
498     * The following code is based on SJM's work with FoF
499     * @see http://minutillo.com/steve/weblog/2004/6/17/php-xml-and-character-encodings-a-tale-of-sadness-rage-and-data-loss
500     *
501     */
502     function php4_create_parser($source, $in_enc, $detect) {
503         if ( !$detect ) {
504             return array(xml_parser_create($in_enc), $source);
505         }
506         
507         if (!$in_enc) {
508             if (preg_match('/<?xml.*encoding=[\'"](.*?)[\'"].*?>/m', $source, $m)) {
509                 $in_enc = strtoupper($m[1]);
510                 $this->source_encoding = $in_enc;
511             }
512             else {
513                 $in_enc = 'UTF-8';
514             }
515         }
516         
517         if ($this->known_encoding($in_enc)) {
518             return array(xml_parser_create($in_enc), $source);
519         }
520         
521         // the dectected encoding is not one of the simple encodings PHP knows
522         
523         // attempt to use the iconv extension to
524         // cast the XML to a known encoding
525         // @see http://php.net/iconv
526        
527         if (function_exists('iconv'))  {
528             $encoded_source = iconv($in_enc,'UTF-8', $source);
529             if ($encoded_source) {
530                 return array(xml_parser_create('UTF-8'), $encoded_source);
531             }
532         }
533         
534         // iconv didn't work, try mb_convert_encoding
535         // @see http://php.net/mbstring
536         if(function_exists('mb_convert_encoding')) {
537             $encoded_source = mb_convert_encoding($source, 'UTF-8', $in_enc );
538             if ($encoded_source) {
539                 return array(xml_parser_create('UTF-8'), $encoded_source);
540             }
541         }
542         
543         // else 
544         $this->error("Feed is in an unsupported character encoding. ($in_enc) " .
545                      "You may see strange artifacts, and mangled characters.",
546                      E_USER_NOTICE);
547             
548         return array(xml_parser_create(), $source);
549     }
550     
551     function known_encoding($enc) {
552         $enc = strtoupper($enc);
553         if ( in_array($enc, $this->_KNOWN_ENCODINGS) ) {
554             return $enc;
555         }
556         else {
557             return false;
558         }
559     }
561     function error ($errormsg, $lvl=E_USER_WARNING) {
562         // append PHP's error message if track_errors enabled
563         if ( $php_errormsg ) { 
564             $errormsg .= " ($php_errormsg)";
565         }
566         if ( MAGPIE_DEBUG ) {
567             trigger_error( $errormsg, $lvl);        
568         }
569         else {
570             error_log( $errormsg, 0);
571         }
572         
573         $notices = E_USER_NOTICE|E_NOTICE;
574         if ( $lvl&$notices ) {
575             $this->WARNING = $errormsg;
576         } else {
577             $this->ERROR = $errormsg;
578         }
579     }
580     
581     
582 } // end class RSS
584 function map_attrs($k, $v) {
585     return "$k=\"$v\"";
589 ?>