TYPO3 CMS  TYPO3_8-7
SchedulerModuleController.php
Go to the documentation of this file.
1 <?php
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 
31 
36 {
42  protected $submittedData = [];
43 
50  protected $messages = [];
51 
55  protected $cshKey;
56 
60  protected $scheduler;
61 
65  protected $backendTemplatePath = '';
66 
70  protected $view;
71 
77  protected $moduleName = 'system_txschedulerM1';
78 
82  protected $moduleUri;
83 
89  protected $moduleTemplate;
90 
94  protected $iconFactory;
95 
99  public function __construct()
100  {
101  $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
102  $this->getLanguageService()->includeLLFile('EXT:scheduler/Resources/Private/Language/locallang.xlf');
103  $this->MCONF = [
104  'name' => $this->moduleName,
105  ];
106  $this->cshKey = '_MOD_' . $this->moduleName;
107  $this->backendTemplatePath = ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Backend/SchedulerModule/';
108  $this->view = GeneralUtility::makeInstance(\TYPO3\CMS\Fluid\View\StandaloneView::class);
109  $this->view->getRequest()->setControllerExtensionName('scheduler');
110  $this->view->setPartialRootPaths([ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Partials/Backend/SchedulerModule/']);
111  $this->moduleUri = BackendUtility::getModuleUrl($this->moduleName);
112  $this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
113 
114  $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
115  $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
116  $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/SplitButtons');
117  }
118 
122  public function init()
123  {
124  parent::init();
125 
126  // Create scheduler instance
127  $this->scheduler = GeneralUtility::makeInstance(\TYPO3\CMS\Scheduler\Scheduler::class);
128  }
129 
133  public function menuConfig()
134  {
135  $this->MOD_MENU = [
136  'function' => [
137  'scheduler' => $this->getLanguageService()->getLL('function.scheduler'),
138  'check' => $this->getLanguageService()->getLL('function.check'),
139  'info' => $this->getLanguageService()->getLL('function.info')
140  ]
141  ];
142  parent::menuConfig();
143  }
144 
148  public function main()
149  {
150  // Access check!
151  // The page will show only if user has admin rights
152  if ($this->getBackendUser()->isAdmin()) {
153  // Set the form
154  $this->content = '<form name="tx_scheduler_form" id="tx_scheduler_form" method="post" action="">';
155 
156  // Prepare main content
157  $this->content .= '<h1>' . $this->getLanguageService()->getLL('function.' . $this->MOD_SETTINGS['function']) . '</h1>';
158  $this->content .= $this->getModuleContent();
159  $this->content .= '</form><div id="extraFieldsHidden"></div>';
160  } else {
161  // If no access, only display the module's title
162  $this->content = '<h1>' . $this->getLanguageService()->getLL('title.') . '</h1>';
163  $this->content .='<div style="padding-top: 5px;"></div>';
164  }
165  $this->getButtons();
166  $this->getModuleMenu();
167  }
168 
172  protected function getModuleMenu()
173  {
174  $menu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
175  $menu->setIdentifier('SchedulerJumpMenu');
176 
177  foreach ($this->MOD_MENU['function'] as $controller => $title) {
178  $item = $menu
179  ->makeMenuItem()
180  ->setHref(
181  BackendUtility::getModuleUrl(
182  $this->moduleName,
183  [
184  'id' => $this->id,
185  'SET' => [
186  'function' => $controller
187  ]
188  ]
189  )
190  )
191  ->setTitle($title);
192  if ($controller === $this->MOD_SETTINGS['function']) {
193  $item->setActive(true);
194  }
195  $menu->addMenuItem($item);
196  }
197  $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);
198  }
199 
205  protected function getModuleContent()
206  {
207  $content = '';
208  $sectionTitle = '';
209  // Get submitted data
210  $this->submittedData = GeneralUtility::_GPmerged('tx_scheduler');
211  $this->submittedData['uid'] = (int)$this->submittedData['uid'];
212  // If a save command was submitted, handle saving now
213  if ($this->CMD === 'save' || $this->CMD === 'saveclose' || $this->CMD === 'savenew') {
214  $previousCMD = GeneralUtility::_GP('previousCMD');
215  // First check the submitted data
216  $result = $this->preprocessData();
217  // If result is ok, proceed with saving
218  if ($result) {
219  $this->saveTask();
220  if ($this->CMD === 'saveclose') {
221  // Unset command, so that default screen gets displayed
222  unset($this->CMD);
223  } elseif ($this->CMD === 'save') {
224  // After saving a "add form", return to edit
225  $this->CMD = 'edit';
226  } elseif ($this->CMD === 'savenew') {
227  // Unset submitted data, so that empty form gets displayed
228  unset($this->submittedData);
229  // After saving a "add/edit form", return to add
230  $this->CMD = 'add';
231  } else {
232  // Return to edit form
233  $this->CMD = $previousCMD;
234  }
235  } else {
236  $this->CMD = $previousCMD;
237  }
238  }
239 
240  // Handle chosen action
241  switch ((string)$this->MOD_SETTINGS['function']) {
242  case 'scheduler':
243  $this->executeTasks();
244 
245  switch ($this->CMD) {
246  case 'add':
247  case 'edit':
248  try {
249  // Try adding or editing
250  $content .= $this->editTaskAction();
251  $sectionTitle = $this->getLanguageService()->getLL('action.' . $this->CMD);
252  } catch (\Exception $e) {
253  if ($e->getCode() === 1305100019) {
254  // Invalid controller class name exception
255  $this->addMessage($e->getMessage(), FlashMessage::ERROR);
256  }
257  // An exception may also happen when the task to
258  // edit could not be found. In this case revert
259  // to displaying the list of tasks
260  // It can also happen when attempting to edit a running task
261  $content .= $this->listTasksAction();
262  }
263  break;
264  case 'delete':
265  $this->deleteTask();
266  $content .= $this->listTasksAction();
267  break;
268  case 'stop':
269  $this->stopTask();
270  $content .= $this->listTasksAction();
271  break;
272  case 'toggleHidden':
273  $this->toggleDisableAction();
274  $content .= $this->listTasksAction();
275  break;
276  case 'setNextExecutionTime':
278  $content .= $this->listTasksAction();
279  break;
280  case 'list':
281 
282  default:
283  $content .= $this->listTasksAction();
284  }
285  break;
286 
287  // Setup check screen
288  case 'check':
289  // @todo move check to the report module
290  $content .= $this->checkScreenAction();
291  break;
292 
293  // Information screen
294  case 'info':
295  $content .= $this->infoScreenAction();
296  break;
297  }
298  // Wrap the content
299  return '<h2>' . $sectionTitle . '</h2><div class="tx_scheduler_mod1">' . $content . '</div>';
300  }
301 
310  public function mainAction(ServerRequestInterface $request, ResponseInterface $response)
311  {
312  $GLOBALS['SOBE'] = $this;
313  $this->init();
314  $this->main();
315 
316  $this->moduleTemplate->setContent($this->content);
317  $response->getBody()->write($this->moduleTemplate->renderContent());
318  return $response;
319  }
320 
327  protected function checkScreenAction()
328  {
329  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'CheckScreen.html');
330 
331  // Display information about last automated run, as stored in the system registry
333  $registry = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Registry::class);
334  $lastRun = $registry->get('tx_scheduler', 'lastRun');
335  if (!is_array($lastRun)) {
336  $message = $this->getLanguageService()->getLL('msg.noLastRun');
338  } else {
339  if (empty($lastRun['end']) || empty($lastRun['start']) || empty($lastRun['type'])) {
340  $message = $this->getLanguageService()->getLL('msg.incompleteLastRun');
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 = $this->getLanguageService()->getLL('label.' . $label);
352  $message = sprintf($this->getLanguageService()->getLL('msg.lastRun'), $type, $startDate, $startTime, $endDate, $endTime);
353  $severity = InfoboxViewHelper::STATE_INFO;
354  }
355  }
356  $this->view->assign('lastRunMessage', $message);
357  $this->view->assign('lastRunSeverity', $severity);
358 
359  // Check if CLI script is executable or not
360  $script = PATH_site . 'typo3/sysext/core/bin/typo3';
361  $this->view->assign('script', $script);
362 
363  // Skip this check if running Windows, as rights do not work the same way on this platform
364  // (i.e. the script will always appear as *not* executable)
365  if (TYPO3_OS === 'WIN') {
366  $isExecutable = true;
367  } else {
368  $isExecutable = is_executable($script);
369  }
370  if ($isExecutable) {
371  $message = $this->getLanguageService()->getLL('msg.cliScriptExecutable');
372  $severity = InfoboxViewHelper::STATE_OK;
373  } else {
374  $message = $this->getLanguageService()->getLL('msg.cliScriptNotExecutable');
375  $severity = InfoboxViewHelper::STATE_ERROR;
376  }
377  $this->view->assign('isExecutableMessage', $message);
378  $this->view->assign('isExecutableSeverity', $severity);
379  $this->view->assign('now', $this->getServerTime());
380 
381  return $this->view->render();
382  }
383 
389  protected function infoScreenAction()
390  {
391  $registeredClasses = $this->getRegisteredClasses();
392  // No classes available, display information message
393  if (empty($registeredClasses)) {
394  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreenNoClasses.html');
395  return $this->view->render();
396  }
397 
398  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreen.html');
399  $this->view->assign('registeredClasses', $registeredClasses);
400 
401  return $this->view->render();
402  }
403 
410  protected function renderTaskProgressBar($progress)
411  {
412  $progressText = $this->getLanguageService()->getLL('status.progress') . ':&nbsp;' . $progress . '%';
413  return '<div class="progress">'
414  . '<div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="' . $progress . '" aria-valuemin="0" aria-valuemax="100" style="width: ' . $progress . '%;">' . $progressText . '</div>'
415  . '</div>';
416  }
417 
421  protected function deleteTask()
422  {
423  try {
424  // Try to fetch the task and delete it
425  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
426  // If the task is currently running, it may not be deleted
427  if ($task->isExecutionRunning()) {
428  $this->addMessage($this->getLanguageService()->getLL('msg.maynotDeleteRunningTask'), FlashMessage::ERROR);
429  } else {
430  if ($this->scheduler->removeTask($task)) {
431  $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was deleted', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
432  $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
433  } else {
434  $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
435  }
436  }
437  } catch (\UnexpectedValueException $e) {
438  // The task could not be unserialized properly, simply delete the database record
439  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_scheduler_task');
440  $result = $queryBuilder->delete('tx_scheduler_task')
441  ->where(
442  $queryBuilder->expr()->eq(
443  'uid',
444  $queryBuilder->createNamedParameter($this->submittedData['uid'], \PDO::PARAM_INT)
445  )
446  )
447  ->execute();
448 
449  if ($result) {
450  $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
451  } else {
452  $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
453  }
454  } catch (\OutOfBoundsException $e) {
455  // The task was not found, for some reason
456  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
457  }
458  }
459 
466  protected function stopTask()
467  {
468  try {
469  // Try to fetch the task and stop it
470  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
471  if ($task->isExecutionRunning()) {
472  // If the task is indeed currently running, clear marked executions
473  $result = $task->unmarkAllExecutions();
474  if ($result) {
475  $this->addMessage($this->getLanguageService()->getLL('msg.stopSuccess'));
476  } else {
477  $this->addMessage($this->getLanguageService()->getLL('msg.stopError'), FlashMessage::ERROR);
478  }
479  } else {
480  // The task is not running, nothing to unmark
481  $this->addMessage($this->getLanguageService()->getLL('msg.maynotStopNonRunningTask'), FlashMessage::WARNING);
482  }
483  } catch (\Exception $e) {
484  // The task was not found, for some reason
485  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
486  }
487  }
488 
492  protected function toggleDisableAction()
493  {
494  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
495  $task->setDisabled(!$task->isDisabled());
496  // If a disabled single task is enabled again, we register it for a
497  // single execution at next scheduler run.
498  if ($task->getType() === AbstractTask::TYPE_SINGLE) {
499  $task->registerSingleExecution(time());
500  }
501  $task->save();
502  }
503 
507  protected function setNextExecutionTimeAction()
508  {
509  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
510  $task->setRunOnNextCronJob(true);
511  $task->save();
512  }
513 
519  protected function editTaskAction()
520  {
521  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'EditTask.html');
522 
523  $registeredClasses = $this->getRegisteredClasses();
524  $registeredTaskGroups = $this->getRegisteredTaskGroups();
525 
526  $taskInfo = [];
527  $task = null;
528  $process = 'edit';
529 
530  if ($this->submittedData['uid'] > 0) {
531  // If editing, retrieve data for existing task
532  try {
533  $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
534  // If there's a registered execution, the task should not be edited
535  if (!empty($taskRecord['serialized_executions'])) {
536  $this->addMessage($this->getLanguageService()->getLL('msg.maynotEditRunningTask'), FlashMessage::ERROR);
537  throw new \LogicException('Runnings tasks cannot not be edited', 1251232849);
538  }
539 
540  // Get the task object
542  $task = unserialize($taskRecord['serialized_task_object']);
543 
544  // Set some task information
545  $taskInfo['disable'] = $taskRecord['disable'];
546  $taskInfo['description'] = $taskRecord['description'];
547  $taskInfo['task_group'] = $taskRecord['task_group'];
548 
549  // Check that the task object is valid
550  if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
551  // The task object is valid, process with fetching current data
552  $taskInfo['class'] = get_class($task);
553  // Get execution information
554  $taskInfo['start'] = (int)$task->getExecution()->getStart();
555  $taskInfo['end'] = (int)$task->getExecution()->getEnd();
556  $taskInfo['interval'] = $task->getExecution()->getInterval();
557  $taskInfo['croncmd'] = $task->getExecution()->getCronCmd();
558  $taskInfo['multiple'] = $task->getExecution()->getMultiple();
559  if (!empty($taskInfo['interval']) || !empty($taskInfo['croncmd'])) {
560  // Guess task type from the existing information
561  // If an interval or a cron command is defined, it's a recurring task
562  $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
563  $taskInfo['frequency'] = $taskInfo['interval'] ?: $taskInfo['croncmd'];
564  } else {
565  // It's not a recurring task
566  // Make sure interval and cron command are both empty
567  $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
568  $taskInfo['frequency'] = '';
569  $taskInfo['end'] = 0;
570  }
571  } else {
572  // The task object is not valid
573  // Issue error message
574  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.invalidTaskClassEdit'), get_class($task)), FlashMessage::ERROR);
575  // Initialize empty values
576  $taskInfo['start'] = 0;
577  $taskInfo['end'] = 0;
578  $taskInfo['frequency'] = '';
579  $taskInfo['multiple'] = false;
580  $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
581  }
582  } catch (\OutOfBoundsException $e) {
583  // Add a message and continue throwing the exception
584  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
585  throw $e;
586  }
587  } else {
588  // If adding a new object, set some default values
589  $taskInfo['class'] = key($registeredClasses);
590  $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
591  $taskInfo['start'] = $GLOBALS['EXEC_TIME'];
592  $taskInfo['end'] = '';
593  $taskInfo['frequency'] = '';
594  $taskInfo['multiple'] = 0;
595  $process = 'add';
596  }
597 
598  // If some data was already submitted, use it to override
599  // existing data
600  if (!empty($this->submittedData)) {
602  }
603 
604  // Get the extra fields to display for each task that needs some
605  $allAdditionalFields = [];
606  if ($process === 'add') {
607  foreach ($registeredClasses as $class => $registrationInfo) {
608  if (!empty($registrationInfo['provider'])) {
610  $providerObject = GeneralUtility::getUserObj($registrationInfo['provider']);
611  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
612  $additionalFields = $providerObject->getAdditionalFields($taskInfo, null, $this);
613  $allAdditionalFields = array_merge($allAdditionalFields, [$class => $additionalFields]);
614  }
615  }
616  }
617  } else {
618  if ($task !== null && !empty($registeredClasses[$taskInfo['class']]['provider'])) {
619  $providerObject = GeneralUtility::getUserObj($registeredClasses[$taskInfo['class']]['provider']);
620  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
621  $allAdditionalFields[$taskInfo['class']] = $providerObject->getAdditionalFields($taskInfo, $task, $this);
622  }
623  }
624  }
625 
626  // Load necessary JavaScript
627  $this->getPageRenderer()->loadJquery();
628  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
629  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker');
630  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/PageBrowser');
631  $this->getPageRenderer()->addJsInlineCode('browse-button', '
632  function setFormValueFromBrowseWin(fieldReference, elValue, elName) {
633  var res = elValue.split("_");
634  var element = document.getElementById(fieldReference);
635  element.value = res[1];
636  }
637  ');
638 
639  // Start rendering the add/edit form
640  $this->view->assign('uid', htmlspecialchars($this->submittedData['uid']));
641  $this->view->assign('cmd', htmlspecialchars($this->CMD));
642 
643  $table = [];
644 
645  // Disable checkbox
646  $label = '<label>' . $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:disable') . '</label>';
647  $table[] =
648  '<div class="form-section" id="task_disable_row"><div class="form-group">'
649  . BackendUtility::wrapInHelp($this->cshKey, 'task_disable', $label)
650  . '<div class="form-control-wrap">'
651  . '<input type="hidden" name="tx_scheduler[disable]" value="0">'
652  . '<input class="checkbox" type="checkbox" name="tx_scheduler[disable]" value="1" id="task_disable" ' . ($taskInfo['disable'] ? ' checked="checked"' : '') . '>'
653  . '</div>'
654  . '</div></div>';
655 
656  // Task class selector
657  $label = '<label>' . $this->getLanguageService()->getLL('label.class') . '</label>';
658 
659  // On editing, don't allow changing of the task class, unless it was not valid
660  if ($this->submittedData['uid'] > 0 && !empty($taskInfo['class'])) {
661  $cell = '<div>' . $registeredClasses[$taskInfo['class']]['title'] . ' (' . $registeredClasses[$taskInfo['class']]['extension'] . ')</div>';
662  $cell .= '<input type="hidden" name="tx_scheduler[class]" id="task_class" value="' . htmlspecialchars($taskInfo['class']) . '">';
663  } else {
664  $cell = '<select name="tx_scheduler[class]" id="task_class" class="form-control">';
665  // Group registered classes by classname
666  $groupedClasses = [];
667  foreach ($registeredClasses as $class => $classInfo) {
668  $groupedClasses[$classInfo['extension']][$class] = $classInfo;
669  }
670  ksort($groupedClasses);
671  // Loop on all grouped classes to display a selector
672  foreach ($groupedClasses as $extension => $class) {
673  $cell .= '<optgroup label="' . htmlspecialchars($extension) . '">';
674  foreach ($groupedClasses[$extension] as $class => $classInfo) {
675  $selected = $class == $taskInfo['class'] ? ' selected="selected"' : '';
676  $cell .= '<option value="' . htmlspecialchars($class) . '"' . 'title="' . htmlspecialchars($classInfo['description']) . '" ' . $selected . '>' . htmlspecialchars($classInfo['title']) . '</option>';
677  }
678  $cell .= '</optgroup>';
679  }
680  $cell .= '</select>';
681  }
682  $table[] =
683  '<div class="form-section" id="task_class_row"><div class="form-group">'
684  . BackendUtility::wrapInHelp($this->cshKey, 'task_class', $label)
685  . '<div class="form-control-wrap">'
686  . $cell
687  . '</div>'
688  . '</div></div>';
689 
690  // Task type selector
691  $label = '<label>' . $this->getLanguageService()->getLL('label.type') . '</label>';
692  $table[] =
693  '<div class="form-section" id="task_type_row"><div class="form-group">'
694  . BackendUtility::wrapInHelp($this->cshKey, 'task_type', $label)
695  . '<div class="form-control-wrap">'
696  . '<select name="tx_scheduler[type]" id="task_type" class="form-control">'
697  . '<option value="1" ' . ((int)$taskInfo['type'] === AbstractTask::TYPE_SINGLE ? ' selected="selected"' : '') . '>' . $this->getLanguageService()->getLL('label.type.single') . '</option>'
698  . '<option value="2" ' . ((int)$taskInfo['type'] === AbstractTask::TYPE_RECURRING ? ' selected="selected"' : '') . '>' . $this->getLanguageService()->getLL('label.type.recurring') . '</option>'
699  . '</select>'
700  . '</div>'
701  . '</div></div>';
702 
703  // Task group selector
704  $label = '<label>' . $this->getLanguageService()->getLL('label.group') . '</label>';
705  $cell = '<select name="tx_scheduler[task_group]" id="task_class" class="form-control">';
706 
707  // Loop on all groups to display a selector
708  $cell .= '<option value="0" title=""></option>';
709  foreach ($registeredTaskGroups as $taskGroup) {
710  $selected = $taskGroup['uid'] == $taskInfo['task_group'] ? ' selected="selected"' : '';
711  $cell .= '<option value="' . $taskGroup['uid'] . '"' . 'title="';
712  $cell .= htmlspecialchars($taskGroup['groupName']) . '"' . $selected . '>';
713  $cell .= htmlspecialchars($taskGroup['groupName']) . '</option>';
714  }
715  $cell .= '</select>';
716 
717  $table[] =
718  '<div class="form-section" id="task_group_row"><div class="form-group">'
719  . BackendUtility::wrapInHelp($this->cshKey, 'task_group', $label)
720  . '<div class="form-control-wrap">'
721  . $cell
722  . '</div>'
723  . '</div></div>';
724 
725  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '%H:%M %m-%d-%Y' : '%H:%M %d-%m-%Y';
726 
727  $label = '<label>' . BackendUtility::wrapInHelp($this->cshKey, 'task_start', $this->getLanguageService()->getLL('label.start')) . '</label>';
728  $value = ($taskInfo['start'] > 0 ? strftime($dateFormat, $taskInfo['start']) : '');
729  $table[] =
730  '<div class="form-section"><div class="row"><div class="form-group col-sm-6" id="task_start_col">'
731  . $label
732  . '<div class="form-control-wrap">'
733  . '<div class="input-group" id="tceforms-datetimefield-task_start_row-wrapper">'
734  . '<input name="tx_scheduler[start]_hr" value="' . htmlspecialchars($value) . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="datetime" type="text" id="tceforms-datetimefield-task_start_row">'
735  . '<input name="tx_scheduler[start]" value="' . htmlspecialchars($taskInfo['start']) . '" type="hidden">'
736  . '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_start_row">' . $this->iconFactory->getIcon('actions-edit-pick-date', Icon::SIZE_SMALL)->render() . '</label></span>'
737  . '</div>'
738  . '</div>'
739  . '</div>';
740 
741  // End date/time field
742  // NOTE: datetime fields need a special id naming scheme
743  $value = ($taskInfo['end'] > 0 ? strftime($dateFormat, $taskInfo['end']) : '');
744  $label = '<label>' . $this->getLanguageService()->getLL('label.end') . '</label>';
745  $table[] =
746  '<div class="form-group col-sm-6" id="task_end_col">'
747  . BackendUtility::wrapInHelp($this->cshKey, 'task_end', $label)
748  . '<div class="form-control-wrap">'
749  . '<div class="input-group" id="tceforms-datetimefield-task_end_row-wrapper">'
750  . '<input name="tx_scheduler[end]_hr" value="' . htmlspecialchars($value) . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="datetime" type="text" id="tceforms-datetimefield-task_end_row">'
751  . '<input name="tx_scheduler[end]" value="' . htmlspecialchars($taskInfo['end']) . '" type="hidden">'
752  . '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_end_row">' . $this->iconFactory->getIcon('actions-edit-pick-date', Icon::SIZE_SMALL)->render() . '</label></span>'
753  . '</div>'
754  . '</div>'
755  . '</div></div></div>';
756 
757  // Frequency input field
758  $label = '<label>' . $this->getLanguageService()->getLL('label.frequency.long') . '</label>';
759  $table[] =
760  '<div class="form-section" id="task_frequency_row"><div class="form-group">'
761  . BackendUtility::wrapInHelp($this->cshKey, 'task_frequency', $label)
762  . '<div class="form-control-wrap">'
763  . '<input type="text" name="tx_scheduler[frequency]" class="form-control" id="task_frequency" value="' . htmlspecialchars($taskInfo['frequency']) . '">'
764  . '</div>'
765  . '</div></div>';
766 
767  // Multiple execution selector
768  $label = '<label>' . $this->getLanguageService()->getLL('label.parallel.long') . '</label>';
769  $table[] =
770  '<div class="form-section" id="task_multiple_row"><div class="form-group">'
771  . BackendUtility::wrapInHelp($this->cshKey, 'task_multiple', $label)
772  . '<div class="form-control-wrap">'
773  . '<input type="hidden" name="tx_scheduler[multiple]" value="0">'
774  . '<input class="checkbox" type="checkbox" name="tx_scheduler[multiple]" value="1" id="task_multiple" ' . ($taskInfo['multiple'] ? 'checked="checked"' : '') . '>'
775  . '</div>'
776  . '</div></div>';
777 
778  // Description
779  $label = '<label>' . $this->getLanguageService()->getLL('label.description') . '</label>';
780  $table[] =
781  '<div class="form-section" id="task_description_row"><div class="form-group">'
782  . BackendUtility::wrapInHelp($this->cshKey, 'task_description', $label)
783  . '<div class="form-control-wrap">'
784  . '<textarea class="form-control" name="tx_scheduler[description]">' . htmlspecialchars($taskInfo['description']) . '</textarea>'
785  . '</div>'
786  . '</div></div>';
787 
788  // Display additional fields
789  $table[] = '<div id="extraFieldsSection">';
790  foreach ($allAdditionalFields as $class => $fields) {
791  if ($class == $taskInfo['class']) {
792  $additionalFieldsStyle = '';
793  } else {
794  $additionalFieldsStyle = ' style="display: none"';
795  }
796  // Add each field to the display, if there are indeed any
797  if (isset($fields) && is_array($fields)) {
798  foreach ($fields as $fieldID => $fieldInfo) {
799  $label = '<label>' . $this->getLanguageService()->sL($fieldInfo['label']) . '</label>';
800  $htmlClassName = strtolower(str_replace('\\', '-', $class));
801  $table[] =
802  '<div class="form-section extraFields extra_fields_' . $htmlClassName . '" ' . $additionalFieldsStyle . ' id="' . $fieldID . '_row"><div class="form-group">'
803  . BackendUtility::wrapInHelp($fieldInfo['cshKey'], $fieldInfo['cshLabel'], $label)
804  . '<div class="form-control-wrap">' . $fieldInfo['code'] . '</div>'
805  . $this->getBrowseButton($fieldID, $fieldInfo)
806  . '</div></div>';
807  }
808  }
809  }
810  $table[] = '</div>';
811 
812  $this->view->assign('table', implode(LF, $table));
813  $this->view->assign('now', $this->getServerTime());
814 
815  return $this->view->render();
816  }
817 
823  protected function getBrowseButton($fieldID, array $fieldInfo)
824  {
825  if (isset($fieldInfo['browser']) && ($fieldInfo['browser'] === 'page')) {
826  $url = BackendUtility::getModuleUrl(
827  'wizard_element_browser',
828  ['mode' => 'db', 'bparams' => $fieldID . '|||pages|']
829  );
830  $title = htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.browse_db'));
831  return '
832  <div><a href="#" data-url=' . htmlspecialchars($url) . ' class="btn btn-default t3js-pageBrowser" title="' . $title . '">
833  <span class="t3js-icon icon icon-size-small icon-state-default icon-actions-insert-record" data-identifier="actions-insert-record">
834  <span class="icon-markup">' . $this->iconFactory->getIcon(
835  'actions-insert-record',
837  )->render() . '</span>
838  </span>
839  </a><span id="page_' . $fieldID . '">&nbsp;' . htmlspecialchars($fieldInfo['pageTitle']) . '</span></div>';
840  }
841  return '';
842  }
843 
847  protected function executeTasks()
848  {
849  // Make sure next automatic scheduler-run is scheduled
850  if (GeneralUtility::_POST('go') !== null) {
851  $this->scheduler->scheduleNextSchedulerRunUsingAtDaemon();
852  }
853  // Continue if some elements have been chosen for execution
854  if (isset($this->submittedData['execute']) && !empty($this->submittedData['execute'])) {
855  // Get list of registered classes
856  $registeredClasses = $this->getRegisteredClasses();
857  // Loop on all selected tasks
858  foreach ($this->submittedData['execute'] as $uid) {
859  try {
860  // Try fetching the task
861  $task = $this->scheduler->fetchTask($uid);
862  $class = get_class($task);
863  $name = $registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')';
864  if (GeneralUtility::_POST('go_cron') !== null) {
865  $task->setRunOnNextCronJob(true);
866  $task->save();
867  } else {
868  // Now try to execute it and report on outcome
869  try {
870  $result = $this->scheduler->executeTask($task);
871  if ($result) {
872  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executed'), $name));
873  } else {
874  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.notExecuted'), $name), FlashMessage::ERROR);
875  }
876  } catch (\Exception $e) {
877  // An exception was thrown, display its message as an error
878  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $name, $e->getMessage()), FlashMessage::ERROR);
879  }
880  }
881  } catch (\OutOfBoundsException $e) {
882  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $uid), FlashMessage::ERROR);
883  } catch (\UnexpectedValueException $e) {
884  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $uid, $e->getMessage()), FlashMessage::ERROR);
885  }
886  }
887  // Record the run in the system registry
888  $this->scheduler->recordLastRun('manual');
889  // Make sure to switch to list view after execution
890  $this->CMD = 'list';
891  }
892  }
893 
899  protected function listTasksAction()
900  {
901  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasks.html');
902 
903  // Define display format for dates
904  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
905 
906  // Get list of registered task groups
907  $registeredTaskGroups = $this->getRegisteredTaskGroups();
908 
909  // add an empty entry for non-grouped tasks
910  // add in front of list
911  array_unshift($registeredTaskGroups, ['uid' => 0, 'groupName' => '']);
912 
913  // Get all registered tasks
914  // Just to get the number of entries
915  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
916  ->getQueryBuilderForTable('tx_scheduler_task');
917  $queryBuilder->getRestrictions()->removeAll();
918 
919  $result = $queryBuilder->select('t.*')
920  ->addSelect(
921  'g.groupName AS taskGroupName',
922  'g.description AS taskGroupDescription',
923  'g.deleted AS isTaskGroupDeleted'
924  )
925  ->from('tx_scheduler_task', 't')
926  ->leftJoin(
927  't',
928  'tx_scheduler_task_group',
929  'g',
930  $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
931  )
932  ->orderBy('g.sorting')
933  ->execute();
934 
935  // Loop on all tasks
936  $temporaryResult = [];
937  while ($row = $result->fetch()) {
938  if ($row['taskGroupName'] === null || $row['isTaskGroupDeleted'] === '1') {
939  $row['taskGroupName'] = '';
940  $row['taskGroupDescription'] = '';
941  $row['task_group'] = 0;
942  }
943  $temporaryResult[$row['task_group']]['groupName'] = $row['taskGroupName'];
944  $temporaryResult[$row['task_group']]['groupDescription'] = $row['taskGroupDescription'];
945  $temporaryResult[$row['task_group']]['tasks'][] = $row;
946  }
947 
948  // No tasks defined, display information message
949  if (empty($temporaryResult)) {
950  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasksNoTasks.html');
951  return $this->view->render();
952  }
953 
954  $this->getPageRenderer()->loadJquery();
955  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
956  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/Tooltip');
957  $table = [];
958  // Header row
959  $table[] =
960  '<thead><tr>'
961  . '<th><a class="btn btn-default" href="#" id="checkall" title="' . htmlspecialchars($this->getLanguageService()->getLL('label.checkAll')) . '" class="icon">' . $this->moduleTemplate->getIconFactory()->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . '</a></th>'
962  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.id')) . '</th>'
963  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('task')) . '</th>'
964  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.type')) . '</th>'
965  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.frequency')) . '</th>'
966  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.parallel')) . '</th>'
967  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.lastExecution')) . '</th>'
968  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.nextExecution')) . '</th>'
969  . '<th></th>'
970  . '</tr></thead>';
971 
972  $registeredClasses = $this->getRegisteredClasses();
973  $iconFactory = GeneralUtility::makeInstance(IconFactory::class);
974  $collapseIcon = $iconFactory->getIcon('actions-view-list-collapse', Icon::SIZE_SMALL)->render();
975  $expandIcon = $iconFactory->getIcon('actions-view-list-expand', Icon::SIZE_SMALL)->render();
976  foreach ($temporaryResult as $taskIndex => $taskGroup) {
977  $collapseExpandIcons = '<span class="taskGroup_' . $taskIndex . '">' . $collapseIcon . '</span>'
978  . '<span class="taskGroup_' . $taskIndex . '" style="display: none;">' . $expandIcon . '</span>';
979  if (!empty($taskGroup['groupName'])) {
980  $groupText = '<strong>' . htmlspecialchars($taskGroup['groupName']) . '</strong>';
981  if (!empty($taskGroup['groupDescription'])) {
982  $groupText .= '<br>' . nl2br(htmlspecialchars($taskGroup['groupDescription']));
983  }
984  $table[] = '<tr class="taskGroup" data-task-group-id="' . $taskIndex . '"><td colspan="8">' . $groupText . '</td><td style="text-align:right;">' . $collapseExpandIcons . '</td></tr>';
985  } else {
986  if (count($temporaryResult) > 1) {
987  $table[] = '<tr class="taskGroup" data-task-group-id="0"><td colspan="8"><strong>' . htmlspecialchars($this->getLanguageService()->getLL('label.noGroup')) . '</strong></td><td style="text-align:right;">' . $collapseExpandIcons . '</td></tr>';
988  }
989  }
990 
991  foreach ($taskGroup['tasks'] as $schedulerRecord) {
992  // Define action icons
993  $link = htmlspecialchars($this->moduleUri . '&CMD=edit&tx_scheduler[uid]=' . $schedulerRecord['uid']);
994  $editAction = '<a data-toggle="tooltip" data-container="body" class="btn btn-default" href="' . $link . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:edit')) . '" class="icon">' .
995  $this->moduleTemplate->getIconFactory()->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
996  if ((int)$schedulerRecord['disable'] === 1) {
997  $translationKey = 'enable';
998  $icon = $this->moduleTemplate->getIconFactory()->getIcon('actions-edit-unhide', Icon::SIZE_SMALL);
999  } else {
1000  $translationKey = 'disable';
1001  $icon = $this->moduleTemplate->getIconFactory()->getIcon('actions-edit-hide', Icon::SIZE_SMALL);
1002  }
1003  $toggleHiddenAction = '<a data-toggle="tooltip" data-container="body" class="btn btn-default" href="' . htmlspecialchars($this->moduleUri
1004  . '&CMD=toggleHidden&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" title="'
1005  . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:' . $translationKey))
1006  . '" class="icon">' . $icon->render() . '</a>';
1007  $deleteAction = '<a data-toggle="tooltip" data-container="body" class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars($this->moduleUri . '&CMD=delete&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" '
1008  . ' data-severity="warning"'
1009  . ' data-title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:delete')) . '"'
1010  . ' data-button-close-text="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:cancel')) . '"'
1011  . ' data-content="' . htmlspecialchars($this->getLanguageService()->getLL('msg.delete')) . '"'
1012  . ' title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:delete')) . '" class="icon">' .
1013  $this->moduleTemplate->getIconFactory()->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>';
1014  $stopAction = '<a data-toggle="tooltip" data-container="body" class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars($this->moduleUri . '&CMD=stop&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" '
1015  . ' data-severity="warning"'
1016  . ' data-title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:stop')) . '"'
1017  . ' data-button-close-text="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:cancel')) . '"'
1018  . ' data-content="' . htmlspecialchars($this->getLanguageService()->getLL('msg.stop')) . '"'
1019  . ' title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:stop')) . '" class="icon">' .
1020  $this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL)->render() . '</a>';
1021  $runAction = '<a class="btn btn-default" data-toggle="tooltip" data-container="body" href="' . htmlspecialchars($this->moduleUri . '&tx_scheduler[execute][]=' . $schedulerRecord['uid']) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('action.run_task')) . '" class="icon">' .
1022  $this->moduleTemplate->getIconFactory()->getIcon('extensions-scheduler-run-task', Icon::SIZE_SMALL)->render() . '</a>';
1023  $runCronAction = '<a class="btn btn-default" data-toggle="tooltip" data-container="body" href="' . htmlspecialchars($this->moduleUri . '&CMD=setNextExecutionTime&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" title="' . $this->getLanguageService()->getLL('action.run_task_cron', true) . '" class="icon">' .
1024  $this->moduleTemplate->getIconFactory()->getIcon('extensions-scheduler-run-task-cron', Icon::SIZE_SMALL)->render() . '</a>';
1025 
1026  // Define some default values
1027  $lastExecution = '-';
1028  $isRunning = false;
1029  $showAsDisabled = false;
1030  $startExecutionElement = '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
1031  // Restore the serialized task and pass it a reference to the scheduler object
1033  $task = unserialize($schedulerRecord['serialized_task_object']);
1034  $class = get_class($task);
1035  if ($class === '__PHP_Incomplete_Class' && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) {
1036  $class = $matches['classname'];
1037  }
1038  // Assemble information about last execution
1039  if (!empty($schedulerRecord['lastexecution_time'])) {
1040  $lastExecution = date($dateFormat, $schedulerRecord['lastexecution_time']);
1041  if ($schedulerRecord['lastexecution_context'] === 'CLI') {
1042  $context = $this->getLanguageService()->getLL('label.cron');
1043  } else {
1044  $context = $this->getLanguageService()->getLL('label.manual');
1045  }
1046  $lastExecution .= ' (' . $context . ')';
1047  }
1048 
1049  if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
1050  // The task object is valid
1051  $labels = [];
1052  $name = htmlspecialchars($registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')');
1053  $additionalInformation = $task->getAdditionalInformation();
1054  if ($task instanceof \TYPO3\CMS\Scheduler\ProgressProviderInterface) {
1055  $progress = round((float)$task->getProgress(), 2);
1056  $name .= $this->renderTaskProgressBar($progress);
1057  }
1058  if (!empty($additionalInformation)) {
1059  $name .= '<div class="additional-information">' . nl2br(htmlspecialchars($additionalInformation)) . '</div>';
1060  }
1061  // Check if task currently has a running execution
1062  if (!empty($schedulerRecord['serialized_executions'])) {
1063  $labels[] = [
1064  'class' => 'success',
1065  'text' => $this->getLanguageService()->getLL('status.running')
1066  ];
1067  $isRunning = true;
1068  }
1069 
1070  // Prepare display of next execution date
1071  // If task is currently running, date is not displayed (as next hasn't been calculated yet)
1072  // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
1073  if ($isRunning || $schedulerRecord['disable']) {
1074  $nextDate = '-';
1075  } else {
1076  $nextDate = date($dateFormat, $schedulerRecord['nextexecution']);
1077  if (empty($schedulerRecord['nextexecution'])) {
1078  $nextDate = $this->getLanguageService()->getLL('none');
1079  } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
1080  $labels[] = [
1081  'class' => 'warning',
1082  'text' => $this->getLanguageService()->getLL('status.late'),
1083  'description' => $this->getLanguageService()->getLL('status.legend.scheduled')
1084  ];
1085  }
1086  }
1087  // Get execution type
1088  if ($task->getType() === AbstractTask::TYPE_SINGLE) {
1089  $execType = $this->getLanguageService()->getLL('label.type.single');
1090  $frequency = '-';
1091  } else {
1092  $execType = $this->getLanguageService()->getLL('label.type.recurring');
1093  if ($task->getExecution()->getCronCmd() == '') {
1094  $frequency = $task->getExecution()->getInterval();
1095  } else {
1096  $frequency = $task->getExecution()->getCronCmd();
1097  }
1098  }
1099  // Get multiple executions setting
1100  if ($task->getExecution()->getMultiple()) {
1101  $multiple = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:yes');
1102  } else {
1103  $multiple = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:no');
1104  }
1105  // Define checkbox
1106  $startExecutionElement = '<label class="btn btn-default btn-checkbox"><input type="checkbox" name="tx_scheduler[execute][]" value="' . $schedulerRecord['uid'] . '" id="task_' . $schedulerRecord['uid'] . '"><span class="t3-icon fa"></span></label>';
1107 
1108  $actions = '<div class="btn-group" role="group">' . $editAction . $toggleHiddenAction . $deleteAction . '</div>';
1109 
1110  // Check the disable status
1111  // Row is shown dimmed if task is disabled, unless it is still running
1112  if ($schedulerRecord['disable'] && !$isRunning) {
1113  $labels[] = [
1114  'class' => 'default',
1115  'text' => $this->getLanguageService()->getLL('status.disabled')
1116  ];
1117  $showAsDisabled = true;
1118  }
1119 
1120  // Show no action links (edit, delete) if task is running
1121  if ($isRunning) {
1122  $actions = '<div class="btn-group" role="group">' . $stopAction . '</div>';
1123  } else {
1124  $actions .= '&nbsp;<div class="btn-group" role="group">' . $runCronAction . $runAction . '</div>';
1125  }
1126 
1127  // Check if the last run failed
1128  if (!empty($schedulerRecord['lastexecution_failure'])) {
1129  // Try to get the stored exception array
1131  $exceptionArray = @unserialize($schedulerRecord['lastexecution_failure']);
1132  // If the exception could not be unserialized, issue a default error message
1133  if (!is_array($exceptionArray) || empty($exceptionArray)) {
1134  $labelDescription = $this->getLanguageService()->getLL('msg.executionFailureDefault');
1135  } else {
1136  $labelDescription = sprintf($this->getLanguageService()->getLL('msg.executionFailureReport'), $exceptionArray['code'], $exceptionArray['message']);
1137  }
1138  $labels[] = [
1139  'class' => 'danger',
1140  'text' => $this->getLanguageService()->getLL('status.failure'),
1141  'description' => $labelDescription
1142  ];
1143  }
1144  // Format the execution status,
1145  // including failure feedback, if any
1146  $taskDesc = '';
1147  if ($schedulerRecord['description'] !== '') {
1148  $taskDesc = '<span class="description">' . nl2br(htmlspecialchars($schedulerRecord['description'])) . '</span>';
1149  }
1150  $taskName = '<span class="name"><a href="' . $link . '">' . $name . '</a></span>';
1151 
1152  $table[] =
1153  '<tr class="' . ($showAsDisabled ? 'disabled' : '') . ' taskGroup_' . $taskIndex . '">'
1154  . '<td><span class="t-span">' . $startExecutionElement . '</span></td>'
1155  . '<td class="right"><span class="t-span">' . $schedulerRecord['uid'] . '</span></td>'
1156  . '<td><span class="t-span">' . $this->makeStatusLabel($labels) . $taskName . $taskDesc . '</span></td>'
1157  . '<td><span class="t-span">' . $execType . '</span></td>'
1158  . '<td><span class="t-span">' . $frequency . '</span></td>'
1159  . '<td><span class="t-span">' . $multiple . '</span></td>'
1160  . '<td><span class="t-span">' . $lastExecution . '</span></td>'
1161  . '<td><span class="t-span">' . $nextDate . '</span></td>'
1162  . '<td class="nowrap"><span class="t-span">' . $actions . '</span></td>'
1163  . '</tr>';
1164  } else {
1165  // The task object is not valid
1166  // Prepare to issue an error
1167  $executionStatusOutput = '<span class="label label-danger">'
1168  . htmlspecialchars(sprintf(
1169  $this->getLanguageService()->getLL('msg.invalidTaskClass'),
1170  $class
1171  ))
1172  . '</span>';
1173  $table[] =
1174  '<tr>'
1175  . '<td>' . $startExecutionElement . '</td>'
1176  . '<td class="right">' . $schedulerRecord['uid'] . '</td>'
1177  . '<td colspan="6">' . $executionStatusOutput . '</td>'
1178  . '<td class="nowrap"><div class="btn-group" role="group">'
1179  . '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'
1180  . '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'
1181  . $deleteAction
1182  . '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'
1183  . '</div></td>'
1184  . '</tr>';
1185  }
1186  }
1187  }
1188 
1189  $this->view->assign('table', '<table class="table table-striped table-hover">' . implode(LF, $table) . '</table>');
1190  $this->view->assign('now', $this->getServerTime());
1191 
1192  return $this->view->render();
1193  }
1194 
1201  protected function makeStatusLabel(array $labels)
1202  {
1203  $htmlLabels = [];
1204  foreach ($labels as $label) {
1205  if (empty($label['text'])) {
1206  continue;
1207  }
1208  $htmlLabels[] = '<span class="label label-' . htmlspecialchars($label['class']) . ' pull-right" title="' . htmlspecialchars($label['description']) . '">' . htmlspecialchars($label['text']) . '</span>';
1209  }
1210 
1211  return implode('&nbsp;', $htmlLabels);
1212  }
1213 
1217  protected function saveTask()
1218  {
1219  // If a task is being edited fetch old task data
1220  if (!empty($this->submittedData['uid'])) {
1221  try {
1222  $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
1224  $task = unserialize($taskRecord['serialized_task_object']);
1225  } catch (\OutOfBoundsException $e) {
1226  // If the task could not be fetched, issue an error message
1227  // and exit early
1228  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
1229  return;
1230  }
1231  // Register single execution
1232  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1233  $task->registerSingleExecution($this->submittedData['start']);
1234  } else {
1235  if (!empty($this->submittedData['croncmd'])) {
1236  // Definition by cron-like syntax
1237  $interval = 0;
1238  $cronCmd = $this->submittedData['croncmd'];
1239  } else {
1240  // Definition by interval
1241  $interval = $this->submittedData['interval'];
1242  $cronCmd = '';
1243  }
1244  // Register recurring execution
1245  $task->registerRecurringExecution($this->submittedData['start'], $interval, $this->submittedData['end'], $this->submittedData['multiple'], $cronCmd);
1246  }
1247  // Set disable flag
1248  $task->setDisabled($this->submittedData['disable']);
1249  // Set description
1250  $task->setDescription($this->submittedData['description']);
1251  // Set task group
1252  $task->setTaskGroup($this->submittedData['task_group']);
1253  // Save additional input values
1254  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1256  $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1257  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1258  $providerObject->saveAdditionalFields($this->submittedData, $task);
1259  }
1260  }
1261  // Save to database
1262  $result = $this->scheduler->saveTask($task);
1263  if ($result) {
1264  $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was updated', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1265  $this->addMessage($this->getLanguageService()->getLL('msg.updateSuccess'));
1266  } else {
1267  $this->addMessage($this->getLanguageService()->getLL('msg.updateError'), FlashMessage::ERROR);
1268  }
1269  } else {
1270  // A new task is being created
1271  // Create an instance of chosen class
1273  $task = GeneralUtility::makeInstance($this->submittedData['class']);
1274  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1275  // Set up single execution
1276  $task->registerSingleExecution($this->submittedData['start']);
1277  } else {
1278  // Set up recurring execution
1279  $task->registerRecurringExecution($this->submittedData['start'], $this->submittedData['interval'], $this->submittedData['end'], $this->submittedData['multiple'], $this->submittedData['croncmd']);
1280  }
1281  // Save additional input values
1282  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1284  $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1285  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1286  $providerObject->saveAdditionalFields($this->submittedData, $task);
1287  }
1288  }
1289  // Set disable flag
1290  $task->setDisabled($this->submittedData['disable']);
1291  // Set description
1292  $task->setDescription($this->submittedData['description']);
1293  // Set description
1294  $task->setTaskGroup($this->submittedData['task_group']);
1295  // Add to database
1296  $result = $this->scheduler->addTask($task);
1297  if ($result) {
1298  $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was added', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1299  $this->addMessage($this->getLanguageService()->getLL('msg.addSuccess'));
1300 
1301  // set the uid of the just created task so that we
1302  // can continue editing after initial saving
1303  $this->submittedData['uid'] = $task->getTaskUid();
1304  } else {
1305  $this->addMessage($this->getLanguageService()->getLL('msg.addError'), FlashMessage::ERROR);
1306  }
1307  }
1308  }
1309 
1310  /*************************
1311  *
1312  * INPUT PROCESSING UTILITIES
1313  *
1314  *************************/
1320  protected function preprocessData()
1321  {
1322  $result = true;
1323  // Validate id
1324  $this->submittedData['uid'] = empty($this->submittedData['uid']) ? 0 : (int)$this->submittedData['uid'];
1325  // Validate selected task class
1326  if (!class_exists($this->submittedData['class'])) {
1327  $this->addMessage($this->getLanguageService()->getLL('msg.noTaskClassFound'), FlashMessage::ERROR);
1328  }
1329  // Check start date
1330  if (empty($this->submittedData['start'])) {
1331  $this->addMessage($this->getLanguageService()->getLL('msg.noStartDate'), FlashMessage::ERROR);
1332  $result = false;
1333  } elseif (is_string($this->submittedData['start']) && (!is_numeric($this->submittedData['start']))) {
1334  try {
1335  $this->submittedData['start'] = $this->convertToTimestamp($this->submittedData['start']);
1336  } catch (\Exception $e) {
1337  $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR);
1338  $result = false;
1339  }
1340  } else {
1341  $this->submittedData['start'] = (int)$this->submittedData['start'];
1342  }
1343  // Check end date, if recurring task
1344  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING && !empty($this->submittedData['end'])) {
1345  if (is_string($this->submittedData['end']) && (!is_numeric($this->submittedData['end']))) {
1346  try {
1347  $this->submittedData['end'] = $this->convertToTimestamp($this->submittedData['end']);
1348  } catch (\Exception $e) {
1349  $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR);
1350  $result = false;
1351  }
1352  } else {
1353  $this->submittedData['end'] = (int)$this->submittedData['end'];
1354  }
1355  if ($this->submittedData['end'] < $this->submittedData['start']) {
1356  $this->addMessage(
1357  $this->getLanguageService()->getLL('msg.endDateSmallerThanStartDate'),
1359  );
1360  $result = false;
1361  }
1362  }
1363  // Set default values for interval and cron command
1364  $this->submittedData['interval'] = 0;
1365  $this->submittedData['croncmd'] = '';
1366  // Check type and validity of frequency, if recurring
1367  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING) {
1368  $frequency = trim($this->submittedData['frequency']);
1369  if (empty($frequency)) {
1370  // Empty frequency, not valid
1371  $this->addMessage($this->getLanguageService()->getLL('msg.noFrequency'), FlashMessage::ERROR);
1372  $result = false;
1373  } else {
1374  $cronErrorCode = 0;
1375  $cronErrorMessage = '';
1376  // Try interpreting the cron command
1377  try {
1379  $this->submittedData['croncmd'] = $frequency;
1380  } catch (\Exception $e) {
1381  // Store the exception's result
1382  $cronErrorMessage = $e->getMessage();
1383  $cronErrorCode = $e->getCode();
1384  // Check if the frequency is a valid number
1385  // If yes, assume it is a frequency in seconds, and unset cron error code
1386  if (is_numeric($frequency)) {
1387  $this->submittedData['interval'] = (int)$frequency;
1388  unset($cronErrorCode);
1389  }
1390  }
1391  // If there's a cron error code, issue validation error message
1392  if (!empty($cronErrorCode)) {
1393  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.frequencyError'), $cronErrorMessage, $cronErrorCode), FlashMessage::ERROR);
1394  $result = false;
1395  }
1396  }
1397  }
1398  // Validate additional input fields
1399  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1401  $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1402  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1403  // The validate method will return true if all went well, but that must not
1404  // override previous false values => AND the returned value with the existing one
1405  $result &= $providerObject->validateAdditionalFields($this->submittedData, $this);
1406  }
1407  }
1408  return $result;
1409  }
1410 
1417  protected function convertToTimestamp(string $input): int
1418  {
1419  // Convert to ISO 8601 dates
1420  $dateTime = new \DateTime($input);
1421  $value = $dateTime->getTimestamp();
1422  if ($value !== 0) {
1423  $value -= date('Z', $value);
1424  }
1425  return $value;
1426  }
1427 
1428  /*************************
1429  *
1430  * APPLICATION LOGIC UTILITIES
1431  *
1432  *************************/
1439  public function addMessage($message, $severity = FlashMessage::OK)
1440  {
1441  $this->moduleTemplate->addFlashMessage($message, '', $severity);
1442  }
1443 
1457  protected function getRegisteredClasses()
1458  {
1459  $list = [];
1460  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'])) {
1461  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] as $class => $registrationInformation) {
1462  $title = isset($registrationInformation['title']) ? $this->getLanguageService()->sL($registrationInformation['title']) : '';
1463  $description = isset($registrationInformation['description']) ? $this->getLanguageService()->sL($registrationInformation['description']) : '';
1464  $list[$class] = [
1465  'extension' => $registrationInformation['extension'],
1466  'title' => $title,
1467  'description' => $description,
1468  'provider' => isset($registrationInformation['additionalFields']) ? $registrationInformation['additionalFields'] : ''
1469  ];
1470  }
1471  }
1472  return $list;
1473  }
1474 
1480  protected function getRegisteredTaskGroups()
1481  {
1482  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1483  ->getQueryBuilderForTable('tx_scheduler_task_group');
1484 
1485  return $queryBuilder
1486  ->select('*')
1487  ->from('tx_scheduler_task_group')
1488  ->orderBy('sorting')
1489  ->execute()
1490  ->fetchAll();
1491  }
1492 
1493  /*************************
1494  *
1495  * RENDERING UTILITIES
1496  *
1497  *************************/
1503  protected function getTemplateMarkers()
1504  {
1505  return [
1506  'CONTENT' => $this->content,
1507  'TITLE' => $this->getLanguageService()->getLL('title')
1508  ];
1509  }
1510 
1514  protected function getButtons()
1515  {
1516  $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1517  // CSH
1518  $helpButton = $buttonBar->makeHelpButton()
1519  ->setModuleName('_MOD_' . $this->moduleName)
1520  ->setFieldName('');
1521  $buttonBar->addButton($helpButton);
1522  // Add and Reload
1523  if (empty($this->CMD) || $this->CMD === 'list' || $this->CMD === 'delete' || $this->CMD === 'stop' || $this->CMD === 'toggleHidden' || $this->CMD === 'setNextExecutionTime') {
1524  $reloadButton = $buttonBar->makeLinkButton()
1525  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.reload'))
1526  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL))
1527  ->setHref($this->moduleUri);
1528  $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1529  if ($this->MOD_SETTINGS['function'] === 'scheduler' && !empty($this->getRegisteredClasses())) {
1530  $addButton = $buttonBar->makeLinkButton()
1531  ->setTitle($this->getLanguageService()->getLL('action.add'))
1532  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-new', Icon::SIZE_SMALL))
1533  ->setHref($this->moduleUri . '&CMD=add');
1534  $buttonBar->addButton($addButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1535  }
1536  }
1537  // Close and Save
1538  if ($this->CMD === 'add' || $this->CMD === 'edit') {
1539  // Close
1540  $closeButton = $buttonBar->makeLinkButton()
1541  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:cancel'))
1542  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-close', Icon::SIZE_SMALL))
1543  ->setOnClick('document.location=' . GeneralUtility::quoteJSvalue($this->moduleUri))
1544  ->setHref('#');
1545  $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1546  // Save, SaveAndClose, SaveAndNew
1547  $saveButtonDropdown = $buttonBar->makeSplitButton();
1548  $saveButton = $buttonBar->makeInputButton()
1549  ->setName('CMD')
1550  ->setValue('save')
1551  ->setForm('tx_scheduler_form')
1552  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
1553  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:save'));
1554  $saveButtonDropdown->addItem($saveButton);
1555  $saveAndNewButton = $buttonBar->makeInputButton()
1556  ->setName('CMD')
1557  ->setValue('savenew')
1558  ->setForm('tx_scheduler_form')
1559  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-new', Icon::SIZE_SMALL))
1560  ->setTitle($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.saveAndCreateNewTask'));
1561  $saveButtonDropdown->addItem($saveAndNewButton);
1562  $saveAndCloseButton = $buttonBar->makeInputButton()
1563  ->setName('CMD')
1564  ->setValue('saveclose')
1565  ->setForm('tx_scheduler_form')
1566  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-close', Icon::SIZE_SMALL))
1567  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:saveAndClose'));
1568  $saveButtonDropdown->addItem($saveAndCloseButton);
1569  $buttonBar->addButton($saveButtonDropdown, ButtonBar::BUTTON_POSITION_LEFT, 3);
1570  }
1571  // Edit
1572  if ($this->CMD === 'edit') {
1573  $deleteButton = $buttonBar->makeInputButton()
1574  ->setName('CMD')
1575  ->setValue('delete')
1576  ->setForm('tx_scheduler_form')
1577  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-edit-delete', Icon::SIZE_SMALL))
1578  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:delete'));
1579  $buttonBar->addButton($deleteButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
1580  }
1581  // Shortcut
1582  $shortcutButton = $buttonBar->makeShortcutButton()
1583  ->setModuleName($this->moduleName)
1584  ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
1585  ->setSetVariables(['function']);
1586  $buttonBar->addButton($shortcutButton);
1587  }
1588 
1592  protected function getServerTime()
1593  {
1594  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e';
1595  return date($dateFormat) . ', GMT ' . date('P') . ')';
1596  }
1597 
1603  protected function getBackendUser()
1604  {
1605  return $GLOBALS['BE_USER'];
1606  }
1607 }
mainAction(ServerRequestInterface $request, ResponseInterface $response)
static makeInstance($className,... $constructorArguments)
$fields
Definition: pages.php:4
static mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']