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