From d5c8159fe1bb3c60dcc7045bf5cf0b20f81da17a Mon Sep 17 00:00:00 2001 From: Ralf Lang Date: Wed, 27 May 2026 11:44:04 +0200 Subject: [PATCH] feat: Amend jcdelepine's previous patch for the very odd chance somebody does HTTPS on 80 or HTTP on 443 --- src/Uri/PortUtil.php | 59 ++++++++++++++++++++++++++++++ src/Uri/RouteUrlWriter.php | 4 +-- test/Unit/Uri/PortUtilTest.php | 66 ++++++++++++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 3 deletions(-) create mode 100644 src/Uri/PortUtil.php create mode 100644 test/Unit/Uri/PortUtilTest.php diff --git a/src/Uri/PortUtil.php b/src/Uri/PortUtil.php new file mode 100644 index 00000000..165f5f13 --- /dev/null +++ b/src/Uri/PortUtil.php @@ -0,0 +1,59 @@ + 80, + 'https' => 443, + ]; + + /** + * Whether a port is the IANA default for the given scheme. + * + * Returns true when the port can be omitted from a URI without + * changing its meaning (RFC 3986 section 3.2.3). + */ + public static function isIanaDefault(string $scheme, int|string|null $port): bool + { + if ($port === null || $port === '') { + return true; + } + $default = self::SCHEME_DEFAULTS[strtolower($scheme)] ?? null; + return $default !== null && (int) $port === $default; + } + + /** + * Strip the port from a host:port string when it is the IANA default for the scheme. + */ + public static function stripDefaultPort(string $scheme, string $host): string + { + if (!str_contains($host, ':')) { + return $host; + } + $lastColon = strrpos($host, ':'); + $port = substr($host, $lastColon + 1); + if (self::isIanaDefault($scheme, $port)) { + return substr($host, 0, $lastColon); + } + return $host; + } +} diff --git a/src/Uri/RouteUrlWriter.php b/src/Uri/RouteUrlWriter.php index c13a76f8..a144b4df 100644 --- a/src/Uri/RouteUrlWriter.php +++ b/src/Uri/RouteUrlWriter.php @@ -62,9 +62,7 @@ public function absoluteUrlFor(string $routeName, array $params = []): ?string ? 'https' : 'http'; - // Strip default port to avoid redirect_uri mismatches with strict OIDC providers. - $defaultPort = $scheme === 'https' ? '443' : '80'; - $host = preg_replace('/^(.+):' . $defaultPort . '$/', '$1', $host); + $host = PortUtil::stripDefaultPort($scheme, $host); return $scheme . '://' . $host . $path; } diff --git a/test/Unit/Uri/PortUtilTest.php b/test/Unit/Uri/PortUtilTest.php new file mode 100644 index 00000000..86285565 --- /dev/null +++ b/test/Unit/Uri/PortUtilTest.php @@ -0,0 +1,66 @@ + + */ + public static function isIanaDefaultProvider(): array + { + return [ + 'https 443 is default' => ['https', 443, true], + 'http 80 is default' => ['http', 80, true], + 'https 80 is NOT default' => ['https', 80, false], + 'http 443 is NOT default' => ['http', 443, false], + 'https 8443 is NOT default' => ['https', 8443, false], + 'http 8080 is NOT default' => ['http', 8080, false], + 'null port is default' => ['https', null, true], + 'empty string port is default' => ['http', '', true], + 'string 443 on https' => ['https', '443', true], + 'string 80 on http' => ['http', '80', true], + 'string 443 on http' => ['http', '443', false], + 'ftp 21 unknown scheme' => ['ftp', 21, false], + ]; + } + + #[Test] + #[DataProvider('isIanaDefaultProvider')] + public function isIanaDefault(string $scheme, int|string|null $port, bool $expected): void + { + $this->assertSame($expected, PortUtil::isIanaDefault($scheme, $port)); + } + + /** + * @return array + */ + public static function stripDefaultPortProvider(): array + { + return [ + 'https host:443 stripped' => ['https', 'example.com:443', 'example.com'], + 'http host:80 stripped' => ['http', 'example.com:80', 'example.com'], + 'https host:80 kept' => ['https', 'example.com:80', 'example.com:80'], + 'http host:443 kept' => ['http', 'example.com:443', 'example.com:443'], + 'https host:8443 kept' => ['https', 'example.com:8443', 'example.com:8443'], + 'no port unchanged' => ['https', 'example.com', 'example.com'], + 'http host:8080 kept' => ['http', 'example.com:8080', 'example.com:8080'], + ]; + } + + #[Test] + #[DataProvider('stripDefaultPortProvider')] + public function stripDefaultPort(string $scheme, string $host, string $expected): void + { + $this->assertSame($expected, PortUtil::stripDefaultPort($scheme, $host)); + } +}