SchedulerModuleController.php 67 KB
Newer Older
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the TYPO3 CMS project.
7
 *
8
9
10
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
11
 *
12
13
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
14
 *
15
16
 * The TYPO3 project - inspiring people to share!
 */
17

18
19
namespace TYPO3\CMS\Scheduler\Controller;

20
21
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
22
use TYPO3\CMS\Backend\Routing\UriBuilder;
23
24
use TYPO3\CMS\Backend\Template\Components\ButtonBar;
use TYPO3\CMS\Backend\Template\ModuleTemplate;
25
use TYPO3\CMS\Backend\Utility\BackendUtility;
26
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
27
use TYPO3\CMS\Core\Core\Environment;
28
use TYPO3\CMS\Core\Database\ConnectionPool;
29
use TYPO3\CMS\Core\Http\HtmlResponse;
30
use TYPO3\CMS\Core\Imaging\Icon;
31
use TYPO3\CMS\Core\Imaging\IconFactory;
32
use TYPO3\CMS\Core\Localization\LanguageService;
33
34
use TYPO3\CMS\Core\Messaging\FlashMessage;
use TYPO3\CMS\Core\Page\PageRenderer;
35
use TYPO3\CMS\Core\Registry;
36
37
38
use TYPO3\CMS\Core\SysLog\Action as SystemLogGenericAction;
use TYPO3\CMS\Core\SysLog\Error as SystemLogErrorClassification;
use TYPO3\CMS\Core\SysLog\Type as SystemLogType;
39
use TYPO3\CMS\Core\Utility\ArrayUtility;
40
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
41
use TYPO3\CMS\Core\Utility\GeneralUtility;
42
use TYPO3\CMS\Fluid\View\StandaloneView;
43
use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper;
44
45
46
47
use TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface;
use TYPO3\CMS\Scheduler\CronCommand\NormalizeCommand;
use TYPO3\CMS\Scheduler\ProgressProviderInterface;
use TYPO3\CMS\Scheduler\Scheduler;
48
use TYPO3\CMS\Scheduler\Task\AbstractTask;
49
use TYPO3\CMS\Scheduler\Task\Enumeration\Action;
50

51
52
/**
 * Module 'TYPO3 Scheduler administration module' for the 'scheduler' extension.
53
 * @internal This class is a specific Backend controller implementation and is not considered part of the Public TYPO3 API.
54
 */
55
class SchedulerModuleController
56
{
57

58
59
60
61
62
    /**
     * Array containing submitted data when editing or adding a task
     *
     * @var array
     */
63
    protected $submittedData = [];
64
65
66
67
68
69
70

    /**
     * Array containing all messages issued by the application logic
     * Contains the error's severity and the message itself
     *
     * @var array
     */
71
    protected $messages = [];
72
73
74
75

    /**
     * @var string Key of the CSH file
     */
76
    protected $cshKey = '_MOD_system_txschedulerM1';
77
78

    /**
79
     * @var Scheduler Local scheduler instance
80
81
82
83
84
85
86
87
88
     */
    protected $scheduler;

    /**
     * @var string
     */
    protected $backendTemplatePath = '';

    /**
89
     * @var StandaloneView
90
91
92
93
94
95
96
97
     */
    protected $view;

    /**
     * @var string Base URI of scheduler module
     */
    protected $moduleUri;

98
99
100
101
102
103
104
    /**
     * ModuleTemplate Container
     *
     * @var ModuleTemplate
     */
    protected $moduleTemplate;

105
106
107
108
109
    /**
     * @var IconFactory
     */
    protected $iconFactory;

110
111
112
113
    /**
     * @var Action
     */
    protected $action;
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132

    /**
     * The module menu items array. Each key represents a key for which values can range between the items in the array of that key.
     *
     * @var array
     */
    protected $MOD_MENU = [
        'function' => []
    ];

    /**
     * Current settings for the keys of the MOD_MENU array
     *
     * @var array
     */
    protected $MOD_SETTINGS = [];

    /**
     * Default constructor
133
134
135
     */
    public function __construct()
    {
136
        $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
137
138
        $this->getLanguageService()->includeLLFile('EXT:scheduler/Resources/Private/Language/locallang.xlf');
        $this->backendTemplatePath = ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Backend/SchedulerModule/';
139
        $this->view = GeneralUtility::makeInstance(StandaloneView::class);
140
        $this->view->getRequest()->setControllerExtensionName('scheduler');
141
        $this->view->setPartialRootPaths([ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Partials/Backend/SchedulerModule/']);
142
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
143
        $this->moduleUri = (string)$uriBuilder->buildUriFromRoute('system_txschedulerM1');
144
        $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
145
        $this->scheduler = GeneralUtility::makeInstance(Scheduler::class);
146

147
        $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
148
149
150
    }

    /**
151
152
153
154
     * Injects the request object for the current request or subrequest
     *
     * @param ServerRequestInterface $request the current request
     * @return ResponseInterface the response with the content
155
     */
156
    public function mainAction(ServerRequestInterface $request): ResponseInterface
157
    {
158
159
160
161
        $parsedBody = $request->getParsedBody();
        $queryParams = $request->getQueryParams();

        $this->setCurrentAction(Action::cast($parsedBody['CMD'] ?? $queryParams['CMD'] ?? null));
162
163
        $this->MOD_MENU = [
            'function' => [
164
165
166
                'scheduler' => $this->getLanguageService()->getLL('function.scheduler'),
                'check' => $this->getLanguageService()->getLL('function.check'),
                'info' => $this->getLanguageService()->getLL('function.info')
167
168
            ]
        ];
169
170
        $settings = $parsedBody['SET'] ?? $queryParams['SET'] ?? null;
        $this->MOD_SETTINGS = BackendUtility::getModuleData($this->MOD_MENU, $settings, 'system_txschedulerM1', '', '', '');
171
172
173
174
175
176

        // Set the form
        $content = '<form name="tx_scheduler_form" id="tx_scheduler_form" method="post" action="">';

        // Prepare main content
        $content .= '<h1>' . $this->getLanguageService()->getLL('function.' . $this->MOD_SETTINGS['function']) . '</h1>';
177
178
        $previousCMD = Action::cast($parsedBody['previousCMD'] ?? $queryParams['previousCMD'] ?? null);
        $content .= $this->getModuleContent($previousCMD);
179
180
        $content .= '<div id="extraFieldsSection"></div></form><div id="extraFieldsHidden"></div>';

181
        $this->getButtons($request);
182
        $this->getModuleMenu();
183
184

        $this->moduleTemplate->setContent($content);
185
        return new HtmlResponse($this->moduleTemplate->renderContent());
186
187
    }

188
189
190
191
192
193
194
195
196
197
    /**
     * Get the current action
     *
     * @return Action
     */
    public function getCurrentAction(): Action
    {
        return $this->action;
    }

198
199
200
    /**
     * Generates the action menu
     */
201
    protected function getModuleMenu(): void
202
203
204
    {
        $menu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
        $menu->setIdentifier('SchedulerJumpMenu');
205
206
        /** @var UriBuilder $uriBuilder */
        $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
207
208
209
210
        foreach ($this->MOD_MENU['function'] as $controller => $title) {
            $item = $menu
                ->makeMenuItem()
                ->setHref(
211
                    (string)$uriBuilder->buildUriFromRoute(
212
                        'system_txschedulerM1',
213
                        [
214
                            'id' => 0,
215
216
217
218
219
220
221
222
223
224
225
226
227
                            'SET' => [
                                'function' => $controller
                            ]
                        ]
                    )
                )
                ->setTitle($title);
            if ($controller === $this->MOD_SETTINGS['function']) {
                $item->setActive(true);
            }
            $menu->addMenuItem($item);
        }
        $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);
228
229
230
231
232
    }

    /**
     * Generate the module's content
     *
233
     * @param Action $previousAction
234
235
     * @return string HTML of the module's main content
     */
236
    protected function getModuleContent(Action $previousAction): string
237
238
239
240
241
242
243
    {
        $content = '';
        $sectionTitle = '';
        // Get submitted data
        $this->submittedData = GeneralUtility::_GPmerged('tx_scheduler');
        $this->submittedData['uid'] = (int)$this->submittedData['uid'];
        // If a save command was submitted, handle saving now
244
        if (in_array((string)$this->getCurrentAction(), [Action::SAVE, Action::SAVE_CLOSE, Action::SAVE_NEW], true)) {
245
246
            // First check the submitted data
            $result = $this->preprocessData();
247

248
249
250
            // If result is ok, proceed with saving
            if ($result) {
                $this->saveTask();
251
252
253
254
255

                if ($this->action->equals(Action::SAVE_CLOSE)) {
                    // Display default screen
                    $this->setCurrentAction(Action::cast(Action::LIST));
                } elseif ($this->action->equals(Action::SAVE)) {
256
                    // After saving a "add form", return to edit
257
258
                    $this->setCurrentAction(Action::cast(Action::EDIT));
                } elseif ($this->action->equals(Action::SAVE_NEW)) {
259
260
261
                    // Unset submitted data, so that empty form gets displayed
                    unset($this->submittedData);
                    // After saving a "add/edit form", return to add
262
                    $this->setCurrentAction(Action::cast(Action::ADD));
263
264
                } else {
                    // Return to edit form
265
                    $this->setCurrentAction($previousAction);
266
267
                }
            } else {
268
                $this->setCurrentAction($previousAction);
269
270
271
272
273
274
275
276
            }
        }

        // Handle chosen action
        switch ((string)$this->MOD_SETTINGS['function']) {
            case 'scheduler':
                $this->executeTasks();

277
278
279
                switch ((string)$this->getCurrentAction()) {
                    case Action::ADD:
                    case Action::EDIT:
280
281
282
                        try {
                            // Try adding or editing
                            $content .= $this->editTaskAction();
283
                            $sectionTitle = $this->getLanguageService()->getLL('action.' . $this->getCurrentAction());
284
285
286
287
                        } catch (\LogicException|\UnexpectedValueException|\OutOfBoundsException $e) {
                            // Catching all types of exceptions that were previously handled and
                            // converted to messages
                            $content .= $this->listTasksAction();
288
                        } catch (\Exception $e) {
289
290
                            // Catching all "unexpected" exceptions not previously handled
                            $this->addMessage($e->getMessage(), FlashMessage::ERROR);
291
292
293
                            $content .= $this->listTasksAction();
                        }
                        break;
294
                    case Action::DELETE:
295
296
297
                        $this->deleteTask();
                        $content .= $this->listTasksAction();
                        break;
298
                    case Action::STOP:
299
300
301
                        $this->stopTask();
                        $content .= $this->listTasksAction();
                        break;
302
                    case Action::TOGGLE_HIDDEN:
303
304
305
                        $this->toggleDisableAction();
                        $content .= $this->listTasksAction();
                        break;
306
                    case Action::SET_NEXT_EXECUTION_TIME:
307
308
309
                        $this->setNextExecutionTimeAction();
                        $content .= $this->listTasksAction();
                        break;
310
                    case Action::LIST:
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
                        $content .= $this->listTasksAction();
                }
                break;

            // Setup check screen
            case 'check':
                // @todo move check to the report module
                $content .= $this->checkScreenAction();
                break;

            // Information screen
            case 'info':
                $content .= $this->infoScreenAction();
                break;
        }
326
327
        // Wrap the content
        return '<h2>' . $sectionTitle . '</h2><div class="tx_scheduler_mod1">' . $content . '</div>';
328
329
330
331
332
333
334
335
    }

    /**
     * This method displays the result of a number of checks
     * on whether the Scheduler is ready to run or running properly
     *
     * @return string Further information
     */
336
    protected function checkScreenAction(): string
337
338
339
340
    {
        $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'CheckScreen.html');

        // Display information about last automated run, as stored in the system registry
341
        $registry = GeneralUtility::makeInstance(Registry::class);
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
        $lastRun = $registry->get('tx_scheduler', 'lastRun');
        if (!is_array($lastRun)) {
            $message = $this->getLanguageService()->getLL('msg.noLastRun');
            $severity = InfoboxViewHelper::STATE_WARNING;
        } else {
            if (empty($lastRun['end']) || empty($lastRun['start']) || empty($lastRun['type'])) {
                $message = $this->getLanguageService()->getLL('msg.incompleteLastRun');
                $severity = InfoboxViewHelper::STATE_WARNING;
            } else {
                $startDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['start']);
                $startTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['start']);
                $endDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['end']);
                $endTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['end']);
                $label = 'automatically';
                if ($lastRun['type'] === 'manual') {
                    $label = 'manually';
                }
                $type = $this->getLanguageService()->getLL('label.' . $label);
                $message = sprintf($this->getLanguageService()->getLL('msg.lastRun'), $type, $startDate, $startTime, $endDate, $endTime);
                $severity = InfoboxViewHelper::STATE_INFO;
            }
        }
        $this->view->assign('lastRunMessage', $message);
        $this->view->assign('lastRunSeverity', $severity);

367
368
        if (Environment::isComposerMode()) {
            $this->view->assign('composerMode', true);
369
        } else {
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
            // Check if CLI script is executable or not
            $script = GeneralUtility::getFileAbsFileName('EXT:core/bin/typo3');
            $this->view->assign('script', $script);
            // Skip this check if running Windows, as rights do not work the same way on this platform
            // (i.e. the script will always appear as *not* executable)
            if (Environment::isWindows()) {
                $isExecutable = true;
            } else {
                $isExecutable = is_executable($script);
            }
            if ($isExecutable) {
                $message = $this->getLanguageService()->getLL('msg.cliScriptExecutable');
                $severity = InfoboxViewHelper::STATE_OK;
            } else {
                $message = $this->getLanguageService()->getLL('msg.cliScriptNotExecutable');
                $severity = InfoboxViewHelper::STATE_ERROR;
            }
            $this->view->assign('isExecutableMessage', $message);
            $this->view->assign('isExecutableSeverity', $severity);
389
        }
390

391
        $this->view->assign('now', $this->getServerTime());
392
393
394
395
396
397
398
399
400

        return $this->view->render();
    }

    /**
     * This method gathers information about all available task classes and displays it
     *
     * @return string html
     */
401
    protected function infoScreenAction(): string
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
    {
        $registeredClasses = $this->getRegisteredClasses();
        // No classes available, display information message
        if (empty($registeredClasses)) {
            $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreenNoClasses.html');
            return $this->view->render();
        }

        $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreen.html');
        $this->view->assign('registeredClasses', $registeredClasses);

        return $this->view->render();
    }

    /**
     * Delete a task from the execution queue
     */
419
    protected function deleteTask(): void
420
421
422
423
424
425
426
427
428
    {
        try {
            // Try to fetch the task and delete it
            $task = $this->scheduler->fetchTask($this->submittedData['uid']);
            // If the task is currently running, it may not be deleted
            if ($task->isExecutionRunning()) {
                $this->addMessage($this->getLanguageService()->getLL('msg.maynotDeleteRunningTask'), FlashMessage::ERROR);
            } else {
                if ($this->scheduler->removeTask($task)) {
429
                    $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()]);
430
431
432
433
434
435
                    $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
                } else {
                    $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
                }
            }
        } catch (\UnexpectedValueException $e) {
436
437
438
439
440
            // The task could not be unserialized properly, simply update the database record
            $taskUid = (int)$this->submittedData['uid'];
            $result = GeneralUtility::makeInstance(ConnectionPool::class)
                ->getConnectionForTable('tx_scheduler_task')
                ->update('tx_scheduler_task', ['deleted' => 1], ['uid' => $taskUid]);
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
            if ($result) {
                $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
            } else {
                $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
            }
        } catch (\OutOfBoundsException $e) {
            // The task was not found, for some reason
            $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
        }
    }

    /**
     * Clears the registered running executions from the task
     * Note that this doesn't actually stop the running script. It just unmarks
     * all executions.
     * @todo find a way to really kill the running task
     */
458
    protected function stopTask(): void
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
    {
        try {
            // Try to fetch the task and stop it
            $task = $this->scheduler->fetchTask($this->submittedData['uid']);
            if ($task->isExecutionRunning()) {
                // If the task is indeed currently running, clear marked executions
                $result = $task->unmarkAllExecutions();
                if ($result) {
                    $this->addMessage($this->getLanguageService()->getLL('msg.stopSuccess'));
                } else {
                    $this->addMessage($this->getLanguageService()->getLL('msg.stopError'), FlashMessage::ERROR);
                }
            } else {
                // The task is not running, nothing to unmark
                $this->addMessage($this->getLanguageService()->getLL('msg.maynotStopNonRunningTask'), FlashMessage::WARNING);
            }
        } catch (\Exception $e) {
            // The task was not found, for some reason
            $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
        }
    }

    /**
     * Toggles the disabled state of the submitted task
     */
484
    protected function toggleDisableAction(): void
485
486
487
    {
        $task = $this->scheduler->fetchTask($this->submittedData['uid']);
        $task->setDisabled(!$task->isDisabled());
488
489
490
491
492
        // If a disabled single task is enabled again, we register it for a
        // single execution at next scheduler run.
        if ($task->getType() === AbstractTask::TYPE_SINGLE) {
            $task->registerSingleExecution(time());
        }
493
494
495
        $task->save();
    }

496
497
498
    /**
     * Sets the next execution time of the submitted task to now
     */
499
    protected function setNextExecutionTimeAction(): void
500
501
502
503
504
505
    {
        $task = $this->scheduler->fetchTask($this->submittedData['uid']);
        $task->setRunOnNextCronJob(true);
        $task->save();
    }

506
507
508
509
510
    /**
     * Return a form to add a new task or edit an existing one
     *
     * @return string HTML form to add or edit a task
     */
511
    protected function editTaskAction(): string
512
513
514
515
516
517
    {
        $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'EditTask.html');

        $registeredClasses = $this->getRegisteredClasses();
        $registeredTaskGroups = $this->getRegisteredTaskGroups();

518
        $taskInfo = [];
519
520
521
522
523
524
525
526
527
528
        $task = null;
        $process = 'edit';

        if ($this->submittedData['uid'] > 0) {
            // If editing, retrieve data for existing task
            try {
                $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
                // If there's a registered execution, the task should not be edited
                if (!empty($taskRecord['serialized_executions'])) {
                    $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
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
                $task = unserialize($taskRecord['serialized_task_object']);

                // Set some task information
                $taskInfo['disable'] = $taskRecord['disable'];
                $taskInfo['description'] = $taskRecord['description'];
                $taskInfo['task_group'] = $taskRecord['task_group'];

                // Check that the task object is valid
                if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
                    // The task object is valid, process with fetching current data
                    $taskInfo['class'] = get_class($task);
                    // Get execution information
                    $taskInfo['start'] = (int)$task->getExecution()->getStart();
                    $taskInfo['end'] = (int)$task->getExecution()->getEnd();
                    $taskInfo['interval'] = $task->getExecution()->getInterval();
                    $taskInfo['croncmd'] = $task->getExecution()->getCronCmd();
                    $taskInfo['multiple'] = $task->getExecution()->getMultiple();
                    if (!empty($taskInfo['interval']) || !empty($taskInfo['croncmd'])) {
                        // Guess task type from the existing information
                        // If an interval or a cron command is defined, it's a recurring task
554
                        $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
555
556
557
558
                        $taskInfo['frequency'] = $taskInfo['interval'] ?: $taskInfo['croncmd'];
                    } else {
                        // It's not a recurring task
                        // Make sure interval and cron command are both empty
559
                        $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
560
561
562
563
564
565
566
567
568
569
570
571
                        $taskInfo['frequency'] = '';
                        $taskInfo['end'] = 0;
                    }
                } else {
                    // The task object is not valid
                    // Issue error message
                    $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.invalidTaskClassEdit'), get_class($task)), FlashMessage::ERROR);
                    // Initialize empty values
                    $taskInfo['start'] = 0;
                    $taskInfo['end'] = 0;
                    $taskInfo['frequency'] = '';
                    $taskInfo['multiple'] = false;
572
                    $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
573
574
575
576
577
578
579
580
581
                }
            } catch (\OutOfBoundsException $e) {
                // Add a message and continue throwing the exception
                $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
                throw $e;
            }
        } else {
            // If adding a new object, set some default values
            $taskInfo['class'] = key($registeredClasses);
582
            $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
583
584
585
586
587
588
589
590
591
592
            $taskInfo['start'] = $GLOBALS['EXEC_TIME'];
            $taskInfo['end'] = '';
            $taskInfo['frequency'] = '';
            $taskInfo['multiple'] = 0;
            $process = 'add';
        }

        // If some data was already submitted, use it to override
        // existing data
        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
599
600
        if ($process === 'add') {
            foreach ($registeredClasses as $class => $registrationInfo) {
                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
610
611
612
        } elseif ($task !== null && !empty($registeredClasses[$taskInfo['class']]['provider'])) {
            // only try to fetch additionalFields if the task is valid
            $providerObject = GeneralUtility::makeInstance($registeredClasses[$taskInfo['class']]['provider']);
            if ($providerObject instanceof AdditionalFieldProviderInterface) {
613
                $allAdditionalFields[$taskInfo['class']] = $providerObject->getAdditionalFields($taskInfo, $task, $this);
614
615
616
617
618
619
620
621
            }
        }

        // Load necessary JavaScript
        $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
        $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker');

        // Start rendering the add/edit form
622
623
        $this->view->assign('uid', htmlspecialchars((string)$this->submittedData['uid']));
        $this->view->assign('cmd', htmlspecialchars((string)$this->getCurrentAction()));
624
625
        $this->view->assign('csh', $this->cshKey);
        $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
634
635

        // Task class selector
        // On editing, don't allow changing of the task class, unless it was not valid
        if ($this->submittedData['uid'] > 0 && !empty($taskInfo['class'])) {
636
637
638
            $this->view->assign('task_class', $taskInfo['class']);
            $this->view->assign('task_class_title', $registeredClasses[$taskInfo['class']]['title']);
            $this->view->assign('task_class_extension', $registeredClasses[$taskInfo['class']]['extension']);
639
640
        } else {
            // Group registered classes by classname
641
            $groupedClasses = [];
642
643
644
645
646
647
648
            foreach ($registeredClasses as $class => $classInfo) {
                $groupedClasses[$classInfo['extension']][$class] = $classInfo;
            }
            ksort($groupedClasses);
            foreach ($groupedClasses as $extension => $class) {
                foreach ($groupedClasses[$extension] as $class => $classInfo) {
                    $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
657
        $this->view->assign('task_type_selected_1', ((int)$taskInfo['type'] === AbstractTask::TYPE_SINGLE ? ' selected="selected"' : ''));
        $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
669
        $this->view->assign('start_value_hr', ($taskInfo['start'] > 0 ? strftime($dateFormat, $taskInfo['start']) : ''));
        $this->view->assign('start_value', $taskInfo['start']);
670
671
672

        // End date/time field
        // NOTE: datetime fields need a special id naming scheme
673
674
        $this->view->assign('end_value_hr', ($taskInfo['end'] > 0 ? strftime($dateFormat, $taskInfo['end']) : ''));
        $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
688
689
690
691
692
693
        foreach ($allAdditionalFields as $class => $fields) {
            if ($class == $taskInfo['class']) {
                $additionalFieldsStyle = '';
            } else {
                $additionalFieldsStyle = ' style="display: none"';
            }
            // 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
698
699
700
701
702
703
704
705
706
                    $field = [];
                    $field['htmlClassName'] = $htmlClassName;
                    $field['code'] = $fieldInfo['code'];
                    $field['cshKey'] = $fieldInfo['cshKey'];
                    $field['cshLabel'] = $fieldInfo['cshLabel'];
                    $field['langLabel'] = $fieldInfo['label'];
                    $field['fieldID'] = $fieldID;
                    $field['additionalFieldsStyle'] = $additionalFieldsStyle;
                    $field['browseButton'] = $this->getBrowseButton($fieldID, $fieldInfo);
                    $additionalFieldList[] = $field;
707
708
709
                }
            }
        }
710
        $this->view->assign('additionalFields', $additionalFieldList);
711

712
        $this->view->assign('returnUrl', (string)GeneralUtility::getIndpEnv('REQUEST_URI'));
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
718
719

        return $this->view->render();
    }

720
721
722
723
724
    /**
     * @param string $fieldID The id of the field witch contains the page id
     * @param array $fieldInfo The array with the field info, contains the page title shown beside the button
     * @return string HTML code for the browse button
     */
725
    protected function getBrowseButton($fieldID, array $fieldInfo): string
726
727
    {
        if (isset($fieldInfo['browser']) && ($fieldInfo['browser'] === 'page')) {
728
            $uriBuilder = GeneralUtility::makeInstance(UriBuilder::class);
729
730
            $url = (string)$uriBuilder->buildUriFromRoute('wizard_element_browser');

731
            $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_core.xlf:labels.browse_db'));
732
            return '
733
                <div><a href="' . htmlspecialchars($url) . '" data-trigger-for="' . htmlspecialchars($fieldID) . '" data-mode="db" data-params="" class="btn btn-default t3js-element-browser" title="' . $title . '">
734
                    <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-insert-record" data-identifier="actions-insert-record">
735
736
                        <span class="icon-markup">' . $this->iconFactory->getIcon(
                'actions-insert-record',
Benni Mack's avatar
Benni Mack committed
737
                Icon::SIZE_SMALL
738
            )->render() . '</span>
739
740
741
                    </span>
                </a><span id="page_' . $fieldID . '">&nbsp;' . htmlspecialchars($fieldInfo['pageTitle']) . '</span></div>';
        }
742
        return '';
743
744
    }

745
746
747
    /**
     * Execute all selected tasks
     */
748
    protected function executeTasks(): void
749
750
751
752
753
754
755
756
757
758
759
760
    {
        // Continue if some elements have been chosen for execution
        if (isset($this->submittedData['execute']) && !empty($this->submittedData['execute'])) {
            // Get list of registered classes
            $registeredClasses = $this->getRegisteredClasses();
            // Loop on all selected tasks
            foreach ($this->submittedData['execute'] as $uid) {
                try {
                    // Try fetching the task
                    $task = $this->scheduler->fetchTask($uid);
                    $class = get_class($task);
                    $name = $registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')';
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
                    if (GeneralUtility::_POST('go_cron') !== null) {
                        $task->setRunOnNextCronJob(true);
                        $task->save();
                    } else {
                        // Now try to execute it and report on outcome
                        try {
                            $result = $this->scheduler->executeTask($task);
                            if ($result) {
                                $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executed'), $name));
                            } else {
                                $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.notExecuted'), $name), FlashMessage::ERROR);
                            }
                        } catch (\Exception $e) {
                            // An exception was thrown, display its message as an error
                            $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $name, $e->getMessage()), FlashMessage::ERROR);
776
777
778
779
780
781
782
783
784
785
786
                        }
                    }
                } catch (\OutOfBoundsException $e) {
                    $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $uid), FlashMessage::ERROR);
                } catch (\UnexpectedValueException $e) {
                    $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $uid, $e->getMessage()), FlashMessage::ERROR);
                }
            }
            // Record the run in the system registry
            $this->scheduler->recordLastRun('manual');
            // Make sure to switch to list view after execution
787
            $this->setCurrentAction(Action::cast(Action::LIST));
788
789
790
791
792
793
794
795
        }
    }

    /**
     * Assemble display of list of scheduled tasks
     *
     * @return string Table of pending tasks
     */
796
    protected function listTasksAction(): string
797
798
799
800
801
802
803
804
805
806
807
    {
        $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasks.html');

        // Define display format for dates
        $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];

        // Get list of registered task groups
        $registeredTaskGroups = $this->getRegisteredTaskGroups();

        // add an empty entry for non-grouped tasks
        // add in front of list
808
        array_unshift($registeredTaskGroups, ['uid' => 0, 'groupName' => '']);
809
810
811

        // Get all registered tasks
        // Just to get the number of entries
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
        $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
            ->getQueryBuilderForTable('tx_scheduler_task');
        $queryBuilder->getRestrictions()->removeAll();

        $result = $queryBuilder->select('t.*')
            ->addSelect(
                'g.groupName AS taskGroupName',
                'g.description AS taskGroupDescription',
                'g.deleted AS isTaskGroupDeleted'
            )
            ->from('tx_scheduler_task', 't')
            ->leftJoin(
                't',
                'tx_scheduler_task_group',
                'g',
                $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
            )
829
830
831
            ->where(
                $queryBuilder->expr()->eq('t.deleted', 0)
            )
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
            ->orderBy('g.sorting')
            ->execute();

        // Loop on all tasks
        $temporaryResult = [];
        while ($row = $result->fetch()) {
            if ($row['taskGroupName'] === null || $row['isTaskGroupDeleted'] === '1') {
                $row['taskGroupName'] = '';
                $row['taskGroupDescription'] = '';
                $row['task_group'] = 0;
            }
            $temporaryResult[$row['task_group']]['groupName'] = $row['taskGroupName'];
            $temporaryResult[$row['task_group']]['groupDescription'] = $row['taskGroupDescription'];
            $temporaryResult[$row['task_group']]['tasks'][] = $row;
        }
847
848

        // No tasks defined, display information message
849
        if (empty($temporaryResult)) {
850
851
            $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasksNoTasks.html');
            return $this->view->render();
852
853
854
        }

        $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
855
        $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
856
857

        $tasks = $temporaryResult;
858
859

        $registeredClasses = $this->getRegisteredClasses();
860
        $missingClasses = [];
861
        foreach ($temporaryResult as $taskIndex => $taskGroup) {
862
            foreach ($taskGroup['tasks'] as $recordIndex => $schedulerRecord) {
863
864
865
866
867
                if ((int)$schedulerRecord['disable'] === 1) {
                    $translationKey = 'enable';
                } else {
                    $translationKey = 'disable';
                }
868
                $tasks[$taskIndex]['tasks'][$recordIndex]['translationKey'] = $translationKey;
869
870
871
872
873
874

                // Define some default values
                $lastExecution = '-';
                $isRunning = false;
                $showAsDisabled = false;
                // Restore the serialized task and pass it a reference to the scheduler object
875
                /** @var \TYPO3\CMS\Scheduler\Task\AbstractTask|ProgressProviderInterface $task */
876
877
                $task = unserialize($schedulerRecord['serialized_task_object']);
                $class = get_class($task);
878
                if ($class === \__PHP_Incomplete_Class::class && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) {
879
880
                    $class = $matches['classname'];
                }
881
                $tasks[$taskIndex]['tasks'][$recordIndex]['class'] = $class;
882
883
                // Assemble information about last execution
                if (!empty($schedulerRecord['lastexecution_time'])) {
884
                    $lastExecution = date($dateFormat, (int)$schedulerRecord['lastexecution_time']);
885
                    if ($schedulerRecord['lastexecution_context'] === 'CLI') {
886
887
888
                        $context = $this->getLanguageService()->getLL('label.cron');
                    } else {
                        $context = $this->getLanguageService()->getLL('label.manual');
889
                    }
890
                    $lastExecution .= ' (' . $context . ')';
891
                }
892
                $tasks[$taskIndex]['tasks'][$recordIndex]['lastExecution'] = $lastExecution;
893

894
                if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
895
                    $tasks[$taskIndex]['tasks'][$recordIndex]['validClass'] = true;
896
                    // The task object is valid
897
                    $labels = [];
898
                    $additionalInformation = $task->getAdditionalInformation();
899
                    if ($task instanceof ProgressProviderInterface) {
900
                        $progress = round((float)$task->getProgress(), 2);
901
                        $tasks[$taskIndex]['tasks'][$recordIndex]['progress'] = $progress;
902
                    }
903
904
905
                    $tasks[$taskIndex]['tasks'][$recordIndex]['classTitle'] = $registeredClasses[$class]['title'];
                    $tasks[$taskIndex]['tasks'][$recordIndex]['classExtension'] = $registeredClasses[$class]['extension'];
                    $tasks[$taskIndex]['tasks'][$recordIndex]['additionalInformation'] = $additionalInformation;
906
907
                    // Check if task currently has a running execution
                    if (!empty($schedulerRecord['serialized_executions'])) {
908
                        $labels[] = [
909
910
                            'class' => 'success',
                            'text' => $this->getLanguageService()->getLL('status.running')
911
                        ];
912
                        $isRunning = true;
913
                    }
914
                    $tasks[$taskIndex]['tasks'][$recordIndex]['isRunning'] = $isRunning;
915

916
917
918
919
920
921
                    // Prepare display of next execution date
                    // If task is currently running, date is not displayed (as next hasn't been calculated yet)
                    // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
                    if ($isRunning || $schedulerRecord['disable']) {
                        $nextDate = '-';
                    } else {
922
                        $nextDate = date($dateFormat, (int)$schedulerRecord['nextexecution']);
923
924
925
                        if (empty($schedulerRecord['nextexecution'])) {
                            $nextDate = $this->getLanguageService()->getLL('none');
                        } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
926
                            $labels[] = [
927
928
929
                                'class' => 'warning',
                                'text' => $this->getLanguageService()->getLL('status.late'),
                                'description' => $this->getLanguageService()->getLL('status.legend.scheduled')
930
                            ];
931
                        }
932
                    }
933
                    $tasks[$taskIndex]['tasks'][$recordIndex]['nextDate'] = $nextDate;
934
935
936
937
938
939
940
941
                    // Get execution type
                    if ($task->getType() === AbstractTask::TYPE_SINGLE) {
                        $execType = $this->getLanguageService()->getLL('label.type.single');
                        $frequency = '-';
                    } else {
                        $execType = $this->getLanguageService()->getLL('label.type.recurring');
                        if ($task->getExecution()->getCronCmd() == '') {
                            $frequency = $task->getExecution()->getInterval();
942
                        } else {
943
                            $frequency = $task->getExecution()->getCronCmd();
944
                        }
945
946
947
948
                    }
                    // Check the disable status
                    // Row is shown dimmed if task is disabled, unless it is still running
                    if ($schedulerRecord['disable'] && !$isRunning) {
949
                        $labels[] = [
950
951
                            'class' => 'default',
                            'text' => $this->getLanguageService()->getLL('status.disabled')
952
                        ];
953
954
                        $showAsDisabled = true;
                    }
955
956
957
958
                    $tasks[$taskIndex]['tasks'][$recordIndex]['execType'] = $execType;
                    $tasks[$taskIndex]['tasks'][$recordIndex]['frequency'] = $frequency;
                    // Get multiple executions setting
                    if ($task->getExecution()->getMultiple()) {
959
                        $multiple = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:yes');
960
                    } else {
961
                        $multiple = $this->getLanguageService()->sL('LLL:EXT:core/Resources/Private/Language/locallang_common.xlf:no');
962
                    }
963
                    $tasks[$taskIndex]['tasks'][$recordIndex]['multiple'] = $multiple;
964

965
966
967
                    // Check if the last run failed
                    if (!empty($schedulerRecord['lastexecution_failure'])) {
                        // Try to get the stored exception array
968
                        /** @var array $exceptionArray */
969
970
971
972
                        $exceptionArray = @unserialize($schedulerRecord['lastexecution_failure']);
                        // If the exception could not be unserialized, issue a default error message
                        if (!is_array($exceptionArray) || empty($exceptionArray)) {
                            $labelDescription = $this->getLanguageService()->getLL('msg.executionFailureDefault');
973
                        } else {
974
                            $labelDescription = sprintf($this->getLanguageService()->getLL('msg.executionFailureReport'), $exceptionArray['code'], $exceptionArray['message']);
975
                        }
976
                        $labels[] = [
977
978
979
                            'class' => 'danger',
                            'text' => $this->getLanguageService()->getLL('status.failure'),
                            'description' => $labelDescription
980
                        ];
981
                    }
982
983
984
                    $tasks[$taskIndex]['tasks'][$recordIndex]['labels'] = $labels;
                    if ($showAsDisabled) {
                        $tasks[$taskIndex]['tasks'][$recordIndex]['showAsDisabled'] = 'disabled';
985
                    }
986
987
988
                } else {
                    $missingClasses[] = $tasks[$taskIndex]['tasks'][$recordIndex];
                    unset($tasks[$taskIndex]['tasks'][$recordIndex]);
989
990
                }
            }
991
        }
992

993
        $this->view->assign('tasks', $tasks);
994
        $this->view->assign('missingClasses', $missingClasses);
995
        $this->view->assign('moduleUri', $this->moduleUri);
996
        $this->view->assign('now', $this->getServerTime());
997
998
999
1000

        return $this->view->render();
    }