‪TYPO3CMS  11.5
Scheduler.php
Go to the documentation of this file.
1 <?php
2 
3 /*
4  * This file is part of the TYPO3 CMS project.
5  *
6  * It is free software; you can redistribute it and/or modify it under
7  * the terms of the GNU General Public License, either version 2
8  * of the License, or any later version.
9  *
10  * For the full copyright and license information, please read the
11  * LICENSE.txt file that was distributed with this source code.
12  *
13  * The TYPO3 project - inspiring people to share!
14  */
15 
16 namespace ‪TYPO3\CMS\Scheduler;
17 
18 use Doctrine\DBAL\Exception as DBALException;
19 use Psr\Log\LoggerInterface;
28 
33 {
34  protected LoggerInterface ‪$logger;
35 
39  public ‪$extConf = [];
40 
44  public function ‪__construct(LoggerInterface ‪$logger)
45  {
46  $this->logger = ‪$logger;
47  // Get configuration from the extension manager
48  $this->extConf = GeneralUtility::makeInstance(ExtensionConfiguration::class)->get('scheduler');
49  if (empty($this->extConf['maxLifetime'])) {
50  $this->extConf['maxLifetime'] = 1440;
51  }
52  // Clean up the serialized execution arrays
53  $this->‪cleanExecutionArrays();
54  }
55 
62  public function ‪addTask(AbstractTask $task)
63  {
64  $taskUid = $task->‪getTaskUid();
65  if (empty($taskUid)) {
66  ‪$fields = [
67  'crdate' => ‪$GLOBALS['EXEC_TIME'],
68  'disable' => (int)$task->‪isDisabled(),
69  'description' => $task->‪getDescription(),
70  'task_group' => $task->‪getTaskGroup(),
71  'serialized_task_object' => 'RESERVED',
72  ];
73  $connection = GeneralUtility::makeInstance(ConnectionPool::class)
74  ->getConnectionForTable('tx_scheduler_task');
75  $result = $connection->insert(
76  'tx_scheduler_task',
77  ‪$fields,
78  ['serialized_task_object' => ‪Connection::PARAM_LOB]
79  );
80 
81  if ($result) {
82  $task->‪setTaskUid($connection->lastInsertId('tx_scheduler_task'));
83  $task->‪save();
84  $result = true;
85  } else {
86  $result = false;
87  }
88  } else {
89  $result = false;
90  }
91  return $result;
92  }
93 
98  protected function ‪cleanExecutionArrays()
99  {
100  $tstamp = ‪$GLOBALS['EXEC_TIME'];
101  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
102  $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
103 
104  // Select all tasks with executions
105  // NOTE: this cleanup is done for disabled tasks too,
106  // to avoid leaving old executions lying around
107  $result = $queryBuilder->select('uid', 'serialized_executions', 'serialized_task_object')
108  ->from('tx_scheduler_task')
109  ->where(
110  $queryBuilder->expr()->neq(
111  'serialized_executions',
112  $queryBuilder->createNamedParameter('', ‪Connection::PARAM_STR)
113  ),
114  $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, ‪Connection::PARAM_INT))
115  )
116  ->executeQuery();
117  $maxDuration = $this->extConf['maxLifetime'] * 60;
118  while ($row = $result->fetchAssociative()) {
119  $executions = [];
120  if ($serialized_executions = unserialize($row['serialized_executions'])) {
121  foreach ($serialized_executions as $task) {
122  if ($tstamp - $task < $maxDuration) {
123  $executions[] = $task;
124  } else {
125  $schedulerTask = unserialize($row['serialized_task_object']);
126  if ($schedulerTask instanceof AbstractTask) {
127  $taskClass = get_class($schedulerTask);
128  $taskTime = date('Y-m-d H:i:s', $schedulerTask->getExecutionTime());
129  } else {
130  $taskClass = 'unknown task';
131  $taskTime = 'unknown time';
132  }
133  $this->‪log('Removing logged execution, assuming that the process is dead. Execution of \'' . $taskClass . '\' (UID: ' . $row['uid'] . ') was started at ' . $taskTime);
134  }
135  }
136  }
137  $executionCount = count($executions);
138  if (!is_array($serialized_executions) || count($serialized_executions) !== $executionCount) {
139  if ($executionCount === 0) {
140  $value = '';
141  } else {
142  $value = serialize($executions);
143  }
144  $connectionPool->getConnectionForTable('tx_scheduler_task')->update(
145  'tx_scheduler_task',
146  ['serialized_executions' => $value],
147  ['uid' => (int)$row['uid']],
148  ['serialized_executions' => Connection::PARAM_LOB]
149  );
150  }
151  }
152  }
153 
162  public function executeTask(AbstractTask $task)
163  {
164  $task->setRunOnNextCronJob(false);
165  // Trigger the saving of the task, as this will calculate its next execution time
166  // This should be calculated all the time, even if the execution is skipped
167  // (in case it is skipped, this pushes back execution to the next possible date)
168  $task->save();
169  // Set a scheduler object for the task again,
170  // as it was removed during the save operation
171  $task->setScheduler();
172  $result = true;
173  // Task is already running and multiple executions are not allowed
174  if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
175  // Log multiple execution error
176  $this->logger->info('Task is already running and multiple executions are not allowed, skipping! Class: {class}, UID: {uid}', [
177  'class' => get_class($task),
178  'uid' => $task->getTaskUid(),
179  ]);
180  $result = false;
181  } else {
182  // Log scheduler invocation
183  $this->logger->info('Start execution. Class: {class}, UID: {uid}', [
184  'class' => get_class($task),
185  'uid' => $task->getTaskUid(),
186  ]);
187  // Register execution
188  $executionID = $task->markExecution();
189  $failure = null;
190  try {
191  // Execute task
192  $successfullyExecuted = $task->execute();
193  if (!$successfullyExecuted) {
194  throw new FailedExecutionException('Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(), 1250596541);
195  }
196  } catch (\Throwable $e) {
197  // Store exception, so that it can be saved to database
198  $failure = $e;
199  }
200  // Un-register execution
201  $task->unmarkExecution($executionID, $failure);
202  // Log completion of execution
203  $this->logger->info('Task executed. Class: {class}, UID: {uid}', [
204  'class' => get_class($task),
205  'uid' => $task->getTaskUid(),
206  ]);
207  // Now that the result of the task execution has been handled,
208  // throw the exception again, if any
209  if ($failure instanceof \Throwable) {
210  throw $failure;
211  }
212  }
213  return $result;
214  }
215 
221  public function recordLastRun($type = 'cron')
222  {
223  // Validate input value
224  if ($type !== 'manual' && $type !== 'cli-by-id') {
225  $type = 'cron';
226  }
227  $registry = GeneralUtility::makeInstance(Registry::class);
228  $runInformation = ['start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type];
229  $registry->set('tx_scheduler', 'lastRun', $runInformation);
230  }
231 
240  public function removeTask(AbstractTask $task)
241  {
242  $taskUid = $task->getTaskUid();
243  if (!empty($taskUid)) {
244  $affectedRows = GeneralUtility::makeInstance(ConnectionPool::class)
245  ->getConnectionForTable('tx_scheduler_task')
246  ->update('tx_scheduler_task', ['deleted' => 1], ['uid' => $taskUid]);
247  $result = $affectedRows === 1;
248  } else {
249  $result = false;
250  }
251  return $result;
252  }
253 
260  public function saveTask(AbstractTask $task)
261  {
262  $result = true;
263  $taskUid = $task->getTaskUid();
264  if (!empty($taskUid)) {
265  try {
266  if ($task->getRunOnNextCronJob()) {
267  $executionTime = time();
268  } else {
269  $executionTime = $task->getNextDueExecution();
270  }
271  $task->setExecutionTime($executionTime);
272  } catch (\Exception $e) {
273  $task->setDisabled(true);
274  $executionTime = 0;
275  }
276  $task->unsetScheduler();
277  $fields = [
278  'nextexecution' => $executionTime,
279  'disable' => (int)$task->isDisabled(),
280  'description' => $task->getDescription(),
281  'task_group' => $task->getTaskGroup(),
282  'serialized_task_object' => serialize($task),
283  ];
284  try {
285  GeneralUtility::makeInstance(ConnectionPool::class)
286  ->getConnectionForTable('tx_scheduler_task')
287  ->update(
288  'tx_scheduler_task',
289  $fields,
290  ['uid' => $taskUid],
291  ['serialized_task_object' => Connection::PARAM_LOB]
292  );
293  } catch (DBALException $e) {
294  $result = false;
295  }
296  } else {
297  $result = false;
298  }
299  return $result;
300  }
301 
312  public function fetchTask($uid = 0): ?AbstractTask
313  {
314  $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
315  $queryBuilder = $connectionPool->getQueryBuilderForTable('tx_scheduler_task');
316 
317  $queryBuilder->select('t.uid', 't.serialized_task_object')
318  ->from('tx_scheduler_task', 't')
319  ->setMaxResults(1);
320  // Define where clause
321  // If no uid is given, take any non-disabled task which has a next execution time in the past
322  if (empty($uid)) {
323  $queryBuilder->getRestrictions()->removeAll();
324  $queryBuilder->leftJoin(
325  't',
326  'tx_scheduler_task_group',
327  'g',
328  $queryBuilder->expr()->eq('t.task_group', $queryBuilder->quoteIdentifier('g.uid'))
329  );
330  $queryBuilder->where(
331  $queryBuilder->expr()->eq('t.disable', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
332  $queryBuilder->expr()->neq('t.nextexecution', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
333  $queryBuilder->expr()->lte(
334  't.nextexecution',
335  $queryBuilder->createNamedParameter($GLOBALS['EXEC_TIME'], Connection::PARAM_INT)
336  ),
337  $queryBuilder->expr()->orX(
338  $queryBuilder->expr()->eq('g.hidden', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT)),
339  $queryBuilder->expr()->isNull('g.hidden')
340  ),
341  $queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
342  );
343  $queryBuilder->orderBy('t.nextexecution', 'ASC');
344  } else {
345  $queryBuilder->where(
346  $queryBuilder->expr()->eq('t.uid', $queryBuilder->createNamedParameter($uid, Connection::PARAM_INT)),
347  $queryBuilder->expr()->eq('t.deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
348  );
349  }
350 
351  $row = $queryBuilder->executeQuery()->fetchAssociative();
352  if (empty($row)) {
353  if (empty($uid)) {
354  // No uid was passed and no overdue task was found
355  return null;
356  }
357  // Although a uid was passed, no task with given was found
358  throw new \OutOfBoundsException('No task with id ' . $uid . ' found', 1422044826);
359  }
361  $task = unserialize($row['serialized_task_object']);
362  if ($this->isValidTaskObject($task)) {
363  // The task is valid, return it
364  $task->setScheduler();
365  if ($task->getTaskGroup() === null) {
366  // Fix invalid task_group=NULL settings in order to avoid exceptions when saving on PostgreSQL
367  $task->setTaskGroup(0);
368  }
369  } else {
370  // Forcibly set the disable flag to 1 in the database,
371  // so that the task does not come up again and again for execution
372  $connectionPool->getConnectionForTable('tx_scheduler_task')->update(
373  'tx_scheduler_task',
374  ['disable' => 1],
375  ['uid' => (int)$row['uid']]
376  );
377  // Throw an exception to raise the problem
378  throw new \UnexpectedValueException('Could not unserialize task', 1255083671);
379  }
380 
381  return $task;
382  }
383 
393  public function fetchTaskRecord($uid)
394  {
395  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
396  ->getQueryBuilderForTable('tx_scheduler_task');
397  $row = $queryBuilder->select('*')
398  ->from('tx_scheduler_task')
399  ->where(
400  $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter((int)$uid, Connection::PARAM_INT)),
401  $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
402  )
403  ->executeQuery()
404  ->fetchAssociative();
405 
406  // If the task is not found, throw an exception
407  if (empty($row)) {
408  throw new \OutOfBoundsException('No task', 1247827245);
409  }
410 
411  return $row;
412  }
413 
422  public function fetchTasksWithCondition($where, $includeDisabledTasks = false)
423  {
424  $tasks = [];
425  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
426  ->getQueryBuilderForTable('tx_scheduler_task');
427 
428  $queryBuilder
429  ->select('serialized_task_object')
430  ->from('tx_scheduler_task')
431  ->where(
432  $queryBuilder->expr()->eq('deleted', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
433  );
434 
435  if (!$includeDisabledTasks) {
436  $queryBuilder->andWhere(
437  $queryBuilder->expr()->eq('disable', $queryBuilder->createNamedParameter(0, Connection::PARAM_INT))
438  );
439  }
440 
441  if (!empty($where)) {
442  $queryBuilder->andWhere(QueryHelper::stripLogicalOperatorPrefix($where));
443  }
444 
445  $result = $queryBuilder->executeQuery();
446  while ($row = $result->fetchAssociative()) {
448  $task = unserialize($row['serialized_task_object']);
449  // Add the task to the list only if it is valid
450  if ($this->isValidTaskObject($task)) {
451  $task->setScheduler();
452  $tasks[] = $task;
453  }
454  }
455 
456  return $tasks;
457  }
458 
471  public function isValidTaskObject($task)
472  {
473  return $task instanceof AbstractTask && get_class($task->getExecution()) !== \__PHP_Incomplete_Class::class;
474  }
475 
484  public function log($message, $status = 0, $code = '')
485  {
486  $messageTemplate = '[scheduler]: {code} - {original_message}';
487  // @todo Replace these magic numbers with constants or enums.
488  switch ((int)$status) {
489  // error (user problem)
490  case 1:
491  $this->logger->alert($messageTemplate, ['code' => $code, 'original_message' => $message]);
492  break;
493  // System Error (which should not happen)
494  case 2:
495  $this->logger->error($messageTemplate, ['code' => $code, 'original_message' => $message]);
496  break;
497  // security notice (admin)
498  case 3:
499  $this->logger->emergency($messageTemplate, ['code' => $code, 'original_message' => $message]);
500  break;
501  // regular message (= 0)
502  default:
503  $this->logger->info($messageTemplate, ['code' => $code, 'original_message' => $message]);
504  }
505  }
506 }
‪TYPO3\CMS\Scheduler
Definition: AbstractAdditionalFieldProvider.php:18
‪TYPO3\CMS\Core\Database\Connection\PARAM_INT
‪const PARAM_INT
Definition: Connection.php:49
‪TYPO3\CMS\Scheduler\Task\AbstractTask\save
‪bool save()
Definition: AbstractTask.php:558
‪TYPO3\CMS\Scheduler\Task\AbstractTask\getTaskUid
‪int getTaskUid()
Definition: AbstractTask.php:138
‪TYPO3\CMS\Scheduler\Scheduler\__construct
‪__construct(LoggerInterface $logger)
Definition: Scheduler.php:43
‪TYPO3\CMS\Core\Configuration\ExtensionConfiguration
Definition: ExtensionConfiguration.php:45
‪TYPO3\CMS\Scheduler\Task\AbstractTask\setTaskUid
‪setTaskUid($id)
Definition: AbstractTask.php:128
‪TYPO3\CMS\Core\Registry
Definition: Registry.php:33
‪TYPO3\CMS\Scheduler\Scheduler\$extConf
‪array $extConf
Definition: Scheduler.php:38
‪TYPO3\CMS\Scheduler\Scheduler\log
‪log($message, $status=0, $code='')
Definition: Scheduler.php:483
‪$fields
‪$fields
Definition: pages.php:5
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:54
‪TYPO3\CMS\Scheduler\Scheduler\addTask
‪bool addTask(AbstractTask $task)
Definition: Scheduler.php:61
‪TYPO3\CMS\Scheduler\Scheduler\$logger
‪LoggerInterface $logger
Definition: Scheduler.php:34
‪TYPO3\CMS\Scheduler\Task\AbstractTask
Definition: AbstractTask.php:35
‪TYPO3\CMS\Core\Database\Query\QueryHelper
Definition: QueryHelper.php:32
‪TYPO3\CMS\Scheduler\Scheduler
Definition: Scheduler.php:33
‪TYPO3\CMS\Scheduler\Task\AbstractTask\isDisabled
‪bool isDisabled()
Definition: AbstractTask.php:178
‪TYPO3\CMS\Scheduler\Scheduler\cleanExecutionArrays
‪cleanExecutionArrays()
Definition: Scheduler.php:97
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Core\SingletonInterface
Definition: SingletonInterface.php:22
‪$GLOBALS
‪$GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['adminpanel']['modules']
Definition: ext_localconf.php:25
‪TYPO3\CMS\Scheduler\Task\AbstractTask\getTaskGroup
‪int getTaskGroup()
Definition: AbstractTask.php:232
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Scheduler\Task\AbstractTask\getDescription
‪string getDescription()
Definition: AbstractTask.php:272
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Core\Database\Connection\PARAM_LOB
‪const PARAM_LOB
Definition: Connection.php:59