[BUGFIX] Replace deprecated assertType in unit tests
[Packages/TYPO3.CMS.git] / typo3 / sysext / scheduler / class.tx_scheduler.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 2005 Christian Jul Jensen <julle@typo3.org>
6 *
7 * All rights reserved
8 *
9 * This script is part of the TYPO3 project. The TYPO3 project is
10 * free software; you can redistribute it and/or modify
11 * it under the terms of the GNU General Public License as published by
12 * the Free Software Foundation; either version 2 of the License, or
13 * (at your option) any later version.
14 *
15 * The GNU General Public License can be found at
16 * http://www.gnu.org/copyleft/gpl.html.
17 *
18 * This script is distributed in the hope that it will be useful,
19 * but WITHOUT ANY WARRANTY; without even the implied warranty of
20 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
21 * GNU General Public License for more details.
22 *
23 * This copyright notice MUST APPEAR in all copies of the script!
24 ***************************************************************/
25 /**
26 * TYPO3 Scheduler. This class handles scheduling and execution of tasks.
27 * Formerly known as "Gabriel TYPO3 arch angel"
28 *
29 * @author Fran├žois Suter <francois@typo3.org>
30 * @author Christian Jul Jensen <julle@typo3.org>
31 *
32 * @package TYPO3
33 * @subpackage tx_scheduler
34 */
35
36 class tx_scheduler implements t3lib_Singleton {
37 /**
38 * @var array $extConf: settings from the extension manager
39 */
40 var $extConf = array();
41
42 /**
43 * Constructor, makes sure all derived client classes are included
44 *
45 * @return void
46 */
47 public function __construct() {
48 // Get configuration from the extension manager
49 $this->extConf = unserialize($GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf']['scheduler']);
50 if (empty($this->extConf['maxLifetime'])) {
51 $this->extConf['maxLifetime'] = 1440;
52 }
53
54 // Clean up the serialized execution arrays
55 $this->cleanExecutionArrays();
56 }
57
58 /**
59 * Adds a task to the pool
60 *
61 * @param tx_scheduler_Task $task: the object representing the task to add
62 * @param string $identifier: the identified of the task
63 * @return boolean True if the task was successfully added, false otherwise
64 */
65 public function addTask(tx_scheduler_Task $task) {
66 $taskUid = $task->getTaskUid();
67 if (empty($taskUid)) {
68 $fields = array(
69 'crdate' => $GLOBALS['EXEC_TIME'],
70 'classname' => get_class($task),
71 'disable' => $task->isDisabled(),
72 'serialized_task_object' => 'RESERVED'
73 );
74 $result = $GLOBALS['TYPO3_DB']->exec_INSERTquery('tx_scheduler_task', $fields);
75 if ($result) {
76 $task->setTaskUid($GLOBALS['TYPO3_DB']->sql_insert_id());
77 $task->save();
78 $result = true;
79 } else {
80 $result = false;
81 }
82 } else {
83 $result = false;
84 }
85 return $result;
86 }
87
88 /**
89 * Cleans the execution lists of the scheduled tasks, executions older than 24h are removed
90 * TODO: find a way to actually kill the job
91 *
92 * @return void
93 */
94 protected function cleanExecutionArrays() {
95 $tstamp = $GLOBALS['EXEC_TIME'];
96
97 // Select all tasks with executions
98 // NOTE: this cleanup is done for disabled tasks too,
99 // to avoid leaving old executions lying around
100 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
101 'uid, classname, serialized_executions',
102 'tx_scheduler_task',
103 'serialized_executions != \'\''
104 );
105
106 $maxDuration = $this->extConf['maxLifetime'] * 60;
107 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
108 if (($serialized_executions = unserialize($row['serialized_executions']))) {
109 $executions = array();
110 foreach ($serialized_executions AS $task) {
111 if (($tstamp - $task) < $maxDuration) {
112 $executions[] = $task;
113 } else {
114 $logMessage = 'Removing logged execution, assuming that the process is dead. Execution of \'' . $row['classname'] . '\' (UID: ' . $row['uid']. ') was started at '.date('Y-m-d H:i:s', $task);
115 $this->log($logMessage);
116 }
117 }
118 }
119
120 if (count($serialized_executions) != count($executions)) {
121 if (count($executions) == 0) {
122 $value = '';
123 } else {
124 $value = serialize($executions);
125 }
126
127 $GLOBALS['TYPO3_DB']->exec_UPDATEquery(
128 'tx_scheduler_task',
129 'uid = ' . intval($row['uid']),
130 array('serialized_executions' => $value)
131 );
132 }
133 }
134 $GLOBALS['TYPO3_DB']->sql_free_result($res);
135 }
136
137 /**
138 * This method executes the given task and properly marks and records that execution
139 * It is expected to return false if the task was barred from running or if it was not saved properly
140 *
141 * @param tx_scheduler_Task $task: the task to execute
142 * @return boolean Whether the task was saved succesfully to the database or not
143 */
144 public function executeTask(tx_scheduler_Task $task) {
145 // Trigger the saving of the task, as this will calculate its next execution time
146 // This should be calculated all the time, even if the execution is skipped
147 // (in case it is skipped, this pushes back execution to the next possible date)
148 $task->save();
149 // Set a scheduler object for the task again,
150 // as it was removed during the save operation
151 $task->setScheduler();
152 $result = true;
153
154 // Task is already running and multiple executions are not allowed
155 if (!$task->areMultipleExecutionsAllowed() && $task->isExecutionRunning()) {
156 // Log multiple execution error
157 $logMessage = 'Task is already running and multiple executions are not allowed, skipping! Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
158 $this->log($logMessage);
159
160 $result = false;
161
162 // Task isn't running or multiple executions are allowed
163 } else {
164 // Log scheduler invocation
165 $logMessage = 'Start execution. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid();
166 $this->log($logMessage);
167
168 // Register execution
169 $executionID = $task->markExecution();
170
171 $failure = null;
172 try {
173 // Execute task
174 $successfullyExecuted = $task->execute();
175
176 if (!$successfullyExecuted) {
177 throw new tx_scheduler_FailedExecutionException(
178 'Task failed to execute successfully. Class: ' . get_class($task) . ', UID: ' . $task->getTaskUid(),
179 1250596541
180 );
181 }
182 } catch(Exception $e) {
183 // Store exception, so that it can be saved to database
184 $failure = $e;
185 }
186
187 // Unregister excution
188 $task->unmarkExecution($executionID, $failure);
189
190 // Log completion of execution
191 $logMessage = 'Task executed. Class: ' . get_class($task). ', UID: ' . $task->getTaskUid();
192 $this->log($logMessage);
193
194 // Now that the result of the task execution has been handled,
195 // throw the exception again, if any
196 if ($failure instanceof Exception) {
197 throw $failure;
198 }
199 }
200
201 return $result;
202 }
203
204 /**
205 * This method stores information about the last run of the Scheduler into the system registry
206 *
207 * @param string $type: Type of run (manual or command-line (assumed to be cron))
208 * @return void
209 */
210 public function recordLastRun($type = 'cron') {
211 // Validate input value
212 if ($type != 'manual') {
213 $type = 'cron';
214 }
215
216 /**
217 * @var t3lib_Registry
218 */
219 $registry = t3lib_div::makeInstance('t3lib_Registry');
220 $runInformation = array('start' => $GLOBALS['EXEC_TIME'], 'end' => time(), 'type' => $type);
221 $registry->set('tx_scheduler', 'lastRun', $runInformation);
222 }
223
224 /**
225 * Removes a task completely from the system.
226 * TODO: find a way to actually kill the existing jobs
227 *
228 * @param tx_scheduler_Task $task: the object representing the task to delete
229 * @return boolean True if task was successfully deleted, false otherwise
230 */
231 public function removeTask(tx_scheduler_Task $task) {
232 $taskUid = $task->getTaskUid();
233 if (!empty($taskUid)) {
234 return $GLOBALS['TYPO3_DB']->exec_DELETEquery('tx_scheduler_task', 'uid = ' . $taskUid);
235 } else {
236 return false;
237 }
238 }
239
240 /**
241 * Updates a task in the pool
242 *
243 * @param tx_scheduler_Task $task: Scheduler task object
244 * @return boolean False if submitted task was not of proper class
245 */
246 public function saveTask(tx_scheduler_Task $task) {
247 $taskUid = $task->getTaskUid();
248 if (!empty($taskUid)) {
249 try {
250 $executionTime = $task->getNextDueExecution();
251 $task->setExecutionTime($executionTime);
252 }
253 catch (Exception $e) {
254 $task->setDisabled(true);
255 $executionTime = 0;
256 }
257 $task->unsetScheduler();
258 $fields = array(
259 'nextexecution' => $executionTime,
260 'classname' => get_class($task),
261 'disable' => $task->isDisabled(),
262 'serialized_task_object' => serialize($task)
263 );
264 return $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', "uid = '" . $taskUid . "'", $fields);
265 } else {
266 return false;
267 }
268 }
269
270 /**
271 * Fetches and unserializes a task object from the db. If an uid is given the object
272 * with the uid is returned, else the object representing the next due task is returned.
273 * If there are no due tasks the method throws an exception.
274 *
275 * @param integer $uid: primary key of a task
276 * @return tx_scheduler_Task The fetched task object
277 */
278 public function fetchTask($uid = 0) {
279 $whereClause = '';
280 // Define where clause
281 // If no uid is given, take any non-disabled task which has a next execution time in the past
282 if (empty($uid)) {
283 $whereClause = 'disable = 0 AND nextexecution != 0 AND nextexecution <= ' . $GLOBALS['EXEC_TIME'];
284 } else {
285 $whereClause = 'uid = ' . intval($uid);
286 }
287
288 $queryArray = array(
289 'SELECT' => 'uid, serialized_task_object',
290 'FROM' => 'tx_scheduler_task',
291 'WHERE' => $whereClause,
292 'LIMIT' => 1
293 );
294 $res = $GLOBALS['TYPO3_DB']->exec_SELECT_queryArray($queryArray);
295
296 // If there are no available tasks, thrown an exception
297 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
298 throw new OutOfBoundsException('No task', 1247827244);
299
300 // Otherwise unserialize the task and return it
301 } else {
302 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
303 $task = unserialize($row['serialized_task_object']);
304
305 if ($this->isValidTaskObject($task)) {
306 // The task is valid, return it
307
308 $task->setScheduler();
309
310 } else {
311 // Forcibly set the disable flag to 1 in the database,
312 // so that the task does not come up again and again for execution
313
314 $GLOBALS['TYPO3_DB']->exec_UPDATEquery('tx_scheduler_task', "uid = '" . $row['uid'] . "'", array('disable' => 1));
315 // Throw an exception to raise the problem
316 throw new UnexpectedValueException('Could not unserialize task', 1255083671);
317 }
318 $GLOBALS['TYPO3_DB']->sql_free_result($res);
319 }
320 return $task;
321 }
322
323 /**
324 * This method is used to get the database record for a given task
325 * It returns the database record and not the task object
326 *
327 * @param integer $uid: primary key of the task to get
328 * @return array Database record for the task
329 * @see tx_scheduler::fetchTask()
330 */
331 public function fetchTaskRecord($uid) {
332 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', 'tx_scheduler_task', "uid = '" . intval($uid) . "'");
333
334 // If the task is not found, throw an exception
335 if ($GLOBALS['TYPO3_DB']->sql_num_rows($res) == 0) {
336 throw new OutOfBoundsException('No task', 1247827244);
337
338 // Otherwise get the task's record
339 } else {
340 $row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res);
341 $GLOBALS['TYPO3_DB']->sql_free_result($res);
342 }
343 return $row;
344 }
345
346 /**
347 * Fetches and unserializes task objects selected with some (SQL) condition
348 * Objects are returned as an array
349 *
350 * @param string $where: part of a SQL where clause (without the "WHERE" keyword)
351 * @param boolean $includeDisabledTasks: true if disabled tasks should be fetched too, false otherwise
352 * @return array List of task objects
353 */
354 public function fetchTasksWithCondition($where, $includeDisabledTasks = false) {
355 $tasks = array();
356 if (!empty($where)) {
357 $whereClause = $where;
358 }
359 if (!$includeDisabledTasks) {
360 if (!empty($whereClause)) {
361 $whereClause .= ' AND ';
362 }
363 $whereClause .= 'disable = 0';
364 }
365 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('serialized_task_object', 'tx_scheduler_task', $whereClause);
366 if ($res) {
367 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($res)) {
368 $task = unserialize($row['serialized_task_object']);
369 // Add the task to the list only if it is valid
370 if ($this->isValidTaskObject($task)) {
371 $task->setScheduler();
372 $tasks[] = $task;
373 }
374 }
375 $GLOBALS['TYPO3_DB']->sql_free_result($res);
376 }
377 return $tasks;
378 }
379
380 /**
381 * This method encapsulates a very simple test for the purpose of clarity.
382 * Registered tasks are stored in the database along with a serialized task object.
383 * When a registered task is fetched, its object is unserialized.
384 * At that point, if the class corresponding to the object is not available anymore
385 * (e.g. because the extension providing it has been uninstalled),
386 * the unserialization will produce an incomplete object.
387 * This test checks whether the unserialized object is of the right (parent) class or not.
388 *
389 * @param object The object to test
390 * @return boolean True if object is a task, false otherwise
391 */
392 public function isValidTaskObject($task) {
393 return $task instanceof tx_scheduler_Task;
394 }
395
396 /**
397 * This is a utility method that writes some message to the BE Log
398 * It could be expanded to write to some other log
399 *
400 * @param string The message to write to the log
401 * @param integer Status (0 = message, 1 = error)
402 * @param mixed Key for the message
403 * @return void
404 */
405 public function log($message, $status = 0, $code = 'scheduler') {
406 // Log only if enabled
407 if (!empty($this->extConf['enableBELog'])) {
408 $GLOBALS['BE_USER']->writelog(
409 4,
410 0,
411 $status,
412 $code,
413 '[scheduler]: ' . $message,
414 array()
415 );
416 }
417 }
418 }
419
420 if (defined('TYPO3_MODE') && isset($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['ext/scheduler/class.tx_scheduler.php'])) {
421 include_once($GLOBALS['TYPO3_CONF_VARS'][TYPO3_MODE]['XCLASS']['ext/scheduler/class.tx_scheduler.php']);
422 }
423
424
425 ?>