f1b2d222dbf716ed60a22d2d38783569b1a271ba
[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
705 return $this->view->render();
706 }
707
708 /**
709 * @param string $fieldID The id of the field witch contains the page id
710 * @param array $fieldInfo The array with the field info, contains the page title shown beside the button
711 * @return string HTML code for the browse button
712 */
713 protected function getBrowseButton($fieldID, array $fieldInfo)
714 {
715 if (isset($fieldInfo['browser']) && ($fieldInfo['browser'] === 'page')) {
716 /** @var \TYPO3\CMS\Backend\Routing\UriBuilder $uriBuilder */
717 $uriBuilder = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Routing\UriBuilder::class);
718 $url = (string)$uriBuilder->buildUriFromRoute(
719 'wizard_element_browser',
720 ['mode' => 'db', 'bparams' => $fieldID . '|||pages|']
721 );
722 $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.browse_db'));
723 return '
724 <div><a href="#" data-url=' . htmlspecialchars($url) . ' class="btn btn-default t3js-pageBrowser" title="' . $title . '">
725 <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-insert-record" data-identifier="actions-insert-record">
726 <span class="icon-markup">' . $this->iconFactory->getIcon(
727 'actions-insert-record',
728 Icon::SIZE_SMALL
729 )->render() . '</span>
730 </span>
731 </a><span id="page_' . $fieldID . '">&nbsp;' . htmlspecialchars($fieldInfo['pageTitle']) . '</span></div>';
732 }
733 return '';
734 }
735
736 /**
737 * Execute all selected tasks
738 */
739 protected function executeTasks()
740 {
741 // Continue if some elements have been chosen for execution
742 if (isset($this->submittedData['execute']) && !empty($this->submittedData['execute'])) {
743 // Get list of registered classes
744 $registeredClasses = $this->getRegisteredClasses();
745 // Loop on all selected tasks
746 foreach ($this->submittedData['execute'] as $uid) {
747 try {
748 // Try fetching the task
749 $task = $this->scheduler->fetchTask($uid);
750 $class = get_class($task);
751 $name = $registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')';
752 if (GeneralUtility::_POST('go_cron') !== null) {
753 $task->setRunOnNextCronJob(true);
754 $task->save();
755 } else {
756 // Now try to execute it and report on outcome
757 try {
758 $result = $this->scheduler->executeTask($task);
759 if ($result) {
760 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executed'), $name));
761 } else {
762 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.notExecuted'), $name), FlashMessage::ERROR);
763 }
764 } catch (\Exception $e) {
765 // An exception was thrown, display its message as an error
766 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $name, $e->getMessage()), FlashMessage::ERROR);
767 }
768 }
769 } catch (\OutOfBoundsException $e) {
770 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $uid), FlashMessage::ERROR);
771 } catch (\UnexpectedValueException $e) {
772 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $uid, $e->getMessage()), FlashMessage::ERROR);
773 }
774 }
775 // Record the run in the system registry
776 $this->scheduler->recordLastRun('manual');
777 // Make sure to switch to list view after execution
778 $this->CMD = 'list';
779 }
780 }
781
782 /**
783 * Assemble display of list of scheduled tasks
784 *
785 * @return string Table of pending tasks
786 */
787 protected function listTasksAction()
788 {
789 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasks.html');
790
791 // Define display format for dates
792 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
793
794 // Get list of registered task groups
795 $registeredTaskGroups = $this->getRegisteredTaskGroups();
796
797 // add an empty entry for non-grouped tasks
798 // add in front of list
799 array_unshift($registeredTaskGroups, ['uid' => 0, 'groupName' => '']);
800
801 // Get all registered tasks
802 // Just to get the number of entries
803 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
804 ->getQueryBuilderForTable('tx_scheduler_task');
805 $queryBuilder->getRestrictions()->removeAll();
806
807 $result = $queryBuilder->select('t.*')
808 ->addSelect(
809 'g.groupName AS taskGroupName',
810 'g.description AS taskGroupDescription',
811 'g.deleted AS isTaskGroupDeleted'
812 )
813 ->from('tx_scheduler_task', 't')
814 ->leftJoin(
815 't',
816 'tx_scheduler_task_group',
817 'g',
818 $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
819 )
820 ->where(
821 $queryBuilder->expr()->eq('t.deleted', 0)
822 )
823 ->orderBy('g.sorting')
824 ->execute();
825
826 // Loop on all tasks
827 $temporaryResult = [];
828 while ($row = $result->fetch()) {
829 if ($row['taskGroupName'] === null || $row['isTaskGroupDeleted'] === '1') {
830 $row['taskGroupName'] = '';
831 $row['taskGroupDescription'] = '';
832 $row['task_group'] = 0;
833 }
834 $temporaryResult[$row['task_group']]['groupName'] = $row['taskGroupName'];
835 $temporaryResult[$row['task_group']]['groupDescription'] = $row['taskGroupDescription'];
836 $temporaryResult[$row['task_group']]['tasks'][] = $row;
837 }
838
839 // No tasks defined, display information message
840 if (empty($temporaryResult)) {
841 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasksNoTasks.html');
842 return $this->view->render();
843 }
844
845 $this->getPageRenderer()->loadJquery();
846 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
847 $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
848
849 $tasks = $temporaryResult;
850
851 $registeredClasses = $this->getRegisteredClasses();
852 $missingClasses = [];
853 foreach ($temporaryResult as $taskIndex => $taskGroup) {
854 foreach ($taskGroup['tasks'] as $recordIndex => $schedulerRecord) {
855 if ((int)$schedulerRecord['disable'] === 1) {
856 $translationKey = 'enable';
857 } else {
858 $translationKey = 'disable';
859 }
860 $tasks[$taskIndex]['tasks'][$recordIndex]['translationKey'] = $translationKey;
861
862 // Define some default values
863 $lastExecution = '-';
864 $isRunning = false;
865 $showAsDisabled = false;
866 // Restore the serialized task and pass it a reference to the scheduler object
867 /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask|ProgressProviderInterface */
868 $task = unserialize($schedulerRecord['serialized_task_object']);
869 $class = get_class($task);
870 if ($class === '__PHP_Incomplete_Class' && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) {
871 $class = $matches['classname'];
872 }
873 $tasks[$taskIndex]['tasks'][$recordIndex]['class'] = $class;
874 // Assemble information about last execution
875 if (!empty($schedulerRecord['lastexecution_time'])) {
876 $lastExecution = date($dateFormat, $schedulerRecord['lastexecution_time']);
877 if ($schedulerRecord['lastexecution_context'] === 'CLI') {
878 $context = $this->getLanguageService()->getLL('label.cron');
879 } else {
880 $context = $this->getLanguageService()->getLL('label.manual');
881 }
882 $lastExecution .= ' (' . $context . ')';
883 }
884 $tasks[$taskIndex]['tasks'][$recordIndex]['lastExecution'] = $lastExecution;
885
886 if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
887 $tasks[$taskIndex]['tasks'][$recordIndex]['validClass'] = true;
888 // The task object is valid
889 $labels = [];
890 $additionalInformation = $task->getAdditionalInformation();
891 if ($task instanceof ProgressProviderInterface) {
892 $progress = round((float)$task->getProgress(), 2);
893 $tasks[$taskIndex]['tasks'][$recordIndex]['progress'] = $progress;
894 }
895 $tasks[$taskIndex]['tasks'][$recordIndex]['classTitle'] = $registeredClasses[$class]['title'];
896 $tasks[$taskIndex]['tasks'][$recordIndex]['classExtension'] = $registeredClasses[$class]['extension'];
897 $tasks[$taskIndex]['tasks'][$recordIndex]['additionalInformation'] = $additionalInformation;
898 // Check if task currently has a running execution
899 if (!empty($schedulerRecord['serialized_executions'])) {
900 $labels[] = [
901 'class' => 'success',
902 'text' => $this->getLanguageService()->getLL('status.running')
903 ];
904 $isRunning = true;
905 }
906 $tasks[$taskIndex]['tasks'][$recordIndex]['isRunning'] = $isRunning;
907
908 // Prepare display of next execution date
909 // If task is currently running, date is not displayed (as next hasn't been calculated yet)
910 // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
911 if ($isRunning || $schedulerRecord['disable']) {
912 $nextDate = '-';
913 } else {
914 $nextDate = date($dateFormat, $schedulerRecord['nextexecution']);
915 if (empty($schedulerRecord['nextexecution'])) {
916 $nextDate = $this->getLanguageService()->getLL('none');
917 } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
918 $labels[] = [
919 'class' => 'warning',
920 'text' => $this->getLanguageService()->getLL('status.late'),
921 'description' => $this->getLanguageService()->getLL('status.legend.scheduled')
922 ];
923 }
924 }
925 $tasks[$taskIndex]['tasks'][$recordIndex]['nextDate'] = $nextDate;
926 // Get execution type
927 if ($task->getType() === AbstractTask::TYPE_SINGLE) {
928 $execType = $this->getLanguageService()->getLL('label.type.single');
929 $frequency = '-';
930 } else {
931 $execType = $this->getLanguageService()->getLL('label.type.recurring');
932 if ($task->getExecution()->getCronCmd() == '') {
933 $frequency = $task->getExecution()->getInterval();
934 } else {
935 $frequency = $task->getExecution()->getCronCmd();
936 }
937 }
938 // Check the disable status
939 // Row is shown dimmed if task is disabled, unless it is still running
940 if ($schedulerRecord['disable'] && !$isRunning) {
941 $labels[] = [
942 'class' => 'default',
943 'text' => $this->getLanguageService()->getLL('status.disabled')
944 ];
945 $showAsDisabled = true;
946 }
947 $tasks[$taskIndex]['tasks'][$recordIndex]['execType'] = $execType;
948 $tasks[$taskIndex]['tasks'][$recordIndex]['frequency'] = $frequency;
949 // Get multiple executions setting
950 if ($task->getExecution()->getMultiple()) {
951 $multiple = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes');
952 } else {
953 $multiple = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no');
954 }
955 $tasks[$taskIndex]['tasks'][$recordIndex]['multiple'] = $multiple;
956
957 // Check if the last run failed
958 if (!empty($schedulerRecord['lastexecution_failure'])) {
959 // Try to get the stored exception array
960 /** @var $exceptionArray array */
961 $exceptionArray = @unserialize($schedulerRecord['lastexecution_failure']);
962 // If the exception could not be unserialized, issue a default error message
963 if (!is_array($exceptionArray) || empty($exceptionArray)) {
964 $labelDescription = $this->getLanguageService()->getLL('msg.executionFailureDefault');
965 } else {
966 $labelDescription = sprintf($this->getLanguageService()->getLL('msg.executionFailureReport'), $exceptionArray['code'], $exceptionArray['message']);
967 }
968 $labels[] = [
969 'class' => 'danger',
970 'text' => $this->getLanguageService()->getLL('status.failure'),
971 'description' => $labelDescription
972 ];
973 }
974 $tasks[$taskIndex]['tasks'][$recordIndex]['labels'] = $labels;
975 if ($showAsDisabled) {
976 $tasks[$taskIndex]['tasks'][$recordIndex]['showAsDisabled'] = 'disabled';
977 }
978 } else {
979 $missingClasses[] = $tasks[$taskIndex]['tasks'][$recordIndex];
980 unset($tasks[$taskIndex]['tasks'][$recordIndex]);
981 }
982 }
983 }
984
985 $this->view->assign('tasks', $tasks);
986 $this->view->assign('missingClasses', $missingClasses);
987 $this->view->assign('moduleUri', $this->moduleUri);
988 $this->view->assign('now', $this->getServerTime());
989
990 return $this->view->render();
991 }
992
993 /**
994 * Generates bootstrap labels containing the label statuses
995 *
996 * @param array $labels
997 * @return string
998 */
999 protected function makeStatusLabel(array $labels)
1000 {
1001 $htmlLabels = [];
1002 foreach ($labels as $label) {
1003 if (empty($label['text'])) {
1004 continue;
1005 }
1006 $htmlLabels[] = '<span class="label label-' . htmlspecialchars($label['class']) . ' pull-right" title="' . htmlspecialchars($label['description']) . '">' . htmlspecialchars($label['text']) . '</span>';
1007 }
1008
1009 return implode('&nbsp;', $htmlLabels);
1010 }
1011
1012 /**
1013 * Saves a task specified in the backend form to the database
1014 */
1015 protected function saveTask()
1016 {
1017 // If a task is being edited fetch old task data
1018 if (!empty($this->submittedData['uid'])) {
1019 try {
1020 $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
1021 /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask */
1022 $task = unserialize($taskRecord['serialized_task_object']);
1023 } catch (\OutOfBoundsException $e) {
1024 // If the task could not be fetched, issue an error message
1025 // and exit early
1026 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
1027 return;
1028 }
1029 // Register single execution
1030 if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1031 $task->registerSingleExecution($this->submittedData['start']);
1032 } else {
1033 if (!empty($this->submittedData['croncmd'])) {
1034 // Definition by cron-like syntax
1035 $interval = 0;
1036 $cronCmd = $this->submittedData['croncmd'];
1037 } else {
1038 // Definition by interval
1039 $interval = $this->submittedData['interval'];
1040 $cronCmd = '';
1041 }
1042 // Register recurring execution
1043 $task->registerRecurringExecution($this->submittedData['start'], $interval, $this->submittedData['end'], $this->submittedData['multiple'], $cronCmd);
1044 }
1045 // Set disable flag
1046 $task->setDisabled($this->submittedData['disable']);
1047 // Set description
1048 $task->setDescription($this->submittedData['description']);
1049 // Set task group
1050 $task->setTaskGroup($this->submittedData['task_group']);
1051 // Save additional input values
1052 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1053 /** @var $providerObject AdditionalFieldProviderInterface */
1054 $providerObject = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1055 if ($providerObject instanceof AdditionalFieldProviderInterface) {
1056 $providerObject->saveAdditionalFields($this->submittedData, $task);
1057 }
1058 }
1059 // Save to database
1060 $result = $this->scheduler->saveTask($task);
1061 if ($result) {
1062 $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was updated', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1063 $this->addMessage($this->getLanguageService()->getLL('msg.updateSuccess'));
1064 } else {
1065 $this->addMessage($this->getLanguageService()->getLL('msg.updateError'), FlashMessage::ERROR);
1066 }
1067 } else {
1068 // A new task is being created
1069 // Create an instance of chosen class
1070 /** @var $task AbstractTask */
1071 $task = GeneralUtility::makeInstance($this->submittedData['class']);
1072 if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1073 // Set up single execution
1074 $task->registerSingleExecution($this->submittedData['start']);
1075 } else {
1076 // Set up recurring execution
1077 $task->registerRecurringExecution($this->submittedData['start'], $this->submittedData['interval'], $this->submittedData['end'], $this->submittedData['multiple'], $this->submittedData['croncmd']);
1078 }
1079 // Save additional input values
1080 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1081 /** @var $providerObject AdditionalFieldProviderInterface */
1082 $providerObject = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1083 if ($providerObject instanceof AdditionalFieldProviderInterface) {
1084 $providerObject->saveAdditionalFields($this->submittedData, $task);
1085 }
1086 }
1087 // Set disable flag
1088 $task->setDisabled($this->submittedData['disable']);
1089 // Set description
1090 $task->setDescription($this->submittedData['description']);
1091 // Set description
1092 $task->setTaskGroup($this->submittedData['task_group']);
1093 // Add to database
1094 $result = $this->scheduler->addTask($task);
1095 if ($result) {
1096 $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was added', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1097 $this->addMessage($this->getLanguageService()->getLL('msg.addSuccess'));
1098
1099 // set the uid of the just created task so that we
1100 // can continue editing after initial saving
1101 $this->submittedData['uid'] = $task->getTaskUid();
1102 } else {
1103 $this->addMessage($this->getLanguageService()->getLL('msg.addError'), FlashMessage::ERROR);
1104 }
1105 }
1106 }
1107
1108 /*************************
1109 *
1110 * INPUT PROCESSING UTILITIES
1111 *
1112 *************************/
1113 /**
1114 * Checks the submitted data and performs some pre-processing on it
1115 *
1116 * @return bool true if everything was ok, false otherwise
1117 */
1118 protected function preprocessData()
1119 {
1120 $result = true;
1121 // Validate id
1122 $this->submittedData['uid'] = empty($this->submittedData['uid']) ? 0 : (int)$this->submittedData['uid'];
1123 // Validate selected task class
1124 if (!class_exists($this->submittedData['class'])) {
1125 $this->addMessage($this->getLanguageService()->getLL('msg.noTaskClassFound'), FlashMessage::ERROR);
1126 }
1127 // Check start date
1128 if (empty($this->submittedData['start'])) {
1129 $this->addMessage($this->getLanguageService()->getLL('msg.noStartDate'), FlashMessage::ERROR);
1130 $result = false;
1131 } elseif (is_string($this->submittedData['start']) && (!is_numeric($this->submittedData['start']))) {
1132 try {
1133 $this->submittedData['start'] = $this->convertToTimestamp($this->submittedData['start']);
1134 } catch (\Exception $e) {
1135 $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR);
1136 $result = false;
1137 }
1138 } else {
1139 $this->submittedData['start'] = (int)$this->submittedData['start'];
1140 }
1141 // Check end date, if recurring task
1142 if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING && !empty($this->submittedData['end'])) {
1143 if (is_string($this->submittedData['end']) && (!is_numeric($this->submittedData['end']))) {
1144 try {
1145 $this->submittedData['end'] = $this->convertToTimestamp($this->submittedData['end']);
1146 } catch (\Exception $e) {
1147 $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR);
1148 $result = false;
1149 }
1150 } else {
1151 $this->submittedData['end'] = (int)$this->submittedData['end'];
1152 }
1153 if ($this->submittedData['end'] < $this->submittedData['start']) {
1154 $this->addMessage(
1155 $this->getLanguageService()->getLL('msg.endDateSmallerThanStartDate'),
1156 FlashMessage::ERROR
1157 );
1158 $result = false;
1159 }
1160 }
1161 // Set default values for interval and cron command
1162 $this->submittedData['interval'] = 0;
1163 $this->submittedData['croncmd'] = '';
1164 // Check type and validity of frequency, if recurring
1165 if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING) {
1166 $frequency = trim($this->submittedData['frequency']);
1167 if (empty($frequency)) {
1168 // Empty frequency, not valid
1169 $this->addMessage($this->getLanguageService()->getLL('msg.noFrequency'), FlashMessage::ERROR);
1170 $result = false;
1171 } else {
1172 $cronErrorCode = 0;
1173 $cronErrorMessage = '';
1174 // Try interpreting the cron command
1175 try {
1176 NormalizeCommand::normalize($frequency);
1177 $this->submittedData['croncmd'] = $frequency;
1178 } catch (\Exception $e) {
1179 // Store the exception's result
1180 $cronErrorMessage = $e->getMessage();
1181 $cronErrorCode = $e->getCode();
1182 // Check if the frequency is a valid number
1183 // If yes, assume it is a frequency in seconds, and unset cron error code
1184 if (is_numeric($frequency)) {
1185 $this->submittedData['interval'] = (int)$frequency;
1186 unset($cronErrorCode);
1187 }
1188 }
1189 // If there's a cron error code, issue validation error message
1190 if (!empty($cronErrorCode)) {
1191 $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.frequencyError'), $cronErrorMessage, $cronErrorCode), FlashMessage::ERROR);
1192 $result = false;
1193 }
1194 }
1195 }
1196 // Validate additional input fields
1197 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1198 /** @var $providerObject AdditionalFieldProviderInterface */
1199 $providerObject = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1200 if ($providerObject instanceof AdditionalFieldProviderInterface) {
1201 // The validate method will return true if all went well, but that must not
1202 // override previous false values => AND the returned value with the existing one
1203 $result &= $providerObject->validateAdditionalFields($this->submittedData, $this);
1204 }
1205 }
1206 return $result;
1207 }
1208
1209 /**
1210 * Convert input to DateTime and retrieve timestamp
1211 *
1212 * @param string $input
1213 * @return int
1214 */
1215 protected function convertToTimestamp(string $input): int
1216 {
1217 // Convert to ISO 8601 dates
1218 $dateTime = new \DateTime($input);
1219 $value = $dateTime->getTimestamp();
1220 if ($value !== 0) {
1221 $value -= date('Z', $value);
1222 }
1223 return $value;
1224 }
1225
1226 /**
1227 * This method is used to add a message to the internal queue
1228 *
1229 * @param string $message The message itself
1230 * @param int $severity Message level (according to FlashMessage class constants)
1231 */
1232 public function addMessage($message, $severity = FlashMessage::OK)
1233 {
1234 $this->moduleTemplate->addFlashMessage($message, '', $severity);
1235 }
1236
1237 /**
1238 * This method fetches a list of all classes that have been registered with the Scheduler
1239 * For each item the following information is provided, as an associative array:
1240 *
1241 * ['extension'] => Key of the extension which provides the class
1242 * ['filename'] => Path to the file containing the class
1243 * ['title'] => String (possibly localized) containing a human-readable name for the class
1244 * ['provider'] => Name of class that implements the interface for additional fields, if necessary
1245 *
1246 * The name of the class itself is used as the key of the list array
1247 *
1248 * @return array List of registered classes
1249 */
1250 protected function getRegisteredClasses()
1251 {
1252 $list = [];
1253 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] ?? [] as $class => $registrationInformation) {
1254 $title = isset($registrationInformation['title']) ? $this->getLanguageService()->sL($registrationInformation['title']) : '';
1255 $description = isset($registrationInformation['description']) ? $this->getLanguageService()->sL($registrationInformation['description']) : '';
1256 $list[$class] = [
1257 'extension' => $registrationInformation['extension'],
1258 'title' => $title,
1259 'description' => $description,
1260 'provider' => $registrationInformation['additionalFields'] ?? ''
1261 ];
1262 }
1263 return $list;
1264 }
1265
1266 /**
1267 * This method fetches list of all group that have been registered with the Scheduler
1268 *
1269 * @return array List of registered groups
1270 */
1271 protected function getRegisteredTaskGroups()
1272 {
1273 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1274 ->getQueryBuilderForTable('tx_scheduler_task_group');
1275
1276 return $queryBuilder
1277 ->select('*')
1278 ->from('tx_scheduler_task_group')
1279 ->orderBy('sorting')
1280 ->execute()
1281 ->fetchAll();
1282 }
1283
1284 /**
1285 * Create the panel of buttons for submitting the form or otherwise perform operations.
1286 */
1287 protected function getButtons()
1288 {
1289 $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1290 // CSH
1291 $helpButton = $buttonBar->makeHelpButton()
1292 ->setModuleName('_MOD_system_txschedulerM1')
1293 ->setFieldName('');
1294 $buttonBar->addButton($helpButton);
1295 // Add and Reload
1296 if (empty($this->CMD) || $this->CMD === 'list' || $this->CMD === 'delete' || $this->CMD === 'stop' || $this->CMD === 'toggleHidden' || $this->CMD === 'setNextExecutionTime') {
1297 $reloadButton = $buttonBar->makeLinkButton()
1298 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
1299 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL))
1300 ->setHref($this->moduleUri);
1301 $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1302 if ($this->MOD_SETTINGS['function'] === 'scheduler' && !empty($this->getRegisteredClasses())) {
1303 $addButton = $buttonBar->makeLinkButton()
1304 ->setTitle($this->getLanguageService()->getLL('action.add'))
1305 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-add', Icon::SIZE_SMALL))
1306 ->setHref($this->moduleUri . '&CMD=add');
1307 $buttonBar->addButton($addButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1308 }
1309 }
1310 // Close and Save
1311 if ($this->CMD === 'add' || $this->CMD === 'edit') {
1312 // Close
1313 $closeButton = $buttonBar->makeLinkButton()
1314 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel'))
1315 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL))
1316 ->setOnClick('document.location=' . GeneralUtility::quoteJSvalue($this->moduleUri))
1317 ->setHref('#');
1318 $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1319 // Save, SaveAndClose, SaveAndNew
1320 $saveButtonDropdown = $buttonBar->makeSplitButton();
1321 $saveButton = $buttonBar->makeInputButton()
1322 ->setName('CMD')
1323 ->setValue('save')
1324 ->setForm('tx_scheduler_form')
1325 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
1326 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:save'));
1327 $saveButtonDropdown->addItem($saveButton);
1328 $saveAndNewButton = $buttonBar->makeInputButton()
1329 ->setName('CMD')
1330 ->setValue('savenew')
1331 ->setForm('tx_scheduler_form')
1332 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-new', Icon::SIZE_SMALL))
1333 ->setTitle($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.saveAndCreateNewTask'));
1334 $saveButtonDropdown->addItem($saveAndNewButton);
1335 $saveAndCloseButton = $buttonBar->makeInputButton()
1336 ->setName('CMD')
1337 ->setValue('saveclose')
1338 ->setForm('tx_scheduler_form')
1339 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-close', Icon::SIZE_SMALL))
1340 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:saveAndClose'));
1341 $saveButtonDropdown->addItem($saveAndCloseButton);
1342 $buttonBar->addButton($saveButtonDropdown, ButtonBar::BUTTON_POSITION_LEFT, 3);
1343 }
1344 // Edit
1345 if ($this->CMD === 'edit') {
1346 $deleteButton = $buttonBar->makeInputButton()
1347 ->setName('CMD')
1348 ->setValue('delete')
1349 ->setForm('tx_scheduler_form')
1350 ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-edit-delete', Icon::SIZE_SMALL))
1351 ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:delete'));
1352 $buttonBar->addButton($deleteButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
1353 }
1354 // Shortcut
1355 $shortcutButton = $buttonBar->makeShortcutButton()
1356 ->setModuleName('system_txschedulerM1')
1357 ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
1358 ->setSetVariables(['function']);
1359 $buttonBar->addButton($shortcutButton);
1360 }
1361
1362 /**
1363 * @return string
1364 */
1365 protected function getServerTime()
1366 {
1367 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e';
1368 return date($dateFormat) . ', GMT ' . date('P') . ')';
1369 }
1370
1371 /**
1372 * Returns the Language Service
1373 * @return LanguageService
1374 */
1375 protected function getLanguageService()
1376 {
1377 return $GLOBALS['LANG'];
1378 }
1379
1380 /**
1381 * Returns the global BackendUserAuthentication object.
1382 *
1383 * @return \TYPO3\CMS\Core\Authentication\BackendUserAuthentication
1384 */
1385 protected function getBackendUser()
1386 {
1387 return $GLOBALS['BE_USER'];
1388 }
1389
1390 /**
1391 * @return PageRenderer
1392 */
1393 protected function getPageRenderer()
1394 {
1395 return GeneralUtility::makeInstance(PageRenderer::class);
1396 }
1397 }