Update website
This commit is contained in:
parent
41ce1aa076
commit
ea0eb1c6e0
4222 changed files with 721797 additions and 14 deletions
|
@ -0,0 +1,193 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
100
admin/phpMyAdmin/libraries/classes/Command/SetVersionCommand.php
Normal file
100
admin/phpMyAdmin/libraries/classes/Command/SetVersionCommand.php
Normal file
|
@ -0,0 +1,100 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
271
admin/phpMyAdmin/libraries/classes/Command/TwigLintCommand.php
Normal file
271
admin/phpMyAdmin/libraries/classes/Command/TwigLintCommand.php
Normal file
|
@ -0,0 +1,271 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,129 @@
|
|||
<?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;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue