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