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