Routing.php 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. <?php
  2. declare(strict_types=1);
  3. namespace PhpMyAdmin;
  4. use FastRoute\Dispatcher;
  5. use Psr\Container\ContainerInterface;
  6. use FastRoute\RouteParser\Std as RouteParserStd;
  7. use FastRoute\DataGenerator\GroupCountBased as DataGeneratorGroupCountBased;
  8. use FastRoute\Dispatcher\GroupCountBased as DispatcherGroupCountBased;
  9. use FastRoute\RouteCollector;
  10. use function htmlspecialchars;
  11. use function mb_strlen;
  12. use function rawurldecode;
  13. use function sprintf;
  14. use function is_writable;
  15. use function file_exists;
  16. use function is_array;
  17. use RuntimeException;
  18. use function var_export;
  19. use function is_readable;
  20. use function trigger_error;
  21. use const E_USER_WARNING;
  22. use function fopen;
  23. use function fwrite;
  24. use function fclose;
  25. /**
  26. * Class used to warm up the routing cache and manage routing.
  27. */
  28. class Routing
  29. {
  30. public const ROUTES_CACHE_FILE = CACHE_DIR . 'routes.cache.php';
  31. public static function getDispatcher(): Dispatcher
  32. {
  33. $routes = require ROOT_PATH . 'libraries/routes.php';
  34. return self::routesCachedDispatcher($routes);
  35. }
  36. public static function skipCache(): bool
  37. {
  38. global $cfg;
  39. return ($cfg['environment'] ?? '') === 'development';
  40. }
  41. public static function canWriteCache(): bool
  42. {
  43. $cacheFileExists = file_exists(self::ROUTES_CACHE_FILE);
  44. $canWriteFile = is_writable(self::ROUTES_CACHE_FILE);
  45. if ($cacheFileExists && $canWriteFile) {
  46. return true;
  47. }
  48. // Write without read does not work, chmod 200 for example
  49. if (! $cacheFileExists && is_writable(CACHE_DIR) && is_readable(CACHE_DIR)) {
  50. return true;
  51. }
  52. return $canWriteFile;
  53. }
  54. private static function routesCachedDispatcher(callable $routeDefinitionCallback): Dispatcher
  55. {
  56. $skipCache = self::skipCache();
  57. // If skip cache is enabled, do not try to read the file
  58. // If no cache skipping then read it and use it
  59. if (! $skipCache && file_exists(self::ROUTES_CACHE_FILE)) {
  60. /** @psalm-suppress MissingFile */
  61. $dispatchData = require self::ROUTES_CACHE_FILE;
  62. if (! is_array($dispatchData)) {
  63. throw new RuntimeException('Invalid cache file "' . self::ROUTES_CACHE_FILE . '"');
  64. }
  65. return new DispatcherGroupCountBased($dispatchData);
  66. }
  67. $routeCollector = new RouteCollector(
  68. new RouteParserStd(),
  69. new DataGeneratorGroupCountBased()
  70. );
  71. $routeDefinitionCallback($routeCollector);
  72. /** @var RouteCollector $routeCollector */
  73. $dispatchData = $routeCollector->getData();
  74. $canWriteCache = self::canWriteCache();
  75. // If skip cache is enabled, do not try to write it
  76. // If no skip cache then try to write if write is possible
  77. if (! $skipCache && $canWriteCache) {
  78. $writeWorks = self::writeCache(
  79. '<?php return ' . var_export($dispatchData, true) . ';'
  80. );
  81. if (! $writeWorks) {
  82. trigger_error(
  83. sprintf(
  84. __(
  85. 'The routing cache could not be written, '
  86. . 'you need to adjust permissions on the folder/file "%s"'
  87. ),
  88. self::ROUTES_CACHE_FILE
  89. ),
  90. E_USER_WARNING
  91. );
  92. }
  93. }
  94. return new DispatcherGroupCountBased($dispatchData);
  95. }
  96. public static function writeCache(string $cacheContents): bool
  97. {
  98. $handle = @fopen(self::ROUTES_CACHE_FILE, 'w');
  99. if ($handle === false) {
  100. return false;
  101. }
  102. $couldWrite = fwrite($handle, $cacheContents);
  103. fclose($handle);
  104. return $couldWrite !== false;
  105. }
  106. public static function getCurrentRoute(): string
  107. {
  108. /** @var string $route */
  109. $route = $_GET['route'] ?? $_POST['route'] ?? '/';
  110. /**
  111. * See FAQ 1.34.
  112. *
  113. * @see https://docs.phpmyadmin.net/en/latest/faq.html#faq1-34
  114. */
  115. if (($route === '/' || $route === '') && isset($_GET['db']) && mb_strlen($_GET['db']) !== 0) {
  116. $route = '/database/structure';
  117. if (isset($_GET['table']) && mb_strlen($_GET['table']) !== 0) {
  118. $route = '/sql';
  119. }
  120. }
  121. return $route;
  122. }
  123. /**
  124. * Call associated controller for a route using the dispatcher
  125. */
  126. public static function callControllerForRoute(
  127. string $route,
  128. Dispatcher $dispatcher,
  129. ContainerInterface $container
  130. ): void {
  131. $routeInfo = $dispatcher->dispatch(
  132. $_SERVER['REQUEST_METHOD'],
  133. rawurldecode($route)
  134. );
  135. if ($routeInfo[0] === Dispatcher::NOT_FOUND) {
  136. /** @var Response $response */
  137. $response = $container->get(Response::class);
  138. $response->setHttpResponseCode(404);
  139. echo Message::error(sprintf(
  140. __('Error 404! The page %s was not found.'),
  141. '<code>' . htmlspecialchars($route) . '</code>'
  142. ))->getDisplay();
  143. return;
  144. }
  145. if ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) {
  146. /** @var Response $response */
  147. $response = $container->get(Response::class);
  148. $response->setHttpResponseCode(405);
  149. echo Message::error(__('Error 405! Request method not allowed.'))->getDisplay();
  150. return;
  151. }
  152. if ($routeInfo[0] !== Dispatcher::FOUND) {
  153. return;
  154. }
  155. [$controllerName, $action] = $routeInfo[1];
  156. $controller = $container->get($controllerName);
  157. $controller->$action($routeInfo[2]);
  158. }
  159. }