3 declare(strict_types
=1);
5 namespace PhpMyAdmin\Routing
;
7 use FastRoute\DataGenerator\GroupCountBased
as DataGeneratorGroupCountBased
;
8 use FastRoute\Dispatcher
;
9 use FastRoute\Dispatcher\GroupCountBased
as DispatcherGroupCountBased
;
10 use FastRoute\RouteCollector
;
11 use FastRoute\RouteParser\Std
as RouteParserStd
;
12 use Fig\Http\Message\StatusCodeInterface
;
13 use PhpMyAdmin\Bookmarks\BookmarkRepository
;
14 use PhpMyAdmin\Config
;
15 use PhpMyAdmin\ConfigStorage\Relation
;
16 use PhpMyAdmin\Console
;
17 use PhpMyAdmin\Controllers\HomeController
;
18 use PhpMyAdmin\Controllers\InvocableController
;
19 use PhpMyAdmin\Controllers\Setup\MainController
;
20 use PhpMyAdmin\Controllers\Setup\ShowConfigController
;
21 use PhpMyAdmin\Controllers\Setup\ValidateController
;
23 use PhpMyAdmin\DatabaseInterface
;
24 use PhpMyAdmin\Http\Factory\ResponseFactory
;
25 use PhpMyAdmin\Http\Response
;
26 use PhpMyAdmin\Http\ServerRequest
;
27 use PhpMyAdmin\LanguageManager
;
28 use PhpMyAdmin\Message
;
29 use PhpMyAdmin\Sanitize
;
30 use PhpMyAdmin\Template
;
31 use Psr\Container\ContainerInterface
;
34 use function array_pop
;
37 use function file_exists
;
38 use function file_put_contents
;
39 use function htmlspecialchars
;
41 use function is_array
;
42 use function is_readable
;
43 use function is_writable
;
44 use function mb_strlen
;
45 use function mb_strpos
;
46 use function mb_strrpos
;
47 use function mb_substr
;
48 use function rawurldecode
;
50 use function trigger_error
;
51 use function urldecode
;
52 use function var_export
;
55 use const E_USER_WARNING
;
58 * Class used to warm up the routing cache and manage routing.
63 * @deprecated Use {@see ServerRequest::getRoute()} instead.
65 * @psalm-var non-empty-string
67 public static string $route = '/';
69 public const ROUTES_CACHE_FILE
= CACHE_DIR
. 'routes.cache.php';
71 public static function skipCache(): bool
73 return (Config
::getInstance()->settings
['environment'] ??
'') === 'development';
76 public static function canWriteCache(): bool
78 $cacheFileExists = file_exists(self
::ROUTES_CACHE_FILE
);
79 $canWriteFile = is_writable(self
::ROUTES_CACHE_FILE
);
80 if ($cacheFileExists && $canWriteFile) {
84 // Write without read does not work, chmod 200 for example
85 if (! $cacheFileExists && is_writable(CACHE_DIR
) && is_readable(CACHE_DIR
)) {
92 public static function getDispatcher(): Dispatcher
94 $skipCache = self
::skipCache();
96 // If skip cache is enabled, do not try to read the file
97 // If no cache skipping then read it and use it
98 if (! $skipCache && file_exists(self
::ROUTES_CACHE_FILE
)) {
99 /** @psalm-suppress MissingFile, UnresolvableInclude, MixedAssignment */
100 $dispatchData = require self
::ROUTES_CACHE_FILE
;
101 if (self
::isRoutesCacheFileValid($dispatchData)) {
102 return new DispatcherGroupCountBased($dispatchData);
106 $routeCollector = new RouteCollector(new RouteParserStd(), new DataGeneratorGroupCountBased());
107 Routes
::collect($routeCollector);
109 $dispatchData = $routeCollector->getData();
110 $canWriteCache = self
::canWriteCache();
112 // If skip cache is enabled, do not try to write it
113 // If no skip cache then try to write if write is possible
114 if (! $skipCache && $canWriteCache) {
115 $writeWorks = self
::writeCache(
116 '<?php return ' . var_export($dispatchData, true) . ';',
122 'The routing cache could not be written, '
123 . 'you need to adjust permissions on the folder/file "%s"',
125 self
::ROUTES_CACHE_FILE
,
132 return new DispatcherGroupCountBased($dispatchData);
135 public static function writeCache(string $cacheContents): bool
137 return @file_put_contents
(self
::ROUTES_CACHE_FILE
, $cacheContents) !== false;
141 * Call associated controller for a route using the dispatcher
143 public static function callControllerForRoute(
144 ServerRequest
$request,
145 Dispatcher
$dispatcher,
146 ContainerInterface
$container,
147 ResponseFactory
$responseFactory,
149 $route = $request->getRoute();
150 $routeInfo = $dispatcher->dispatch($request->getMethod(), rawurldecode($route));
152 if ($routeInfo[0] === Dispatcher
::NOT_FOUND
) {
153 $response = $responseFactory->createResponse(StatusCodeInterface
::STATUS_NOT_FOUND
);
155 return $response->write(Message
::error(sprintf(
156 __('Error 404! The page %s was not found.'),
157 '<code>' . htmlspecialchars($route) . '</code>',
161 if ($routeInfo[0] === Dispatcher
::METHOD_NOT_ALLOWED
) {
162 $response = $responseFactory->createResponse(StatusCodeInterface
::STATUS_METHOD_NOT_ALLOWED
);
164 return $response->write(Message
::error(__('Error 405! Request method not allowed.'))->getDisplay());
167 if ($routeInfo[0] !== Dispatcher
::FOUND
) {
168 return $responseFactory->createResponse(StatusCodeInterface
::STATUS_BAD_REQUEST
);
171 /** @psalm-var class-string<InvocableController> $controllerName */
172 $controllerName = $routeInfo[1];
174 $controller = $container->get($controllerName);
175 assert($controller instanceof InvocableController
);
177 return $controller($request->withAttribute('routeVars', $routeInfo[2]));
180 /** @psalm-assert-if-true array[] $dispatchData */
181 private static function isRoutesCacheFileValid(mixed $dispatchData): bool
183 return is_array($dispatchData)
184 && isset($dispatchData[1])
185 && is_array($dispatchData[1])
186 && isset($dispatchData[0]['GET']['/'])
187 && $dispatchData[0]['GET']['/'] === HomeController
::class;
190 public static function callSetupController(ServerRequest
$request, ResponseFactory
$responseFactory): Response
192 $route = $request->getRoute();
193 $template = new Template();
194 if ($route === '/setup' ||
$route === '/') {
195 $dbi = DatabaseInterface
::getInstance();
196 $relation = new Relation($dbi);
197 $console = new Console($relation, $template, new BookmarkRepository($dbi, $relation));
199 return (new MainController($responseFactory, $template, $console))($request);
202 if ($route === '/setup/show-config') {
203 return (new ShowConfigController())($request);
206 if ($route === '/setup/validate') {
207 return (new ValidateController($responseFactory))($request);
210 $response = $responseFactory->createResponse(StatusCodeInterface
::STATUS_NOT_FOUND
);
212 return $response->write($template->render('error/generic', [
213 'lang' => $GLOBALS['lang'] ??
'en',
214 'dir' => LanguageManager
::$textDir,
215 'error_message' => Sanitize
::convertBBCode(sprintf(
216 __('Error 404! The page %s was not found.'),
217 '[code]' . htmlspecialchars($route) . '[/code]',
223 * PATH_INFO could be compromised if set, so remove it from PHP_SELF
224 * and provide a clean PHP_SELF here
226 public static function getCleanPathInfo(): string
228 $pmaPhpSelf = Core
::getEnv('PHP_SELF');
229 if ($pmaPhpSelf === '') {
230 $pmaPhpSelf = urldecode(Core
::getEnv('REQUEST_URI'));
233 $pathInfo = Core
::getEnv('PATH_INFO');
234 if ($pathInfo !== '' && $pmaPhpSelf !== '') {
235 $questionPos = mb_strpos($pmaPhpSelf, '?');
236 if ($questionPos != false) {
237 $pmaPhpSelf = mb_substr($pmaPhpSelf, 0, $questionPos);
240 $pathInfoPos = mb_strrpos($pmaPhpSelf, $pathInfo);
241 if ($pathInfoPos !== false) {
242 $pathInfoPart = mb_substr($pmaPhpSelf, $pathInfoPos, mb_strlen($pathInfo));
243 if ($pathInfoPart === $pathInfo) {
244 $pmaPhpSelf = mb_substr($pmaPhpSelf, 0, $pathInfoPos);
250 foreach (explode('/', $pmaPhpSelf) as $part) {
251 // ignore parts that have no value
252 if ($part === '' ||
$part === '.') {
256 if ($part !== '..') {
257 // cool, we found a new part
259 } elseif ($path !== []) {
260 // going back up? sure
264 // Here we intentionall ignore case where we go too up
265 // as there is nothing sane to do
268 /** TODO: Do we really need htmlspecialchars here? */
269 return htmlspecialchars('/' . implode('/', $path));