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