9f60bd92f99e9c154223ff26733a7f9e51828fd0
[Packages/TYPO3.CMS.git] / typo3 / sysext / scheduler / Classes / Controller / SchedulerModuleController.php
1 <?php
2 namespace TYPO3\CMS\Scheduler\Controller;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Utility\GeneralUtility;
18 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
19 use TYPO3\CMS\Backend\Utility\BackendUtility;
20 use TYPO3\CMS\Backend\Utility\IconUtility;
21 use TYPO3\CMS\Core\Messaging\FlashMessage;
22
23 /**
24 * Module 'TYPO3 Scheduler administration module' for the 'scheduler' extension.
25 *
26 * @author Fran├žois Suter <francois@typo3.org>
27 * @author Christian Jul Jensen <julle@typo3.org>
28 * @author Ingo Renner <ingo@typo3.org>
29 */
30 class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClass {
31
32 /**
33 * Back path to typo3 main dir
34 *
35 * @var string
36 */
37 public $backPath;
38
39 /**
40 * Array containing submitted data when editing or adding a task
41 *
42 * @var array
43 */
44 protected $submittedData = array();
45
46 /**
47 * Array containing all messages issued by the application logic
48 * Contains the error's severity and the message itself
49 *
50 * @var array
51 */
52 protected $messages = array();
53
54 /**
55 * @var string Key of the CSH file
56 */
57 protected $cshKey;
58
59 /**
60 * @var \TYPO3\CMS\Scheduler\Scheduler Local scheduler instance
61 */
62 protected $scheduler;
63
64 /**
65 * @var \TYPO3\CMS\Core\Page\PageRenderer
66 */
67 protected $pageRenderer;
68
69 /**
70 * @var string
71 */
72 protected $backendTemplatePath = '';
73
74 /**
75 * @var \TYPO3\CMS\Fluid\View\StandaloneView
76 */
77 protected $view;
78
79 /**
80 * The name of the module
81 *
82 * @var string
83 */
84 protected $moduleName = 'system_txschedulerM1';
85
86 /**
87 * @return \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController
88 */
89 public function __construct() {
90 $GLOBALS['LANG']->includeLLFile('EXT:scheduler/Resources/Private/Language/locallang.xlf');
91 $this->MCONF = array(
92 'name' => $this->moduleName,
93 );
94 $this->backPath = $GLOBALS['BACK_PATH'];
95 $this->cshKey = '_MOD_' . $this->moduleName;
96 $this->backendTemplatePath = ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Backend/SchedulerModule/';
97 $this->view = GeneralUtility::makeInstance(\TYPO3\CMS\Fluid\View\StandaloneView::class);
98 $this->view->getRequest()->setControllerExtensionName('scheduler');
99 }
100
101 /**
102 * Initializes the backend module
103 *
104 * @return void
105 */
106 public function init() {
107 parent::init();
108
109 // Initialize document
110 $this->doc = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Template\DocumentTemplate::class);
111 $this->doc->setModuleTemplate(ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Module.html');
112 $this->doc->backPath = $this->backPath;
113 $this->doc->bodyTagId = 'typo3-mod-php';
114 $this->doc->bodyTagAdditions = 'class="tx_scheduler_mod1"';
115
116 $this->pageRenderer = $this->doc->getPageRenderer();
117 $this->pageRenderer->addCssFile(ExtensionManagementUtility::extRelPath('scheduler') . 'Resources/Public/Styles/styles.css');
118
119 // Create scheduler instance
120 $this->scheduler = GeneralUtility::makeInstance(\TYPO3\CMS\Scheduler\Scheduler::class);
121 }
122
123 /**
124 * Adds items to the ->MOD_MENU array. Used for the function menu selector.
125 *
126 * @return void
127 */
128 public function menuConfig() {
129 $this->MOD_MENU = array(
130 'function' => array(
131 'scheduler' => $GLOBALS['LANG']->getLL('function.scheduler'),
132 'check' => $GLOBALS['LANG']->getLL('function.check'),
133 'info' => $GLOBALS['LANG']->getLL('function.info')
134 )
135 );
136 parent::menuConfig();
137 }
138
139 /**
140 * Main function of the module. Write the content to $this->content
141 *
142 * @return void
143 */
144 public function main() {
145 // Access check!
146 // The page will show only if user has admin rights
147 if ($GLOBALS['BE_USER']->isAdmin()) {
148 // Set the form
149 $this->doc->form = '<form name="tx_scheduler_form" id="tx_scheduler_form" method="post" action="">';
150 $this->pageRenderer->addInlineSetting('scheduler', 'runningIcon', ExtensionManagementUtility::extRelPath('scheduler') . 'Resources/Public/Images/status_running.png');
151
152 // Prepare main content
153 $this->content = $this->doc->header($GLOBALS['LANG']->getLL('function.' . $this->MOD_SETTINGS['function']));
154 $this->content .= $this->getModuleContent();
155 } else {
156 // If no access, only display the module's title
157 $this->content = $this->doc->header($GLOBALS['LANG']->getLL('title'));
158 $this->content .= $this->doc->spacer(5);
159 }
160 // Place content inside template
161 $content = $this->doc->moduleBody(array(), $this->getDocHeaderButtons(), $this->getTemplateMarkers());
162 // Renders the module page
163 $this->content = $this->doc->render($GLOBALS['LANG']->getLL('title'), $content);
164 }
165
166 /**
167 * Generate the module's content
168 *
169 * @return string HTML of the module's main content
170 */
171 protected function getModuleContent() {
172 $content = '';
173 $sectionTitle = '';
174 // Get submitted data
175 $this->submittedData = GeneralUtility::_GPmerged('tx_scheduler');
176 $this->submittedData['uid'] = (int)$this->submittedData['uid'];
177 // If a save command was submitted, handle saving now
178 if ($this->CMD === 'save' || $this->CMD === 'saveclose' || $this->CMD === 'savenew') {
179 $previousCMD = GeneralUtility::_GP('previousCMD');
180 // First check the submitted data
181 $result = $this->preprocessData();
182 // If result is ok, proceed with saving
183 if ($result) {
184 $this->saveTask();
185 if ($this->CMD === 'saveclose') {
186 // Unset command, so that default screen gets displayed
187 unset($this->CMD);
188 } elseif ($this->CMD === 'save') {
189 // After saving a "add form", return to edit
190 $this->CMD = 'edit';
191 } elseif ($this->CMD === 'savenew') {
192 // Unset submitted data, so that empty form gets displayed
193 unset($this->submittedData);
194 // After saving a "add/edit form", return to add
195 $this->CMD = 'add';
196 } else {
197 // Return to edit form
198 $this->CMD = $previousCMD;
199 }
200 } else {
201 $this->CMD = $previousCMD;
202 }
203 }
204
205 // Handle chosen action
206 switch ((string)$this->MOD_SETTINGS['function']) {
207 case 'scheduler':
208 $this->executeTasks();
209
210 switch ($this->CMD) {
211 case 'add':
212 case 'edit':
213 try {
214 // Try adding or editing
215 $content .= $this->editTaskAction();
216 $sectionTitle = $GLOBALS['LANG']->getLL('action.' . $this->CMD);
217 } catch (\Exception $e) {
218 // An exception may happen when the task to
219 // edit could not be found. In this case revert
220 // to displaying the list of tasks
221 // It can also happen when attempting to edit a running task
222 $content .= $this->listTasksAction();
223 }
224 break;
225 case 'delete':
226 $this->deleteTask();
227 $content .= $this->listTasksAction();
228 break;
229 case 'stop':
230 $this->stopTask();
231 $content .= $this->listTasksAction();
232 break;
233 case 'list':
234
235 default:
236 $content .= $this->listTasksAction();
237 }
238 break;
239
240 // Setup check screen
241 case 'check':
242 // @todo move check to the report module
243 $content .= $this->checkScreenAction();
244 break;
245
246 // Information screen
247 case 'info':
248 $content .= $this->infoScreenAction();
249 break;
250 }
251 // Wrap the content in a section
252 return $this->doc->section($sectionTitle, '<div class="tx_scheduler_mod1">' . $content . '</div>', FALSE, TRUE);
253 }
254
255 /**
256 * This method actually prints out the module's HTML content
257 *
258 * @return void
259 */
260 public function render() {
261 echo $this->content;
262 }
263
264 /**
265 * This method checks the status of the '_cli_scheduler' user
266 * It will differentiate between a non-existing user and an existing,
267 * but disabled user (as per enable fields)
268 *
269 * @return int -1 If user doesn't exist, 0 If user exist but not enabled, 1 If user exists and is enabled
270 */
271 protected function checkSchedulerUser() {
272 $schedulerUserStatus = -1;
273 // Assemble base WHERE clause
274 $where = 'username = \'_cli_scheduler\' AND admin = 0' . BackendUtility::deleteClause('be_users');
275 // Check if user exists at all
276 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('1', 'be_users', $where);
277 if ($GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
278 $schedulerUserStatus = 0;
279 $GLOBALS['TYPO3_DB']->sql_free_result($res);
280 // Check if user exists and is enabled
281 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('1', 'be_users', $where . BackendUtility::BEenableFields('be_users'));
282 if ($GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
283 $schedulerUserStatus = 1;
284 }
285 }
286 $GLOBALS['TYPO3_DB']->sql_free_result($res);
287 return $schedulerUserStatus;
288 }
289
290 /**
291 * This method creates the "cli_scheduler" BE user if it doesn't exist
292 *
293 * @return void
294 */
295 protected function createSchedulerUser() {
296 // Check _cli_scheduler user status
297 $checkUser = $this->checkSchedulerUser();
298 // Prepare default message
299 $message = $GLOBALS['LANG']->getLL('msg.userExists');
300 $severity = FlashMessage::WARNING;
301 // If the user does not exist, try creating it
302 if ($checkUser == -1) {
303 // Prepare necessary data for _cli_scheduler user creation
304 $password = md5(uniqid('scheduler', TRUE));
305 $data = array('be_users' => array('NEW' => array('username' => '_cli_scheduler', 'password' => $password, 'pid' => 0)));
306 /** @var $tcemain \TYPO3\CMS\Core\DataHandling\DataHandler */
307 $tcemain = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
308 $tcemain->stripslashes_values = 0;
309 $tcemain->start($data, array());
310 $tcemain->process_datamap();
311 // Check if a new uid was indeed generated (i.e. a new record was created)
312 // (counting TCEmain errors doesn't work as some failures don't report errors)
313 $numberOfNewIDs = count($tcemain->substNEWwithIDs);
314 if ($numberOfNewIDs == 1) {
315 $message = $GLOBALS['LANG']->getLL('msg.userCreated');
316 $severity = FlashMessage::OK;
317 } else {
318 $message = $GLOBALS['LANG']->getLL('msg.userNotCreated');
319 $severity = FlashMessage::ERROR;
320 }
321 }
322 $this->addMessage($message, $severity);
323 }
324
325 /**
326 * This method displays the result of a number of checks
327 * on whether the Scheduler is ready to run or running properly
328 *
329 * @return string Further information
330 */
331 protected function checkScreenAction() {
332 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'CheckScreen.html');
333
334 // First, check if _cli_scheduler user creation was requested
335 if ($this->CMD === 'user') {
336 $this->createSchedulerUser();
337 }
338
339 // Display information about last automated run, as stored in the system registry
340 /** @var $registry \TYPO3\CMS\Core\Registry */
341 $registry = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Registry::class);
342 $lastRun = $registry->get('tx_scheduler', 'lastRun');
343 if (!is_array($lastRun)) {
344 $message = $GLOBALS['LANG']->getLL('msg.noLastRun');
345 $severity = FlashMessage::WARNING;
346 } else {
347 if (empty($lastRun['end']) || empty($lastRun['start']) || empty($lastRun['type'])) {
348 $message = $GLOBALS['LANG']->getLL('msg.incompleteLastRun');
349 $severity = FlashMessage::WARNING;
350 } else {
351 $startDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['start']);
352 $startTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['start']);
353 $endDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['end']);
354 $endTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['end']);
355 $label = 'automatically';
356 if ($lastRun['type'] === 'manual') {
357 $label = 'manually';
358 }
359 $type = $GLOBALS['LANG']->getLL('label.' . $label);
360 $message = sprintf($GLOBALS['LANG']->getLL('msg.lastRun'), $type, $startDate, $startTime, $endDate, $endTime);
361 $severity = FlashMessage::INFO;
362 }
363 }
364 /** @var $flashMessage FlashMessage */
365 $flashMessage = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessage::class, $message, '', $severity);
366 $this->view->assign('lastRun', $flashMessage->render());
367
368 // Check CLI user
369 $checkUser = $this->checkSchedulerUser();
370 if ($checkUser == -1) {
371 $link = $GLOBALS['MCONF']['_'] . '&SET[function]=check&CMD=user';
372 $message = sprintf($GLOBALS['LANG']->getLL('msg.schedulerUserMissing'), htmlspecialchars($link));
373 $severity = FlashMessage::ERROR;
374 } elseif ($checkUser == 0) {
375 $message = $GLOBALS['LANG']->getLL('msg.schedulerUserFoundButDisabled');
376 $severity = FlashMessage::WARNING;
377 } else {
378 $message = $GLOBALS['LANG']->getLL('msg.schedulerUserFound');
379 $severity = FlashMessage::OK;
380 }
381 $flashMessage = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessage::class, $message, '', $severity);
382 $this->view->assign('cliUser', $flashMessage->render());
383
384 // Check if CLI script is executable or not
385 $script = PATH_typo3 . 'cli_dispatch.phpsh';
386 $this->view->assign('script', $script);
387
388 $isExecutable = FALSE;
389 // Skip this check if running Windows, as rights do not work the same way on this platform
390 // (i.e. the script will always appear as *not* executable)
391 if (TYPO3_OS === 'WIN') {
392 $isExecutable = TRUE;
393 } else {
394 $isExecutable = is_executable($script);
395 }
396 if ($isExecutable) {
397 $message = $GLOBALS['LANG']->getLL('msg.cliScriptExecutable');
398 $severity = FlashMessage::OK;
399 } else {
400 $message = $GLOBALS['LANG']->getLL('msg.cliScriptNotExecutable');
401 $severity = FlashMessage::ERROR;
402 }
403 $flashMessage = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessage::class, $message, '', $severity);
404 $this->view->assign('isExecutable', $flashMessage->render());
405
406 return $this->view->render();
407 }
408
409 /**
410 * This method gathers information about all available task classes and displays it
411 *
412 * @return string html
413 */
414 protected function infoScreenAction() {
415 $registeredClasses = self::getRegisteredClasses();
416 // No classes available, display information message
417 if (count($registeredClasses) == 0) {
418 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreenNoClasses.html');
419 return $this->view->render();
420 }
421
422 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreen.html');
423 $this->view->assign('registeredClasses', $registeredClasses);
424
425 return $this->view->render();
426 }
427
428 /**
429 * Renders the task progress bar.
430 *
431 * @param float $progress Task progress
432 * @return string Progress bar markup
433 */
434 protected function renderTaskProgressBar($progress) {
435 $progressText = $GLOBALS['LANG']->getLL('status.progress') . ':&nbsp;' . $progress . '%';
436 return '<div class="progress">'
437 . '<div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="' . $progress . '" aria-valuemin="0" aria-valuemax="100" style="width: ' . $progress . '%;">' . $progressText . '</div>'
438 . '</div>';
439 }
440
441 /**
442 * Delete a task from the execution queue
443 *
444 * @return void
445 */
446 protected function deleteTask() {
447 try {
448 // Try to fetch the task and delete it
449 $task = $this->scheduler->fetchTask($this->submittedData['uid']);
450 // If the task is currently running, it may not be deleted
451 if ($task->isExecutionRunning()) {
452 $this->addMessage($GLOBALS['LANG']->getLL('msg.maynotDeleteRunningTask'), FlashMessage::ERROR);
453 } else {
454 if ($this->scheduler->removeTask($task)) {
455 $GLOBALS['BE_USER']->writeLog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was deleted', array($task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()));
456 $this->addMessage($GLOBALS['LANG']->getLL('msg.deleteSuccess'));
457 } else {
458 $this->addMessage($GLOBALS['LANG']->getLL('msg.deleteError'), FlashMessage::ERROR);
459 }
460 }
461 } catch (\UnexpectedValueException $e) {
462 // The task could not be unserialized properly, simply delete the database record
463 $result = $GLOBALS['TYPO3_DB']->exec_DELETEquery('tx_scheduler_task', 'uid = ' . (int)$this->submittedData['uid']);
464 if ($result) {
465 $this->addMessage($GLOBALS['LANG']->getLL('msg.deleteSuccess'));
466 } else {
467 $this->addMessage($GLOBALS['LANG']->getLL('msg.deleteError'), FlashMessage::ERROR);
468 }
469 } catch (\OutOfBoundsException $e) {
470 // The task was not found, for some reason
471 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
472 }
473 }
474
475 /**
476 * Clears the registered running executions from the task
477 * Note that this doesn't actually stop the running script. It just unmarks
478 * all executions.
479 * @todo find a way to really kill the running task
480 *
481 * @return void
482 */
483 protected function stopTask() {
484 try {
485 // Try to fetch the task and stop it
486 $task = $this->scheduler->fetchTask($this->submittedData['uid']);
487 if ($task->isExecutionRunning()) {
488 // If the task is indeed currently running, clear marked executions
489 $result = $task->unmarkAllExecutions();
490 if ($result) {
491 $this->addMessage($GLOBALS['LANG']->getLL('msg.stopSuccess'));
492 } else {
493 $this->addMessage($GLOBALS['LANG']->getLL('msg.stopError'), FlashMessage::ERROR);
494 }
495 } else {
496 // The task is not running, nothing to unmark
497 $this->addMessage($GLOBALS['LANG']->getLL('msg.maynotStopNonRunningTask'), FlashMessage::WARNING);
498 }
499 } catch (\Exception $e) {
500 // The task was not found, for some reason
501 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
502 }
503 }
504
505 /**
506 * Return a form to add a new task or edit an existing one
507 *
508 * @return string HTML form to add or edit a task
509 */
510 protected function editTaskAction() {
511 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'EditTask.html');
512
513 $registeredClasses = self::getRegisteredClasses();
514 $registeredTaskGroups = self::getRegisteredTaskGroups();
515
516 $taskInfo = array();
517 $task = NULL;
518 $process = 'edit';
519
520 if ($this->submittedData['uid'] > 0) {
521 // If editing, retrieve data for existing task
522 try {
523 $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
524 // If there's a registered execution, the task should not be edited
525 if (!empty($taskRecord['serialized_executions'])) {
526 $this->addMessage($GLOBALS['LANG']->getLL('msg.maynotEditRunningTask'), FlashMessage::ERROR);
527 throw new \LogicException('Runnings tasks cannot not be edited', 1251232849);
528 }
529
530 // Get the task object
531 /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask */
532 $task = unserialize($taskRecord['serialized_task_object']);
533
534 // Set some task information
535 $taskInfo['disable'] = $taskRecord['disable'];
536 $taskInfo['description'] = $taskRecord['description'];
537 $taskInfo['task_group'] = $taskRecord['task_group'];
538
539 // Check that the task object is valid
540 if ($this->scheduler->isValidTaskObject($task)) {
541 // The task object is valid, process with fetching current data
542 $taskInfo['class'] = get_class($task);
543 // Get execution information
544 $taskInfo['start'] = (int)$task->getExecution()->getStart();
545 $taskInfo['end'] = (int)$task->getExecution()->getEnd();
546 $taskInfo['interval'] = $task->getExecution()->getInterval();
547 $taskInfo['croncmd'] = $task->getExecution()->getCronCmd();
548 $taskInfo['multiple'] = $task->getExecution()->getMultiple();
549 if (!empty($taskInfo['interval']) || !empty($taskInfo['croncmd'])) {
550 // Guess task type from the existing information
551 // If an interval or a cron command is defined, it's a recurring task
552 // @todo remove magic numbers for the type, use class constants instead
553 $taskInfo['type'] = 2;
554 $taskInfo['frequency'] = $taskInfo['interval'] ?: $taskInfo['croncmd'];
555 } else {
556 // It's not a recurring task
557 // Make sure interval and cron command are both empty
558 $taskInfo['type'] = 1;
559 $taskInfo['frequency'] = '';
560 $taskInfo['end'] = 0;
561 }
562 } else {
563 // The task object is not valid
564 // Issue error message
565 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.invalidTaskClassEdit'), get_class($task)), FlashMessage::ERROR);
566 // Initialize empty values
567 $taskInfo['start'] = 0;
568 $taskInfo['end'] = 0;
569 $taskInfo['frequency'] = '';
570 $taskInfo['multiple'] = FALSE;
571 $taskInfo['type'] = 1;
572 }
573 } catch (\OutOfBoundsException $e) {
574 // Add a message and continue throwing the exception
575 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
576 throw $e;
577 }
578 } else {
579 // If adding a new object, set some default values
580 $taskInfo['class'] = key($registeredClasses);
581 $taskInfo['type'] = 2;
582 $taskInfo['start'] = $GLOBALS['EXEC_TIME'];
583 $taskInfo['end'] = '';
584 $taskInfo['frequency'] = '';
585 $taskInfo['multiple'] = 0;
586 $process = 'add';
587 }
588
589 // If some data was already submitted, use it to override
590 // existing data
591 if (count($this->submittedData) > 0) {
592 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($taskInfo, $this->submittedData);
593 }
594
595 // Get the extra fields to display for each task that needs some
596 $allAdditionalFields = array();
597 if ($process === 'add') {
598 foreach ($registeredClasses as $class => $registrationInfo) {
599 if (!empty($registrationInfo['provider'])) {
600 /** @var $providerObject \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface */
601 $providerObject = GeneralUtility::getUserObj($registrationInfo['provider']);
602 if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
603 $additionalFields = $providerObject->getAdditionalFields($taskInfo, NULL, $this);
604 $allAdditionalFields = array_merge($allAdditionalFields, array($class => $additionalFields));
605 }
606 }
607 }
608 } else {
609 if (!empty($registeredClasses[$taskInfo['class']]['provider'])) {
610 $providerObject = GeneralUtility::getUserObj($registeredClasses[$taskInfo['class']]['provider']);
611 if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
612 $allAdditionalFields[$taskInfo['class']] = $providerObject->getAdditionalFields($taskInfo, $task, $this);
613 }
614 }
615 }
616
617 // Load necessary JavaScript
618 $this->pageRenderer->loadExtJS();
619 $this->pageRenderer->loadJquery();
620 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
621 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker');
622
623 // Define a style for hiding
624 // Some fields will be hidden when the task is not recurring
625 $style = '';
626 if ($taskInfo['type'] == 1) {
627 $style = ' style="display: none"';
628 }
629
630 // Start rendering the add/edit form
631 $this->view->assign('uid', htmlspecialchars($this->submittedData['uid']));
632 $this->view->assign('cmd', htmlspecialchars($this->CMD));
633
634 $table = array();
635
636 // Disable checkbox
637 $label = '<label for="task_disable">' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:disable') . '</label>';
638 $table[] = '<div id="task_disable_row" class="form-group">' .
639 BackendUtility::wrapInHelp($this->cshKey, 'task_disable', $label) .
640 '<input type="hidden" name="tx_scheduler[disable]" value="0" />
641 <input class="checkbox" type="checkbox" name="tx_scheduler[disable]" value="1" id="task_disable"' . ($taskInfo['disable'] == 1 ? ' checked="checked"' : '') . ' />
642 </div>';
643
644 // Task class selector
645 $label = '<label for="task_class">' . $GLOBALS['LANG']->getLL('label.class') . '</label>';
646
647 // On editing, don't allow changing of the task class, unless it was not valid
648 if ($this->submittedData['uid'] > 0 && !empty($taskInfo['class'])) {
649 $cell = '<div>' . $registeredClasses[$taskInfo['class']]['title'] . ' (' . $registeredClasses[$taskInfo['class']]['extension'] . ')</div>';
650 $cell .= '<input type="hidden" name="tx_scheduler[class]" id="task_class" value="' . htmlspecialchars($taskInfo['class']) . '" />';
651 } else {
652 $cell = '<select name="tx_scheduler[class]" id="task_class" class="form-control">';
653 // Group registered classes by classname
654 $groupedClasses = array();
655 foreach ($registeredClasses as $class => $classInfo) {
656 $groupedClasses[$classInfo['extension']][$class] = $classInfo;
657 }
658 ksort($groupedClasses);
659 // Loop on all grouped classes to display a selector
660 foreach ($groupedClasses as $extension => $class) {
661 $cell .= '<optgroup label="' . htmlspecialchars($extension) . '">';
662 foreach ($groupedClasses[$extension] as $class => $classInfo) {
663 $selected = $class == $taskInfo['class'] ? ' selected="selected"' : '';
664 $cell .= '<option value="' . $class . '"' . 'title="' . htmlspecialchars($classInfo['description']) . '"' . $selected . '>' . htmlspecialchars($classInfo['title']) . '</option>';
665 }
666 $cell .= '</optgroup>';
667 }
668 $cell .= '</select>';
669 }
670 $table[] = '<div id="task_class_row" class="form-group">' .
671 BackendUtility::wrapInHelp($this->cshKey, 'task_class', $label) .
672 $cell .
673 '</div>';
674
675 // Task type selector
676 $label = '<label for="task_type">' . $GLOBALS['LANG']->getLL('label.type') . '</label>';
677 $table[] = '<div id="task_type_row" class="form-group">' .
678 BackendUtility::wrapInHelp($this->cshKey, 'task_type', $label) .
679 '<select name="tx_scheduler[type]" id="task_type" class="form-control">
680 <option value="1"' . ($taskInfo['type'] == 1 ? ' selected="selected"' : '') . '>' . $GLOBALS['LANG']->getLL('label.type.single') . '</option>
681 <option value="2"' . ($taskInfo['type'] == 2 ? ' selected="selected"' : '') . '>' . $GLOBALS['LANG']->getLL('label.type.recurring') . '</option>
682 </select>
683 </div>';
684
685 // Task group selector
686 $label = '<label for="task_group">' . $GLOBALS['LANG']->getLL('label.group') . '</label>';
687 $cell = '<select name="tx_scheduler[task_group]" id="task_class" class="form-control">';
688
689 // Loop on all groups to display a selector
690 $cell .= '<option value="0" title=""></option>';
691 foreach ($registeredTaskGroups as $taskGroup) {
692 $selected = $taskGroup['uid'] == $taskInfo['task_group'] ? ' selected="selected"' : '';
693 $cell .= '<option value="' . $taskGroup['uid'] . '"' . 'title="';
694 $cell .= htmlspecialchars($taskGroup['groupName']) . '"' . $selected . '>';
695 $cell .= htmlspecialchars($taskGroup['groupName']) . '</option>';
696 }
697 $cell .= '</select>';
698
699 $table[] = '<div id="task_group_row" class="form-group">' .
700 BackendUtility::wrapInHelp($this->cshKey, 'task_group', $label) .
701 $cell .
702 '</div>';
703
704 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '%H:%M %m-%d-%Y' : '%H:%M %d-%m-%Y';
705
706 $label = '<label for="tceforms-datetimefield-task_start">' . BackendUtility::wrapInHelp($this->cshKey, 'task_start', $GLOBALS['LANG']->getLL('label.start')) . '</label>';
707 $value = ($taskInfo['start'] > 0 ? strftime($dateFormat, $taskInfo['start']) : '');
708 $table[] = '<div class="form-group">' .
709 $label .
710 '<div class="input-group date t3js-datetimepicker" data-date-offset="0" id="tceforms-datetimefield-task_start_row-wrapper">' .
711 '<input name="tx_scheduler[start]_hr" value="' . $value . '" class="form-control datetime" type="text" ' .
712 'id="tceforms-datetimefield-task_start_row">' .
713 '<input name="tx_scheduler[start]" value="' . $taskInfo['start'] . '" type="hidden">' .
714 '<span class="input-group-addon datepickerbutton"><span class="fa fa-calendar"></span></span>' .
715 '</div>' .
716 '</div>';
717
718
719 // End date/time field
720 // NOTE: datetime fields need a special id naming scheme
721 $value = ($taskInfo['end'] > 0 ? strftime($dateFormat, $taskInfo['end']) : '');
722 $label = '<label for="tceforms-datetimefield-task_end">' . $GLOBALS['LANG']->getLL('label.end') . '</label>';
723 $table[] = '<div class="form-group">' .
724 BackendUtility::wrapInHelp($this->cshKey, 'task_end', $label) .
725 '<div class="input-group date t3js-datetimepicker" data-date-offset="0" id="tceforms-datetimefield-task_end_row-wrapper">' .
726 '<input name="tx_scheduler[end]_hr" value="' . $value . '" class="form-control datetime" type="text" ' .
727 'id="tceforms-datetimefield-task_end_row">' .
728 '<input name="tx_scheduler[end]" value="' . $taskInfo['end'] . '" type="hidden">' .
729 '<span class="input-group-addon datepickerbutton"><span class="fa fa-calendar"></span></span>' .
730 '</div>' .
731 '</div>';
732
733 // Frequency input field
734 $label = '<label for="task_frequency">' . $GLOBALS['LANG']->getLL('label.frequency.long') . '</label>';
735 $table[] = '<div id="task_frequency_row" class="form-group">' .
736 BackendUtility::wrapInHelp($this->cshKey, 'task_frequency', $label) .
737 '<input type="text" name="tx_scheduler[frequency]" class="form-control" id="task_frequency" value="' . htmlspecialchars($taskInfo['frequency']) . '" />
738 </div>';
739
740 // Multiple execution selector
741 $label = '<label for="task_multiple">' . $GLOBALS['LANG']->getLL('label.parallel.long') . '</label>';
742 $table[] = '<div id="task_multiple_row" class="form-group">' .
743 BackendUtility::wrapInHelp($this->cshKey, 'task_multiple', $label) .
744 '<input type="hidden" name="tx_scheduler[multiple]" value="0" />
745 <input class="checkbox" type="checkbox" name="tx_scheduler[multiple]" value="1" id="task_multiple"' . ($taskInfo['multiple'] == 1 ? ' checked="checked"' : '') . ' />
746 </div>';
747
748 // Description
749 $label = '<label for="task_description">' . $GLOBALS['LANG']->getLL('label.description') . '</label>';
750 $table[] = '<div id="task_description_row" class="form-group">' .
751 BackendUtility::wrapInHelp($this->cshKey, 'task_description', $label) .
752 '<textarea class="form-control" name="tx_scheduler[description]">' . htmlspecialchars($taskInfo['description']) . '</textarea>
753 </div>';
754
755 // Display additional fields
756 foreach ($allAdditionalFields as $class => $fields) {
757 if ($class == $taskInfo['class']) {
758 $additionalFieldsStyle = '';
759 } else {
760 $additionalFieldsStyle = ' style="display: none"';
761 }
762 // Add each field to the display, if there are indeed any
763 if (isset($fields) && is_array($fields)) {
764 foreach ($fields as $fieldID => $fieldInfo) {
765 $label = '<label for="' . $fieldID . '">' . $GLOBALS['LANG']->sL($fieldInfo['label']) . '</label>';
766 $htmlClassName = strtolower(str_replace('\\', '-', $class));
767 $table[] = '<div id="' . $fieldID . '_row"' . $additionalFieldsStyle . ' class="form-group extraFields extra_fields_' . $htmlClassName . '">' .
768 BackendUtility::wrapInHelp($fieldInfo['cshKey'], $fieldInfo['cshLabel'], $label) .
769 '<div>' . $fieldInfo['code'] . '</div>
770 </div>';
771 }
772 }
773 }
774
775 $this->view->assign('table', implode(LF, $table));
776
777 // Server date time
778 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e';
779 $this->view->assign('now', date($dateFormat) . ', GMT ' . date('P') . ')');
780
781 return $this->view->render();
782 }
783
784 /**
785 * Execute all selected tasks
786 *
787 * @return void
788 */
789 protected function executeTasks() {
790 // Make sure next automatic scheduler-run is scheduled
791 if (GeneralUtility::_POST('go') !== NULL) {
792 $this->scheduler->scheduleNextSchedulerRunUsingAtDaemon();
793 }
794 // Continue if some elements have been chosen for execution
795 if (isset($this->submittedData['execute']) && count($this->submittedData['execute']) > 0) {
796 // Get list of registered classes
797 $registeredClasses = self::getRegisteredClasses();
798 // Loop on all selected tasks
799 foreach ($this->submittedData['execute'] as $uid) {
800 try {
801 // Try fetching the task
802 $task = $this->scheduler->fetchTask($uid);
803 $class = get_class($task);
804 $name = $registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')';
805 // Now try to execute it and report on outcome
806 try {
807 $result = $this->scheduler->executeTask($task);
808 if ($result) {
809 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.executed'), $name));
810 } else {
811 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.notExecuted'), $name), FlashMessage::ERROR);
812 }
813 } catch (\Exception $e) {
814 // An exception was thrown, display its message as an error
815 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.executionFailed'), $name, $e->getMessage()), FlashMessage::ERROR);
816 }
817 } catch (\OutOfBoundsException $e) {
818 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.taskNotFound'), $uid), FlashMessage::ERROR);
819 } catch (\UnexpectedValueException $e) {
820 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.executionFailed'), $uid, $e->getMessage()), FlashMessage::ERROR);
821 }
822 }
823 // Record the run in the system registry
824 $this->scheduler->recordLastRun('manual');
825 // Make sure to switch to list view after execution
826 $this->CMD = 'list';
827 }
828 }
829
830 /**
831 * Assemble display of list of scheduled tasks
832 *
833 * @return string Table of pending tasks
834 */
835 protected function listTasksAction() {
836 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasks.html');
837
838 // Define display format for dates
839 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
840
841 // Get list of registered classes
842 $registeredClasses = self::getRegisteredClasses();
843 // Get list of registered task groups
844 $registeredTaskGroups = self::getRegisteredTaskGroups();
845
846 // add an empty entry for non-grouped tasks
847 // add in front of list
848 array_unshift($registeredTaskGroups, array('uid' => 0, 'groupName' => ''));
849
850 // Get all registered tasks
851 // Just to get the number of entries
852 $query = array(
853 'SELECT' => '*',
854 'FROM' => 'tx_scheduler_task',
855 'WHERE' => '1=1',
856 'ORDERBY' => ''
857 );
858 $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($query);
859 $numRows = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
860 $GLOBALS['TYPO3_DB']->sql_free_result($res);
861
862 // No tasks defined, display information message
863 if ($numRows == 0) {
864 $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasksNoTasks.html');
865
866 /** @var $flashMessage FlashMessage */
867 $flashMessage = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessage::class, $GLOBALS['LANG']->getLL('msg.noTasks'), '', FlashMessage::INFO);
868 $this->view->assign('message', $flashMessage->render());
869 return $this->view->render();
870 } else {
871 $this->pageRenderer->loadJquery();
872 $this->pageRenderer->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
873 $table = array();
874 // Header row
875 $table[] = '<thead><tr>
876 <td><a href="#" id="checkall" title="' . $GLOBALS['LANG']->getLL('label.checkAll', TRUE) . '" class="icon">' . IconUtility::getSpriteIcon('actions-document-select') . '</a></td>
877 <td>&nbsp;</td>
878 <td>' . $GLOBALS['LANG']->getLL('label.id', TRUE). '</td>
879 <td colspan="2">' . $GLOBALS['LANG']->getLL('task', TRUE). '</td>
880 <td>' . $GLOBALS['LANG']->getLL('label.type', TRUE). '</td>
881 <td>' . $GLOBALS['LANG']->getLL('label.frequency', TRUE). '</td>
882 <td>' . $GLOBALS['LANG']->getLL('label.parallel', TRUE). '</td>
883 <td>' . $GLOBALS['LANG']->getLL('label.lastExecution', TRUE). '</td>
884 <td>' . $GLOBALS['LANG']->getLL('label.nextExecution', TRUE). '</td>
885 </tr></thead>';
886
887 foreach ($registeredTaskGroups as $taskGroup) {
888 $query = array(
889 'SELECT' => '*',
890 'FROM' => 'tx_scheduler_task',
891 'WHERE' => 'task_group=' . $taskGroup['uid'],
892 'ORDERBY' => 'nextexecution'
893 );
894
895 $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($query);
896 $numRows = $GLOBALS['TYPO3_DB']->sql_num_rows($res);
897
898 if ($numRows === 0) {
899 continue;
900 }
901
902 if ($taskGroup['groupName'] !== '') {
903 $groupText = '<strong>' . htmlspecialchars($taskGroup['groupName']) . '</strong>';
904 if (!empty($taskGroup['description'])) {
905 $groupText .= '<br />' . nl2br(htmlspecialchars($taskGroup['description']));
906 }
907 $table[] = '<tr><td colspan="10">' . $groupText . '</td></tr>';
908 }
909
910 // Loop on all tasks
911 while ($schedulerRecord = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
912 // Define action icons
913 $editAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&CMD=edit&tx_scheduler[uid]=' . $schedulerRecord['uid'] . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:edit', TRUE) . '" class="icon">' .
914 IconUtility::getSpriteIcon('actions-document-open') . '</a>';
915 $deleteAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&CMD=delete&tx_scheduler[uid]=' . $schedulerRecord['uid'] . '" onclick="return confirm(\'' . $GLOBALS['LANG']->getLL('msg.delete') . '\');" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:delete', TRUE) . '" class="icon">' .
916 IconUtility::getSpriteIcon('actions-edit-delete') . '</a>';
917 $stopAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&CMD=stop&tx_scheduler[uid]=' . $schedulerRecord['uid'] . '" onclick="return confirm(\'' . $GLOBALS['LANG']->getLL('msg.stop') . '\');" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:stop', TRUE) . '" class="icon">' .
918 '<img ' . IconUtility::skinImg($this->backPath, (ExtensionManagementUtility::extRelPath('scheduler') . '/Resources/Public/Images/stop.png')) . ' alt="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:stop') . '" /></a>';
919 $runAction = '<a href="' . $GLOBALS['MCONF']['_'] . '&tx_scheduler[execute][]=' . $schedulerRecord['uid'] . '" title="' . $GLOBALS['LANG']->getLL('action.run_task') . '" class="icon">' .
920 IconUtility::getSpriteIcon('extensions-scheduler-run-task') . '</a>';
921
922 // Define some default values
923 $lastExecution = '-';
924 $isRunning = FALSE;
925 $showAsDisabled = FALSE;
926 $executionStatus = 'scheduled';
927 $executionStatusOutput = '';
928 $name = '';
929 $nextDate = '-';
930 $execType = '-';
931 $frequency = '-';
932 $multiple = '-';
933 $startExecutionElement = '&nbsp;';
934 // Restore the serialized task and pass it a reference to the scheduler object
935 /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask|\TYPO3\CMS\Scheduler\ProgressProviderInterface */
936 $task = unserialize($schedulerRecord['serialized_task_object']);
937 $class = get_class($task);
938 if ($class === '__PHP_Incomplete_Class' && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) {
939 $class = $matches['classname'];
940 }
941 // Assemble information about last execution
942 $context = '';
943 if (!empty($schedulerRecord['lastexecution_time'])) {
944 $lastExecution = date($dateFormat, $schedulerRecord['lastexecution_time']);
945 if ($schedulerRecord['lastexecution_context'] == 'CLI') {
946 $context = $GLOBALS['LANG']->getLL('label.cron');
947 } else {
948 $context = $GLOBALS['LANG']->getLL('label.manual');
949 }
950 $lastExecution .= ' (' . $context . ')';
951 }
952
953 if ($this->scheduler->isValidTaskObject($task)) {
954 // The task object is valid
955 $name = '<div class="title">' . htmlspecialchars($registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')') . '</div>';
956 $additionalInformation = $task->getAdditionalInformation();
957 if ($task instanceof \TYPO3\CMS\Scheduler\ProgressProviderInterface) {
958 $progress = round(floatval($task->getProgress()), 2);
959 $name .= $this->renderTaskProgressBar($progress);
960 }
961 if (!empty($additionalInformation)) {
962 $name .= '<div class="additional-information">[' . htmlspecialchars($additionalInformation) . ']</div>';
963 }
964 // Check if task currently has a running execution
965 if (!empty($schedulerRecord['serialized_executions'])) {
966 $isRunning = TRUE;
967 $executionStatus = 'running';
968 }
969
970 // Prepare display of next execution date
971 // If task is currently running, date is not displayed (as next hasn't been calculated yet)
972 // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
973 if ($isRunning || $schedulerRecord['disable'] == 1) {
974 $nextDate = '-';
975 } else {
976 $nextDate = date($dateFormat, $schedulerRecord['nextexecution']);
977 if (empty($schedulerRecord['nextexecution'])) {
978 $nextDate = $GLOBALS['LANG']->getLL('none');
979 } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
980 // Next execution is overdue, highlight date
981 $nextDate = '<span class="late" title="' . $GLOBALS['LANG']->getLL('status.legend.scheduled') . '">' . $nextDate . '</span>';
982 $executionStatus = 'late';
983 }
984 }
985 // Get execution type
986 if ($task->getExecution()->getInterval() == 0 && $task->getExecution()->getCronCmd() == '') {
987 $execType = $GLOBALS['LANG']->getLL('label.type.single');
988 $frequency = '-';
989 } else {
990 $execType = $GLOBALS['LANG']->getLL('label.type.recurring');
991 if ($task->getExecution()->getCronCmd() == '') {
992 $frequency = $task->getExecution()->getInterval();
993 } else {
994 $frequency = $task->getExecution()->getCronCmd();
995 }
996 }
997 // Get multiple executions setting
998 if ($task->getExecution()->getMultiple()) {
999 $multiple = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:yes');
1000 } else {
1001 $multiple = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:no');
1002 }
1003 // Define checkbox
1004 $startExecutionElement = '<input type="checkbox" name="tx_scheduler[execute][]" value="' . $schedulerRecord['uid'] . '" id="task_' . $schedulerRecord['uid'] . '" class="checkboxes" />';
1005
1006 $actions = $editAction . $deleteAction;
1007
1008 // Check the disable status
1009 // Row is shown dimmed if task is disabled, unless it is still running
1010 if ($schedulerRecord['disable'] == 1 && !$isRunning) {
1011 $showAsDisabled = TRUE;
1012 $executionStatus = 'disabled';
1013 }
1014
1015 // Show no action links (edit, delete) if task is running
1016 if ($isRunning) {
1017 $actions = $stopAction;
1018 } else {
1019 $actions .= $runAction;
1020 }
1021
1022 // Check if the last run failed
1023 $failureOutput = '';
1024 if (!empty($schedulerRecord['lastexecution_failure'])) {
1025 // Try to get the stored exception object
1026 /** @var $exception \Exception */
1027 $exception = unserialize($schedulerRecord['lastexecution_failure']);
1028 // If the exception could not be unserialized, issue a default error message
1029 if ($exception === FALSE || $exception instanceof \__PHP_Incomplete_Class) {
1030 $failureDetail = $GLOBALS['LANG']->getLL('msg.executionFailureDefault');
1031 } else {
1032 $failureDetail = sprintf($GLOBALS['LANG']->getLL('msg.executionFailureReport'), $exception->getCode(), $exception->getMessage());
1033 }
1034 $failureOutput = ' <img ' . IconUtility::skinImg(ExtensionManagementUtility::extRelPath('scheduler'), 'Resources/Public/Images/status_failure.png') . ' alt="' . htmlspecialchars($GLOBALS['LANG']->getLL('status.failure')) . '" title="' . htmlspecialchars($failureDetail) . '" />';
1035 }
1036 // Format the execution status,
1037 // including failure feedback, if any
1038 $executionStatusOutput = '<img ' . IconUtility::skinImg(ExtensionManagementUtility::extRelPath('scheduler'), ('Resources/Public/Images/status_' . $executionStatus . '.png')) . ' id="executionstatus_' . $schedulerRecord['uid'] . '" alt="' . htmlspecialchars($GLOBALS['LANG']->getLL(('status.' . $executionStatus))) . '" title="' . htmlspecialchars($GLOBALS['LANG']->getLL(('status.legend.' . $executionStatus))) . '" />' . $failureOutput;
1039 if ($schedulerRecord['description'] !== '') {
1040 $taskName = '<span title="' . htmlspecialchars($schedulerRecord['description']) . '">' . $name . '</span>';
1041 } else {
1042 $taskName = $name;
1043 }
1044
1045 $table[] = '<tr class="' . ($showAsDisabled ? 'disabled' : '') . '">
1046 <td>' . $startExecutionElement . '</td>
1047 <td class="right">' . $actions . '</td>
1048 <td class="right">' . $schedulerRecord['uid'] . '</td>
1049 <td>' . $executionStatusOutput . '</td>
1050 <td>' . $taskName . '</td>
1051 <td>' . $execType . '</td>
1052 <td>' . $frequency . '</td>
1053 <td>' . $multiple . '</td>
1054 <td>' . $lastExecution . '</td>
1055 <td>' . $nextDate . '</td>
1056 </tr>';
1057 } else {
1058 // The task object is not valid
1059 // Prepare to issue an error
1060 /** @var $flashMessage FlashMessage */
1061 $flashMessage = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessage::class, sprintf($GLOBALS['LANG']->getLL('msg.invalidTaskClass'), $class), '', FlashMessage::ERROR);
1062 $executionStatusOutput = $flashMessage->render();
1063 $table[] = '<tr>
1064 <td>' . $startExecutionElement . '</td>
1065 <td class="right">' . $deleteAction . '</td>
1066 <td class="right">' . $schedulerRecord['uid'] . '</td>
1067 <td colspan="6">' . $executionStatusOutput . '</td>
1068 </tr>';
1069 }
1070 }
1071 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1072 }
1073
1074 $this->view->assign('table', '<table class="t3-table">' . implode(LF, $table) . '</table>');
1075
1076 // Server date time
1077 $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e';
1078 $this->view->assign('now', date($dateFormat) . ', GMT ' . date('P') . ')');
1079 }
1080
1081 return $this->view->render();
1082 }
1083
1084 /**
1085 * Saves a task specified in the backend form to the database
1086 *
1087 * @return void
1088 */
1089 protected function saveTask() {
1090 // If a task is being edited fetch old task data
1091 if (!empty($this->submittedData['uid'])) {
1092 try {
1093 $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
1094 /** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask */
1095 $task = unserialize($taskRecord['serialized_task_object']);
1096 } catch (\OutOfBoundsException $e) {
1097 // If the task could not be fetched, issue an error message
1098 // and exit early
1099 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
1100 return;
1101 }
1102 // Register single execution
1103 if ($this->submittedData['type'] == 1) {
1104 $task->registerSingleExecution($this->submittedData['start']);
1105 } else {
1106 if (!empty($this->submittedData['croncmd'])) {
1107 // Definition by cron-like syntax
1108 $interval = 0;
1109 $cronCmd = $this->submittedData['croncmd'];
1110 } else {
1111 // Definition by interval
1112 $interval = $this->submittedData['interval'];
1113 $cronCmd = '';
1114 }
1115 // Register recurring execution
1116 $task->registerRecurringExecution($this->submittedData['start'], $interval, $this->submittedData['end'], $this->submittedData['multiple'], $cronCmd);
1117 }
1118 // Set disable flag
1119 $task->setDisabled($this->submittedData['disable']);
1120 // Set description
1121 $task->setDescription($this->submittedData['description']);
1122 // Set task group
1123 $task->setTaskGroup($this->submittedData['task_group']);
1124 // Save additional input values
1125 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1126 /** @var $providerObject \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface */
1127 $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1128 if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1129 $providerObject->saveAdditionalFields($this->submittedData, $task);
1130 }
1131 }
1132 // Save to database
1133 $result = $this->scheduler->saveTask($task);
1134 if ($result) {
1135 $GLOBALS['BE_USER']->writeLog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was updated', array($task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()));
1136 $this->addMessage($GLOBALS['LANG']->getLL('msg.updateSuccess'));
1137 } else {
1138 $this->addMessage($GLOBALS['LANG']->getLL('msg.updateError'), FlashMessage::ERROR);
1139 }
1140 } else {
1141 // A new task is being created
1142 // Create an instance of chosen class
1143 $task = GeneralUtility::makeInstance($this->submittedData['class']);
1144 if ($this->submittedData['type'] == 1) {
1145 // Set up single execution
1146 $task->registerSingleExecution($this->submittedData['start']);
1147 } else {
1148 // Set up recurring execution
1149 $task->registerRecurringExecution($this->submittedData['start'], $this->submittedData['interval'], $this->submittedData['end'], $this->submittedData['multiple'], $this->submittedData['croncmd']);
1150 }
1151 // Save additional input values
1152 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1153 /** @var $providerObject \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface */
1154 $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1155 if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1156 $providerObject->saveAdditionalFields($this->submittedData, $task);
1157 }
1158 }
1159 // Set disable flag
1160 $task->setDisabled($this->submittedData['disable']);
1161 // Set description
1162 $task->setDescription($this->submittedData['description']);
1163 // Set description
1164 $task->setTaskGroup($this->submittedData['task_group']);
1165 // Add to database
1166 $result = $this->scheduler->addTask($task);
1167 if ($result) {
1168 $GLOBALS['BE_USER']->writeLog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was added', array($task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()));
1169 $this->addMessage($GLOBALS['LANG']->getLL('msg.addSuccess'));
1170
1171 // set the uid of the just created task so that we
1172 // can continue editing after initial saving
1173 $this->submittedData['uid'] = $task->getTaskUid();
1174 } else {
1175 $this->addMessage($GLOBALS['LANG']->getLL('msg.addError'), FlashMessage::ERROR);
1176 }
1177 }
1178 }
1179
1180 /*************************
1181 *
1182 * INPUT PROCESSING UTILITIES
1183 *
1184 *************************/
1185 /**
1186 * Checks the submitted data and performs some pre-processing on it
1187 *
1188 * @return bool TRUE if everything was ok, FALSE otherwise
1189 */
1190 protected function preprocessData() {
1191 $result = TRUE;
1192 // Validate id
1193 $this->submittedData['uid'] = empty($this->submittedData['uid']) ? 0 : (int)$this->submittedData['uid'];
1194 // Validate selected task class
1195 if (!class_exists($this->submittedData['class'])) {
1196 $this->addMessage($GLOBALS['LANG']->getLL('msg.noTaskClassFound'), FlashMessage::ERROR);
1197 }
1198 // Check start date
1199 if (empty($this->submittedData['start'])) {
1200 $this->addMessage($GLOBALS['LANG']->getLL('msg.noStartDate'), FlashMessage::ERROR);
1201 $result = FALSE;
1202 } else {
1203 try {
1204 $this->submittedData['start'] = (int)$this->submittedData['start'];
1205 } catch (\Exception $e) {
1206 $this->addMessage($GLOBALS['LANG']->getLL('msg.invalidStartDate'), FlashMessage::ERROR);
1207 $result = FALSE;
1208 }
1209 }
1210 // Check end date, if recurring task
1211 if ($this->submittedData['type'] == 2 && !empty($this->submittedData['end'])) {
1212 try {
1213 $this->submittedData['end'] = (int)$this->submittedData['end'];
1214 if ($this->submittedData['end'] < $this->submittedData['start']) {
1215 $this->addMessage($GLOBALS['LANG']->getLL('msg.endDateSmallerThanStartDate'), FlashMessage::ERROR);
1216 $result = FALSE;
1217 }
1218 } catch (\Exception $e) {
1219 $this->addMessage($GLOBALS['LANG']->getLL('msg.invalidEndDate'), FlashMessage::ERROR);
1220 $result = FALSE;
1221 }
1222 }
1223 // Set default values for interval and cron command
1224 $this->submittedData['interval'] = 0;
1225 $this->submittedData['croncmd'] = '';
1226 // Check type and validity of frequency, if recurring
1227 if ($this->submittedData['type'] == 2) {
1228 $frequency = trim($this->submittedData['frequency']);
1229 if (empty($frequency)) {
1230 // Empty frequency, not valid
1231 $this->addMessage($GLOBALS['LANG']->getLL('msg.noFrequency'), FlashMessage::ERROR);
1232 $result = FALSE;
1233 } else {
1234 $cronErrorCode = 0;
1235 $cronErrorMessage = '';
1236 // Try interpreting the cron command
1237 try {
1238 \TYPO3\CMS\Scheduler\CronCommand\NormalizeCommand::normalize($frequency);
1239 $this->submittedData['croncmd'] = $frequency;
1240 } catch (\Exception $e) {
1241 // Store the exception's result
1242 $cronErrorMessage = $e->getMessage();
1243 $cronErrorCode = $e->getCode();
1244 // Check if the frequency is a valid number
1245 // If yes, assume it is a frequency in seconds, and unset cron error code
1246 if (is_numeric($frequency)) {
1247 $this->submittedData['interval'] = (int)$frequency;
1248 unset($cronErrorCode);
1249 }
1250 }
1251 // If there's a cron error code, issue validation error message
1252 if (!empty($cronErrorCode)) {
1253 $this->addMessage(sprintf($GLOBALS['LANG']->getLL('msg.frequencyError'), $cronErrorMessage, $cronErrorCode), FlashMessage::ERROR);
1254 $result = FALSE;
1255 }
1256 }
1257 }
1258 // Validate additional input fields
1259 if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1260 /** @var $providerObject \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface */
1261 $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1262 if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1263 // The validate method will return TRUE if all went well, but that must not
1264 // override previous FALSE values => AND the returned value with the existing one
1265 $result &= $providerObject->validateAdditionalFields($this->submittedData, $this);
1266 }
1267 }
1268 return $result;
1269 }
1270
1271 /**
1272 * This method checks whether the given string can be considered a valid date or not
1273 * Allowed values are anything that matches natural language (see PHP function strtotime())
1274 * or TYPO3's date syntax: HH:ii yyyy-mm-dd
1275 * If the string is a valid date, the corresponding timestamp is returned.
1276 * Otherwise an exception is thrown
1277 *
1278 * @param string $string String to check
1279 * @return int Unix timestamp
1280 * @throws \InvalidArgumentException
1281 * @deprecated since TYPO3 CMS 7, will be removed in CMS 8, as the unified datetime picker with a separate timestamp field is used.
1282 */
1283 public function checkDate($string) {
1284 GeneralUtility::logDeprecatedFunction();
1285 // Try with strtotime
1286 $timestamp = strtotime($string);
1287 // That failed. Try TYPO3's standard date/time input format
1288 if ($timestamp === FALSE) {
1289 // Split time and date
1290 $dateParts = GeneralUtility::trimExplode(' ', $string, TRUE);
1291 // Proceed if there are indeed two parts
1292 // Extract each component of date and time
1293 if (count($dateParts) == 2) {
1294 list($time, $date) = $dateParts;
1295 list($hour, $minutes) = GeneralUtility::trimExplode(':', $time, TRUE);
1296 list($day, $month, $year) = GeneralUtility::trimExplode('-', $date, TRUE);
1297 // Get a timestamp from all these parts
1298 $timestamp = @mktime($hour, $minutes, 0, $month, $day, $year);
1299 }
1300 // If the timestamp is still FALSE, throw an exception
1301 if ($timestamp === FALSE) {
1302 throw new \InvalidArgumentException('"' . $string . '" seems not to be a correct date.', 1294587694);
1303 }
1304 }
1305 return $timestamp;
1306 }
1307
1308 /*************************
1309 *
1310 * APPLICATION LOGIC UTILITIES
1311 *
1312 *************************/
1313 /**
1314 * This method is used to add a message to the internal queue
1315 *
1316 * @param string $message The message itself
1317 * @param int $severity Message level (according to FlashMessage class constants)
1318 * @return void
1319 */
1320 public function addMessage($message, $severity = FlashMessage::OK) {
1321 $flashMessage = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessage::class, $message, '', $severity);
1322 /** @var $flashMessageService FlashMessageService */
1323 $flashMessageService = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Messaging\FlashMessageService::class);
1324 /** @var $defaultFlashMessageQueue FlashMessageQueue */
1325 $defaultFlashMessageQueue = $flashMessageService->getMessageQueueByIdentifier();
1326 $defaultFlashMessageQueue->enqueue($flashMessage);
1327 }
1328
1329 /**
1330 * This method fetches a list of all classes that have been registered with the Scheduler
1331 * For each item the following information is provided, as an associative array:
1332 *
1333 * ['extension'] => Key of the extension which provides the class
1334 * ['filename'] => Path to the file containing the class
1335 * ['title'] => String (possibly localized) containing a human-readable name for the class
1336 * ['provider'] => Name of class that implements the interface for additional fields, if necessary
1337 *
1338 * The name of the class itself is used as the key of the list array
1339 *
1340 * @return array List of registered classes
1341 */
1342 static protected function getRegisteredClasses() {
1343 $list = array();
1344 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'])) {
1345 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] as $class => $registrationInformation) {
1346 $title = isset($registrationInformation['title']) ? $GLOBALS['LANG']->sL($registrationInformation['title']) : '';
1347 $description = isset($registrationInformation['description']) ? $GLOBALS['LANG']->sL($registrationInformation['description']) : '';
1348 $list[$class] = array(
1349 'extension' => $registrationInformation['extension'],
1350 'title' => $title,
1351 'description' => $description,
1352 'provider' => isset($registrationInformation['additionalFields']) ? $registrationInformation['additionalFields'] : ''
1353 );
1354 }
1355 }
1356 return $list;
1357 }
1358
1359 /**
1360 * This method fetches list of all group that have been registered with the Scheduler
1361 *
1362 * @return array List of registered groups
1363 */
1364 static protected function getRegisteredTaskGroups() {
1365 $list = array();
1366
1367 // Get all registered task groups
1368 $query = array(
1369 'SELECT' => '*',
1370 'FROM' => 'tx_scheduler_task_group',
1371 'WHERE' => '1=1' . BackendUtility::BEenableFields('tx_scheduler_task_group'),
1372 'ORDERBY' => 'sorting'
1373 );
1374 $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($query);
1375
1376 while (($groupRecord = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) !== FALSE) {
1377 $list[] = $groupRecord;
1378 }
1379 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1380
1381 return $list;
1382 }
1383
1384 /*************************
1385 *
1386 * RENDERING UTILITIES
1387 *
1388 *************************/
1389 /**
1390 * Gets the filled markers that are used in the HTML template.
1391 *
1392 * @return array The filled marker array
1393 */
1394 protected function getTemplateMarkers() {
1395 return array(
1396 'CSH' => BackendUtility::wrapInHelp('_MOD_system_txschedulerM1', ''),
1397 'FUNC_MENU' => $this->getFunctionMenu(),
1398 'CONTENT' => $this->content,
1399 'TITLE' => $GLOBALS['LANG']->getLL('title')
1400 );
1401 }
1402
1403 /**
1404 * Gets the function menu selector for this backend module.
1405 *
1406 * @return string The HTML representation of the function menu selector
1407 */
1408 protected function getFunctionMenu() {
1409 return BackendUtility::getFuncMenu(0, 'SET[function]', $this->MOD_SETTINGS['function'], $this->MOD_MENU['function']);
1410 }
1411
1412 /**
1413 * Gets the buttons that shall be rendered in the docHeader.
1414 *
1415 * @return array Available buttons for the docHeader
1416 */
1417 protected function getDocHeaderButtons() {
1418 $buttons = array(
1419 'addtask' => '',
1420 'close' => '',
1421 'save' => '',
1422 'saveclose' => '',
1423 'savenew' => '',
1424 'delete' => '',
1425 'reload' => '',
1426 'shortcut' => $this->getShortcutButton()
1427 );
1428 if (empty($this->CMD) || $this->CMD === 'list' || $this->CMD === 'delete') {
1429 $buttons['reload'] = '<a href="' . $GLOBALS['MCONF']['_'] . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:labels.reload', TRUE) . '">' . IconUtility::getSpriteIcon('actions-system-refresh') . '</a>';
1430 if ($this->MOD_SETTINGS['function'] === 'scheduler' && count(self::getRegisteredClasses())) {
1431 $link = $GLOBALS['MCONF']['_'] . '&CMD=add';
1432 $image = IconUtility::getSpriteIcon('actions-document-new', array('alt' => $GLOBALS['LANG']->getLL('action.add')));
1433 $buttons['addtask'] = '<a href="' . htmlspecialchars($link) . '" ' . 'title="' . $GLOBALS['LANG']->getLL('action.add') . '">' . $image . '</a>';
1434 }
1435 }
1436 if ($this->CMD === 'add' || $this->CMD === 'edit') {
1437 $buttons['close'] = '<a href="#" onclick="document.location=\'' . $GLOBALS['MCONF']['_'] . '\'" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:cancel', TRUE) . '">' . IconUtility::getSpriteIcon('actions-document-close') . '</a>';
1438 $buttons['save'] = '<button style="padding: 0; margin: 0; cursor: pointer;" type="submit" name="CMD" value="save" class="c-inputButton" src="clear.gif" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:save', TRUE) . '" />' . IconUtility::getSpriteIcon('actions-document-save') . '</button>';
1439 $buttons['saveclose'] = '<button style="padding: 0; margin: 0; cursor: pointer;" type="submit" name="CMD" value="saveclose" class="c-inputButton" src="clear.gif" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:saveAndClose', TRUE) . '" />' . IconUtility::getSpriteIcon('actions-document-save-close') . '</button>';
1440 $buttons['savenew'] = '<button style="padding: 0; margin: 0; cursor: pointer;" type="submit" name="CMD" value="savenew" class="c-inputButton" src="clear.gif" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:saveAndCreateNewDoc', TRUE) . '" />' . IconUtility::getSpriteIcon('actions-document-save-new') . '</button>';
1441 }
1442 if ($this->CMD === 'edit') {
1443 $buttons['delete'] = '<button style="padding: 0; margin: 0; cursor: pointer;" type="submit" name="CMD" value="delete" class="c-inputButton" src="clear.gif" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_common.xlf:delete', TRUE) . '" />' . IconUtility::getSpriteIcon('actions-edit-delete') . '</button>';
1444 }
1445 return $buttons;
1446 }
1447
1448 /**
1449 * Gets the button to set a new shortcut in the backend (if current user is allowed to).
1450 *
1451 * @return string HTML representation of the shortcut button
1452 */
1453 protected function getShortcutButton() {
1454 $result = '';
1455 if ($GLOBALS['BE_USER']->mayMakeShortcut()) {
1456 $result = $this->doc->makeShortcutIcon('', 'function', $this->moduleName);
1457 }
1458 return $result;
1459 }
1460
1461 }