Updated for better reporting when loading from remote url fails. Now displays error...
[moodle.git] / rss / class.RSS.php
1 <?php
2 /*
3  * Project:     MagpieRSS: a simple RSS integration tool
4  * File:        rss_parse.inc includes code for parsing
5  *                              RSS, and returning an RSS object
6  * Author:      Kellan Elliott-McCrea <kellan@protest.net>
7  * Version:             0.51
8  * License:             GPL
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  */
18  
20 /* 
21  * NOTES ON RSS PARSING PHILOSOPHY (moderately important):
22  * MagpieRSS parse all versions of RSS with a few limitation (mod_content, and
23  * mod_taxonomy support is shaky) into a simple object, with 2 fields, 
24  * the hash 'channel', and the array 'items'.
25  *
26  * MagpieRSS is a forgiving and inclusive parser.  It currently makes no
27  * attempt to enforce the validity on an RSS feed.  It will include any
28  * properly formatted tags it finds, allowing to you to mix RSS 0.93, with RSS
29  * 1.0, with tags or your own imagining.  This sort of witches brew is a bad
30  * bad idea!  But Magpie is less pendantic then I am.
31  *
32  * RSS validators are readily available on the web at:
33  * http://feeds.archive.org/validator/
34  * http://www.ldodds.com/rss_validator/1.0/validator.html
35  *
36  */
38 /*
39  * EXAMPLE PARSE RESULTS:
40  *
41  * Magpie tries to parse RSS into ease to use PHP datastructures.
42  *
43  * For example, Magpie on encountering RSS 1.0 item entry:
44  *
45  * <item rdf:about="http://protest.net/NorthEast/calendrome.cgi?span=event&#38;ID=210257">
46  * <title>Weekly Peace Vigil</title>
47  * <link>http://protest.net/NorthEast/calendrome.cgi?span=event&#38;ID=210257</link>
48  * <description>Wear a white ribbon</description>
49  * <dc:subject>Peace</dc:subject>
50  * <ev:startdate>2002-06-01T11:00:00</ev:startdate>
51  * <ev:location>Northampton, MA</ev:location>
52  * <ev:enddate>2002-06-01T12:00:00</ev:enddate>
53  * <ev:type>Protest</ev:type>
54  * </item>
55  * 
56  * Would transform it into the following associative array, and push it
57  * onto the array $rss-items
58  *
59  * array(
60  *      title => 'Weekly Peace Vigil',
61  *      link =>
62  *      'http://protest.net/NorthEast/calendrome.cgi?span=event&#38;ID=210257',
63  *      description => 'Wear a white ribbon',
64  *      dc => array (
65  *                      subject => 'Peace'
66  *              ),
67  *      ev => array (
68  *              startdate => '2002-06-01T11:00:00',
69  *              enddate => '2002-06-01T12:00:00',
70  *              type => 'Protest',
71  *              location => 'Northampton, MA'
72  *      )
73  * )
74  *
75  */
77 class MagpieRSS {
78     /*
79      * Hybrid parser, and object.  (probably a bad idea! :)
80      *
81      * Useage Example:
82      *
83      * $some_rss = "<?xml version="1.0"......
84      *
85      * $rss = new MagpieRSS( $some_rss );
86      *
87      * // print rss chanel title
88      * echo $rss->channel['title'];
89      *
90      * // print the title of each item
91      * foreach ($rss->items as $item ) {
92      *    echo $item[title];
93      * }
94      *
95      * see rss_fetch.inc for a simpler interface
96      */
97      
98     var $parser;
99     
100     var $current_item   = array();      // item currently being parsed
101     var $items                  = array();      // collection of parsed items
102     var $channel                = array();      // hash of channel fields
103     var $textinput              = array();
104     var $image                  = array();
105     
106     var $parent_field   = array('RDF');
107     var $current_field  = '';
108     var $current_namespace      = false;
109     
110     var $ERROR = "";
111     
112     /*======================================================================*\
113     Function: MagpieRSS
114     Purpose:  Constructor, sets up XML parser,parses source,
115               and populates object.. 
116     Input:        String containing the RSS to be parsed
117     \*======================================================================*/
118     function MagpieRSS ($source) {
119         
120         # if PHP xml isn't compiled in, die
121         #
122         if (!function_exists('xml_parser_create')) {
123             $this->error( "Failed to load PHP's XML Extension. " . 
124                           "http://www.php.net/manual/en/ref.xml.php",
125                            E_USER_ERROR );
126         }
127         
128         $parser = @xml_parser_create();
129         
130         if (!is_resource($parser))
131         {
132             $this->error( "Failed to create an instance of PHP's XML parser. " .
133                           "http://www.php.net/manual/en/ref.xml.php",
134                           E_USER_ERROR );
135         }
136     
137         
138         $this->parser = $parser;
139         
140         # pass in parser, and a reference to this object
141         # setup handlers
142         #
143         xml_set_object( $this->parser, $this );
144         xml_set_element_handler($this->parser, 'start_element', 'end_element');
145         xml_set_character_data_handler( $this->parser, 'cdata' ); 
146     
147         
148         $status = xml_parse( $this->parser, $source );
149         
150         if (! $status ) {
151             $errorcode = xml_get_error_code( $this->parser );
152             if ( $errorcode != XML_ERROR_NONE ) {
153                 $xml_error = xml_error_string( $errorcode );
154                 $error_line = xml_get_current_line_number($this->parser);
155                 $error_col = xml_get_current_column_number($this->parser);
156                 $errormsg = "$xml_error at line $error_line, column $error_col";
157     
158                 $this->error( $errormsg );
159             }
160         }
161         
162         xml_parser_free( $this->parser );
163     }
164     
165     function start_element ($p, $element, &$attrs) {
166         $element        = strtolower( $element );
167         # check for a namespace, and split if found
168         #
169         $namespace      = false;
170         if ( strpos( $element, ':' ) ) {
171             list($namespace, $element) = split( ':', $element, 2); 
172         }
173         $this->current_field = $element;
174         if ( $namespace and $namespace != 'rdf' ) {
175             $this->current_namespace = $namespace;
176         }
177         
178         if ( $element == 'channel' ) {
179             array_unshift( $this->parent_field, 'channel' );
180         }
181         elseif ( $element == 'items' ) {
182             array_unshift( $this->parent_field, 'items' );
183         }
184         elseif ( $element == 'item' ) {
185             array_unshift( $this->parent_field, 'item' );
186         }
187         elseif ( $element == 'textinput' ) {
188             array_unshift( $this->parent_field, 'textinput' );
189         }
190         elseif ( $element == 'image' ) {
191             array_unshift( $this->parent_field, 'image' );
192         }
193         
194     }
195     
196     function end_element ($p, $element) {
197         $element = strtolower($element);
198                             
199         if ( $element == 'item' ) {     
200             $this->items[] = $this->current_item;
201             $this->current_item = array();
202             array_shift( $this->parent_field );
203         }
204         elseif ( $element == 'channel' or $element == 'items' or 
205                  $element == 'textinput' or $element == 'image' ) {
206             array_shift( $this->parent_field );
207         }
208         
209         $this->current_field = '';
210         $this->current_namespace = false;
211     }
212     
213     function cdata ($p, $text) {
214         # skip item, channel, items first time we see them
215         #
216         if ( $this->parent_field[0] == $this->current_field or
217              ! $this->current_field ) {
218             return;
219         }
220         elseif ( $this->parent_field[0] == 'channel') {
221             if ( $this->current_namespace ) {
222                 $this->append(
223                     $this->channel[ $this->current_namespace ][ $this->current_field ],
224                     $text);
225             }
226             else {
227                 $this->append($this->channel[ $this->current_field ], $text);
228             }
229         
230         }
231         elseif ( $this->parent_field[0] == 'item' ) {
232             if ( $this->current_namespace ) {
233                 $this->append(
234                     $this->current_item[ $this->current_namespace ][$this->current_field ],
235                     $text);
236             }
237             else {
238                 $this->append(
239                     $this->current_item[ $this->current_field ],
240                     $text );
241             }
242         }
243         elseif ( $this->parent_field[0] == 'textinput' ) {
244             if ( $this->current_namespace ) {
245                 $this->append(
246                     $this->textinput[ $this->current_namespace ][ $this->current_field ],
247                      $text );
248             }
249             else {
250                 $this->append(
251                     $this->textinput[ $this->current_field ],
252                     $text );
253             }
254         }
255         elseif ( $this->parent_field[0] == 'image' ) {
256             if ( $this->current_namespace ) {
257                 $this->append(
258                     $this->image[ $this->current_namespace ][ $this->current_field ],
259                     $text );
260             }
261             else {
262                 $this->append(
263                     $this->image[ $this->current_field ],
264                     $text );
265             }
266         }
267     }
268     
269     function append (&$str1, $str2="") {
270         if (!isset($str1) ) {
271             $str1="";
272         }
273         $str1 .= $str2;
274     }
275     
276     function error ($errormsg, $lvl=E_USER_WARNING) {
277         // append PHP's error message if track_errors enabled
278         if ( isset($php_errormsg) ) {
279             $errormsg .= " ($php_errormsg)";
280         }
281         $this->ERROR = $errormsg;
282         //if ( MAGPIE_DEBUG ) {
283         //      trigger_error( $errormsg, $lvl);                
284         //}
285         //else {
286             error_log( $errormsg, 0);
287         //}
288     }
289         
290     
291     /*======================================================================*\
292     EVERYTHING BELOW HERE IS FOR DEBUGGING PURPOSES
293     \*======================================================================*/
294     function show_list () {
295         echo "<ol>\n";
296         foreach ($this->items as $item) {
297             echo "<li>", $this->show_item( $item );
298         }
299         echo "</ol>";
300     }
301     
302     function show_channel () {
303         echo "channel:<br>";
304         echo "<ul>";
305         while ( list($key, $value) = each( $this->channel ) ) {
306             echo "<li> $key: $value";
307         }
308         echo "</ul>";
309     }
310     
311     function show_item ($item) {
312         echo "item: $item[title]";
313         echo "<ul>";
314         while ( list($key, $value) = each($item) ) {
315             if ( is_array($value) ) {
316                 echo "<br><b>$key</b>";
317                 echo "<ul>";
318                 while ( list( $ns_key, $ns_value) = each( $value ) ) {
319                     echo "<li>$ns_key: $ns_value";
320                 }
321                 echo "</ul>";
322             }
323             else {
324                 echo "<li> $key: $value";
325             }
326         }
327         echo "</ul>";
328     }
329     
330     /*======================================================================*\
331     END DEBUGGING FUNCTIONS     
332     \*======================================================================*/
333     
334 } # end class RSS
335 ?>