SchedulerModuleController.php 66.4 KB
Newer Older
1
2
3
<?php
namespace TYPO3\CMS\Scheduler\Controller;

4
/*
5
 * This file is part of the TYPO3 CMS project.
6
 *
7
8
9
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
10
 *
11
12
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
13
 *
14
15
 * The TYPO3 project - inspiring people to share!
 */
16

17
18
use TYPO3\CMS\Backend\Utility\BackendUtility;
use TYPO3\CMS\Backend\Utility\IconUtility;
19
use TYPO3\CMS\Core\Database\DatabaseConnection;
20
21
use TYPO3\CMS\Core\Imaging\Icon;
use TYPO3\CMS\Core\Imaging\IconFactory;
22
use TYPO3\CMS\Core\Messaging\FlashMessage;
23
24
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
use TYPO3\CMS\Core\Messaging\FlashMessageService;
25
use TYPO3\CMS\Core\Page\PageRenderer;
26
27
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
28
use TYPO3\CMS\Fluid\ViewHelpers\Be\InfoboxViewHelper;
29
30
use TYPO3\CMS\Saltedpasswords\Salt\SaltFactory;
use TYPO3\CMS\Saltedpasswords\Utility\SaltedPasswordsUtility;
31
use TYPO3\CMS\Scheduler\Task\AbstractTask;
32

33
34
35
36
37
38
39
40
/**
 * Module 'TYPO3 Scheduler administration module' for the 'scheduler' extension.
 */
class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClass {

	/**
	 * Array containing submitted data when editing or adding a task
	 *
41
	 * @var array
42
43
44
45
46
47
48
	 */
	protected $submittedData = array();

	/**
	 * Array containing all messages issued by the application logic
	 * Contains the error's severity and the message itself
	 *
49
	 * @var array
50
51
52
53
	 */
	protected $messages = array();

	/**
54
	 * @var string Key of the CSH file
55
56
57
58
	 */
	protected $cshKey;

	/**
59
	 * @var \TYPO3\CMS\Scheduler\Scheduler Local scheduler instance
60
61
62
63
	 */
	protected $scheduler;

	/**
64
65
66
67
68
69
70
71
72
	 * @var string
	 */
	protected $backendTemplatePath = '';

	/**
	 * @var \TYPO3\CMS\Fluid\View\StandaloneView
	 */
	protected $view;

73
74
75
76
77
78
79
	/**
	 * The name of the module
	 *
	 * @var string
	 */
	protected $moduleName = 'system_txschedulerM1';

80
81
82
83
84
	/**
	 * @var string Base URI of scheduler module
	 */
	protected $moduleUri;

85
86
87
88
89
	/**
	 * @var IconFactory
	 */
	protected $iconFactory;

90
	/**
91
92
93
	 * @return \TYPO3\CMS\Scheduler\Controller\SchedulerModuleController
	 */
	public function __construct() {
94
		$this->getLanguageService()->includeLLFile('EXT:scheduler/Resources/Private/Language/locallang.xlf');
95
96
97
98
		$this->MCONF = array(
			'name' => $this->moduleName,
		);
		$this->cshKey = '_MOD_' . $this->moduleName;
99
		$this->backendTemplatePath = ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Backend/SchedulerModule/';
100
		$this->view = GeneralUtility::makeInstance(\TYPO3\CMS\Fluid\View\StandaloneView::class);
101
		$this->view->getRequest()->setControllerExtensionName('scheduler');
102
		$this->moduleUri = BackendUtility::getModuleUrl($this->moduleName);
103
		$this->iconFactory = GeneralUtility::makeInstance(IconFactory::class);
104
105
106

		$pageRenderer = GeneralUtility::makeInstance(PageRenderer::class);
		$pageRenderer->loadRequireJsModule('TYPO3/CMS/Backend/Modal');
107
108
109
110
111
112
113
114
115
	}

	/**
	 * Initializes the backend module
	 *
	 * @return void
	 */
	public function init() {
		parent::init();
116

117
		// Initialize document
118
		$this->doc = GeneralUtility::makeInstance(\TYPO3\CMS\Backend\Template\DocumentTemplate::class);
119
		$this->doc->setModuleTemplate(ExtensionManagementUtility::extPath('scheduler') . 'Resources/Private/Templates/Module.html');
120
121
		$this->doc->bodyTagId = 'typo3-mod-php';
		$this->doc->bodyTagAdditions = 'class="tx_scheduler_mod1"';
122

123
		// Create scheduler instance
124
		$this->scheduler = GeneralUtility::makeInstance(\TYPO3\CMS\Scheduler\Scheduler::class);
125
126
127
128
129
130
131
132
133
134
	}

	/**
	 * Adds items to the ->MOD_MENU array. Used for the function menu selector.
	 *
	 * @return void
	 */
	public function menuConfig() {
		$this->MOD_MENU = array(
			'function' => array(
135
136
137
				'scheduler' => $this->getLanguageService()->getLL('function.scheduler'),
				'check' => $this->getLanguageService()->getLL('function.check'),
				'info' => $this->getLanguageService()->getLL('function.info')
138
139
140
141
142
143
144
145
146
147
148
149
150
			)
		);
		parent::menuConfig();
	}

	/**
	 * Main function of the module. Write the content to $this->content
	 *
	 * @return void
	 */
	public function main() {
		// Access check!
		// The page will show only if user has admin rights
151
		if ($this->getBackendUser()->isAdmin()) {
152
153
			// Set the form
			$this->doc->form = '<form name="tx_scheduler_form" id="tx_scheduler_form" method="post" action="">';
154

155
			// Prepare main content
156
			$this->content = $this->doc->header($this->getLanguageService()->getLL('function.' . $this->MOD_SETTINGS['function']));
157
158
159
			$this->content .= $this->getModuleContent();
		} else {
			// If no access, only display the module's title
160
			$this->content = $this->doc->header($this->getLanguageService()->getLL('title'));
161
162
163
164
165
			$this->content .= $this->doc->spacer(5);
		}
		// Place content inside template
		$content = $this->doc->moduleBody(array(), $this->getDocHeaderButtons(), $this->getTemplateMarkers());
		// Renders the module page
166
		$this->content = $this->doc->render($this->getLanguageService()->getLL('title'), $content);
167
168
169
170
171
172
173
174
175
176
177
	}

	/**
	 * Generate the module's content
	 *
	 * @return string HTML of the module's main content
	 */
	protected function getModuleContent() {
		$content = '';
		$sectionTitle = '';
		// Get submitted data
178
		$this->submittedData = GeneralUtility::_GPmerged('tx_scheduler');
179
		$this->submittedData['uid'] = (int)$this->submittedData['uid'];
180
		// If a save command was submitted, handle saving now
181
		if ($this->CMD === 'save' || $this->CMD === 'saveclose' || $this->CMD === 'savenew') {
182
			$previousCMD = GeneralUtility::_GP('previousCMD');
183
184
185
186
187
			// First check the submitted data
			$result = $this->preprocessData();
			// If result is ok, proceed with saving
			if ($result) {
				$this->saveTask();
188
				if ($this->CMD === 'saveclose') {
189
190
					// Unset command, so that default screen gets displayed
					unset($this->CMD);
191
				} elseif ($this->CMD === 'save') {
192
193
					// After saving a "add form", return to edit
					$this->CMD = 'edit';
194
195
196
197
198
				} elseif ($this->CMD === 'savenew') {
					// Unset submitted data, so that empty form gets displayed
					unset($this->submittedData);
					// After saving a "add/edit form", return to add
					$this->CMD = 'add';
199
200
201
202
				} else {
					// Return to edit form
					$this->CMD = $previousCMD;
				}
203
204
205
206
			} else {
				$this->CMD = $previousCMD;
			}
		}
207

208
		// Handle chosen action
209
		switch ((string)$this->MOD_SETTINGS['function']) {
210
211
			case 'scheduler':
				$this->executeTasks();
212

213
214
215
216
217
				switch ($this->CMD) {
					case 'add':
					case 'edit':
						try {
							// Try adding or editing
218
							$content .= $this->editTaskAction();
219
							$sectionTitle = $this->getLanguageService()->getLL('action.' . $this->CMD);
220
						} catch (\Exception $e) {
221
222
223
224
225
							if ($e->getCode() === 1305100019) {
								// Invalid controller class name exception
								$this->addMessage($e->getMessage(), FlashMessage::ERROR);
							}
							// An exception may also happen when the task to
226
227
228
							// edit could not be found. In this case revert
							// to displaying the list of tasks
							// It can also happen when attempting to edit a running task
229
							$content .= $this->listTasksAction();
230
231
232
233
						}
						break;
					case 'delete':
						$this->deleteTask();
234
						$content .= $this->listTasksAction();
235
236
237
						break;
					case 'stop':
						$this->stopTask();
238
						$content .= $this->listTasksAction();
239
						break;
240
241
242
243
					case 'toggleHidden':
						$this->toggleDisableAction();
						$content .= $this->listTasksAction();
						break;
244
245
246
					case 'list':

					default:
247
						$content .= $this->listTasksAction();
248
249
				}
				break;
250
251

			// Setup check screen
252
			case 'check':
253
				// @todo move check to the report module
254
				$content .= $this->checkScreenAction();
255
				break;
256
257

			// Information screen
258
			case 'info':
259
				$content .= $this->infoScreenAction();
260
261
262
				break;
		}
		// Wrap the content in a section
263
		return $this->doc->section($sectionTitle, '<div class="tx_scheduler_mod1">' . $content . '</div>', FALSE, TRUE);
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
	}

	/**
	 * This method actually prints out the module's HTML content
	 *
	 * @return void
	 */
	public function render() {
		echo $this->content;
	}

	/**
	 * This method checks the status of the '_cli_scheduler' user
	 * It will differentiate between a non-existing user and an existing,
	 * but disabled user (as per enable fields)
	 *
280
	 * @return int -1 If user doesn't exist, 0 If user exist but not enabled, 1 If user exists and is enabled
281
282
283
284
	 */
	protected function checkSchedulerUser() {
		$schedulerUserStatus = -1;
		// Assemble base WHERE clause
285
		$where = 'username = \'_cli_scheduler\' AND admin = 0' . BackendUtility::deleteClause('be_users');
286
		// Check if user exists at all
287
288
		$res = $this->getDatabaseConnection()->exec_SELECTquery('1', 'be_users', $where);
		if ($this->getDatabaseConnection()->sql_fetch_assoc($res)) {
289
			$schedulerUserStatus = 0;
290
			$this->getDatabaseConnection()->sql_free_result($res);
291
			// Check if user exists and is enabled
292
293
			$res = $this->getDatabaseConnection()->exec_SELECTquery('1', 'be_users', $where . BackendUtility::BEenableFields('be_users'));
			if ($this->getDatabaseConnection()->sql_fetch_assoc($res)) {
294
295
296
				$schedulerUserStatus = 1;
			}
		}
297
		$this->getDatabaseConnection()->sql_free_result($res);
298
299
300
301
302
303
304
305
306
307
308
309
		return $schedulerUserStatus;
	}

	/**
	 * This method creates the "cli_scheduler" BE user if it doesn't exist
	 *
	 * @return void
	 */
	protected function createSchedulerUser() {
		// Check _cli_scheduler user status
		$checkUser = $this->checkSchedulerUser();
		// Prepare default message
310
		$message = $this->getLanguageService()->getLL('msg.userExists');
311
		$severity = FlashMessage::WARNING;
312
313
314
		// If the user does not exist, try creating it
		if ($checkUser == -1) {
			// Prepare necessary data for _cli_scheduler user creation
315
316
317
318
319
			$password = uniqid('scheduler', TRUE);
			if (SaltedPasswordsUtility::isUsageEnabled()) {
				$objInstanceSaltedPW = SaltFactory::getSaltingInstance();
				$password = $objInstanceSaltedPW->getHashedPassword($password);
			}
320
			$data = array('be_users' => array('NEW' => array('username' => '_cli_scheduler', 'password' => $password, 'pid' => 0)));
321
			/** @var $tcemain \TYPO3\CMS\Core\DataHandling\DataHandler */
322
			$tcemain = GeneralUtility::makeInstance(\TYPO3\CMS\Core\DataHandling\DataHandler::class);
323
324
325
326
327
328
			$tcemain->stripslashes_values = 0;
			$tcemain->start($data, array());
			$tcemain->process_datamap();
			// Check if a new uid was indeed generated (i.e. a new record was created)
			// (counting TCEmain errors doesn't work as some failures don't report errors)
			$numberOfNewIDs = count($tcemain->substNEWwithIDs);
329
			if ($numberOfNewIDs === 1) {
330
				$message = $this->getLanguageService()->getLL('msg.userCreated');
331
				$severity = FlashMessage::OK;
332
			} else {
333
				$message = $this->getLanguageService()->getLL('msg.userNotCreated');
334
				$severity = FlashMessage::ERROR;
335
336
337
338
339
340
341
342
343
344
345
			}
		}
		$this->addMessage($message, $severity);
	}

	/**
	 * This method displays the result of a number of checks
	 * on whether the Scheduler is ready to run or running properly
	 *
	 * @return string Further information
	 */
346
347
348
	protected function checkScreenAction() {
		$this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'CheckScreen.html');

349
		// First, check if _cli_scheduler user creation was requested
350
		if ($this->CMD === 'user') {
351
352
			$this->createSchedulerUser();
		}
353

354
		// Display information about last automated run, as stored in the system registry
355
		/** @var $registry \TYPO3\CMS\Core\Registry */
356
		$registry = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Registry::class);
357
		$lastRun = $registry->get('tx_scheduler', 'lastRun');
358
		if (!is_array($lastRun)) {
359
			$message = $this->getLanguageService()->getLL('msg.noLastRun');
360
			$severity = InfoboxViewHelper::STATE_WARNING;
361
		} else {
362
			if (empty($lastRun['end']) || empty($lastRun['start']) || empty($lastRun['type'])) {
363
				$message = $this->getLanguageService()->getLL('msg.incompleteLastRun');
364
				$severity = InfoboxViewHelper::STATE_WARNING;
365
366
367
368
369
370
			} else {
				$startDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['start']);
				$startTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['start']);
				$endDate = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'], $lastRun['end']);
				$endTime = date($GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'], $lastRun['end']);
				$label = 'automatically';
371
				if ($lastRun['type'] === 'manual') {
372
373
					$label = 'manually';
				}
374
375
				$type = $this->getLanguageService()->getLL('label.' . $label);
				$message = sprintf($this->getLanguageService()->getLL('msg.lastRun'), $type, $startDate, $startTime, $endDate, $endTime);
376
				$severity = InfoboxViewHelper::STATE_INFO;
377
378
			}
		}
379
380
		$this->view->assign('lastRunMessage', $message);
		$this->view->assign('lastRunSeverity', $severity);
381

382
383
384
		// Check CLI user
		$checkUser = $this->checkSchedulerUser();
		if ($checkUser == -1) {
385
			$link = $this->moduleUri . '&SET[function]=check&CMD=user';
386
			$message = sprintf($this->getLanguageService()->getLL('msg.schedulerUserMissing'), htmlspecialchars($link));
387
			$severity = InfoboxViewHelper::STATE_ERROR;
388
		} elseif ($checkUser == 0) {
389
			$message = $this->getLanguageService()->getLL('msg.schedulerUserFoundButDisabled');
390
			$severity = InfoboxViewHelper::STATE_WARNING;
391
		} else {
392
			$message = $this->getLanguageService()->getLL('msg.schedulerUserFound');
393
			$severity = InfoboxViewHelper::STATE_OK;
394
		}
395
396
		$this->view->assign('cliUserMessage', $message);
		$this->view->assign('cliUserSeverity', $severity);
397

398
399
		// Check if CLI script is executable or not
		$script = PATH_typo3 . 'cli_dispatch.phpsh';
400
401
		$this->view->assign('script', $script);

402
403
404
405
406
407
408
409
		// Skip this check if running Windows, as rights do not work the same way on this platform
		// (i.e. the script will always appear as *not* executable)
		if (TYPO3_OS === 'WIN') {
			$isExecutable = TRUE;
		} else {
			$isExecutable = is_executable($script);
		}
		if ($isExecutable) {
410
			$message = $this->getLanguageService()->getLL('msg.cliScriptExecutable');
411
			$severity = InfoboxViewHelper::STATE_OK;
412
		} else {
413
			$message = $this->getLanguageService()->getLL('msg.cliScriptNotExecutable');
414
			$severity = InfoboxViewHelper::STATE_ERROR;
415
		}
416
417
		$this->view->assign('isExecutableMessage', $message);
		$this->view->assign('isExecutableSeverity', $severity);
418
419

		return $this->view->render();
420
421
422
423
424
	}

	/**
	 * This method gathers information about all available task classes and displays it
	 *
425
	 * @return string html
426
	 */
427
	protected function infoScreenAction() {
428
		$registeredClasses = $this->getRegisteredClasses();
429
		// No classes available, display information message
430
		if (empty($registeredClasses)) {
431
432
			$this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreenNoClasses.html');
			return $this->view->render();
433
434
		}

435
436
437
438
		$this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'InfoScreen.html');
		$this->view->assign('registeredClasses', $registeredClasses);

		return $this->view->render();
439
440
441
442
443
444
445
446
447
	}

	/**
	 * Renders the task progress bar.
	 *
	 * @param float $progress Task progress
	 * @return string Progress bar markup
	 */
	protected function renderTaskProgressBar($progress) {
448
		$progressText = $this->getLanguageService()->getLL('status.progress') . ':&nbsp;' . $progress . '%';
449
		return '<div class="progress">'
450
451
		. '<div class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="' . $progress . '" aria-valuemin="0" aria-valuemax="100" style="width: ' . $progress . '%;">' . $progressText . '</div>'
		. '</div>';
452
453
454
455
456
457
458
459
460
461
462
463
464
	}

	/**
	 * Delete a task from the execution queue
	 *
	 * @return void
	 */
	protected function deleteTask() {
		try {
			// Try to fetch the task and delete it
			$task = $this->scheduler->fetchTask($this->submittedData['uid']);
			// If the task is currently running, it may not be deleted
			if ($task->isExecutionRunning()) {
465
				$this->addMessage($this->getLanguageService()->getLL('msg.maynotDeleteRunningTask'), FlashMessage::ERROR);
466
467
			} else {
				if ($this->scheduler->removeTask($task)) {
468
					$this->getBackendUser()->writeLog(4, 0, 0, 0, 'Scheduler task "%s" (UID: %s, Class: "%s") was deleted', array($task->getTaskTitle(), $task->getTaskUid(), $task->getTaskClassName()));
469
					$this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
470
				} else {
471
					$this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
472
473
474
475
				}
			}
		} catch (\UnexpectedValueException $e) {
			// The task could not be unserialized properly, simply delete the database record
476
			$result = $this->getDatabaseConnection()->exec_DELETEquery('tx_scheduler_task', 'uid = ' . (int)$this->submittedData['uid']);
477
			if ($result) {
478
				$this->addMessage($this->getLanguageService()->getLL('msg.deleteSuccess'));
479
			} else {
480
				$this->addMessage($this->getLanguageService()->getLL('msg.deleteError'), FlashMessage::ERROR);
481
482
483
			}
		} catch (\OutOfBoundsException $e) {
			// The task was not found, for some reason
484
			$this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
485
486
487
488
489
490
491
		}
	}

	/**
	 * Clears the registered running executions from the task
	 * Note that this doesn't actually stop the running script. It just unmarks
	 * all executions.
492
	 * @todo find a way to really kill the running task
493
494
495
496
497
498
499
500
501
502
503
	 *
	 * @return void
	 */
	protected function stopTask() {
		try {
			// Try to fetch the task and stop it
			$task = $this->scheduler->fetchTask($this->submittedData['uid']);
			if ($task->isExecutionRunning()) {
				// If the task is indeed currently running, clear marked executions
				$result = $task->unmarkAllExecutions();
				if ($result) {
504
					$this->addMessage($this->getLanguageService()->getLL('msg.stopSuccess'));
505
				} else {
506
					$this->addMessage($this->getLanguageService()->getLL('msg.stopError'), FlashMessage::ERROR);
507
508
509
				}
			} else {
				// The task is not running, nothing to unmark
510
				$this->addMessage($this->getLanguageService()->getLL('msg.maynotStopNonRunningTask'), FlashMessage::WARNING);
511
512
513
			}
		} catch (\Exception $e) {
			// The task was not found, for some reason
514
			$this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
515
516
517
		}
	}

518
519
520
521
522
523
524
525
526
527
528
	/**
	 * Toggles the disabled state of the submitted task
	 *
	 * @return void
	 */
	protected function toggleDisableAction() {
		$task = $this->scheduler->fetchTask($this->submittedData['uid']);
		$task->setDisabled(!$task->isDisabled());
		$task->save();
	}

529
530
531
	/**
	 * Return a form to add a new task or edit an existing one
	 *
532
	 * @return string HTML form to add or edit a task
533
	 */
534
535
536
	protected function editTaskAction() {
		$this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'EditTask.html');

537
538
		$registeredClasses = $this->getRegisteredClasses();
		$registeredTaskGroups = $this->getRegisteredTaskGroups();
539

540
541
542
		$taskInfo = array();
		$task = NULL;
		$process = 'edit';
543

544
545
546
547
548
549
		if ($this->submittedData['uid'] > 0) {
			// If editing, retrieve data for existing task
			try {
				$taskRecord = $this->scheduler->fetchTaskRecord($this->submittedData['uid']);
				// If there's a registered execution, the task should not be edited
				if (!empty($taskRecord['serialized_executions'])) {
550
					$this->addMessage($this->getLanguageService()->getLL('msg.maynotEditRunningTask'), FlashMessage::ERROR);
551
552
					throw new \LogicException('Runnings tasks cannot not be edited', 1251232849);
				}
553

554
				// Get the task object
555
				/** @var $task \TYPO3\CMS\Scheduler\Task\AbstractTask */
556
				$task = unserialize($taskRecord['serialized_task_object']);
557

558
559
				// Set some task information
				$taskInfo['disable'] = $taskRecord['disable'];
560
				$taskInfo['description'] = $taskRecord['description'];
561
				$taskInfo['task_group'] = $taskRecord['task_group'];
562

563
				// Check that the task object is valid
564
				if (isset($registeredClasses[get_class($task)]) && $this->scheduler->isValidTaskObject($task)) {
565
					// The task object is valid, process with fetching current data
566
					$taskInfo['class'] = get_class($task);
567
					// Get execution information
568
569
					$taskInfo['start'] = (int)$task->getExecution()->getStart();
					$taskInfo['end'] = (int)$task->getExecution()->getEnd();
570
571
572
573
574
575
					$taskInfo['interval'] = $task->getExecution()->getInterval();
					$taskInfo['croncmd'] = $task->getExecution()->getCronCmd();
					$taskInfo['multiple'] = $task->getExecution()->getMultiple();
					if (!empty($taskInfo['interval']) || !empty($taskInfo['croncmd'])) {
						// Guess task type from the existing information
						// If an interval or a cron command is defined, it's a recurring task
576
						// @todo remove magic numbers for the type, use class constants instead
577
						$taskInfo['type'] = 2;
578
						$taskInfo['frequency'] = $taskInfo['interval'] ?: $taskInfo['croncmd'];
579
580
581
582
583
584
585
586
587
588
					} else {
						// It's not a recurring task
						// Make sure interval and cron command are both empty
						$taskInfo['type'] = 1;
						$taskInfo['frequency'] = '';
						$taskInfo['end'] = 0;
					}
				} else {
					// The task object is not valid
					// Issue error message
589
					$this->addMessage(sprintf($this->getLanguageService()->getLL('msg.invalidTaskClassEdit'), get_class($task)), FlashMessage::ERROR);
590
591
592
593
594
595
596
597
598
					// Initialize empty values
					$taskInfo['start'] = 0;
					$taskInfo['end'] = 0;
					$taskInfo['frequency'] = '';
					$taskInfo['multiple'] = FALSE;
					$taskInfo['type'] = 1;
				}
			} catch (\OutOfBoundsException $e) {
				// Add a message and continue throwing the exception
599
				$this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $this->submittedData['uid']), FlashMessage::ERROR);
600
601
602
603
604
605
606
607
608
609
610
611
				throw $e;
			}
		} else {
			// If adding a new object, set some default values
			$taskInfo['class'] = key($registeredClasses);
			$taskInfo['type'] = 2;
			$taskInfo['start'] = $GLOBALS['EXEC_TIME'];
			$taskInfo['end'] = '';
			$taskInfo['frequency'] = '';
			$taskInfo['multiple'] = 0;
			$process = 'add';
		}
612
613
614

		// If some data was already submitted, use it to override
		// existing data
615
		if (!empty($this->submittedData)) {
616
			\TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($taskInfo, $this->submittedData);
617
		}
618

619
620
		// Get the extra fields to display for each task that needs some
		$allAdditionalFields = array();
621
		if ($process === 'add') {
622
623
624
			foreach ($registeredClasses as $class => $registrationInfo) {
				if (!empty($registrationInfo['provider'])) {
					/** @var $providerObject \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface */
625
					$providerObject = GeneralUtility::getUserObj($registrationInfo['provider']);
626
627
628
629
630
631
632
633
					if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
						$additionalFields = $providerObject->getAdditionalFields($taskInfo, NULL, $this);
						$allAdditionalFields = array_merge($allAdditionalFields, array($class => $additionalFields));
					}
				}
			}
		} else {
			if (!empty($registeredClasses[$taskInfo['class']]['provider'])) {
634
				$providerObject = GeneralUtility::getUserObj($registeredClasses[$taskInfo['class']]['provider']);
635
636
637
638
639
				if ($providerObject instanceof \TYPO3\CMS\Scheduler\AdditionalFieldProviderInterface) {
					$allAdditionalFields[$taskInfo['class']] = $providerObject->getAdditionalFields($taskInfo, $task, $this);
				}
			}
		}
640

641
		// Load necessary JavaScript
642
643
644
		$this->getPageRenderer()->loadJquery();
		$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
		$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Backend/DateTimePicker');
645

646
		// Start rendering the add/edit form
647
648
649
		$this->view->assign('uid', htmlspecialchars($this->submittedData['uid']));
		$this->view->assign('cmd', htmlspecialchars($this->CMD));

650
		$table = array();
651

652
		// Disable checkbox
653
		$label = '<label for="task_disable">' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:disable') . '</label>';
654
655
656
657
658
659
660
661
		$table[] =
			'<div class="form-section" id="task_disable_row"><div class="form-group">'
				. BackendUtility::wrapInHelp($this->cshKey, 'task_disable', $label)
				. '<div class="form-control-wrap">'
					. '<input type="hidden" name="tx_scheduler[disable]" value="0">'
					. '<input class="checkbox" type="checkbox" name="tx_scheduler[disable]" value="1" id="task_disable" ' . ($taskInfo['disable'] ? ' checked="checked"' : '') . '>'
				. '</div>'
			. '</div></div>';
662

663
		// Task class selector
664
		$label = '<label for="task_class">' . $this->getLanguageService()->getLL('label.class') . '</label>';
665

666
667
		// On editing, don't allow changing of the task class, unless it was not valid
		if ($this->submittedData['uid'] > 0 && !empty($taskInfo['class'])) {
668
			$cell = '<div>' . $registeredClasses[$taskInfo['class']]['title'] . ' (' . $registeredClasses[$taskInfo['class']]['extension'] . ')</div>';
669
			$cell .= '<input type="hidden" name="tx_scheduler[class]" id="task_class" value="' . htmlspecialchars($taskInfo['class']) . '">';
670
		} else {
671
			$cell = '<select name="tx_scheduler[class]" id="task_class" class="form-control">';
672
673
674
675
676
677
678
679
			// Group registered classes by classname
			$groupedClasses = array();
			foreach ($registeredClasses as $class => $classInfo) {
				$groupedClasses[$classInfo['extension']][$class] = $classInfo;
			}
			ksort($groupedClasses);
			// Loop on all grouped classes to display a selector
			foreach ($groupedClasses as $extension => $class) {
680
				$cell .= '<optgroup label="' . htmlspecialchars($extension) . '">';
681
682
				foreach ($groupedClasses[$extension] as $class => $classInfo) {
					$selected = $class == $taskInfo['class'] ? ' selected="selected"' : '';
683
					$cell .= '<option value="' . htmlspecialchars($class) . '"' . 'title="' . htmlspecialchars($classInfo['description']) . '" ' . $selected . '>' . htmlspecialchars($classInfo['title']) . '</option>';
684
685
686
687
688
				}
				$cell .= '</optgroup>';
			}
			$cell .= '</select>';
		}
689
690
691
692
693
694
695
		$table[] =
			'<div class="form-section" id="task_class_row"><div class="form-group">'
				. BackendUtility::wrapInHelp($this->cshKey, 'task_class', $label)
				. '<div class="form-control-wrap">'
					. $cell
				. '</div>'
			. '</div></div>';
696

697
		// Task type selector
698
		$label = '<label for="task_type">' . $this->getLanguageService()->getLL('label.type') . '</label>';
699
700
701
702
703
704
705
706
707
708
		$table[] =
			'<div class="form-section" id="task_type_row"><div class="form-group">'
				. BackendUtility::wrapInHelp($this->cshKey, 'task_type', $label)
				. '<div class="form-control-wrap">'
					. '<select name="tx_scheduler[type]" id="task_type" class="form-control">'
						. '<option value="1" ' . ((int)$taskInfo['type'] === 1 ? ' selected="selected"' : '') . '>' . $this->getLanguageService()->getLL('label.type.single') . '</option>'
						. '<option value="2" ' . ((int)$taskInfo['type'] === 2 ? ' selected="selected"' : '') . '>' . $this->getLanguageService()->getLL('label.type.recurring') . '</option>'
					. '</select>'
				. '</div>'
			. '</div></div>';
709

710
		// Task group selector
711
		$label = '<label for="task_group">' . $this->getLanguageService()->getLL('label.group') . '</label>';
712
713
		$cell = '<select name="tx_scheduler[task_group]" id="task_class" class="form-control">';

714
715
716
717
718
719
720
721
722
		// Loop on all groups to display a selector
		$cell .= '<option value="0" title=""></option>';
		foreach ($registeredTaskGroups as $taskGroup) {
			$selected = $taskGroup['uid'] == $taskInfo['task_group'] ? ' selected="selected"' : '';
			$cell .= '<option value="' . $taskGroup['uid'] . '"' . 'title="';
			$cell .= htmlspecialchars($taskGroup['groupName']) . '"' . $selected . '>';
			$cell .= htmlspecialchars($taskGroup['groupName']) . '</option>';
		}
		$cell .= '</select>';
723

724
725
726
727
728
729
730
		$table[] =
			'<div class="form-section" id="task_group_row"><div class="form-group">'
				. BackendUtility::wrapInHelp($this->cshKey, 'task_group', $label)
				. '<div class="form-control-wrap">'
					. $cell
				. '</div>'
			. '</div></div>';
731

732
733
		$dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['USdateFormat'] ? '%H:%M %m-%d-%Y' : '%H:%M %d-%m-%Y';

734
		$label = '<label for="tceforms-datetimefield-task_start">' . BackendUtility::wrapInHelp($this->cshKey, 'task_start', $this->getLanguageService()->getLL('label.start')) . '</label>';
735
		$value = ($taskInfo['start'] > 0 ? strftime($dateFormat, $taskInfo['start']) : '');
736
737
738
739
740
741
742
743
744
745
746
		$table[] =
			'<div class="form-section"><div class="row"><div class="form-group col-sm-6">'
				. $label
				. '<div class="form-control-wrap">'
					. '<div class="input-group" id="tceforms-datetimefield-task_start_row-wrapper">'
						. '<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">'
						. '<input name="tx_scheduler[start]" value="' . $taskInfo['start'] . '" type="hidden">'
						. '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_start_row"><span class="fa fa-calendar"></span></label></span>'
					. '</div>'
				. '</div>'
			. '</div>';
747

748
749
		// End date/time field
		// NOTE: datetime fields need a special id naming scheme
750
		$value = ($taskInfo['end'] > 0 ? strftime($dateFormat, $taskInfo['end']) : '');
751
		$label = '<label for="tceforms-datetimefield-task_end">' . $this->getLanguageService()->getLL('label.end') . '</label>';
752
753
754
755
756
757
758
759
760
761
762
		$table[] =
			'<div class="form-group col-sm-6">'
				. BackendUtility::wrapInHelp($this->cshKey, 'task_end', $label)
				. '<div class="form-control-wrap">'
					. '<div class="input-group" id="tceforms-datetimefield-task_end_row-wrapper">'
						. '<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">'
						. '<input name="tx_scheduler[end]" value="' . $taskInfo['end'] . '" type="hidden">'
						. '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_end_row"><span class="fa fa-calendar"></span></label></span>'
					. '</div>'
				. '</div>'
			. '</div></div></div>';
763

764
		// Frequency input field
765
		$label = '<label for="task_frequency">' . $this->getLanguageService()->getLL('label.frequency.long') . '</label>';
766
767
768
769
770
771
772
		$table[] =
			'<div class="form-section" id="task_frequency_row"><div class="form-group">'
				. BackendUtility::wrapInHelp($this->cshKey, 'task_frequency', $label)
				. '<div class="form-control-wrap">'
					. '<input type="text" name="tx_scheduler[frequency]" class="form-control" id="task_frequency" value="' . htmlspecialchars($taskInfo['frequency']) . '">'
				. '</div>'
			. '</div></div>';
773

774
		// Multiple execution selector
775
		$label = '<label for="task_multiple">' . $this->getLanguageService()->getLL('label.parallel.long') . '</label>';
776
777
778
779
780
781
782
783
		$table[] =
			'<div class="form-section" id="task_multiple_row"><div class="form-group">'
				. BackendUtility::wrapInHelp($this->cshKey, 'task_multiple', $label)
				. '<div class="form-control-wrap">'
					. '<input type="hidden"   name="tx_scheduler[multiple]" value="0">'
					. '<input class="checkbox" type="checkbox" name="tx_scheduler[multiple]" value="1" id="task_multiple" ' . ($taskInfo['multiple'] ? 'checked="checked"' : '') . '>'
				. '</div>'
			. '</div></div>';
784

785
		// Description
786
		$label = '<label for="task_description">' . $this->getLanguageService()->getLL('label.description') . '</label>';
787
788
789
790
791
792
793
		$table[] =
			'<div class="form-section" id="task_description_row"><div class="form-group">'
				. BackendUtility::wrapInHelp($this->cshKey, 'task_description', $label)
				. '<div class="form-control-wrap">'
					. '<textarea class="form-control" name="tx_scheduler[description]">' . htmlspecialchars($taskInfo['description']) . '</textarea>'
				. '</div>'
			. '</div></div>';
794

795
796
797
798
799
800
801
802
803
804
		// Display additional fields
		foreach ($allAdditionalFields as $class => $fields) {
			if ($class == $taskInfo['class']) {
				$additionalFieldsStyle = '';
			} else {
				$additionalFieldsStyle = ' style="display: none"';
			}
			// Add each field to the display, if there are indeed any
			if (isset($fields) && is_array($fields)) {
				foreach ($fields as $fieldID => $fieldInfo) {
805
					$label = '<label for="' . $fieldID . '">' . $this->getLanguageService()->sL($fieldInfo['label']) . '</label>';
806
					$htmlClassName = strtolower(str_replace('\\', '-', $class));
807
808
809
810
811
812

					$table[] =
						'<div class="form-section extraFields extra_fields_' . $htmlClassName . '" ' . $additionalFieldsStyle . ' id="' . $fieldID . '_row"><div class="form-group">'
							. BackendUtility::wrapInHelp($fieldInfo['cshKey'], $fieldInfo['cshLabel'], $label)
							. '<div class="form-control-wrap">' . $fieldInfo['code'] . '</div>'
						. '</div></div>';
813
814
815
				}
			}
		}
816

817
		$this->view->assign('table', implode(LF, $table));
818

819
820
821
822
823
		// Server date time
		$dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'] . ' T (e';
		$this->view->assign('now', date($dateFormat) . ', GMT ' . date('P') . ')');

		return $this->view->render();
824
825
826
827
828
829
830
831
832
	}

	/**
	 * Execute all selected tasks
	 *
	 * @return void
	 */
	protected function executeTasks() {
		// Make sure next automatic scheduler-run is scheduled
833
		if (GeneralUtility::_POST('go') !== NULL) {
834
835
836
			$this->scheduler->scheduleNextSchedulerRunUsingAtDaemon();
		}
		// Continue if some elements have been chosen for execution
837
		if (isset($this->submittedData['execute']) && !empty($this->submittedData['execute'])) {
838
			// Get list of registered classes
839
			$registeredClasses = $this->getRegisteredClasses();
840
841
842
843
844
845
			// Loop on all selected tasks
			foreach ($this->submittedData['execute'] as $uid) {
				try {
					// Try fetching the task
					$task = $this->scheduler->fetchTask($uid);
					$class = get_class($task);
846
					$name = $registeredClasses[$class]['title'] . ' (' . $registeredClasses[$class]['extension'] . ')';
847
848
849
850
					// Now try to execute it and report on outcome
					try {
						$result = $this->scheduler->executeTask($task);
						if ($result) {
851
							$this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executed'), $name));
852
						} else {
853
							$this->addMessage(sprintf($this->getLanguageService()->getLL('msg.notExecuted'), $name), FlashMessage::ERROR);
854
855
856
						}
					} catch (\Exception $e) {
						// An exception was thrown, display its message as an error
857
						$this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $name, $e->getMessage()), FlashMessage::ERROR);
858
859
					}
				} catch (\OutOfBoundsException $e) {
860
					$this->addMessage(sprintf($this->getLanguageService()->getLL('msg.taskNotFound'), $uid), FlashMessage::ERROR);
861
				} catch (\UnexpectedValueException $e) {
862
					$this->addMessage(sprintf($this->getLanguageService()->getLL('msg.executionFailed'), $uid, $e->getMessage()), FlashMessage::ERROR);
863
864
865
866
867
868
869
870
871
872
873
874
875
876
				}
			}
			// Record the run in the system registry
			$this->scheduler->recordLastRun('manual');
			// Make sure to switch to list view after execution
			$this->CMD = 'list';
		}
	}

	/**
	 * Assemble display of list of scheduled tasks
	 *
	 * @return string Table of pending tasks
	 */
877
878
879
	protected function listTasksAction() {
		$this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasks.html');

880
		// Define display format for dates
881
		$dateFormat = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
882

883
		// Get list of registered classes
884
		$registeredClasses = $this->getRegisteredClasses();
885
		// Get list of registered task groups
886
		$registeredTaskGroups = $this->getRegisteredTaskGroups();
887
888
889
890
891

		// add an empty entry for non-grouped tasks
		// add in front of list
		array_unshift($registeredTaskGroups, array('uid' => 0, 'groupName' => ''));

892
		// Get all registered tasks
893
		// Just to get the number of entries
894
		$query = array(
895
896
897
898
899
900
901
902
903
904
			'SELECT' => '
				tx_scheduler_task.*,
				tx_scheduler_task_group.groupName as taskGroupName,
				tx_scheduler_task_group.description as taskGroupDescription,
				tx_scheduler_task_group.deleted as isTaskGroupDeleted
				',
			'FROM' => '
				tx_scheduler_task
				LEFT JOIN tx_scheduler_task_group ON tx_scheduler_task_group.uid = tx_scheduler_task.task_group
				',
905
			'WHERE' => '1=1',
906
			'ORDERBY' => 'tx_scheduler_task_group.sorting'
907
		);
908
909
		$res = $this->getDatabaseConnection()->exec_SELECT_queryArray($query);
		$numRows = $this->getDatabaseConnection()->sql_num_rows($res);
910

911
912
		// No tasks defined, display information message
		if ($numRows == 0) {
913
914
			$this->view->setTemplatePathAndFilename($this->backendTemplatePath . 'ListTasksNoTasks.html');
			return $this->view->render();
915
		} else {
916
917
			$this->getPageRenderer()->loadJquery();
			$this->getPageRenderer()->loadRequireJsModule('TYPO3/CMS/Scheduler/Scheduler');
918
919
			$table = array();
			// Header row
920
921
			$table[] =
				'<thead><tr>'
922
					. '<th><a href="#" id="checkall" title="' . $this->getLanguageService()->getLL('label.checkAll', TRUE) . '" class="icon">' . $this->iconFactory->getIcon('actions-document-select', Icon::SIZE_SMALL) . '</a></th>'
923
924
925
926
927
928
929
930
931
					. '<th>' . $this->getLanguageService()->getLL('label.id', TRUE). '</th>'
					. '<th>' . $this->getLanguageService()->getLL('task', TRUE). '</th>'
					. '<th>' . $this->getLanguageService()->getLL('label.type', TRUE). '</th>'
					. '<th>' . $this->getLanguageService()->getLL('label.frequency', TRUE). '</th>'
					. '<th>' . $this->getLanguageService()->getLL('label.parallel', TRUE). '</th>'
					. '<th>' . $this->getLanguageService()->getLL('label.lastExecution', TRUE). '</th>'
					. '<th>' . $this->getLanguageService()->getLL('label.nextExecution', TRUE). '</th>'
					. '<th></th>'
				. '</tr></thead>';
932

933
934
935
936
937
938
939
			// Loop on all tasks
			$temporaryResult = array();
			while ($row = $this->getDatabaseConnection()->sql_fetch_assoc($res)) {
				if ($row['taskGroupName'] === NULL || $row['isTaskGroupDeleted'] === '1') {
					$row['taskGroupName'] = '';
					$row['taskGroupDescription'] = '';
					$row['task_group'] = 0;
940
				}
941
942
943
944
				$temporaryResult[$row['task_group']]['groupName'] = $row['taskGroupName'];
				$temporaryResult[$row['task_group']]['groupDescription'] = $row['taskGroupDescription'];
				$temporaryResult[$row['task_group']]['tasks'][] = $row;
			}
945
			$registeredClasses = $this->getRegisteredClasses();
946
			foreach ($temporaryResult as $taskGroup) {
947
948
				if (!empty($taskGroup['groupName'])) {
					$groupText = '<strong>' . htmlspecialchars($taskGroup['groupName']) . '</strong>';
949
					if (!empty($taskGroup['groupDescription'])) {
950
						$groupText .= '<br>' . nl2br(htmlspecialchars($taskGroup['groupDescription']));
951
					}
952
953
					$table[] = '<tr><td colspan="9">' . $groupText . '</td></tr>';
				}
954
955

				foreach ($taskGroup['tasks'] as $schedulerRecord) {// Define action icons
956
957
					$link = htmlspecialchars($this->moduleUri . '&CMD=edit&tx_scheduler[uid]=' . $schedulerRecord['uid']);
					$editAction = '<a class="btn btn-default" href="' . $link . '" title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:edit', TRUE) . '" class="icon">' .
958
						IconUtility::getSpriteIcon('actions-document-open') . '</a>';
959
960
					if ((int)$schedulerRecord['disable'] === 1) {
						$translationKey = 'enable';
961
						$icon = $this->iconFactory->getIcon('actions-edit-unhide', Icon::SIZE_SMALL);
962
963
					} else {
						$translationKey = 'disable';
964
						$icon = $this->iconFactory->getIcon('actions-edit-hide', Icon::SIZE_SMALL);
965
					}
966
967
968
969
					$toggleHiddenAction = '<a class="btn btn-default" href="' . htmlspecialchars($this->moduleUri
						. '&CMD=toggleHidden&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" title="'
						. $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:' . $translationKey, TRUE)
						. '" class="icon">' . $icon . '</a>';
970
971
972
973
974
975
					$deleteAction = '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars($this->moduleUri . '&CMD=delete&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" '
						. ' data-severity="warning"'
						. ' data-title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:delete', TRUE) . '"'
						. ' data-button-close-text="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:cancel', TRUE) . '"'
						. ' data-content="' . $this->getLanguageService()->getLL('msg.delete', TRUE) . '"'
						. ' title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:delete', TRUE) . '" class="icon">' .
976
						$this->iconFactory->getIcon('actions-edit-delete', Icon::SIZE_SMALL) . '</a>';
977
978
979
980
981
982
					$stopAction = '<a class="btn btn-default t3js-modal-trigger" href="' . htmlspecialchars($this->moduleUri . '&CMD=stop&tx_scheduler[uid]=' . $schedulerRecord['uid']) . '" '
						. ' data-severity="warning"'
						. ' data-title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:stop', TRUE) . '"'
						. ' data-button-close-text="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:cancel', TRUE) . '"'
						. ' data-content="' . $this->getLanguageService()->getLL('msg.stop', TRUE) . '"'
						. ' title="' . $this->getLanguageService()->sL('LLL:EXT:lang/locallang_common.xlf:stop', TRUE) . '" class="icon">' .
983
						$this->iconFactory->getIcon('actions-document-close', Icon::SIZE_SMALL) . '</a>';
984
					$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">' .