2 do_action('load_feed_engine');
5 * Project: MagpieRSS: a simple RSS integration tool
6 * File: A compiled file for RSS syndication
7 * Author: Kellan Elliott-McCrea <kellan@protest.net>
13 define('ATOM', 'Atom');
14 define('MAGPIE_USER_AGENT', 'WordPress/' . $GLOBALS['wp_version']);
18 var $current_item = array(); // item currently being parsed
19 var $items = array(); // collection of parsed items
20 var $channel = array(); // hash of channel fields
21 var $textinput = array();
27 var $stack = array(); // parser stack
28 var $inchannel = false;
30 var $incontent = false; // if in Atom <content mode="xml"> field
31 var $intextinput = false;
33 var $current_field = '';
34 var $current_namespace = false;
38 var $_CONTENT_CONSTRUCTS = array('content', 'summary', 'info', 'title', 'tagline', 'copyright');
40 function MagpieRSS ($source) {
42 # if PHP xml isn't compiled in, die
44 if ( !function_exists('xml_parser_create') )
45 trigger_error( "Failed to load PHP's XML Extension. http://www.php.net/manual/en/ref.xml.php" );
47 $parser = @xml_parser_create();
49 if ( !is_resource($parser) )
50 trigger_error( "Failed to create an instance of PHP's XML parser. http://www.php.net/manual/en/ref.xml.php");
53 $this->parser = $parser;
55 # pass in parser, and a reference to this object
58 xml_set_object( $this->parser, $this );
59 xml_set_element_handler($this->parser,
60 'feed_start_element', 'feed_end_element' );
62 xml_set_character_data_handler( $this->parser, 'feed_cdata' );
64 $status = xml_parse( $this->parser, $source );
67 $errorcode = xml_get_error_code( $this->parser );
68 if ( $errorcode != XML_ERROR_NONE ) {
69 $xml_error = xml_error_string( $errorcode );
70 $error_line = xml_get_current_line_number($this->parser);
71 $error_col = xml_get_current_column_number($this->parser);
72 $errormsg = "$xml_error at line $error_line, column $error_col";
74 $this->error( $errormsg );
78 xml_parser_free( $this->parser );
83 function feed_start_element($p, $element, &$attrs) {
84 $el = $element = strtolower($element);
85 $attrs = array_change_key_case($attrs, CASE_LOWER);
87 // check for a namespace, and split if found
89 if ( strpos( $element, ':' ) ) {
90 list($ns, $el) = split( ':', $element, 2);
92 if ( $ns and $ns != 'rdf' ) {
93 $this->current_namespace = $ns;
96 # if feed type isn't set, then this is first element of feed
97 # identify feed from root element
99 if (!isset($this->feed_type) ) {
100 if ( $el == 'rdf' ) {
101 $this->feed_type = RSS;
102 $this->feed_version = '1.0';
104 elseif ( $el == 'rss' ) {
105 $this->feed_type = RSS;
106 $this->feed_version = $attrs['version'];
108 elseif ( $el == 'feed' ) {
109 $this->feed_type = ATOM;
110 $this->feed_version = $attrs['version'];
111 $this->inchannel = true;
116 if ( $el == 'channel' )
118 $this->inchannel = true;
120 elseif ($el == 'item' or $el == 'entry' )
122 $this->initem = true;
123 if ( isset($attrs['rdf:about']) ) {
124 $this->current_item['about'] = $attrs['rdf:about'];
128 // if we're in the default namespace of an RSS feed,
129 // record textinput or image fields
131 $this->feed_type == RSS and
132 $this->current_namespace == '' and
135 $this->intextinput = true;
139 $this->feed_type == RSS and
140 $this->current_namespace == '' and
143 $this->inimage = true;
146 # handle atom content constructs
147 elseif ( $this->feed_type == ATOM and in_array($el, $this->_CONTENT_CONSTRUCTS) )
149 // avoid clashing w/ RSS mod_content
150 if ($el == 'content' ) {
151 $el = 'atom_content';
154 $this->incontent = $el;
159 // if inside an Atom content construct (e.g. content or summary) field treat tags as text
160 elseif ($this->feed_type == ATOM and $this->incontent )
162 // if tags are inlined, then flatten
163 $attrs_str = join(' ',
164 array_map('map_attrs',
166 array_values($attrs) ) );
168 $this->append_content( "<$element $attrs_str>" );
170 array_unshift( $this->stack, $el );
173 // Atom support many links per containging element.
174 // Magpie treats link elements of type rel='alternate'
175 // as being equivalent to RSS's simple link element.
177 elseif ($this->feed_type == ATOM and $el == 'link' )
179 if ( isset($attrs['rel']) and $attrs['rel'] == 'alternate' )
184 $link_el = 'link_' . $attrs['rel'];
187 $this->append($link_el, $attrs['href']);
189 // set stack[0] to current element
191 array_unshift($this->stack, $el);
197 function feed_cdata ($p, $text) {
199 if ($this->feed_type == ATOM and $this->incontent)
201 $this->append_content( $text );
204 $current_el = join('_', array_reverse($this->stack));
205 $this->append($current_el, $text);
209 function feed_end_element ($p, $el) {
210 $el = strtolower($el);
212 if ( $el == 'item' or $el == 'entry' )
214 $this->items[] = $this->current_item;
215 $this->current_item = array();
216 $this->initem = false;
218 elseif ($this->feed_type == RSS and $this->current_namespace == '' and $el == 'textinput' )
220 $this->intextinput = false;
222 elseif ($this->feed_type == RSS and $this->current_namespace == '' and $el == 'image' )
224 $this->inimage = false;
226 elseif ($this->feed_type == ATOM and in_array($el, $this->_CONTENT_CONSTRUCTS) )
228 $this->incontent = false;
230 elseif ($el == 'channel' or $el == 'feed' )
232 $this->inchannel = false;
234 elseif ($this->feed_type == ATOM and $this->incontent ) {
235 // balance tags properly
236 // note: i don't think this is actually neccessary
237 if ( $this->stack[0] == $el )
239 $this->append_content("</$el>");
242 $this->append_content("<$el />");
245 array_shift( $this->stack );
248 array_shift( $this->stack );
251 $this->current_namespace = false;
254 function concat (&$str1, $str2="") {
255 if (!isset($str1) ) {
261 function append_content($text) {
262 if ( $this->initem ) {
263 $this->concat( $this->current_item[ $this->incontent ], $text );
265 elseif ( $this->inchannel ) {
266 $this->concat( $this->channel[ $this->incontent ], $text );
270 // smart append - field and namespace aware
271 function append($el, $text) {
275 if ( $this->current_namespace )
277 if ( $this->initem ) {
279 $this->current_item[ $this->current_namespace ][ $el ], $text);
281 elseif ($this->inchannel) {
283 $this->channel[ $this->current_namespace][ $el ], $text );
285 elseif ($this->intextinput) {
287 $this->textinput[ $this->current_namespace][ $el ], $text );
289 elseif ($this->inimage) {
291 $this->image[ $this->current_namespace ][ $el ], $text );
295 if ( $this->initem ) {
297 $this->current_item[ $el ], $text);
299 elseif ($this->intextinput) {
301 $this->textinput[ $el ], $text );
303 elseif ($this->inimage) {
305 $this->image[ $el ], $text );
307 elseif ($this->inchannel) {
309 $this->channel[ $el ], $text );
315 function normalize () {
316 // if atom populate rss fields
317 if ( $this->is_atom() ) {
318 $this->channel['descripton'] = $this->channel['tagline'];
319 for ( $i = 0; $i < count($this->items); $i++) {
320 $item = $this->items[$i];
321 if ( isset($item['summary']) )
322 $item['description'] = $item['summary'];
323 if ( isset($item['atom_content']))
324 $item['content']['encoded'] = $item['atom_content'];
326 $this->items[$i] = $item;
329 elseif ( $this->is_rss() ) {
330 $this->channel['tagline'] = $this->channel['description'];
331 for ( $i = 0; $i < count($this->items); $i++) {
332 $item = $this->items[$i];
333 if ( isset($item['description']))
334 $item['summary'] = $item['description'];
335 if ( isset($item['content']['encoded'] ) )
336 $item['atom_content'] = $item['content']['encoded'];
338 $this->items[$i] = $item;
344 if ( $this->feed_type == RSS ) {
345 return $this->feed_version;
353 if ( $this->feed_type == ATOM ) {
354 return $this->feed_version;
361 function map_attrs($k, $v) {
365 function error( $errormsg, $lvl = E_USER_WARNING ) {
366 // append PHP's error message if track_errors enabled
367 if ( isset($php_errormsg) ) {
368 $errormsg .= " ($php_errormsg)";
370 if ( MAGPIE_DEBUG ) {
371 trigger_error( $errormsg, $lvl);
373 error_log( $errormsg, 0);
378 require_once( dirname(__FILE__) . '/class-snoopy.php');
380 if ( !function_exists('fetch_rss') ) :
381 function fetch_rss ($url) {
382 // initialize constants
385 if ( !isset($url) ) {
386 // error("fetch_rss called without a url");
390 // if cache is disabled
391 if ( !MAGPIE_CACHE_ON ) {
392 // fetch file, and parse it
393 $resp = _fetch_remote_file( $url );
394 if ( is_success( $resp->status ) ) {
395 return _response_to_rss( $resp );
398 // error("Failed to fetch $url and cache is off");
406 // 2. if there is a hit, make sure its fresh
407 // 3. if cached obj fails freshness check, fetch remote
408 // 4. if remote fails, return stale object, or error
410 $cache = new RSSCache( MAGPIE_CACHE_DIR, MAGPIE_CACHE_AGE );
412 if (MAGPIE_DEBUG and $cache->ERROR) {
413 debug($cache->ERROR, E_USER_WARNING);
417 $cache_status = 0; // response of check_cache
418 $request_headers = array(); // HTTP headers to send with fetch
419 $rss = 0; // parsed RSS object
420 $errormsg = 0; // errors, if any
422 if (!$cache->ERROR) {
423 // return cache HIT, MISS, or STALE
424 $cache_status = $cache->check_cache( $url );
427 // if object cached, and cache is fresh, return cached obj
428 if ( $cache_status == 'HIT' ) {
429 $rss = $cache->get( $url );
430 if ( isset($rss) and $rss ) {
431 $rss->from_cache = 1;
432 if ( MAGPIE_DEBUG > 1) {
433 debug("MagpieRSS: Cache HIT", E_USER_NOTICE);
439 // else attempt a conditional get
442 if ( $cache_status == 'STALE' ) {
443 $rss = $cache->get( $url );
444 if ( $rss->etag and $rss->last_modified ) {
445 $request_headers['If-None-Match'] = $rss->etag;
446 $request_headers['If-Last-Modified'] = $rss->last_modified;
450 $resp = _fetch_remote_file( $url, $request_headers );
452 if (isset($resp) and $resp) {
453 if ($resp->status == '304' ) {
454 // we have the most current copy
455 if ( MAGPIE_DEBUG > 1) {
456 debug("Got 304 for $url");
458 // reset cache on 304 (at minutillo insistent prodding)
459 $cache->set($url, $rss);
462 elseif ( is_success( $resp->status ) ) {
463 $rss = _response_to_rss( $resp );
465 if (MAGPIE_DEBUG > 1) {
466 debug("Fetch successful");
468 // add object to cache
469 $cache->set( $url, $rss );
474 $errormsg = "Failed to fetch $url. ";
475 if ( $resp->error ) {
476 # compensate for Snoopy's annoying habbit to tacking
478 $http_error = substr($resp->error, 0, -2);
479 $errormsg .= "(HTTP Error: $http_error)";
482 $errormsg .= "(HTTP Response: " . $resp->response_code .')';
487 $errormsg = "Unable to retrieve RSS file for unknown reasons.";
492 // attempt to return cached object
494 if ( MAGPIE_DEBUG ) {
495 debug("Returning STALE object for $url");
500 // else we totally failed
501 // error( $errormsg );
505 } // end if ( !MAGPIE_CACHE_ON ) {
509 function _fetch_remote_file ($url, $headers = "" ) {
510 // Snoopy is an HTTP client in PHP
511 $client = new Snoopy();
512 $client->agent = MAGPIE_USER_AGENT;
513 $client->read_timeout = MAGPIE_FETCH_TIME_OUT;
514 $client->use_gzip = MAGPIE_USE_GZIP;
515 if (is_array($headers) ) {
516 $client->rawheaders = $headers;
519 @$client->fetch($url);
524 function _response_to_rss ($resp) {
525 $rss = new MagpieRSS( $resp->results );
527 // if RSS parsed successfully
528 if ( $rss and !$rss->ERROR) {
530 // find Etag, and Last-Modified
531 foreach($resp->headers as $h) {
532 // 2003-03-02 - Nicola Asuni (www.tecnick.com) - fixed bug "Undefined offset: 1"
533 if (strpos($h, ": ")) {
534 list($field, $val) = explode(": ", $h, 2);
541 if ( $field == 'ETag' ) {
545 if ( $field == 'Last-Modified' ) {
546 $rss->last_modified = $val;
551 } // else construct error message
553 $errormsg = "Failed to parse RSS file.";
556 $errormsg .= " (" . $rss->ERROR . ")";
561 } // end if ($rss and !$rss->error)
564 /*=======================================================================*\
566 Purpose: setup constants with default values
567 check for user overrides
568 \*=======================================================================*/
570 if ( defined('MAGPIE_INITALIZED') ) {
574 define('MAGPIE_INITALIZED', 1);
577 if ( !defined('MAGPIE_CACHE_ON') ) {
578 define('MAGPIE_CACHE_ON', 1);
581 if ( !defined('MAGPIE_CACHE_DIR') ) {
582 define('MAGPIE_CACHE_DIR', './cache');
585 if ( !defined('MAGPIE_CACHE_AGE') ) {
586 define('MAGPIE_CACHE_AGE', 60*60); // one hour
589 if ( !defined('MAGPIE_CACHE_FRESH_ONLY') ) {
590 define('MAGPIE_CACHE_FRESH_ONLY', 0);
593 if ( !defined('MAGPIE_DEBUG') ) {
594 define('MAGPIE_DEBUG', 0);
597 if ( !defined('MAGPIE_USER_AGENT') ) {
598 $ua = 'WordPress/' . $GLOBALS['wp_version'];
600 if ( MAGPIE_CACHE_ON ) {
604 $ua = $ua . '; No cache)';
607 define('MAGPIE_USER_AGENT', $ua);
610 if ( !defined('MAGPIE_FETCH_TIME_OUT') ) {
611 define('MAGPIE_FETCH_TIME_OUT', 2); // 2 second timeout
614 // use gzip encoding to fetch rss files if supported?
615 if ( !defined('MAGPIE_USE_GZIP') ) {
616 define('MAGPIE_USE_GZIP', true);
620 function is_info ($sc) {
621 return $sc >= 100 && $sc < 200;
624 function is_success ($sc) {
625 return $sc >= 200 && $sc < 300;
628 function is_redirect ($sc) {
629 return $sc >= 300 && $sc < 400;
632 function is_error ($sc) {
633 return $sc >= 400 && $sc < 600;
636 function is_client_error ($sc) {
637 return $sc >= 400 && $sc < 500;
640 function is_server_error ($sc) {
641 return $sc >= 500 && $sc < 600;
645 var $BASE_CACHE = 'wp-content/cache'; // where the cache files are stored
646 var $MAX_AGE = 43200; // when are files stale, default twelve hours
647 var $ERROR = ''; // accumulate error messages
649 function RSSCache ($base='', $age='') {
651 $this->BASE_CACHE = $base;
654 $this->MAX_AGE = $age;
659 /*=======================================================================*\
661 Purpose: add an item to the cache, keyed on url
662 Input: url from wich the rss file was fetched
663 Output: true on sucess
664 \*=======================================================================*/
665 function set ($url, $rss) {
667 $cache_option = 'rss_' . $this->file_name( $url );
668 $cache_timestamp = 'rss_' . $this->file_name( $url ) . '_ts';
670 if ( !$wpdb->get_var("SELECT option_name FROM $wpdb->options WHERE option_name = '$cache_option'") )
671 add_option($cache_option, '', '', 'no');
672 if ( !$wpdb->get_var("SELECT option_name FROM $wpdb->options WHERE option_name = '$cache_timestamp'") )
673 add_option($cache_timestamp, '', '', 'no');
675 update_option($cache_option, $rss);
676 update_option($cache_timestamp, time() );
678 return $cache_option;
681 /*=======================================================================*\
683 Purpose: fetch an item from the cache
684 Input: url from wich the rss file was fetched
685 Output: cached object on HIT, false on MISS
686 \*=======================================================================*/
687 function get ($url) {
689 $cache_option = 'rss_' . $this->file_name( $url );
691 if ( ! get_option( $cache_option ) ) {
693 "Cache doesn't contain: $url (cache option: $cache_option)"
698 $rss = get_option( $cache_option );
703 /*=======================================================================*\
704 Function: check_cache
705 Purpose: check a url for membership in the cache
706 and whether the object is older then MAX_AGE (ie. STALE)
707 Input: url from wich the rss file was fetched
708 Output: cached object on HIT, false on MISS
709 \*=======================================================================*/
710 function check_cache ( $url ) {
712 $cache_option = $this->file_name( $url );
713 $cache_timestamp = 'rss_' . $this->file_name( $url ) . '_ts';
715 if ( $mtime = get_option($cache_timestamp) ) {
716 // find how long ago the file was added to the cache
717 // and whether that is longer then MAX_AGE
718 $age = time() - $mtime;
719 if ( $this->MAX_AGE > $age ) {
720 // object exists and is current
724 // object exists but is old
729 // object does not exist
734 /*=======================================================================*\
736 \*=======================================================================*/
737 function serialize ( $rss ) {
738 return serialize( $rss );
741 /*=======================================================================*\
742 Function: unserialize
743 \*=======================================================================*/
744 function unserialize ( $data ) {
745 return unserialize( $data );
748 /*=======================================================================*\
750 Purpose: map url to location in cache
751 Input: url from wich the rss file was fetched
753 \*=======================================================================*/
754 function file_name ($url) {
758 /*=======================================================================*\
760 Purpose: register error
761 \*=======================================================================*/
762 function error ($errormsg, $lvl=E_USER_WARNING) {
763 // append PHP's error message if track_errors enabled
764 if ( isset($php_errormsg) ) {
765 $errormsg .= " ($php_errormsg)";
767 $this->ERROR = $errormsg;
768 if ( MAGPIE_DEBUG ) {
769 trigger_error( $errormsg, $lvl);
772 error_log( $errormsg, 0);
775 function debug ($debugmsg, $lvl=E_USER_NOTICE) {
776 if ( MAGPIE_DEBUG ) {
777 $this->error("MagpieRSS [debug] $debugmsg", $lvl);
782 if ( !function_exists('parse_w3cdtf') ) :
783 function parse_w3cdtf ( $date_str ) {
785 # regex to match wc3dtf
786 $pat = "/(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})(:(\d{2}))?(?:([-+])(\d{2}):?(\d{2})|(Z))?/";
788 if ( preg_match( $pat, $date_str, $match ) ) {
789 list( $year, $month, $day, $hours, $minutes, $seconds) =
790 array( $match[1], $match[2], $match[3], $match[4], $match[5], $match[7]);
792 # calc epoch for current date assuming GMT
793 $epoch = gmmktime( $hours, $minutes, $seconds, $month, $day, $year);
796 if ( $match[11] == 'Z' ) {
800 list( $tz_mod, $tz_hour, $tz_min ) =
801 array( $match[8], $match[9], $match[10]);
803 # zero out the variables
804 if ( ! $tz_hour ) { $tz_hour = 0; }
805 if ( ! $tz_min ) { $tz_min = 0; }
807 $offset_secs = (($tz_hour*60)+$tz_min)*60;
809 # is timezone ahead of GMT? then subtract offset
811 if ( $tz_mod == '+' ) {
812 $offset_secs = $offset_secs * -1;
815 $offset = $offset_secs;
817 $epoch = $epoch + $offset;
826 if ( !function_exists('wp_rss') ) :
827 function wp_rss( $url, $num_items = -1 ) {
828 if ( $rss = fetch_rss( $url ) ) {
831 if ( $num_items !== -1 ) {
832 $rss->items = array_slice( $rss->items, 0, $num_items );
835 foreach ( $rss->items as $item ) {
837 '<li><a href="%1$s" title="%2$s">%3$s</a></li>',
838 clean_url( $item['link'] ),
839 attribute_escape( strip_tags( $item['description'] ) ),
840 htmlentities( $item['title'] )
846 _e( 'An error has occurred, which probably means the feed is down. Try again later.' );
851 if ( !function_exists('get_rss') ) :
852 function get_rss ($url, $num_items = 5) { // Like get posts, but for RSS
853 $rss = fetch_rss($url);
855 $rss->items = array_slice($rss->items, 0, $num_items);
856 foreach ($rss->items as $item ) {
858 echo "<a href='$item[link]' title='$item[description]'>";
859 echo htmlentities($item['title']);