From d1fb91bf85e3a0b42b832fa590fb5f11e3ddc328 Mon Sep 17 00:00:00 2001 From: Alexander Schnitzler <git@alexanderschnitzler.de> Date: Sun, 10 Sep 2017 17:17:08 +0200 Subject: [PATCH] [FEATURE] Introduce scheduler task to execute console commands This commit introduces a task that is similar to the extbase task that can run command controllers via the scheduler. Since TYPO3 8.7 LTS, a lot of command controllers have already been migrated to symfony console commands, which is breaking considering the fact that the command controllers could have been registered as scheduler tasks. Therefore TYPO3 needs a way to dispatch regular console commands via the scheduler. This will be achieved by introducing a new task provided by the scheduler extension which provides a safe migration path for tx_scheduler records. Resolves: #82390 Resolves: #79462 Releases: master Change-Id: Ie488a3d46965a3dafbd649ab5d432ca14d09a25e Reviewed-on: https://review.typo3.org/54104 Tested-by: TYPO3com <no-reply@typo3.com> Reviewed-by: Sebastian Fischer <typo3@evoweb.de> Reviewed-by: Joerg Boesche <typo3@joergboesche.de> Reviewed-by: Benni Mack <benni@typo3.org> Reviewed-by: Henning Liebe <h.liebe@neusta.de> Reviewed-by: Stefan Neufeind <typo3.neufeind@speedpartner.de> Tested-by: Stefan Neufeind <typo3.neufeind@speedpartner.de> Tested-by: Benni Mack <benni@typo3.org> --- .../core/Classes/Console/CommandRegistry.php | 27 +- .../Console/UnknownCommandException.php | 25 ++ ...ceSchedulerTaskToExecuteConsoleCommand.rst | 23 ++ .../Unit/Console/CommandRegistryTest.php | 34 ++ ...edulableCommandAdditionalFieldProvider.php | 331 ++++++++++++++++++ .../Task/ExecuteSchedulableCommandTask.php | 129 +++++++ .../Resources/Private/Language/locallang.xlf | 15 + typo3/sysext/scheduler/ext_localconf.php | 8 + 8 files changed, 591 insertions(+), 1 deletion(-) create mode 100644 typo3/sysext/core/Classes/Console/UnknownCommandException.php create mode 100644 typo3/sysext/core/Documentation/Changelog/master/Feature-79462-IntroduceSchedulerTaskToExecuteConsoleCommand.rst create mode 100644 typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandAdditionalFieldProvider.php create mode 100644 typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandTask.php diff --git a/typo3/sysext/core/Classes/Console/CommandRegistry.php b/typo3/sysext/core/Classes/Console/CommandRegistry.php index ce79263708e8..cbfc91a2aa68 100644 --- a/typo3/sysext/core/Classes/Console/CommandRegistry.php +++ b/typo3/sysext/core/Classes/Console/CommandRegistry.php @@ -56,6 +56,26 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface } } + /** + * @param string $identifier + * @throws CommandNameAlreadyInUseException + * @throws UnknownCommandException + * @return Command + */ + public function getCommandByIdentifier(string $identifier): Command + { + $this->populateCommandsFromPackages(); + + if (!isset($this->commands[$identifier])) { + throw new UnknownCommandException( + sprintf('Command "%s" has not been registered.', $identifier), + 1510906768 + ); + } + + return $this->commands[$identifier] ?? null; + } + /** * Find all Configuration/Commands.php files of extensions and create a registry from it. * The file should return an array with a command key as key and the command description @@ -79,7 +99,12 @@ class CommandRegistry implements \IteratorAggregate, SingletonInterface foreach ($this->packageManager->getActivePackages() as $package) { $commandsOfExtension = $package->getPackagePath() . 'Configuration/Commands.php'; if (@is_file($commandsOfExtension)) { - $commands = require_once $commandsOfExtension; + /* + * We use require instead of require_once here because it eases the testability as require_once returns + * a boolean from the second execution on. As this class is a singleton, this require is only called + * once per request anyway. + */ + $commands = require $commandsOfExtension; if (is_array($commands)) { foreach ($commands as $commandName => $commandConfig) { if (array_key_exists($commandName, $this->commands)) { diff --git a/typo3/sysext/core/Classes/Console/UnknownCommandException.php b/typo3/sysext/core/Classes/Console/UnknownCommandException.php new file mode 100644 index 000000000000..e583bd7f3cbb --- /dev/null +++ b/typo3/sysext/core/Classes/Console/UnknownCommandException.php @@ -0,0 +1,25 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Core\Console; + +/* + * 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! + */ + +use TYPO3\CMS\Core\Exception; + +/** + * Exception thrown when an unregistered command is asked for + */ +class UnknownCommandException extends Exception +{ +} diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-79462-IntroduceSchedulerTaskToExecuteConsoleCommand.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-79462-IntroduceSchedulerTaskToExecuteConsoleCommand.rst new file mode 100644 index 000000000000..cf8d855ceadb --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/master/Feature-79462-IntroduceSchedulerTaskToExecuteConsoleCommand.rst @@ -0,0 +1,23 @@ +.. include:: ../../Includes.txt + +===================================================================== +Feature: #79462 - Introduce scheduler task to execute console command +===================================================================== + +See :issue:`79462` + +Description +=========== + +A scheduler task has been introduced to execute (symfony) console commands. In the past this was +already possible for Extbase command controller commands but as the core migrates all command +controllers to native symfony commands, the scheduler needs to be able to execute them. + + +Impact +====== + +Symfony commands can be executed via the scheduler which provides a migration path away from +command controllers to native symfony commands. + +.. index:: CLI, NotScanned diff --git a/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php b/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php index e348d305ee62..a6f7cd8983c2 100644 --- a/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php +++ b/typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php @@ -16,9 +16,11 @@ namespace TYPO3\CMS\Core\Tests\Unit\Console; */ use org\bovigo\vfs\vfsStream; +use Prophecy\Prophecy\ObjectProphecy; use Symfony\Component\Console\Command\Command; use TYPO3\CMS\Core\Console\CommandNameAlreadyInUseException; use TYPO3\CMS\Core\Console\CommandRegistry; +use TYPO3\CMS\Core\Console\UnknownCommandException; use TYPO3\CMS\Core\Package\PackageInterface; use TYPO3\CMS\Core\Package\PackageManager; use TYPO3\TestingFramework\Core\Unit\UnitTestCase; @@ -113,4 +115,36 @@ class CommandRegistryTest extends UnitTestCase $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal()); iterator_to_array($commandRegistry); } + + /** + * @test + */ + public function getCommandByIdentifierReturnsRegisteredCommand() + { + /** @var PackageInterface|ObjectProphecy $package */ + $package = $this->prophesize(PackageInterface::class); + $package->getPackagePath()->willReturn($this->rootDirectory->getChild('package1')->url() . '/'); + $package->getPackageKey()->willReturn('package1'); + + $this->packageManagerProphecy->getActivePackages()->willReturn([$package->reveal()]); + + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal()); + $command = $commandRegistry->getCommandByIdentifier('first:command'); + + $this->assertInstanceOf(Command::class, $command); + } + + /** + * @test + */ + public function throwsUnknowCommandExceptionIfUnregisteredCommandIsRequested() + { + $this->packageManagerProphecy->getActivePackages()->willReturn([]); + + $this->expectException(UnknownCommandException::class); + $this->expectExceptionCode(1510906768); + + $commandRegistry = new CommandRegistry($this->packageManagerProphecy->reveal()); + $commandRegistry->getCommandByIdentifier('foo'); + } } diff --git a/typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandAdditionalFieldProvider.php b/typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandAdditionalFieldProvider.php new file mode 100644 index 000000000000..e2974504f768 --- /dev/null +++ b/typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandAdditionalFieldProvider.php @@ -0,0 +1,331 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Scheduler\Task; + +/* + * 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! + */ + +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\InvalidArgumentException; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputDefinition; +use TYPO3\CMS\Core\Console\CommandRegistry; +use TYPO3\CMS\Core\Messaging\FlashMessage; +use TYPO3\CMS\Core\Messaging\FlashMessageService; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Lang\LanguageService; +use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface; +use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController; +use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder; + +/** + * Class TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandAdditionalFieldProvider + */ +class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFieldProviderInterface +{ + /** + * Commands that should not be schedulable, like scheduler:run, + * which would start a recursion. + * + * @var array + */ + protected static $blacklistedCommands = [ + \TYPO3\CMS\Scheduler\Command\SchedulerCommand::class, // scheduler:run + \TYPO3\CMS\Extbase\Command\CoreCommand::class, // _core_command + \TYPO3\CMS\Extbase\Command\HelpCommand::class, // _extbase_help + ]; + + /** + * @var array|Command[] + */ + protected $schedulableCommands = []; + + /** + * @var \TYPO3\CMS\Extbase\Mvc\Cli\CommandManager + */ + protected $commandManager; + + /** + * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface + */ + protected $objectManager; + + /** + * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService + */ + protected $reflectionService; + + /** + * @var ExecuteSchedulableCommandTask + */ + protected $task; + + public function __construct() + { + $commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class); + foreach ($commandRegistry as $commandIdentifier => $command) { + if (in_array(get_class($command), static::$blacklistedCommands, true)) { + continue; + } + $this->schedulableCommands[$commandIdentifier] = $command; + } + + ksort($this->schedulableCommands); + } + + /** + * Render additional information fields within the scheduler backend. + * + * @param array &$taskInfo Array information of task to return + * @param mixed $task \TYPO3\CMS\Scheduler\Task\AbstractTask or \TYPO3\CMS\Scheduler\Execution instance + * @param SchedulerModuleController $schedulerModule Reference to the calling object (BE module of the Scheduler) + * @return array Additional fields + * @see \TYPO3\CMS\Scheduler\AdditionalFieldProvider#getAdditionalFields($taskInfo, $task, $schedulerModule) + */ + public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule): array + { + $this->task = $task; + if ($this->task !== null) { + $this->task->setScheduler(); + } + + $fields = []; + $fields['action'] = $this->getActionField(); + + if ($this->task !== null && isset($this->schedulableCommands[$this->task->getCommandIdentifier()])) { + $command = $this->schedulableCommands[$this->task->getCommandIdentifier()]; + $fields['description'] = $this->getCommandDescriptionField($command->getDescription()); + $argumentFields = $this->getCommandArgumentFields($command->getDefinition()); + $fields = array_merge($fields, $argumentFields); + $this->task->save(); // todo: this seems to be superfluous + } + + return $fields; + } + + /** + * Validates additional selected fields + * + * @param array &$submittedData + * @param SchedulerModuleController $schedulerModule + * @return bool + */ + public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule): bool + { + if (!isset($this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']])) { + return false; + } + + $command = $this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']]; + + /** @var FlashMessageService $flashMessageService */ + $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class); + + $hasErrors = false; + foreach ($command->getDefinition()->getArguments() as $argument) { + foreach ((array)$submittedData['task_executeschedulablecommand']['arguments'] as $argumentName => $argumentValue) { + /** @var string $argumentName */ + /** @var string $argumentValue */ + if ($argument->getName() !== $argumentName) { + continue; + } + + if ($argument->isRequired() && trim($argumentValue) === '') { + // Argument is required and argument value is empty0 + $flashMessageService->getMessageQueueByIdentifier()->addMessage( + new FlashMessage( + sprintf( + $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.mandatoryArgumentMissing'), + $argumentName + ), + $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.updateError'), + FlashMessage::ERROR + ) + ); + $hasErrors = true; + } + } + } + return $hasErrors === false; + } + + /** + * Saves additional field values + * + * @param array $submittedData + * @param AbstractTask $task + * @return bool + */ + public function saveAdditionalFields(array $submittedData, AbstractTask $task): bool + { + $command = $this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']]; + + /** @var ExecuteSchedulableCommandTask $task */ + $task->setCommandIdentifier($submittedData['task_executeschedulablecommand']['command']); + + $arguments = []; + foreach ((array)$submittedData['task_executeschedulablecommand']['arguments'] as $argumentName => $argumentValue) { + try { + $argumentDefinition = $command->getDefinition()->getArgument($argumentName); + } catch (InvalidArgumentException $e) { + continue; + } + + if ($argumentDefinition->isArray()) { + $argumentValue = GeneralUtility::trimExplode(',', $argumentValue, true); + } + + $arguments[$argumentName] = $argumentValue; + } + + $task->setArguments($arguments); + return true; + } + + /** + * Get description of selected command + * + * @param string $description + * @return array + */ + protected function getCommandDescriptionField(string $description): array + { + return [ + 'code' => '', + 'label' => '<strong>' . $description . '</strong>' + ]; + } + + /** + * Gets a select field containing all possible schedulable commands + * + * @return array + */ + protected function getActionField(): array + { + $currentlySelectedCommand = $this->task !== null ? $this->task->getCommandIdentifier() : ''; + $options = []; + foreach ($this->schedulableCommands as $commandIdentifier => $command) { + $options[$commandIdentifier] = $commandIdentifier . ': ' . $command->getDescription(); + } + return [ + 'code' => $this->renderSelectField($options, $currentlySelectedCommand), + 'label' => $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.schedulableCommandName') + ]; + } + + /** + * Gets a set of fields covering arguments which can or must be used. + * Also registers the default values of those fields with the Task, allowing + * them to be read upon execution. + * + * @param InputDefinition $inputDefinition + * @return array + */ + protected function getCommandArgumentFields(InputDefinition $inputDefinition): array + { + $fields = []; + $argumentValues = $this->task->getArguments(); + foreach ($inputDefinition->getArguments() as $argument) { + $name = $argument->getName(); + $defaultValue = $argument->getDefault(); + $this->task->addDefaultValue($name, $defaultValue); + $value = $argumentValues[$name] ?? $defaultValue; + + if (is_array($value) && $argument->isArray()) { + $value = implode(',', $value); + } + + $fields[$name] = [ + 'code' => $this->renderField($argument, (string)$value), + 'label' => $this->getArgumentLabel($argument) + ]; + } + + return $fields; + } + + /** + * Get a human-readable label for a command argument + * + * @param InputArgument $argument + * @return string + */ + protected function getArgumentLabel(InputArgument $argument): string + { + return 'Argument: ' . $argument->getName() . '. <em>' . htmlspecialchars($argument->getDescription()) . '</em>'; + } + + /** + * @param array $options + * @param string $selectedOptionValue + * @return string + */ + protected function renderSelectField(array $options, string $selectedOptionValue): string + { + $selectTag = new TagBuilder(); + $selectTag->setTagName('select'); + $selectTag->forceClosingTag(true); + $selectTag->addAttribute('class', 'form-control'); + $selectTag->addAttribute('name', 'tx_scheduler[task_executeschedulablecommand][command]'); + + $optionsHtml = ''; + foreach ($options as $value => $label) { + $optionTag = new TagBuilder(); + $optionTag->setTagName('option'); + $optionTag->forceClosingTag(true); + $optionTag->addAttribute('title', (string)$label); + $optionTag->addAttribute('value', (string)$value); + $optionTag->setContent($label); + + if ($value === $selectedOptionValue) { + $optionTag->addAttribute('selected', 'selected'); + } + + $optionsHtml .= $optionTag->render(); + } + + $selectTag->setContent($optionsHtml); + return $selectTag->render(); + } + + /** + * Renders a field for defining an argument's value + * + * @param InputArgument $argument + * @param mixed $currentValue + * @return string + */ + protected function renderField(InputArgument $argument, string $currentValue): string + { + $name = $argument->getName(); + $fieldName = 'tx_scheduler[task_executeschedulablecommand][arguments][' . $name . ']'; + + $inputTag = new TagBuilder(); + $inputTag->setTagName('input'); + $inputTag->addAttribute('type', 'text'); + $inputTag->addAttribute('name', $fieldName); + $inputTag->addAttribute('value', $currentValue); + $inputTag->addAttribute('class', 'form-control'); + + return $inputTag->render(); + } + + /** + * @return LanguageService + */ + public function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandTask.php b/typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandTask.php new file mode 100644 index 000000000000..35edb58ccaaf --- /dev/null +++ b/typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandTask.php @@ -0,0 +1,129 @@ +<?php +declare(strict_types=1); +namespace TYPO3\CMS\Scheduler\Task; + +/* + * 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! + */ + +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\NullOutput; +use TYPO3\CMS\Core\Console\CommandRegistry; +use TYPO3\CMS\Core\Console\UnknownCommandException; +use TYPO3\CMS\Core\Utility\GeneralUtility; +use TYPO3\CMS\Lang\LanguageService; + +/** + * Class TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandTask + */ +class ExecuteSchedulableCommandTask extends AbstractTask +{ + /** + * @var string + */ + protected $commandIdentifier = ''; + + /** + * @var array + */ + protected $arguments = []; + + /** + * @var array + */ + protected $defaults = []; + + /** + * @param string $commandIdentifier + */ + public function setCommandIdentifier(string $commandIdentifier) + { + $this->commandIdentifier = $commandIdentifier; + } + + /** + * @return string + */ + public function getCommandIdentifier(): string + { + return $this->commandIdentifier; + } + + /** + * This is the main method that is called when a task is executed + * It MUST be implemented by all classes inheriting from this one + * Note that there is no error handling, errors and failures are expected + * to be handled and logged by the client implementations. + * Should return TRUE on successful execution, FALSE on error. + * + * @throws \Exception + * + * @return bool Returns TRUE on successful execution, FALSE on error + */ + public function execute(): bool + { + try { + $commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class); + $schedulableCommand = $commandRegistry->getCommandByIdentifier($this->commandIdentifier); + } catch (UnknownCommandException $e) { + throw new \RuntimeException( + sprintf( + $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.unregisteredCommand'), + $this->commandIdentifier + ), + 1505055445, + $e + ); + } + + $input = new ArrayInput($this->getArguments(), $schedulableCommand->getDefinition()); + $output = new NullOutput(); + + return $schedulableCommand->run($input, $output) === 0; + } + + /** + * @return array + */ + public function getArguments(): array + { + return $this->arguments; + } + + /** + * @param array $arguments + */ + public function setArguments(array $arguments) + { + $this->arguments = $arguments; + } + + /** + * @param string $argumentName + * @param mixed $argumentValue + */ + public function addDefaultValue(string $argumentName, $argumentValue) + { + if (is_bool($argumentValue)) { + $argumentValue = (int)$argumentValue; + } + $this->defaults[$argumentName] = $argumentValue; + } + + /** + * @return LanguageService + */ + public function getLanguageService(): LanguageService + { + return $GLOBALS['LANG']; + } +} diff --git a/typo3/sysext/scheduler/Resources/Private/Language/locallang.xlf b/typo3/sysext/scheduler/Resources/Private/Language/locallang.xlf index 9b359697cecf..9122e9736168 100644 --- a/typo3/sysext/scheduler/Resources/Private/Language/locallang.xlf +++ b/typo3/sysext/scheduler/Resources/Private/Language/locallang.xlf @@ -162,6 +162,9 @@ <trans-unit id="label.noGroup"> <source>(no task group defined)</source> </trans-unit> + <trans-unit id="label.schedulableCommandName"> + <source><![CDATA[Schedulable Command. <em>Save and reopen to define command arguments</em>]]></source> + </trans-unit> <trans-unit id="msg.addError"> <source>The task could not be added.</source> </trans-unit> @@ -306,6 +309,12 @@ <trans-unit id="msg.noDatabaseTablesSelected"> <source>Please select at least one database table.</source> </trans-unit> + <trans-unit id="msg.unregisteredCommand"> + <source>Command with identifier "%s" has not been registered.</source> + </trans-unit> + <trans-unit id="msg.mandatoryArgumentMissing"> + <source>Argument "%s" is mandatory</source> + </trans-unit> <trans-unit id="none"> <source>None</source> </trans-unit> @@ -390,6 +399,12 @@ <trans-unit id="recyclerGarbageCollection.description"> <source>This task empties all "_recycler_" folders below fileadmin. This helps free some space in the file system.</source> </trans-unit> + <trans-unit id="executeSchedulableCommandTask.name"> + <source>Execute console commands</source> + </trans-unit> + <trans-unit id="executeSchedulableCommandTask.description"> + <source>Allows regular console commands to be configured and executed through the scheduler framework.</source> + </trans-unit> <trans-unit id="optimizeDatabaseTable.name"> <source>Optimize MySQL database tables</source> </trans-unit> diff --git a/typo3/sysext/scheduler/ext_localconf.php b/typo3/sysext/scheduler/ext_localconf.php index b5f8badbd090..a8ec1ec7c430 100644 --- a/typo3/sysext/scheduler/ext_localconf.php +++ b/typo3/sysext/scheduler/ext_localconf.php @@ -53,6 +53,14 @@ $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Sched 'additionalFields' => \TYPO3\CMS\Scheduler\Task\RecyclerGarbageCollectionAdditionalFieldProvider::class ]; +// Add execute schedulable command task +$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][\TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandTask::class] = [ + 'extension' => 'scheduler', + 'title' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:executeSchedulableCommandTask.name', + 'description' => 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:executeSchedulableCommandTask.name', + 'additionalFields' => \TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandAdditionalFieldProvider::class +]; + // Save any previous option array for table garbage collection task // to temporary variable so it can be pre-populated by other // extensions and LocalConfiguration/AdditionalConfiguration -- GitLab