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