Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 310 and 311] [Versions 311 and 400] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

   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 dayofweek value. */
  45      const DAYOFWEEKMIN = 0;
  46      /** Maximum dayofweek value. */
  47      const DAYOFWEEKMAX = 6;
  48  
  49      /** @var string $hour - Pattern to work out the valid hours */
  50      private $hour = '*';
  51  
  52      /** @var string $minute - Pattern to work out the valid minutes */
  53      private $minute = '*';
  54  
  55      /** @var string $day - Pattern to work out the valid days */
  56      private $day = '*';
  57  
  58      /** @var string $month - Pattern to work out the valid months */
  59      private $month = '*';
  60  
  61      /** @var string $dayofweek - Pattern to work out the valid dayofweek */
  62      private $dayofweek = '*';
  63  
  64      /** @var int $lastruntime - When this task was last run */
  65      private $lastruntime = 0;
  66  
  67      /** @var boolean $customised - Has this task been changed from it's default schedule? */
  68      private $customised = false;
  69  
  70      /** @var boolean $overridden - Does the task have values set VIA config? */
  71      private $overridden = false;
  72  
  73      /** @var int $disabled - Is this task disabled in cron? */
  74      private $disabled = false;
  75  
  76      /**
  77       * Get the last run time for this scheduled task.
  78       * @return int
  79       */
  80      public function get_last_run_time() {
  81          return $this->lastruntime;
  82      }
  83  
  84      /**
  85       * Set the last run time for this scheduled task.
  86       * @param int $lastruntime
  87       */
  88      public function set_last_run_time($lastruntime) {
  89          $this->lastruntime = $lastruntime;
  90      }
  91  
  92      /**
  93       * Has this task been changed from it's default config?
  94       * @return bool
  95       */
  96      public function is_customised() {
  97          return $this->customised;
  98      }
  99  
 100      /**
 101       * Has this task been changed from it's default config?
 102       * @param bool
 103       */
 104      public function set_customised($customised) {
 105          $this->customised = $customised;
 106      }
 107  
 108      /**
 109       * Has this task been changed from it's default config?
 110       * @return bool
 111       */
 112      public function is_overridden(): bool {
 113          return $this->overridden;
 114      }
 115  
 116      /**
 117       * Set the overridden value.
 118       * @param bool $overridden
 119       */
 120      public function set_overridden(bool $overridden): void {
 121          $this->overridden = $overridden;
 122      }
 123  
 124      /**
 125       * Setter for $minute. Accepts a special 'R' value
 126       * which will be translated to a random minute.
 127       * @param string $minute
 128       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 129       *      If false, they are left as 'R'
 130       */
 131      public function set_minute($minute, $expandr = true) {
 132          if ($minute === 'R' && $expandr) {
 133              $minute = mt_rand(self::MINUTEMIN, self::MINUTEMAX);
 134          }
 135          $this->minute = $minute;
 136      }
 137  
 138      /**
 139       * Getter for $minute.
 140       * @return string
 141       */
 142      public function get_minute() {
 143          return $this->minute;
 144      }
 145  
 146      /**
 147       * Setter for $hour. Accepts a special 'R' value
 148       * which will be translated to a random hour.
 149       * @param string $hour
 150       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 151       *      If false, they are left as 'R'
 152       */
 153      public function set_hour($hour, $expandr = true) {
 154          if ($hour === 'R' && $expandr) {
 155              $hour = mt_rand(self::HOURMIN, self::HOURMAX);
 156          }
 157          $this->hour = $hour;
 158      }
 159  
 160      /**
 161       * Getter for $hour.
 162       * @return string
 163       */
 164      public function get_hour() {
 165          return $this->hour;
 166      }
 167  
 168      /**
 169       * Setter for $month.
 170       * @param string $month
 171       */
 172      public function set_month($month) {
 173          $this->month = $month;
 174      }
 175  
 176      /**
 177       * Getter for $month.
 178       * @return string
 179       */
 180      public function get_month() {
 181          return $this->month;
 182      }
 183  
 184      /**
 185       * Setter for $day.
 186       * @param string $day
 187       */
 188      public function set_day($day) {
 189          $this->day = $day;
 190      }
 191  
 192      /**
 193       * Getter for $day.
 194       * @return string
 195       */
 196      public function get_day() {
 197          return $this->day;
 198      }
 199  
 200      /**
 201       * Setter for $dayofweek.
 202       * @param string $dayofweek
 203       * @param bool $expandr - if true (default) an 'R' value in a time is expanded to an appropriate int.
 204       *      If false, they are left as 'R'
 205       */
 206      public function set_day_of_week($dayofweek, $expandr = true) {
 207          if ($dayofweek === 'R' && $expandr) {
 208              $dayofweek = mt_rand(self::DAYOFWEEKMIN, self::DAYOFWEEKMAX);
 209          }
 210          $this->dayofweek = $dayofweek;
 211      }
 212  
 213      /**
 214       * Getter for $dayofweek.
 215       * @return string
 216       */
 217      public function get_day_of_week() {
 218          return $this->dayofweek;
 219      }
 220  
 221      /**
 222       * Setter for $disabled.
 223       * @param bool $disabled
 224       */
 225      public function set_disabled($disabled) {
 226          $this->disabled = (bool)$disabled;
 227      }
 228  
 229      /**
 230       * Getter for $disabled.
 231       * @return bool
 232       */
 233      public function get_disabled() {
 234          return $this->disabled;
 235      }
 236  
 237      /**
 238       * Override this function if you want this scheduled task to run, even if the component is disabled.
 239       *
 240       * @return bool
 241       */
 242      public function get_run_if_component_disabled() {
 243          return false;
 244      }
 245  
 246      /**
 247       * Take a cron field definition and return an array of valid numbers with the range min-max.
 248       *
 249       * @param string $field - The field definition.
 250       * @param int $min - The minimum allowable value.
 251       * @param int $max - The maximum allowable value.
 252       * @return array(int)
 253       */
 254      public function eval_cron_field($field, $min, $max) {
 255          // Cleanse the input.
 256          $field = trim($field);
 257  
 258          // Format for a field is:
 259          // <fieldlist> := <range>(/<step>)(,<fieldlist>)
 260          // <step>  := int
 261          // <range> := <any>|<int>|<min-max>
 262          // <any>   := *
 263          // <min-max> := int-int
 264          // End of format BNF.
 265  
 266          // This function is complicated but is covered by unit tests.
 267          $range = array();
 268  
 269          $matches = array();
 270          preg_match_all('@[0-9]+|\*|,|/|-@', $field, $matches);
 271  
 272          $last = 0;
 273          $inrange = false;
 274          $instep = false;
 275  
 276          foreach ($matches[0] as $match) {
 277              if ($match == '*') {
 278                  array_push($range, range($min, $max));
 279              } else if ($match == '/') {
 280                  $instep = true;
 281              } else if ($match == '-') {
 282                  $inrange = true;
 283              } else if (is_numeric($match)) {
 284                  if ($instep) {
 285                      $i = 0;
 286                      for ($i = 0; $i < count($range[count($range) - 1]); $i++) {
 287                          if (($i) % $match != 0) {
 288                              $range[count($range) - 1][$i] = -1;
 289                          }
 290                      }
 291                      $inrange = false;
 292                  } else if ($inrange) {
 293                      if (count($range)) {
 294                          $range[count($range) - 1] = range($last, $match);
 295                      }
 296                      $inrange = false;
 297                  } else {
 298                      if ($match >= $min && $match <= $max) {
 299                          array_push($range, $match);
 300                      }
 301                      $last = $match;
 302                  }
 303              }
 304          }
 305  
 306          // Flatten the result.
 307          $result = array();
 308          foreach ($range as $r) {
 309              if (is_array($r)) {
 310                  foreach ($r as $rr) {
 311                      if ($rr >= $min && $rr <= $max) {
 312                          $result[$rr] = 1;
 313                      }
 314                  }
 315              } else if (is_numeric($r)) {
 316                  if ($r >= $min && $r <= $max) {
 317                      $result[$r] = 1;
 318                  }
 319              }
 320          }
 321          $result = array_keys($result);
 322          sort($result, SORT_NUMERIC);
 323          return $result;
 324      }
 325  
 326      /**
 327       * Assuming $list is an ordered list of items, this function returns the item
 328       * in the list that is greater than or equal to the current value (or 0). If
 329       * no value is greater than or equal, this will return the first valid item in the list.
 330       * If list is empty, this function will return 0.
 331       *
 332       * @param int $current The current value
 333       * @param int[] $list The list of valid items.
 334       * @return int $next.
 335       */
 336      private function next_in_list($current, $list) {
 337          foreach ($list as $l) {
 338              if ($l >= $current) {
 339                  return $l;
 340              }
 341          }
 342          if (count($list)) {
 343              return $list[0];
 344          }
 345  
 346          return 0;
 347      }
 348  
 349      /**
 350       * Calculate when this task should next be run based on the schedule.
 351       * @return int $nextruntime.
 352       */
 353      public function get_next_scheduled_time() {
 354          global $CFG;
 355  
 356          $validminutes = $this->eval_cron_field($this->minute, self::MINUTEMIN, self::MINUTEMAX);
 357          $validhours = $this->eval_cron_field($this->hour, self::HOURMIN, self::HOURMAX);
 358  
 359          // We need to change to the server timezone before using php date() functions.
 360          \core_date::set_default_server_timezone();
 361  
 362          $daysinmonth = date("t");
 363          $validdays = $this->eval_cron_field($this->day, 1, $daysinmonth);
 364          $validdaysofweek = $this->eval_cron_field($this->dayofweek, 0, 7);
 365          $validmonths = $this->eval_cron_field($this->month, 1, 12);
 366          $nextvalidyear = date('Y');
 367  
 368          $currentminute = date("i") + 1;
 369          $currenthour = date("H");
 370          $currentday = date("j");
 371          $currentmonth = date("n");
 372          $currentdayofweek = date("w");
 373  
 374          $nextvalidminute = $this->next_in_list($currentminute, $validminutes);
 375          if ($nextvalidminute < $currentminute) {
 376              $currenthour += 1;
 377          }
 378          $nextvalidhour = $this->next_in_list($currenthour, $validhours);
 379          if ($nextvalidhour < $currenthour) {
 380              $currentdayofweek += 1;
 381              $currentday += 1;
 382          }
 383          $nextvaliddayofmonth = $this->next_in_list($currentday, $validdays);
 384          $nextvaliddayofweek = $this->next_in_list($currentdayofweek, $validdaysofweek);
 385          $daysincrementbymonth = $nextvaliddayofmonth - $currentday;
 386          if ($nextvaliddayofmonth < $currentday) {
 387              $daysincrementbymonth += $daysinmonth;
 388          }
 389  
 390          $daysincrementbyweek = $nextvaliddayofweek - $currentdayofweek;
 391          if ($nextvaliddayofweek < $currentdayofweek) {
 392              $daysincrementbyweek += 7;
 393          }
 394  
 395          // Special handling for dayofmonth vs dayofweek:
 396          // if either field is * - use the other field
 397          // otherwise - choose the soonest (see man 5 cron).
 398          if ($this->dayofweek == '*') {
 399              $daysincrement = $daysincrementbymonth;
 400          } else if ($this->day == '*') {
 401              $daysincrement = $daysincrementbyweek;
 402          } else {
 403              // Take the smaller increment of days by month or week.
 404              $daysincrement = $daysincrementbymonth;
 405              if ($daysincrementbyweek < $daysincrementbymonth) {
 406                  $daysincrement = $daysincrementbyweek;
 407              }
 408          }
 409  
 410          $nextvaliddayofmonth = $currentday + $daysincrement;
 411          if ($nextvaliddayofmonth > $daysinmonth) {
 412              $currentmonth += 1;
 413              $nextvaliddayofmonth -= $daysinmonth;
 414          }
 415  
 416          $nextvalidmonth = $this->next_in_list($currentmonth, $validmonths);
 417          if ($nextvalidmonth < $currentmonth) {
 418              $nextvalidyear += 1;
 419          }
 420  
 421          // Work out the next valid time.
 422          $nexttime = mktime($nextvalidhour,
 423                             $nextvalidminute,
 424                             0,
 425                             $nextvalidmonth,
 426                             $nextvaliddayofmonth,
 427                             $nextvalidyear);
 428  
 429          return $nexttime;
 430      }
 431  
 432      /**
 433       * Get a descriptive name for this task (shown to admins).
 434       *
 435       * @return string
 436       */
 437      public abstract function get_name();
 438  
 439  }