Skip to content

Commit 58bd13f

Browse files
Merge pull request #18589 from MauricioFauth/psr15-middleware
Add support for PSR-15 middleware
2 parents 7aa9c4a + 54a080c commit 58bd13f

13 files changed

Lines changed: 427 additions & 262 deletions

composer.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
"phpmyadmin/twig-i18n-extension": "^4.0.1",
6868
"psr/http-factory": "^1.0",
6969
"psr/http-message": "^1.1",
70+
"psr/http-server-handler": "^1.0",
71+
"psr/http-server-middleware": "^1.0",
7072
"slim/psr7": "^1.6.1",
7173
"symfony/config": "^6.2",
7274
"symfony/dependency-injection": "^6.2",

libraries/classes/Application.php

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,36 @@
66

77
use Fig\Http\Message\StatusCodeInterface;
88
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
9+
use Laminas\HttpHandlerRunner\RequestHandlerRunner;
910
use PhpMyAdmin\Config\ConfigFile;
1011
use PhpMyAdmin\Config\Settings\Server;
1112
use PhpMyAdmin\ConfigStorage\Relation;
1213
use PhpMyAdmin\Dbal\Connection;
1314
use PhpMyAdmin\Exceptions\AuthenticationPluginException;
1415
use PhpMyAdmin\Exceptions\ConfigException;
15-
use PhpMyAdmin\Exceptions\MissingExtensionException;
1616
use PhpMyAdmin\Exceptions\SessionHandlerException;
1717
use PhpMyAdmin\Http\Factory\ResponseFactory;
1818
use PhpMyAdmin\Http\Factory\ServerRequestFactory;
19+
use PhpMyAdmin\Http\Handler\ApplicationHandler;
20+
use PhpMyAdmin\Http\Handler\QueueRequestHandler;
1921
use PhpMyAdmin\Http\Response;
2022
use PhpMyAdmin\Http\ServerRequest;
2123
use PhpMyAdmin\Identifiers\DatabaseName;
2224
use PhpMyAdmin\Identifiers\TableName;
25+
use PhpMyAdmin\Middleware\ErrorHandling;
26+
use PhpMyAdmin\Middleware\OutputBuffering;
27+
use PhpMyAdmin\Middleware\PhpExtensionsChecking;
2328
use PhpMyAdmin\Plugins\AuthenticationPlugin;
2429
use PhpMyAdmin\Plugins\AuthenticationPluginFactory;
2530
use PhpMyAdmin\Routing\Routing;
2631
use PhpMyAdmin\SqlParser\Lexer;
2732
use PhpMyAdmin\Theme\ThemeManager;
2833
use PhpMyAdmin\Tracking\Tracker;
34+
use Psr\Http\Message\ResponseInterface;
35+
use Psr\Http\Message\ServerRequestInterface;
2936
use RuntimeException;
3037
use Symfony\Component\DependencyInjection\ContainerInterface;
38+
use Throwable;
3139

3240
use function __;
3341
use function count;
@@ -51,7 +59,7 @@
5159
use const CONFIG_FILE;
5260
use const E_USER_ERROR;
5361

54-
final class Application
62+
class Application
5563
{
5664
private static ServerRequest|null $request = null;
5765

@@ -73,32 +81,33 @@ public static function init(): self
7381

7482
public function run(bool $isSetupPage = false): void
7583
{
76-
$request = self::getRequest()->withAttribute('isSetupPage', $isSetupPage);
77-
$response = $this->handle($request);
78-
if ($response === null) {
79-
return;
80-
}
81-
82-
$emitter = new SapiEmitter();
83-
$emitter->emit($response);
84+
$requestHandler = new QueueRequestHandler(new ApplicationHandler($this));
85+
$requestHandler->add(new ErrorHandling($this->errorHandler));
86+
$requestHandler->add(new OutputBuffering());
87+
$requestHandler->add(new PhpExtensionsChecking($this, $this->template, $this->responseFactory));
88+
89+
$runner = new RequestHandlerRunner(
90+
$requestHandler,
91+
new SapiEmitter(),
92+
static fn (): ServerRequestInterface => self::getRequest()->withAttribute('isSetupPage', $isSetupPage),
93+
function (Throwable $throwable): ResponseInterface {
94+
$response = $this->responseFactory->createResponse(StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
95+
$response->getBody()->write(sprintf('An error occurred: %s', $throwable->getMessage()));
96+
97+
return $response;
98+
},
99+
);
100+
101+
$runner->run();
84102
}
85103

86-
private function handle(ServerRequest $request): Response|null
104+
public function handle(ServerRequest $request): Response|null
87105
{
88106
$isSetupPage = (bool) $request->getAttribute('isSetupPage');
89107

90108
$GLOBALS['errorHandler'] = $this->errorHandler;
91109
$GLOBALS['config'] = $this->config;
92110

93-
try {
94-
$this->checkRequiredPhpExtensions();
95-
} catch (MissingExtensionException $exception) {
96-
// Disables template caching because the cache directory is not known yet.
97-
$this->template->disableCache();
98-
99-
return $this->getGenericErrorResponse($exception->getMessage());
100-
}
101-
102111
$resultOfServerConfigurationCheck = $this->checkServerConfiguration();
103112
if ($resultOfServerConfigurationCheck !== null) {
104113
return $this->getGenericErrorResponse($resultOfServerConfigurationCheck);
@@ -316,7 +325,7 @@ private function handle(ServerRequest $request): Response|null
316325
/**
317326
* Checks that required PHP extensions are there.
318327
*/
319-
private function checkRequiredPhpExtensions(): void
328+
public function checkRequiredPhpExtensions(): void
320329
{
321330
/**
322331
* Warning about mbstring.

libraries/classes/ErrorHandler.php

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55
namespace PhpMyAdmin;
66

77
use ErrorException;
8+
use Fig\Http\Message\StatusCodeInterface;
9+
use Laminas\HttpHandlerRunner\Emitter\SapiEmitter;
10+
use PhpMyAdmin\Exceptions\ExitException;
811
use Throwable;
912

1013
use function __;
@@ -13,8 +16,6 @@
1316
use function defined;
1417
use function error_reporting;
1518
use function htmlspecialchars;
16-
use function set_error_handler;
17-
use function set_exception_handler;
1819
use function trigger_error;
1920

2021
use const E_COMPILE_ERROR;
@@ -57,17 +58,6 @@ class ErrorHandler
5758

5859
public function __construct()
5960
{
60-
/**
61-
* Do not set ourselves as error handler in case of testsuite.
62-
*
63-
* This behavior is not tested there and breaks other tests as they
64-
* rely on PHPUnit doing it's own error handling which we break here.
65-
*/
66-
if (! defined('TESTSUITE')) {
67-
set_exception_handler($this->handleException(...));
68-
set_error_handler($this->handleError(...));
69-
}
70-
7161
if (! Util::isErrorReportingAvailable()) {
7262
return;
7363
}
@@ -334,7 +324,14 @@ protected function dispFatalError(Error $error): never
334324

335325
$response->addHTML($error->getDisplay());
336326
$response->addHTML('</body></html>');
337-
$response->callExit();
327+
328+
if (defined('TESTSUITE')) {
329+
throw new ExitException();
330+
}
331+
332+
(new SapiEmitter())->emit($response->response()->withStatus(StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR));
333+
334+
exit;
338335
}
339336

340337
/**
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMyAdmin\Http\Handler;
6+
7+
use PhpMyAdmin\Application;
8+
use PhpMyAdmin\Exceptions\ExitException;
9+
use PhpMyAdmin\Http\ServerRequest;
10+
use PhpMyAdmin\ResponseRenderer;
11+
use Psr\Http\Message\ResponseInterface;
12+
use Psr\Http\Message\ServerRequestInterface;
13+
use Psr\Http\Server\RequestHandlerInterface;
14+
15+
use function assert;
16+
17+
final class ApplicationHandler implements RequestHandlerInterface
18+
{
19+
public function __construct(private readonly Application $application)
20+
{
21+
}
22+
23+
public function handle(ServerRequestInterface $request): ResponseInterface
24+
{
25+
assert($request instanceof ServerRequest);
26+
try {
27+
$response = $this->application->handle($request);
28+
if ($response === null) {
29+
throw new ExitException();
30+
}
31+
} catch (ExitException) {
32+
$response = ResponseRenderer::getInstance()->response();
33+
}
34+
35+
return $response;
36+
}
37+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMyAdmin\Http\Handler;
6+
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
use Psr\Http\Server\MiddlewareInterface;
10+
use Psr\Http\Server\RequestHandlerInterface;
11+
12+
use function array_shift;
13+
14+
final class QueueRequestHandler implements RequestHandlerInterface
15+
{
16+
/** @psalm-var list<MiddlewareInterface> */
17+
private array $middleware = [];
18+
19+
public function __construct(private readonly RequestHandlerInterface $fallbackHandler)
20+
{
21+
}
22+
23+
public function add(MiddlewareInterface $middleware): void
24+
{
25+
$this->middleware[] = $middleware;
26+
}
27+
28+
public function handle(ServerRequestInterface $request): ResponseInterface
29+
{
30+
if ($this->middleware === []) {
31+
return $this->fallbackHandler->handle($request);
32+
}
33+
34+
$middleware = array_shift($this->middleware);
35+
36+
return $middleware->process($request, $this);
37+
}
38+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMyAdmin\Middleware;
6+
7+
use PhpMyAdmin\ErrorHandler;
8+
use Psr\Http\Message\ResponseInterface;
9+
use Psr\Http\Message\ServerRequestInterface;
10+
use Psr\Http\Server\MiddlewareInterface;
11+
use Psr\Http\Server\RequestHandlerInterface;
12+
13+
use function restore_error_handler;
14+
use function restore_exception_handler;
15+
use function set_error_handler;
16+
use function set_exception_handler;
17+
18+
final class ErrorHandling implements MiddlewareInterface
19+
{
20+
public function __construct(private readonly ErrorHandler $errorHandler)
21+
{
22+
}
23+
24+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
25+
{
26+
set_exception_handler($this->errorHandler->handleException(...));
27+
set_error_handler($this->errorHandler->handleError(...));
28+
29+
$response = $handler->handle($request);
30+
31+
restore_error_handler();
32+
restore_exception_handler();
33+
34+
return $response;
35+
}
36+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMyAdmin\Middleware;
6+
7+
use Psr\Http\Message\ResponseInterface;
8+
use Psr\Http\Message\ServerRequestInterface;
9+
use Psr\Http\Server\MiddlewareInterface;
10+
use Psr\Http\Server\RequestHandlerInterface;
11+
use Throwable;
12+
13+
use function is_string;
14+
use function ob_end_clean;
15+
use function ob_get_clean;
16+
use function ob_start;
17+
18+
final class OutputBuffering implements MiddlewareInterface
19+
{
20+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
21+
{
22+
try {
23+
ob_start();
24+
$response = $handler->handle($request);
25+
$output = ob_get_clean();
26+
} catch (Throwable $throwable) {
27+
ob_end_clean();
28+
29+
throw $throwable;
30+
}
31+
32+
$body = $response->getBody();
33+
if (is_string($output) && $output !== '' && $body->isWritable()) {
34+
$body->write($output);
35+
}
36+
37+
return $response;
38+
}
39+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpMyAdmin\Middleware;
6+
7+
use Fig\Http\Message\StatusCodeInterface;
8+
use PhpMyAdmin\Application;
9+
use PhpMyAdmin\Exceptions\MissingExtensionException;
10+
use PhpMyAdmin\Http\Factory\ResponseFactory;
11+
use PhpMyAdmin\Template;
12+
use Psr\Http\Message\ResponseInterface;
13+
use Psr\Http\Message\ServerRequestInterface;
14+
use Psr\Http\Server\MiddlewareInterface;
15+
use Psr\Http\Server\RequestHandlerInterface;
16+
17+
final class PhpExtensionsChecking implements MiddlewareInterface
18+
{
19+
public function __construct(
20+
private readonly Application $application,
21+
private readonly Template $template,
22+
private readonly ResponseFactory $responseFactory,
23+
) {
24+
}
25+
26+
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
27+
{
28+
try {
29+
$this->application->checkRequiredPhpExtensions();
30+
} catch (MissingExtensionException $exception) {
31+
// Disables template caching because the cache directory is not known yet.
32+
$this->template->disableCache();
33+
$output = $this->template->render('error/generic', [
34+
'lang' => 'en',
35+
'dir' => 'ltr',
36+
'error_message' => $exception->getMessage(),
37+
]);
38+
$response = $this->responseFactory->createResponse(StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
39+
$response->getBody()->write($output);
40+
41+
return $response;
42+
}
43+
44+
return $handler->handle($request);
45+
}
46+
}

0 commit comments

Comments
 (0)