diff --git a/app/services_controllers.php b/app/services_controllers.php index d87d3bd2ac..09b3d2859e 100644 --- a/app/services_controllers.php +++ b/app/services_controllers.php @@ -57,7 +57,7 @@ return [ ], ChangeLogController::class => [ 'class' => ChangeLogController::class, - 'arguments' => ['$response' => '@response', '$config' => '@config'], + 'arguments' => ['@response', '@config', '@' . ResponseFactory::class, '@template'], ], CheckRelationsController::class => [ 'class' => CheckRelationsController::class, @@ -219,7 +219,7 @@ return [ ], Database\Structure\AddPrefixController::class => [ 'class' => Database\Structure\AddPrefixController::class, - 'arguments' => ['$response' => '@response'], + 'arguments' => ['@response', '@' . ResponseFactory::class, '@template'], ], Database\Structure\AddPrefixTableController::class => [ 'class' => Database\Structure\AddPrefixTableController::class, @@ -251,11 +251,11 @@ return [ ], Database\Structure\ChangePrefixFormController::class => [ 'class' => Database\Structure\ChangePrefixFormController::class, - 'arguments' => ['$response' => '@response'], + 'arguments' => ['@response', '@' . ResponseFactory::class, '@template'], ], Database\Structure\CopyFormController::class => [ 'class' => Database\Structure\CopyFormController::class, - 'arguments' => ['$response' => '@response'], + 'arguments' => ['@response', '@' . ResponseFactory::class, '@template'], ], Database\Structure\CopyTableController::class => [ 'class' => Database\Structure\CopyTableController::class, @@ -448,12 +448,9 @@ return [ ], LicenseController::class => [ 'class' => LicenseController::class, - 'arguments' => ['$response' => '@response'], - ], - LintController::class => [ - 'class' => LintController::class, - 'arguments' => ['$response' => '@response'], + 'arguments' => ['@response', '@' . ResponseFactory::class], ], + LintController::class => ['class' => LintController::class, 'arguments' => ['@' . ResponseFactory::class]], LogoutController::class => [ 'class' => LogoutController::class, 'arguments' => ['@' . AuthenticationPluginFactory::class], @@ -541,7 +538,7 @@ return [ ], PhpInfoController::class => [ 'class' => PhpInfoController::class, - 'arguments' => ['$response' => '@response'], + 'arguments' => ['@response', '@' . ResponseFactory::class, '@config'], ], Preferences\ExportController::class => [ 'class' => Preferences\ExportController::class, @@ -591,6 +588,7 @@ return [ '$relation' => '@relation', '$config' => '@config', '$themeManager' => '@' . PhpMyAdmin\Theme\ThemeManager::class, + '$responseFactory' => '@' . ResponseFactory::class, ], ], Preferences\NavigationController::class => [ @@ -619,7 +617,7 @@ return [ ], SchemaExportController::class => [ 'class' => SchemaExportController::class, - 'arguments' => ['$export' => '@export', '$response' => '@response'], + 'arguments' => ['@export', '@response', '@' . ResponseFactory::class], ], Server\BinlogController::class => [ 'class' => Server\BinlogController::class, @@ -988,7 +986,7 @@ return [ ], Table\GetFieldController::class => [ 'class' => Table\GetFieldController::class, - 'arguments' => ['$response' => '@response', '$dbi' => '@dbi'], + 'arguments' => ['@response', '@dbi', '@' . ResponseFactory::class], ], Table\GisVisualizationController::class => [ 'class' => Table\GisVisualizationController::class, @@ -997,6 +995,7 @@ return [ '$template' => '@template', '$dbi' => '@dbi', '$dbTableExists' => '@' . DbTableExists::class, + '$responseFactory' => '@' . ResponseFactory::class, ], ], Table\ImportController::class => [ @@ -1263,6 +1262,7 @@ return [ '$tracking' => '@tracking', '$trackingChecker' => '@tracking_checker', '$dbTableExists' => '@' . DbTableExists::class, + '$responseFactory' => '@' . ResponseFactory::class, ], ], Triggers\IndexController::class => [ @@ -1318,6 +1318,7 @@ return [ '$relation' => '@relation', '$dbi' => '@dbi', '$dbTableExists' => '@' . DbTableExists::class, + '$responseFactory' => '@' . ResponseFactory::class, ], ], UserPasswordController::class => [ @@ -1326,7 +1327,7 @@ return [ ], VersionCheckController::class => [ 'class' => VersionCheckController::class, - 'arguments' => ['$response' => '@response', '$versionInformation' => '@version_information'], + 'arguments' => ['@version_information', '@' . ResponseFactory::class], ], View\CreateController::class => [ 'class' => View\CreateController::class, diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4f8ffff0a0..f78492c0ac 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2786,7 +2786,7 @@ parameters: path: src/Controllers/LicenseController.php - - message: "#^Parameter \\#2 \\.\\.\\.\\$values of function printf expects bool\\|float\\|int\\|string\\|null, mixed given\\.$#" + message: "#^Parameter \\#1 \\$path of function basename expects string, mixed given\\.$#" count: 1 path: src/Controllers/LicenseController.php @@ -4380,16 +4380,6 @@ parameters: count: 3 path: src/Controllers/Table/GetFieldController.php - - - message: "#^Parameter \\#1 \\$string of function mb_strlen expects string, string\\|null given\\.$#" - count: 1 - path: src/Controllers/Table/GetFieldController.php - - - - message: "#^Parameter \\#1 \\$test of static method PhpMyAdmin\\\\Mime\\:\\:detect\\(\\) expects string, string\\|null given\\.$#" - count: 1 - path: src/Controllers/Table/GetFieldController.php - - message: "#^Cannot access offset 'back' on mixed\\.$#" count: 1 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index e284b1f35c..0ca1317422 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -811,11 +811,6 @@ - - - - - @@ -1305,11 +1300,6 @@ - - - - - getParsedBodyParam('add_prefix', '')]]> @@ -1339,18 +1329,10 @@ - - - - - - - - @@ -2045,19 +2027,11 @@ - - - - - - - - @@ -2327,14 +2301,6 @@ - - - - - - - - @@ -2537,11 +2503,6 @@ - - - - - @@ -3513,14 +3474,6 @@ - - - - - - - - @@ -3537,9 +3490,6 @@ - - - @@ -4206,9 +4156,6 @@ - - - @@ -4382,11 +4329,6 @@ - - - - - @@ -12955,6 +12897,11 @@ settings]]> + + + + + 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/Controllers/ChangeLogController.php b/src/Controllers/ChangeLogController.php index a90c8afa9c..64f3bc0775 100644 --- a/src/Controllers/ChangeLogController.php +++ b/src/Controllers/ChangeLogController.php @@ -6,9 +6,11 @@ namespace PhpMyAdmin\Controllers; use PhpMyAdmin\Config; use PhpMyAdmin\Core; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\ResponseRenderer; +use PhpMyAdmin\Template; use PhpMyAdmin\Url; use function __; @@ -20,20 +22,26 @@ use function is_readable; use function ob_get_clean; use function ob_start; use function preg_replace; -use function printf; use function readgzfile; +use function sprintf; use function str_ends_with; final class ChangeLogController implements InvocableController { - public function __construct(private readonly ResponseRenderer $response, private readonly Config $config) - { + public function __construct( + private readonly ResponseRenderer $response, + private readonly Config $config, + private readonly ResponseFactory $responseFactory, + private readonly Template $template, + ) { } - public function __invoke(ServerRequest $request): Response|null + public function __invoke(ServerRequest $request): Response { - $this->response->disable(); - $this->response->getHeader()->sendHttpHeaders(); + $response = $this->responseFactory->createResponse(); + foreach ($this->response->getHeader()->getHttpHeaders() as $name => $value) { + $response = $response->withHeader($name, $value); + } $filename = $this->config->getChangeLogFilePath(); @@ -42,16 +50,12 @@ final class ChangeLogController implements InvocableController */ // Check if the file is available, some distributions remove these. if (! @is_readable($filename)) { - printf( - __( - 'The %s file is not available on this system, please visit %s for more information.', - ), + return $response->write(sprintf( + __('The %s file is not available on this system, please visit %s for more information.'), basename($filename), 'phpmyadmin.net', - ); - - return null; + )); } // Test if the file is in a compressed format @@ -109,12 +113,8 @@ final class ChangeLogController implements InvocableController '/a href="/' => 'a target="_blank" rel="noopener noreferrer" href="', ]; - $this->response->addHeader('Content-Type', 'text/html; charset=utf-8'); - - $this->response->render('changelog', [ + return $response->write($this->template->render('changelog', [ 'changelog' => preg_replace(array_keys($replaces), $replaces, $changelog), - ]); - - return null; + ])); } } diff --git a/src/Controllers/Database/Structure/AddPrefixController.php b/src/Controllers/Database/Structure/AddPrefixController.php index d4a232c38e..8f4dae0b9e 100644 --- a/src/Controllers/Database/Structure/AddPrefixController.php +++ b/src/Controllers/Database/Structure/AddPrefixController.php @@ -6,16 +6,21 @@ namespace PhpMyAdmin\Controllers\Database\Structure; use PhpMyAdmin\Controllers\InvocableController; use PhpMyAdmin\Current; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\ResponseRenderer; +use PhpMyAdmin\Template; use function __; final class AddPrefixController implements InvocableController { - public function __construct(private readonly ResponseRenderer $response) - { + public function __construct( + private readonly ResponseRenderer $response, + private readonly ResponseFactory $responseFactory, + private readonly Template $template, + ) { } public function __invoke(ServerRequest $request): Response|null @@ -35,9 +40,11 @@ final class AddPrefixController implements InvocableController $params['selected'][] = $selectedValue; } - $this->response->disable(); - $this->response->render('database/structure/add_prefix', ['url_params' => $params]); + $response = $this->responseFactory->createResponse(); + foreach ($this->response->getHeader()->getHttpHeaders() as $name => $value) { + $response = $response->withHeader($name, $value); + } - return null; + return $response->write($this->template->render('database/structure/add_prefix', ['url_params' => $params])); } } diff --git a/src/Controllers/Database/Structure/ChangePrefixFormController.php b/src/Controllers/Database/Structure/ChangePrefixFormController.php index c034db49fd..0672c5c7c4 100644 --- a/src/Controllers/Database/Structure/ChangePrefixFormController.php +++ b/src/Controllers/Database/Structure/ChangePrefixFormController.php @@ -6,16 +6,21 @@ namespace PhpMyAdmin\Controllers\Database\Structure; use PhpMyAdmin\Controllers\InvocableController; use PhpMyAdmin\Current; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\ResponseRenderer; +use PhpMyAdmin\Template; use function __; final class ChangePrefixFormController implements InvocableController { - public function __construct(private readonly ResponseRenderer $response) - { + public function __construct( + private readonly ResponseRenderer $response, + private readonly ResponseFactory $responseFactory, + private readonly Template $template, + ) { } public function __invoke(ServerRequest $request): Response|null @@ -40,12 +45,14 @@ final class ChangePrefixFormController implements InvocableController $urlParams['selected'][] = $selectedValue; } - $this->response->disable(); - $this->response->render('database/structure/change_prefix_form', [ + $response = $this->responseFactory->createResponse(); + foreach ($this->response->getHeader()->getHttpHeaders() as $name => $value) { + $response = $response->withHeader($name, $value); + } + + return $response->write($this->template->render('database/structure/change_prefix_form', [ 'route' => $route, 'url_params' => $urlParams, - ]); - - return null; + ])); } } diff --git a/src/Controllers/Database/Structure/CopyFormController.php b/src/Controllers/Database/Structure/CopyFormController.php index 00b4b5ae20..f3faf6f7f9 100644 --- a/src/Controllers/Database/Structure/CopyFormController.php +++ b/src/Controllers/Database/Structure/CopyFormController.php @@ -7,16 +7,21 @@ namespace PhpMyAdmin\Controllers\Database\Structure; use PhpMyAdmin\Controllers\InvocableController; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\ResponseRenderer; +use PhpMyAdmin\Template; use function __; final class CopyFormController implements InvocableController { - public function __construct(private readonly ResponseRenderer $response) - { + public function __construct( + private readonly ResponseRenderer $response, + private readonly ResponseFactory $responseFactory, + private readonly Template $template, + ) { } public function __invoke(ServerRequest $request): Response|null @@ -44,12 +49,14 @@ final class CopyFormController implements InvocableController } } - $this->response->disable(); - $this->response->render('database/structure/copy_form', [ + $response = $this->responseFactory->createResponse(); + foreach ($this->response->getHeader()->getHttpHeaders() as $name => $value) { + $response = $response->withHeader($name, $value); + } + + return $response->write($this->template->render('database/structure/copy_form', [ 'url_params' => $urlParams, 'options' => $databasesList->getList(), - ]); - - return null; + ])); } } diff --git a/src/Controllers/LicenseController.php b/src/Controllers/LicenseController.php index 962a8929bb..b7dda4d2b4 100644 --- a/src/Controllers/LicenseController.php +++ b/src/Controllers/LicenseController.php @@ -1,52 +1,59 @@ response->disable(); - $this->response->addHeader('Content-Type', 'text/plain; charset=utf-8'); + $response = $this->responseFactory->createResponse(); + foreach ($this->response->getHeader()->getHttpHeaders() as $name => $value) { + $response = $response->withHeader($name, $value); + } $filename = LICENSE_FILE; // Check if the file is available, some distributions remove these. - if (@is_readable($filename)) { - readfile($filename); - - return null; + if (! @is_readable($filename)) { + return $response->write(sprintf( + __('The %s file is not available on this system, please visit %s for more information.'), + basename($filename), + 'phpmyadmin.net', + )); } - printf( - __( - 'The %s file is not available on this system, please visit %s for more information.', - ), - $filename, - 'https://www.phpmyadmin.net/', - ); + $response = $response->withHeader('Content-Type', 'text/plain; charset=utf-8'); - return null; + ob_start(); + readfile($filename); + $license = (string) ob_get_clean(); + + return $response->write($license); } } diff --git a/src/Controllers/LintController.php b/src/Controllers/LintController.php index d632034e1f..5db9fe6d20 100644 --- a/src/Controllers/LintController.php +++ b/src/Controllers/LintController.php @@ -1,23 +1,18 @@ "DELIMITER $$ CREATE TRIGGER `a` AFTER INSERT ON `b` FOR EACH ROW\n", ]; - public function __construct(private readonly ResponseRenderer $response) + public function __construct(private readonly ResponseFactory $responseFactory) { } @@ -70,15 +65,11 @@ final class LintController implements InvocableController } } - // Disabling standard response. - $this->response->disable(); - + $response = $this->responseFactory->createResponse(); foreach (Core::headerJSON() as $name => $value) { - header(sprintf('%s: %s', $name, $value)); + $response = $response->withHeader($name, $value); } - echo json_encode($lints); - - return null; + return $response->write((string) json_encode($lints)); } } diff --git a/src/Controllers/PhpInfoController.php b/src/Controllers/PhpInfoController.php index 289e03b686..762c7b90e3 100644 --- a/src/Controllers/PhpInfoController.php +++ b/src/Controllers/PhpInfoController.php @@ -1,17 +1,18 @@ response->disable(); - $this->response->getHeader()->sendHttpHeaders(); - - if (! Config::getInstance()->settings['ShowPhpInfo']) { - return null; + $response = $this->responseFactory->createResponse(); + foreach ($this->response->getHeader()->getHttpHeaders() as $name => $value) { + $response = $response->withHeader($name, $value); } - phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES); + if (! $this->config->settings['ShowPhpInfo']) { + $response = $response->withHeader('Location', $this->response->fixRelativeUrlForRedirect('./')); - return null; + return $response->withStatus(StatusCodeInterface::STATUS_FOUND); + } + + ob_start(); + phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES); + $phpInfo = (string) ob_get_clean(); + + return $response->write($phpInfo); } } diff --git a/src/Controllers/Preferences/ManageController.php b/src/Controllers/Preferences/ManageController.php index 355359f253..fb7878d681 100644 --- a/src/Controllers/Preferences/ManageController.php +++ b/src/Controllers/Preferences/ManageController.php @@ -11,6 +11,7 @@ use PhpMyAdmin\ConfigStorage\Relation; use PhpMyAdmin\Controllers\InvocableController; use PhpMyAdmin\Core; use PhpMyAdmin\File; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Message; @@ -51,6 +52,7 @@ final class ManageController implements InvocableController private readonly Relation $relation, private readonly Config $config, private readonly ThemeManager $themeManager, + private readonly ResponseFactory $responseFactory, ) { } @@ -69,29 +71,29 @@ final class ManageController implements InvocableController $GLOBALS['error'] = ''; if ($request->hasBodyParam('submit_export') && $request->getParsedBodyParam('export_type') === 'text_file') { // export to JSON file - $this->response->disable(); + $response = $this->responseFactory->createResponse(); $filename = 'phpMyAdmin-config-' . urlencode(Core::getEnv('HTTP_HOST')) . '.json'; Core::downloadHeader($filename, 'application/json'); $settings = $this->userPreferences->load(); - echo json_encode($settings['config_data'], JSON_PRETTY_PRINT); - return null; + return $response->write((string) json_encode($settings['config_data'], JSON_PRETTY_PRINT)); } if ($request->hasBodyParam('submit_export') && $request->getParsedBodyParam('export_type') === 'php_file') { - // export to JSON file - $this->response->disable(); + // export to PHP file + $response = $this->responseFactory->createResponse(); $filename = 'phpMyAdmin-config-' . urlencode(Core::getEnv('HTTP_HOST')) . '.php'; Core::downloadHeader($filename, 'application/php'); $settings = $this->userPreferences->load(); - echo '/* ' . __('phpMyAdmin configuration snippet') . " */\n\n"; - echo '/* ' . __('Paste it to your config.inc.php') . " */\n\n"; + + $output = '/* ' . __('phpMyAdmin configuration snippet') . " */\n\n"; + $output .= '/* ' . __('Paste it to your config.inc.php') . " */\n\n"; foreach ($settings['config_data'] as $key => $val) { - echo '$cfg[\'' . str_replace('/', '\'][\'', $key) . '\'] = '; - echo var_export($val, true) . ";\n"; + $output .= '$cfg[\'' . str_replace('/', '\'][\'', $key) . '\'] = '; + $output .= var_export($val, true) . ";\n"; } - return null; + return $response->write($output); } if ($request->hasBodyParam('submit_get_json')) { diff --git a/src/Controllers/SchemaExportController.php b/src/Controllers/SchemaExportController.php index e9c8fda199..ac23784550 100644 --- a/src/Controllers/SchemaExportController.php +++ b/src/Controllers/SchemaExportController.php @@ -8,6 +8,7 @@ use PhpMyAdmin\Core; use PhpMyAdmin\Exceptions\ExportException; use PhpMyAdmin\Export\Export; use PhpMyAdmin\Html\MySQLDocumentation; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Identifiers\DatabaseName; @@ -23,8 +24,11 @@ use function mb_strlen; */ final class SchemaExportController implements InvocableController { - public function __construct(private readonly Export $export, private readonly ResponseRenderer $response) - { + public function __construct( + private readonly Export $export, + private readonly ResponseRenderer $response, + private readonly ResponseFactory $responseFactory, + ) { } public function __invoke(ServerRequest $request): Response|null @@ -54,14 +58,13 @@ final class SchemaExportController implements InvocableController return null; } - $this->response->disable(); + $response = $this->responseFactory->createResponse(); Core::downloadHeader( $exportInfo['fileName'], $exportInfo['mediaType'], mb_strlen($exportInfo['fileData'], '8bit'), ); - echo $exportInfo['fileData']; - return null; + return $response->write($exportInfo['fileData']); } } diff --git a/src/Controllers/Table/GetFieldController.php b/src/Controllers/Table/GetFieldController.php index e23cc4ca3e..e49b9a08a2 100644 --- a/src/Controllers/Table/GetFieldController.php +++ b/src/Controllers/Table/GetFieldController.php @@ -9,6 +9,7 @@ use PhpMyAdmin\Core; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\Html\Generator; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Message; @@ -27,14 +28,15 @@ use function sprintf; */ final class GetFieldController implements InvocableController { - public function __construct(private readonly ResponseRenderer $response, private readonly DatabaseInterface $dbi) - { + public function __construct( + private readonly ResponseRenderer $response, + private readonly DatabaseInterface $dbi, + private readonly ResponseFactory $responseFactory, + ) { } public function __invoke(ServerRequest $request): Response|null { - $this->response->disable(); - if (! $this->response->checkParameters(['db', 'table'])) { return null; } @@ -83,16 +85,18 @@ final class GetFieldController implements InvocableController return null; } + $result ??= ''; + /* Avoid corrupting data */ ini_set('url_rewriter.tags', ''); + $response = $this->responseFactory->createResponse(); Core::downloadHeader( Current::$table . '-' . $transformKey . '.bin', Mime::detect($result), mb_strlen($result, '8bit'), ); - echo $result; - return null; + return $response->write($result); } } diff --git a/src/Controllers/Table/GisVisualizationController.php b/src/Controllers/Table/GisVisualizationController.php index 6b9be5223b..281187fffb 100644 --- a/src/Controllers/Table/GisVisualizationController.php +++ b/src/Controllers/Table/GisVisualizationController.php @@ -13,6 +13,7 @@ use PhpMyAdmin\DbTableExists; use PhpMyAdmin\FieldMetadata; use PhpMyAdmin\Gis\GisVisualization; use PhpMyAdmin\Html\Generator; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Identifiers\DatabaseName; @@ -26,6 +27,8 @@ use function __; use function in_array; use function is_array; use function is_string; +use function ob_get_clean; +use function ob_start; /** * Handles creation of the GIS visualizations. @@ -37,6 +40,7 @@ final class GisVisualizationController implements InvocableController private readonly Template $template, private readonly DatabaseInterface $dbi, private readonly DbTableExists $dbTableExists, + private readonly ResponseFactory $responseFactory, ) { } @@ -112,11 +116,13 @@ final class GisVisualizationController implements InvocableController $visualization = GisVisualization::get($sqlQuery, $visualizationSettings, $rows, $pos); if (isset($_GET['saveToFile'])) { - $this->response->disable(); + $response = $this->responseFactory->createResponse(); $filename = $visualization->getSpatialColumn(); + ob_start(); $visualization->toFile($filename, $_GET['fileFormat']); + $output = ob_get_clean(); - return null; + return $response->write((string) $output); } $this->response->addScriptFiles(['vendor/openlayers/OpenLayers.js', 'table/gis_visualization.js']); diff --git a/src/Controllers/Table/TrackingController.php b/src/Controllers/Table/TrackingController.php index a4f2143b05..0f49fdb0ef 100644 --- a/src/Controllers/Table/TrackingController.php +++ b/src/Controllers/Table/TrackingController.php @@ -10,6 +10,7 @@ use PhpMyAdmin\Controllers\InvocableController; use PhpMyAdmin\Core; use PhpMyAdmin\Current; use PhpMyAdmin\DbTableExists; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Identifiers\DatabaseName; @@ -42,6 +43,7 @@ final class TrackingController implements InvocableController private readonly Tracking $tracking, private readonly TrackingChecker $trackingChecker, private readonly DbTableExists $dbTableExists, + private readonly ResponseFactory $responseFactory, ) { } @@ -138,11 +140,10 @@ final class TrackingController implements InvocableController // Export as file download if ($reportExportType === 'sqldumpfile') { $downloadInfo = $this->tracking->getDownloadInfoForExport($tableParam, $entries); - $this->response->disable(); + $response = $this->responseFactory->createResponse(); Core::downloadHeader($downloadInfo['filename'], 'text/x-sql', mb_strlen($downloadInfo['dump'])); - echo $downloadInfo['dump']; - return null; + return $response->write($downloadInfo['dump']); } // Export as SQL execution diff --git a/src/Controllers/Transformation/WrapperController.php b/src/Controllers/Transformation/WrapperController.php index 01e26f3197..c728110936 100644 --- a/src/Controllers/Transformation/WrapperController.php +++ b/src/Controllers/Transformation/WrapperController.php @@ -9,6 +9,7 @@ use PhpMyAdmin\Controllers\InvocableController; use PhpMyAdmin\Core; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\DbTableExists; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Identifiers\DatabaseName; @@ -24,6 +25,8 @@ use function __; use function htmlspecialchars; use function is_numeric; use function is_string; +use function ob_get_clean; +use function ob_start; use function round; use function sprintf; use function str_contains; @@ -42,6 +45,7 @@ final class WrapperController implements InvocableController private readonly Relation $relation, private readonly DatabaseInterface $dbi, private readonly DbTableExists $dbTableExists, + private readonly ResponseFactory $responseFactory, ) { } @@ -107,9 +111,10 @@ final class WrapperController implements InvocableController } } - // Disabling standard response, we are sending binary here - $this->response->disable(); - $this->response->getHeader()->sendHttpHeaders(); + $response = $this->responseFactory->createResponse(); + foreach ($this->response->getHeader()->getHttpHeaders() as $name => $value) { + $response = $response->withHeader($name, $value); + } /** @psalm-suppress MixedAssignment */ $contentType = $request->getParam('ct'); @@ -133,19 +138,15 @@ final class WrapperController implements InvocableController $resize = $request->getParam('resize'); if ($resize !== 'jpeg' && $resize !== 'png') { if (str_contains(strtolower($contentMediaType), 'html')) { - echo htmlspecialchars($row[$transformKey]); - - return null; + return $response->write(htmlspecialchars($row[$transformKey])); } - echo $row[$transformKey]; - - return null; + return $response->write($row[$transformKey]); } $srcImage = ImageWrapper::fromString($row[$transformKey]); if ($srcImage === null) { - return null; + return $response; } $newHeight = $this->formatSize($request->getParam('newHeight')); @@ -172,18 +173,21 @@ final class WrapperController implements InvocableController $destImage = ImageWrapper::create($destWidth, $destHeight); if ($destImage === null) { - return null; + return $response; } $destImage->copyResampled($srcImage, 0, 0, 0, 0, $destWidth, $destHeight, $srcWidth, $srcHeight); + ob_start(); if ($resize === 'jpeg') { $destImage->jpeg(null, 75); } else { $destImage->png(); } - return null; + $output = ob_get_clean(); + + return $response->write((string) $output); } private function formatSize(mixed $size): int diff --git a/src/Controllers/VersionCheckController.php b/src/Controllers/VersionCheckController.php index 38c116cae5..cfed961ec8 100644 --- a/src/Controllers/VersionCheckController.php +++ b/src/Controllers/VersionCheckController.php @@ -5,14 +5,12 @@ declare(strict_types=1); namespace PhpMyAdmin\Controllers; use PhpMyAdmin\Core; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Response; use PhpMyAdmin\Http\ServerRequest; -use PhpMyAdmin\ResponseRenderer; use PhpMyAdmin\VersionInformation; -use function header; use function json_encode; -use function sprintf; /** * A caching proxy for retrieving version information from https://www.phpmyadmin.net/. @@ -20,29 +18,22 @@ use function sprintf; final class VersionCheckController implements InvocableController { public function __construct( - private readonly ResponseRenderer $response, private readonly VersionInformation $versionInformation, + private readonly ResponseFactory $responseFactory, ) { } - public function __invoke(ServerRequest $request): Response|null + public function __invoke(ServerRequest $request): Response { - $_GET['ajax_request'] = 'true'; - - // Disabling standard response. - $this->response->disable(); - - // Always send the correct headers + $response = $this->responseFactory->createResponse(); foreach (Core::headerJSON() as $name => $value) { - header(sprintf('%s: %s', $name, $value)); + $response = $response->withHeader($name, $value); } $versionDetails = $this->versionInformation->getLatestVersions(); if ($versionDetails === null) { - echo json_encode([]); - - return null; + return $response->write((string) json_encode([])); } $latestCompatible = $this->versionInformation->getLatestCompatibleVersion($versionDetails); @@ -53,8 +44,6 @@ final class VersionCheckController implements InvocableController $date = $latestCompatible->date; } - echo json_encode(['version' => $version, 'date' => $date]); - - return null; + return $response->write((string) json_encode(['version' => $version, 'date' => $date])); } } 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/Controllers/ChangeLogControllerTest.php b/tests/unit/Controllers/ChangeLogControllerTest.php index 95c3c7bff8..178110206b 100644 --- a/tests/unit/Controllers/ChangeLogControllerTest.php +++ b/tests/unit/Controllers/ChangeLogControllerTest.php @@ -4,8 +4,10 @@ declare(strict_types=1); namespace PhpMyAdmin\Tests\Controllers; +use Fig\Http\Message\StatusCodeInterface; use PhpMyAdmin\Config; use PhpMyAdmin\Controllers\ChangeLogController; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Factory\ServerRequestFactory; use PhpMyAdmin\Template; use PhpMyAdmin\Tests\AbstractTestCase; @@ -27,34 +29,33 @@ final class ChangeLogControllerTest extends AbstractTestCase $responseRenderer = new ResponseRenderer(); $template = new Template(); - $controller = new ChangeLogController($responseRenderer, $config); - $controller($request); + $controller = new ChangeLogController($responseRenderer, $config, ResponseFactory::create(), $template); + $response = $controller($request); - self::assertTrue($responseRenderer->isDisabled()); - $response = $responseRenderer->getResponse(); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); self::assertSame(['text/html; charset=utf-8'], $response->getHeader('Content-Type')); // phpcs:disable Generic.Files.LineLength.TooLong $changelog = <<<'HTML' -phpMyAdmin - ChangeLog -====================== + phpMyAdmin - ChangeLog + ====================== -5.2.2 (not yet released) -- issue #17522 Fix case where the routes cache file is invalid -- issue Upgrade slim/psr7 to 1.4.1 for CVE-2023-30536 - GHSA-q2qj-628g-vhfw + 5.2.2 (not yet released) + - issue #17522 Fix case where the routes cache file is invalid + - issue Upgrade slim/psr7 to 1.4.1 for CVE-2023-30536 - GHSA-q2qj-628g-vhfw -5.2.1 (2023-02-07) -- issue #16418 Fix FAQ 1.44 about manually removing vendor folders -- issue [security] Fix an XSS attack through the drag-and-drop upload feature (PMASA-2023-01) + 5.2.1 (2023-02-07) + - issue #16418 Fix FAQ 1.44 about manually removing vendor folders + - issue [security] Fix an XSS attack through the drag-and-drop upload feature (PMASA-2023-01) - --- Older ChangeLogs can be found on our project website --- - https://www.phpmyadmin.net/old-stuff/ChangeLogs/ + --- Older ChangeLogs can be found on our project website --- + https://www.phpmyadmin.net/old-stuff/ChangeLogs/ -HTML; + HTML; // phpcs:enable $expected = $template->render('changelog', ['changelog' => $changelog]); - self::assertSame($expected, $responseRenderer->getHTMLResult()); + self::assertSame($expected, (string) $response->getBody()); } #[RequiresPhpExtension('zlib')] @@ -66,16 +67,18 @@ HTML; $request = ServerRequestFactory::create()->createServerRequest('GET', 'http://example.com/'); $responseRenderer = new ResponseRenderer(); - $controller = new ChangeLogController($responseRenderer, $config); - $controller($request); + $controller = new ChangeLogController($responseRenderer, $config, ResponseFactory::create(), new Template()); + $response = $controller($request); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['text/html; charset=utf-8'], $response->getHeader('Content-Type')); self::assertStringContainsString( '- ' . 'issue #16418 Fix ' . 'FAQ 1.44 about manually removing vendor folders', - $responseRenderer->getHTMLResult(), + (string) $response->getBody(), ); } @@ -87,15 +90,16 @@ HTML; $request = ServerRequestFactory::create()->createServerRequest('GET', 'http://example.com/'); $responseRenderer = new ResponseRenderer(); - $controller = new ChangeLogController($responseRenderer, $config); - $controller($request); + $controller = new ChangeLogController($responseRenderer, $config, ResponseFactory::create(), new Template()); + $response = $controller($request); - self::assertSame('', $responseRenderer->getHTMLResult()); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['text/html; charset=utf-8'], $response->getHeader('Content-Type')); self::assertSame( 'The InvalidChangeLog file is not available on this system, please visit' . ' phpmyadmin.net for more information.', - self::getActualOutputForAssertion(), + (string) $response->getBody(), ); } } diff --git a/tests/unit/Controllers/Database/Structure/AddPrefixControllerTest.php b/tests/unit/Controllers/Database/Structure/AddPrefixControllerTest.php new file mode 100644 index 0000000000..14fee1a563 --- /dev/null +++ b/tests/unit/Controllers/Database/Structure/AddPrefixControllerTest.php @@ -0,0 +1,42 @@ +createServerRequest('POST', 'http://example.com/') + ->withQueryParams(['db' => 'test_db']) + ->withParsedBody(['selected_tbl' => ['test_table']]); + + $template = new Template(); + $controller = new AddPrefixController(new ResponseRenderer(), ResponseFactory::create(), $template); + $response = $controller($request); + + $expected = $template->render( + 'database/structure/add_prefix', + ['url_params' => ['db' => 'test_db', 'selected' => ['test_table']]], + ); + + self::assertNotNull($response); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['text/html; charset=utf-8'], $response->getHeader('Content-Type')); + self::assertSame($expected, (string) $response->getBody()); + } +} diff --git a/tests/unit/Controllers/Database/Structure/ChangePrefixFormControllerTest.php b/tests/unit/Controllers/Database/Structure/ChangePrefixFormControllerTest.php new file mode 100644 index 0000000000..fbc540a871 --- /dev/null +++ b/tests/unit/Controllers/Database/Structure/ChangePrefixFormControllerTest.php @@ -0,0 +1,42 @@ +createServerRequest('POST', 'http://example.com/') + ->withQueryParams(['db' => 'test_db']) + ->withParsedBody(['selected_tbl' => ['test_table']]); + + $template = new Template(); + $controller = new ChangePrefixFormController(new ResponseRenderer(), ResponseFactory::create(), $template); + $response = $controller($request); + + $expected = $template->render('database/structure/change_prefix_form', [ + 'route' => '/database/structure/replace-prefix', + 'url_params' => ['db' => 'test_db', 'selected' => ['test_table']], + ]); + + self::assertNotNull($response); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['text/html; charset=utf-8'], $response->getHeader('Content-Type')); + self::assertSame($expected, (string) $response->getBody()); + } +} diff --git a/tests/unit/Controllers/Database/Structure/CopyFormControllerTest.php b/tests/unit/Controllers/Database/Structure/CopyFormControllerTest.php new file mode 100644 index 0000000000..5280d109d8 --- /dev/null +++ b/tests/unit/Controllers/Database/Structure/CopyFormControllerTest.php @@ -0,0 +1,56 @@ +createDbiDummy(); + $dbiDummy->addResult( + 'SELECT `SCHEMA_NAME` FROM `INFORMATION_SCHEMA`.`SCHEMATA`', + [['test_db'], ['test_db_1'], ['test_db_2']], + ['SCHEMA_NAME'], + ); + + DatabaseInterface::$instance = $this->createDatabaseInterface($dbiDummy); + Current::$database = 'test_db'; + + $request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/') + ->withQueryParams(['db' => 'test_db']) + ->withParsedBody(['selected_tbl' => ['test_table']]); + + $template = new Template(); + $controller = new CopyFormController(new ResponseRenderer(), ResponseFactory::create(), $template); + $response = $controller($request); + + $expected = $template->render('database/structure/copy_form', [ + 'url_params' => ['db' => 'test_db', 'selected' => ['test_table']], + 'options' => [ + ['name' => 'test_db_1', 'is_selected' => false], + ['name' => 'test_db_2', 'is_selected' => false], + ], + ]); + + self::assertNotNull($response); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['text/html; charset=utf-8'], $response->getHeader('Content-Type')); + self::assertSame($expected, (string) $response->getBody()); + + $dbiDummy->assertAllQueriesConsumed(); + } +} diff --git a/tests/unit/Controllers/LicenseControllerTest.php b/tests/unit/Controllers/LicenseControllerTest.php new file mode 100644 index 0000000000..d5b2eaed0a --- /dev/null +++ b/tests/unit/Controllers/LicenseControllerTest.php @@ -0,0 +1,32 @@ +createServerRequest('GET', 'http://example.com/'); + + $controller = new LicenseController(new ResponseRenderer(), ResponseFactory::create()); + $response = $controller($request); + + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['text/plain; charset=utf-8'], $response->getHeader('Content-Type')); + + $body = (string) $response->getBody(); + self::assertStringContainsString('GNU GENERAL PUBLIC LICENSE', $body); + self::assertStringContainsString('Version 2, June 1991', $body); + } +} diff --git a/tests/unit/Controllers/LintControllerTest.php b/tests/unit/Controllers/LintControllerTest.php index 90b41fa07a..5440ccb18c 100644 --- a/tests/unit/Controllers/LintControllerTest.php +++ b/tests/unit/Controllers/LintControllerTest.php @@ -4,19 +4,18 @@ declare(strict_types=1); namespace PhpMyAdmin\Tests\Controllers; +use Fig\Http\Message\StatusCodeInterface; use PhpMyAdmin\Controllers\LintController; use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Tests\AbstractTestCase; -use PhpMyAdmin\Tests\Stubs\ResponseRenderer; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; use function json_encode; #[CoversClass(LintController::class)] -#[RunTestsInSeparateProcesses] -class LintControllerTest extends AbstractTestCase +final class LintControllerTest extends AbstractTestCase { protected function setUp(): void { @@ -31,9 +30,12 @@ class LintControllerTest extends AbstractTestCase $request->method('isAjax')->willReturn(true); $request->method('getParsedBodyParam')->willReturnMap([['sql_query', '', ''], ['options', null, null]]); - $this->getLintController()($request); + $response = $this->getLintController()($request); - $output = $this->getActualOutputForAssertion(); + self::assertNotNull($response); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['application/json; charset=UTF-8'], $response->getHeader('Content-Type')); + $output = (string) $response->getBody(); self::assertJson($output); self::assertJsonStringEqualsJsonString('[]', $output); } @@ -47,9 +49,12 @@ class LintControllerTest extends AbstractTestCase ['options', null, null], ]); - $this->getLintController()($request); + $response = $this->getLintController()($request); - $output = $this->getActualOutputForAssertion(); + self::assertNotNull($response); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['application/json; charset=UTF-8'], $response->getHeader('Content-Type')); + $output = (string) $response->getBody(); self::assertJson($output); self::assertJsonStringEqualsJsonString('[]', $output); } @@ -99,15 +104,18 @@ class LintControllerTest extends AbstractTestCase ['options', null, null], ]); - $this->getLintController()($request); + $response = $this->getLintController()($request); - $output = $this->getActualOutputForAssertion(); + self::assertNotNull($response); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['application/json; charset=UTF-8'], $response->getHeader('Content-Type')); + $output = (string) $response->getBody(); self::assertJson($output); self::assertJsonStringEqualsJsonString($expectedJson, $output); } private function getLintController(): LintController { - return new LintController(new ResponseRenderer()); + return new LintController(ResponseFactory::create()); } } diff --git a/tests/unit/Controllers/PhpInfoControllerTest.php b/tests/unit/Controllers/PhpInfoControllerTest.php new file mode 100644 index 0000000000..5d157e4633 --- /dev/null +++ b/tests/unit/Controllers/PhpInfoControllerTest.php @@ -0,0 +1,60 @@ +settings['ShowPhpInfo'] = true; + + $request = ServerRequestFactory::create()->createServerRequest('GET', 'http://example.com/'); + + $controller = new PhpInfoController(new ResponseRenderer(), ResponseFactory::create(), $config); + $response = $controller($request); + + ob_start(); + phpinfo(INFO_GENERAL | INFO_CONFIGURATION | INFO_MODULES); + $expected = (string) ob_get_clean(); + + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['text/html; charset=utf-8'], $response->getHeader('Content-Type')); + self::assertSame($expected, (string) $response->getBody()); + } + + public function testWithShowPhpInfoEqualsFalse(): void + { + $config = Config::getInstance(); + $config->settings['ShowPhpInfo'] = false; + $config->settings['PmaAbsoluteUri'] = 'http://localhost/phpmyadmin'; + + $request = ServerRequestFactory::create()->createServerRequest('GET', 'http://example.com/'); + + $controller = new PhpInfoController(new ResponseRenderer(), ResponseFactory::create(), $config); + $response = $controller($request); + + self::assertSame(StatusCodeInterface::STATUS_FOUND, $response->getStatusCode()); + self::assertSame('/phpmyadmin/', $response->getHeaderLine('Location')); + } +} diff --git a/tests/unit/Controllers/SchemaExportControllerTest.php b/tests/unit/Controllers/SchemaExportControllerTest.php index 1f3f7a1e63..3b8b6633aa 100644 --- a/tests/unit/Controllers/SchemaExportControllerTest.php +++ b/tests/unit/Controllers/SchemaExportControllerTest.php @@ -4,16 +4,23 @@ declare(strict_types=1); namespace PhpMyAdmin\Tests\Controllers; +use Fig\Http\Message\StatusCodeInterface; use PhpMyAdmin\Controllers\SchemaExportController; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\Export\Export; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Tests\AbstractTestCase; use PhpMyAdmin\Tests\Stubs\ResponseRenderer; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; + +use function function_exists; +use function xdebug_get_headers; #[CoversClass(SchemaExportController::class)] -class SchemaExportControllerTest extends AbstractTestCase +#[RunTestsInSeparateProcesses] +final class SchemaExportControllerTest extends AbstractTestCase { public function testExport(): void { @@ -28,13 +35,20 @@ class SchemaExportControllerTest extends AbstractTestCase 'fileData' => 'file data', ]); - $response = new ResponseRenderer(); - $controller = new SchemaExportController($export, $response); - $controller($request); - $output = $this->getActualOutputForAssertion(); - self::assertSame('file data', $output); - self::assertTrue($response->isDisabled()); - self::assertSame('', $response->getHTMLResult()); - self::assertSame([], $response->getJSONResult()); + $controller = new SchemaExportController($export, new ResponseRenderer(), ResponseFactory::create()); + $response = $controller($request); + + self::assertNotNull($response); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame('file data', (string) $response->getBody()); + + if (! function_exists('xdebug_get_headers')) { + return; + } + + $headersList = xdebug_get_headers(); + self::assertContains('Content-Disposition: attachment; filename="file.svg"', $headersList); + self::assertContains('Content-Type: image/svg+xml', $headersList); + self::assertContains('Content-Length: 9', $headersList); } } diff --git a/tests/unit/Controllers/Table/GetFieldControllerTest.php b/tests/unit/Controllers/Table/GetFieldControllerTest.php index ec9f7173d0..62bdb74373 100644 --- a/tests/unit/Controllers/Table/GetFieldControllerTest.php +++ b/tests/unit/Controllers/Table/GetFieldControllerTest.php @@ -4,10 +4,12 @@ declare(strict_types=1); namespace PhpMyAdmin\Tests\Controllers\Table; +use Fig\Http\Message\StatusCodeInterface; use PhpMyAdmin\Controllers\Table\GetFieldController; use PhpMyAdmin\Core; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Tests\AbstractTestCase; use PhpMyAdmin\Tests\Stubs\ResponseRenderer; @@ -51,7 +53,10 @@ class GetFieldControllerTest extends AbstractTestCase $dbi = $this->createDatabaseInterface($dummyDbi); DatabaseInterface::$instance = $dbi; - (new GetFieldController(new ResponseRenderer(), $dbi))($request); - $this->expectOutputString('46494c45'); + $response = (new GetFieldController(new ResponseRenderer(), $dbi, ResponseFactory::create()))($request); + + self::assertNotNull($response); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame('46494c45', (string) $response->getBody()); } } diff --git a/tests/unit/Controllers/Table/GisVisualizationControllerTest.php b/tests/unit/Controllers/Table/GisVisualizationControllerTest.php index 20feac71c4..5835cbd61a 100644 --- a/tests/unit/Controllers/Table/GisVisualizationControllerTest.php +++ b/tests/unit/Controllers/Table/GisVisualizationControllerTest.php @@ -9,6 +9,7 @@ use PhpMyAdmin\Core; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\DbTableExists; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Factory\ServerRequestFactory; use PhpMyAdmin\Template; use PhpMyAdmin\Tests\AbstractTestCase; @@ -111,8 +112,17 @@ class GisVisualizationControllerTest extends AbstractTestCase $request = ServerRequestFactory::create()->createServerRequest('POST', 'http://example.com/') ->withQueryParams(['db' => 'test_db', 'table' => 'test_table']); - $response = new ResponseRenderer(); - (new GisVisualizationController($response, $template, $dbi, new DbTableExists($dbi)))($request); - self::assertSame($expected, $response->getHTMLResult()); + $responseRenderer = new ResponseRenderer(); + $controller = new GisVisualizationController( + $responseRenderer, + $template, + $dbi, + new DbTableExists($dbi), + ResponseFactory::create(), + ); + $response = $controller($request); + + self::assertNull($response); + self::assertSame($expected, $responseRenderer->getHTMLResult()); } } diff --git a/tests/unit/Controllers/Table/TrackingControllerTest.php b/tests/unit/Controllers/Table/TrackingControllerTest.php index 952edb783b..2231edf179 100644 --- a/tests/unit/Controllers/Table/TrackingControllerTest.php +++ b/tests/unit/Controllers/Table/TrackingControllerTest.php @@ -11,6 +11,7 @@ use PhpMyAdmin\Controllers\Table\TrackingController; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\DbTableExists; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\Factory\ServerRequestFactory; use PhpMyAdmin\SqlQueryForm; use PhpMyAdmin\Template; @@ -51,13 +52,13 @@ class TrackingControllerTest extends AbstractTestCase $request = ServerRequestFactory::create()->createServerRequest('GET', 'http://example.com/') ->withQueryParams(['db' => 'test_db', 'table' => 'test_table']); - $response = new ResponseRenderer(); + $responseRenderer = new ResponseRenderer(); $template = new Template(); $trackingChecker = self::createStub(TrackingChecker::class); $relation = new Relation($this->dbi); $bookmarkRepository = new BookmarkRepository($this->dbi, $relation); - (new TrackingController( - $response, + $response = (new TrackingController( + $responseRenderer, new Tracking( new SqlQueryForm($template, $this->dbi, $bookmarkRepository), $template, @@ -67,8 +68,11 @@ class TrackingControllerTest extends AbstractTestCase ), $trackingChecker, new DbTableExists($this->dbi), + ResponseFactory::create(), ))($request); + self::assertNull($response); + $main = $template->render('table/tracking/main', [ 'url_params' => [ 'db' => Current::$database, @@ -101,6 +105,6 @@ class TrackingControllerTest extends AbstractTestCase 'main' => $main, ]); - self::assertSame($expected, $response->getHTMLResult()); + self::assertSame($expected, $responseRenderer->getHTMLResult()); } } diff --git a/tests/unit/Controllers/VersionCheckControllerTest.php b/tests/unit/Controllers/VersionCheckControllerTest.php index 21d2f7d609..f0ea438c92 100644 --- a/tests/unit/Controllers/VersionCheckControllerTest.php +++ b/tests/unit/Controllers/VersionCheckControllerTest.php @@ -4,19 +4,18 @@ declare(strict_types=1); namespace PhpMyAdmin\Tests\Controllers; +use Fig\Http\Message\StatusCodeInterface; use PhpMyAdmin\Controllers\VersionCheckController; use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Http\Factory\ResponseFactory; use PhpMyAdmin\Http\ServerRequest; use PhpMyAdmin\Release; use PhpMyAdmin\Tests\AbstractTestCase; -use PhpMyAdmin\Tests\Stubs\ResponseRenderer; use PhpMyAdmin\VersionInformation; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\Attributes\RunTestsInSeparateProcesses; #[CoversClass(VersionCheckController::class)] -#[RunTestsInSeparateProcesses] -class VersionCheckControllerTest extends AbstractTestCase +final class VersionCheckControllerTest extends AbstractTestCase { protected function setUp(): void { @@ -27,7 +26,6 @@ class VersionCheckControllerTest extends AbstractTestCase public function testWithLatestCompatibleVersion(): void { - $_GET = []; $versionInfo = [ new Release('5.1.3', '2022-02-11', '>=7.1,<8.1', '>=5.5'), new Release('4.9.10', '2022-02-11', '>=5.5,<8.0', '>=5.5'), @@ -39,19 +37,18 @@ class VersionCheckControllerTest extends AbstractTestCase ->with(self::equalTo($versionInfo)) ->willReturn($versionInfo[0]); - (new VersionCheckController( - new ResponseRenderer(), + $response = (new VersionCheckController( $versionInformation, + ResponseFactory::create(), ))(self::createStub(ServerRequest::class)); - $output = $this->getActualOutputForAssertion(); - self::assertTrue(isset($_GET['ajax_request'])); - self::assertSame('{"version":"5.1.3","date":"2022-02-11"}', $output); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['application/json; charset=UTF-8'], $response->getHeader('Content-Type')); + self::assertSame('{"version":"5.1.3","date":"2022-02-11"}', (string) $response->getBody()); } public function testWithoutLatestCompatibleVersion(): void { - $_GET = []; $versionInfo = [ new Release('5.1.3', '2022-02-11', '>=7.1,<8.1', '>=5.5'), new Release('4.9.10', '2022-02-11', '>=5.5,<8.0', '>=5.5'), @@ -63,31 +60,29 @@ class VersionCheckControllerTest extends AbstractTestCase ->with(self::equalTo($versionInfo)) ->willReturn(null); - (new VersionCheckController( - new ResponseRenderer(), + $response = (new VersionCheckController( $versionInformation, + ResponseFactory::create(), ))(self::createStub(ServerRequest::class)); - $output = $this->getActualOutputForAssertion(); - self::assertTrue(isset($_GET['ajax_request'])); - self::assertSame('{"version":"","date":""}', $output); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['application/json; charset=UTF-8'], $response->getHeader('Content-Type')); + self::assertSame('{"version":"","date":""}', (string) $response->getBody()); } public function testWithoutLatestVersion(): void { - $_GET = []; - $versionInformation = $this->createMock(VersionInformation::class); $versionInformation->expects(self::once())->method('getLatestVersions')->willReturn(null); $versionInformation->expects(self::never())->method('getLatestCompatibleVersion'); - (new VersionCheckController( - new ResponseRenderer(), + $response = (new VersionCheckController( $versionInformation, + ResponseFactory::create(), ))(self::createStub(ServerRequest::class)); - $output = $this->getActualOutputForAssertion(); - self::assertTrue(isset($_GET['ajax_request'])); - self::assertSame('[]', $output); + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + self::assertSame(['application/json; charset=UTF-8'], $response->getHeader('Content-Type')); + self::assertSame('[]', (string) $response->getBody()); } } diff --git a/tests/unit/Stubs/ResponseRenderer.php b/tests/unit/Stubs/ResponseRenderer.php index e2ed1c687a..c4804bf15d 100644 --- a/tests/unit/Stubs/ResponseRenderer.php +++ b/tests/unit/Stubs/ResponseRenderer.php @@ -172,11 +172,6 @@ class ResponseRenderer extends \PhpMyAdmin\ResponseRenderer return $this->isAjax; } - public function isDisabled(): bool - { - return $this->isDisabled; - } - public function getResponse(): Response { return $this->response; 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()); } }