diff --git a/resources/templates/javascript/redirect.twig b/resources/templates/javascript/redirect.twig index 7aebb65d06..7572cde105 100644 --- a/resources/templates/javascript/redirect.twig +++ b/resources/templates/javascript/redirect.twig @@ -3,3 +3,4 @@ window.location = '{{ url|escape('js') }}'; }; +{{ t('Taking you to the target site.') }} diff --git a/src/Application.php b/src/Application.php index d76589b6b3..720563d637 100644 --- a/src/Application.php +++ b/src/Application.php @@ -103,7 +103,7 @@ class Application $requestHandler->add(new RequestProblemChecking($this->template, $this->responseFactory)); $requestHandler->add(new CurrentServerGlobalSetting($this->config)); $requestHandler->add(new ThemeInitialization()); - $requestHandler->add(new UrlRedirection($this->config)); + $requestHandler->add(new UrlRedirection($this->config, $this->template, $this->responseFactory)); $requestHandler->add(new SetupPageRedirection($this->config, $this->responseFactory)); $requestHandler->add(new MinimumCommonRedirection($this->config, $this->responseFactory)); $requestHandler->add(new LanguageAndThemeCookieSaving($this->config)); diff --git a/src/Http/Middleware/UrlRedirection.php b/src/Http/Middleware/UrlRedirection.php index 056a963d45..76a7e6c190 100644 --- a/src/Http/Middleware/UrlRedirection.php +++ b/src/Http/Middleware/UrlRedirection.php @@ -6,6 +6,9 @@ namespace PhpMyAdmin\Http\Middleware; use PhpMyAdmin\Config; use PhpMyAdmin\Container\ContainerBuilder; +use PhpMyAdmin\Http\Factory\ResponseFactory; +use PhpMyAdmin\ResponseRenderer; +use PhpMyAdmin\Template; use PhpMyAdmin\Theme\ThemeManager; use PhpMyAdmin\UrlRedirector; use Psr\Http\Message\ResponseInterface; @@ -13,12 +16,13 @@ use Psr\Http\Message\ServerRequestInterface; use Psr\Http\Server\MiddlewareInterface; use Psr\Http\Server\RequestHandlerInterface; -use function is_string; - final class UrlRedirection implements MiddlewareInterface { - public function __construct(private readonly Config $config) - { + public function __construct( + private readonly Config $config, + private readonly Template $template, + private readonly ResponseFactory $responseFactory, + ) { } public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface @@ -32,11 +36,8 @@ final class UrlRedirection implements MiddlewareInterface $themeManager = $container->get(ThemeManager::class); $this->config->loadUserPreferences($themeManager, true); - return UrlRedirector::redirect($this->getUrlParam($request->getQueryParams()['url'] ?? null)); - } + $urlRedirector = new UrlRedirector(ResponseRenderer::getInstance(), $this->template, $this->responseFactory); - private function getUrlParam(mixed $url): string - { - return is_string($url) ? $url : ''; + return $urlRedirector->redirect($request->getQueryParams()['url'] ?? null); } } diff --git a/src/ResponseRenderer.php b/src/ResponseRenderer.php index 6231f5090d..7cf1903494 100644 --- a/src/ResponseRenderer.php +++ b/src/ResponseRenderer.php @@ -428,21 +428,26 @@ class ResponseRenderer throw new ExitException($message); } + /** + * Avoid relative path redirect problems in case user entered URL + * like /phpmyadmin/index.php/ which some web servers happily accept. + */ + public function fixRelativeUrlForRedirect(string $url): string + { + if (! str_starts_with($url, '.')) { + return $url; + } + + return $this->config->getRootPath() . substr($url, 2); + } + /** * @psalm-param non-empty-string $url * @psalm-param StatusCodeInterface::STATUS_* $statusCode */ public function redirect(string $url, int $statusCode = StatusCodeInterface::STATUS_FOUND): void { - /** - * Avoid relative path redirect problems in case user entered URL - * like /phpmyadmin/index.php/ which some web servers happily accept. - */ - if (str_starts_with($url, '.')) { - $url = $this->config->getRootPath() . substr($url, 2); - } - - $this->addHeader('Location', $url); + $this->addHeader('Location', $this->fixRelativeUrlForRedirect($url)); $this->setStatusCode($statusCode); } diff --git a/src/UrlRedirector.php b/src/UrlRedirector.php index 6fdf4c7a1d..96e17c2b4c 100644 --- a/src/UrlRedirector.php +++ b/src/UrlRedirector.php @@ -4,10 +4,11 @@ declare(strict_types=1); namespace PhpMyAdmin; -use PhpMyAdmin\Container\ContainerBuilder; +use Fig\Http\Message\StatusCodeInterface; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; -use function __; +use function is_string; use function preg_match; /** @@ -15,37 +16,36 @@ use function preg_match; */ final class UrlRedirector { - public static function redirect(string $url): Response + public function __construct( + private readonly ResponseRenderer $response, + private readonly Template $template, + private readonly ResponseFactory $responseFactory, + ) { + } + + public function redirect(mixed $urlParam): Response { - $container = ContainerBuilder::getContainer(); - - // Only output the http headers - $response = ResponseRenderer::getInstance(); - $response->getHeader()->sendHttpHeaders(); - $response->disable(); + $response = $this->responseFactory->createResponse(); + foreach ($this->response->getHeader()->getHttpHeaders() as $name => $value) { + $response = $response->withHeader($name, $value); + } + $url = is_string($urlParam) ? $urlParam : ''; if ( $url === '' || ! preg_match('/^https:\/\/[^\n\r]*$/', $url) || ! Core::isAllowedDomain($url) ) { - $response->redirect('./'); + $response = $response->withHeader('Location', $this->response->fixRelativeUrlForRedirect('./')); - return $response->response(); + return $response->withStatus(StatusCodeInterface::STATUS_FOUND); } /** * JavaScript redirection is necessary. Because if header() is used then web browser sometimes does not change * the HTTP_REFERER field and so with old URL as Referer, token also goes to external site. - * - * @var Template $template */ - $template = $container->get('template'); - echo $template->render('javascript/redirect', ['url' => $url]); - // Display redirecting msg on screen. - // Do not display the value of $_GET['url'] to avoid showing injected content - echo __('Taking you to the target site.'); - return $response->response(); + return $response->write($this->template->render('javascript/redirect', ['url' => $url])); } } diff --git a/tests/unit/UrlRedirectorTest.php b/tests/unit/UrlRedirectorTest.php index 577261e86f..862673c510 100644 --- a/tests/unit/UrlRedirectorTest.php +++ b/tests/unit/UrlRedirectorTest.php @@ -6,43 +6,44 @@ namespace PhpMyAdmin\Tests; use Fig\Http\Message\StatusCodeInterface; use PhpMyAdmin\Config; -use PhpMyAdmin\ResponseRenderer; +use PhpMyAdmin\Http\Factory\ResponseFactory; +use PhpMyAdmin\Template; +use PhpMyAdmin\Tests\Stubs\ResponseRenderer; use PhpMyAdmin\UrlRedirector; use PHPUnit\Framework\Attributes\CoversClass; -use ReflectionProperty; #[CoversClass(UrlRedirector::class)] final class UrlRedirectorTest extends AbstractTestCase { public function testRedirectWithDisallowedUrl(): void { - (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); - $GLOBALS['lang'] = 'en'; $config = Config::getInstance(); $config->settings['PmaAbsoluteUri'] = 'http://localhost/phpmyadmin'; - $response = UrlRedirector::redirect('https://user:pass@example.com/'); + $urlRedirector = new UrlRedirector(new ResponseRenderer(), new Template(), ResponseFactory::create()); + + $response = $urlRedirector->redirect('https://user:pass@example.com/'); self::assertSame('/phpmyadmin/', $response->getHeaderLine('Location')); self::assertSame(StatusCodeInterface::STATUS_FOUND, $response->getStatusCode()); } public function testRedirectWithAllowedUrl(): void { - (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); - $GLOBALS['lang'] = 'en'; $_SERVER['SERVER_NAME'] = 'localhost'; - UrlRedirector::redirect('https://phpmyadmin.net/'); - $output = self::getActualOutputForAssertion(); - $expected = <<<'HTML' - -Taking you to the target site. -HTML; + $urlRedirector = new UrlRedirector(new ResponseRenderer(), new Template(), ResponseFactory::create()); - self::assertSame($expected, $output); + $response = $urlRedirector->redirect('https://phpmyadmin.net/'); + $expected = <<<'HTML' + + Taking you to the target site. + + HTML; + + self::assertSame($expected, (string) $response->getBody()); } }