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