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