TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
|
||||
/**
|
||||
* Extend the Application class to be able to provide a custom version string
|
||||
*/
|
||||
class Application extends \Symfony\Component\Console\Application
|
||||
{
|
||||
/**
|
||||
* Create a custom version of getLongVersion
|
||||
*/
|
||||
public function getLongVersion(): string
|
||||
{
|
||||
return sprintf(
|
||||
'%1$s <info>%2$s</info> (<comment>Application Context:</comment> <info>%3$s</info>) - PHP <info>%4$s</info>',
|
||||
$this->getName(),
|
||||
$this->getVersion(),
|
||||
Environment::getContext(),
|
||||
PHP_VERSION
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
use Symfony\Component\Console\Application as SymfonyConsoleApplication;
|
||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||
use Symfony\Component\Console\Input\ArgvInput;
|
||||
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
use TYPO3\CMS\Core\Adapter\EventDispatcherAdapter as SymfonyEventDispatcher;
|
||||
use TYPO3\CMS\Core\Authentication\CommandLineUserAuthentication;
|
||||
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||
use TYPO3\CMS\Core\Context\Context;
|
||||
use TYPO3\CMS\Core\Context\DateTimeAspect;
|
||||
use TYPO3\CMS\Core\Context\UserAspect;
|
||||
use TYPO3\CMS\Core\Context\VisibilityAspect;
|
||||
use TYPO3\CMS\Core\Context\WorkspaceAspect;
|
||||
use TYPO3\CMS\Core\Core\ApplicationInterface;
|
||||
use TYPO3\CMS\Core\Core\BootService;
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Domain\DateTimeFactory;
|
||||
use TYPO3\CMS\Core\Information\Typo3Version;
|
||||
use TYPO3\CMS\Core\Localization\LanguageServiceFactory;
|
||||
|
||||
/**
|
||||
* Entry point for the TYPO3 Command Line for Commands
|
||||
* In addition to a simple Symfony Command, this also sets up a CLI user
|
||||
*/
|
||||
class CommandApplication implements ApplicationInterface
|
||||
{
|
||||
protected Context $context;
|
||||
|
||||
protected CommandRegistry $commandRegistry;
|
||||
|
||||
protected ConfigurationManager $configurationManager;
|
||||
|
||||
protected BootService $bootService;
|
||||
|
||||
protected LanguageServiceFactory $languageServiceFactory;
|
||||
|
||||
protected SymfonyConsoleApplication $application;
|
||||
|
||||
public function __construct(
|
||||
Context $context,
|
||||
CommandRegistry $commandRegistry,
|
||||
EventDispatcherInterface $eventDispatcher,
|
||||
ConfigurationManager $configurationMananger,
|
||||
BootService $bootService,
|
||||
LanguageServiceFactory $languageServiceFactory
|
||||
) {
|
||||
$this->context = $context;
|
||||
$this->commandRegistry = $commandRegistry;
|
||||
$this->configurationManager = $configurationMananger;
|
||||
$this->bootService = $bootService;
|
||||
$this->languageServiceFactory = $languageServiceFactory;
|
||||
|
||||
$this->checkEnvironmentOrDie();
|
||||
$this->application = new Application('TYPO3 CMS', (new Typo3Version())->getVersion());
|
||||
$this->application->setAutoExit(false);
|
||||
$this->application->setDispatcher($eventDispatcher);
|
||||
$this->application->setCommandLoader($commandRegistry);
|
||||
// Replace default list command with TYPO3 override
|
||||
$this->application->addCommands([$commandRegistry->get('list')]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the Symfony Console application in this TYPO3 application
|
||||
*/
|
||||
public function run()
|
||||
{
|
||||
$input = new ArgvInput();
|
||||
$output = new ConsoleOutput();
|
||||
|
||||
$commandName = $this->getCommandName($input);
|
||||
if ($this->wantsFullBoot($commandName)) {
|
||||
// Do a full container boot if command is not a 1:1 matching low-level command
|
||||
$container = $this->bootService->getContainer();
|
||||
$eventDispatcher = $container->get(SymfonyEventDispatcher::class);
|
||||
$commandRegistry = $container->get(CommandRegistry::class);
|
||||
$this->application->setDispatcher($eventDispatcher);
|
||||
$this->application->setCommandLoader($commandRegistry);
|
||||
$this->context = $container->get(Context::class);
|
||||
|
||||
$realName = $this->resolveShortcut($commandName, $commandRegistry);
|
||||
$isLowLevelCommandShortcut = $realName !== null && !$this->wantsFullBoot($realName);
|
||||
// Load ext_localconf, except if a low level command shortcut was found
|
||||
// or if essential configuration is missing
|
||||
if (!$isLowLevelCommandShortcut && Bootstrap::checkIfEssentialConfigurationExists($this->configurationManager)) {
|
||||
$this->bootService->loadExtLocalconfDatabase();
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure output is not buffered, so command-line output and interaction can take place.
|
||||
// Bootstrap does not open a buffer anymore, but third-party extension code may have done
|
||||
// so while ext_localconf.php files were loaded.
|
||||
while (ob_get_level()) {
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
$this->initializeContext();
|
||||
// create the BE_USER object (not logged in yet)
|
||||
Bootstrap::initializeBackendUser(CommandLineUserAuthentication::class);
|
||||
$GLOBALS['LANG'] = $this->languageServiceFactory->createFromUserPreferences($GLOBALS['BE_USER']);
|
||||
|
||||
$exitCode = $this->application->run($input, $output);
|
||||
// exit codes > 255 are not handled in UNIX
|
||||
if ($exitCode > 255) {
|
||||
$exitCode = 255;
|
||||
}
|
||||
|
||||
exit($exitCode);
|
||||
}
|
||||
|
||||
private function resolveShortcut(string $commandName, CommandRegistry $commandRegistry): ?string
|
||||
{
|
||||
if ($commandRegistry->has($commandName)) {
|
||||
return $commandName;
|
||||
}
|
||||
|
||||
$allCommands = $commandRegistry->getNames();
|
||||
$expr = implode('[^:]*:', array_map(preg_quote(...), explode(':', $commandName))) . '[^:]*';
|
||||
$commands = preg_grep('{^' . $expr . '}', $allCommands);
|
||||
|
||||
if ($commands === false || count($commands) === 0) {
|
||||
$commands = preg_grep('{^' . $expr . '}i', $allCommands);
|
||||
}
|
||||
|
||||
if ($commands === false || count($commands) !== 1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return reset($commands);
|
||||
}
|
||||
|
||||
protected function wantsFullBoot(string $commandName): bool
|
||||
{
|
||||
if ($commandName === 'help') {
|
||||
return true;
|
||||
}
|
||||
return !$this->commandRegistry->has($commandName);
|
||||
}
|
||||
|
||||
protected function getCommandName(ArgvInput $input): string
|
||||
{
|
||||
try {
|
||||
$input->bind($this->application->getDefinition());
|
||||
} catch (ExceptionInterface $e) {
|
||||
// Errors must be ignored, full binding/validation happens later when the console application runs.
|
||||
}
|
||||
|
||||
return $input->getFirstArgument() ?? 'list';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check the script is called from a cli environment.
|
||||
*/
|
||||
protected function checkEnvironmentOrDie(): void
|
||||
{
|
||||
if (PHP_SAPI !== 'cli') {
|
||||
die('Not called from a command line interface (e.g. a shell or scheduler).' . LF);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the Context used for accessing data and finding out the current state of the application
|
||||
*/
|
||||
protected function initializeContext(): void
|
||||
{
|
||||
$this->context->setAspect('date', new DateTimeAspect(DateTimeFactory::createFromTimestamp($GLOBALS['EXEC_TIME'])));
|
||||
$this->context->setAspect('visibility', new VisibilityAspect(true, true, false, true));
|
||||
$this->context->setAspect('workspace', new WorkspaceAspect(0));
|
||||
$this->context->setAspect('backend.user', new UserAspect(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\CommandLoader\CommandLoaderInterface;
|
||||
use Symfony\Component\Console\Descriptor\ApplicationDescription;
|
||||
use Symfony\Component\Console\Exception\CommandNotFoundException;
|
||||
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
|
||||
/**
|
||||
* Registry for Symfony commands, populated via dependency injection tags
|
||||
*/
|
||||
class CommandRegistry implements CommandLoaderInterface, SingletonInterface
|
||||
{
|
||||
/**
|
||||
* Map of command configurations with the command name as key
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
protected array $commandConfigurations = [];
|
||||
|
||||
/**
|
||||
* Map of command aliases
|
||||
*
|
||||
* @var array[]
|
||||
*/
|
||||
protected array $aliases = [];
|
||||
|
||||
public function __construct(
|
||||
protected readonly ContainerInterface $container
|
||||
) {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function has(string $name): bool
|
||||
{
|
||||
return array_key_exists($name, $this->commandConfigurations);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get(string $name): Command
|
||||
{
|
||||
try {
|
||||
return $this->getCommandByIdentifier($name);
|
||||
} catch (UnknownCommandException $e) {
|
||||
throw new CommandNotFoundException($e->getMessage(), [], 1567969355, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getNames(): array
|
||||
{
|
||||
return array_keys($this->commandConfigurations);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configuration form all commands which are schedulable.
|
||||
* By using the #[AsNonSchedulableCommand] attribute, all commands
|
||||
* are filtered out which utilize this attribute, as the Symfony
|
||||
* #[AsCommand] attribute does not allow to set additional meta-data.
|
||||
*/
|
||||
public function getSchedulableCommandsConfiguration(): array
|
||||
{
|
||||
return array_filter(
|
||||
$this->commandConfigurations,
|
||||
static fn(array $configuration): bool => ($configuration['schedulable'] ?? true)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all commands which are allowed for scheduling recurring commands.
|
||||
* @todo Not used by core. Consider deprecating!
|
||||
*/
|
||||
public function getSchedulableCommands(): \Generator
|
||||
{
|
||||
foreach ($this->commandConfigurations as $commandName => $configuration) {
|
||||
if ($configuration['schedulable'] ?? true) {
|
||||
yield $commandName => $this->getInstance($configuration['serviceName']);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @throws UnknownCommandException
|
||||
*/
|
||||
public function getCommandByIdentifier(string $identifier): Command
|
||||
{
|
||||
if (!isset($this->commandConfigurations[$identifier])) {
|
||||
throw new UnknownCommandException(
|
||||
sprintf('Command "%s" has not been registered.', $identifier),
|
||||
1510906768
|
||||
);
|
||||
}
|
||||
|
||||
return $this->getInstance($this->commandConfigurations[$identifier]['serviceName']);
|
||||
}
|
||||
|
||||
protected function getInstance(string $service): Command
|
||||
{
|
||||
return $this->container->get($service);
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function getNamespaces(): array
|
||||
{
|
||||
$namespaces = [];
|
||||
foreach ($this->commandConfigurations as $commandName => $configuration) {
|
||||
if ($configuration['hidden']) {
|
||||
continue;
|
||||
}
|
||||
if ($configuration['aliasFor'] !== null) {
|
||||
continue;
|
||||
}
|
||||
$namespace = $configuration['namespace'];
|
||||
$namespaces[$namespace]['id'] = $namespace;
|
||||
$namespaces[$namespace]['commands'][] = $commandName;
|
||||
}
|
||||
|
||||
ksort($namespaces);
|
||||
foreach ($namespaces as &$commands) {
|
||||
ksort($commands);
|
||||
}
|
||||
|
||||
return $namespaces;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the commands (registered in the given namespace if provided).
|
||||
*
|
||||
* The array keys are the full names and the values the command instances.
|
||||
*
|
||||
* @return array An array of Command descriptors
|
||||
* @internal
|
||||
*/
|
||||
public function filter(?string $namespace = null): array
|
||||
{
|
||||
$commands = [];
|
||||
foreach ($this->commandConfigurations as $commandName => $configuration) {
|
||||
if ($configuration['hidden']) {
|
||||
continue;
|
||||
}
|
||||
if ($namespace !== null && $namespace !== $this->extractNamespace($commandName, substr_count($namespace, ':') + 1)) {
|
||||
continue;
|
||||
}
|
||||
if ($configuration['aliasFor'] !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$commands[$commandName] = $configuration;
|
||||
$commands[$commandName]['aliases'] = $this->aliases[$commandName] ?? [];
|
||||
}
|
||||
|
||||
return $commands;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
public function addLazyCommand(
|
||||
string $commandName,
|
||||
string $serviceName,
|
||||
?string $description = null,
|
||||
bool $hidden = false,
|
||||
bool $schedulable = false,
|
||||
?string $aliasFor = null
|
||||
): void {
|
||||
|
||||
if ($schedulable) {
|
||||
// Workaround: Symfony #[AsCommand] can not utilize an extra 'schedulable' key. Thus, we
|
||||
// evaluate the additional #[AsUnschedulableCommand] as well.
|
||||
try {
|
||||
$reflection = new \ReflectionClass($serviceName);
|
||||
$attributes = $reflection->getAttributes(AsNonSchedulableCommand::class);
|
||||
if ($attributes !== []) {
|
||||
// The presence of the attribute alone is sufficient, no further reflection
|
||||
// or construction is required at this time. Might be needed if the attribute
|
||||
// ever gets properties.
|
||||
$schedulable = false;
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
}
|
||||
}
|
||||
$this->commandConfigurations[$commandName] = [
|
||||
'name' => $aliasFor ?? $commandName,
|
||||
'serviceName' => $serviceName,
|
||||
'description' => $description,
|
||||
'hidden' => $hidden,
|
||||
'schedulable' => $schedulable,
|
||||
'aliasFor' => $aliasFor,
|
||||
'namespace' => $this->extractNamespace($commandName, 1),
|
||||
];
|
||||
|
||||
if ($aliasFor !== null) {
|
||||
$this->aliases[$aliasFor][] = $commandName;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the namespace part of the command name.
|
||||
*
|
||||
* This method is not part of public API and should not be used directly.
|
||||
*
|
||||
* @return string The namespace of the command
|
||||
*/
|
||||
private function extractNamespace(string $name, ?int $limit = null): string
|
||||
{
|
||||
$parts = explode(':', $name, -1);
|
||||
if (count($parts) === 0) {
|
||||
return ApplicationDescription::GLOBAL_NAMESPACE;
|
||||
}
|
||||
|
||||
return implode(':', $limit === null ? $parts : array_slice($parts, 0, $limit));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Core\Console;
|
||||
|
||||
use TYPO3\CMS\Core\Exception;
|
||||
|
||||
/**
|
||||
* Exception thrown when an unregistered command is asked for
|
||||
*/
|
||||
class UnknownCommandException extends Exception {}
|
||||
Reference in New Issue
Block a user