SchedulerModuleController   F
last analyzed

Complexity

Total Complexity 184

Size/Duplication

Total Lines 1384
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 184
eloc 727
dl 0
loc 1384
rs 1.873
c 0
b 0
f 0

28 Methods

Rating   Name   Duplication   Size   Complexity  
A makeStatusLabel() 0 11 3
A getRegisteredTaskGroups() 0 11 1
A addMessage() 0 3 1
A getRegisteredClasses() 0 14 4
C saveTask() 0 89 12
A convertToTimestamp() 0 9 2
B executeTasks() 0 40 9
F preprocessData() 0 89 20
F listTasksAction() 0 203 29
A __construct() 0 18 1
A getServerTime() 0 4 1
A toggleDisableAction() 0 10 2
A getLanguageService() 0 3 1
A setNextExecutionTimeAction() 0 5 1
D getModuleContent() 0 90 18
B deleteTask() 0 30 6
A getCurrentAction() 0 3 1
A getBackendUser() 0 3 1
B getButtons() 0 91 8
A mainAction() 0 40 1
F editTaskAction() 0 207 36
A getBrowseButton() 0 17 3
A infoScreenAction() 0 13 2
A getModuleMenu() 0 25 3
A stopTask() 0 20 4
A setCurrentAction() 0 3 1
A getSectionTitle() 0 7 4
B checkScreenAction() 0 58 9

How to fix   Complexity   

Complex Class

Complex classes like SchedulerModuleController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchedulerModuleController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
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
18
namespace TYPO3\CMS\Scheduler\Controller;
19
20
use Psr\Http\Message\ResponseInterface;
21
use Psr\Http\Message\ServerRequestInterface;
22
use TYPO3\CMS\Backend\Routing\UriBuilder;
23
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
24
use TYPO3\CMS\Backend\Template\ModuleTemplate;
25
use TYPO3\CMS\Backend\Template\ModuleTemplateFactory;
26
use TYPO3\CMS\Backend\Utility\BackendUtility;
27
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
28
use TYPO3\CMS\Core\Core\Environment;
29
use TYPO3\CMS\Core\Database\ConnectionPool;
30
use TYPO3\CMS\Core\Http\HtmlResponse;
31
use TYPO3\CMS\Core\Imaging\Icon;
32
use TYPO3\CMS\Core\Imaging\IconFactory;
33
use TYPO3\CMS\Core\Localization\LanguageService;
34
use TYPO3\CMS\Core\Messaging\FlashMessage;
35
use TYPO3\CMS\Core\Page\PageRenderer;
36
use TYPO3\CMS\Core\Registry;
37
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
38
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
39
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
40
use TYPO3\CMS\Core\Utility\ArrayUtility;
41
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
42
use TYPO3\CMS\Core\Utility\GeneralUtility;
43
use TYPO3\CMS\Fluid\View\StandaloneView;
44
use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper;
45
use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface;
46
use TYPO3\CMS\Scheduler\CronCommand\NormalizeCommand;
47
use TYPO3\CMS\Scheduler\ProgressProviderInterface;
48
use TYPO3\CMS\Scheduler\Scheduler;
49
use TYPO3\CMS\Scheduler\Task\AbstractTask;
50
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
51
52
/**
53
 * Module 'TYPO3 Scheduler administration module' for the 'scheduler' extension.
54
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
55
 */
56
class SchedulerModuleController
57
{
58
59
    /**
60
     * Array containing submitted data when editing or adding a task
61
     *
62
     * @var array
63
     */
64
    protected $submittedData = [];
65
66
    /**
67
     * Array containing all messages issued by the application logic
68
     * Contains the error's severity and the message itself
69
     *
70
     * @var array
71
     */
72
    protected $messages = [];
73
74
    /**
75
     * @var string Key of the CSH file
76
     */
77
    protected $cshKey = '_MOD_system_txschedulerM1';
78
79
    /**
80
     * @var string
81
     */
82
    protected $backendTemplatePath = '';
83
84
    /**
85
     * @var StandaloneView
86
     */
87
    protected $view;
88
89
    /**
90
     * @var string Base URI of scheduler module
91
     */
92
    protected $moduleUri;
93
94
    /**
95
     * ModuleTemplate Container
96
     *
97
     * @var ModuleTemplate
98
     */
99
    protected $moduleTemplate;
100
101
    /**
102
     * @var Action
103
     */
104
    protected $action;
105
106
    /**
107
     * The module menu items array. Each key represents a key for which values can range between the items in the array of that key.
108
     *
109
     * @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
     * @var array
119
     */
120
    protected $MOD_SETTINGS = [];
121
122
    protected Scheduler $scheduler;
123
    protected IconFactory $iconFactory;
124
    protected PageRenderer $pageRenderer;
125
    protected UriBuilder $uriBuilder;
126
    protected ModuleTemplateFactory $moduleTemplateFactory;
127
128
    public function __construct(
129
        Scheduler $scheduler,
130
        IconFactory $iconFactory,
131
        PageRenderer $pageRenderer,
132
        UriBuilder $uriBuilder,
133
        ModuleTemplateFactory $moduleTemplateFactory
134
    ) {
135
        $this->scheduler = $scheduler;
136
        $this->iconFactory = $iconFactory;
137
        $this->pageRenderer = $pageRenderer;
138
        $this->uriBuilder = $uriBuilder;
139
        $this->moduleTemplateFactory = $moduleTemplateFactory;
140
        $this->getLanguageService()->includeLLFile('EXT:scheduler/Resources/Private/Language/locallang.xlf');
141
        $this->backendTemplatePath = ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Backend/SchedulerModule/';
142
        $this->view = GeneralUtility::makeInstance(StandaloneView::class);
143
        $this->view->getRequest()->setControllerExtensionName('scheduler');
144
        $this->view->setPartialRootPaths(['EXT:scheduler/Resources/Private/Partials/Backend/SchedulerModule/']);
145
        $this->moduleUri = (string)$this->uriBuilder->buildUriFromRoute('system_txschedulerM1');
146
    }
147
148
    /**
149
     * Injects the request object for the current request or subrequest
150
     *
151
     * @param ServerRequestInterface $request the current request
152
     * @return ResponseInterface the response with the content
153
     */
154
    public function mainAction(ServerRequestInterface $request): ResponseInterface
155
    {
156
        $this->moduleTemplate = $this->moduleTemplateFactory->create($request);
157
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
158
        $parsedBody = $request->getParsedBody();
159
        $queryParams = $request->getQueryParams();
160
161
        $this->setCurrentAction(Action::cast($parsedBody['CMD'] ?? $queryParams['CMD'] ?? null));
162
        $this->MOD_MENU = [
163
            'function' => [
164
                'scheduler' => $this->getLanguageService()->getLL('function.scheduler'),
165
                'check' => $this->getLanguageService()->getLL('function.check'),
166
                'info' => $this->getLanguageService()->getLL('function.info')
167
            ]
168
        ];
169
        $settings = $parsedBody['SET'] ?? $queryParams['SET'] ?? null;
170
        $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, $settings, 'system_txschedulerM1', '', '', '');
171
172
        $previousCMD = Action::cast($parsedBody['previousCMD'] ?? $queryParams['previousCMD'] ?? null);
173
        // Prepare main content
174
        $content = $this->getModuleContent($previousCMD, $request->getAttribute('normalizedParams')->getRequestUri());
175
176
        $this->view->setTemplatePathAndFilename(
177
            GeneralUtility::getFileAbsFileName('EXT:scheduler/Resources/Private/Templates/Backend/SchedulerModule/Index.html')
178
        );
179
        $this->view->assignMultiple([
180
           'headline' =>  $this->getLanguageService()->getLL('function.' . $this->MOD_SETTINGS['function']),
181
           'sectionTitle' => $this->getSectionTitle(),
182
           'content' => $content,
183
        ]);
184
185
        $this->getButtons($request);
186
        $this->getModuleMenu();
187
188
        $this->moduleTemplate->setContent($this->view->render());
189
        $this->moduleTemplate->setTitle(
190
            $this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang_mod.xlf:mlang_tabs_tab'),
191
            $this->getLanguageService()->getLL('function.' . $this->MOD_SETTINGS['function'])
192
        );
193
        return new HtmlResponse($this->moduleTemplate->renderContent());
194
    }
195
196
    /**
197
     * Get the current action
198
     *
199
     * @return Action
200
     */
201
    public function getCurrentAction(): Action
202
    {
203
        return $this->action;
204
    }
205
206
    /**
207
     * Generates the action menu
208
     */
209
    protected function getModuleMenu(): void
210
    {
211
        $menu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
212
        $menu->setIdentifier('SchedulerJumpMenu');
213
        foreach ($this->MOD_MENU['function'] as $controller => $title) {
214
            $item = $menu
215
                ->makeMenuItem()
216
                ->setHref(
217
                    (string)$this->uriBuilder->buildUriFromRoute(
218
                        'system_txschedulerM1',
219
                        [
220
                            'id' => 0,
221
                            'SET' => [
222
                                'function' => $controller
223
                            ]
224
                        ]
225
                    )
226
                )
227
                ->setTitle($title);
228
            if ($controller === $this->MOD_SETTINGS['function']) {
229
                $item->setActive(true);
230
            }
231
            $menu->addMenuItem($item);
232
        }
233
        $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);
234
    }
235
236
    /**
237
     * Generate the module's content
238
     *
239
     * @param Action $previousAction
240
     * @param string $requestUri
241
     * @return string HTML of the module's main content
242
     */
243
    protected function getModuleContent(Action $previousAction, string $requestUri): string
244
    {
245
        $content = '';
246
        // Get submitted data
247
        $this->submittedData = GeneralUtility::_GPmerged('tx_scheduler');
248
        $this->submittedData['uid'] = (int)($this->submittedData['uid'] ?? 0);
249
        // If a save command was submitted, handle saving now
250
        if (in_array((string)$this->getCurrentAction(), [Action::SAVE, Action::SAVE_CLOSE, Action::SAVE_NEW], true)) {
251
            // First check the submitted data
252
            $result = $this->preprocessData();
253
254
            // If result is ok, proceed with saving
255
            if ($result) {
256
                $this->saveTask();
257
258
                if ($this->action->equals(Action::SAVE_CLOSE)) {
259
                    // Display default screen
260
                    $this->setCurrentAction(Action::cast(Action::LIST));
261
                } elseif ($this->action->equals(Action::SAVE)) {
262
                    // After saving a "add form", return to edit
263
                    $this->setCurrentAction(Action::cast(Action::EDIT));
264
                } elseif ($this->action->equals(Action::SAVE_NEW)) {
265
                    // Unset submitted data, so that empty form gets displayed
266
                    unset($this->submittedData);
267
                    // After saving a "add/edit form", return to add
268
                    $this->setCurrentAction(Action::cast(Action::ADD));
269
                } else {
270
                    // Return to edit form
271
                    $this->setCurrentAction($previousAction);
272
                }
273
            } else {
274
                $this->setCurrentAction($previousAction);
275
            }
276
        }
277
278
        // Handle chosen action
279
        switch ((string)$this->MOD_SETTINGS['function']) {
280
            case 'scheduler':
281
                $this->executeTasks();
282
283
                switch ((string)$this->getCurrentAction()) {
284
                    case Action::ADD:
285
                    case Action::EDIT:
286
                        try {
287
                            // Try adding or editing
288
                            $content .= $this->editTaskAction($requestUri);
289
                        } catch (\LogicException|\UnexpectedValueException|\OutOfBoundsException $e) {
290
                            // Catching all types of exceptions that were previously handled and
291
                            // converted to messages
292
                            $content .= $this->listTasksAction();
293
                        } catch (\Exception $e) {
294
                            // Catching all "unexpected" exceptions not previously handled
295
                            $this->addMessage($e->getMessage(), FlashMessage::ERROR);
296
                            $content .= $this->listTasksAction();
297
                        }
298
                        break;
299
                    case Action::DELETE:
300
                        $this->deleteTask();
301
                        $content .= $this->listTasksAction();
302
                        break;
303
                    case Action::STOP:
304
                        $this->stopTask();
305
                        $content .= $this->listTasksAction();
306
                        break;
307
                    case Action::TOGGLE_HIDDEN:
308
                        $this->toggleDisableAction();
309
                        $content .= $this->listTasksAction();
310
                        break;
311
                    case Action::SET_NEXT_EXECUTION_TIME:
312
                        $this->setNextExecutionTimeAction();
313
                        $content .= $this->listTasksAction();
314
                        break;
315
                    case Action::LIST:
316
                        $content .= $this->listTasksAction();
317
                }
318
                break;
319
320
            // Setup check screen
321
            case 'check':
322
                // @todo move check to the report module
323
                $content .= $this->checkScreenAction();
324
                break;
325
326
            // Information screen
327
            case 'info':
328
                $content .= $this->infoScreenAction();
329
                break;
330
        }
331
332
        return $content;
333
    }
334
335
    /**
336
     * This method displays the result of a number of checks
337
     * on whether the Scheduler is ready to run or running properly
338
     *
339
     * @return string Further information
340
     */
341
    protected function checkScreenAction(): string
342
    {
343
        $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'CheckScreen.html');
344
345
        // Display information about last automated run, as stored in the system registry
346
        $registry = GeneralUtility::makeInstance(Registry::class);
347
        $lastRun = $registry->get('tx_scheduler', 'lastRun');
348
        if (!is_array($lastRun)) {
349
            $message = $this->getLanguageService()->getLL('msg.noLastRun');
350
            $severity = InfoboxViewHelper::STATE_WARNING;
351
        } else {
352
            if (empty($lastRun['end']) || empty($lastRun['start']) || empty($lastRun['type'])) {
353
                $message = $this->getLanguageService()->getLL('msg.incompleteLastRun');
354
                $severity = InfoboxViewHelper::STATE_WARNING;
355
            } else {
356
                $startDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['start']);
357
                $startTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['start']);
358
                $endDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['end']);
359
                $endTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['end']);
360
                $label = 'automatically';
361
                if ($lastRun['type'] === 'manual') {
362
                    $label = 'manually';
363
                }
364
                $type = $this->getLanguageService()->getLL('label.' . $label);
365
                $message = sprintf($this->getLanguageService()->getLL('msg.lastRun'), $type, $startDate, $startTime, $endDate, $endTime);
366
                $severity = InfoboxViewHelper::STATE_INFO;
367
            }
368
        }
369
        $this->view->assign('lastRunMessage', $message);
370
        $this->view->assign('lastRunSeverity', $severity);
371
372
        if (Environment::isComposerMode()) {
373
            $this->view->assign('composerMode', true);
374
        } else {
375
            // Check if CLI script is executable or not
376
            $script = GeneralUtility::getFileAbsFileName('EXT:core/bin/typo3');
377
            $this->view->assign('script', $script);
378
            // Skip this check if running Windows, as rights do not work the same way on this platform
379
            // (i.e. the script will always appear as *not* executable)
380
            if (Environment::isWindows()) {
381
                $isExecutable = true;
382
            } else {
383
                $isExecutable = is_executable($script);
384
            }
385
            if ($isExecutable) {
386
                $message = $this->getLanguageService()->getLL('msg.cliScriptExecutable');
387
                $severity = InfoboxViewHelper::STATE_OK;
388
            } else {
389
                $message = $this->getLanguageService()->getLL('msg.cliScriptNotExecutable');
390
                $severity = InfoboxViewHelper::STATE_ERROR;
391
            }
392
            $this->view->assign('isExecutableMessage', $message);
393
            $this->view->assign('isExecutableSeverity', $severity);
394
        }
395
396
        $this->view->assign('now', $this->getServerTime());
397
398
        return $this->view->render();
399
    }
400
401
    /**
402
     * This method gathers information about all available task classes and displays it
403
     *
404
     * @return string html
405
     */
406
    protected function infoScreenAction(): string
407
    {
408
        $registeredClasses = $this->getRegisteredClasses();
409
        // No classes available, display information message
410
        if (empty($registeredClasses)) {
411
            $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreenNoClasses.html');
412
            return $this->view->render();
413
        }
414
415
        $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreen.html');
416
        $this->view->assign('registeredClasses', $registeredClasses);
417
418
        return $this->view->render();
419
    }
420
421
    /**
422
     * Delete a task from the execution queue
423
     */
424
    protected function deleteTask(): void
425
    {
426
        try {
427
            // Try to fetch the task and delete it
428
            $task = $this->scheduler->fetchTask($this->submittedData['uid']);
429
            // If the task is currently running, it may not be deleted
430
            if ($task->isExecutionRunning()) {
431
                $this->addMessage($this->getLanguageService()->getLL('msg.maynotDeleteRunningTask'), FlashMessage::ERROR);
432
            } else {
433
                if ($this->scheduler->removeTask($task)) {
434
                    $this->getBackendUser()->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was deleted', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
435
                    $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
436
                } else {
437
                    $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
438
                }
439
            }
440
        } catch (\UnexpectedValueException $e) {
441
            // The task could not be unserialized properly, simply update the database record
442
            $taskUid = (int)$this->submittedData['uid'];
443
            $result = GeneralUtility::makeInstance(ConnectionPool::class)
444
                ->getConnectionForTable('tx_scheduler_task')
445
                ->update('tx_scheduler_task', ['deleted' => 1], ['uid' => $taskUid]);
446
            if ($result) {
447
                $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
448
            } else {
449
                $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
450
            }
451
        } catch (\OutOfBoundsException $e) {
452
            // The task was not found, for some reason
453
            $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
454
        }
455
    }
456
457
    /**
458
     * Clears the registered running executions from the task
459
     * Note that this doesn't actually stop the running script. It just unmarks
460
     * all executions.
461
     * @todo find a way to really kill the running task
462
     */
463
    protected function stopTask(): void
464
    {
465
        try {
466
            // Try to fetch the task and stop it
467
            $task = $this->scheduler->fetchTask($this->submittedData['uid']);
468
            if ($task->isExecutionRunning()) {
469
                // If the task is indeed currently running, clear marked executions
470
                $result = $task->unmarkAllExecutions();
471
                if ($result) {
472
                    $this->addMessage($this->getLanguageService()->getLL('msg.stopSuccess'));
473
                } else {
474
                    $this->addMessage($this->getLanguageService()->getLL('msg.stopError'), FlashMessage::ERROR);
475
                }
476
            } else {
477
                // The task is not running, nothing to unmark
478
                $this->addMessage($this->getLanguageService()->getLL('msg.maynotStopNonRunningTask'), FlashMessage::WARNING);
479
            }
480
        } catch (\Exception $e) {
481
            // The task was not found, for some reason
482
            $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
483
        }
484
    }
485
486
    /**
487
     * Toggles the disabled state of the submitted task
488
     */
489
    protected function toggleDisableAction(): void
490
    {
491
        $task = $this->scheduler->fetchTask($this->submittedData['uid']);
492
        $task->setDisabled(!$task->isDisabled());
493
        // If a disabled single task is enabled again, we register it for a
494
        // single execution at next scheduler run.
495
        if ($task->getType() === AbstractTask::TYPE_SINGLE) {
496
            $task->registerSingleExecution(time());
497
        }
498
        $task->save();
499
    }
500
501
    /**
502
     * Sets the next execution time of the submitted task to now
503
     */
504
    protected function setNextExecutionTimeAction(): void
505
    {
506
        $task = $this->scheduler->fetchTask($this->submittedData['uid']);
507
        $task->setRunOnNextCronJob(true);
508
        $task->save();
509
    }
510
511
    /**
512
     * Return a form to add a new task or edit an existing one
513
     *
514
     * @param string $requestUri
515
     * @return string HTML form to add or edit a task
516
     */
517
    protected function editTaskAction(string $requestUri): string
518
    {
519
        $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'EditTask.html');
520
521
        $registeredClasses = $this->getRegisteredClasses();
522
        $registeredTaskGroups = $this->getRegisteredTaskGroups();
523
524
        $taskInfo = [];
525
        $task = null;
526
        $process = 'edit';
527
528
        if ($this->submittedData['uid'] > 0) {
529
            // If editing, retrieve data for existing task
530
            try {
531
                $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
532
                // If there's a registered execution, the task should not be edited
533
                if (!empty($taskRecord['serialized_executions'])) {
534
                    $this->addMessage($this->getLanguageService()->getLL('msg.maynotEditRunningTask'), FlashMessage::ERROR);
535
                    throw new \LogicException('Running tasks cannot not be edited', 1251232849);
536
                }
537
538
                // Get the task object
539
                /** @var \TYPO3\CMS\Scheduler\Task\AbstractTask $task */
540
                $task = unserialize($taskRecord['serialized_task_object']);
541
542
                // Set some task information
543
                $taskInfo['disable'] = $taskRecord['disable'];
544
                $taskInfo['description'] = $taskRecord['description'];
545
                $taskInfo['task_group'] = $taskRecord['task_group'];
546
547
                // Check that the task object is valid
548
                if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
549
                    // The task object is valid, process with fetching current data
550
                    $taskInfo['class'] = get_class($task);
551
                    // Get execution information
552
                    $taskInfo['start'] = (int)$task->getExecution()->getStart();
553
                    $taskInfo['end'] = (int)$task->getExecution()->getEnd();
554
                    $taskInfo['interval'] = $task->getExecution()->getInterval();
555
                    $taskInfo['croncmd'] = $task->getExecution()->getCronCmd();
556
                    $taskInfo['multiple'] = $task->getExecution()->getMultiple();
557
                    if (!empty($taskInfo['interval']) || !empty($taskInfo['croncmd'])) {
558
                        // Guess task type from the existing information
559
                        // If an interval or a cron command is defined, it's a recurring task
560
                        $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
561
                        $taskInfo['frequency'] = $taskInfo['interval'] ?: $taskInfo['croncmd'];
562
                    } else {
563
                        // It's not a recurring task
564
                        // Make sure interval and cron command are both empty
565
                        $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
566
                        $taskInfo['frequency'] = '';
567
                        $taskInfo['end'] = 0;
568
                    }
569
                } else {
570
                    // The task object is not valid
571
                    // Issue error message
572
                    $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.invalidTaskClassEdit'), get_class($task)), FlashMessage::ERROR);
573
                    // Initialize empty values
574
                    $taskInfo['start'] = 0;
575
                    $taskInfo['end'] = 0;
576
                    $taskInfo['frequency'] = '';
577
                    $taskInfo['multiple'] = false;
578
                    $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
579
                }
580
            } catch (\OutOfBoundsException $e) {
581
                // Add a message and continue throwing the exception
582
                $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
583
                throw $e;
584
            }
585
        } else {
586
            // If adding a new object, set some default values
587
            $taskInfo['class'] = key($registeredClasses);
588
            $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
589
            $taskInfo['start'] = $GLOBALS['EXEC_TIME'];
590
            $taskInfo['end'] = '';
591
            $taskInfo['frequency'] = '';
592
            $taskInfo['multiple'] = 0;
593
            $process = 'add';
594
        }
595
596
        // If some data was already submitted, use it to override
597
        // existing data
598
        if (!empty($this->submittedData)) {
599
            ArrayUtility::mergeRecursiveWithOverrule($taskInfo, $this->submittedData);
600
        }
601
602
        // Get the extra fields to display for each task that needs some
603
        $allAdditionalFields = [];
604
        if ($process === 'add') {
605
            foreach ($registeredClasses as $class => $registrationInfo) {
606
                if (!empty($registrationInfo['provider'])) {
607
                    /** @var AdditionalFieldProviderInterface $providerObject */
608
                    $providerObject = GeneralUtility::makeInstance($registrationInfo['provider']);
609
                    if ($providerObject instanceof AdditionalFieldProviderInterface) {
610
                        $additionalFields = $providerObject->getAdditionalFields($taskInfo, null, $this);
611
                        $allAdditionalFields = array_merge($allAdditionalFields, [$class => $additionalFields]);
612
                    }
613
                }
614
            }
615
        } elseif ($task !== null && !empty($registeredClasses[$taskInfo['class']]['provider'])) {
616
            // only try to fetch additionalFields if the task is valid
617
            $providerObject = GeneralUtility::makeInstance($registeredClasses[$taskInfo['class']]['provider']);
618
            if ($providerObject instanceof AdditionalFieldProviderInterface) {
619
                $allAdditionalFields[$taskInfo['class']] = $providerObject->getAdditionalFields($taskInfo, $task, $this);
620
            }
621
        }
622
623
        // Load necessary JavaScript
624
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
625
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker');
626
627
        // Start rendering the add/edit form
628
        $this->view->assign('uid', htmlspecialchars((string)$this->submittedData['uid']));
629
        $this->view->assign('cmd', htmlspecialchars((string)$this->getCurrentAction()));
630
        $this->view->assign('csh', $this->cshKey);
631
        $this->view->assign('lang', 'LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:');
632
633
        $table = [];
634
635
        // Disable checkbox
636
        $this->view->assign('task_disable', (($taskInfo['disable'] ?? false) ? ' checked="checked"' : ''));
637
        $this->view->assign('task_disable_label', 'LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:disable');
638
639
        // Task class selector
640
        // On editing, don't allow changing of the task class, unless it was not valid
641
        if ($this->submittedData['uid'] > 0 && !empty($taskInfo['class'])) {
642
            $this->view->assign('task_class', $taskInfo['class']);
643
            $this->view->assign('task_class_title', $registeredClasses[$taskInfo['class']]['title']);
644
            $this->view->assign('task_class_extension', $registeredClasses[$taskInfo['class']]['extension']);
645
        } else {
646
            // Group registered classes by classname
647
            $groupedClasses = [];
648
            foreach ($registeredClasses as $class => $classInfo) {
649
                $groupedClasses[$classInfo['extension']][$class] = $classInfo;
650
            }
651
            ksort($groupedClasses);
652
            foreach ($groupedClasses as $extension => $class) {
653
                foreach ($groupedClasses[$extension] as $class => $classInfo) {
0 ignored issues
show
Comprehensibility Bug introduced by
$class is overwriting a variable from outer foreach loop.
Loading history...
654
                    $selected = $class == $taskInfo['class'] ? ' selected="selected"' : '';
655
                    $groupedClasses[$extension][$class]['selected'] = $selected;
656
                }
657
            }
658
            $this->view->assign('groupedClasses', $groupedClasses);
659
        }
660
661
        // Task type selector
662
        $this->view->assign('task_type_selected_1', ((int)$taskInfo['type'] === AbstractTask::TYPE_SINGLE ? ' selected="selected"' : ''));
663
        $this->view->assign('task_type_selected_2', ((int)$taskInfo['type'] === AbstractTask::TYPE_RECURRING ? ' selected="selected"' : ''));
664
665
        // Task group selector
666
        foreach ($registeredTaskGroups as $key => $taskGroup) {
667
            $selected = $taskGroup['uid'] == $taskInfo['task_group'] ? ' selected="selected"' : '';
668
            $registeredTaskGroups[$key]['selected'] = $selected;
669
        }
670
        $this->view->assign('registeredTaskGroups', $registeredTaskGroups);
671
672
        // Start date/time field
673
        $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '%H:%M %m-%d-%Y' : '%H:%M %d-%m-%Y';
674
        $this->view->assign('start_value_hr', ($taskInfo['start'] > 0 ? strftime($dateFormat, $taskInfo['start']) : ''));
675
        $this->view->assign('start_value', $taskInfo['start']);
676
677
        // End date/time field
678
        // NOTE: datetime fields need a special id naming scheme
679
        $this->view->assign('end_value_hr', ($taskInfo['end'] > 0 ? strftime($dateFormat, $taskInfo['end']) : ''));
680
        $this->view->assign('end_value', $taskInfo['end']);
681
682
        // Frequency input field
683
        $this->view->assign('frequency', $taskInfo['frequency']);
684
685
        // Multiple execution selector
686
        $this->view->assign('multiple', ($taskInfo['multiple'] ? 'checked="checked"' : ''));
687
688
        // Description
689
        $this->view->assign('description', $taskInfo['description'] ?? '');
690
691
        // Display additional fields
692
        $additionalFieldList = [];
693
        foreach ($allAdditionalFields as $class => $fields) {
694
            if ($class == $taskInfo['class']) {
695
                $additionalFieldsStyle = '';
696
            } else {
697
                $additionalFieldsStyle = ' style="display: none"';
698
            }
699
            // Add each field to the display, if there are indeed any
700
            if (is_array($fields)) {
701
                foreach ($fields as $fieldID => $fieldInfo) {
702
                    $htmlClassName = strtolower(str_replace('\\', '-', (string)$class));
703
                    $field = [];
704
                    $field['htmlClassName'] = $htmlClassName;
705
                    $field['code'] = $fieldInfo['code'];
706
                    $field['cshKey'] = $fieldInfo['cshKey'] ?? '';
707
                    $field['cshLabel'] = $fieldInfo['cshLabel'] ?? '';
708
                    $field['langLabel'] = $fieldInfo['label'] ?? '';
709
                    $field['fieldID'] = $fieldID;
710
                    $field['additionalFieldsStyle'] = $additionalFieldsStyle;
711
                    $field['browseButton'] = $this->getBrowseButton($fieldID, $fieldInfo);
712
                    $additionalFieldList[] = $field;
713
                }
714
            }
715
        }
716
        $this->view->assign('additionalFields', $additionalFieldList);
717
718
        $this->view->assign('returnUrl', $requestUri);
719
        $this->view->assign('table', implode(LF, $table));
720
        $this->view->assign('now', $this->getServerTime());
721
        $this->view->assign('frequencyOptions', (array)$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['frequencyOptions']);
722
723
        return $this->view->render();
724
    }
725
726
    /**
727
     * @param string $fieldID The id of the field witch contains the page id
728
     * @param array $fieldInfo The array with the field info, contains the page title shown beside the button
729
     * @return string HTML code for the browse button
730
     */
731
    protected function getBrowseButton($fieldID, array $fieldInfo): string
732
    {
733
        if (isset($fieldInfo['browser']) && ($fieldInfo['browser'] === 'page')) {
734
            $url = (string)$this->uriBuilder->buildUriFromRoute('wizard_element_browser');
735
736
            $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.browse_db'));
737
            return '
738
                <div><a href="' . htmlspecialchars($url) . '" data-trigger-for="' . htmlspecialchars($fieldID) . '" data-mode="db" data-params="" class="btn btn-default t3js-element-browser" title="' . $title . '">
739
                    <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-insert-record" data-identifier="actions-insert-record">
740
                        <span class="icon-markup">' . $this->iconFactory->getIcon(
741
                'actions-insert-record',
742
                Icon::SIZE_SMALL
743
            )->render() . '</span>
744
                    </span>
745
                </a><span id="page_' . $fieldID . '">&nbsp;' . htmlspecialchars($fieldInfo['pageTitle']) . '</span></div>';
746
        }
747
        return '';
748
    }
749
750
    /**
751
     * Execute all selected tasks
752
     */
753
    protected function executeTasks(): void
754
    {
755
        // Continue if some elements have been chosen for execution
756
        if (isset($this->submittedData['execute']) && !empty($this->submittedData['execute'])) {
757
            // Get list of registered classes
758
            $registeredClasses = $this->getRegisteredClasses();
759
            // Loop on all selected tasks
760
            foreach ($this->submittedData['execute'] as $uid) {
761
                try {
762
                    // Try fetching the task
763
                    $task = $this->scheduler->fetchTask($uid);
764
                    $class = get_class($task);
765
                    $name = $registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')';
766
                    if (GeneralUtility::_POST('go_cron') !== null) {
767
                        $task->setRunOnNextCronJob(true);
768
                        $task->save();
769
                    } else {
770
                        // Now try to execute it and report on outcome
771
                        try {
772
                            $result = $this->scheduler->executeTask($task);
773
                            if ($result) {
774
                                $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executed'), $name));
775
                            } else {
776
                                $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.notExecuted'), $name), FlashMessage::ERROR);
777
                            }
778
                        } catch (\Exception $e) {
779
                            // An exception was thrown, display its message as an error
780
                            $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $name, $e->getMessage()), FlashMessage::ERROR);
781
                        }
782
                    }
783
                } catch (\OutOfBoundsException $e) {
784
                    $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $uid), FlashMessage::ERROR);
785
                } catch (\UnexpectedValueException $e) {
786
                    $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $uid, $e->getMessage()), FlashMessage::ERROR);
787
                }
788
            }
789
            // Record the run in the system registry
790
            $this->scheduler->recordLastRun('manual');
791
            // Make sure to switch to list view after execution
792
            $this->setCurrentAction(Action::cast(Action::LIST));
793
        }
794
    }
795
796
    /**
797
     * Assemble display of list of scheduled tasks
798
     *
799
     * @return string Table of pending tasks
800
     */
801
    protected function listTasksAction(): string
802
    {
803
        $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasks.html');
804
805
        // Define display format for dates
806
        $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
807
808
        // Get list of registered task groups
809
        $registeredTaskGroups = $this->getRegisteredTaskGroups();
810
811
        // add an empty entry for non-grouped tasks
812
        // add in front of list
813
        array_unshift($registeredTaskGroups, ['uid' => 0, 'groupName' => '']);
814
815
        // Get all registered tasks
816
        // Just to get the number of entries
817
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
818
            ->getQueryBuilderForTable('tx_scheduler_task');
819
        $queryBuilder->getRestrictions()->removeAll();
820
821
        $result = $queryBuilder->select('t.*')
822
            ->addSelect(
823
                'g.groupName AS taskGroupName',
824
                'g.description AS taskGroupDescription',
825
                'g.deleted AS isTaskGroupDeleted'
826
            )
827
            ->from('tx_scheduler_task', 't')
828
            ->leftJoin(
829
                't',
830
                'tx_scheduler_task_group',
831
                'g',
832
                $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
833
            )
834
            ->where(
835
                $queryBuilder->expr()->eq('t.deleted', 0)
836
            )
837
            ->orderBy('g.sorting')
838
            ->execute();
839
840
        // Loop on all tasks
841
        $temporaryResult = [];
842
        while ($row = $result->fetch()) {
843
            if ($row['taskGroupName'] === null || $row['isTaskGroupDeleted'] === '1') {
844
                $row['taskGroupName'] = '';
845
                $row['taskGroupDescription'] = '';
846
                $row['task_group'] = 0;
847
            }
848
            $temporaryResult[$row['task_group']]['groupName'] = $row['taskGroupName'];
849
            $temporaryResult[$row['task_group']]['groupDescription'] = $row['taskGroupDescription'];
850
            $temporaryResult[$row['task_group']]['tasks'][] = $row;
851
        }
852
853
        // No tasks defined, display information message
854
        if (empty($temporaryResult)) {
855
            $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasksNoTasks.html');
856
            return $this->view->render();
857
        }
858
859
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
860
        $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
861
862
        $tasks = $temporaryResult;
863
864
        $registeredClasses = $this->getRegisteredClasses();
865
        $missingClasses = [];
866
        foreach ($temporaryResult as $taskIndex => $taskGroup) {
867
            foreach ($taskGroup['tasks'] as $recordIndex => $schedulerRecord) {
868
                if ((int)$schedulerRecord['disable'] === 1) {
869
                    $translationKey = 'enable';
870
                } else {
871
                    $translationKey = 'disable';
872
                }
873
                $tasks[$taskIndex]['tasks'][$recordIndex]['translationKey'] = $translationKey;
874
875
                // Define some default values
876
                $lastExecution = '-';
877
                $isRunning = false;
878
                $showAsDisabled = false;
879
                // Restore the serialized task and pass it a reference to the scheduler object
880
                /** @var \TYPO3\CMS\Scheduler\Task\AbstractTask|ProgressProviderInterface $task */
881
                $task = unserialize($schedulerRecord['serialized_task_object']);
882
                $class = get_class($task);
883
                if ($class === \__PHP_Incomplete_Class::class && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) {
884
                    $class = $matches['classname'];
885
                }
886
                $tasks[$taskIndex]['tasks'][$recordIndex]['class'] = $class;
887
                // Assemble information about last execution
888
                if (!empty($schedulerRecord['lastexecution_time'])) {
889
                    $lastExecution = date($dateFormat, (int)$schedulerRecord['lastexecution_time']);
890
                    if ($schedulerRecord['lastexecution_context'] === 'CLI') {
891
                        $context = $this->getLanguageService()->getLL('label.cron');
892
                    } else {
893
                        $context = $this->getLanguageService()->getLL('label.manual');
894
                    }
895
                    $lastExecution .= ' (' . $context . ')';
896
                }
897
                $tasks[$taskIndex]['tasks'][$recordIndex]['lastExecution'] = $lastExecution;
898
899
                if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
900
                    $tasks[$taskIndex]['tasks'][$recordIndex]['validClass'] = true;
901
                    // The task object is valid
902
                    $labels = [];
903
                    $additionalInformation = $task->getAdditionalInformation();
0 ignored issues
show
Bug introduced by
The method getAdditionalInformation() does not exist on TYPO3\CMS\Scheduler\ProgressProviderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

903
                    /** @scrutinizer ignore-call */ 
904
                    $additionalInformation = $task->getAdditionalInformation();

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
904
                    if ($task instanceof ProgressProviderInterface) {
905
                        $progress = round((float)$task->getProgress(), 2);
906
                        $tasks[$taskIndex]['tasks'][$recordIndex]['progress'] = $progress;
907
                    }
908
                    $tasks[$taskIndex]['tasks'][$recordIndex]['classTitle'] = $registeredClasses[$class]['title'];
909
                    $tasks[$taskIndex]['tasks'][$recordIndex]['classExtension'] = $registeredClasses[$class]['extension'];
910
                    $tasks[$taskIndex]['tasks'][$recordIndex]['additionalInformation'] = $additionalInformation;
911
                    // Check if task currently has a running execution
912
                    if (!empty($schedulerRecord['serialized_executions'])) {
913
                        $labels[] = [
914
                            'class' => 'success',
915
                            'text' => $this->getLanguageService()->getLL('status.running')
916
                        ];
917
                        $isRunning = true;
918
                    }
919
                    $tasks[$taskIndex]['tasks'][$recordIndex]['isRunning'] = $isRunning;
920
921
                    // Prepare display of next execution date
922
                    // If task is currently running, date is not displayed (as next hasn't been calculated yet)
923
                    // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
924
                    if ($isRunning || $schedulerRecord['disable']) {
925
                        $nextDate = '-';
926
                    } else {
927
                        $nextDate = date($dateFormat, (int)$schedulerRecord['nextexecution']);
928
                        if (empty($schedulerRecord['nextexecution'])) {
929
                            $nextDate = $this->getLanguageService()->getLL('none');
930
                        } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
931
                            $labels[] = [
932
                                'class' => 'warning',
933
                                'text' => $this->getLanguageService()->getLL('status.late'),
934
                                'description' => $this->getLanguageService()->getLL('status.legend.scheduled')
935
                            ];
936
                        }
937
                    }
938
                    $tasks[$taskIndex]['tasks'][$recordIndex]['nextDate'] = $nextDate;
939
                    // Get execution type
940
                    if ($task->getType() === AbstractTask::TYPE_SINGLE) {
0 ignored issues
show
Bug introduced by
The method getType() does not exist on TYPO3\CMS\Scheduler\ProgressProviderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

940
                    if ($task->/** @scrutinizer ignore-call */ getType() === AbstractTask::TYPE_SINGLE) {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
941
                        $execType = $this->getLanguageService()->getLL('label.type.single');
942
                        $frequency = '-';
943
                    } else {
944
                        $execType = $this->getLanguageService()->getLL('label.type.recurring');
945
                        if ($task->getExecution()->getCronCmd() == '') {
0 ignored issues
show
Bug introduced by
The method getExecution() does not exist on TYPO3\CMS\Scheduler\ProgressProviderInterface. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

945
                        if ($task->/** @scrutinizer ignore-call */ getExecution()->getCronCmd() == '') {

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
946
                            $frequency = $task->getExecution()->getInterval();
947
                        } else {
948
                            $frequency = $task->getExecution()->getCronCmd();
949
                        }
950
                    }
951
                    // Check the disable status
952
                    // Row is shown dimmed if task is disabled, unless it is still running
953
                    if ($schedulerRecord['disable'] && !$isRunning) {
954
                        $labels[] = [
955
                            'class' => 'default',
956
                            'text' => $this->getLanguageService()->getLL('status.disabled')
957
                        ];
958
                        $showAsDisabled = true;
959
                    }
960
                    $tasks[$taskIndex]['tasks'][$recordIndex]['execType'] = $execType;
961
                    $tasks[$taskIndex]['tasks'][$recordIndex]['frequency'] = $frequency;
962
                    // Get multiple executions setting
963
                    if ($task->getExecution()->getMultiple()) {
964
                        $multiple = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes');
965
                    } else {
966
                        $multiple = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no');
967
                    }
968
                    $tasks[$taskIndex]['tasks'][$recordIndex]['multiple'] = $multiple;
969
970
                    // Check if the last run failed
971
                    if (!empty($schedulerRecord['lastexecution_failure'])) {
972
                        // Try to get the stored exception array
973
                        /** @var array $exceptionArray */
974
                        $exceptionArray = @unserialize($schedulerRecord['lastexecution_failure']);
975
                        // If the exception could not be unserialized, issue a default error message
976
                        if (!is_array($exceptionArray) || empty($exceptionArray)) {
977
                            $labelDescription = $this->getLanguageService()->getLL('msg.executionFailureDefault');
978
                        } else {
979
                            $labelDescription = sprintf($this->getLanguageService()->getLL('msg.executionFailureReport'), $exceptionArray['code'], $exceptionArray['message']);
980
                        }
981
                        $labels[] = [
982
                            'class' => 'danger',
983
                            'text' => $this->getLanguageService()->getLL('status.failure'),
984
                            'description' => $labelDescription
985
                        ];
986
                    }
987
                    $tasks[$taskIndex]['tasks'][$recordIndex]['labels'] = $labels;
988
                    if ($showAsDisabled) {
989
                        $tasks[$taskIndex]['tasks'][$recordIndex]['showAsDisabled'] = 'disabled';
990
                    }
991
                } else {
992
                    $missingClasses[] = $tasks[$taskIndex]['tasks'][$recordIndex];
993
                    unset($tasks[$taskIndex]['tasks'][$recordIndex]);
994
                }
995
            }
996
        }
997
998
        $this->view->assign('tasks', $tasks);
999
        $this->view->assign('missingClasses', $missingClasses);
1000
        $this->view->assign('moduleUri', $this->moduleUri);
1001
        $this->view->assign('now', $this->getServerTime());
1002
1003
        return $this->view->render();
1004
    }
1005
1006
    /**
1007
     * Generates bootstrap labels containing the label statuses
1008
     *
1009
     * @param array $labels
1010
     * @return string
1011
     */
1012
    protected function makeStatusLabel(array $labels): string
1013
    {
1014
        $htmlLabels = [];
1015
        foreach ($labels as $label) {
1016
            if (empty($label['text'])) {
1017
                continue;
1018
            }
1019
            $htmlLabels[] = '<span class="label label-' . htmlspecialchars($label['class']) . ' pull-right" title="' . htmlspecialchars($label['description']) . '">' . htmlspecialchars($label['text']) . '</span>';
1020
        }
1021
1022
        return implode('&nbsp;', $htmlLabels);
1023
    }
1024
1025
    /**
1026
     * Saves a task specified in the backend form to the database
1027
     */
1028
    protected function saveTask(): void
1029
    {
1030
        // If a task is being edited fetch old task data
1031
        if (!empty($this->submittedData['uid'])) {
1032
            try {
1033
                $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
1034
                /** @var \TYPO3\CMS\Scheduler\Task\AbstractTask $task */
1035
                $task = unserialize($taskRecord['serialized_task_object']);
1036
            } catch (\OutOfBoundsException $e) {
1037
                // If the task could not be fetched, issue an error message
1038
                // and exit early
1039
                $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
1040
                return;
1041
            }
1042
            // Register single execution
1043
            if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1044
                $task->registerSingleExecution($this->submittedData['start']);
1045
            } else {
1046
                if (!empty($this->submittedData['croncmd'])) {
1047
                    // Definition by cron-like syntax
1048
                    $interval = 0;
1049
                    $cronCmd = $this->submittedData['croncmd'];
1050
                } else {
1051
                    // Definition by interval
1052
                    $interval = $this->submittedData['interval'];
1053
                    $cronCmd = '';
1054
                }
1055
                // Register recurring execution
1056
                $task->registerRecurringExecution($this->submittedData['start'], $interval, $this->submittedData['end'], $this->submittedData['multiple'], $cronCmd);
1057
            }
1058
            // Set disable flag
1059
            $task->setDisabled($this->submittedData['disable']);
1060
            // Set description
1061
            $task->setDescription($this->submittedData['description']);
1062
            // Set task group
1063
            $task->setTaskGroup($this->submittedData['task_group']);
1064
            // Save additional input values
1065
            if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1066
                /** @var AdditionalFieldProviderInterface $providerObject */
1067
                $providerObject = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1068
                if ($providerObject instanceof AdditionalFieldProviderInterface) {
0 ignored issues
show
introduced by
$providerObject is always a sub-type of TYPO3\CMS\Scheduler\Addi...lFieldProviderInterface.
Loading history...
1069
                    $providerObject->saveAdditionalFields($this->submittedData, $task);
1070
                }
1071
            }
1072
            // Save to database
1073
            $result = $this->scheduler->saveTask($task);
1074
            if ($result) {
1075
                $this->getBackendUser()->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was updated', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1076
                $this->addMessage($this->getLanguageService()->getLL('msg.updateSuccess'));
1077
            } else {
1078
                $this->addMessage($this->getLanguageService()->getLL('msg.updateError'), FlashMessage::ERROR);
1079
            }
1080
        } else {
1081
            // A new task is being created
1082
            // Create an instance of chosen class
1083
            /** @var AbstractTask $task */
1084
            $task = GeneralUtility::makeInstance($this->submittedData['class']);
1085
            if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1086
                // Set up single execution
1087
                $task->registerSingleExecution($this->submittedData['start']);
1088
            } else {
1089
                // Set up recurring execution
1090
                $task->registerRecurringExecution($this->submittedData['start'], $this->submittedData['interval'], $this->submittedData['end'], $this->submittedData['multiple'], $this->submittedData['croncmd']);
1091
            }
1092
            // Save additional input values
1093
            if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1094
                /** @var AdditionalFieldProviderInterface $providerObject */
1095
                $providerObject = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1096
                if ($providerObject instanceof AdditionalFieldProviderInterface) {
0 ignored issues
show
introduced by
$providerObject is always a sub-type of TYPO3\CMS\Scheduler\Addi...lFieldProviderInterface.
Loading history...
1097
                    $providerObject->saveAdditionalFields($this->submittedData, $task);
1098
                }
1099
            }
1100
            // Set disable flag
1101
            $task->setDisabled($this->submittedData['disable']);
1102
            // Set description
1103
            $task->setDescription($this->submittedData['description']);
1104
            // Set description
1105
            $task->setTaskGroup($this->submittedData['task_group']);
1106
            // Add to database
1107
            $result = $this->scheduler->addTask($task);
1108
            if ($result) {
1109
                $this->getBackendUser()->writelog(SystemLogType::EXTENSION, SystemLogGenericAction::UNDEFINED, SystemLogErrorClassification::MESSAGE, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was added', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1110
                $this->addMessage($this->getLanguageService()->getLL('msg.addSuccess'));
1111
1112
                // set the uid of the just created task so that we
1113
                // can continue editing after initial saving
1114
                $this->submittedData['uid'] = $task->getTaskUid();
1115
            } else {
1116
                $this->addMessage($this->getLanguageService()->getLL('msg.addError'), FlashMessage::ERROR);
1117
            }
1118
        }
1119
    }
1120
1121
    /*************************
1122
     *
1123
     * INPUT PROCESSING UTILITIES
1124
     *
1125
     *************************/
1126
    /**
1127
     * Checks the submitted data and performs some pre-processing on it
1128
     *
1129
     * @return bool true if everything was ok, false otherwise
1130
     */
1131
    protected function preprocessData()
1132
    {
1133
        $cronErrorCode = 0;
1134
        $result = true;
1135
        // Validate id
1136
        $this->submittedData['uid'] = empty($this->submittedData['uid']) ? 0 : (int)$this->submittedData['uid'];
1137
        // Validate selected task class
1138
        if (!class_exists($this->submittedData['class'])) {
1139
            $this->addMessage($this->getLanguageService()->getLL('msg.noTaskClassFound'), FlashMessage::ERROR);
1140
        }
1141
        // Check start date
1142
        if (empty($this->submittedData['start'])) {
1143
            $this->addMessage($this->getLanguageService()->getLL('msg.noStartDate'), FlashMessage::ERROR);
1144
            $result = false;
1145
        } elseif (is_string($this->submittedData['start']) && (!is_numeric($this->submittedData['start']))) {
1146
            try {
1147
                $this->submittedData['start'] = $this->convertToTimestamp($this->submittedData['start']);
1148
            } catch (\Exception $e) {
1149
                $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR);
1150
                $result = false;
1151
            }
1152
        } else {
1153
            $this->submittedData['start'] = (int)$this->submittedData['start'];
1154
        }
1155
        // Check end date, if recurring task
1156
        if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING && !empty($this->submittedData['end'])) {
1157
            if (is_string($this->submittedData['end']) && (!is_numeric($this->submittedData['end']))) {
1158
                try {
1159
                    $this->submittedData['end'] = $this->convertToTimestamp($this->submittedData['end']);
1160
                } catch (\Exception $e) {
1161
                    $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR);
1162
                    $result = false;
1163
                }
1164
            } else {
1165
                $this->submittedData['end'] = (int)$this->submittedData['end'];
1166
            }
1167
            if ($this->submittedData['end'] < $this->submittedData['start']) {
1168
                $this->addMessage(
1169
                    $this->getLanguageService()->getLL('msg.endDateSmallerThanStartDate'),
1170
                    FlashMessage::ERROR
1171
                );
1172
                $result = false;
1173
            }
1174
        }
1175
        // Set default values for interval and cron command
1176
        $this->submittedData['interval'] = 0;
1177
        $this->submittedData['croncmd'] = '';
1178
        // Check type and validity of frequency, if recurring
1179
        if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING) {
1180
            $frequency = trim($this->submittedData['frequency']);
1181
            if (empty($frequency)) {
1182
                // Empty frequency, not valid
1183
                $this->addMessage($this->getLanguageService()->getLL('msg.noFrequency'), FlashMessage::ERROR);
1184
                $result = false;
1185
            } else {
1186
                $cronErrorMessage = '';
1187
                // Try interpreting the cron command
1188
                try {
1189
                    NormalizeCommand::normalize($frequency);
1190
                    $this->submittedData['croncmd'] = $frequency;
1191
                } catch (\Exception $e) {
1192
                    // Store the exception's result
1193
                    $cronErrorMessage = $e->getMessage();
1194
                    $cronErrorCode = $e->getCode();
1195
                    // Check if the frequency is a valid number
1196
                    // If yes, assume it is a frequency in seconds, and unset cron error code
1197
                    if (is_numeric($frequency)) {
1198
                        $this->submittedData['interval'] = (int)$frequency;
1199
                        $cronErrorCode = 0;
1200
                    }
1201
                }
1202
                // If there's a cron error code, issue validation error message
1203
                if ($cronErrorCode > 0) {
1204
                    $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.frequencyError'), $cronErrorMessage, $cronErrorCode), FlashMessage::ERROR);
1205
                    $result = false;
1206
                }
1207
            }
1208
        }
1209
        // Validate additional input fields
1210
        if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1211
            /** @var AdditionalFieldProviderInterface $providerObject */
1212
            $providerObject = GeneralUtility::makeInstance($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1213
            if ($providerObject instanceof AdditionalFieldProviderInterface) {
0 ignored issues
show
introduced by
$providerObject is always a sub-type of TYPO3\CMS\Scheduler\Addi...lFieldProviderInterface.
Loading history...
1214
                // The validate method will return true if all went well, but that must not
1215
                // override previous false values => AND the returned value with the existing one
1216
                $result &= $providerObject->validateAdditionalFields($this->submittedData, $this);
1217
            }
1218
        }
1219
        return (bool)$result;
1220
    }
1221
1222
    /**
1223
     * Convert input to DateTime and retrieve timestamp
1224
     *
1225
     * @param string $input
1226
     * @return int
1227
     */
1228
    protected function convertToTimestamp(string $input): int
1229
    {
1230
        // Convert to ISO 8601 dates
1231
        $dateTime = new \DateTime($input);
1232
        $value = $dateTime->getTimestamp();
1233
        if ($value !== 0) {
1234
            $value -= (int)date('Z', $value);
1235
        }
1236
        return $value;
1237
    }
1238
1239
    /**
1240
     * This method is used to add a message to the internal queue
1241
     *
1242
     * @param string $message The message itself
1243
     * @param int $severity Message level (according to FlashMessage class constants)
1244
     */
1245
    protected function addMessage($message, $severity = FlashMessage::OK)
1246
    {
1247
        $this->moduleTemplate->addFlashMessage($message, '', $severity);
1248
    }
1249
1250
    /**
1251
     * This method fetches a list of all classes that have been registered with the Scheduler
1252
     * For each item the following information is provided, as an associative array:
1253
     *
1254
     * ['extension']	=>	Key of the extension which provides the class
1255
     * ['filename']		=>	Path to the file containing the class
1256
     * ['title']		=>	String (possibly localized) containing a human-readable name for the class
1257
     * ['provider']		=>	Name of class that implements the interface for additional fields, if necessary
1258
     *
1259
     * The name of the class itself is used as the key of the list array
1260
     *
1261
     * @return array List of registered classes
1262
     */
1263
    protected function getRegisteredClasses(): array
1264
    {
1265
        $list = [];
1266
        foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] ?? [] as $class => $registrationInformation) {
1267
            $title = isset($registrationInformation['title']) ? $this->getLanguageService()->sL($registrationInformation['title']) : '';
1268
            $description = isset($registrationInformation['description']) ? $this->getLanguageService()->sL($registrationInformation['description']) : '';
1269
            $list[$class] = [
1270
                'extension' => $registrationInformation['extension'],
1271
                'title' => $title,
1272
                'description' => $description,
1273
                'provider' => $registrationInformation['additionalFields'] ?? ''
1274
            ];
1275
        }
1276
        return $list;
1277
    }
1278
1279
    /**
1280
     * This method fetches list of all group that have been registered with the Scheduler
1281
     *
1282
     * @return array List of registered groups
1283
     */
1284
    protected function getRegisteredTaskGroups(): array
1285
    {
1286
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1287
            ->getQueryBuilderForTable('tx_scheduler_task_group');
1288
1289
        return $queryBuilder
1290
            ->select('*')
1291
            ->from('tx_scheduler_task_group')
1292
            ->orderBy('sorting')
1293
            ->execute()
1294
            ->fetchAll();
1295
    }
1296
1297
    /**
1298
     * Create the panel of buttons for submitting the form or otherwise perform operations.
1299
     *
1300
     * @param ServerRequestInterface $request
1301
     */
1302
    protected function getButtons(ServerRequestInterface $request): void
1303
    {
1304
        $queryParams = $request->getQueryParams();
1305
        $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1306
        // CSH
1307
        $helpButton = $buttonBar->makeHelpButton()
1308
            ->setModuleName('_MOD_system_txschedulerM1')
1309
            ->setFieldName('');
1310
        $buttonBar->addButton($helpButton);
1311
1312
        // Add and Reload
1313
        if (in_array((string)$this->getCurrentAction(), [Action::LIST, Action::DELETE, Action::STOP, Action::TOGGLE_HIDDEN, Action::SET_NEXT_EXECUTION_TIME], true)) {
1314
            $reloadButton = $buttonBar->makeLinkButton()
1315
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.reload'))
1316
                ->setIcon($this->iconFactory->getIcon('actions-refresh', Icon::SIZE_SMALL))
1317
                ->setHref($this->moduleUri);
1318
            $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1319
            if ($this->MOD_SETTINGS['function'] === 'scheduler' && !empty($this->getRegisteredClasses())) {
1320
                $addButton = $buttonBar->makeLinkButton()
1321
                    ->setTitle($this->getLanguageService()->getLL('action.add'))
1322
                    ->setIcon($this->iconFactory->getIcon('actions-add', Icon::SIZE_SMALL))
1323
                    ->setHref($this->moduleUri . '&CMD=' . Action::ADD);
1324
                $buttonBar->addButton($addButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1325
            }
1326
        }
1327
1328
        // Close and Save
1329
        if (in_array((string)$this->getCurrentAction(), [Action::ADD, Action::EDIT], true)) {
1330
            // Close
1331
            $closeButton = $buttonBar->makeLinkButton()
1332
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel'))
1333
                ->setIcon($this->iconFactory->getIcon('actions-close', Icon::SIZE_SMALL))
1334
                ->setHref($this->moduleUri);
1335
            $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1336
            // Save, SaveAndClose, SaveAndNew
1337
            $saveButtonDropdown = $buttonBar->makeSplitButton();
1338
            $saveButton = $buttonBar->makeInputButton()
1339
                ->setName('CMD')
1340
                ->setValue(Action::SAVE)
1341
                ->setForm('tx_scheduler_form')
1342
                ->setIcon($this->iconFactory->getIcon('actions-document-save', Icon::SIZE_SMALL))
1343
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:save'));
1344
            $saveButtonDropdown->addItem($saveButton);
1345
            $saveAndNewButton = $buttonBar->makeInputButton()
1346
                ->setName('CMD')
1347
                ->setValue(Action::SAVE_NEW)
1348
                ->setForm('tx_scheduler_form')
1349
                ->setIcon($this->iconFactory->getIcon('actions-document-save-new', Icon::SIZE_SMALL))
1350
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.saveAndCreateNewTask'));
1351
            $saveButtonDropdown->addItem($saveAndNewButton);
1352
            $saveAndCloseButton = $buttonBar->makeInputButton()
1353
                ->setName('CMD')
1354
                ->setValue(Action::SAVE_CLOSE)
1355
                ->setForm('tx_scheduler_form')
1356
                ->setIcon($this->iconFactory->getIcon('actions-document-save-close', Icon::SIZE_SMALL))
1357
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:saveAndClose'));
1358
            $saveButtonDropdown->addItem($saveAndCloseButton);
1359
            $buttonBar->addButton($saveButtonDropdown, ButtonBar::BUTTON_POSITION_LEFT, 3);
1360
        }
1361
1362
        // Delete
1363
        if (($queryParams['tx_scheduler']['uid'] ?? false) && $this->getCurrentAction()->equals(Action::EDIT)) {
1364
            $deleteButton = $buttonBar->makeLinkButton()
1365
                ->setHref($this->moduleUri . '&CMD=' . Action::DELETE . '&tx_scheduler[uid]=' . $queryParams['tx_scheduler']['uid'])
1366
                ->setClasses('t3js-modal-trigger')
1367
                ->setDataAttributes([
1368
                    'severity' => 'warning',
1369
                    'title' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:delete'),
1370
                    'button-close-text' => $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:cancel'),
1371
                    'bs-content' => $this->getLanguageService()->getLL('msg.delete'),
1372
                ])
1373
                ->setIcon($this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL))
1374
                ->setTitle($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:delete'));
1375
            $buttonBar->addButton($deleteButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
1376
        }
1377
1378
        // Shortcut
1379
        $shortcutArguments = [
1380
            'CMD' => (string)Action::cast($queryParams['CMD'] ?? null),
1381
            'SET' => [
1382
                'function' => $this->MOD_SETTINGS['function'],
1383
            ]
1384
        ];
1385
        if (isset($queryParams['tx_scheduler']['uid'])) {
1386
            $shortcutArguments['tx_scheduler']['uid'] = $queryParams['tx_scheduler']['uid'];
1387
        }
1388
        $shortcutButton = $buttonBar->makeShortcutButton()
1389
            ->setRouteIdentifier('system_txschedulerM1')
1390
            ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
1391
            ->setArguments($shortcutArguments);
1392
        $buttonBar->addButton($shortcutButton);
1393
    }
1394
1395
    /**
1396
     * Set the current action
1397
     *
1398
     * @param Action $action
1399
     */
1400
    protected function setCurrentAction(Action $action): void
1401
    {
1402
        $this->action = $action;
1403
    }
1404
1405
    /**
1406
     * @return string
1407
     */
1408
    protected function getServerTime(): string
1409
    {
1410
        $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e';
1411
        return date($dateFormat) . ', GMT ' . date('P') . ')';
1412
    }
1413
1414
    protected function getSectionTitle(): string
1415
    {
1416
        $currentAction = (string)$this->getCurrentAction();
1417
1418
        return ($this->MOD_SETTINGS['function'] ?? '') === 'scheduler' && ($currentAction === Action::ADD || $currentAction === Action::EDIT)
1419
            ? $this->getLanguageService()->getLL('action.' . $currentAction)
1420
            : '';
1421
    }
1422
1423
    /**
1424
     * Returns the Language Service
1425
     * @return LanguageService
1426
     */
1427
    protected function getLanguageService(): LanguageService
1428
    {
1429
        return $GLOBALS['LANG'];
1430
    }
1431
1432
    /**
1433
     * Returns the global BackendUserAuthentication object.
1434
     *
1435
     * @return BackendUserAuthentication
1436
     */
1437
    protected function getBackendUser(): BackendUserAuthentication
1438
    {
1439
        return $GLOBALS['BE_USER'];
1440
    }
1441
}
1442