diff --git a/phpstan-baseline.php b/phpstan-baseline.php index 1bec5a750..abd5d1957 100644 --- a/phpstan-baseline.php +++ b/phpstan-baseline.php @@ -361,18 +361,6 @@ 'count' => 1, 'path' => __DIR__ . '/src/Cache/SelectOptionsCacher.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'host\' on array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int\\<0, 65535\\>, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 2, - 'path' => __DIR__ . '/src/Canonical.php', -]; -$ignoreErrors[] = [ - 'message' => '#^Cannot access offset \'scheme\' on array\\{scheme\\?\\: string, host\\?\\: string, port\\?\\: int\\<0, 65535\\>, user\\?\\: string, pass\\?\\: string, path\\?\\: string, query\\?\\: string, fragment\\?\\: string\\}\\|false\\.$#', - 'identifier' => 'offsetAccess.nonOffsetAccessible', - 'count' => 3, - 'path' => __DIR__ . '/src/Canonical.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Bolt\\\\Canonical\\:\\:generateLink\\(\\) has parameter \\$canonical with no type specified\\.$#', 'identifier' => 'missingType.parameter', @@ -1357,12 +1345,6 @@ 'count' => 1, 'path' => __DIR__ . '/src/Controller/Backend/ResetPasswordController.php', ]; -$ignoreErrors[] = [ - 'message' => '#^Method Bolt\\\\Controller\\\\Backend\\\\ResetPasswordController\\:\\:buildResetEmail\\(\\) has parameter \\$config with no value type specified in iterable type array\\.$#', - 'identifier' => 'missingType.iterableValue', - 'count' => 1, - 'path' => __DIR__ . '/src/Controller/Backend/ResetPasswordController.php', -]; $ignoreErrors[] = [ 'message' => '#^Method Bolt\\\\Controller\\\\Backend\\\\ResetPasswordController\\:\\:buildResetEmail\\(\\) has parameter \\$resetToken with no type specified\\.$#', 'identifier' => 'missingType.parameter', diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 37b5d0cdd..45dd4aa6c 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -22,7 +22,7 @@ - + diff --git a/src/Canonical.php b/src/Canonical.php index 68f670a7a..a2fac3d3c 100644 --- a/src/Canonical.php +++ b/src/Canonical.php @@ -58,6 +58,16 @@ public function setRequest(?Request $request = null): void $requestUrl = parse_url($this->request->getSchemeAndHttpHost()); + // Handle test environment or malformed URLs + if ($requestUrl === false || ! isset($requestUrl['scheme'])) { + $this->setScheme('http'); + $this->setHost('localhost'); + $this->setPort(null); + $_SERVER['CANONICAL_HOST'] = 'localhost'; + $_SERVER['CANONICAL_SCHEME'] = 'http'; + return; + } + $configCanonical = (string) $this->config->get('general/canonical', $this->getRequest()->getSchemeAndHttpHost()); if (mb_strpos($configCanonical, 'http') !== 0) { @@ -66,6 +76,16 @@ public function setRequest(?Request $request = null): void $configUrl = parse_url($configCanonical); + // Handle malformed canonical URL + if ($configUrl === false || ! isset($configUrl['scheme']) || ! isset($configUrl['host'])) { + $this->setScheme('http'); + $this->setHost('localhost'); + $this->setPort(null); + $_SERVER['CANONICAL_HOST'] = 'localhost'; + $_SERVER['CANONICAL_SCHEME'] = 'http'; + return; + } + $this->setScheme($configUrl['scheme']); $this->setHost($configUrl['host']); $this->setPort($configUrl['port'] ?? null); diff --git a/src/Controller/Backend/ResetPasswordController.php b/src/Controller/Backend/ResetPasswordController.php index e8a305f57..ecdefd7ae 100644 --- a/src/Controller/Backend/ResetPasswordController.php +++ b/src/Controller/Backend/ResetPasswordController.php @@ -4,6 +4,7 @@ namespace Bolt\Controller\Backend; +use Bolt\Collection\DeepCollection; use Bolt\Configuration\Config; use Bolt\Controller\TwigAwareController; use Bolt\Entity\User; @@ -180,7 +181,7 @@ protected function processSendingPasswordResetEmail(string $emailFormData, Maile return $this->redirectToRoute('bolt_check_email'); } - protected function buildResetEmail(array $config, $user, $resetToken): Email + protected function buildResetEmail(DeepCollection $config, $user, $resetToken): Email { return (new TemplatedEmail()) ->from(new Address($config['mail_from'], $config['mail_name'])) diff --git a/tests/php/Controller/Backend/ResetPasswordControllerTest.php b/tests/php/Controller/Backend/ResetPasswordControllerTest.php new file mode 100644 index 000000000..1706f8503 --- /dev/null +++ b/tests/php/Controller/Backend/ResetPasswordControllerTest.php @@ -0,0 +1,39 @@ +get() + * returned DeepCollection instead of array + * + * The bug was in Canonical::setRequest() which didn't handle test environment properly, + * causing parse_url() to return false/incomplete array, leading to TypeError when + * trying to access array keys. + */ +class ResetPasswordControllerTest extends DbAwareTestCase +{ + /** + * Test that the reset password page loads successfully. + * This is the main regression test - the bug prevented this page from loading. + */ + public function testResetPasswordPageLoads(): void + { + // Navigate directly to reset password page (this is what the "Forgotten password" link does) + $crawler = $this->client->request('GET', '/bolt/reset-password'); + + // Verify page loads successfully (this would fail with TypeError before the fix) + $this->assertResponseIsSuccessful(); + $this->assertRouteSame('bolt_forgot_password_request'); + + // Verify the form exists with email input + $this->assertSelectorExists('form'); + $this->assertSelectorExists('input[name="reset_password_request_form[email]"]'); + } +}