Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/phpmyadmin/phpmyadmin.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/lint-and-analyse-php.yml2
-rw-r--r--composer.json4
-rw-r--r--libraries/classes/Command/TwigLintCommand.php274
-rwxr-xr-xscripts/console6
-rw-r--r--test/classes/Command/TwigLintCommandTest.php188
-rw-r--r--test/classes/_data/file_listing/subfolder/one.ini1
-rw-r--r--test/classes/_data/file_listing/subfolder/zero.txt1
7 files changed, 468 insertions, 8 deletions
diff --git a/.github/workflows/lint-and-analyse-php.yml b/.github/workflows/lint-and-analyse-php.yml
index b9eb9a7d3d..1b0aa68ae1 100644
--- a/.github/workflows/lint-and-analyse-php.yml
+++ b/.github/workflows/lint-and-analyse-php.yml
@@ -75,7 +75,7 @@ jobs:
- name: Check coding-standard
run: composer phpcs
- name: Check twig templates
- run: php scripts/console lint:twig templates --show-deprecations
+ run: composer run twig-lint
analyse-php:
runs-on: ubuntu-latest
diff --git a/composer.json b/composer.json
index 9a3ce40736..7828d719c6 100644
--- a/composer.json
+++ b/composer.json
@@ -96,8 +96,6 @@
"roave/security-advisories": "dev-latest",
"samyoul/u2f-php-server": "^1.1",
"symfony/console": "^5.2.3",
- "symfony/finder": "^5.2.3",
- "symfony/twig-bridge": "^5.2.3",
"tecnickcom/tcpdf": "^6.4.1",
"vimeo/psalm": "^4.8.1"
},
@@ -119,7 +117,7 @@
"@phpunit"
],
"update:baselines": "phpstan analyse --generate-baseline && psalm --set-baseline=psalm-baseline.xml",
- "twig-lint": "php scripts/console lint:twig templates --ansi --show-deprecations"
+ "twig-lint": "php scripts/console lint:twig --ansi --show-deprecations"
},
"config":{
"sort-packages": true,
diff --git a/libraries/classes/Command/TwigLintCommand.php b/libraries/classes/Command/TwigLintCommand.php
new file mode 100644
index 0000000000..07742a2ec7
--- /dev/null
+++ b/libraries/classes/Command/TwigLintCommand.php
@@ -0,0 +1,274 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpMyAdmin\Command;
+
+use PhpMyAdmin\Template;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Twig\Error\Error;
+use Twig\Loader\ArrayLoader;
+use Twig\Source;
+
+use function array_push;
+use function closedir;
+use function count;
+use function explode;
+use function file_get_contents;
+use function is_dir;
+use function is_file;
+use function max;
+use function min;
+use function opendir;
+use function preg_match;
+use function readdir;
+use function restore_error_handler;
+use function set_error_handler;
+use function sprintf;
+
+use const DIRECTORY_SEPARATOR;
+use const E_USER_DEPRECATED;
+
+/**
+ * Command that will validate your template syntax and output encountered errors.
+ * Author: Marc Weistroff <marc.weistroff@sensiolabs.com>
+ * Author: Jérôme Tamarelle <jerome@tamarelle.net>
+ *
+ * Copyright (c) 2013-2021 Fabien Potencier
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is furnished
+ * to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in all
+ * copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+ * THE SOFTWARE.
+ */
+class TwigLintCommand extends Command
+{
+ /** @var string|null */
+ protected static $defaultName = 'lint:twig';
+
+ /** @var string|null */
+ protected static $defaultDescription = 'Lint a Twig template and outputs encountered errors';
+
+ /**
+ * @return void
+ */
+ protected function configure()
+ {
+ $this
+ ->setDescription((string) self::$defaultDescription)
+ ->addOption('show-deprecations', null, InputOption::VALUE_NONE, 'Show deprecations as errors');
+ }
+
+ protected function findFiles(string $baseFolder): array
+ {
+ /* Open the handle */
+ $handle = @opendir($baseFolder);
+ if ($handle === false) {
+ return [];
+ }
+
+ $foundFiles = [];
+
+ while (($file = readdir($handle)) !== false) {
+ if ($file === '.' || $file === '..') {
+ continue;
+ }
+
+ $itemPath = $baseFolder . DIRECTORY_SEPARATOR . $file;
+
+ if (is_dir($itemPath)) {
+ array_push($foundFiles, ...$this->findFiles($itemPath));
+ continue;
+ }
+
+ if (! is_file($itemPath)) {
+ continue;
+ }
+
+ $foundFiles[] = $itemPath;
+ }
+
+ /* Close the handle */
+ closedir($handle);
+
+ return $foundFiles;
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int
+ {
+ $io = new SymfonyStyle($input, $output);
+ $showDeprecations = $input->getOption('show-deprecations');
+
+ if ($showDeprecations) {
+ $prevErrorHandler = set_error_handler(
+ static function (int $level, string $message, string $file, int $line) use (&$prevErrorHandler) {
+ if ($level === E_USER_DEPRECATED) {
+ $templateLine = 0;
+ if (preg_match('/ at line (\d+)[ .]/', $message, $matches)) {
+ $templateLine = (int) $matches[1];
+ }
+
+ throw new Error($message, $templateLine);
+ }
+
+ return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false;
+ }
+ );
+ }
+
+ try {
+ $filesInfo = $this->getFilesInfo(ROOT_PATH . 'templates');
+ } finally {
+ if ($showDeprecations) {
+ restore_error_handler();
+ }
+ }
+
+ return $this->display($output, $io, $filesInfo);
+ }
+
+ protected function getFilesInfo(string $templatesPath): array
+ {
+ $filesInfo = [];
+ $filesFound = $this->findFiles($templatesPath);
+ foreach ($filesFound as $file) {
+ $filesInfo[] = $this->validate($this->getTemplateContents($file), $file);
+ }
+
+ return $filesInfo;
+ }
+
+ /**
+ * Allows easier testing
+ */
+ protected function getTemplateContents(string $filePath): string
+ {
+ return (string) file_get_contents($filePath);
+ }
+
+ private function validate(string $template, string $file): array
+ {
+ $twig = Template::getTwigEnvironment(null);
+
+ $realLoader = $twig->getLoader();
+ try {
+ $temporaryLoader = new ArrayLoader([$file => $template]);
+ $twig->setLoader($temporaryLoader);
+ $nodeTree = $twig->parse($twig->tokenize(new Source($template, $file)));
+ $twig->compile($nodeTree);
+ $twig->setLoader($realLoader);
+ } catch (Error $e) {
+ $twig->setLoader($realLoader);
+
+ return [
+ 'template' => $template,
+ 'file' => $file,
+ 'line' => $e->getTemplateLine(),
+ 'valid' => false,
+ 'exception' => $e,
+ ];
+ }
+
+ return ['template' => $template, 'file' => $file, 'valid' => true];
+ }
+
+ private function display(OutputInterface $output, SymfonyStyle $io, array $filesInfo): int
+ {
+ $errors = 0;
+
+ foreach ($filesInfo as $info) {
+ if ($info['valid'] && $output->isVerbose()) {
+ $io->comment('<info>OK</info>' . ($info['file'] ? sprintf(' in %s', $info['file']) : ''));
+ } elseif (! $info['valid']) {
+ ++$errors;
+ $this->renderException($io, $info['template'], $info['exception'], $info['file']);
+ }
+ }
+
+ if ($errors === 0) {
+ $io->success(sprintf('All %d Twig files contain valid syntax.', count($filesInfo)));
+
+ return Command::SUCCESS;
+ }
+
+ $io->warning(
+ sprintf(
+ '%d Twig files have valid syntax and %d contain errors.',
+ count($filesInfo) - $errors,
+ $errors
+ )
+ );
+
+ return Command::FAILURE;
+ }
+
+ private function renderException(
+ SymfonyStyle $output,
+ string $template,
+ Error $exception,
+ ?string $file = null
+ ): void {
+ $line = $exception->getTemplateLine();
+
+ if ($file) {
+ $output->text(sprintf('<error> ERROR </error> in %s (line %s)', $file, $line));
+ } else {
+ $output->text(sprintf('<error> ERROR </error> (line %s)', $line));
+ }
+
+ // If the line is not known (this might happen for deprecations if we fail at detecting the line for instance),
+ // we render the message without context, to ensure the message is displayed.
+ if ($line <= 0) {
+ $output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage()));
+
+ return;
+ }
+
+ foreach ($this->getContext($template, $line) as $lineNumber => $code) {
+ $output->text(sprintf(
+ '%s %-6s %s',
+ $lineNumber === $line ? '<error> >> </error>' : ' ',
+ $lineNumber,
+ $code
+ ));
+ if ($lineNumber !== $line) {
+ continue;
+ }
+
+ $output->text(sprintf('<error> >> %s</error> ', $exception->getRawMessage()));
+ }
+ }
+
+ private function getContext(string $template, int $line, int $context = 3): array
+ {
+ $lines = explode("\n", $template);
+
+ $position = max(0, $line - $context);
+ $max = min(count($lines), $line - 1 + $context);
+
+ $result = [];
+ while ($position < $max) {
+ $result[$position + 1] = $lines[$position];
+ ++$position;
+ }
+
+ return $result;
+ }
+}
diff --git a/scripts/console b/scripts/console
index 28d9fe84d2..1893d86eac 100755
--- a/scripts/console
+++ b/scripts/console
@@ -4,12 +4,11 @@
use PhpMyAdmin\Command\CacheWarmupCommand;
use PhpMyAdmin\Command\SetVersionCommand;
use PhpMyAdmin\Command\WriteGitRevisionCommand;
+use PhpMyAdmin\Command\TwigLintCommand;
use PhpMyAdmin\Config;
use PhpMyAdmin\Core;
use PhpMyAdmin\DatabaseInterface;
-use PhpMyAdmin\Template;
use PhpMyAdmin\Tests\Stubs\DbiDummy;
-use Symfony\Bridge\Twig\Command\LintCommand;
use Symfony\Component\Console\Application;
if (! defined('ROOT_PATH')) {
@@ -31,12 +30,11 @@ $cfg['environment'] = 'production';
$config = new Config(CONFIG_FILE);
$config->set('environment', $cfg['environment']);
$dbi = new DatabaseInterface(new DbiDummy());
-$twig = Template::getTwigEnvironment(ROOT_PATH . 'twig-templates');
$application = new Application('phpMyAdmin Console Tool');
$application->add(new CacheWarmupCommand());
-$application->add(new LintCommand($twig));
+$application->add(new TwigLintCommand());
$application->add(new SetVersionCommand());
$application->add(new WriteGitRevisionCommand());
diff --git a/test/classes/Command/TwigLintCommandTest.php b/test/classes/Command/TwigLintCommandTest.php
new file mode 100644
index 0000000000..a74314992f
--- /dev/null
+++ b/test/classes/Command/TwigLintCommandTest.php
@@ -0,0 +1,188 @@
+<?php
+
+declare(strict_types=1);
+
+namespace PhpMyAdmin\Tests\Command;
+
+use PhpMyAdmin\Command\TwigLintCommand;
+use PhpMyAdmin\Tests\AbstractTestCase;
+use Symfony\Component\Console\Command\Command;
+use Twig\Error\SyntaxError;
+use Twig\Source;
+
+use function class_exists;
+use function sort;
+
+use const SORT_NATURAL;
+use const SORT_REGULAR;
+
+/**
+ * @covers \PhpMyAdmin\Command\TwigLintCommand
+ */
+class TwigLintCommandTest extends AbstractTestCase
+{
+ /** @var TwigLintCommand */
+ private $command;
+
+ public function setUp(): void
+ {
+ global $cfg, $config;
+
+ if (! class_exists(Command::class)) {
+ $this->markTestSkipped('The Symfony Console is missing');
+ }
+
+ parent::loadContainerBuilder();
+ $cfg['environment'] = 'development';
+ $config = null;
+
+ $this->command = new TwigLintCommand();
+ }
+
+ /**
+ * @covers getTemplateContents
+ */
+ public function testGetTemplateContents(): void
+ {
+ $contents = $this->callFunction($this->command, TwigLintCommand::class, 'getTemplateContents', [
+ ROOT_PATH . 'test/classes/_data/file_listing/subfolder/one.ini',
+ ]);
+
+ $this->assertSame('key=value' . "\n", $contents);
+ }
+
+ /**
+ * @covers findFiles
+ */
+ public function testFindFiles(): void
+ {
+ $filesFound = $this->callFunction($this->command, TwigLintCommand::class, 'findFiles', [
+ ROOT_PATH . 'test/classes/_data/file_listing',
+ ]);
+
+ // Sort results to avoid file system test specific failures
+ sort($filesFound, SORT_NATURAL);
+
+ $this->assertEquals([
+ ROOT_PATH . 'test/classes/_data/file_listing/one.txt',
+ ROOT_PATH . 'test/classes/_data/file_listing/subfolder/one.ini',
+ ROOT_PATH . 'test/classes/_data/file_listing/subfolder/zero.txt',
+ ROOT_PATH . 'test/classes/_data/file_listing/two.md',
+ ], $filesFound);
+ }
+
+ /**
+ * @covers getFilesInfo
+ */
+ public function testGetFilesInfo(): void
+ {
+ $filesInfos = $this->callFunction($this->command, TwigLintCommand::class, 'getFilesInfo', [
+ ROOT_PATH . 'test/classes/_data/file_listing',
+ ]);
+
+ // Sort results to avoid file system test specific failures
+ sort($filesInfos, SORT_REGULAR);
+
+ $this->assertEquals([
+ [
+ 'template' => '',
+ 'file' => ROOT_PATH . 'test/classes/_data/file_listing/one.txt',
+ 'valid' => true,
+ ],
+ [
+ 'template' => '',
+ 'file' => ROOT_PATH . 'test/classes/_data/file_listing/two.md',
+ 'valid' => true,
+ ],
+ [
+ 'template' => '0000' . "\n",
+ 'file' => ROOT_PATH . 'test/classes/_data/file_listing/subfolder/zero.txt',
+ 'valid' => true,
+ ],
+ [
+ 'template' => 'key=value' . "\n",
+ 'file' => ROOT_PATH . 'test/classes/_data/file_listing/subfolder/one.ini',
+ 'valid' => true,
+ ],
+ ], $filesInfos);
+ }
+
+ /**
+ * @covers validate
+ */
+ public function testGetFilesInfoInvalidFile(): void
+ {
+ $command = $this->getMockBuilder(TwigLintCommand::class)
+ ->onlyMethods(['getTemplateContents', 'findFiles'])
+ ->getMock();
+
+ $command->expects($this->exactly(1))
+ ->method('findFiles')
+ ->willReturn(
+ [
+ 'foo.twig',
+ 'foo-invalid.twig',
+ ]
+ );
+
+ $command->expects($this->exactly(2))
+ ->method('getTemplateContents')
+ ->withConsecutive(
+ ['foo.twig'],
+ ['foo-invalid.twig']
+ )
+ ->willReturnOnConsecutiveCalls(
+ '{{ file }}',
+ '{{ file }'
+ );
+
+ $filesFound = $this->callFunction($command, TwigLintCommand::class, 'getFilesInfo', [
+ ROOT_PATH . 'test/classes/_data/file_listing',
+ ]);
+
+ $this->assertEquals([
+ [
+ 'template' => '{{ file }}',
+ 'file' => 'foo.twig',
+ 'valid' => true,
+ ],
+ [
+ 'template' => '{{ file }',
+ 'file' => 'foo-invalid.twig',
+ 'valid' => false,
+ 'line' => 1,
+ 'exception' => new SyntaxError('Unexpected "}".', 1, new Source(
+ '{{ file }',
+ 'foo-invalid.twig'
+ )),
+ ],
+ ], $filesFound);
+ }
+
+ /**
+ * @covers getContext
+ */
+ public function testGetContext(): void
+ {
+ $context = $this->callFunction($this->command, TwigLintCommand::class, 'getContext', [
+ '{{ file }',
+ 0,
+ ]);
+
+ $this->assertEquals([1 => '{{ file }'], $context);
+
+ $context = $this->callFunction($this->command, TwigLintCommand::class, 'getContext', [
+ '{{ file }',
+ 3,
+ ]);
+
+ $this->assertEquals([1 => '{{ file }'], $context);
+
+ $context = $this->callFunction($this->command, TwigLintCommand::class, 'getContext', [
+ '{{ file }',
+ 5,
+ ]);
+
+ $this->assertEquals([], $context);
+ }
+}
diff --git a/test/classes/_data/file_listing/subfolder/one.ini b/test/classes/_data/file_listing/subfolder/one.ini
new file mode 100644
index 0000000000..7b89edbafe
--- /dev/null
+++ b/test/classes/_data/file_listing/subfolder/one.ini
@@ -0,0 +1 @@
+key=value
diff --git a/test/classes/_data/file_listing/subfolder/zero.txt b/test/classes/_data/file_listing/subfolder/zero.txt
new file mode 100644
index 0000000000..739d79706d
--- /dev/null
+++ b/test/classes/_data/file_listing/subfolder/zero.txt
@@ -0,0 +1 @@
+0000