Passed
Push — master ( 5fd064...896ccb )
by Fabio
04:59
created

TDbCronModule::addTask()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

127
	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...
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

127
	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...
128
	{
129
		return new TCronTaskInfo('cronclean', 'Prado\\Util\\Cron\\TDbCronCleanLogTask', $this->getId(), Prado::localize('DbCron Clean Log Task'), Prado::localize('Clears the database of cron log items before the specified time period.'));
130
	}
131
	
132
	/**
133
	 * @param \Prado\Security\Permissions\TPermissionsManager $manager
0 ignored issues
show
Bug introduced by
The type Prado\Security\Permissions\TPermissionsManager was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
134
	 * @return \Prado\Security\Permissions\TPermissionEvent[]
135
	 */
136
	public function getPermissions($manager)
137
	{
138
		$userIsOwnerAllowedRule = new TUserOwnerRule();
139
		return array_merge([
140
			new TPermissionEvent(static::PERM_CRON_LOG_READ, 'Cron read Db log.', ['dyGetCronLog', 'dyGetCronLogCount']),
141
			new TPermissionEvent(static::PERM_CRON_LOG_DELETE, 'Cron delete Db log.', ['dyClearCronLog', 'dyRemoveCronLogItem']),
142
			new TPermissionEvent(static::PERM_CRON_ADD_TASK, 'Cron add Db Task.', ['dyAddTask']),
143
			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\Permissio...uthorizationRule[]|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

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

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

386
	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...
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

386
	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...
387
	{
388
		$runtimeTasks = $this->getRuntimeTasks();
389
		if (!$runtimeTasks) {
0 ignored issues
show
introduced by
$runtimeTasks is of type Prado\Util\Cron\TCronTask, thus it always evaluated to true.
Loading history...
390
			return;
391
		}
392
		$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

392
		$numtasks = count(/** @scrutinizer ignore-type */ $runtimeTasks);
Loading history...
393
		$this->logCron($numtasks);
394
		foreach ($runtimeTasks as $key => $task) {
395
			$this->runTask($task);
396
		}
397
		$this->filterStaleTasks();
398
		return $numtasks;
399
	}
400
	
401
	/**
402
	 * Adds a task to being run time.  If this is the first runtime task this
403
	 * method adds {@link executeRuntimeTasks} to TApplication::onEndRequest.
404
	 * @param TCronTask $task
405
	 */
406
	public function addRuntimeTask($task)
407
	{
408
		if ($this->_runtimeTasks === null) {
409
			Prado::getApplication()->attachEventHandler('onEndRequest', [$this, 'executeRuntimeTasks']);
410
			$this->_runtimeTasks = [];
411
		}
412
		$this->_runtimeTasks[$task->getName()] = $task;
413
	}
414
	
415
	/**
416
	 * Gets the runtime tasks.
417
	 * @return \Prado\Util\Cron\TCronTask the tasks to run on {@link executeRuntimeTasks}
418
	 */
419
	public function getRuntimeTasks()
420
	{
421
		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...
422
	}
423
	
424
	/**
425
	 * Removes a task from being run time.  If there are no runtime tasks left
426
	 * then it removes {@link executeRuntimeTasks} from TApplication::onEndRequest.
427
	 * @param TCronTask $untask
428
	 */
429
	public function removeRuntimeTask($untask)
430
	{
431
		if ($this->_runtimeTasks === null) {
432
			return;
433
		}
434
		$name = is_string($untask) ? $untask : $untask->getName();
0 ignored issues
show
introduced by
The condition is_string($untask) is always false.
Loading history...
435
		unset($this->_runtimeTasks[$name]);
436
		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...
437
			$this->_runtimeTasks = null;
438
			Prado::getApplication()->detachEventHandler('onEndRequest', [$this, 'executeRuntimeTasks']);
439
		}
440
	}
441
	
442
	/**
443
	 * Clears all tasks from being run time, and removes the handler from onEndRequest.
444
	 */
445
	public function clearRuntimeTasks()
446
	{
447
		if ($this->_runtimeTasks === null) {
448
			return;
449
		}
450
		$this->_runtimeTasks = null;
451
		Prado::getApplication()->detachEventHandler('onEndRequest', [$this, 'executeRuntimeTasks']);
452
	}
453
	
454
	/**
455
	 *
456
	 * @param string $taskName
457
	 * @param bool $checkExisting
458
	 * @param bool $asObject returns the database row if false.
459
	 */
460
	public function getTask($taskName, $checkExisting = true, $asObject = true)
461
	{
462
		$this->ensureTable();
463
		
464
		if ($checkExisting) {
465
			$this->ensureTasks();
466
			if ($asObject) {
467
				if (isset($this->_tasks[$taskName])) {
468
					return $this->_tasks[$taskName];
469
				}
470
				if (isset($this->_configTasks[$taskName])) {
471
					return $this->_configTasks[$taskName];
472
				}
473
			} else {
474
				if (isset($this->_taskRows[$taskName])) {
475
					return $this->_taskRows[$taskName];
476
				}
477
			}
478
		}
479
		
480
		
481
		$cmd = $this->getDbConnection()->createCommand(
482
			"SELECT * FROM {$this->_tableName} WHERE name=:name AND active IS NOT NULL LIMIT 1"
483
		);
484
		$cmd->bindValue(":name", $taskName, PDO::PARAM_STR);
485
		
486
		$result = $cmd->queryRow();
487
		
488
		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...
489
			return null;
490
		}
491
		
492
		if ($asObject) {
493
			return @unserialize($result['options']);
494
		}
495
		
496
		return $result;
497
	}
498
	
499
	/**
500
	 * Adds a task to the database.  Validates the name and cannot add a task with an existing name.
501
	 * This updates the table row data as well.
502
	 * @param TCronTask $task
503
	 * @param bool $runtime should the task be added to the Run Time Task after being added
504
	 * @return bool was the task added
505
	 */
506
	public function addTask($task, $runtime = false)
507
	{
508
		if ($this->dyAddTask(false, $task, $runtime) === true) {
509
			return false;
510
		}
511
		return $this->addTaskInternal($task, $runtime);
512
	}
513
	
514
	/**
515
	 * Adds a task to the database.  Validates the name and cannot add a task with an existing name.
516
	 * This updates the table row data as well.
517
	 * @param Prado\Util\Cron\TCronTask $task
0 ignored issues
show
Bug introduced by
The type Prado\Prado\Util\Cron\TCronTask was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
518
	 * @param bool $runtime should the task be added to the Run Time Task after being added
519
	 * @return bool was the task added
520
	 */
521
	protected function addTaskInternal($task, $runtime = false)
522
	{
523
		$this->ensureTable();
524
		$this->ensureTasks(false);
525
		$name = $task->getName();
526
		if (!preg_match(TDbCronModule::NAME_VALIDATOR_REGEX, $name)) {
527
			return false;
528
		}
529
		if (isset($this->_tasks[$name])) {
530
			return false;
531
		}
532
		try {
533
			$task->getScheduler();
534
		} catch (TInvalidDataValueException $e) {
535
			return false;
536
		}
537
		
538
		$cmd = $this->getDbConnection()->createCommand(
539
			"INSERT INTO {$this->_tableName} " .
540
				"(name, schedule, task, moduleid, username, options, lastexectime, processcount, active)" .
541
				" VALUES (:name, :schedule, :task, :mid, :username, :options, :time, :count, :active)"
542
		);
543
		$cmd->bindValue(":name", $name, PDO::PARAM_STR);
544
		$cmd->bindValue(":schedule", $schedule = $task->getSchedule(), PDO::PARAM_STR);
545
		$cmd->bindValue(":task", $taskExec = $task->getTask(), PDO::PARAM_STR);
546
		$cmd->bindValue(":mid", $mid = $task->getModuleId(), PDO::PARAM_STR);
547
		$cmd->bindValue(":username", $username = $task->getUserName(), PDO::PARAM_STR);
548
		$cmd->bindValue(":options", $serial = serialize($task), PDO::PARAM_STR);
549
		$cmd->bindValue(":time", $time = $task->getLastExecTime(), PDO::PARAM_STR);
550
		$cmd->bindValue(":count", $count = $task->getProcessCount(), PDO::PARAM_INT);
551
		$cmd->bindValue(":active", $active = (isset($this->_configTasks[$name]) ? '0' : '1'), PDO::PARAM_INT);
552
		$cmd->execute();
553
		
554
		if ($this->_tasks !== null && !isset($this->_configTasks[$name])) {
555
			$this->_tasks[$name] = $task;
556
			$this->_taskRows[$name] = [];
557
			$this->_taskRows[$name]['name'] = $name;
558
			$this->_taskRows[$name]['schedule'] = $schedule;
559
			$this->_taskRows[$name]['task'] = $taskExec;
560
			$this->_taskRows[$name]['moduleid'] = $mid;
561
			$this->_taskRows[$name]['username'] = $username;
562
			$this->_taskRows[$name]['options'] = $serial;
563
			$this->_taskRows[$name]['processcount'] = $count;
564
			$this->_taskRows[$name]['lastexectime'] = $time;
565
			$this->_taskRows[$name]['active'] = $active;
566
		}
567
		if ($runtime) {
568
			$this->addRuntimeTask($task);
569
		}
570
		return true;
571
	}
572
	
573
	/**
574
	 * Updates a task from its unique name.  If the Task is not in the DB it returns false
575
	 * @param TCronTask $task
576
	 * @return bool was the task updated
577
	 */
578
	public function updateTask($task)
579
	{
580
		if ($this->dyUpdateTask(false, $task, ['extra' => ['username' => $task->getUserName()]]) === true) {
581
			return false;
582
		}
583
		return $this->updateTaskInternal($task);
584
	}
585
	
586
	/**
587
	 * Updates a task from its unique name.  If the Task is not in the DB it returns false
588
	 * @param Prado\Util\Cron\TCronTask $task
589
	 * @return bool was the task updated
590
	 */
591
	protected function updateTaskInternal($task)
592
	{
593
		$this->ensureTable();
594
		$this->ensureTasks(false);
595
		$name = $task->getName();
596
		if (!$this->taskExists($name)) {
597
			return false;
598
		}
599
		try {
600
			$task->getScheduler();
601
		} catch (TInvalidDataValueException $e) {
602
			return false;
603
		}
604
		
605
		$cmd = $this->getDbConnection()->createCommand(
606
			"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"
607
		);
608
		$cmd->bindValue(":schedule", $schedule = $task->getSchedule(), PDO::PARAM_STR);
609
		$cmd->bindValue(":task", $taskExec = $task->getTask(), PDO::PARAM_STR);
610
		$cmd->bindValue(":mid", $mid = $task->getModuleId(), PDO::PARAM_STR);
611
		$cmd->bindValue(":username", $username = $task->getUserName(), PDO::PARAM_STR);
612
		$cmd->bindValue(":options", $serial = serialize($task), PDO::PARAM_STR);
613
		$cmd->bindValue(":count", $count = $task->getProcessCount(), PDO::PARAM_STR);
614
		$cmd->bindValue(":time", $time = $task->getLastExecTime(), PDO::PARAM_STR);
615
		$cmd->bindValue(":name", $name, PDO::PARAM_STR);
616
		$cmd->execute();
617
		
618
		if ($this->_tasks !== null) {
619
			$this->_taskRows[$name]['schedule'] = $schedule;
620
			$this->_taskRows[$name]['task'] = $taskExec;
621
			$this->_taskRows[$name]['moduleid'] = $mid;
622
			$this->_taskRows[$name]['username'] = $username;
623
			$this->_taskRows[$name]['options'] = $serial;
624
			$this->_taskRows[$name]['processcount'] = $count;
625
			$this->_taskRows[$name]['lastexectime'] = $time;
626
		}
627
		return true;
628
	}
629
	
630
	/**
631
	 * Removes a task from the database table.
632
	 * This also removes the task from the current tasks, the taskRow, and runtime Tasks.
633
	 *
634
	 * This cannot remove tasks that are current configuration tasks.  Only tasks
635
	 * that exist can be removed.
636
	 * @param string|TCronTask $untask the task to remove from the DB
637
	 * @return bool was the task removed
638
	 */
639
	public function removeTask($untask)
640
	{
641
		$task = null;
642
		if (is_string($untask)) {
643
			$task = $this->getTask($untask);
644
			if (!$task) {
645
				return false;
646
			}
647
		}
648
		if ($this->dyRemoveTask(false, $untask, ['extra' => ['username' => ($task ?? $untask)->getUserName()]]) === true) {
649
			return false;
650
		}
651
		return $this->removeTaskInternal($untask);
652
	}
653
	
654
	/**
655
	 * Removes a task from the database table.
656
	 * This also removes the task from the current tasks, the taskRow, and runtime Tasks.
657
	 *
658
	 * This cannot remove tasks that are current configuration tasks.  Only tasks
659
	 * that exist can be removed.
660
	 * @param Prado\Util\Cron\TCronTask|string $untask the task to remove from the DB
661
	 * @return bool was the task removed
662
	 */
663
	protected function removeTaskInternal($untask)
664
	{
665
		$this->ensureTable();
666
		$this->ensureTasks(false);
667
		
668
		$name = is_string($untask) ? $untask : $untask->getName();
669
		if (isset($this->_configTasks[$name])) {
670
			return false;
671
		}
672
		if (!$this->taskExists($name)) {
673
			return false;
674
		}
675
		
676
		$cmd = $this->getDbConnection()->createCommand(
677
			"DELETE FROM {$this->_tableName} WHERE name=:name AND active IS NOT NULL"
678
		);
679
		$cmd->bindValue(":name", $name, PDO::PARAM_STR);
680
		$cmd->execute();
681
		
682
		// Remove task to list of tasks
683
		unset($this->_tasks[$name]);
684
		unset($this->_taskRows[$name]);
685
		$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

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