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