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