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;
  20  use coding_exception;
  21  
  22  /**
  23   * Context maintenance and helper methods.
  24   *
  25   * This is "extends context" is a bloody hack that tires to work around the deficiencies
  26   * in the "protected" keyword in PHP, this helps us to hide all the internals of context
  27   * level implementation from the rest of code, the code completion returns what developers need.
  28   *
  29   * Thank you Tim Hunt for helping me with this nasty trick.
  30   *
  31   * @package   core_access
  32   * @category  access
  33   * @copyright Petr Skoda
  34   * @license   https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   * @since     Moodle 4.2
  36   */
  37  abstract class context_helper extends context {
  38  
  39      /**
  40       * @var array An array definitions of all context levels
  41       */
  42      private static $alllevels;
  43  
  44      /**
  45       * Reset internal context levels array.
  46       */
  47      public static function reset_levels() {
  48          self::$alllevels = null;
  49      }
  50  
  51      /**
  52       * Initialise context levels, call before using self::$alllevels.
  53       */
  54      private static function init_levels():void {
  55          global $CFG;
  56  
  57          if (isset(self::$alllevels)) {
  58              return;
  59          }
  60          self::$alllevels = array(
  61              CONTEXT_SYSTEM => \core\context\system::class,
  62              CONTEXT_USER => \core\context\user::class,
  63              CONTEXT_COURSECAT => \core\context\coursecat::class,
  64              CONTEXT_COURSE => \core\context\course::class,
  65              CONTEXT_MODULE => \core\context\module::class,
  66              CONTEXT_BLOCK => \core\context\block::class,
  67          );
  68  
  69          if (empty($CFG->custom_context_classes)) {
  70              return;
  71          }
  72  
  73          $levels = $CFG->custom_context_classes;
  74          if (!is_array($levels)) {
  75              $levels = @unserialize($levels);
  76          }
  77          if (!is_array($levels)) {
  78              debugging('Invalid $CFG->custom_context_classes detected, value ignored.', DEBUG_DEVELOPER);
  79              return;
  80          }
  81  
  82          // Unsupported custom levels, use with care!!!
  83          foreach ($levels as $level => $classname) {
  84              self::$alllevels[$level] = $classname;
  85          }
  86          ksort(self::$alllevels);
  87      }
  88  
  89      /**
  90       * Converts legacy context_* class name to new class name.
  91       *
  92       * NOTE: this is needed for external API which uses short context names.
  93       * @since Moodle 4.2
  94       *
  95       * @param int|string $extlevel
  96       * @return string|null context class name or null if not found
  97       */
  98      public static function parse_external_level($extlevel): ?string {
  99          self::init_levels();
 100          if (is_number($extlevel)) {
 101              if (isset(self::$alllevels[$extlevel])) {
 102                  return self::$alllevels[$extlevel];
 103              } else {
 104                  return null;
 105              }
 106          }
 107          if ($extlevel && is_string($extlevel)) {
 108              $found = null;
 109              foreach (self::$alllevels as $classname) {
 110                  if ($classname::get_short_name() === $extlevel) {
 111                      if ($found) {
 112                          debugging("Duplicate short context level name found '$extlevel', use numeric value instead",
 113                              DEBUG_DEVELOPER);
 114                      } else {
 115                          $found = $classname;
 116                      }
 117                  }
 118              }
 119              return $found;
 120          }
 121          return null;
 122      }
 123  
 124      /**
 125       * Resolve reference to context used in behat feature files.
 126       *
 127       * @param string $level
 128       * @param string $reference
 129       * @return context|null
 130       */
 131      public static function resolve_behat_reference(string $level, string $reference): ?context {
 132          global $DB;
 133  
 134          if (!PHPUNIT_TEST && !defined('BEHAT_SITE_RUNNING')) {
 135              throw new coding_exception('resolve_behat_reference() cannot be used outside of tests');
 136          }
 137          self::init_levels();
 138  
 139          $classname = null;
 140          if (is_number($level)) {
 141              if (isset(self::$alllevels[$level])) {
 142                  $classname = self::$alllevels[$level];
 143              }
 144          } else {
 145              foreach (self::$alllevels as $levelclassname) {
 146                  if ($level === $levelclassname::get_level_name()) {
 147                      $classname = $levelclassname;
 148                      break;
 149                  }
 150                  if ($level === $levelclassname::get_short_name()) {
 151                      $classname = $levelclassname;
 152                      break;
 153                  }
 154              }
 155          }
 156          if (!$classname) {
 157              return null;
 158          }
 159  
 160          if ($classname::LEVEL === context\system::LEVEL) {
 161              return context\system::instance();
 162          }
 163  
 164          if (trim($reference) === '') {
 165              return null;
 166          }
 167  
 168          $table = $classname::get_instance_table();
 169          if (!$table) {
 170              return null;
 171          }
 172  
 173          $columns = $classname::get_behat_reference_columns();
 174          foreach ($columns as $column) {
 175              $instance = $DB->get_record($table, [$column => $reference]);
 176              if ($instance) {
 177                  $context = $classname::instance($instance->id, IGNORE_MISSING);
 178                  if ($context) {
 179                      return $context;
 180                  }
 181                  return null;
 182              }
 183          }
 184  
 185          return null;
 186      }
 187  
 188      /**
 189       * Returns a class name of the context level class
 190       *
 191       * @param int $contextlevel (CONTEXT_SYSTEM, etc.)
 192       * @return string class name of the context class
 193       * @throws coding_exception if level does not exist
 194       */
 195      public static function get_class_for_level(int $contextlevel): string {
 196          self::init_levels();
 197          if (isset(self::$alllevels[$contextlevel])) {
 198              return self::$alllevels[$contextlevel];
 199          } else {
 200              throw new coding_exception('Invalid context level specified');
 201          }
 202      }
 203  
 204      /**
 205       * Returns a list of all context levels
 206       *
 207       * @return array int=>string (level=>level class name)
 208       */
 209      public static function get_all_levels(): array {
 210          self::init_levels();
 211          return self::$alllevels;
 212      }
 213  
 214      /**
 215       * Get list of possible child levels for given level.
 216       * @since Moodle 4.2
 217       *
 218       * @param int $parentlevel
 219       * @return int[] list of context levels that my be children of given context level.
 220       */
 221      public static function get_child_levels(int $parentlevel): array {
 222          self::init_levels();
 223          $result = [];
 224          $definitions = self::$alllevels;
 225  
 226          $recursion = function(int $pl) use (&$result, $definitions, &$recursion): void {
 227              foreach ($definitions as $contextlevel => $classname) {
 228                  $parentlevels = $classname::get_possible_parent_levels();
 229                  if (in_array($pl, $parentlevels)) {
 230                      if (isset($result[$contextlevel])) {
 231                          continue;
 232                      }
 233                      $result[$contextlevel] = $contextlevel;
 234                      $recursion($contextlevel);
 235                  }
 236              }
 237          };
 238          $recursion($parentlevel);
 239  
 240          $classname = self::get_class_for_level($parentlevel);
 241          $parentlevels = $classname::get_possible_parent_levels();
 242          if (!in_array($parentlevel, $parentlevels)) {
 243              unset($result[$parentlevel]);
 244          }
 245  
 246          return array_values($result);
 247      }
 248  
 249      /**
 250       * Returns context levels that compatible with role archetype assignments.
 251       * @since Moodle 4.2
 252       *
 253       * @param string $archetype
 254       * @return array
 255       */
 256      public static function get_compatible_levels(string $archetype): array {
 257          self::init_levels();
 258          $result = [];
 259  
 260          foreach (self::$alllevels as $contextlevel => $classname) {
 261              $compatiblearchetypes = $classname::get_compatible_role_archetypes();
 262              foreach ($compatiblearchetypes as $at) {
 263                  if ($at === $archetype) {
 264                      $result[] = $contextlevel;
 265                  }
 266              }
 267          }
 268  
 269          return $result;
 270      }
 271  
 272      /**
 273       * Remove stale contexts that belonged to deleted instances.
 274       * Ideally all code should cleanup contexts properly, unfortunately accidents happen...
 275       *
 276       * @return void
 277       */
 278      public static function cleanup_instances() {
 279          global $DB;
 280          self::init_levels();
 281  
 282          $sqls = array();
 283          foreach (self::$alllevels as $classname) {
 284              $sqls[] = $classname::get_cleanup_sql();
 285          }
 286  
 287          $sql = implode(" UNION ", $sqls);
 288  
 289          // It is probably better to use transactions, it might be faster too.
 290          $transaction = $DB->start_delegated_transaction();
 291  
 292          $rs = $DB->get_recordset_sql($sql);
 293          foreach ($rs as $record) {
 294              $context = context::create_instance_from_record($record);
 295              $context->delete();
 296          }
 297          $rs->close();
 298  
 299          $transaction->allow_commit();
 300      }
 301  
 302      /**
 303       * Create all context instances at the given level and above.
 304       *
 305       * @param int $contextlevel null means all levels
 306       * @param bool $buildpaths
 307       * @return void
 308       */
 309      public static function create_instances($contextlevel = null, $buildpaths = true) {
 310          self::init_levels();
 311          foreach (self::$alllevels as $level => $classname) {
 312              if ($contextlevel && $contextlevel != context\block::LEVEL && $level > $contextlevel) {
 313                  // Skip potential sub-contexts,
 314                  // in case of blocks build all contexts because plugin contexts may have higher levels.
 315                  continue;
 316              }
 317              $classname::create_level_instances();
 318              if ($buildpaths) {
 319                  $classname::build_paths(false);
 320              }
 321          }
 322      }
 323  
 324      /**
 325       * Rebuild paths and depths in all context levels.
 326       *
 327       * @param bool $force false means add missing only
 328       * @return void
 329       */
 330      public static function build_all_paths($force = false) {
 331          self::init_levels();
 332          foreach (self::$alllevels as $classname) {
 333              $classname::build_paths($force);
 334          }
 335  
 336          // Reset static course cache - it might have incorrect cached data.
 337          accesslib_clear_all_caches(true);
 338      }
 339  
 340      /**
 341       * Resets the cache to remove all data.
 342       */
 343      public static function reset_caches() {
 344          context::reset_caches();
 345      }
 346  
 347      /**
 348       * Returns all fields necessary for context preloading from user $rec.
 349       *
 350       * This helps with performance when dealing with hundreds of contexts.
 351       *
 352       * @param string $tablealias context table alias in the query
 353       * @return array (table.column=>alias, ...)
 354       */
 355      public static function get_preload_record_columns($tablealias) {
 356          return [
 357              "$tablealias.id" => "ctxid",
 358              "$tablealias.path" => "ctxpath",
 359              "$tablealias.depth" => "ctxdepth",
 360              "$tablealias.contextlevel" => "ctxlevel",
 361              "$tablealias.instanceid" => "ctxinstance",
 362              "$tablealias.locked" => "ctxlocked",
 363          ];
 364      }
 365  
 366      /**
 367       * Returns all fields necessary for context preloading from user $rec.
 368       *
 369       * This helps with performance when dealing with hundreds of contexts.
 370       *
 371       * @param string $tablealias context table alias in the query
 372       * @return string
 373       */
 374      public static function get_preload_record_columns_sql($tablealias) {
 375          return "$tablealias.id AS ctxid, " .
 376              "$tablealias.path AS ctxpath, " .
 377              "$tablealias.depth AS ctxdepth, " .
 378              "$tablealias.contextlevel AS ctxlevel, " .
 379              "$tablealias.instanceid AS ctxinstance, " .
 380              "$tablealias.locked AS ctxlocked";
 381      }
 382  
 383      /**
 384       * Preloads context cache with information from db record and strips the cached info.
 385       *
 386       * The db request has to contain all columns from context_helper::get_preload_record_columns().
 387       *
 388       * @param stdClass $rec
 389       * @return void This is intentional. See MDL-37115. You will need to get the context
 390       *      in the normal way, but it is now cached, so that will be fast.
 391       */
 392      public static function preload_from_record(stdClass $rec): void {
 393          context::preload_from_record($rec);
 394      }
 395  
 396      /**
 397       * Preload a set of contexts using their contextid.
 398       *
 399       * @param   array $contextids
 400       */
 401      public static function preload_contexts_by_id(array $contextids): void {
 402          global $DB;
 403  
 404          // Determine which contexts are not already cached.
 405          $tofetch = [];
 406          foreach ($contextids as $contextid) {
 407              if (!self::cache_get_by_id($contextid)) {
 408                  $tofetch[] = $contextid;
 409              }
 410          }
 411  
 412          if (count($tofetch) > 1) {
 413              // There are at least two to fetch.
 414              // There is no point only fetching a single context as this would be no more efficient than calling the existing code.
 415              list($insql, $inparams) = $DB->get_in_or_equal($tofetch, SQL_PARAMS_NAMED);
 416              $ctxs = $DB->get_records_select('context', "id {$insql}", $inparams, '',
 417                  self::get_preload_record_columns_sql('{context}'));
 418              foreach ($ctxs as $ctx) {
 419                  self::preload_from_record($ctx);
 420              }
 421          }
 422      }
 423  
 424      /**
 425       * Preload all contexts instances from course.
 426       *
 427       * To be used if you expect multiple queries for course activities...
 428       *
 429       * @param int $courseid
 430       */
 431      public static function preload_course($courseid) {
 432          // Users can call this multiple times without doing any harm.
 433          if (isset(context::$cache_preloaded[$courseid])) {
 434              return;
 435          }
 436          $coursecontext = context\course::instance($courseid);
 437          $coursecontext->get_child_contexts();
 438  
 439          context::$cache_preloaded[$courseid] = true;
 440      }
 441  
 442      /**
 443       * Delete context instance
 444       *
 445       * @param int $contextlevel
 446       * @param int $instanceid
 447       * @return void
 448       */
 449      public static function delete_instance($contextlevel, $instanceid) {
 450          global $DB;
 451  
 452          // Double check the context still exists.
 453          if ($record = $DB->get_record('context', array('contextlevel' => $contextlevel, 'instanceid' => $instanceid))) {
 454              $context = context::create_instance_from_record($record);
 455              $context->delete();
 456          }
 457      }
 458  
 459      /**
 460       * Returns the name of specified context level
 461       *
 462       * @param int $contextlevel
 463       * @return string name of the context level
 464       */
 465      public static function get_level_name($contextlevel) {
 466          $classname = self::get_class_for_level($contextlevel);
 467          return $classname::get_level_name();
 468      }
 469  
 470      /**
 471       * Gets the current context to be used for navigation tree filtering.
 472       *
 473       * @param context|null $context The current context to be checked against.
 474       * @return context|null the context that navigation tree filtering should use.
 475       */
 476      public static function get_navigation_filter_context(?context $context): ?context {
 477          global $CFG;
 478          if (!empty($CFG->filternavigationwithsystemcontext)) {
 479              return context\system::instance();
 480          } else {
 481              return $context;
 482          }
 483      }
 484  }