public function rename_attributes($attribs = '')
$attribs = $this->rename_attributes;
$this->sanitize->rename_attributes($attribs);
* @param string[]|string $attribs
public function strip_attributes($attribs = '')
$attribs = $this->strip_attributes;
$this->sanitize->strip_attributes($attribs);
* @param array<string, array<string, string>>|'' $attribs
public function add_attributes($attribs = '')
$attribs = $this->add_attributes;
$this->sanitize->add_attributes($attribs);
* Set the output encoding
* Allows you to override SimplePie's output to match that of your webpage.
* This is useful for times when your webpages are not being served as
* UTF-8. This setting will be obeyed by {@see handle_content_type()}, and
* is similar to {@see set_input_encoding()}.
* It should be noted, however, that not all character encodings can support
* all characters. If your page is being served as ISO-8859-1 and you try
* to display a Japanese feed, you'll likely see garbled characters.
* Because of this, it is highly recommended to ensure that your webpages
* The number of supported character encodings depends on whether your web
* host supports {@link http://php.net/mbstring mbstring},
* {@link http://php.net/iconv iconv}, or both. See
* {@link http://simplepie.org/wiki/faq/Supported_Character_Encodings} for
* @param string $encoding
public function set_output_encoding(string $encoding = 'UTF-8')
$this->sanitize->set_output_encoding($encoding);
public function strip_comments(bool $strip = false)
$this->sanitize->strip_comments($strip);
* Set element/attribute key/value pairs of HTML attributes
* containing URLs that need to be resolved relative to the feed
* Defaults to |a|@href, |area|@href, |blockquote|@cite, |del|@cite,
* |form|@action, |img|@longdesc, |img|@src, |input|@src, |ins|@cite,
* @param array<string, string|string[]>|null $element_attribute Element/attribute key/value pairs, null for default
public function set_url_replacements(?array $element_attribute = null)
$this->sanitize->set_url_replacements($element_attribute);
* Set the list of domains for which to force HTTPS.
* @see Sanitize::set_https_domains()
* @param array<string> $domains List of HTTPS domains. Example array('biz', 'example.com', 'example.org', 'www.example.net').
public function set_https_domains(array $domains = [])
$this->sanitize->set_https_domains($domains);
* Set the handler to enable the display of cached images.
* @param string|false $page Web-accessible path to the handler_image.php file.
* @param string $qs The query string that the value should be passed to.
public function set_image_handler($page = false, string $qs = 'i')
$this->sanitize->set_image_handler($page . '?' . $qs . '=');
$this->image_handler = '';
* Set the limit for items returned per-feed with multifeeds
* @param int $limit The maximum number of items to return.
public function set_item_limit(int $limit = 0)
$this->item_limit = $limit;
* Enable throwing exceptions
* @param bool $enable Should we throw exceptions, or use the old-style error property?
public function enable_exceptions(bool $enable = true)
$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 bool 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);
if (\PHP_VERSION_ID < 80000) {
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) !== Sanitize::class) {
$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
$cache = $this->registry->get_class(Cache::class);
\assert($cache !== null, 'Cache must be defined');
$this->sanitize->pass_cache_data(
$http_client = $this->get_http_client();
if ($http_client instanceof Psr18Client) {
$this->sanitize->set_http_client(
$http_client->getHttpClient(),
$http_client->getRequestFactory(),
$http_client->getUriFactory()
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 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'] = 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 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__]);
* 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{array<string, string>, string}|bool 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 ($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);
// @phpstan-ignore-next-line Enforce PHPDoc type.
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'] !== 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' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER,
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->get_http_client()->request(Client::METHOD_GET, $this->feed_url, $headers);
$this->status_code = $file->get_status_code();
} catch (ClientException $th) {
$this->check_modified = false;
if ($this->force_cache_fallback) {
$this->data['cache_expiration_time'] = $this->cache_duration + time();
$cache->set_data($cacheKey, $this->data, $this->cache_duration);
$failedFileReason = $th->getMessage();
if ($this->status_code === 304) {
// Set raw_data to false here too, to signify that the cache
$this->data['cache_expiration_time'] = $this->cache_duration + time();
$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 File && $this->file->get_final_requested_uri() === $this->feed_url) {
} elseif (isset($failedFileReason)) {
// Do not try to fetch again if we already failed once.
// If the file connection had an error, set SimplePie::error to that and quit
$this->error = $failedFileReason;
return !empty($this->data);
'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER,
$file = $this->get_http_client()->request(Client::METHOD_GET, $this->feed_url, $headers);
} catch (ClientException $th) {
// If the file connection has an error, set SimplePie::error to that and quit
$this->error = $th->getMessage();
return !empty($this->data);
$this->status_code = $file->get_status_code();
// If the file connection has an error, set SimplePie::error to that and quit
if (!(!Misc::is_remote_uri($file->get_final_requested_uri()) || ($file->get_status_code() === 200 || $file->get_status_code() > 206 && $file->get_status_code() < 300))) {
$this->error = 'Retrieved unsupported status code "' . $this->status_code . '"';
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 instanceof File) ? File::fromResponse($file) : $file,