$this->max_checked_feeds,
$http_client = $this->get_http_client();
if ($http_client instanceof Psr18Client) {
$locate->set_http_client(
$http_client->getHttpClient(),
$http_client->getRequestFactory(),
$http_client->getUriFactory()
if (!$locate->is_feed($file)) {
$copyStatusCode = $file->get_status_code();
$copyContentType = $file->get_header_line('content-type');
if (class_exists('DOMXpath') && function_exists('Mf2\parse')) {
$doc = new \DOMDocument();
@$doc->loadHTML($file->get_body_content());
$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 ")]';
/** @var \DOMNodeList<\DOMElement> $result */
$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
$hub = $locate->get_rel_link('hub');
$self = $locate->get_rel_link('self');
$file = $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 ($this->all_discovered_feeds !== null) {
$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 (SimplePieException $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->get_final_requested_uri(),
'build' => 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->get_final_requested_uri();
$this->raw_data = $file->get_body_content();
$this->permanent_url = $file->get_permanent_uri();
foreach ($file->get_headers() as $key => $values) {
$headers[$key] = implode(', ', $values);
$sniffer = $this->registry->create(Sniffer::class, [&$file]);
$sniffed = $sniffer->get_type();
return [$headers, $sniffed];
* Get the error message for the occurred error
* @return string|string[]|null 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|false 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(string $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 self::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 self::TYPE_NONE Unknown.
* @see self::TYPE_RSS_090 RSS 0.90.
* @see self::TYPE_RSS_091_NETSCAPE RSS 0.91 (Netscape).
* @see self::TYPE_RSS_091_USERLAND RSS 0.91 (Userland).
* @see self::TYPE_RSS_091 RSS 0.91.
* @see self::TYPE_RSS_092 RSS 0.92.
* @see self::TYPE_RSS_093 RSS 0.93.
* @see self::TYPE_RSS_094 RSS 0.94.
* @see self::TYPE_RSS_10 RSS 1.0.
* @see self::TYPE_RSS_20 RSS 2.0.x.
* @see self::TYPE_RSS_RDF RDF-based RSS.
* @see self::TYPE_RSS_SYNDICATION Non-RDF-based RSS (truly intended as syndication format).
* @see self::TYPE_RSS_ALL Any version of RSS.
* @see self::TYPE_ATOM_03 Atom 0.3.
* @see self::TYPE_ATOM_10 Atom 1.0.
* @see self::TYPE_ATOM_ALL Any version of Atom.
* @see self::TYPE_ALL Any known/supported feed type.
* @return int-mask-of<self::TYPE_*> constant
public function get_type()
if (!isset($this->data['type'])) {
$this->data['type'] = self::TYPE_ALL;
if (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'])) {
$this->data['type'] &= self::TYPE_ATOM_10;
} elseif (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'])) {
$this->data['type'] &= self::TYPE_ATOM_03;
} elseif (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'])) {
if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['channel'])
|| isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['image'])
|| isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['item'])
|| isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_10]['textinput'])) {
$this->data['type'] &= self::TYPE_RSS_10;
if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['channel'])
|| isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['image'])
|| isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['item'])
|| isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][self::NAMESPACE_RSS_090]['textinput'])) {
$this->data['type'] &= self::TYPE_RSS_090;
} elseif (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'])) {
$this->data['type'] &= self::TYPE_RSS_ALL;
if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) {
switch (trim($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['attribs']['']['version'])) {
$this->data['type'] &= self::TYPE_RSS_091;
if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][self::NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) {
switch (trim($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][self::NAMESPACE_RSS_20]['skiphours']['hour'][0]['data'])) {
$this->data['type'] &= self::TYPE_RSS_091_NETSCAPE;
$this->data['type'] &= self::TYPE_RSS_091_USERLAND;
$this->data['type'] &= self::TYPE_RSS_092;
$this->data['type'] &= self::TYPE_RSS_093;
$this->data['type'] &= self::TYPE_RSS_094;
$this->data['type'] &= self::TYPE_RSS_20;
$this->data['type'] = self::TYPE_NONE;
return $this->data['type'];
* Get the URL for the feed
* When the 'permanent' mode is enabled, returns the original feed URL,
* except in the case of an `HTTP 301 Moved Permanently` status response,
* in which case the location of the first redirection is returned.
* When the 'permanent' mode is disabled (default),
* may or may not be different from the URL passed to {@see set_feed_url()},
* depending on whether auto-discovery was used, and whether there were
* any redirects along the way.
* @since Preview Release (previously called `get_feed_url()` since SimplePie 0.8.)
* @todo Support <itunes:new-feed-url>
* @todo Also, |atom:link|@rel=self
* @param bool $permanent Permanent mode to return only the original URL or the first redirection
* iff it is a 301 redirection
public function subscribe_url(bool $permanent = false)
if ($this->permanent_url !== null) {
// sanitize encodes ampersands which are required when used in a url.
if ($this->feed_url !== null) {
* Get data for an feed-level element
* This method allows you to get access to ANY element/attribute that is a
* sub-element of the opening feed tag.
* The return value is an indexed array of elements matching the given
* namespace and tag name. Each element has `attribs`, `data` and `child`
* subkeys. For `attribs` and `child`, these contain namespace subkeys.
* `attribs` then has one level of associative name => value data (where
* `value` is a string) after the namespace. `child` has tag-indexed keys
* after the namespace, each member of which is an indexed array matching
* // This is probably a bad example because we already support
* // <media:content> natively, but it shows you how to parse through
* $group = $item->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'group');
* $content = $group[0]['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['content'];
* $file = $content[0]['attribs']['']['url'];
* @see http://simplepie.org/wiki/faq/supported_xml_namespaces
* @param string $namespace The URL of the XML namespace of the elements you're trying to access
* @param string $tag Tag name
* @return array<array<string, mixed>>|null
public function get_feed_tags(string $namespace, string $tag)
$type = $this->get_type();
if ($type & self::TYPE_ATOM_10) {
if (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag])) {
return $this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['child'][$namespace][$tag];
if ($type & self::TYPE_ATOM_03) {
if (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag])) {
return $this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['child'][$namespace][$tag];
if ($type & self::TYPE_RSS_RDF) {
if (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag])) {
return $this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['child'][$namespace][$tag];
if ($type & self::TYPE_RSS_SYNDICATION) {
if (isset($this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag])) {
return $this->data['child'][self::NAMESPACE_RSS_20]['rss'][0]['child'][$namespace][$tag];
* Get data for an channel-level element
* This method allows you to get access to ANY element/attribute in the
* channel/header section of the feed.
* See {@see SimplePie::get_feed_tags()} for a description of the return value
* @see http://simplepie.org/wiki/faq/supported_xml_namespaces
* @param string $namespace The URL of the XML namespace of the elements you're trying to access
* @param string $tag Tag name
* @return array<array<string, mixed>>|null
public function get_channel_tags(string $namespace, string $tag)
$type = $this->get_type();
if ($type & self::TYPE_ATOM_ALL) {
if ($return = $this->get_feed_tags($namespace, $tag)) {
if ($type & self::TYPE_RSS_10) {
if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'channel')) {
if (isset($channel[0]['child'][$namespace][$tag])) {
return $channel[0]['child'][$namespace][$tag];
if ($type & self::TYPE_RSS_090) {
if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'channel')) {
if (isset($channel[0]['child'][$namespace][$tag])) {
return $channel[0]['child'][$namespace][$tag];
if ($type & self::TYPE_RSS_SYNDICATION) {
if ($channel = $this->get_feed_tags(self::NAMESPACE_RSS_20, 'channel')) {
if (isset($channel[0]['child'][$namespace][$tag])) {
return $channel[0]['child'][$namespace][$tag];
* Get data for an channel-level element
* This method allows you to get access to ANY element/attribute in the
* image/logo section of the feed.
* See {@see SimplePie::get_feed_tags()} for a description of the return value
* @see http://simplepie.org/wiki/faq/supported_xml_namespaces
* @param string $namespace The URL of the XML namespace of the elements you're trying to access
* @param string $tag Tag name
* @return array<array<string, mixed>>|null
public function get_image_tags(string $namespace, string $tag)
$type = $this->get_type();
if ($type & self::TYPE_RSS_10) {
if ($image = $this->get_feed_tags(self::NAMESPACE_RSS_10, 'image')) {
if (isset($image[0]['child'][$namespace][$tag])) {
return $image[0]['child'][$namespace][$tag];
if ($type & self::TYPE_RSS_090) {
if ($image = $this->get_feed_tags(self::NAMESPACE_RSS_090, 'image')) {
if (isset($image[0]['child'][$namespace][$tag])) {
return $image[0]['child'][$namespace][$tag];
if ($type & self::TYPE_RSS_SYNDICATION) {
if ($image = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'image')) {
if (isset($image[0]['child'][$namespace][$tag])) {
return $image[0]['child'][$namespace][$tag];
* Get the base URL value from the feed
* Uses `<xml:base>` if available,
* otherwise uses the first 'self' link or the first 'alternate' link of the feed,
* or failing that, the URL of the feed itself.
* @param array<string, mixed> $element
public function get_base(array $element = [])
if (!empty($element['xml_base_explicit']) && isset($element['xml_base'])) {
return $element['xml_base'];
if (($link = $this->get_link(0, 'alternate')) !== null) {
if (($link = $this->get_link(0, 'self')) !== null) {
return $this->subscribe_url() ?? '';
* @see Sanitize::sanitize()
* @param string $data Data to sanitize
* @param int-mask-of<SimplePie::CONSTRUCT_*> $type
* @param string $base Base URL to resolve URLs against
* @return string Sanitized data
public function sanitize(string $data, int $type, string $base = '')