// This really returns string|false but changing encoding is uncommon and we are going to deprecate it, so let’s just lie to PHPStan in the interest of cleaner annotations.
return $this->sanitize->sanitize($data, $type, $base);
} catch (SimplePieException $e) {
if (!$this->enable_exceptions) {
$this->error = $e->getMessage();
$this->registry->call(Misc::class, 'error', [$this->error, E_USER_WARNING, $e->getFile(), $e->getLine()]);
* Get the title of the feed
* Uses `<atom:title>`, `<title>` or `<dc:title>`
* @since 1.0 (previously called `get_feed_title` since 0.8)
public function get_title()
if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'title')) {
return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'title')) {
return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'title')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'title')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'title')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'title')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'title')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
* Get a category for the feed
* @param int $key The category that you want to return. Remember that arrays begin with 0, not 1
public function get_category(int $key = 0)
$categories = $this->get_categories();
if (isset($categories[$key])) {
return $categories[$key];
* Get all categories for the feed
* Uses `<atom:category>`, `<category>` or `<dc:subject>`
* @return array<Category>|null List of {@see Category} objects
public function get_categories()
foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'category') as $category) {
if (isset($category['attribs']['']['term'])) {
$term = $this->sanitize($category['attribs']['']['term'], self::CONSTRUCT_TEXT);
if (isset($category['attribs']['']['scheme'])) {
$scheme = $this->sanitize($category['attribs']['']['scheme'], self::CONSTRUCT_TEXT);
if (isset($category['attribs']['']['label'])) {
$label = $this->sanitize($category['attribs']['']['label'], self::CONSTRUCT_TEXT);
$categories[] = $this->registry->create(Category::class, [$term, $scheme, $label]);
foreach ((array) $this->get_channel_tags(self::NAMESPACE_RSS_20, 'category') as $category) {
// This is really the label, but keep this as the term also for BC.
// Label will also work on retrieving because that falls back to term.
$term = $this->sanitize($category['data'], self::CONSTRUCT_TEXT);
if (isset($category['attribs']['']['domain'])) {
$scheme = $this->sanitize($category['attribs']['']['domain'], self::CONSTRUCT_TEXT);
$categories[] = $this->registry->create(Category::class, [$term, $scheme, null]);
foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_11, 'subject') as $category) {
$categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], self::CONSTRUCT_TEXT), null, null]);
foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_10, 'subject') as $category) {
$categories[] = $this->registry->create(Category::class, [$this->sanitize($category['data'], self::CONSTRUCT_TEXT), null, null]);
if (!empty($categories)) {
return array_unique($categories);
* Get an author for the feed
* @param int $key The author that you want to return. Remember that arrays begin with 0, not 1
public function get_author(int $key = 0)
$authors = $this->get_authors();
if (isset($authors[$key])) {
* Get all authors for the feed
* Uses `<atom:author>`, `<author>`, `<dc:creator>` or `<itunes:author>`
* @return array<Author>|null List of {@see Author} objects
public function get_authors()
foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'author') as $author) {
if (isset($author['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'])) {
$name = $this->sanitize($author['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'], self::CONSTRUCT_TEXT);
if (isset($author['child'][self::NAMESPACE_ATOM_10]['uri'][0]['data'])) {
$uri = $author['child'][self::NAMESPACE_ATOM_10]['uri'][0];
$uri = $this->sanitize($uri['data'], self::CONSTRUCT_IRI, $this->get_base($uri));
if (isset($author['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'])) {
$email = $this->sanitize($author['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'], self::CONSTRUCT_TEXT);
if ($name !== null || $email !== null || $uri !== null) {
$authors[] = $this->registry->create(Author::class, [$name, $uri, $email]);
if ($author = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'author')) {
if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'])) {
$name = $this->sanitize($author[0]['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'], self::CONSTRUCT_TEXT);
if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['url'][0]['data'])) {
$url = $author[0]['child'][self::NAMESPACE_ATOM_03]['url'][0];
$url = $this->sanitize($url['data'], self::CONSTRUCT_IRI, $this->get_base($url));
if (isset($author[0]['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'])) {
$email = $this->sanitize($author[0]['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'], self::CONSTRUCT_TEXT);
if ($name !== null || $email !== null || $url !== null) {
$authors[] = $this->registry->create(Author::class, [$name, $url, $email]);
foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_11, 'creator') as $author) {
$authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]);
foreach ((array) $this->get_channel_tags(self::NAMESPACE_DC_10, 'creator') as $author) {
$authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]);
foreach ((array) $this->get_channel_tags(self::NAMESPACE_ITUNES, 'author') as $author) {
$authors[] = $this->registry->create(Author::class, [$this->sanitize($author['data'], self::CONSTRUCT_TEXT), null, null]);
return array_unique($authors);
* Get a contributor for the feed
* @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1
public function get_contributor(int $key = 0)
$contributors = $this->get_contributors();
if (isset($contributors[$key])) {
return $contributors[$key];
* Get all contributors for the feed
* Uses `<atom:contributor>`
* @return array<Author>|null List of {@see Author} objects
public function get_contributors()
foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'contributor') as $contributor) {
if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'])) {
$name = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_10]['name'][0]['data'], self::CONSTRUCT_TEXT);
if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['uri'][0]['data'])) {
$uri = $contributor['child'][self::NAMESPACE_ATOM_10]['uri'][0];
$uri = $this->sanitize($uri['data'], self::CONSTRUCT_IRI, $this->get_base($uri));
if (isset($contributor['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'])) {
$email = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_10]['email'][0]['data'], self::CONSTRUCT_TEXT);
if ($name !== null || $email !== null || $uri !== null) {
$contributors[] = $this->registry->create(Author::class, [$name, $uri, $email]);
foreach ((array) $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'contributor') as $contributor) {
if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'])) {
$name = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_03]['name'][0]['data'], self::CONSTRUCT_TEXT);
if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['url'][0]['data'])) {
$url = $contributor['child'][self::NAMESPACE_ATOM_03]['url'][0];
$url = $this->sanitize($url['data'], self::CONSTRUCT_IRI, $this->get_base($url));
if (isset($contributor['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'])) {
$email = $this->sanitize($contributor['child'][self::NAMESPACE_ATOM_03]['email'][0]['data'], self::CONSTRUCT_TEXT);
if ($name !== null || $email !== null || $url !== null) {
$contributors[] = $this->registry->create(Author::class, [$name, $url, $email]);
if (!empty($contributors)) {
return array_unique($contributors);
* Get a single link for the feed
* @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8)
* @param int $key The link that you want to return. Remember that arrays begin with 0, not 1
* @param string $rel The relationship of the link to return
* @return string|null Link URL
public function get_link(int $key = 0, string $rel = 'alternate')
$links = $this->get_links($rel);
if (isset($links[$key])) {
* Get the permalink for the item
* Returns the first link available with a relationship of "alternate".
* Identical to {@see get_link()} with key 0
* @since 1.0 (previously called `get_feed_link` since Preview Release, `get_feed_permalink()` since 0.8)
* @internal Added for parity between the parent-level and the item/entry-level.
* @return string|null Link URL
public function get_permalink()
return $this->get_link(0);
* Get all links for the feed
* Uses `<atom:link>` or `<link>`
* @param string $rel The relationship of links to return
* @return array<string>|null Links found for the feed (strings)
public function get_links(string $rel = 'alternate')
if (!isset($this->data['links'])) {
$this->data['links'] = [];
if ($links = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'link')) {
foreach ($links as $link) {
if (isset($link['attribs']['']['href'])) {
$link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate';
$this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], self::CONSTRUCT_IRI, $this->get_base($link));
if ($links = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'link')) {
foreach ($links as $link) {
if (isset($link['attribs']['']['href'])) {
$link_rel = (isset($link['attribs']['']['rel'])) ? $link['attribs']['']['rel'] : 'alternate';
$this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], self::CONSTRUCT_IRI, $this->get_base($link));
if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'link')) {
$this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0]));
if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'link')) {
$this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0]));
if ($links = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'link')) {
$this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], self::CONSTRUCT_IRI, $this->get_base($links[0]));
$keys = array_keys($this->data['links']);
foreach ($keys as $key) {
if ($this->registry->call(Misc::class, 'is_isegment_nz_nc', [$key])) {
if (isset($this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key])) {
$this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key] = array_merge($this->data['links'][$key], $this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key]);
$this->data['links'][$key] = &$this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key];
$this->data['links'][self::IANA_LINK_RELATIONS_REGISTRY . $key] = &$this->data['links'][$key];
} elseif (substr($key, 0, 41) === self::IANA_LINK_RELATIONS_REGISTRY) {
$this->data['links'][substr($key, 41)] = &$this->data['links'][$key];
$this->data['links'][$key] = array_unique($this->data['links'][$key]);
if (isset($this->data['headers']['link'])) {
$link_headers = $this->data['headers']['link'];
if (is_array($link_headers)) {
$link_headers = implode(',', $link_headers);
// https://datatracker.ietf.org/doc/html/rfc8288
if (is_string($link_headers) &&
preg_match_all('/<(?P<uri>[^>]+)>\s*;\s*rel\s*=\s*(?P<quote>"?)' . preg_quote($rel) . '(?P=quote)\s*(?=,|$)/i', $link_headers, $matches)) {
if (isset($this->data['links'][$rel])) {
return $this->data['links'][$rel];
* @return ?array<Response>
public function get_all_discovered_feeds()
return $this->all_discovered_feeds;
* Get the content for the item
* Uses `<atom:subtitle>`, `<atom:tagline>`, `<description>`,
* `<dc:description>`, `<itunes:summary>` or `<itunes:subtitle>`
* @since 1.0 (previously called `get_feed_description()` since 0.8)
public function get_description()
if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'subtitle')) {
return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'tagline')) {
return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_10, 'description')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_090, 'description')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_MAYBE_HTML, $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'description')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'description')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'description')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'summary')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_ITUNES, 'subtitle')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_HTML, $this->get_base($return[0]));
* Get the copyright info for the feed
* Uses `<atom:rights>`, `<atom:copyright>` or `<dc:rights>`
* @since 1.0 (previously called `get_feed_copyright()` since 0.8)
public function get_copyright()
if ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_10, 'rights')) {
return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_ATOM_03, 'copyright')) {
return $this->sanitize($return[0]['data'], $this->registry->call(Misc::class, 'atom_03_construct_type', [$return[0]['attribs']]), $this->get_base($return[0]));
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'copyright')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'rights')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'rights')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
* Get the language for the feed
* Uses `<language>`, `<dc:language>`, or @xml_lang
* @since 1.0 (previously called `get_feed_language()` since 0.8)
public function get_language()
if ($return = $this->get_channel_tags(self::NAMESPACE_RSS_20, 'language')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_11, 'language')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
} elseif ($return = $this->get_channel_tags(self::NAMESPACE_DC_10, 'language')) {
return $this->sanitize($return[0]['data'], self::CONSTRUCT_TEXT);
} elseif (isset($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['xml_lang'])) {
return $this->sanitize($this->data['child'][self::NAMESPACE_ATOM_10]['feed'][0]['xml_lang'], self::CONSTRUCT_TEXT);
} elseif (isset($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['xml_lang'])) {
return $this->sanitize($this->data['child'][self::NAMESPACE_ATOM_03]['feed'][0]['xml_lang'], self::CONSTRUCT_TEXT);
} elseif (isset($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['xml_lang'])) {
return $this->sanitize($this->data['child'][self::NAMESPACE_RDF]['RDF'][0]['xml_lang'], self::CONSTRUCT_TEXT);
} elseif (isset($this->data['headers']['content-language'])) {
return $this->sanitize($this->data['headers']['content-language'], self::CONSTRUCT_TEXT);
* Get the latitude coordinates for the item
* Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications
* Uses `<geo:lat>` or `<georss:point>`
* @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo
* @link http://www.georss.org/ GeoRSS
public function get_latitude()
if ($return = $this->get_channel_tags(self::NAMESPACE_W3C_BASIC_GEO, 'lat')) {
return (float) $return[0]['data'];
} elseif (($return = $this->get_channel_tags(self::NAMESPACE_GEORSS, 'point')) && preg_match('/^((?:-)?[0-9]+(?:\.[0-9]+)) ((?:-)?[0-9]+(?:\.[0-9]+))$/', trim($return[0]['data']), $match)) {
return (float) $match[1];
* Get the longitude coordinates for the feed
* Compatible with the W3C WGS84 Basic Geo and GeoRSS specifications
* Uses `<geo:long>`, `<geo:lon>` or `<georss:point>`