MDL-55707 grades: Stop infinite loop when regrading.
[moodle.git] / lib / classes / task / scheduled_task.php
blob42500ab2d0d15238c41426bca9c067261cc626aa
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * Scheduled task abstract class.
20 * @package core
21 * @category task
22 * @copyright 2013 Damyon Wiese
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25 namespace core\task;
27 /**
28 * Abstract class defining a scheduled task.
29 * @copyright 2013 Damyon Wiese
30 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
32 abstract class scheduled_task extends task_base {
34 /** Minimum minute value. */
35 const MINUTEMIN = 0;
36 /** Maximum minute value. */
37 const MINUTEMAX = 59;
39 /** Minimum hour value. */
40 const HOURMIN = 0;
41 /** Maximum hour value. */
42 const HOURMAX = 23;
44 /** Minimum dayofweek value. */
45 const DAYOFWEEKMIN = 0;
46 /** Maximum dayofweek value. */
47 const DAYOFWEEKMAX = 6;
49 /** @var string $hour - Pattern to work out the valid hours */
50 private $hour = '*';
52 /** @var string $minute - Pattern to work out the valid minutes */
53 private $minute = '*';
55 /** @var string $day - Pattern to work out the valid days */
56 private $day = '*';
58 /** @var string $month - Pattern to work out the valid months */
59 private $month = '*';
61 /** @var string $dayofweek - Pattern to work out the valid dayofweek */
62 private $dayofweek = '*';
64 /** @var int $lastruntime - When this task was last run */
65 private $lastruntime = 0;
67 /** @var boolean $customised - Has this task been changed from it's default schedule? */
68 private $customised = false;
70 /** @var int $disabled - Is this task disabled in cron? */
71 private $disabled = false;
73 /**
74 * Get the last run time for this scheduled task.
75 * @return int
77 public function get_last_run_time() {
78 return $this->lastruntime;
81 /**
82 * Set the last run time for this scheduled task.
83 * @param int $lastruntime
85 public function set_last_run_time($lastruntime) {
86 $this->lastruntime = $lastruntime;
89 /**
90 * Has this task been changed from it's default config?
91 * @return bool
93 public function is_customised() {
94 return $this->customised;
97 /**
98 * Has this task been changed from it's default config?
99 * @param bool
101 public function set_customised($customised) {
102 $this->customised = $customised;
106 * Setter for $minute. Accepts a special 'R' value
107 * which will be translated to a random minute.
108 * @param string $minute
110 public function set_minute($minute) {
111 if ($minute === 'R') {
112 $minute = mt_rand(self::HOURMIN, self::HOURMAX);
114 $this->minute = $minute;
118 * Getter for $minute.
119 * @return string
121 public function get_minute() {
122 return $this->minute;
126 * Setter for $hour. Accepts a special 'R' value
127 * which will be translated to a random hour.
128 * @param string $hour
130 public function set_hour($hour) {
131 if ($hour === 'R') {
132 $hour = mt_rand(self::HOURMIN, self::HOURMAX);
134 $this->hour = $hour;
138 * Getter for $hour.
139 * @return string
141 public function get_hour() {
142 return $this->hour;
146 * Setter for $month.
147 * @param string $month
149 public function set_month($month) {
150 $this->month = $month;
154 * Getter for $month.
155 * @return string
157 public function get_month() {
158 return $this->month;
162 * Setter for $day.
163 * @param string $day
165 public function set_day($day) {
166 $this->day = $day;
170 * Getter for $day.
171 * @return string
173 public function get_day() {
174 return $this->day;
178 * Setter for $dayofweek.
179 * @param string $dayofweek
181 public function set_day_of_week($dayofweek) {
182 if ($dayofweek === 'R') {
183 $dayofweek = mt_rand(self::DAYOFWEEKMIN, self::DAYOFWEEKMAX);
185 $this->dayofweek = $dayofweek;
189 * Getter for $dayofweek.
190 * @return string
192 public function get_day_of_week() {
193 return $this->dayofweek;
197 * Setter for $disabled.
198 * @param bool $disabled
200 public function set_disabled($disabled) {
201 $this->disabled = (bool)$disabled;
205 * Getter for $disabled.
206 * @return bool
208 public function get_disabled() {
209 return $this->disabled;
213 * Override this function if you want this scheduled task to run, even if the component is disabled.
215 * @return bool
217 public function get_run_if_component_disabled() {
218 return false;
222 * Take a cron field definition and return an array of valid numbers with the range min-max.
224 * @param string $field - The field definition.
225 * @param int $min - The minimum allowable value.
226 * @param int $max - The maximum allowable value.
227 * @return array(int)
229 public function eval_cron_field($field, $min, $max) {
230 // Cleanse the input.
231 $field = trim($field);
233 // Format for a field is:
234 // <fieldlist> := <range>(/<step>)(,<fieldlist>)
235 // <step> := int
236 // <range> := <any>|<int>|<min-max>
237 // <any> := *
238 // <min-max> := int-int
239 // End of format BNF.
241 // This function is complicated but is covered by unit tests.
242 $range = array();
244 $matches = array();
245 preg_match_all('@[0-9]+|\*|,|/|-@', $field, $matches);
247 $last = 0;
248 $inrange = false;
249 $instep = false;
251 foreach ($matches[0] as $match) {
252 if ($match == '*') {
253 array_push($range, range($min, $max));
254 } else if ($match == '/') {
255 $instep = true;
256 } else if ($match == '-') {
257 $inrange = true;
258 } else if (is_numeric($match)) {
259 if ($instep) {
260 $i = 0;
261 for ($i = 0; $i < count($range[count($range) - 1]); $i++) {
262 if (($i) % $match != 0) {
263 $range[count($range) - 1][$i] = -1;
266 $inrange = false;
267 } else if ($inrange) {
268 if (count($range)) {
269 $range[count($range) - 1] = range($last, $match);
271 $inrange = false;
272 } else {
273 if ($match >= $min && $match <= $max) {
274 array_push($range, $match);
276 $last = $match;
281 // Flatten the result.
282 $result = array();
283 foreach ($range as $r) {
284 if (is_array($r)) {
285 foreach ($r as $rr) {
286 if ($rr >= $min && $rr <= $max) {
287 $result[$rr] = 1;
290 } else if (is_numeric($r)) {
291 if ($r >= $min && $r <= $max) {
292 $result[$r] = 1;
296 $result = array_keys($result);
297 sort($result, SORT_NUMERIC);
298 return $result;
302 * Assuming $list is an ordered list of items, this function returns the item
303 * in the list that is greater than or equal to the current value (or 0). If
304 * no value is greater than or equal, this will return the first valid item in the list.
305 * If list is empty, this function will return 0.
307 * @param int $current The current value
308 * @param int[] $list The list of valid items.
309 * @return int $next.
311 private function next_in_list($current, $list) {
312 foreach ($list as $l) {
313 if ($l >= $current) {
314 return $l;
317 if (count($list)) {
318 return $list[0];
321 return 0;
325 * Calculate when this task should next be run based on the schedule.
326 * @return int $nextruntime.
328 public function get_next_scheduled_time() {
329 global $CFG;
331 $validminutes = $this->eval_cron_field($this->minute, self::MINUTEMIN, self::MINUTEMAX);
332 $validhours = $this->eval_cron_field($this->hour, self::HOURMIN, self::HOURMAX);
334 // We need to change to the server timezone before using php date() functions.
335 \core_date::set_default_server_timezone();
337 $daysinmonth = date("t");
338 $validdays = $this->eval_cron_field($this->day, 1, $daysinmonth);
339 $validdaysofweek = $this->eval_cron_field($this->dayofweek, 0, 7);
340 $validmonths = $this->eval_cron_field($this->month, 1, 12);
341 $nextvalidyear = date('Y');
343 $currentminute = date("i") + 1;
344 $currenthour = date("H");
345 $currentday = date("j");
346 $currentmonth = date("n");
347 $currentdayofweek = date("w");
349 $nextvalidminute = $this->next_in_list($currentminute, $validminutes);
350 if ($nextvalidminute < $currentminute) {
351 $currenthour += 1;
353 $nextvalidhour = $this->next_in_list($currenthour, $validhours);
354 if ($nextvalidhour < $currenthour) {
355 $currentdayofweek += 1;
356 $currentday += 1;
358 $nextvaliddayofmonth = $this->next_in_list($currentday, $validdays);
359 $nextvaliddayofweek = $this->next_in_list($currentdayofweek, $validdaysofweek);
360 $daysincrementbymonth = $nextvaliddayofmonth - $currentday;
361 if ($nextvaliddayofmonth < $currentday) {
362 $daysincrementbymonth += $daysinmonth;
365 $daysincrementbyweek = $nextvaliddayofweek - $currentdayofweek;
366 if ($nextvaliddayofweek < $currentdayofweek) {
367 $daysincrementbyweek += 7;
370 // Special handling for dayofmonth vs dayofweek:
371 // if either field is * - use the other field
372 // otherwise - choose the soonest (see man 5 cron).
373 if ($this->dayofweek == '*') {
374 $daysincrement = $daysincrementbymonth;
375 } else if ($this->day == '*') {
376 $daysincrement = $daysincrementbyweek;
377 } else {
378 // Take the smaller increment of days by month or week.
379 $daysincrement = $daysincrementbymonth;
380 if ($daysincrementbyweek < $daysincrementbymonth) {
381 $daysincrement = $daysincrementbyweek;
385 $nextvaliddayofmonth = $currentday + $daysincrement;
386 if ($nextvaliddayofmonth > $daysinmonth) {
387 $currentmonth += 1;
388 $nextvaliddayofmonth -= $daysinmonth;
391 $nextvalidmonth = $this->next_in_list($currentmonth, $validmonths);
392 if ($nextvalidmonth < $currentmonth) {
393 $nextvalidyear += 1;
396 // Work out the next valid time.
397 $nexttime = mktime($nextvalidhour,
398 $nextvalidminute,
400 $nextvalidmonth,
401 $nextvaliddayofmonth,
402 $nextvalidyear);
404 return $nexttime;
408 * Get a descriptive name for this task (shown to admins).
410 * @return string
412 public abstract function get_name();