diff --git a/index.php b/index.php index c1a2273cd0..6fe4765a7d 100644 --- a/index.php +++ b/index.php @@ -4,10 +4,7 @@ */ declare(strict_types=1); -use FastRoute\Dispatcher; -use PhpMyAdmin\Message; -use PhpMyAdmin\Response; -use function FastRoute\simpleDispatcher; +use PhpMyAdmin\Routing; if (! defined('ROOT_PATH')) { // phpcs:disable PSR1.Files.SideEffects @@ -15,52 +12,9 @@ if (! defined('ROOT_PATH')) { // phpcs:enable } -global $containerBuilder, $route; - -/** @var string $route */ -$route = $_GET['route'] ?? $_POST['route'] ?? '/'; - -/** - * See FAQ 1.34. - * - * @see https://docs.phpmyadmin.net/en/latest/faq.html#faq1-34 - */ -if (($route === '/' || $route === '') && isset($_GET['db']) && mb_strlen($_GET['db']) !== 0) { - $route = '/database/structure'; - if (isset($_GET['table']) && mb_strlen($_GET['table']) !== 0) { - $route = '/sql'; - } -} - -if ($route === '/import-status') { - // phpcs:disable PSR1.Files.SideEffects - define('PMA_MINIMUM_COMMON', true); - // phpcs:enable -} +global $route; require_once ROOT_PATH . 'libraries/common.inc.php'; -$routes = require ROOT_PATH . 'libraries/routes.php'; -$dispatcher = simpleDispatcher($routes); -$routeInfo = $dispatcher->dispatch( - $_SERVER['REQUEST_METHOD'], - rawurldecode($route) -); -if ($routeInfo[0] === Dispatcher::NOT_FOUND) { - /** @var Response $response */ - $response = $containerBuilder->get(Response::class); - $response->setHttpResponseCode(404); - Message::error(sprintf( - __('Error 404! The page %s was not found.'), - '' . $route . '' - ))->display(); -} elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { - /** @var Response $response */ - $response = $containerBuilder->get(Response::class); - $response->setHttpResponseCode(405); - Message::error(__('Error 405! Request method not allowed.'))->display(); -} elseif ($routeInfo[0] === Dispatcher::FOUND) { - [$controllerName, $action] = $routeInfo[1]; - $controller = $containerBuilder->get($controllerName); - $controller->$action($routeInfo[2]); -} +$dispatcher = Routing::getDispatcher(); +Routing::callControllerForRoute($route, $dispatcher); diff --git a/libraries/cache/.gitignore b/libraries/cache/.gitignore new file mode 100644 index 0000000000..a68d087bfe --- /dev/null +++ b/libraries/cache/.gitignore @@ -0,0 +1,2 @@ +/* +!/.gitignore diff --git a/libraries/classes/Command/CacheWarmupCommand.php b/libraries/classes/Command/CacheWarmupCommand.php index 87c23f9b6c..a0c0ed32e3 100644 --- a/libraries/classes/Command/CacheWarmupCommand.php +++ b/libraries/classes/Command/CacheWarmupCommand.php @@ -6,6 +6,7 @@ namespace PhpMyAdmin\Command; use PhpMyAdmin\Config; use PhpMyAdmin\DatabaseInterface; +use PhpMyAdmin\Routing; use PhpMyAdmin\Tests\Stubs\DbiDummy; use PhpMyAdmin\Twig\CoreExtension; use PhpMyAdmin\Twig\I18nExtension; @@ -41,13 +42,50 @@ final class CacheWarmupCommand extends Command protected function configure(): void { $this->setDescription('Warms up the Twig templates cache'); + $this->addOption('twig', null, null, 'Warm up twig templates cache.'); + $this->addOption('routing', null, null, 'Warm up routing cache.'); $this->setHelp('The %command.name% command warms up the cache of the Twig templates.'); } protected function execute(InputInterface $input, OutputInterface $output): int + { + if ($input->getOption('twig') === true && $input->getOption('routing') === true) { + $output->writeln('Please specify --twig or --routing'); + return 1; + } elseif ($input->getOption('twig') === true) { + return $this->warmUpTwigCache($output); + } elseif ($input->getOption('routing') === true) { + return $this->warmUpRoutingCache($output); + } else { + $output->writeln('Warming up all caches.', OutputInterface::VERBOSITY_VERBOSE); + $twigCode = $this->warmUptwigCache($output); + if ($twigCode !== 0) { + $output->writeln('Twig cache generation had an error.'); + return $twigCode; + } + $routingCode = $this->warmUpTwigCache($output); + if ($routingCode !== 0) { + $output->writeln('Routing cache generation had an error.'); + return $twigCode; + } + $output->writeln('Warm up of all caches done.', OutputInterface::VERBOSITY_VERBOSE); + return 0; + } + } + + private function warmUpRoutingCache(OutputInterface $output): int + { + $output->writeln('Warming up the routing cache', OutputInterface::VERBOSITY_VERBOSE); + Routing::getDispatcher(); + $output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE); + return 0; + } + + private function warmUpTwigCache(OutputInterface $output): int { global $cfg, $PMA_Config, $dbi; + $output->writeln('Warming up the twig cache', OutputInterface::VERBOSITY_VERBOSE); $cfg['environment'] = 'production'; $PMA_Config = new Config(CONFIG_FILE); $PMA_Config->set('environment', $cfg['environment']); @@ -77,12 +115,15 @@ final class CacheWarmupCommand extends Command /** @var CacheInterface $twigCache */ $twigCache = $twig->getCache(false); + $output->writeln('Searching for files...', OutputInterface::VERBOSITY_VERY_VERBOSE); + $replacements = []; $templates = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($tplDir), RecursiveIteratorIterator::LEAVES_ONLY ); + $output->writeln('Warming templates', OutputInterface::VERBOSITY_VERY_VERBOSE); foreach ($templates as $file) { // Skip test files if (strpos($file->getPathname(), '/test/') !== false) { @@ -91,6 +132,7 @@ final class CacheWarmupCommand extends Command // force compilation if ($file->isFile() && $file->getExtension() === 'twig') { $name = str_replace($tplDir . '/', '', $file->getPathname()); + $output->writeln('Loading: ' . $name, OutputInterface::VERBOSITY_DEBUG); $template = $twig->loadTemplate($name); // Generate line map @@ -101,6 +143,7 @@ final class CacheWarmupCommand extends Command } } + $output->writeln('Writing replacements...', OutputInterface::VERBOSITY_VERY_VERBOSE); // Store replacements in JSON $handle = fopen($tmpDir . '/replace.json', 'w'); if ($handle === false) { @@ -109,6 +152,7 @@ final class CacheWarmupCommand extends Command fwrite($handle, (string) json_encode($replacements)); fclose($handle); + $output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE); return 0; } diff --git a/libraries/classes/Display/ImportAjax.php b/libraries/classes/Display/ImportAjax.php index 476c98226d..002f84d749 100644 --- a/libraries/classes/Display/ImportAjax.php +++ b/libraries/classes/Display/ImportAjax.php @@ -45,7 +45,7 @@ class ImportAjax * list of available plugins */ $plugins = [ - // PHP 5.4 session-based upload progress is problematic, see bug 3964 + // in PHP 5.4 session-based upload progress was problematic, see closed bug 3964 //"session", 'progress', 'apc', diff --git a/libraries/classes/Routing.php b/libraries/classes/Routing.php new file mode 100644 index 0000000000..fdf158bb6a --- /dev/null +++ b/libraries/classes/Routing.php @@ -0,0 +1,80 @@ + CACHE_DIR . 'routes.cache', + 'cacheDisabled' => ($cfg['environment'] ?? '') === 'development', + ]); + } + + public static function getCurrentRoute(): string + { + /** @var string $route */ + $route = $_GET['route'] ?? $_POST['route'] ?? '/'; + + /** + * See FAQ 1.34. + * + * @see https://docs.phpmyadmin.net/en/latest/faq.html#faq1-34 + */ + if (($route === '/' || $route === '') && isset($_GET['db']) && mb_strlen($_GET['db']) !== 0) { + $route = '/database/structure'; + if (isset($_GET['table']) && mb_strlen($_GET['table']) !== 0) { + $route = '/sql'; + } + } + return $route; + } + + /** + * Call associated controller for a route using the dispatcher + * @param string $route The current route + * @param Dispatcher $dispatcher The dispatcher + */ + public static function callControllerForRoute(string $route, Dispatcher $dispatcher): void + { + global $containerBuilder; + $routeInfo = $dispatcher->dispatch( + $_SERVER['REQUEST_METHOD'], + rawurldecode($route) + ); + if ($routeInfo[0] === Dispatcher::NOT_FOUND) { + /** @var Response $response */ + $response = $containerBuilder->get(Response::class); + $response->setHttpResponseCode(404); + Message::error(sprintf( + __('Error 404! The page %s was not found.'), + '' . $route . '' + ))->display(); + } elseif ($routeInfo[0] === Dispatcher::METHOD_NOT_ALLOWED) { + /** @var Response $response */ + $response = $containerBuilder->get(Response::class); + $response->setHttpResponseCode(405); + Message::error(__('Error 405! Request method not allowed.'))->display(); + } elseif ($routeInfo[0] === Dispatcher::FOUND) { + [$controllerName, $action] = $routeInfo[1]; + $controller = $containerBuilder->get($controllerName); + $controller->$action($routeInfo[2]); + } + } +} diff --git a/libraries/common.inc.php b/libraries/common.inc.php index 1117f6d1f2..6af5caa56f 100644 --- a/libraries/common.inc.php +++ b/libraries/common.inc.php @@ -40,6 +40,7 @@ use PhpMyAdmin\Message; use PhpMyAdmin\MoTranslator\Loader; use PhpMyAdmin\Plugins\AuthenticationPlugin; use PhpMyAdmin\Response; +use PhpMyAdmin\Routing; use PhpMyAdmin\Sanitize; use PhpMyAdmin\Session; use PhpMyAdmin\SqlParser\Lexer; @@ -50,7 +51,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; -global $containerBuilder, $error_handler, $PMA_Config, $server, $dbi, $lang, $cfg, $isConfigLoading, $auth_plugin; +global $containerBuilder, $error_handler, $PMA_Config, $server, $dbi, $lang, $cfg, $isConfigLoading, $auth_plugin, $route; /** * block attempts to directly run this script @@ -95,6 +96,14 @@ if (! @is_readable(AUTOLOAD_FILE)) { } require_once AUTOLOAD_FILE; +$route = Routing::getCurrentRoute(); + +if ($route === '/import-status') { + // phpcs:disable PSR1.Files.SideEffects + define('PMA_MINIMUM_COMMON', true); + // phpcs:enable +} + $containerBuilder = new ContainerBuilder(); $loader = new PhpFileLoader($containerBuilder, new FileLocator(__DIR__)); $loader->load('services_loader.php'); diff --git a/libraries/vendor_config.php b/libraries/vendor_config.php index 674fedc778..9cae9f1ebe 100644 --- a/libraries/vendor_config.php +++ b/libraries/vendor_config.php @@ -79,3 +79,8 @@ define('LOCALE_PATH', ROOT_PATH . 'locale/'); * is used) */ define('K_PATH_IMAGES', ROOT_PATH); + +/** + * Define the cache directory for routing cache an other cache files + */ +define('CACHE_DIR', ROOT_PATH . 'libraries/cache/'); diff --git a/phpcs.xml.dist b/phpcs.xml.dist index 5be43db5ff..a3f2a10055 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -74,6 +74,7 @@ */node_modules/* + */libraries/cache/* */test/sami-config.php *.twig */twig-templates/* diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 544bb6390b..490d8874a5 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -12,6 +12,7 @@ parameters: excludes_analyse: - examples/openid.php - node_modules/* + - libraries/cache/* - test/sami-config.php - tmp/* - twig-templates/* diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 1b95f0ee71..db35e32a38 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -25,6 +25,7 @@ examples node_modules + libraries/cache test tmp vendor diff --git a/scripts/console b/scripts/console index 678b7aac2d..cfdd9b80e4 100755 --- a/scripts/console +++ b/scripts/console @@ -13,6 +13,12 @@ define('PHPMYADMIN', true); require_once ROOT_PATH . 'libraries/vendor_config.php'; require_once AUTOLOAD_FILE; +if (! class_exists(Application::class)) { + echo 'Be sure to have dev-dependencies installed.' . PHP_EOL; + echo 'Command aborted.' . PHP_EOL; + exit(1); +} + $application = new Application('phpMyAdmin Console Tool'); $application->add(new AdvisoryRulesCommand()); diff --git a/scripts/create-release.sh b/scripts/create-release.sh index a053322da8..5b2e17e8ac 100755 --- a/scripts/create-release.sh +++ b/scripts/create-release.sh @@ -80,7 +80,7 @@ while [ $# -gt 0 ] ; do exit 1 fi elif [ -z "$branch" ] ; then - branch=`echo $1 | tr -d -c '0-9A-Za-z_-'` + branch=`echo $1 | tr -d -c '/0-9A-Za-z_-'` if [ "x$branch" != "x$1" ] ; then echo "Invalid branch: $1" exit 1 @@ -218,11 +218,8 @@ echo "* Removing unneeded files" # Remove developer information rm -rf .github -# Remove phpcs coding standard definition -rm -rf PMAStandard - # Testsuite setup -rm -f .travis.yml .coveralls.yml .scrutinizer.yml .jshintrc .weblate codecov.yml +rm -f .travis.yml .scrutinizer.yml .jshintrc .weblate codecov.yml # Remove readme for github rm -f README.rst @@ -243,7 +240,7 @@ if [ ! -d libraries/tcpdf ] ; then cp composer.json composer.json.backup echo "* Running composer" composer config platform.php "$PHP_REQ" - composer update --no-dev + composer update --no-dev --optimize-autoloader # Parse the required versions from composer.json PACKAGES_VERSIONS='' @@ -256,7 +253,7 @@ if [ ! -d libraries/tcpdf ] ; then do PACKAGES_VERSIONS="$PACKAGES_VERSIONS $PACKAGES:`awk "/require-dev/ {printline = 1; print; next } printline" composer.json | grep "$PACKAGES" | awk -F [\\"] '{print $4}'`" done - composer require --update-no-dev $PACKAGES_VERSIONS + composer require --optimize-autoloader --update-no-dev $PACKAGES_VERSIONS mv composer.json.backup composer.json echo "* Cleanup of composer packages" @@ -304,6 +301,11 @@ fi # Remove Bootstrap theme rm -rf themes/bootstrap +composer update +# Warm up the routing cache +./scripts/console cache:warmup --routing +composer update --no-dev --optimize-autoloader + # Remove git metadata rm .git find . -name .gitignore -print0 | xargs -0 -r rm -f @@ -329,7 +331,7 @@ if [ $do_test -eq 1 ] ; then rm -f .phpunit.result.cache # Remove libs installed for testing rm -rf build - composer update --no-dev + composer update --no-dev --optimize-autoloader fi @@ -351,7 +353,7 @@ for kit in $KITS ; do # Testsuite rm -rf test/ rm phpunit.xml.* build.xml - rm -f .editorconfig .eslintignore .eslintrc.json .stylelintrc.json phpstan.neon.dist phpcs.xml.dist + rm -f .editorconfig .eslintignore .jshintrc .eslintrc.json .stylelintrc.json phpstan.neon.dist phpstan-baseline.neon phpcs.xml.dist # Gettext po files rm -rf po/ # Documentation source code diff --git a/scripts/update-po b/scripts/update-po index 411c30066a..1f8083385d 100755 --- a/scripts/update-po +++ b/scripts/update-po @@ -13,7 +13,7 @@ set -e # Generate Twig template cache in clean dir rm -rf twig-templates/ -php scripts/console cache:warmup +php scripts/console cache:warmup --twig # Update pot (template), ensure that advisor is at the end LOCS=`ls po/*.po | sed 's@.*/\(.*\)\.po@\1@'` diff --git a/test/classes/RoutingTest.php b/test/classes/RoutingTest.php new file mode 100644 index 0000000000..d6860141e5 --- /dev/null +++ b/test/classes/RoutingTest.php @@ -0,0 +1,101 @@ +assertInstanceOf(Dispatcher::class, $dispatcher); + $this->assertSame([ + Dispatcher::FOUND, + [ + HomeController::class, + 'index', + ], + [], + ], $dispatcher->dispatch('GET', '/')); + } + + /** + * Test for Routing::getCurrentRoute + * + * @return void + */ + public function testGetCurrentRouteNoParams(): void + { + $this->assertSame('/', Routing::getCurrentRoute()); + } + + /** + * Test for Routing::getCurrentRoute + * + * @return void + */ + public function testGetCurrentRouteGet(): void + { + $_GET['route'] = '/test'; + $this->assertSame('/test', Routing::getCurrentRoute()); + } + + /** + * Test for Routing::getCurrentRoute + * + * @return void + */ + public function testGetCurrentRoutePost(): void + { + $_POST['route'] = '/testpost'; + $this->assertSame('/testpost', Routing::getCurrentRoute()); + } + + /** + * Test for Routing::getCurrentRoute + * + * @return void + */ + public function testGetCurrentRouteGetIsOverPost(): void + { + $_GET['route'] = '/testget'; + $_POST['route'] = '/testpost'; + $this->assertSame('/testget', Routing::getCurrentRoute()); + } + + /** + * Test for Routing::getCurrentRoute + * + * @return void + */ + public function testGetCurrentRouteRedirectDbStructure(): void + { + $_GET['db'] = 'testDB'; + $this->assertSame('/database/structure', Routing::getCurrentRoute()); + } + + /** + * Test for Routing::getCurrentRoute + * + * @return void + */ + public function testGetCurrentRouteRedirectSql(): void + { + $_GET['db'] = 'testDB'; + $_GET['table'] = 'tableTest'; + $this->assertSame('/sql', Routing::getCurrentRoute()); + } +}