[FEATURE] Introduce scheduler task to execute console commands
[Packages/TYPO3.CMS.git] / typo3 / sysext / scheduler / Classes / Task / ExecuteSchedulableCommandAdditionalFieldProvider.php
1 <?php
2 declare(strict_types=1);
3 namespace TYPO3\CMS\Scheduler\Task;
4
5 /*
6 * This file is part of the TYPO3 CMS project.
7 *
8 * It is free software; you can redistribute it and/or modify it under
9 * the terms of the GNU General Public License, either version 2
10 * of the License, or any later version.
11 *
12 * For the full copyright and license information, please read the
13 * LICENSE.txt file that was distributed with this source code.
14 *
15 * The TYPO3 project - inspiring people to share!
16 */
17
18 use Symfony\Component\Console\Command\Command;
19 use Symfony\Component\Console\Exception\InvalidArgumentException;
20 use Symfony\Component\Console\Input\InputArgument;
21 use Symfony\Component\Console\Input\InputDefinition;
22 use TYPO3\CMS\Core\Console\CommandRegistry;
23 use TYPO3\CMS\Core\Messaging\FlashMessage;
24 use TYPO3\CMS\Core\Messaging\FlashMessageService;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Lang\LanguageService;
27 use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface;
28 use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
29 use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;
30
31 /**
32 * Class TYPO3\CMS\Scheduler\Task\ExecuteSchedulableCommandAdditionalFieldProvider
33 */
34 class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFieldProviderInterface
35 {
36 /**
37 * Commands that should not be schedulable, like scheduler:run,
38 * which would start a recursion.
39 *
40 * @var array
41 */
42 protected static $blacklistedCommands = [
43 \TYPO3\CMS\Scheduler\Command\SchedulerCommand::class, // scheduler:run
44 \TYPO3\CMS\Extbase\Command\CoreCommand::class, // _core_command
45 \TYPO3\CMS\Extbase\Command\HelpCommand::class, // _extbase_help
46 ];
47
48 /**
49 * @var array|Command[]
50 */
51 protected $schedulableCommands = [];
52
53 /**
54 * @var \TYPO3\CMS\Extbase\Mvc\Cli\CommandManager
55 */
56 protected $commandManager;
57
58 /**
59 * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
60 */
61 protected $objectManager;
62
63 /**
64 * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
65 */
66 protected $reflectionService;
67
68 /**
69 * @var ExecuteSchedulableCommandTask
70 */
71 protected $task;
72
73 public function __construct()
74 {
75 $commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class);
76 foreach ($commandRegistry as $commandIdentifier => $command) {
77 if (in_array(get_class($command), static::$blacklistedCommands, true)) {
78 continue;
79 }
80 $this->schedulableCommands[$commandIdentifier] = $command;
81 }
82
83 ksort($this->schedulableCommands);
84 }
85
86 /**
87 * Render additional information fields within the scheduler backend.
88 *
89 * @param array &$taskInfo Array information of task to return
90 * @param mixed $task \TYPO3\CMS\Scheduler\Task\AbstractTask or \TYPO3\CMS\Scheduler\Execution instance
91 * @param SchedulerModuleController $schedulerModule Reference to the calling object (BE module of the Scheduler)
92 * @return array Additional fields
93 * @see \TYPO3\CMS\Scheduler\AdditionalFieldProvider#getAdditionalFields($taskInfo, $task, $schedulerModule)
94 */
95 public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule): array
96 {
97 $this->task = $task;
98 if ($this->task !== null) {
99 $this->task->setScheduler();
100 }
101
102 $fields = [];
103 $fields['action'] = $this->getActionField();
104
105 if ($this->task !== null && isset($this->schedulableCommands[$this->task->getCommandIdentifier()])) {
106 $command = $this->schedulableCommands[$this->task->getCommandIdentifier()];
107 $fields['description'] = $this->getCommandDescriptionField($command->getDescription());
108 $argumentFields = $this->getCommandArgumentFields($command->getDefinition());
109 $fields = array_merge($fields, $argumentFields);
110 $this->task->save(); // todo: this seems to be superfluous
111 }
112
113 return $fields;
114 }
115
116 /**
117 * Validates additional selected fields
118 *
119 * @param array &$submittedData
120 * @param SchedulerModuleController $schedulerModule
121 * @return bool
122 */
123 public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule): bool
124 {
125 if (!isset($this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']])) {
126 return false;
127 }
128
129 $command = $this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']];
130
131 /** @var FlashMessageService $flashMessageService */
132 $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
133
134 $hasErrors = false;
135 foreach ($command->getDefinition()->getArguments() as $argument) {
136 foreach ((array)$submittedData['task_executeschedulablecommand']['arguments'] as $argumentName => $argumentValue) {
137 /** @var string $argumentName */
138 /** @var string $argumentValue */
139 if ($argument->getName() !== $argumentName) {
140 continue;
141 }
142
143 if ($argument->isRequired() && trim($argumentValue) === '') {
144 // Argument is required and argument value is empty0
145 $flashMessageService->getMessageQueueByIdentifier()->addMessage(
146 new FlashMessage(
147 sprintf(
148 $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.mandatoryArgumentMissing'),
149 $argumentName
150 ),
151 $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.updateError'),
152 FlashMessage::ERROR
153 )
154 );
155 $hasErrors = true;
156 }
157 }
158 }
159 return $hasErrors === false;
160 }
161
162 /**
163 * Saves additional field values
164 *
165 * @param array $submittedData
166 * @param AbstractTask $task
167 * @return bool
168 */
169 public function saveAdditionalFields(array $submittedData, AbstractTask $task): bool
170 {
171 $command = $this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']];
172
173 /** @var ExecuteSchedulableCommandTask $task */
174 $task->setCommandIdentifier($submittedData['task_executeschedulablecommand']['command']);
175
176 $arguments = [];
177 foreach ((array)$submittedData['task_executeschedulablecommand']['arguments'] as $argumentName => $argumentValue) {
178 try {
179 $argumentDefinition = $command->getDefinition()->getArgument($argumentName);
180 } catch (InvalidArgumentException $e) {
181 continue;
182 }
183
184 if ($argumentDefinition->isArray()) {
185 $argumentValue = GeneralUtility::trimExplode(',', $argumentValue, true);
186 }
187
188 $arguments[$argumentName] = $argumentValue;
189 }
190
191 $task->setArguments($arguments);
192 return true;
193 }
194
195 /**
196 * Get description of selected command
197 *
198 * @param string $description
199 * @return array
200 */
201 protected function getCommandDescriptionField(string $description): array
202 {
203 return [
204 'code' => '',
205 'label' => '<strong>' . $description . '</strong>'
206 ];
207 }
208
209 /**
210 * Gets a select field containing all possible schedulable commands
211 *
212 * @return array
213 */
214 protected function getActionField(): array
215 {
216 $currentlySelectedCommand = $this->task !== null ? $this->task->getCommandIdentifier() : '';
217 $options = [];
218 foreach ($this->schedulableCommands as $commandIdentifier => $command) {
219 $options[$commandIdentifier] = $commandIdentifier . ': ' . $command->getDescription();
220 }
221 return [
222 'code' => $this->renderSelectField($options, $currentlySelectedCommand),
223 'label' => $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.schedulableCommandName')
224 ];
225 }
226
227 /**
228 * Gets a set of fields covering arguments which can or must be used.
229 * Also registers the default values of those fields with the Task, allowing
230 * them to be read upon execution.
231 *
232 * @param InputDefinition $inputDefinition
233 * @return array
234 */
235 protected function getCommandArgumentFields(InputDefinition $inputDefinition): array
236 {
237 $fields = [];
238 $argumentValues = $this->task->getArguments();
239 foreach ($inputDefinition->getArguments() as $argument) {
240 $name = $argument->getName();
241 $defaultValue = $argument->getDefault();
242 $this->task->addDefaultValue($name, $defaultValue);
243 $value = $argumentValues[$name] ?? $defaultValue;
244
245 if (is_array($value) && $argument->isArray()) {
246 $value = implode(',', $value);
247 }
248
249 $fields[$name] = [
250 'code' => $this->renderField($argument, (string)$value),
251 'label' => $this->getArgumentLabel($argument)
252 ];
253 }
254
255 return $fields;
256 }
257
258 /**
259 * Get a human-readable label for a command argument
260 *
261 * @param InputArgument $argument
262 * @return string
263 */
264 protected function getArgumentLabel(InputArgument $argument): string
265 {
266 return 'Argument: ' . $argument->getName() . '. <em>' . htmlspecialchars($argument->getDescription()) . '</em>';
267 }
268
269 /**
270 * @param array $options
271 * @param string $selectedOptionValue
272 * @return string
273 */
274 protected function renderSelectField(array $options, string $selectedOptionValue): string
275 {
276 $selectTag = new TagBuilder();
277 $selectTag->setTagName('select');
278 $selectTag->forceClosingTag(true);
279 $selectTag->addAttribute('class', 'form-control');
280 $selectTag->addAttribute('name', 'tx_scheduler[task_executeschedulablecommand][command]');
281
282 $optionsHtml = '';
283 foreach ($options as $value => $label) {
284 $optionTag = new TagBuilder();
285 $optionTag->setTagName('option');
286 $optionTag->forceClosingTag(true);
287 $optionTag->addAttribute('title', (string)$label);
288 $optionTag->addAttribute('value', (string)$value);
289 $optionTag->setContent($label);
290
291 if ($value === $selectedOptionValue) {
292 $optionTag->addAttribute('selected', 'selected');
293 }
294
295 $optionsHtml .= $optionTag->render();
296 }
297
298 $selectTag->setContent($optionsHtml);
299 return $selectTag->render();
300 }
301
302 /**
303 * Renders a field for defining an argument's value
304 *
305 * @param InputArgument $argument
306 * @param mixed $currentValue
307 * @return string
308 */
309 protected function renderField(InputArgument $argument, string $currentValue): string
310 {
311 $name = $argument->getName();
312 $fieldName = 'tx_scheduler[task_executeschedulablecommand][arguments][' . $name . ']';
313
314 $inputTag = new TagBuilder();
315 $inputTag->setTagName('input');
316 $inputTag->addAttribute('type', 'text');
317 $inputTag->addAttribute('name', $fieldName);
318 $inputTag->addAttribute('value', $currentValue);
319 $inputTag->addAttribute('class', 'form-control');
320
321 return $inputTag->render();
322 }
323
324 /**
325 * @return LanguageService
326 */
327 public function getLanguageService(): LanguageService
328 {
329 return $GLOBALS['LANG'];
330 }
331 }