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