mirror of
https://github.com/moodle/moodle.git
synced 2025-01-18 05:58:34 +01:00
1094 lines
36 KiB
PHP
1094 lines
36 KiB
PHP
<?php
|
|
// This file is part of Moodle - https://moodle.org/
|
|
//
|
|
// Moodle is free software: you can redistribute it and/or modify
|
|
// it under the terms of the GNU General Public License as published by
|
|
// the Free Software Foundation, either version 3 of the License, or
|
|
// (at your option) any later version.
|
|
//
|
|
// Moodle is distributed in the hope that it will be useful,
|
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
// GNU General Public License for more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License
|
|
// along with Moodle. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
namespace core;
|
|
|
|
use stdClass, IteratorAggregate, ArrayIterator;
|
|
use coding_exception, moodle_url;
|
|
|
|
/**
|
|
* Basic moodle context abstraction class.
|
|
*
|
|
* Google confirms that no other important framework is using "context" class,
|
|
* we could use something else like mcontext or moodle_context, but we need to type
|
|
* this very often which would be annoying and it would take too much space...
|
|
*
|
|
* This class is derived from stdClass for backwards compatibility with
|
|
* odl $context record that was returned from DML $DB->get_record()
|
|
*
|
|
* @package core_access
|
|
* @category access
|
|
* @copyright Petr Skoda
|
|
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
* @since Moodle 4.2
|
|
*
|
|
* @property-read int $id context id
|
|
* @property-read int $contextlevel CONTEXT_SYSTEM, CONTEXT_COURSE, etc.
|
|
* @property-read int $instanceid id of related instance in each context
|
|
* @property-read string $path path to context, starts with system context
|
|
* @property-read int $depth
|
|
* @property-read bool $locked true means write capabilities are ignored in this context or parents
|
|
*/
|
|
abstract class context extends stdClass implements IteratorAggregate {
|
|
|
|
/** @var string Default sorting of capabilities in {@see get_capabilities} */
|
|
protected const DEFAULT_CAPABILITY_SORT = 'contextlevel, component, name';
|
|
|
|
/**
|
|
* The context id
|
|
* Can be accessed publicly through $context->id
|
|
* @var int
|
|
*/
|
|
protected $_id;
|
|
|
|
/**
|
|
* The context level
|
|
* Can be accessed publicly through $context->contextlevel
|
|
* @var int One of CONTEXT_* e.g. CONTEXT_COURSE, CONTEXT_MODULE
|
|
*/
|
|
protected $_contextlevel;
|
|
|
|
/**
|
|
* Id of the item this context is related to e.g. COURSE_CONTEXT => course.id
|
|
* Can be accessed publicly through $context->instanceid
|
|
* @var int
|
|
*/
|
|
protected $_instanceid;
|
|
|
|
/**
|
|
* The path to the context always starting from the system context
|
|
* Can be accessed publicly through $context->path
|
|
* @var string
|
|
*/
|
|
protected $_path;
|
|
|
|
/**
|
|
* The depth of the context in relation to parent contexts
|
|
* Can be accessed publicly through $context->depth
|
|
* @var int
|
|
*/
|
|
protected $_depth;
|
|
|
|
/**
|
|
* Whether this context is locked or not.
|
|
*
|
|
* Can be accessed publicly through $context->locked.
|
|
*
|
|
* @var int
|
|
*/
|
|
protected $_locked;
|
|
|
|
/**
|
|
* @var array Context caching info
|
|
*/
|
|
private static $cache_contextsbyid = array();
|
|
|
|
/**
|
|
* @var array Context caching info
|
|
*/
|
|
private static $cache_contexts = array();
|
|
|
|
/**
|
|
* Context count
|
|
* Why do we do count contexts? Because count($array) is horribly slow for large arrays
|
|
* @var int
|
|
*/
|
|
protected static $cache_count = 0;
|
|
|
|
/**
|
|
* @var array Context caching info
|
|
*/
|
|
protected static $cache_preloaded = array();
|
|
|
|
/**
|
|
* @var context\system The system context once initialised
|
|
*/
|
|
protected static $systemcontext = null;
|
|
|
|
/**
|
|
* Returns short context name.
|
|
*
|
|
* @since Moodle 4.2
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function get_short_name(): string {
|
|
// NOTE: it would be more correct to make this abstract,
|
|
// unfortunately there are tests that attempt to mock context classes.
|
|
throw new \coding_exception('get_short_name() method must be overridden in custom context levels');
|
|
}
|
|
|
|
/**
|
|
* Resets the cache to remove all data.
|
|
*/
|
|
protected static function reset_caches() {
|
|
self::$cache_contextsbyid = array();
|
|
self::$cache_contexts = array();
|
|
self::$cache_count = 0;
|
|
self::$cache_preloaded = array();
|
|
|
|
self::$systemcontext = null;
|
|
}
|
|
|
|
/**
|
|
* Adds a context to the cache. If the cache is full, discards a batch of
|
|
* older entries.
|
|
*
|
|
* @param context $context New context to add
|
|
* @return void
|
|
*/
|
|
protected static function cache_add(context $context) {
|
|
if (isset(self::$cache_contextsbyid[$context->id])) {
|
|
// Already cached, no need to do anything - this is relatively cheap, we do all this because count() is slow.
|
|
return;
|
|
}
|
|
|
|
if (self::$cache_count >= CONTEXT_CACHE_MAX_SIZE) {
|
|
$i = 0;
|
|
foreach (self::$cache_contextsbyid as $ctx) {
|
|
$i++;
|
|
if ($i <= 100) {
|
|
// We want to keep the first contexts to be loaded on this page, hopefully they will be needed again later.
|
|
continue;
|
|
}
|
|
if ($i > (CONTEXT_CACHE_MAX_SIZE / 3)) {
|
|
// We remove oldest third of the contexts to make room for more contexts.
|
|
break;
|
|
}
|
|
unset(self::$cache_contextsbyid[$ctx->id]);
|
|
unset(self::$cache_contexts[$ctx->contextlevel][$ctx->instanceid]);
|
|
self::$cache_count--;
|
|
}
|
|
}
|
|
|
|
self::$cache_contexts[$context->contextlevel][$context->instanceid] = $context;
|
|
self::$cache_contextsbyid[$context->id] = $context;
|
|
self::$cache_count++;
|
|
}
|
|
|
|
/**
|
|
* Removes a context from the cache.
|
|
*
|
|
* @param context $context Context object to remove
|
|
* @return void
|
|
*/
|
|
protected static function cache_remove(context $context) {
|
|
if (!isset(self::$cache_contextsbyid[$context->id])) {
|
|
// Not cached, no need to do anything - this is relatively cheap, we do all this because count() is slow.
|
|
return;
|
|
}
|
|
unset(self::$cache_contexts[$context->contextlevel][$context->instanceid]);
|
|
unset(self::$cache_contextsbyid[$context->id]);
|
|
|
|
self::$cache_count--;
|
|
|
|
if (self::$cache_count < 0) {
|
|
self::$cache_count = 0;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets a context from the cache.
|
|
*
|
|
* @param int $contextlevel Context level
|
|
* @param int $instance Instance ID
|
|
* @return context|bool Context or false if not in cache
|
|
*/
|
|
protected static function cache_get($contextlevel, $instance) {
|
|
if (isset(self::$cache_contexts[$contextlevel][$instance])) {
|
|
return self::$cache_contexts[$contextlevel][$instance];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Gets a context from the cache based on its id.
|
|
*
|
|
* @param int $id Context ID
|
|
* @return context|bool Context or false if not in cache
|
|
*/
|
|
protected static function cache_get_by_id($id) {
|
|
if (isset(self::$cache_contextsbyid[$id])) {
|
|
return self::$cache_contextsbyid[$id];
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Preloads context information from db record and strips the cached info.
|
|
*
|
|
* @param stdClass $rec
|
|
* @return context|null (modifies $rec)
|
|
*/
|
|
protected static function preload_from_record(stdClass $rec) {
|
|
$notenoughdata = false;
|
|
$notenoughdata = $notenoughdata || empty($rec->ctxid);
|
|
$notenoughdata = $notenoughdata || empty($rec->ctxlevel);
|
|
$notenoughdata = $notenoughdata || !isset($rec->ctxinstance);
|
|
$notenoughdata = $notenoughdata || empty($rec->ctxpath);
|
|
$notenoughdata = $notenoughdata || empty($rec->ctxdepth);
|
|
$notenoughdata = $notenoughdata || !isset($rec->ctxlocked);
|
|
if ($notenoughdata) {
|
|
// The record does not have enough data, passed here repeatedly or context does not exist yet.
|
|
if (isset($rec->ctxid) && !isset($rec->ctxlocked)) {
|
|
debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
|
|
}
|
|
return null;
|
|
}
|
|
|
|
$record = (object) [
|
|
'id' => $rec->ctxid,
|
|
'contextlevel' => $rec->ctxlevel,
|
|
'instanceid' => $rec->ctxinstance,
|
|
'path' => $rec->ctxpath,
|
|
'depth' => $rec->ctxdepth,
|
|
'locked' => $rec->ctxlocked,
|
|
];
|
|
|
|
unset($rec->ctxid);
|
|
unset($rec->ctxlevel);
|
|
unset($rec->ctxinstance);
|
|
unset($rec->ctxpath);
|
|
unset($rec->ctxdepth);
|
|
unset($rec->ctxlocked);
|
|
|
|
return self::create_instance_from_record($record);
|
|
}
|
|
|
|
|
|
/* ====== magic methods ======= */
|
|
|
|
/**
|
|
* Magic setter method, we do not want anybody to modify properties from the outside
|
|
* @param string $name
|
|
* @param mixed $value
|
|
*/
|
|
public function __set($name, $value) {
|
|
debugging('Can not change context instance properties!');
|
|
}
|
|
|
|
/**
|
|
* Magic method getter, redirects to read only values.
|
|
* @param string $name
|
|
* @return mixed
|
|
*/
|
|
public function __get($name) {
|
|
switch ($name) {
|
|
case 'id':
|
|
return $this->_id;
|
|
case 'contextlevel':
|
|
return $this->_contextlevel;
|
|
case 'instanceid':
|
|
return $this->_instanceid;
|
|
case 'path':
|
|
return $this->_path;
|
|
case 'depth':
|
|
return $this->_depth;
|
|
case 'locked':
|
|
return $this->is_locked();
|
|
|
|
default:
|
|
debugging('Invalid context property accessed! '.$name);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Full support for isset on our magic read only properties.
|
|
* @param string $name
|
|
* @return bool
|
|
*/
|
|
public function __isset($name) {
|
|
switch ($name) {
|
|
case 'id':
|
|
return isset($this->_id);
|
|
case 'contextlevel':
|
|
return isset($this->_contextlevel);
|
|
case 'instanceid':
|
|
return isset($this->_instanceid);
|
|
case 'path':
|
|
return isset($this->_path);
|
|
case 'depth':
|
|
return isset($this->_depth);
|
|
case 'locked':
|
|
// Locked is always set.
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* All properties are read only, sorry.
|
|
* @param string $name
|
|
*/
|
|
public function __unset($name) {
|
|
debugging('Can not unset context instance properties!');
|
|
}
|
|
|
|
/* ====== implementing method from interface IteratorAggregate ====== */
|
|
|
|
/**
|
|
* Create an iterator because magic vars can't be seen by 'foreach'.
|
|
*
|
|
* Now we can convert context object to array using convert_to_array(),
|
|
* and feed it properly to json_encode().
|
|
*/
|
|
public function getIterator(): \Traversable {
|
|
$ret = array(
|
|
'id' => $this->id,
|
|
'contextlevel' => $this->contextlevel,
|
|
'instanceid' => $this->instanceid,
|
|
'path' => $this->path,
|
|
'depth' => $this->depth,
|
|
'locked' => $this->locked,
|
|
);
|
|
return new ArrayIterator($ret);
|
|
}
|
|
|
|
/* ====== general context methods ====== */
|
|
|
|
/**
|
|
* Constructor is protected so that devs are forced to
|
|
* use context_xxx::instance() or context::instance_by_id().
|
|
*
|
|
* @param stdClass $record
|
|
*/
|
|
protected function __construct(stdClass $record) {
|
|
$this->_id = (int)$record->id;
|
|
$this->_contextlevel = (int)$record->contextlevel;
|
|
$this->_instanceid = $record->instanceid;
|
|
$this->_path = $record->path;
|
|
$this->_depth = $record->depth;
|
|
|
|
if (isset($record->locked)) {
|
|
$this->_locked = $record->locked;
|
|
} else if (!during_initial_install() && !moodle_needs_upgrading()) {
|
|
debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* This function is also used to work around 'protected' keyword problems in context_helper.
|
|
*
|
|
* @param stdClass $record
|
|
* @return context instance
|
|
*/
|
|
protected static function create_instance_from_record(stdClass $record) {
|
|
$classname = context_helper::get_class_for_level($record->contextlevel);
|
|
|
|
if ($context = self::cache_get_by_id($record->id)) {
|
|
return $context;
|
|
}
|
|
|
|
$context = new $classname($record);
|
|
self::cache_add($context);
|
|
|
|
return $context;
|
|
}
|
|
|
|
/**
|
|
* Copy prepared new contexts from temp table to context table,
|
|
* we do this in db specific way for perf reasons only.
|
|
*/
|
|
protected static function merge_context_temp_table() {
|
|
global $DB;
|
|
|
|
/* MDL-11347:
|
|
* - mysql does not allow to use FROM in UPDATE statements
|
|
* - using two tables after UPDATE works in mysql, but might give unexpected
|
|
* results in pg 8 (depends on configuration)
|
|
* - using table alias in UPDATE does not work in pg < 8.2
|
|
*
|
|
* Different code for each database - mostly for performance reasons
|
|
*/
|
|
|
|
$dbfamily = $DB->get_dbfamily();
|
|
if ($dbfamily == 'mysql') {
|
|
$updatesql = "UPDATE {context} ct, {context_temp} temp
|
|
SET ct.path = temp.path,
|
|
ct.depth = temp.depth,
|
|
ct.locked = temp.locked
|
|
WHERE ct.id = temp.id";
|
|
} else if ($dbfamily == 'oracle') {
|
|
$updatesql = "UPDATE {context} ct
|
|
SET (ct.path, ct.depth, ct.locked) =
|
|
(SELECT temp.path, temp.depth, temp.locked
|
|
FROM {context_temp} temp
|
|
WHERE temp.id=ct.id)
|
|
WHERE EXISTS (SELECT 'x'
|
|
FROM {context_temp} temp
|
|
WHERE temp.id = ct.id)";
|
|
} else if ($dbfamily == 'postgres' || $dbfamily == 'mssql') {
|
|
$updatesql = "UPDATE {context}
|
|
SET path = temp.path,
|
|
depth = temp.depth,
|
|
locked = temp.locked
|
|
FROM {context_temp} temp
|
|
WHERE temp.id={context}.id";
|
|
} else {
|
|
// Sqlite and others.
|
|
$updatesql = "UPDATE {context}
|
|
SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id),
|
|
depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id),
|
|
locked = (SELECT locked FROM {context_temp} WHERE id = {context}.id)
|
|
WHERE id IN (SELECT id FROM {context_temp})";
|
|
}
|
|
|
|
$DB->execute($updatesql);
|
|
}
|
|
|
|
/**
|
|
* Get a context instance as an object, from a given context id.
|
|
*
|
|
* @param int $id context id
|
|
* @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
|
|
* MUST_EXIST means throw exception if no record found
|
|
* @return context|bool the context object or false if not found
|
|
*/
|
|
public static function instance_by_id($id, $strictness = MUST_EXIST) {
|
|
global $DB;
|
|
|
|
if (get_called_class() !== 'core\context' && get_called_class() !== 'core\context_helper') {
|
|
// Some devs might confuse context->id and instanceid, better prevent these mistakes completely.
|
|
throw new coding_exception('use only context::instance_by_id() for real context levels use ::instance() methods');
|
|
}
|
|
|
|
if ($id == SYSCONTEXTID) {
|
|
return context\system::instance(0, $strictness);
|
|
}
|
|
|
|
if (is_array($id) || is_object($id) || empty($id)) {
|
|
throw new coding_exception('Invalid context id specified context::instance_by_id()');
|
|
}
|
|
|
|
if ($context = self::cache_get_by_id($id)) {
|
|
return $context;
|
|
}
|
|
|
|
if ($record = $DB->get_record('context', array('id' => $id), '*', $strictness)) {
|
|
return self::create_instance_from_record($record);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Update context info after moving context in the tree structure.
|
|
*
|
|
* @param context $newparent
|
|
* @return void
|
|
*/
|
|
public function update_moved(context $newparent) {
|
|
global $DB;
|
|
|
|
$frompath = $this->_path;
|
|
$newpath = $newparent->path . '/' . $this->_id;
|
|
|
|
$trans = $DB->start_delegated_transaction();
|
|
|
|
$setdepth = '';
|
|
if (($newparent->depth + 1) != $this->_depth) {
|
|
$diff = $newparent->depth - $this->_depth + 1;
|
|
$setdepth = ", depth = depth + $diff";
|
|
}
|
|
$sql = "UPDATE {context}
|
|
SET path = ?
|
|
$setdepth
|
|
WHERE id = ?";
|
|
$params = array($newpath, $this->_id);
|
|
$DB->execute($sql, $params);
|
|
|
|
$this->_path = $newpath;
|
|
$this->_depth = $newparent->depth + 1;
|
|
|
|
$sql = "UPDATE {context}
|
|
SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath) + 1))."
|
|
$setdepth
|
|
WHERE path LIKE ?";
|
|
$params = array($newpath, "{$frompath}/%");
|
|
$DB->execute($sql, $params);
|
|
|
|
$this->mark_dirty();
|
|
|
|
self::reset_caches();
|
|
|
|
$trans->allow_commit();
|
|
}
|
|
|
|
/**
|
|
* Set whether this context has been locked or not.
|
|
*
|
|
* @param bool $locked
|
|
* @return $this
|
|
*/
|
|
public function set_locked(bool $locked) {
|
|
global $DB;
|
|
|
|
if ($this->_locked == $locked) {
|
|
return $this;
|
|
}
|
|
|
|
$this->_locked = $locked;
|
|
$DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]);
|
|
$this->mark_dirty();
|
|
|
|
if ($locked) {
|
|
$eventname = '\\core\\event\\context_locked';
|
|
} else {
|
|
$eventname = '\\core\\event\\context_unlocked';
|
|
}
|
|
$event = $eventname::create(['context' => $this, 'objectid' => $this->id]);
|
|
$event->trigger();
|
|
|
|
self::reset_caches();
|
|
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Remove all context path info and optionally rebuild it.
|
|
*
|
|
* @param bool $rebuild
|
|
* @return void
|
|
*/
|
|
public function reset_paths($rebuild = true) {
|
|
global $DB;
|
|
|
|
if ($this->_path) {
|
|
$this->mark_dirty();
|
|
}
|
|
$DB->set_field_select('context', 'depth', 0, "path LIKE '%/$this->_id/%'");
|
|
$DB->set_field_select('context', 'path', null, "path LIKE '%/$this->_id/%'");
|
|
if ($this->_contextlevel != CONTEXT_SYSTEM) {
|
|
$DB->set_field('context', 'depth', 0, array('id' => $this->_id));
|
|
$DB->set_field('context', 'path', null, array('id' => $this->_id));
|
|
$this->_depth = 0;
|
|
$this->_path = null;
|
|
}
|
|
|
|
if ($rebuild) {
|
|
context_helper::build_all_paths(false);
|
|
}
|
|
|
|
self::reset_caches();
|
|
}
|
|
|
|
/**
|
|
* Delete all data linked to content, do not delete the context record itself
|
|
*/
|
|
public function delete_content() {
|
|
global $CFG, $DB;
|
|
|
|
blocks_delete_all_for_context($this->_id);
|
|
filter_delete_all_for_context($this->_id);
|
|
|
|
require_once($CFG->dirroot . '/comment/lib.php');
|
|
\comment::delete_comments(array('contextid' => $this->_id));
|
|
|
|
require_once($CFG->dirroot.'/rating/lib.php');
|
|
$delopt = new stdclass();
|
|
$delopt->contextid = $this->_id;
|
|
$rm = new \rating_manager();
|
|
$rm->delete_ratings($delopt);
|
|
|
|
// Delete all files attached to this context.
|
|
$fs = get_file_storage();
|
|
$fs->delete_area_files($this->_id);
|
|
|
|
// Delete all repository instances attached to this context.
|
|
require_once($CFG->dirroot . '/repository/lib.php');
|
|
\repository::delete_all_for_context($this->_id);
|
|
|
|
// Delete all advanced grading data attached to this context.
|
|
require_once($CFG->dirroot.'/grade/grading/lib.php');
|
|
\grading_manager::delete_all_for_context($this->_id);
|
|
|
|
// Now delete stuff from role related tables, role_unassign_all
|
|
// and unenrol should be called earlier to do proper cleanup.
|
|
$DB->delete_records('role_assignments', array('contextid' => $this->_id));
|
|
$DB->delete_records('role_names', array('contextid' => $this->_id));
|
|
$this->delete_capabilities();
|
|
}
|
|
|
|
/**
|
|
* Unassign all capabilities from a context.
|
|
*/
|
|
public function delete_capabilities() {
|
|
global $DB;
|
|
|
|
$ids = $DB->get_fieldset_select('role_capabilities', 'DISTINCT roleid', 'contextid = ?', array($this->_id));
|
|
if ($ids) {
|
|
$DB->delete_records('role_capabilities', array('contextid' => $this->_id));
|
|
|
|
// Reset any cache of these roles, including MUC.
|
|
accesslib_clear_role_cache($ids);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete the context content and the context record itself
|
|
*/
|
|
public function delete() {
|
|
global $DB;
|
|
|
|
if ($this->_contextlevel <= CONTEXT_SYSTEM) {
|
|
throw new coding_exception('Cannot delete system context');
|
|
}
|
|
|
|
// Double check the context still exists.
|
|
if (!$DB->record_exists('context', array('id' => $this->_id))) {
|
|
self::cache_remove($this);
|
|
return;
|
|
}
|
|
|
|
$this->delete_content();
|
|
$DB->delete_records('context', array('id' => $this->_id));
|
|
// Purge static context cache if entry present.
|
|
self::cache_remove($this);
|
|
|
|
// Inform search engine to delete data related to this context.
|
|
\core_search\manager::context_deleted($this);
|
|
}
|
|
|
|
/* ====== context level related methods ====== */
|
|
|
|
/**
|
|
* Utility method for context creation
|
|
*
|
|
* @param int $contextlevel
|
|
* @param int $instanceid
|
|
* @param string $parentpath
|
|
* @return stdClass context record
|
|
*/
|
|
protected static function insert_context_record($contextlevel, $instanceid, $parentpath) {
|
|
global $DB;
|
|
|
|
$record = new stdClass();
|
|
$record->contextlevel = $contextlevel;
|
|
$record->instanceid = $instanceid;
|
|
$record->depth = 0;
|
|
$record->path = null; // Not known before insert.
|
|
$record->locked = 0;
|
|
|
|
$record->id = $DB->insert_record('context', $record);
|
|
|
|
// Now add path if known - it can be added later.
|
|
if (!is_null($parentpath)) {
|
|
$record->path = $parentpath.'/'.$record->id;
|
|
$record->depth = substr_count($record->path, '/');
|
|
$DB->update_record('context', $record);
|
|
}
|
|
|
|
return $record;
|
|
}
|
|
|
|
/**
|
|
* Returns human readable context identifier.
|
|
*
|
|
* @param boolean $withprefix whether to prefix the name of the context with the
|
|
* type of context, e.g. User, Course, Forum, etc.
|
|
* @param boolean $short whether to use the short name of the thing. Only applies
|
|
* to course contexts
|
|
* @param boolean $escape Whether the returned name of the thing is to be
|
|
* HTML escaped or not.
|
|
* @return string the human readable context name.
|
|
*/
|
|
public function get_context_name($withprefix = true, $short = false, $escape = true) {
|
|
// Must be implemented in all context levels.
|
|
throw new coding_exception('can not get name of abstract context');
|
|
}
|
|
|
|
/**
|
|
* Whether the current context is locked.
|
|
*
|
|
* @return bool
|
|
*/
|
|
public function is_locked() {
|
|
if ($this->_locked) {
|
|
return true;
|
|
}
|
|
|
|
if ($parent = $this->get_parent_context()) {
|
|
return $parent->is_locked();
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Returns the most relevant URL for this context.
|
|
*
|
|
* @return moodle_url
|
|
*/
|
|
abstract public function get_url();
|
|
|
|
/**
|
|
* Returns context instance database name.
|
|
*
|
|
* @return string|null table name for all levels except system.
|
|
*/
|
|
protected static function get_instance_table(): ?string {
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Returns list of columns that can be used from behat
|
|
* to look up context by reference.
|
|
*
|
|
* @return array list of column names from instance table
|
|
*/
|
|
protected static function get_behat_reference_columns(): array {
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Returns list of all role archetypes that are compatible
|
|
* with role assignments in context level.
|
|
* @since Moodle 4.2
|
|
*
|
|
* @return string[]
|
|
*/
|
|
protected static function get_compatible_role_archetypes(): array {
|
|
// Override if archetype roles should be allowed to be assigned in context level.
|
|
return [];
|
|
}
|
|
|
|
/**
|
|
* Returns list of all possible parent context levels,
|
|
* it may include itself if nesting is allowed.
|
|
* @since Moodle 4.2
|
|
*
|
|
* @return int[]
|
|
*/
|
|
public static function get_possible_parent_levels(): array {
|
|
// Override if other type of parents are expected.
|
|
return [context\system::LEVEL];
|
|
}
|
|
|
|
/**
|
|
* Returns array of relevant context capability records.
|
|
*
|
|
* @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display
|
|
* @return array
|
|
*/
|
|
abstract public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT);
|
|
|
|
/**
|
|
* Recursive function which, given a context, find all its children context ids.
|
|
*
|
|
* For course category contexts it will return immediate children and all subcategory contexts.
|
|
* It will NOT recurse into courses or subcategories categories.
|
|
* If you want to do that, call it on the returned courses/categories.
|
|
*
|
|
* When called for a course context, it will return the modules and blocks
|
|
* displayed in the course page and blocks displayed on the module pages.
|
|
*
|
|
* If called on a user/course/module context it _will_ populate the cache with the appropriate
|
|
* contexts ;-)
|
|
*
|
|
* @return array Array of child records
|
|
*/
|
|
public function get_child_contexts() {
|
|
global $DB;
|
|
|
|
if (empty($this->_path) || empty($this->_depth)) {
|
|
debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
|
|
return array();
|
|
}
|
|
|
|
$sql = "SELECT ctx.*
|
|
FROM {context} ctx
|
|
WHERE ctx.path LIKE ?";
|
|
$params = array($this->_path.'/%');
|
|
$records = $DB->get_records_sql($sql, $params);
|
|
|
|
$result = array();
|
|
foreach ($records as $record) {
|
|
$result[$record->id] = self::create_instance_from_record($record);
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Determine if the current context is a parent of the possible child.
|
|
*
|
|
* @param context $possiblechild
|
|
* @param bool $includeself Whether to check the current context
|
|
* @return bool
|
|
*/
|
|
public function is_parent_of(context $possiblechild, bool $includeself): bool {
|
|
// A simple substring check is used on the context path.
|
|
// The possible child's path is used as a haystack, with the current context as the needle.
|
|
// The path is prefixed with '+' to ensure that the parent always starts at the top.
|
|
// It is suffixed with '+' to ensure that parents are not included.
|
|
// The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
|
|
// The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
|
|
// The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
|
|
$haystacksuffix = $includeself ? '/+' : '+';
|
|
|
|
$strpos = strpos(
|
|
"+{$possiblechild->path}{$haystacksuffix}",
|
|
"+{$this->path}/"
|
|
);
|
|
return $strpos === 0;
|
|
}
|
|
|
|
/**
|
|
* Returns parent contexts of this context in reversed order, i.e. parent first,
|
|
* then grand parent, etc.
|
|
*
|
|
* @param bool $includeself true means include self too
|
|
* @return array of context instances
|
|
*/
|
|
public function get_parent_contexts($includeself = false) {
|
|
if (!$contextids = $this->get_parent_context_ids($includeself)) {
|
|
return array();
|
|
}
|
|
|
|
// Preload the contexts to reduce DB calls.
|
|
context_helper::preload_contexts_by_id($contextids);
|
|
|
|
$result = array();
|
|
foreach ($contextids as $contextid) {
|
|
// Do NOT change this to self!
|
|
$parent = context_helper::instance_by_id($contextid, MUST_EXIST);
|
|
$result[$parent->id] = $parent;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Determine if the current context is a child of the possible parent.
|
|
*
|
|
* @param context $possibleparent
|
|
* @param bool $includeself Whether to check the current context
|
|
* @return bool
|
|
*/
|
|
public function is_child_of(context $possibleparent, bool $includeself): bool {
|
|
// A simple substring check is used on the context path.
|
|
// The current context is used as a haystack, with the possible parent as the needle.
|
|
// The path is prefixed with '+' to ensure that the parent always starts at the top.
|
|
// It is suffixed with '+' to ensure that children are not included.
|
|
// The needle always suffixes with a '/' to ensure that the contextid uses a complete match (i.e. 142/ instead of 14).
|
|
// The haystack is suffixed with '/+' if $includeself is true to allow the current context to match.
|
|
// The haystack is suffixed with '+' if $includeself is false to prevent the current context from matching.
|
|
$haystacksuffix = $includeself ? '/+' : '+';
|
|
|
|
$strpos = strpos(
|
|
"+{$this->path}{$haystacksuffix}",
|
|
"+{$possibleparent->path}/"
|
|
);
|
|
return $strpos === 0;
|
|
}
|
|
|
|
/**
|
|
* Returns parent context ids of this context in reversed order, i.e. parent first,
|
|
* then grand parent, etc.
|
|
*
|
|
* @param bool $includeself true means include self too
|
|
* @return array of context ids
|
|
*/
|
|
public function get_parent_context_ids($includeself = false) {
|
|
if (empty($this->_path)) {
|
|
return array();
|
|
}
|
|
|
|
$parentcontexts = trim($this->_path, '/'); // Kill leading slash.
|
|
$parentcontexts = explode('/', $parentcontexts);
|
|
if (!$includeself) {
|
|
array_pop($parentcontexts); // And remove its own id.
|
|
}
|
|
|
|
return array_reverse($parentcontexts);
|
|
}
|
|
|
|
/**
|
|
* Returns parent context paths of this context.
|
|
*
|
|
* @param bool $includeself true means include self too
|
|
* @return array of context paths
|
|
*/
|
|
public function get_parent_context_paths($includeself = false) {
|
|
if (empty($this->_path)) {
|
|
return array();
|
|
}
|
|
|
|
$contextids = explode('/', $this->_path);
|
|
|
|
$path = '';
|
|
$paths = array();
|
|
foreach ($contextids as $contextid) {
|
|
if ($contextid) {
|
|
$path .= '/' . $contextid;
|
|
$paths[$contextid] = $path;
|
|
}
|
|
}
|
|
|
|
if (!$includeself) {
|
|
unset($paths[$this->_id]);
|
|
}
|
|
|
|
return $paths;
|
|
}
|
|
|
|
/**
|
|
* Returns parent context
|
|
*
|
|
* @return context|false
|
|
*/
|
|
public function get_parent_context() {
|
|
if (empty($this->_path) || $this->_id == SYSCONTEXTID) {
|
|
return false;
|
|
}
|
|
|
|
$parentcontexts = trim($this->_path, '/'); // Kill leading slash.
|
|
$parentcontexts = explode('/', $parentcontexts);
|
|
array_pop($parentcontexts); // Self.
|
|
$contextid = array_pop($parentcontexts); // Immediate parent.
|
|
|
|
// Do NOT change this to self!
|
|
return context_helper::instance_by_id($contextid, MUST_EXIST);
|
|
}
|
|
|
|
/**
|
|
* Is this context part of any course? If yes return course context.
|
|
*
|
|
* @param bool $strict true means throw exception if not found, false means return false if not found
|
|
* @return context\course|false context of the enclosing course, null if not found or exception
|
|
*/
|
|
public function get_course_context($strict = true) {
|
|
if ($strict) {
|
|
throw new coding_exception('Context does not belong to any course.');
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns sql necessary for purging of stale context instances.
|
|
*
|
|
* @return string cleanup SQL
|
|
*/
|
|
protected static function get_cleanup_sql() {
|
|
throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels');
|
|
}
|
|
|
|
/**
|
|
* Rebuild context paths and depths at context level.
|
|
*
|
|
* @param bool $force
|
|
* @return void
|
|
*/
|
|
protected static function build_paths($force) {
|
|
throw new coding_exception('build_paths() method must be implemented in all context levels');
|
|
}
|
|
|
|
/**
|
|
* Create missing context instances at given level
|
|
*
|
|
* @return void
|
|
*/
|
|
protected static function create_level_instances() {
|
|
throw new coding_exception('create_level_instances() method must be implemented in all context levels');
|
|
}
|
|
|
|
/**
|
|
* Reset all cached permissions and definitions if the necessary.
|
|
* @return void
|
|
*/
|
|
public function reload_if_dirty() {
|
|
global $ACCESSLIB_PRIVATE, $USER;
|
|
|
|
// Load dirty contexts list if needed.
|
|
if (CLI_SCRIPT) {
|
|
if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
|
|
// We do not load dirty flags in CLI and cron.
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = array();
|
|
}
|
|
} else {
|
|
if (!isset($USER->access['time'])) {
|
|
// Nothing has been loaded yet, so we do not need to check dirty flags now.
|
|
return;
|
|
}
|
|
|
|
// From skodak: No idea why -2 is there, server cluster time difference maybe...
|
|
$changedsince = $USER->access['time'] - 2;
|
|
|
|
if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $changedsince);
|
|
}
|
|
|
|
if (!isset($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
|
|
$ACCESSLIB_PRIVATE->dirtyusers[$USER->id] = get_cache_flag('accesslib/dirtyusers', $USER->id, $changedsince);
|
|
}
|
|
}
|
|
|
|
$dirty = false;
|
|
|
|
if (!empty($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
|
|
$dirty = true;
|
|
} else if (!empty($ACCESSLIB_PRIVATE->dirtycontexts)) {
|
|
$paths = $this->get_parent_context_paths(true);
|
|
|
|
foreach ($paths as $path) {
|
|
if (isset($ACCESSLIB_PRIVATE->dirtycontexts[$path])) {
|
|
$dirty = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($dirty) {
|
|
// Reload all capabilities of USER and others - preserving loginas, roleswitches, etc.
|
|
// Then cleanup any marks of dirtyness... at least from our short term memory!
|
|
reload_all_capabilities();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Mark a context as dirty (with timestamp) so as to force reloading of the context.
|
|
*/
|
|
public function mark_dirty() {
|
|
global $CFG, $USER, $ACCESSLIB_PRIVATE;
|
|
|
|
if (during_initial_install()) {
|
|
return;
|
|
}
|
|
|
|
// Only if it is a non-empty string.
|
|
if (is_string($this->_path) && $this->_path !== '') {
|
|
set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time() + $CFG->sessiontimeout);
|
|
if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
|
|
$ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1;
|
|
} else {
|
|
if (CLI_SCRIPT) {
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
|
|
} else {
|
|
if (isset($USER->access['time'])) {
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time'] - 2);
|
|
} else {
|
|
$ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
|
|
}
|
|
// Flags not loaded yet, it will be done later in $context->reload_if_dirty().
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|