TDbCronModule::getCronLog()   B
last analyzed

Complexity

Conditions 9
Paths 49

Size

Total Lines 40
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 9
eloc 28
c 1
b 0
f 0
nc 49
nop 4
dl 0
loc 40
rs 8.0555
1
<?php
2
3
/**
4
 * TDbCronModule class file.
5
 *
6
 * @author Brad Anderson <[email protected]>
7
 * @link https://github.com/pradosoft/prado
8
 * @license https://github.com/pradosoft/prado/blob/master/LICENSE
9
 */
10
11
namespace Prado\Util\Cron;
12
13
use Exception;
14
use PDO;
15
use Prado\Security\Permissions\TPermissionEvent;
16
use Prado\Security\Permissions\TUserOwnerRule;
17
use Prado\Data\TDataSourceConfig;
18
use Prado\Data\TDbConnection;
19
use Prado\Exceptions\TConfigurationException;
20
use Prado\Exceptions\TInvalidDataValueException;
21
use Prado\Exceptions\TInvalidOperationException;
22
use Prado\Prado;
23
use Prado\TPropertyValue;
24
use Prado\Util\TLogger;
25
26
/**
27
 * TDbCronModule class.
28
 *
29
 * TDbCronModule does everything that TCronModule does but stores the tasks and
30
 * persistent data in its own database table.
31
 *
32
 * The TDbCronModule allows for adding, updating, and removing tasks from the
33
 * application and shell.  It can log executing tasks to the table as well.
34
 *
35
 * There are log maintenance methods and {@see \Prado\Util\Cron\TDbCronCleanLogTask} for cleaning
36
 * the cron logs.
37
 *
38
 * Runtime Tasks can be added for execution onEndRequest.  Singleton tasks can
39
 * be added to TDbCronModule, and scheduled to execute during Runtime at
40
 * onEndRequest.  Then if it does not execute onEndRequest, then the next
41
 * shell cron will execute the task.  This could occur if the user presses stop
42
 * before the page completes.
43
 *
44
 * @author Brad Anderson <[email protected]>
45
 * @since 4.2.0
46
 * @method bool dyClearCronLog(bool $return, int $seconds)
47
 * @method bool dyGetCronLog(bool $return, string $name, int $pageSize, int $offset, string $sortingDesc)
48
 * @method bool dyGetCronLogCount(bool $return, string $name)
49
 * @method bool dyRemoveCronLogItem(bool $return, int $taskUID)
50
 * @method bool dyAddTask(bool $return,\Prado\Util\Cron\TCronTask $task, bool $runtime)
51
 * @method bool dyUpdateTask(bool $return, \Prado\Util\Cron\TCronTask $task, array $extraData)
52
 * @method bool dyRemoveTask(bool $return, \Prado\Util\Cron\TCronTask|string $untask, array $extraData)
53
 */
54
class TDbCronModule extends TCronModule implements \Prado\Util\IDbModule
55
{
56
	/** Name Regular Expression, no spaces, single or double quotes, less than or greater than, no percent, and cannot start with star */
57
	public const NAME_VALIDATOR_REGEX = '/^[^\s`\'\"\\*<>%][^\s`\'\"<>%]*$/i';
58
59
	public const PERM_CRON_LOG_READ = 'cron_log_read';
60
61
	public const PERM_CRON_LOG_DELETE = 'cron_log_delete';
62
63
	public const PERM_CRON_ADD_TASK = 'cron_add_task';
64
65
	public const PERM_CRON_UPDATE_TASK = 'cron_update_task';
66
67
	public const PERM_CRON_REMOVE_TASK = 'cron_remove_task';
68
69
	/** @var string name of the db table for cron tasks, default 'crontabs' */
70
	private $_tableName = 'crontabs';
71
72
	/** @var bool auto create the db table for cron, default true */
73
	private $_autoCreate = true;
74
75
	/** @var bool has the table been verified to be in the DB */
76
	private $_tableEnsured = false;
77
78
	/** @var bool log the cron tasks, in the table, as they run, default true  */
79
	private $_logCronTasks = true;
80
81
	/** @var array[]|TCronTask[] the tasks created from the (parent) application configuration */
82
	private $_configTasks;
83
84
	/** @var bool are the tasks Initialized */
85
	private $_tasksInitialized = false;
86
87
	/** @var array[]|TCronTask[] the tasks manually added to the database */
88
	private $_tasks = [];
89
90
	/** @var array[] the row data from the database */
91
	private $_taskRows;
92
93
	/** @var string the ID of TDataSourceConfig module  */
94
	private $_connID = '';
95
96
	/**  @var TDbConnection the DB connection instance  */
97
	private $_conn;
98
99
	/** @var TCronTask[] */
100
	private $_runtimeTasks;
101
102
	/**
103
	 * constructs the instances, sets the _shellClass.
104
	 */
105
	public function __construct()
106
	{
107
		$this->_shellClass = \Prado\Util\Cron\TShellDbCronAction::class;
108
		parent::__construct();
109
	}
110
	/**
111
	 * Initializes the module. Keeps track of the configured tasks different than db tasks.
112
	 * @param array|\Prado\Xml\TXmlElement $config
113
	 */
114
	public function init($config)
115
	{
116
		parent::init($config);
117
118
		$this->_configTasks = parent::getRawTasks();
119
	}
120
121
	/**
122
	 * the global event handling requests for cron task info
123
	 * @param TDbCronModule $cron
124
	 * @param null $param
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $param is correct as it would always require null to be passed?
Loading history...
125
	 */
126
	public function fxGetCronTaskInfos($cron, $param)
0 ignored issues
show
Unused Code introduced by
The parameter $param is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

126
	public function fxGetCronTaskInfos($cron, /** @scrutinizer ignore-unused */ $param)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $cron is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

126
	public function fxGetCronTaskInfos(/** @scrutinizer ignore-unused */ $cron, $param)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
127
	{
128
		return new TCronTaskInfo('cronclean', \Prado\Util\Cron\TDbCronCleanLogTask::class, $this->getId(), Prado::localize('DbCron Clean Log Task'), Prado::localize('Clears the database of cron log items before the specified time period.'));
129
	}
130
131
	/**
132
	 * @param \Prado\Security\Permissions\TPermissionsManager $manager
133
	 * @return \Prado\Security\Permissions\TPermissionEvent[]
134
	 */
135
	public function getPermissions($manager)
136
	{
137
		$userIsOwnerAllowedRule = new TUserOwnerRule();
138
		return array_merge([
139
			new TPermissionEvent(static::PERM_CRON_LOG_READ, 'Cron read Db log.', ['dyGetCronLog', 'dyGetCronLogCount']),
140
			new TPermissionEvent(static::PERM_CRON_LOG_DELETE, 'Cron delete Db log.', ['dyClearCronLog', 'dyRemoveCronLogItem']),
141
			new TPermissionEvent(static::PERM_CRON_ADD_TASK, 'Cron add Db Task.', ['dyAddTask']),
142
			new TPermissionEvent(static::PERM_CRON_UPDATE_TASK, 'Cron update Db task.', ['dyUpdateTask'], $userIsOwnerAllowedRule),
0 ignored issues
show
Bug introduced by
$userIsOwnerAllowedRule of type Prado\Security\Permissions\TUserOwnerRule is incompatible with the type Prado\Security\TAuthorizationRule[]|null expected by parameter $rules of Prado\Security\Permissio...ionEvent::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

142
			new TPermissionEvent(static::PERM_CRON_UPDATE_TASK, 'Cron update Db task.', ['dyUpdateTask'], /** @scrutinizer ignore-type */ $userIsOwnerAllowedRule),
Loading history...
143
			new TPermissionEvent(static::PERM_CRON_REMOVE_TASK, 'Cron remove Db task.', ['dyRemoveTask'], $userIsOwnerAllowedRule),
144
		], parent::getPermissions($manager));
145
	}
146
147
	/**
148
	 * This checks for "name".  The name cannot by '*' or have spaces, `, ', ", <, or >t characters.
149
	 * @param array $properties the task as an array of properties
150
	 * @throws TConfigurationException when the name is invalid.
151
	 */
152
	public function validateTask($properties)
153
	{
154
		$name = $properties[parent::NAME_KEY] ?? '';
155
		if (!preg_match(TDbCronModule::NAME_VALIDATOR_REGEX, $name)) {
156
			throw new TConfigurationException('dbcron_invalid_name', $name);
157
		}
158
		parent::validateTask($properties);
159
	}
160
161
	/**
162
	 * Sets the lastExecTime and processCount for a task from the DB.
163
	 * Also resets the lastExecTime when not in the DB
164
	 * @param string $name name of the task to get Persistent Data from
165
	 * @param object $task
166
	 * @return bool is the persistent data set or not
167
	 */
168
	protected function setPersistentData($name, $task)
169
	{
170
		if (isset($this->_taskRows[$name])) {
171
			$task->setLastExecTime($this->_taskRows[$name]['lastexectime']);
172
			$task->setProcessCount((int) $this->_taskRows[$name]['processcount']);
173
			if (serialize($task) !== $this->_taskRows[$name]['options']) {
174
				$this->updateTaskInternal($task);
175
				return false;
176
			}
177
			return true;
178
		} else {
179
			$this->addTaskInternal($task);
180
		}
181
		return false;
182
	}
183
184
	/**
185
	 * reads in all the tasks from the db, instances them if they are active db tasks.
186
	 * otherwise the rows are kept for persistent data.
187
	 * @param bool $initConfigTasks initialize the configuration
188
	 * @return \Prado\Util\Cron\TCronTask[]
189
	 */
190
	protected function ensureTasks($initConfigTasks = true)
191
	{
192
		if ($this->_tasksInitialized !== true) {
193
			$this->ensureTable();
194
			$this->_taskRows = $this->_tasks = [];
195
			$cmd = $this->getDbConnection()->createCommand(
196
				"SELECT * FROM {$this->_tableName} WHERE active IS NOT NULL ORDER BY tabuid"
197
			);
198
			$results = $cmd->query();
199
200
			Prado::log('Reading DB Cron Configuration', TLogger::NOTICE, TDbCronModule::class);
201
			foreach ($results->readAll() as $data) {
202
				if ($data['active']) {
203
					$task = $this->_tasks[$data['name']] = @unserialize($data['options']);
0 ignored issues
show
Unused Code introduced by
The assignment to $task is dead and can be removed.
Loading history...
204
				}
205
				$this->_taskRows[$data['name']] = $data;
206
			}
207
			$this->_tasksInitialized = true;
208
		}
209
		if ($initConfigTasks) {
210
			$this->_configTasks = parent::ensureTasks();
211
		}
212
		return array_merge($this->_tasks, $this->_configTasks ?? []);
213
	}
214
215
	/**
216
	 * @throws TConfigurationException when the configuration task names interfere with the db tasks names.
217
	 * @return array[TCronTask] combines the active configuration and db cron tasks
0 ignored issues
show
Documentation Bug introduced by
The doc comment array[TCronTask] at position 1 could not be parsed: Expected ']' at position 1, but found '['.
Loading history...
218
	 */
219
	public function getTasks()
220
	{
221
		$this->ensureTasks();
222
		if ($colliding = array_intersect_key($this->_tasks, $this->_configTasks)) {
223
			throw new TConfigurationException('dbcron_conflicting_task_names', implode(', ', array_keys($colliding)));
224
		}
225
		return array_merge($this->_tasks, $this->_configTasks);
226
	}
227
228
	/**
229
	 * checks for the table, and if not there and autoCreate, then creates the table else throw error.
230
	 * @throws TConfigurationException if the table does not exist and cannot autoCreate
231
	 */
232
	protected function ensureTable()
233
	{
234
		if ($this->_tableEnsured) {
235
			return;
236
		}
237
		$db = $this->getDbConnection();
238
		$sql = 'SELECT * FROM ' . $this->_tableName . ' WHERE 0=1';
239
		try {
240
			$db->createCommand($sql)->query()->close();
241
		} catch (Exception $e) {
242
			// DB table not exists
243
			if ($this->_autoCreate) {
244
				$this->createDbTable();
245
			} else {
246
				throw new TConfigurationException('dbcron_table_nonexistent', $this->_tableName);
247
			}
248
		}
249
		$this->_tableEnsured = true;
250
	}
251
252
253
	/**
254
	 * creates the module table
255
	 */
256
	protected function createDbTable()
257
	{
258
		$db = $this->getDbConnection();
259
		$driver = $db->getDriverName();
260
		$autotype = 'INTEGER';
261
		$autoidAttributes = '';
262
		if ($driver === 'mysql') {
263
			$autoidAttributes = ' AUTO_INCREMENT';
264
		} elseif ($driver === 'sqlite') {
265
			$autoidAttributes = ' AUTOINCREMENT';
266
		} elseif ($driver === 'postgresql') {
267
			$autotype = 'SERIAL';
268
		}
269
		$postIndices = '; CREATE INDEX tname ON ' . $this->_tableName . '(`name`);' .
270
			'CREATE INDEX tclass ON ' . $this->_tableName . '(`task`);' .
271
			'CREATE INDEX tactive ON ' . $this->_tableName . '(`active`);';
272
273
		$sql = 'CREATE TABLE IF NOT EXISTS ' . $this->_tableName . ' (
274
			`tabuid` ' . $autotype . ' PRIMARY KEY' . $autoidAttributes . ', 
275
			`name` VARCHAR (127) NOT NULL, 
276
			`schedule` VARCHAR (127) NOT NULL, 
277
			`task` VARCHAR (256) NOT NULL, 
278
			`moduleid` VARCHAR (127) NULL, 
279
			`username` VARCHAR (127) NULL, 
280
			`options` MEDIUMTEXT NULL, 
281
			`processcount` INT NOT NULL DEFAULT 0, 
282
			`lastexectime` VARCHAR (20) NULL DEFAULT `0`, 
283
			`active` BOOLEAN NULL
284
			)' . $postIndices;
285
286
		//`lastexectime` DECIMAL(12,8) NULL DEFAULT 0,
287
288
		$cmd = $this->getDbConnection()->createCommand($sql);
289
290
		$cmd->execute();
291
	}
292
293
	/**
294
	 * logCronTask adds a task log to the table.
295
	 * @param TCronTask $task
296
	 * @param string $username
297
	 */
298
	protected function logCronTask($task, $username)
299
	{
300
		parent::logCronTask($task, $username);
301
302
		$app = $this->getApplication();
0 ignored issues
show
Unused Code introduced by
The assignment to $app is dead and can be removed.
Loading history...
303
304
		$logid = null;
305
		if ($this->getLogCronTasks()) {
306
			$this->ensureTable();
307
308
			$cmd = $this->getDbConnection()->createCommand(
309
				"INSERT INTO {$this->_tableName} " .
310
					"(name, schedule, task, moduleid, username, options, processcount, lastexectime, active)" .
311
					" VALUES (:name, :schedule, :task, :mid, :username, :options, :count, :time, NULL)"
312
			);
313
			$cmd->bindValue(":name", $task->getName(), PDO::PARAM_STR);
314
			$cmd->bindValue(":task", $task->getTask(), PDO::PARAM_STR);
315
			$cmd->bindValue(":schedule", $task->getSchedule(), PDO::PARAM_STR);
316
			$cmd->bindValue(":mid", $task->getModuleId(), PDO::PARAM_STR);
317
			$cmd->bindValue(":username", $username, PDO::PARAM_STR);
318
			$cmd->bindValue(":options", serialize($task), PDO::PARAM_STR);
319
			$cmd->bindValue(":count", $task->getProcessCount(), PDO::PARAM_INT);
320
			$cmd->bindValue(":time", (int) microtime(true), PDO::PARAM_STR);
321
			$cmd->execute();
322
			$logid = $this->getDbConnection()->getLastInsertID();
323
		}
324
		return $logid;
325
	}
326
327
	/**
328
	 * This updates the LastExecTime and ProcessCount in the database
329
	 * @param TCronTask $task
330
	 */
331
	protected function updateTaskInfo($task)
332
	{
333
		$task->setLastExecTime($time = (int) microtime(true));
0 ignored issues
show
Bug introduced by
$time = (int)microtime(true) of type integer is incompatible with the type Prado\Util\Cron\numeric expected by parameter $v of Prado\Util\Cron\TCronTask::setLastExecTime(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

333
		$task->setLastExecTime(/** @scrutinizer ignore-type */ $time = (int) microtime(true));
Loading history...
334
		$task->setProcessCount($count = ($task->getProcessCount() + 1));
335
336
		$cmd = $this->getDbConnection()->createCommand(
337
			"UPDATE {$this->_tableName} SET processcount=:count, lastexectime=:time, options=:task WHERE name=:name AND active IS NOT NULL"
338
		);
339
		$cmd->bindValue(":count", $count, PDO::PARAM_STR);
340
		$cmd->bindValue(":time", $time, PDO::PARAM_STR);
341
		$cmd->bindValue(":task", serialize($task), PDO::PARAM_STR);
342
		$cmd->bindValue(":name", $task->getName(), PDO::PARAM_STR);
343
		$cmd->execute();
344
	}
345
346
	/**
347
	 * this removes any stale database rows from changing configTasks
348
	 */
349
	protected function filterStaleTasks()
350
	{
351
		$this->ensureTasks();
352
		$configTasks = $this->_taskRows;
353
354
		//remove non-configuration tasks
355
		foreach ($this->_taskRows as $name => $data) {
356
			if ($data['active']) {
357
				unset($configTasks[$name]);
358
			}
359
		}
360
361
		//remove configuration tasks
362
		foreach ($this->_configTasks as $name => $data) {
363
			unset($configTasks[$name]);
364
		}
365
366
		//remaining are stale
367
		if (count($configTasks)) {
368
			foreach ($configTasks as $name => $task) {
369
				$this->removeTaskInternal($name);
370
			}
371
		}
372
	}
373
374
	/**
375
	 * This executes the Run Time Tasks, this method is automatically added
376
	 * to TApplication::onEndRequest when there are RuntimeTasks via {@see addRuntimeTask}.
377
	 * @param null|\Prado\TApplication $sender
378
	 * @param null|mixed $param
379
	 * @return int number of tasks run
380
	 */
381
	public function executeRuntimeTasks($sender = null, $param = null)
0 ignored issues
show
Unused Code introduced by
The parameter $param is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

381
	public function executeRuntimeTasks($sender = null, /** @scrutinizer ignore-unused */ $param = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $sender is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

381
	public function executeRuntimeTasks(/** @scrutinizer ignore-unused */ $sender = null, $param = null)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
382
	{
383
		$runtimeTasks = $this->getRuntimeTasks();
384
		if (!$runtimeTasks) {
0 ignored issues
show
introduced by
$runtimeTasks is of type Prado\Util\Cron\TCronTask, thus it always evaluated to true.
Loading history...
385
			return;
386
		}
387
		$numtasks = count($runtimeTasks);
0 ignored issues
show
Bug introduced by
$runtimeTasks of type Prado\Util\Cron\TCronTask is incompatible with the type Countable|array expected by parameter $value of count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

387
		$numtasks = count(/** @scrutinizer ignore-type */ $runtimeTasks);
Loading history...
388
		$cronlogger = $this->asa(TCronModule::SHELL_LOG_BEHAVIOR);
389
		if ($cronlogger) {
0 ignored issues
show
introduced by
$cronlogger is of type object, thus it always evaluated to true.
Loading history...
390
			$enabled = $cronlogger->getEnabled();
391
			$cronlogger->setEnabled(false);
392
		}
393
		foreach ($runtimeTasks as $key => $task) {
394
			$this->runTask($task);
395
		}
396
		if ($cronlogger) {
0 ignored issues
show
introduced by
$cronlogger is of type object, thus it always evaluated to true.
Loading history...
397
			$cronlogger->setEnabled($enabled);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $enabled does not seem to be defined for all execution paths leading up to this point.
Loading history...
398
		}
399
		return $numtasks;
400
	}
401
402
	/**
403
	 * Adds a task to being run time.  If this is the first runtime task this
404
	 * method adds {@see executeRuntimeTasks} to TApplication::onEndRequest.
405
	 * @param TCronTask $task
406
	 */
407
	public function addRuntimeTask($task)
408
	{
409
		if ($this->_runtimeTasks === null) {
410
			Prado::getApplication()->attachEventHandler('onEndRequest', [$this, 'executeRuntimeTasks']);
411
			$this->_runtimeTasks = [];
412
		}
413
		$this->_runtimeTasks[$task->getName()] = $task;
414
	}
415
416
	/**
417
	 * Gets the runtime tasks.
418
	 * @return \Prado\Util\Cron\TCronTask the tasks to run on {@see executeRuntimeTasks}
419
	 */
420
	public function getRuntimeTasks()
421
	{
422
		return $this->_runtimeTasks;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->_runtimeTasks returns the type Prado\Util\Cron\TCronTask[] which is incompatible with the documented return type Prado\Util\Cron\TCronTask.
Loading history...
423
	}
424
425
	/**
426
	 * Removes a task from being run time.  If there are no runtime tasks left
427
	 * then it removes {@see executeRuntimeTasks} from TApplication::onEndRequest.
428
	 * @param TCronTask $untask
429
	 */
430
	public function removeRuntimeTask($untask)
431
	{
432
		if ($this->_runtimeTasks === null) {
433
			return;
434
		}
435
		$name = is_string($untask) ? $untask : $untask->getName();
0 ignored issues
show
introduced by
The condition is_string($untask) is always false.
Loading history...
436
		unset($this->_runtimeTasks[$name]);
437
		if (!$this->_runtimeTasks) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->_runtimeTasks of type Prado\Util\Cron\TCronTask[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
438
			$this->_runtimeTasks = null;
439
			Prado::getApplication()->detachEventHandler('onEndRequest', [$this, 'executeRuntimeTasks']);
440
		}
441
	}
442
443
	/**
444
	 * Clears all tasks from being run time, and removes the handler from onEndRequest.
445
	 */
446
	public function clearRuntimeTasks()
447
	{
448
		if ($this->_runtimeTasks === null) {
449
			return;
450
		}
451
		$this->_runtimeTasks = null;
452
		Prado::getApplication()->detachEventHandler('onEndRequest', [$this, 'executeRuntimeTasks']);
453
	}
454
455
	/**
456
	 *
457
	 * @param string $taskName
458
	 * @param bool $checkExisting
459
	 * @param bool $asObject returns the database row if false.
460
	 */
461
	public function getTask($taskName, $checkExisting = true, $asObject = true)
462
	{
463
		$this->ensureTable();
464
465
		if ($checkExisting) {
466
			$this->ensureTasks();
467
			if ($asObject) {
468
				if (isset($this->_tasks[$taskName])) {
469
					return $this->_tasks[$taskName];
470
				}
471
				if (isset($this->_configTasks[$taskName])) {
472
					return $this->_configTasks[$taskName];
473
				}
474
			} else {
475
				if (isset($this->_taskRows[$taskName])) {
476
					return $this->_taskRows[$taskName];
477
				}
478
			}
479
		}
480
481
482
		$cmd = $this->getDbConnection()->createCommand(
483
			"SELECT * FROM {$this->_tableName} WHERE name=:name AND active IS NOT NULL LIMIT 1"
484
		);
485
		$cmd->bindValue(":name", $taskName, PDO::PARAM_STR);
486
487
		$result = $cmd->queryRow();
488
489
		if (!$result) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $result of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
490
			return null;
491
		}
492
493
		if ($asObject) {
494
			return @unserialize($result['options']);
495
		}
496
497
		return $result;
498
	}
499
500
	/**
501
	 * Adds a task to the database.  Validates the name and cannot add a task with an existing name.
502
	 * This updates the table row data as well.
503
	 * @param TCronTask $task
504
	 * @param bool $runtime should the task be added to the Run Time Task after being added
505
	 * @return bool was the task added
506
	 */
507
	public function addTask($task, $runtime = false)
508
	{
509
		if ($this->dyAddTask(false, $task, $runtime) === true) {
510
			return false;
511
		}
512
		return $this->addTaskInternal($task, $runtime);
513
	}
514
515
	/**
516
	 * Adds a task to the database.  Validates the name and cannot add a task with an existing name.
517
	 * This updates the table row data as well.
518
	 * @param \Prado\Util\Cron\TCronTask $task
519
	 * @param bool $runtime should the task be added to the Run Time Task after being added
520
	 * @return bool was the task added
521
	 */
522
	protected function addTaskInternal($task, $runtime = false)
523
	{
524
		$this->ensureTable();
525
		$this->ensureTasks(false);
526
		$name = $task->getName();
527
		if (!preg_match(TDbCronModule::NAME_VALIDATOR_REGEX, $name)) {
528
			return false;
529
		}
530
		if (isset($this->_tasks[$name])) {
531
			return false;
532
		}
533
		try {
534
			$task->getScheduler();
535
		} catch (TInvalidDataValueException $e) {
536
			return false;
537
		}
538
		$task->resetTaskLastExecTime();
539
540
		$cmd = $this->getDbConnection()->createCommand(
541
			"INSERT INTO {$this->_tableName} " .
542
				"(name, schedule, task, moduleid, username, options, lastexectime, processcount, active)" .
543
				" VALUES (:name, :schedule, :task, :mid, :username, :options, :time, :count, :active)"
544
		);
545
		$cmd->bindValue(":name", $name, PDO::PARAM_STR);
546
		$cmd->bindValue(":schedule", $schedule = $task->getSchedule(), PDO::PARAM_STR);
547
		$cmd->bindValue(":task", $taskExec = $task->getTask(), PDO::PARAM_STR);
548
		$cmd->bindValue(":mid", $mid = $task->getModuleId(), PDO::PARAM_STR);
549
		$cmd->bindValue(":username", $username = $task->getUserName(), PDO::PARAM_STR);
550
		$cmd->bindValue(":options", $serial = serialize($task), PDO::PARAM_STR);
551
		$cmd->bindValue(":time", $time = $task->getLastExecTime(), PDO::PARAM_STR);
552
		$cmd->bindValue(":count", $count = $task->getProcessCount(), PDO::PARAM_INT);
553
		$cmd->bindValue(":active", $active = (isset($this->_configTasks[$name]) ? '0' : '1'), PDO::PARAM_INT);
554
		$cmd->execute();
555
556
		if ($this->_tasks !== null && !isset($this->_configTasks[$name])) {
557
			$this->_tasks[$name] = $task;
558
			$this->_taskRows[$name] = [];
559
			$this->_taskRows[$name]['name'] = $name;
560
			$this->_taskRows[$name]['schedule'] = $schedule;
561
			$this->_taskRows[$name]['task'] = $taskExec;
562
			$this->_taskRows[$name]['moduleid'] = $mid;
563
			$this->_taskRows[$name]['username'] = $username;
564
			$this->_taskRows[$name]['options'] = $serial;
565
			$this->_taskRows[$name]['processcount'] = $count;
566
			$this->_taskRows[$name]['lastexectime'] = $time;
567
			$this->_taskRows[$name]['active'] = $active;
568
		}
569
		if ($runtime) {
570
			$this->addRuntimeTask($task);
571
		}
572
		return true;
573
	}
574
575
	/**
576
	 * Updates a task from its unique name.  If the Task is not in the DB it returns false
577
	 * @param TCronTask $task
578
	 * @return bool was the task updated
579
	 */
580
	public function updateTask($task)
581
	{
582
		if ($this->dyUpdateTask(false, $task, ['extra' => ['username' => $task->getUserName()]]) === true) {
583
			return false;
584
		}
585
		return $this->updateTaskInternal($task);
586
	}
587
588
	/**
589
	 * Updates a task from its unique name.  If the Task is not in the DB it returns false
590
	 * @param \Prado\Util\Cron\TCronTask $task
591
	 * @return bool was the task updated
592
	 */
593
	protected function updateTaskInternal($task)
594
	{
595
		$this->ensureTable();
596
		$this->ensureTasks(false);
597
		$name = $task->getName();
598
		if (!$this->taskExists($name)) {
599
			return false;
600
		}
601
		try {
602
			$task->getScheduler();
603
		} catch (TInvalidDataValueException $e) {
604
			return false;
605
		}
606
		$schedule = $task->getSchedule();
607
		if ($schedule != $this->_taskRows[$name]['schedule']) {
608
			$task->resetTaskLastExecTime();
609
		}
610
611
		$cmd = $this->getDbConnection()->createCommand(
612
			"UPDATE {$this->_tableName} SET schedule=:schedule, task=:task, moduleid=:mid, username=:username, options=:options, processcount=:count, lastexectime=:time WHERE name=:name AND active IS NOT NULL"
613
		);
614
		$cmd->bindValue(":schedule", $schedule, PDO::PARAM_STR);
615
		$cmd->bindValue(":task", $taskExec = $task->getTask(), PDO::PARAM_STR);
616
		$cmd->bindValue(":mid", $mid = $task->getModuleId(), PDO::PARAM_STR);
617
		$cmd->bindValue(":username", $username = $task->getUserName(), PDO::PARAM_STR);
618
		$cmd->bindValue(":options", $serial = serialize($task), PDO::PARAM_STR);
619
		$cmd->bindValue(":count", $count = $task->getProcessCount(), PDO::PARAM_STR);
620
		$cmd->bindValue(":time", $time = $task->getLastExecTime(), PDO::PARAM_STR);
621
		$cmd->bindValue(":name", $name, PDO::PARAM_STR);
622
		$cmd->execute();
623
624
		if ($this->_tasks !== null) {
625
			$this->_taskRows[$name]['schedule'] = $schedule;
626
			$this->_taskRows[$name]['task'] = $taskExec;
627
			$this->_taskRows[$name]['moduleid'] = $mid;
628
			$this->_taskRows[$name]['username'] = $username;
629
			$this->_taskRows[$name]['options'] = $serial;
630
			$this->_taskRows[$name]['processcount'] = $count;
631
			$this->_taskRows[$name]['lastexectime'] = $time;
632
		}
633
		return true;
634
	}
635
636
	/**
637
	 * Removes a task from the database table.
638
	 * This also removes the task from the current tasks, the taskRow, and runtime Tasks.
639
	 *
640
	 * This cannot remove tasks that are current configuration tasks.  Only tasks
641
	 * that exist can be removed.
642
	 * @param string|TCronTask $untask the task to remove from the DB
643
	 * @return bool was the task removed
644
	 */
645
	public function removeTask($untask)
646
	{
647
		$task = null;
648
		if (is_string($untask)) {
649
			$task = $this->getTask($untask);
650
			if (!$task) {
651
				return false;
652
			}
653
		}
654
		if ($this->dyRemoveTask(false, $untask, ['extra' => ['username' => ($task ?? $untask)->getUserName()]]) === true) {
655
			return false;
656
		}
657
		return $this->removeTaskInternal($untask);
658
	}
659
660
	/**
661
	 * Removes a task from the database table.
662
	 * This also removes the task from the current tasks, the taskRow, and runtime Tasks.
663
	 *
664
	 * This cannot remove tasks that are current configuration tasks.  Only tasks
665
	 * that exist can be removed.
666
	 * @param \Prado\Util\Cron\TCronTask|string $untask the task to remove from the DB
667
	 * @return bool was the task removed
668
	 */
669
	protected function removeTaskInternal($untask)
670
	{
671
		$this->ensureTable();
672
		$this->ensureTasks(false);
673
		$name = is_subclass_of($untask, \Prado\Util\Cron\TCronTask::class) ? $untask->getName() : $untask;
674
		if (isset($this->_configTasks[$name])) {
675
			return false;
676
		}
677
		if (!$this->taskExists($name)) {
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type Prado\Util\Cron\TCronTask; however, parameter $name of Prado\Util\Cron\TDbCronModule::taskExists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

677
		if (!$this->taskExists(/** @scrutinizer ignore-type */ $name)) {
Loading history...
678
			return false;
679
		}
680
681
		$cmd = $this->getDbConnection()->createCommand(
682
			"DELETE FROM {$this->_tableName} WHERE name=:name AND active IS NOT NULL"
683
		);
684
		$cmd->bindValue(":name", $name, PDO::PARAM_STR);
685
		$cmd->execute();
686
687
		// Remove task to list of tasks
688
		unset($this->_tasks[$name]);
689
		unset($this->_taskRows[$name]);
690
		$this->removeRuntimeTask($name);
0 ignored issues
show
Bug introduced by
It seems like $name can also be of type string; however, parameter $untask of Prado\Util\Cron\TDbCronModule::removeRuntimeTask() does only seem to accept Prado\Util\Cron\TCronTask, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

690
		$this->removeRuntimeTask(/** @scrutinizer ignore-type */ $name);
Loading history...
691
		return true;
692
	}
693
694
	/**
695
	 * taskExists checks for a task or task name in the database
696
	 * @param string $name task to check in the database
697
	 * @throws \Prado\Exceptions\TDbException if the Fields and table is not correct
698
	 * @return bool whether the task name exists in the database table
699
	 */
700
	public function taskExists($name)
701
	{
702
		$this->ensureTable();
703
704
		$db = $this->getDbConnection();
705
		$cmd = $db->createCommand(
706
			"SELECT COUNT(*) AS count FROM {$this->_tableName} WHERE name=:name AND active IS NOT NULL"
707
		);
708
		$cmd->bindParameter(":name", $name, PDO::PARAM_STR);
709
		return $cmd->queryScalar() > 0;
710
	}
711
712
	/**
713
	 * deletes the cron log items before time minus $seconds.
714
	 * @param int $seconds the number of seconds before Now
715
	 */
716
	public function clearCronLog($seconds)
717
	{
718
		if ($this->dyClearCronLog(false, $seconds) === true) {
719
			return false;
720
		}
721
		$this->ensureTable();
722
723
		$seconds = (int) $seconds;
724
		$cmd = $this->getDbConnection()->createCommand(
725
			"SELECT COUNT(*) FROM {$this->_tableName} WHERE active IS NULL AND lastexectime <= :time"
726
		);
727
		$time = time() - $seconds;
728
		$cmd->bindParameter(":time", $time, PDO::PARAM_STR);
729
		$count = $cmd->queryScalar();
730
		$cmd = $this->getDbConnection()->createCommand(
731
			"DELETE FROM {$this->_tableName} WHERE active IS NULL AND lastexectime <= :time"
732
		);
733
		$cmd->bindParameter(":time", $time, PDO::PARAM_STR);
734
		$cmd->execute();
735
736
		return $count;
737
	}
738
739
	/**
740
	 * Deletes one cron log item from the database
741
	 * @param int $taskUID
742
	 */
743
	public function removeCronLogItem($taskUID)
744
	{
745
		if ($this->dyRemoveCronLogItem(false, $taskUID) === true) {
746
			return false;
747
		}
748
		$this->ensureTable();
749
		$taskUID = (int) $taskUID;
750
751
		$cmd = $this->getDbConnection()->createCommand(
752
			"DELETE FROM {$this->_tableName} WHERE active IS NULL AND tabuid = :uid"
753
		);
754
		$cmd->bindParameter(":uid", $taskUID, PDO::PARAM_INT);
755
		$cmd->execute();
756
	}
757
758
	/**
759
	 * @param null|string $name name of the logs to look for, or null for all
760
	 * @return int the number of log items of all or of $name
761
	 */
762
	public function getCronLogCount($name = null)
763
	{
764
		if ($this->dyGetCronLogCount(false, $name) === true) {
765
			return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
766
		}
767
		$this->ensureTable();
768
769
		$db = $this->getDbConnection();
770
		$where = '';
771
		if (is_string($name)) {
772
			$where = 'name=:name AND ';
773
		}
774
		$cmd = $db->createCommand(
775
			"SELECT COUNT(*) AS count FROM {$this->_tableName} WHERE {$where}active IS NULL"
776
		);
777
		if (is_string($name)) {
778
			$cmd->bindParameter(":name", $name, PDO::PARAM_STR);
779
		}
780
		return (int) $cmd->queryScalar();
781
	}
782
783
	/**
784
	 * Gets the cron log table of specific named or all tasks.
785
	 * @param null|string $name name of the tasks to get from the log, or null for all
786
	 * @param int $pageSize
787
	 * @param int $offset
788
	 * @param null|bool $sortingDesc sort by descending execution time.
789
	 */
790
	public function getCronLog($name, $pageSize, $offset, $sortingDesc = null)
791
	{
792
		if ($this->dyGetCronLog(false, $name, $pageSize, $offset, $sortingDesc) === true) {
793
			return false;
794
		}
795
		$this->ensureTable();
796
797
		$db = $this->getDbConnection();
798
		$driver = $db->getDriverName();
799
800
		$limit = $orderby = $where = '';
801
		if (is_string($name)) {
802
			$where = 'name=:name AND ';
803
		}
804
		$pageSize = (int) $pageSize;
805
		$offset = (int) $offset;
806
		if ($pageSize !== 0) {
807
			if ($offset !== 0) {
808
				if ($driver === 'postgresql') {
809
					$limit = " LIMIT {$pageSize} OFFSET {$offset}";
810
				} else {
811
					$limit = " LIMIT {$offset}, {$pageSize}";
812
				}
813
			} else {
814
				$limit = " LIMIT {$pageSize}";
815
			}
816
			$sortingDesc ??= true;
817
		}
818
		if ($sortingDesc !== null) {
819
			$sortingDesc = TPropertyValue::ensureBoolean($sortingDesc) ? 'DESC' : 'ASC';
820
			$orderby = " ORDER BY lastExecTime $sortingDesc, tabuid $sortingDesc, processCount $sortingDesc";
821
		}
822
		$cmd = $db->createCommand(
823
			"SELECT * FROM {$this->_tableName} WHERE {$where}active IS NULL{$orderby}{$limit}"
824
		);
825
		if (is_string($name)) {
826
			$cmd->bindParameter(":name", $name, PDO::PARAM_STR);
827
		}
828
		$results = $cmd->query();
829
		return $results->readAll();
830
	}
831
832
	/**
833
	 * Creates the DB connection. If no ConnectionId is provided, then this
834
	 * creates a sqlite database in runtime named 'cron.jobs'.
835
	 * @throws TConfigurationException if module ID is invalid or empty
836
	 * @return \Prado\Data\TDbConnection the created DB connection
837
	 */
838
	protected function createDbConnection()
839
	{
840
		if ($this->_connID !== '') {
841
			$config = $this->getApplication()->getModule($this->_connID);
842
			if ($config instanceof TDataSourceConfig) {
843
				return $config->getDbConnection();
844
			} else {
845
				throw new TConfigurationException('dbcron_connectionid_invalid', $this->_connID);
846
			}
847
		} else {
848
			$db = new TDbConnection();
849
			// default to SQLite3 database
850
			$dbFile = $this->getApplication()->getRuntimePath() . DIRECTORY_SEPARATOR . 'cron.jobs';
851
			$db->setConnectionString('sqlite:' . $dbFile);
852
			return $db;
853
		}
854
	}
855
856
	/**
857
	 * @return \Prado\Data\TDbConnection the DB connection instance
858
	 */
859
	public function getDbConnection()
860
	{
861
		if ($this->_conn === null) {
862
			$this->_conn = $this->createDbConnection();
863
			$this->_conn->setActive(true);
864
		}
865
		return $this->_conn;
866
	}
867
868
	/**
869
	 * @return null|string the ID of a {@see \Prado\Data\TDataSourceConfig} module. Defaults to empty string, meaning not set.
870
	 */
871
	public function getConnectionID()
872
	{
873
		return $this->_connID;
874
	}
875
876
	/**
877
	 * Sets the ID of a TDataSourceConfig module.
878
	 * The datasource module will be used to establish the DB connection for this cron module.
879
	 * @param string $value ID of the {@see \Prado\Data\TDataSourceConfig} module
880
	 * @throws TInvalidOperationException when trying to set this property but the module is already initialized.
881
	 */
882
	public function setConnectionID($value)
883
	{
884
		if ($this->_initialized) {
885
			throw new TInvalidOperationException('dbcron_property_unchangeable', 'ConnectionID');
886
		}
887
		$this->_connID = $value;
888
	}
889
890
	/**
891
	 * @return bool should tasks that run be logged, default true
892
	 */
893
	public function getLogCronTasks()
894
	{
895
		return $this->_logCronTasks;
896
	}
897
898
	/**
899
	 * @param bool $log should tasks that run be logged
900
	 */
901
	public function setLogCronTasks($log)
902
	{
903
		$this->_logCronTasks = TPropertyValue::ensureBoolean($log);
904
	}
905
906
	/**
907
	 * @return string table in the database for cron tasks and logs. Defaults to 'crontabs'
908
	 */
909
	public function getTableName()
910
	{
911
		return $this->_tableName;
912
	}
913
914
	/**
915
	 * @param string $table table in the database for cron tasks and logs
916
	 * @throws TInvalidOperationException when trying to set this property but the module is already initialized.
917
	 */
918
	public function setTableName($table)
919
	{
920
		if ($this->_initialized) {
921
			throw new TInvalidOperationException('dbcron_property_unchangeable', 'TableName');
922
		}
923
		$this->_tableName = TPropertyValue::ensureString($table);
924
	}
925
926
	/**
927
	 * @return bool whether the cron DB table should be automatically created if not exists. Defaults to true.
928
	 * @see setTableName
929
	 */
930
	public function getAutoCreateCronTable()
931
	{
932
		return $this->_autoCreate;
933
	}
934
935
	/**
936
	 * @param bool $value whether the cron DB table should be automatically created if not exists.
937
	 * @throws TInvalidOperationException when trying to set this property but the module is already initialized.
938
	 * @see setTableName
939
	 */
940
	public function setAutoCreateCronTable($value)
941
	{
942
		if ($this->_initialized) {
943
			throw new TInvalidOperationException('dbcron_property_unchangeable', 'AutoCreateCronTable');
944
		}
945
		$this->_autoCreate = TPropertyValue::ensureBoolean($value);
946
	}
947
}
948