emptyTaskLog()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 5
ccs 0
cts 3
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Functions to support schedules tasks
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * @version 2.0 dev
11
 *
12
 */
13
14
use ElkArte\Helper\Util;
15
16
/**
17
 * Calculate the next time the passed tasks should be triggered.
18
 *
19
 * @param string[]|string $tasks = array() the tasks
20
 * @param bool $forceUpdate
21
 * @package ScheduledTasks
22
 */
23
function calculateNextTrigger($tasks = array(), $forceUpdate = false)
24
{
25
	global $modSettings;
26
27
	$db = database();
28
29
	$task_query = '';
30
31
	if (!is_array($tasks))
32
	{
33
		$tasks = [$tasks];
34
	}
35
36
	// Actually have something passed?
37
	if (!empty($tasks))
38
	{
39
		if (!isset($tasks[0]) || is_numeric($tasks[0]))
40
		{
41
			$task_query = ' AND id_task IN ({array_int:tasks})';
42
		}
43
		else
44
		{
45
			$task_query = ' AND task IN ({array_string:tasks})';
46
		}
47
	}
48
49
	$nextTaskTime = empty($tasks) ? time() + 86400 : $modSettings['next_task_time'];
50
51
	// Get the critical info for the tasks.
52
	$request = $db->query('', '
53
		SELECT 
54
			id_task, next_time, time_offset, time_regularity, time_unit, task
55
		FROM {db_prefix}scheduled_tasks
56
		WHERE disabled = {int:no_disabled}
57
			' . $task_query,
58
		array(
59
			'no_disabled' => 0,
60
			'tasks' => $tasks,
61
		)
62
	);
63
	$tasks = array();
64
	$scheduleTaskImmediate = !empty($modSettings['scheduleTaskImmediate']) ? Util::unserialize($modSettings['scheduleTaskImmediate']) : array();
65
	while (($row = $request->fetch_assoc()))
66
	{
67
		// scheduleTaskImmediate is a way to speed up scheduled tasks and fire them as fast as possible
68
		if (!empty($scheduleTaskImmediate) && isset($scheduleTaskImmediate[$row['task']]))
69
		{
70
			$next_time = next_time(1, '', rand(0, 60), true);
71
		}
72
		else
73
		{
74
			$next_time = next_time($row['time_regularity'], $row['time_unit'], $row['time_offset']);
75
		}
76
77
		// Only bother moving the task if it's out of place or we're forcing it!
78
		if ($forceUpdate || $next_time < $row['next_time'] || $row['next_time'] < time())
79
		{
80
			$tasks[$row['id_task']] = $next_time;
81
		}
82
		else
83
		{
84
			$next_time = $row['next_time'];
85
		}
86
87
		// If this is sooner than the current next task, make this the next task.
88
		if ($next_time < $nextTaskTime)
89
		{
90
			$nextTaskTime = $next_time;
91
		}
92
	}
93
	$request->free_result();
94
95
	// Now make the changes!
96
	foreach ($tasks as $id => $time)
97
	{
98
		$db->query('', '
99
			UPDATE {db_prefix}scheduled_tasks
100
			SET 
101
				next_time = {int:next_time}
102
			WHERE id_task = {int:id_task}',
103
			array(
104
				'next_time' => $time,
105
				'id_task' => $id,
106
			)
107
		);
108
	}
109
110
	// If the next task is now different update.
111
	if ($modSettings['next_task_time'] != $nextTaskTime)
112
	{
113
		updateSettings(array('next_task_time' => $nextTaskTime));
114
	}
115
}
116
117
/**
118
 * Returns a time stamp of the next instance of these time parameters.
119
 *
120
 * @param int $regularity number of $unit units
121
 * @param string $unit time units, m, h, d, w
122
 * @param int $offset
123
 * @param bool $immediate
124
 * @return int
125
 * @package ScheduledTasks
126
 */
127
function next_time($regularity, $unit, $offset, $immediate = false)
128
{
129
	// Just in case!
130
	if ((int) $regularity === 0)
131 1
	{
132
		$regularity = 2;
133
	}
134
135
	$curMin = date('i', time());
136 1
137
	// If we have scheduleTaskImmediate running, then it's 10 seconds
138
	if (empty($unit) && $immediate)
139 1
	{
140
		$next_time = time() + 10;
141
	}
142
	// If the unit is minutes only check regularity in minutes.
143
	elseif ($unit === 'm')
144 1
	{
145
		$off = date('i', $offset);
146
147
		// If it's now just pretend it ain't,
148
		if ($off == $curMin)
149
		{
150
			$next_time = time() + $regularity;
151
		}
152
		else
153
		{
154
			// Make sure that the offset is always in the past.
155
			$off = $off > $curMin ? $off - 60 : $off;
156
157
			while ($off <= $curMin)
158
			{
159
				$off += $regularity;
160
			}
161
162
			// Now we know when the time should be!
163
			$next_time = time() + 60 * ($off - $curMin);
164
		}
165
	}
166
	// Otherwise, work out what the offset would be with todays date.
167
	else
168
	{
169
		$next_time = mktime(date('H', $offset), date('i', $offset), 0, date('m'), date('d'), date('Y'));
0 ignored issues
show
Bug introduced by
date('m') of type string is incompatible with the type integer expected by parameter $month of mktime(). ( Ignorable by Annotation )

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

169
		$next_time = mktime(date('H', $offset), date('i', $offset), 0, /** @scrutinizer ignore-type */ date('m'), date('d'), date('Y'));
Loading history...
Bug introduced by
date('Y') of type string is incompatible with the type integer expected by parameter $year of mktime(). ( Ignorable by Annotation )

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

169
		$next_time = mktime(date('H', $offset), date('i', $offset), 0, date('m'), date('d'), /** @scrutinizer ignore-type */ date('Y'));
Loading history...
Bug introduced by
date('H', $offset) of type string is incompatible with the type integer expected by parameter $hour of mktime(). ( Ignorable by Annotation )

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

169
		$next_time = mktime(/** @scrutinizer ignore-type */ date('H', $offset), date('i', $offset), 0, date('m'), date('d'), date('Y'));
Loading history...
Bug introduced by
date('d') of type string is incompatible with the type integer expected by parameter $day of mktime(). ( Ignorable by Annotation )

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

169
		$next_time = mktime(date('H', $offset), date('i', $offset), 0, date('m'), /** @scrutinizer ignore-type */ date('d'), date('Y'));
Loading history...
Bug introduced by
date('i', $offset) of type string is incompatible with the type integer expected by parameter $minute of mktime(). ( Ignorable by Annotation )

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

169
		$next_time = mktime(date('H', $offset), /** @scrutinizer ignore-type */ date('i', $offset), 0, date('m'), date('d'), date('Y'));
Loading history...
170 1
171
		// Make the time offset in the past!
172
		if ($next_time > time())
173 1
		{
174
			$next_time -= 86400;
175 1
		}
176
177
		// Default we'll jump in hours.
178
		$applyOffset = 3600;
179 1
180
		// 24 hours = 1 day.
181
		if ($unit === 'd')
182 1
		{
183
			$applyOffset = 86400;
184 1
		}
185
186
		// Otherwise a week.
187
		if ($unit === 'w')
188 1
		{
189
			$applyOffset = 604800;
190
		}
191
192
		$applyOffset *= $regularity;
193 1
194
		// Just add on the offset.
195
		while ($next_time <= time())
196 1
		{
197
			$next_time += $applyOffset;
198 1
		}
199
	}
200
201
	return $next_time;
202 1
}
203
204
/**
205
 * Loads a basic tasks list.
206
 *
207
 * @param int[] $tasks
208
 * @return array
209
 * @package ScheduledTasks
210
 */
211
function loadTasks($tasks)
212
{
213
	$db = database();
214
215
	$task = array();
216
	$db->fetchQuery('
217
		SELECT 
218
			id_task, task
219
		FROM {db_prefix}scheduled_tasks
220
		WHERE id_task IN ({array_int:tasks})
221
		LIMIT ' . count($tasks),
222
		array(
223
			'tasks' => $tasks,
224
		)
225
	)->fetch_callback(
226
		function ($row) use (&$task) {
227
			$task[$row['id_task']] = $row['task'];
228
		}
229
	);
230
231
	return $task;
232
}
233
234
/**
235
 * Logs a task.
236
 *
237
 * @param int $id_log the id of the log entry of the task just run. If empty it is considered a new log entry
238
 * @param int $task_id the id of the task run (from the table scheduled_tasks)
239
 * @param int|null $total_time How long the task took to finish. If NULL (default value) -1 will be used
240
 * @return int the id_log value
241
 * @package ScheduledTasks
242
 */
243
function logTask($id_log, $task_id, $total_time = null)
244
{
245
	$db = database();
246
247
	if (empty($id_log))
248 1
	{
249
		$db->insert('',
250 1
			'{db_prefix}log_scheduled_tasks',
251
			array('id_task' => 'int', 'time_run' => 'int', 'time_taken' => 'float'),
252 1
			array($task_id, time(), $total_time ?? -1),
253 1
			array('id_task')
254 1
		);
255 1
256 1
		return $db->insert_id('{db_prefix}log_scheduled_tasks');
0 ignored issues
show
Bug Best Practice introduced by
The expression return $db->insert_id('{...x}log_scheduled_tasks') also could return the type boolean which is incompatible with the documented return type integer.
Loading history...
257
	}
258
	else
259 1
	{
260
		$db->query('', '
261
			UPDATE {db_prefix}log_scheduled_tasks
262
			SET 
263 1
				time_taken = {float:time_taken}
264
			WHERE id_log = {int:id_log}',
265
			array(
266
				'time_taken' => $total_time,
267
				'id_log' => $id_log,
268
			)
269 1
		);
270 1
271
		return $id_log;
272
	}
273
}
274 1
275
/**
276
 * All the scheduled tasks associated with the id passed to the function are
277
 * enabled, while the remaining are disabled
278
 *
279
 * @param int[] $enablers array od task IDs
280
 * @package ScheduledTasks
281
 */
282
function updateTaskStatus($enablers)
283
{
284
	$db = database();
285
286
	$db->query('', '
287
		UPDATE {db_prefix}scheduled_tasks
288
		SET 
289
			disabled = CASE WHEN id_task IN ({array_int:id_task_enable}) THEN 0 ELSE 1 END',
290
		array(
291
			'id_task_enable' => $enablers,
292
		)
293
	);
294
}
295
296
/**
297
 * Sets the task status to enabled / disabled by task name (i.e. function)
298
 *
299
 * @param string $enabler the name (the function) of a task
300
 * @param bool $enable is if the tasks should be enabled or disabled
301
 * @package ScheduledTasks
302
 */
303
function toggleTaskStatusByName($enabler, $enable = true)
304
{
305
	$db = database();
306
307
	$db->query('', '
308
		UPDATE {db_prefix}scheduled_tasks
309
		SET 
310
			disabled = {int:status}
311
		WHERE task = {string:task_enable}',
312
		array(
313
			'task_enable' => $enabler,
314
			'status' => $enable ? 0 : 1,
315
		)
316
	);
317
}
318
319
/**
320
 * Update the properties of a scheduled task.
321
 *
322
 * @param int $id_task
323
 * @param int|null $disabled
324
 * @param int|null $offset
325
 * @param int|null $interval
326
 * @param string|null $unit
327
 * @package ScheduledTasks
328
 */
329
function updateTask($id_task, $disabled = null, $offset = null, $interval = null, $unit = null)
330
{
331
	$db = database();
332
333
	$sets = array(
334
		'disabled' => 'disabled = {int:disabled}',
335
		'offset' => 'time_offset = {int:time_offset}',
336
		'interval' => 'time_regularity = {int:time_regularity}',
337
		'unit' => 'time_unit = {string:time_unit}',
338
	);
339
340
	$updates = array();
341
	foreach ($sets as $key => $set)
342
	{
343
		if (isset($$key))
344
		{
345
			$updates[] = $set;
346
		}
347
	}
348
349
	$db->query('', '
350
		UPDATE {db_prefix}scheduled_tasks
351
		SET 
352
			' . (implode(',', $updates)) . '
353
		WHERE id_task = {int:id_task}',
354
		array(
355
			'disabled' => $disabled,
356
			'time_offset' => $offset,
357
			'time_regularity' => $interval,
358
			'id_task' => $id_task,
359
			'time_unit' => $unit,
360
		)
361
	);
362
}
363
364
/**
365
 * Loads the details from a given task.
366
 *
367
 * @param int $id_task
368
 *
369
 * @return array
370
 * @throws \ElkArte\Exceptions\Exception no_access
371
 * @package ScheduledTasks
372
 *
373
 */
374
function loadTaskDetails($id_task)
375
{
376
	$db = database();
377
378
	$task = array();
379
380
	$db->fetchQuery('
381
		SELECT 
382
			id_task, next_time, time_offset, time_regularity, time_unit, disabled, task
383
		FROM {db_prefix}scheduled_tasks
384
		WHERE id_task = {int:id_task}',
385
		array(
386
			'id_task' => $id_task,
387
		)
388
	)->fetch_callback(
389
		function ($row) use (&$task) {
390
			global $txt;
391
392
			$task = array(
393
				'id' => $row['id_task'],
394
				'function' => $row['task'],
395
				'name' => $txt['scheduled_task_' . $row['task']] ?? $row['task'],
396
				'desc' => $txt['scheduled_task_desc_' . $row['task']] ?? '',
397
				'next_time' => $row['disabled'] ? $txt['scheduled_tasks_na'] : standardTime($row['next_time'] == 0 ? time() : $row['next_time'], true, 'server'),
398
				'disabled' => $row['disabled'],
399
				'offset' => $row['time_offset'],
400
				'regularity' => $row['time_regularity'],
401
				'offset_formatted' => date('H:i', $row['time_offset']),
402
				'unit' => $row['time_unit'],
403
			);
404
		}
405
	);
406
407
	// Should never, ever, happen!
408
	if (empty($task))
409
	{
410
		throw new \ElkArte\Exceptions\Exception('no_access', false);
411
	}
412
413
	return $task;
414
}
415
416
/**
417
 * Returns an array of registered scheduled tasks.
418
 *
419
 * - Used also by createList() callbacks.
420
 *
421
 * @return array
422
 * @package ScheduledTasks
423
 */
424
function scheduledTasks()
425
{
426
	$db = database();
427
428
	$known_tasks = array();
429
	$db->fetchQuery('
430
		SELECT 
431
			id_task, next_time, time_offset, time_regularity, time_unit, disabled, task
432
		FROM {db_prefix}scheduled_tasks',
433
		array()
434
	)->fetch_callback(
435
		function ($row) use (&$known_tasks) {
436
			global $txt;
437
438
			// Find the next for regularity - don't offset as it's always server time!
439
			$offset = sprintf($txt['scheduled_task_reg_starting'], date('H:i', $row['time_offset']));
440
			$repeating = sprintf($txt['scheduled_task_reg_repeating'], $row['time_regularity'], $txt['scheduled_task_reg_unit_' . $row['time_unit']]);
441
442
			$known_tasks[] = array(
443
				'id' => $row['id_task'],
444
				'function' => $row['task'],
445
				'name' => $txt['scheduled_task_' . $row['task']] ?? $row['task'],
446
				'desc' => $txt['scheduled_task_desc_' . $row['task']] ?? '',
447
				'next_time' => $row['disabled'] ? $txt['scheduled_tasks_na'] : standardTime(($row['next_time'] == 0 ? time() : $row['next_time']), true, 'server'),
448
				'disabled' => $row['disabled'],
449
				'checked_state' => $row['disabled'] ? '' : 'checked="checked"',
450
				'regularity' => $offset . ', ' . $repeating,
451
			);
452
		}
453
	);
454
455
	return $known_tasks;
456
}
457
458
/**
459
 * Return task log entries, within the passed limits.
460
 *
461
 * - Used by createList() callbacks.
462
 *
463
 * @param int $start The item to start with (for pagination purposes)
464
 * @param int $items_per_page The number of items to show per page
465
 * @param string $sort A string indicating how to sort the results
466
 *
467
 * @return array
468
 * @package ScheduledTasks
469
 */
470
function getTaskLogEntries($start, $items_per_page, $sort)
471
{
472
	$db = database();
473
474
	$log_entries = array();
475
	$db->fetchQuery('
476
		SELECT 
477
			lst.id_log, lst.id_task, lst.time_run, lst.time_taken, st.task
478
		FROM {db_prefix}log_scheduled_tasks AS lst
479
			INNER JOIN {db_prefix}scheduled_tasks AS st ON (st.id_task = lst.id_task)
480
		ORDER BY ' . $sort . '
481
		LIMIT ' . $items_per_page . '  OFFSET ' . $start,
482
		array()
483
	)->fetch_callback(
484
		function ($row) use (&$log_entries) {
485
			global $txt;
486
487
			$log_entries[] = array(
488
				'id' => $row['id_log'],
489
				'name' => $txt['scheduled_task_' . $row['task']] ?? $row['task'],
490
				'time_run' => $row['time_run'],
491
				// -1 means failed task, but in order to look better in the UI we switch it to 0
492
				'time_taken' => $row['time_taken'] == -1 ? 0 : $row['time_taken'],
493
				'task_completed' => $row['time_taken'] != -1,
494
			);
495
		}
496
	);
497
498
	return $log_entries;
499
}
500
501
/**
502
 * Return the number of task log entries.
503
 *
504
 * - Used by createList() callbacks.
505
 *
506
 * @return int
507
 * @package ScheduledTasks
508
 */
509
function countTaskLogEntries()
510
{
511
	$db = database();
512
513
	$request = $db->query('', '
514
		SELECT 
515
			COUNT(*)
516
		FROM {db_prefix}log_scheduled_tasks',
517
		array()
518
	);
519
	list ($num_entries) = $request->fetch_row();
520
	$request->free_result();
521
522
	return $num_entries;
523
}
524
525
/**
526
 * Empty the scheduled tasks log.
527
 */
528
function emptyTaskLog()
529
{
530
	$db = database();
531
532
	$db->truncate('{db_prefix}log_scheduled_tasks');
533
}
534
535
/**
536
 * Process the next tasks, one by one, and update the results.
537
 *
538
 * @param int $ts = 0
539
 * @package ScheduledTasks
540
 */
541
function processNextTasks($ts = 0)
542
{
543
	$db = database();
544
545
	// Select the next task to do.
546
	$request = $db->fetchQuery('
547
		SELECT 
548
			id_task, task, next_time, time_offset, time_regularity, time_unit
549
		FROM {db_prefix}scheduled_tasks
550
		WHERE disabled = {int:not_disabled}
551
			AND next_time <= {int:current_time}
552
		ORDER BY next_time ASC
553
		LIMIT 1',
554
		array(
555
			'not_disabled' => 0,
556 1
			'current_time' => time(),
557
		)
558
	);
559 1
	if ($request->num_rows() !== 0)
560
	{
561
		// The two important things really...
562
		$row = $request->fetch_assoc();
563
564
		// When should this next be run?
565
		$next_time = next_time($row['time_regularity'], $row['time_unit'], $row['time_offset']);
566
567
		// How long in seconds is the gap?
568 1
		$duration = $row['time_regularity'];
569 1
		switch ($row['time_unit'])
570
		{
571
			case 'm':
572 1
				$duration *= 60;
573
				break;
574
			case 'h':
575 1
			 	$duration *= 3600;
576
				break;
577
			case 'd':
578 1
		   		$duration *= 86400;
579
				break;
580
			case 'w':
581 1
				$duration *= 604800;
582 1
				break;
583
		}
584 1
585
		// If we were really late running this task actually skip the next one.
586
		if (time() + ($duration / 2) > $next_time)
587 1
		{
588
			$next_time += $duration;
589
		}
590 1
591 1
		// Update it now, so no others run this!
592 1
		$affected_rows = $db->query('', '
593
			UPDATE {db_prefix}scheduled_tasks
594
			SET 
595
				next_time = {int:next_time}
596
			WHERE id_task = {int:id_task}
597
				AND next_time = {int:current_next_time}',
598
			array(
599 1
				'next_time' => $next_time,
600
				'id_task' => $row['id_task'],
601
				'current_next_time' => $row['next_time'],
602
			)
603
		)->affected_rows();
604
605 1
		// Do also some timestamp checking,
606
		// and do this only if we updated it before.
607
		if ((empty($ts) || $ts == $row['next_time']) && $affected_rows)
608
		{
609
			ignore_user_abort(true);
610
			run_this_task($row['id_task'], $row['task']);
611
		}
612 1
613 1
	}
614 1
}
615
616 1
/**
617
 * Calls the supplied task_name so that it is executed.
618
 *
619
 * - Logs that the task was executed with success or if it failed
620 1
 *
621
 * @param int $id_task specific id of the task to run, used for logging
622 1
 * @param string $task_name name of the task, class name, function name, method in ScheduledTask.class
623 1
 * @package ScheduledTasks
624
 */
625
function run_this_task($id_task, $task_name)
626
{
627 1
	global $time_start, $modSettings;
628
629
	// Let's start logging the task and saying we failed it
630
	$log_task_id = logTask(0, $id_task);
631
632
	$class = '\\ElkArte\\ScheduledTasks\\Tasks\\' . implode('', array_map('ucfirst', explode('_', $task_name)));
633
634
	if (class_exists($class))
635
	{
636
		$task = new $class();
637
638
		$completed = $task->run();
639
	}
640
	else
641 1
	{
642
		$completed = false;
643
	}
644 1
645
	$scheduleTaskImmediate = !empty($modSettings['scheduleTaskImmediate']) ? Util::unserialize($modSettings['scheduleTaskImmediate']) : array();
646 1
	// Log that we did it ;)
647
	if ($completed)
648 1
	{
649
		// Taking care of scheduleTaskImmediate having a maximum of 10 "fast" executions
650 1
		if (!empty($scheduleTaskImmediate) && isset($scheduleTaskImmediate[$task_name]))
651
		{
652 1
			$scheduleTaskImmediate[$task_name]++;
653
654
			if ($scheduleTaskImmediate[$task_name] > 9)
655
			{
656
				removeScheduleTaskImmediate($task_name, false);
657
			}
658
			else
659 1
			{
660
				updateSettings(array('scheduleTaskImmediate' => serialize($scheduleTaskImmediate)));
661 1
			}
662
		}
663
664 1
		$total_time = round(microtime(true) - $time_start, 3);
665
666
		// If the task ended successfully, then log the proper time taken to complete
667
		logTask($log_task_id, $id_task, $total_time);
0 ignored issues
show
Bug introduced by
$total_time of type double is incompatible with the type integer|null expected by parameter $total_time of logTask(). ( Ignorable by Annotation )

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

667
		logTask($log_task_id, $id_task, /** @scrutinizer ignore-type */ $total_time);
Loading history...
668
	}
669
}
670
671
/**
672
 * Retrieve info if there's any next task scheduled and when.
673
 *
674
 * @return mixed int|false
675
 * @package ScheduledTasks
676
 */
677
function nextTime()
678 1
{
679
	$db = database();
680
681 1
	// The next stored timestamp, is there any?
682
	$request = $db->query('', '
683 1
		SELECT 
684
			next_time
685
		FROM {db_prefix}scheduled_tasks
686
		WHERE disabled = {int:not_disabled}
687
		ORDER BY next_time ASC
688
		LIMIT 1',
689
		array(
690
			'not_disabled' => 0,
691
		)
692
	);
693
	// No new task scheduled?
694 1
	if ($request->num_rows() === 0)
695
	{
696
		$result = false;
697 1
	}
698
	else
699
	{
700
		list ($result) = $request->fetch_row();
701
	}
702
703
	$request->free_result();
704
705 1
	return $result;
706
}
707