Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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