Make WordPress Core


Ignore:
Timestamp:
09/16/2025 10:45:37 PM (2 months ago)
Author:
SergeyBiryukov
Message:

External Libraries: Update the SimplePie library to version 1.9.0.

References:

Follow-up to [59141], [60490].

Props swissspidy, TobiasBg, SergeyBiryukov.
Fixes #63961.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/SimplePie/src/File.php

    r59141 r60771  
    11<?php
    22
    3 /**
    4  * SimplePie
    5  *
    6  * A PHP-Based RSS and Atom Feed Framework.
    7  * Takes the hard work out of managing a complete RSS/Atom solution.
    8  *
    9  * Copyright (c) 2004-2022, Ryan Parman, Sam Sneddon, Ryan McCue, and contributors
    10  * All rights reserved.
    11  *
    12  * Redistribution and use in source and binary forms, with or without modification, are
    13  * permitted provided that the following conditions are met:
    14  *
    15  *  * Redistributions of source code must retain the above copyright notice, this list of
    16  *    conditions and the following disclaimer.
    17  *
    18  *  * Redistributions in binary form must reproduce the above copyright notice, this list
    19  *    of conditions and the following disclaimer in the documentation and/or other materials
    20  *    provided with the distribution.
    21  *
    22  *  * Neither the name of the SimplePie Team nor the names of its contributors may be used
    23  *    to endorse or promote products derived from this software without specific prior
    24  *    written permission.
    25  *
    26  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
    27  * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
    28  * AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS
    29  * AND CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
    30  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
    31  * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
    32  * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
    33  * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
    34  * POSSIBILITY OF SUCH DAMAGE.
    35  *
    36  * @package SimplePie
    37  * @copyright 2004-2016 Ryan Parman, Sam Sneddon, Ryan McCue
    38  * @author Ryan Parman
    39  * @author Sam Sneddon
    40  * @author Ryan McCue
    41  * @link http://simplepie.org/ SimplePie
    42  * @license http://www.opensource.org/licenses/bsd-license.php BSD License
    43  */
     3// SPDX-FileCopyrightText: 2004-2023 Ryan Parman, Sam Sneddon, Ryan McCue
     4// SPDX-License-Identifier: BSD-3-Clause
     5
     6declare(strict_types=1);
    447
    458namespace SimplePie;
     9
     10use SimplePie\HTTP\Response;
    4611
    4712/**
     
    5217 * This class can be overloaded with {@see \SimplePie\SimplePie::set_file_class()}
    5318 *
    54  * @package SimplePie
    55  * @subpackage HTTP
    5619 * @todo Move to properly supporting RFC2616 (HTTP/1.1)
    5720 */
    58 class File
     21class File implements Response
    5922{
     23    /**
     24     * @var string The final URL after following all redirects
     25     * @deprecated Use `get_final_requested_uri()` method.
     26     */
    6027    public $url;
     28
     29    /**
     30     * @var ?string User agent to use in requests
     31     * @deprecated Set the user agent in constructor.
     32     */
    6133    public $useragent;
     34
     35    /** @var bool */
    6236    public $success = true;
     37
     38    /** @var array<string, non-empty-array<string>> Canonical representation of headers */
     39    private $parsed_headers = [];
     40    /** @var array<string, string> Last known value of $headers property (used to detect external modification) */
     41    private $last_headers = [];
     42    /**
     43     * @var array<string, string> Headers as string for BC
     44     * @deprecated Use `get_headers()` method.
     45     */
    6346    public $headers = [];
     47
     48    /**
     49     * @var ?string Body of the HTTP response
     50     * @deprecated Use `get_body_content()` method.
     51     */
    6452    public $body;
     53
     54    /**
     55     * @var int Status code of the HTTP response
     56     * @deprecated Use `get_status_code()` method.
     57     */
    6558    public $status_code = 0;
     59
     60    /** @var non-negative-int Number of redirect that were already performed during this request sequence. */
    6661    public $redirects = 0;
     62
     63    /** @var ?string */
    6764    public $error;
     65
     66    /**
     67     * @var int-mask-of<SimplePie::FILE_SOURCE_*> Bit mask representing the method used to fetch the file and whether it is a local file or remote file obtained over HTTP.
     68     * @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()`.
     69     */
    6870    public $method = \SimplePie\SimplePie::FILE_SOURCE_NONE;
     71
     72    /**
     73     * @var string The permanent URL or the resource (first URL after the prefix of (only) permanent redirects)
     74     * @deprecated Use `get_permanent_uri()` method.
     75     */
    6976    public $permanent_url;
    70 
    71     public function __construct($url, $timeout = 10, $redirects = 5, $headers = null, $useragent = null, $force_fsockopen = false, $curl_options = [])
    72     {
    73         if (class_exists('idna_convert')) {
    74             $idn = new \idna_convert();
     77    /** @var bool Whether the permanent URL is still writeable (prefix of permanent redirects has not ended) */
     78    private $permanentUrlMutable = true;
     79
     80    /**
     81     * @param string $url
     82     * @param int $timeout
     83     * @param int $redirects
     84     * @param ?array<string, string> $headers
     85     * @param ?string $useragent
     86     * @param bool $force_fsockopen
     87     * @param array<int, mixed> $curl_options
     88     */
     89    public function __construct(string $url, int $timeout = 10, int $redirects = 5, ?array $headers = null, ?string $useragent = null, bool $force_fsockopen = false, array $curl_options = [])
     90    {
     91        if (function_exists('idn_to_ascii')) {
    7592            $parsed = \SimplePie\Misc::parse_url($url);
    76             $url = \SimplePie\Misc::compress_parse_url($parsed['scheme'], $idn->encode($parsed['authority']), $parsed['path'], $parsed['query'], null);
     93            if ($parsed['authority'] !== '' && !ctype_print($parsed['authority'])) {
     94                $authority = (string) \idn_to_ascii($parsed['authority'], \IDNA_NONTRANSITIONAL_TO_ASCII, \INTL_IDNA_VARIANT_UTS46);
     95                $url = \SimplePie\Misc::compress_parse_url($parsed['scheme'], $authority, $parsed['path'], $parsed['query'], null);
     96            }
    7797        }
    7898        $this->url = $url;
    79         $this->permanent_url = $url;
     99        if ($this->permanentUrlMutable) {
     100            $this->permanent_url = $url;
     101        }
    80102        $this->useragent = $useragent;
    81103        if (preg_match('/^http(s)?:\/\//i', $url)) {
    82104            if ($useragent === null) {
    83                 $useragent = ini_get('user_agent');
     105                $useragent = (string) ini_get('user_agent');
    84106                $this->useragent = $useragent;
    85107            }
     
    94116                    $headers2[] = "$key: $value";
    95117                }
     118                if (isset($curl_options[CURLOPT_HTTPHEADER])) {
     119                    if (is_array($curl_options[CURLOPT_HTTPHEADER])) {
     120                        $headers2 = array_merge($headers2, $curl_options[CURLOPT_HTTPHEADER]);
     121                    }
     122                    unset($curl_options[CURLOPT_HTTPHEADER]);
     123                }
    96124                if (version_compare(\SimplePie\Misc::get_curl_version(), '7.10.5', '>=')) {
    97125                    curl_setopt($fp, CURLOPT_ENCODING, '');
     
    110138                }
    111139
    112                 $this->headers = curl_exec($fp);
    113                 if (curl_errno($fp) === 23 || curl_errno($fp) === 61) {
     140                $responseHeaders = curl_exec($fp);
     141                if (curl_errno($fp) === CURLE_WRITE_ERROR || curl_errno($fp) === CURLE_BAD_CONTENT_ENCODING) {
    114142                    curl_setopt($fp, CURLOPT_ENCODING, 'none');
    115                     $this->headers = curl_exec($fp);
     143                    $responseHeaders = curl_exec($fp);
    116144                }
    117145                $this->status_code = curl_getinfo($fp, CURLINFO_HTTP_CODE);
     
    124152                        $this->url = $info['url'];
    125153                    }
    126                     curl_close($fp);
    127                     $this->headers = \SimplePie\HTTP\Parser::prepareHeaders($this->headers, $info['redirect_count'] + 1);
    128                     $parser = new \SimplePie\HTTP\Parser($this->headers);
     154                    // For PHPStan: We already checked that error did not occur.
     155                    assert(is_array($info) && $info['redirect_count'] >= 0);
     156                    if (\PHP_VERSION_ID < 80000) {
     157                        curl_close($fp);
     158                    }
     159                    $responseHeaders = \SimplePie\HTTP\Parser::prepareHeaders((string) $responseHeaders, $info['redirect_count'] + 1);
     160                    $parser = new \SimplePie\HTTP\Parser($responseHeaders, true);
    129161                    if ($parser->parse()) {
    130                         $this->headers = $parser->headers;
    131                         $this->body = trim($parser->body);
     162                        $this->set_headers($parser->headers);
     163                        $this->body = $parser->body;
    132164                        $this->status_code = $parser->status_code;
    133                         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) {
     165                        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) {
    134166                            $this->redirects++;
    135                             $location = \SimplePie\Misc::absolutize_url($this->headers['location'], $url);
    136                             $previousStatusCode = $this->status_code;
     167                            $location = \SimplePie\Misc::absolutize_url($locationHeader, $url);
     168                            if ($location === false) {
     169                                $this->error = "Invalid redirect location, trying to base “{$locationHeader}” onto “{$url}”";
     170                                $this->success = false;
     171                                return;
     172                            }
     173                            $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308);
    137174                            $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options);
    138                             $this->permanent_url = ($previousStatusCode == 301) ? $location : $url;
    139175                            return;
    140176                        }
     
    143179            } else {
    144180                $this->method = \SimplePie\SimplePie::FILE_SOURCE_REMOTE | \SimplePie\SimplePie::FILE_SOURCE_FSOCKOPEN;
    145                 $url_parts = parse_url($url);
     181                if (($url_parts = parse_url($url)) === false) {
     182                    throw new \InvalidArgumentException('Malformed URL: ' . $url);
     183                }
     184                if (!isset($url_parts['host'])) {
     185                    throw new \InvalidArgumentException('Missing hostname: ' . $url);
     186                }
    146187                $socket_host = $url_parts['host'];
    147188                if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') {
    148                     $socket_host = "ssl://$url_parts[host]";
     189                    $socket_host = 'ssl://' . $socket_host;
    149190                    $url_parts['port'] = 443;
    150191                }
     
    185226                    $info = stream_get_meta_data($fp);
    186227
    187                     $this->headers = '';
     228                    $responseHeaders = '';
    188229                    while (!$info['eof'] && !$info['timed_out']) {
    189                         $this->headers .= fread($fp, 1160);
     230                        $responseHeaders .= fread($fp, 1160);
    190231                        $info = stream_get_meta_data($fp);
    191232                    }
    192233                    if (!$info['timed_out']) {
    193                         $parser = new \SimplePie\HTTP\Parser($this->headers);
     234                        $parser = new \SimplePie\HTTP\Parser($responseHeaders, true);
    194235                        if ($parser->parse()) {
    195                             $this->headers = $parser->headers;
     236                            $this->set_headers($parser->headers);
    196237                            $this->body = $parser->body;
    197238                            $this->status_code = $parser->status_code;
    198                             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) {
     239                            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) {
    199240                                $this->redirects++;
    200                                 $location = \SimplePie\Misc::absolutize_url($this->headers['location'], $url);
    201                                 $previousStatusCode = $this->status_code;
     241                                $location = \SimplePie\Misc::absolutize_url($locationHeader, $url);
     242                                $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308);
     243                                if ($location === false) {
     244                                    $this->error = "Invalid redirect location, trying to base “{$locationHeader}” onto “{$url}”";
     245                                    $this->success = false;
     246                                    return;
     247                                }
    202248                                $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options);
    203                                 $this->permanent_url = ($previousStatusCode == 301) ? $location : $url;
    204249                                return;
    205250                            }
    206                             if (isset($this->headers['content-encoding'])) {
     251                            if (($contentEncodingHeader = $this->get_header_line('content-encoding')) !== '') {
    207252                                // Hey, we act dumb elsewhere, so let's do that here too
    208                                 switch (strtolower(trim($this->headers['content-encoding'], "\x09\x0A\x0D\x20"))) {
     253                                switch (strtolower(trim($contentEncodingHeader, "\x09\x0A\x0D\x20"))) {
    209254                                    case 'gzip':
    210255                                    case 'x-gzip':
    211                                         $decoder = new \SimplePie\Gzdecode($this->body);
    212                                         if (!$decoder->parse()) {
     256                                        if (($decompressed = gzdecode($this->body)) === false) {
    213257                                            $this->error = 'Unable to decode HTTP "gzip" stream';
    214258                                            $this->success = false;
    215259                                        } else {
    216                                             $this->body = trim($decoder->data);
     260                                            $this->body = $decompressed;
    217261                                        }
    218262                                        break;
     
    223267                                        } elseif (($decompressed = gzuncompress($this->body)) !== false) {
    224268                                            $this->body = $decompressed;
    225                                         } elseif (function_exists('gzdecode') && ($decompressed = gzdecode($this->body)) !== false) {
     269                                        } elseif (($decompressed = gzdecode($this->body)) !== false) {
    226270                                            $this->body = $decompressed;
    227271                                        } else {
     
    246290        } else {
    247291            $this->method = \SimplePie\SimplePie::FILE_SOURCE_LOCAL | \SimplePie\SimplePie::FILE_SOURCE_FILE_GET_CONTENTS;
    248             if (empty($url) || !($this->body = trim(file_get_contents($url)))) {
    249                 $this->error = 'file_get_contents could not read the file';
     292            if (empty($url) || !is_readable($url) ||  false === $filebody = file_get_contents($url)) {
     293                $this->body = '';
     294                $this->error = sprintf('file "%s" is not readable', $url);
    250295                $this->success = false;
     296            } else {
     297                $this->body = $filebody;
     298                $this->status_code = 200;
    251299            }
    252300        }
     301        if ($this->success) {
     302            assert($this->body !== null); // For PHPStan
     303            // Leading whitespace may cause XML parsing errors (XML declaration cannot be preceded by anything other than BOM) so we trim it.
     304            // 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.
     305            // 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.
     306            $this->body = preg_replace('/^[ \n\r\t\v]+</', '<', $this->body);
     307        }
     308    }
     309
     310    public function get_permanent_uri(): string
     311    {
     312        return (string) $this->permanent_url;
     313    }
     314
     315    public function get_final_requested_uri(): string
     316    {
     317        return (string) $this->url;
     318    }
     319
     320    public function get_status_code(): int
     321    {
     322        return (int) $this->status_code;
     323    }
     324
     325    public function get_headers(): array
     326    {
     327        $this->maybe_update_headers();
     328        return $this->parsed_headers;
     329    }
     330
     331    public function has_header(string $name): bool
     332    {
     333        $this->maybe_update_headers();
     334        return $this->get_header($name) !== [];
     335    }
     336
     337    public function get_header(string $name): array
     338    {
     339        $this->maybe_update_headers();
     340        return $this->parsed_headers[strtolower($name)] ?? [];
     341    }
     342
     343    public function with_header(string $name, $value)
     344    {
     345        $this->maybe_update_headers();
     346        $new = clone $this;
     347
     348        $newHeader = [
     349            strtolower($name) => (array) $value,
     350        ];
     351        $new->set_headers($newHeader + $this->get_headers());
     352
     353        return $new;
     354    }
     355
     356    public function get_header_line(string $name): string
     357    {
     358        $this->maybe_update_headers();
     359        return implode(', ', $this->get_header($name));
     360    }
     361
     362    public function get_body_content(): string
     363    {
     364        return (string) $this->body;
     365    }
     366
     367    /**
     368     * Check if the $headers property was changed and update the internal state accordingly.
     369     */
     370    private function maybe_update_headers(): void
     371    {
     372        if ($this->headers !== $this->last_headers) {
     373            $this->parsed_headers = array_map(
     374                function (string $header_line): array {
     375                    if (strpos($header_line, ',') === false) {
     376                        return [$header_line];
     377                    } else {
     378                        return array_map('trim', explode(',', $header_line));
     379                    }
     380                },
     381                $this->headers
     382            );
     383        }
     384        $this->last_headers = $this->headers;
     385    }
     386
     387    /**
     388     * Sets headers internally.
     389     *
     390     * @param array<string, non-empty-array<string>> $headers
     391     */
     392    private function set_headers(array $headers): void
     393    {
     394        $this->parsed_headers = $headers;
     395        $this->headers = self::flatten_headers($headers);
     396        $this->last_headers = $this->headers;
     397    }
     398
     399    /**
     400     * Converts PSR-7 compatible headers into a legacy format.
     401     *
     402     * @param array<string, non-empty-array<string>> $headers
     403     *
     404     * @return array<string, string>
     405     */
     406    private function flatten_headers(array $headers): array
     407    {
     408        return array_map(function (array $values): string {
     409            return implode(',', $values);
     410        }, $headers);
     411    }
     412
     413    /**
     414     * Create a File instance from another Response
     415     *
     416     * For BC reasons in some places there MUST be a `File` instance
     417     * instead of a `Response` implementation
     418     *
     419     * @see Locator::__construct()
     420     * @internal
     421     */
     422    final public static function fromResponse(Response $response): self
     423    {
     424        $headers = [];
     425
     426        foreach ($response->get_headers() as $name => $header) {
     427            $headers[$name] = implode(', ', $header);
     428        }
     429
     430        /** @var File */
     431        $file = (new \ReflectionClass(File::class))->newInstanceWithoutConstructor();
     432
     433        $file->url = $response->get_final_requested_uri();
     434        $file->useragent = null;
     435        $file->headers = $headers;
     436        $file->body = $response->get_body_content();
     437        $file->status_code = $response->get_status_code();
     438        $file->permanent_url = $response->get_permanent_uri();
     439
     440        return $file;
    253441    }
    254442}
Note: See TracChangeset for help on using the changeset viewer.