Passed
Push — master ( d5a28b...d12ca0 )
by
unknown
17:01
created

SchedulerModuleController::getPageRenderer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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

897
                    /** @scrutinizer ignore-call */ 
898
                    $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...
898
                    if ($task instanceof ProgressProviderInterface) {
899
                        $progress = round((float)$task->getProgress(), 2);
900
                        $tasks[$taskIndex]['tasks'][$recordIndex]['progress'] = $progress;
901
                    }
902
                    $tasks[$taskIndex]['tasks'][$recordIndex]['classTitle'] = $registeredClasses[$class]['title'];
903
                    $tasks[$taskIndex]['tasks'][$recordIndex]['classExtension'] = $registeredClasses[$class]['extension'];
904
                    $tasks[$taskIndex]['tasks'][$recordIndex]['additionalInformation'] = $additionalInformation;
905
                    // Check if task currently has a running execution
906
                    if (!empty($schedulerRecord['serialized_executions'])) {
907
                        $labels[] = [
908
                            'class' => 'success',
909
                            'text' => $this->getLanguageService()->getLL('status.running')
910
                        ];
911
                        $isRunning = true;
912
                    }
913
                    $tasks[$taskIndex]['tasks'][$recordIndex]['isRunning'] = $isRunning;
914
915
                    // Prepare display of next execution date
916
                    // If task is currently running, date is not displayed (as next hasn't been calculated yet)
917
                    // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
918
                    if ($isRunning || $schedulerRecord['disable']) {
919
                        $nextDate = '-';
920
                    } else {
921
                        $nextDate = date($dateFormat, (int)$schedulerRecord['nextexecution']);
922
                        if (empty($schedulerRecord['nextexecution'])) {
923
                            $nextDate = $this->getLanguageService()->getLL('none');
924
                        } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
925
                            $labels[] = [
926
                                'class' => 'warning',
927
                                'text' => $this->getLanguageService()->getLL('status.late'),
928
                                'description' => $this->getLanguageService()->getLL('status.legend.scheduled')
929
                            ];
930
                        }
931
                    }
932
                    $tasks[$taskIndex]['tasks'][$recordIndex]['nextDate'] = $nextDate;
933
                    // Get execution type
934
                    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

934
                    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...
935
                        $execType = $this->getLanguageService()->getLL('label.type.single');
936
                        $frequency = '-';
937
                    } else {
938
                        $execType = $this->getLanguageService()->getLL('label.type.recurring');
939
                        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

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