diff --git a/docs/faq.rst b/docs/faq.rst index 42a9ea2d3d50..4fbc5f25a9e8 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -2235,9 +2235,10 @@ logs. Currently there are two variables available: User name of currently active user (they do not have to be logged in). ``userStatus`` Status of currently active user, one of ``ok`` (user is logged in), - ``mysql-denied`` (MySQL denied user login), ``allow-denied`` (user denied + ``server-denied`` (database server denied user login), ``allow-denied`` (user denied by allow/deny rules), ``root-denied`` (root is denied in configuration), - ``empty-denied`` (empty password is denied). + ``empty-denied`` (empty password is denied), + ``no-activity`` (automatically logged out due to inactivity). ``LogFormat`` directive for Apache can look like following: diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 97b17b59f4cd..2456ccc0fd9c 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -7672,6 +7672,7 @@ + @@ -7761,6 +7762,9 @@ selectedServer]]> selectedServer, $singleSignonCfgUpdate)]]> + + + selectedServer['SignonURL'])]]> @@ -14310,6 +14314,7 @@ settings]]> settings]]> settings]]> + settings]]> diff --git a/src/Exceptions/AuthenticationFailure.php b/src/Exceptions/AuthenticationFailure.php new file mode 100644 index 000000000000..ab3b9f663408 --- /dev/null +++ b/src/Exceptions/AuthenticationFailure.php @@ -0,0 +1,78 @@ +authenticate(); + try { + $response = $authPlugin->authenticate(); + if ($response !== null) { + return $response; + } + } catch (AuthenticationFailure $exception) { + return $authPlugin->showFailure($exception); + } catch (Throwable $exception) { + $response = $this->responseFactory->createResponse(StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR); + + return $response->write($this->template->render('error/generic', [ + 'lang' => $GLOBALS['lang'] ?? 'en', + 'dir' => LanguageManager::$textDir, + 'error_message' => $exception->getMessage(), + ])); + } + $currentServer = new Server(Config::getInstance()->selectedServer); /* Enable LOAD DATA LOCAL INFILE for LDI plugin */ @@ -71,7 +88,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface // phpcs:enable } - $this->connectToDatabaseServer(DatabaseInterface::getInstance(), $authPlugin, $currentServer); + try { + $this->connectToDatabaseServer(DatabaseInterface::getInstance(), $currentServer); + } catch (AuthenticationFailure $exception) { + return $authPlugin->showFailure($exception); + } // Relation should only be initialized after the connection is successful /** @var Relation $relation */ @@ -81,9 +102,16 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface // Tracker can only be activated after the relation has been initialized Tracker::enable(); - $authPlugin->rememberCredentials(); + $response = $authPlugin->rememberCredentials(); + if ($response !== null) { + return $response; + } + assert($request instanceof ServerRequest); - $authPlugin->checkTwoFactor($request); + $response = $authPlugin->checkTwoFactor($request); + if ($response !== null) { + return $response; + } } catch (ExitException) { return ResponseRenderer::getInstance()->response(); } @@ -94,11 +122,9 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface return $handler->handle($request); } - private function connectToDatabaseServer( - DatabaseInterface $dbi, - AuthenticationPlugin $auth, - Server $currentServer, - ): void { + /** @throws AuthenticationFailure */ + private function connectToDatabaseServer(DatabaseInterface $dbi, Server $currentServer): void + { /** * Try to connect MySQL with the control user profile (will be used to get the privileges list for the current * user but the true user link must be open after this one, so it would be default one for all the scripts). @@ -111,7 +137,7 @@ private function connectToDatabaseServer( // Connects to the server (validates user's login) $userConnection = $dbi->connect($currentServer, ConnectionType::User); if ($userConnection === null) { - $auth->showFailure('mysql-denied'); + throw AuthenticationFailure::deniedByDatabaseServer(); } if ($controlConnection !== null) { diff --git a/src/Plugins/Auth/AuthenticationConfig.php b/src/Plugins/Auth/AuthenticationConfig.php index 673ad930eef2..8bb09392ccdc 100644 --- a/src/Plugins/Auth/AuthenticationConfig.php +++ b/src/Plugins/Auth/AuthenticationConfig.php @@ -10,7 +10,9 @@ use PhpMyAdmin\Config; use PhpMyAdmin\DatabaseInterface; use PhpMyAdmin\Error\ErrorHandler; +use PhpMyAdmin\Exceptions\AuthenticationFailure; use PhpMyAdmin\Html\Generator; +use PhpMyAdmin\Http\Response; use PhpMyAdmin\Plugins\AuthenticationPlugin; use PhpMyAdmin\ResponseRenderer; use PhpMyAdmin\Server\Select; @@ -18,6 +20,8 @@ use function __; use function count; +use function ob_get_clean; +use function ob_start; use function sprintf; use function trigger_error; @@ -32,17 +36,18 @@ class AuthenticationConfig extends AuthenticationPlugin /** * Displays authentication form */ - public function showLoginForm(): void + public function showLoginForm(): Response|null { - $response = ResponseRenderer::getInstance(); - if (! $response->isAjax()) { - return; + $responseRenderer = ResponseRenderer::getInstance(); + if (! $responseRenderer->isAjax()) { + return null; } - $response->setRequestStatus(false); + $responseRenderer->setRequestStatus(false); // reload_flag removes the token parameter from the URL and reloads - $response->addJSON('reload_flag', '1'); - $response->callExit(); + $responseRenderer->addJSON('reload_flag', '1'); + + return $responseRenderer->response(); } /** @@ -65,12 +70,10 @@ public function readCredentials(): bool /** * User is not allowed to login to MySQL -> authentication failed - * - * @param string $failure String describing why authentication has failed */ - public function showFailure(string $failure): never + public function showFailure(AuthenticationFailure $failure): Response { - parent::showFailure($failure); + $this->logFailure($failure); $connError = DatabaseInterface::getInstance()->getError(); if ($connError === '' || $connError === '0') { @@ -78,12 +81,14 @@ public function showFailure(string $failure): never } /* HTML header */ - $response = ResponseRenderer::getInstance(); - $response->setMinimalFooter(); - $header = $response->getHeader(); + $responseRenderer = ResponseRenderer::getInstance(); + $responseRenderer->setMinimalFooter(); + $header = $responseRenderer->getHeader(); $header->setBodyId('loginform'); $header->setTitle(__('Access denied!')); $header->disableMenuAndConsole(); + + ob_start(); echo '

'; @@ -95,8 +100,8 @@ public function showFailure(string $failure): never '; $config = Config::getInstance(); - if ($failure === 'allow-denied') { - trigger_error(__('Access denied!'), E_USER_NOTICE); + if ($failure->failureType === AuthenticationFailure::ALLOW_DENIED) { + trigger_error($failure->getMessage(), E_USER_NOTICE); } else { // Check whether user has configured something if ($config->sourceMtime == 0) { @@ -158,6 +163,9 @@ public function showFailure(string $failure): never } echo '' , "\n"; - $response->callExit(); + + $responseRenderer->addHTML((string) ob_get_clean()); + + return $responseRenderer->response(); } } diff --git a/src/Plugins/Auth/AuthenticationCookie.php b/src/Plugins/Auth/AuthenticationCookie.php index 5756237b6ddf..85cc6be8e021 100644 --- a/src/Plugins/Auth/AuthenticationCookie.php +++ b/src/Plugins/Auth/AuthenticationCookie.php @@ -11,14 +11,15 @@ use PhpMyAdmin\Core; use PhpMyAdmin\Current; use PhpMyAdmin\Error\ErrorHandler; +use PhpMyAdmin\Exceptions\AuthenticationFailure; use PhpMyAdmin\Exceptions\SessionHandlerException; +use PhpMyAdmin\Http\Response; use PhpMyAdmin\LanguageManager; use PhpMyAdmin\Message; use PhpMyAdmin\Plugins\AuthenticationPlugin; use PhpMyAdmin\ResponseRenderer; use PhpMyAdmin\Server\Select; use PhpMyAdmin\Session; -use PhpMyAdmin\Template; use PhpMyAdmin\Url; use PhpMyAdmin\Util; use PhpMyAdmin\Utils\SessionCache; @@ -63,11 +64,11 @@ class AuthenticationCookie extends AuthenticationPlugin * * @global string $conn_error the last connection error */ - public function showLoginForm(): never + public function showLoginForm(): Response { $GLOBALS['conn_error'] ??= null; - $response = ResponseRenderer::getInstance(); + $responseRenderer = ResponseRenderer::getInstance(); /** * When sending login modal after session has expired, send the @@ -75,8 +76,8 @@ public function showLoginForm(): never * in all the forms having a hidden token. */ $sessionExpired = isset($_REQUEST['check_timeout']) || isset($_REQUEST['session_timedout']); - if (! $sessionExpired && $response->loginPage()) { - $response->callExit(); + if (! $sessionExpired && $responseRenderer->loginPage()) { + return $responseRenderer->response(); } /** @@ -85,8 +86,8 @@ public function showLoginForm(): never * in all the forms having a hidden token. */ if ($sessionExpired) { - $response->setRequestStatus(false); - $response->addJSON('new_token', $_SESSION[' PMA_token ']); + $responseRenderer->setRequestStatus(false); + $responseRenderer->addJSON('new_token', $_SESSION[' PMA_token ']); } /** @@ -94,7 +95,7 @@ public function showLoginForm(): never * using the modal was successful after session expiration. */ if (isset($_REQUEST['session_timedout'])) { - $response->addJSON('logged_in', 0); + $responseRenderer->addJSON('logged_in', 0); } $config = Config::getInstance(); @@ -159,7 +160,7 @@ public function showLoginForm(): never $configFooter = Config::renderFooter(); - $response->addHTML($this->template->render('login/form', [ + $responseRenderer->addHTML($this->template->render('login/form', [ 'login_header' => $loginHeader, 'is_demo' => $config->config->debug->demo, 'error_messages' => $errorMessages, @@ -190,7 +191,7 @@ public function showLoginForm(): never 'config_footer' => $configFooter, ])); - $response->callExit(); + return $responseRenderer->response(); } /** @@ -206,6 +207,9 @@ public function showLoginForm(): never * it returns true if all seems ok which usually leads to auth_set_user() * * it directly switches to showFailure() if user inactivity timeout is reached + * + * @throws AuthenticationFailure + * @throws SessionHandlerException */ public function readCredentials(): bool { @@ -309,19 +313,8 @@ public function readCredentials(): bool $GLOBALS['pma_auth_server'] = Core::sanitizeMySQLHost($_REQUEST['pma_servername']); } - try { - /* Secure current session on login to avoid session fixation */ - Session::secure(); - } catch (SessionHandlerException $exception) { - $responseRenderer = ResponseRenderer::getInstance(); - $responseRenderer->addHTML((new Template())->render('error/generic', [ - 'lang' => $GLOBALS['lang'] ?? 'en', - 'dir' => LanguageManager::$textDir, - 'error_message' => $exception->getMessage(), - ])); - - $responseRenderer->callExit(); - } + /* Secure current session on login to avoid session fixation */ + Session::secure(); return true; } @@ -371,7 +364,7 @@ public function readCredentials(): bool SessionCache::remove('table_priv'); SessionCache::remove('proc_priv'); - $this->showFailure('no-activity'); + throw AuthenticationFailure::loggedOutDueToInactivity(); } // check password cookie @@ -440,7 +433,7 @@ public function storeCredentials(): bool /** * Stores user credentials after successful login. */ - public function rememberCredentials(): void + public function rememberCredentials(): Response|null { // Name and password cookies need to be refreshed each time // Duration = one month for username @@ -465,18 +458,18 @@ public function rememberCredentials(): void // user logged in successfully after session expiration if (isset($_REQUEST['session_timedout'])) { - $response = ResponseRenderer::getInstance(); - $response->addJSON('logged_in', 1); - $response->addJSON('success', 1); - $response->addJSON('new_token', $_SESSION[' PMA_token ']); + $responseRenderer = ResponseRenderer::getInstance(); + $responseRenderer->addJSON('logged_in', 1); + $responseRenderer->addJSON('success', 1); + $responseRenderer->addJSON('new_token', $_SESSION[' PMA_token ']); - $response->callExit(); + return $responseRenderer->response(); } // Set server cookies if required (once per session) and, in this case, // force reload to ensure the client accepts cookies if ($GLOBALS['from_cookie']) { - return; + return null; } /** @@ -484,11 +477,11 @@ public function rememberCredentials(): void */ Util::clearUserCache(); - $response = ResponseRenderer::getInstance(); - $response->disable(); - $response->redirect('./index.php?route=/' . Url::getCommonRaw($urlParams, '&')); + $responseRenderer = ResponseRenderer::getInstance(); + $responseRenderer->disable(); + $responseRenderer->redirect('./index.php?route=/' . Url::getCommonRaw($urlParams, '&')); - $response->callExit(); + return $responseRenderer->response(); } /** @@ -539,27 +532,23 @@ public function storePasswordCookie(string $password): void * * prepares error message and switches to showLoginForm() which display the error * and the login form - * - * @param string $failure String describing why authentication has failed */ - public function showFailure(string $failure): never + public function showFailure(AuthenticationFailure $failure): Response { - $GLOBALS['conn_error'] ??= null; - - parent::showFailure($failure); + $this->logFailure($failure); // Deletes password cookie and displays the login form Config::getInstance()->removeCookie('pmaAuth-' . Current::$server); $GLOBALS['conn_error'] = $this->getErrorMessage($failure); - $response = ResponseRenderer::getInstance(); + $responseRenderer = ResponseRenderer::getInstance(); // needed for PHP-CGI (not need for FastCGI or mod-php) - $response->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); - $response->addHeader('Pragma', 'no-cache'); + $responseRenderer->addHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); + $responseRenderer->addHeader('Pragma', 'no-cache'); - $this->showLoginForm(); + return $this->showLoginForm(); } /** diff --git a/src/Plugins/Auth/AuthenticationHttp.php b/src/Plugins/Auth/AuthenticationHttp.php index 9707ffe426d4..5fe1e95c9beb 100644 --- a/src/Plugins/Auth/AuthenticationHttp.php +++ b/src/Plugins/Auth/AuthenticationHttp.php @@ -12,6 +12,8 @@ use PhpMyAdmin\Config; use PhpMyAdmin\Core; use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Exceptions\AuthenticationFailure; +use PhpMyAdmin\Http\Response; use PhpMyAdmin\LanguageManager; use PhpMyAdmin\Message; use PhpMyAdmin\Plugins\AuthenticationPlugin; @@ -34,23 +36,24 @@ class AuthenticationHttp extends AuthenticationPlugin /** * Displays authentication form and redirect as necessary */ - public function showLoginForm(): never + public function showLoginForm(): Response { - $response = ResponseRenderer::getInstance(); - if ($response->isAjax()) { - $response->setRequestStatus(false); + $responseRenderer = ResponseRenderer::getInstance(); + if ($responseRenderer->isAjax()) { + $responseRenderer->setRequestStatus(false); // reload_flag removes the token parameter from the URL and reloads - $response->addJSON('reload_flag', '1'); - $response->callExit(); + $responseRenderer->addJSON('reload_flag', '1'); + + return $responseRenderer->response(); } - $this->authForm(); + return $this->authForm(); } /** * Displays authentication form */ - public function authForm(): never + public function authForm(): Response { $config = Config::getInstance(); if (empty($config->selectedServer['auth_http_realm'])) { @@ -92,7 +95,7 @@ public function authForm(): never $response->addHTML(Config::renderFooter()); - $response->callExit(); + return $response->response(); } /** @@ -178,25 +181,24 @@ public function readCredentials(): bool /** * User is not allowed to login to MySQL -> authentication failed - * - * @param string $failure String describing why authentication has failed */ - public function showFailure(string $failure): never + public function showFailure(AuthenticationFailure $failure): Response { - parent::showFailure($failure); + $this->logFailure($failure); $error = DatabaseInterface::getInstance()->getError(); if ($error && $GLOBALS['errno'] != 1045) { - echo $this->template->render('error/generic', [ + $responseRenderer = ResponseRenderer::getInstance(); + $responseRenderer->addHTML($this->template->render('error/generic', [ 'lang' => $GLOBALS['lang'] ?? 'en', 'dir' => LanguageManager::$textDir, 'error_message' => $error, - ]); + ])); - ResponseRenderer::getInstance()->callExit(); + return $responseRenderer->response(); } - $this->authForm(); + return $this->authForm(); } /** diff --git a/src/Plugins/Auth/AuthenticationSignon.php b/src/Plugins/Auth/AuthenticationSignon.php index 9e069442018e..9beacf6f6959 100644 --- a/src/Plugins/Auth/AuthenticationSignon.php +++ b/src/Plugins/Auth/AuthenticationSignon.php @@ -8,10 +8,13 @@ namespace PhpMyAdmin\Plugins\Auth; use PhpMyAdmin\Config; +use PhpMyAdmin\Exceptions\AuthenticationFailure; +use PhpMyAdmin\Http\Response; use PhpMyAdmin\LanguageManager; use PhpMyAdmin\Plugins\AuthenticationPlugin; use PhpMyAdmin\ResponseRenderer; use PhpMyAdmin\Util; +use RuntimeException; use function __; use function array_merge; @@ -24,6 +27,7 @@ use function session_set_cookie_params; use function session_start; use function session_write_close; +use function sprintf; /** * Handles the SignOn authentication method @@ -33,25 +37,25 @@ class AuthenticationSignon extends AuthenticationPlugin /** * Displays authentication form */ - public function showLoginForm(): never + public function showLoginForm(): Response { - $response = ResponseRenderer::getInstance(); - $response->disable(); + $responseRenderer = ResponseRenderer::getInstance(); + $responseRenderer->disable(); unset($_SESSION['LAST_SIGNON_URL']); $config = Config::getInstance(); if (empty($config->selectedServer['SignonURL'])) { - echo $this->template->render('error/generic', [ + $responseRenderer->addHTML($this->template->render('error/generic', [ 'lang' => $GLOBALS['lang'] ?? 'en', 'dir' => LanguageManager::$textDir, 'error_message' => 'You must set SignonURL!', - ]); + ])); - $response->callExit(); - } else { - $response->redirect($config->selectedServer['SignonURL']); + return $responseRenderer->response(); } - $response->callExit(); + $responseRenderer->redirect($config->selectedServer['SignonURL']); + + return $responseRenderer->response(); } /** @@ -90,6 +94,8 @@ public function setCookieParams(array|null $sessionCookieParams = null): void /** * Gets authentication credentials + * + * @throws RuntimeException */ public function readCredentials(): bool { @@ -118,13 +124,10 @@ public function readCredentials(): bool /* Handle script based auth */ if ($scriptName !== '') { if (! @file_exists($scriptName)) { - echo $this->template->render('error/generic', [ - 'lang' => $GLOBALS['lang'] ?? 'en', - 'dir' => LanguageManager::$textDir, - 'error_message' => __('Can not find signon authentication script:') . ' ' . $scriptName, - ]); - - ResponseRenderer::getInstance()->callExit(); + throw new RuntimeException(sprintf( + __('Can not find signon authentication script: %s'), + '$cfg[\'Servers\'][$i][\'SignonScript\']', + )); } include $scriptName; @@ -233,12 +236,10 @@ public function readCredentials(): bool /** * User is not allowed to login to MySQL -> authentication failed - * - * @param string $failure String describing why authentication has failed */ - public function showFailure(string $failure): never + public function showFailure(AuthenticationFailure $failure): Response { - parent::showFailure($failure); + $this->logFailure($failure); /* Session name */ $sessionName = Config::getInstance()->selectedServer['SignonSession']; @@ -260,7 +261,7 @@ public function showFailure(string $failure): never $_SESSION['PMA_single_signon_error_message'] = $this->getErrorMessage($failure); } - $this->showLoginForm(); + return $this->showLoginForm(); } /** diff --git a/src/Plugins/AuthenticationPlugin.php b/src/Plugins/AuthenticationPlugin.php index 09bde9a24dd8..ceeccbdcdfc9 100644 --- a/src/Plugins/AuthenticationPlugin.php +++ b/src/Plugins/AuthenticationPlugin.php @@ -1,19 +1,16 @@ authentication failed - * - * @param string $failure String describing why authentication has failed */ - public function showFailure(string $failure): void + abstract public function showFailure(AuthenticationFailure $failure): Response; + + protected function logFailure(AuthenticationFailure $failure): void { - Logging::logUser(Config::getInstance(), $this->user, $failure); + Logging::logUser(Config::getInstance(), $this->user, $failure->failureType); } /** @@ -157,38 +157,25 @@ public function getLoginFormURL(): string /** * Returns error message for failed authentication. - * - * @param string $failure String describing why authentication has failed */ - public function getErrorMessage(string $failure): string + public function getErrorMessage(AuthenticationFailure $failure): string { - if ($failure === 'empty-denied') { - return __('Login without a password is forbidden by configuration (see AllowNoPassword)'); + if ($failure->failureType === AuthenticationFailure::NO_ACTIVITY) { + return sprintf($failure->getMessage(), (int) Config::getInstance()->settings['LoginCookieValidity']); } - if ($failure === 'root-denied' || $failure === 'allow-denied') { - return __('Access denied!'); - } - - if ($failure === 'no-activity') { - return sprintf( - __('You have been automatically logged out due to inactivity of %s seconds.' - . ' Once you log in again, you should be able to resume the work where you left off.'), - (int) Config::getInstance()->settings['LoginCookieValidity'], - ); - } - - $dbiError = DatabaseInterface::getInstance()->getError(); - if ($dbiError !== '') { - return htmlspecialchars($dbiError); - } + if ($failure->failureType === AuthenticationFailure::SERVER_DENIED) { + $dbiError = DatabaseInterface::getInstance()->getError(); + if ($dbiError !== '') { + return htmlspecialchars($dbiError); + } - if (isset($GLOBALS['errno'])) { - return '#' . $GLOBALS['errno'] . ' ' - . __('Cannot log in to the MySQL server'); + if (isset($GLOBALS['errno'])) { + return '#' . $GLOBALS['errno'] . ' ' . $failure->getMessage(); + } } - return __('Cannot log in to the MySQL server'); + return $failure->getMessage(); } /** @@ -235,28 +222,23 @@ public function setSessionAccessTime(): void * High level authentication interface * * Gets the credentials or shows login form if necessary + * + * @throws AuthenticationFailure + * @throws Exception */ - public function authenticate(): void + public function authenticate(): Response|null { $success = $this->readCredentials(); /* Show login form (this exits) */ if (! $success) { /* Force generating of new session */ - try { - Session::secure(); - } catch (SessionHandlerException $exception) { - $responseRenderer = ResponseRenderer::getInstance(); - $responseRenderer->addHTML((new Template())->render('error/generic', [ - 'lang' => $GLOBALS['lang'] ?? 'en', - 'dir' => LanguageManager::$textDir, - 'error_message' => $exception->getMessage(), - ])); - - $responseRenderer->callExit(); - } + Session::secure(); - $this->showLoginForm(); + $response = $this->showLoginForm(); + if ($response !== null) { + return $response; + } } /* Store credentials (eg. in cookies) */ @@ -265,10 +247,14 @@ public function authenticate(): void $this->checkRules(); /* clear user cache */ Util::clearUserCache(); + + return null; } /** * Check configuration defined restrictions for authentication + * + * @throws AuthenticationFailure */ public function checkRules(): void { @@ -287,13 +273,13 @@ public function checkRules(): void // Ejects the user if banished if ($allowDenyForbidden) { - $this->showFailure('allow-denied'); + throw AuthenticationFailure::deniedByAllowDenyRules(); } } // is root allowed? if (! $config->selectedServer['AllowRoot'] && $config->selectedServer['user'] === 'root') { - $this->showFailure('root-denied'); + throw AuthenticationFailure::rootDeniedByConfiguration(); } // is a login without password allowed? @@ -301,39 +287,37 @@ public function checkRules(): void return; } - $this->showFailure('empty-denied'); + throw AuthenticationFailure::emptyPasswordDeniedByConfiguration(); } /** - * Checks whether two factor authentication is active - * for given user and performs it. - * - * @throws ExitException + * Checks whether two-factor authentication is active for given user and performs it. */ - public function checkTwoFactor(ServerRequest $request): void + public function checkTwoFactor(ServerRequest $request): Response|null { $twofactor = new TwoFactor($this->user); /* Do we need to show the form? */ if ($twofactor->check($request)) { - return; + return null; } - $response = ResponseRenderer::getInstance(); - if ($response->loginPage()) { - $response->callExit(); + $responseRenderer = ResponseRenderer::getInstance(); + if ($responseRenderer->loginPage()) { + return $responseRenderer->response(); } - $response->addHTML($this->template->render('login/header', ['session_expired' => false])); - $response->addHTML(Message::rawNotice( + $responseRenderer->addHTML($this->template->render('login/header', ['session_expired' => false])); + $responseRenderer->addHTML(Message::rawNotice( __('You have enabled two factor authentication, please confirm your login.'), )->getDisplay()); - $response->addHTML($this->template->render('login/twofactor', [ + $responseRenderer->addHTML($this->template->render('login/twofactor', [ 'form' => $twofactor->render($request), 'show_submit' => $twofactor->showSubmit(), ])); - $response->addHTML($this->template->render('login/footer')); - $response->addHTML(Config::renderFooter()); - $response->callExit(); + $responseRenderer->addHTML($this->template->render('login/footer')); + $responseRenderer->addHTML(Config::renderFooter()); + + return $responseRenderer->response(); } } diff --git a/tests/unit/Exceptions/AuthenticationFailureTest.php b/tests/unit/Exceptions/AuthenticationFailureTest.php new file mode 100644 index 000000000000..2743787552f0 --- /dev/null +++ b/tests/unit/Exceptions/AuthenticationFailureTest.php @@ -0,0 +1,55 @@ +failureType); + self::assertSame('Access denied!', $exception->getMessage()); + } + + public function testEmptyDenied(): void + { + $exception = AuthenticationFailure::emptyPasswordDeniedByConfiguration(); + self::assertSame('empty-denied', $exception->failureType); + self::assertSame( + 'Login without a password is forbidden by configuration (see AllowNoPassword).', + $exception->getMessage(), + ); + } + + public function testNoActivity(): void + { + $exception = AuthenticationFailure::loggedOutDueToInactivity(); + self::assertSame('no-activity', $exception->failureType); + self::assertSame( + 'You have been automatically logged out due to inactivity of %s seconds.' + . ' Once you log in again, you should be able to resume the work where you left off.', + $exception->getMessage(), + ); + } + + public function testRootDenied(): void + { + $exception = AuthenticationFailure::rootDeniedByConfiguration(); + self::assertSame('root-denied', $exception->failureType); + self::assertSame('Access denied!', $exception->getMessage()); + } + + public function testServerDenied(): void + { + $exception = AuthenticationFailure::deniedByDatabaseServer(); + self::assertSame('server-denied', $exception->failureType); + self::assertSame('Cannot log in to the database server.', $exception->getMessage()); + } +} diff --git a/tests/unit/Plugins/Auth/AuthenticationConfigTest.php b/tests/unit/Plugins/Auth/AuthenticationConfigTest.php index 9548c614e371..0c04236868be 100644 --- a/tests/unit/Plugins/Auth/AuthenticationConfigTest.php +++ b/tests/unit/Plugins/Auth/AuthenticationConfigTest.php @@ -7,17 +7,15 @@ use PhpMyAdmin\Config; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; -use PhpMyAdmin\Exceptions\ExitException; +use PhpMyAdmin\Exceptions\AuthenticationFailure; use PhpMyAdmin\Plugins\Auth\AuthenticationConfig; use PhpMyAdmin\ResponseRenderer; use PhpMyAdmin\Tests\AbstractTestCase; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Medium; use ReflectionProperty; -use Throwable; -use function ob_get_clean; -use function ob_start; +use function json_decode; #[CoversClass(AuthenticationConfig::class)] #[Medium] @@ -55,12 +53,25 @@ protected function tearDown(): void unset($this->object); } - public function testAuth(): void + public function testShowLoginFormWithoutAjax(): void + { + (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); + ResponseRenderer::getInstance()->setAjax(false); + self::assertNull($this->object->showLoginForm()); + } + + public function testShowLoginFormWithAjax(): void { (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); ResponseRenderer::getInstance()->setAjax(true); - $this->expectException(ExitException::class); - $this->object->showLoginForm(); + $response = $this->object->showLoginForm(); + self::assertNotNull($response); + $body = (string) $response->getBody(); + self::assertJson($body); + $json = json_decode($body, true); + self::assertIsArray($json); + self::assertArrayHasKey('reload_flag', $json); + self::assertSame('1', $json['reload_flag']); } public function testAuthCheck(): void @@ -89,17 +100,9 @@ public function testAuthFails(): void (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); - ob_start(); - try { - $this->object->showFailure(''); - } catch (Throwable $throwable) { - } - - $html = ob_get_clean(); - - self::assertInstanceOf(ExitException::class, $throwable); + $response = $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer()); - self::assertIsString($html); + $html = (string) $response->getBody(); self::assertStringContainsString( 'You probably did not create a configuration file. You might want ' . diff --git a/tests/unit/Plugins/Auth/AuthenticationCookieTest.php b/tests/unit/Plugins/Auth/AuthenticationCookieTest.php index dcab3642bdec..593c5a32e771 100644 --- a/tests/unit/Plugins/Auth/AuthenticationCookieTest.php +++ b/tests/unit/Plugins/Auth/AuthenticationCookieTest.php @@ -4,10 +4,11 @@ namespace PhpMyAdmin\Tests\Plugins\Auth; +use Fig\Http\Message\StatusCodeInterface; use PhpMyAdmin\Config; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; -use PhpMyAdmin\Error\ErrorHandler; +use PhpMyAdmin\Exceptions\AuthenticationFailure; use PhpMyAdmin\Exceptions\ExitException; use PhpMyAdmin\Plugins\Auth\AuthenticationCookie; use PhpMyAdmin\ResponseRenderer; @@ -24,6 +25,7 @@ use function base64_decode; use function base64_encode; use function is_readable; +use function json_decode; use function json_encode; use function mb_strlen; use function ob_get_clean; @@ -76,37 +78,21 @@ public function testAuthErrorAJAX(): void { $GLOBALS['conn_error'] = true; - $responseStub = new ResponseRendererStub(); - $responseStub->setAjax(true); - (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); - - try { - $this->object->showLoginForm(); - } catch (Throwable $throwable) { - } - - self::assertInstanceOf(ExitException::class, $throwable); - $response = $responseStub->getResponse(); - self::assertSame(200, $response->getStatusCode()); - self::assertFalse($responseStub->hasSuccessState()); - self::assertSame(['redirect_flag' => '1'], $responseStub->getJSONResult()); - } - - private function getAuthErrorMockResponse(): void - { - // mock error handler - - $mockErrorHandler = $this->getMockBuilder(ErrorHandler::class) - ->disableOriginalConstructor() - ->onlyMethods(['hasDisplayErrors']) - ->getMock(); - - $mockErrorHandler->expects(self::once()) - ->method('hasDisplayErrors') - ->with() - ->willReturn(true); - - ErrorHandler::$instance = $mockErrorHandler; + (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); + $responseRenderer = ResponseRenderer::getInstance(); + $responseRenderer->setAjax(true); + + $response = $this->object->showLoginForm(); + + self::assertSame(StatusCodeInterface::STATUS_OK, $response->getStatusCode()); + $body = (string) $response->getBody(); + self::assertJson($body); + $json = json_decode($body, true); + self::assertIsArray($json); + self::assertArrayHasKey('success', $json); + self::assertFalse($json['success']); + self::assertArrayHasKey('redirect_flag', $json); + self::assertSame('1', $json['redirect_flag']); } public function testAuthError(): void @@ -132,17 +118,11 @@ public function testAuthError(): void Current::$table = 'testTable'; $config->settings['Servers'] = [1, 2]; - $responseStub = new ResponseRendererStub(); - (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); - - try { - $this->object->showLoginForm(); - } catch (Throwable $throwable) { - } + (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); - $result = $responseStub->getHTMLResult(); + $response = $this->object->showLoginForm(); - self::assertInstanceOf(ExitException::class, $throwable); + $result = (string) $response->getBody(); self::assertStringContainsString(' id="imLogo"', $result); @@ -202,14 +182,9 @@ public function testAuthCaptcha(): void $responseStub = new ResponseRendererStub(); (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); - try { - $this->object->showLoginForm(); - } catch (Throwable $throwable) { - } - - $result = $responseStub->getHTMLResult(); + $response = $this->object->showLoginForm(); - self::assertInstanceOf(ExitException::class, $throwable); + $result = (string) $response->getBody(); self::assertStringContainsString('id="imLogo"', $result); @@ -263,14 +238,9 @@ public function testAuthCaptchaCheckbox(): void $responseStub = new ResponseRendererStub(); (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); - try { - $this->object->showLoginForm(); - } catch (Throwable $throwable) { - } - - $result = $responseStub->getHTMLResult(); + $response = $this->object->showLoginForm(); - self::assertInstanceOf(ExitException::class, $throwable); + $result = (string) $response->getBody(); self::assertStringContainsString('id="imLogo"', $result); @@ -569,18 +539,14 @@ public function testAuthCheckAuthFails(): void // mock for blowfish function $this->object = $this->getMockBuilder(AuthenticationCookie::class) ->disableOriginalConstructor() - ->onlyMethods(['showFailure', 'cookieDecrypt']) + ->onlyMethods(['cookieDecrypt']) ->getMock(); $this->object->expects(self::once()) ->method('cookieDecrypt') ->willReturn('testBF'); - $this->object->expects(self::once()) - ->method('showFailure') - ->willThrowException(new ExitException()); - - $this->expectException(ExitException::class); + $this->expectExceptionObject(AuthenticationFailure::loggedOutDueToInactivity()); $this->object->readCredentials(); } @@ -626,6 +592,7 @@ public function testAuthSetUserWithHeaders(): void $config->selectedServer['user'] = 'pmaUser'; $config->settings['Servers'][1] = $arr; $config->settings['AllowArbitraryServer'] = true; + $config->settings['PmaAbsoluteUri'] = 'http://localhost/phpmyadmin'; $GLOBALS['pma_auth_server'] = 'b 2'; $this->object->password = 'testPW'; $config->settings['LoginCookieStore'] = 100; @@ -635,8 +602,13 @@ public function testAuthSetUserWithHeaders(): void (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); $this->object->storeCredentials(); - $this->expectException(ExitException::class); - $this->object->rememberCredentials(); + $response = $this->object->rememberCredentials(); + self::assertNotNull($response); + self::assertSame(StatusCodeInterface::STATUS_FOUND, $response->getStatusCode()); + self::assertStringEndsWith( + '/phpmyadmin/index.php?route=/&db=db&table=table&lang=en', + $response->getHeaderLine('Location'), + ); } public function testAuthFailsNoPass(): void @@ -656,11 +628,11 @@ public function testAuthFailsNoPass(): void (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); try { - $this->object->showFailure('empty-denied'); + $this->object->showFailure(AuthenticationFailure::emptyPasswordDeniedByConfiguration()); } catch (Throwable $throwable) { } - self::assertInstanceOf(ExitException::class, $throwable); + self::assertInstanceOf(ExitException::class, $throwable ?? null); $response = $responseStub->getResponse(); self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control')); self::assertSame(['no-cache'], $response->getHeader('Pragma')); @@ -668,7 +640,7 @@ public function testAuthFailsNoPass(): void self::assertSame( $GLOBALS['conn_error'], - 'Login without a password is forbidden by configuration (see AllowNoPassword)', + 'Login without a password is forbidden by configuration (see AllowNoPassword).', ); } @@ -724,11 +696,11 @@ public function testAuthFailsDeny(): void (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); try { - $this->object->showFailure('allow-denied'); + $this->object->showFailure(AuthenticationFailure::deniedByAllowDenyRules()); } catch (Throwable $throwable) { } - self::assertInstanceOf(ExitException::class, $throwable); + self::assertInstanceOf(ExitException::class, $throwable ?? null); $response = $responseStub->getResponse(); self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control')); self::assertSame(['no-cache'], $response->getHeader('Pragma')); @@ -756,11 +728,11 @@ public function testAuthFailsActivity(): void (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); try { - $this->object->showFailure('no-activity'); + $this->object->showFailure(AuthenticationFailure::loggedOutDueToInactivity()); } catch (Throwable $throwable) { } - self::assertInstanceOf(ExitException::class, $throwable); + self::assertInstanceOf(ExitException::class, $throwable ?? null); $response = $responseStub->getResponse(); self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control')); self::assertSame(['no-cache'], $response->getHeader('Pragma')); @@ -801,17 +773,17 @@ public function testAuthFailsDBI(): void (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); try { - $this->object->showFailure(''); + $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer()); } catch (Throwable $throwable) { } - self::assertInstanceOf(ExitException::class, $throwable); + self::assertInstanceOf(ExitException::class, $throwable ?? null); $response = $responseStub->getResponse(); self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control')); self::assertSame(['no-cache'], $response->getHeader('Pragma')); self::assertSame(200, $response->getStatusCode()); - self::assertSame($GLOBALS['conn_error'], '#42 Cannot log in to the MySQL server'); + self::assertSame($GLOBALS['conn_error'], '#42 Cannot log in to the database server.'); } public function testAuthFailsErrno(): void @@ -842,17 +814,17 @@ public function testAuthFailsErrno(): void (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); try { - $this->object->showFailure(''); + $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer()); } catch (Throwable $throwable) { } - self::assertInstanceOf(ExitException::class, $throwable); + self::assertInstanceOf(ExitException::class, $throwable ?? null); $response = $responseStub->getResponse(); self::assertSame(['no-store, no-cache, must-revalidate'], $response->getHeader('Cache-Control')); self::assertSame(['no-cache'], $response->getHeader('Pragma')); self::assertSame(200, $response->getStatusCode()); - self::assertSame($GLOBALS['conn_error'], 'Cannot log in to the MySQL server'); + self::assertSame($GLOBALS['conn_error'], 'Cannot log in to the database server.'); } public function testGetEncryptionSecretEmpty(): void @@ -954,9 +926,10 @@ public function testAuthenticate(): void $_POST['pma_password'] = 'testPassword'; ob_start(); - $this->object->authenticate(); + $response = $this->object->authenticate(); $result = ob_get_clean(); + self::assertNull($response); /* Nothing should be printed */ self::assertSame('', $result); @@ -999,31 +972,20 @@ public function testCheckRules( $config->selectedServer['AllowNoPassword'] = $nopass; $config->selectedServer['AllowDeny'] = $rules; - if ($expected !== '') { - $this->getAuthErrorMockResponse(); - } - - $responseStub = new ResponseRendererStub(); - (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); - + $exception = null; try { $this->object->checkRules(); - } catch (Throwable $throwable) { - } - - $result = $responseStub->getHTMLResult(); - - if ($expected !== '') { - self::assertInstanceOf(ExitException::class, $throwable ?? null); + } catch (AuthenticationFailure $exception) { } if ($expected === '') { - self::assertSame($expected, $result); - } else { - self::assertStringContainsString($expected, $result); + self::assertNull($exception, 'checkRules() should not throw an exception.'); + + return; } - ErrorHandler::$instance = null; + self::assertInstanceOf(AuthenticationFailure::class, $exception); + self::assertSame($expected, $exception->failureType); } /** @return mixed[] */ @@ -1031,9 +993,9 @@ public static function checkRulesProvider(): array { return [ 'nopass-ok' => ['testUser', '', '1.2.3.4', true, true, [], ''], - 'nopass' => ['testUser', '', '1.2.3.4', true, false, [], 'Login without a password is forbidden'], + 'nopass' => ['testUser', '', '1.2.3.4', true, false, [], AuthenticationFailure::EMPTY_DENIED], 'root-ok' => ['root', 'root', '1.2.3.4', true, true, [], ''], - 'root' => ['root', 'root', '1.2.3.4', false, true, [], 'Access denied!'], + 'root' => ['root', 'root', '1.2.3.4', false, true, [], AuthenticationFailure::ROOT_DENIED], 'rules-deny-allow-ok' => [ 'root', 'root', @@ -1050,7 +1012,7 @@ public static function checkRulesProvider(): array true, true, ['order' => 'deny,allow', 'rules' => ['allow root 1.2.3.4', 'deny % from all']], - 'Access denied!', + AuthenticationFailure::ALLOW_DENIED, ], 'rules-allow-deny-ok' => [ 'root', @@ -1068,7 +1030,7 @@ public static function checkRulesProvider(): array true, true, ['order' => 'allow,deny', 'rules' => ['deny user from all', 'allow root 1.2.3.4']], - 'Access denied!', + AuthenticationFailure::ALLOW_DENIED, ], 'rules-explicit-ok' => [ 'root', @@ -1086,7 +1048,7 @@ public static function checkRulesProvider(): array true, true, ['order' => 'explicit', 'rules' => ['deny user from all', 'allow root 1.2.3.4']], - 'Access denied!', + AuthenticationFailure::ALLOW_DENIED, ], ]; } diff --git a/tests/unit/Plugins/Auth/AuthenticationHttpTest.php b/tests/unit/Plugins/Auth/AuthenticationHttpTest.php index 49614ac3aeec..47f4eb5bf9da 100644 --- a/tests/unit/Plugins/Auth/AuthenticationHttpTest.php +++ b/tests/unit/Plugins/Auth/AuthenticationHttpTest.php @@ -7,7 +7,7 @@ use PhpMyAdmin\Config; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; -use PhpMyAdmin\Exceptions\ExitException; +use PhpMyAdmin\Exceptions\AuthenticationFailure; use PhpMyAdmin\Plugins\Auth\AuthenticationHttp; use PhpMyAdmin\ResponseRenderer; use PhpMyAdmin\Tests\AbstractTestCase; @@ -16,11 +16,9 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Medium; use ReflectionProperty; -use Throwable; use function base64_encode; -use function ob_get_clean; -use function ob_start; +use function json_decode; #[CoversClass(AuthenticationHttp::class)] #[Medium] @@ -82,13 +80,8 @@ public function testAuthVerbose(): void $responseStub = new ResponseRendererStub(); (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); - try { - $this->object->showLoginForm(); - } catch (Throwable $throwable) { - } + $response = $this->object->showLoginForm(); - self::assertInstanceOf(ExitException::class, $throwable); - $response = $responseStub->getResponse(); self::assertSame(['Basic realm="phpMyAdmin verboseMessag"'], $response->getHeader('WWW-Authenticate')); self::assertSame(401, $response->getStatusCode()); } @@ -103,13 +96,8 @@ public function testAuthHost(): void $responseStub = new ResponseRendererStub(); (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); - try { - $this->object->showLoginForm(); - } catch (Throwable $throwable) { - } + $response = $this->object->showLoginForm(); - self::assertInstanceOf(ExitException::class, $throwable); - $response = $responseStub->getResponse(); self::assertSame(['Basic realm="phpMyAdmin hst"'], $response->getHeader('WWW-Authenticate')); self::assertSame(401, $response->getStatusCode()); } @@ -124,13 +112,8 @@ public function testAuthRealm(): void $responseStub = new ResponseRendererStub(); (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, $responseStub); - try { - $this->object->showLoginForm(); - } catch (Throwable $throwable) { - } + $response = $this->object->showLoginForm(); - self::assertInstanceOf(ExitException::class, $throwable); - $response = $responseStub->getResponse(); self::assertSame(['Basic realm="realmmessage"'], $response->getHeader('WWW-Authenticate')); self::assertSame(401, $response->getStatusCode()); } @@ -271,6 +254,8 @@ public function testAuthFails(): void $config = Config::getInstance(); $config->selectedServer['host'] = ''; $_REQUEST = []; + Current::$server = 0; + (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); ResponseRenderer::getInstance()->setAjax(false); $dbi = $this->getMockBuilder(DatabaseInterface::class) @@ -284,40 +269,48 @@ public function testAuthFails(): void DatabaseInterface::$instance = $dbi; $GLOBALS['errno'] = 31; - ob_start(); - try { - $this->object->showFailure(''); - } catch (Throwable $throwable) { - } - - $result = ob_get_clean(); - - self::assertInstanceOf(ExitException::class, $throwable); + (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); + ResponseRenderer::getInstance()->setAjax(false); - self::assertIsString($result); + $response = $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer()); + $result = (string) $response->getBody(); self::assertStringContainsString('

error 123

', $result); - $this->object = $this->getMockBuilder(AuthenticationHttp::class) - ->disableOriginalConstructor() - ->onlyMethods(['authForm']) - ->getMock(); - - $this->object->expects(self::exactly(2)) - ->method('authForm') - ->willThrowException(new ExitException()); // case 2 $config->selectedServer['host'] = 'host'; $GLOBALS['errno'] = 1045; - try { - $this->object->showFailure(''); - } catch (ExitException) { - } + (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); + ResponseRenderer::getInstance()->setAjax(false); + + $response = $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer()); + $result = (string) $response->getBody(); + self::assertStringContainsString('Wrong username/password. Access denied.', $result); + + (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); + ResponseRenderer::getInstance()->setAjax(false); // case 3 $GLOBALS['errno'] = 1043; - $this->expectException(ExitException::class); - $this->object->showFailure(''); + $response = $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer()); + $result = (string) $response->getBody(); + self::assertStringContainsString('Wrong username/password. Access denied.', $result); + } + + public function testShowLoginFormWithAjax(): void + { + Current::$database = ''; + Current::$table = ''; + (new ReflectionProperty(ResponseRenderer::class, 'instance'))->setValue(null, null); + ResponseRenderer::getInstance()->setAjax(true); + $response = (new AuthenticationHttp())->showLoginForm(); + + $body = (string) $response->getBody(); + self::assertJson($body); + $json = json_decode($body, true); + self::assertIsArray($json); + self::assertArrayHasKey('reload_flag', $json); + self::assertSame('1', $json['reload_flag']); } } diff --git a/tests/unit/Plugins/Auth/AuthenticationSignonTest.php b/tests/unit/Plugins/Auth/AuthenticationSignonTest.php index bd3f3fcb539f..be56ae1a675a 100644 --- a/tests/unit/Plugins/Auth/AuthenticationSignonTest.php +++ b/tests/unit/Plugins/Auth/AuthenticationSignonTest.php @@ -8,6 +8,7 @@ use PhpMyAdmin\Config\Settings\Server; use PhpMyAdmin\Current; use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Exceptions\AuthenticationFailure; use PhpMyAdmin\Exceptions\ExitException; use PhpMyAdmin\Plugins\Auth\AuthenticationSignon; use PhpMyAdmin\ResponseRenderer; @@ -15,10 +16,7 @@ use PhpMyAdmin\Tests\Stubs\ResponseRenderer as ResponseRendererStub; use PHPUnit\Framework\Attributes\CoversClass; use ReflectionProperty; -use Throwable; -use function ob_get_clean; -use function ob_start; use function session_get_cookie_params; use function session_id; use function session_name; @@ -61,20 +59,8 @@ public function testAuth(): void Config::getInstance()->selectedServer['SignonURL'] = ''; $_REQUEST = []; ResponseRenderer::getInstance()->setAjax(false); - - ob_start(); - try { - $this->object->showLoginForm(); - } catch (Throwable $throwable) { - } - - $result = ob_get_clean(); - - self::assertInstanceOf(ExitException::class, $throwable); - - self::assertIsString($result); - - self::assertStringContainsString('You must set SignonURL!', $result); + $response = $this->object->showLoginForm(); + self::assertStringContainsString('You must set SignonURL!', (string) $response->getBody()); } public function testAuthLogoutURL(): void @@ -260,12 +246,12 @@ public function testAuthFailsForbidden(): void ->willThrowException(new ExitException()); try { - $this->object->showFailure('empty-denied'); + $this->object->showFailure(AuthenticationFailure::emptyPasswordDeniedByConfiguration()); } catch (ExitException) { } self::assertSame( - 'Login without a password is forbidden by configuration (see AllowNoPassword)', + 'Login without a password is forbidden by configuration (see AllowNoPassword).', $_SESSION['PMA_single_signon_error_message'], ); } @@ -285,7 +271,7 @@ public function testAuthFailsDeny(): void ->willThrowException(new ExitException()); try { - $this->object->showFailure('allow-denied'); + $this->object->showFailure(AuthenticationFailure::deniedByAllowDenyRules()); } catch (ExitException) { } @@ -310,7 +296,7 @@ public function testAuthFailsTimeout(): void $config->settings['LoginCookieValidity'] = '1440'; try { - $this->object->showFailure('no-activity'); + $this->object->showFailure(AuthenticationFailure::loggedOutDueToInactivity()); } catch (ExitException) { } @@ -347,7 +333,7 @@ public function testAuthFailsMySQLError(): void DatabaseInterface::$instance = $dbi; try { - $this->object->showFailure(''); + $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer()); } catch (ExitException) { } @@ -380,11 +366,11 @@ public function testAuthFailsConnect(): void DatabaseInterface::$instance = $dbi; try { - $this->object->showFailure(''); + $this->object->showFailure(AuthenticationFailure::deniedByDatabaseServer()); } catch (ExitException) { } - self::assertSame('Cannot log in to the MySQL server', $_SESSION['PMA_single_signon_error_message']); + self::assertSame('Cannot log in to the database server.', $_SESSION['PMA_single_signon_error_message']); } public function testSetCookieParamsDefaults(): void diff --git a/tests/unit/Plugins/AuthenticationPluginTest.php b/tests/unit/Plugins/AuthenticationPluginTest.php index 2a149cb7e7ed..6d52611872be 100644 --- a/tests/unit/Plugins/AuthenticationPluginTest.php +++ b/tests/unit/Plugins/AuthenticationPluginTest.php @@ -5,8 +5,10 @@ namespace PhpMyAdmin\Tests\Plugins; use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Exceptions\AuthenticationFailure; use PhpMyAdmin\Exceptions\ExitException; use PhpMyAdmin\Http\Factory\ServerRequestFactory; +use PhpMyAdmin\Http\Response; use PhpMyAdmin\Plugins\AuthenticationPlugin; use PhpMyAdmin\ResponseRenderer; use PhpMyAdmin\Tests\AbstractTestCase; @@ -26,14 +28,20 @@ public function testCheckTwoFactor(): void DatabaseInterface::$instance = $dbi; $object = new class extends AuthenticationPlugin { - public function showLoginForm(): void + public function showLoginForm(): Response|null { + return null; } public function readCredentials(): bool { return false; } + + public function showFailure(AuthenticationFailure $failure): Response + { + throw new ExitException(); + } }; $_SESSION['two_factor_check'] = false; @@ -45,12 +53,9 @@ public function readCredentials(): bool $request = ServerRequestFactory::create()->createServerRequest('GET', 'http://example.com/'); $object->user = 'test_user'; - try { - $object->checkTwoFactor($request); - } catch (ExitException) { - } + $response = $object->checkTwoFactor($request); - $response = $responseRenderer->response(); + self::assertNotNull($response); self::assertStringContainsString( 'You have enabled two factor authentication, please confirm your login.', (string) $response->getBody(),