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