TYPO3 v15 dev-main snapshot ()

This commit is contained in:
2026-08-10 22:31:09 +02:00
commit af8cc155b5
6818 changed files with 642608 additions and 0 deletions
+102
View File
@@ -0,0 +1,102 @@
<?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\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Command\Output\MessageRenderer;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\SystemResource\Publishing\SystemResourcePublisherInterface;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
use TYPO3\CMS\Install\Middleware\AssetPublishing;
class AssetPublishCommand extends Command
{
public function __construct(
protected readonly BootService $bootService,
protected readonly PackageManager $packageManager,
protected readonly MessageRenderer $messageRenderer,
) {
parent::__construct('asset:publish');
}
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this->setDescription('Publish public assets.');
$this->setHelp(
'Publishes public assets. '
. 'Needs to be run after composer install.'
);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$failsafeContainer = $this->bootService->getFailsafeContainer();
$failsafeResourcePublisher = $failsafeContainer->has(AssetPublishing::class) ? $failsafeContainer->get(SystemResourcePublisherInterface::class) : null;
try {
$container = $this->bootService->loadExtLocalconfDatabase(false, false);
} catch (\Throwable $e) {
if ($output->isVerbose()) {
throw $e;
}
$output->writeln('<error>Can not initialize dependency injection container. Increase verbosity to get the full error message.</error>');
return self::FAILURE;
}
$resourcePublisher = $container->get(SystemResourcePublisherInterface::class);
$output->getFormatter()->setStyle('bold', new OutputFormatterStyle(null, null, ['bold']));
$output->writeln('<bold>Publishing assets from extensions…</bold>');
$exitCode = self::SUCCESS;
foreach ($this->packageManager->getAvailablePackages() as $package) {
$messages = $resourcePublisher->publishResources($package);
if ($package->isPartOfMinimalUsableSystem()) {
// Publish resources for install tool, if it is installed
$failsafeResourcePublisher?->publishResources($package);
}
$exitCode = $this->determineExitCode($exitCode, $messages);
$this->messageRenderer->renderAll($messages, $output);
}
$output->writeln('<bold>done.</bold>');
return $exitCode;
}
private function determineExitCode(int $currentCode, FlashMessageQueue $queue): int
{
if ($currentCode === self::FAILURE) {
return self::FAILURE;
}
foreach ($queue->getAllMessages() as $message) {
if ($message->getSeverity() === ContextualFeedbackSeverity::ERROR) {
return self::FAILURE;
}
}
return $currentCode;
}
}
+124
View File
@@ -0,0 +1,124 @@
<?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\Command;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
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 TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Event\CacheFlushEvent;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\DependencyInjection\Cache\ContainerBackend;
class CacheFlushCommand extends Command
{
public function __construct(
protected readonly BootService $bootService,
protected readonly FrontendInterface $dependencyInjectionCache
) {
parent::__construct('cache:flush');
}
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this->setDescription('Flush TYPO3 caches.');
$this->setHelp(
'Clears TYPO3 caches. '
. 'Useful after code changes during development or after deployments. '
. 'You can flush a specific cache group (system, pages, di) or all caches.'
);
$this->setDefinition([
new InputOption('group', 'g', InputOption::VALUE_OPTIONAL, 'Cache group to flush (system, pages, di, or all).', 'all'),
]);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$group = $input->getOption('group') ?? 'all';
$this->flushDependencyInjectionCaches($group);
if ($group === 'di') {
if ($output->isVerbose()) {
$io->success('Dependency Injection caches flushed.');
}
return Command::SUCCESS;
}
$container = $this->bootService->getContainer(true);
$this->flushCoreCaches($group, $container);
$this->bootService->loadExtLocalconfDatabase(false, true);
$eventDispatcher = $container->get(EventDispatcherInterface::class);
$groups = $group === 'all' ? $container->get(CacheManager::class)->getCacheGroups() : [$group];
$event = new CacheFlushEvent($groups);
$eventDispatcher->dispatch($event);
if (count($event->getErrors()) > 0) {
$io->error('Errors occurred while flushing caches.');
foreach ($event->getErrors() as $error) {
$io->error($error);
}
return Command::FAILURE;
}
if ($output->isVerbose()) {
if ($group === 'all') {
$io->success('All caches flushed.');
} else {
$io->success(sprintf('Caches for group "%s" flushed.', $group));
}
}
return Command::SUCCESS;
}
protected function flushDependencyInjectionCaches(string $group): void
{
if ($group !== 'di' && $group !== 'system' && $group !== 'all') {
return;
}
if ($this->dependencyInjectionCache->getBackend() instanceof ContainerBackend) {
$diCacheBackend = $this->dependencyInjectionCache->getBackend();
// We need to remove using the forceFlush method because the DI cache backend disables the flush method
$diCacheBackend->forceFlush();
}
}
protected function flushCoreCaches(string $group, ContainerInterface $container): void
{
if ($group !== 'system' && $group !== 'all') {
return;
}
$container->get('cache.core')->flush();
}
}
+80
View File
@@ -0,0 +1,80 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
#[AsCommand('cache:flushtags', 'Cache clearing caches with tags.')]
class CacheFlushTagsCommand extends Command
{
public function __construct(
protected readonly CacheManager $cacheManager,
) {
parent::__construct();
}
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this->setDescription('Flush TYPO3 caches with tags.');
$this->setHelp('This command can be used to clear the caches with specific tags, for example after code updates in local development and after deployments.');
$this->setDefinition([
new InputArgument(
'tags',
InputArgument::REQUIRED,
'Array of tags (specified as comma separated values) to flush.'
),
new InputOption(
'groups',
'g',
InputOption::VALUE_REQUIRED,
'Array of groups (specified as comma separated values) for which to flush tags. If no group is specified, caches of all groups are flushed.',
'all'
),
]);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$groups = GeneralUtility::trimExplode(',', $input->getOption('groups') ?? '', true);
$tags = GeneralUtility::trimExplode(',', $input->getArgument('tags') ?? '', true);
foreach ($groups as $group) {
if ($group === 'all') {
$this->cacheManager->flushCachesByTags($tags);
continue;
}
$this->cacheManager->flushCachesInGroupByTags($group, $tags);
}
return Command::SUCCESS;
}
}
+108
View File
@@ -0,0 +1,108 @@
<?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\Command;
use Psr\EventDispatcher\EventDispatcherInterface;
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 TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Event\CacheWarmupEvent;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;
use TYPO3\CMS\Core\Configuration\Extension\ExtLocalconfFactory;
use TYPO3\CMS\Core\Configuration\Tca\TcaFactory;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\DependencyInjection\ContainerBuilder;
use TYPO3\CMS\Core\Package\PackageManager;
class CacheWarmupCommand extends Command
{
public function __construct(
protected readonly ContainerBuilder $containerBuilder,
protected readonly PackageManager $packageManager,
protected readonly BootService $bootService,
protected readonly FrontendInterface $dependencyInjectionCache
) {
parent::__construct('cache:warmup');
}
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this->setDescription('Warmup TYPO3 caches.');
$this->setHelp(
<<<'EOF'
This command is useful for deployments to warmup caches during release preparation.
<fg=yellow>
Cache warming does not work if the PHP version used to execute the command differs from
the PHP version used in the web context.
See: https://docs.typo3.org/permalink/changelog:important-107649-1760090777
</>
EOF
);
$this->setDefinition([
new InputOption('group', 'g', InputOption::VALUE_OPTIONAL, 'The cache group to warmup (system, pages, di or all)', 'all'),
]);
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$group = $input->getOption('group') ?? 'all';
if ($group === 'di' || $group === 'system' || $group === 'all') {
$this->containerBuilder->warmupCache($this->packageManager, $this->dependencyInjectionCache);
if ($group === 'di') {
return Command::SUCCESS;
}
}
$container = $this->bootService->getContainer();
$allowExtFileCaches = true;
if ($group === 'system' || $group === 'all') {
$allowExtFileCaches = false;
$container->get(ExtLocalconfFactory::class)->createCacheEntry();
}
// Perform a full boot to load localconf (requirement for extensions and for TCA loading).
$this->bootService->loadExtLocalconfDatabase(false, $allowExtFileCaches);
if ($group === 'system' || $group === 'all') {
$tcaFactory = $container->get(TcaFactory::class);
$tcaFactory->createBaseTcaCacheFile($GLOBALS['TCA']);
}
$eventDispatcher = $container->get(EventDispatcherInterface::class);
$groups = $group === 'all' ? $container->get(CacheManager::class)->getCacheGroups() : [$group];
$event = new CacheWarmupEvent($groups);
$eventDispatcher->dispatch($event);
if (count($event->getErrors()) > 0) {
return Command::FAILURE;
}
return Command::SUCCESS;
}
}
+358
View File
@@ -0,0 +1,358 @@
<?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\Command;
use Psr\Container\ContainerInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Completion\CompletionInput;
use Symfony\Component\Console\Completion\CompletionSuggestions;
use Symfony\Component\Console\Exception\InvalidOptionException;
use Symfony\Component\Console\Exception\RuntimeException;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Logger\ConsoleLogger;
use Symfony\Component\Console\Output\ConsoleOutputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ChoiceQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use Symfony\Component\DependencyInjection\Attribute\AutowireLocator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\Messenger\EventListener\StopWorkerOnFailureLimitListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMemoryLimitListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnMessageLimitListener;
use Symfony\Component\Messenger\EventListener\StopWorkerOnTimeLimitListener;
use Symfony\Component\Messenger\MessageBusInterface;
use Symfony\Component\Messenger\Transport\Sync\SyncTransport;
use Symfony\Component\Messenger\Worker;
use TYPO3\CMS\Core\EventDispatcher\ListenerProvider;
/**
* Almost full version of the symfony command with the same name.
*/
#[AsCommand(name: 'messenger:consume', description: 'Consume messages')]
class ConsumeMessagesCommand extends Command
{
private const int DEFAULT_KEEPALIVE_INTERVAL = 5;
private ?Worker $worker = null;
private ?LoggerInterface $logger = null;
private array $receiverNames;
public function __construct(
#[Autowire(service: 'messenger.bus.default')]
private readonly MessageBusInterface $messageBus,
#[AutowireLocator('messenger.receiver', indexAttribute: 'identifier')]
private readonly ContainerInterface $receiverLocator,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly ListenerProvider $listenerProvider,
private readonly ContainerInterface $container,
#[AutowireIterator('messenger.receiver', indexAttribute: 'identifier')]
iterable $receiverNamesIterator,
private readonly array $busIds = [],
#[AutowireLocator('messenger.rate_limiter', indexAttribute: 'identifier')]
private readonly ?ContainerInterface $rateLimiterLocator = null,
private readonly ?array $signals = null,
) {
$this->receiverNames = array_keys([...$receiverNamesIterator]);
parent::__construct();
}
protected function configure(): void
{
$defaultReceiverName = count($this->receiverNames) === 1 ? current($this->receiverNames) : null;
$this
->setDefinition(
[
new InputArgument(
'receivers',
InputArgument::IS_ARRAY,
'Names of the receivers/transports to consume in order of priority',
$defaultReceiverName ? [$defaultReceiverName] : []
),
new InputOption('limit', 'l', InputOption::VALUE_REQUIRED, 'Limit the number of received messages'),
new InputOption('failure-limit', 'f', InputOption::VALUE_REQUIRED, 'The number of failed messages the worker can consume'),
new InputOption('memory-limit', 'm', InputOption::VALUE_REQUIRED, 'The memory limit the worker can consume'),
new InputOption('time-limit', 't', InputOption::VALUE_REQUIRED, 'The time limit in seconds the worker can handle new messages'),
new InputOption('sleep', null, InputOption::VALUE_REQUIRED, 'Seconds to sleep before asking for new messages after no messages were found', 1),
new InputOption('bus', 'b', InputOption::VALUE_REQUIRED, 'Name of the bus to which received messages should be dispatched (if not passed, bus is determined automatically)'),
new InputOption('queues', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Limit receivers to only consume from the specified queues'),
new InputOption('all', null, InputOption::VALUE_NONE, 'Consume messages from all receivers'),
new InputOption('keepalive', null, InputOption::VALUE_OPTIONAL, 'Whether to use the transport\'s keepalive mechanism if implemented', self::DEFAULT_KEEPALIVE_INTERVAL),
]
)
->setHelp(
<<<'EOF'
The <info>%command.name%</info> command consumes messages and dispatches them to the message bus.
<info>php %command.full_name% <receiver-name></info>
To receive from multiple transports, pass each name:
<info>php %command.full_name% receiver1 receiver2</info>
Use the --limit option to limit the number of messages received:
<info>php %command.full_name% <receiver-name> --limit=10</info>
Use the --failure-limit option to stop the worker when the given number of failed messages is reached:
<info>php %command.full_name% <receiver-name> --failure-limit=2</info>
Use the --memory-limit option to stop the worker if it exceeds a given memory usage limit. You can use shorthand byte values [K, M or G]:
<info>php %command.full_name% <receiver-name> --memory-limit=128M</info>
Use the --time-limit option to stop the worker when the given time limit (in seconds) is reached.
If a message is being handled, the worker will stop after the processing is finished:
<info>php %command.full_name% <receiver-name> --time-limit=3600</info>
Use the --bus option to specify the message bus to dispatch received messages
to instead of trying to determine it automatically. This is required if the
messages didn't originate from Messenger:
<info>php %command.full_name% <receiver-name> --bus=event_bus</info>
Use the --queues option to limit a receiver to only certain queues (only supported by some receivers):
<info>php %command.full_name% <receiver-name> --queues=fasttrack</info>
Use the --all option to consume from all receivers:
<info>php %command.full_name% --all</info>
EOF
)
;
}
protected function initialize(InputInterface $input, OutputInterface $output): void
{
if ($input->hasParameterOption('--keepalive')) {
$this->getApplication()->setAlarmInterval((int)($input->getOption('keepalive') ?? self::DEFAULT_KEEPALIVE_INTERVAL));
}
}
protected function interact(InputInterface $input, OutputInterface $output): void
{
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
if ($input->getOption('all')) {
return;
}
if ($this->receiverNames && !$input->getArgument('receivers')) {
if (count($this->receiverNames) === 1) {
$input->setArgument('receivers', $this->receiverNames);
return;
}
$io->block('Which transports/receivers do you want to consume?', null, 'fg=white;bg=blue', ' ', true);
$io->writeln('Choose which receivers you want to consume messages from in order of priority.');
$io->writeln(sprintf('Hint: to consume from multiple, use a list of their names, e.g. <comment>%s</comment>', implode(', ', $this->receiverNames)));
$question = new ChoiceQuestion('Select receivers to consume:', $this->receiverNames, 0);
$question->setMultiselect(true);
$input->setArgument('receivers', $io->askQuestion($question));
}
if (!$input->getArgument('receivers')) {
throw new RuntimeException('Please pass at least one receiver.', 1605305001);
}
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->logger = new ConsoleLogger($output);
$receivers = [];
$rateLimiters = [];
$receiverNames = $input->getOption('all') ? $this->receiverNames : $input->getArgument('receivers');
foreach ($receiverNames as $receiverName) {
if (!$this->receiverLocator->has($receiverName)) {
$message = sprintf('The receiver "%s" does not exist.', $receiverName);
if ($this->receiverNames) {
$message .= sprintf(' Valid receivers are: %s.', implode(', ', $this->receiverNames));
}
throw new RuntimeException($message, 1605305002);
}
$receiver = $this->receiverLocator->get($receiverName);
if ($receiver instanceof SyncTransport) {
$idx = array_search($receiverName, $receiverNames);
unset($receiverNames[$idx]);
continue;
}
$receivers[$receiverName] = $receiver;
if ($this->rateLimiterLocator?->has($receiverName)) {
$rateLimiters[$receiverName] = $this->rateLimiterLocator->get($receiverName);
}
}
$stopsWhen = [];
if (null !== $limit = $input->getOption('limit')) {
if (!is_numeric($limit) || $limit <= 0) {
throw new InvalidOptionException(sprintf('Option "limit" must be a positive integer, "%s" passed.', $limit), 1605305003);
}
$stopsWhen[] = "processed {$limit} messages";
$this->addSubscriber(new StopWorkerOnMessageLimitListener((int)$limit, $this->logger));
}
if ($failureLimit = $input->getOption('failure-limit')) {
$stopsWhen[] = "reached {$failureLimit} failed messages";
$this->addSubscriber(new StopWorkerOnFailureLimitListener((int)$failureLimit, $this->logger));
}
if ($memoryLimit = $input->getOption('memory-limit')) {
$stopsWhen[] = "exceeded {$memoryLimit} of memory";
$this->addSubscriber(new StopWorkerOnMemoryLimitListener($this->convertToBytes($memoryLimit), $this->logger));
}
if (null !== $timeLimit = $input->getOption('time-limit')) {
if (!is_numeric($timeLimit) || $timeLimit <= 0) {
throw new InvalidOptionException(sprintf('Option "time-limit" must be a positive integer, "%s" passed.', $timeLimit), 1605305004);
}
$stopsWhen[] = "been running for {$timeLimit}s";
$this->addSubscriber(new StopWorkerOnTimeLimitListener((int)$timeLimit, $this->logger));
}
$stopsWhen[] = 'received a stop signal via the messenger:stop-workers command';
$io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output);
$io->success(sprintf('Consuming messages from transport%s "%s".', count($receivers) > 1 ? 's' : '', implode(', ', $receiverNames)));
if ($stopsWhen) {
$last = array_pop($stopsWhen);
$stopsWhen = ($stopsWhen ? implode(', ', $stopsWhen) . ' or ' : '') . $last;
$io->comment("The worker will automatically exit once it has {$stopsWhen}.");
}
$io->comment('Quit the worker with CONTROL-C.');
if ($output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
$io->comment('Re-run the command with a -vv option to see logs about consumed messages.');
}
$this->worker = new Worker($receivers, $this->messageBus, $this->eventDispatcher, $this->logger, $rateLimiters);
$options = [
'sleep' => $input->getOption('sleep') * 1000000,
];
if ($queues = $input->getOption('queues')) {
$options['queues'] = $queues;
}
try {
$this->worker->run($options);
} finally {
$this->worker = null;
}
return Command::SUCCESS;
}
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
{
if ($input->mustSuggestArgumentValuesFor('receivers')) {
$suggestions->suggestValues(array_diff($this->receiverNames, array_diff($input->getArgument('receivers'), [$input->getCompletionValue()])));
return;
}
if ($input->mustSuggestOptionValuesFor('bus')) {
$suggestions->suggestValues($this->busIds);
}
}
public function getSubscribedSignals(): array
{
return $this->signals ?? (extension_loaded('pcntl') ? [SIGTERM, SIGINT, SIGQUIT, SIGALRM] : []);
}
public function handleSignal(int $signal, int|false $previousExitCode = 0): int|false
{
if (!$this->worker) {
return false;
}
if (defined('SIGALRM') && $signal === SIGALRM) {
$this->logger?->debug('Sending keepalive request.', ['transport_names' => $this->worker->getMetadata()->getTransportNames()]);
$this->worker->keepalive($this->getApplication()->getAlarmInterval());
return false;
}
$this->logger?->info('Received signal {signal}.', ['signal' => $signal, 'transport_names' => $this->worker->getMetadata()->getTransportNames()]);
$this->worker->stop();
return false;
}
private function convertToBytes(string $memoryLimit): int
{
$memoryLimit = strtolower($memoryLimit);
$max = ltrim($memoryLimit, '+');
if (str_starts_with($max, '0x')) {
$max = intval($max, 16);
} elseif (str_starts_with($max, '0')) {
$max = intval($max, 8);
} else {
$max = (float)$max;
}
switch (substr(rtrim($memoryLimit, 'b'), -1)) {
case 't': $max *= 1024;
// no break
case 'g': $max *= 1024;
// no break
case 'm': $max *= 1024;
// no break
case 'k': $max *= 1024;
}
return (int)$max;
}
/**
* @todo: This method show be removed when we can add event subscribers dynamically.
*/
private function addSubscriber(EventSubscriberInterface $subscriber): void
{
$this->container->set($subscriber::class, $subscriber);
foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
$this->listenerProvider->addListener(
$eventName,
$subscriber::class,
is_string($params) ? $params : $params[0],
);
}
}
}
@@ -0,0 +1,128 @@
<?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\Command\Descriptor;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\Descriptor\ApplicationDescription;
use Symfony\Component\Console\Descriptor\TextDescriptor as SymfonyTextDescriptor;
use Symfony\Component\Console\Helper\Helper;
use Symfony\Component\Console\Input\InputDefinition;
use TYPO3\CMS\Core\Console\CommandRegistry;
/**
* Text descriptor.
*
* @internal
*/
class TextDescriptor extends SymfonyTextDescriptor
{
private CommandRegistry $commandRegistry;
private bool $degraded;
public function __construct(CommandRegistry $commandRegistry, bool $degraded)
{
$this->commandRegistry = $commandRegistry;
$this->degraded = $degraded;
}
/**
* {@inheritdoc}
*/
protected function describeApplication(Application $application, array $options = []): void
{
$describedNamespace = $options['namespace'] ?? null;
$rawOutput = $options['raw_text'] ?? false;
$commands = $this->commandRegistry->filter($describedNamespace);
if ($rawOutput) {
$width = $this->getColumnWidth(['' => ['commands' => array_keys($commands)]]);
foreach ($commands as $command) {
$this->write(sprintf("%-{$width}s %s\n", $command['name'], strip_tags($command['description'] ?? '')), true);
}
return;
}
if ($this->degraded) {
$this->write("<error>Failed to boot dependency injection, only lowlevel commands are available.</error>\n\n", true);
}
$namespaces = $this->commandRegistry->getNamespaces();
$help = $application->getHelp();
if ($help !== '') {
$this->write($help . "\n\n", true);
}
$this->write("<comment>Usage:</comment>\n", true);
$this->write(" command [options] [arguments]\n\n");
$this->describeInputDefinition(new InputDefinition($application->getDefinition()->getOptions()));
$this->write("\n\n");
if ($describedNamespace) {
$this->write(sprintf('<comment>Available commands for the "%s" namespace:</comment>', $describedNamespace), true);
$namespace = $namespaces[$describedNamespace] ?? [];
$width = $this->getColumnWidth(['' => $namespace]);
$this->describeNamespace($namespace, $commands, $width);
} else {
$this->write('<comment>Available commands:</comment>', true);
// calculate max. width based on available commands per namespace
$width = $this->getColumnWidth($namespaces);
foreach ($namespaces as $namespace) {
if ($namespace['id'] !== ApplicationDescription::GLOBAL_NAMESPACE) {
$this->write("\n");
$this->write(' <comment>' . $namespace['id'] . '</comment>', true);
}
$this->describeNamespace($namespace, $commands, $width);
}
}
$this->write("\n");
if ($this->degraded) {
$this->write("\n<error>Failed to boot dependency injection, only lowlevel commands are available.</error>\n", true);
}
}
private function describeNamespace(array $namespace, array $commands, int $width): void
{
foreach ($namespace['commands'] as $name) {
$this->write("\n");
$spacingWidth = $width - Helper::length($name);
$command = $commands[$name];
$aliases = count($command['aliases']) ? '[' . implode('|', $command['aliases']) . '] ' : '';
$this->write(sprintf(' <info>%s</info>%s%s', $name, str_repeat(' ', $spacingWidth), $aliases . $command['description']), true);
}
}
private function getColumnWidth(array $namespaces): int
{
$widths = [];
foreach ($namespaces as $name => $namespace) {
$widths[] = Helper::length($name);
foreach ($namespace['commands'] as $commandName) {
$widths[] = Helper::length($commandName);
}
}
return $widths ? max($widths) + 2 : 0;
}
}
+64
View File
@@ -0,0 +1,64 @@
<?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\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Core\ClassLoadingInformation;
use TYPO3\CMS\Core\Core\Environment;
/**
* Command for dumping the class-loading information.
*/
class DumpAutoloadCommand extends Command
{
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this->setName('dumpautoload');
$this->setDescription('Updates class loading information in non-composer mode.');
$this->setHelp('This command is only needed during development. The extension manager takes care of creating or updating this info properly during extension (de-)activation.');
$this->setAliases([
'extensionmanager:extension:dumpclassloadinginformation',
'extension:dumpclassloadinginformation',
]);
}
/**
* This command is not needed in composer mode.
*/
public function isEnabled(): bool
{
return !Environment::isComposerMode();
}
/**
* Dumps the class loading information
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
ClassLoadingInformation::dumpClassLoadingInformation();
$io->success('Class loading information has been updated.');
return Command::SUCCESS;
}
}
@@ -0,0 +1,27 @@
<?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\Command\Exception;
use TYPO3\CMS\Core\Exception as Typo3InstallException;
/**
* This exception is thrown in UpgradeWizardRunCommand, if a requested wizard exists but does not need to make any changes.
*
* @internal for use in UpgradeWizardRunCommand only and not part of public API.
*/
final class WizardDoesNotNeedToMakeChangesException extends Typo3InstallException {}
@@ -0,0 +1,27 @@
<?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\Command\Exception;
use TYPO3\CMS\Core\Exception as Typo3InstallException;
/**
* This exception is thrown in the UpgradeWizardRunCommand, if a requested wizard is already marked as done.
*
* @internal for use in UpgradeWizardRunCommand only and not part of public API.
*/
final class WizardMarkedAsDoneException extends Typo3InstallException {}
@@ -0,0 +1,27 @@
<?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\Command\Exception;
use TYPO3\CMS\Core\Exception as Typo3InstallException;
/**
* This exception is thrown in UpgradeWizardRunCommand, if a requested wizard could not be found.
*
* @internal for use in UpgradeWizardRunCommand only and not part of public API.
*/
final class WizardNotFoundException extends Typo3InstallException {}
+125
View File
@@ -0,0 +1,125 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\FormatterHelper;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Helper\TableCell;
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 TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
use TYPO3\CMS\Core\Package\PackageManager;
/**
* Command for listing all extensions known to the system.
*
* If the command is called with the verbose option, also shows the description of the package.
*/
#[AsCommand('extension:list', 'Shows the list of extensions available to the system.')]
#[AsNonSchedulableCommand]
class ExtensionListCommand extends Command
{
public function __construct(private readonly PackageManager $packageManager)
{
parent::__construct();
}
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this
->addOption(
'all',
'a',
InputOption::VALUE_NONE,
'Also display currently inactive/uninstalled extensions.'
)
->addOption(
'inactive',
'i',
InputOption::VALUE_NONE,
'Only show inactive/uninstalled extensions available for installation.'
);
}
/**
* Shows the list of all extensions
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$onlyShowInactiveExtensions = $input->getOption('inactive');
$showAlsoInactiveExtensions = $input->getOption('all');
if ($onlyShowInactiveExtensions) {
$packages = $this->packageManager->getAvailablePackages();
$io->title('All inactive/currently uninstalled extensions');
} elseif ($showAlsoInactiveExtensions) {
$packages = $this->packageManager->getAvailablePackages();
$io->title('All installed (= active) and available (= inactive/currently uninstalled) extensions');
} else {
$packages = $this->packageManager->getActivePackages();
$io->title('All installed (= active) extensions');
}
$table = new Table($output);
$table->setHeaders([
'Extension Key',
'Version',
'Type',
'Status',
]);
$table->setColumnWidths([30, 10, 8, 6]);
/** @var FormatterHelper $formatter */
$formatter = $this->getHelper('formatter');
foreach ($packages as $package) {
$isActivePackage = $this->packageManager->isPackageActive($package->getPackageKey());
if (!$package->getPackageMetaData()->isExtensionType()) {
continue;
}
// Do not show the package if it is active but we only want to see inactive packages
if ($onlyShowInactiveExtensions && $isActivePackage) {
continue;
}
$type = $package->getPackageMetaData()->isFrameworkType() ? 'System' : 'Local';
// Ensure that the inactive extensions are shown as well
if ($onlyShowInactiveExtensions || ($showAlsoInactiveExtensions && !$isActivePackage)) {
$status = '<comment>inactive</comment>';
} else {
$status = '<info>active</info>';
}
$table->addRow([$package->getPackageKey(), $package->getPackageMetaData()->getVersion(), $type, $status]);
// Also show the title of the extension, if verbose option is set
if ($output->isVerbose()) {
$title = (string)$package->getPackageMetaData()->getTitle();
$table->addRow([new TableCell(' ' . $formatter->truncate($title, 80) . "\n\n", ['colspan' => 4])]);
}
}
$table->render();
return Command::SUCCESS;
}
}
+67
View File
@@ -0,0 +1,67 @@
<?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\Command;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Command\ListCommand as SymfonyListCommand;
use Symfony\Component\Console\Helper\DescriptorHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Command\Descriptor\TextDescriptor;
use TYPO3\CMS\Core\Console\CommandRegistry;
use TYPO3\CMS\Core\Core\BootService;
/**
* ListCommand displays the list of all available commands for the application.
*/
class ListCommand extends SymfonyListCommand
{
public function __construct(
protected readonly ContainerInterface $failsafeContainer,
protected readonly BootService $bootService,
) {
parent::__construct();
}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$degraded = false;
try {
$container = $this->bootService->getContainer();
} catch (\Throwable $e) {
$container = $this->failsafeContainer;
$degraded = true;
}
$commandRegistry = $container->get(CommandRegistry::class);
$helper = new DescriptorHelper();
$helper->register('txt', new TextDescriptor($commandRegistry, $degraded));
$helper->describe($output, $this->getApplication(), [
'format' => $input->getOption('format'),
'raw_text' => $input->getOption('raw'),
'namespace' => $input->getArgument('namespace'),
]);
return Command::SUCCESS;
}
}
@@ -0,0 +1,52 @@
<?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\Command\Output;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
class MessageRenderer
{
public function renderAll(FlashMessageQueue $queue, OutputInterface $output): void
{
foreach ($queue->getAllMessages() as $message) {
$this->renderOne($message, $output);
}
}
public function renderOne(FlashMessage $message, OutputInterface $output): void
{
[$style, $verbosity] = match ($message->getSeverity()) {
ContextualFeedbackSeverity::INFO,
ContextualFeedbackSeverity::NOTICE,
ContextualFeedbackSeverity::OK => ['info', $output::VERBOSITY_VERBOSE],
ContextualFeedbackSeverity::WARNING => ['comment', $output::VERBOSITY_NORMAL],
ContextualFeedbackSeverity::ERROR => ['error', $output::VERBOSITY_NORMAL],
};
$formattedMessage = sprintf(
"<%s><bold>%s</bold>\n%s</%s>\n",
$style,
$message->getTitle(),
$message->getMessage(),
$style,
);
$output->writeln($formattedMessage, $verbosity);
}
}
+83
View File
@@ -0,0 +1,83 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
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 TYPO3\CMS\Core\Mail\DelayedTransportInterface;
use TYPO3\CMS\Core\Mail\FileSpool;
use TYPO3\CMS\Core\Mail\MailerInterface;
/**
* Command for sending spooled messages.
*
* Inspired and partially taken from symfony's swiftmailer package, adapted for Symfony/Mailer.
*
* @link https://github.com/symfony/swiftmailer-bundle/blob/master/Command/SendEmailCommand.php
*/
#[AsCommand('mailer:spool:send', 'Sends emails from the spool.', ['swiftmailer:spool:send'])]
class SendEmailCommand extends Command
{
public function __construct(protected readonly MailerInterface $mailer)
{
parent::__construct('mailer:spool:send');
}
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this
->addOption('message-limit', null, InputOption::VALUE_REQUIRED, 'The maximum number of messages to send.')
->addOption('time-limit', null, InputOption::VALUE_REQUIRED, 'The time limit for sending messages (in seconds).')
->addOption('recover-timeout', null, InputOption::VALUE_REQUIRED, 'The timeout for recovering messages that have taken too long to send (in seconds).');
}
/**
* Executes the mailer command
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$transport = $this->mailer->getTransport();
if ($transport instanceof DelayedTransportInterface) {
if ($transport instanceof FileSpool) {
$transport->setMessageLimit((int)$input->getOption('message-limit'));
$transport->setTimeLimit((int)$input->getOption('time-limit'));
$recoverTimeout = (int)$input->getOption('recover-timeout');
if ($recoverTimeout) {
$transport->recover($recoverTimeout);
} else {
$transport->recover();
}
}
$sent = $transport->flushQueue($this->mailer->getRealTransport());
$io->comment($sent . ' emails sent');
return Command::SUCCESS;
}
$io->error('The Mailer Transport is not set to "spool".');
return Command::FAILURE;
}
}
+106
View File
@@ -0,0 +1,106 @@
<?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\Command;
use Psr\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Console\Attribute\AsCommand;
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 TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Package\Event\PackagesMayHaveChangedEvent;
use TYPO3\CMS\Core\Package\PackageManager;
use TYPO3\CMS\Core\Package\PackageSetup;
/**
* Command for setting up all extensions via CLI.
*/
#[AsCommand('extension:setup', 'Set up extensions and perform database migrations.')]
#[AsNonSchedulableCommand]
class SetupExtensionsCommand extends Command
{
public function __construct(
private readonly PackageManager $packageManager,
private readonly EventDispatcherInterface $eventDispatcher,
private readonly PackageSetup $packageSetup,
) {
parent::__construct();
}
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this
->setDescription('Set up extensions')
->setHelp(
<<<'EOD'
Setup all extensions or the given extension by extension key. This must
be performed after new extensions are required via Composer.
The command performs all necessary setup operations, such as database
schema changes, static data import, distribution files import etc.
The given extension keys must be recognized by TYPO3 or will be ignored.
EOD
)
->addOption(
'extension',
'-e',
InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
'Only set up extensions with given key'
);
}
/**
* Sets up one or all extensions
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
Bootstrap::initializeBackendAuthentication();
$this->eventDispatcher->dispatch(new PackagesMayHaveChangedEvent());
$io = new SymfonyStyle($input, $output);
$extensionKeys = $input->getOption('extension');
$packagesToSetUp = $this->packageManager->getActivePackages();
if (!empty($extensionKeys)) {
$packagesToSetUp = array_filter(
$packagesToSetUp,
static function ($extKey) use ($extensionKeys) {
return in_array($extKey, $extensionKeys, true);
},
ARRAY_FILTER_USE_KEY
);
}
if (empty($packagesToSetUp)) {
$io->error('Given extension(s) "' . implode(', ', $extensionKeys) . '" not found in the system.');
return Command::FAILURE;
}
$messages = $this->packageSetup->setup($packagesToSetUp);
foreach ($messages as $message) {
$io->warning($message->getMessage());
}
$io->success('Extension(s) "' . implode(', ', array_keys($packagesToSetUp)) . '" successfully set up.');
return Command::SUCCESS;
}
}
+96
View File
@@ -0,0 +1,96 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
use TYPO3\CMS\Core\Site\SiteFinder;
/**
* Command for listing all configured sites
*/
#[AsCommand('site:list', 'Shows the list of sites available to the system.')]
#[AsNonSchedulableCommand]
class SiteListCommand extends Command
{
public function __construct(protected readonly SiteFinder $siteFinder)
{
parent::__construct();
}
/**
* Shows a table with all configured sites
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$sites = $this->siteFinder->getAllSites();
if (empty($sites)) {
$io->title('No sites configured');
$io->note('Configure new sites in the "Sites" module.');
return Command::SUCCESS;
}
$io->title('All configured sites');
$table = new Table($output);
$table->setHeaders([
'Identifier',
'Root PID',
'Base URL',
'Language',
'Locale',
'Status',
]);
foreach ($sites as $site) {
$baseUrls = [];
$languages = [];
$locales = [];
$status = [];
foreach ($site->getAllLanguages() as $language) {
$baseUrls[] = (string)$language->getBase();
$languages[] = sprintf(
'%s (id:%d)',
$language->getTitle(),
$language->getLanguageId()
);
$locales[] = (string)$language->getLocale();
$status[] = $language->isEnabled()
? '<fg=green>enabled</>'
: '<fg=yellow>disabled</>';
}
$table->addRow(
[
'<options=bold>' . $site->getIdentifier() . '</>',
$site->getRootPageId(),
implode("\n", $baseUrls),
implode("\n", $languages),
implode("\n", $locales),
implode("\n", $status),
]
);
}
$table->render();
return Command::SUCCESS;
}
}
+125
View File
@@ -0,0 +1,125 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\Table;
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 TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Site\Set\SetRegistry;
/**
* Command for listing all configured sites
*/
#[AsCommand('site:sets:list', 'Shows the list of available site sets.')]
#[AsNonSchedulableCommand]
class SiteSetsListCommand extends Command
{
public function __construct(
protected readonly SetRegistry $setRegistry
) {
parent::__construct();
}
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this->setDefinition([
new InputOption('all', 'a', InputOption::VALUE_NONE, 'Show all sets, including hidden ones.'),
]);
}
/**
* Shows a table with all configured sites
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$showAll = $input->getOption('all') ?? false;
$sets = $this->setRegistry->getAllSets();
if ($sets === []) {
$io->title('No site sets configured');
$io->note('Configure new sites by placing a Configuration/Sets/MySetName/config.yaml in an extension.');
return Command::SUCCESS;
}
$io->title('All configured site sets');
$table = new Table($output);
$table->setHeaders([
'Name',
'Label',
'Dependencies',
]);
foreach ($sets as $set) {
if ($set->hidden && !$showAll) {
continue;
}
$table->addRow(
[
'<options=bold>' . $set->name . ($set->hidden ? ' (hidden)' : '') . '</>',
$this->getLanguageService()->sL($set->label),
implode(', ', [
...$set->dependencies,
...array_map(static fn(string $d): string => '(' . $d . ')', $set->optionalDependencies),
]),
]
);
}
$table->render();
$invalidSets = $this->setRegistry->getInvalidSets();
if ($invalidSets !== []) {
$io->newLine();
$io->newLine();
$io->title('Invalid site set configurations');
$table = new Table($output);
$table->setHeaders([
'Set',
'Error',
]);
foreach ($invalidSets as $invalidSet) {
$table->addRow(
[
$invalidSet['name'],
sprintf(
$this->getLanguageService()->sL($invalidSet['error']->getLabel()),
$invalidSet['name'],
$invalidSet['context'],
),
]
);
}
$table->render();
}
return Command::SUCCESS;
}
protected function getLanguageService(): LanguageService
{
return $GLOBALS['LANG'];
}
}
+65
View File
@@ -0,0 +1,65 @@
<?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\Command;
use Symfony\Component\Console\Attribute\AsCommand;
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 Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Yaml\Yaml;
use TYPO3\CMS\Core\Attribute\AsNonSchedulableCommand;
use TYPO3\CMS\Core\Site\SiteFinder;
/**
* Command for showing the configuration of a site
*/
#[AsCommand('site:show', 'Shows the configuration of the specified site.')]
#[AsNonSchedulableCommand]
class SiteShowCommand extends Command
{
public function __construct(protected readonly SiteFinder $siteFinder)
{
parent::__construct();
}
/**
* Defines the allowed options for this command
*/
protected function configure(): void
{
$this->addArgument(
'identifier',
InputArgument::REQUIRED,
'The identifier of the site'
);
}
/**
* Shows the configuration of a site
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$site = $this->siteFinder->getSiteByIdentifier($input->getArgument('identifier'));
$io->title('Site configuration for ' . $input->getArgument('identifier'));
$io->block(Yaml::dump($site->getConfiguration(), 4));
return Command::SUCCESS;
}
}
@@ -0,0 +1,171 @@
<?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\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\ProgressBar;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\NullOutput;
use Symfony\Component\Console\Output\OutputInterface;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\Localization\LanguagePackService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Core function for updating language packs
* @internal to provide "language:update" command in `EXT:core` and not part of public API.
*/
class UpdateLanguagePackCommand extends Command
{
public function __construct(
string $name,
private readonly BootService $bootService
) {
parent::__construct($name);
}
/**
* Configure the command by defining the name, options and arguments
*/
protected function configure(): void
{
$this->setDescription('Update the language files of all activated extensions')
->addArgument(
'locales',
InputArgument::IS_ARRAY | InputArgument::OPTIONAL,
'Provide iso codes separated by space to update only selected language packs. Example `bin/typo3 language:update de ja`.',
[]
)
->addOption(
'no-progress',
null,
InputOption::VALUE_NONE,
'Disable progress bar.'
)
->addOption(
'fail-on-warnings',
null,
InputOption::VALUE_NONE,
'Fail command when translation was not found on the server.'
)
->addOption(
'skip-extension',
null,
InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
'Skip extension. Useful for e.g. for not public extensions, which don\'t have language packs.',
[]
);
}
/**
* Update language packs of all active languages for all active extensions
*
* @throws \InvalidArgumentException
* @throws \TYPO3\CMS\Core\Cache\Exception\NoSuchCacheException
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$container = $this->bootService->loadExtLocalconfDatabase();
$languagePackService = $container->get(LanguagePackService::class);
$noProgress = $input->getOption('no-progress') || $output->isVerbose();
$isos = (array)$input->getArgument('locales');
$skipExtensions = (array)$input->getOption('skip-extension');
$failOnWarnings = (bool)$input->getOption('fail-on-warnings');
$status = Command::SUCCESS;
// Condition for the scheduler command, e.g. "de fr pt"
if (count($isos) === 1 && str_contains($isos[0], ' ')) {
$isos = GeneralUtility::trimExplode(' ', $isos[0], true);
}
if (empty($isos)) {
$isos = $languagePackService->getActiveLanguages();
}
$output->writeln('<info>Updating language packs</info>');
$extensions = $languagePackService->getExtensionLanguagePackDetails();
if ($noProgress) {
$progressBarOutput = new NullOutput();
} else {
$progressBarOutput = $output;
}
$downloads = [];
$packageCount = 0;
foreach ($extensions as $extensionKey => $extension) {
if (in_array($extensionKey, $skipExtensions, true)) {
continue;
}
$downloads[$extensionKey] = [];
foreach ($extension['packs'] as $iso => $pack) {
if (!in_array($iso, $isos, true)) {
continue;
}
$downloads[$extensionKey][] = $iso;
$packageCount++;
}
if (empty($downloads[$extensionKey])) {
unset($downloads[$extensionKey]);
}
}
$progressBar = new ProgressBar($progressBarOutput, $packageCount);
foreach ($downloads as $extension => $extensionLanguages) {
foreach ($extensionLanguages as $iso) {
if ($noProgress) {
$output->writeln(sprintf('<info>Fetching pack for language "%s" for extension "%s"</info>', $iso, $extension), $output::VERBOSITY_VERY_VERBOSE);
}
$result = $languagePackService->languagePackDownload($extension, $iso);
if ($noProgress) {
switch ($result) {
case 'failed':
$output->writeln(sprintf('<comment>Fetching pack for language "%s" for extension "%s" failed</comment>', $iso, $extension));
break;
case 'update':
$output->writeln(sprintf('<info>Updated pack for language "%s" for extension "%s"</info>', $iso, $extension));
break;
case 'new':
$output->writeln(sprintf('<info>Fetching new pack for language "%s" for extension "%s"</info>', $iso, $extension));
break;
case 'skipped':
$output->writeln(sprintf('<info>Skipped pack for language "%s" for extension "%s"</info>', $iso, $extension));
break;
}
}
// Fail only if --fail-on-warnings is set and a language pack was not found.
if ($failOnWarnings && $result === 'failed') {
$status = Command::FAILURE;
}
$progressBar->advance();
}
}
$languagePackService->setLastUpdatedIsoCode($isos);
$progressBar->finish();
$output->writeln('');
// Flush language cache
GeneralUtility::makeInstance(CacheManager::class)->getCache('l10n')->flush();
return $status;
}
}
@@ -0,0 +1,134 @@
<?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\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 Symfony\Component\Console\Style\StyleInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Service\UpgradeWizardsService;
use TYPO3\CMS\Core\Upgrades\ChattyInterface;
use TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface;
/**
* Upgrade wizard command for listing wizards
*
* @internal not part of public API.
*/
class UpgradeWizardListCommand extends Command
{
private UpgradeWizardsService $upgradeWizardsService;
/**
* @var OutputInterface|StyleInterface
*/
private $output;
public function __construct(
string $name,
private readonly BootService $bootService,
) {
parent::__construct($name);
}
/**
* Bootstrap running of upgradeWizards
*/
protected function bootstrap(): void
{
$this->upgradeWizardsService = $this->bootService
->loadExtLocalconfDatabase(false, false)
->get(UpgradeWizardsService::class);
Bootstrap::initializeBackendAuthentication();
}
/**
* Configure the command by defining the name, options and arguments
*/
protected function configure(): void
{
$this->setDescription('List available upgrade wizards.')
->addOption(
'all',
'a',
InputOption::VALUE_NONE,
'Include wizards already done.'
);
}
/**
* List available upgrade wizards. If -all is given, already done wizards are listed, too.
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->output = new SymfonyStyle($input, $output);
$this->bootstrap();
$wizards = [];
$all = $input->getOption('all');
foreach ($this->upgradeWizardsService->getUpgradeWizardIdentifiers() as $identifier) {
$upgradeWizard = $this->getWizard($identifier, (bool)$all);
if ($upgradeWizard !== null) {
$wizardInfo = [
'identifier' => $identifier,
'title' => $upgradeWizard->getTitle(),
'description' => wordwrap($upgradeWizard->getDescription()),
];
if ($all === true) {
$wizardInfo['status'] = $this->upgradeWizardsService->isWizardDone($identifier) ? 'DONE' : 'AVAILABLE';
}
$wizards[] = $wizardInfo;
}
}
if (empty($wizards)) {
$this->output->success('No wizards available.');
} elseif ($all === true) {
$this->output->table(['Identifier', 'Title', 'Description', 'Status'], $wizards);
} else {
$this->output->table(['Identifier', 'Title', 'Description'], $wizards);
}
return Command::SUCCESS;
}
/**
* Get Wizard instance by identifier
* Returns null if wizard is already done
*/
protected function getWizard(string $identifier, bool $all = false): ?UpgradeWizardInterface
{
// already done
if (!$all && $this->upgradeWizardsService->isWizardDone($identifier)) {
return null;
}
$wizard = $this->upgradeWizardsService->getUpgradeWizard($identifier);
if ($wizard === null) {
return null;
}
if ($wizard instanceof ChattyInterface) {
$wizard->setOutput($this->output);
}
return !$all ? $wizard->updateNecessary() ? $wizard : null : $wizard;
}
}
@@ -0,0 +1,85 @@
<?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\Command;
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 Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Service\UpgradeWizardsService;
/**
* Upgrade wizard command for marking wizards as undone
*
* @internal not part of public API.
*/
class UpgradeWizardMarkUndoneCommand extends Command
{
private UpgradeWizardsService $upgradeWizardsService;
public function __construct(
string $name,
private readonly BootService $bootService,
) {
parent::__construct($name);
}
/**
* Bootstrap running of upgradeWizards
*/
protected function bootstrap(): void
{
$this->upgradeWizardsService = $this->bootService
->loadExtLocalconfDatabase(false, false)
->get(UpgradeWizardsService::class);
Bootstrap::initializeBackendAuthentication();
}
/**
* Configure the command by defining the name, options and arguments
*/
protected function configure(): void
{
$this->setDescription('Mark upgrade wizard as undone.')
->addArgument(
'wizardIdentifier',
InputArgument::REQUIRED
);
}
/**
* Mark an upgrade wizard as undone
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$this->bootstrap();
$wizardIdentifier = (string)$input->getArgument('wizardIdentifier');
$wizardInformation = $this->upgradeWizardsService->getWizardInformationByIdentifier($wizardIdentifier);
$hasBeenMarkedUndone = $this->upgradeWizardsService->markWizardUndone($wizardIdentifier);
if ($hasBeenMarkedUndone) {
$io->success('The wizard "' . $wizardInformation['title'] . '" has been marked as undone.');
return Command::SUCCESS;
}
$io->error('The wizard "' . $wizardInformation['title'] . '" could not be marked undone, because it was most likely not yet run.');
return Command::FAILURE;
}
}
+316
View File
@@ -0,0 +1,316 @@
<?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\Command;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
use Symfony\Component\Console\Style\SymfonyStyle;
use TYPO3\CMS\Core\Command\Exception\WizardDoesNotNeedToMakeChangesException;
use TYPO3\CMS\Core\Command\Exception\WizardMarkedAsDoneException;
use TYPO3\CMS\Core\Command\Exception\WizardNotFoundException;
use TYPO3\CMS\Core\Configuration\Exception\SettingsWriteException;
use TYPO3\CMS\Core\Core\BootService;
use TYPO3\CMS\Core\Core\Bootstrap;
use TYPO3\CMS\Core\Service\DatabaseUpgradeWizardsService;
use TYPO3\CMS\Core\Service\Exception\ConfigurationChangedException;
use TYPO3\CMS\Core\Service\Exception\SilentConfigurationUpgradeReadonlyException;
use TYPO3\CMS\Core\Service\SilentConfigurationUpgradeService;
use TYPO3\CMS\Core\Service\UpgradeWizardsService;
use TYPO3\CMS\Core\Upgrades\ChattyInterface;
use TYPO3\CMS\Core\Upgrades\ConfirmableInterface;
use TYPO3\CMS\Core\Upgrades\PrerequisiteCollection;
use TYPO3\CMS\Core\Upgrades\RepeatableInterface;
use TYPO3\CMS\Core\Upgrades\UpgradeWizardInterface;
use TYPO3\CMS\Core\Utility\GeneralUtility;
/**
* Upgrade wizard command for running wizards
*
* @internal not part of public API.
*/
class UpgradeWizardRunCommand extends Command
{
private const int MAX_SILENT_UPGRADE_TRIES = 100;
private UpgradeWizardsService $upgradeWizardsService;
private DatabaseUpgradeWizardsService $databaseUpgradeWizardsService;
/**
* @var OutputInterface|\Symfony\Component\Console\Style\StyleInterface
*/
private $output;
/**
* @var InputInterface
*/
private $input;
public function __construct(
string $name,
private readonly BootService $bootService,
private readonly SilentConfigurationUpgradeService $configurationUpgradeService,
) {
parent::__construct($name);
}
/**
* Bootstrap running of upgrade wizard,
* ensure database is utf-8
*/
protected function bootstrap(): void
{
$container = $this->bootService
->loadExtLocalconfDatabase(false, false);
$this->upgradeWizardsService = $container->get(UpgradeWizardsService::class);
$this->databaseUpgradeWizardsService = $container->get(DatabaseUpgradeWizardsService::class);
Bootstrap::initializeBackendAuthentication();
$this->databaseUpgradeWizardsService->isDatabaseCharsetUtf8()
?: $this->databaseUpgradeWizardsService->setDatabaseCharsetUtf8();
// Ensure SilentConfigurationUpdates are also run on CLI, if necessary. Due to the fact that single silent
// upgrade tasks throwing a `ConfigurationChangedException` on changes and stopping the execution of following
// upgrades, we need to handle this in a loop handling this exception. Other errors leading to a direct stop,
// with an additionally concrete handling for readonly `settings.php`. To be safe against future end-less loops,
// a max-try check is used.
$loopSafety = 0;
do {
try {
$this->configurationUpgradeService->execute();
$success = true;
} catch (ConfigurationChangedException) {
// Due to the fact that single silent upgrade tasks emits and stops the upgrade chain, we need to
// handle this as not successful and continue processing the upgrade chain. Therefore, the loop.
$success = false;
} catch (SettingsWriteException $e) {
// Readonly or not-writable `settings.php`. Throw a more meaning full exception and stop the upgrade.
throw new SilentConfigurationUpgradeReadonlyException(1688462973, $e);
}
$loopSafety++;
} while ($success === false && $loopSafety < self::MAX_SILENT_UPGRADE_TRIES);
}
/**
* Configure the command by defining the name, options and arguments
*/
protected function configure(): void
{
$this->setDescription('Run upgrade wizard. Without arguments all available wizards will be run.')
->addArgument(
'wizardName',
InputArgument::OPTIONAL
)->setHelp(
'This command allows running upgrade wizards on CLI. To run a single wizard add the '
. 'identifier of the wizard as argument. The identifier of the wizard is the name it is '
. 'registered with in ext_localconf.'
);
}
/**
* Update language packs of all active languages for all active extensions
*/
protected function execute(InputInterface $input, OutputInterface $output): int
{
$this->output = new SymfonyStyle($input, $output);
$this->input = $input;
$this->bootstrap();
$wizardToExecute = (string)$input->getArgument('wizardName');
if ($wizardToExecute === '') {
return $this->runAllWizards();
}
try {
$upgradeWizard = $this->getWizard($wizardToExecute);
} catch (WizardMarkedAsDoneException|WizardDoesNotNeedToMakeChangesException $e) {
$this->output->note($e->getMessage());
return Command::SUCCESS;
} catch (WizardNotFoundException $e) {
$this->output->error($e->getMessage());
return Command::FAILURE;
}
$prerequisitesFulfilled = $this->handlePrerequisites([$upgradeWizard]);
if ($prerequisitesFulfilled === true) {
return $this->runSingleWizard($upgradeWizard);
}
return Command::FAILURE;
}
/**
* Get Wizard instance by class name and identifier
* Returns null if wizard is already done
*/
protected function getWizard(string $identifier): UpgradeWizardInterface
{
// already done
if ($this->upgradeWizardsService->isWizardDone($identifier)) {
throw new WizardMarkedAsDoneException(
sprintf('Wizard %s already marked as done', $identifier),
1713880347
);
}
$wizard = $this->upgradeWizardsService->getUpgradeWizard($identifier);
if ($wizard === null) {
throw new WizardNotFoundException(
sprintf('No such wizard: %s', $identifier),
1713880629
);
}
if ($wizard instanceof ChattyInterface) {
$wizard->setOutput($this->output);
}
if ($wizard->updateNecessary()) {
return $wizard;
}
if (!($wizard instanceof RepeatableInterface)) {
$this->upgradeWizardsService->markWizardAsDone($wizard);
throw new WizardMarkedAsDoneException(
sprintf('Wizard %s does not need to make changes. Marking wizard as done.', $identifier),
1713880485
);
}
throw new WizardDoesNotNeedToMakeChangesException(
sprintf('Wizard %s does not need to make changes.', $identifier),
1713880493
);
}
/**
* Handles prerequisites of update wizards, allows a more flexible definition and declaration of dependencies
* Currently implemented prerequisites include "database needs to be up-to-date" and "referenceIndex needs to be up-
* to-date"
* At the moment the install tool automatically displays the database updates when necessary but can't do more
* prerequisites
*
* @param UpgradeWizardInterface[] $instances
*/
protected function handlePrerequisites(array $instances): bool
{
$prerequisites = GeneralUtility::makeInstance(PrerequisiteCollection::class);
foreach ($instances as $instance) {
foreach ($instance->getPrerequisites() as $prerequisite) {
$prerequisites->add($prerequisite);
}
}
$result = true;
foreach ($prerequisites as $prerequisite) {
if ($prerequisite instanceof ChattyInterface) {
$prerequisite->setOutput($this->output);
}
if (!$prerequisite->isFulfilled()) {
$this->output->writeln('Prerequisite "' . $prerequisite->getTitle() . '" not fulfilled, will ensure.');
$result = $prerequisite->ensure();
if ($result === false) {
$this->output->error(
'<error>Error running '
. $prerequisite->getTitle()
. '. Please ensure this prerequisite manually and try again.</error>'
);
break;
}
} else {
$this->output->writeln('Prerequisite "' . $prerequisite->getTitle() . '" fulfilled.');
}
}
return $result;
}
protected function runSingleWizard(
UpgradeWizardInterface $instance
): int {
$this->output->title('Running Wizard "' . $instance->getTitle() . '"');
if ($instance instanceof ConfirmableInterface) {
$confirmation = $instance->getConfirmation();
$defaultString = $confirmation->getDefaultValue() ? 'Y/n' : 'y/N';
$question = new ConfirmationQuestion(
sprintf(
'<info>%s</info>' . LF . '%s' . LF . '%s %s (%s)',
$confirmation->getTitle(),
$confirmation->getMessage(),
$confirmation->getConfirm(),
$confirmation->getDeny(),
$defaultString
),
$confirmation->getDefaultValue()
);
/** @var QuestionHelper $helper */
$helper = $this->getHelper('question');
if (!$helper->ask($this->input, $this->output, $question)) {
if ($confirmation->isRequired()) {
$this->output->error('You have to acknowledge this wizard to continue');
return Command::FAILURE;
}
if ($instance instanceof RepeatableInterface) {
$this->output->note('No changes applied.');
} else {
$this->upgradeWizardsService->markWizardAsDone($instance);
$this->output->note('No changes applied, marking wizard as done.');
}
return Command::SUCCESS;
}
}
if ($instance->executeUpdate()) {
$this->output->success('Successfully ran wizard ' . $instance->getTitle());
if (!$instance instanceof RepeatableInterface) {
$this->upgradeWizardsService->markWizardAsDone($instance);
}
return Command::SUCCESS;
}
$this->output->error('<error>Something went wrong while running ' . $instance->getTitle() . '</error>');
return Command::FAILURE;
}
/**
* Get list of registered upgrade wizards.
*
* @return int 0 if all wizards were successful, 1 on error
*/
public function runAllWizards(): int
{
$returnCode = Command::SUCCESS;
$wizardInstances = [];
foreach ($this->upgradeWizardsService->getUpgradeWizardIdentifiers() as $identifier) {
try {
$wizardInstances[] = $this->getWizard($identifier);
} catch (WizardMarkedAsDoneException|WizardDoesNotNeedToMakeChangesException|WizardNotFoundException) {
// NOOP
}
}
if (count($wizardInstances) > 0) {
$prerequisitesResult = $this->handlePrerequisites($wizardInstances);
if ($prerequisitesResult === false) {
$returnCode = Command::FAILURE;
$this->output->error('Error handling prerequisites, aborting.');
} else {
$this->output->title('Found ' . count($wizardInstances) . ' wizard(s) to run.');
foreach ($wizardInstances as $wizardInstance) {
$result = $this->runSingleWizard($wizardInstance);
if ($result > 0) {
$returnCode = Command::FAILURE;
}
}
}
} else {
$this->output->success('No wizards left to run.');
}
return $returnCode;
}
}