[BUGFIX] Add support for options in "Execute console commands" 96/61696/16
authorSebastian Mazza <sebastian@mazza.at>
Fri, 6 Dec 2019 14:58:17 +0000 (15:58 +0100)
committerDaniel Goerz <daniel.goerz@posteo.de>
Thu, 16 Jan 2020 12:31:14 +0000 (13:31 +0100)
Resolves: #86917
Releases: master, 9.5
Change-Id: I740c839635143fef2ec33b9b7dd2c6b84f843007
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/61696
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Sascha Rademacher <sascha.rademacher+typo3@gmail.com>
Tested-by: Susanne Moog <look@susi.dev>
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Sascha Rademacher <sascha.rademacher+typo3@gmail.com>
Reviewed-by: Susanne Moog <look@susi.dev>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandAdditionalFieldProvider.php
typo3/sysext/scheduler/Classes/Task/ExecuteSchedulableCommandTask.php
typo3/sysext/scheduler/Resources/Private/Language/locallang.xlf

index df84d82..166b729 100644 (file)
@@ -19,6 +19,7 @@ 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 Symfony\Component\Console\Input\InputOption;
 use TYPO3\CMS\Core\Console\CommandRegistry;
 use TYPO3\CMS\Core\Localization\LanguageService;
 use TYPO3\CMS\Core\Messaging\FlashMessage;
@@ -87,6 +88,8 @@ class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFiel
             $fields['description'] = $this->getCommandDescriptionField($command->getDescription());
             $argumentFields = $this->getCommandArgumentFields($command->getDefinition());
             $fields = array_merge($fields, $argumentFields);
+            $optionFields = $this->getCommandOptionFields($command->getDefinition());
+            $fields = array_merge($fields, $optionFields);
             $this->task->save(); // todo: this seems to be superfluous
         }
 
@@ -136,6 +139,36 @@ class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFiel
                 }
             }
         }
+
+        foreach ($command->getDefinition()->getOptions() as $optionDefinition) {
+            $optionEnabled = $submittedData['task_executeschedulablecommand']['options'][$optionDefinition->getName()] ?? false;
+            $optionValue = $submittedData['task_executeschedulablecommand']['option_values'][$optionDefinition->getName()] ?? $optionDefinition->getDefault();
+            if ($optionEnabled && $optionDefinition->isValueRequired()) {
+                if ($optionDefinition->isArray()) {
+                    $testValues = is_array($optionValue) ? $optionValue : GeneralUtility::trimExplode(',', $optionValue, false);
+                } else {
+                    $testValues = [$optionValue];
+                }
+
+                foreach ($testValues as $testValue) {
+                    if ($testValue === null || trim($testValue) === '') {
+                        // An option that requires a value is used with an empty value
+                        $flashMessageService->getMessageQueueByIdentifier()->addMessage(
+                            new FlashMessage(
+                                sprintf(
+                                    $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.mandatoryArgumentMissing'),
+                                    $optionDefinition->getName()
+                                ),
+                                $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.updateError'),
+                                FlashMessage::ERROR
+                            )
+                        );
+                        $hasErrors = true;
+                    }
+                }
+            }
+        }
+
         return $hasErrors === false;
     }
 
@@ -168,7 +201,30 @@ class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFiel
             $arguments[$argumentName] = $argumentValue;
         }
 
+        $options = [];
+        $optionValues = [];
+        foreach ($command->getDefinition()->getOptions() as $optionDefinition) {
+            $optionEnabled = $submittedData['task_executeschedulablecommand']['options'][$optionDefinition->getName()] ?? false;
+            $options[$optionDefinition->getName()] = (bool)$optionEnabled;
+
+            if ($optionDefinition->isValueRequired() || $optionDefinition->isValueOptional() || $optionDefinition->isArray()) {
+                $optionValue = $submittedData['task_executeschedulablecommand']['option_values'][$optionDefinition->getName()] ?? $optionDefinition->getDefault();
+                if ($optionDefinition->isArray() && !is_array($optionValue)) {
+                    // Do not remove empty array values.
+                    // One empty array element indicates the existence of one occurence of an array option (InputOption::VALUE_IS_ARRAY) without a value.
+                    // Empty array elements are also required for command options like "-vvv" (can be entered as ",,").
+                    $optionValue = GeneralUtility::trimExplode(',', $optionValue, false);
+                }
+            } else {
+                // boolean flag: option value must be true if option is added or false otherwise
+                $optionValue = (bool)$optionEnabled;
+            }
+            $optionValues[$optionDefinition->getName()] = $optionValue;
+        }
+
         $task->setArguments($arguments);
+        $task->setOptions($options);
+        $task->setOptionValues($optionValues);
         return true;
     }
 
@@ -227,7 +283,7 @@ class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFiel
             }
 
             $fields[$name] = [
-                'code' => $this->renderField($argument, (string)$value),
+                'code' => $this->renderArgumentField($argument, (string)$value),
                 'label' => $this->getArgumentLabel($argument)
             ];
         }
@@ -236,6 +292,39 @@ class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFiel
     }
 
     /**
+     * Gets a set of fields covering options 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 getCommandOptionFields(InputDefinition $inputDefinition): array
+    {
+        $fields = [];
+        $enabledOptions = $this->task->getOptions();
+        $optionValues = $this->task->getOptionValues();
+        foreach ($inputDefinition->getOptions() as $option) {
+            $name = $option->getName();
+            $defaultValue = $option->getDefault();
+            $this->task->addDefaultValue($name, $defaultValue);
+            $enabled = $enabledOptions[$name] ?? false;
+            $value = $optionValues[$name] ?? $defaultValue;
+
+            if (is_array($value) && $option->isArray()) {
+                $value = implode(',', $value);
+            }
+
+            $fields[$name] = [
+                'code' => $this->renderOptionField($option, (bool)$enabled, (string)$value),
+                'label' => $this->getOptionLabel($option)
+            ];
+        }
+
+        return $fields;
+    }
+
+    /**
      * Get a human-readable label for a command argument
      *
      * @param InputArgument $argument
@@ -247,6 +336,17 @@ class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFiel
     }
 
     /**
+     * Get a human-readable label for a command option
+     *
+     * @param InputOption $option
+     * @return string
+     */
+    protected function getOptionLabel(InputOption $option): string
+    {
+        return 'Option: ' . htmlspecialchars($option->getName()) . '. <em>' . htmlspecialchars($option->getDescription()) . '</em>';
+    }
+
+    /**
      * @param array $options
      * @param string $selectedOptionValue
      * @return string
@@ -286,7 +386,7 @@ class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFiel
      * @param mixed $currentValue
      * @return string
      */
-    protected function renderField(InputArgument $argument, string $currentValue): string
+    protected function renderArgumentField(InputArgument $argument, string $currentValue): string
     {
         $name = $argument->getName();
         $fieldName = 'tx_scheduler[task_executeschedulablecommand][arguments][' . $name . ']';
@@ -302,6 +402,46 @@ class ExecuteSchedulableCommandAdditionalFieldProvider implements AdditionalFiel
     }
 
     /**
+     * Renders a field for defining an option's value
+     *
+     * @param InputOption $option
+     * @param mixed $currentValue
+     * @return string
+     */
+    protected function renderOptionField(InputOption $option, bool $enabled, string $currentValue): string
+    {
+        $name = $option->getName();
+
+        $checkboxFieldName = 'tx_scheduler[task_executeschedulablecommand][options][' . $name . ']';
+        $checkboxId = 'tx_scheduler_task_executeschedulablecommand_options_' . $name;
+        $checkboxTag = new TagBuilder();
+        $checkboxTag->setTagName('input');
+        $checkboxTag->addAttribute('id', $checkboxId);
+        $checkboxTag->addAttribute('name', $checkboxFieldName);
+        $checkboxTag->addAttribute('type', 'checkbox');
+        if ($enabled) {
+            $checkboxTag->addAttribute('checked', 'checked');
+        }
+        $html = '<label for="' . $checkboxId . '">'
+            . $checkboxTag->render()
+            . ' ' . $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.addOptionToCommand')
+            . '</label>';
+
+        if ($option->isValueRequired() || $option->isValueOptional() || $option->isArray()) {
+            $valueFieldName = 'tx_scheduler[task_executeschedulablecommand][option_values][' . $name . ']';
+            $inputTag = new TagBuilder();
+            $inputTag->setTagName('input');
+            $inputTag->addAttribute('name', $valueFieldName);
+            $inputTag->addAttribute('type', 'text');
+            $inputTag->addAttribute('value', $currentValue);
+            $inputTag->addAttribute('class', 'form-control');
+            $html .=  $inputTag->render();
+        }
+
+        return $html;
+    }
+
+    /**
      * @return LanguageService
      */
     public function getLanguageService(): LanguageService
index f90c519..be52aff 100644 (file)
@@ -15,6 +15,7 @@ namespace TYPO3\CMS\Scheduler\Task;
  * The TYPO3 project - inspiring people to share!
  */
 
+use Symfony\Component\Console\Exception\InvalidOptionException;
 use Symfony\Component\Console\Input\ArrayInput;
 use Symfony\Component\Console\Output\NullOutput;
 use TYPO3\CMS\Core\Console\CommandRegistry;
@@ -39,6 +40,16 @@ class ExecuteSchedulableCommandTask extends AbstractTask
     /**
      * @var array
      */
+    protected $options = [];
+
+    /**
+     * @var array
+     */
+    protected $optionValues = [];
+
+    /**
+     * @var array
+     */
     protected $defaults = [];
 
     /**
@@ -84,7 +95,7 @@ class ExecuteSchedulableCommandTask extends AbstractTask
             );
         }
 
-        $input = new ArrayInput($this->getArguments(), $schedulableCommand->getDefinition());
+        $input = new ArrayInput($this->getParameters(false), $schedulableCommand->getDefinition());
         $output = new NullOutput();
 
         return $schedulableCommand->run($input, $output) === 0;
@@ -110,7 +121,7 @@ class ExecuteSchedulableCommandTask extends AbstractTask
         }
 
         try {
-            $input = new ArrayInput($this->getArguments(), $schedulableCommand->getDefinition());
+            $input = new ArrayInput($this->getParameters(true), $schedulableCommand->getDefinition());
             $arguments = $input->__toString();
         } catch (\Symfony\Component\Console\Exception\RuntimeException $e) {
             return $label . "\n"
@@ -118,6 +129,12 @@ class ExecuteSchedulableCommandTask extends AbstractTask
                     $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.errorParsingArguments'),
                     $e->getMessage()
                 );
+        } catch (InvalidOptionException $e) {
+            return $label . "\n"
+                . sprintf(
+                    $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:msg.errorParsingOptions'),
+                    $e->getMessage()
+                );
         }
         if ($arguments !== '') {
             $label .= ' ' . $arguments;
@@ -126,22 +143,36 @@ class ExecuteSchedulableCommandTask extends AbstractTask
         return $label;
     }
 
-    /**
-     * @return array
-     */
     public function getArguments(): array
     {
         return $this->arguments;
     }
 
-    /**
-     * @param array $arguments
-     */
     public function setArguments(array $arguments)
     {
         $this->arguments = $arguments;
     }
 
+    public function getOptions(): array
+    {
+        return $this->options;
+    }
+
+    public function setOptions(array $options)
+    {
+        $this->options = $options;
+    }
+
+    public function getOptionValues(): array
+    {
+        return $this->optionValues;
+    }
+
+    public function setOptionValues(array $optionValues)
+    {
+        $this->optionValues = $optionValues;
+    }
+
     /**
      * @param string $argumentName
      * @param mixed $argumentValue
@@ -153,4 +184,16 @@ class ExecuteSchedulableCommandTask extends AbstractTask
         }
         $this->defaults[$argumentName] = $argumentValue;
     }
+
+    private function getParameters(bool $forDisplay): array
+    {
+        $options = [];
+        foreach ($this->options as $name => $enabled) {
+            if ($enabled) {
+                $value = $this->optionValues[$name] ?? null;
+                $options['--' . $name] = ($forDisplay && $value === true) ? '' : $value;
+            }
+        }
+        return array_merge($this->arguments, $options);
+    }
 }
index 9df7c59..c40482c 100644 (file)
                        <trans-unit id="label.saveAndCreateNewTask" resname="label.saveAndCreateNewTask">
                                <source>Save and create new task</source>
                        </trans-unit>
+                       <trans-unit id="label.addOptionToCommand" resname="label.addOptionToCommand">
+                               <source>Add option</source>
+                       </trans-unit>
                        <trans-unit id="msg.addError" resname="msg.addError">
                                <source>The task could not be added.</source>
                        </trans-unit>
                        <trans-unit id="msg.errorParsingArguments" resname="msg.errorParsingArguments">
                                <source>Error parsing current set of arguments: "%s".</source>
                        </trans-unit>
+                       <trans-unit id="msg.errorParsingOptions" resname="msg.errorParsingOptions">
+                               <source>Error parsing current set of options: "%s".</source>
+                       </trans-unit>
                        <trans-unit id="msg.mandatoryArgumentMissing" resname="msg.mandatoryArgumentMissing">
                                <source>Argument "%s" is mandatory</source>
                        </trans-unit>