See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 403]
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. 13 // 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/>. 16 17 /** 18 * Scheduled task abstract class. 19 * 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 24 */ 25 namespace core\task; 26 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 31 */ 32 abstract class scheduled_task extends task_base { 33 34 /** Minimum minute value. */ 35 const MINUTEMIN = 0; 36 /** Maximum minute value. */ 37 const MINUTEMAX = 59; 38 39 /** Minimum hour value. */ 40 const HOURMIN = 0; 41 /** Maximum hour value. */ 42 const HOURMAX = 23; 43 44 /** Minimum day of month value. */ 45 const DAYMIN = 1; 46 /** Maximum day of month value. */ 47 const DAYMAX = 31; 48 49 /** Minimum month value. */ 50 const MONTHMIN = 1; 51 /** Maximum month value. */ 52 const MONTHMAX = 12; 53 54 /** Minimum dayofweek value. */ 55 const DAYOFWEEKMIN = 0; 56 /** Maximum dayofweek value. */ 57 const DAYOFWEEKMAX = 6; 58 /** Maximum dayofweek value allowed in input (7 = 0). */ 59 const DAYOFWEEKMAXINPUT = 7; 60 61 /** 62 * Minute field identifier. 63 */ 64 const FIELD_MINUTE = 'minute'; 65 /** 66 * Hour field identifier. 67 */ 68 const FIELD_HOUR = 'hour'; 69 /** 70 * Day-of-month field identifier. 71 */ 72 const FIELD_DAY = 'day'; 73 /** 74 * Month field identifier. 75 */ 76 const FIELD_MONTH = 'month'; 77 /** 78 * Day-of-week field identifier. 79 */ 80 const FIELD_DAYOFWEEK = 'dayofweek'; 81 82 /** 83 * Time used for the next scheduled time when a task should never run. This is 2222-01-01 00:00 GMT 84 * which is a large time that still fits in 10 digits. 85 */ 86 const NEVER_RUN_TIME = 7952342400; 87 88 /** @var string $hour - Pattern to work out the valid hours */ 89 private $hour = '*'; 90 91 /** @var string $minute - Pattern to work out the valid minutes */ 92 private $minute = '*'; 93 94 /** @var string $day - Pattern to work out the valid days */ 95 private $day = '*'; 96 97 /** @var string $month - Pattern to work out the valid months */ 98 private $month = '*'; 99 100 /** @var string $dayofweek - Pattern to work out the valid dayofweek */ 101 private $dayofweek = '*'; 102 103 /** @var int $lastruntime - When this task was last run */ 104 private $lastruntime = 0; 105 106 /** @var boolean $customised - Has this task been changed from it's default schedule? */ 107 private $customised = false; 108 109 /** @var boolean $overridden - Does the task have values set VIA config? */ 110 private $overridden = false; 111 112 /** @var int $disabled - Is this task disabled in cron? */ 113 private $disabled = false; 114 115 /** 116 * Get the last run time for this scheduled task. 117 * 118 * @return int 119 */ 120 public function get_last_run_time() { 121 return $this->lastruntime; 122 } 123 124 /** 125 * Set the last run time for this scheduled task. 126 * 127 * @param int $lastruntime 128 */ 129 public function set_last_run_time($lastruntime) { 130 $this->lastruntime = $lastruntime; 131 } 132 133 /** 134 * Has this task been changed from it's default config? 135 * 136 * @return bool 137 */ 138 public function is_customised() { 139 return $this->customised; 140 } 141 142 /** 143 * Has this task been changed from it's default config? 144 * 145 * @param bool 146 */ 147 public function set_customised($customised) { 148 $this->customised = $customised; 149 } 150 151 /** 152 * Has this task been changed from it's default config? 153 * 154 * @return bool 155 */ 156 public function is_overridden(): bool { 157 return $this->overridden; 158 } 159 160 /** 161 * Set the overridden value. 162 * 163 * @param bool $overridden 164 */ 165 public function set_overridden(bool $overridden): void { 166 $this->overridden = $overridden; 167 } 168 169 /** 170 * Setter for $minute. Accepts a special 'R' value 171 * which will be translated to a random minute. 172 * 173 * @param string $minute 174 * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int. 175 * If false, they are left as 'R' 176 */ 177 public function set_minute($minute, $expandr = true) { 178 if ($minute === 'R' && $expandr) { 179 $minute = mt_rand(self::MINUTEMIN, self::MINUTEMAX); 180 } 181 $this->minute = $minute; 182 } 183 184 /** 185 * Getter for $minute. 186 * 187 * @return string 188 */ 189 public function get_minute() { 190 return $this->minute; 191 } 192 193 /** 194 * Setter for $hour. Accepts a special 'R' value 195 * which will be translated to a random hour. 196 * 197 * @param string $hour 198 * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int. 199 * If false, they are left as 'R' 200 */ 201 public function set_hour($hour, $expandr = true) { 202 if ($hour === 'R' && $expandr) { 203 $hour = mt_rand(self::HOURMIN, self::HOURMAX); 204 } 205 $this->hour = $hour; 206 } 207 208 /** 209 * Getter for $hour. 210 * 211 * @return string 212 */ 213 public function get_hour() { 214 return $this->hour; 215 } 216 217 /** 218 * Setter for $month. 219 * 220 * @param string $month 221 */ 222 public function set_month($month) { 223 $this->month = $month; 224 } 225 226 /** 227 * Getter for $month. 228 * 229 * @return string 230 */ 231 public function get_month() { 232 return $this->month; 233 } 234 235 /** 236 * Setter for $day. 237 * 238 * @param string $day 239 */ 240 public function set_day($day) { 241 $this->day = $day; 242 } 243 244 /** 245 * Getter for $day. 246 * 247 * @return string 248 */ 249 public function get_day() { 250 return $this->day; 251 } 252 253 /** 254 * Setter for $dayofweek. 255 * 256 * @param string $dayofweek 257 * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int. 258 * If false, they are left as 'R' 259 */ 260 public function set_day_of_week($dayofweek, $expandr = true) { 261 if ($dayofweek === 'R' && $expandr) { 262 $dayofweek = mt_rand(self::DAYOFWEEKMIN, self::DAYOFWEEKMAX); 263 } 264 $this->dayofweek = $dayofweek; 265 } 266 267 /** 268 * Getter for $dayofweek. 269 * 270 * @return string 271 */ 272 public function get_day_of_week() { 273 return $this->dayofweek; 274 } 275 276 /** 277 * Setter for $disabled. 278 * 279 * @param bool $disabled 280 */ 281 public function set_disabled($disabled) { 282 $this->disabled = (bool)$disabled; 283 } 284 285 /** 286 * Getter for $disabled. 287 * @return bool 288 */ 289 public function get_disabled() { 290 return $this->disabled; 291 } 292 293 /** 294 * Override this function if you want this scheduled task to run, even if the component is disabled. 295 * 296 * @return bool 297 */ 298 public function get_run_if_component_disabled() { 299 return false; 300 } 301 302 /** 303 * Informs whether the given field is valid. 304 * Use the constants FIELD_* to identify the field. 305 * Have to be called after the method set_{field}(string). 306 * 307 * @param string $field field identifier; expected values from constants FIELD_*. 308 * 309 * @return bool true if given field is valid. false otherwise. 310 */ 311 public function is_valid(string $field): bool { 312 return !empty($this->get_valid($field)); 313 } 314 315 /** 316 * Calculates the list of valid values according to the given field and stored expression. 317 * 318 * @param string $field field identifier. Must be one of those FIELD_*. 319 * 320 * @return array(int) list of matching values. 321 * 322 * @throws \coding_exception when passed an invalid field identifier. 323 */ 324 private function get_valid(string $field): array { 325 switch($field) { 326 case self::FIELD_MINUTE: 327 $min = self::MINUTEMIN; 328 $max = self::MINUTEMAX; 329 break; 330 case self::FIELD_HOUR: 331 $min = self::HOURMIN; 332 $max = self::HOURMAX; 333 break; 334 case self::FIELD_DAY: 335 $min = self::DAYMIN; 336 $max = self::DAYMAX; 337 break; 338 case self::FIELD_MONTH: 339 $min = self::MONTHMIN; 340 $max = self::MONTHMAX; 341 break; 342 case self::FIELD_DAYOFWEEK: 343 $min = self::DAYOFWEEKMIN; 344 $max = self::DAYOFWEEKMAXINPUT; 345 break; 346 default: 347 throw new \coding_exception("Field '$field' is not a valid crontab identifier."); 348 } 349 350 $result = $this->eval_cron_field($this->{$field}, $min, $max); 351 if ($field === self::FIELD_DAYOFWEEK) { 352 // For day of week, 0 and 7 both mean Sunday; if there is a 7 we set 0. The result array is sorted. 353 if (end($result) === 7) { 354 // Remove last element. 355 array_pop($result); 356 // Insert 0 as first element if it's not there already. 357 if (reset($result) !== 0) { 358 array_unshift($result, 0); 359 } 360 } 361 } 362 return $result; 363 } 364 365 /** 366 * Take a cron field definition and return an array of valid numbers with the range min-max. 367 * 368 * @param string $field - The field definition. 369 * @param int $min - The minimum allowable value. 370 * @param int $max - The maximum allowable value. 371 * @return array(int) 372 */ 373 public function eval_cron_field($field, $min, $max) { 374 // Cleanse the input. 375 $field = trim($field); 376 377 // Format for a field is: 378 // <fieldlist> := <range>(/<step>)(,<fieldlist>) 379 // <step> := int 380 // <range> := <any>|<int>|<min-max> 381 // <any> := * 382 // <min-max> := int-int 383 // End of format BNF. 384 385 // This function is complicated but is covered by unit tests. 386 $range = array(); 387 388 $matches = array(); 389 preg_match_all('@[0-9]+|\*|,|/|-@', $field, $matches); 390 391 $last = 0; 392 $inrange = false; 393 $instep = false; 394 foreach ($matches[0] as $match) { 395 if ($match == '*') { 396 array_push($range, range($min, $max)); 397 } else if ($match == '/') { 398 $instep = true; 399 } else if ($match == '-') { 400 $inrange = true; 401 } else if (is_numeric($match)) { 402 if ($min > $match || $match > $max) { 403 // This is a value error: The value lays out of the expected range of values. 404 return []; 405 } 406 if ($instep) { 407 // Normalise range property, account for "5/10". 408 $insteprange = $range[count($range) - 1]; 409 if (!is_array($insteprange)) { 410 $range[count($range) - 1] = range($insteprange, $max); 411 } 412 for ($i = 0; $i < count($range[count($range) - 1]); $i++) { 413 if (($i) % $match != 0) { 414 $range[count($range) - 1][$i] = -1; 415 } 416 } 417 $instep = false; 418 } else if ($inrange) { 419 if (count($range)) { 420 $range[count($range) - 1] = range($last, $match); 421 } 422 $inrange = false; 423 } else { 424 array_push($range, $match); 425 $last = $match; 426 } 427 } 428 } 429 430 // If inrange or instep were not processed, there is a syntax error. 431 // Cleanup any existing values to show up the error. 432 if ($inrange || $instep) { 433 return []; 434 } 435 436 // Flatten the result. 437 $result = array(); 438 foreach ($range as $r) { 439 if (is_array($r)) { 440 foreach ($r as $rr) { 441 if ($rr >= $min && $rr <= $max) { 442 $result[$rr] = 1; 443 } 444 } 445 } else if (is_numeric($r)) { 446 if ($r >= $min && $r <= $max) { 447 $result[$r] = 1; 448 } 449 } 450 } 451 $result = array_keys($result); 452 sort($result, SORT_NUMERIC); 453 return $result; 454 } 455 456 /** 457 * Assuming $list is an ordered list of items, this function returns the item 458 * in the list that is greater than or equal to the current value (or 0). If 459 * no value is greater than or equal, this will return the first valid item in the list. 460 * If list is empty, this function will return 0. 461 * 462 * @param int $current The current value 463 * @param int[] $list The list of valid items. 464 * @return int $next. 465 */ 466 private function next_in_list($current, $list) { 467 foreach ($list as $l) { 468 if ($l >= $current) { 469 return $l; 470 } 471 } 472 if (count($list)) { 473 return $list[0]; 474 } 475 476 return 0; 477 } 478 479 /** 480 * Calculate when this task should next be run based on the schedule. 481 * 482 * @param int $now Current time, for testing (leave 0 to use default time) 483 * @return int $nextruntime. 484 */ 485 public function get_next_scheduled_time(int $now = 0): int { 486 if (!$now) { 487 $now = time(); 488 } 489 490 // We need to change to the server timezone before using php date() functions. 491 \core_date::set_default_server_timezone(); 492 493 $validminutes = $this->get_valid(self::FIELD_MINUTE); 494 $validhours = $this->get_valid(self::FIELD_HOUR); 495 $validdays = $this->get_valid(self::FIELD_DAY); 496 $validdaysofweek = $this->get_valid(self::FIELD_DAYOFWEEK); 497 $validmonths = $this->get_valid(self::FIELD_MONTH); 498 499 // If any of the fields contain no valid data then the task will never run. 500 if (!$validminutes || !$validhours || !$validdays || !$validdaysofweek || !$validmonths) { 501 return self::NEVER_RUN_TIME; 502 } 503 504 $result = self::get_next_scheduled_time_inner($now, $validminutes, $validhours, $validdays, $validdaysofweek, $validmonths); 505 return $result; 506 } 507 508 /** 509 * Recursively calculate the next valid time for this task. 510 * 511 * @param int $now Start time 512 * @param array $validminutes Valid minutes 513 * @param array $validhours Valid hours 514 * @param array $validdays Valid days 515 * @param array $validdaysofweek Valid days of week 516 * @param array $validmonths Valid months 517 * @param int $originalyear Zero for first call, original year for recursive calls 518 * @return int Next run time 519 */ 520 protected function get_next_scheduled_time_inner(int $now, array $validminutes, array $validhours, 521 array $validdays, array $validdaysofweek, array $validmonths, int $originalyear = 0) { 522 $currentyear = (int)date('Y', $now); 523 if ($originalyear) { 524 // In recursive calls, check we didn't go more than 8 years ahead, that indicates the 525 // user has chosen an impossible date. 8 years is the maximum time, considering a task 526 // set to run on 29 February over a century boundary when a leap year is skipped. 527 if ($currentyear - $originalyear > 8) { 528 // Use this time if it's never going to happen. 529 return self::NEVER_RUN_TIME; 530 } 531 $firstyear = $originalyear; 532 } else { 533 $firstyear = $currentyear; 534 } 535 $currentmonth = (int)date('n', $now); 536 537 // Evaluate month first. 538 $nextvalidmonth = $this->next_in_list($currentmonth, $validmonths); 539 if ($nextvalidmonth < $currentmonth) { 540 $currentyear += 1; 541 } 542 // If we moved to another month, set the current time to start of month, and restart calculations. 543 if ($nextvalidmonth !== $currentmonth) { 544 $newtime = strtotime($currentyear . '-' . $nextvalidmonth . '-01 00:00'); 545 return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays, 546 $validdaysofweek, $validmonths, $firstyear); 547 } 548 549 // Special handling for dayofmonth vs dayofweek (see man 5 cron). If both are specified, then 550 // it is ok to continue when either matches. If only one is specified then it must match. 551 $currentday = (int)date("j", $now); 552 $currentdayofweek = (int)date("w", $now); 553 $nextvaliddayofmonth = self::next_in_list($currentday, $validdays); 554 $nextvaliddayofweek = self::next_in_list($currentdayofweek, $validdaysofweek); 555 $daysincrementbymonth = $nextvaliddayofmonth - $currentday; 556 $daysinmonth = (int)date('t', $now); 557 if ($nextvaliddayofmonth < $currentday) { 558 $daysincrementbymonth += $daysinmonth; 559 } 560 561 $daysincrementbyweek = $nextvaliddayofweek - $currentdayofweek; 562 if ($nextvaliddayofweek < $currentdayofweek) { 563 $daysincrementbyweek += 7; 564 } 565 566 if ($this->dayofweek == '*') { 567 $daysincrement = $daysincrementbymonth; 568 } else if ($this->day == '*') { 569 $daysincrement = $daysincrementbyweek; 570 } else { 571 // Take the smaller increment of days by month or week. 572 $daysincrement = min($daysincrementbymonth, $daysincrementbyweek); 573 } 574 575 // If we moved day, recurse using new start time. 576 if ($daysincrement != 0) { 577 $newtime = strtotime($currentyear . '-' . $currentmonth . '-' . $currentday . 578 ' 00:00 +' . $daysincrement . ' days'); 579 return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays, 580 $validdaysofweek, $validmonths, $firstyear); 581 } 582 583 $currenthour = (int)date('H', $now); 584 $nextvalidhour = $this->next_in_list($currenthour, $validhours); 585 if ($nextvalidhour != $currenthour) { 586 if ($nextvalidhour < $currenthour) { 587 $offset = ' +1 day'; 588 } else { 589 $offset = ''; 590 } 591 $newtime = strtotime($currentyear . '-' . $currentmonth . '-' . $currentday . ' ' . $nextvalidhour . 592 ':00' . $offset); 593 return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays, 594 $validdaysofweek, $validmonths, $firstyear); 595 } 596 597 // Round time down to an exact minute because we need to use numeric calculations on it now. 598 // If we construct times based on all the components, it will mess up around DST changes 599 // (because there are two times with the same representation). 600 $now = intdiv($now, 60) * 60; 601 602 $currentminute = (int)date('i', $now); 603 $nextvalidminute = $this->next_in_list($currentminute, $validminutes); 604 if ($nextvalidminute == $currentminute && !$originalyear) { 605 // This is not a recursive call so time has not moved on at all yet. We can't use the 606 // same minute as now because it has already happened, it has to be at least one minute 607 // later, so update time and retry. 608 $newtime = $now + 60; 609 return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays, 610 $validdaysofweek, $validmonths, $firstyear); 611 } 612 613 if ($nextvalidminute < $currentminute) { 614 // The time is in the next hour so we need to recurse. Don't use strtotime at this 615 // point because it will mess up around DST changes. 616 $minutesforward = $nextvalidminute + 60 - $currentminute; 617 $newtime = $now + $minutesforward * 60; 618 return $this->get_next_scheduled_time_inner($newtime, $validminutes, $validhours, $validdays, 619 $validdaysofweek, $validmonths, $firstyear); 620 } 621 622 // The next valid minute is in the same hour so it must be valid according to all other 623 // checks and we can finally return it. 624 return $now + ($nextvalidminute - $currentminute) * 60; 625 } 626 627 /** 628 * Informs whether this task can be run. 629 * 630 * @return bool true when this task can be run. false otherwise. 631 */ 632 public function can_run(): bool { 633 return $this->is_component_enabled() || $this->get_run_if_component_disabled(); 634 } 635 636 /** 637 * Checks whether the component and the task disabled flag enables to run this task. 638 * This do not checks whether the task manager allows running them or if the 639 * site allows tasks to "run now". 640 * 641 * @return bool true if task is enabled. false otherwise. 642 */ 643 public function is_enabled(): bool { 644 return $this->can_run() && !$this->get_disabled(); 645 } 646 647 /** 648 * Produces a valid id string to use as id attribute based on the given FQCN class name. 649 * 650 * @param string $classname FQCN of a task. 651 * @return string valid string to be used as id attribute. 652 */ 653 public static function get_html_id(string $classname): string { 654 return str_replace('\\', '-', ltrim($classname, '\\')); 655 } 656 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body