TYPO3 CMS  TYPO3_7-6
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 
33 
38 {
44  protected $submittedData = [];
45 
52  protected $messages = [];
53 
57  protected $cshKey;
58 
62  protected $scheduler;
63 
67  protected $backendTemplatePath = '';
68 
72  protected $view;
73 
79  protected $moduleName = 'system_txschedulerM1';
80 
84  protected $moduleUri;
85 
91  protected $moduleTemplate;
92 
96  public function __construct()
97  {
98  $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
99  $this->getLanguageService()->includeLLFile('EXT:scheduler/Resources/Private/Language/locallang.xlf');
100  $this->MCONF = [
101  'name' => $this->moduleName,
102  ];
103  $this->cshKey = '_MOD_' . $this->moduleName;
104  $this->backendTemplatePath = ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Backend/SchedulerModule/';
105  $this->view = GeneralUtility::makeInstance(\TYPO3\CMS\Fluid\View\StandaloneView::class);
106  $this->view->getRequest()->setControllerExtensionName('scheduler');
107  $this->moduleUri = BackendUtility::getModuleUrl($this->moduleName);
108 
109  $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
110  $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
111  $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/SplitButtons');
112  }
113 
119  public function init()
120  {
121  parent::init();
122 
123  // Create scheduler instance
124  $this->scheduler = GeneralUtility::makeInstance(\TYPO3\CMS\Scheduler\Scheduler::class);
125  }
126 
132  public function menuConfig()
133  {
134  $this->MOD_MENU = [
135  'function' => [
136  'scheduler' => $this->getLanguageService()->getLL('function.scheduler'),
137  'check' => $this->getLanguageService()->getLL('function.check'),
138  'info' => $this->getLanguageService()->getLL('function.info')
139  ]
140  ];
141  parent::menuConfig();
142  }
143 
149  public function main()
150  {
151  // Access check!
152  // The page will show only if user has admin rights
153  if ($this->getBackendUser()->isAdmin()) {
154  // Set the form
155  $this->content = '<form name="tx_scheduler_form" id="tx_scheduler_form" method="post" action="">';
156 
157  // Prepare main content
158  $this->content .= '<h1>' . $this->getLanguageService()->getLL('function.' . $this->MOD_SETTINGS['function']) . '</h1>';
159  $this->content .= $this->getModuleContent();
160  $this->content .= '</form>';
161  } else {
162  // If no access, only display the module's title
163  $this->content = '<h1>' . $this->getLanguageService()->getLL('title.') . '</h1>';
164  $this->content .='<div style="padding-top: 5px;"></div>';
165  }
166  $this->getButtons();
167  $this->getModuleMenu();
168  }
169 
173  protected function getModuleMenu()
174  {
175  $menu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
176  $menu->setIdentifier('SchedulerJumpMenu');
177 
178  foreach ($this->MOD_MENU['function'] as $controller => $title) {
179  $item = $menu
180  ->makeMenuItem()
181  ->setHref(
182  BackendUtility::getModuleUrl(
183  $this->moduleName,
184  [
185  'id' => $this->id,
186  'SET' => [
187  'function' => $controller
188  ]
189  ]
190  )
191  )
192  ->setTitle($title);
193  if ($controller === $this->MOD_SETTINGS['function']) {
194  $item->setActive(true);
195  }
196  $menu->addMenuItem($item);
197  }
198  $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);
199  }
200 
206  protected function getModuleContent()
207  {
208  $content = '';
209  $sectionTitle = '';
210  // Get submitted data
211  $this->submittedData = GeneralUtility::_GPmerged('tx_scheduler');
212  $this->submittedData['uid'] = (int)$this->submittedData['uid'];
213  // If a save command was submitted, handle saving now
214  if ($this->CMD === 'save' || $this->CMD === 'saveclose' || $this->CMD === 'savenew') {
215  $previousCMD = GeneralUtility::_GP('previousCMD');
216  // First check the submitted data
217  $result = $this->preprocessData();
218  // If result is ok, proceed with saving
219  if ($result) {
220  $this->saveTask();
221  if ($this->CMD === 'saveclose') {
222  // Unset command, so that default screen gets displayed
223  unset($this->CMD);
224  } elseif ($this->CMD === 'save') {
225  // After saving a "add form", return to edit
226  $this->CMD = 'edit';
227  } elseif ($this->CMD === 'savenew') {
228  // Unset submitted data, so that empty form gets displayed
229  unset($this->submittedData);
230  // After saving a "add/edit form", return to add
231  $this->CMD = 'add';
232  } else {
233  // Return to edit form
234  $this->CMD = $previousCMD;
235  }
236  } else {
237  $this->CMD = $previousCMD;
238  }
239  }
240 
241  // Handle chosen action
242  switch ((string)$this->MOD_SETTINGS['function']) {
243  case 'scheduler':
244  $this->executeTasks();
245 
246  switch ($this->CMD) {
247  case 'add':
248  case 'edit':
249  try {
250  // Try adding or editing
251  $content .= $this->editTaskAction();
252  $sectionTitle = $this->getLanguageService()->getLL('action.' . $this->CMD);
253  } catch (\Exception $e) {
254  if ($e->getCode() === 1305100019) {
255  // Invalid controller class name exception
256  $this->addMessage($e->getMessage(), FlashMessage::ERROR);
257  }
258  // An exception may also happen when the task to
259  // edit could not be found. In this case revert
260  // to displaying the list of tasks
261  // It can also happen when attempting to edit a running task
262  $content .= $this->listTasksAction();
263  }
264  break;
265  case 'delete':
266  $this->deleteTask();
267  $content .= $this->listTasksAction();
268  break;
269  case 'stop':
270  $this->stopTask();
271  $content .= $this->listTasksAction();
272  break;
273  case 'toggleHidden':
274  $this->toggleDisableAction();
275  $content .= $this->listTasksAction();
276  break;
277  case 'list':
278 
279  default:
280  $content .= $this->listTasksAction();
281  }
282  break;
283 
284  // Setup check screen
285  case 'check':
286  // @todo move check to the report module
287  $content .= $this->checkScreenAction();
288  break;
289 
290  // Information screen
291  case 'info':
292  $content .= $this->infoScreenAction();
293  break;
294  }
295  // Wrap the content
296  return '<h2>' . $sectionTitle . '</h2><div class="tx_scheduler_mod1">' . $content . '</div>';
297  }
298 
307  public function mainAction(ServerRequestInterface $request, ResponseInterface $response)
308  {
309  $GLOBALS['SOBE'] = $this;
310  $this->init();
311  $this->main();
312 
313  $this->moduleTemplate->setContent($this->content);
314  $response->getBody()->write($this->moduleTemplate->renderContent());
315  return $response;
316  }
317 
324  public function render()
325  {
327  echo $this->content;
328  }
329 
337  protected function checkSchedulerUser()
338  {
339  $schedulerUserStatus = -1;
340  // Assemble base WHERE clause
341  $where = 'username = \'_cli_scheduler\' AND admin = 0' . BackendUtility::deleteClause('be_users');
342  // Check if user exists at all
343  $res = $this->getDatabaseConnection()->exec_SELECTquery('1', 'be_users', $where);
344  if ($this->getDatabaseConnection()->sql_fetch_assoc($res)) {
345  $schedulerUserStatus = 0;
346  $this->getDatabaseConnection()->sql_free_result($res);
347  // Check if user exists and is enabled
348  $res = $this->getDatabaseConnection()->exec_SELECTquery('1', 'be_users', $where . BackendUtility::BEenableFields('be_users'));
349  if ($this->getDatabaseConnection()->sql_fetch_assoc($res)) {
350  $schedulerUserStatus = 1;
351  }
352  }
353  $this->getDatabaseConnection()->sql_free_result($res);
354  return $schedulerUserStatus;
355  }
356 
362  protected function createSchedulerUser()
363  {
364  // Check _cli_scheduler user status
365  $checkUser = $this->checkSchedulerUser();
366  // Prepare default message
367  $message = $this->getLanguageService()->getLL('msg.userExists');
368  $severity = FlashMessage::WARNING;
369  // If the user does not exist, try creating it
370  if ($checkUser == -1) {
371  // Prepare necessary data for _cli_scheduler user creation
372  $password = StringUtility::getUniqueId('scheduler');
374  $objInstanceSaltedPW = SaltFactory::getSaltingInstance();
375  $password = $objInstanceSaltedPW->getHashedPassword($password);
376  }
377  $data = ['be_users' => ['NEW' => ['username' => '_cli_scheduler', 'password' => $password, 'pid' => 0]]];
379  $tcemain = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
380  $tcemain->stripslashes_values = 0;
381  $tcemain->start($data, []);
382  $tcemain->process_datamap();
383  // Check if a new uid was indeed generated (i.e. a new record was created)
384  // (counting TCEmain errors doesn't work as some failures don't report errors)
385  $numberOfNewIDs = count($tcemain->substNEWwithIDs);
386  if ($numberOfNewIDs === 1) {
387  $message = $this->getLanguageService()->getLL('msg.userCreated');
388  $severity = FlashMessage::OK;
389  } else {
390  $message = $this->getLanguageService()->getLL('msg.userNotCreated');
391  $severity = FlashMessage::ERROR;
392  }
393  }
394  $this->addMessage($message, $severity);
395  }
396 
403  protected function checkScreenAction()
404  {
405  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'CheckScreen.html');
406 
407  // First, check if _cli_scheduler user creation was requested
408  if ($this->CMD === 'user') {
409  $this->createSchedulerUser();
410  }
411 
412  // Display information about last automated run, as stored in the system registry
414  $registry = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Registry::class);
415  $lastRun = $registry->get('tx_scheduler', 'lastRun');
416  if (!is_array($lastRun)) {
417  $message = $this->getLanguageService()->getLL('msg.noLastRun');
419  } else {
420  if (empty($lastRun['end']) || empty($lastRun['start']) || empty($lastRun['type'])) {
421  $message = $this->getLanguageService()->getLL('msg.incompleteLastRun');
423  } else {
424  $startDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['start']);
425  $startTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['start']);
426  $endDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['end']);
427  $endTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['end']);
428  $label = 'automatically';
429  if ($lastRun['type'] === 'manual') {
430  $label = 'manually';
431  }
432  $type = $this->getLanguageService()->getLL('label.' . $label);
433  $message = sprintf($this->getLanguageService()->getLL('msg.lastRun'), $type, $startDate, $startTime, $endDate, $endTime);
434  $severity = InfoboxViewHelper::STATE_INFO;
435  }
436  }
437  $this->view->assign('lastRunMessage', $message);
438  $this->view->assign('lastRunSeverity', $severity);
439 
440  // Check CLI user
441  $checkUser = $this->checkSchedulerUser();
442  if ($checkUser == -1) {
443  $link = $this->moduleUri . '&SET[function]=check&CMD=user';
444  $message = sprintf($this->getLanguageService()->getLL('msg.schedulerUserMissing'), htmlspecialchars($link));
445  $severity = InfoboxViewHelper::STATE_ERROR;
446  } elseif ($checkUser == 0) {
447  $message = $this->getLanguageService()->getLL('msg.schedulerUserFoundButDisabled');
449  } else {
450  $message = $this->getLanguageService()->getLL('msg.schedulerUserFound');
451  $severity = InfoboxViewHelper::STATE_OK;
452  }
453  $this->view->assign('cliUserMessage', $message);
454  $this->view->assign('cliUserSeverity', $severity);
455 
456  // Check if CLI script is executable or not
457  $script = PATH_typo3 . 'cli_dispatch.phpsh';
458  $this->view->assign('script', $script);
459 
460  // Skip this check if running Windows, as rights do not work the same way on this platform
461  // (i.e. the script will always appear as *not* executable)
462  if (TYPO3_OS === 'WIN') {
463  $isExecutable = true;
464  } else {
465  $isExecutable = is_executable($script);
466  }
467  if ($isExecutable) {
468  $message = $this->getLanguageService()->getLL('msg.cliScriptExecutable');
469  $severity = InfoboxViewHelper::STATE_OK;
470  } else {
471  $message = $this->getLanguageService()->getLL('msg.cliScriptNotExecutable');
472  $severity = InfoboxViewHelper::STATE_ERROR;
473  }
474  $this->view->assign('isExecutableMessage', $message);
475  $this->view->assign('isExecutableSeverity', $severity);
476 
477  return $this->view->render();
478  }
479 
485  protected function infoScreenAction()
486  {
487  $registeredClasses = $this->getRegisteredClasses();
488  // No classes available, display information message
489  if (empty($registeredClasses)) {
490  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreenNoClasses.html');
491  return $this->view->render();
492  }
493 
494  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreen.html');
495  $this->view->assign('registeredClasses', $registeredClasses);
496 
497  return $this->view->render();
498  }
499 
506  protected function renderTaskProgressBar($progress)
507  {
508  $progressText = $this->getLanguageService()->getLL('status.progress') . ':&nbsp;' . $progress . '%';
509  return '<div class="progress">'
510  . '<div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="' . $progress . '" aria-valuemin="0" aria-valuemax="100" style="width: ' . $progress . '%;">' . $progressText . '</div>'
511  . '</div>';
512  }
513 
519  protected function deleteTask()
520  {
521  try {
522  // Try to fetch the task and delete it
523  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
524  // If the task is currently running, it may not be deleted
525  if ($task->isExecutionRunning()) {
526  $this->addMessage($this->getLanguageService()->getLL('msg.maynotDeleteRunningTask'), FlashMessage::ERROR);
527  } else {
528  if ($this->scheduler->removeTask($task)) {
529  $this->getBackendUser()->writeLog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was deleted', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
530  $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
531  } else {
532  $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
533  }
534  }
535  } catch (\UnexpectedValueException $e) {
536  // The task could not be unserialized properly, simply delete the database record
537  $result = $this->getDatabaseConnection()->exec_DELETEquery('tx_scheduler_task', 'uid = ' . (int)$this->submittedData['uid']);
538  if ($result) {
539  $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
540  } else {
541  $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
542  }
543  } catch (\OutOfBoundsException $e) {
544  // The task was not found, for some reason
545  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
546  }
547  }
548 
557  protected function stopTask()
558  {
559  try {
560  // Try to fetch the task and stop it
561  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
562  if ($task->isExecutionRunning()) {
563  // If the task is indeed currently running, clear marked executions
564  $result = $task->unmarkAllExecutions();
565  if ($result) {
566  $this->addMessage($this->getLanguageService()->getLL('msg.stopSuccess'));
567  } else {
568  $this->addMessage($this->getLanguageService()->getLL('msg.stopError'), FlashMessage::ERROR);
569  }
570  } else {
571  // The task is not running, nothing to unmark
572  $this->addMessage($this->getLanguageService()->getLL('msg.maynotStopNonRunningTask'), FlashMessage::WARNING);
573  }
574  } catch (\Exception $e) {
575  // The task was not found, for some reason
576  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
577  }
578  }
579 
585  protected function toggleDisableAction()
586  {
587  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
588  $task->setDisabled(!$task->isDisabled());
589  // If a disabled single task is enabled again, we register it for a
590  // single execution at next scheduler run.
591  if ($task->getType() === AbstractTask::TYPE_SINGLE) {
592  $task->registerSingleExecution(time());
593  }
594  $task->save();
595  }
596 
602  protected function editTaskAction()
603  {
604  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'EditTask.html');
605 
606  $registeredClasses = $this->getRegisteredClasses();
607  $registeredTaskGroups = $this->getRegisteredTaskGroups();
608 
609  $taskInfo = [];
610  $task = null;
611  $process = 'edit';
612 
613  if ($this->submittedData['uid'] > 0) {
614  // If editing, retrieve data for existing task
615  try {
616  $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
617  // If there's a registered execution, the task should not be edited
618  if (!empty($taskRecord['serialized_executions'])) {
619  $this->addMessage($this->getLanguageService()->getLL('msg.maynotEditRunningTask'), FlashMessage::ERROR);
620  throw new \LogicException('Runnings tasks cannot not be edited', 1251232849);
621  }
622 
623  // Get the task object
625  $task = unserialize($taskRecord['serialized_task_object']);
626 
627  // Set some task information
628  $taskInfo['disable'] = $taskRecord['disable'];
629  $taskInfo['description'] = $taskRecord['description'];
630  $taskInfo['task_group'] = $taskRecord['task_group'];
631 
632  // Check that the task object is valid
633  if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
634  // The task object is valid, process with fetching current data
635  $taskInfo['class'] = get_class($task);
636  // Get execution information
637  $taskInfo['start'] = (int)$task->getExecution()->getStart();
638  $taskInfo['end'] = (int)$task->getExecution()->getEnd();
639  $taskInfo['interval'] = $task->getExecution()->getInterval();
640  $taskInfo['croncmd'] = $task->getExecution()->getCronCmd();
641  $taskInfo['multiple'] = $task->getExecution()->getMultiple();
642  if (!empty($taskInfo['interval']) || !empty($taskInfo['croncmd'])) {
643  // Guess task type from the existing information
644  // If an interval or a cron command is defined, it's a recurring task
645  $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
646  $taskInfo['frequency'] = $taskInfo['interval'] ?: $taskInfo['croncmd'];
647  } else {
648  // It's not a recurring task
649  // Make sure interval and cron command are both empty
650  $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
651  $taskInfo['frequency'] = '';
652  $taskInfo['end'] = 0;
653  }
654  } else {
655  // The task object is not valid
656  // Issue error message
657  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.invalidTaskClassEdit'), get_class($task)), FlashMessage::ERROR);
658  // Initialize empty values
659  $taskInfo['start'] = 0;
660  $taskInfo['end'] = 0;
661  $taskInfo['frequency'] = '';
662  $taskInfo['multiple'] = false;
663  $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
664  }
665  } catch (\OutOfBoundsException $e) {
666  // Add a message and continue throwing the exception
667  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
668  throw $e;
669  }
670  } else {
671  // If adding a new object, set some default values
672  $taskInfo['class'] = key($registeredClasses);
673  $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
674  $taskInfo['start'] = $GLOBALS['EXEC_TIME'];
675  $taskInfo['end'] = '';
676  $taskInfo['frequency'] = '';
677  $taskInfo['multiple'] = 0;
678  $process = 'add';
679  }
680 
681  // If some data was already submitted, use it to override
682  // existing data
683  if (!empty($this->submittedData)) {
685  }
686 
687  // Get the extra fields to display for each task that needs some
688  $allAdditionalFields = [];
689  if ($process === 'add') {
690  foreach ($registeredClasses as $class => $registrationInfo) {
691  if (!empty($registrationInfo['provider'])) {
693  $providerObject = GeneralUtility::getUserObj($registrationInfo['provider']);
694  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
695  $additionalFields = $providerObject->getAdditionalFields($taskInfo, null, $this);
696  $allAdditionalFields = array_merge($allAdditionalFields, [$class => $additionalFields]);
697  }
698  }
699  }
700  } else {
701  if (!empty($registeredClasses[$taskInfo['class']]['provider'])) {
702  $providerObject = GeneralUtility::getUserObj($registeredClasses[$taskInfo['class']]['provider']);
703  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
704  $allAdditionalFields[$taskInfo['class']] = $providerObject->getAdditionalFields($taskInfo, $task, $this);
705  }
706  }
707  }
708 
709  // Load necessary JavaScript
710  $this->getPageRenderer()->loadJquery();
711  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
712  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker');
713 
714  // Start rendering the add/edit form
715  $this->view->assign('uid', htmlspecialchars($this->submittedData['uid']));
716  $this->view->assign('cmd', htmlspecialchars($this->CMD));
717 
718  $table = [];
719 
720  // Disable checkbox
721  $label = '<label for="task_disable">' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:disable') . '</label>';
722  $table[] =
723  '<div class="form-section" id="task_disable_row"><div class="form-group">'
724  . BackendUtility::wrapInHelp($this->cshKey, 'task_disable', $label)
725  . '<div class="form-control-wrap">'
726  . '<input type="hidden" name="tx_scheduler[disable]" value="0">'
727  . '<input class="checkbox" type="checkbox" name="tx_scheduler[disable]" value="1" id="task_disable" ' . ($taskInfo['disable'] ? ' checked="checked"' : '') . '>'
728  . '</div>'
729  . '</div></div>';
730 
731  // Task class selector
732  $label = '<label for="task_class">' . $this->getLanguageService()->getLL('label.class') . '</label>';
733 
734  // On editing, don't allow changing of the task class, unless it was not valid
735  if ($this->submittedData['uid'] > 0 && !empty($taskInfo['class'])) {
736  $cell = '<div>' . $registeredClasses[$taskInfo['class']]['title'] . ' (' . $registeredClasses[$taskInfo['class']]['extension'] . ')</div>';
737  $cell .= '<input type="hidden" name="tx_scheduler[class]" id="task_class" value="' . htmlspecialchars($taskInfo['class']) . '">';
738  } else {
739  $cell = '<select name="tx_scheduler[class]" id="task_class" class="form-control">';
740  // Group registered classes by classname
741  $groupedClasses = [];
742  foreach ($registeredClasses as $class => $classInfo) {
743  $groupedClasses[$classInfo['extension']][$class] = $classInfo;
744  }
745  ksort($groupedClasses);
746  // Loop on all grouped classes to display a selector
747  foreach ($groupedClasses as $extension => $class) {
748  $cell .= '<optgroup label="' . htmlspecialchars($extension) . '">';
749  foreach ($groupedClasses[$extension] as $class => $classInfo) {
750  $selected = $class == $taskInfo['class'] ? ' selected="selected"' : '';
751  $cell .= '<option value="' . htmlspecialchars($class) . '"' . 'title="' . htmlspecialchars($classInfo['description']) . '" ' . $selected . '>' . htmlspecialchars($classInfo['title']) . '</option>';
752  }
753  $cell .= '</optgroup>';
754  }
755  $cell .= '</select>';
756  }
757  $table[] =
758  '<div class="form-section" id="task_class_row"><div class="form-group">'
759  . BackendUtility::wrapInHelp($this->cshKey, 'task_class', $label)
760  . '<div class="form-control-wrap">'
761  . $cell
762  . '</div>'
763  . '</div></div>';
764 
765  // Task type selector
766  $label = '<label for="task_type">' . $this->getLanguageService()->getLL('label.type') . '</label>';
767  $table[] =
768  '<div class="form-section" id="task_type_row"><div class="form-group">'
769  . BackendUtility::wrapInHelp($this->cshKey, 'task_type', $label)
770  . '<div class="form-control-wrap">'
771  . '<select name="tx_scheduler[type]" id="task_type" class="form-control">'
772  . '<option value="1" ' . ((int)$taskInfo['type'] === AbstractTask::TYPE_SINGLE ? ' selected="selected"' : '') . '>' . $this->getLanguageService()->getLL('label.type.single') . '</option>'
773  . '<option value="2" ' . ((int)$taskInfo['type'] === AbstractTask::TYPE_RECURRING ? ' selected="selected"' : '') . '>' . $this->getLanguageService()->getLL('label.type.recurring') . '</option>'
774  . '</select>'
775  . '</div>'
776  . '</div></div>';
777 
778  // Task group selector
779  $label = '<label for="task_group">' . $this->getLanguageService()->getLL('label.group') . '</label>';
780  $cell = '<select name="tx_scheduler[task_group]" id="task_class" class="form-control">';
781 
782  // Loop on all groups to display a selector
783  $cell .= '<option value="0" title=""></option>';
784  foreach ($registeredTaskGroups as $taskGroup) {
785  $selected = $taskGroup['uid'] == $taskInfo['task_group'] ? ' selected="selected"' : '';
786  $cell .= '<option value="' . $taskGroup['uid'] . '"' . 'title="';
787  $cell .= htmlspecialchars($taskGroup['groupName']) . '"' . $selected . '>';
788  $cell .= htmlspecialchars($taskGroup['groupName']) . '</option>';
789  }
790  $cell .= '</select>';
791 
792  $table[] =
793  '<div class="form-section" id="task_group_row"><div class="form-group">'
794  . BackendUtility::wrapInHelp($this->cshKey, 'task_group', $label)
795  . '<div class="form-control-wrap">'
796  . $cell
797  . '</div>'
798  . '</div></div>';
799 
800  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '%H:%M %m-%d-%Y' : '%H:%M %d-%m-%Y';
801 
802  $label = '<label for="tceforms-datetimefield-task_start">' . BackendUtility::wrapInHelp($this->cshKey, 'task_start', $this->getLanguageService()->getLL('label.start')) . '</label>';
803  $value = ($taskInfo['start'] > 0 ? strftime($dateFormat, $taskInfo['start']) : '');
804  $table[] =
805  '<div class="form-section"><div class="row"><div class="form-group col-sm-6" id="task_start_col">'
806  . $label
807  . '<div class="form-control-wrap">'
808  . '<div class="input-group" id="tceforms-datetimefield-task_start_row-wrapper">'
809  . '<input name="tx_scheduler[start]_hr" value="' . htmlspecialchars($value) . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="datetime" data-date-offset="0" type="text" id="tceforms-datetimefield-task_start_row">'
810  . '<input name="tx_scheduler[start]" value="' . htmlspecialchars($taskInfo['start']) . '" type="hidden">'
811  . '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_start_row"><span class="fa fa-calendar"></span></label></span>'
812  . '</div>'
813  . '</div>'
814  . '</div>';
815 
816  // End date/time field
817  // NOTE: datetime fields need a special id naming scheme
818  $value = ($taskInfo['end'] > 0 ? strftime($dateFormat, $taskInfo['end']) : '');
819  $label = '<label for="tceforms-datetimefield-task_end">' . $this->getLanguageService()->getLL('label.end') . '</label>';
820  $table[] =
821  '<div class="form-group col-sm-6" id="task_end_col">'
822  . BackendUtility::wrapInHelp($this->cshKey, 'task_end', $label)
823  . '<div class="form-control-wrap">'
824  . '<div class="input-group" id="tceforms-datetimefield-task_end_row-wrapper">'
825  . '<input name="tx_scheduler[end]_hr" value="' . htmlspecialchars($value) . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="datetime" data-date-offset="0" type="text" id="tceforms-datetimefield-task_end_row">'
826  . '<input name="tx_scheduler[end]" value="' . htmlspecialchars($taskInfo['end']) . '" type="hidden">'
827  . '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_end_row"><span class="fa fa-calendar"></span></label></span>'
828  . '</div>'
829  . '</div>'
830  . '</div></div></div>';
831 
832  // Frequency input field
833  $label = '<label for="task_frequency">' . $this->getLanguageService()->getLL('label.frequency.long') . '</label>';
834  $table[] =
835  '<div class="form-section" id="task_frequency_row"><div class="form-group">'
836  . BackendUtility::wrapInHelp($this->cshKey, 'task_frequency', $label)
837  . '<div class="form-control-wrap">'
838  . '<input type="text" name="tx_scheduler[frequency]" class="form-control" id="task_frequency" value="' . htmlspecialchars($taskInfo['frequency']) . '">'
839  . '</div>'
840  . '</div></div>';
841 
842  // Multiple execution selector
843  $label = '<label for="task_multiple">' . $this->getLanguageService()->getLL('label.parallel.long') . '</label>';
844  $table[] =
845  '<div class="form-section" id="task_multiple_row"><div class="form-group">'
846  . BackendUtility::wrapInHelp($this->cshKey, 'task_multiple', $label)
847  . '<div class="form-control-wrap">'
848  . '<input type="hidden" name="tx_scheduler[multiple]" value="0">'
849  . '<input class="checkbox" type="checkbox" name="tx_scheduler[multiple]" value="1" id="task_multiple" ' . ($taskInfo['multiple'] ? 'checked="checked"' : '') . '>'
850  . '</div>'
851  . '</div></div>';
852 
853  // Description
854  $label = '<label for="task_description">' . $this->getLanguageService()->getLL('label.description') . '</label>';
855  $table[] =
856  '<div class="form-section" id="task_description_row"><div class="form-group">'
857  . BackendUtility::wrapInHelp($this->cshKey, 'task_description', $label)
858  . '<div class="form-control-wrap">'
859  . '<textarea class="form-control" name="tx_scheduler[description]">' . htmlspecialchars($taskInfo['description']) . '</textarea>'
860  . '</div>'
861  . '</div></div>';
862 
863  // Display additional fields
864  foreach ($allAdditionalFields as $class => $fields) {
865  if ($class == $taskInfo['class']) {
866  $additionalFieldsStyle = '';
867  } else {
868  $additionalFieldsStyle = ' style="display: none"';
869  }
870  // Add each field to the display, if there are indeed any
871  if (isset($fields) && is_array($fields)) {
872  foreach ($fields as $fieldID => $fieldInfo) {
873  $label = '<label for="' . $fieldID . '">' . $this->getLanguageService()->sL($fieldInfo['label']) . '</label>';
874  $htmlClassName = strtolower(str_replace('\\', '-', $class));
875 
876  $table[] =
877  '<div class="form-section extraFields extra_fields_' . $htmlClassName . '" ' . $additionalFieldsStyle . ' id="' . $fieldID . '_row"><div class="form-group">'
878  . BackendUtility::wrapInHelp($fieldInfo['cshKey'], $fieldInfo['cshLabel'], $label)
879  . '<div class="form-control-wrap">' . $fieldInfo['code'] . '</div>'
880  . '</div></div>';
881  }
882  }
883  }
884 
885  $this->view->assign('table', implode(LF, $table));
886 
887  // Server date time
888  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e';
889  $this->view->assign('now', date($dateFormat) . ', GMT ' . date('P') . ')');
890 
891  return $this->view->render();
892  }
893 
899  protected function executeTasks()
900  {
901  // Make sure next automatic scheduler-run is scheduled
902  if (GeneralUtility::_POST('go') !== null) {
903  $this->scheduler->scheduleNextSchedulerRunUsingAtDaemon();
904  }
905  // Continue if some elements have been chosen for execution
906  if (isset($this->submittedData['execute']) && !empty($this->submittedData['execute'])) {
907  // Get list of registered classes
908  $registeredClasses = $this->getRegisteredClasses();
909  // Loop on all selected tasks
910  foreach ($this->submittedData['execute'] as $uid) {
911  try {
912  // Try fetching the task
913  $task = $this->scheduler->fetchTask($uid);
914  $class = get_class($task);
915  $name = $registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')';
916  // Now try to execute it and report on outcome
917  try {
918  $result = $this->scheduler->executeTask($task);
919  if ($result) {
920  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executed'), $name));
921  } else {
922  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.notExecuted'), $name), FlashMessage::ERROR);
923  }
924  } catch (\Exception $e) {
925  // An exception was thrown, display its message as an error
926  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $name, $e->getMessage()), FlashMessage::ERROR);
927  }
928  } catch (\OutOfBoundsException $e) {
929  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $uid), FlashMessage::ERROR);
930  } catch (\UnexpectedValueException $e) {
931  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $uid, $e->getMessage()), FlashMessage::ERROR);
932  }
933  }
934  // Record the run in the system registry
935  $this->scheduler->recordLastRun('manual');
936  // Make sure to switch to list view after execution
937  $this->CMD = 'list';
938  }
939  }
940 
946  protected function listTasksAction()
947  {
948  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasks.html');
949 
950  // Define display format for dates
951  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
952 
953  // Get list of registered classes
954  $registeredClasses = $this->getRegisteredClasses();
955  // Get list of registered task groups
956  $registeredTaskGroups = $this->getRegisteredTaskGroups();
957 
958  // add an empty entry for non-grouped tasks
959  // add in front of list
960  array_unshift($registeredTaskGroups, ['uid' => 0, 'groupName' => '']);
961 
962  // Get all registered tasks
963  // Just to get the number of entries
964  $query = [
965  'SELECT' => '
966  tx_scheduler_task.*,
967  tx_scheduler_task_group.groupName as taskGroupName,
968  tx_scheduler_task_group.description as taskGroupDescription,
969  tx_scheduler_task_group.deleted as isTaskGroupDeleted
970  ',
971  'FROM' => '
972  tx_scheduler_task
973  LEFT JOIN tx_scheduler_task_group ON tx_scheduler_task_group.uid = tx_scheduler_task.task_group
974  ',
975  'WHERE' => '1=1',
976  'GROUPBY' => '',
977  'ORDERBY' => 'tx_scheduler_task_group.sorting'
978  ];
979  $res = $this->getDatabaseConnection()->exec_SELECT_queryArray($query);
980  $numRows = $this->getDatabaseConnection()->sql_num_rows($res);
981 
982  // No tasks defined, display information message
983  if ($numRows == 0) {
984  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasksNoTasks.html');
985  return $this->view->render();
986  } else {
987  $this->getPageRenderer()->loadJquery();
988  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
989  $table = [];
990  // Header row
991  $table[] =
992  '<thead><tr>'
993  . '<th><a href="#" id="checkall" title="' . $this->getLanguageService()->getLL('label.checkAll', true) . '" class="icon">' . $this->moduleTemplate->getIconFactory()->getIcon('actions-document-select', Icon::SIZE_SMALL)->render() . '</a></th>'
994  . '<th>' . $this->getLanguageService()->getLL('label.id', true) . '</th>'
995  . '<th>' . $this->getLanguageService()->getLL('task', true) . '</th>'
996  . '<th>' . $this->getLanguageService()->getLL('label.type', true) . '</th>'
997  . '<th>' . $this->getLanguageService()->getLL('label.frequency', true) . '</th>'
998  . '<th>' . $this->getLanguageService()->getLL('label.parallel', true) . '</th>'
999  . '<th>' . $this->getLanguageService()->getLL('label.lastExecution', true) . '</th>'
1000  . '<th>' . $this->getLanguageService()->getLL('label.nextExecution', true) . '</th>'
1001  . '<th></th>'
1002  . '</tr></thead>';
1003 
1004  // Loop on all tasks
1005  $temporaryResult = [];
1006  while ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) {
1007  if ($row['taskGroupName'] === null || $row['isTaskGroupDeleted'] === '1') {
1008  $row['taskGroupName'] = '';
1009  $row['taskGroupDescription'] = '';
1010  $row['task_group'] = 0;
1011  }
1012  $temporaryResult[$row['task_group']]['groupName'] = $row['taskGroupName'];
1013  $temporaryResult[$row['task_group']]['groupDescription'] = $row['taskGroupDescription'];
1014  $temporaryResult[$row['task_group']]['tasks'][] = $row;
1015  }
1016  $registeredClasses = $this->getRegisteredClasses();
1017  foreach ($temporaryResult as $taskGroup) {
1018  if (!empty($taskGroup['groupName'])) {
1019  $groupText = '<strong>' . htmlspecialchars($taskGroup['groupName']) . '</strong>';
1020  if (!empty($taskGroup['groupDescription'])) {
1021  $groupText .= '<br>' . nl2br(htmlspecialchars($taskGroup['groupDescription']));
1022  }
1023  $table[] = '<tr><td colspan="9">' . $groupText . '</td></tr>';
1024  }
1025 
1026  foreach ($taskGroup['tasks'] as $schedulerRecord) {
1027  // Define action icons
1028  $link = htmlspecialchars($this->moduleUri . '&CMD=edit&tx_scheduler[uid]=' . $schedulerRecord['uid']);
1029  $editAction = '<a class="btn btn-default" href="' . $link . '" title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:edit', true) . '" class="icon">' .
1030  $this->moduleTemplate->getIconFactory()->getIcon('actions-document-open', Icon::SIZE_SMALL)->render() . '</a>';
1031  if ((int)$schedulerRecord['disable'] === 1) {
1032  $translationKey = 'enable';
1033  $icon = $this->moduleTemplate->getIconFactory()->getIcon('actions-edit-unhide', Icon::SIZE_SMALL);
1034  } else {
1035  $translationKey = 'disable';
1036  $icon = $this->moduleTemplate->getIconFactory()->getIcon('actions-edit-hide', Icon::SIZE_SMALL);
1037  }
1038  $toggleHiddenAction = '<a class="btn btn-default" href="' . htmlspecialchars($this->moduleUri
1039  . '&CMD=toggleHidden&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" title="'
1040  . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:' . $translationKey, true)
1041  . '" class="icon">' . $icon->render() . '</a>';
1042  $deleteAction = '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars($this->moduleUri . '&CMD=delete&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" '
1043  . ' data-severity="warning"'
1044  . ' data-title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:delete', true) . '"'
1045  . ' data-button-close-text="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:cancel', true) . '"'
1046  . ' data-content="' . $this->getLanguageService()->getLL('msg.delete', true) . '"'
1047  . ' title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:delete', true) . '" class="icon">' .
1048  $this->moduleTemplate->getIconFactory()->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>';
1049  $stopAction = '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars($this->moduleUri . '&CMD=stop&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" '
1050  . ' data-severity="warning"'
1051  . ' data-title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:stop', true) . '"'
1052  . ' data-button-close-text="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:cancel', true) . '"'
1053  . ' data-content="' . $this->getLanguageService()->getLL('msg.stop', true) . '"'
1054  . ' title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:stop', true) . '" class="icon">' .
1055  $this->moduleTemplate->getIconFactory()->getIcon('actions-document-close', Icon::SIZE_SMALL)->render() . '</a>';
1056  $runAction = '<a class="btn btn-default" href="' . htmlspecialchars($this->moduleUri . '&tx_scheduler[execute][]=' . $schedulerRecord['uid']) . '" title="' . $this->getLanguageService()->getLL('action.run_task', true) . '" class="icon">' .
1057  $this->moduleTemplate->getIconFactory()->getIcon('extensions-scheduler-run-task', Icon::SIZE_SMALL)->render() . '</a>';
1058 
1059  // Define some default values
1060  $lastExecution = '-';
1061  $isRunning = false;
1062  $showAsDisabled = false;
1063  $startExecutionElement = '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
1064  // Restore the serialized task and pass it a reference to the scheduler object
1066  $task = unserialize($schedulerRecord['serialized_task_object']);
1067  $class = get_class($task);
1068  if ($class === '__PHP_Incomplete_Class' && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) {
1069  $class = $matches['classname'];
1070  }
1071  // Assemble information about last execution
1072  if (!empty($schedulerRecord['lastexecution_time'])) {
1073  $lastExecution = date($dateFormat, $schedulerRecord['lastexecution_time']);
1074  if ($schedulerRecord['lastexecution_context'] == 'CLI') {
1075  $context = $this->getLanguageService()->getLL('label.cron');
1076  } else {
1077  $context = $this->getLanguageService()->getLL('label.manual');
1078  }
1079  $lastExecution .= ' (' . $context . ')';
1080  }
1081 
1082  if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
1083  // The task object is valid
1084  $labels = [];
1085  $name = htmlspecialchars($registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')');
1086  $additionalInformation = $task->getAdditionalInformation();
1087  if ($task instanceof \TYPO3\CMS\Scheduler\ProgressProviderInterface) {
1088  $progress = round(floatval($task->getProgress()), 2);
1089  $name .= $this->renderTaskProgressBar($progress);
1090  }
1091  if (!empty($additionalInformation)) {
1092  $name .= '<div class="additional-information">' . nl2br(htmlspecialchars($additionalInformation)) . '</div>';
1093  }
1094  // Check if task currently has a running execution
1095  if (!empty($schedulerRecord['serialized_executions'])) {
1096  $labels[] = [
1097  'class' => 'success',
1098  'text' => $this->getLanguageService()->getLL('status.running')
1099  ];
1100  $isRunning = true;
1101  }
1102 
1103  // Prepare display of next execution date
1104  // If task is currently running, date is not displayed (as next hasn't been calculated yet)
1105  // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
1106  if ($isRunning || $schedulerRecord['disable']) {
1107  $nextDate = '-';
1108  } else {
1109  $nextDate = date($dateFormat, $schedulerRecord['nextexecution']);
1110  if (empty($schedulerRecord['nextexecution'])) {
1111  $nextDate = $this->getLanguageService()->getLL('none');
1112  } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
1113  $labels[] = [
1114  'class' => 'warning',
1115  'text' => $this->getLanguageService()->getLL('status.late'),
1116  'description' => $this->getLanguageService()->getLL('status.legend.scheduled')
1117  ];
1118  }
1119  }
1120  // Get execution type
1121  if ($task->getType() === AbstractTask::TYPE_SINGLE) {
1122  $execType = $this->getLanguageService()->getLL('label.type.single');
1123  $frequency = '-';
1124  } else {
1125  $execType = $this->getLanguageService()->getLL('label.type.recurring');
1126  if ($task->getExecution()->getCronCmd() == '') {
1127  $frequency = $task->getExecution()->getInterval();
1128  } else {
1129  $frequency = $task->getExecution()->getCronCmd();
1130  }
1131  }
1132  // Get multiple executions setting
1133  if ($task->getExecution()->getMultiple()) {
1134  $multiple = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:yes');
1135  } else {
1136  $multiple = $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:no');
1137  }
1138  // Define checkbox
1139  $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>';
1140 
1141  $actions = $editAction . $toggleHiddenAction . $deleteAction;
1142 
1143  // Check the disable status
1144  // Row is shown dimmed if task is disabled, unless it is still running
1145  if ($schedulerRecord['disable'] && !$isRunning) {
1146  $labels[] = [
1147  'class' => 'default',
1148  'text' => $this->getLanguageService()->getLL('status.disabled')
1149  ];
1150  $showAsDisabled = true;
1151  }
1152 
1153  // Show no action links (edit, delete) if task is running
1154  if ($isRunning) {
1155  $actions = $stopAction;
1156  } else {
1157  $actions .= $runAction;
1158  }
1159 
1160  // Check if the last run failed
1161  if (!empty($schedulerRecord['lastexecution_failure'])) {
1162  // Try to get the stored exception array
1164  $exceptionArray = @unserialize($schedulerRecord['lastexecution_failure']);
1165  // If the exception could not be unserialized, issue a default error message
1166  if (!is_array($exceptionArray) || empty($exceptionArray)) {
1167  $labelDescription = $this->getLanguageService()->getLL('msg.executionFailureDefault');
1168  } else {
1169  $labelDescription = sprintf($this->getLanguageService()->getLL('msg.executionFailureReport'), $exceptionArray['code'], $exceptionArray['message']);
1170  }
1171  $labels[] = [
1172  'class' => 'danger',
1173  'text' => $this->getLanguageService()->getLL('status.failure'),
1174  'description' => $labelDescription
1175  ];
1176  }
1177  // Format the execution status,
1178  // including failure feedback, if any
1179  $taskDesc = '';
1180  if ($schedulerRecord['description'] !== '') {
1181  $taskDesc = '<span class="description">' . nl2br(htmlspecialchars($schedulerRecord['description'])) . '</span>';
1182  }
1183  $taskName = '<span class="name"><a href="' . $link . '">' . $name . '</a></span>';
1184 
1185  $table[] =
1186  '<tr class="' . ($showAsDisabled ? 'disabled' : '') . '">'
1187  . '<td>' . $startExecutionElement . '</td>'
1188  . '<td class="right">' . $schedulerRecord['uid'] . '</td>'
1189  . '<td>' . $this->makeStatusLabel($labels) . $taskName . $taskDesc . '</td>'
1190  . '<td>' . $execType . '</td>'
1191  . '<td>' . $frequency . '</td>'
1192  . '<td>' . $multiple . '</td>'
1193  . '<td>' . $lastExecution . '</td>'
1194  . '<td>' . $nextDate . '</td>'
1195  . '<td nowrap="nowrap"><div class="btn-group" role="group">' . $actions . '</div></td>'
1196  . '</tr>';
1197  } else {
1198  // The task object is not valid
1199  // Prepare to issue an error
1200  $executionStatusOutput = '<span class="label label-danger">'
1201  . htmlspecialchars(sprintf(
1202  $this->getLanguageService()->getLL('msg.invalidTaskClass'),
1203  $class
1204  ))
1205  . '</span>';
1206  $table[] =
1207  '<tr>'
1208  . '<td>' . $startExecutionElement . '</td>'
1209  . '<td class="right">' . $schedulerRecord['uid'] . '</td>'
1210  . '<td colspan="6">' . $executionStatusOutput . '</td>'
1211  . '<td nowrap="nowrap"><div class="btn-group" role="group">'
1212  . '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'
1213  . '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'
1214  . $deleteAction
1215  . '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'
1216  . '</div></td>'
1217  . '</tr>';
1218  }
1219  }
1220  }
1221  $this->getDatabaseConnection()->sql_free_result($res);
1222 
1223  $this->view->assign('table', '<table class="table table-striped table-hover">' . implode(LF, $table) . '</table>');
1224 
1225  // Server date time
1226  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e';
1227  $this->view->assign('now', date($dateFormat) . ', GMT ' . date('P') . ')');
1228  }
1229 
1230  return $this->view->render();
1231  }
1232 
1239  protected function makeStatusLabel(array $labels)
1240  {
1241  $htmlLabels = [];
1242  foreach ($labels as $label) {
1243  if (empty($label['text'])) {
1244  continue;
1245  }
1246  $htmlLabels[] = '<span class="label label-' . htmlspecialchars($label['class']) . ' pull-right" title="' . htmlspecialchars($label['description']) . '">' . htmlspecialchars($label['text']) . '</span>';
1247  }
1248 
1249  return implode('&nbsp;', $htmlLabels);
1250  }
1251 
1257  protected function saveTask()
1258  {
1259  // If a task is being edited fetch old task data
1260  if (!empty($this->submittedData['uid'])) {
1261  try {
1262  $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
1264  $task = unserialize($taskRecord['serialized_task_object']);
1265  } catch (\OutOfBoundsException $e) {
1266  // If the task could not be fetched, issue an error message
1267  // and exit early
1268  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
1269  return;
1270  }
1271  // Register single execution
1272  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1273  $task->registerSingleExecution($this->submittedData['start']);
1274  } else {
1275  if (!empty($this->submittedData['croncmd'])) {
1276  // Definition by cron-like syntax
1277  $interval = 0;
1278  $cronCmd = $this->submittedData['croncmd'];
1279  } else {
1280  // Definition by interval
1281  $interval = $this->submittedData['interval'];
1282  $cronCmd = '';
1283  }
1284  // Register recurring execution
1285  $task->registerRecurringExecution($this->submittedData['start'], $interval, $this->submittedData['end'], $this->submittedData['multiple'], $cronCmd);
1286  }
1287  // Set disable flag
1288  $task->setDisabled($this->submittedData['disable']);
1289  // Set description
1290  $task->setDescription($this->submittedData['description']);
1291  // Set task group
1292  $task->setTaskGroup($this->submittedData['task_group']);
1293  // Save additional input values
1294  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1296  $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1297  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1298  $providerObject->saveAdditionalFields($this->submittedData, $task);
1299  }
1300  }
1301  // Save to database
1302  $result = $this->scheduler->saveTask($task);
1303  if ($result) {
1304  $this->getBackendUser()->writeLog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was updated', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1305  $this->addMessage($this->getLanguageService()->getLL('msg.updateSuccess'));
1306  } else {
1307  $this->addMessage($this->getLanguageService()->getLL('msg.updateError'), FlashMessage::ERROR);
1308  }
1309  } else {
1310  // A new task is being created
1311  // Create an instance of chosen class
1313  $task = GeneralUtility::makeInstance($this->submittedData['class']);
1314  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1315  // Set up single execution
1316  $task->registerSingleExecution($this->submittedData['start']);
1317  } else {
1318  // Set up recurring execution
1319  $task->registerRecurringExecution($this->submittedData['start'], $this->submittedData['interval'], $this->submittedData['end'], $this->submittedData['multiple'], $this->submittedData['croncmd']);
1320  }
1321  // Save additional input values
1322  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1324  $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1325  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1326  $providerObject->saveAdditionalFields($this->submittedData, $task);
1327  }
1328  }
1329  // Set disable flag
1330  $task->setDisabled($this->submittedData['disable']);
1331  // Set description
1332  $task->setDescription($this->submittedData['description']);
1333  // Set description
1334  $task->setTaskGroup($this->submittedData['task_group']);
1335  // Add to database
1336  $result = $this->scheduler->addTask($task);
1337  if ($result) {
1338  $this->getBackendUser()->writeLog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was added', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1339  $this->addMessage($this->getLanguageService()->getLL('msg.addSuccess'));
1340 
1341  // set the uid of the just created task so that we
1342  // can continue editing after initial saving
1343  $this->submittedData['uid'] = $task->getTaskUid();
1344  } else {
1345  $this->addMessage($this->getLanguageService()->getLL('msg.addError'), FlashMessage::ERROR);
1346  }
1347  }
1348  }
1349 
1350  /*************************
1351  *
1352  * INPUT PROCESSING UTILITIES
1353  *
1354  *************************/
1360  protected function preprocessData()
1361  {
1362  $result = true;
1363  // Validate id
1364  $this->submittedData['uid'] = empty($this->submittedData['uid']) ? 0 : (int)$this->submittedData['uid'];
1365  // Validate selected task class
1366  if (!class_exists($this->submittedData['class'])) {
1367  $this->addMessage($this->getLanguageService()->getLL('msg.noTaskClassFound'), FlashMessage::ERROR);
1368  }
1369  // Check start date
1370  if (empty($this->submittedData['start'])) {
1371  $this->addMessage($this->getLanguageService()->getLL('msg.noStartDate'), FlashMessage::ERROR);
1372  $result = false;
1373  } else {
1374  try {
1375  $this->submittedData['start'] = (int)$this->submittedData['start'];
1376  } catch (\Exception $e) {
1377  $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR);
1378  $result = false;
1379  }
1380  }
1381  // Check end date, if recurring task
1382  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING && !empty($this->submittedData['end'])) {
1383  try {
1384  $this->submittedData['end'] = (int)$this->submittedData['end'];
1385  if ($this->submittedData['end'] < $this->submittedData['start']) {
1386  $this->addMessage($this->getLanguageService()->getLL('msg.endDateSmallerThanStartDate'), FlashMessage::ERROR);
1387  $result = false;
1388  }
1389  } catch (\Exception $e) {
1390  $this->addMessage($this->getLanguageService()->getLL('msg.invalidEndDate'), FlashMessage::ERROR);
1391  $result = false;
1392  }
1393  }
1394  // Set default values for interval and cron command
1395  $this->submittedData['interval'] = 0;
1396  $this->submittedData['croncmd'] = '';
1397  // Check type and validity of frequency, if recurring
1398  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING) {
1399  $frequency = trim($this->submittedData['frequency']);
1400  if (empty($frequency)) {
1401  // Empty frequency, not valid
1402  $this->addMessage($this->getLanguageService()->getLL('msg.noFrequency'), FlashMessage::ERROR);
1403  $result = false;
1404  } else {
1405  $cronErrorCode = 0;
1406  $cronErrorMessage = '';
1407  // Try interpreting the cron command
1408  try {
1410  $this->submittedData['croncmd'] = $frequency;
1411  } catch (\Exception $e) {
1412  // Store the exception's result
1413  $cronErrorMessage = $e->getMessage();
1414  $cronErrorCode = $e->getCode();
1415  // Check if the frequency is a valid number
1416  // If yes, assume it is a frequency in seconds, and unset cron error code
1417  if (is_numeric($frequency)) {
1418  $this->submittedData['interval'] = (int)$frequency;
1419  unset($cronErrorCode);
1420  }
1421  }
1422  // If there's a cron error code, issue validation error message
1423  if (!empty($cronErrorCode)) {
1424  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.frequencyError'), $cronErrorMessage, $cronErrorCode), FlashMessage::ERROR);
1425  $result = false;
1426  }
1427  }
1428  }
1429  // Validate additional input fields
1430  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1432  $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1433  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1434  // The validate method will return true if all went well, but that must not
1435  // override previous false values => AND the returned value with the existing one
1436  $result &= $providerObject->validateAdditionalFields($this->submittedData, $this);
1437  }
1438  }
1439  return $result;
1440  }
1441 
1454  public function checkDate($string)
1455  {
1457  // Try with strtotime
1458  $timestamp = strtotime($string);
1459  // That failed. Try TYPO3's standard date/time input format
1460  if ($timestamp === false) {
1461  // Split time and date
1462  $dateParts = GeneralUtility::trimExplode(' ', $string, true);
1463  // Proceed if there are indeed two parts
1464  // Extract each component of date and time
1465  if (count($dateParts) == 2) {
1466  list($time, $date) = $dateParts;
1467  list($hour, $minutes) = GeneralUtility::trimExplode(':', $time, true);
1468  list($day, $month, $year) = GeneralUtility::trimExplode('-', $date, true);
1469  // Get a timestamp from all these parts
1470  $timestamp = @mktime($hour, $minutes, 0, $month, $day, $year);
1471  }
1472  // If the timestamp is still false, throw an exception
1473  if ($timestamp === false) {
1474  throw new \InvalidArgumentException('"' . $string . '" seems not to be a correct date.', 1294587694);
1475  }
1476  }
1477  return $timestamp;
1478  }
1479 
1480  /*************************
1481  *
1482  * APPLICATION LOGIC UTILITIES
1483  *
1484  *************************/
1492  public function addMessage($message, $severity = FlashMessage::OK)
1493  {
1494  $this->moduleTemplate->addFlashMessage($message, '', $severity);
1495  }
1496 
1510  protected function getRegisteredClasses()
1511  {
1512  $list = [];
1513  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'])) {
1514  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] as $class => $registrationInformation) {
1515  $title = isset($registrationInformation['title']) ? $this->getLanguageService()->sL($registrationInformation['title']) : '';
1516  $description = isset($registrationInformation['description']) ? $this->getLanguageService()->sL($registrationInformation['description']) : '';
1517  $list[$class] = [
1518  'extension' => $registrationInformation['extension'],
1519  'title' => $title,
1520  'description' => $description,
1521  'provider' => isset($registrationInformation['additionalFields']) ? $registrationInformation['additionalFields'] : ''
1522  ];
1523  }
1524  }
1525  return $list;
1526  }
1527 
1533  protected function getRegisteredTaskGroups()
1534  {
1535  $list = [];
1536 
1537  // Get all registered task groups
1538  $query = [
1539  'SELECT' => '*',
1540  'FROM' => 'tx_scheduler_task_group',
1541  'WHERE' => '1=1'
1542  . BackendUtility::BEenableFields('tx_scheduler_task_group')
1543  . BackendUtility::deleteClause('tx_scheduler_task_group'),
1544  'GROUPBY' => '',
1545  'ORDERBY' => 'sorting'
1546  ];
1547  $res = $this->getDatabaseConnection()->exec_SELECT_queryArray($query);
1548 
1549  while (($groupRecord = $this->getDatabaseConnection()->sql_fetch_assoc($res)) !== false) {
1550  $list[] = $groupRecord;
1551  }
1552  $this->getDatabaseConnection()->sql_free_result($res);
1553 
1554  return $list;
1555  }
1556 
1557  /*************************
1558  *
1559  * RENDERING UTILITIES
1560  *
1561  *************************/
1567  protected function getTemplateMarkers()
1568  {
1569  return [
1570  'CONTENT' => $this->content,
1571  'TITLE' => $this->getLanguageService()->getLL('title')
1572  ];
1573  }
1574 
1578  protected function getButtons()
1579  {
1580  $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1581  // CSH
1582  $helpButton = $buttonBar->makeHelpButton()
1583  ->setModuleName('_MOD_' . $this->moduleName)
1584  ->setFieldName('');
1585  $buttonBar->addButton($helpButton);
1586  // Add and Reload
1587  if (empty($this->CMD) || $this->CMD === 'list' || $this->CMD === 'delete' || $this->CMD === 'stop' || $this->CMD === 'toggleHidden') {
1588  $reloadButton = $buttonBar->makeLinkButton()
1589  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/locallang_core.xlf:labels.reload'))
1590  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL))
1591  ->setHref($this->moduleUri);
1592  $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1593  if ($this->MOD_SETTINGS['function'] === 'scheduler' && !empty($this->getRegisteredClasses())) {
1594  $addButton = $buttonBar->makeLinkButton()
1595  ->setTitle($this->getLanguageService()->getLL('action.add'))
1596  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-new', Icon::SIZE_SMALL))
1597  ->setHref($this->moduleUri . '&CMD=add');
1598  $buttonBar->addButton($addButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1599  }
1600  }
1601  // Close and Save
1602  if ($this->CMD === 'add' || $this->CMD === 'edit') {
1603  // Close
1604  $closeButton = $buttonBar->makeLinkButton()
1605  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:cancel'))
1606  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-close', Icon::SIZE_SMALL))
1607  ->setOnClick('document.location=' . GeneralUtility::quoteJSvalue($this->moduleUri))
1608  ->setHref('#');
1609  $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1610  // Save, SaveAndClose, SaveAndNew
1611  $saveButtonDropdown = $buttonBar->makeSplitButton();
1612  $saveButton = $buttonBar->makeInputButton()
1613  ->setName('CMD')
1614  ->setValue('save')
1615  ->setForm('tx_scheduler_form')
1616  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
1617  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:save'));
1618  $saveButtonDropdown->addItem($saveButton);
1619  $saveAndNewButton = $buttonBar->makeInputButton()
1620  ->setName('CMD')
1621  ->setValue('savenew')
1622  ->setForm('tx_scheduler_form')
1623  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-new', Icon::SIZE_SMALL))
1624  ->setTitle($this->getLanguageService()->sL('LLL:EXT:scheduler/Resources/Private/Language/locallang.xlf:label.saveAndCreateNewTask'));
1625  $saveButtonDropdown->addItem($saveAndNewButton);
1626  $saveAndCloseButton = $buttonBar->makeInputButton()
1627  ->setName('CMD')
1628  ->setValue('saveclose')
1629  ->setForm('tx_scheduler_form')
1630  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-close', Icon::SIZE_SMALL))
1631  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:saveAndClose'));
1632  $saveButtonDropdown->addItem($saveAndCloseButton);
1633  $buttonBar->addButton($saveButtonDropdown, ButtonBar::BUTTON_POSITION_LEFT, 3);
1634  }
1635  // Edit
1636  if ($this->CMD === 'edit') {
1637  $deleteButton = $buttonBar->makeInputButton()
1638  ->setName('CMD')
1639  ->setValue('delete')
1640  ->setForm('tx_scheduler_form')
1641  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-edit-delete', Icon::SIZE_SMALL))
1642  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:delete'));
1643  $buttonBar->addButton($deleteButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
1644  }
1645  // Shortcut
1646  $shortcutButton = $buttonBar->makeShortcutButton()
1647  ->setModuleName($this->moduleName)
1648  ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
1649  ->setSetVariables(['function']);
1650  $buttonBar->addButton($shortcutButton);
1651  }
1652 
1658  protected function getBackendUser()
1659  {
1660  return $GLOBALS['BE_USER'];
1661  }
1662 
1668  protected function getDatabaseConnection()
1669  {
1670  return $GLOBALS['TYPO3_DB'];
1671  }
1672 }
mainAction(ServerRequestInterface $request, ResponseInterface $response)
static getSaltingInstance($saltedHash='', $mode=TYPO3_MODE)
Definition: SaltFactory.php:82
static BEenableFields($table, $inv=false)
static trimExplode($delim, $string, $removeEmptyValues=false, $limit=0)
static mergeRecursiveWithOverrule(array &$original, array $overrule, $addKeys=true, $includeEmptyValues=true, $enableUnsetFeature=true)
$uid
Definition: server.php:38
if(TYPO3_MODE==='BE') $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['t3lib/class.t3lib_tsfebeuserauth.php']['frontendEditingController']['default']
static deleteClause($table, $tableAlias='')