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