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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body