| @@ -1,15 +1,29 @@ | ||
| 1 | 1 | <?php |
| 2 | -################################################################################ | |
| 3 | -## class FeedFinder: find likely feeds using autodetection and/or guesswork #### | |
| 4 | -################################################################################ | |
| 2 | +/** | |
| 3 | + * class FeedFinder: find likely feeds using autodetection and/or guesswork | |
| 4 | + * @version 2010.0622 | |
| 5 | + * @uses SimplePie_Misc | |
| 6 | + */ | |
| 5 | 7 | |
| 8 | +if ( ! class_exists( 'SimplePie' ) ) : | |
| 9 | + require_once ABSPATH . WPINC . '/class-simplepie.php'; | |
| 10 | +endif; | |
| 11 | +require_once dirname( __FILE__ ) . '/feedwordpresshtml.class.php'; | |
| 12 | + | |
| 13 | +/** | |
| 14 | + * FeedFinder attempts to get all possible RSS/Atom feeds from a site. | |
| 15 | + * | |
| 16 | + * @see SimplePie_Misc | |
| 17 | + */ | |
| 6 | 18 | class FeedFinder { |
| 7 | 19 | var $uri = NULL; |
| 20 | + var $credentials = NULL; | |
| 8 | 21 | var $_cache_uri = NULL; |
| 9 | - | |
| 22 | + | |
| 10 | 23 | var $verify = FALSE; |
| 11 | - | |
| 24 | + var $fallbacks = 3; | |
| 25 | + | |
| 12 | 26 | var $_response = NULL; |
| 13 | 27 | var $_data = NULL; |
| 14 | 28 | var $_error = NULL; |
| 15 | 29 | var $_head = NULL; |
| @@ -19,65 +33,139 @@ | ||
| 19 | 33 | 'application/rss+xml', |
| 20 | 34 | 'text/xml', |
| 21 | 35 | 'application/atom+xml', |
| 22 | 36 | 'application/x.atom+xml', |
| 23 | - 'application/x-atom+xml' | |
| 37 | + 'application/x-atom+xml', | |
| 24 | 38 | ); |
| 25 | 39 | var $_feed_markers = array('\\<feed', '\\<rss', 'xmlns="http://purl.org/rss/1.0'); |
| 26 | 40 | var $_html_markers = array('\\<html'); |
| 41 | + var $_opml_markers = array('\\<opml', '\\<outline'); | |
| 27 | 42 | var $_obvious_feed_url = array('[./]rss', '[./]rdf', '[./]atom', '[./]feed', '\.xml'); |
| 28 | 43 | var $_maybe_feed_url = array ('rss', 'rdf', 'atom', 'feed', 'xml'); |
| 29 | 44 | |
| 30 | - function FeedFinder ($uri = NULL, $verify = TRUE) { | |
| 45 | + function __construct( $uri = NULL, $params = array(), $fallbacks = 3 ) { | |
| 46 | + if (is_bool($params)) : | |
| 47 | + $params = array("verify" => $params); | |
| 48 | + endif; | |
| 49 | + | |
| 50 | + $params = wp_parse_args($params, array( | |
| 51 | + "verify" => true, | |
| 52 | + "authentication" => NULL, | |
| 53 | + "username" => NULL, | |
| 54 | + "password" => NULL, | |
| 55 | + )); | |
| 56 | + $verify = $params['verify']; | |
| 57 | + $this->credentials = array( | |
| 58 | + "authentication" => $params['authentication'], | |
| 59 | + "username" => $params['username'], | |
| 60 | + "password" => $params['password'], | |
| 61 | + ); | |
| 62 | + | |
| 31 | 63 | $this->uri = $uri; $this->verify = $verify; |
| 64 | + $this->fallbacks = $fallbacks; | |
| 32 | 65 | } /* FeedFinder::FeedFinder () */ |
| 33 | 66 | |
| 34 | - function find ($uri = NULL) { | |
| 67 | + function FeedFinder( $uri = NULL, $params = array(), $fallbacks = 3 ) { | |
| 68 | + self::__construct( $uri, $params, $fallbacks ); | |
| 69 | + } | |
| 70 | + | |
| 71 | + function find ($uri = NULL, $params = array()) { | |
| 72 | + $params = wp_parse_args($params, array( // Defaults | |
| 73 | + "authentication" => -1, | |
| 74 | + "username" => NULL, | |
| 75 | + "password" => NULL, | |
| 76 | + )); | |
| 77 | + | |
| 78 | + // Equivalents | |
| 79 | + if ($params['authentication']=='-') : | |
| 80 | + $params['authentication'] = NULL; | |
| 81 | + $params['username'] = NULL; | |
| 82 | + $params['password'] = NULL; | |
| 83 | + endif; | |
| 84 | + | |
| 85 | + // Set/reset | |
| 86 | + if ($params['authentication'] != -1) : | |
| 87 | + $this->credentials = array( | |
| 88 | + "authentication" => $params['authentication'], | |
| 89 | + "username" => $params['username'], | |
| 90 | + "password" => $params['password'], | |
| 91 | + ); | |
| 92 | + endif; | |
| 93 | + | |
| 35 | 94 | $ret = array (); |
| 36 | - if (!is_null($this->data($uri))) { | |
| 37 | - if ($this->is_feed($uri)) { | |
| 38 | - $ret = array($this->uri); | |
| 39 | - } else { | |
| 40 | - // Assume that we have HTML or XHTML (even if we don't, who's it gonna hurt?) | |
| 41 | - // Autodiscovery is the preferred method | |
| 42 | - $href = $this->_link_rel_feeds(); | |
| 43 | - | |
| 44 | - // ... but we'll also take the little orange buttons | |
| 45 | - $href = array_merge($href, $this->_a_href_feeds(TRUE)); | |
| 46 | - | |
| 47 | - // If all that failed, look harder | |
| 48 | - if (count($href) == 0) $href = $this->_a_href_feeds(FALSE); | |
| 95 | + if ( !is_null($this->data($uri))) : | |
| 96 | + if ($this->is_opml($uri)) : | |
| 97 | + $href = $this->_opml_rss_uris(); | |
| 98 | + else : | |
| 99 | + if ($this->is_feed($uri)) : | |
| 100 | + $href = array($this->uri); | |
| 101 | + else : | |
| 102 | + // Assume that we have HTML or XHTML (even if we don't, who's | |
| 103 | + // it gonna hurt?) Autodiscovery is the preferred method. | |
| 104 | + $href = $this->_link_rel_feeds(); | |
| 49 | 105 | |
| 50 | - // Our search may turn up duplicate URIs. We only need to do any given URI once. | |
| 51 | - // Props to Camilo <http://projects.radgeek.com/2008/12/14/feedwordpress-20081214/#comment-20090122160414> | |
| 52 | - $href = array_unique($href); | |
| 106 | + // ... but we'll also take the little orange buttons | |
| 107 | + if ($this->fallbacks > 0) : | |
| 108 | + $href = array_merge($href, $this->_a_href_feeds(TRUE)); | |
| 109 | + endif; | |
| 53 | 110 | |
| 54 | - // Verify feeds and resolve relative URIs | |
| 55 | - foreach ($href as $u) { | |
| 56 | - $the_uri = Relative_URI::resolve($u, $this->uri); | |
| 57 | - if ($this->verify) { | |
| 58 | - $feed =& new FeedFinder($the_uri); | |
| 59 | - if ($feed->is_feed()) $ret[] = $the_uri; | |
| 60 | - $feed = NULL; | |
| 61 | - } else { | |
| 62 | - $ret[] = $the_uri; | |
| 63 | - } | |
| 64 | - } /* foreach */ | |
| 65 | - } /* if */ | |
| 66 | - } /* if */ | |
| 67 | - return array_unique($ret); | |
| 111 | + // If all that failed, look harder | |
| 112 | + if ($this->fallbacks > 1) : | |
| 113 | + if (count($href) == 0) : | |
| 114 | + $href = $this->_a_href_feeds(FALSE); | |
| 115 | + endif; | |
| 116 | + endif; | |
| 117 | + | |
| 118 | + // Our search may turn up duplicate URIs. We only need to do | |
| 119 | + // any given URI once. Props to Camilo <http://projects.radgeek.com/2008/12/14/feedwordpress-20081214/#comment-20090122160414> | |
| 120 | + $href = array_unique($href); | |
| 121 | + endif; | |
| 122 | + | |
| 123 | + // Try some clever URL little tricks before we go | |
| 124 | + if ($this->fallbacks > 2) : | |
| 125 | + $href = array_merge($href, $this->_url_manipulation_feeds()); | |
| 126 | + endif; | |
| 127 | + endif; | |
| 128 | + | |
| 129 | + $href = array_unique($href); | |
| 130 | + | |
| 131 | + // Verify feeds and resolve relative URIs | |
| 132 | + foreach ($href as $u) : | |
| 133 | + $the_uri = SimplePie_Misc::absolutize_url($u, $this->uri); | |
| 134 | + if ($this->verify and ($u != $this->uri and $the_uri != $this->uri)) : | |
| 135 | + $feed = new FeedFinder($the_uri, $this->credentials); | |
| 136 | + if ($feed->is_feed()) : $ret[] = $the_uri; endif; | |
| 137 | + unset($feed); | |
| 138 | + else : | |
| 139 | + $ret[] = $the_uri; | |
| 140 | + endif; | |
| 141 | + endforeach; | |
| 142 | + endif; | |
| 143 | + | |
| 144 | + if ($this->is_401($uri)) : | |
| 145 | + $ret = array_merge(array( | |
| 146 | + new WP_Error('http_request_failed', '401 Not authorized', array("uri" => $this->uri, "status" => 401)), | |
| 147 | + ), $ret); | |
| 148 | + endif; | |
| 149 | + | |
| 150 | + return array_values($ret); | |
| 68 | 151 | } /* FeedFinder::find () */ |
| 69 | 152 | |
| 70 | 153 | function data ($uri = NULL) { |
| 71 | 154 | $this->_get($uri); |
| 72 | 155 | return $this->_data; |
| 73 | - } | |
| 156 | + } /* FeedFinder::data () */ | |
| 74 | 157 | |
| 158 | + function upload_data ($data) { | |
| 159 | + $this->uri = 'tag:localhost'; | |
| 160 | + $this->_data = $data; | |
| 161 | + } /* FeedFinder::upload_data () */ | |
| 162 | + | |
| 75 | 163 | function status ($uri = NULL) { |
| 76 | 164 | $this->_get($uri); |
| 77 | - | |
| 78 | - if (isset($this->_response->status)) : | |
| 79 | - $ret = $this->_response->status; | |
| 165 | + | |
| 166 | + if ( !is_wp_error($this->_response) and isset($this->_response['response']['code'])) : | |
| 167 | + $ret = $this->_response['response']['code']; | |
| 80 | 168 | else : |
| 81 | 169 | $ret = NULL; |
| 82 | 170 | endif; |
| 83 | 171 | return $ret; |
| @@ -82,12 +170,24 @@ | ||
| 82 | 170 | endif; |
| 83 | 171 | return $ret; |
| 84 | 172 | } |
| 85 | 173 | |
| 86 | - function error () { | |
| 87 | - return $this->_error; | |
| 174 | + function error ($index = NULL) { | |
| 175 | + $message = NULL; | |
| 176 | + if (count($this->_error) > 0) : | |
| 177 | + if (is_scalar($index) and !is_null($index)) : | |
| 178 | + $message = $this->_error[$index]; | |
| 179 | + else : | |
| 180 | + $message = implode(" / ", $this->_error)."\n"; | |
| 181 | + endif; | |
| 182 | + endif; | |
| 183 | + return $message; | |
| 88 | 184 | } |
| 89 | - | |
| 185 | + | |
| 186 | + function is_401 ($uri = NULL) { | |
| 187 | + return (intval($this->status($uri))==401); | |
| 188 | + } /* FeedFinder::is_401 () */ | |
| 189 | + | |
| 90 | 190 | function is_feed ($uri = NULL) { |
| 91 | 191 | $data = $this->data($uri); |
| 92 | 192 | |
| 93 | 193 | return ( |
| @@ -100,27 +200,45 @@ | ||
| 100 | 200 | ) |
| 101 | 201 | ); |
| 102 | 202 | } /* FeedFinder::is_feed () */ |
| 103 | 203 | |
| 204 | + function is_opml ($uri = NULL) { | |
| 205 | + $data = $this->data($uri); | |
| 206 | + return ( | |
| 207 | + preg_match ( | |
| 208 | + "\007(".implode('|',$this->_opml_markers).")\007i", | |
| 209 | + $data | |
| 210 | + ) | |
| 211 | + ); | |
| 212 | + } /* FeedFinder::is_opml () */ | |
| 213 | + | |
| 104 | 214 | # --- Private methods --- |
| 105 | 215 | function _get ($uri = NULL) { |
| 106 | 216 | if ($uri) $this->uri = $uri; |
| 107 | 217 | |
| 108 | 218 | // Is the result not yet cached? |
| 109 | - if ($this->_cache_uri !== $this->uri) : | |
| 219 | + if ($this->uri != 'tag:localhost' and $this->_cache_uri !== $this->uri) : | |
| 110 | 220 | $headers['Connection'] = 'close'; |
| 111 | 221 | $headers['Accept'] = 'application/atom+xml application/rdf+xml application/rss+xml application/xml text/html */*'; |
| 112 | 222 | $headers['User-Agent'] = 'feedfinder/1.2 (compatible; PHP FeedFinder) +http://projects.radgeek.com/feedwordpress'; |
| 113 | 223 | |
| 114 | - // Use function provided by MagpieRSS package | |
| 115 | - $client = _fetch_remote_file($this->uri, $headers); | |
| 116 | - if (isset($client->error)) : | |
| 117 | - $this->_error = $client->error; | |
| 224 | + // Use WordPress API function | |
| 225 | + $client = wp_remote_request($this->uri, array_merge( | |
| 226 | + $this->credentials, | |
| 227 | + array( | |
| 228 | + 'headers' => $headers, | |
| 229 | + 'timeout' => FeedWordPress::fetch_timeout(), | |
| 230 | + ) | |
| 231 | + )); | |
| 232 | + | |
| 233 | + $this->_response = $client; | |
| 234 | + if (is_wp_error($client)) : | |
| 235 | + $this->_data = NULL; | |
| 236 | + $this->_error = $client->get_error_messages(); | |
| 118 | 237 | else : |
| 238 | + $this->_data = $client['body']; | |
| 119 | 239 | $this->_error = NULL; |
| 120 | 240 | endif; |
| 121 | - $this->_response = $client; | |
| 122 | - $this->_data = $client->results; | |
| 123 | 241 | |
| 124 | 242 | // Kilroy was here |
| 125 | 243 | $this->_cache_uri = $this->uri; |
| 126 | 244 | endif; |
| @@ -125,8 +243,25 @@ | ||
| 125 | 243 | $this->_cache_uri = $this->uri; |
| 126 | 244 | endif; |
| 127 | 245 | } /* FeedFinder::_get () */ |
| 128 | 246 | |
| 247 | + function _opml_rss_uris () { | |
| 248 | + // Really we should parse the XML and use the structure to | |
| 249 | + // return something intelligent to programs that want to use it | |
| 250 | + // Oh babe! maybe some day... | |
| 251 | + | |
| 252 | + $opml = $this->data(); | |
| 253 | + | |
| 254 | + $rx = FeedWordPressHTML::attributeRegex('outline', 'xmlUrl'); | |
| 255 | + if (preg_match_all($rx, $opml, $matches, PREG_SET_ORDER)) : | |
| 256 | + foreach ($matches as $m) : | |
| 257 | + $match = FeedWordPressHTML::attributeMatch($m); | |
| 258 | + $r[] = $match['value']; | |
| 259 | + endforeach; | |
| 260 | + endif; | |
| 261 | + return $r; | |
| 262 | + } /* FeedFinder::_opml_rss_uris () */ | |
| 263 | + | |
| 129 | 264 | function _link_rel_feeds () { |
| 130 | 265 | $links = $this->_tags('link'); |
| 131 | 266 | $link_count = count($links); |
| 132 | 267 | |
| @@ -139,9 +274,9 @@ | ||
| 139 | 274 | } /* if */ |
| 140 | 275 | } /* if */ |
| 141 | 276 | } /* for */ |
| 142 | 277 | return $href; |
| 143 | - } | |
| 278 | + } /* FeedFinder::_link_rel_feeds () */ | |
| 144 | 279 | |
| 145 | 280 | function _a_href_feeds ($obvious = TRUE) { |
| 146 | 281 | $pattern = ($obvious ? $this->_obvious_feed_url : $this->_maybe_feed_url); |
| 147 | 282 | |
| @@ -155,13 +290,68 @@ | ||
| 155 | 290 | $href[] = $links[$n]['href']; |
| 156 | 291 | } /* if */ |
| 157 | 292 | } /* for */ |
| 158 | 293 | return $href; |
| 159 | - } | |
| 294 | + } /* FeedFinder::_link_a_href_feeds () */ | |
| 160 | 295 | |
| 296 | + function _url_manipulation_feeds () { | |
| 297 | + $href = array(); | |
| 298 | + | |
| 299 | + // check for HTTP GET parameters that look feed-like. | |
| 300 | + $bits = parse_url($this->uri); | |
| 301 | + foreach (array('rss', 'rss2', 'atom', 'rdf') as $format) : | |
| 302 | + if (isset($bits['query']) and (strlen($bits['query']) > 0)) : | |
| 303 | + $newQuery = preg_replace('/([?&=])(rss2?|atom|rdf)/i', '$1'.$format, $bits['query']); | |
| 304 | + else : | |
| 305 | + $newQuery = NULL; | |
| 306 | + endif; | |
| 307 | + | |
| 308 | + if (isset($bits['path']) and (strlen($bits['path']) > 0)) : | |
| 309 | + $newPath = preg_replace('!([/.])(rss2?|atom|rdf)!i', '$1'.$format, $bits['path']); | |
| 310 | + else : | |
| 311 | + $newPath = NULL; | |
| 312 | + endif; | |
| 313 | + | |
| 314 | + // Reassemble, check and return | |
| 315 | + $credentials = ''; | |
| 316 | + if (isset($bits['user'])) : | |
| 317 | + $credentials = $bits['user']; | |
| 318 | + if (isset($bits['pass'])) : | |
| 319 | + $credentials .= ':'.$bits['pass']; | |
| 320 | + endif; | |
| 321 | + $credentials .= '@'; | |
| 322 | + endif; | |
| 323 | + | |
| 324 | + // Variations on a theme | |
| 325 | + $newUrl[0] = ('' | |
| 326 | + .(isset($bits['scheme']) ? $bits['scheme'].':' : '') | |
| 327 | + .(isset($bits['host']) ? '//'.$credentials.$bits['host'] : '') | |
| 328 | + .( !is_null($newPath) ? $newPath : '') | |
| 329 | + .( !is_null($newQuery) ? '?'.$newQuery : '') | |
| 330 | + .(isset($bits['fragment']) ? '#'.$bits['fragment'] : '') | |
| 331 | + ); | |
| 332 | + $newUrl[1] = ('' | |
| 333 | + .(isset($bits['scheme']) ? $bits['scheme'].':' : '') | |
| 334 | + .(isset($bits['host']) ? '//'.$credentials.$bits['host'] : '') | |
| 335 | + .( !is_null($newPath) ? $newPath : '') | |
| 336 | + .(isset($bits['query']) ? '?'.$bits['query'] : '') | |
| 337 | + .(isset($bits['fragment']) ? '#'.$bits['fragment'] : '') | |
| 338 | + ); | |
| 339 | + $newUrl[2] = ('' | |
| 340 | + .(isset($bits['scheme']) ? $bits['scheme'].':' : '') | |
| 341 | + .(isset($bits['host']) ? '//'.$credentials.$bits['host'] : '') | |
| 342 | + .(isset($bits['path']) ? $bits['path'] : '') | |
| 343 | + .( !is_null($newQuery) ? '?'.$newQuery : '') | |
| 344 | + .(isset($bits['fragment']) ? '#'.$bits['fragment'] : '') | |
| 345 | + ); | |
| 346 | + $href = array_merge($href, $newUrl); | |
| 347 | + endforeach; | |
| 348 | + return array_unique($href); | |
| 349 | + } /* FeedFinder::_url_manipulation_feeds () */ | |
| 350 | + | |
| 161 | 351 | function _tags ($tag) { |
| 162 | 352 | $html = $this->data(); |
| 163 | - | |
| 353 | + | |
| 164 | 354 | // search through the HTML, save all <link> tags |
| 165 | 355 | // and store each link's attributes in an associative array |
| 166 | 356 | preg_match_all('/<'.$tag.'\s+(.*?)\s*\/?>/si', $html, $matches); |
| 167 | 357 | $links = $matches[1]; |
| @@ -178,7 +368,7 @@ | ||
| 178 | 368 | } /* foreach */ |
| 179 | 369 | $ret[$n] = $final_link; |
| 180 | 370 | } /* for */ |
| 181 | 371 | return $ret; |
| 182 | - } | |
| 372 | + } /* FeedFinder::_tags () */ | |
| 183 | 373 | } /* class FeedFinder */ |
| 184 | 374 | |