1<?php
2
3declare(strict_types=1);
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
18namespace TYPO3\CMS\Scheduler\Task;
19
20use Symfony\Component\Console\Command\Command;
21use Symfony\Component\Console\Exception\InvalidArgumentException;
22use Symfony\Component\Console\Input\InputArgument;
23use Symfony\Component\Console\Input\InputDefinition;
24use Symfony\Component\Console\Input\InputOption;
25use TYPO3\CMS\Core\Console\CommandRegistry;
26use TYPO3\CMS\Core\Localization\LanguageService;
27use TYPO3\CMS\Core\Messaging\FlashMessage;
28use TYPO3\CMS\Core\Messaging\FlashMessageService;
29use TYPO3\CMS\Core\Utility\GeneralUtility;
30use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface;
31use TYPO3\CMS\Scheduler\Controller\SchedulerModuleController;
32use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder;
33
34/**
35 * @internal This class is a specific scheduler task implementation is not considered part of the Public TYPO3 API.
36 */
37class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFieldProviderInterface
38{
39    /**
40     * @var Command[]
41     */
42    protected $schedulableCommands = [];
43
44    /**
45     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
46     */
47    protected $objectManager;
48
49    /**
50     * @var \TYPO3\CMS\Extbase\Reflection\ReflectionService
51     */
52    protected $reflectionService;
53
54    /**
55     * @var ExecuteSchedulableCommandTask
56     */
57    protected $task;
58
59    public function __construct()
60    {
61        $commandRegistry = GeneralUtility::makeInstance(CommandRegistry::class);
62        foreach ($commandRegistry->getSchedulableCommands() as $commandIdentifier => $command) {
63            $this->schedulableCommands[$commandIdentifier] = $command;
64        }
65
66        ksort($this->schedulableCommands);
67    }
68
69    /**
70     * Render additional information fields within the scheduler backend.
71     *
72     * @param array $taskInfo Array information of task to return
73     * @param ExecuteSchedulableCommandTask|null $task When editing, reference to the current task. NULL when adding.
74     * @param SchedulerModuleController $schedulerModule Reference to the calling object (BE module of the Scheduler)
75     * @return array Additional fields
76     * @see \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface::getAdditionalFields
77     */
78    public function getAdditionalFields(array &$taskInfo, $task, SchedulerModuleController $schedulerModule): array
79    {
80        $this->task = $task;
81        if ($this->task !== null) {
82            $this->task->setScheduler();
83        }
84
85        $fields = [];
86        $fields['action'] = $this->getActionField();
87
88        if ($this->task !== null && isset($this->schedulableCommands[$this->task->getCommandIdentifier()])) {
89            $command = $this->schedulableCommands[$this->task->getCommandIdentifier()];
90            $fields['description'] = $this->getCommandDescriptionField($command->getDescription());
91            $argumentFields = $this->getCommandArgumentFields($command->getDefinition());
92            $fields = array_merge($fields, $argumentFields);
93            $optionFields = $this->getCommandOptionFields($command->getDefinition());
94            $fields = array_merge($fields, $optionFields);
95            $this->task->save(); // todo: this seems to be superfluous
96        }
97
98        return $fields;
99    }
100
101    /**
102     * Validates additional selected fields
103     *
104     * @param array $submittedData
105     * @param SchedulerModuleController $schedulerModule
106     * @return bool
107     */
108    public function validateAdditionalFields(array &$submittedData, SchedulerModuleController $schedulerModule): bool
109    {
110        if (!isset($this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']])) {
111            return false;
112        }
113
114        $command = $this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']];
115
116        /** @var FlashMessageService $flashMessageService */
117        $flashMessageService = GeneralUtility::makeInstance(FlashMessageService::class);
118
119        $hasErrors = false;
120        foreach ($command->getDefinition()->getArguments() as $argument) {
121            foreach ((array)$submittedData['task_executeschedulablecommand']['arguments'] as $argumentName => $argumentValue) {
122                /** @var string $argumentName */
123                /** @var string $argumentValue */
124                if ($argument->getName() !== $argumentName) {
125                    continue;
126                }
127
128                if ($argument->isRequired() && trim($argumentValue) === '') {
129                    // Argument is required and argument value is empty0
130                    $flashMessageService->getMessageQueueByIdentifier()->addMessage(
131                        new FlashMessage(
132                            sprintf(
133                                $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.mandatoryArgumentMissing'),
134                                $argumentName
135                            ),
136                            $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.updateError'),
137                            FlashMessage::ERROR
138                        )
139                    );
140                    $hasErrors = true;
141                }
142            }
143        }
144
145        foreach ($command->getDefinition()->getOptions() as $optionDefinition) {
146            $optionEnabled = $submittedData['task_executeschedulablecommand']['options'][$optionDefinition->getName()] ?? false;
147            $optionValue = $submittedData['task_executeschedulablecommand']['option_values'][$optionDefinition->getName()] ?? $optionDefinition->getDefault();
148            if ($optionEnabled && $optionDefinition->isValueRequired()) {
149                if ($optionDefinition->isArray()) {
150                    $testValues = is_array($optionValue) ? $optionValue : GeneralUtility::trimExplode(',', $optionValue, false);
151                } else {
152                    $testValues = [$optionValue];
153                }
154
155                foreach ($testValues as $testValue) {
156                    if ($testValue === null || trim($testValue) === '') {
157                        // An option that requires a value is used with an empty value
158                        $flashMessageService->getMessageQueueByIdentifier()->addMessage(
159                            new FlashMessage(
160                                sprintf(
161                                    $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.mandatoryArgumentMissing'),
162                                    $optionDefinition->getName()
163                                ),
164                                $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.updateError'),
165                                FlashMessage::ERROR
166                            )
167                        );
168                        $hasErrors = true;
169                    }
170                }
171            }
172        }
173
174        return $hasErrors === false;
175    }
176
177    /**
178     * Saves additional field values
179     *
180     * @param array $submittedData
181     * @param ExecuteSchedulableCommandTask $task
182     * @return bool
183     */
184    public function saveAdditionalFields(array $submittedData, AbstractTask $task): bool
185    {
186        $command = $this->schedulableCommands[$submittedData['task_executeschedulablecommand']['command']];
187
188        /** @var ExecuteSchedulableCommandTask $task */
189        $task->setCommandIdentifier($submittedData['task_executeschedulablecommand']['command']);
190
191        $arguments = [];
192        foreach ((array)$submittedData['task_executeschedulablecommand']['arguments'] as $argumentName => $argumentValue) {
193            try {
194                $argumentDefinition = $command->getDefinition()->getArgument($argumentName);
195            } catch (InvalidArgumentException $e) {
196                continue;
197            }
198
199            if ($argumentDefinition->isArray()) {
200                $argumentValue = GeneralUtility::trimExplode(',', $argumentValue, true);
201            }
202
203            $arguments[$argumentName] = $argumentValue;
204        }
205
206        $options = [];
207        $optionValues = [];
208        foreach ($command->getDefinition()->getOptions() as $optionDefinition) {
209            $optionEnabled = $submittedData['task_executeschedulablecommand']['options'][$optionDefinition->getName()] ?? false;
210            $options[$optionDefinition->getName()] = (bool)$optionEnabled;
211
212            if ($optionDefinition->isValueRequired() || $optionDefinition->isValueOptional() || $optionDefinition->isArray()) {
213                $optionValue = $submittedData['task_executeschedulablecommand']['option_values'][$optionDefinition->getName()] ?? $optionDefinition->getDefault();
214                if ($optionDefinition->isArray() && !is_array($optionValue)) {
215                    // Do not remove empty array values.
216                    // One empty array element indicates the existence of one occurrence of an array option (InputOption::VALUE_IS_ARRAY) without a value.
217                    // Empty array elements are also required for command options like "-vvv" (can be entered as ",,").
218                    $optionValue = GeneralUtility::trimExplode(',', $optionValue, false);
219                }
220            } else {
221                // boolean flag: option value must be true if option is added or false otherwise
222                $optionValue = (bool)$optionEnabled;
223            }
224            $optionValues[$optionDefinition->getName()] = $optionValue;
225        }
226
227        $task->setArguments($arguments);
228        $task->setOptions($options);
229        $task->setOptionValues($optionValues);
230        return true;
231    }
232
233    /**
234     * Get description of selected command
235     *
236     * @param string $description
237     * @return array
238     */
239    protected function getCommandDescriptionField(string $description): array
240    {
241        return [
242            'code' => '',
243            'label' => $description
244        ];
245    }
246
247    /**
248     * Gets a select field containing all possible schedulable commands
249     *
250     * @return array
251     */
252    protected function getActionField(): array
253    {
254        $currentlySelectedCommand = $this->task !== null ? $this->task->getCommandIdentifier() : '';
255        $options = [];
256        foreach ($this->schedulableCommands as $commandIdentifier => $command) {
257            $options[$commandIdentifier] = $commandIdentifier . ': ' . $command->getDescription();
258        }
259        return [
260            'code' => $this->renderSelectField($options, $currentlySelectedCommand),
261            'label' => $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.schedulableCommandName')
262        ];
263    }
264
265    /**
266     * Gets a set of fields covering arguments which can or must be used.
267     * Also registers the default values of those fields with the Task, allowing
268     * them to be read upon execution.
269     *
270     * @param InputDefinition $inputDefinition
271     * @return array
272     */
273    protected function getCommandArgumentFields(InputDefinition $inputDefinition): array
274    {
275        $fields = [];
276        $argumentValues = $this->task->getArguments();
277        foreach ($inputDefinition->getArguments() as $argument) {
278            $name = $argument->getName();
279            $defaultValue = $argument->getDefault();
280            $this->task->addDefaultValue($name, $defaultValue);
281            $value = $argumentValues[$name] ?? $defaultValue;
282
283            if (is_array($value) && $argument->isArray()) {
284                $value = implode(',', $value);
285            }
286
287            $fields[$name] = [
288                'code' => $this->renderArgumentField($argument, (string)$value),
289                'label' => $this->getArgumentLabel($argument)
290            ];
291        }
292
293        return $fields;
294    }
295
296    /**
297     * Gets a set of fields covering options which can or must be used.
298     * Also registers the default values of those fields with the Task, allowing
299     * them to be read upon execution.
300     *
301     * @param InputDefinition $inputDefinition
302     * @return array
303     */
304    protected function getCommandOptionFields(InputDefinition $inputDefinition): array
305    {
306        $fields = [];
307        $enabledOptions = $this->task->getOptions();
308        $optionValues = $this->task->getOptionValues();
309        foreach ($inputDefinition->getOptions() as $option) {
310            $name = $option->getName();
311            $defaultValue = $option->getDefault();
312            $this->task->addDefaultValue($name, $defaultValue);
313            $enabled = $enabledOptions[$name] ?? false;
314            $value = $optionValues[$name] ?? $defaultValue;
315
316            if (is_array($value) && $option->isArray()) {
317                $value = implode(',', $value);
318            }
319
320            $fields[$name] = [
321                'code' => $this->renderOptionField($option, (bool)$enabled, (string)$value),
322                'label' => $this->getOptionLabel($option)
323            ];
324        }
325
326        return $fields;
327    }
328
329    /**
330     * Get a human-readable label for a command argument
331     *
332     * @param InputArgument $argument
333     * @return string
334     */
335    protected function getArgumentLabel(InputArgument $argument): string
336    {
337        return 'Argument: ' . $argument->getName() . '. ' . htmlspecialchars($argument->getDescription());
338    }
339
340    /**
341     * Get a human-readable label for a command option
342     *
343     * @param InputOption $option
344     * @return string
345     */
346    protected function getOptionLabel(InputOption $option): string
347    {
348        return 'Option: ' . htmlspecialchars($option->getName()) . '. ' . htmlspecialchars($option->getDescription());
349    }
350
351    /**
352     * @param array $options
353     * @param string $selectedOptionValue
354     * @return string
355     */
356    protected function renderSelectField(array $options, string $selectedOptionValue): string
357    {
358        $selectTag = new TagBuilder();
359        $selectTag->setTagName('select');
360        $selectTag->forceClosingTag(true);
361        $selectTag->addAttribute('class', 'form-control');
362        $selectTag->addAttribute('name', 'tx_scheduler[task_executeschedulablecommand][command]');
363
364        $optionsHtml = '';
365        foreach ($options as $value => $label) {
366            $optionTag = new TagBuilder();
367            $optionTag->setTagName('option');
368            $optionTag->forceClosingTag(true);
369            $optionTag->addAttribute('title', (string)$label);
370            $optionTag->addAttribute('value', (string)$value);
371            $optionTag->setContent($label);
372
373            if ($value === $selectedOptionValue) {
374                $optionTag->addAttribute('selected', 'selected');
375            }
376
377            $optionsHtml .= $optionTag->render();
378        }
379
380        $selectTag->setContent($optionsHtml);
381        return $selectTag->render();
382    }
383
384    /**
385     * Renders a field for defining an argument's value
386     *
387     * @param InputArgument $argument
388     * @param string $currentValue
389     * @return string
390     */
391    protected function renderArgumentField(InputArgument $argument, string $currentValue): string
392    {
393        $name = $argument->getName();
394        $fieldName = 'tx_scheduler[task_executeschedulablecommand][arguments][' . $name . ']';
395
396        $inputTag = new TagBuilder();
397        $inputTag->setTagName('input');
398        $inputTag->addAttribute('type', 'text');
399        $inputTag->addAttribute('name', $fieldName);
400        $inputTag->addAttribute('value', $currentValue);
401        $inputTag->addAttribute('class', 'form-control');
402
403        return $inputTag->render();
404    }
405
406    /**
407     * Renders a field for defining an option's value
408     *
409     * @param InputOption $option
410     * @param string $currentValue
411     * @return string
412     */
413    protected function renderOptionField(InputOption $option, bool $enabled, string $currentValue): string
414    {
415        $name = $option->getName();
416
417        $checkboxFieldName = 'tx_scheduler[task_executeschedulablecommand][options][' . $name . ']';
418        $checkboxId = 'tx_scheduler_task_executeschedulablecommand_options_' . $name;
419        $checkboxTag = new TagBuilder();
420        $checkboxTag->setTagName('input');
421        $checkboxTag->addAttribute('id', $checkboxId);
422        $checkboxTag->addAttribute('name', $checkboxFieldName);
423        $checkboxTag->addAttribute('type', 'checkbox');
424        if ($enabled) {
425            $checkboxTag->addAttribute('checked', 'checked');
426        }
427        $html = '<label for="' . $checkboxId . '">'
428            . $checkboxTag->render()
429            . ' ' . $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.addOptionToCommand')
430            . '</label>';
431
432        if ($option->isValueRequired() || $option->isValueOptional() || $option->isArray()) {
433            $valueFieldName = 'tx_scheduler[task_executeschedulablecommand][option_values][' . $name . ']';
434            $inputTag = new TagBuilder();
435            $inputTag->setTagName('input');
436            $inputTag->addAttribute('name', $valueFieldName);
437            $inputTag->addAttribute('type', 'text');
438            $inputTag->addAttribute('value', $currentValue);
439            $inputTag->addAttribute('class', 'form-control');
440            $html .= $inputTag->render();
441        }
442
443        return $html;
444    }
445
446    /**
447     * @return LanguageService
448     */
449    public function getLanguageService(): LanguageService
450    {
451        return $GLOBALS['LANG'];
452    }
453}
454