diff --git a/wp-includes/SimplePie/autoloader.php b/wp-includes/SimplePie/autoloader.php index b1635ff15c..4356b456f9 100644 --- a/wp-includes/SimplePie/autoloader.php +++ b/wp-includes/SimplePie/autoloader.php @@ -1,46 +1,7 @@ - * @link http://galvani.cz/ - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @version 0.2.9 - */ +// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue +// SPDX-License-Identifier: BSD-3-Clause + +declare(strict_types=1); use SimplePie\Cache\Redis; @@ -16,6 +11,7 @@ class_exists('SimplePie\Cache\Redis'); // @trigger_error(sprintf('Using the "SimplePie_Cache_Redis" class is deprecated since SimplePie 1.7.0, use "SimplePie\Cache\Redis" instead.'), \E_USER_DEPRECATED); +/** @phpstan-ignore-next-line */ if (\false) { /** @deprecated since SimplePie 1.7.0, use "SimplePie\Cache\Redis" instead */ class SimplePie_Cache_Redis extends Redis diff --git a/wp-includes/SimplePie/library/SimplePie/Caption.php b/wp-includes/SimplePie/library/SimplePie/Caption.php index 1f3a475c87..0162101e76 100644 --- a/wp-includes/SimplePie/library/SimplePie/Caption.php +++ b/wp-includes/SimplePie/library/SimplePie/Caption.php @@ -1,46 +1,9 @@ data = $data; } @@ -96,7 +58,8 @@ class SimplePie_Decode_HTML_Entities */ public function parse() { - while (($this->position = strpos($this->data, '&', $this->position)) !== false) { + while (($position = strpos($this->data, '&', $this->position)) !== false) { + $this->position = $position; $this->consume(); $this->entity(); $this->consumed = ''; @@ -108,7 +71,7 @@ class SimplePie_Decode_HTML_Entities * Consume the next byte * * @access private - * @return mixed The next byte, or false, if there is no more data + * @return string|false The next byte, or false, if there is no more data */ public function consume() { @@ -125,9 +88,9 @@ class SimplePie_Decode_HTML_Entities * * @access private * @param string $chars Characters to consume - * @return mixed A series of characters that match the range, or false + * @return string|false A series of characters that match the range, or false */ - public function consume_range($chars) + public function consume_range(string $chars) { if ($len = strspn($this->data, $chars, $this->position)) { $data = substr($this->data, $this->position, $len); @@ -143,6 +106,7 @@ class SimplePie_Decode_HTML_Entities * Unconsume one byte * * @access private + * @return void */ public function unconsume() { @@ -154,6 +118,7 @@ class SimplePie_Decode_HTML_Entities * Decode an entity * * @access private + * @return void */ public function entity() { @@ -187,9 +152,13 @@ class SimplePie_Decode_HTML_Entities static $windows_1252_specials = [0x0D => "\x0A", 0x80 => "\xE2\x82\xAC", 0x81 => "\xEF\xBF\xBD", 0x82 => "\xE2\x80\x9A", 0x83 => "\xC6\x92", 0x84 => "\xE2\x80\x9E", 0x85 => "\xE2\x80\xA6", 0x86 => "\xE2\x80\xA0", 0x87 => "\xE2\x80\xA1", 0x88 => "\xCB\x86", 0x89 => "\xE2\x80\xB0", 0x8A => "\xC5\xA0", 0x8B => "\xE2\x80\xB9", 0x8C => "\xC5\x92", 0x8D => "\xEF\xBF\xBD", 0x8E => "\xC5\xBD", 0x8F => "\xEF\xBF\xBD", 0x90 => "\xEF\xBF\xBD", 0x91 => "\xE2\x80\x98", 0x92 => "\xE2\x80\x99", 0x93 => "\xE2\x80\x9C", 0x94 => "\xE2\x80\x9D", 0x95 => "\xE2\x80\xA2", 0x96 => "\xE2\x80\x93", 0x97 => "\xE2\x80\x94", 0x98 => "\xCB\x9C", 0x99 => "\xE2\x84\xA2", 0x9A => "\xC5\xA1", 0x9B => "\xE2\x80\xBA", 0x9C => "\xC5\x93", 0x9D => "\xEF\xBF\xBD", 0x9E => "\xC5\xBE", 0x9F => "\xC5\xB8"]; if ($hex) { - $codepoint = hexdec($codepoint); + // Cap to PHP_INT_MAX to ensure consistent behaviour if $codepoint is so large + // it cannot fit into int – just casting float to int might return junk (e.g. a negative number). + // If it is so large, `Misc::codepoint_to_utf8` will just return a replacement character. + $codepoint = (int) min(hexdec($codepoint), \PHP_INT_MAX); } else { - $codepoint = intval($codepoint); + // Casting string to int caps at PHP_INT_MAX automatically. + $codepoint = (int) $codepoint; } if (isset($windows_1252_specials[$codepoint])) { @@ -579,7 +548,9 @@ class SimplePie_Decode_HTML_Entities ]; for ($i = 0, $match = null; $i < 9 && $this->consume() !== false; $i++) { - $consumed = substr($this->consumed, 1); + // Cast for PHPStan on PHP < 8.0: We consumed as per the loop condition, + // so `$this->consumed` is non-empty and the substr offset is valid. + $consumed = (string) substr($this->consumed, 1); if (isset($entities[$consumed])) { $match = $consumed; } diff --git a/wp-includes/SimplePie/library/SimplePie/Enclosure.php b/wp-includes/SimplePie/library/SimplePie/Enclosure.php index 51e75d3d5e..dd3740ddb8 100644 --- a/wp-includes/SimplePie/library/SimplePie/Enclosure.php +++ b/wp-includes/SimplePie/library/SimplePie/Enclosure.php @@ -1,46 +1,9 @@ + */ class SimplePie_HTTP_Parser extends Parser { } diff --git a/wp-includes/SimplePie/library/SimplePie/IRI.php b/wp-includes/SimplePie/library/SimplePie/IRI.php index b947d63bf5..7b1e81e2ae 100644 --- a/wp-includes/SimplePie/library/SimplePie/IRI.php +++ b/wp-includes/SimplePie/library/SimplePie/IRI.php @@ -1,46 +1,9 @@ name = $name; $this->link = $link; $this->email = $email; diff --git a/wp-includes/SimplePie/src/Cache.php b/wp-includes/SimplePie/src/Cache.php index 8ed7038b49..587fb1522e 100644 --- a/wp-includes/SimplePie/src/Cache.php +++ b/wp-includes/SimplePie/src/Cache.php @@ -1,46 +1,9 @@ > */ protected static $handlers = [ - 'mysql' => 'SimplePie\Cache\MySQL', - 'memcache' => 'SimplePie\Cache\Memcache', - 'memcached' => 'SimplePie\Cache\Memcached', - 'redis' => 'SimplePie\Cache\Redis' + 'mysql' => Cache\MySQL::class, + 'memcache' => Cache\Memcache::class, + 'memcached' => Cache\Memcached::class, + 'redis' => Cache\Redis::class, ]; /** @@ -88,7 +49,7 @@ class Cache * @param Base::TYPE_FEED|Base::TYPE_IMAGE $extension 'spi' or 'spc' * @return Base Type of object depends on scheme of `$location` */ - public static function get_handler($location, $filename, $extension) + public static function get_handler(string $location, string $filename, $extension) { $type = explode(':', $location, 2); $type = $type[0]; @@ -104,8 +65,12 @@ class Cache * Create a new SimplePie\Cache object * * @deprecated since SimplePie 1.3.1, use {@see get_handler()} instead + * @param string $location + * @param string $filename + * @param Base::TYPE_FEED|Base::TYPE_IMAGE $extension + * @return Base */ - public function create($location, $filename, $extension) + public function create(string $location, string $filename, $extension) { trigger_error('Cache::create() has been replaced with Cache::get_handler() since SimplePie 1.3.1, use the registry system instead.', \E_USER_DEPRECATED); @@ -117,8 +82,9 @@ class Cache * * @param string $type DSN type to register for * @param class-string $class Name of handler class. Must implement Base + * @return void */ - public static function register($type, $class) + public static function register(string $type, $class) { self::$handlers[$type] = $class; } @@ -127,12 +93,17 @@ class Cache * Parse a URL into an array * * @param string $url - * @return array + * @return array */ - public static function parse_URL($url) + public static function parse_URL(string $url) { - $params = parse_url($url); - $params['extras'] = []; + $parsedUrl = parse_url($url); + + if ($parsedUrl === false) { + return []; + } + + $params = array_merge($parsedUrl, ['extras' => []]); if (isset($params['query'])) { parse_str($params['query'], $params['extras']); } diff --git a/wp-includes/SimplePie/src/Cache/Base.php b/wp-includes/SimplePie/src/Cache/Base.php index 674fe8c10e..7d6747a0bd 100644 --- a/wp-includes/SimplePie/src/Cache/Base.php +++ b/wp-includes/SimplePie/src/Cache/Base.php @@ -1,46 +1,9 @@ |\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property * @return bool Successfulness */ public function save($data); @@ -90,7 +51,7 @@ interface Base /** * Retrieve the data saved to the cache * - * @return array Data for SimplePie::$data + * @return array Data for SimplePie::$data */ public function load(); diff --git a/wp-includes/SimplePie/src/Cache/BaseDataCache.php b/wp-includes/SimplePie/src/Cache/BaseDataCache.php index 44dbd66d44..60207b0750 100644 --- a/wp-includes/SimplePie/src/Cache/BaseDataCache.php +++ b/wp-includes/SimplePie/src/Cache/BaseDataCache.php @@ -1,46 +1,9 @@ * * @param string $key The key of the item to store. - * @param array $value The value of the item to store, must be serializable. + * @param array $value The value of the item to store, must be serializable. * @param null|int $ttl Optional. The TTL value of this item. If no value is sent and * the driver supports TTL then the library may set a default value * for it or let the driver take care of that. diff --git a/wp-includes/SimplePie/src/Cache/CallableNameFilter.php b/wp-includes/SimplePie/src/Cache/CallableNameFilter.php index 0583d0d9b2..e95fa29175 100644 --- a/wp-includes/SimplePie/src/Cache/CallableNameFilter.php +++ b/wp-includes/SimplePie/src/Cache/CallableNameFilter.php @@ -1,61 +1,25 @@ callable = $callable; @@ -78,7 +42,7 @@ final class CallableNameFilter implements NameFilter * and encodings or longer lengths, but MUST support at least that * minimum. * - * @param string $name The name for the cache will be most likly an url with query string + * @param string $name The name for the cache will be most likely an url with query string * * @return string the new cache name */ diff --git a/wp-includes/SimplePie/src/Cache/DB.php b/wp-includes/SimplePie/src/Cache/DB.php index a5357b26da..5dac94f407 100644 --- a/wp-includes/SimplePie/src/Cache/DB.php +++ b/wp-includes/SimplePie/src/Cache/DB.php @@ -1,54 +1,17 @@ } First item is the serialized data for storage, second item is the unique ID for this item */ - protected static function prepare_simplepie_object_for_cache($data) + protected static function prepare_simplepie_object_for_cache(\SimplePie\SimplePie $data) { $items = $data->get_items(); $items_by_id = []; diff --git a/wp-includes/SimplePie/src/Cache/DataCache.php b/wp-includes/SimplePie/src/Cache/DataCache.php index e754132431..02ec59e9c7 100644 --- a/wp-includes/SimplePie/src/Cache/DataCache.php +++ b/wp-includes/SimplePie/src/Cache/DataCache.php @@ -1,46 +1,9 @@ * * @param string $key The key of the item to store. - * @param array $value The value of the item to store, must be serializable. + * @param array $value The value of the item to store, must be serializable. * @param null|int $ttl Optional. The TTL value of this item. If no value is sent and * the driver supports TTL then the library may set a default value * for it or let the driver take care of that. diff --git a/wp-includes/SimplePie/src/Cache/File.php b/wp-includes/SimplePie/src/Cache/File.php index 0ffcdf9e25..110a77c43e 100644 --- a/wp-includes/SimplePie/src/Cache/File.php +++ b/wp-includes/SimplePie/src/Cache/File.php @@ -1,54 +1,15 @@ location = $location; $this->filename = $name; @@ -100,7 +61,7 @@ class File implements Base /** * Save data to the cache * - * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property * @return bool Successfulness */ public function save($data) @@ -119,12 +80,12 @@ class File implements Base /** * Retrieve the data saved to the cache * - * @return array Data for SimplePie::$data + * @return array|false Data for SimplePie::$data */ public function load() { if (file_exists($this->name) && is_readable($this->name)) { - return unserialize(file_get_contents($this->name)); + return unserialize((string) file_get_contents($this->name)); } return false; } @@ -132,7 +93,7 @@ class File implements Base /** * Retrieve the last modified time for the cache * - * @return int Timestamp + * @return int|false Timestamp */ public function mtime() { diff --git a/wp-includes/SimplePie/src/Cache/Memcache.php b/wp-includes/SimplePie/src/Cache/Memcache.php index d20f9bfcf6..261bb5d484 100644 --- a/wp-includes/SimplePie/src/Cache/Memcache.php +++ b/wp-includes/SimplePie/src/Cache/Memcache.php @@ -1,46 +1,9 @@ */ protected $options; @@ -90,7 +51,7 @@ class Memcache implements Base * @param string $name Unique ID for the cache * @param Base::TYPE_FEED|Base::TYPE_IMAGE $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data */ - public function __construct($location, $name, $type) + public function __construct(string $location, string $name, $type) { $this->options = [ 'host' => '127.0.0.1', @@ -111,7 +72,7 @@ class Memcache implements Base /** * Save data to the cache * - * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property * @return bool Successfulness */ public function save($data) @@ -125,7 +86,7 @@ class Memcache implements Base /** * Retrieve the data saved to the cache * - * @return array Data for SimplePie::$data + * @return array|false Data for SimplePie::$data */ public function load() { @@ -140,7 +101,7 @@ class Memcache implements Base /** * Retrieve the last modified time for the cache * - * @return int Timestamp + * @return int|false Timestamp */ public function mtime() { diff --git a/wp-includes/SimplePie/src/Cache/Memcached.php b/wp-includes/SimplePie/src/Cache/Memcached.php index 8a8d31e7b2..924f31fca5 100644 --- a/wp-includes/SimplePie/src/Cache/Memcached.php +++ b/wp-includes/SimplePie/src/Cache/Memcached.php @@ -1,46 +1,10 @@ */ protected $options; @@ -87,7 +48,7 @@ class Memcached implements Base * @param string $name Unique ID for the cache * @param Base::TYPE_FEED|Base::TYPE_IMAGE $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data */ - public function __construct($location, $name, $type) + public function __construct(string $location, string $name, $type) { $this->options = [ 'host' => '127.0.0.1', @@ -107,7 +68,7 @@ class Memcached implements Base /** * Save data to the cache - * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property * @return bool Successfulness */ public function save($data) @@ -121,7 +82,7 @@ class Memcached implements Base /** * Retrieve the data saved to the cache - * @return array Data for SimplePie::$data + * @return array|false Data for SimplePie::$data */ public function load() { @@ -164,9 +125,10 @@ class Memcached implements Base /** * Set the last modified time and data to NativeMemcached + * @param string|false $data * @return bool Success status */ - private function setData($data) + private function setData($data): bool { if ($data !== false) { $this->cache->set($this->name . '_mtime', time(), (int)$this->options['extras']['timeout']); diff --git a/wp-includes/SimplePie/src/Cache/MySQL.php b/wp-includes/SimplePie/src/Cache/MySQL.php index 1d7986bfc3..73699ad75f 100644 --- a/wp-includes/SimplePie/src/Cache/MySQL.php +++ b/wp-includes/SimplePie/src/Cache/MySQL.php @@ -1,46 +1,9 @@ */ protected $options; @@ -87,7 +48,7 @@ class MySQL extends DB * @param string $name Unique ID for the cache * @param Base::TYPE_FEED|Base::TYPE_IMAGE $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data */ - public function __construct($location, $name, $type) + public function __construct(string $location, string $name, $type) { $this->options = [ 'user' => null, @@ -147,7 +108,7 @@ class MySQL extends DB /** * Save data to the cache * - * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property * @return bool Successfulness */ public function save($data) @@ -268,7 +229,7 @@ class MySQL extends DB /** * Retrieve the data saved to the cache * - * @return array Data for SimplePie::$data + * @return array|false Data for SimplePie::$data */ public function load() { @@ -310,7 +271,7 @@ class MySQL extends DB $query->bindValue(':feed', $this->id); if ($query->execute()) { while ($row = $query->fetchColumn()) { - $feed['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['entry'][] = unserialize($row); + $feed['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['entry'][] = unserialize((string) $row); } } else { return false; @@ -325,7 +286,7 @@ class MySQL extends DB /** * Retrieve the last modified time for the cache * - * @return int Timestamp + * @return int|false Timestamp */ public function mtime() { @@ -336,7 +297,7 @@ class MySQL extends DB $query = $this->mysql->prepare('SELECT `mtime` FROM `' . $this->options['extras']['prefix'] . 'cache_data` WHERE `id` = :id'); $query->bindValue(':id', $this->id); if ($query->execute() && ($time = $query->fetchColumn())) { - return $time; + return (int) $time; } return false; diff --git a/wp-includes/SimplePie/src/Cache/NameFilter.php b/wp-includes/SimplePie/src/Cache/NameFilter.php index 19ce7b0a23..9babb41f96 100644 --- a/wp-includes/SimplePie/src/Cache/NameFilter.php +++ b/wp-includes/SimplePie/src/Cache/NameFilter.php @@ -1,53 +1,14 @@ * * @param string $key The key of the item to store. - * @param array $value The value of the item to store, must be serializable. + * @param array $value The value of the item to store, must be serializable. * @param null|int $ttl Optional. The TTL value of this item. If no value is sent and * the driver supports TTL then the library may set a default value * for it or let the driver take care of that. * * @return bool True on success and false on failure. * - * @throws InvalidArgumentException + * @throws InvalidArgumentException&Throwable * MUST be thrown if the $key string is not a legal value. */ public function set_data(string $key, array $value, ?int $ttl = null): bool @@ -136,7 +98,7 @@ final class Psr16 implements DataCache * * @return bool True if the item was successfully removed. False if there was an error. * - * @throws InvalidArgumentException + * @throws InvalidArgumentException&Throwable * MUST be thrown if the $key string is not a legal value. */ public function delete_data(string $key): bool diff --git a/wp-includes/SimplePie/src/Cache/Redis.php b/wp-includes/SimplePie/src/Cache/Redis.php index 5997668c00..3ba0a3ed59 100644 --- a/wp-includes/SimplePie/src/Cache/Redis.php +++ b/wp-includes/SimplePie/src/Cache/Redis.php @@ -1,46 +1,10 @@ +// SPDX-License-Identifier: BSD-3-Clause + +declare(strict_types=1); namespace SimplePie\Cache; @@ -55,8 +19,6 @@ use Redis as NativeRedis; * connect to redis on `localhost` on port 6379. All tables will be * prefixed with `simple_primary-` and data will expire after 3600 seconds * - * @package SimplePie - * @subpackage Caching * @uses Redis * @deprecated since SimplePie 1.8.0, use implementation of "Psr\SimpleCache\CacheInterface" instead */ @@ -72,7 +34,7 @@ class Redis implements Base /** * Options * - * @var array + * @var array */ protected $options; @@ -88,9 +50,9 @@ class Redis implements Base * * @param string $location Location string (from SimplePie::$cache_location) * @param string $name Unique ID for the cache - * @param Base::TYPE_FEED|Base::TYPE_IMAGE $type Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data + * @param Base::TYPE_FEED|Base::TYPE_IMAGE|array|null $options Either TYPE_FEED for SimplePie data, or TYPE_IMAGE for image data */ - public function __construct($location, $name, $options = null) + public function __construct(string $location, string $name, $options = null) { //$this->cache = \flow\simple\cache\Redis::getRedisClientInstance(); $parsed = \SimplePie\Cache::parse_URL($location); @@ -118,6 +80,7 @@ class Redis implements Base /** * @param NativeRedis $cache + * @return void */ public function setRedisClient(NativeRedis $cache) { @@ -127,7 +90,7 @@ class Redis implements Base /** * Save data to the cache * - * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property + * @param array|\SimplePie\SimplePie $data Data to store in the cache. If passed a SimplePie object, only cache the $data property * @return bool Successfulness */ public function save($data) @@ -146,7 +109,7 @@ class Redis implements Base /** * Retrieve the data saved to the cache * - * @return array Data for SimplePie::$data + * @return array|false Data for SimplePie::$data */ public function load() { @@ -161,7 +124,7 @@ class Redis implements Base /** * Retrieve the last modified time for the cache * - * @return int Timestamp + * @return int|false Timestamp */ public function mtime() { diff --git a/wp-includes/SimplePie/src/Caption.php b/wp-includes/SimplePie/src/Caption.php index ef72c0b7ac..0c25fa5ce1 100644 --- a/wp-includes/SimplePie/src/Caption.php +++ b/wp-includes/SimplePie/src/Caption.php @@ -1,46 +1,9 @@ type = $type; $this->lang = $lang; $this->startTime = $startTime; diff --git a/wp-includes/SimplePie/src/Category.php b/wp-includes/SimplePie/src/Category.php index d7da8e6831..5439ef4415 100644 --- a/wp-includes/SimplePie/src/Category.php +++ b/wp-includes/SimplePie/src/Category.php @@ -1,46 +1,9 @@ term = $term; $this->scheme = $scheme; @@ -144,7 +104,7 @@ class Category * @param bool $strict * @return string|null */ - public function get_label($strict = false) + public function get_label(bool $strict = false) { if ($this->label === null && $strict !== true) { return $this->get_term(); diff --git a/wp-includes/SimplePie/src/Content/Type/Sniffer.php b/wp-includes/SimplePie/src/Content/Type/Sniffer.php index 8c7e089ac5..d1da5fd71f 100644 --- a/wp-includes/SimplePie/src/Content/Type/Sniffer.php +++ b/wp-includes/SimplePie/src/Content/Type/Sniffer.php @@ -1,49 +1,16 @@ file = $file; } @@ -84,19 +57,21 @@ class Sniffer */ public function get_type() { - if (isset($this->file->headers['content-type'])) { - if (!isset($this->file->headers['content-encoding']) - && ($this->file->headers['content-type'] === 'text/plain' - || $this->file->headers['content-type'] === 'text/plain; charset=ISO-8859-1' - || $this->file->headers['content-type'] === 'text/plain; charset=iso-8859-1' - || $this->file->headers['content-type'] === 'text/plain; charset=UTF-8')) { + $content_type = $this->file->has_header('content-type') ? $this->file->get_header_line('content-type') : null; + $content_encoding = $this->file->has_header('content-encoding') ? $this->file->get_header_line('content-encoding') : null; + if ($content_type !== null) { + if ($content_encoding === null + && ($content_type === 'text/plain' + || $content_type === 'text/plain; charset=ISO-8859-1' + || $content_type === 'text/plain; charset=iso-8859-1' + || $content_type === 'text/plain; charset=UTF-8')) { return $this->text_or_binary(); } - if (($pos = strpos($this->file->headers['content-type'], ';')) !== false) { - $official = substr($this->file->headers['content-type'], 0, $pos); + if (($pos = strpos($content_type, ';')) !== false) { + $official = substr($content_type, 0, $pos); } else { - $official = $this->file->headers['content-type']; + $official = $content_type; } $official = trim(strtolower($official)); @@ -130,12 +105,14 @@ class Sniffer */ public function text_or_binary() { - if (substr($this->file->body, 0, 2) === "\xFE\xFF" - || substr($this->file->body, 0, 2) === "\xFF\xFE" - || substr($this->file->body, 0, 4) === "\x00\x00\xFE\xFF" - || substr($this->file->body, 0, 3) === "\xEF\xBB\xBF") { + $body = $this->file->get_body_content(); + + if (substr($body, 0, 2) === "\xFE\xFF" + || substr($body, 0, 2) === "\xFF\xFE" + || substr($body, 0, 4) === "\x00\x00\xFE\xFF" + || substr($body, 0, 3) === "\xEF\xBB\xBF") { return 'text/plain'; - } elseif (preg_match('/[\x00-\x08\x0E-\x1A\x1C-\x1F]/', $this->file->body)) { + } elseif (preg_match('/[\x00-\x08\x0E-\x1A\x1C-\x1F]/', $body)) { return 'application/octet-stream'; } @@ -149,25 +126,27 @@ class Sniffer */ public function unknown() { - $ws = strspn($this->file->body, "\x09\x0A\x0B\x0C\x0D\x20"); - if (strtolower(substr($this->file->body, $ws, 14)) === 'file->body, $ws, 5)) === 'file->body, $ws, 7)) === 'file->get_body_content(); + + $ws = strspn($body, "\x09\x0A\x0B\x0C\x0D\x20"); + if (strtolower(substr($body, $ws, 14)) === 'file->body, 0, 5) === '%PDF-') { + } elseif (substr($body, 0, 5) === '%PDF-') { return 'application/pdf'; - } elseif (substr($this->file->body, 0, 11) === '%!PS-Adobe-') { + } elseif (substr($body, 0, 11) === '%!PS-Adobe-') { return 'application/postscript'; - } elseif (substr($this->file->body, 0, 6) === 'GIF87a' - || substr($this->file->body, 0, 6) === 'GIF89a') { + } elseif (substr($body, 0, 6) === 'GIF87a' + || substr($body, 0, 6) === 'GIF89a') { return 'image/gif'; - } elseif (substr($this->file->body, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") { + } elseif (substr($body, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") { return 'image/png'; - } elseif (substr($this->file->body, 0, 3) === "\xFF\xD8\xFF") { + } elseif (substr($body, 0, 3) === "\xFF\xD8\xFF") { return 'image/jpeg'; - } elseif (substr($this->file->body, 0, 2) === "\x42\x4D") { + } elseif (substr($body, 0, 2) === "\x42\x4D") { return 'image/bmp'; - } elseif (substr($this->file->body, 0, 4) === "\x00\x00\x01\x00") { + } elseif (substr($body, 0, 4) === "\x00\x00\x01\x00") { return 'image/vnd.microsoft.icon'; } @@ -177,20 +156,22 @@ class Sniffer /** * Sniff images * - * @return string Actual Content-Type + * @return string|false Actual Content-Type */ public function image() { - if (substr($this->file->body, 0, 6) === 'GIF87a' - || substr($this->file->body, 0, 6) === 'GIF89a') { + $body = $this->file->get_body_content(); + + if (substr($body, 0, 6) === 'GIF87a' + || substr($body, 0, 6) === 'GIF89a') { return 'image/gif'; - } elseif (substr($this->file->body, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") { + } elseif (substr($body, 0, 8) === "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") { return 'image/png'; - } elseif (substr($this->file->body, 0, 3) === "\xFF\xD8\xFF") { + } elseif (substr($body, 0, 3) === "\xFF\xD8\xFF") { return 'image/jpeg'; - } elseif (substr($this->file->body, 0, 2) === "\x42\x4D") { + } elseif (substr($body, 0, 2) === "\x42\x4D") { return 'image/bmp'; - } elseif (substr($this->file->body, 0, 4) === "\x00\x00\x01\x00") { + } elseif (substr($body, 0, 4) === "\x00\x00\x01\x00") { return 'image/vnd.microsoft.icon'; } @@ -204,16 +185,18 @@ class Sniffer */ public function feed_or_html() { - $len = strlen($this->file->body); - $pos = strspn($this->file->body, "\x09\x0A\x0D\x20\xEF\xBB\xBF"); + $body = $this->file->get_body_content(); + + $len = strlen($body); + $pos = strspn($body, "\x09\x0A\x0D\x20\xEF\xBB\xBF"); while ($pos < $len) { - switch ($this->file->body[$pos]) { + switch ($body[$pos]) { case "\x09": case "\x0A": case "\x0D": case "\x20": - $pos += strspn($this->file->body, "\x09\x0A\x0D\x20", $pos); + $pos += strspn($body, "\x09\x0A\x0D\x20", $pos); continue 2; case '<': @@ -224,29 +207,29 @@ class Sniffer return 'text/html'; } - if (substr($this->file->body, $pos, 3) === '!--') { + if (substr($body, $pos, 3) === '!--') { $pos += 3; - if ($pos < $len && ($pos = strpos($this->file->body, '-->', $pos)) !== false) { + if ($pos < $len && ($pos = strpos($body, '-->', $pos)) !== false) { $pos += 3; } else { return 'text/html'; } - } elseif (substr($this->file->body, $pos, 1) === '!') { - if ($pos < $len && ($pos = strpos($this->file->body, '>', $pos)) !== false) { + } elseif (substr($body, $pos, 1) === '!') { + if ($pos < $len && ($pos = strpos($body, '>', $pos)) !== false) { $pos++; } else { return 'text/html'; } - } elseif (substr($this->file->body, $pos, 1) === '?') { - if ($pos < $len && ($pos = strpos($this->file->body, '?>', $pos)) !== false) { + } elseif (substr($body, $pos, 1) === '?') { + if ($pos < $len && ($pos = strpos($body, '?>', $pos)) !== false) { $pos += 2; } else { return 'text/html'; } - } elseif (substr($this->file->body, $pos, 3) === 'rss' - || substr($this->file->body, $pos, 7) === 'rdf:RDF') { + } elseif (substr($body, $pos, 3) === 'rss' + || substr($body, $pos, 7) === 'rdf:RDF') { return 'application/rss+xml'; - } elseif (substr($this->file->body, $pos, 4) === 'feed') { + } elseif (substr($body, $pos, 4) === 'feed') { return 'application/atom+xml'; } else { return 'text/html'; diff --git a/wp-includes/SimplePie/src/Copyright.php b/wp-includes/SimplePie/src/Copyright.php index 150362f015..080e332bd0 100644 --- a/wp-includes/SimplePie/src/Copyright.php +++ b/wp-includes/SimplePie/src/Copyright.php @@ -1,46 +1,9 @@ url = $url; $this->label = $label; } diff --git a/wp-includes/SimplePie/src/Credit.php b/wp-includes/SimplePie/src/Credit.php index 947f231e81..3fc0ea1c17 100644 --- a/wp-includes/SimplePie/src/Credit.php +++ b/wp-includes/SimplePie/src/Credit.php @@ -1,46 +1,9 @@ role = $role; $this->scheme = $scheme; $this->name = $name; diff --git a/wp-includes/SimplePie/src/Enclosure.php b/wp-includes/SimplePie/src/Enclosure.php index e7dcb25270..89231c7f99 100644 --- a/wp-includes/SimplePie/src/Enclosure.php +++ b/wp-includes/SimplePie/src/Enclosure.php @@ -1,46 +1,9 @@ bitrate = $bitrate; $this->captions = $captions; $this->categories = $categories; @@ -254,10 +250,12 @@ class Enclosure $this->type = $type; $this->width = $width; - if (class_exists('idna_convert')) { - $idn = new \idna_convert(); - $parsed = \SimplePie\Misc::parse_url($link); - $this->link = \SimplePie\Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], $parsed['fragment']); + if (function_exists('idn_to_ascii')) { + $parsed = \SimplePie\Misc::parse_url($link ?? ''); + if ($parsed['authority'] !== '' && !ctype_print($parsed['authority'])) { + $authority = (string) \idn_to_ascii($parsed['authority'], \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46); + $this->link = \SimplePie\Misc::compress_parse_url($parsed['scheme'], $authority, $parsed['path'], $parsed['query'], $parsed['fragment']); + } } $this->handler = $this->get_handler(); // Needs to load last } @@ -293,7 +291,7 @@ class Enclosure * @param int $key * @return \SimplePie\Caption|null */ - public function get_caption($key = 0) + public function get_caption(int $key = 0) { $captions = $this->get_captions(); if (isset($captions[$key])) { @@ -306,7 +304,7 @@ class Enclosure /** * Get all captions * - * @return array|null Array of {@see \SimplePie\Caption} objects + * @return Caption[]|null */ public function get_captions() { @@ -323,7 +321,7 @@ class Enclosure * @param int $key * @return \SimplePie\Category|null */ - public function get_category($key = 0) + public function get_category(int $key = 0) { $categories = $this->get_categories(); if (isset($categories[$key])) { @@ -336,7 +334,7 @@ class Enclosure /** * Get all categories * - * @return array|null Array of {@see \SimplePie\Category} objects + * @return \SimplePie\Category[]|null */ public function get_categories() { @@ -381,7 +379,7 @@ class Enclosure * @param int $key * @return \SimplePie\Credit|null */ - public function get_credit($key = 0) + public function get_credit(int $key = 0) { $credits = $this->get_credits(); if (isset($credits[$key])) { @@ -394,7 +392,7 @@ class Enclosure /** * Get all credits * - * @return array|null Array of {@see \SimplePie\Credit} objects + * @return Credit[]|null */ public function get_credits() { @@ -425,7 +423,7 @@ class Enclosure * @param bool $convert Convert seconds into hh:mm:ss * @return string|int|null 'hh:mm:ss' string if `$convert` was specified, otherwise integer (or null if none found) */ - public function get_duration($convert = false) + public function get_duration(bool $convert = false) { if ($this->duration !== null) { if ($convert) { @@ -500,7 +498,7 @@ class Enclosure * @param int $key * @return string|null Hash as per `media:hash`, prefixed with "$algo:" */ - public function get_hash($key = 0) + public function get_hash(int $key = 0) { $hashes = $this->get_hashes(); if (isset($hashes[$key])) { @@ -513,7 +511,7 @@ class Enclosure /** * Get all credits * - * @return array|null Array of strings, see {@see get_hash()} + * @return string[]|null Array of strings, see {@see get_hash()} */ public function get_hashes() { @@ -559,7 +557,7 @@ class Enclosure * @param int $key * @return string|null */ - public function get_keyword($key = 0) + public function get_keyword(int $key = 0) { $keywords = $this->get_keywords(); if (isset($keywords[$key])) { @@ -572,7 +570,7 @@ class Enclosure /** * Get all keywords * - * @return array|null Array of strings + * @return string[]|null */ public function get_keywords() { @@ -586,7 +584,7 @@ class Enclosure /** * Get length * - * @return float Length in bytes + * @return ?int Length in bytes */ public function get_length() { @@ -647,7 +645,7 @@ class Enclosure * @param int $key * @return \SimplePie\Rating|null */ - public function get_rating($key = 0) + public function get_rating(int $key = 0) { $ratings = $this->get_ratings(); if (isset($ratings[$key])) { @@ -660,7 +658,7 @@ class Enclosure /** * Get all ratings * - * @return array|null Array of {@see \SimplePie\Rating} objects + * @return Rating[]|null */ public function get_ratings() { @@ -677,7 +675,7 @@ class Enclosure * @param int $key * @return \SimplePie\Restriction|null */ - public function get_restriction($key = 0) + public function get_restriction(int $key = 0) { $restrictions = $this->get_restrictions(); if (isset($restrictions[$key])) { @@ -690,7 +688,7 @@ class Enclosure /** * Get all restrictions * - * @return array|null Array of {@see \SimplePie\Restriction} objects + * @return Restriction[]|null */ public function get_restrictions() { @@ -736,7 +734,7 @@ class Enclosure * @param int $key * @return string|null Thumbnail URL */ - public function get_thumbnail($key = 0) + public function get_thumbnail(int $key = 0) { $thumbnails = $this->get_thumbnails(); if (isset($thumbnails[$key])) { @@ -749,7 +747,7 @@ class Enclosure /** * Get all thumbnails * - * @return array|null Array of thumbnail URLs + * @return string[]|null Array of thumbnail URLs */ public function get_thumbnails() { @@ -808,7 +806,7 @@ class Enclosure * * @deprecated Use the second parameter to {@see embed} instead * - * @param array|string $options See first parameter to {@see embed} + * @param array|string $options See first parameter to {@see embed} * @return string HTML string to output */ public function native_embed($options = '') @@ -857,11 +855,11 @@ class Enclosure * `width` and `height` set to `auto` will default to 480x270 video resolution. * * @todo If the dimensions for media:content are defined, use them when width/height are set to 'auto'. - * @param array|string $options Comma-separated key:value list, or array + * @param array|string $options Comma-separated key:value list, or array * @param bool $native Use `` * @return string HTML string to output */ - public function embed($options = '', $native = false) + public function embed($options = '', bool $native = false) { // Set up defaults $audio = ''; @@ -933,7 +931,7 @@ class Enclosure } } - $mime = explode('/', $type, 2); + $mime = explode('/', (string) $type, 2); $mime = $mime[0]; // Process values for 'auto' @@ -994,7 +992,10 @@ class Enclosure // Flash Media Player file types. // Preferred handler for MP3 file types. elseif ($handler === 'fmedia' || ($handler === 'mp3' && $mediaplayer !== '')) { - $height += 20; + if (is_numeric($height)) { + $height += 20; + } + if ($native) { $embed .= "get_link().'?file_extension=.'.$this->get_extension()) . "&autostart=false&repeat=$loop&showdigits=true&showfsbutton=false\">"; } else { @@ -1005,7 +1006,10 @@ class Enclosure // QuickTime 7 file types. Need to test with QuickTime 6. // Only handle MP3's if the Flash Media Player is not present. elseif ($handler === 'quicktime' || ($handler === 'mp3' && $mediaplayer === '')) { - $height += 16; + if (is_numeric($height)) { + $height += 16; + } + if ($native) { if ($placeholder !== '') { $embed .= "get_link() . "\" src=\"$placeholder\" width=\"$width\" height=\"$height\" autoplay=\"false\" target=\"myself\" controller=\"false\" loop=\"$loop\" scale=\"aspect\" bgcolor=\"$bgcolor\" pluginspage=\"http://apple.com/quicktime/download/\">"; @@ -1019,7 +1023,10 @@ class Enclosure // Windows Media elseif ($handler === 'wmedia') { - $height += 45; + if (is_numeric($height)) { + $height += 45; + } + if ($native) { $embed .= "get_link() . "\" autosize=\"1\" width=\"$width\" height=\"$height\" showcontrols=\"1\" showstatusbar=\"0\" showdisplay=\"0\" autostart=\"0\">"; } else { @@ -1044,9 +1051,9 @@ class Enclosure * * @see get_type() * @param bool $find_handler Internal use only, use {@see get_handler()} instead - * @return string MIME type + * @return string|null MIME type */ - public function get_real_type($find_handler = false) + public function get_real_type(bool $find_handler = false) { // Mime-types by handler. $types_flash = ['application/x-shockwave-flash', 'application/futuresplash']; // Flash @@ -1055,10 +1062,9 @@ class Enclosure $types_wmedia = ['application/asx', 'application/x-mplayer2', 'audio/x-ms-wma', 'audio/x-ms-wax', 'video/x-ms-asf-plugin', 'video/x-ms-asf', 'video/x-ms-wm', 'video/x-ms-wmv', 'video/x-ms-wvx']; // Windows Media $types_mp3 = ['audio/mp3', 'audio/x-mp3', 'audio/mpeg', 'audio/x-mpeg']; // MP3 - if ($this->get_type() !== null) { - $type = strtolower($this->type); - } else { - $type = null; + $type = $this->get_type(); + if ($type !== null) { + $type = strtolower($type); } // If we encounter an unsupported mime-type, check the file extension and guess intelligently. @@ -1114,9 +1120,9 @@ class Enclosure $type = 'audio/x-ms-wma'; break; - // Video mime-types case '3gp': case '3gpp': + // Video mime-types $type = 'video/3gpp'; break; @@ -1177,8 +1183,8 @@ class Enclosure $type = 'video/x-ms-wvx'; break; - // Flash mime-types case 'spl': + // Flash mime-types $type = 'application/futuresplash'; break; diff --git a/wp-includes/SimplePie/src/Exception.php b/wp-includes/SimplePie/src/Exception.php index ae67ae38cd..c5d3ba3a02 100644 --- a/wp-includes/SimplePie/src/Exception.php +++ b/wp-includes/SimplePie/src/Exception.php @@ -1,46 +1,9 @@ > Canonical representation of headers */ + private $parsed_headers = []; + /** @var array Last known value of $headers property (used to detect external modification) */ + private $last_headers = []; + /** + * @var array Headers as string for BC + * @deprecated Use `get_headers()` method. + */ + public $headers = []; + + /** + * @var ?string Body of the HTTP response + * @deprecated Use `get_body_content()` method. + */ + public $body; + + /** + * @var int Status code of the HTTP response + * @deprecated Use `get_status_code()` method. + */ + public $status_code = 0; + + /** @var non-negative-int Number of redirect that were already performed during this request sequence. */ + public $redirects = 0; + + /** @var ?string */ + public $error; + + /** + * @var int-mask-of Bit mask representing the method used to fetch the file and whether it is a local file or remote file obtained over HTTP. + * @deprecated Backend is implementation detail which you should not care about; to see if the file was retrieved over HTTP, check if `get_final_requested_uri()` with `Misc::is_remote_uri()`. + */ + public $method = \SimplePie\SimplePie::FILE_SOURCE_NONE; + + /** + * @var string The permanent URL or the resource (first URL after the prefix of (only) permanent redirects) + * @deprecated Use `get_permanent_uri()` method. + */ + public $permanent_url; + /** @var bool Whether the permanent URL is still writeable (prefix of permanent redirects has not ended) */ + private $permanentUrlMutable = true; + + /** + * @param string $url + * @param int $timeout + * @param int $redirects + * @param ?array $headers + * @param ?string $useragent + * @param bool $force_fsockopen + * @param array $curl_options + */ + public function __construct(string $url, int $timeout = 10, int $redirects = 5, ?array $headers = null, ?string $useragent = null, bool $force_fsockopen = false, array $curl_options = []) { - if (class_exists('idna_convert')) { - $idn = new \idna_convert(); + if (function_exists('idn_to_ascii')) { $parsed = \SimplePie\Misc::parse_url($url); - $url = \SimplePie\Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], null); + if ($parsed['authority'] !== '' && !ctype_print($parsed['authority'])) { + $authority = (string) \idn_to_ascii($parsed['authority'], \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46); + $url = \SimplePie\Misc::compress_parse_url($parsed['scheme'], $authority, $parsed['path'], $parsed['query'], null); + } } $this->url = $url; - $this->permanent_url = $url; + if ($this->permanentUrlMutable) { + $this->permanent_url = $url; + } $this->useragent = $useragent; if (preg_match('/^http(s)?:\/\//i', $url)) { if ($useragent === null) { - $useragent = ini_get('user_agent'); + $useragent = (string) ini_get('user_agent'); $this->useragent = $useragent; } if (!is_array($headers)) { @@ -93,6 +115,12 @@ class File foreach ($headers as $key => $value) { $headers2[] = "$key: $value"; } + if (isset($curl_options[CURLOPT_HTTPHEADER])) { + if (is_array($curl_options[CURLOPT_HTTPHEADER])) { + $headers2 = array_merge($headers2, $curl_options[CURLOPT_HTTPHEADER]); + } + unset($curl_options[CURLOPT_HTTPHEADER]); + } if (version_compare(\SimplePie\Misc::get_curl_version(), '7.10.5', '>=')) { curl_setopt($fp, CURLOPT_ENCODING, ''); } @@ -109,10 +137,10 @@ class File curl_setopt($fp, $curl_param, $curl_value); } - $this->headers = curl_exec($fp); - if (curl_errno($fp) === 23 || curl_errno($fp) === 61) { + $responseHeaders = curl_exec($fp); + if (curl_errno($fp) === CURLE_WRITE_ERROR || curl_errno($fp) === CURLE_BAD_CONTENT_ENCODING) { curl_setopt($fp, CURLOPT_ENCODING, 'none'); - $this->headers = curl_exec($fp); + $responseHeaders = curl_exec($fp); } $this->status_code = curl_getinfo($fp, CURLINFO_HTTP_CODE); if (curl_errno($fp)) { @@ -123,29 +151,42 @@ class File if ($info = curl_getinfo($fp)) { $this->url = $info['url']; } - curl_close($fp); - $this->headers = \SimplePie\HTTP\Parser::prepareHeaders($this->headers, $info['redirect_count'] + 1); - $parser = new \SimplePie\HTTP\Parser($this->headers); + // For PHPStan: We already checked that error did not occur. + assert(is_array($info) && $info['redirect_count'] >= 0); + if (\PHP_VERSION_ID < 80000) { + curl_close($fp); + } + $responseHeaders = \SimplePie\HTTP\Parser::prepareHeaders((string) $responseHeaders, $info['redirect_count'] + 1); + $parser = new \SimplePie\HTTP\Parser($responseHeaders, true); if ($parser->parse()) { - $this->headers = $parser->headers; - $this->body = trim($parser->body); + $this->set_headers($parser->headers); + $this->body = $parser->body; $this->status_code = $parser->status_code; - if ((in_array($this->status_code, [300, 301, 302, 303, 307]) || $this->status_code > 307 && $this->status_code < 400) && isset($this->headers['location']) && $this->redirects < $redirects) { + if ((in_array($this->status_code, [300, 301, 302, 303, 307]) || $this->status_code > 307 && $this->status_code < 400) && ($locationHeader = $this->get_header_line('location')) !== '' && $this->redirects < $redirects) { $this->redirects++; - $location = \SimplePie\Misc::absolutize_url($this->headers['location'], $url); - $previousStatusCode = $this->status_code; + $location = \SimplePie\Misc::absolutize_url($locationHeader, $url); + if ($location === false) { + $this->error = "Invalid redirect location, trying to base “{$locationHeader}” onto “{$url}”"; + $this->success = false; + return; + } + $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); - $this->permanent_url = ($previousStatusCode == 301) ? $location : $url; return; } } } } else { $this->method = \SimplePie\SimplePie::FILE_SOURCE_REMOTE | \SimplePie\SimplePie::FILE_SOURCE_FSOCKOPEN; - $url_parts = parse_url($url); + if (($url_parts = parse_url($url)) === false) { + throw new \InvalidArgumentException('Malformed URL: ' . $url); + } + if (!isset($url_parts['host'])) { + throw new \InvalidArgumentException('Missing hostname: ' . $url); + } $socket_host = $url_parts['host']; if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { - $socket_host = "ssl://$url_parts[host]"; + $socket_host = 'ssl://' . $socket_host; $url_parts['port'] = 443; } if (!isset($url_parts['port'])) { @@ -184,36 +225,39 @@ class File $info = stream_get_meta_data($fp); - $this->headers = ''; + $responseHeaders = ''; while (!$info['eof'] && !$info['timed_out']) { - $this->headers .= fread($fp, 1160); + $responseHeaders .= fread($fp, 1160); $info = stream_get_meta_data($fp); } if (!$info['timed_out']) { - $parser = new \SimplePie\HTTP\Parser($this->headers); + $parser = new \SimplePie\HTTP\Parser($responseHeaders, true); if ($parser->parse()) { - $this->headers = $parser->headers; + $this->set_headers($parser->headers); $this->body = $parser->body; $this->status_code = $parser->status_code; - if ((in_array($this->status_code, [300, 301, 302, 303, 307]) || $this->status_code > 307 && $this->status_code < 400) && isset($this->headers['location']) && $this->redirects < $redirects) { + if ((in_array($this->status_code, [300, 301, 302, 303, 307]) || $this->status_code > 307 && $this->status_code < 400) && ($locationHeader = $this->get_header_line('location')) !== '' && $this->redirects < $redirects) { $this->redirects++; - $location = \SimplePie\Misc::absolutize_url($this->headers['location'], $url); - $previousStatusCode = $this->status_code; + $location = \SimplePie\Misc::absolutize_url($locationHeader, $url); + $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); + if ($location === false) { + $this->error = "Invalid redirect location, trying to base “{$locationHeader}” onto “{$url}”"; + $this->success = false; + return; + } $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); - $this->permanent_url = ($previousStatusCode == 301) ? $location : $url; return; } - if (isset($this->headers['content-encoding'])) { + if (($contentEncodingHeader = $this->get_header_line('content-encoding')) !== '') { // Hey, we act dumb elsewhere, so let's do that here too - switch (strtolower(trim($this->headers['content-encoding'], "\x09\x0A\x0D\x20"))) { + switch (strtolower(trim($contentEncodingHeader, "\x09\x0A\x0D\x20"))) { case 'gzip': case 'x-gzip': - $decoder = new \SimplePie\Gzdecode($this->body); - if (!$decoder->parse()) { + if (($decompressed = gzdecode($this->body)) === false) { $this->error = 'Unable to decode HTTP "gzip" stream'; $this->success = false; } else { - $this->body = trim($decoder->data); + $this->body = $decompressed; } break; @@ -222,7 +266,7 @@ class File $this->body = $decompressed; } elseif (($decompressed = gzuncompress($this->body)) !== false) { $this->body = $decompressed; - } elseif (function_exists('gzdecode') && ($decompressed = gzdecode($this->body)) !== false) { + } elseif (($decompressed = gzdecode($this->body)) !== false) { $this->body = $decompressed; } else { $this->error = 'Unable to decode HTTP "deflate" stream'; @@ -245,11 +289,155 @@ class File } } else { $this->method = \SimplePie\SimplePie::FILE_SOURCE_LOCAL | \SimplePie\SimplePie::FILE_SOURCE_FILE_GET_CONTENTS; - if (empty($url) || !($this->body = trim(file_get_contents($url)))) { - $this->error = 'file_get_contents could not read the file'; + if (empty($url) || !is_readable($url) || false === $filebody = file_get_contents($url)) { + $this->body = ''; + $this->error = sprintf('file "%s" is not readable', $url); $this->success = false; + } else { + $this->body = $filebody; + $this->status_code = 200; } } + if ($this->success) { + assert($this->body !== null); // For PHPStan + // Leading whitespace may cause XML parsing errors (XML declaration cannot be preceded by anything other than BOM) so we trim it. + // Note that unlike built-in `trim` function’s default settings, we do not trim `\x00` to avoid breaking characters in UTF-16 or UTF-32 encoded strings. + // We also only do that when the whitespace is followed by `<`, so that we do not break e.g. UTF-16LE encoded whitespace like `\n\x00` in half. + $this->body = preg_replace('/^[ \n\r\t\v]+body); + } + } + + public function get_permanent_uri(): string + { + return (string) $this->permanent_url; + } + + public function get_final_requested_uri(): string + { + return (string) $this->url; + } + + public function get_status_code(): int + { + return (int) $this->status_code; + } + + public function get_headers(): array + { + $this->maybe_update_headers(); + return $this->parsed_headers; + } + + public function has_header(string $name): bool + { + $this->maybe_update_headers(); + return $this->get_header($name) !== []; + } + + public function get_header(string $name): array + { + $this->maybe_update_headers(); + return $this->parsed_headers[strtolower($name)] ?? []; + } + + public function with_header(string $name, $value) + { + $this->maybe_update_headers(); + $new = clone $this; + + $newHeader = [ + strtolower($name) => (array) $value, + ]; + $new->set_headers($newHeader + $this->get_headers()); + + return $new; + } + + public function get_header_line(string $name): string + { + $this->maybe_update_headers(); + return implode(', ', $this->get_header($name)); + } + + public function get_body_content(): string + { + return (string) $this->body; + } + + /** + * Check if the $headers property was changed and update the internal state accordingly. + */ + private function maybe_update_headers(): void + { + if ($this->headers !== $this->last_headers) { + $this->parsed_headers = array_map( + function (string $header_line): array { + if (strpos($header_line, ',') === false) { + return [$header_line]; + } else { + return array_map('trim', explode(',', $header_line)); + } + }, + $this->headers + ); + } + $this->last_headers = $this->headers; + } + + /** + * Sets headers internally. + * + * @param array> $headers + */ + private function set_headers(array $headers): void + { + $this->parsed_headers = $headers; + $this->headers = self::flatten_headers($headers); + $this->last_headers = $this->headers; + } + + /** + * Converts PSR-7 compatible headers into a legacy format. + * + * @param array> $headers + * + * @return array + */ + private function flatten_headers(array $headers): array + { + return array_map(function (array $values): string { + return implode(',', $values); + }, $headers); + } + + /** + * Create a File instance from another Response + * + * For BC reasons in some places there MUST be a `File` instance + * instead of a `Response` implementation + * + * @see Locator::__construct() + * @internal + */ + final public static function fromResponse(Response $response): self + { + $headers = []; + + foreach ($response->get_headers() as $name => $header) { + $headers[$name] = implode(', ', $header); + } + + /** @var File */ + $file = (new \ReflectionClass(File::class))->newInstanceWithoutConstructor(); + + $file->url = $response->get_final_requested_uri(); + $file->useragent = null; + $file->headers = $headers; + $file->body = $response->get_body_content(); + $file->status_code = $response->get_status_code(); + $file->permanent_url = $response->get_permanent_uri(); + + return $file; } } diff --git a/wp-includes/SimplePie/src/Gzdecode.php b/wp-includes/SimplePie/src/Gzdecode.php index acb120126e..2c97707837 100644 --- a/wp-includes/SimplePie/src/Gzdecode.php +++ b/wp-includes/SimplePie/src/Gzdecode.php @@ -1,55 +1,18 @@ compressed_data = $data; $this->compressed_size = strlen($data); @@ -224,10 +187,10 @@ class Gzdecode // MTIME $mtime = substr($this->compressed_data, $this->position, 4); // Reverse the string if we're on a big-endian arch because l is the only signed long and is machine endianness - if (current(unpack('S', "\x00\x01")) === 1) { + if (current((array) unpack('S', "\x00\x01")) === 1) { $mtime = strrev($mtime); } - $this->MTIME = current(unpack('l', $mtime)); + $this->MTIME = current((array) unpack('l', $mtime)); $this->position += 4; // Get the XFL (eXtra FLags) @@ -248,7 +211,7 @@ class Gzdecode } // Get the length of the extra field - $len = current(unpack('v', substr($this->compressed_data, $this->position, 2))); + $len = current((array) unpack('v', substr($this->compressed_data, $this->position, 2))); $this->position += 2; // Check the length of the string is still valid @@ -300,7 +263,7 @@ class Gzdecode $this->min_compressed_size += $len + 2; if ($this->compressed_size >= $this->min_compressed_size) { // Read the CRC - $crc = current(unpack('v', substr($this->compressed_data, $this->position, 2))); + $crc = current((array) unpack('v', substr($this->compressed_data, $this->position, 2))); // Check the CRC matches if ((crc32(substr($this->compressed_data, 0, $this->position)) & 0xFFFF) === $crc) { @@ -314,14 +277,15 @@ class Gzdecode } // Decompress the actual data - if (($this->data = gzinflate(substr($this->compressed_data, $this->position, -8))) === false) { + if (($data = gzinflate(substr($this->compressed_data, $this->position, -8))) === false) { return false; } + $this->data = $data; $this->position = $this->compressed_size - 8; // Check CRC of data - $crc = current(unpack('V', substr($this->compressed_data, $this->position, 4))); + $crc = current((array) unpack('V', substr($this->compressed_data, $this->position, 4))); $this->position += 4; /*if (extension_loaded('hash') && sprintf('%u', current(unpack('V', hash('crc32b', $this->data)))) !== sprintf('%u', $crc)) { @@ -329,7 +293,7 @@ class Gzdecode }*/ // Check ISIZE of data - $isize = current(unpack('V', substr($this->compressed_data, $this->position, 4))); + $isize = current((array) unpack('V', substr($this->compressed_data, $this->position, 4))); $this->position += 4; if (sprintf('%u', strlen($this->data) & 0xFFFFFFFF) !== sprintf('%u', $isize)) { return false; diff --git a/wp-includes/SimplePie/src/HTTP/Client.php b/wp-includes/SimplePie/src/HTTP/Client.php new file mode 100644 index 0000000000..507c1b0f1f --- /dev/null +++ b/wp-includes/SimplePie/src/HTTP/Client.php @@ -0,0 +1,28 @@ + $headers + * + * @throws ClientException if anything goes wrong requesting the data + */ + public function request(string $method, string $url, array $headers = []): Response; +} diff --git a/wp-includes/SimplePie/src/HTTP/ClientException.php b/wp-includes/SimplePie/src/HTTP/ClientException.php new file mode 100644 index 0000000000..d7c0056808 --- /dev/null +++ b/wp-includes/SimplePie/src/HTTP/ClientException.php @@ -0,0 +1,19 @@ +} */ + private $options; + + /** + * @param array{timeout?: int, redirects?: int, useragent?: string, force_fsockopen?: bool, curl_options?: array} $options + */ + public function __construct(Registry $registry, array $options = []) + { + $this->registry = $registry; + $this->options = $options; + } + + /** + * send a request and return the response + * + * @param Client::METHOD_* $method + * @param array $headers + * + * @throws ClientException if anything goes wrong requesting the data + */ + public function request(string $method, string $url, array $headers = []): Response + { + // @phpstan-ignore-next-line Enforce PHPDoc type. + if ($method !== self::METHOD_GET) { + throw new InvalidArgumentException(sprintf( + '%s(): Argument #1 ($method) only supports method "%s".', + __METHOD__, + self::METHOD_GET + ), 1); + } + + try { + $file = $this->registry->create(File::class, [ + $url, + $this->options['timeout'] ?? 10, + $this->options['redirects'] ?? 5, + $headers, + $this->options['useragent'] ?? Misc::get_default_useragent(), + $this->options['force_fsockopen'] ?? false, + $this->options['curl_options'] ?? [] + ]); + } catch (Throwable $th) { + throw new ClientException($th->getMessage(), $th->getCode(), $th); + } + + if ($file->error !== null && $file->get_status_code() === 0) { + throw new ClientException($file->error); + } + + return $file; + } +} diff --git a/wp-includes/SimplePie/src/HTTP/Parser.php b/wp-includes/SimplePie/src/HTTP/Parser.php index 524885a1a6..e9bcc46716 100644 --- a/wp-includes/SimplePie/src/HTTP/Parser.php +++ b/wp-includes/SimplePie/src/HTTP/Parser.php @@ -1,54 +1,15 @@ > : array) */ public $headers = []; @@ -144,14 +110,14 @@ class Parser protected $position = 0; /** - * Name of the hedaer currently being parsed + * Name of the header currently being parsed * * @var string */ protected $name = ''; /** - * Value of the hedaer currently being parsed + * Value of the header currently being parsed * * @var string */ @@ -161,11 +127,13 @@ class Parser * Create an instance of the class with the input data * * @param string $data Input data + * @param Psr7Compatible $psr7Compatible Whether the data types are in format compatible with PSR-7. */ - public function __construct($data) + public function __construct(string $data, bool $psr7Compatible = false) { $this->data = $data; $this->data_length = strlen($this->data); + $this->psr7Compatible = $psr7Compatible; } /** @@ -184,7 +152,8 @@ class Parser return true; } - $this->http_version = ''; + // Reset the parser state. + $this->http_version = 0.0; $this->status_code = 0; $this->reason = ''; $this->headers = []; @@ -218,15 +187,16 @@ class Parser /** * Parse the HTTP version + * @return void */ protected function http_version() { if (strpos($this->data, "\x0A") !== false && strtoupper(substr($this->data, 0, 5)) === 'HTTP/') { $len = strspn($this->data, '0123456789.', 5); - $this->http_version = substr($this->data, 5, $len); + $http_version = substr($this->data, 5, $len); $this->position += 5 + $len; - if (substr_count($this->http_version, '.') <= 1) { - $this->http_version = (float) $this->http_version; + if (substr_count($http_version, '.') <= 1) { + $this->http_version = (float) $http_version; $this->position += strspn($this->data, "\x09\x20", $this->position); $this->state = self::STATE_STATUS; } else { @@ -239,6 +209,7 @@ class Parser /** * Parse the status code + * @return void */ protected function status() { @@ -253,6 +224,7 @@ class Parser /** * Parse the reason phrase + * @return void */ protected function reason() { @@ -262,8 +234,39 @@ class Parser $this->state = self::STATE_NEW_LINE; } + private function add_header(string $name, string $value): void + { + if ($this->psr7Compatible) { + // For PHPStan: should be enforced by template parameter but PHPStan is not smart enough. + /** @var array> */ + $headers = &$this->headers; + $headers[$name][] = $value; + } else { + // For PHPStan: should be enforced by template parameter but PHPStan is not smart enough. + /** @var array) */ + $headers = &$this->headers; + $headers[$name] .= ', ' . $value; + } + } + + private function replace_header(string $name, string $value): void + { + if ($this->psr7Compatible) { + // For PHPStan: should be enforced by template parameter but PHPStan is not smart enough. + /** @var array> */ + $headers = &$this->headers; + $headers[$name] = [$value]; + } else { + // For PHPStan: should be enforced by template parameter but PHPStan is not smart enough. + /** @var array) */ + $headers = &$this->headers; + $headers[$name] = $value; + } + } + /** * Deal with a new line, shifting data around as needed + * @return void */ protected function new_line() { @@ -272,9 +275,9 @@ class Parser $this->name = strtolower($this->name); // We should only use the last Content-Type header. c.f. issue #1 if (isset($this->headers[$this->name]) && $this->name !== 'content-type') { - $this->headers[$this->name] .= ', ' . $this->value; + $this->add_header($this->name, $this->value); } else { - $this->headers[$this->name] = $this->value; + $this->replace_header($this->name, $this->value); } } $this->name = ''; @@ -292,6 +295,7 @@ class Parser /** * Parse a header name + * @return void */ protected function name() { @@ -312,6 +316,7 @@ class Parser /** * Parse LWS, replacing consecutive LWS characters with a single space + * @return void */ protected function linear_whitespace() { @@ -328,6 +333,7 @@ class Parser /** * See what state to move to while within non-quoted header values + * @return void */ protected function value() { @@ -362,6 +368,7 @@ class Parser /** * Parse a header value while outside quotes + * @return void */ protected function value_char() { @@ -373,6 +380,7 @@ class Parser /** * See what state to move to while within quoted header values + * @return void */ protected function quote() { @@ -404,6 +412,7 @@ class Parser /** * Parse a header value while within quotes + * @return void */ protected function quote_char() { @@ -415,6 +424,7 @@ class Parser /** * Parse an escaped character within quotes + * @return void */ protected function quote_escaped() { @@ -425,6 +435,7 @@ class Parser /** * Parse the body + * @return void */ protected function body() { @@ -439,6 +450,7 @@ class Parser /** * Parsed a "Transfer-Encoding: chunked" body + * @return void */ protected function chunked() { @@ -459,6 +471,9 @@ class Parser } $length = hexdec(trim($matches[1])); + // For PHPStan: this will only be float when larger than PHP_INT_MAX. + // But even on 32-bit systems, it would mean 2GiB chunk, which sounds unlikely. + \assert(\is_int($length), "Length needs to be shorter than PHP_INT_MAX"); if ($length === 0) { // Ignore trailer headers $this->state = self::STATE_EMIT; @@ -485,11 +500,11 @@ class Parser * Prepare headers (take care of proxies headers) * * @param string $headers Raw headers - * @param integer $count Redirection count. Default to 1. + * @param non-negative-int $count Redirection count. Default to 1. * * @return string */ - public static function prepareHeaders($headers, $count = 1) + public static function prepareHeaders(string $headers, int $count = 1) { $data = explode("\r\n\r\n", $headers, $count); $data = array_pop($data); diff --git a/wp-includes/SimplePie/src/HTTP/Psr18Client.php b/wp-includes/SimplePie/src/HTTP/Psr18Client.php new file mode 100644 index 0000000000..b920333e4d --- /dev/null +++ b/wp-includes/SimplePie/src/HTTP/Psr18Client.php @@ -0,0 +1,162 @@ +httpClient = $httpClient; + $this->requestFactory = $requestFactory; + $this->uriFactory = $uriFactory; + } + + public function getHttpClient(): ClientInterface + { + return $this->httpClient; + } + + public function getRequestFactory(): RequestFactoryInterface + { + return $this->requestFactory; + } + + public function getUriFactory(): UriFactoryInterface + { + return $this->uriFactory; + } + + /** + * send a request and return the response + * + * @param Client::METHOD_* $method + * @param string $url + * @param array $headers + * + * @throws ClientException if anything goes wrong requesting the data + */ + public function request(string $method, string $url, array $headers = []): Response + { + if ($method !== self::METHOD_GET) { + throw new InvalidArgumentException(sprintf( + '%s(): Argument #1 ($method) only supports method "%s".', + __METHOD__, + self::METHOD_GET + ), 1); + } + + if (preg_match('/^http(s)?:\/\//i', $url)) { + return $this->requestUrl($method, $url, $headers); + } + + return $this->requestLocalFile($url); + } + + /** + * @param array $headers + */ + private function requestUrl(string $method, string $url, array $headers): Response + { + $permanentUrl = $url; + $requestedUrl = $url; + $remainingRedirects = $this->allowedRedirects; + + $request = $this->requestFactory->createRequest( + $method, + $this->uriFactory->createUri($requestedUrl) + ); + + foreach ($headers as $name => $value) { + $request = $request->withHeader($name, $value); + } + + do { + $followRedirect = false; + + try { + $response = $this->httpClient->sendRequest($request); + } catch (ClientExceptionInterface $th) { + throw new ClientException($th->getMessage(), $th->getCode(), $th); + } + + $statusCode = $response->getStatusCode(); + + // If we have a redirect + if (in_array($statusCode, [300, 301, 302, 303, 307]) && $response->hasHeader('Location')) { + // Prevent infinity redirect loops + if ($remainingRedirects <= 0) { + break; + } + + $remainingRedirects--; + $followRedirect = true; + + $requestedUrl = $response->getHeaderLine('Location'); + + if ($statusCode === 301) { + $permanentUrl = $requestedUrl; + } + + $request = $request->withUri($this->uriFactory->createUri($requestedUrl)); + } + } while ($followRedirect); + + return new Psr7Response($response, $permanentUrl, $requestedUrl); + } + + private function requestLocalFile(string $path): Response + { + if (!is_readable($path)) { + throw new ClientException(sprintf('file "%s" is not readable', $path)); + } + + try { + $raw = file_get_contents($path); + } catch (Throwable $th) { + throw new ClientException($th->getMessage(), $th->getCode(), $th); + } + + if ($raw === false) { + throw new ClientException('file_get_contents() could not read the file', 1); + } + + return new RawTextResponse($raw, $path); + } +} diff --git a/wp-includes/SimplePie/src/HTTP/Psr7Response.php b/wp-includes/SimplePie/src/HTTP/Psr7Response.php new file mode 100644 index 0000000000..418fddf523 --- /dev/null +++ b/wp-includes/SimplePie/src/HTTP/Psr7Response.php @@ -0,0 +1,91 @@ +response = $response; + $this->permanent_url = $permanent_url; + $this->requested_url = $requested_url; + } + + public function get_permanent_uri(): string + { + return $this->permanent_url; + } + + public function get_final_requested_uri(): string + { + return $this->requested_url; + } + + public function get_status_code(): int + { + return $this->response->getStatusCode(); + } + + public function get_headers(): array + { + // The filtering is probably redundant but let’s make PHPStan happy. + return array_filter($this->response->getHeaders(), function (array $header): bool { + return count($header) >= 1; + }); + } + + public function has_header(string $name): bool + { + return $this->response->hasHeader($name); + } + + public function with_header(string $name, $value) + { + return new self($this->response->withHeader($name, $value), $this->permanent_url, $this->requested_url); + } + + public function get_header(string $name): array + { + return $this->response->getHeader($name); + } + + public function get_header_line(string $name): string + { + return $this->response->getHeaderLine($name); + } + + public function get_body_content(): string + { + return $this->response->getBody()->__toString(); + } +} diff --git a/wp-includes/SimplePie/src/HTTP/RawTextResponse.php b/wp-includes/SimplePie/src/HTTP/RawTextResponse.php new file mode 100644 index 0000000000..fee5e53725 --- /dev/null +++ b/wp-includes/SimplePie/src/HTTP/RawTextResponse.php @@ -0,0 +1,98 @@ +> + */ + private $headers = []; + + /** + * @var string + */ + private $requested_url; + + public function __construct(string $raw_text, string $filepath) + { + $this->raw_text = $raw_text; + $this->permanent_url = $filepath; + $this->requested_url = $filepath; + } + + public function get_permanent_uri(): string + { + return $this->permanent_url; + } + + public function get_final_requested_uri(): string + { + return $this->requested_url; + } + + public function get_status_code(): int + { + return 200; + } + + public function get_headers(): array + { + return $this->headers; + } + + public function has_header(string $name): bool + { + return isset($this->headers[strtolower($name)]); + } + + public function get_header(string $name): array + { + return isset($this->headers[strtolower($name)]) ? $this->headers[$name] : []; + } + + public function with_header(string $name, $value) + { + $new = clone $this; + + $newHeader = [ + strtolower($name) => (array) $value, + ]; + $new->headers = $newHeader + $this->headers; + + return $new; + } + + public function get_header_line(string $name): string + { + return isset($this->headers[strtolower($name)]) ? implode(", ", $this->headers[$name]) : ''; + } + + public function get_body_content(): string + { + return $this->raw_text; + } +} diff --git a/wp-includes/SimplePie/src/HTTP/Response.php b/wp-includes/SimplePie/src/HTTP/Response.php new file mode 100644 index 0000000000..cc52967586 --- /dev/null +++ b/wp-includes/SimplePie/src/HTTP/Response.php @@ -0,0 +1,171 @@ +get_headers() as $name => $values) { + * echo $name . ': ' . implode(', ', $values); + * } + * + * // Emit headers iteratively: + * foreach ($message->get_headers() as $name => $values) { + * foreach ($values as $value) { + * header(sprintf('%s: %s', $name, $value), false); + * } + * } + * + * @return array> Returns an associative array of the message's headers. + * Each key MUST be a header name, and each value MUST be an array of + * strings for that header. + */ + public function get_headers(): array; + + /** + * Checks if a header exists by the given case-insensitive name. + * + * @param string $name Case-insensitive header field name. + * @return bool Returns true if any header names match the given header + * name using a case-insensitive string comparison. Returns false if + * no matching header name is found in the message. + */ + public function has_header(string $name): bool; + + /** + * Retrieves a message header value by the given case-insensitive name. + * + * This method returns an array of all the header values of the given + * case-insensitive header name. + * + * If the header does not appear in the message, this method MUST return an + * empty array. + * + * @param string $name Case-insensitive header field name. + * @return string[] An array of string values as provided for the given + * header. If the header does not appear in the message, this method MUST + * return an empty array. + */ + public function get_header(string $name): array; + + /** + * Return an instance with the provided value replacing the specified header. + * + * This method MUST be implemented in such a way as to retain the + * immutability of the message, and MUST return an instance that has the + * new and/or updated header and value. + * + * @param string $name Case-insensitive header field name. + * @param string|non-empty-array $value Header value(s). + * @return static + * @throws \InvalidArgumentException for invalid header names or values. + */ + public function with_header(string $name, $value); + + /** + * Retrieves a comma-separated string of the values for a single header. + * + * This method returns all of the header values of the given + * case-insensitive header name as a string concatenated together using + * a comma. + * + * NOTE: Not all header values may be appropriately represented using + * comma concatenation. For such headers, use getHeader() instead + * and supply your own delimiter when concatenating. + * + * If the header does not appear in the message, this method MUST return + * an empty string. + * + * @param string $name Case-insensitive header field name. + * @return string A string of values as provided for the given header + * concatenated together using a comma. If the header does not appear in + * the message, this method MUST return an empty string. + */ + public function get_header_line(string $name): string; + + /** + * get the body as string + * + * @return string + */ + public function get_body_content(): string; +} diff --git a/wp-includes/SimplePie/src/IRI.php b/wp-includes/SimplePie/src/IRI.php index dfd5eaa8a1..7fc538cd43 100644 --- a/wp-includes/SimplePie/src/IRI.php +++ b/wp-includes/SimplePie/src/IRI.php @@ -1,87 +1,53 @@ > */ protected $normalization = [ 'acap' => [ @@ -139,7 +107,7 @@ class IRI */ public function __toString() { - return $this->get_iri(); + return (string) $this->get_iri(); } /** @@ -147,11 +115,13 @@ class IRI * * @param string $name Property name * @param mixed $value Property value + * @return void */ - public function __set($name, $value) + public function __set(string $name, $value) { - if (method_exists($this, 'set_' . $name)) { - call_user_func([$this, 'set_' . $name], $value); + $callable = [$this, 'set_' . $name]; + if (is_callable($callable)) { + call_user_func($callable, $value); } elseif ( $name === 'iauthority' || $name === 'iuserinfo' @@ -170,7 +140,7 @@ class IRI * @param string $name Property name * @return mixed */ - public function __get($name) + public function __get(string $name) { // isset() returns false for null, we don't want to do that // Also why we use array_key_exists below instead of isset() @@ -187,7 +157,7 @@ class IRI $return = $this->$name; } // host -> ihost - elseif (($prop = 'i' . $name) && array_key_exists($prop, $props)) { + elseif (array_key_exists($prop = 'i' . $name, $props)) { $name = $prop; $return = $this->$prop; } @@ -213,7 +183,7 @@ class IRI * @param string $name Property name * @return bool */ - public function __isset($name) + public function __isset(string $name) { return method_exists($this, 'get_' . $name) || isset($this->$name); } @@ -222,26 +192,29 @@ class IRI * Overload __unset() to provide access via properties * * @param string $name Property name + * @return void */ - public function __unset($name) + public function __unset(string $name) { - if (method_exists($this, 'set_' . $name)) { - call_user_func([$this, 'set_' . $name], ''); + $callable = [$this, 'set_' . $name]; + if (is_callable($callable)) { + call_user_func($callable, ''); } } /** * Create a new IRI object, from a specified string * - * @param string $iri + * @param string|null $iri */ - public function __construct($iri = null) + public function __construct(?string $iri = null) { $this->set_iri($iri); } /** * Clean up + * @return void */ public function __destruct() { @@ -321,27 +294,21 @@ class IRI * Parse an IRI into scheme/authority/path/query/fragment segments * * @param string $iri - * @return array + * @return array{ + * scheme: string|null, + * authority: string|null, + * path: string, + * query: string|null, + * fragment: string|null, + * }|false */ - protected function parse_iri($iri) + protected function parse_iri(string $iri) { $iri = trim($iri, "\x20\x09\x0A\x0C\x0D"); - if (preg_match('/^((?P[^:\/?#]+):)?(\/\/(?P[^\/?#]*))?(?P[^?#]*)(\?(?P[^#]*))?(#(?P.*))?$/', $iri, $match)) { - if ($match[1] === '') { - $match['scheme'] = null; - } - if (!isset($match[3]) || $match[3] === '') { - $match['authority'] = null; - } - if (!isset($match[5])) { - $match['path'] = ''; - } - if (!isset($match[6]) || $match[6] === '') { - $match['query'] = null; - } - if (!isset($match[8]) || $match[8] === '') { - $match['fragment'] = null; - } + if (preg_match('/^(?:(?P[^:\/?#]+):)?(:?\/\/(?P[^\/?#]*))?(?P[^?#]*)(?:\?(?P[^#]*))?(?:#(?P.*))?$/', $iri, $match, \PREG_UNMATCHED_AS_NULL)) { + // TODO: Remove once we require PHP ≥ 7.4. + $match['query'] = $match['query'] ?? null; + $match['fragment'] = $match['fragment'] ?? null; return $match; } @@ -355,7 +322,7 @@ class IRI * @param string $input * @return string */ - protected function remove_dot_segments($input) + protected function remove_dot_segments(string $input) { $output = ''; while (strpos($input, './') !== false || strpos($input, '/.') !== false || $input === '.' || $input === '..') { @@ -404,13 +371,15 @@ class IRI * @param bool $iprivate Allow iprivate * @return string */ - protected function replace_invalid_with_pct_encoding($string, $extra_chars, $iprivate = false) + protected function replace_invalid_with_pct_encoding(string $string, string $extra_chars, bool $iprivate = false) { // Normalize as many pct-encoded sections as possible $string = preg_replace_callback('/(?:%[A-Fa-f0-9]{2})+/', [$this, 'remove_iunreserved_percent_encoded'], $string); + \assert(\is_string($string), "For PHPStan: Should not occur, the regex is valid"); // Replace invalid percent characters $string = preg_replace('/%(?![A-Fa-f0-9]{2})/', '%25', $string); + \assert(\is_string($string), "For PHPStan: Should not occur, the regex is valid"); // Add unreserved and % to $extra_chars (the latter is safe because all // pct-encoded sections are now valid). @@ -525,10 +494,10 @@ class IRI * Removes sequences of percent encoded bytes that represent UTF-8 * encoded characters in iunreserved * - * @param array $match PCRE match + * @param array{string} $match PCRE match, a capture group #0 consisting of a sequence of valid percent-encoded bytes * @return string Replacement */ - protected function remove_iunreserved_percent_encoded($match) + protected function remove_iunreserved_percent_encoded(array $match) { // As we just have valid percent encoded sequences we can just explode // and ignore the first member of the returned array (an empty string). @@ -631,7 +600,8 @@ class IRI } } else { for ($j = $start; $j <= $i; $j++) { - $string .= chr(hexdec($bytes[$j])); + // Cast for PHPStan, this will always be a number between 0 and 0xFF so hexdec will return int. + $string .= chr((int) hexdec($bytes[$j])); } } } @@ -648,6 +618,9 @@ class IRI return $string; } + /** + * @return void + */ protected function scheme_normalization() { if (isset($this->normalization[$this->scheme]['iuserinfo']) && $this->iuserinfo === $this->normalization[$this->scheme]['iuserinfo']) { @@ -708,15 +681,15 @@ class IRI * Set the entire IRI. Returns true on success, false on failure (if there * are any invalid characters). * - * @param string $iri + * @param string|null $iri * @return bool */ - public function set_iri($iri, $clear_cache = false) + public function set_iri(?string $iri, bool $clear_cache = false) { static $cache; if ($clear_cache) { $cache = null; - return; + return false; } if (!$cache) { $cache = []; @@ -768,10 +741,10 @@ class IRI * Set the scheme. Returns true on success, false on failure (if there are * any invalid characters). * - * @param string $scheme + * @param string|null $scheme * @return bool */ - public function set_scheme($scheme) + public function set_scheme(?string $scheme) { if ($scheme === null) { $this->scheme = null; @@ -788,15 +761,15 @@ class IRI * Set the authority. Returns true on success, false on failure (if there are * any invalid characters). * - * @param string $authority + * @param string|null $authority * @return bool */ - public function set_authority($authority, $clear_cache = false) + public function set_authority(?string $authority, bool $clear_cache = false) { static $cache; if ($clear_cache) { $cache = null; - return; + return false; } if (!$cache) { $cache = []; @@ -820,13 +793,16 @@ class IRI $remaining = $authority; if (($iuserinfo_end = strrpos($remaining, '@')) !== false) { - $iuserinfo = substr($remaining, 0, $iuserinfo_end); + // Cast for PHPStan on PHP < 8.0. It does not detect that + // the range is not flipped so substr cannot return false. + $iuserinfo = (string) substr($remaining, 0, $iuserinfo_end); $remaining = substr($remaining, $iuserinfo_end + 1); } else { $iuserinfo = null; } if (($port_start = strpos($remaining, ':', intval(strpos($remaining, ']')))) !== false) { - if (($port = substr($remaining, $port_start + 1)) === false) { + $port = substr($remaining, $port_start + 1); + if ($port === false) { $port = null; } $remaining = substr($remaining, 0, $port_start); @@ -851,10 +827,10 @@ class IRI /** * Set the iuserinfo. * - * @param string $iuserinfo + * @param string|null $iuserinfo * @return bool */ - public function set_userinfo($iuserinfo) + public function set_userinfo(?string $iuserinfo) { if ($iuserinfo === null) { $this->iuserinfo = null; @@ -870,10 +846,10 @@ class IRI * Set the ihost. Returns true on success, false on failure (if there are * any invalid characters). * - * @param string $ihost + * @param string|null $ihost * @return bool */ - public function set_host($ihost) + public function set_host(?string $ihost) { if ($ihost === null) { $this->ihost = null; @@ -914,7 +890,7 @@ class IRI * Set the port. Returns true on success, false on failure (if there are * any invalid characters). * - * @param string $port + * @param string|int|null $port * @return bool */ public function set_port($port) @@ -922,7 +898,7 @@ class IRI if ($port === null) { $this->port = null; return true; - } elseif (strspn($port, '0123456789') === strlen($port)) { + } elseif (strspn((string) $port, '0123456789') === strlen((string) $port)) { $this->port = (int) $port; $this->scheme_normalization(); return true; @@ -935,15 +911,15 @@ class IRI /** * Set the ipath. * - * @param string $ipath + * @param string|null $ipath * @return bool */ - public function set_path($ipath, $clear_cache = false) + public function set_path(?string $ipath, bool $clear_cache = false) { static $cache; if ($clear_cache) { $cache = null; - return; + return false; } if (!$cache) { $cache = []; @@ -968,10 +944,10 @@ class IRI /** * Set the iquery. * - * @param string $iquery + * @param string|null $iquery * @return bool */ - public function set_query($iquery) + public function set_query(?string $iquery) { if ($iquery === null) { $this->iquery = null; @@ -985,10 +961,10 @@ class IRI /** * Set the ifragment. * - * @param string $ifragment + * @param string|null $ifragment * @return bool */ - public function set_fragment($ifragment) + public function set_fragment(?string $ifragment) { if ($ifragment === null) { $this->ifragment = null; @@ -1002,9 +978,10 @@ class IRI /** * Convert an IRI to a URI (or parts thereof) * + * @param string $string * @return string */ - public function to_uri($string) + public function to_uri(string $string) { static $non_ascii; if (!$non_ascii) { @@ -1025,7 +1002,7 @@ class IRI /** * Get the complete IRI * - * @return string + * @return string|false */ public function get_iri() { @@ -1062,13 +1039,13 @@ class IRI */ public function get_uri() { - return $this->to_uri($this->get_iri()); + return $this->to_uri((string) $this->get_iri()); } /** * Get the complete iauthority * - * @return string + * @return ?string */ protected function get_iauthority() { @@ -1092,7 +1069,7 @@ class IRI /** * Get the complete authority * - * @return string + * @return ?string */ protected function get_authority() { diff --git a/wp-includes/SimplePie/src/Item.php b/wp-includes/SimplePie/src/Item.php index 3ec8f2f982..c2f7460c61 100644 --- a/wp-includes/SimplePie/src/Item.php +++ b/wp-includes/SimplePie/src/Item.php @@ -1,46 +1,9 @@ */ public $data = []; @@ -80,6 +40,11 @@ class Item implements RegistryAware */ protected $registry; + /** + * @var Sanitize|null + */ + private $sanitize = null; + /** * Create a new item object * @@ -87,9 +52,9 @@ class Item implements RegistryAware * {@see \SimplePie\SimplePie::get_item}. Avoid creating this manually. * * @param \SimplePie\SimplePie $feed Parent feed - * @param array $data Raw data + * @param array $data Raw data */ - public function __construct($feed, $data) + public function __construct(\SimplePie\SimplePie $feed, array $data) { $this->feed = $feed; $this->data = $data; @@ -102,8 +67,9 @@ class Item implements RegistryAware * * @since 1.3 * @param \SimplePie\Registry $registry + * @return void */ - public function set_registry(\SimplePie\Registry $registry)/* : void */ + public function set_registry(\SimplePie\Registry $registry) { $this->registry = $registry; } @@ -140,9 +106,9 @@ class Item implements RegistryAware * @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 + * @return array>|null */ - public function get_item_tags($namespace, $tag) + public function get_item_tags(string $namespace, string $tag) { if (isset($this->data['child'][$namespace][$tag])) { return $this->data['child'][$namespace][$tag]; @@ -152,13 +118,31 @@ class Item implements RegistryAware } /** - * Get the base URL value. - * Uses ``, or item link, or feed base URL. + * Get base URL of the item itself. + * Returns `` or feed base URL. + * Similar to `Item::get_base()` but can safely be used during initialisation methods + * such as `Item::get_links()` (`Item::get_base()` and `Item::get_links()` call each-other) + * and is not affected by enclosures. * - * @param array $element + * @param array $element + * @see get_base + */ + private function get_own_base(array $element = []): string + { + if (!empty($element['xml_base_explicit']) && isset($element['xml_base'])) { + return $element['xml_base']; + } + return $this->feed->get_base(); + } + + /** + * Get the base URL value. + * Uses ``, or item link, or enclosure link, or feed base URL. + * + * @param array $element * @return string */ - public function get_base($element = []) + public function get_base(array $element = []) { if (!empty($element['xml_base_explicit']) && isset($element['xml_base'])) { return $element['xml_base']; @@ -176,12 +160,13 @@ class Item implements RegistryAware * @access private * @see \SimplePie\SimplePie::sanitize() * @param string $data Data to sanitize - * @param int $type One of the \SimplePie\SimplePie::CONSTRUCT_* constants + * @param int-mask-of $type * @param string $base Base URL to resolve URLs against * @return string Sanitized data */ - public function sanitize($data, $type, $base = '') + public function sanitize(string $data, int $type, string $base = '') { + // 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->feed->sanitize($data, $type, $base); } @@ -209,11 +194,11 @@ class Item implements RegistryAware * MD5 hash based on the permalink, title and content. * * @since Beta 2 - * @param boolean $hash Should we force using a hash instead of the supplied ID? + * @param bool $hash Should we force using a hash instead of the supplied ID? * @param string|false $fn User-supplied function to generate an hash * @return string|null */ - public function get_id($hash = false, $fn = 'md5') + public function get_id(bool $hash = false, $fn = 'md5') { if (!$hash) { if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'id')) { @@ -286,10 +271,10 @@ class Item implements RegistryAware * `` * * @since 0.8 - * @param boolean $description_only Should we avoid falling back to the content? + * @param bool $description_only Should we avoid falling back to the content? * @return string|null */ - public function get_description($description_only = false) + public function get_description(bool $description_only = false) { if (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'summary')) && ($return = $this->sanitize($tags[0]['data'], $this->registry->call(Misc::class, 'atom_10_construct_type', [$tags[0]['attribs']]), $this->get_base($tags[0])))) { @@ -336,10 +321,10 @@ class Item implements RegistryAware * Uses `` or `` (RSS 1.0 Content Module) * * @since 1.0 - * @param boolean $content_only Should we avoid falling back to the description? + * @param bool $content_only Should we avoid falling back to the description? * @return string|null */ - public function get_content($content_only = false) + public function get_content(bool $content_only = false) { if (($tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'content')) && ($return = $this->sanitize($tags[0]['data'], $this->registry->call(Misc::class, 'atom_10_content_construct_type', [$tags[0]['attribs']]), $this->get_base($tags[0])))) { @@ -363,7 +348,7 @@ class Item implements RegistryAware * Uses `` * * - * @return array|null + * @return array{url: string, height?: string, width?: string, time?: string}|null */ public function get_thumbnail() { @@ -390,7 +375,7 @@ class Item implements RegistryAware * @param int $key The category that you want to return. Remember that arrays begin with 0, not 1 * @return \SimplePie\Category|null */ - public function get_category($key = 0) + public function get_category(int $key = 0) { $categories = $this->get_categories(); if (isset($categories[$key])) { @@ -462,7 +447,7 @@ class Item implements RegistryAware * @param int $key The author that you want to return. Remember that arrays begin with 0, not 1 * @return \SimplePie\Author|null */ - public function get_author($key = 0) + public function get_author(int $key = 0) { $authors = $this->get_authors(); if (isset($authors[$key])) { @@ -479,7 +464,7 @@ class Item implements RegistryAware * @param int $key The contrbutor that you want to return. Remember that arrays begin with 0, not 1 * @return \SimplePie\Author|null */ - public function get_contributor($key = 0) + public function get_contributor(int $key = 0) { $contributors = $this->get_contributors(); if (isset($contributors[$key])) { @@ -508,7 +493,8 @@ class Item implements RegistryAware $name = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); } if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]['data'])) { - $uri = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0])); + $uri = $contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]; + $uri = $this->sanitize($uri['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($uri)); } if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'])) { $email = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); @@ -525,7 +511,8 @@ class Item implements RegistryAware $name = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); } if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]['data'])) { - $url = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0])); + $url = $contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]; + $url = $this->sanitize($url['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($url)); } if (isset($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'])) { $email = $this->sanitize($contributor['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); @@ -561,7 +548,8 @@ class Item implements RegistryAware $name = $this->sanitize($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); } if (isset($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]['data'])) { - $uri = $this->sanitize($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0])); + $uri = $author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['uri'][0]; + $uri = $this->sanitize($uri['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($uri)); } if (isset($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'])) { $email = $this->sanitize($author['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_10]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); @@ -578,7 +566,8 @@ class Item implements RegistryAware $name = $this->sanitize($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['name'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); } if (isset($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]['data'])) { - $url = $this->sanitize($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0])); + $url = $author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['url'][0]; + $url = $this->sanitize($url['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($url)); } if (isset($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'])) { $email = $this->sanitize($author[0]['child'][\SimplePie\SimplePie::NAMESPACE_ATOM_03]['email'][0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT); @@ -617,7 +606,7 @@ class Item implements RegistryAware * Uses `` or `` * * @since 1.1 - * @return string + * @return string|null */ public function get_copyright() { @@ -644,9 +633,9 @@ class Item implements RegistryAware * @since Beta 2 (previously called `get_item_date` since 0.8) * * @param string $date_format Supports any PHP date format from {@see http://php.net/date} (empty for the raw data) - * @return int|string|null + * @return ($date_format is 'U' ? ?int : ?string) */ - public function get_date($date_format = 'j F Y, g:i a') + public function get_date(string $date_format = 'j F Y, g:i a') { if (!isset($this->data['date'])) { if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'published')) { @@ -675,7 +664,6 @@ class Item implements RegistryAware } } if ($this->data['date']) { - $date_format = (string) $date_format; switch ($date_format) { case '': return $this->sanitize($this->data['date']['raw'], \SimplePie\SimplePie::CONSTRUCT_TEXT); @@ -700,9 +688,9 @@ class Item implements RegistryAware * {@see get_gmdate} * * @param string $date_format Supports any PHP date format from {@see http://php.net/date} (empty for the raw data) - * @return int|string|null + * @return ($date_format is 'U' ? ?int : ?string) */ - public function get_updated_date($date_format = 'j F Y, g:i a') + public function get_updated_date(string $date_format = 'j F Y, g:i a') { if (!isset($this->data['updated'])) { if ($return = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'updated')) { @@ -717,7 +705,6 @@ class Item implements RegistryAware } } if ($this->data['updated']) { - $date_format = (string) $date_format; switch ($date_format) { case '': return $this->sanitize($this->data['updated']['raw'], \SimplePie\SimplePie::CONSTRUCT_TEXT); @@ -744,12 +731,16 @@ class Item implements RegistryAware * @since 1.0 * * @param string $date_format Supports any PHP date format from {@see http://php.net/strftime} (empty for the raw data) - * @return int|string|null + * @return string|null|false see `strftime` for when this can return `false` */ - public function get_local_date($date_format = '%c') + public function get_local_date(string $date_format = '%c') { - if (!$date_format) { - return $this->sanitize($this->get_date(''), \SimplePie\SimplePie::CONSTRUCT_TEXT); + if ($date_format === '') { + if (($raw_date = $this->get_date('')) === null) { + return null; + } + + return $this->sanitize($raw_date, \SimplePie\SimplePie::CONSTRUCT_TEXT); } elseif (($date = $this->get_date('U')) !== null && $date !== false) { return strftime($date_format, $date); } @@ -762,9 +753,9 @@ class Item implements RegistryAware * * @see get_date * @param string $date_format Supports any PHP date format from {@see http://php.net/date} - * @return int|string|null + * @return string|null */ - public function get_gmdate($date_format = 'j F Y, g:i a') + public function get_gmdate(string $date_format = 'j F Y, g:i a') { $date = $this->get_date('U'); if ($date === null) { @@ -779,9 +770,9 @@ class Item implements RegistryAware * * @see get_updated_date * @param string $date_format Supports any PHP date format from {@see http://php.net/date} - * @return int|string|null + * @return string|null */ - public function get_updated_gmdate($date_format = 'j F Y, g:i a') + public function get_updated_gmdate(string $date_format = 'j F Y, g:i a') { $date = $this->get_updated_date('U'); if ($date === null) { @@ -822,7 +813,7 @@ class Item implements RegistryAware * @param string $rel The relationship of the link to return * @return string|null Link URL */ - public function get_link($key = 0, $rel = 'alternate') + public function get_link(int $key = 0, string $rel = 'alternate') { $links = $this->get_links($rel); if ($links && $links[$key] !== null) { @@ -839,36 +830,36 @@ class Item implements RegistryAware * * @since Beta 2 * @param string $rel The relationship of links to return - * @return array|null Links found for the item (strings) + * @return array|null Links found for the item (strings) */ - public function get_links($rel = 'alternate') + public function get_links(string $rel = 'alternate') { if (!isset($this->data['links'])) { $this->data['links'] = []; foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_10, 'link') 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'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($link)); + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($link)); } } foreach ((array) $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ATOM_03, 'link') 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'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($link)); + $this->data['links'][$link_rel][] = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($link)); } } if ($links = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_10, 'link')) { - $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($links[0])); + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($links[0])); } if ($links = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_090, 'link')) { - $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($links[0])); + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($links[0])); } if ($links = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'link')) { - $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($links[0])); + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($links[0])); } if ($links = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_RSS_20, 'guid')) { if (!isset($links[0]['attribs']['']['isPermaLink']) || strtolower(trim($links[0]['attribs']['']['isPermaLink'])) === 'true') { - $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($links[0])); + $this->data['links']['alternate'][] = $this->sanitize($links[0]['data'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($links[0])); } } @@ -881,8 +872,8 @@ class Item implements RegistryAware } else { $this->data['links'][\SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY . $key] = &$this->data['links'][$key]; } - } elseif (substr($key, 0, 41) === \SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY) { - $this->data['links'][substr($key, 41)] = &$this->data['links'][$key]; + } elseif (substr((string) $key, 0, 41) === \SimplePie\SimplePie::IANA_LINK_RELATIONS_REGISTRY) { + $this->data['links'][substr((string) $key, 41)] = &$this->data['links'][$key]; } $this->data['links'][$key] = array_unique($this->data['links'][$key]); } @@ -904,7 +895,7 @@ class Item implements RegistryAware * @param int $key The enclosure that you want to return. Remember that arrays begin with 0, not 1 * @return \SimplePie\Enclosure|null */ - public function get_enclosure($key = 0, $prefer = null) + public function get_enclosure(int $key = 0) { $enclosures = $this->get_enclosures(); if (isset($enclosures[$key])) { @@ -944,7 +935,7 @@ class Item implements RegistryAware $keywords_parent = null; $player_parent = null; $ratings_parent = null; - $restrictions_parent = null; + $restrictions_parent = []; $thumbnails_parent = null; $title_parent = null; @@ -1139,20 +1130,19 @@ class Item implements RegistryAware } // DURATION - if ($duration_parent = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'duration')) { + $duration_tags = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'duration'); + if ($duration_tags !== null) { $seconds = null; $minutes = null; $hours = null; - if (isset($duration_parent[0]['data'])) { - $temp = explode(':', $this->sanitize($duration_parent[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); - if (sizeof($temp) > 0) { - $seconds = (int) array_pop($temp); - } - if (sizeof($temp) > 0) { + if (isset($duration_tags[0]['data'])) { + $temp = explode(':', $this->sanitize($duration_tags[0]['data'], \SimplePie\SimplePie::CONSTRUCT_TEXT)); + $seconds = (int) array_pop($temp); + if (count($temp) > 0) { $minutes = (int) array_pop($temp); $seconds += $minutes * 60; } - if (sizeof($temp) > 0) { + if (count($temp) > 0) { $hours = (int) array_pop($temp); $seconds += $hours * 3600; } @@ -1236,11 +1226,11 @@ class Item implements RegistryAware // PLAYER if ($player_parent = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'player')) { if (isset($player_parent[0]['attribs']['']['url'])) { - $player_parent = $this->sanitize($player_parent[0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $player_parent = $this->sanitize($player_parent[0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($player_parent[0])); } } elseif ($player_parent = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'player')) { if (isset($player_parent[0]['attribs']['']['url'])) { - $player_parent = $this->sanitize($player_parent[0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $player_parent = $this->sanitize($player_parent[0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($player_parent[0])); } } @@ -1315,11 +1305,11 @@ class Item implements RegistryAware } } elseif ($restrictions = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'block')) { foreach ($restrictions as $restriction) { - $restriction_relationship = 'allow'; + $restriction_relationship = Restriction::RELATIONSHIP_ALLOW; $restriction_type = null; $restriction_value = 'itunes'; if (isset($restriction['data']) && strtolower($restriction['data']) === 'yes') { - $restriction_relationship = 'deny'; + $restriction_relationship = Restriction::RELATIONSHIP_DENY; } $restrictions_parent[] = $this->registry->create(Restriction::class, [$restriction_relationship, $restriction_type, $restriction_value]); } @@ -1341,32 +1331,32 @@ class Item implements RegistryAware } } elseif ($restrictions = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_ITUNES, 'block')) { foreach ($restrictions as $restriction) { - $restriction_relationship = 'allow'; + $restriction_relationship = Restriction::RELATIONSHIP_ALLOW; $restriction_type = null; $restriction_value = 'itunes'; if (isset($restriction['data']) && strtolower($restriction['data']) === 'yes') { - $restriction_relationship = 'deny'; + $restriction_relationship = Restriction::RELATIONSHIP_DENY; } $restrictions_parent[] = $this->registry->create(Restriction::class, [$restriction_relationship, $restriction_type, $restriction_value]); } } - if (is_array($restrictions_parent)) { + if (count($restrictions_parent) > 0) { $restrictions_parent = array_values(array_unique($restrictions_parent)); } else { - $restrictions_parent = [new \SimplePie\Restriction('allow', null, 'default')]; + $restrictions_parent = [new \SimplePie\Restriction(Restriction::RELATIONSHIP_ALLOW, null, 'default')]; } // THUMBNAILS if ($thumbnails = $this->get_item_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'thumbnail')) { foreach ($thumbnails as $thumbnail) { if (isset($thumbnail['attribs']['']['url'])) { - $thumbnails_parent[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $thumbnails_parent[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); } } } elseif ($thumbnails = $parent->get_channel_tags(\SimplePie\SimplePie::NAMESPACE_MEDIARSS, 'thumbnail')) { foreach ($thumbnails as $thumbnail) { if (isset($thumbnail['attribs']['']['url'])) { - $thumbnails_parent[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $thumbnails_parent[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); } } } @@ -1490,7 +1480,7 @@ class Item implements RegistryAware if (isset($content['attribs']['']['width'])) { $width = $this->sanitize($content['attribs']['']['width'], \SimplePie\SimplePie::CONSTRUCT_TEXT); } - $url = $this->sanitize($content['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $url = $this->sanitize($content['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($content)); // Checking the other optional media: elements. Priority: media:content, media:group, item, channel @@ -1749,9 +1739,11 @@ class Item implements RegistryAware // PLAYER if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'])) { - $player = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $playerElem = $content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]; + $player = $this->sanitize($playerElem['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($playerElem)); } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'])) { - $player = $this->sanitize($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $playerElem = $group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]; + $player = $this->sanitize($playerElem['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($playerElem)); } else { $player = $player_parent; } @@ -1841,14 +1833,14 @@ class Item implements RegistryAware // THUMBNAILS if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'])) { foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) { - $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); } if (is_array($thumbnails)) { $thumbnails = array_values(array_unique($thumbnails)); } } elseif (isset($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'])) { foreach ($group['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) { - $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); } if (is_array($thumbnails)) { $thumbnails = array_values(array_unique($thumbnails)); @@ -1946,7 +1938,7 @@ class Item implements RegistryAware $width = $this->sanitize($content['attribs']['']['width'], \SimplePie\SimplePie::CONSTRUCT_TEXT); } if (isset($content['attribs']['']['url'])) { - $url = $this->sanitize($content['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $url = $this->sanitize($content['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($content)); } // Checking the other optional media: elements. Priority: media:content, media:group, item, channel @@ -2101,7 +2093,8 @@ class Item implements RegistryAware // PLAYER if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'])) { if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'])) { - $player = $this->sanitize($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $playerElem = $content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['player'][0]; + $player = $this->sanitize($playerElem['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($playerElem)); } } else { $player = $player_parent; @@ -2157,7 +2150,7 @@ class Item implements RegistryAware if (isset($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'])) { foreach ($content['child'][\SimplePie\SimplePie::NAMESPACE_MEDIARSS]['thumbnail'] as $thumbnail) { if (isset($thumbnail['attribs']['']['url'])) { - $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI); + $thumbnails[] = $this->sanitize($thumbnail['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($thumbnail)); } } if (is_array($thumbnails)) { @@ -2197,7 +2190,7 @@ class Item implements RegistryAware $url = null; $width = null; - $url = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($link)); + $url = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($link)); if (isset($link['attribs']['']['type'])) { $type = $this->sanitize($link['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); } @@ -2233,7 +2226,7 @@ class Item implements RegistryAware $url = null; $width = null; - $url = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($link)); + $url = $this->sanitize($link['attribs']['']['href'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($link)); if (isset($link['attribs']['']['type'])) { $type = $this->sanitize($link['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); } @@ -2264,8 +2257,8 @@ class Item implements RegistryAware $url = null; $width = null; - $url = $this->sanitize($enclosure['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_base($enclosure)); - $url = $this->feed->sanitize->https_url($url); + $url = $this->sanitize($enclosure['attribs']['']['url'], \SimplePie\SimplePie::CONSTRUCT_IRI, $this->get_own_base($enclosure)); + $url = $this->get_sanitize()->https_url($url); if (isset($enclosure['attribs']['']['type'])) { $type = $this->sanitize($enclosure['attribs']['']['type'], \SimplePie\SimplePie::CONSTRUCT_TEXT); } @@ -2278,7 +2271,7 @@ class Item implements RegistryAware } } - if (sizeof($this->data['enclosures']) === 0 && ($url || $type || $length || $bitrate || $captions_parent || $categories_parent || $channels || $copyrights_parent || $credits_parent || $description_parent || $duration_parent || $expression || $framerate || $hashes_parent || $height || $keywords_parent || $lang || $medium || $player_parent || $ratings_parent || $restrictions_parent || $samplingrate || $thumbnails_parent || $title_parent || $width)) { + if (count($this->data['enclosures']) === 0 && ($url || $type || $length || $bitrate || $captions_parent || $categories_parent || $channels || $copyrights_parent || $credits_parent || $description_parent || $duration_parent || $expression || $framerate || $hashes_parent || $height || $keywords_parent || $lang || $medium || $player_parent || $ratings_parent || $samplingrate || $thumbnails_parent || $title_parent || $width)) { // Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor $this->data['enclosures'][] = $this->registry->create(Enclosure::class, [$url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width]); } @@ -2302,7 +2295,7 @@ class Item implements RegistryAware * @since 1.0 * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo * @link http://www.georss.org/ GeoRSS - * @return string|null + * @return float|null */ public function get_latitude() { @@ -2325,7 +2318,7 @@ class Item implements RegistryAware * @since 1.0 * @link http://www.w3.org/2003/01/geo/ W3C WGS84 Basic Geo * @link http://www.georss.org/ GeoRSS - * @return string|null + * @return float|null */ public function get_longitude() { @@ -2354,6 +2347,20 @@ class Item implements RegistryAware return null; } + + public function set_sanitize(Sanitize $sanitize): void + { + $this->sanitize = $sanitize; + } + + protected function get_sanitize(): Sanitize + { + if ($this->sanitize === null) { + $this->sanitize = new Sanitize(); + } + + return $this->sanitize; + } } class_alias('SimplePie\Item', 'SimplePie_Item'); diff --git a/wp-includes/SimplePie/src/Locator.php b/wp-includes/SimplePie/src/Locator.php index e17b3a3761..30a7fe5259 100644 --- a/wp-includes/SimplePie/src/Locator.php +++ b/wp-includes/SimplePie/src/Locator.php @@ -1,76 +1,70 @@ */ public $cached_entities = []; + /** @var string */ public $http_base; + /** @var string */ public $base; + /** @var int */ public $base_location = 0; + /** @var int */ public $checked_feeds = 0; + /** @var int */ public $max_checked_feeds = 10; + /** @var bool */ public $force_fsockopen = false; + /** @var array */ public $curl_options = []; + /** @var ?\DomDocument */ public $dom; + /** @var ?Registry */ protected $registry; - public function __construct(\SimplePie\File $file, $timeout = 10, $useragent = null, $max_checked_feeds = 10, $force_fsockopen = false, $curl_options = []) + /** + * @var Client|null + */ + private $http_client = null; + + /** + * @param array $curl_options + */ + public function __construct(File $file, int $timeout = 10, ?string $useragent = null, int $max_checked_feeds = 10, bool $force_fsockopen = false, array $curl_options = []) { $this->file = $file; $this->useragent = $useragent; @@ -79,12 +73,14 @@ class Locator implements RegistryAware $this->force_fsockopen = $force_fsockopen; $this->curl_options = $curl_options; - if (class_exists('DOMDocument') && $this->file->body != '') { + $body = $this->file->get_body_content(); + + if (class_exists('DOMDocument') && $body != '') { $this->dom = new \DOMDocument(); - set_error_handler(['SimplePie\Misc', 'silence_errors']); + set_error_handler([Misc::class, 'silence_errors']); try { - $this->dom->loadHTML($this->file->body); + $this->dom->loadHTML($body); } catch (\Throwable $ex) { $this->dom = null; } @@ -94,18 +90,41 @@ class Locator implements RegistryAware } } - public function set_registry(\SimplePie\Registry $registry)/* : void */ + /** + * Set a PSR-18 client and PSR-17 factories + * + * Allows you to use your own HTTP client implementations. + */ + final public function set_http_client( + ClientInterface $http_client, + RequestFactoryInterface $request_factory, + UriFactoryInterface $uri_factory + ): void { + $this->http_client = new Psr18Client($http_client, $request_factory, $uri_factory); + } + + /** + * @return void + */ + public function set_registry(\SimplePie\Registry $registry) { $this->registry = $registry; } - public function find($type = \SimplePie\SimplePie::LOCATOR_ALL, &$working = null) + /** + * @param SimplePie::LOCATOR_* $type + * @param array|null $working + * @return Response|null + */ + public function find(int $type = \SimplePie\SimplePie::LOCATOR_ALL, ?array &$working = null) { + assert($this->registry !== null); + if ($this->is_feed($this->file)) { return $this->file; } - if ($this->file->method & \SimplePie\SimplePie::FILE_SOURCE_REMOTE) { + if (Misc::is_remote_uri($this->file->get_final_requested_uri())) { $sniffer = $this->registry->create(Content\Type\Sniffer::class, [$this->file]); if ($sniffer->get_type() !== 'text/html') { return null; @@ -140,9 +159,14 @@ class Locator implements RegistryAware return null; } - public function is_feed($file, $check_html = false) + /** + * @return bool + */ + public function is_feed(Response $file, bool $check_html = false) { - if ($file->method & \SimplePie\SimplePie::FILE_SOURCE_REMOTE) { + assert($this->registry !== null); + + if (Misc::is_remote_uri($file->get_final_requested_uri())) { $sniffer = $this->registry->create(Content\Type\Sniffer::class, [$file]); $sniffed = $sniffer->get_type(); $mime_types = ['application/rss+xml', 'application/rdf+xml', @@ -153,19 +177,24 @@ class Locator implements RegistryAware } return in_array($sniffed, $mime_types); - } elseif ($file->method & \SimplePie\SimplePie::FILE_SOURCE_LOCAL) { + } elseif (is_file($file->get_final_requested_uri())) { return true; } else { return false; } } + /** + * @return void + */ public function get_base() { + assert($this->registry !== null); + if ($this->dom === null) { throw new \SimplePie\Exception('DOMDocument not found, unable to use locator'); } - $this->http_base = $this->file->url; + $this->http_base = $this->file->get_final_requested_uri(); $this->base = $this->http_base; $elements = $this->dom->getElementsByTagName('base'); foreach ($elements as $element) { @@ -181,6 +210,9 @@ class Locator implements RegistryAware } } + /** + * @return array|null + */ public function autodiscovery() { $done = []; @@ -196,8 +228,15 @@ class Locator implements RegistryAware return null; } - protected function search_elements_by_tag($name, &$done, $feeds) + /** + * @param string[] $done + * @param array $feeds + * @return array + */ + protected function search_elements_by_tag(string $name, array &$done, array $feeds) { + assert($this->registry !== null); + if ($this->dom === null) { throw new \SimplePie\Exception('DOMDocument not found, unable to use locator'); } @@ -223,11 +262,17 @@ class Locator implements RegistryAware if (!in_array($href, $done) && in_array('feed', $rel) || (in_array('alternate', $rel) && !in_array('stylesheet', $rel) && $link->hasAttribute('type') && in_array(strtolower($this->registry->call(Misc::class, 'parse_mime', [$link->getAttribute('type')])), ['text/html', 'application/rss+xml', 'application/atom+xml'])) && !isset($feeds[$href])) { $this->checked_feeds++; $headers = [ - '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', + 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, ]; - $feed = $this->registry->create(File::class, [$href, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options]); - if ($feed->success && ($feed->method & \SimplePie\SimplePie::FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed, true)) { - $feeds[$href] = $feed; + + try { + $feed = $this->get_http_client()->request(Client::METHOD_GET, $href, $headers); + + if ((!Misc::is_remote_uri($feed->get_final_requested_uri()) || ($feed->get_status_code() === 200 || $feed->get_status_code() > 206 && $feed->get_status_code() < 300)) && $this->is_feed($feed, true)) { + $feeds[$href] = $feed; + } + } catch (ClientException $th) { + // Just mark it as done and continue. } } $done[] = $href; @@ -237,8 +282,13 @@ class Locator implements RegistryAware return $feeds; } + /** + * @return true|null + */ public function get_links() { + assert($this->registry !== null); + if ($this->dom === null) { throw new \SimplePie\Exception('DOMDocument not found, unable to use locator'); } @@ -258,7 +308,7 @@ class Locator implements RegistryAware continue; } - $current = $this->registry->call(Misc::class, 'parse_url', [$this->file->url]); + $current = $this->registry->call(Misc::class, 'parse_url', [$this->file->get_final_requested_uri()]); if ($parsed['authority'] === '' || $parsed['authority'] === $current['authority']) { $this->local[] = $href; @@ -276,8 +326,15 @@ class Locator implements RegistryAware return null; } - public function get_rel_link($rel) + /** + * Extracts first `link` element with given `rel` attribute inside the `head` element. + * + * @return string|null + */ + public function get_rel_link(string $rel) { + assert($this->registry !== null); + if ($this->dom === null) { throw new \SimplePie\Exception('DOMDocument not found, unable to use '. 'locator'); @@ -288,8 +345,10 @@ class Locator implements RegistryAware } $xpath = new \DOMXpath($this->dom); - $query = '//a[@rel and @href] | //link[@rel and @href]'; - foreach ($xpath->query($query) as $link) { + $query = '(//head)[1]/link[@rel and @href]'; + /** @var \DOMNodeList<\DOMElement> */ + $queryResult = $xpath->query($query); + foreach ($queryResult as $link) { $href = trim($link->getAttribute('href')); $parsed = $this->registry->call(Misc::class, 'parse_url', [$href]); if ($parsed['scheme'] === '' || @@ -317,33 +376,49 @@ class Locator implements RegistryAware } } } + return null; } - public function extension(&$array) + /** + * @param string[] $array + * @return array|null + */ + public function extension(array &$array) { foreach ($array as $key => $value) { if ($this->checked_feeds === $this->max_checked_feeds) { break; } - if (in_array(strtolower(strrchr($value, '.')), ['.rss', '.rdf', '.atom', '.xml'])) { + $extension = strrchr($value, '.'); + if ($extension !== false && in_array(strtolower($extension), ['.rss', '.rdf', '.atom', '.xml'])) { $this->checked_feeds++; $headers = [ - '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', + 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, ]; - $feed = $this->registry->create(File::class, [$value, $this->timeout, 5, $headers, $this->useragent, $this->force_fsockopen, $this->curl_options]); - if ($feed->success && ($feed->method & \SimplePie\SimplePie::FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed)) { - return [$feed]; - } else { - unset($array[$key]); + + try { + $feed = $this->get_http_client()->request(Client::METHOD_GET, $value, $headers); + + if ((!Misc::is_remote_uri($feed->get_final_requested_uri()) || ($feed->get_status_code() === 200 || $feed->get_status_code() > 206 && $feed->get_status_code() < 300)) && $this->is_feed($feed)) { + return [$feed]; + } + } catch (ClientException $th) { + // Just unset and continue. } + + unset($array[$key]); } } return null; } - public function body(&$array) + /** + * @param string[] $array + * @return array|null + */ + public function body(array &$array) { foreach ($array as $key => $value) { if ($this->checked_feeds === $this->max_checked_feeds) { @@ -352,18 +427,52 @@ class Locator implements RegistryAware if (preg_match('/(feed|rss|rdf|atom|xml)/i', $value)) { $this->checked_feeds++; $headers = [ - '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', + 'Accept' => SimplePie::DEFAULT_HTTP_ACCEPT_HEADER, ]; - $feed = $this->registry->create(File::class, [$value, $this->timeout, 5, null, $this->useragent, $this->force_fsockopen, $this->curl_options]); - if ($feed->success && ($feed->method & \SimplePie\SimplePie::FILE_SOURCE_REMOTE === 0 || ($feed->status_code === 200 || $feed->status_code > 206 && $feed->status_code < 300)) && $this->is_feed($feed)) { - return [$feed]; - } else { - unset($array[$key]); + + try { + $feed = $this->get_http_client()->request(Client::METHOD_GET, $value, $headers); + + if ((!Misc::is_remote_uri($feed->get_final_requested_uri()) || ($feed->get_status_code() === 200 || $feed->get_status_code() > 206 && $feed->get_status_code() < 300)) && $this->is_feed($feed)) { + return [$feed]; + } + } catch (ClientException $th) { + // Just unset and continue. } + + unset($array[$key]); } } return null; } + + /** + * Get a HTTP client + */ + private function get_http_client(): Client + { + assert($this->registry !== null); + + if ($this->http_client === null) { + $options = [ + 'timeout' => $this->timeout, + 'redirects' => 5, + 'force_fsockopen' => $this->force_fsockopen, + 'curl_options' => $this->curl_options, + ]; + + if ($this->useragent !== null) { + $options['useragent'] = $this->useragent; + } + + return new FileClient( + $this->registry, + $options + ); + } + + return $this->http_client; + } } class_alias('SimplePie\Locator', 'SimplePie_Locator', false); diff --git a/wp-includes/SimplePie/src/Misc.php b/wp-includes/SimplePie/src/Misc.php index cc5db8aea3..6f9197202c 100644 --- a/wp-includes/SimplePie/src/Misc.php +++ b/wp-includes/SimplePie/src/Misc.php @@ -1,46 +1,9 @@ get_uri(); } + /** + * @internal + */ + public static function is_remote_uri(string $uri): bool + { + return preg_match('/^https?:\/\//i', $uri) === 1; + } + /** * Get a HTML/XML element from a HTML string * * @deprecated since SimplePie 1.3, use DOMDocument instead (parsing HTML with regex is bad!) * @param string $realname Element name (including namespace prefix if applicable) * @param string $string HTML document - * @return array + * @return array, content?: string}> */ - public static function get_element($realname, $string) + public static function get_element(string $realname, string $string) { // trigger_error(sprintf('Using method "' . __METHOD__ . '" is deprecated since SimplePie 1.3, use "DOMDocument" instead.'), \E_USER_DEPRECATED); @@ -116,11 +92,11 @@ class Misc } $return[$i]['attribs'] = []; if (isset($matches[$i][2][0]) && preg_match_all('/[\x09\x0A\x0B\x0C\x0D\x20]+([^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x2F\x3D\x3E]*)(?:[\x09\x0A\x0B\x0C\x0D\x20]*=[\x09\x0A\x0B\x0C\x0D\x20]*(?:"([^"]*)"|\'([^\']*)\'|([^\x09\x0A\x0B\x0C\x0D\x20\x22\x27\x3E][^\x09\x0A\x0B\x0C\x0D\x20\x3E]*)?))?/', ' ' . $matches[$i][2][0] . ' ', $attribs, PREG_SET_ORDER)) { - for ($j = 0, $total_attribs = count($attribs); $j < $total_attribs; $j++) { - if (count($attribs[$j]) === 2) { - $attribs[$j][2] = $attribs[$j][1]; + foreach ($attribs as $attrib) { + if (count($attrib) === 2) { + $attrib[2] = $attrib[1]; } - $return[$i]['attribs'][strtolower($attribs[$j][1])]['data'] = Misc::entities_decode(end($attribs[$j])); + $return[$i]['attribs'][strtolower($attrib[1])]['data'] = Misc::entities_decode(end($attrib)); } } } @@ -128,9 +104,16 @@ class Misc return $return; } - public static function element_implode($element) + /** + * @deprecated since SimplePie 1.9.0. If you need it, you can copy the function to your codebase. But you should consider using `DOMDocument` for any DOM wrangling. + * @param array{tag: string, self_closing: bool, attribs: array, content: string} $element + * @return string + */ + public static function element_implode(array $element) { - $full = "<$element[tag]"; + // trigger_error(sprintf('Using method "' . __METHOD__ . '" is deprecated since SimplePie 1.9.'), \E_USER_DEPRECATED); + + $full = "<{$element['tag']}"; foreach ($element['attribs'] as $key => $value) { $key = strtolower($key); $full .= " $key=\"" . htmlspecialchars($value['data'], ENT_COMPAT, 'UTF-8') . '"'; @@ -138,12 +121,19 @@ class Misc if ($element['self_closing']) { $full .= ' />'; } else { - $full .= ">$element[content]"; + $full .= ">{$element['content']}"; } return $full; } - public static function error($message, $level, $file, $line) + /** + * @param string $message + * @param int $level + * @param string $file + * @param int $line + * @return string + */ + public static function error(string $message, int $level, string $file, int $line) { if ((error_reporting() & $level) > 0) { switch ($level) { @@ -179,7 +169,10 @@ class Misc return $message; } - public static function fix_protocol($url, $http = 1) + /** + * @return string + */ + public static function fix_protocol(string $url, int $http = 1) { $url = Misc::normalize_url($url); $parsed = Misc::parse_url($url); @@ -204,8 +197,11 @@ class Misc /** * @deprecated since SimplePie 1.8.0, use PHP native array_replace_recursive() instead. + * @param array $array1 + * @param array $array2 + * @return array */ - public static function array_merge_recursive($array1, $array2) + public static function array_merge_recursive(array $array1, array $array2) { foreach ($array2 as $key => $value) { if (is_array($value)) { @@ -218,7 +214,10 @@ class Misc return $array1; } - public static function parse_url($url) + /** + * @return array + */ + public static function parse_url(string $url) { $iri = new \SimplePie\IRI($url); return [ @@ -230,7 +229,10 @@ class Misc ]; } - public static function compress_parse_url($scheme = '', $authority = '', $path = '', $query = '', $fragment = '') + /** + * @return string + */ + public static function compress_parse_url(string $scheme = '', string $authority = '', string $path = '', string $query = '', ?string $fragment = '') { $iri = new \SimplePie\IRI(''); $iri->scheme = $scheme; @@ -241,17 +243,26 @@ class Misc return $iri->get_uri(); } - public static function normalize_url($url) + /** + * @return string + */ + public static function normalize_url(string $url) { $iri = new \SimplePie\IRI($url); return $iri->get_uri(); } - public static function percent_encoding_normalization($match) + /** + * @deprecated since SimplePie 1.9.0. This functionality is part of `IRI` – if you need it standalone, consider copying the function to your codebase. + * @param array $match + * @return string + */ + public static function percent_encoding_normalization(array $match) { $integer = hexdec($match[1]); if ($integer >= 0x41 && $integer <= 0x5A || $integer >= 0x61 && $integer <= 0x7A || $integer >= 0x30 && $integer <= 0x39 || $integer === 0x2D || $integer === 0x2E || $integer === 0x5F || $integer === 0x7E) { - return chr($integer); + // Cast for PHPStan, the value would only be float when above PHP_INT_MAX, which would not go in this branch. + return chr((int) $integer); } return strtoupper($match[0]); @@ -264,7 +275,7 @@ class Misc * @param string $string Windows-1252 encoded string * @return string UTF-8 encoded string */ - public static function windows_1252_to_utf8($string) + public static function windows_1252_to_utf8(string $string) { static $convert_table = ["\x80" => "\xE2\x82\xAC", "\x81" => "\xEF\xBF\xBD", "\x82" => "\xE2\x80\x9A", "\x83" => "\xC6\x92", "\x84" => "\xE2\x80\x9E", "\x85" => "\xE2\x80\xA6", "\x86" => "\xE2\x80\xA0", "\x87" => "\xE2\x80\xA1", "\x88" => "\xCB\x86", "\x89" => "\xE2\x80\xB0", "\x8A" => "\xC5\xA0", "\x8B" => "\xE2\x80\xB9", "\x8C" => "\xC5\x92", "\x8D" => "\xEF\xBF\xBD", "\x8E" => "\xC5\xBD", "\x8F" => "\xEF\xBF\xBD", "\x90" => "\xEF\xBF\xBD", "\x91" => "\xE2\x80\x98", "\x92" => "\xE2\x80\x99", "\x93" => "\xE2\x80\x9C", "\x94" => "\xE2\x80\x9D", "\x95" => "\xE2\x80\xA2", "\x96" => "\xE2\x80\x93", "\x97" => "\xE2\x80\x94", "\x98" => "\xCB\x9C", "\x99" => "\xE2\x84\xA2", "\x9A" => "\xC5\xA1", "\x9B" => "\xE2\x80\xBA", "\x9C" => "\xC5\x93", "\x9D" => "\xEF\xBF\xBD", "\x9E" => "\xC5\xBE", "\x9F" => "\xC5\xB8", "\xA0" => "\xC2\xA0", "\xA1" => "\xC2\xA1", "\xA2" => "\xC2\xA2", "\xA3" => "\xC2\xA3", "\xA4" => "\xC2\xA4", "\xA5" => "\xC2\xA5", "\xA6" => "\xC2\xA6", "\xA7" => "\xC2\xA7", "\xA8" => "\xC2\xA8", "\xA9" => "\xC2\xA9", "\xAA" => "\xC2\xAA", "\xAB" => "\xC2\xAB", "\xAC" => "\xC2\xAC", "\xAD" => "\xC2\xAD", "\xAE" => "\xC2\xAE", "\xAF" => "\xC2\xAF", "\xB0" => "\xC2\xB0", "\xB1" => "\xC2\xB1", "\xB2" => "\xC2\xB2", "\xB3" => "\xC2\xB3", "\xB4" => "\xC2\xB4", "\xB5" => "\xC2\xB5", "\xB6" => "\xC2\xB6", "\xB7" => "\xC2\xB7", "\xB8" => "\xC2\xB8", "\xB9" => "\xC2\xB9", "\xBA" => "\xC2\xBA", "\xBB" => "\xC2\xBB", "\xBC" => "\xC2\xBC", "\xBD" => "\xC2\xBD", "\xBE" => "\xC2\xBE", "\xBF" => "\xC2\xBF", "\xC0" => "\xC3\x80", "\xC1" => "\xC3\x81", "\xC2" => "\xC3\x82", "\xC3" => "\xC3\x83", "\xC4" => "\xC3\x84", "\xC5" => "\xC3\x85", "\xC6" => "\xC3\x86", "\xC7" => "\xC3\x87", "\xC8" => "\xC3\x88", "\xC9" => "\xC3\x89", "\xCA" => "\xC3\x8A", "\xCB" => "\xC3\x8B", "\xCC" => "\xC3\x8C", "\xCD" => "\xC3\x8D", "\xCE" => "\xC3\x8E", "\xCF" => "\xC3\x8F", "\xD0" => "\xC3\x90", "\xD1" => "\xC3\x91", "\xD2" => "\xC3\x92", "\xD3" => "\xC3\x93", "\xD4" => "\xC3\x94", "\xD5" => "\xC3\x95", "\xD6" => "\xC3\x96", "\xD7" => "\xC3\x97", "\xD8" => "\xC3\x98", "\xD9" => "\xC3\x99", "\xDA" => "\xC3\x9A", "\xDB" => "\xC3\x9B", "\xDC" => "\xC3\x9C", "\xDD" => "\xC3\x9D", "\xDE" => "\xC3\x9E", "\xDF" => "\xC3\x9F", "\xE0" => "\xC3\xA0", "\xE1" => "\xC3\xA1", "\xE2" => "\xC3\xA2", "\xE3" => "\xC3\xA3", "\xE4" => "\xC3\xA4", "\xE5" => "\xC3\xA5", "\xE6" => "\xC3\xA6", "\xE7" => "\xC3\xA7", "\xE8" => "\xC3\xA8", "\xE9" => "\xC3\xA9", "\xEA" => "\xC3\xAA", "\xEB" => "\xC3\xAB", "\xEC" => "\xC3\xAC", "\xED" => "\xC3\xAD", "\xEE" => "\xC3\xAE", "\xEF" => "\xC3\xAF", "\xF0" => "\xC3\xB0", "\xF1" => "\xC3\xB1", "\xF2" => "\xC3\xB2", "\xF3" => "\xC3\xB3", "\xF4" => "\xC3\xB4", "\xF5" => "\xC3\xB5", "\xF6" => "\xC3\xB6", "\xF7" => "\xC3\xB7", "\xF8" => "\xC3\xB8", "\xF9" => "\xC3\xB9", "\xFA" => "\xC3\xBA", "\xFB" => "\xC3\xBB", "\xFC" => "\xC3\xBC", "\xFD" => "\xC3\xBD", "\xFE" => "\xC3\xBE", "\xFF" => "\xC3\xBF"]; @@ -277,22 +288,22 @@ class Misc * @param string $data Raw data in $input encoding * @param string $input Encoding of $data * @param string $output Encoding you want - * @return string|boolean False if we can't convert it + * @return string|false False if we can't convert it */ - public static function change_encoding($data, $input, $output) + public static function change_encoding(string $data, string $input, string $output) { $input = Misc::encoding($input); $output = Misc::encoding($output); // We fail to fail on non US-ASCII bytes if ($input === 'US-ASCII') { - static $non_ascii_octects = ''; - if (!$non_ascii_octects) { + static $non_ascii_octets = ''; + if (!$non_ascii_octets) { for ($i = 0x80; $i <= 0xFF; $i++) { - $non_ascii_octects .= chr($i); + $non_ascii_octets .= chr($i); } } - $data = substr($data, 0, strcspn($data, $non_ascii_octects)); + $data = substr($data, 0, strcspn($data, $non_ascii_octets)); } // This is first, as behaviour of this is completely predictable @@ -316,7 +327,10 @@ class Misc return false; } - protected static function change_encoding_mbstring($data, $input, $output) + /** + * @return string|false + */ + protected static function change_encoding_mbstring(string $data, string $input, string $output) { if ($input === 'windows-949') { $input = 'EUC-KR'; @@ -348,18 +362,18 @@ class Misc return false; } - protected static function change_encoding_iconv($data, $input, $output) + /** + * @return string|false + */ + protected static function change_encoding_iconv(string $data, string $input, string $output) { return @iconv($input, $output, $data); } /** - * @param string $data - * @param string $input - * @param string $output * @return string|false */ - protected static function change_encoding_uconverter($data, $input, $output) + protected static function change_encoding_uconverter(string $data, string $input, string $output) { return @\UConverter::transcode($data, $output, $input); } @@ -375,10 +389,11 @@ class Misc * @param string $charset Character set to standardise * @return string Standardised name */ - public static function encoding($charset) + public static function encoding(string $charset) { // Normalization from UTS #22 - switch (strtolower(preg_replace('/(?:[^a-zA-Z0-9]+|([^0-9])0+)/', '\1', $charset))) { + // Cast for PHPStan, the regex should not fail. + switch (strtolower((string) preg_replace('/(?:[^a-zA-Z0-9]+|([^0-9])0+)/', '\1', $charset))) { case 'adobestandardencoding': case 'csadobestandardencoding': return 'Adobe-Standard-Encoding'; @@ -1687,16 +1702,15 @@ class Misc } } + /** + * @return string + */ public static function get_curl_version() { if (is_array($curl = curl_version())) { $curl = $curl['version']; - } elseif (substr($curl, 0, 5) === 'curl/') { - $curl = substr($curl, 5, strcspn($curl, "\x09\x0A\x0B\x0C\x0D", 5)); - } elseif (substr($curl, 0, 8) === 'libcurl/') { - $curl = substr($curl, 8, strcspn($curl, "\x09\x0A\x0B\x0C\x0D", 8)); } else { - $curl = 0; + $curl = '0'; } return $curl; } @@ -1704,11 +1718,14 @@ class Misc /** * Strip HTML comments * + * @deprecated since SimplePie 1.9.0. If you need it, you can copy the function to your codebase. But you should consider using `DOMDocument` for any DOM wrangling. * @param string $data Data to strip comments from * @return string Comment stripped string */ - public static function strip_comments($data) + public static function strip_comments(string $data) { + // trigger_error(sprintf('Using method "' . __METHOD__ . '" is deprecated since SimplePie 1.9.'), \E_USER_DEPRECATED); + $output = ''; while (($start = strpos($data, '