From cff0d3638eb7aaf1f16628809eede643cea034a2 Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 30 May 2026 20:54:03 +0200 Subject: [PATCH 01/12] Improve SimplePie redirects (POST to GET, remove authentication cross-origin) --- src/File.php | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/File.php b/src/File.php index 13a50afdf..87869dbd7 100644 --- a/src/File.php +++ b/src/File.php @@ -179,6 +179,45 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, $this->success = false; return; } + + // FreshRSS: POST to GET on redirect + if (isset($curl_options[CURLOPT_POST]) && in_array($this->status_code, [301, 302, 303], true)) { // Not for 307 and 308, which must not change the HTTP method + unset($curl_options[CURLOPT_POST]); + unset($curl_options[CURLOPT_POSTFIELDS]); + if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { + $curl_options[CURLOPT_HTTPHEADER] = array_filter($curl_options[CURLOPT_HTTPHEADER], fn (mixed $header): bool => + is_string($header) && !str_starts_with(strtolower(trim($header)), 'content-type:')); + } + } + // FreshRSS: cross-origin authentication headers removal + if (($url_parts_from = parse_url($url)) === false) { + throw new \InvalidArgumentException('Malformed URL: ' . $url); + } + if (($url_parts_to = parse_url($location)) === false) { + $this->error = "Invalid redirect location: malformed URL “{$url}”"; + $this->success = false; + return; + } + foreach ([&$url_parts_from, &$url_parts_to] as &$url_parts) { + $url_parts['port'] ??= match ($url_parts['scheme']) { + 'http' => 80, + 'https' => 443, + default => 0, + }; + } + unset($url_parts); + $sameOriginRedirect = + ($url_parts_from['scheme'] ?? '') === ($url_parts_to['scheme'] ?? '') && + ($url_parts_from['host'] ?? '') === ($url_parts_to['host'] ?? '') && + ($url_parts_from['port'] ?? '') === ($url_parts_to['port'] ?? ''); + if (!$sameOriginRedirect) { + unset($curl_options[CURLOPT_COOKIE]); + if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { + $curl_options[CURLOPT_HTTPHEADER] = array_filter($curl_options[CURLOPT_HTTPHEADER], fn (mixed $header): bool => + is_string($header) && !preg_match('/^(Cookie|Authorization)\\s*:/i', $header)); + } + } + $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); return; From 0c3b3d7a55ed3c8fd86f2b0ad5d80f41634cf86a Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 30 May 2026 21:18:55 +0200 Subject: [PATCH 02/12] PHP 7.2+ compatibility --- src/File.php | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/File.php b/src/File.php index 87869dbd7..14cf37fcf 100644 --- a/src/File.php +++ b/src/File.php @@ -185,8 +185,12 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, unset($curl_options[CURLOPT_POST]); unset($curl_options[CURLOPT_POSTFIELDS]); if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { - $curl_options[CURLOPT_HTTPHEADER] = array_filter($curl_options[CURLOPT_HTTPHEADER], fn (mixed $header): bool => - is_string($header) && !str_starts_with(strtolower(trim($header)), 'content-type:')); + $curl_options[CURLOPT_HTTPHEADER] = array_filter( + $curl_options[CURLOPT_HTTPHEADER], + function ($header) { + return is_string($header) && substr(strtolower(trim($header)), 0, 13) !== 'content-type:'; + } + ); } } // FreshRSS: cross-origin authentication headers removal @@ -199,11 +203,13 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, return; } foreach ([&$url_parts_from, &$url_parts_to] as &$url_parts) { - $url_parts['port'] ??= match ($url_parts['scheme']) { - 'http' => 80, - 'https' => 443, - default => 0, - }; + if (!isset($url_parts['port']) && isset($url_parts['scheme'])) { + if ($url_parts['scheme'] === 'http') { + $url_parts['port'] = 80; + } elseif ($url_parts['scheme'] === 'https') { + $url_parts['port'] = 443; + } + } } unset($url_parts); $sameOriginRedirect = @@ -213,8 +219,12 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, if (!$sameOriginRedirect) { unset($curl_options[CURLOPT_COOKIE]); if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { - $curl_options[CURLOPT_HTTPHEADER] = array_filter($curl_options[CURLOPT_HTTPHEADER], fn (mixed $header): bool => - is_string($header) && !preg_match('/^(Cookie|Authorization)\\s*:/i', $header)); + $curl_options[CURLOPT_HTTPHEADER] = array_filter( + $curl_options[CURLOPT_HTTPHEADER], + function ($header) { + return is_string($header) && !preg_match('/^(Cookie|Authorization)\s*:/i', $header); + } + ); } } From 0d69814c6b880fbeabb5b7ded2fee29dee17f899 Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 30 May 2026 23:20:23 +0200 Subject: [PATCH 03/12] Normalize URLs to lowercase when parsing --- src/File.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/File.php b/src/File.php index 14cf37fcf..576ac5b23 100644 --- a/src/File.php +++ b/src/File.php @@ -194,10 +194,10 @@ function ($header) { } } // FreshRSS: cross-origin authentication headers removal - if (($url_parts_from = parse_url($url)) === false) { + if (($url_parts_from = parse_url(strtolower($url))) === false) { throw new \InvalidArgumentException('Malformed URL: ' . $url); } - if (($url_parts_to = parse_url($location)) === false) { + if (($url_parts_to = parse_url(strtolower($location))) === false) { $this->error = "Invalid redirect location: malformed URL “{$url}”"; $this->success = false; return; From 4c8a01258991bd7ddc2d709d92dd778ab4eaafa1 Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 30 May 2026 23:32:19 +0200 Subject: [PATCH 04/12] Fix wrong URL printed in error message --- src/File.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index 576ac5b23..d0e526c3d 100644 --- a/src/File.php +++ b/src/File.php @@ -198,7 +198,7 @@ function ($header) { throw new \InvalidArgumentException('Malformed URL: ' . $url); } if (($url_parts_to = parse_url(strtolower($location))) === false) { - $this->error = "Invalid redirect location: malformed URL “{$url}”"; + $this->error = "Invalid redirect location: malformed URL “{$location}”"; $this->success = false; return; } From 010a7dd29b04b019d10b17f287ef159bfead8be6 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Thu, 11 Jun 2026 12:00:29 +0200 Subject: [PATCH 05/12] Same logic for fsockopen --- src/File.php | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/src/File.php b/src/File.php index d0e526c3d..20bfd3228 100644 --- a/src/File.php +++ b/src/File.php @@ -305,6 +305,41 @@ function ($header) { $this->success = false; return; } + + // FreshRSS: POST to GET on redirect is not applicable here as fsockopen only ever performs GET requests. + // FreshRSS: cross-origin authentication headers removal + if (($url_parts_from = parse_url(strtolower($url))) === false) { + throw new \InvalidArgumentException('Malformed URL: ' . $url); + } + if (($url_parts_to = parse_url(strtolower($location))) === false) { + $this->error = "Invalid redirect location: malformed URL “{$location}”"; + $this->success = false; + return; + } + foreach ([&$url_parts_from, &$url_parts_to] as &$url_parts) { + if (!isset($url_parts['port']) && isset($url_parts['scheme'])) { + if ($url_parts['scheme'] === 'http') { + $url_parts['port'] = 80; + } elseif ($url_parts['scheme'] === 'https') { + $url_parts['port'] = 443; + } + } + } + unset($url_parts); + $sameOriginRedirect = + ($url_parts_from['scheme'] ?? '') === ($url_parts_to['scheme'] ?? '') && + ($url_parts_from['host'] ?? '') === ($url_parts_to['host'] ?? '') && + ($url_parts_from['port'] ?? '') === ($url_parts_to['port'] ?? ''); + if (!$sameOriginRedirect) { + $headers = array_filter( + $headers, + function (string $key) { + return !preg_match('/^(Cookie|Authorization)$/i', $key); + }, + ARRAY_FILTER_USE_KEY + ); + } + $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); return; } From 55d17deddd72f4da3f1d0b98b2e89f6610888dbb Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 13 Jun 2026 19:10:36 +0200 Subject: [PATCH 06/12] Also unset `CURLOPT_USERPWD` during redirects --- src/File.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/File.php b/src/File.php index 20bfd3228..62bc9e4dc 100644 --- a/src/File.php +++ b/src/File.php @@ -218,6 +218,7 @@ function ($header) { ($url_parts_from['port'] ?? '') === ($url_parts_to['port'] ?? ''); if (!$sameOriginRedirect) { unset($curl_options[CURLOPT_COOKIE]); + unset($curl_options[CURLOPT_USERPWD]); if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { $curl_options[CURLOPT_HTTPHEADER] = array_filter( $curl_options[CURLOPT_HTTPHEADER], From 56ecb13930173bc416d647b50f9470d9a1f65ebc Mon Sep 17 00:00:00 2001 From: Inverle Date: Sat, 13 Jun 2026 19:38:11 +0200 Subject: [PATCH 07/12] Add infinite redirects for the `fsockopen()` path too --- src/File.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index 62bc9e4dc..a9792aaa2 100644 --- a/src/File.php +++ b/src/File.php @@ -297,7 +297,8 @@ function ($header) { $this->body = $parser->body; $this->status_code = $parser->status_code; $this->on_http_response($responseHeaders); - 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) { + 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 || $redirects === -1)) { // FreshRSS: added infinite redirects for -1 $this->redirects++; $location = \SimplePie\Misc::absolutize_url($locationHeader, $url); $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); From 10bff0a99bb5e0f54b286dd5f31b9afb6f99110f Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sun, 21 Jun 2026 22:30:54 +0200 Subject: [PATCH 08/12] Draft rework CURLOPT_FOLLOWLOCATION --- src/File.php | 20 ++++- tests/Integration/HTTP/ClientsTest.php | 115 +++++++++++++++++++++++++ 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/src/File.php b/src/File.php index a9792aaa2..3b2f1f07c 100644 --- a/src/File.php +++ b/src/File.php @@ -170,8 +170,22 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, if ($parser->parse()) { $this->set_headers($parser->headers); $this->body = $responseBody; - 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 || $redirects === -1)) { // FreshRSS: added infinite redirects for -1 + // Three-tier redirect handling: + // 1. CURLOPT_FOLLOWLOCATION = truthy: Native cURL redirect handling without own security checks + // 2. CURLOPT_FOLLOWLOCATION = falsy: No redirect handling at all + // 3. CURLOPT_FOLLOWLOCATION not set or null: Custom PHP redirect handling (default, recommended) + $followLocation = $curl_options[CURLOPT_FOLLOWLOCATION] ?? null; + + if ($followLocation) { + // Native cURL redirect handling + $finalUrl = curl_getinfo($fp, CURLINFO_EFFECTIVE_URL); + if (is_string($finalUrl) && $finalUrl !== '') { + $this->url = $finalUrl; + } + } elseif ($followLocation === null) { + // Custom PHP redirect handling (default) + 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 || $redirects === -1)) { // FreshRSS: added infinite redirects for -1 $this->redirects++; $location = \SimplePie\Misc::absolutize_url($locationHeader, $url); if ($location === false) { @@ -232,6 +246,7 @@ function ($header) { $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); return; + } } } } @@ -608,7 +623,6 @@ private static function curlInit( foreach ($curl_options as $curl_param => $curl_value) { curl_setopt($fp, $curl_param, $curl_value); } - curl_setopt($fp, CURLOPT_FOLLOWLOCATION, false); // FreshRSS return $fp; } diff --git a/tests/Integration/HTTP/ClientsTest.php b/tests/Integration/HTTP/ClientsTest.php index 77ff455ad..4a8041e59 100644 --- a/tests/Integration/HTTP/ClientsTest.php +++ b/tests/Integration/HTTP/ClientsTest.php @@ -221,6 +221,121 @@ public function testRedirectsChain(Client $client): void self::assertSame($finalLocation, $response->get_final_requested_uri()); } + /** + * Test native cURL redirect handling when CURLOPT_FOLLOWLOCATION is enabled + * This test verifies that redirects are followed by cURL natively instead of custom handling + */ + public function testNativeCurlRedirectHandling(): void + { + $client = new FileClient(new Registry(), [ + 'redirects' => 10, + 'force_fsockopen' => false, + 'curl_options' => [ + CURLOPT_FOLLOWLOCATION => true, // Enable native cURL redirect handling + ], + ]); + + $server = new MockWebServer(); + $server->start(); + + $server->setDefaultResponse(new NotFoundResponse()); + $url = $server->setResponseOfPath( + '/redirect-start', + new MockWebServerResponse('', ['Location: /redirect-end'], 302) + ); + $finalLocation = $server->setResponseOfPath( + '/redirect-end', + new MockWebServerResponse('OK', [], 200) + ); + + $response = $client->request(Client::METHOD_GET, $url); + + $server->stop(); + + self::assertSame(200, $response->get_status_code()); + self::assertSame('OK', $response->get_body_content()); + // With native cURL handling, permanent_uri should be the final URL (cURL doesn't track permanent redirects separately) + self::assertSame($finalLocation, $response->get_final_requested_uri()); + } + + /** + * Test that native cURL redirect handling bypasses custom security features + * This is expected behavior - when using CURLOPT_FOLLOWLOCATION, cURL handles everything + */ + public function testNativeCurlRedirectHandlingBypassesSecurityFeatures(): void + { + // This test documents that native cURL handling bypasses security features + // In a real scenario, you would test that auth headers are NOT removed on cross-origin redirects + + $client = new FileClient(new Registry(), [ + 'redirects' => 10, + 'force_fsockopen' => false, + 'curl_options' => [ + CURLOPT_FOLLOWLOCATION => true, // Native handling bypasses security features + ], + ]); + + $server = new MockWebServer(); + $server->start(); + + $server->setDefaultResponse(new NotFoundResponse()); + // Create a redirect chain + $url = $server->setResponseOfPath( + '/start', + new MockWebServerResponse('', ['Location: /end'], 302) + ); + $finalLocation = $server->setResponseOfPath( + '/end', + new MockWebServerResponse('OK', [], 200) + ); + + $response = $client->request(Client::METHOD_GET, $url); + $server->stop(); + + // Verify redirect was followed + self::assertSame(200, $response->get_status_code()); + self::assertSame('OK', $response->get_body_content()); + self::assertSame($finalLocation, $response->get_final_requested_uri()); + } + + /** + * Test that CURLOPT_FOLLOWLOCATION = false prevents all redirect handling + * This verifies the "no redirect handling" mode where the client is responsible for redirects + */ + public function testNoRedirectHandling(): void + { + $client = new FileClient(new Registry(), [ + 'redirects' => 10, + 'force_fsockopen' => false, + 'curl_options' => [ + CURLOPT_FOLLOWLOCATION => false, // Disable all redirect handling + ], + ]); + + $server = new MockWebServer(); + $server->start(); + + $server->setDefaultResponse(new NotFoundResponse()); + $url = $server->setResponseOfPath( + '/redirect-start', + new MockWebServerResponse('Redirecting', ['Location: /redirect-end'], 302) + ); + $server->setResponseOfPath( + '/redirect-end', + new MockWebServerResponse('Final', [], 200) + ); + + $response = $client->request(Client::METHOD_GET, $url); + $server->stop(); + + // Verify redirect was NOT followed - should return the 302 response + self::assertSame(302, $response->get_status_code()); + self::assertSame('Redirecting', $response->get_body_content()); + self::assertStringContainsString('/redirect-start', $response->get_final_requested_uri()); + // The location header should be present for the client to handle + self::assertNotEmpty($response->get_header_line('location')); + } + /** * @return iterable */ From 1313590de73acbba76bc0e87e79e49d90089f81d Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 27 Jun 2026 10:41:47 +0200 Subject: [PATCH 09/12] A bit of documentation --- src/File.php | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/File.php b/src/File.php index 3b2f1f07c..300d30668 100644 --- a/src/File.php +++ b/src/File.php @@ -84,7 +84,11 @@ class File implements Response * @param ?array $headers * @param ?string $useragent * @param bool $force_fsockopen - * @param array $curl_options + * @param array $curl_options Specify cURL options. + * Special case for HTTP redirect handling: + * * CURLOPT_FOLLOWLOCATION = truthy: Native cURL handling of HTTP redirections _without_ SimplePie security checks; + * * CURLOPT_FOLLOWLOCATION = falsy: No HTTP redirections at all; + * * CURLOPT_FOLLOWLOCATION not set or null: SimplePie PHP handling of HTTP redirections with security checks (default, recommended). */ public function __construct(string $url, int $timeout = 10, int $redirects = 5, ?array $headers = null, ?string $useragent = null, bool $force_fsockopen = false, array $curl_options = []) { @@ -170,20 +174,17 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, if ($parser->parse()) { $this->set_headers($parser->headers); $this->body = $responseBody; - // Three-tier redirect handling: - // 1. CURLOPT_FOLLOWLOCATION = truthy: Native cURL redirect handling without own security checks - // 2. CURLOPT_FOLLOWLOCATION = falsy: No redirect handling at all - // 3. CURLOPT_FOLLOWLOCATION not set or null: Custom PHP redirect handling (default, recommended) + // Handling of HTTP redirections: $followLocation = $curl_options[CURLOPT_FOLLOWLOCATION] ?? null; if ($followLocation) { - // Native cURL redirect handling + // Native cURL handling of HTTP redirections $finalUrl = curl_getinfo($fp, CURLINFO_EFFECTIVE_URL); if (is_string($finalUrl) && $finalUrl !== '') { $this->url = $finalUrl; } } elseif ($followLocation === null) { - // Custom PHP redirect handling (default) + // SimplePie PHP handling of HTTP redirections (default) 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 || $redirects === -1)) { // FreshRSS: added infinite redirects for -1 $this->redirects++; @@ -247,6 +248,8 @@ function ($header) { $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); return; } + // } elseif ($followLocation == false) { + // No HTTP redirections at all } } } From c4e0a976531f89d543f1b1f02fa098ef31a78f01 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 27 Jun 2026 10:44:41 +0200 Subject: [PATCH 10/12] Minor doc --- src/File.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/File.php b/src/File.php index 300d30668..bff54475a 100644 --- a/src/File.php +++ b/src/File.php @@ -86,9 +86,9 @@ class File implements Response * @param bool $force_fsockopen * @param array $curl_options Specify cURL options. * Special case for HTTP redirect handling: + * * CURLOPT_FOLLOWLOCATION not set or null: SimplePie PHP handling of HTTP redirections with security checks (default, recommended). * * CURLOPT_FOLLOWLOCATION = truthy: Native cURL handling of HTTP redirections _without_ SimplePie security checks; * * CURLOPT_FOLLOWLOCATION = falsy: No HTTP redirections at all; - * * CURLOPT_FOLLOWLOCATION not set or null: SimplePie PHP handling of HTTP redirections with security checks (default, recommended). */ public function __construct(string $url, int $timeout = 10, int $redirects = 5, ?array $headers = null, ?string $useragent = null, bool $force_fsockopen = false, array $curl_options = []) { From d2532117e90420c3399a120f477bf7ca35997165 Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 27 Jun 2026 10:51:03 +0200 Subject: [PATCH 11/12] Move curl_close --- src/File.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/File.php b/src/File.php index bff54475a..0594ce00e 100644 --- a/src/File.php +++ b/src/File.php @@ -167,9 +167,6 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, $responseHeaders = \SimplePie\HTTP\Parser::prepareHeaders($responseHeaders); } $this->on_http_response($responseHeaders . $responseBody, $curl_options); - if (\PHP_VERSION_ID < 80000) { - curl_close($fp); - } $parser = new \SimplePie\HTTP\Parser($responseHeaders, true); if ($parser->parse()) { $this->set_headers($parser->headers); @@ -252,6 +249,9 @@ function ($header) { // No HTTP redirections at all } } + if (\PHP_VERSION_ID < 80000) { + curl_close($fp); + } } } else { $this->method = \SimplePie\SimplePie::FILE_SOURCE_REMOTE | \SimplePie\SimplePie::FILE_SOURCE_FSOCKOPEN; From c698b4b7d85a58ea4be7c96b021b287a5cdbe3ed Mon Sep 17 00:00:00 2001 From: Alexandre Alapetite Date: Sat, 27 Jun 2026 10:57:12 +0200 Subject: [PATCH 12/12] composer run fix --- src/File.php | 110 +++++++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/src/File.php b/src/File.php index 0594ce00e..cf22f0633 100644 --- a/src/File.php +++ b/src/File.php @@ -184,68 +184,68 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, // SimplePie PHP handling of HTTP redirections (default) 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 || $redirects === -1)) { // FreshRSS: added infinite redirects for -1 - $this->redirects++; - $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; - } - - // FreshRSS: POST to GET on redirect - if (isset($curl_options[CURLOPT_POST]) && in_array($this->status_code, [301, 302, 303], true)) { // Not for 307 and 308, which must not change the HTTP method - unset($curl_options[CURLOPT_POST]); - unset($curl_options[CURLOPT_POSTFIELDS]); - if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { - $curl_options[CURLOPT_HTTPHEADER] = array_filter( - $curl_options[CURLOPT_HTTPHEADER], - function ($header) { - return is_string($header) && substr(strtolower(trim($header)), 0, 13) !== 'content-type:'; - } - ); + $this->redirects++; + $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; } - } - // FreshRSS: cross-origin authentication headers removal - if (($url_parts_from = parse_url(strtolower($url))) === false) { - throw new \InvalidArgumentException('Malformed URL: ' . $url); - } - if (($url_parts_to = parse_url(strtolower($location))) === false) { - $this->error = "Invalid redirect location: malformed URL “{$location}”"; - $this->success = false; - return; - } - foreach ([&$url_parts_from, &$url_parts_to] as &$url_parts) { - if (!isset($url_parts['port']) && isset($url_parts['scheme'])) { - if ($url_parts['scheme'] === 'http') { - $url_parts['port'] = 80; - } elseif ($url_parts['scheme'] === 'https') { - $url_parts['port'] = 443; + + // FreshRSS: POST to GET on redirect + if (isset($curl_options[CURLOPT_POST]) && in_array($this->status_code, [301, 302, 303], true)) { // Not for 307 and 308, which must not change the HTTP method + unset($curl_options[CURLOPT_POST]); + unset($curl_options[CURLOPT_POSTFIELDS]); + if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { + $curl_options[CURLOPT_HTTPHEADER] = array_filter( + $curl_options[CURLOPT_HTTPHEADER], + function ($header) { + return is_string($header) && substr(strtolower(trim($header)), 0, 13) !== 'content-type:'; + } + ); } } - } - unset($url_parts); - $sameOriginRedirect = - ($url_parts_from['scheme'] ?? '') === ($url_parts_to['scheme'] ?? '') && - ($url_parts_from['host'] ?? '') === ($url_parts_to['host'] ?? '') && - ($url_parts_from['port'] ?? '') === ($url_parts_to['port'] ?? ''); - if (!$sameOriginRedirect) { - unset($curl_options[CURLOPT_COOKIE]); - unset($curl_options[CURLOPT_USERPWD]); - if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { - $curl_options[CURLOPT_HTTPHEADER] = array_filter( - $curl_options[CURLOPT_HTTPHEADER], - function ($header) { - return is_string($header) && !preg_match('/^(Cookie|Authorization)\s*:/i', $header); + // FreshRSS: cross-origin authentication headers removal + if (($url_parts_from = parse_url(strtolower($url))) === false) { + throw new \InvalidArgumentException('Malformed URL: ' . $url); + } + if (($url_parts_to = parse_url(strtolower($location))) === false) { + $this->error = "Invalid redirect location: malformed URL “{$location}”"; + $this->success = false; + return; + } + foreach ([&$url_parts_from, &$url_parts_to] as &$url_parts) { + if (!isset($url_parts['port']) && isset($url_parts['scheme'])) { + if ($url_parts['scheme'] === 'http') { + $url_parts['port'] = 80; + } elseif ($url_parts['scheme'] === 'https') { + $url_parts['port'] = 443; } - ); + } + } + unset($url_parts); + $sameOriginRedirect = + ($url_parts_from['scheme'] ?? '') === ($url_parts_to['scheme'] ?? '') && + ($url_parts_from['host'] ?? '') === ($url_parts_to['host'] ?? '') && + ($url_parts_from['port'] ?? '') === ($url_parts_to['port'] ?? ''); + if (!$sameOriginRedirect) { + unset($curl_options[CURLOPT_COOKIE]); + unset($curl_options[CURLOPT_USERPWD]); + if (is_array($curl_options[CURLOPT_HTTPHEADER] ?? null)) { + $curl_options[CURLOPT_HTTPHEADER] = array_filter( + $curl_options[CURLOPT_HTTPHEADER], + function ($header) { + return is_string($header) && !preg_match('/^(Cookie|Authorization)\s*:/i', $header); + } + ); + } } - } - $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); - $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); - return; + $this->permanentUrlMutable = $this->permanentUrlMutable && ($this->status_code == 301 || $this->status_code == 308); + $this->__construct($location, $timeout, $redirects, $headers, $useragent, $force_fsockopen, $curl_options); + return; } - // } elseif ($followLocation == false) { + // } elseif ($followLocation == false) { // No HTTP redirections at all } }