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