1<?php 2declare(strict_types = 1); 3namespace TYPO3\CMS\Scheduler\Controller; 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 Psr\Http\Message\ResponseInterface; 19use Psr\Http\Message\ServerRequestInterface; 20use TYPO3\CMS\Backend\Routing\UriBuilder; 21use TYPO3\CMS\Backend\Template\Components\ButtonBar; 22use TYPO3\CMS\Backend\Template\ModuleTemplate; 23use TYPO3\CMS\Backend\Utility\BackendUtility; 24use TYPO3\CMS\Core\Authentication\BackendUserAuthentication; 25use TYPO3\CMS\Core\Compatibility\PublicMethodDeprecationTrait; 26use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait; 27use TYPO3\CMS\Core\Core\Environment; 28use TYPO3\CMS\Core\Database\ConnectionPool; 29use TYPO3\CMS\Core\Http\HtmlResponse; 30use TYPO3\CMS\Core\Imaging\Icon; 31use TYPO3\CMS\Core\Imaging\IconFactory; 32use TYPO3\CMS\Core\Localization\LanguageService; 33use TYPO3\CMS\Core\Messaging\FlashMessage; 34use TYPO3\CMS\Core\Page\PageRenderer; 35use TYPO3\CMS\Core\Registry; 36use TYPO3\CMS\Core\Utility\ArrayUtility; 37use TYPO3\CMS\Core\Utility\ExtensionManagementUtility; 38use TYPO3\CMS\Core\Utility\GeneralUtility; 39use TYPO3\CMS\Fluid\View\StandaloneView; 40use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper; 41use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface; 42use TYPO3\CMS\Scheduler\CronCommand\NormalizeCommand; 43use TYPO3\CMS\Scheduler\ProgressProviderInterface; 44use TYPO3\CMS\Scheduler\Scheduler; 45use TYPO3\CMS\Scheduler\Task\AbstractTask; 46use TYPO3\CMS\Scheduler\Task\Enumeration\Action; 47 48/** 49 * Module 'TYPO3 Scheduler administration module' for the 'scheduler' extension. 50 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API. 51 */ 52class SchedulerModuleController 53{ 54 use PublicMethodDeprecationTrait; 55 use PublicPropertyDeprecationTrait; 56 57 /** 58 * @var array 59 */ 60 private $deprecatedPublicMethods = [ 61 'addMessage' => 'Using SchedulerModuleController::addMessage() is deprecated and will not be possible anymore in TYPO3 v10.0.', 62 ]; 63 64 /** 65 * @var array 66 */ 67 private $deprecatedPublicProperties = [ 68 'CMD' => 'Using SchedulerModuleController::$CMD is deprecated and will not be possible anymore in TYPO3 v10.0. Use SchedulerModuleController::getCurrentAction() instead.', 69 ]; 70 71 /** 72 * Array containing submitted data when editing or adding a task 73 * 74 * @var array 75 */ 76 protected $submittedData = []; 77 78 /** 79 * Array containing all messages issued by the application logic 80 * Contains the error's severity and the message itself 81 * 82 * @var array 83 */ 84 protected $messages = []; 85 86 /** 87 * @var string Key of the CSH file 88 */ 89 protected $cshKey = '_MOD_system_txschedulerM1'; 90 91 /** 92 * @var Scheduler Local scheduler instance 93 */ 94 protected $scheduler; 95 96 /** 97 * @var string 98 */ 99 protected $backendTemplatePath = ''; 100 101 /** 102 * @var StandaloneView 103 */ 104 protected $view; 105 106 /** 107 * @var string Base URI of scheduler module 108 */ 109 protected $moduleUri; 110 111 /** 112 * ModuleTemplate Container 113 * 114 * @var ModuleTemplate 115 */ 116 protected $moduleTemplate; 117 118 /** 119 * @var IconFactory 120 */ 121 protected $iconFactory; 122 123 /** 124 * The value of GET/POST var, 'CMD' 125 * 126 * @var mixed 127 */ 128 protected $CMD; 129 130 /** 131 * @var Action 132 */ 133 protected $action; 134 135 /** 136 * The module menu items array. Each key represents a key for which values can range between the items in the array of that key. 137 * 138 * @var array 139 */ 140 protected $MOD_MENU = [ 141 'function' => [] 142 ]; 143 144 /** 145 * Current settings for the keys of the MOD_MENU array 146 * 147 * @see $MOD_MENU 148 * @var array 149 */ 150 protected $MOD_SETTINGS = []; 151 152 /** 153 * Default constructor 154 */ 155 public function __construct() 156 { 157 $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class); 158 $this->getLanguageService()->includeLLFile('EXT:scheduler/Resources/Private/Language/locallang.xlf'); 159 $this->backendTemplatePath = ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Backend/SchedulerModule/'; 160 $this->view = GeneralUtility::makeInstance(StandaloneView::class); 161 $this->view->getRequest()->setControllerExtensionName('scheduler'); 162 $this->view->setPartialRootPaths([ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Partials/Backend/SchedulerModule/']); 163 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 164 $this->moduleUri = (string)$uriBuilder->buildUriFromRoute('system_txschedulerM1'); 165 $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class); 166 $this->scheduler = GeneralUtility::makeInstance(Scheduler::class); 167 168 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Modal'); 169 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/SplitButtons'); 170 } 171 172 /** 173 * Injects the request object for the current request or subrequest 174 * 175 * @param ServerRequestInterface $request the current request 176 * @return ResponseInterface the response with the content 177 */ 178 public function mainAction(ServerRequestInterface $request): ResponseInterface 179 { 180 $parsedBody = $request->getParsedBody(); 181 $queryParams = $request->getQueryParams(); 182 183 $this->setCurrentAction(Action::cast($parsedBody['CMD'] ?? $queryParams['CMD'] ?? null)); 184 $this->MOD_MENU = [ 185 'function' => [ 186 'scheduler' => $this->getLanguageService()->getLL('function.scheduler'), 187 'check' => $this->getLanguageService()->getLL('function.check'), 188 'info' => $this->getLanguageService()->getLL('function.info') 189 ] 190 ]; 191 $settings = $parsedBody['SET'] ?? $queryParams['SET'] ?? null; 192 $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, $settings, 'system_txschedulerM1', '', '', ''); 193 194 // Set the form 195 $content = '<form name="tx_scheduler_form" id="tx_scheduler_form" method="post" action="">'; 196 197 // Prepare main content 198 $content .= '<h1>' . $this->getLanguageService()->getLL('function.' . $this->MOD_SETTINGS['function']) . '</h1>'; 199 $previousCMD = Action::cast($parsedBody['previousCMD'] ?? $queryParams['previousCMD'] ?? null); 200 $content .= $this->getModuleContent($previousCMD); 201 $content .= '<div id="extraFieldsSection"></div></form><div id="extraFieldsHidden"></div>'; 202 203 $this->getButtons($request); 204 $this->getModuleMenu(); 205 206 $this->moduleTemplate->setContent($content); 207 return new HtmlResponse($this->moduleTemplate->renderContent()); 208 } 209 210 /** 211 * Get the current action 212 * 213 * @return Action 214 */ 215 public function getCurrentAction(): Action 216 { 217 return $this->action; 218 } 219 220 /** 221 * Generates the action menu 222 */ 223 protected function getModuleMenu(): void 224 { 225 $menu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu(); 226 $menu->setIdentifier('SchedulerJumpMenu'); 227 /** @var UriBuilder $uriBuilder */ 228 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 229 foreach ($this->MOD_MENU['function'] as $controller => $title) { 230 $item = $menu 231 ->makeMenuItem() 232 ->setHref( 233 (string)$uriBuilder->buildUriFromRoute( 234 'system_txschedulerM1', 235 [ 236 'id' => 0, 237 'SET' => [ 238 'function' => $controller 239 ] 240 ] 241 ) 242 ) 243 ->setTitle($title); 244 if ($controller === $this->MOD_SETTINGS['function']) { 245 $item->setActive(true); 246 } 247 $menu->addMenuItem($item); 248 } 249 $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu); 250 } 251 252 /** 253 * Generate the module's content 254 * 255 * @param Action $previousAction 256 * @return string HTML of the module's main content 257 */ 258 protected function getModuleContent(Action $previousAction): string 259 { 260 $content = ''; 261 $sectionTitle = ''; 262 // Get submitted data 263 $this->submittedData = GeneralUtility::_GPmerged('tx_scheduler'); 264 $this->submittedData['uid'] = (int)$this->submittedData['uid']; 265 // If a save command was submitted, handle saving now 266 if (in_array((string)$this->getCurrentAction(), [Action::SAVE, Action::SAVE_CLOSE, Action::SAVE_NEW], true)) { 267 // First check the submitted data 268 $result = $this->preprocessData(); 269 270 // If result is ok, proceed with saving 271 if ($result) { 272 $this->saveTask(); 273 274 if ($this->action->equals(Action::SAVE_CLOSE)) { 275 // Display default screen 276 $this->setCurrentAction(Action::cast(Action::LIST)); 277 } elseif ($this->action->equals(Action::SAVE)) { 278 // After saving a "add form", return to edit 279 $this->setCurrentAction(Action::cast(Action::EDIT)); 280 } elseif ($this->action->equals(Action::SAVE_NEW)) { 281 // Unset submitted data, so that empty form gets displayed 282 unset($this->submittedData); 283 // After saving a "add/edit form", return to add 284 $this->setCurrentAction(Action::cast(Action::ADD)); 285 } else { 286 // Return to edit form 287 $this->setCurrentAction($previousAction); 288 } 289 } else { 290 $this->setCurrentAction($previousAction); 291 } 292 } 293 294 // Handle chosen action 295 switch ((string)$this->MOD_SETTINGS['function']) { 296 case 'scheduler': 297 $this->executeTasks(); 298 299 switch ((string)$this->getCurrentAction()) { 300 case Action::ADD: 301 case Action::EDIT: 302 try { 303 // Try adding or editing 304 $content .= $this->editTaskAction(); 305 $sectionTitle = $this->getLanguageService()->getLL('action.' . $this->getCurrentAction()); 306 } catch (\LogicException|\UnexpectedValueException|\OutOfBoundsException $e) { 307 // Catching all types of exceptions that were previously handled and 308 // converted to messages 309 $content .= $this->listTasksAction(); 310 } catch (\Exception $e) { 311 // Catching all "unexpected" exceptions not previously handled 312 $this->addMessage($e->getMessage(), FlashMessage::ERROR); 313 $content .= $this->listTasksAction(); 314 } 315 break; 316 case Action::DELETE: 317 $this->deleteTask(); 318 $content .= $this->listTasksAction(); 319 break; 320 case Action::STOP: 321 $this->stopTask(); 322 $content .= $this->listTasksAction(); 323 break; 324 case Action::TOGGLE_HIDDEN: 325 $this->toggleDisableAction(); 326 $content .= $this->listTasksAction(); 327 break; 328 case Action::SET_NEXT_EXECUTION_TIME: 329 $this->setNextExecutionTimeAction(); 330 $content .= $this->listTasksAction(); 331 break; 332 case Action::LIST: 333 $content .= $this->listTasksAction(); 334 } 335 break; 336 337 // Setup check screen 338 case 'check': 339 // @todo move check to the report module 340 $content .= $this->checkScreenAction(); 341 break; 342 343 // Information screen 344 case 'info': 345 $content .= $this->infoScreenAction(); 346 break; 347 } 348 // Wrap the content 349 return '<h2>' . $sectionTitle . '</h2><div class="tx_scheduler_mod1">' . $content . '</div>'; 350 } 351 352 /** 353 * This method displays the result of a number of checks 354 * on whether the Scheduler is ready to run or running properly 355 * 356 * @return string Further information 357 */ 358 protected function checkScreenAction(): string 359 { 360 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'CheckScreen.html'); 361 362 // Display information about last automated run, as stored in the system registry 363 $registry = GeneralUtility::makeInstance(Registry::class); 364 $lastRun = $registry->get('tx_scheduler', 'lastRun'); 365 if (!is_array($lastRun)) { 366 $message = $this->getLanguageService()->getLL('msg.noLastRun'); 367 $severity = InfoboxViewHelper::STATE_WARNING; 368 } else { 369 if (empty($lastRun['end']) || empty($lastRun['start']) || empty($lastRun['type'])) { 370 $message = $this->getLanguageService()->getLL('msg.incompleteLastRun'); 371 $severity = InfoboxViewHelper::STATE_WARNING; 372 } else { 373 $startDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['start']); 374 $startTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['start']); 375 $endDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['end']); 376 $endTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['end']); 377 $label = 'automatically'; 378 if ($lastRun['type'] === 'manual') { 379 $label = 'manually'; 380 } 381 $type = $this->getLanguageService()->getLL('label.' . $label); 382 $message = sprintf($this->getLanguageService()->getLL('msg.lastRun'), $type, $startDate, $startTime, $endDate, $endTime); 383 $severity = InfoboxViewHelper::STATE_INFO; 384 } 385 } 386 $this->view->assign('lastRunMessage', $message); 387 $this->view->assign('lastRunSeverity', $severity); 388 389 if (Environment::isComposerMode()) { 390 $this->view->assign('composerMode', true); 391 } else { 392 // Check if CLI script is executable or not 393 $script = GeneralUtility::getFileAbsFileName('EXT:core/bin/typo3'); 394 $this->view->assign('script', $script); 395 // Skip this check if running Windows, as rights do not work the same way on this platform 396 // (i.e. the script will always appear as *not* executable) 397 if (Environment::isWindows()) { 398 $isExecutable = true; 399 } else { 400 $isExecutable = is_executable($script); 401 } 402 if ($isExecutable) { 403 $message = $this->getLanguageService()->getLL('msg.cliScriptExecutable'); 404 $severity = InfoboxViewHelper::STATE_OK; 405 } else { 406 $message = $this->getLanguageService()->getLL('msg.cliScriptNotExecutable'); 407 $severity = InfoboxViewHelper::STATE_ERROR; 408 } 409 $this->view->assign('isExecutableMessage', $message); 410 $this->view->assign('isExecutableSeverity', $severity); 411 } 412 413 $this->view->assign('now', $this->getServerTime()); 414 415 return $this->view->render(); 416 } 417 418 /** 419 * This method gathers information about all available task classes and displays it 420 * 421 * @return string html 422 */ 423 protected function infoScreenAction(): string 424 { 425 $registeredClasses = $this->getRegisteredClasses(); 426 // No classes available, display information message 427 if (empty($registeredClasses)) { 428 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreenNoClasses.html'); 429 return $this->view->render(); 430 } 431 432 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreen.html'); 433 $this->view->assign('registeredClasses', $registeredClasses); 434 435 return $this->view->render(); 436 } 437 438 /** 439 * Delete a task from the execution queue 440 */ 441 protected function deleteTask(): void 442 { 443 try { 444 // Try to fetch the task and delete it 445 $task = $this->scheduler->fetchTask($this->submittedData['uid']); 446 // If the task is currently running, it may not be deleted 447 if ($task->isExecutionRunning()) { 448 $this->addMessage($this->getLanguageService()->getLL('msg.maynotDeleteRunningTask'), FlashMessage::ERROR); 449 } else { 450 if ($this->scheduler->removeTask($task)) { 451 $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was deleted', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]); 452 $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess')); 453 } else { 454 $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR); 455 } 456 } 457 } catch (\UnexpectedValueException $e) { 458 // The task could not be unserialized properly, simply update the database record 459 $taskUid = (int)$this->submittedData['uid']; 460 $result = GeneralUtility::makeInstance(ConnectionPool::class) 461 ->getConnectionForTable('tx_scheduler_task') 462 ->update('tx_scheduler_task', ['deleted' => 1], ['uid' => $taskUid]); 463 if ($result) { 464 $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess')); 465 } else { 466 $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR); 467 } 468 } catch (\OutOfBoundsException $e) { 469 // The task was not found, for some reason 470 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR); 471 } 472 } 473 474 /** 475 * Clears the registered running executions from the task 476 * Note that this doesn't actually stop the running script. It just unmarks 477 * all executions. 478 * @todo find a way to really kill the running task 479 */ 480 protected function stopTask(): void 481 { 482 try { 483 // Try to fetch the task and stop it 484 $task = $this->scheduler->fetchTask($this->submittedData['uid']); 485 if ($task->isExecutionRunning()) { 486 // If the task is indeed currently running, clear marked executions 487 $result = $task->unmarkAllExecutions(); 488 if ($result) { 489 $this->addMessage($this->getLanguageService()->getLL('msg.stopSuccess')); 490 } else { 491 $this->addMessage($this->getLanguageService()->getLL('msg.stopError'), FlashMessage::ERROR); 492 } 493 } else { 494 // The task is not running, nothing to unmark 495 $this->addMessage($this->getLanguageService()->getLL('msg.maynotStopNonRunningTask'), FlashMessage::WARNING); 496 } 497 } catch (\Exception $e) { 498 // The task was not found, for some reason 499 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR); 500 } 501 } 502 503 /** 504 * Toggles the disabled state of the submitted task 505 */ 506 protected function toggleDisableAction(): void 507 { 508 $task = $this->scheduler->fetchTask($this->submittedData['uid']); 509 $task->setDisabled(!$task->isDisabled()); 510 // If a disabled single task is enabled again, we register it for a 511 // single execution at next scheduler run. 512 if ($task->getType() === AbstractTask::TYPE_SINGLE) { 513 $task->registerSingleExecution(time()); 514 } 515 $task->save(); 516 } 517 518 /** 519 * Sets the next execution time of the submitted task to now 520 */ 521 protected function setNextExecutionTimeAction(): void 522 { 523 $task = $this->scheduler->fetchTask($this->submittedData['uid']); 524 $task->setRunOnNextCronJob(true); 525 $task->save(); 526 } 527 528 /** 529 * Return a form to add a new task or edit an existing one 530 * 531 * @return string HTML form to add or edit a task 532 */ 533 protected function editTaskAction(): string 534 { 535 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'EditTask.html'); 536 537 $registeredClasses = $this->getRegisteredClasses(); 538 $registeredTaskGroups = $this->getRegisteredTaskGroups(); 539 540 $taskInfo = []; 541 $task = null; 542 $process = 'edit'; 543 544 if ($this->submittedData['uid'] > 0) { 545 // If editing, retrieve data for existing task 546 try { 547 $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']); 548 // If there's a registered execution, the task should not be edited 549 if (!empty($taskRecord['serialized_executions'])) { 550 $this->addMessage($this->getLanguageService()->getLL('msg.maynotEditRunningTask'), FlashMessage::ERROR); 551 throw new \LogicException('Runnings tasks cannot not be edited', 1251232849); 552 } 553 554 // Get the task object 555 /** @var \TYPO3\CMS\Scheduler\Task\AbstractTask $task */ 556 $task = unserialize($taskRecord['serialized_task_object']); 557 558 // Set some task information 559 $taskInfo['disable'] = $taskRecord['disable']; 560 $taskInfo['description'] = $taskRecord['description']; 561 $taskInfo['task_group'] = $taskRecord['task_group']; 562 563 // Check that the task object is valid 564 if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) { 565 // The task object is valid, process with fetching current data 566 $taskInfo['class'] = get_class($task); 567 // Get execution information 568 $taskInfo['start'] = (int)$task->getExecution()->getStart(); 569 $taskInfo['end'] = (int)$task->getExecution()->getEnd(); 570 $taskInfo['interval'] = $task->getExecution()->getInterval(); 571 $taskInfo['croncmd'] = $task->getExecution()->getCronCmd(); 572 $taskInfo['multiple'] = $task->getExecution()->getMultiple(); 573 if (!empty($taskInfo['interval']) || !empty($taskInfo['croncmd'])) { 574 // Guess task type from the existing information 575 // If an interval or a cron command is defined, it's a recurring task 576 $taskInfo['type'] = AbstractTask::TYPE_RECURRING; 577 $taskInfo['frequency'] = $taskInfo['interval'] ?: $taskInfo['croncmd']; 578 } else { 579 // It's not a recurring task 580 // Make sure interval and cron command are both empty 581 $taskInfo['type'] = AbstractTask::TYPE_SINGLE; 582 $taskInfo['frequency'] = ''; 583 $taskInfo['end'] = 0; 584 } 585 } else { 586 // The task object is not valid 587 // Issue error message 588 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.invalidTaskClassEdit'), get_class($task)), FlashMessage::ERROR); 589 // Initialize empty values 590 $taskInfo['start'] = 0; 591 $taskInfo['end'] = 0; 592 $taskInfo['frequency'] = ''; 593 $taskInfo['multiple'] = false; 594 $taskInfo['type'] = AbstractTask::TYPE_SINGLE; 595 } 596 } catch (\OutOfBoundsException $e) { 597 // Add a message and continue throwing the exception 598 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR); 599 throw $e; 600 } 601 } else { 602 // If adding a new object, set some default values 603 $taskInfo['class'] = key($registeredClasses); 604 $taskInfo['type'] = AbstractTask::TYPE_RECURRING; 605 $taskInfo['start'] = $GLOBALS['EXEC_TIME']; 606 $taskInfo['end'] = ''; 607 $taskInfo['frequency'] = ''; 608 $taskInfo['multiple'] = 0; 609 $process = 'add'; 610 } 611 612 // If some data was already submitted, use it to override 613 // existing data 614 if (!empty($this->submittedData)) { 615 ArrayUtility::mergeRecursiveWithOverrule($taskInfo, $this->submittedData); 616 } 617 618 // Get the extra fields to display for each task that needs some 619 $allAdditionalFields = []; 620 if ($process === 'add') { 621 foreach ($registeredClasses as $class => $registrationInfo) { 622 if (!empty($registrationInfo['provider'])) { 623 /** @var AdditionalFieldProviderInterface $providerObject */ 624 $providerObject = GeneralUtility::makeInstance($registrationInfo['provider']); 625 if ($providerObject instanceof AdditionalFieldProviderInterface) { 626 $additionalFields = $providerObject->getAdditionalFields($taskInfo, null, $this); 627 $allAdditionalFields = array_merge($allAdditionalFields, [$class => $additionalFields]); 628 } 629 } 630 } 631 } elseif ($task !== null && !empty($registeredClasses[$taskInfo['class']]['provider'])) { 632 // only try to fetch additionalFields if the task is valid 633 $providerObject = GeneralUtility::makeInstance($registeredClasses[$taskInfo['class']]['provider']); 634 if ($providerObject instanceof AdditionalFieldProviderInterface) { 635 $allAdditionalFields[$taskInfo['class']] = $providerObject->getAdditionalFields($taskInfo, $task, $this); 636 } 637 } 638 639 // Load necessary JavaScript 640 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler'); 641 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker'); 642 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/PageBrowser'); 643 $this->getPageRenderer()->addJsInlineCode('browse-button', ' 644 function setFormValueFromBrowseWin(fieldReference, elValue, elName) { 645 var res = elValue.split("_"); 646 var element = document.getElementById(fieldReference); 647 element.value = res[1]; 648 } 649 '); 650 651 // Start rendering the add/edit form 652 $this->view->assign('uid', htmlspecialchars((string)$this->submittedData['uid'])); 653 $this->view->assign('cmd', htmlspecialchars((string)$this->getCurrentAction())); 654 $this->view->assign('csh', $this->cshKey); 655 $this->view->assign('lang', 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:'); 656 657 $table = []; 658 659 // Disable checkbox 660 $this->view->assign('task_disable', ($taskInfo['disable'] ? ' checked="checked"' : '')); 661 $this->view->assign('task_disable_label', 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable'); 662 663 // Task class selector 664 // On editing, don't allow changing of the task class, unless it was not valid 665 if ($this->submittedData['uid'] > 0 && !empty($taskInfo['class'])) { 666 $this->view->assign('task_class', $taskInfo['class']); 667 $this->view->assign('task_class_title', $registeredClasses[$taskInfo['class']]['title']); 668 $this->view->assign('task_class_extension', $registeredClasses[$taskInfo['class']]['extension']); 669 } else { 670 // Group registered classes by classname 671 $groupedClasses = []; 672 foreach ($registeredClasses as $class => $classInfo) { 673 $groupedClasses[$classInfo['extension']][$class] = $classInfo; 674 } 675 ksort($groupedClasses); 676 foreach ($groupedClasses as $extension => $class) { 677 foreach ($groupedClasses[$extension] as $class => $classInfo) { 678 $selected = $class == $taskInfo['class'] ? ' selected="selected"' : ''; 679 $groupedClasses[$extension][$class]['selected'] = $selected; 680 } 681 } 682 $this->view->assign('groupedClasses', $groupedClasses); 683 } 684 685 // Task type selector 686 $this->view->assign('task_type_selected_1', ((int)$taskInfo['type'] === AbstractTask::TYPE_SINGLE ? ' selected="selected"' : '')); 687 $this->view->assign('task_type_selected_2', ((int)$taskInfo['type'] === AbstractTask::TYPE_RECURRING ? ' selected="selected"' : '')); 688 689 // Task group selector 690 foreach ($registeredTaskGroups as $key => $taskGroup) { 691 $selected = $taskGroup['uid'] == $taskInfo['task_group'] ? ' selected="selected"' : ''; 692 $registeredTaskGroups[$key]['selected'] = $selected; 693 } 694 $this->view->assign('registeredTaskGroups', $registeredTaskGroups); 695 696 // Start date/time field 697 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '%H:%M %m-%d-%Y' : '%H:%M %d-%m-%Y'; 698 $this->view->assign('start_value_hr', ($taskInfo['start'] > 0 ? strftime($dateFormat, $taskInfo['start']) : '')); 699 $this->view->assign('start_value', $taskInfo['start']); 700 701 // End date/time field 702 // NOTE: datetime fields need a special id naming scheme 703 $this->view->assign('end_value_hr', ($taskInfo['end'] > 0 ? strftime($dateFormat, $taskInfo['end']) : '')); 704 $this->view->assign('end_value', $taskInfo['end']); 705 706 // Frequency input field 707 $this->view->assign('frequency', $taskInfo['frequency']); 708 709 // Multiple execution selector 710 $this->view->assign('multiple', ($taskInfo['multiple'] ? 'checked="checked"' : '')); 711 712 // Description 713 $this->view->assign('description', $taskInfo['description']); 714 715 // Display additional fields 716 $additionalFieldList = []; 717 foreach ($allAdditionalFields as $class => $fields) { 718 if ($class == $taskInfo['class']) { 719 $additionalFieldsStyle = ''; 720 } else { 721 $additionalFieldsStyle = ' style="display: none"'; 722 } 723 // Add each field to the display, if there are indeed any 724 if (isset($fields) && is_array($fields)) { 725 foreach ($fields as $fieldID => $fieldInfo) { 726 $htmlClassName = strtolower(str_replace('\\', '-', $class)); 727 $field = []; 728 $field['htmlClassName'] = $htmlClassName; 729 $field['code'] = $fieldInfo['code']; 730 $field['cshKey'] = $fieldInfo['cshKey']; 731 $field['cshLabel'] = $fieldInfo['cshLabel']; 732 $field['langLabel'] = $fieldInfo['label']; 733 $field['fieldID'] = $fieldID; 734 $field['additionalFieldsStyle'] = $additionalFieldsStyle; 735 $field['browseButton'] = $this->getBrowseButton($fieldID, $fieldInfo); 736 $additionalFieldList[] = $field; 737 } 738 } 739 } 740 $this->view->assign('additionalFields', $additionalFieldList); 741 742 $this->view->assign('returnUrl', (string)GeneralUtility::getIndpEnv('REQUEST_URI')); 743 $this->view->assign('table', implode(LF, $table)); 744 $this->view->assign('now', $this->getServerTime()); 745 $this->view->assign('frequencyOptions', (array)$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['frequencyOptions']); 746 747 return $this->view->render(); 748 } 749 750 /** 751 * @param string $fieldID The id of the field witch contains the page id 752 * @param array $fieldInfo The array with the field info, contains the page title shown beside the button 753 * @return string HTML code for the browse button 754 */ 755 protected function getBrowseButton($fieldID, array $fieldInfo): string 756 { 757 if (isset($fieldInfo['browser']) && ($fieldInfo['browser'] === 'page')) { 758 $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class); 759 $url = (string)$uriBuilder->buildUriFromRoute( 760 'wizard_element_browser', 761 ['mode' => 'db', 'bparams' => $fieldID . '|||pages|'] 762 ); 763 $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.browse_db')); 764 return ' 765 <div><a href="#" data-url=' . htmlspecialchars($url) . ' class="btn btn-default t3js-pageBrowser" title="' . $title . '"> 766 <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-insert-record" data-identifier="actions-insert-record"> 767 <span class="icon-markup">' . $this->iconFactory->getIcon( 768 'actions-insert-record', 769 Icon::SIZE_SMALL 770 )->render() . '</span> 771 </span> 772 </a><span id="page_' . $fieldID . '"> ' . htmlspecialchars($fieldInfo['pageTitle']) . '</span></div>'; 773 } 774 return ''; 775 } 776 777 /** 778 * Execute all selected tasks 779 */ 780 protected function executeTasks(): void 781 { 782 // Continue if some elements have been chosen for execution 783 if (isset($this->submittedData['execute']) && !empty($this->submittedData['execute'])) { 784 // Get list of registered classes 785 $registeredClasses = $this->getRegisteredClasses(); 786 // Loop on all selected tasks 787 foreach ($this->submittedData['execute'] as $uid) { 788 try { 789 // Try fetching the task 790 $task = $this->scheduler->fetchTask($uid); 791 $class = get_class($task); 792 $name = $registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')'; 793 if (GeneralUtility::_POST('go_cron') !== null) { 794 $task->setRunOnNextCronJob(true); 795 $task->save(); 796 } else { 797 // Now try to execute it and report on outcome 798 try { 799 $result = $this->scheduler->executeTask($task); 800 if ($result) { 801 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executed'), $name)); 802 } else { 803 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.notExecuted'), $name), FlashMessage::ERROR); 804 } 805 } catch (\Exception $e) { 806 // An exception was thrown, display its message as an error 807 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $name, $e->getMessage()), FlashMessage::ERROR); 808 } 809 } 810 } catch (\OutOfBoundsException $e) { 811 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $uid), FlashMessage::ERROR); 812 } catch (\UnexpectedValueException $e) { 813 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $uid, $e->getMessage()), FlashMessage::ERROR); 814 } 815 } 816 // Record the run in the system registry 817 $this->scheduler->recordLastRun('manual'); 818 // Make sure to switch to list view after execution 819 $this->setCurrentAction(Action::cast(Action::LIST)); 820 } 821 } 822 823 /** 824 * Assemble display of list of scheduled tasks 825 * 826 * @return string Table of pending tasks 827 */ 828 protected function listTasksAction(): string 829 { 830 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasks.html'); 831 832 // Define display format for dates 833 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm']; 834 835 // Get list of registered task groups 836 $registeredTaskGroups = $this->getRegisteredTaskGroups(); 837 838 // add an empty entry for non-grouped tasks 839 // add in front of list 840 array_unshift($registeredTaskGroups, ['uid' => 0, 'groupName' => '']); 841 842 // Get all registered tasks 843 // Just to get the number of entries 844 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 845 ->getQueryBuilderForTable('tx_scheduler_task'); 846 $queryBuilder->getRestrictions()->removeAll(); 847 848 $result = $queryBuilder->select('t.*') 849 ->addSelect( 850 'g.groupName AS taskGroupName', 851 'g.description AS taskGroupDescription', 852 'g.deleted AS isTaskGroupDeleted' 853 ) 854 ->from('tx_scheduler_task', 't') 855 ->leftJoin( 856 't', 857 'tx_scheduler_task_group', 858 'g', 859 $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid')) 860 ) 861 ->where( 862 $queryBuilder->expr()->eq('t.deleted', 0) 863 ) 864 ->orderBy('g.sorting') 865 ->execute(); 866 867 // Loop on all tasks 868 $temporaryResult = []; 869 while ($row = $result->fetch()) { 870 if ($row['taskGroupName'] === null || $row['isTaskGroupDeleted'] === '1') { 871 $row['taskGroupName'] = ''; 872 $row['taskGroupDescription'] = ''; 873 $row['task_group'] = 0; 874 } 875 $temporaryResult[$row['task_group']]['groupName'] = $row['taskGroupName']; 876 $temporaryResult[$row['task_group']]['groupDescription'] = $row['taskGroupDescription']; 877 $temporaryResult[$row['task_group']]['tasks'][] = $row; 878 } 879 880 // No tasks defined, display information message 881 if (empty($temporaryResult)) { 882 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasksNoTasks.html'); 883 return $this->view->render(); 884 } 885 886 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler'); 887 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip'); 888 889 $tasks = $temporaryResult; 890 891 $registeredClasses = $this->getRegisteredClasses(); 892 $missingClasses = []; 893 foreach ($temporaryResult as $taskIndex => $taskGroup) { 894 foreach ($taskGroup['tasks'] as $recordIndex => $schedulerRecord) { 895 if ((int)$schedulerRecord['disable'] === 1) { 896 $translationKey = 'enable'; 897 } else { 898 $translationKey = 'disable'; 899 } 900 $tasks[$taskIndex]['tasks'][$recordIndex]['translationKey'] = $translationKey; 901 902 // Define some default values 903 $lastExecution = '-'; 904 $isRunning = false; 905 $showAsDisabled = false; 906 // Restore the serialized task and pass it a reference to the scheduler object 907 /** @var \TYPO3\CMS\Scheduler\Task\AbstractTask|ProgressProviderInterface $task */ 908 $task = unserialize($schedulerRecord['serialized_task_object']); 909 $class = get_class($task); 910 if ($class === '__PHP_Incomplete_Class' && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) { 911 $class = $matches['classname']; 912 } 913 $tasks[$taskIndex]['tasks'][$recordIndex]['class'] = $class; 914 // Assemble information about last execution 915 if (!empty($schedulerRecord['lastexecution_time'])) { 916 $lastExecution = date($dateFormat, (int)$schedulerRecord['lastexecution_time']); 917 if ($schedulerRecord['lastexecution_context'] === 'CLI') { 918 $context = $this->getLanguageService()->getLL('label.cron'); 919 } else { 920 $context = $this->getLanguageService()->getLL('label.manual'); 921 } 922 $lastExecution .= ' (' . $context . ')'; 923 } 924 $tasks[$taskIndex]['tasks'][$recordIndex]['lastExecution'] = $lastExecution; 925 926 if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) { 927 $tasks[$taskIndex]['tasks'][$recordIndex]['validClass'] = true; 928 // The task object is valid 929 $labels = []; 930 $additionalInformation = $task->getAdditionalInformation(); 931 if ($task instanceof ProgressProviderInterface) { 932 $progress = round((float)$task->getProgress(), 2); 933 $tasks[$taskIndex]['tasks'][$recordIndex]['progress'] = $progress; 934 } 935 $tasks[$taskIndex]['tasks'][$recordIndex]['classTitle'] = $registeredClasses[$class]['title']; 936 $tasks[$taskIndex]['tasks'][$recordIndex]['classExtension'] = $registeredClasses[$class]['extension']; 937 $tasks[$taskIndex]['tasks'][$recordIndex]['additionalInformation'] = $additionalInformation; 938 // Check if task currently has a running execution 939 if (!empty($schedulerRecord['serialized_executions'])) { 940 $labels[] = [ 941 'class' => 'success', 942 'text' => $this->getLanguageService()->getLL('status.running') 943 ]; 944 $isRunning = true; 945 } 946 $tasks[$taskIndex]['tasks'][$recordIndex]['isRunning'] = $isRunning; 947 948 // Prepare display of next execution date 949 // If task is currently running, date is not displayed (as next hasn't been calculated yet) 950 // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway) 951 if ($isRunning || $schedulerRecord['disable']) { 952 $nextDate = '-'; 953 } else { 954 $nextDate = date($dateFormat, (int)$schedulerRecord['nextexecution']); 955 if (empty($schedulerRecord['nextexecution'])) { 956 $nextDate = $this->getLanguageService()->getLL('none'); 957 } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) { 958 $labels[] = [ 959 'class' => 'warning', 960 'text' => $this->getLanguageService()->getLL('status.late'), 961 'description' => $this->getLanguageService()->getLL('status.legend.scheduled') 962 ]; 963 } 964 } 965 $tasks[$taskIndex]['tasks'][$recordIndex]['nextDate'] = $nextDate; 966 // Get execution type 967 if ($task->getType() === AbstractTask::TYPE_SINGLE) { 968 $execType = $this->getLanguageService()->getLL('label.type.single'); 969 $frequency = '-'; 970 } else { 971 $execType = $this->getLanguageService()->getLL('label.type.recurring'); 972 if ($task->getExecution()->getCronCmd() == '') { 973 $frequency = $task->getExecution()->getInterval(); 974 } else { 975 $frequency = $task->getExecution()->getCronCmd(); 976 } 977 } 978 // Check the disable status 979 // Row is shown dimmed if task is disabled, unless it is still running 980 if ($schedulerRecord['disable'] && !$isRunning) { 981 $labels[] = [ 982 'class' => 'default', 983 'text' => $this->getLanguageService()->getLL('status.disabled') 984 ]; 985 $showAsDisabled = true; 986 } 987 $tasks[$taskIndex]['tasks'][$recordIndex]['execType'] = $execType; 988 $tasks[$taskIndex]['tasks'][$recordIndex]['frequency'] = $frequency; 989 // Get multiple executions setting 990 if ($task->getExecution()->getMultiple()) { 991 $multiple = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes'); 992 } else { 993 $multiple = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no'); 994 } 995 $tasks[$taskIndex]['tasks'][$recordIndex]['multiple'] = $multiple; 996 997 // Check if the last run failed 998 if (!empty($schedulerRecord['lastexecution_failure'])) { 999 // Try to get the stored exception array 1000 /** @var array $exceptionArray */ 1001 $exceptionArray = @unserialize($schedulerRecord['lastexecution_failure']); 1002 // If the exception could not be unserialized, issue a default error message 1003 if (!is_array($exceptionArray) || empty($exceptionArray)) { 1004 $labelDescription = $this->getLanguageService()->getLL('msg.executionFailureDefault'); 1005 } else { 1006 $labelDescription = sprintf($this->getLanguageService()->getLL('msg.executionFailureReport'), $exceptionArray['code'], $exceptionArray['message']); 1007 } 1008 $labels[] = [ 1009 'class' => 'danger', 1010 'text' => $this->getLanguageService()->getLL('status.failure'), 1011 'description' => $labelDescription 1012 ]; 1013 } 1014 $tasks[$taskIndex]['tasks'][$recordIndex]['labels'] = $labels; 1015 if ($showAsDisabled) { 1016 $tasks[$taskIndex]['tasks'][$recordIndex]['showAsDisabled'] = 'disabled'; 1017 } 1018 } else { 1019 $missingClasses[] = $tasks[$taskIndex]['tasks'][$recordIndex]; 1020 unset($tasks[$taskIndex]['tasks'][$recordIndex]); 1021 } 1022 } 1023 } 1024 1025 $this->view->assign('tasks', $tasks); 1026 $this->view->assign('missingClasses', $missingClasses); 1027 $this->view->assign('moduleUri', $this->moduleUri); 1028 $this->view->assign('now', $this->getServerTime()); 1029 1030 return $this->view->render(); 1031 } 1032 1033 /** 1034 * Generates bootstrap labels containing the label statuses 1035 * 1036 * @param array $labels 1037 * @return string 1038 */ 1039 protected function makeStatusLabel(array $labels): string 1040 { 1041 $htmlLabels = []; 1042 foreach ($labels as $label) { 1043 if (empty($label['text'])) { 1044 continue; 1045 } 1046 $htmlLabels[] = '<span class="label label-' . htmlspecialchars($label['class']) . ' pull-right" title="' . htmlspecialchars($label['description']) . '">' . htmlspecialchars($label['text']) . '</span>'; 1047 } 1048 1049 return implode(' ', $htmlLabels); 1050 } 1051 1052 /** 1053 * Saves a task specified in the backend form to the database 1054 */ 1055 protected function saveTask(): void 1056 { 1057 // If a task is being edited fetch old task data 1058 if (!empty($this->submittedData['uid'])) { 1059 try { 1060 $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']); 1061 /** @var \TYPO3\CMS\Scheduler\Task\AbstractTask $task */ 1062 $task = unserialize($taskRecord['serialized_task_object']); 1063 } catch (\OutOfBoundsException $e) { 1064 // If the task could not be fetched, issue an error message 1065 // and exit early 1066 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR); 1067 return; 1068 } 1069 // Register single execution 1070 if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) { 1071 $task->registerSingleExecution($this->submittedData['start']); 1072 } else { 1073 if (!empty($this->submittedData['croncmd'])) { 1074 // Definition by cron-like syntax 1075 $interval = 0; 1076 $cronCmd = $this->submittedData['croncmd']; 1077 } else { 1078 // Definition by interval 1079 $interval = $this->submittedData['interval']; 1080 $cronCmd = ''; 1081 } 1082 // Register recurring execution 1083 $task->registerRecurringExecution($this->submittedData['start'], $interval, $this->submittedData['end'], $this->submittedData['multiple'], $cronCmd); 1084 } 1085 // Set disable flag 1086 $task->setDisabled($this->submittedData['disable']); 1087 // Set description 1088 $task->setDescription($this->submittedData['description']); 1089 // Set task group 1090 $task->setTaskGroup($this->submittedData['task_group']); 1091 // Save additional input values 1092 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) { 1093 /** @var AdditionalFieldProviderInterface $providerObject */ 1094 $providerObject = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']); 1095 if ($providerObject instanceof AdditionalFieldProviderInterface) { 1096 $providerObject->saveAdditionalFields($this->submittedData, $task); 1097 } 1098 } 1099 // Save to database 1100 $result = $this->scheduler->saveTask($task); 1101 if ($result) { 1102 $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was updated', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]); 1103 $this->addMessage($this->getLanguageService()->getLL('msg.updateSuccess')); 1104 } else { 1105 $this->addMessage($this->getLanguageService()->getLL('msg.updateError'), FlashMessage::ERROR); 1106 } 1107 } else { 1108 // A new task is being created 1109 // Create an instance of chosen class 1110 /** @var AbstractTask $task */ 1111 $task = GeneralUtility::makeInstance($this->submittedData['class']); 1112 if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) { 1113 // Set up single execution 1114 $task->registerSingleExecution($this->submittedData['start']); 1115 } else { 1116 // Set up recurring execution 1117 $task->registerRecurringExecution($this->submittedData['start'], $this->submittedData['interval'], $this->submittedData['end'], $this->submittedData['multiple'], $this->submittedData['croncmd']); 1118 } 1119 // Save additional input values 1120 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) { 1121 /** @var AdditionalFieldProviderInterface $providerObject */ 1122 $providerObject = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']); 1123 if ($providerObject instanceof AdditionalFieldProviderInterface) { 1124 $providerObject->saveAdditionalFields($this->submittedData, $task); 1125 } 1126 } 1127 // Set disable flag 1128 $task->setDisabled($this->submittedData['disable']); 1129 // Set description 1130 $task->setDescription($this->submittedData['description']); 1131 // Set description 1132 $task->setTaskGroup($this->submittedData['task_group']); 1133 // Add to database 1134 $result = $this->scheduler->addTask($task); 1135 if ($result) { 1136 $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was added', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]); 1137 $this->addMessage($this->getLanguageService()->getLL('msg.addSuccess')); 1138 1139 // set the uid of the just created task so that we 1140 // can continue editing after initial saving 1141 $this->submittedData['uid'] = $task->getTaskUid(); 1142 } else { 1143 $this->addMessage($this->getLanguageService()->getLL('msg.addError'), FlashMessage::ERROR); 1144 } 1145 } 1146 } 1147 1148 /************************* 1149 * 1150 * INPUT PROCESSING UTILITIES 1151 * 1152 *************************/ 1153 /** 1154 * Checks the submitted data and performs some pre-processing on it 1155 * 1156 * @return bool true if everything was ok, false otherwise 1157 */ 1158 protected function preprocessData() 1159 { 1160 $result = true; 1161 // Validate id 1162 $this->submittedData['uid'] = empty($this->submittedData['uid']) ? 0 : (int)$this->submittedData['uid']; 1163 // Validate selected task class 1164 if (!class_exists($this->submittedData['class'])) { 1165 $this->addMessage($this->getLanguageService()->getLL('msg.noTaskClassFound'), FlashMessage::ERROR); 1166 } 1167 // Check start date 1168 if (empty($this->submittedData['start'])) { 1169 $this->addMessage($this->getLanguageService()->getLL('msg.noStartDate'), FlashMessage::ERROR); 1170 $result = false; 1171 } elseif (is_string($this->submittedData['start']) && (!is_numeric($this->submittedData['start']))) { 1172 try { 1173 $this->submittedData['start'] = $this->convertToTimestamp($this->submittedData['start']); 1174 } catch (\Exception $e) { 1175 $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR); 1176 $result = false; 1177 } 1178 } else { 1179 $this->submittedData['start'] = (int)$this->submittedData['start']; 1180 } 1181 // Check end date, if recurring task 1182 if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING && !empty($this->submittedData['end'])) { 1183 if (is_string($this->submittedData['end']) && (!is_numeric($this->submittedData['end']))) { 1184 try { 1185 $this->submittedData['end'] = $this->convertToTimestamp($this->submittedData['end']); 1186 } catch (\Exception $e) { 1187 $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR); 1188 $result = false; 1189 } 1190 } else { 1191 $this->submittedData['end'] = (int)$this->submittedData['end']; 1192 } 1193 if ($this->submittedData['end'] < $this->submittedData['start']) { 1194 $this->addMessage( 1195 $this->getLanguageService()->getLL('msg.endDateSmallerThanStartDate'), 1196 FlashMessage::ERROR 1197 ); 1198 $result = false; 1199 } 1200 } 1201 // Set default values for interval and cron command 1202 $this->submittedData['interval'] = 0; 1203 $this->submittedData['croncmd'] = ''; 1204 // Check type and validity of frequency, if recurring 1205 if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING) { 1206 $frequency = trim($this->submittedData['frequency']); 1207 if (empty($frequency)) { 1208 // Empty frequency, not valid 1209 $this->addMessage($this->getLanguageService()->getLL('msg.noFrequency'), FlashMessage::ERROR); 1210 $result = false; 1211 } else { 1212 $cronErrorCode = 0; 1213 $cronErrorMessage = ''; 1214 // Try interpreting the cron command 1215 try { 1216 NormalizeCommand::normalize($frequency); 1217 $this->submittedData['croncmd'] = $frequency; 1218 } catch (\Exception $e) { 1219 // Store the exception's result 1220 $cronErrorMessage = $e->getMessage(); 1221 $cronErrorCode = $e->getCode(); 1222 // Check if the frequency is a valid number 1223 // If yes, assume it is a frequency in seconds, and unset cron error code 1224 if (is_numeric($frequency)) { 1225 $this->submittedData['interval'] = (int)$frequency; 1226 unset($cronErrorCode); 1227 } 1228 } 1229 // If there's a cron error code, issue validation error message 1230 if (!empty($cronErrorCode)) { 1231 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.frequencyError'), $cronErrorMessage, $cronErrorCode), FlashMessage::ERROR); 1232 $result = false; 1233 } 1234 } 1235 } 1236 // Validate additional input fields 1237 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) { 1238 /** @var AdditionalFieldProviderInterface $providerObject */ 1239 $providerObject = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']); 1240 if ($providerObject instanceof AdditionalFieldProviderInterface) { 1241 // The validate method will return true if all went well, but that must not 1242 // override previous false values => AND the returned value with the existing one 1243 $result &= $providerObject->validateAdditionalFields($this->submittedData, $this); 1244 } 1245 } 1246 return $result; 1247 } 1248 1249 /** 1250 * Convert input to DateTime and retrieve timestamp 1251 * 1252 * @param string $input 1253 * @return int 1254 */ 1255 protected function convertToTimestamp(string $input): int 1256 { 1257 // Convert to ISO 8601 dates 1258 $dateTime = new \DateTime($input); 1259 $value = $dateTime->getTimestamp(); 1260 if ($value !== 0) { 1261 $value -= date('Z', $value); 1262 } 1263 return $value; 1264 } 1265 1266 /** 1267 * This method is used to add a message to the internal queue 1268 * 1269 * @param string $message The message itself 1270 * @param int $severity Message level (according to FlashMessage class constants) 1271 */ 1272 protected function addMessage($message, $severity = FlashMessage::OK) 1273 { 1274 $this->moduleTemplate->addFlashMessage($message, '', $severity); 1275 } 1276 1277 /** 1278 * This method fetches a list of all classes that have been registered with the Scheduler 1279 * For each item the following information is provided, as an associative array: 1280 * 1281 * ['extension'] => Key of the extension which provides the class 1282 * ['filename'] => Path to the file containing the class 1283 * ['title'] => String (possibly localized) containing a human-readable name for the class 1284 * ['provider'] => Name of class that implements the interface for additional fields, if necessary 1285 * 1286 * The name of the class itself is used as the key of the list array 1287 * 1288 * @return array List of registered classes 1289 */ 1290 protected function getRegisteredClasses(): array 1291 { 1292 $list = []; 1293 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] ?? [] as $class => $registrationInformation) { 1294 $title = isset($registrationInformation['title']) ? $this->getLanguageService()->sL($registrationInformation['title']) : ''; 1295 $description = isset($registrationInformation['description']) ? $this->getLanguageService()->sL($registrationInformation['description']) : ''; 1296 $list[$class] = [ 1297 'extension' => $registrationInformation['extension'], 1298 'title' => $title, 1299 'description' => $description, 1300 'provider' => $registrationInformation['additionalFields'] ?? '' 1301 ]; 1302 } 1303 return $list; 1304 } 1305 1306 /** 1307 * This method fetches list of all group that have been registered with the Scheduler 1308 * 1309 * @return array List of registered groups 1310 */ 1311 protected function getRegisteredTaskGroups(): array 1312 { 1313 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 1314 ->getQueryBuilderForTable('tx_scheduler_task_group'); 1315 1316 return $queryBuilder 1317 ->select('*') 1318 ->from('tx_scheduler_task_group') 1319 ->orderBy('sorting') 1320 ->execute() 1321 ->fetchAll(); 1322 } 1323 1324 /** 1325 * Create the panel of buttons for submitting the form or otherwise perform operations. 1326 * 1327 * @param ServerRequestInterface $request 1328 */ 1329 protected function getButtons(ServerRequestInterface $request): void 1330 { 1331 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar(); 1332 // CSH 1333 $helpButton = $buttonBar->makeHelpButton() 1334 ->setModuleName('_MOD_system_txschedulerM1') 1335 ->setFieldName(''); 1336 $buttonBar->addButton($helpButton); 1337 1338 // Add and Reload 1339 if (in_array((string)$this->getCurrentAction(), [Action::LIST, Action::DELETE, Action::STOP, Action::TOGGLE_HIDDEN, Action::SET_NEXT_EXECUTION_TIME], true)) { 1340 $reloadButton = $buttonBar->makeLinkButton() 1341 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload')) 1342 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL)) 1343 ->setHref($this->moduleUri); 1344 $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT, 1); 1345 if ($this->MOD_SETTINGS['function'] === 'scheduler' && !empty($this->getRegisteredClasses())) { 1346 $addButton = $buttonBar->makeLinkButton() 1347 ->setTitle($this->getLanguageService()->getLL('action.add')) 1348 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-add', Icon::SIZE_SMALL)) 1349 ->setHref($this->moduleUri . '&CMD=' . Action::ADD); 1350 $buttonBar->addButton($addButton, ButtonBar::BUTTON_POSITION_LEFT, 2); 1351 } 1352 } 1353 1354 // Close and Save 1355 if (in_array((string)$this->getCurrentAction(), [Action::ADD, Action::EDIT], true)) { 1356 // Close 1357 $closeButton = $buttonBar->makeLinkButton() 1358 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel')) 1359 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL)) 1360 ->setOnClick('document.location=' . GeneralUtility::quoteJSvalue($this->moduleUri)) 1361 ->setHref('#'); 1362 $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2); 1363 // Save, SaveAndClose, SaveAndNew 1364 $saveButtonDropdown = $buttonBar->makeSplitButton(); 1365 $saveButton = $buttonBar->makeInputButton() 1366 ->setName('CMD') 1367 ->setValue(Action::SAVE) 1368 ->setForm('tx_scheduler_form') 1369 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL)) 1370 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:save')); 1371 $saveButtonDropdown->addItem($saveButton); 1372 $saveAndNewButton = $buttonBar->makeInputButton() 1373 ->setName('CMD') 1374 ->setValue(Action::SAVE_NEW) 1375 ->setForm('tx_scheduler_form') 1376 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-new', Icon::SIZE_SMALL)) 1377 ->setTitle($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.saveAndCreateNewTask')); 1378 $saveButtonDropdown->addItem($saveAndNewButton); 1379 $saveAndCloseButton = $buttonBar->makeInputButton() 1380 ->setName('CMD') 1381 ->setValue(Action::SAVE_CLOSE) 1382 ->setForm('tx_scheduler_form') 1383 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-close', Icon::SIZE_SMALL)) 1384 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:saveAndClose')); 1385 $saveButtonDropdown->addItem($saveAndCloseButton); 1386 $buttonBar->addButton($saveButtonDropdown, ButtonBar::BUTTON_POSITION_LEFT, 3); 1387 } 1388 1389 // Delete 1390 if ($this->getCurrentAction()->equals(Action::EDIT)) { 1391 $deleteButton = $buttonBar->makeLinkButton() 1392 ->setHref($this->moduleUri . '&CMD=' . Action::DELETE . '&tx_scheduler[uid]=' . $request->getQueryParams()['tx_scheduler']['uid']) 1393 ->setClasses('t3js-modal-trigger') 1394 ->setDataAttributes([ 1395 'severity' => 'warning', 1396 'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:delete'), 1397 'button-close-text' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel'), 1398 'content' => $this->getLanguageService()->getLL('msg.delete'), 1399 ]) 1400 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-edit-delete', Icon::SIZE_SMALL)) 1401 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:delete')); 1402 $buttonBar->addButton($deleteButton, ButtonBar::BUTTON_POSITION_LEFT, 4); 1403 } 1404 1405 // Shortcut 1406 $shortcutButton = $buttonBar->makeShortcutButton() 1407 ->setModuleName('system_txschedulerM1') 1408 ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']]) 1409 ->setSetVariables(['function']); 1410 $buttonBar->addButton($shortcutButton); 1411 } 1412 1413 /** 1414 * Set the current action 1415 * 1416 * @param Action $action 1417 */ 1418 protected function setCurrentAction(Action $action): void 1419 { 1420 $this->action = $action; 1421 // @deprecated since TYPO3 v9, will be removed with TYPO3 v10.0 1422 $this->CMD = (string)$action; 1423 } 1424 1425 /** 1426 * @return string 1427 */ 1428 protected function getServerTime(): string 1429 { 1430 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e'; 1431 return date($dateFormat) . ', GMT ' . date('P') . ')'; 1432 } 1433 1434 /** 1435 * Returns the Language Service 1436 * @return LanguageService 1437 */ 1438 protected function getLanguageService(): LanguageService 1439 { 1440 return $GLOBALS['LANG']; 1441 } 1442 1443 /** 1444 * Returns the global BackendUserAuthentication object. 1445 * 1446 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication 1447 */ 1448 protected function getBackendUser(): BackendUserAuthentication 1449 { 1450 return $GLOBALS['BE_USER']; 1451 } 1452 1453 /** 1454 * @return PageRenderer 1455 */ 1456 protected function getPageRenderer(): PageRenderer 1457 { 1458 return GeneralUtility::makeInstance(PageRenderer::class); 1459 } 1460} 1461