Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.0.x will end 8 May 2023 (12 months).
  • Bug fixes for security issues in 4.0.x will end 13 November 2023 (18 months).
  • PHP version: minimum PHP 7.3.0 Note: the minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is also supported.

Differences Between: [Versions 310 and 400] [Versions 311 and 400] [Versions 39 and 400] [Versions 400 and 401] [Versions 400 and 402] [Versions 400 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  
  59      /**
  60       * Minute field identifier.
  61       */
  62      const FIELD_MINUTE = 'minute';
  63      /**
  64       * Hour field identifier.
  65       */
  66      const FIELD_HOUR = 'hour';
  67      /**
  68       * Day-of-month field identifier.
  69       */
  70      const FIELD_DAY = 'day';
  71      /**
  72       * Month field identifier.
  73       */
  74      const FIELD_MONTH = 'month';
  75      /**
  76       * Day-of-week field identifier.
  77       */
  78      const FIELD_DAYOFWEEK = 'dayofweek';
  79  
  80      /** @var string $hour - Pattern to work out the valid hours */
  81      private $hour = '*';
  82  
  83      /** @var string $minute - Pattern to work out the valid minutes */
  84      private $minute = '*';
  85  
  86      /** @var string $day - Pattern to work out the valid days */
  87      private $day = '*';
  88  
  89      /** @var string $month - Pattern to work out the valid months */
  90      private $month = '*';
  91  
  92      /** @var string $dayofweek - Pattern to work out the valid dayofweek */
  93      private $dayofweek = '*';
  94  
  95      /** @var int $lastruntime - When this task was last run */
  96      private $lastruntime = 0;
  97  
  98      /** @var boolean $customised - Has this task been changed from it's default schedule? */
  99      private $customised = false;
 100  
 101      /** @var boolean $overridden - Does the task have values set VIA config? */
 102      private $overridden = false;
 103  
 104      /** @var int $disabled - Is this task disabled in cron? */
 105      private $disabled = false;
 106  
 107      /**
 108       * Get the last run time for this scheduled task.
 109       *
 110       * @return int
 111       */
 112      public function get_last_run_time() {
 113          return $this->lastruntime;
 114      }
 115  
 116      /**
 117       * Set the last run time for this scheduled task.
 118       *
 119       * @param int $lastruntime
 120       */
 121      public function set_last_run_time($lastruntime) {
 122          $this->lastruntime = $lastruntime;
 123      }
 124  
 125      /**
 126       * Has this task been changed from it's default config?
 127       *
 128       * @return bool
 129       */
 130      public function is_customised() {
 131          return $this->customised;
 132      }
 133  
 134      /**
 135       * Has this task been changed from it's default config?
 136       *
 137       * @param bool
 138       */
 139      public function set_customised($customised) {
 140          $this->customised = $customised;
 141      }
 142  
 143      /**
 144       * Has this task been changed from it's default config?
 145       *
 146       * @return bool
 147       */
 148      public function is_overridden(): bool {
 149          return $this->overridden;
 150      }
 151  
 152      /**
 153       * Set the overridden value.
 154       *
 155       * @param bool $overridden
 156       */
 157      public function set_overridden(bool $overridden): void {
 158          $this->overridden = $overridden;
 159      }
 160  
 161      /**
 162       * Setter for $minute. Accepts a special 'R' value
 163       * which will be translated to a random minute.
 164       *
 165       * @param string $minute
 166       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 167       *      If false, they are left as 'R'
 168       */
 169      public function set_minute($minute, $expandr = true) {
 170          if ($minute === 'R' && $expandr) {
 171              $minute = mt_rand(self::MINUTEMIN, self::MINUTEMAX);
 172          }
 173          $this->minute = $minute;
 174      }
 175  
 176      /**
 177       * Getter for $minute.
 178       *
 179       * @return string
 180       */
 181      public function get_minute() {
 182          return $this->minute;
 183      }
 184  
 185      /**
 186       * Setter for $hour. Accepts a special 'R' value
 187       * which will be translated to a random hour.
 188       *
 189       * @param string $hour
 190       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 191       *      If false, they are left as 'R'
 192       */
 193      public function set_hour($hour, $expandr = true) {
 194          if ($hour === 'R' && $expandr) {
 195              $hour = mt_rand(self::HOURMIN, self::HOURMAX);
 196          }
 197          $this->hour = $hour;
 198      }
 199  
 200      /**
 201       * Getter for $hour.
 202       *
 203       * @return string
 204       */
 205      public function get_hour() {
 206          return $this->hour;
 207      }
 208  
 209      /**
 210       * Setter for $month.
 211       *
 212       * @param string $month
 213       */
 214      public function set_month($month) {
 215          $this->month = $month;
 216      }
 217  
 218      /**
 219       * Getter for $month.
 220       *
 221       * @return string
 222       */
 223      public function get_month() {
 224          return $this->month;
 225      }
 226  
 227      /**
 228       * Setter for $day.
 229       *
 230       * @param string $day
 231       */
 232      public function set_day($day) {
 233          $this->day = $day;
 234      }
 235  
 236      /**
 237       * Getter for $day.
 238       *
 239       * @return string
 240       */
 241      public function get_day() {
 242          return $this->day;
 243      }
 244  
 245      /**
 246       * Setter for $dayofweek.
 247       *
 248       * @param string $dayofweek
 249       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 250       *      If false, they are left as 'R'
 251       */
 252      public function set_day_of_week($dayofweek, $expandr = true) {
 253          if ($dayofweek === 'R' && $expandr) {
 254              $dayofweek = mt_rand(self::DAYOFWEEKMIN, self::DAYOFWEEKMAX);
 255          }
 256          $this->dayofweek = $dayofweek;
 257      }
 258  
 259      /**
 260       * Getter for $dayofweek.
 261       *
 262       * @return string
 263       */
 264      public function get_day_of_week() {
 265          return $this->dayofweek;
 266      }
 267  
 268      /**
 269       * Setter for $disabled.
 270       *
 271       * @param bool $disabled
 272       */
 273      public function set_disabled($disabled) {
 274          $this->disabled = (bool)$disabled;
 275      }
 276  
 277      /**
 278       * Getter for $disabled.
 279       * @return bool
 280       */
 281      public function get_disabled() {
 282          return $this->disabled;
 283      }
 284  
 285      /**
 286       * Override this function if you want this scheduled task to run, even if the component is disabled.
 287       *
 288       * @return bool
 289       */
 290      public function get_run_if_component_disabled() {
 291          return false;
 292      }
 293  
 294      /**
 295       * Informs whether the given field is valid.
 296       * Use the constants FIELD_* to identify the field.
 297       * Have to be called after the method set_{field}(string).
 298       *
 299       * @param string $field field identifier; expected values from constants FIELD_*.
 300       *
 301       * @return bool true if given field is valid. false otherwise.
 302       */
 303      public function is_valid(string $field): bool {
 304          return !empty($this->get_valid($field));
 305      }
 306  
 307      /**
 308       * Calculates the list of valid values according to the given field and stored expression.
 309       *
 310       * @param string $field field identifier. Must be one of those FIELD_*.
 311       *
 312       * @return array(int) list of matching values.
 313       *
 314       * @throws \coding_exception when passed an invalid field identifier.
 315       */
 316      private function get_valid(string $field): array {
 317          switch($field) {
 318              case self::FIELD_MINUTE:
 319                  $min = self::MINUTEMIN;
 320                  $max = self::MINUTEMAX;
 321                  break;
 322              case self::FIELD_HOUR:
 323                  $min = self::HOURMIN;
 324                  $max = self::HOURMAX;
 325                  break;
 326              case self::FIELD_DAY:
 327                  $min = self::DAYMIN;
 328                  $max = self::DAYMAX;
 329                  break;
 330              case self::FIELD_MONTH:
 331                  $min = self::MONTHMIN;
 332                  $max = self::MONTHMAX;
 333                  break;
 334              case self::FIELD_DAYOFWEEK:
 335                  $min = self::DAYOFWEEKMIN;
 336                  $max = self::DAYOFWEEKMAX;
 337                  break;
 338              default:
 339                  throw new \coding_exception("Field '$field' is not a valid crontab identifier.");
 340          }
 341  
 342          return $this->eval_cron_field($this->{$field}, $min, $max);
 343      }
 344  
 345      /**
 346       * Take a cron field definition and return an array of valid numbers with the range min-max.
 347       *
 348       * @param string $field - The field definition.
 349       * @param int $min - The minimum allowable value.
 350       * @param int $max - The maximum allowable value.
 351       * @return array(int)
 352       */
 353      public function eval_cron_field($field, $min, $max) {
 354          // Cleanse the input.
 355          $field = trim($field);
 356  
 357          // Format for a field is:
 358          // <fieldlist> := <range>(/<step>)(,<fieldlist>)
 359          // <step>  := int
 360          // <range> := <any>|<int>|<min-max>
 361          // <any>   := *
 362          // <min-max> := int-int
 363          // End of format BNF.
 364  
 365          // This function is complicated but is covered by unit tests.
 366          $range = array();
 367  
 368          $matches = array();
 369          preg_match_all('@[0-9]+|\*|,|/|-@', $field, $matches);
 370  
 371          $last = 0;
 372          $inrange = false;
 373          $instep = false;
 374          foreach ($matches[0] as $match) {
 375              if ($match == '*') {
 376                  array_push($range, range($min, $max));
 377              } else if ($match == '/') {
 378                  $instep = true;
 379              } else if ($match == '-') {
 380                  $inrange = true;
 381              } else if (is_numeric($match)) {
 382                  if ($min > $match || $match > $max) {
 383                      // This is a value error: The value lays out of the expected range of values.
 384                      return [];
 385                  }
 386                  if ($instep) {
 387                      for ($i = 0; $i < count($range[count($range) - 1]); $i++) {
 388                          if (($i) % $match != 0) {
 389                              $range[count($range) - 1][$i] = -1;
 390                          }
 391                      }
 392                      $instep = false;
 393                  } else if ($inrange) {
 394                      if (count($range)) {
 395                          $range[count($range) - 1] = range($last, $match);
 396                      }
 397                      $inrange = false;
 398                  } else {
 399                      array_push($range, $match);
 400                      $last = $match;
 401                  }
 402              }
 403          }
 404  
 405          // If inrange or instep were not processed, there is a syntax error.
 406          // Cleanup any existing values to show up the error.
 407          if ($inrange || $instep) {
 408              return [];
 409          }
 410  
 411          // Flatten the result.
 412          $result = array();
 413          foreach ($range as $r) {
 414              if (is_array($r)) {
 415                  foreach ($r as $rr) {
 416                      if ($rr >= $min && $rr <= $max) {
 417                          $result[$rr] = 1;
 418                      }
 419                  }
 420              } else if (is_numeric($r)) {
 421                  if ($r >= $min && $r <= $max) {
 422                      $result[$r] = 1;
 423                  }
 424              }
 425          }
 426          $result = array_keys($result);
 427          sort($result, SORT_NUMERIC);
 428          return $result;
 429      }
 430  
 431      /**
 432       * Assuming $list is an ordered list of items, this function returns the item
 433       * in the list that is greater than or equal to the current value (or 0). If
 434       * no value is greater than or equal, this will return the first valid item in the list.
 435       * If list is empty, this function will return 0.
 436       *
 437       * @param int $current The current value
 438       * @param int[] $list The list of valid items.
 439       * @return int $next.
 440       */
 441      private function next_in_list($current, $list) {
 442          foreach ($list as $l) {
 443              if ($l >= $current) {
 444                  return $l;
 445              }
 446          }
 447          if (count($list)) {
 448              return $list[0];
 449          }
 450  
 451          return 0;
 452      }
 453  
 454      /**
 455       * Calculate when this task should next be run based on the schedule.
 456       *
 457       * @return int $nextruntime.
 458       */
 459      public function get_next_scheduled_time() {
 460          // We need to change to the server timezone before using php date() functions.
 461          \core_date::set_default_server_timezone();
 462  
 463          $validminutes = $this->get_valid(self::FIELD_MINUTE);
 464          $validhours = $this->get_valid(self::FIELD_HOUR);
 465          $validdays = $this->get_valid(self::FIELD_DAY);
 466          $validdaysofweek = $this->get_valid(self::FIELD_DAYOFWEEK);
 467          $validmonths = $this->get_valid(self::FIELD_MONTH);
 468  
 469          $nextvalidyear = date('Y');
 470  
 471          $currentminute = date("i") + 1;
 472          $currenthour = date("H");
 473          $currentday = date("j");
 474          $currentmonth = date("n");
 475          $currentdayofweek = date("w");
 476  
 477          $nextvalidminute = $this->next_in_list($currentminute, $validminutes);
 478          if ($nextvalidminute < $currentminute) {
 479              $currenthour += 1;
 480          }
 481          $nextvalidhour = $this->next_in_list($currenthour, $validhours);
 482          if ($nextvalidhour < $currenthour) {
 483              $currentdayofweek += 1;
 484              $currentday += 1;
 485          }
 486          $nextvaliddayofmonth = $this->next_in_list($currentday, $validdays);
 487          $nextvaliddayofweek = $this->next_in_list($currentdayofweek, $validdaysofweek);
 488          $daysincrementbymonth = $nextvaliddayofmonth - $currentday;
 489          $daysinmonth = date('t');
 490          if ($nextvaliddayofmonth < $currentday) {
 491              $daysincrementbymonth += $daysinmonth;
 492          }
 493  
 494          $daysincrementbyweek = $nextvaliddayofweek - $currentdayofweek;
 495          if ($nextvaliddayofweek < $currentdayofweek) {
 496              $daysincrementbyweek += 7;
 497          }
 498  
 499          // Special handling for dayofmonth vs dayofweek:
 500          // if either field is * - use the other field
 501          // otherwise - choose the soonest (see man 5 cron).
 502          if ($this->dayofweek == '*') {
 503              $daysincrement = $daysincrementbymonth;
 504          } else if ($this->day == '*') {
 505              $daysincrement = $daysincrementbyweek;
 506          } else {
 507              // Take the smaller increment of days by month or week.
 508              $daysincrement = $daysincrementbymonth;
 509              if ($daysincrementbyweek < $daysincrementbymonth) {
 510                  $daysincrement = $daysincrementbyweek;
 511              }
 512          }
 513  
 514          $nextvaliddayofmonth = $currentday + $daysincrement;
 515          if ($nextvaliddayofmonth > $daysinmonth) {
 516              $currentmonth += 1;
 517              $nextvaliddayofmonth -= $daysinmonth;
 518          }
 519  
 520          $nextvalidmonth = $this->next_in_list($currentmonth, $validmonths);
 521          if ($nextvalidmonth < $currentmonth) {
 522              $nextvalidyear += 1;
 523          }
 524  
 525          // Work out the next valid time.
 526          $nexttime = mktime($nextvalidhour,
 527                             $nextvalidminute,
 528                             0,
 529                             $nextvalidmonth,
 530                             $nextvaliddayofmonth,
 531                             $nextvalidyear);
 532  
 533          return $nexttime;
 534      }
 535  
 536      /**
 537       * Informs whether this task can be run.
 538       *
 539       * @return bool true when this task can be run. false otherwise.
 540       */
 541      public function can_run(): bool {
 542          return $this->is_component_enabled() || $this->get_run_if_component_disabled();
 543      }
 544  
 545      /**
 546       * Checks whether the component and the task disabled flag enables to run this task.
 547       * This do not checks whether the task manager allows running them or if the
 548       * site allows tasks to "run now".
 549       *
 550       * @return bool true if task is enabled. false otherwise.
 551       */
 552      public function is_enabled(): bool {
 553          return $this->can_run() && !$this->get_disabled();
 554      }
 555  
 556      /**
 557       * Produces a valid id string to use as id attribute based on the given FQCN class name.
 558       *
 559       * @param string $classname FQCN of a task.
 560       * @return string valid string to be used as id attribute.
 561       */
 562      public static function get_html_id(string $classname): string {
 563          return str_replace('\\', '-', ltrim($classname, '\\'));
 564      }
 565  
 566      /**
 567       * Get a descriptive name for this task (shown to admins).
 568       *
 569       * @return string
 570       */
 571      abstract public function get_name();
 572  
 573  }