Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.
   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  namespace core;
  18  
  19  use coding_exception;
  20  use core_php_time_limit;
  21  use moodle_exception;
  22  use stdClass;
  23  
  24  // Disable the moodle.PHP.ForbiddenFunctions.FoundWithAlternative sniff for this file.
  25  // It detects uses of error_log() which are valid in this file.
  26  // phpcs:disable moodle.PHP.ForbiddenFunctions.FoundWithAlternative
  27  
  28  /**
  29   * Cron and adhoc task functionality.
  30   *
  31   * @package    core
  32   * @copyright  2023 Andrew Lyons <andrew@nicols.co.uk>
  33   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  34   */
  35  class cron {
  36  
  37      /** @var ?stdClass A copy of the standard cron 'user' */
  38      protected static ?stdClass $cronuser = null;
  39  
  40      /** @var ?stdClass The cron user's session data */
  41      protected static ?stdClass $cronsession = null;
  42  
  43      /**
  44       * Use a default value of 3 minutes.
  45       * The recommended cron frequency is every minute, and the default adhoc concurrency is 3.
  46       * A default value of 3 minutes allows all adhoc tasks to be run concurrently at their default value.
  47       *
  48       * @var int The default keepalive value for the main cron runner
  49       */
  50      public const DEFAULT_MAIN_PROCESS_KEEPALIVE = 3 * MINSECS;
  51  
  52      /**
  53       * @var int The max keepalive value for the main cron runner
  54       */
  55      public const MAX_MAIN_PROCESS_KEEPALIVE = 15 * MINSECS;
  56  
  57      /**
  58       * Execute cron tasks
  59       *
  60       * @param int|null $keepalive The keepalive time for this cron run.
  61       */
  62      public static function run_main_process(?int $keepalive = null): void {
  63          global $CFG, $DB;
  64  
  65          if (CLI_MAINTENANCE) {
  66              echo "CLI maintenance mode active, cron execution suspended.\n";
  67              exit(1);
  68          }
  69  
  70          if (moodle_needs_upgrading()) {
  71              echo "Moodle upgrade pending, cron execution suspended.\n";
  72              exit(1);
  73          }
  74  
  75          require_once($CFG->libdir . '/adminlib.php');
  76  
  77          if (!empty($CFG->showcronsql)) {
  78              $DB->set_debug(true);
  79          }
  80          if (!empty($CFG->showcrondebugging)) {
  81              set_debugging(DEBUG_DEVELOPER, true);
  82          }
  83  
  84          core_php_time_limit::raise();
  85  
  86          // Increase memory limit.
  87          raise_memory_limit(MEMORY_EXTRA);
  88  
  89          // Emulate normal session. - we use admin account by default.
  90          self::setup_user();
  91  
  92          // Start output log.
  93          $timenow = time();
  94          mtrace("Server Time: " . date('r', $timenow) . "\n\n");
  95  
  96          // Record start time and interval between the last cron runs.
  97          $laststart = get_config('tool_task', 'lastcronstart');
  98          set_config('lastcronstart', $timenow, 'tool_task');
  99          if ($laststart) {
 100              // Record the interval between last two runs (always store at least 1 second).
 101              set_config('lastcroninterval', max(1, $timenow - $laststart), 'tool_task');
 102          }
 103  
 104          // Determine the time when the cron should finish.
 105          if ($keepalive === null) {
 106              $keepalive = get_config('core', 'cron_keepalive');
 107              if ($keepalive === false) {
 108                  $keepalive = self::DEFAULT_MAIN_PROCESS_KEEPALIVE;
 109              }
 110          }
 111  
 112          if ($keepalive > self::MAX_MAIN_PROCESS_KEEPALIVE) {
 113              // Attempt to prevent abnormally long keepalives.
 114              mtrace("Cron keepalive time is too long, reducing to 15 minutes.");
 115              $keepalive = self::MAX_MAIN_PROCESS_KEEPALIVE;
 116          }
 117  
 118          // Calculate the finish time based on the start time and keepalive.
 119          $finishtime = $timenow + $keepalive;
 120  
 121          do {
 122              $startruntime = microtime();
 123  
 124              // Run all scheduled tasks.
 125              self::run_scheduled_tasks(time(), $timenow);
 126  
 127              // Run adhoc tasks.
 128              self::run_adhoc_tasks(time(), 0, true, $timenow);
 129  
 130              mtrace("Cron run completed correctly");
 131  
 132              gc_collect_cycles();
 133  
 134              $completiontime = date('H:i:s');
 135              $difftime = microtime_diff($startruntime, microtime());
 136              $memoryused = display_size(memory_get_usage());
 137  
 138              $message = "Cron completed at {$completiontime} in {$difftime} seconds. Memory used: {$memoryused}.";
 139  
 140              // Check if we should continue to run.
 141              // Only continue to run if:
 142              // - The finish time has not been reached; and
 143              // - The graceful exit flag has not been set; and
 144              // - The static caches have not been cleared since the start of the cron run.
 145              $remaining = $finishtime - time();
 146              $runagain = $remaining > 0;
 147              $runagain = $runagain && !\core\local\cli\shutdown::should_gracefully_exit();
 148              $runagain = $runagain && !\core\task\manager::static_caches_cleared_since($timenow);
 149  
 150              if ($runagain) {
 151                  $message .= " Continuing to check for tasks for {$remaining} more seconds.";
 152                  mtrace($message);
 153                  sleep(1);
 154  
 155                  // Re-check the graceful exit and cache clear flags after sleeping as these may have changed.
 156                  $runagain = $runagain && !\core\local\cli\shutdown::should_gracefully_exit();
 157                  $runagain = $runagain && !\core\task\manager::static_caches_cleared_since($timenow);
 158              } else {
 159                  mtrace($message);
 160              }
 161          } while ($runagain);
 162      }
 163  
 164      /**
 165       * Execute all queued scheduled tasks, applying necessary concurrency limits and time limits.
 166       *
 167       * @param   int       $startruntime The time this run started.
 168       * @param   null|int  $startprocesstime The time the process that owns this runner started.
 169       * @throws \moodle_exception
 170       */
 171      public static function run_scheduled_tasks(
 172          int $startruntime,
 173          ?int $startprocesstime = null,
 174      ): void {
 175          // Allow a restriction on the number of scheduled task runners at once.
 176          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 177          $maxruns = get_config('core', 'task_scheduled_concurrency_limit');
 178          $maxruntime = get_config('core', 'task_scheduled_max_runtime');
 179  
 180          if ($startprocesstime === null) {
 181              $startprocesstime = $startruntime;
 182          }
 183  
 184          $scheduledlock = null;
 185          for ($run = 0; $run < $maxruns; $run++) {
 186              // If we can't get a lock instantly it means runner N is already running
 187              // so fail as fast as possible and try N+1 so we don't limit the speed at
 188              // which we bring new runners into the pool.
 189              if ($scheduledlock = $cronlockfactory->get_lock("scheduled_task_runner_{$run}", 0)) {
 190                  break;
 191              }
 192          }
 193  
 194          if (!$scheduledlock) {
 195              mtrace("Skipping processing of scheduled tasks. Concurrency limit reached.");
 196              return;
 197          }
 198  
 199          $starttime = time();
 200  
 201          // Run all scheduled tasks.
 202          try {
 203              while (
 204                  !\core\local\cli\shutdown::should_gracefully_exit() &&
 205                  !\core\task\manager::static_caches_cleared_since($startprocesstime) &&
 206                  $task = \core\task\manager::get_next_scheduled_task($startruntime)
 207              ) {
 208                  self::run_inner_scheduled_task($task);
 209                  unset($task);
 210  
 211                  if ((time() - $starttime) > $maxruntime) {
 212                      mtrace("Stopping processing of scheduled tasks as time limit has been reached.");
 213                      break;
 214                  }
 215              }
 216          } finally {
 217              // Release the scheduled task runner lock.
 218              $scheduledlock->release();
 219          }
 220      }
 221  
 222      /**
 223       * Execute all queued adhoc tasks, applying necessary concurrency limits and time limits.
 224       *
 225       * @param   int     $startruntime The time this run started.
 226       * @param   int     $keepalive Keep this public static function alive for N seconds and poll for new adhoc tasks.
 227       * @param   bool    $checklimits Should we check limits?
 228       * @param   null|int $startprocesstime The time this process started.
 229       * @param   int|null $maxtasks Limit number of tasks to run`
 230       * @param   null|string $classname Run only tasks of this class
 231       * @throws \moodle_exception
 232       */
 233      public static function run_adhoc_tasks(
 234          int $startruntime,
 235          $keepalive = 0,
 236          $checklimits = true,
 237          ?int $startprocesstime = null,
 238          ?int $maxtasks = null,
 239          ?string $classname = null,
 240      ): void {
 241          // Allow a restriction on the number of adhoc task runners at once.
 242          $cronlockfactory = \core\lock\lock_config::get_lock_factory('cron');
 243          $maxruns = get_config('core', 'task_adhoc_concurrency_limit');
 244          $maxruntime = get_config('core', 'task_adhoc_max_runtime');
 245  
 246          if ($startprocesstime === null) {
 247              $startprocesstime = $startruntime;
 248          }
 249  
 250          $adhoclock = null;
 251          if ($checklimits) {
 252              for ($run = 0; $run < $maxruns; $run++) {
 253                  // If we can't get a lock instantly it means runner N is already running
 254                  // so fail as fast as possible and try N+1 so we don't limit the speed at
 255                  // which we bring new runners into the pool.
 256                  if ($adhoclock = $cronlockfactory->get_lock("adhoc_task_runner_{$run}", 0)) {
 257                      break;
 258                  }
 259              }
 260  
 261              if (!$adhoclock) {
 262                  mtrace("Skipping processing of adhoc tasks. Concurrency limit reached.");
 263                  return;
 264              }
 265          }
 266  
 267          $humantimenow = date('r', $startruntime);
 268          $finishtime = $startruntime + $keepalive;
 269          $waiting = false;
 270          $taskcount = 0;
 271  
 272          // Run all adhoc tasks.
 273          while (
 274              !\core\local\cli\shutdown::should_gracefully_exit() &&
 275              !\core\task\manager::static_caches_cleared_since($startprocesstime)
 276          ) {
 277  
 278              if ($checklimits && (time() - $startruntime) >= $maxruntime) {
 279                  if ($waiting) {
 280                      $waiting = false;
 281                      mtrace('');
 282                  }
 283                  mtrace("Stopping processing of adhoc tasks as time limit has been reached.");
 284                  break;
 285              }
 286  
 287              try {
 288                  $task = \core\task\manager::get_next_adhoc_task(time(), $checklimits, $classname);
 289              } catch (\Throwable $e) {
 290                  if ($adhoclock) {
 291                      // Release the adhoc task runner lock.
 292                      $adhoclock->release();
 293                  }
 294                  throw $e;
 295              }
 296  
 297              if ($task) {
 298                  if ($waiting) {
 299                      mtrace('');
 300                  }
 301                  $waiting = false;
 302                  self::run_inner_adhoc_task($task);
 303                  self::set_process_title("Waiting for next adhoc task");
 304                  $taskcount++;
 305                  if ($maxtasks && $taskcount >= $maxtasks) {
 306                      break;
 307                  }
 308                  unset($task);
 309              } else {
 310                  $timeleft = $finishtime - time();
 311                  if ($timeleft <= 0) {
 312                      break;
 313                  }
 314                  if (!$waiting) {
 315                      mtrace('Waiting for more adhoc tasks to be queued ', '');
 316                  } else {
 317                      mtrace('.', '');
 318                  }
 319                  $waiting = true;
 320                  self::set_process_title("Waiting {$timeleft}s for next adhoc task");
 321                  sleep(1);
 322              }
 323          }
 324  
 325          if ($waiting) {
 326              mtrace('');
 327          }
 328  
 329          mtrace("Ran {$taskcount} adhoc tasks found at {$humantimenow}");
 330  
 331          if ($adhoclock) {
 332              // Release the adhoc task runner lock.
 333              $adhoclock->release();
 334          }
 335      }
 336  
 337      /**
 338       * Execute an adhoc task.
 339       *
 340       * @param   int       $taskid
 341       */
 342      public static function run_adhoc_task(int $taskid): void {
 343          $task = \core\task\manager::get_adhoc_task($taskid);
 344          if (!$task->get_fail_delay() && $task->get_next_run_time() > time()) {
 345              throw new \moodle_exception('wontrunfuturescheduledtask');
 346          }
 347  
 348          self::run_inner_adhoc_task($task);
 349          self::set_process_title("Running adhoc task $taskid");
 350      }
 351  
 352      /**
 353       * Execute all failed adhoc tasks.
 354       *
 355       * @param string|null  $classname Run only tasks of this class
 356       */
 357      public static function run_failed_adhoc_tasks(?string $classname = null): void {
 358          global $DB;
 359  
 360          $where = 'faildelay > 0';
 361          $params = [];
 362          if ($classname) {
 363              $where .= ' AND classname = :classname';
 364              $params['classname'] = \core\task\manager::get_canonical_class_name($classname);
 365          }
 366          $tasks = $DB->get_records_sql("SELECT * from {task_adhoc} WHERE $where", $params);
 367          foreach ($tasks as $t) {
 368              self::run_adhoc_task($t->id);
 369          }
 370      }
 371  
 372      /**
 373       * Shared code that handles running of a single scheduled task within the cron.
 374       *
 375       * Not intended for calling directly outside of this library!
 376       *
 377       * @param \core\task\task_base $task
 378       */
 379      public static function run_inner_scheduled_task(\core\task\task_base $task) {
 380          global $CFG, $DB;
 381          $debuglevel = $CFG->debug;
 382  
 383          \core\task\manager::scheduled_task_starting($task);
 384          \core\task\logmanager::start_logging($task);
 385  
 386          $fullname = $task->get_name() . ' (' . get_class($task) . ')';
 387          mtrace('Execute scheduled task: ' . $fullname);
 388          self::set_process_title('Scheduled task: ' . get_class($task));
 389          self::trace_time_and_memory();
 390          $predbqueries = null;
 391          $predbqueries = $DB->perf_get_queries();
 392          $pretime = microtime(1);
 393  
 394          // Ensure that we have a clean session with the correct cron user.
 395          self::setup_user();
 396  
 397          try {
 398              get_mailer('buffer');
 399              self::prepare_core_renderer();
 400              // Temporarily increase debug level if task has failed and debugging isn't already at maximum.
 401              if ($debuglevel !== DEBUG_DEVELOPER && $faildelay = $task->get_fail_delay()) {
 402                  mtrace('Debugging increased temporarily due to faildelay of ' . $faildelay);
 403                  set_debugging(DEBUG_DEVELOPER);
 404              }
 405              $task->execute();
 406              if ($DB->is_transaction_started()) {
 407                  throw new coding_exception("Task left transaction open");
 408              }
 409              if (isset($predbqueries)) {
 410                  mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
 411                  mtrace("... used " . (microtime(1) - $pretime) . " seconds");
 412              }
 413              mtrace('Scheduled task complete: ' . $fullname);
 414              \core\task\manager::scheduled_task_complete($task);
 415          } catch (\Throwable $e) {
 416              if ($DB && $DB->is_transaction_started()) {
 417                  error_log('Database transaction aborted automatically in ' . get_class($task));
 418                  $DB->force_transaction_rollback();
 419              }
 420              if (isset($predbqueries)) {
 421                  mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
 422                  mtrace("... used " . (microtime(1) - $pretime) . " seconds");
 423              }
 424              mtrace('Scheduled task failed: ' . $fullname . ',' . $e->getMessage());
 425              if ($CFG->debugdeveloper) {
 426                  if (!empty($e->debuginfo)) {
 427                      mtrace("Debug info:");
 428                      mtrace($e->debuginfo);
 429                  }
 430                  mtrace("Backtrace:");
 431                  mtrace(format_backtrace($e->getTrace(), true));
 432              }
 433              \core\task\manager::scheduled_task_failed($task);
 434          } finally {
 435              // Reset debugging if it changed.
 436              if ($CFG->debug !== $debuglevel) {
 437                  set_debugging($debuglevel);
 438              }
 439              // Reset back to the standard admin user.
 440              self::setup_user();
 441              self::set_process_title('Waiting for next scheduled task');
 442              self::prepare_core_renderer(true);
 443          }
 444          get_mailer('close');
 445      }
 446  
 447      /**
 448       * Shared code that handles running of a single adhoc task within the cron.
 449       *
 450       * @param \core\task\adhoc_task $task
 451       */
 452      public static function run_inner_adhoc_task(\core\task\adhoc_task $task) {
 453          global $CFG, $DB;
 454          $debuglevel = $CFG->debug;
 455  
 456          \core\task\manager::adhoc_task_starting($task);
 457          \core\task\logmanager::start_logging($task);
 458  
 459          mtrace("Execute adhoc task: " . get_class($task));
 460          mtrace("Adhoc task id: " . $task->get_id());
 461          mtrace("Adhoc task custom data: " . $task->get_custom_data_as_string());
 462          self::set_process_title('Adhoc task: ' . $task->get_id() . ' ' . get_class($task));
 463          self::trace_time_and_memory();
 464          $predbqueries = null;
 465          $predbqueries = $DB->perf_get_queries();
 466          $pretime = microtime(1);
 467  
 468          if ($userid = $task->get_userid()) {
 469              // This task has a userid specified.
 470              if ($user = \core_user::get_user($userid)) {
 471                  // User found. Check that they are suitable.
 472                  try {
 473                      \core_user::require_active_user($user, true, true);
 474                  } catch (moodle_exception $e) {
 475                      mtrace("User {$userid} cannot be used to run an adhoc task: " . get_class($task) . ". Cancelling task.");
 476                      $user = null;
 477                  }
 478              } else {
 479                  // Unable to find the user for this task.
 480                  // A user missing in the database will never reappear.
 481                  mtrace("User {$userid} could not be found for adhoc task: " . get_class($task) . ". Cancelling task.");
 482              }
 483  
 484              if (empty($user)) {
 485                  // A user missing in the database will never reappear so the task needs to be failed to ensure that locks are
 486                  // removed, and then removed to prevent future runs.
 487                  // A task running as a user should only be run as that user.
 488                  \core\task\manager::adhoc_task_failed($task);
 489                  $DB->delete_records('task_adhoc', ['id' => $task->get_id()]);
 490  
 491                  return;
 492              }
 493  
 494              self::setup_user($user);
 495          } else {
 496              // No user specified, ensure that we have a clean session with the correct cron user.
 497              self::setup_user();
 498          }
 499  
 500          try {
 501              get_mailer('buffer');
 502              self::prepare_core_renderer();
 503              // Temporarily increase debug level if task has failed and debugging isn't already at maximum.
 504              if ($debuglevel !== DEBUG_DEVELOPER && $faildelay = $task->get_fail_delay()) {
 505                  mtrace('Debugging increased temporarily due to faildelay of ' . $faildelay);
 506                  set_debugging(DEBUG_DEVELOPER);
 507              }
 508              $task->execute();
 509              if ($DB->is_transaction_started()) {
 510                  throw new coding_exception("Task left transaction open");
 511              }
 512              if (isset($predbqueries)) {
 513                  mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
 514                  mtrace("... used " . (microtime(1) - $pretime) . " seconds");
 515              }
 516              mtrace("Adhoc task complete: " . get_class($task));
 517              \core\task\manager::adhoc_task_complete($task);
 518          } catch (\Throwable $e) {
 519              if ($DB && $DB->is_transaction_started()) {
 520                  error_log('Database transaction aborted automatically in ' . get_class($task));
 521                  $DB->force_transaction_rollback();
 522              }
 523              if (isset($predbqueries)) {
 524                  mtrace("... used " . ($DB->perf_get_queries() - $predbqueries) . " dbqueries");
 525                  mtrace("... used " . (microtime(1) - $pretime) . " seconds");
 526              }
 527              mtrace("Adhoc task failed: " . get_class($task) . "," . $e->getMessage());
 528              if ($CFG->debugdeveloper) {
 529                  if (!empty($e->debuginfo)) {
 530                      mtrace("Debug info:");
 531                      mtrace($e->debuginfo);
 532                  }
 533                  mtrace("Backtrace:");
 534                  mtrace(format_backtrace($e->getTrace(), true));
 535              }
 536              \core\task\manager::adhoc_task_failed($task);
 537          } finally {
 538              // Reset debug level if it changed.
 539              if ($CFG->debug !== $debuglevel) {
 540                  set_debugging($debuglevel);
 541              }
 542              // Reset back to the standard admin user.
 543              self::setup_user();
 544              self::prepare_core_renderer(true);
 545          }
 546          get_mailer('close');
 547      }
 548  
 549      /**
 550       * Sets the process title
 551       *
 552       * This makes it very easy for a sysadmin to immediately see what task
 553       * a cron process is running at any given moment.
 554       *
 555       * @param string $title process status title
 556       */
 557      public static function set_process_title(string $title) {
 558          global $CFG;
 559          if (CLI_SCRIPT) {
 560              require_once($CFG->libdir . '/clilib.php');
 561              $datetime = userdate(time(), '%b %d, %H:%M:%S');
 562              cli_set_process_title_suffix("$datetime $title");
 563          }
 564      }
 565  
 566      /**
 567       * Output some standard information during cron runs. Specifically current time
 568       * and memory usage. This method also does gc_collect_cycles() (before displaying
 569       * memory usage) to try to help PHP manage memory better.
 570       */
 571      public static function trace_time_and_memory() {
 572          gc_collect_cycles();
 573          mtrace('... started ' . date('H:i:s') . '. Current memory use ' . display_size(memory_get_usage()) . '.');
 574      }
 575  
 576      /**
 577       * Prepare the output renderer for the cron run.
 578       *
 579       * This involves creating a new $PAGE, and $OUTPUT fresh for each task and prevents any one task from influencing
 580       * any other.
 581       *
 582       * @param   bool    $restore Whether to restore the original PAGE and OUTPUT
 583       */
 584      public static function prepare_core_renderer($restore = false) {
 585          global $OUTPUT, $PAGE;
 586  
 587          // Store the original PAGE and OUTPUT values so that they can be reset at a later point to the original.
 588          // This should not normally be required, but may be used in places such as the scheduled task tool's "Run now"
 589          // functionality.
 590          static $page = null;
 591          static $output = null;
 592  
 593          if (null === $page) {
 594              $page = $PAGE;
 595          }
 596  
 597          if (null === $output) {
 598              $output = $OUTPUT;
 599          }
 600  
 601          if (!empty($restore)) {
 602              $PAGE = $page;
 603              $page = null;
 604  
 605              $OUTPUT = $output;
 606              $output = null;
 607          } else {
 608              // Setup a new General renderer.
 609              // Cron tasks may produce output to be used in web, so we must use the appropriate renderer target.
 610              // This allows correct use of templates, etc.
 611              $PAGE = new \moodle_page();
 612              $OUTPUT = new \core_renderer($PAGE, RENDERER_TARGET_GENERAL);
 613          }
 614      }
 615  
 616      /**
 617       * Sets up a user and course environment in cron.
 618       *
 619       * Note: This function is intended only for use in:
 620       * - the cron runner scripts
 621       * - individual tasks which extend the adhoc_task and scheduled_task classes
 622       * - unit tests related to tasks
 623       * - other parts of the cron/task system
 624       *
 625       * Please note that this function stores cache data statically.
 626       * @see reset_user_cache() to reset this cache.
 627       *
 628       * @param null|stdClass $user full user object, null means default cron user (admin)
 629       * @param null|stdClass $course full course record, null means $SITE
 630       * @param null|bool $leavepagealone If specified, stops it messing with global page object
 631       */
 632      public static function setup_user(?stdClass $user = null, ?stdClass $course = null, bool $leavepagealone = false): void {
 633          // This function uses the $GLOBALS super global. Disable the VariableNameLowerCase sniff for this function.
 634          // phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
 635          global $CFG, $SITE, $PAGE;
 636  
 637          if (!CLI_SCRIPT && !$leavepagealone) {
 638              throw new coding_exception('It is not possible to use \core\cron\setup_user() in normal requests!');
 639          }
 640  
 641          if (empty(self::$cronuser)) {
 642              // The cron user is essentially the admin user, but with some value removed.
 643              // We ginore the timezone language, and locale preferences - use the site default instead.
 644              self::$cronuser = get_admin();
 645              self::$cronuser->timezone = $CFG->timezone;
 646              self::$cronuser->lang = '';
 647              self::$cronuser->theme = '';
 648              unset(self::$cronuser->description);
 649  
 650              self::$cronsession = new stdClass();
 651          }
 652  
 653          if (!$user) {
 654              // Cached default cron user (==modified admin for now).
 655              \core\session\manager::init_empty_session();
 656              \core\session\manager::set_user(self::$cronuser);
 657              $GLOBALS['SESSION'] = self::$cronsession;
 658          } else {
 659              // Emulate real user session - needed for caps in cron.
 660              if ($GLOBALS['USER']->id != $user->id) {
 661                  \core\session\manager::init_empty_session();
 662                  \core\session\manager::set_user($user);
 663              }
 664          }
 665  
 666          // TODO MDL-19774 relying on global $PAGE in cron is a bad idea.
 667          // Temporary hack so that cron does not give fatal errors.
 668          if (!$leavepagealone) {
 669              $PAGE = new \moodle_page();
 670              $PAGE->set_course($course ?? $SITE);
 671          }
 672  
 673          // TODO: it should be possible to improve perf by caching some limited number of users here.
 674          // phpcs:enable
 675      }
 676  
 677      /**
 678       * Resets the cache for the cron user used by `setup_user()`.
 679       */
 680      public static function reset_user_cache(): void {
 681          self::$cronuser = null;
 682          self::$cronsession = null;
 683          \core\session\manager::init_empty_session();
 684      }
 685  }