Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.2.x will end 22 April 2024 (12 months).
  • Bug fixes for security issues in 4.2.x will end 7 October 2024 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.1.x is supported too.
   1  <?php
   2  // This file is part of Moodle - https://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 <https://www.gnu.org/licenses/>.
  16  
  17  namespace core;
  18  
  19  use stdClass, IteratorAggregate, ArrayIterator;
  20  use coding_exception, moodle_url;
  21  
  22  /**
  23   * Basic moodle context abstraction class.
  24   *
  25   * Google confirms that no other important framework is using "context" class,
  26   * we could use something else like mcontext or moodle_context, but we need to type
  27   * this very often which would be annoying and it would take too much space...
  28   *
  29   * This class is derived from stdClass for backwards compatibility with
  30   * odl $context record that was returned from DML $DB->get_record()
  31   *
  32   * @package   core_access
  33   * @category  access
  34   * @copyright Petr Skoda
  35   * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  36   * @since     Moodle 4.2
  37   *
  38   * @property-read int $id context id
  39   * @property-read int $contextlevel CONTEXT_SYSTEM, CONTEXT_COURSE, etc.
  40   * @property-read int $instanceid id of related instance in each context
  41   * @property-read string $path path to context, starts with system context
  42   * @property-read int $depth
  43   * @property-read bool $locked true means write capabilities are ignored in this context or parents
  44   */
  45  abstract class context extends stdClass implements IteratorAggregate {
  46  
  47      /** @var string Default sorting of capabilities in {@see get_capabilities} */
  48      protected const DEFAULT_CAPABILITY_SORT = 'contextlevel, component, name';
  49  
  50      /**
  51       * The context id
  52       * Can be accessed publicly through $context->id
  53       * @var int
  54       */
  55      protected $_id;
  56  
  57      /**
  58       * The context level
  59       * Can be accessed publicly through $context->contextlevel
  60       * @var int One of CONTEXT_* e.g. CONTEXT_COURSE, CONTEXT_MODULE
  61       */
  62      protected $_contextlevel;
  63  
  64      /**
  65       * Id of the item this context is related to e.g. COURSE_CONTEXT => course.id
  66       * Can be accessed publicly through $context->instanceid
  67       * @var int
  68       */
  69      protected $_instanceid;
  70  
  71      /**
  72       * The path to the context always starting from the system context
  73       * Can be accessed publicly through $context->path
  74       * @var string
  75       */
  76      protected $_path;
  77  
  78      /**
  79       * The depth of the context in relation to parent contexts
  80       * Can be accessed publicly through $context->depth
  81       * @var int
  82       */
  83      protected $_depth;
  84  
  85      /**
  86       * Whether this context is locked or not.
  87       *
  88       * Can be accessed publicly through $context->locked.
  89       *
  90       * @var int
  91       */
  92      protected $_locked;
  93  
  94      /**
  95       * @var array Context caching info
  96       */
  97      private static $cache_contextsbyid = array();
  98  
  99      /**
 100       * @var array Context caching info
 101       */
 102      private static $cache_contexts = array();
 103  
 104      /**
 105       * Context count
 106       * Why do we do count contexts? Because count($array) is horribly slow for large arrays
 107       * @var int
 108       */
 109      protected static $cache_count = 0;
 110  
 111      /**
 112       * @var array Context caching info
 113       */
 114      protected static $cache_preloaded = array();
 115  
 116      /**
 117       * @var context\system The system context once initialised
 118       */
 119      protected static $systemcontext = null;
 120  
 121      /**
 122       * Returns short context name.
 123       *
 124       * @since Moodle 4.2
 125       *
 126       * @return string
 127       */
 128      public static function get_short_name(): string {
 129          // NOTE: it would be more correct to make this abstract,
 130          // unfortunately there are tests that attempt to mock context classes.
 131          throw new \coding_exception('get_short_name() method must be overridden in custom context levels');
 132      }
 133  
 134      /**
 135       * Resets the cache to remove all data.
 136       */
 137      protected static function reset_caches() {
 138          self::$cache_contextsbyid = array();
 139          self::$cache_contexts = array();
 140          self::$cache_count = 0;
 141          self::$cache_preloaded = array();
 142  
 143          self::$systemcontext = null;
 144      }
 145  
 146      /**
 147       * Adds a context to the cache. If the cache is full, discards a batch of
 148       * older entries.
 149       *
 150       * @param context $context New context to add
 151       * @return void
 152       */
 153      protected static function cache_add(context $context) {
 154          if (isset(self::$cache_contextsbyid[$context->id])) {
 155              // Already cached, no need to do anything - this is relatively cheap, we do all this because count() is slow.
 156              return;
 157          }
 158  
 159          if (self::$cache_count >= CONTEXT_CACHE_MAX_SIZE) {
 160              $i = 0;
 161              foreach (self::$cache_contextsbyid as $ctx) {
 162                  $i++;
 163                  if ($i <= 100) {
 164                      // We want to keep the first contexts to be loaded on this page, hopefully they will be needed again later.
 165                      continue;
 166                  }
 167                  if ($i > (CONTEXT_CACHE_MAX_SIZE / 3)) {
 168                      // We remove oldest third of the contexts to make room for more contexts.
 169                      break;
 170                  }
 171                  unset(self::$cache_contextsbyid[$ctx->id]);
 172                  unset(self::$cache_contexts[$ctx->contextlevel][$ctx->instanceid]);
 173                  self::$cache_count--;
 174              }
 175          }
 176  
 177          self::$cache_contexts[$context->contextlevel][$context->instanceid] = $context;
 178          self::$cache_contextsbyid[$context->id] = $context;
 179          self::$cache_count++;
 180      }
 181  
 182      /**
 183       * Removes a context from the cache.
 184       *
 185       * @param context $context Context object to remove
 186       * @return void
 187       */
 188      protected static function cache_remove(context $context) {
 189          if (!isset(self::$cache_contextsbyid[$context->id])) {
 190              // Not cached, no need to do anything - this is relatively cheap, we do all this because count() is slow.
 191              return;
 192          }
 193          unset(self::$cache_contexts[$context->contextlevel][$context->instanceid]);
 194          unset(self::$cache_contextsbyid[$context->id]);
 195  
 196          self::$cache_count--;
 197  
 198          if (self::$cache_count < 0) {
 199              self::$cache_count = 0;
 200          }
 201      }
 202  
 203      /**
 204       * Gets a context from the cache.
 205       *
 206       * @param int $contextlevel Context level
 207       * @param int $instance Instance ID
 208       * @return context|bool Context or false if not in cache
 209       */
 210      protected static function cache_get($contextlevel, $instance) {
 211          if (isset(self::$cache_contexts[$contextlevel][$instance])) {
 212              return self::$cache_contexts[$contextlevel][$instance];
 213          }
 214          return false;
 215      }
 216  
 217      /**
 218       * Gets a context from the cache based on its id.
 219       *
 220       * @param int $id Context ID
 221       * @return context|bool Context or false if not in cache
 222       */
 223      protected static function cache_get_by_id($id) {
 224          if (isset(self::$cache_contextsbyid[$id])) {
 225              return self::$cache_contextsbyid[$id];
 226          }
 227          return false;
 228      }
 229  
 230      /**
 231       * Preloads context information from db record and strips the cached info.
 232       *
 233       * @param stdClass $rec
 234       * @return context|null (modifies $rec)
 235       */
 236      protected static function preload_from_record(stdClass $rec) {
 237          $notenoughdata = false;
 238          $notenoughdata = $notenoughdata || empty($rec->ctxid);
 239          $notenoughdata = $notenoughdata || empty($rec->ctxlevel);
 240          $notenoughdata = $notenoughdata || !isset($rec->ctxinstance);
 241          $notenoughdata = $notenoughdata || empty($rec->ctxpath);
 242          $notenoughdata = $notenoughdata || empty($rec->ctxdepth);
 243          $notenoughdata = $notenoughdata || !isset($rec->ctxlocked);
 244          if ($notenoughdata) {
 245              // The record does not have enough data, passed here repeatedly or context does not exist yet.
 246              if (isset($rec->ctxid) && !isset($rec->ctxlocked)) {
 247                  debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
 248              }
 249              return null;
 250          }
 251  
 252          $record = (object) [
 253              'id' => $rec->ctxid,
 254              'contextlevel' => $rec->ctxlevel,
 255              'instanceid' => $rec->ctxinstance,
 256              'path' => $rec->ctxpath,
 257              'depth' => $rec->ctxdepth,
 258              'locked' => $rec->ctxlocked,
 259          ];
 260  
 261          unset($rec->ctxid);
 262          unset($rec->ctxlevel);
 263          unset($rec->ctxinstance);
 264          unset($rec->ctxpath);
 265          unset($rec->ctxdepth);
 266          unset($rec->ctxlocked);
 267  
 268          return self::create_instance_from_record($record);
 269      }
 270  
 271  
 272      /* ====== magic methods ======= */
 273  
 274      /**
 275       * Magic setter method, we do not want anybody to modify properties from the outside
 276       * @param string $name
 277       * @param mixed $value
 278       */
 279      public function __set($name, $value) {
 280          debugging('Can not change context instance properties!');
 281      }
 282  
 283      /**
 284       * Magic method getter, redirects to read only values.
 285       * @param string $name
 286       * @return mixed
 287       */
 288      public function __get($name) {
 289          switch ($name) {
 290              case 'id':
 291                  return $this->_id;
 292              case 'contextlevel':
 293                  return $this->_contextlevel;
 294              case 'instanceid':
 295                  return $this->_instanceid;
 296              case 'path':
 297                  return $this->_path;
 298              case 'depth':
 299                  return $this->_depth;
 300              case 'locked':
 301                  return $this->is_locked();
 302  
 303              default:
 304                  debugging('Invalid context property accessed! '.$name);
 305                  return null;
 306          }
 307      }
 308  
 309      /**
 310       * Full support for isset on our magic read only properties.
 311       * @param string $name
 312       * @return bool
 313       */
 314      public function __isset($name) {
 315          switch ($name) {
 316              case 'id':
 317                  return isset($this->_id);
 318              case 'contextlevel':
 319                  return isset($this->_contextlevel);
 320              case 'instanceid':
 321                  return isset($this->_instanceid);
 322              case 'path':
 323                  return isset($this->_path);
 324              case 'depth':
 325                  return isset($this->_depth);
 326              case 'locked':
 327                  // Locked is always set.
 328                  return true;
 329              default:
 330                  return false;
 331          }
 332      }
 333  
 334      /**
 335       * All properties are read only, sorry.
 336       * @param string $name
 337       */
 338      public function __unset($name) {
 339          debugging('Can not unset context instance properties!');
 340      }
 341  
 342      /* ====== implementing method from interface IteratorAggregate ====== */
 343  
 344      /**
 345       * Create an iterator because magic vars can't be seen by 'foreach'.
 346       *
 347       * Now we can convert context object to array using convert_to_array(),
 348       * and feed it properly to json_encode().
 349       */
 350      public function getIterator(): \Traversable {
 351          $ret = array(
 352              'id' => $this->id,
 353              'contextlevel' => $this->contextlevel,
 354              'instanceid' => $this->instanceid,
 355              'path' => $this->path,
 356              'depth' => $this->depth,
 357              'locked' => $this->locked,
 358          );
 359          return new ArrayIterator($ret);
 360      }
 361  
 362      /* ====== general context methods ====== */
 363  
 364      /**
 365       * Constructor is protected so that devs are forced to
 366       * use context_xxx::instance() or context::instance_by_id().
 367       *
 368       * @param stdClass $record
 369       */
 370      protected function __construct(stdClass $record) {
 371          $this->_id = (int)$record->id;
 372          $this->_contextlevel = (int)$record->contextlevel;
 373          $this->_instanceid = $record->instanceid;
 374          $this->_path = $record->path;
 375          $this->_depth = $record->depth;
 376  
 377          if (isset($record->locked)) {
 378              $this->_locked = $record->locked;
 379          } else if (!during_initial_install() && !moodle_needs_upgrading()) {
 380              debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
 381          }
 382      }
 383  
 384      /**
 385       * This function is also used to work around 'protected' keyword problems in context_helper.
 386       *
 387       * @param stdClass $record
 388       * @return context instance
 389       */
 390      protected static function create_instance_from_record(stdClass $record) {
 391          $classname = context_helper::get_class_for_level($record->contextlevel);
 392  
 393          if ($context = self::cache_get_by_id($record->id)) {
 394              return $context;
 395          }
 396  
 397          $context = new $classname($record);
 398          self::cache_add($context);
 399  
 400          return $context;
 401      }
 402  
 403      /**
 404       * Copy prepared new contexts from temp table to context table,
 405       * we do this in db specific way for perf reasons only.
 406       */
 407      protected static function merge_context_temp_table() {
 408          global $DB;
 409  
 410          /* MDL-11347:
 411           *  - mysql does not allow to use FROM in UPDATE statements
 412           *  - using two tables after UPDATE works in mysql, but might give unexpected
 413           *    results in pg 8 (depends on configuration)
 414           *  - using table alias in UPDATE does not work in pg < 8.2
 415           *
 416           * Different code for each database - mostly for performance reasons
 417           */
 418  
 419          $dbfamily = $DB->get_dbfamily();
 420          if ($dbfamily == 'mysql') {
 421              $updatesql = "UPDATE {context} ct, {context_temp} temp
 422                               SET ct.path = temp.path,
 423                                   ct.depth = temp.depth,
 424                                   ct.locked = temp.locked
 425                             WHERE ct.id = temp.id";
 426          } else if ($dbfamily == 'oracle') {
 427              $updatesql = "UPDATE {context} ct
 428                               SET (ct.path, ct.depth, ct.locked) =
 429                                   (SELECT temp.path, temp.depth, temp.locked
 430                                      FROM {context_temp} temp
 431                                     WHERE temp.id=ct.id)
 432                             WHERE EXISTS (SELECT 'x'
 433                                             FROM {context_temp} temp
 434                                             WHERE temp.id = ct.id)";
 435          } else if ($dbfamily == 'postgres' || $dbfamily == 'mssql') {
 436              $updatesql = "UPDATE {context}
 437                               SET path = temp.path,
 438                                   depth = temp.depth,
 439                                   locked = temp.locked
 440                              FROM {context_temp} temp
 441                             WHERE temp.id={context}.id";
 442          } else {
 443              // Sqlite and others.
 444              $updatesql = "UPDATE {context}
 445                               SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id),
 446                                   depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id),
 447                                   locked = (SELECT locked FROM {context_temp} WHERE id = {context}.id)
 448                               WHERE id IN (SELECT id FROM {context_temp})";
 449          }
 450  
 451          $DB->execute($updatesql);
 452      }
 453  
 454      /**
 455       * Get a context instance as an object, from a given context id.
 456       *
 457       * @param int $id context id
 458       * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
 459       *                        MUST_EXIST means throw exception if no record found
 460       * @return context|bool the context object or false if not found
 461       */
 462      public static function instance_by_id($id, $strictness = MUST_EXIST) {
 463          global $DB;
 464  
 465          if (get_called_class() !== 'core\context' && get_called_class() !== 'core\context_helper') {
 466              // Some devs might confuse context->id and instanceid, better prevent these mistakes completely.
 467              throw new coding_exception('use only context::instance_by_id() for real context levels use ::instance() methods');
 468          }
 469  
 470          if ($id == SYSCONTEXTID) {
 471              return context\system::instance(0, $strictness);
 472          }
 473  
 474          if (is_array($id) || is_object($id) || empty($id)) {
 475              throw new coding_exception('Invalid context id specified context::instance_by_id()');
 476          }
 477  
 478          if ($context = self::cache_get_by_id($id)) {
 479              return $context;
 480          }
 481  
 482          if ($record = $DB->get_record('context', array('id' => $id), '*', $strictness)) {
 483              return self::create_instance_from_record($record);
 484          }
 485  
 486          return false;
 487      }
 488  
 489      /**
 490       * Update context info after moving context in the tree structure.
 491       *
 492       * @param context $newparent
 493       * @return void
 494       */
 495      public function update_moved(context $newparent) {
 496          global $DB;
 497  
 498          $frompath = $this->_path;
 499          $newpath = $newparent->path . '/' . $this->_id;
 500  
 501          $trans = $DB->start_delegated_transaction();
 502  
 503          $setdepth = '';
 504          if (($newparent->depth + 1) != $this->_depth) {
 505              $diff = $newparent->depth - $this->_depth + 1;
 506              $setdepth = ", depth = depth + $diff";
 507          }
 508          $sql = "UPDATE {context}
 509                     SET path = ?
 510                         $setdepth
 511                   WHERE id = ?";
 512          $params = array($newpath, $this->_id);
 513          $DB->execute($sql, $params);
 514  
 515          $this->_path = $newpath;
 516          $this->_depth = $newparent->depth + 1;
 517  
 518          $sql = "UPDATE {context}
 519                     SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath) + 1))."
 520                         $setdepth
 521                   WHERE path LIKE ?";
 522          $params = array($newpath, "{$frompath}/%");
 523          $DB->execute($sql, $params);
 524  
 525          $this->mark_dirty();
 526  
 527          self::reset_caches();
 528  
 529          $trans->allow_commit();
 530      }
 531  
 532      /**
 533       * Set whether this context has been locked or not.
 534       *
 535       * @param   bool    $locked
 536       * @return  $this
 537       */
 538      public function set_locked(bool $locked) {
 539          global $DB;
 540  
 541          if ($this->_locked == $locked) {
 542              return $this;
 543          }
 544  
 545          $this->_locked = $locked;
 546          $DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]);
 547          $this->mark_dirty();
 548  
 549          if ($locked) {
 550              $eventname = '\\core\\event\\context_locked';
 551          } else {
 552              $eventname = '\\core\\event\\context_unlocked';
 553          }
 554          $event = $eventname::create(['context' => $this, 'objectid' => $this->id]);
 555          $event->trigger();
 556  
 557          self::reset_caches();
 558  
 559          return $this;
 560      }
 561  
 562      /**
 563       * Remove all context path info and optionally rebuild it.
 564       *
 565       * @param bool $rebuild
 566       * @return void
 567       */
 568      public function reset_paths($rebuild = true) {
 569          global $DB;
 570  
 571          if ($this->_path) {
 572              $this->mark_dirty();
 573          }
 574          $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$this->_id/%'");
 575          $DB->set_field_select('context', 'path', null, "path LIKE '%/$this->_id/%'");
 576          if ($this->_contextlevel != CONTEXT_SYSTEM) {
 577              $DB->set_field('context', 'depth', 0, array('id' => $this->_id));
 578              $DB->set_field('context', 'path', null, array('id' => $this->_id));
 579              $this->_depth = 0;
 580              $this->_path = null;
 581          }
 582  
 583          if ($rebuild) {
 584              context_helper::build_all_paths(false);
 585          }
 586  
 587          self::reset_caches();
 588      }
 589  
 590      /**
 591       * Delete all data linked to content, do not delete the context record itself
 592       */
 593      public function delete_content() {
 594          global $CFG, $DB;
 595  
 596          blocks_delete_all_for_context($this->_id);
 597          filter_delete_all_for_context($this->_id);
 598  
 599          require_once($CFG->dirroot . '/comment/lib.php');
 600          \comment::delete_comments(array('contextid' => $this->_id));
 601  
 602          require_once($CFG->dirroot.'/rating/lib.php');
 603          $delopt = new stdclass();
 604          $delopt->contextid = $this->_id;
 605          $rm = new \rating_manager();
 606          $rm->delete_ratings($delopt);
 607  
 608          // Delete all files attached to this context.
 609          $fs = get_file_storage();
 610          $fs->delete_area_files($this->_id);
 611  
 612          // Delete all repository instances attached to this context.
 613          require_once($CFG->dirroot . '/repository/lib.php');
 614          \repository::delete_all_for_context($this->_id);
 615  
 616          // Delete all advanced grading data attached to this context.
 617          require_once($CFG->dirroot.'/grade/grading/lib.php');
 618          \grading_manager::delete_all_for_context($this->_id);
 619  
 620          // Now delete stuff from role related tables, role_unassign_all
 621          // and unenrol should be called earlier to do proper cleanup.
 622          $DB->delete_records('role_assignments', array('contextid' => $this->_id));
 623          $DB->delete_records('role_names', array('contextid' => $this->_id));
 624          $this->delete_capabilities();
 625      }
 626  
 627      /**
 628       * Unassign all capabilities from a context.
 629       */
 630      public function delete_capabilities() {
 631          global $DB;
 632  
 633          $ids = $DB->get_fieldset_select('role_capabilities', 'DISTINCT roleid', 'contextid = ?', array($this->_id));
 634          if ($ids) {
 635              $DB->delete_records('role_capabilities', array('contextid' => $this->_id));
 636  
 637              // Reset any cache of these roles, including MUC.
 638              accesslib_clear_role_cache($ids);
 639          }
 640      }
 641  
 642      /**
 643       * Delete the context content and the context record itself
 644       */
 645      public function delete() {
 646          global $DB;
 647  
 648          if ($this->_contextlevel <= CONTEXT_SYSTEM) {
 649              throw new coding_exception('Cannot delete system context');
 650          }
 651  
 652          // Double check the context still exists.
 653          if (!$DB->record_exists('context', array('id' => $this->_id))) {
 654              self::cache_remove($this);
 655              return;
 656          }
 657  
 658          $this->delete_content();
 659          $DB->delete_records('context', array('id' => $this->_id));
 660          // Purge static context cache if entry present.
 661          self::cache_remove($this);
 662  
 663          // Inform search engine to delete data related to this context.
 664          \core_search\manager::context_deleted($this);
 665      }
 666  
 667      /* ====== context level related methods ====== */
 668  
 669      /**
 670       * Utility method for context creation
 671       *
 672       * @param int $contextlevel
 673       * @param int $instanceid
 674       * @param string $parentpath
 675       * @return stdClass context record
 676       */
 677      protected static function insert_context_record($contextlevel, $instanceid, $parentpath) {
 678          global $DB;
 679  
 680          $record = new stdClass();
 681          $record->contextlevel = $contextlevel;
 682          $record->instanceid = $instanceid;
 683          $record->depth = 0;
 684          $record->path = null; // Not known before insert.
 685          $record->locked = 0;
 686  
 687          $record->id = $DB->insert_record('context', $record);
 688  
 689          // Now add path if known - it can be added later.
 690          if (!is_null($parentpath)) {
 691              $record->path = $parentpath.'/'.$record->id;
 692              $record->depth = substr_count($record->path, '/');
 693              $DB->update_record('context', $record);
 694          }
 695  
 696          return $record;
 697      }
 698  
 699      /**
 700       * Returns human readable context identifier.
 701       *
 702       * @param boolean $withprefix whether to prefix the name of the context with the
 703       *      type of context, e.g. User, Course, Forum, etc.
 704       * @param boolean $short whether to use the short name of the thing. Only applies
 705       *      to course contexts
 706       * @param boolean $escape Whether the returned name of the thing is to be
 707       *      HTML escaped or not.
 708       * @return string the human readable context name.
 709       */
 710      public function get_context_name($withprefix = true, $short = false, $escape = true) {
 711          // Must be implemented in all context levels.
 712          throw new coding_exception('can not get name of abstract context');
 713      }
 714  
 715      /**
 716       * Whether the current context is locked.
 717       *
 718       * @return  bool
 719       */
 720      public function is_locked() {
 721          if ($this->_locked) {
 722              return true;
 723          }
 724  
 725          if ($parent = $this->get_parent_context()) {
 726              return $parent->is_locked();
 727          }
 728  
 729          return false;
 730      }
 731  
 732      /**
 733       * Returns the most relevant URL for this context.
 734       *
 735       * @return moodle_url
 736       */
 737      abstract public function get_url();
 738  
 739      /**
 740       * Returns context instance database name.
 741       *
 742       * @return string|null table name for all levels except system.
 743       */
 744      protected static function get_instance_table(): ?string {
 745          return null;
 746      }
 747  
 748      /**
 749       * Returns list of columns that can be used from behat
 750       * to look up context by reference.
 751       *
 752       * @return array list of column names from instance table
 753       */
 754      protected static function get_behat_reference_columns(): array {
 755          return [];
 756      }
 757  
 758      /**
 759       * Returns list of all role archetypes that are compatible
 760       * with role assignments in context level.
 761       * @since Moodle 4.2
 762       *
 763       * @return string[]
 764       */
 765      protected static function get_compatible_role_archetypes(): array {
 766          // Override if archetype roles should be allowed to be assigned in context level.
 767          return [];
 768      }
 769  
 770      /**
 771       * Returns list of all possible parent context levels,
 772       * it may include itself if nesting is allowed.
 773       * @since Moodle 4.2
 774       *
 775       * @return int[]
 776       */
 777      public static function get_possible_parent_levels(): array {
 778          // Override if other type of parents are expected.
 779          return [context\system::LEVEL];
 780      }
 781  
 782      /**
 783       * Returns array of relevant context capability records.
 784       *
 785       * @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display
 786       * @return array
 787       */
 788      abstract public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT);
 789  
 790      /**
 791       * Recursive function which, given a context, find all its children context ids.
 792       *
 793       * For course category contexts it will return immediate children and all subcategory contexts.
 794       * It will NOT recurse into courses or subcategories categories.
 795       * If you want to do that, call it on the returned courses/categories.
 796       *
 797       * When called for a course context, it will return the modules and blocks
 798       * displayed in the course page and blocks displayed on the module pages.
 799       *
 800       * If called on a user/course/module context it _will_ populate the cache with the appropriate
 801       * contexts ;-)
 802       *
 803       * @return array Array of child records
 804       */
 805      public function get_child_contexts() {
 806          global $DB;
 807  
 808          if (empty($this->_path) || empty($this->_depth)) {
 809              debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
 810              return array();
 811          }
 812  
 813          $sql = "SELECT ctx.*
 814                    FROM {context} ctx
 815                   WHERE ctx.path LIKE ?";
 816          $params = array($this->_path.'/%');
 817          $records = $DB->get_records_sql($sql, $params);
 818  
 819          $result = array();
 820          foreach ($records as $record) {
 821              $result[$record->id] = self::create_instance_from_record($record);
 822          }
 823  
 824          return $result;
 825      }
 826  
 827      /**
 828       * Determine if the current context is a parent of the possible child.
 829       *
 830       * @param   context $possiblechild
 831       * @param   bool $includeself Whether to check the current context
 832       * @return  bool
 833       */
 834      public function is_parent_of(context $possiblechild, bool $includeself): bool {
 835          // A simple substring check is used on the context path.
 836          // The possible child's path is used as a haystack, with the current context as the needle.
 837          // The path is prefixed with '+' to ensure that the parent always starts at the top.
 838          // It is suffixed with '+' to ensure that parents are not included.
 839          // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
 840          // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
 841          // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
 842          $haystacksuffix = $includeself ? '/+' : '+';
 843  
 844          $strpos = strpos(
 845              "+{$possiblechild->path}{$haystacksuffix}",
 846              "+{$this->path}/"
 847          );
 848          return $strpos === 0;
 849      }
 850  
 851      /**
 852       * Returns parent contexts of this context in reversed order, i.e. parent first,
 853       * then grand parent, etc.
 854       *
 855       * @param bool $includeself true means include self too
 856       * @return array of context instances
 857       */
 858      public function get_parent_contexts($includeself = false) {
 859          if (!$contextids = $this->get_parent_context_ids($includeself)) {
 860              return array();
 861          }
 862  
 863          // Preload the contexts to reduce DB calls.
 864          context_helper::preload_contexts_by_id($contextids);
 865  
 866          $result = array();
 867          foreach ($contextids as $contextid) {
 868              // Do NOT change this to self!
 869              $parent = context_helper::instance_by_id($contextid, MUST_EXIST);
 870              $result[$parent->id] = $parent;
 871          }
 872  
 873          return $result;
 874      }
 875  
 876      /**
 877       * Determine if the current context is a child of the possible parent.
 878       *
 879       * @param   context $possibleparent
 880       * @param   bool $includeself Whether to check the current context
 881       * @return  bool
 882       */
 883      public function is_child_of(context $possibleparent, bool $includeself): bool {
 884          // A simple substring check is used on the context path.
 885          // The current context is used as a haystack, with the possible parent as the needle.
 886          // The path is prefixed with '+' to ensure that the parent always starts at the top.
 887          // It is suffixed with '+' to ensure that children are not included.
 888          // The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
 889          // The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
 890          // The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
 891          $haystacksuffix = $includeself ? '/+' : '+';
 892  
 893          $strpos = strpos(
 894              "+{$this->path}{$haystacksuffix}",
 895              "+{$possibleparent->path}/"
 896          );
 897          return $strpos === 0;
 898      }
 899  
 900      /**
 901       * Returns parent context ids of this context in reversed order, i.e. parent first,
 902       * then grand parent, etc.
 903       *
 904       * @param bool $includeself true means include self too
 905       * @return array of context ids
 906       */
 907      public function get_parent_context_ids($includeself = false) {
 908          if (empty($this->_path)) {
 909              return array();
 910          }
 911  
 912          $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
 913          $parentcontexts = explode('/', $parentcontexts);
 914          if (!$includeself) {
 915              array_pop($parentcontexts); // And remove its own id.
 916          }
 917  
 918          return array_reverse($parentcontexts);
 919      }
 920  
 921      /**
 922       * Returns parent context paths of this context.
 923       *
 924       * @param bool $includeself true means include self too
 925       * @return array of context paths
 926       */
 927      public function get_parent_context_paths($includeself = false) {
 928          if (empty($this->_path)) {
 929              return array();
 930          }
 931  
 932          $contextids = explode('/', $this->_path);
 933  
 934          $path = '';
 935          $paths = array();
 936          foreach ($contextids as $contextid) {
 937              if ($contextid) {
 938                  $path .= '/' . $contextid;
 939                  $paths[$contextid] = $path;
 940              }
 941          }
 942  
 943          if (!$includeself) {
 944              unset($paths[$this->_id]);
 945          }
 946  
 947          return $paths;
 948      }
 949  
 950      /**
 951       * Returns parent context
 952       *
 953       * @return context|false
 954       */
 955      public function get_parent_context() {
 956          if (empty($this->_path) || $this->_id == SYSCONTEXTID) {
 957              return false;
 958          }
 959  
 960          $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
 961          $parentcontexts = explode('/', $parentcontexts);
 962          array_pop($parentcontexts); // Self.
 963          $contextid = array_pop($parentcontexts); // Immediate parent.
 964  
 965          // Do NOT change this to self!
 966          return context_helper::instance_by_id($contextid, MUST_EXIST);
 967      }
 968  
 969      /**
 970       * Is this context part of any course? If yes return course context.
 971       *
 972       * @param bool $strict true means throw exception if not found, false means return false if not found
 973       * @return context\course|false context of the enclosing course, null if not found or exception
 974       */
 975      public function get_course_context($strict = true) {
 976          if ($strict) {
 977              throw new coding_exception('Context does not belong to any course.');
 978          } else {
 979              return false;
 980          }
 981      }
 982  
 983      /**
 984       * Returns sql necessary for purging of stale context instances.
 985       *
 986       * @return string cleanup SQL
 987       */
 988      protected static function get_cleanup_sql() {
 989          throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels');
 990      }
 991  
 992      /**
 993       * Rebuild context paths and depths at context level.
 994       *
 995       * @param bool $force
 996       * @return void
 997       */
 998      protected static function build_paths($force) {
 999          throw new coding_exception('build_paths() method must be implemented in all context levels');
1000      }
1001  
1002      /**
1003       * Create missing context instances at given level
1004       *
1005       * @return void
1006       */
1007      protected static function create_level_instances() {
1008          throw new coding_exception('create_level_instances() method must be implemented in all context levels');
1009      }
1010  
1011      /**
1012       * Reset all cached permissions and definitions if the necessary.
1013       * @return void
1014       */
1015      public function reload_if_dirty() {
1016          global $ACCESSLIB_PRIVATE, $USER;
1017  
1018          // Load dirty contexts list if needed.
1019          if (CLI_SCRIPT) {
1020              if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1021                  // We do not load dirty flags in CLI and cron.
1022                  $ACCESSLIB_PRIVATE->dirtycontexts = array();
1023              }
1024          } else {
1025              if (!isset($USER->access['time'])) {
1026                  // Nothing has been loaded yet, so we do not need to check dirty flags now.
1027                  return;
1028              }
1029  
1030              // From skodak: No idea why -2 is there, server cluster time difference maybe...
1031              $changedsince = $USER->access['time'] - 2;
1032  
1033              if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1034                  $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $changedsince);
1035              }
1036  
1037              if (!isset($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
1038                  $ACCESSLIB_PRIVATE->dirtyusers[$USER->id] = get_cache_flag('accesslib/dirtyusers', $USER->id, $changedsince);
1039              }
1040          }
1041  
1042          $dirty = false;
1043  
1044          if (!empty($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
1045              $dirty = true;
1046          } else if (!empty($ACCESSLIB_PRIVATE->dirtycontexts)) {
1047              $paths = $this->get_parent_context_paths(true);
1048  
1049              foreach ($paths as $path) {
1050                  if (isset($ACCESSLIB_PRIVATE->dirtycontexts[$path])) {
1051                      $dirty = true;
1052                      break;
1053                  }
1054              }
1055          }
1056  
1057          if ($dirty) {
1058              // Reload all capabilities of USER and others - preserving loginas, roleswitches, etc.
1059              // Then cleanup any marks of dirtyness... at least from our short term memory!
1060              reload_all_capabilities();
1061          }
1062      }
1063  
1064      /**
1065       * Mark a context as dirty (with timestamp) so as to force reloading of the context.
1066       */
1067      public function mark_dirty() {
1068          global $CFG, $USER, $ACCESSLIB_PRIVATE;
1069  
1070          if (during_initial_install()) {
1071              return;
1072          }
1073  
1074          // Only if it is a non-empty string.
1075          if (is_string($this->_path) && $this->_path !== '') {
1076              set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time() + $CFG->sessiontimeout);
1077              if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1078                  $ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1;
1079              } else {
1080                  if (CLI_SCRIPT) {
1081                      $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
1082                  } else {
1083                      if (isset($USER->access['time'])) {
1084                          $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time'] - 2);
1085                      } else {
1086                          $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
1087                      }
1088                      // Flags not loaded yet, it will be done later in $context->reload_if_dirty().
1089                  }
1090              }
1091          }
1092      }
1093  }