Update website

This commit is contained in:
Guilhem Lavaux 2025-02-11 21:30:02 +01:00
parent 0a686aeb9a
commit c4ffa0f6ee
4360 changed files with 1727 additions and 718385 deletions

View file

@ -1,193 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Command;
use PhpMyAdmin\Config;
use PhpMyAdmin\DatabaseInterface;
use PhpMyAdmin\Routing;
use PhpMyAdmin\Template;
use PhpMyAdmin\Tests\Stubs\DbiDummy;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Twig\Cache\CacheInterface;
use function file_put_contents;
use function is_file;
use function json_encode;
use function sprintf;
use function str_contains;
use function str_replace;
use const CACHE_DIR;
final class CacheWarmupCommand extends Command
{
/** @var string|null */
protected static $defaultName = 'cache:warmup';
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->addOption('twig-po', null, null, 'Warm up twig templates and write file mappings.');
$this->addOption(
'env',
null,
InputArgument::OPTIONAL,
'Defines the environment (production or development) for twig warmup',
'production'
);
$this->setHelp('The <info>%command.name%</info> command warms up the cache of the Twig templates.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string $env */
$env = $input->getOption('env');
if ($input->getOption('twig') === true && $input->getOption('routing') === true) {
$output->writeln('Please specify --twig or --routing');
return Command::FAILURE;
}
if ($input->getOption('twig') === true) {
return $this->warmUpTwigCache($output, $env, false);
}
if ($input->getOption('twig-po') === true) {
return $this->warmUpTwigCache($output, $env, true);
}
if ($input->getOption('routing') === true) {
return $this->warmUpRoutingCache($output);
}
$output->writeln('Warming up all caches.', OutputInterface::VERBOSITY_VERBOSE);
$twigCode = $this->warmUpTwigCache($output, $env, false);
if ($twigCode !== 0) {
$output->writeln('Twig cache generation had an error.');
return $twigCode;
}
$routingCode = $this->warmUpRoutingCache($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 Command::SUCCESS;
}
private function warmUpRoutingCache(OutputInterface $output): int
{
$output->writeln('Warming up the routing cache', OutputInterface::VERBOSITY_VERBOSE);
Routing::getDispatcher();
if (is_file(Routing::ROUTES_CACHE_FILE)) {
$output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE);
return Command::SUCCESS;
}
$output->writeln(
sprintf(
'Warm up did not work, the folder "%s" is probably not writable.',
CACHE_DIR
),
OutputInterface::VERBOSITY_NORMAL
);
return Command::FAILURE;
}
private function warmUpTwigCache(
OutputInterface $output,
string $environment,
bool $writeReplacements
): int {
global $cfg, $config, $dbi;
$output->writeln('Warming up the twig cache', OutputInterface::VERBOSITY_VERBOSE);
$config = new Config(CONFIG_FILE);
$cfg['environment'] = $environment;
$config->set('environment', $cfg['environment']);
$dbi = new DatabaseInterface(new DbiDummy());
$tmpDir = ROOT_PATH . 'twig-templates';
$twig = Template::getTwigEnvironment($tmpDir);
$output->writeln('Searching for files...', OutputInterface::VERBOSITY_VERY_VERBOSE);
$templates = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(Template::TEMPLATES_FOLDER),
RecursiveIteratorIterator::LEAVES_ONLY
);
/** @var CacheInterface $twigCache */
$twigCache = $twig->getCache(false);
$replacements = [];
$output->writeln(
'Twig debug is: ' . ($twig->isDebug() ? 'enabled' : 'disabled'),
OutputInterface::VERBOSITY_DEBUG
);
$output->writeln('Warming templates', OutputInterface::VERBOSITY_VERY_VERBOSE);
foreach ($templates as $file) {
// Skip test files
if (str_contains($file->getPathname(), '/test/')) {
continue;
}
// force compilation
if (! $file->isFile() || $file->getExtension() !== 'twig') {
continue;
}
$name = str_replace(Template::TEMPLATES_FOLDER . '/', '', $file->getPathname());
$output->writeln('Loading: ' . $name, OutputInterface::VERBOSITY_DEBUG);
/** @psalm-suppress InternalMethod */
$template = $twig->loadTemplate($twig->getTemplateClass($name), $name);
if (! $writeReplacements) {
continue;
}
// Generate line map
/** @psalm-suppress InternalMethod */
$cacheFilename = $twigCache->generateKey($name, $twig->getTemplateClass($name));
$template_file = 'templates/' . $name;
$cache_file = str_replace($tmpDir, 'twig-templates', $cacheFilename);
/** @psalm-suppress InternalMethod */
$replacements[$cache_file] = [$template_file, $template->getDebugInfo()];
}
if (! $writeReplacements) {
$output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE);
return Command::SUCCESS;
}
$output->writeln('Writing replacements...', OutputInterface::VERBOSITY_VERY_VERBOSE);
// Store replacements in JSON
if (file_put_contents($tmpDir . '/replace.json', (string) json_encode($replacements)) === false) {
return Command::FAILURE;
}
$output->writeln('Replacements written done.', OutputInterface::VERBOSITY_VERBOSE);
$output->writeln('Warm up done.', OutputInterface::VERBOSITY_VERBOSE);
return Command::SUCCESS;
}
}

View file

@ -1,82 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function file_get_contents;
use function file_put_contents;
use function intval;
use function is_array;
use function json_decode;
use function preg_replace_callback;
use const ROOT_PATH;
final class FixPoTwigCommand extends Command
{
/** @var string|null */
protected static $defaultName = 'fix-po-twig';
private const POT_FILE = ROOT_PATH . 'po/phpmyadmin.pot';
private const REPLACE_FILE = ROOT_PATH . 'twig-templates/replace.json';
protected function configure(): void
{
$this->setDescription('Fixes POT file for Twig templates');
$this->setHelp(
'The <info>%command.name%</info> command fixes the Twig file name and line number in the'
. ' POT file to match the Twig template and not the compiled Twig file.'
);
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$replaceFile = file_get_contents(self::REPLACE_FILE);
if ($replaceFile === false) {
return Command::FAILURE;
}
$replacements = json_decode($replaceFile, true);
if (! is_array($replacements)) {
return Command::FAILURE;
}
/* Read pot file */
$pot = file_get_contents(self::POT_FILE);
if ($pot === false) {
return Command::FAILURE;
}
/* Do the replacements */
$pot = preg_replace_callback(
'@(twig-templates[0-9a-f/]*.php):([0-9]*)@',
static function (array $matches) use ($replacements): string {
$filename = $matches[1];
$line = intval($matches[2]);
$replace = $replacements[$filename];
foreach ($replace[1] as $cacheLine => $result) {
if ($line >= $cacheLine) {
return $replace[0] . ':' . $result;
}
}
return $replace[0] . ':0';
},
$pot
);
if ($pot === null) {
return Command::FAILURE;
}
if (file_put_contents(self::POT_FILE, $pot) === false) {
return Command::FAILURE;
}
return Command::SUCCESS;
}
}

View file

@ -1,100 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Command;
use RangeException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use function file_put_contents;
use function preg_match;
use function sprintf;
final class SetVersionCommand extends Command
{
/** @var string */
protected static $defaultName = 'set-version';
/** @var string */
private static $generatedClassTemplate = <<<'PHP'
<?php
declare(strict_types=1);
namespace PhpMyAdmin;
use const VERSION_SUFFIX;
/**
* This class is generated by scripts/console.
*
* @see \PhpMyAdmin\Command\SetVersionCommand
*/
final class Version
{
// The VERSION_SUFFIX constant is defined at libraries/constants.php
public const VERSION = '%1$u.%2$u.%3$u%4$s' . VERSION_SUFFIX;
public const SERIES = '%1$u.%2$u';
public const MAJOR = %1$u;
public const MINOR = %2$u;
public const PATCH = %3$u;
public const ID = %1$u%2$02u%3$02u;
public const PRE_RELEASE_NAME = '%5$s';
public const IS_DEV = %6$s;
}
PHP;
protected function configure(): void
{
$this->setDescription('Sets the version number');
$this->setHelp('This command generates the PhpMyAdmin\Version class based on the version number provided.');
$this->addArgument('version', InputArgument::REQUIRED, 'The version number');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string $version */
$version = $input->getArgument('version');
$generatedClass = $this->getGeneratedClass($version);
if (! $this->writeGeneratedClassFile($generatedClass)) {
return Command::FAILURE;
}
$output->writeln('PhpMyAdmin\Version class successfully generated!');
return Command::SUCCESS;
}
private function getGeneratedClass(string $version): string
{
// Do not allow any major below 5
$return = preg_match('/^([5-9]+)\.(\d{1,2})\.(\d{1,2})(-([a-z0-9]+))?$/', $version, $matches);
if ($return === false || $return === 0) {
throw new RangeException('The version number is in the wrong format: ' . $version);
}
return sprintf(
self::$generatedClassTemplate,
$matches[1],
$matches[2],
$matches[3],
$matches[4] ?? '',
$matches[5] ?? '',
($matches[5] ?? '') === 'dev' ? 'true' : 'false'
);
}
private function writeGeneratedClassFile(string $generatedClass): bool
{
$result = file_put_contents(ROOT_PATH . 'libraries/classes/Version.php', $generatedClass);
return $result !== false;
}
}

View file

@ -1,271 +0,0 @@
<?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';
protected function configure(): void
{
$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;
}
}

View file

@ -1,129 +0,0 @@
<?php
declare(strict_types=1);
namespace PhpMyAdmin\Command;
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 function file_put_contents;
use function is_string;
use function shell_exec;
use function sprintf;
use function str_replace;
use function trim;
class WriteGitRevisionCommand extends Command
{
/** @var string */
protected static $defaultName = 'write-revision-info';
/** @var string */
private static $generatedClassTemplate = <<<'PHP'
<?php
declare(strict_types=1);
/**
* This file is generated by scripts/console.
*
* @see \PhpMyAdmin\Command\WriteGitRevisionCommand
*/
return [
'revision' => '%s',
'revisionUrl' => '%s',
'branch' => '%s',
'branchUrl' => '%s',
];
PHP;
protected function configure(): void
{
$this->setDescription('Write Git revision');
$this->addOption(
'remote-commit-url',
null,
InputOption::VALUE_OPTIONAL,
'The remote URL to a commit',
'https://github.com/phpmyadmin/phpmyadmin/commit/%s'
);
$this->addOption(
'remote-branch-url',
null,
InputOption::VALUE_OPTIONAL,
'The remote URL to a branch',
'https://github.com/phpmyadmin/phpmyadmin/tree/%s'
);
$this->setHelp('This command generates the revision-info.php file from Git data.');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
/** @var string $commitUrlFormat */
$commitUrlFormat = $input->getOption('remote-commit-url');
/** @var string $branchUrlFormat */
$branchUrlFormat = $input->getOption('remote-branch-url');
$generatedClass = $this->getRevisionInfo($commitUrlFormat, $branchUrlFormat);
if ($generatedClass === null) {
$output->writeln('No revision information detected.');
return Command::SUCCESS;
}
if (! $this->writeGeneratedFile($generatedClass)) {
return Command::FAILURE;
}
$output->writeln('revision-info.php successfully generated!');
return Command::SUCCESS;
}
private function getRevisionInfo(string $commitUrlFormat, string $branchUrlFormat): ?string
{
$revisionText = $this->gitCli('describe --always');
if ($revisionText === null) {
return null;
}
$commitHash = $this->gitCli('log -1 --format="%H"');
if ($commitHash === null) {
return null;
}
$branchName = $this->gitCli('symbolic-ref -q HEAD') ?? $this->gitCli('name-rev --name-only HEAD 2>/dev/null');
if ($branchName === null) {
return null;
}
$branchName = trim(str_replace('refs/heads/', '', $branchName));
return sprintf(
self::$generatedClassTemplate,
trim($revisionText),
sprintf($commitUrlFormat, trim($commitHash)),
trim($branchName),
sprintf($branchUrlFormat, $branchName)
);
}
protected function gitCli(string $command): ?string
{
/** @psalm-suppress ForbiddenCode */
$output = shell_exec('git ' . $command);
return is_string($output) ? $output : null;
}
private function writeGeneratedFile(string $generatedClass): bool
{
$result = file_put_contents(ROOT_PATH . 'revision-info.php', $generatedClass);
return $result !== false;
}
}