MDL-51360 core_grades: Improve documentation of grade_get_grades().
[moodle.git] / lib / classes / context.php
blob34f42fe8799689982977f2848d3c123520c7b26e
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.
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/>.
17 namespace core;
19 use stdClass, IteratorAggregate, ArrayIterator;
20 use coding_exception, moodle_url;
22 /**
23 * Basic moodle context abstraction class.
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...
29 * This class is derived from stdClass for backwards compatibility with
30 * odl $context record that was returned from DML $DB->get_record()
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
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
45 abstract class context extends stdClass implements IteratorAggregate {
47 /** @var string Default sorting of capabilities in {@see get_capabilities} */
48 protected const DEFAULT_CAPABILITY_SORT = 'contextlevel, component, name';
50 /**
51 * The context id
52 * Can be accessed publicly through $context->id
53 * @var int
55 protected $_id;
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
62 protected $_contextlevel;
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
69 protected $_instanceid;
71 /**
72 * The path to the context always starting from the system context
73 * Can be accessed publicly through $context->path
74 * @var string
76 protected $_path;
78 /**
79 * The depth of the context in relation to parent contexts
80 * Can be accessed publicly through $context->depth
81 * @var int
83 protected $_depth;
85 /**
86 * Whether this context is locked or not.
88 * Can be accessed publicly through $context->locked.
90 * @var int
92 protected $_locked;
94 /**
95 * @var array Context caching info
97 private static $cache_contextsbyid = array();
99 /**
100 * @var array Context caching info
102 private static $cache_contexts = array();
105 * Context count
106 * Why do we do count contexts? Because count($array) is horribly slow for large arrays
107 * @var int
109 protected static $cache_count = 0;
112 * @var array Context caching info
114 protected static $cache_preloaded = array();
117 * @var context\system The system context once initialised
119 protected static $systemcontext = null;
122 * Returns short context name.
124 * @since Moodle 4.2
126 * @return string
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');
135 * Resets the cache to remove all data.
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();
143 self::$systemcontext = null;
147 * Adds a context to the cache. If the cache is full, discards a batch of
148 * older entries.
150 * @param context $context New context to add
151 * @return void
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;
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;
167 if ($i > (CONTEXT_CACHE_MAX_SIZE / 3)) {
168 // We remove oldest third of the contexts to make room for more contexts.
169 break;
171 unset(self::$cache_contextsbyid[$ctx->id]);
172 unset(self::$cache_contexts[$ctx->contextlevel][$ctx->instanceid]);
173 self::$cache_count--;
177 self::$cache_contexts[$context->contextlevel][$context->instanceid] = $context;
178 self::$cache_contextsbyid[$context->id] = $context;
179 self::$cache_count++;
183 * Removes a context from the cache.
185 * @param context $context Context object to remove
186 * @return void
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;
193 unset(self::$cache_contexts[$context->contextlevel][$context->instanceid]);
194 unset(self::$cache_contextsbyid[$context->id]);
196 self::$cache_count--;
198 if (self::$cache_count < 0) {
199 self::$cache_count = 0;
204 * Gets a context from the cache.
206 * @param int $contextlevel Context level
207 * @param int $instance Instance ID
208 * @return context|bool Context or false if not in cache
210 protected static function cache_get($contextlevel, $instance) {
211 if (isset(self::$cache_contexts[$contextlevel][$instance])) {
212 return self::$cache_contexts[$contextlevel][$instance];
214 return false;
218 * Gets a context from the cache based on its id.
220 * @param int $id Context ID
221 * @return context|bool Context or false if not in cache
223 protected static function cache_get_by_id($id) {
224 if (isset(self::$cache_contextsbyid[$id])) {
225 return self::$cache_contextsbyid[$id];
227 return false;
231 * Preloads context information from db record and strips the cached info.
233 * @param stdClass $rec
234 * @return context|null (modifies $rec)
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);
249 return null;
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,
261 unset($rec->ctxid);
262 unset($rec->ctxlevel);
263 unset($rec->ctxinstance);
264 unset($rec->ctxpath);
265 unset($rec->ctxdepth);
266 unset($rec->ctxlocked);
268 return self::create_instance_from_record($record);
272 /* ====== magic methods ======= */
275 * Magic setter method, we do not want anybody to modify properties from the outside
276 * @param string $name
277 * @param mixed $value
279 public function __set($name, $value) {
280 debugging('Can not change context instance properties!');
284 * Magic method getter, redirects to read only values.
285 * @param string $name
286 * @return mixed
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();
303 default:
304 debugging('Invalid context property accessed! '.$name);
305 return null;
310 * Full support for isset on our magic read only properties.
311 * @param string $name
312 * @return bool
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;
335 * All properties are read only, sorry.
336 * @param string $name
338 public function __unset($name) {
339 debugging('Can not unset context instance properties!');
342 /* ====== implementing method from interface IteratorAggregate ====== */
345 * Create an iterator because magic vars can't be seen by 'foreach'.
347 * Now we can convert context object to array using convert_to_array(),
348 * and feed it properly to json_encode().
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,
359 return new ArrayIterator($ret);
362 /* ====== general context methods ====== */
365 * Constructor is protected so that devs are forced to
366 * use context_xxx::instance() or context::instance_by_id().
368 * @param stdClass $record
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;
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);
385 * This function is also used to work around 'protected' keyword problems in context_helper.
387 * @param stdClass $record
388 * @return context instance
390 protected static function create_instance_from_record(stdClass $record) {
391 $classname = context_helper::get_class_for_level($record->contextlevel);
393 if ($context = self::cache_get_by_id($record->id)) {
394 return $context;
397 $context = new $classname($record);
398 self::cache_add($context);
400 return $context;
404 * Copy prepared new contexts from temp table to context table,
405 * we do this in db specific way for perf reasons only.
407 protected static function merge_context_temp_table() {
408 global $DB;
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
416 * Different code for each database - mostly for performance reasons
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})";
451 $DB->execute($updatesql);
455 * Get a context instance as an object, from a given context id.
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
462 public static function instance_by_id($id, $strictness = MUST_EXIST) {
463 global $DB;
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');
470 if ($id == SYSCONTEXTID) {
471 return context\system::instance(0, $strictness);
474 if (is_array($id) || is_object($id) || empty($id)) {
475 throw new coding_exception('Invalid context id specified context::instance_by_id()');
478 if ($context = self::cache_get_by_id($id)) {
479 return $context;
482 if ($record = $DB->get_record('context', array('id' => $id), '*', $strictness)) {
483 return self::create_instance_from_record($record);
486 return false;
490 * Update context info after moving context in the tree structure.
492 * @param context $newparent
493 * @return void
495 public function update_moved(context $newparent) {
496 global $DB;
498 $frompath = $this->_path;
499 $newpath = $newparent->path . '/' . $this->_id;
501 $trans = $DB->start_delegated_transaction();
503 $setdepth = '';
504 if (($newparent->depth + 1) != $this->_depth) {
505 $diff = $newparent->depth - $this->_depth + 1;
506 $setdepth = ", depth = depth + $diff";
508 $sql = "UPDATE {context}
509 SET path = ?
510 $setdepth
511 WHERE id = ?";
512 $params = array($newpath, $this->_id);
513 $DB->execute($sql, $params);
515 $this->_path = $newpath;
516 $this->_depth = $newparent->depth + 1;
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);
525 $this->mark_dirty();
527 self::reset_caches();
529 $trans->allow_commit();
533 * Set whether this context has been locked or not.
535 * @param bool $locked
536 * @return $this
538 public function set_locked(bool $locked) {
539 global $DB;
541 if ($this->_locked == $locked) {
542 return $this;
545 $this->_locked = $locked;
546 $DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]);
547 $this->mark_dirty();
549 if ($locked) {
550 $eventname = '\\core\\event\\context_locked';
551 } else {
552 $eventname = '\\core\\event\\context_unlocked';
554 $event = $eventname::create(['context' => $this, 'objectid' => $this->id]);
555 $event->trigger();
557 self::reset_caches();
559 return $this;
563 * Remove all context path info and optionally rebuild it.
565 * @param bool $rebuild
566 * @return void
568 public function reset_paths($rebuild = true) {
569 global $DB;
571 if ($this->_path) {
572 $this->mark_dirty();
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;
583 if ($rebuild) {
584 context_helper::build_all_paths(false);
587 self::reset_caches();
591 * Delete all data linked to content, do not delete the context record itself
593 public function delete_content() {
594 global $CFG, $DB;
596 blocks_delete_all_for_context($this->_id);
597 filter_delete_all_for_context($this->_id);
599 require_once($CFG->dirroot . '/comment/lib.php');
600 \comment::delete_comments(array('contextid' => $this->_id));
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);
608 // Delete all files attached to this context.
609 $fs = get_file_storage();
610 $fs->delete_area_files($this->_id);
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);
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);
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();
628 * Unassign all capabilities from a context.
630 public function delete_capabilities() {
631 global $DB;
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));
637 // Reset any cache of these roles, including MUC.
638 accesslib_clear_role_cache($ids);
643 * Delete the context content and the context record itself
645 public function delete() {
646 global $DB;
648 if ($this->_contextlevel <= CONTEXT_SYSTEM) {
649 throw new coding_exception('Cannot delete system context');
652 // Double check the context still exists.
653 if (!$DB->record_exists('context', array('id' => $this->_id))) {
654 self::cache_remove($this);
655 return;
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);
663 // Inform search engine to delete data related to this context.
664 \core_search\manager::context_deleted($this);
667 /* ====== context level related methods ====== */
670 * Utility method for context creation
672 * @param int $contextlevel
673 * @param int $instanceid
674 * @param string $parentpath
675 * @return stdClass context record
677 protected static function insert_context_record($contextlevel, $instanceid, $parentpath) {
678 global $DB;
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;
687 $record->id = $DB->insert_record('context', $record);
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);
696 return $record;
700 * Returns human readable context identifier.
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.
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');
716 * Whether the current context is locked.
718 * @return bool
720 public function is_locked() {
721 if ($this->_locked) {
722 return true;
725 if ($parent = $this->get_parent_context()) {
726 return $parent->is_locked();
729 return false;
733 * Returns the most relevant URL for this context.
735 * @return moodle_url
737 abstract public function get_url();
740 * Returns context instance database name.
742 * @return string|null table name for all levels except system.
744 protected static function get_instance_table(): ?string {
745 return null;
749 * Returns list of columns that can be used from behat
750 * to look up context by reference.
752 * @return array list of column names from instance table
754 protected static function get_behat_reference_columns(): array {
755 return [];
759 * Returns list of all role archetypes that are compatible
760 * with role assignments in context level.
761 * @since Moodle 4.2
763 * @return string[]
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 [];
771 * Returns list of all possible parent context levels,
772 * it may include itself if nesting is allowed.
773 * @since Moodle 4.2
775 * @return int[]
777 public static function get_possible_parent_levels(): array {
778 // Override if other type of parents are expected.
779 return [context\system::LEVEL];
783 * Returns array of relevant context capability records.
785 * @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display
786 * @return array
788 abstract public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT);
791 * Recursive function which, given a context, find all its children context ids.
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.
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.
800 * If called on a user/course/module context it _will_ populate the cache with the appropriate
801 * contexts ;-)
803 * @return array Array of child records
805 public function get_child_contexts() {
806 global $DB;
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();
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);
819 $result = array();
820 foreach ($records as $record) {
821 $result[$record->id] = self::create_instance_from_record($record);
824 return $result;
828 * Determine if the current context is a parent of the possible child.
830 * @param context $possiblechild
831 * @param bool $includeself Whether to check the current context
832 * @return bool
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 ? '/+' : '+';
844 $strpos = strpos(
845 "+{$possiblechild->path}{$haystacksuffix}",
846 "+{$this->path}/"
848 return $strpos === 0;
852 * Returns parent contexts of this context in reversed order, i.e. parent first,
853 * then grand parent, etc.
855 * @param bool $includeself true means include self too
856 * @return array of context instances
858 public function get_parent_contexts($includeself = false) {
859 if (!$contextids = $this->get_parent_context_ids($includeself)) {
860 return array();
863 // Preload the contexts to reduce DB calls.
864 context_helper::preload_contexts_by_id($contextids);
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;
873 return $result;
877 * Determine if the current context is a child of the possible parent.
879 * @param context $possibleparent
880 * @param bool $includeself Whether to check the current context
881 * @return bool
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 ? '/+' : '+';
893 $strpos = strpos(
894 "+{$this->path}{$haystacksuffix}",
895 "+{$possibleparent->path}/"
897 return $strpos === 0;
901 * Returns parent context ids of this context in reversed order, i.e. parent first,
902 * then grand parent, etc.
904 * @param bool $includeself true means include self too
905 * @return array of context ids
907 public function get_parent_context_ids($includeself = false) {
908 if (empty($this->_path)) {
909 return array();
912 $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
913 $parentcontexts = explode('/', $parentcontexts);
914 if (!$includeself) {
915 array_pop($parentcontexts); // And remove its own id.
918 return array_reverse($parentcontexts);
922 * Returns parent context paths of this context.
924 * @param bool $includeself true means include self too
925 * @return array of context paths
927 public function get_parent_context_paths($includeself = false) {
928 if (empty($this->_path)) {
929 return array();
932 $contextids = explode('/', $this->_path);
934 $path = '';
935 $paths = array();
936 foreach ($contextids as $contextid) {
937 if ($contextid) {
938 $path .= '/' . $contextid;
939 $paths[$contextid] = $path;
943 if (!$includeself) {
944 unset($paths[$this->_id]);
947 return $paths;
951 * Returns parent context
953 * @return context|false
955 public function get_parent_context() {
956 if (empty($this->_path) || $this->_id == SYSCONTEXTID) {
957 return false;
960 $parentcontexts = trim($this->_path, '/'); // Kill leading slash.
961 $parentcontexts = explode('/', $parentcontexts);
962 array_pop($parentcontexts); // Self.
963 $contextid = array_pop($parentcontexts); // Immediate parent.
965 // Do NOT change this to self!
966 return context_helper::instance_by_id($contextid, MUST_EXIST);
970 * Is this context part of any course? If yes return course context.
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
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;
984 * Returns sql necessary for purging of stale context instances.
986 * @return string cleanup SQL
988 protected static function get_cleanup_sql() {
989 throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels');
993 * Rebuild context paths and depths at context level.
995 * @param bool $force
996 * @return void
998 protected static function build_paths($force) {
999 throw new coding_exception('build_paths() method must be implemented in all context levels');
1003 * Create missing context instances at given level
1005 * @return void
1007 protected static function create_level_instances() {
1008 throw new coding_exception('create_level_instances() method must be implemented in all context levels');
1012 * Reset all cached permissions and definitions if the necessary.
1013 * @return void
1015 public function reload_if_dirty() {
1016 global $ACCESSLIB_PRIVATE, $USER;
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();
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;
1030 // From skodak: No idea why -2 is there, server cluster time difference maybe...
1031 $changedsince = $USER->access['time'] - 2;
1033 if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
1034 $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $changedsince);
1037 if (!isset($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
1038 $ACCESSLIB_PRIVATE->dirtyusers[$USER->id] = get_cache_flag('accesslib/dirtyusers', $USER->id, $changedsince);
1042 $dirty = false;
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);
1049 foreach ($paths as $path) {
1050 if (isset($ACCESSLIB_PRIVATE->dirtycontexts[$path])) {
1051 $dirty = true;
1052 break;
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();
1065 * Mark a context as dirty (with timestamp) so as to force reloading of the context.
1067 public function mark_dirty() {
1068 global $CFG, $USER, $ACCESSLIB_PRIVATE;
1070 if (during_initial_install()) {
1071 return;
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);
1088 // Flags not loaded yet, it will be done later in $context->reload_if_dirty().