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