$this->enable_exceptions = $enable;
* Initialize the feed object
* This is what makes everything happen. Period. This is where all of the
* configuration options get processed, feeds are fetched, cached, and
* parsed, and all of that other good stuff.
* @return boolean True if successful, false otherwise
// Check absolute bare minimum requirements.
if (!extension_loaded('xml') || !extension_loaded('pcre')) {
$this->error = 'XML or PCRE extensions not loaded!';
// Then check the xml extension is sane (i.e., libxml 2.7.x issue on PHP < 5.2.9 and libxml 2.7.0 to 2.7.2 on any version) if we don't have xmlreader.
elseif (!extension_loaded('xmlreader')) {
static $xml_is_sane = null;
if ($xml_is_sane === null) {
$parser_check = xml_parser_create();
xml_parse_into_struct($parser_check, '<foo>&</foo>', $values);
xml_parser_free($parser_check);
$xml_is_sane = isset($values[0]['value']);
// The default sanitize class gets set in the constructor, check if it has
if ($this->registry->get_class(Sanitize::class) !== 'SimplePie\Sanitize') {
$this->sanitize = $this->registry->create(Sanitize::class);
if (method_exists($this->sanitize, 'set_registry')) {
$this->sanitize->set_registry($this->registry);
// Pass whatever was set with config options over to the sanitizer.
// Pass the classes in for legacy support; new classes should use the registry instead
$this->sanitize->pass_cache_data(
$this->registry->get_class(Cache::class),
$this->sanitize->pass_file_data($this->registry->get_class(File::class), $this->timeout, $this->useragent, $this->force_fsockopen, $this->curl_options);
if (!empty($this->multifeed_url)) {
$this->multifeed_objects = [];
foreach ($this->multifeed_url as $url) {
$this->multifeed_objects[$i] = clone $this;
$this->multifeed_objects[$i]->set_feed_url($url);
$single_success = $this->multifeed_objects[$i]->init();
$success |= $single_success;
$this->error[$i] = $this->multifeed_objects[$i]->error();
} elseif ($this->feed_url === null && $this->raw_data === null) {
$this->check_modified = false;
$this->multifeed_objects = [];
if ($this->feed_url !== null) {
$parsed_feed_url = $this->registry->call(Misc::class, 'parse_url', [$this->feed_url]);
// Decide whether to enable caching
if ($this->enable_cache && $parsed_feed_url['scheme'] !== '') {
$cache = $this->get_cache($this->feed_url);
// Fetch the data via \SimplePie\File into $this->raw_data
if (($fetched = $this->fetch_data($cache)) === true) {
} elseif ($fetched === false) {
[$headers, $sniffed] = $fetched;
if (empty($this->raw_data)) {
$this->error = "A feed could not be found at `$this->feed_url`. Empty body.";
$this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]);
// Set up array of possible encodings
// First check to see if input has been overridden.
if ($this->input_encoding !== false) {
$encodings[] = strtoupper($this->input_encoding);
$application_types = ['application/xml', 'application/xml-dtd', 'application/xml-external-parsed-entity'];
$text_types = ['text/xml', 'text/xml-external-parsed-entity'];
// RFC 3023 (only applies to sniffed content)
if (in_array($sniffed, $application_types) || substr($sniffed, 0, 12) === 'application/' && substr($sniffed, -4) === '+xml') {
if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) {
$encodings[] = strtoupper($charset[1]);
$encodings = array_merge($encodings, $this->registry->call(Misc::class, 'xml_encoding', [$this->raw_data, &$this->registry]));
} elseif (in_array($sniffed, $text_types) || substr($sniffed, 0, 5) === 'text/' && substr($sniffed, -4) === '+xml') {
if (isset($headers['content-type']) && preg_match('/;\x20?charset=([^;]*)/i', $headers['content-type'], $charset)) {
$encodings[] = strtoupper($charset[1]);
$encodings[] = 'US-ASCII';
// Text MIME-type default
elseif (substr($sniffed, 0, 5) === 'text/') {
// Fallback to XML 1.0 Appendix F.1/UTF-8/ISO-8859-1
$encodings = array_merge($encodings, $this->registry->call(Misc::class, 'xml_encoding', [$this->raw_data, &$this->registry]));
$encodings[] = 'ISO-8859-1';
// There's no point in trying an encoding twice
$encodings = array_unique($encodings);
// Loop through each possible encoding, till we return something, or run out of possibilities
foreach ($encodings as $encoding) {
// Change the encoding to UTF-8 (as we always use UTF-8 internally)
if ($utf8_data = $this->registry->call(Misc::class, 'change_encoding', [$this->raw_data, $encoding, 'UTF-8'])) {
$parser = $this->registry->create(Parser::class);
if ($parser->parse($utf8_data, 'UTF-8', $this->permanent_url)) {
$this->data = $parser->get_data();
if (!($this->get_type() & ~self::TYPE_NONE)) {
$this->error = "A feed could not be found at `$this->feed_url`. This does not appear to be a valid RSS or Atom feed.";
$this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]);
$this->data['headers'] = $headers;
$this->data['build'] = \SimplePie\Misc::get_build();
// Cache the file if caching is enabled
$this->data['cache_expiration_time'] = $this->cache_duration + time();
if ($cache && !$cache->set_data($this->get_cache_filename($this->feed_url), $this->data, $this->cache_duration)) {
trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
// We have an error, just set \SimplePie\Misc::error to it and quit
$this->error = $this->feed_url;
$this->error .= sprintf(' is invalid XML, likely due to invalid characters. XML error: %s at line %d, column %d', $parser->get_error_string(), $parser->get_current_line(), $parser->get_current_column());
$this->error = 'The data could not be converted to UTF-8.';
if (!extension_loaded('mbstring') && !extension_loaded('iconv') && !class_exists('\UConverter')) {
$this->error .= ' You MUST have either the iconv, mbstring or intl (PHP 5.5+) extension installed and enabled.';
if (!extension_loaded('iconv')) {
$missingExtensions[] = 'iconv';
if (!extension_loaded('mbstring')) {
$missingExtensions[] = 'mbstring';
if (!class_exists('\UConverter')) {
$missingExtensions[] = 'intl (PHP 5.5+)';
$this->error .= ' Try installing/enabling the ' . implode(' or ', $missingExtensions) . ' extension.';
$this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]);
* Fetch the data via \SimplePie\File
* If the data is already cached, attempt to fetch it from there instead
* @param Base|DataCache|false $cache Cache handler, or false to not load from the cache
* @return array|true Returns true if the data was loaded from the cache, or an array of HTTP headers and sniffed type
protected function fetch_data(&$cache)
if (is_object($cache) && $cache instanceof Base) {
// @trigger_error(sprintf('Providing $cache as "\SimplePie\Cache\Base" in %s() is deprecated since SimplePie 1.8.0, please provide "\SimplePie\Cache\DataCache" implementation instead.', __METHOD__), \E_USER_DEPRECATED);
$cache = new BaseDataCache($cache);
if ($cache !== false && !$cache instanceof DataCache) {
throw new InvalidArgumentException(sprintf(
'%s(): Argument #1 ($cache) must be of type %s|false',
$cacheKey = $this->get_cache_filename($this->feed_url);
// If it's enabled, use the cache
$this->data = $cache->get_data($cacheKey, []);
if (!empty($this->data)) {
// If the cache is for an outdated build of SimplePie
if (!isset($this->data['build']) || $this->data['build'] !== \SimplePie\Misc::get_build()) {
$cache->delete_data($cacheKey);
// If we've hit a collision just rerun it with caching disabled
elseif (isset($this->data['url']) && $this->data['url'] !== $this->feed_url) {
// If we've got a non feed_url stored (if the page isn't actually a feed, or is a redirect) use that URL.
elseif (isset($this->data['feed_url'])) {
// Do not need to do feed autodiscovery yet.
if ($this->data['feed_url'] !== $this->data['url']) {
$this->set_feed_url($this->data['feed_url']);
$this->data['url'] = $this->data['feed_url'];
$cache->set_data($this->get_cache_filename($this->feed_url), $this->data, $this->autodiscovery_cache_duration);
$cache->delete_data($this->get_cache_filename($this->feed_url));
// Check if the cache has been updated
elseif (isset($this->data['cache_expiration_time']) && $this->data['cache_expiration_time'] > time()) {
// Want to know if we tried to send last-modified and/or etag headers
// when requesting this file. (Note that it's up to the file to
// support this, but we don't always send the headers either.)
$this->check_modified = true;
if (isset($this->data['headers']['last-modified']) || isset($this->data['headers']['etag'])) {
'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
if (isset($this->data['headers']['last-modified'])) {
$headers['if-modified-since'] = $this->data['headers']['last-modified'];
if (isset($this->data['headers']['etag'])) {
$headers['if-none-match'] = $this->data['headers']['etag'];
$file = $this->registry->create(File::class, [$this->feed_url, $this->timeout / 10, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options]);
$this->status_code = $file->status_code;
if ($file->status_code === 304) {
// Set raw_data to false here too, to signify that the cache
$cache->set_data($cacheKey, $this->data, $this->cache_duration);
$this->check_modified = false;
if ($this->force_cache_fallback) {
$cache->set_data($cacheKey, $this->data, $this->cache_duration);
// If the cache is still valid, just return true
// If we don't already have the file (it'll only exist if we've opened it to check if the cache has been modified), open it.
if ($this->file instanceof \SimplePie\File && $this->file->url === $this->feed_url) {
'Accept' => 'application/atom+xml, application/rss+xml, application/rdf+xml;q=0.9, application/xml;q=0.8, text/xml;q=0.8, text/html;q=0.7, unknown/unknown;q=0.1, application/unknown;q=0.1, */*;q=0.1',
$file = $this->registry->create(File::class, [$this->feed_url, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options]);
$this->status_code = $file->status_code;
// If the file connection has an error, set SimplePie::error to that and quit
if (!$file->success && !($file->method & self::FILE_SOURCE_REMOTE === 0 || ($file->status_code === 200 || $file->status_code > 206 && $file->status_code < 300))) {
$this->error = $file->error;
return !empty($this->data);
if (!$this->force_feed) {
// Check if the supplied URL is a feed, if it isn't, look for it.
$locate = $this->registry->create(Locator::class, [&$file, $this->timeout, $this->useragent, $this->max_checked_feeds, $this->force_fsockopen, $this->curl_options]);
if (!$locate->is_feed($file)) {
$copyStatusCode = $file->status_code;
$copyContentType = $file->headers['content-type'];
if (class_exists('DOMXpath') && function_exists('Mf2\parse')) {
$doc = new \DOMDocument();
@$doc->loadHTML($file->body);
$xpath = new \DOMXpath($doc);
// Check for both h-feed and h-entry, as both a feed with no entries
// and a list of entries without an h-feed wrapper are both valid.
$query = '//*[contains(concat(" ", @class, " "), " h-feed ") or '.
'contains(concat(" ", @class, " "), " h-entry ")]';
$result = $xpath->query($query);
$microformats = $result->length !== 0;
// Now also do feed discovery, but if microformats were found don't
// overwrite the current value of file.
$discovered = $locate->find(
$this->all_discovered_feeds
if ($hub = $locate->get_rel_link('hub')) {
$self = $locate->get_rel_link('self');
$this->store_links($file, $hub, $self);
// Push the current file onto all_discovered feeds so the user can
// be shown this as one of the options.
if (isset($this->all_discovered_feeds)) {
$this->all_discovered_feeds[] = $file;
// We need to unset this so that if SimplePie::set_file() has
// been called that object is untouched
$this->error = "A feed could not be found at `$this->feed_url`; the status code is `$copyStatusCode` and content-type is `$copyContentType`";
$this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, __FILE__, __LINE__]);
} catch (\SimplePie\Exception $e) {
// We need to unset this so that if SimplePie::set_file() has been called that object is untouched
// This is usually because DOMDocument doesn't exist
$this->error = $e->getMessage();
$this->registry->call(Misc::class, 'error', [$this->error, E_USER_NOTICE, $e->getFile(), $e->getLine()]);
'url' => $this->feed_url,
'feed_url' => $file->url,
'build' => \SimplePie\Misc::get_build(),
'cache_expiration_time' => $this->cache_duration + time(),
if (!$cache->set_data($cacheKey, $this->data, $this->cache_duration)) {
trigger_error("$this->cache_location is not writable. Make sure you've set the correct relative or absolute path, and that the location is server-writable.", E_USER_WARNING);
$this->feed_url = $file->url;
$this->raw_data = $file->body;
$this->permanent_url = $file->permanent_url;
$headers = $file->headers;
$sniffer = $this->registry->create(Sniffer::class, [&$file]);
$sniffed = $sniffer->get_type();
return [$headers, $sniffed];
* Get the error message for the occurred error
* @return string|array Error message, or array of messages for multifeeds
* Get the last HTTP status code
* @return int Status code
public function status_code()
return $this->status_code;
* This is the same as the old `$feed->enable_xml_dump(true)`, but returns
* the data instead of printing it.
* @return string|boolean Raw XML data, false if the cache is used
public function get_raw_data()
* Get the character encoding used for output
public function get_encoding()
return $this->sanitize->output_encoding;
* Send the content-type header with correct encoding
* This method ensures that the SimplePie-enabled page is being served with
* the correct {@link http://www.iana.org/assignments/media-types/ mime-type}
* and character encoding HTTP headers (character encoding determined by the
* {@see set_output_encoding} config option).
* This won't work properly if any content or whitespace has already been
* sent to the browser, because it relies on PHP's
* {@link http://php.net/header header()} function, and these are the
* circumstances under which the function works.
* Because it's setting these settings for the entire page (as is the nature
* of HTTP headers), this should only be used once per page (again, at the
* @param string $mime MIME type to serve the page as
public function handle_content_type($mime = 'text/html')
$header = "Content-type: $mime;";
if ($this->get_encoding()) {
$header .= ' charset=' . $this->get_encoding();
$header .= ' charset=UTF-8';
* Get the type of the feed
* This returns a \SimplePie\SimplePie::TYPE_* constant, which can be tested against
* using {@link http://php.net/language.operators.bitwise bitwise operators}
* @since 0.8 (usage changed to using constants in 1.0)
* @see \SimplePie\SimplePie::TYPE_NONE Unknown.
* @see \SimplePie\SimplePie::TYPE_RSS_090 RSS 0.90.
* @see \SimplePie\SimplePie::TYPE_RSS_091_NETSCAPE RSS 0.91 (Netscape).
* @see \SimplePie\SimplePie::TYPE_RSS_091_USERLAND RSS 0.91 (Userland).
* @see \SimplePie\SimplePie::TYPE_RSS_091 RSS 0.91.
* @see \SimplePie\SimplePie::TYPE_RSS_092 RSS 0.92.
* @see \SimplePie\SimplePie::TYPE_RSS_093 RSS 0.93.