diff --git a/src/File.php b/src/File.php index 13a50afdf..a9792aaa2 100644 --- a/src/File.php +++ b/src/File.php @@ -179,6 +179,56 @@ 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], + function ($header) { + return is_string($header) && substr(strtolower(trim($header)), 0, 13) !== 'content-type:'; + } + ); + } + } + // 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; @@ -247,7 +297,8 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, $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); @@ -256,6 +307,41 @@ public function __construct(string $url, int $timeout = 10, int $redirects = 5, $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; }