TYPO3CMS  8
 All Classes Namespaces Files Functions Variables Pages
SchedulerModuleController.php
Go to the documentation of this file.
1 <?php
2 namespace TYPO3\CMS\Scheduler\Controller;
3 
4 /*
5  * This file is part of the TYPO3 CMS project.
6  *
7  * It is free software; you can redistribute it and/or modify it under
8  * the terms of the GNU General Public License, either version 2
9  * of the License, or any later version.
10  *
11  * For the full copyright and license information, please read the
12  * LICENSE.txt file that was distributed with this source code.
13  *
14  * The TYPO3 project - inspiring people to share!
15  */
16 
17 use Psr\Http\Message\ResponseInterface;
18 use Psr\Http\Message\ServerRequestInterface;
35 
40 {
46  protected $submittedData = [];
47 
54  protected $messages = [];
55 
59  protected $cshKey;
60 
64  protected $scheduler;
65 
69  protected $backendTemplatePath = '';
70 
74  protected $view;
75 
81  protected $moduleName = 'system_txschedulerM1';
82 
86  protected $moduleUri;
87 
93  protected $moduleTemplate;
94 
98  public function __construct()
99  {
100  $this->moduleTemplate = GeneralUtility::makeInstance(ModuleTemplate::class);
101  $this->getLanguageService()->includeLLFile('EXT:scheduler/Resources/Private/Language/locallang.xlf');
102  $this->MCONF = [
103  'name' => $this->moduleName,
104  ];
105  $this->cshKey = '_MOD_' . $this->moduleName;
106  $this->backendTemplatePath = ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Backend/SchedulerModule/';
107  $this->view = GeneralUtility::makeInstance(\TYPO3\CMS\Fluid\View\StandaloneView::class);
108  $this->view->getRequest()->setControllerExtensionName('scheduler');
109  $this->view->setPartialRootPaths([ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Partials/Backend/SchedulerModule/']);
110  $this->moduleUri = BackendUtility::getModuleUrl($this->moduleName);
111 
112  $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
113  $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
114  $pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/SplitButtons');
115  }
116 
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 
135  public function menuConfig()
136  {
137  $this->MOD_MENU = [
138  'function' => [
139  'scheduler' => $this->getLanguageService()->getLL('function.scheduler'),
140  'check' => $this->getLanguageService()->getLL('function.check'),
141  'info' => $this->getLanguageService()->getLL('function.info')
142  ]
143  ];
144  parent::menuConfig();
145  }
146 
152  public function main()
153  {
154  // Access check!
155  // The page will show only if user has admin rights
156  if ($this->getBackendUser()->isAdmin()) {
157  // Set the form
158  $this->content = '<form name="tx_scheduler_form" id="tx_scheduler_form" method="post" action="">';
159 
160  // Prepare main content
161  $this->content .= '<h1>' . $this->getLanguageService()->getLL('function.' . $this->MOD_SETTINGS['function']) . '</h1>';
162  $this->content .= $this->getModuleContent();
163  $this->content .= '</form>';
164  } else {
165  // If no access, only display the module's title
166  $this->content = '<h1>' . $this->getLanguageService()->getLL('title.') . '</h1>';
167  $this->content .='<div style="padding-top: 5px;"></div>';
168  }
169  $this->getButtons();
170  $this->getModuleMenu();
171  }
172 
176  protected function getModuleMenu()
177  {
178  $menu = $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->makeMenu();
179  $menu->setIdentifier('SchedulerJumpMenu');
180 
181  foreach ($this->MOD_MENU['function'] as $controller => $title) {
182  $item = $menu
183  ->makeMenuItem()
184  ->setHref(
185  BackendUtility::getModuleUrl(
186  $this->moduleName,
187  [
188  'id' => $this->id,
189  'SET' => [
190  'function' => $controller
191  ]
192  ]
193  )
194  )
195  ->setTitle($title);
196  if ($controller === $this->MOD_SETTINGS['function']) {
197  $item->setActive(true);
198  }
199  $menu->addMenuItem($item);
200  }
201  $this->moduleTemplate->getDocHeaderComponent()->getMenuRegistry()->addMenu($menu);
202  }
203 
209  protected function getModuleContent()
210  {
211  $content = '';
212  $sectionTitle = '';
213  // Get submitted data
214  $this->submittedData = GeneralUtility::_GPmerged('tx_scheduler');
215  $this->submittedData['uid'] = (int)$this->submittedData['uid'];
216  // If a save command was submitted, handle saving now
217  if ($this->CMD === 'save' || $this->CMD === 'saveclose' || $this->CMD === 'savenew') {
218  $previousCMD = GeneralUtility::_GP('previousCMD');
219  // First check the submitted data
220  $result = $this->preprocessData();
221  // If result is ok, proceed with saving
222  if ($result) {
223  $this->saveTask();
224  if ($this->CMD === 'saveclose') {
225  // Unset command, so that default screen gets displayed
226  unset($this->CMD);
227  } elseif ($this->CMD === 'save') {
228  // After saving a "add form", return to edit
229  $this->CMD = 'edit';
230  } elseif ($this->CMD === 'savenew') {
231  // Unset submitted data, so that empty form gets displayed
232  unset($this->submittedData);
233  // After saving a "add/edit form", return to add
234  $this->CMD = 'add';
235  } else {
236  // Return to edit form
237  $this->CMD = $previousCMD;
238  }
239  } else {
240  $this->CMD = $previousCMD;
241  }
242  }
243 
244  // Handle chosen action
245  switch ((string)$this->MOD_SETTINGS['function']) {
246  case 'scheduler':
247  $this->executeTasks();
248 
249  switch ($this->CMD) {
250  case 'add':
251  case 'edit':
252  try {
253  // Try adding or editing
254  $content .= $this->editTaskAction();
255  $sectionTitle = $this->getLanguageService()->getLL('action.' . $this->CMD);
256  } catch (\Exception $e) {
257  if ($e->getCode() === 1305100019) {
258  // Invalid controller class name exception
259  $this->addMessage($e->getMessage(), FlashMessage::ERROR);
260  }
261  // An exception may also happen when the task to
262  // edit could not be found. In this case revert
263  // to displaying the list of tasks
264  // It can also happen when attempting to edit a running task
265  $content .= $this->listTasksAction();
266  }
267  break;
268  case 'delete':
269  $this->deleteTask();
270  $content .= $this->listTasksAction();
271  break;
272  case 'stop':
273  $this->stopTask();
274  $content .= $this->listTasksAction();
275  break;
276  case 'toggleHidden':
277  $this->toggleDisableAction();
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 
328  protected function checkSchedulerUser()
329  {
330  $schedulerUserStatus = -1;
331  // Check if user exists at all
332  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('be_users');
333  $queryBuilder->getRestrictions()
334  ->removeAll()
335  ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
336 
337  $cliUserExists = (int)$queryBuilder->count('*')
338  ->from('be_users')
339  ->where(
340  $queryBuilder->expr()->eq(
341  'username',
342  $queryBuilder->createNamedParameter('_cli_scheduler', \PDO::PARAM_STR)
343  ),
344  $queryBuilder->expr()->eq('admin', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
345  )
346  ->execute()
347  ->fetchColumn();
348 
349  if ($cliUserExists !== 0) {
350  $schedulerUserStatus = 0;
351  // Check if user exists and is enabled
352  $queryBuilder->setRestrictions(GeneralUtility::makeInstance(DefaultRestrictionContainer::class));
353  $cliUserExistsAndEnabled = (int)$queryBuilder->execute()->fetchColumn();
354  if ($cliUserExistsAndEnabled !== 0) {
355  $schedulerUserStatus = 1;
356  }
357  }
358  return $schedulerUserStatus;
359  }
360 
366  protected function createSchedulerUser()
367  {
368  // Check _cli_scheduler user status
369  $checkUser = $this->checkSchedulerUser();
370  // Prepare default message
371  $message = $this->getLanguageService()->getLL('msg.userExists');
372  $severity = FlashMessage::WARNING;
373  // If the user does not exist, try creating it
374  if ($checkUser == -1) {
375  // Prepare necessary data for _cli_scheduler user creation
376  $password = StringUtility::getUniqueId('scheduler');
378  $objInstanceSaltedPW = SaltFactory::getSaltingInstance();
379  $password = $objInstanceSaltedPW->getHashedPassword($password);
380  }
381  $data = ['be_users' => ['NEW' => ['username' => '_cli_scheduler', 'password' => $password, 'pid' => 0]]];
383  $dataHandler = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
384  $dataHandler->start($data, []);
385  $dataHandler->process_datamap();
386  // Check if a new uid was indeed generated (i.e. a new record was created)
387  // (counting DataHandler errors doesn't work as some failures don't report errors)
388  $numberOfNewIDs = count($dataHandler->substNEWwithIDs);
389  if ($numberOfNewIDs === 1) {
390  $message = $this->getLanguageService()->getLL('msg.userCreated');
391  $severity = FlashMessage::OK;
392  } else {
393  $message = $this->getLanguageService()->getLL('msg.userNotCreated');
394  $severity = FlashMessage::ERROR;
395  }
396  }
397  $this->addMessage($message, $severity);
398  }
399 
406  protected function checkScreenAction()
407  {
408  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'CheckScreen.html');
409 
410  // First, check if _cli_scheduler user creation was requested
411  if ($this->CMD === 'user') {
412  $this->createSchedulerUser();
413  }
414 
415  // Display information about last automated run, as stored in the system registry
417  $registry = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Registry::class);
418  $lastRun = $registry->get('tx_scheduler', 'lastRun');
419  if (!is_array($lastRun)) {
420  $message = $this->getLanguageService()->getLL('msg.noLastRun');
422  } else {
423  if (empty($lastRun['end']) || empty($lastRun['start']) || empty($lastRun['type'])) {
424  $message = $this->getLanguageService()->getLL('msg.incompleteLastRun');
426  } else {
427  $startDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['start']);
428  $startTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['start']);
429  $endDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['end']);
430  $endTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['end']);
431  $label = 'automatically';
432  if ($lastRun['type'] === 'manual') {
433  $label = 'manually';
434  }
435  $type = $this->getLanguageService()->getLL('label.' . $label);
436  $message = sprintf($this->getLanguageService()->getLL('msg.lastRun'), $type, $startDate, $startTime, $endDate, $endTime);
437  $severity = InfoboxViewHelper::STATE_INFO;
438  }
439  }
440  $this->view->assign('lastRunMessage', $message);
441  $this->view->assign('lastRunSeverity', $severity);
442 
443  // Check CLI user
444  $checkUser = $this->checkSchedulerUser();
445  if ($checkUser == -1) {
446  $link = $this->moduleUri . '&SET[function]=check&CMD=user';
447  $message = sprintf($this->getLanguageService()->getLL('msg.schedulerUserMissing'), htmlspecialchars($link));
448  $severity = InfoboxViewHelper::STATE_ERROR;
449  } elseif ($checkUser == 0) {
450  $message = $this->getLanguageService()->getLL('msg.schedulerUserFoundButDisabled');
452  } else {
453  $message = $this->getLanguageService()->getLL('msg.schedulerUserFound');
454  $severity = InfoboxViewHelper::STATE_OK;
455  }
456  $this->view->assign('cliUserMessage', $message);
457  $this->view->assign('cliUserSeverity', $severity);
458 
459  // Check if CLI script is executable or not
460  $script = PATH_typo3 . 'cli_dispatch.phpsh';
461  $this->view->assign('script', $script);
462 
463  // Skip this check if running Windows, as rights do not work the same way on this platform
464  // (i.e. the script will always appear as *not* executable)
465  if (TYPO3_OS === 'WIN') {
466  $isExecutable = true;
467  } else {
468  $isExecutable = is_executable($script);
469  }
470  if ($isExecutable) {
471  $message = $this->getLanguageService()->getLL('msg.cliScriptExecutable');
472  $severity = InfoboxViewHelper::STATE_OK;
473  } else {
474  $message = $this->getLanguageService()->getLL('msg.cliScriptNotExecutable');
475  $severity = InfoboxViewHelper::STATE_ERROR;
476  }
477  $this->view->assign('isExecutableMessage', $message);
478  $this->view->assign('isExecutableSeverity', $severity);
479  $this->view->assign('now', $this->getServerTime());
480 
481  return $this->view->render();
482  }
483 
489  protected function infoScreenAction()
490  {
491  $registeredClasses = $this->getRegisteredClasses();
492  // No classes available, display information message
493  if (empty($registeredClasses)) {
494  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreenNoClasses.html');
495  return $this->view->render();
496  }
497 
498  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreen.html');
499  $this->view->assign('registeredClasses', $registeredClasses);
500 
501  return $this->view->render();
502  }
503 
510  protected function renderTaskProgressBar($progress)
511  {
512  $progressText = $this->getLanguageService()->getLL('status.progress') . ':&nbsp;' . $progress . '%';
513  return '<div class="progress">'
514  . '<div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="' . $progress . '" aria-valuemin="0" aria-valuemax="100" style="width: ' . $progress . '%;">' . $progressText . '</div>'
515  . '</div>';
516  }
517 
523  protected function deleteTask()
524  {
525  try {
526  // Try to fetch the task and delete it
527  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
528  // If the task is currently running, it may not be deleted
529  if ($task->isExecutionRunning()) {
530  $this->addMessage($this->getLanguageService()->getLL('msg.maynotDeleteRunningTask'), FlashMessage::ERROR);
531  } else {
532  if ($this->scheduler->removeTask($task)) {
533  $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was deleted', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
534  $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
535  } else {
536  $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
537  }
538  }
539  } catch (\UnexpectedValueException $e) {
540  // The task could not be unserialized properly, simply delete the database record
541  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable('tx_scheduler_task');
542  $result = $queryBuilder->delete('tx_scheduler_task')
543  ->where(
544  $queryBuilder->expr()->eq(
545  'uid',
546  $queryBuilder->createNamedParameter($this->submittedData['uid'], \PDO::PARAM_INT)
547  )
548  )
549  ->execute();
550 
551  if ($result) {
552  $this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
553  } else {
554  $this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
555  }
556  } catch (\OutOfBoundsException $e) {
557  // The task was not found, for some reason
558  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
559  }
560  }
561 
570  protected function stopTask()
571  {
572  try {
573  // Try to fetch the task and stop it
574  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
575  if ($task->isExecutionRunning()) {
576  // If the task is indeed currently running, clear marked executions
577  $result = $task->unmarkAllExecutions();
578  if ($result) {
579  $this->addMessage($this->getLanguageService()->getLL('msg.stopSuccess'));
580  } else {
581  $this->addMessage($this->getLanguageService()->getLL('msg.stopError'), FlashMessage::ERROR);
582  }
583  } else {
584  // The task is not running, nothing to unmark
585  $this->addMessage($this->getLanguageService()->getLL('msg.maynotStopNonRunningTask'), FlashMessage::WARNING);
586  }
587  } catch (\Exception $e) {
588  // The task was not found, for some reason
589  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
590  }
591  }
592 
598  protected function toggleDisableAction()
599  {
600  $task = $this->scheduler->fetchTask($this->submittedData['uid']);
601  $task->setDisabled(!$task->isDisabled());
602  // If a disabled single task is enabled again, we register it for a
603  // single execution at next scheduler run.
604  if ($task->getType() === AbstractTask::TYPE_SINGLE) {
605  $task->registerSingleExecution(time());
606  }
607  $task->save();
608  }
609 
615  protected function editTaskAction()
616  {
617  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'EditTask.html');
618 
619  $registeredClasses = $this->getRegisteredClasses();
620  $registeredTaskGroups = $this->getRegisteredTaskGroups();
621 
622  $taskInfo = [];
623  $task = null;
624  $process = 'edit';
625 
626  if ($this->submittedData['uid'] > 0) {
627  // If editing, retrieve data for existing task
628  try {
629  $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
630  // If there's a registered execution, the task should not be edited
631  if (!empty($taskRecord['serialized_executions'])) {
632  $this->addMessage($this->getLanguageService()->getLL('msg.maynotEditRunningTask'), FlashMessage::ERROR);
633  throw new \LogicException('Runnings tasks cannot not be edited', 1251232849);
634  }
635 
636  // Get the task object
638  $task = unserialize($taskRecord['serialized_task_object']);
639 
640  // Set some task information
641  $taskInfo['disable'] = $taskRecord['disable'];
642  $taskInfo['description'] = $taskRecord['description'];
643  $taskInfo['task_group'] = $taskRecord['task_group'];
644 
645  // Check that the task object is valid
646  if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
647  // The task object is valid, process with fetching current data
648  $taskInfo['class'] = get_class($task);
649  // Get execution information
650  $taskInfo['start'] = (int)$task->getExecution()->getStart();
651  $taskInfo['end'] = (int)$task->getExecution()->getEnd();
652  $taskInfo['interval'] = $task->getExecution()->getInterval();
653  $taskInfo['croncmd'] = $task->getExecution()->getCronCmd();
654  $taskInfo['multiple'] = $task->getExecution()->getMultiple();
655  if (!empty($taskInfo['interval']) || !empty($taskInfo['croncmd'])) {
656  // Guess task type from the existing information
657  // If an interval or a cron command is defined, it's a recurring task
658  $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
659  $taskInfo['frequency'] = $taskInfo['interval'] ?: $taskInfo['croncmd'];
660  } else {
661  // It's not a recurring task
662  // Make sure interval and cron command are both empty
663  $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
664  $taskInfo['frequency'] = '';
665  $taskInfo['end'] = 0;
666  }
667  } else {
668  // The task object is not valid
669  // Issue error message
670  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.invalidTaskClassEdit'), get_class($task)), FlashMessage::ERROR);
671  // Initialize empty values
672  $taskInfo['start'] = 0;
673  $taskInfo['end'] = 0;
674  $taskInfo['frequency'] = '';
675  $taskInfo['multiple'] = false;
676  $taskInfo['type'] = AbstractTask::TYPE_SINGLE;
677  }
678  } catch (\OutOfBoundsException $e) {
679  // Add a message and continue throwing the exception
680  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
681  throw $e;
682  }
683  } else {
684  // If adding a new object, set some default values
685  $taskInfo['class'] = key($registeredClasses);
686  $taskInfo['type'] = AbstractTask::TYPE_RECURRING;
687  $taskInfo['start'] = $GLOBALS['EXEC_TIME'];
688  $taskInfo['end'] = '';
689  $taskInfo['frequency'] = '';
690  $taskInfo['multiple'] = 0;
691  $process = 'add';
692  }
693 
694  // If some data was already submitted, use it to override
695  // existing data
696  if (!empty($this->submittedData)) {
698  }
699 
700  // Get the extra fields to display for each task that needs some
701  $allAdditionalFields = [];
702  if ($process === 'add') {
703  foreach ($registeredClasses as $class => $registrationInfo) {
704  if (!empty($registrationInfo['provider'])) {
706  $providerObject = GeneralUtility::getUserObj($registrationInfo['provider']);
707  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
708  $additionalFields = $providerObject->getAdditionalFields($taskInfo, null, $this);
709  $allAdditionalFields = array_merge($allAdditionalFields, [$class => $additionalFields]);
710  }
711  }
712  }
713  } else {
714  if (!empty($registeredClasses[$taskInfo['class']]['provider'])) {
715  $providerObject = GeneralUtility::getUserObj($registeredClasses[$taskInfo['class']]['provider']);
716  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
717  $allAdditionalFields[$taskInfo['class']] = $providerObject->getAdditionalFields($taskInfo, $task, $this);
718  }
719  }
720  }
721 
722  // Load necessary JavaScript
723  $this->getPageRenderer()->loadJquery();
724  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
725  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker');
726 
727  // Start rendering the add/edit form
728  $this->view->assign('uid', htmlspecialchars($this->submittedData['uid']));
729  $this->view->assign('cmd', htmlspecialchars($this->CMD));
730 
731  $table = [];
732 
733  // Disable checkbox
734  $label = '<label>' . $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:disable') . '</label>';
735  $table[] =
736  '<div class="form-section" id="task_disable_row"><div class="form-group">'
737  . BackendUtility::wrapInHelp($this->cshKey, 'task_disable', $label)
738  . '<div class="form-control-wrap">'
739  . '<input type="hidden" name="tx_scheduler[disable]" value="0">'
740  . '<input class="checkbox" type="checkbox" name="tx_scheduler[disable]" value="1" id="task_disable" ' . ($taskInfo['disable'] ? ' checked="checked"' : '') . '>'
741  . '</div>'
742  . '</div></div>';
743 
744  // Task class selector
745  $label = '<label>' . $this->getLanguageService()->getLL('label.class') . '</label>';
746 
747  // On editing, don't allow changing of the task class, unless it was not valid
748  if ($this->submittedData['uid'] > 0 && !empty($taskInfo['class'])) {
749  $cell = '<div>' . $registeredClasses[$taskInfo['class']]['title'] . ' (' . $registeredClasses[$taskInfo['class']]['extension'] . ')</div>';
750  $cell .= '<input type="hidden" name="tx_scheduler[class]" id="task_class" value="' . htmlspecialchars($taskInfo['class']) . '">';
751  } else {
752  $cell = '<select name="tx_scheduler[class]" id="task_class" class="form-control">';
753  // Group registered classes by classname
754  $groupedClasses = [];
755  foreach ($registeredClasses as $class => $classInfo) {
756  $groupedClasses[$classInfo['extension']][$class] = $classInfo;
757  }
758  ksort($groupedClasses);
759  // Loop on all grouped classes to display a selector
760  foreach ($groupedClasses as $extension => $class) {
761  $cell .= '<optgroup label="' . htmlspecialchars($extension) . '">';
762  foreach ($groupedClasses[$extension] as $class => $classInfo) {
763  $selected = $class == $taskInfo['class'] ? ' selected="selected"' : '';
764  $cell .= '<option value="' . htmlspecialchars($class) . '"' . 'title="' . htmlspecialchars($classInfo['description']) . '" ' . $selected . '>' . htmlspecialchars($classInfo['title']) . '</option>';
765  }
766  $cell .= '</optgroup>';
767  }
768  $cell .= '</select>';
769  }
770  $table[] =
771  '<div class="form-section" id="task_class_row"><div class="form-group">'
772  . BackendUtility::wrapInHelp($this->cshKey, 'task_class', $label)
773  . '<div class="form-control-wrap">'
774  . $cell
775  . '</div>'
776  . '</div></div>';
777 
778  // Task type selector
779  $label = '<label>' . $this->getLanguageService()->getLL('label.type') . '</label>';
780  $table[] =
781  '<div class="form-section" id="task_type_row"><div class="form-group">'
782  . BackendUtility::wrapInHelp($this->cshKey, 'task_type', $label)
783  . '<div class="form-control-wrap">'
784  . '<select name="tx_scheduler[type]" id="task_type" class="form-control">'
785  . '<option value="1" ' . ((int)$taskInfo['type'] === AbstractTask::TYPE_SINGLE ? ' selected="selected"' : '') . '>' . $this->getLanguageService()->getLL('label.type.single') . '</option>'
786  . '<option value="2" ' . ((int)$taskInfo['type'] === AbstractTask::TYPE_RECURRING ? ' selected="selected"' : '') . '>' . $this->getLanguageService()->getLL('label.type.recurring') . '</option>'
787  . '</select>'
788  . '</div>'
789  . '</div></div>';
790 
791  // Task group selector
792  $label = '<label>' . $this->getLanguageService()->getLL('label.group') . '</label>';
793  $cell = '<select name="tx_scheduler[task_group]" id="task_class" class="form-control">';
794 
795  // Loop on all groups to display a selector
796  $cell .= '<option value="0" title=""></option>';
797  foreach ($registeredTaskGroups as $taskGroup) {
798  $selected = $taskGroup['uid'] == $taskInfo['task_group'] ? ' selected="selected"' : '';
799  $cell .= '<option value="' . $taskGroup['uid'] . '"' . 'title="';
800  $cell .= htmlspecialchars($taskGroup['groupName']) . '"' . $selected . '>';
801  $cell .= htmlspecialchars($taskGroup['groupName']) . '</option>';
802  }
803  $cell .= '</select>';
804 
805  $table[] =
806  '<div class="form-section" id="task_group_row"><div class="form-group">'
807  . BackendUtility::wrapInHelp($this->cshKey, 'task_group', $label)
808  . '<div class="form-control-wrap">'
809  . $cell
810  . '</div>'
811  . '</div></div>';
812 
813  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '%H:%M %m-%d-%Y' : '%H:%M %d-%m-%Y';
814 
815  $label = '<label>' . BackendUtility::wrapInHelp($this->cshKey, 'task_start', $this->getLanguageService()->getLL('label.start')) . '</label>';
816  $value = ($taskInfo['start'] > 0 ? strftime($dateFormat, $taskInfo['start']) : '');
817  $table[] =
818  '<div class="form-section"><div class="row"><div class="form-group col-sm-6" id="task_start_col">'
819  . $label
820  . '<div class="form-control-wrap">'
821  . '<div class="input-group" id="tceforms-datetimefield-task_start_row-wrapper">'
822  . '<input name="tx_scheduler[start]_hr" value="' . $value . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="datetime" data-date-offset="0" type="text" id="tceforms-datetimefield-task_start_row">'
823  . '<input name="tx_scheduler[start]" value="' . $taskInfo['start'] . '" type="hidden">'
824  . '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_start_row"><span class="fa fa-calendar"></span></label></span>'
825  . '</div>'
826  . '</div>'
827  . '</div>';
828 
829  // End date/time field
830  // NOTE: datetime fields need a special id naming scheme
831  $value = ($taskInfo['end'] > 0 ? strftime($dateFormat, $taskInfo['end']) : '');
832  $label = '<label>' . $this->getLanguageService()->getLL('label.end') . '</label>';
833  $table[] =
834  '<div class="form-group col-sm-6" id="task_end_col">'
835  . BackendUtility::wrapInHelp($this->cshKey, 'task_end', $label)
836  . '<div class="form-control-wrap">'
837  . '<div class="input-group" id="tceforms-datetimefield-task_end_row-wrapper">'
838  . '<input name="tx_scheduler[end]_hr" value="' . $value . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="datetime" data-date-offset="0" type="text" id="tceforms-datetimefield-task_end_row">'
839  . '<input name="tx_scheduler[end]" value="' . $taskInfo['end'] . '" type="hidden">'
840  . '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_end_row"><span class="fa fa-calendar"></span></label></span>'
841  . '</div>'
842  . '</div>'
843  . '</div></div></div>';
844 
845  // Frequency input field
846  $label = '<label>' . $this->getLanguageService()->getLL('label.frequency.long') . '</label>';
847  $table[] =
848  '<div class="form-section" id="task_frequency_row"><div class="form-group">'
849  . BackendUtility::wrapInHelp($this->cshKey, 'task_frequency', $label)
850  . '<div class="form-control-wrap">'
851  . '<input type="text" name="tx_scheduler[frequency]" class="form-control" id="task_frequency" value="' . htmlspecialchars($taskInfo['frequency']) . '">'
852  . '</div>'
853  . '</div></div>';
854 
855  // Multiple execution selector
856  $label = '<label>' . $this->getLanguageService()->getLL('label.parallel.long') . '</label>';
857  $table[] =
858  '<div class="form-section" id="task_multiple_row"><div class="form-group">'
859  . BackendUtility::wrapInHelp($this->cshKey, 'task_multiple', $label)
860  . '<div class="form-control-wrap">'
861  . '<input type="hidden" name="tx_scheduler[multiple]" value="0">'
862  . '<input class="checkbox" type="checkbox" name="tx_scheduler[multiple]" value="1" id="task_multiple" ' . ($taskInfo['multiple'] ? 'checked="checked"' : '') . '>'
863  . '</div>'
864  . '</div></div>';
865 
866  // Description
867  $label = '<label>' . $this->getLanguageService()->getLL('label.description') . '</label>';
868  $table[] =
869  '<div class="form-section" id="task_description_row"><div class="form-group">'
870  . BackendUtility::wrapInHelp($this->cshKey, 'task_description', $label)
871  . '<div class="form-control-wrap">'
872  . '<textarea class="form-control" name="tx_scheduler[description]">' . htmlspecialchars($taskInfo['description']) . '</textarea>'
873  . '</div>'
874  . '</div></div>';
875 
876  // Display additional fields
877  foreach ($allAdditionalFields as $class => $fields) {
878  if ($class == $taskInfo['class']) {
879  $additionalFieldsStyle = '';
880  } else {
881  $additionalFieldsStyle = ' style="display: none"';
882  }
883  // Add each field to the display, if there are indeed any
884  if (isset($fields) && is_array($fields)) {
885  foreach ($fields as $fieldID => $fieldInfo) {
886  $label = '<label>' . $this->getLanguageService()->sL($fieldInfo['label']) . '</label>';
887  $htmlClassName = strtolower(str_replace('\\', '-', $class));
888 
889  $table[] =
890  '<div class="form-section extraFields extra_fields_' . $htmlClassName . '" ' . $additionalFieldsStyle . ' id="' . $fieldID . '_row"><div class="form-group">'
891  . BackendUtility::wrapInHelp($fieldInfo['cshKey'], $fieldInfo['cshLabel'], $label)
892  . '<div class="form-control-wrap">' . $fieldInfo['code'] . '</div>'
893  . '</div></div>';
894  }
895  }
896  }
897 
898  $this->view->assign('table', implode(LF, $table));
899  $this->view->assign('now', $this->getServerTime());
900 
901  return $this->view->render();
902  }
903 
909  protected function executeTasks()
910  {
911  // Make sure next automatic scheduler-run is scheduled
912  if (GeneralUtility::_POST('go') !== null) {
913  $this->scheduler->scheduleNextSchedulerRunUsingAtDaemon();
914  }
915  // Continue if some elements have been chosen for execution
916  if (isset($this->submittedData['execute']) && !empty($this->submittedData['execute'])) {
917  // Get list of registered classes
918  $registeredClasses = $this->getRegisteredClasses();
919  // Loop on all selected tasks
920  foreach ($this->submittedData['execute'] as $uid) {
921  try {
922  // Try fetching the task
923  $task = $this->scheduler->fetchTask($uid);
924  $class = get_class($task);
925  $name = $registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')';
926  // Now try to execute it and report on outcome
927  try {
928  $result = $this->scheduler->executeTask($task);
929  if ($result) {
930  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executed'), $name));
931  } else {
932  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.notExecuted'), $name), FlashMessage::ERROR);
933  }
934  } catch (\Exception $e) {
935  // An exception was thrown, display its message as an error
936  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $name, $e->getMessage()), FlashMessage::ERROR);
937  }
938  } catch (\OutOfBoundsException $e) {
939  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $uid), FlashMessage::ERROR);
940  } catch (\UnexpectedValueException $e) {
941  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $uid, $e->getMessage()), FlashMessage::ERROR);
942  }
943  }
944  // Record the run in the system registry
945  $this->scheduler->recordLastRun('manual');
946  // Make sure to switch to list view after execution
947  $this->CMD = 'list';
948  }
949  }
950 
956  protected function listTasksAction()
957  {
958  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasks.html');
959 
960  // Define display format for dates
961  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
962 
963  // Get list of registered task groups
964  $registeredTaskGroups = $this->getRegisteredTaskGroups();
965 
966  // add an empty entry for non-grouped tasks
967  // add in front of list
968  array_unshift($registeredTaskGroups, ['uid' => 0, 'groupName' => '']);
969 
970  // Get all registered tasks
971  // Just to get the number of entries
972  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
973  ->getQueryBuilderForTable('tx_scheduler_task');
974  $queryBuilder->getRestrictions()->removeAll();
975 
976  $result = $queryBuilder->select('t.*')
977  ->addSelect(
978  'g.groupName AS taskGroupName',
979  'g.description AS taskGroupDescription',
980  'g.deleted AS isTaskGroupDeleted'
981  )
982  ->from('tx_scheduler_task', 't')
983  ->leftJoin(
984  't',
985  'tx_scheduler_task_group',
986  'g',
987  $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
988  )
989  ->orderBy('g.sorting')
990  ->execute();
991 
992  // Loop on all tasks
993  $temporaryResult = [];
994  while ($row = $result->fetch()) {
995  if ($row['taskGroupName'] === null || $row['isTaskGroupDeleted'] === '1') {
996  $row['taskGroupName'] = '';
997  $row['taskGroupDescription'] = '';
998  $row['task_group'] = 0;
999  }
1000  $temporaryResult[$row['task_group']]['groupName'] = $row['taskGroupName'];
1001  $temporaryResult[$row['task_group']]['groupDescription'] = $row['taskGroupDescription'];
1002  $temporaryResult[$row['task_group']]['tasks'][] = $row;
1003  }
1004 
1005  // No tasks defined, display information message
1006  if (empty($temporaryResult)) {
1007  $this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasksNoTasks.html');
1008  return $this->view->render();
1009  }
1010 
1011  $this->getPageRenderer()->loadJquery();
1012  $this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
1013  $table = [];
1014  // Header row
1015  $table[] =
1016  '<thead><tr>'
1017  . '<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>'
1018  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.id')) . '</th>'
1019  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('task')) . '</th>'
1020  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.type')) . '</th>'
1021  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.frequency')) . '</th>'
1022  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.parallel')) . '</th>'
1023  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.lastExecution')) . '</th>'
1024  . '<th>' . htmlspecialchars($this->getLanguageService()->getLL('label.nextExecution')) . '</th>'
1025  . '<th></th>'
1026  . '</tr></thead>';
1027 
1028  $registeredClasses = $this->getRegisteredClasses();
1029  foreach ($temporaryResult as $taskGroup) {
1030  if (!empty($taskGroup['groupName'])) {
1031  $groupText = '<strong>' . htmlspecialchars($taskGroup['groupName']) . '</strong>';
1032  if (!empty($taskGroup['groupDescription'])) {
1033  $groupText .= '<br>' . nl2br(htmlspecialchars($taskGroup['groupDescription']));
1034  }
1035  $table[] = '<tr><td colspan="9">' . $groupText . '</td></tr>';
1036  }
1037 
1038  foreach ($taskGroup['tasks'] as $schedulerRecord) {
1039  // Define action icons
1040  $link = htmlspecialchars($this->moduleUri . '&CMD=edit&tx_scheduler[uid]=' . $schedulerRecord['uid']);
1041  $editAction = '<a class="btn btn-default" href="' . $link . '" title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:edit')) . '" class="icon">' .
1042  $this->moduleTemplate->getIconFactory()->getIcon('actions-open', Icon::SIZE_SMALL)->render() . '</a>';
1043  if ((int)$schedulerRecord['disable'] === 1) {
1044  $translationKey = 'enable';
1045  $icon = $this->moduleTemplate->getIconFactory()->getIcon('actions-edit-unhide', Icon::SIZE_SMALL);
1046  } else {
1047  $translationKey = 'disable';
1048  $icon = $this->moduleTemplate->getIconFactory()->getIcon('actions-edit-hide', Icon::SIZE_SMALL);
1049  }
1050  $toggleHiddenAction = '<a class="btn btn-default" href="' . htmlspecialchars($this->moduleUri
1051  . '&CMD=toggleHidden&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" title="'
1052  . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:' . $translationKey))
1053  . '" class="icon">' . $icon->render() . '</a>';
1054  $deleteAction = '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars($this->moduleUri . '&CMD=delete&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" '
1055  . ' data-severity="warning"'
1056  . ' data-title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:delete')) . '"'
1057  . ' data-button-close-text="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:cancel')) . '"'
1058  . ' data-content="' . htmlspecialchars($this->getLanguageService()->getLL('msg.delete')) . '"'
1059  . ' title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:delete')) . '" class="icon">' .
1060  $this->moduleTemplate->getIconFactory()->getIcon('actions-edit-delete', Icon::SIZE_SMALL)->render() . '</a>';
1061  $stopAction = '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars($this->moduleUri . '&CMD=stop&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" '
1062  . ' data-severity="warning"'
1063  . ' data-title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:stop')) . '"'
1064  . ' data-button-close-text="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:cancel')) . '"'
1065  . ' data-content="' . htmlspecialchars($this->getLanguageService()->getLL('msg.stop')) . '"'
1066  . ' title="' . htmlspecialchars($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:stop')) . '" class="icon">' .
1067  $this->moduleTemplate->getIconFactory()->getIcon('actions-document-close', Icon::SIZE_SMALL)->render() . '</a>';
1068  $runAction = '<a class="btn btn-default" href="' . htmlspecialchars($this->moduleUri . '&tx_scheduler[execute][]=' . $schedulerRecord['uid']) . '" title="' . htmlspecialchars($this->getLanguageService()->getLL('action.run_task')) . '" class="icon">' .
1069  $this->moduleTemplate->getIconFactory()->getIcon('extensions-scheduler-run-task', Icon::SIZE_SMALL)->render() . '</a>';
1070 
1071  // Define some default values
1072  $lastExecution = '-';
1073  $isRunning = false;
1074  $showAsDisabled = false;
1075  $startExecutionElement = '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>';
1076  // Restore the serialized task and pass it a reference to the scheduler object
1078  $task = unserialize($schedulerRecord['serialized_task_object']);
1079  $class = get_class($task);
1080  if ($class === '__PHP_Incomplete_Class' && preg_match('/^O:[0-9]+:"(?P<classname>.+?)"/', $schedulerRecord['serialized_task_object'], $matches) === 1) {
1081  $class = $matches['classname'];
1082  }
1083  // Assemble information about last execution
1084  if (!empty($schedulerRecord['lastexecution_time'])) {
1085  $lastExecution = date($dateFormat, $schedulerRecord['lastexecution_time']);
1086  if ($schedulerRecord['lastexecution_context'] == 'CLI') {
1087  $context = $this->getLanguageService()->getLL('label.cron');
1088  } else {
1089  $context = $this->getLanguageService()->getLL('label.manual');
1090  }
1091  $lastExecution .= ' (' . $context . ')';
1092  }
1093 
1094  if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
1095  // The task object is valid
1096  $labels = [];
1097  $name = htmlspecialchars($registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')');
1098  $additionalInformation = $task->getAdditionalInformation();
1099  if ($task instanceof \TYPO3\CMS\Scheduler\ProgressProviderInterface) {
1100  $progress = round((float)$task->getProgress(), 2);
1101  $name .= $this->renderTaskProgressBar($progress);
1102  }
1103  if (!empty($additionalInformation)) {
1104  $name .= '<div class="additional-information">' . nl2br(htmlspecialchars($additionalInformation)) . '</div>';
1105  }
1106  // Check if task currently has a running execution
1107  if (!empty($schedulerRecord['serialized_executions'])) {
1108  $labels[] = [
1109  'class' => 'success',
1110  'text' => $this->getLanguageService()->getLL('status.running')
1111  ];
1112  $isRunning = true;
1113  }
1114 
1115  // Prepare display of next execution date
1116  // If task is currently running, date is not displayed (as next hasn't been calculated yet)
1117  // Also hide the date if task is disabled (the information doesn't make sense, as it will not run anyway)
1118  if ($isRunning || $schedulerRecord['disable']) {
1119  $nextDate = '-';
1120  } else {
1121  $nextDate = date($dateFormat, $schedulerRecord['nextexecution']);
1122  if (empty($schedulerRecord['nextexecution'])) {
1123  $nextDate = $this->getLanguageService()->getLL('none');
1124  } elseif ($schedulerRecord['nextexecution'] < $GLOBALS['EXEC_TIME']) {
1125  $labels[] = [
1126  'class' => 'warning',
1127  'text' => $this->getLanguageService()->getLL('status.late'),
1128  'description' => $this->getLanguageService()->getLL('status.legend.scheduled')
1129  ];
1130  }
1131  }
1132  // Get execution type
1133  if ($task->getType() === AbstractTask::TYPE_SINGLE) {
1134  $execType = $this->getLanguageService()->getLL('label.type.single');
1135  $frequency = '-';
1136  } else {
1137  $execType = $this->getLanguageService()->getLL('label.type.recurring');
1138  if ($task->getExecution()->getCronCmd() == '') {
1139  $frequency = $task->getExecution()->getInterval();
1140  } else {
1141  $frequency = $task->getExecution()->getCronCmd();
1142  }
1143  }
1144  // Get multiple executions setting
1145  if ($task->getExecution()->getMultiple()) {
1146  $multiple = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:yes');
1147  } else {
1148  $multiple = $this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:no');
1149  }
1150  // Define checkbox
1151  $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>';
1152 
1153  $actions = '<div class="btn btn-group" role="group">' . $editAction . $toggleHiddenAction . $deleteAction . '</div>';
1154 
1155  // Check the disable status
1156  // Row is shown dimmed if task is disabled, unless it is still running
1157  if ($schedulerRecord['disable'] && !$isRunning) {
1158  $labels[] = [
1159  'class' => 'default',
1160  'text' => $this->getLanguageService()->getLL('status.disabled')
1161  ];
1162  $showAsDisabled = true;
1163  }
1164 
1165  // Show no action links (edit, delete) if task is running
1166  if ($isRunning) {
1167  $actions = '<div class="btn btn-group" role="group">' . $stopAction . '</div>';
1168  } else {
1169  $actions .= '<div class="btn btn-group" role="group">' . $runAction . '</div>';
1170  }
1171 
1172  // Check if the last run failed
1173  if (!empty($schedulerRecord['lastexecution_failure'])) {
1174  // Try to get the stored exception array
1176  $exceptionArray = @unserialize($schedulerRecord['lastexecution_failure']);
1177  // If the exception could not be unserialized, issue a default error message
1178  if (!is_array($exceptionArray) || empty($exceptionArray)) {
1179  $labelDescription = $this->getLanguageService()->getLL('msg.executionFailureDefault');
1180  } else {
1181  $labelDescription = sprintf($this->getLanguageService()->getLL('msg.executionFailureReport'), $exceptionArray['code'], $exceptionArray['message']);
1182  }
1183  $labels[] = [
1184  'class' => 'danger',
1185  'text' => $this->getLanguageService()->getLL('status.failure'),
1186  'description' => $labelDescription
1187  ];
1188  }
1189  // Format the execution status,
1190  // including failure feedback, if any
1191  $taskDesc = '';
1192  if ($schedulerRecord['description'] !== '') {
1193  $taskDesc = '<span class="description">' . nl2br(htmlspecialchars($schedulerRecord['description'])) . '</span>';
1194  }
1195  $taskName = '<span class="name"><a href="' . $link . '">' . $name . '</a></span>';
1196 
1197  $table[] =
1198  '<tr class="' . ($showAsDisabled ? 'disabled' : '') . '">'
1199  . '<td>' . $startExecutionElement . '</td>'
1200  . '<td class="right">' . $schedulerRecord['uid'] . '</td>'
1201  . '<td>' . $this->makeStatusLabel($labels) . $taskName . $taskDesc . '</td>'
1202  . '<td>' . $execType . '</td>'
1203  . '<td>' . $frequency . '</td>'
1204  . '<td>' . $multiple . '</td>'
1205  . '<td>' . $lastExecution . '</td>'
1206  . '<td>' . $nextDate . '</td>'
1207  . '<td nowrap="nowrap">' . $actions . '</td>'
1208  . '</tr>';
1209  } else {
1210  // The task object is not valid
1211  // Prepare to issue an error
1212  $executionStatusOutput = '<span class="label label-danger">'
1213  . htmlspecialchars(sprintf(
1214  $this->getLanguageService()->getLL('msg.invalidTaskClass'),
1215  $class
1216  ))
1217  . '</span>';
1218  $table[] =
1219  '<tr>'
1220  . '<td>' . $startExecutionElement . '</td>'
1221  . '<td class="right">' . $schedulerRecord['uid'] . '</td>'
1222  . '<td colspan="6">' . $executionStatusOutput . '</td>'
1223  . '<td nowrap="nowrap"><div class="btn-group" role="group">'
1224  . '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'
1225  . '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'
1226  . $deleteAction
1227  . '<span class="btn btn-default disabled">' . $this->moduleTemplate->getIconFactory()->getIcon('empty-empty', Icon::SIZE_SMALL)->render() . '</span>'
1228  . '</div></td>'
1229  . '</tr>';
1230  }
1231  }
1232  }
1233 
1234  $this->view->assign('table', '<table class="table table-striped table-hover">' . implode(LF, $table) . '</table>');
1235  $this->view->assign('now', $this->getServerTime());
1236 
1237  return $this->view->render();
1238  }
1239 
1246  protected function makeStatusLabel(array $labels)
1247  {
1248  $htmlLabels = [];
1249  foreach ($labels as $label) {
1250  if (empty($label['text'])) {
1251  continue;
1252  }
1253  $htmlLabels[] = '<span class="label label-' . htmlspecialchars($label['class']) . ' pull-right" title="' . htmlspecialchars($label['description']) . '">' . htmlspecialchars($label['text']) . '</span>';
1254  }
1255 
1256  return implode('&nbsp;', $htmlLabels);
1257  }
1258 
1264  protected function saveTask()
1265  {
1266  // If a task is being edited fetch old task data
1267  if (!empty($this->submittedData['uid'])) {
1268  try {
1269  $taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
1271  $task = unserialize($taskRecord['serialized_task_object']);
1272  } catch (\OutOfBoundsException $e) {
1273  // If the task could not be fetched, issue an error message
1274  // and exit early
1275  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
1276  return;
1277  }
1278  // Register single execution
1279  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1280  $task->registerSingleExecution($this->submittedData['start']);
1281  } else {
1282  if (!empty($this->submittedData['croncmd'])) {
1283  // Definition by cron-like syntax
1284  $interval = 0;
1285  $cronCmd = $this->submittedData['croncmd'];
1286  } else {
1287  // Definition by interval
1288  $interval = $this->submittedData['interval'];
1289  $cronCmd = '';
1290  }
1291  // Register recurring execution
1292  $task->registerRecurringExecution($this->submittedData['start'], $interval, $this->submittedData['end'], $this->submittedData['multiple'], $cronCmd);
1293  }
1294  // Set disable flag
1295  $task->setDisabled($this->submittedData['disable']);
1296  // Set description
1297  $task->setDescription($this->submittedData['description']);
1298  // Set task group
1299  $task->setTaskGroup($this->submittedData['task_group']);
1300  // Save additional input values
1301  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1303  $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1304  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1305  $providerObject->saveAdditionalFields($this->submittedData, $task);
1306  }
1307  }
1308  // Save to database
1309  $result = $this->scheduler->saveTask($task);
1310  if ($result) {
1311  $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was updated', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1312  $this->addMessage($this->getLanguageService()->getLL('msg.updateSuccess'));
1313  } else {
1314  $this->addMessage($this->getLanguageService()->getLL('msg.updateError'), FlashMessage::ERROR);
1315  }
1316  } else {
1317  // A new task is being created
1318  // Create an instance of chosen class
1320  $task = GeneralUtility::makeInstance($this->submittedData['class']);
1321  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_SINGLE) {
1322  // Set up single execution
1323  $task->registerSingleExecution($this->submittedData['start']);
1324  } else {
1325  // Set up recurring execution
1326  $task->registerRecurringExecution($this->submittedData['start'], $this->submittedData['interval'], $this->submittedData['end'], $this->submittedData['multiple'], $this->submittedData['croncmd']);
1327  }
1328  // Save additional input values
1329  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1331  $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1332  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1333  $providerObject->saveAdditionalFields($this->submittedData, $task);
1334  }
1335  }
1336  // Set disable flag
1337  $task->setDisabled($this->submittedData['disable']);
1338  // Set description
1339  $task->setDescription($this->submittedData['description']);
1340  // Set description
1341  $task->setTaskGroup($this->submittedData['task_group']);
1342  // Add to database
1343  $result = $this->scheduler->addTask($task);
1344  if ($result) {
1345  $this->getBackendUser()->writelog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was added', [$task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()]);
1346  $this->addMessage($this->getLanguageService()->getLL('msg.addSuccess'));
1347 
1348  // set the uid of the just created task so that we
1349  // can continue editing after initial saving
1350  $this->submittedData['uid'] = $task->getTaskUid();
1351  } else {
1352  $this->addMessage($this->getLanguageService()->getLL('msg.addError'), FlashMessage::ERROR);
1353  }
1354  }
1355  }
1356 
1357  /*************************
1358  *
1359  * INPUT PROCESSING UTILITIES
1360  *
1361  *************************/
1367  protected function preprocessData()
1368  {
1369  $result = true;
1370  // Validate id
1371  $this->submittedData['uid'] = empty($this->submittedData['uid']) ? 0 : (int)$this->submittedData['uid'];
1372  // Validate selected task class
1373  if (!class_exists($this->submittedData['class'])) {
1374  $this->addMessage($this->getLanguageService()->getLL('msg.noTaskClassFound'), FlashMessage::ERROR);
1375  }
1376  // Check start date
1377  if (empty($this->submittedData['start'])) {
1378  $this->addMessage($this->getLanguageService()->getLL('msg.noStartDate'), FlashMessage::ERROR);
1379  $result = false;
1380  } else {
1381  try {
1382  $this->submittedData['start'] = (int)$this->submittedData['start'];
1383  } catch (\Exception $e) {
1384  $this->addMessage($this->getLanguageService()->getLL('msg.invalidStartDate'), FlashMessage::ERROR);
1385  $result = false;
1386  }
1387  }
1388  // Check end date, if recurring task
1389  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING && !empty($this->submittedData['end'])) {
1390  try {
1391  $this->submittedData['end'] = (int)$this->submittedData['end'];
1392  if ($this->submittedData['end'] < $this->submittedData['start']) {
1393  $this->addMessage($this->getLanguageService()->getLL('msg.endDateSmallerThanStartDate'), FlashMessage::ERROR);
1394  $result = false;
1395  }
1396  } catch (\Exception $e) {
1397  $this->addMessage($this->getLanguageService()->getLL('msg.invalidEndDate'), FlashMessage::ERROR);
1398  $result = false;
1399  }
1400  }
1401  // Set default values for interval and cron command
1402  $this->submittedData['interval'] = 0;
1403  $this->submittedData['croncmd'] = '';
1404  // Check type and validity of frequency, if recurring
1405  if ((int)$this->submittedData['type'] === AbstractTask::TYPE_RECURRING) {
1406  $frequency = trim($this->submittedData['frequency']);
1407  if (empty($frequency)) {
1408  // Empty frequency, not valid
1409  $this->addMessage($this->getLanguageService()->getLL('msg.noFrequency'), FlashMessage::ERROR);
1410  $result = false;
1411  } else {
1412  $cronErrorCode = 0;
1413  $cronErrorMessage = '';
1414  // Try interpreting the cron command
1415  try {
1417  $this->submittedData['croncmd'] = $frequency;
1418  } catch (\Exception $e) {
1419  // Store the exception's result
1420  $cronErrorMessage = $e->getMessage();
1421  $cronErrorCode = $e->getCode();
1422  // Check if the frequency is a valid number
1423  // If yes, assume it is a frequency in seconds, and unset cron error code
1424  if (is_numeric($frequency)) {
1425  $this->submittedData['interval'] = (int)$frequency;
1426  unset($cronErrorCode);
1427  }
1428  }
1429  // If there's a cron error code, issue validation error message
1430  if (!empty($cronErrorCode)) {
1431  $this->addMessage(sprintf($this->getLanguageService()->getLL('msg.frequencyError'), $cronErrorMessage, $cronErrorCode), FlashMessage::ERROR);
1432  $result = false;
1433  }
1434  }
1435  }
1436  // Validate additional input fields
1437  if (!empty($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields'])) {
1439  $providerObject = GeneralUtility::getUserObj($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'][$this->submittedData['class']]['additionalFields']);
1440  if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
1441  // The validate method will return true if all went well, but that must not
1442  // override previous false values => AND the returned value with the existing one
1443  $result &= $providerObject->validateAdditionalFields($this->submittedData, $this);
1444  }
1445  }
1446  return $result;
1447  }
1448 
1449  /*************************
1450  *
1451  * APPLICATION LOGIC UTILITIES
1452  *
1453  *************************/
1461  public function addMessage($message, $severity = FlashMessage::OK)
1462  {
1463  $this->moduleTemplate->addFlashMessage($message, '', $severity);
1464  }
1465 
1479  protected function getRegisteredClasses()
1480  {
1481  $list = [];
1482  if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'])) {
1483  foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['scheduler']['tasks'] as $class => $registrationInformation) {
1484  $title = isset($registrationInformation['title']) ? $this->getLanguageService()->sL($registrationInformation['title']) : '';
1485  $description = isset($registrationInformation['description']) ? $this->getLanguageService()->sL($registrationInformation['description']) : '';
1486  $list[$class] = [
1487  'extension' => $registrationInformation['extension'],
1488  'title' => $title,
1489  'description' => $description,
1490  'provider' => isset($registrationInformation['additionalFields']) ? $registrationInformation['additionalFields'] : ''
1491  ];
1492  }
1493  }
1494  return $list;
1495  }
1496 
1502  protected function getRegisteredTaskGroups()
1503  {
1504  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
1505  ->getQueryBuilderForTable('tx_scheduler_task_group');
1506 
1507  return $queryBuilder
1508  ->select('*')
1509  ->from('tx_scheduler_task_group')
1510  ->orderBy('sorting')
1511  ->execute()
1512  ->fetchAll();
1513  }
1514 
1515  /*************************
1516  *
1517  * RENDERING UTILITIES
1518  *
1519  *************************/
1525  protected function getTemplateMarkers()
1526  {
1527  return [
1528  'CONTENT' => $this->content,
1529  'TITLE' => $this->getLanguageService()->getLL('title')
1530  ];
1531  }
1532 
1536  protected function getButtons()
1537  {
1538  $buttonBar = $this->moduleTemplate->getDocHeaderComponent()->getButtonBar();
1539  // CSH
1540  $helpButton = $buttonBar->makeHelpButton()
1541  ->setModuleName('_MOD_' . $this->moduleName)
1542  ->setFieldName('');
1543  $buttonBar->addButton($helpButton);
1544  // Add and Reload
1545  if (empty($this->CMD) || $this->CMD === 'list' || $this->CMD === 'delete' || $this->CMD === 'stop' || $this->CMD === 'toggleHidden') {
1546  $reloadButton = $buttonBar->makeLinkButton()
1547  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_core.xlf:labels.reload'))
1548  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-refresh', Icon::SIZE_SMALL))
1549  ->setHref($this->moduleUri);
1550  $buttonBar->addButton($reloadButton, ButtonBar::BUTTON_POSITION_RIGHT, 1);
1551  if ($this->MOD_SETTINGS['function'] === 'scheduler' && !empty($this->getRegisteredClasses())) {
1552  $addButton = $buttonBar->makeLinkButton()
1553  ->setTitle($this->getLanguageService()->getLL('action.add'))
1554  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-new', Icon::SIZE_SMALL))
1555  ->setHref($this->moduleUri . '&CMD=add');
1556  $buttonBar->addButton($addButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1557  }
1558  }
1559  // Close and Save
1560  if ($this->CMD === 'add' || $this->CMD === 'edit') {
1561  // Close
1562  $closeButton = $buttonBar->makeLinkButton()
1563  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:cancel'))
1564  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-close', Icon::SIZE_SMALL))
1565  ->setOnClick('document.location=' . GeneralUtility::quoteJSvalue($this->moduleUri))
1566  ->setHref('#');
1567  $buttonBar->addButton($closeButton, ButtonBar::BUTTON_POSITION_LEFT, 2);
1568  // Save, SaveAndClose, SaveAndNew
1569  $saveButtonDropdown = $buttonBar->makeSplitButton();
1570  $saveButton = $buttonBar->makeInputButton()
1571  ->setName('CMD')
1572  ->setValue('save')
1573  ->setForm('tx_scheduler_form')
1574  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save', Icon::SIZE_SMALL))
1575  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:save'));
1576  $saveButtonDropdown->addItem($saveButton);
1577  $saveAndNewButton = $buttonBar->makeInputButton()
1578  ->setName('CMD')
1579  ->setValue('savenew')
1580  ->setForm('tx_scheduler_form')
1581  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-new', Icon::SIZE_SMALL))
1582  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:saveAndCreateNewDoc'));
1583  $saveButtonDropdown->addItem($saveAndNewButton);
1584  $saveAndCloseButton = $buttonBar->makeInputButton()
1585  ->setName('CMD')
1586  ->setValue('saveclose')
1587  ->setForm('tx_scheduler_form')
1588  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-document-save-close', Icon::SIZE_SMALL))
1589  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:saveAndClose'));
1590  $saveButtonDropdown->addItem($saveAndCloseButton);
1591  $buttonBar->addButton($saveButtonDropdown, ButtonBar::BUTTON_POSITION_LEFT, 3);
1592  }
1593  // Edit
1594  if ($this->CMD === 'edit') {
1595  $deleteButton = $buttonBar->makeInputButton()
1596  ->setName('CMD')
1597  ->setValue('delete')
1598  ->setForm('tx_scheduler_form')
1599  ->setIcon($this->moduleTemplate->getIconFactory()->getIcon('actions-edit-delete', Icon::SIZE_SMALL))
1600  ->setTitle($this->getLanguageService()->sL('LLL:EXT:lang/Resources/Private/Language/locallang_common.xlf:delete'));
1601  $buttonBar->addButton($deleteButton, ButtonBar::BUTTON_POSITION_LEFT, 4);
1602  }
1603  // Shortcut
1604  $shortcutButton = $buttonBar->makeShortcutButton()
1605  ->setModuleName($this->moduleName)
1606  ->setDisplayName($this->MOD_MENU['function'][$this->MOD_SETTINGS['function']])
1607  ->setSetVariables(['function']);
1608  $buttonBar->addButton($shortcutButton);
1609  }
1610 
1614  protected function getServerTime()
1615  {
1616  $dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e';
1617  return date($dateFormat) . ', GMT ' . date('P') . ')';
1618  }
1619 
1625  protected function getBackendUser()
1626  {
1627  return $GLOBALS['BE_USER'];
1628  }
1629 }
mainAction(ServerRequestInterface $request, ResponseInterface $response)
static getSaltingInstance($saltedHash= '', $mode=TYPO3_MODE)
Definition: SaltFactory.php:83
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']
static makeInstance($className,...$constructorArguments)