[FEATURE] Introduce scheduler task to execute console commands 04/54104/14
authorAlexander Schnitzler <git@alexanderschnitzler.de>
Sun, 10 Sep 2017 15:17:08 +0000 (17:17 +0200)
committerBenni Mack <benni@typo3.org>
Mon, 27 Nov 2017 22:25:44 +0000 (23:25 +0100)
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>
typo3/sysext/core/Classes/Console/CommandRegistry.php
typo3/sysext/core/Classes/Console/UnknownCommandException.php [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-79462-IntroduceSchedulerTaskToExecuteConsoleCommand.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Console/CommandRegistryTest.php
typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandAdditionalFieldProvider.php [new file with mode: 0644]
typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandTask.php [new file with mode: 0644]
typo3/sysext/scheduler/Resources/Private/Language/locallang.xlf
typo3/sysext/scheduler/ext_localconf.php

index ce79263..cbfc91a 100644 (file)
@@ -57,6 +57,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
      * as value. The command description must be an array and have a class key that defines
@@ -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 (file)
index 0000000..e583bd7
--- /dev/null
@@ -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 (file)
index 0000000..cf8d855
--- /dev/null
@@ -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
index e348d30..a6f7cd8 100644 (file)
@@ -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 (file)
index 0000000..e297450
--- /dev/null
@@ -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 (file)
index 0000000..35edb58
--- /dev/null
@@ -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'];
+    }
+}
index 9b35969..9122e97 100644 (file)
                        <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>
                        <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>
                        <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>
index b5f8bad..a8ec1ec 100644 (file)
@@ -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