Changeset 60771 for trunk/src/wp-includes/SimplePie/src/File.php
- Timestamp:
- 09/16/2025 10:45:37 PM (2 months ago)
- File:
-
- 1 edited
-
trunk/src/wp-includes/SimplePie/src/File.php (modified) (9 diffs)
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/wp-includes/SimplePie/src/File.php
r59141 r60771 1 1 <?php 2 2 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 6 declare(strict_types=1); 44 7 45 8 namespace SimplePie; 9 10 use SimplePie\HTTP\Response; 46 11 47 12 /** … … 52 17 * This class can be overloaded with {@see \SimplePie\SimplePie::set_file_class()} 53 18 * 54 * @package SimplePie55 * @subpackage HTTP56 19 * @todo Move to properly supporting RFC2616 (HTTP/1.1) 57 20 */ 58 class File 21 class File implements Response 59 22 { 23 /** 24 * @var string The final URL after following all redirects 25 * @deprecated Use `get_final_requested_uri()` method. 26 */ 60 27 public $url; 28 29 /** 30 * @var ?string User agent to use in requests 31 * @deprecated Set the user agent in constructor. 32 */ 61 33 public $useragent; 34 35 /** @var bool */ 62 36 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 */ 63 46 public $headers = []; 47 48 /** 49 * @var ?string Body of the HTTP response 50 * @deprecated Use `get_body_content()` method. 51 */ 64 52 public $body; 53 54 /** 55 * @var int Status code of the HTTP response 56 * @deprecated Use `get_status_code()` method. 57 */ 65 58 public $status_code = 0; 59 60 /** @var non-negative-int Number of redirect that were already performed during this request sequence. */ 66 61 public $redirects = 0; 62 63 /** @var ?string */ 67 64 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 */ 68 70 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 */ 69 76 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')) { 75 92 $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 } 77 97 } 78 98 $this->url = $url; 79 $this->permanent_url = $url; 99 if ($this->permanentUrlMutable) { 100 $this->permanent_url = $url; 101 } 80 102 $this->useragent = $useragent; 81 103 if (preg_match('/^http(s)?:\/\//i', $url)) { 82 104 if ($useragent === null) { 83 $useragent = ini_get('user_agent');105 $useragent = (string) ini_get('user_agent'); 84 106 $this->useragent = $useragent; 85 107 } … … 94 116 $headers2[] = "$key: $value"; 95 117 } 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 } 96 124 if (version_compare(\SimplePie\Misc::get_curl_version(), '7.10.5', '>=')) { 97 125 curl_setopt($fp, CURLOPT_ENCODING, ''); … … 110 138 } 111 139 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) { 114 142 curl_setopt($fp, CURLOPT_ENCODING, 'none'); 115 $ this->headers = curl_exec($fp);143 $responseHeaders = curl_exec($fp); 116 144 } 117 145 $this->status_code = curl_getinfo($fp, CURLINFO_HTTP_CODE); … … 124 152 $this->url = $info['url']; 125 153 } 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); 129 161 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; 132 164 $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) { 134 166 $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); 137 174 $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); 138 $this->permanent_url = ($previousStatusCode == 301) ? $location : $url;139 175 return; 140 176 } … … 143 179 } else { 144 180 $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 } 146 187 $socket_host = $url_parts['host']; 147 188 if (isset($url_parts['scheme']) && strtolower($url_parts['scheme']) === 'https') { 148 $socket_host = "ssl://$url_parts[host]";189 $socket_host = 'ssl://' . $socket_host; 149 190 $url_parts['port'] = 443; 150 191 } … … 185 226 $info = stream_get_meta_data($fp); 186 227 187 $ this->headers = '';228 $responseHeaders = ''; 188 229 while (!$info['eof'] && !$info['timed_out']) { 189 $ this->headers .= fread($fp, 1160);230 $responseHeaders .= fread($fp, 1160); 190 231 $info = stream_get_meta_data($fp); 191 232 } 192 233 if (!$info['timed_out']) { 193 $parser = new \SimplePie\HTTP\Parser($ this->headers);234 $parser = new \SimplePie\HTTP\Parser($responseHeaders, true); 194 235 if ($parser->parse()) { 195 $this-> headers = $parser->headers;236 $this->set_headers($parser->headers); 196 237 $this->body = $parser->body; 197 238 $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) { 199 240 $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 } 202 248 $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); 203 $this->permanent_url = ($previousStatusCode == 301) ? $location : $url;204 249 return; 205 250 } 206 if ( isset($this->headers['content-encoding'])) {251 if (($contentEncodingHeader = $this->get_header_line('content-encoding')) !== '') { 207 252 // 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"))) { 209 254 case 'gzip': 210 255 case 'x-gzip': 211 $decoder = new \SimplePie\Gzdecode($this->body); 212 if (!$decoder->parse()) { 256 if (($decompressed = gzdecode($this->body)) === false) { 213 257 $this->error = 'Unable to decode HTTP "gzip" stream'; 214 258 $this->success = false; 215 259 } else { 216 $this->body = trim($decoder->data);260 $this->body = $decompressed; 217 261 } 218 262 break; … … 223 267 } elseif (($decompressed = gzuncompress($this->body)) !== false) { 224 268 $this->body = $decompressed; 225 } elseif ( function_exists('gzdecode') &&($decompressed = gzdecode($this->body)) !== false) {269 } elseif (($decompressed = gzdecode($this->body)) !== false) { 226 270 $this->body = $decompressed; 227 271 } else { … … 246 290 } else { 247 291 $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); 250 295 $this->success = false; 296 } else { 297 $this->body = $filebody; 298 $this->status_code = 200; 251 299 } 252 300 } 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; 253 441 } 254 442 }
Note: See TracChangeset
for help on using the changeset viewer.