[BUGFIX] Fix several typos in php comments
[Packages/TYPO3.CMS.git] / typo3 / sysext / scheduler / Classes / Scheduler.php
1 <?php
2 namespace TYPO3\CMS\Scheduler;
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\Log\LoggerAwareInterface;
18 use Psr\Log\LoggerAwareTrait;
19 use Psr\Log\LoggerInterface;
20 use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
21 use TYPO3\CMS\Core\Database\Connection;
22 use TYPO3\CMS\Core\Database\ConnectionPool;
23 use TYPO3\CMS\Core\Database\Query\QueryHelper;
24 use TYPO3\CMS\Core\Log\LogManager;
25 use TYPO3\CMS\Core\Registry;
26 use TYPO3\CMS\Core\SingletonInterface;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28
29 /**
30 * TYPO3 Scheduler. This class handles scheduling and execution of tasks.
31 */
32 class Scheduler implements SingletonInterface, LoggerAwareInterface
33 {
34 use LoggerAwareTrait;
35
36 /**
37 * @var array $extConf Settings from the extension manager
38 */
39 public $extConf = [];
40
41 /**
42 * Constructor, makes sure all derived client classes are included
43 */
44 public function __construct()
45 {
46 // Get configuration from the extension manager
47 $this->extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('scheduler');
48 if (empty($this->extConf['maxLifetime'])) {
49 $this->extConf['maxLifetime'] = 1440;
50 }
51 // Clean up the serialized execution arrays
52 $this->cleanExecutionArrays();
53 }
54
55 /**
56 * Adds a task to the pool
57 *
58 * @param Task\AbstractTask $task The object representing the task to add
59 * @return bool TRUE if the task was successfully added, FALSE otherwise
60 */
61 public function addTask(Task\AbstractTask $task)
62 {
63 $taskUid = $task->getTaskUid();
64 if (empty($taskUid)) {
65 $fields = [
66 'crdate' => $GLOBALS['EXEC_TIME'],
67 'disable' => (int)$task->isDisabled(),
68 'description' => $task->getDescription(),
69 'task_group' => $task->getTaskGroup(),
70 'serialized_task_object' => 'RESERVED'
71 ];
72 $connection = GeneralUtility::makeInstance(ConnectionPool::class)
73 ->getConnectionForTable('tx_scheduler_task');
74 $result = $connection->insert(
75 'tx_scheduler_task',
76 $fields,
77 ['serialized_task_object' => Connection::PARAM_LOB]
78 );
79
80 if ($result) {
81 $task->setTaskUid($connection->lastInsertId('tx_scheduler_task'));
82 $task->save();
83 $result = true;
84 } else {
85 $result = false;
86 }
87 } else {
88 $result = false;
89 }
90 return $result;
91 }
92
93 /**
94 * Cleans the execution lists of the scheduled tasks, executions older than 24h are removed
95 * @todo find a way to actually kill the job
96 */
97 protected function cleanExecutionArrays()
98 {
99 $tstamp = $GLOBALS['EXEC_TIME'];
100 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
101 $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
102
103 // Select all tasks with executions
104 // NOTE: this cleanup is done for disabled tasks too,
105 // to avoid leaving old executions lying around
106 $result = $queryBuilder->select('uid', 'serialized_executions', 'serialized_task_object')
107 ->from('tx_scheduler_task')
108 ->where(
109 $queryBuilder->expr()->neq(
110 'serialized_executions',
111 $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
112 ),
113 $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
114 )
115 ->execute();
116 $maxDuration = $this->extConf['maxLifetime'] * 60;
117 while ($row = $result->fetch()) {
118 $executions = [];
119 if ($serialized_executions = unserialize($row['serialized_executions'])) {
120 foreach ($serialized_executions as $task) {
121 if ($tstamp - $task < $maxDuration) {
122 $executions[] = $task;
123 } else {
124 $task = unserialize($row['serialized_task_object']);
125 $this->log('Removing logged execution, assuming that the process is dead. Execution of \'' . get_class($task) . '\' (UID: ' . $row['uid'] . ') was started at ' . date('Y-m-d H:i:s', $task->getExecutionTime()));
126 }
127 }
128 }
129 $executionCount = count($executions);
130 if (count($serialized_executions) !== $executionCount) {
131 if ($executionCount === 0) {
132 $value = '';
133 } else {
134 $value = serialize($executions);
135 }
136 $connectionPool->getConnectionForTable('tx_scheduler_task')->update(
137 'tx_scheduler_task',
138 ['serialized_executions' => $value],
139 ['uid' => (int)$row['uid']],
140 ['serialized_executions' => Connection::PARAM_LOB]
141 );
142 }
143 }
144 }
145
146 /**
147 * This method executes the given task and properly marks and records that execution
148 * It is expected to return FALSE if the task was barred from running or if it was not saved properly
149 *
150 * @param Task\AbstractTask $task The task to execute
151 * @return bool Whether the task was saved successfully to the database or not
152 * @throws \Throwable
153 */
154 public function executeTask(Task\AbstractTask $task)
155 {
156 $task->setRunOnNextCronJob(false);
157 // Trigger the saving of the task, as this will calculate its next execution time
158 // This should be calculated all the time, even if the execution is skipped
159 // (in case it is skipped, this pushes back execution to the next possible date)
160 $task->save();
161 // Set a scheduler object for the task again,
162 // as it was removed during the save operation
163 $task->setScheduler();
164 $result = true;
165 // Task is already running and multiple executions are not allowed
166 if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
167 // Log multiple execution error
168 $logMessage = 'Task is already running and multiple executions are not allowed, skipping! Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
169 $this->logger->info($logMessage);
170 $result = false;
171 } else {
172 // Log scheduler invocation
173 $this->logger->info('Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid());
174 // Register execution
175 $executionID = $task->markExecution();
176 $failure = null;
177 try {
178 // Execute task
179 $successfullyExecuted = $task->execute();
180 if (!$successfullyExecuted) {
181 throw new FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
182 }
183 } catch (\Throwable $e) {
184 // Store exception, so that it can be saved to database
185 $failure = $e;
186 }
187 // Un-register execution
188 $task->unmarkExecution($executionID, $failure);
189 // Log completion of execution
190 $this->logger->info('Task executed. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid());
191 // Now that the result of the task execution has been handled,
192 // throw the exception again, if any
193 if ($failure instanceof \Throwable) {
194 throw $failure;
195 }
196 }
197 return $result;
198 }
199
200 /**
201 * This method stores information about the last run of the Scheduler into the system registry
202 *
203 * @param string $type Type of run (manual or command-line (assumed to be cron))
204 */
205 public function recordLastRun($type = 'cron')
206 {
207 // Validate input value
208 if ($type !== 'manual' && $type !== 'cli-by-id') {
209 $type = 'cron';
210 }
211 /** @var Registry $registry */
212 $registry = GeneralUtility::makeInstance(Registry::class);
213 $runInformation = ['start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type];
214 $registry->set('tx_scheduler', 'lastRun', $runInformation);
215 }
216
217 /**
218 * Removes a task completely from the system.
219 *
220 * @todo find a way to actually kill the existing jobs
221 *
222 * @param Task\AbstractTask $task The object representing the task to delete
223 * @return bool TRUE if task was successfully deleted, FALSE otherwise
224 */
225 public function removeTask(Task\AbstractTask $task)
226 {
227 $taskUid = $task->getTaskUid();
228 if (!empty($taskUid)) {
229 $result = GeneralUtility::makeInstance(ConnectionPool::class)
230 ->getConnectionForTable('tx_scheduler_task')
231 ->update('tx_scheduler_task', ['deleted' => 1], ['uid' => $taskUid]);
232 } else {
233 $result = false;
234 }
235 return $result;
236 }
237
238 /**
239 * Updates a task in the pool
240 *
241 * @param Task\AbstractTask $task Scheduler task object
242 * @return bool False if submitted task was not of proper class
243 */
244 public function saveTask(Task\AbstractTask $task)
245 {
246 $result = true;
247 $taskUid = $task->getTaskUid();
248 if (!empty($taskUid)) {
249 try {
250 if ($task->getRunOnNextCronJob()) {
251 $executionTime = time();
252 } else {
253 $executionTime = $task->getNextDueExecution();
254 }
255 $task->setExecutionTime($executionTime);
256 } catch (\Exception $e) {
257 $task->setDisabled(true);
258 $executionTime = 0;
259 }
260 $task->unsetScheduler();
261 $fields = [
262 'nextexecution' => $executionTime,
263 'disable' => (int)$task->isDisabled(),
264 'description' => $task->getDescription(),
265 'task_group' => $task->getTaskGroup(),
266 'serialized_task_object' => serialize($task)
267 ];
268 try {
269 GeneralUtility::makeInstance(ConnectionPool::class)
270 ->getConnectionForTable('tx_scheduler_task')
271 ->update(
272 'tx_scheduler_task',
273 $fields,
274 ['uid' => $taskUid],
275 ['serialized_task_object' => Connection::PARAM_LOB]
276 );
277 } catch (\Doctrine\DBAL\DBALException $e) {
278 $result = false;
279 }
280 } else {
281 $result = false;
282 }
283 return $result;
284 }
285
286 /**
287 * Fetches and unserializes a task object from the db. If a uid is given the object
288 * with the uid is returned, else the object representing the next due task is returned.
289 * If there are no due tasks the method throws an exception.
290 *
291 * @param int $uid Primary key of a task
292 * @return Task\AbstractTask The fetched task object
293 * @throws \OutOfBoundsException
294 * @throws \UnexpectedValueException
295 */
296 public function fetchTask($uid = 0)
297 {
298 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
299 $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
300
301 $queryBuilder->select('t.uid', 't.serialized_task_object')
302 ->from('tx_scheduler_task', 't')
303 ->setMaxResults(1);
304 // Define where clause
305 // If no uid is given, take any non-disabled task which has a next execution time in the past
306 if (empty($uid)) {
307 $queryBuilder->getRestrictions()->removeAll();
308 $queryBuilder->leftJoin(
309 't',
310 'tx_scheduler_task_group',
311 'g',
312 $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
313 );
314 $queryBuilder->where(
315 $queryBuilder->expr()->eq('t.disable', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
316 $queryBuilder->expr()->neq('t.nextexecution', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
317 $queryBuilder->expr()->lte(
318 't.nextexecution',
319 $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], \PDO::PARAM_INT)
320 ),
321 $queryBuilder->expr()->orX(
322 $queryBuilder->expr()->eq('g.hidden', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)),
323 $queryBuilder->expr()->isNull('g.hidden')
324 ),
325 $queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
326 );
327 $queryBuilder->orderBy('t.nextexecution', 'ASC');
328 } else {
329 $queryBuilder->where(
330 $queryBuilder->expr()->eq('t.uid', $queryBuilder->createNamedParameter($uid, \PDO::PARAM_INT)),
331 $queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
332 );
333 }
334
335 $row = $queryBuilder->execute()->fetch();
336 if (empty($row)) {
337 if (empty($uid)) {
338 // No uid was passed and no overdue task was found
339 throw new \OutOfBoundsException('No tasks available for execution', 1247827244);
340 }
341 // Although a uid was passed, no task with given was found
342 throw new \OutOfBoundsException('No task with id ' . $uid . ' found', 1422044826);
343 }
344 /** @var Task\AbstractTask $task */
345 $task = unserialize($row['serialized_task_object']);
346 if ($this->isValidTaskObject($task)) {
347 // The task is valid, return it
348 $task->setScheduler();
349 } else {
350 // Forcibly set the disable flag to 1 in the database,
351 // so that the task does not come up again and again for execution
352 $connectionPool->getConnectionForTable('tx_scheduler_task')->update(
353 'tx_scheduler_task',
354 ['disable' => 1],
355 ['uid' => (int)$row['uid']]
356 );
357 // Throw an exception to raise the problem
358 throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
359 }
360
361 return $task;
362 }
363
364 /**
365 * This method is used to get the database record for a given task
366 * It returns the database record and not the task object
367 *
368 * @param int $uid Primary key of the task to get
369 * @return array Database record for the task
370 * @see \TYPO3\CMS\Scheduler\Scheduler::fetchTask()
371 * @throws \OutOfBoundsException
372 */
373 public function fetchTaskRecord($uid)
374 {
375 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
376 ->getQueryBuilderForTable('tx_scheduler_task');
377 $row = $queryBuilder->select('*')
378 ->from('tx_scheduler_task')
379 ->where(
380 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, \PDO::PARAM_INT)),
381 $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
382 )
383 ->execute()
384 ->fetch();
385
386 // If the task is not found, throw an exception
387 if (empty($row)) {
388 throw new \OutOfBoundsException('No task', 1247827245);
389 }
390
391 return $row;
392 }
393
394 /**
395 * Fetches and unserializes task objects selected with some (SQL) condition
396 * Objects are returned as an array
397 *
398 * @param string $where Part of a SQL where clause (without the "WHERE" keyword)
399 * @param bool $includeDisabledTasks TRUE if disabled tasks should be fetched too, FALSE otherwise
400 * @return array List of task objects
401 */
402 public function fetchTasksWithCondition($where, $includeDisabledTasks = false)
403 {
404 $tasks = [];
405 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
406 ->getQueryBuilderForTable('tx_scheduler_task');
407
408 $queryBuilder
409 ->select('serialized_task_object')
410 ->from('tx_scheduler_task')
411 ->where(
412 $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
413 );
414
415 if (!$includeDisabledTasks) {
416 $queryBuilder->andWhere(
417 $queryBuilder->expr()->eq('disable', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT))
418 );
419 }
420
421 if (!empty($where)) {
422 $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
423 }
424
425 $result = $queryBuilder->execute();
426 while ($row = $result->fetch()) {
427 /** @var Task\AbstractTask $task */
428 $task = unserialize($row['serialized_task_object']);
429 // Add the task to the list only if it is valid
430 if ($this->isValidTaskObject($task)) {
431 $task->setScheduler();
432 $tasks[] = $task;
433 }
434 }
435
436 return $tasks;
437 }
438
439 /**
440 * This method encapsulates a very simple test for the purpose of clarity.
441 * Registered tasks are stored in the database along with a serialized task object.
442 * When a registered task is fetched, its object is unserialized.
443 * At that point, if the class corresponding to the object is not available anymore
444 * (e.g. because the extension providing it has been uninstalled),
445 * the unserialization will produce an incomplete object.
446 * This test checks whether the unserialized object is of the right (parent) class or not.
447 *
448 * @param object $task The object to test
449 * @return bool TRUE if object is a task, FALSE otherwise
450 */
451 public function isValidTaskObject($task)
452 {
453 return $task instanceof Task\AbstractTask && get_class($task->getExecution()) !== '__PHP_Incomplete_Class';
454 }
455
456 /**
457 * This is a utility method that writes some message to the BE Log
458 * It could be expanded to write to some other log
459 *
460 * @param string $message The message to write to the log
461 * @param int $status Status (0 = message, 1 = error)
462 * @param mixed $code Key for the message
463 */
464 public function log($message, $status = 0, $code = '')
465 {
466 // this method could be called from the constructor (via "cleanExecutionArrays") and no logger is instantiated
467 // by then, that's why check if the logger is available
468 if (!($this->logger instanceof LoggerInterface)) {
469 $this->setLogger(GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__));
470 }
471 $message = trim('[scheduler]: ' . $code) . ' - ' . $message;
472 switch ((int)$status) {
473 // error (user problem)
474 case 1:
475 $this->logger->alert($message);
476 break;
477 // System Error (which should not happen)
478 case 2:
479 $this->logger->error($message);
480 break;
481 // security notice (admin)
482 case 3:
483 $this->logger->emergency($message);
484 break;
485 // regular message (= 0)
486 default:
487 $this->logger->info($message);
488 }
489 }
490 }