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