From 728c3c4bd1102d86a1b7867b2cc542cf5f4a725e Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Thu, 2 Jun 2022 10:33:33 +0200 Subject: [PATCH 1/2] MDL-74936 core: namespace all context class --- lib/accesslib.php | 2792 +---------------------------- lib/classes/context.php | 1047 +++++++++++ lib/classes/context/block.php | 246 +++ lib/classes/context/course.php | 250 +++ lib/classes/context/coursecat.php | 256 +++ lib/classes/context/module.php | 301 ++++ lib/classes/context/system.php | 275 +++ lib/classes/context/user.php | 204 +++ lib/classes/context_helper.php | 361 ++++ 9 files changed, 2952 insertions(+), 2780 deletions(-) create mode 100644 lib/classes/context.php create mode 100644 lib/classes/context/block.php create mode 100644 lib/classes/context/course.php create mode 100644 lib/classes/context/coursecat.php create mode 100644 lib/classes/context/module.php create mode 100644 lib/classes/context/system.php create mode 100644 lib/classes/context/user.php create mode 100644 lib/classes/context_helper.php diff --git a/lib/accesslib.php b/lib/accesslib.php index 9ea453a07c2..9326a794e1c 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -5024,2786 +5024,6 @@ function role_change_permission($roleid, $context, $capname, $permission) { assign_capability($capname, $permission, $roleid, $context->id, true); } - -/** - * 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 {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.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 - */ -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; - - /** - * Resets the cache to remove all data. - * @static - */ - 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. - * - * @static - * @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. - * - * @static - * @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. - * - * @static - * @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. - * - * @static - * @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. - * - * @static - * @param stdClass $rec - * @return void (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; - } - - $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 context::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. - * @static - * @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 = context::cache_get_by_id($record->id)) { - return $context; - } - - $context = new $classname($record); - context::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. - * @static - */ - 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' or $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. - * - * @static - * @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() !== 'context' and get_called_class() !== '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) or is_object($id) or empty($id)) { - throw new coding_exception('Invalid context id specified context::instance_by_id()'); - } - - if ($context = context::cache_get_by_id($id)) { - return $context; - } - - if ($record = $DB->get_record('context', array('id'=>$id), '*', $strictness)) { - return context::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(); - - context::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); - } - - context::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))) { - context::cache_remove($this); - return; - } - - $this->delete_content(); - $DB->delete_records('context', array('id'=>$this->_id)); - // purge static context cache if entry present - context::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 - * - * @static - * @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 - */ - public abstract function get_url(); - - /** - * Returns array of relevant context capability records. - * - * @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display - * @return array - */ - public abstract 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) or 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] = context::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) { - $parent = context::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 - */ - public function get_parent_context() { - if (empty($this->_path) or $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 - - return context::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 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. - * - * @static - * @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. - * - * @static - * @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 - * - * @static - * @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() - } - } - } - } -} - - -/** - * Context maintenance and helper methods. - * - * This is "extends context" is a bloody hack that tires to work around the deficiencies - * in the "protected" keyword in PHP, this helps us to hide all the internals of context - * level implementation from the rest of code, the code completion returns what developers need. - * - * Thank you Tim Hunt for helping me with this nasty trick. - * - * @package core_access - * @category access - * @copyright Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -class context_helper extends context { - - /** - * @var array An array mapping context levels to classes - */ - private static $alllevels; - - /** - * Instance does not make sense here, only static use - */ - protected function __construct() { - } - - /** - * Reset internal context levels array. - */ - public static function reset_levels() { - self::$alllevels = null; - } - - /** - * Initialise context levels, call before using self::$alllevels. - */ - private static function init_levels() { - global $CFG; - - if (isset(self::$alllevels)) { - return; - } - self::$alllevels = array( - CONTEXT_SYSTEM => 'context_system', - CONTEXT_USER => 'context_user', - CONTEXT_COURSECAT => 'context_coursecat', - CONTEXT_COURSE => 'context_course', - CONTEXT_MODULE => 'context_module', - CONTEXT_BLOCK => 'context_block', - ); - - if (empty($CFG->custom_context_classes)) { - return; - } - - $levels = $CFG->custom_context_classes; - if (!is_array($levels)) { - $levels = @unserialize($levels); - } - if (!is_array($levels)) { - debugging('Invalid $CFG->custom_context_classes detected, value ignored.', DEBUG_DEVELOPER); - return; - } - - // Unsupported custom levels, use with care!!! - foreach ($levels as $level => $classname) { - self::$alllevels[$level] = $classname; - } - ksort(self::$alllevels); - } - - /** - * Returns a class name of the context level class - * - * @static - * @param int $contextlevel (CONTEXT_SYSTEM, etc.) - * @return string class name of the context class - */ - public static function get_class_for_level($contextlevel) { - self::init_levels(); - if (isset(self::$alllevels[$contextlevel])) { - return self::$alllevels[$contextlevel]; - } else { - throw new coding_exception('Invalid context level specified'); - } - } - - /** - * Returns a list of all context levels - * - * @static - * @return array int=>string (level=>level class name) - */ - public static function get_all_levels() { - self::init_levels(); - return self::$alllevels; - } - - /** - * Remove stale contexts that belonged to deleted instances. - * Ideally all code should cleanup contexts properly, unfortunately accidents happen... - * - * @static - * @return void - */ - public static function cleanup_instances() { - global $DB; - self::init_levels(); - - $sqls = array(); - foreach (self::$alllevels as $level=>$classname) { - $sqls[] = $classname::get_cleanup_sql(); - } - - $sql = implode(" UNION ", $sqls); - - // it is probably better to use transactions, it might be faster too - $transaction = $DB->start_delegated_transaction(); - - $rs = $DB->get_recordset_sql($sql); - foreach ($rs as $record) { - $context = context::create_instance_from_record($record); - $context->delete(); - } - $rs->close(); - - $transaction->allow_commit(); - } - - /** - * Create all context instances at the given level and above. - * - * @static - * @param int $contextlevel null means all levels - * @param bool $buildpaths - * @return void - */ - public static function create_instances($contextlevel = null, $buildpaths = true) { - self::init_levels(); - foreach (self::$alllevels as $level=>$classname) { - if ($contextlevel and $level > $contextlevel) { - // skip potential sub-contexts - continue; - } - $classname::create_level_instances(); - if ($buildpaths) { - $classname::build_paths(false); - } - } - } - - /** - * Rebuild paths and depths in all context levels. - * - * @static - * @param bool $force false means add missing only - * @return void - */ - public static function build_all_paths($force = false) { - self::init_levels(); - foreach (self::$alllevels as $classname) { - $classname::build_paths($force); - } - - // reset static course cache - it might have incorrect cached data - accesslib_clear_all_caches(true); - } - - /** - * Resets the cache to remove all data. - * @static - */ - public static function reset_caches() { - context::reset_caches(); - } - - /** - * Returns all fields necessary for context preloading from user $rec. - * - * This helps with performance when dealing with hundreds of contexts. - * - * @static - * @param string $tablealias context table alias in the query - * @return array (table.column=>alias, ...) - */ - public static function get_preload_record_columns($tablealias) { - return [ - "$tablealias.id" => "ctxid", - "$tablealias.path" => "ctxpath", - "$tablealias.depth" => "ctxdepth", - "$tablealias.contextlevel" => "ctxlevel", - "$tablealias.instanceid" => "ctxinstance", - "$tablealias.locked" => "ctxlocked", - ]; - } - - /** - * Returns all fields necessary for context preloading from user $rec. - * - * This helps with performance when dealing with hundreds of contexts. - * - * @static - * @param string $tablealias context table alias in the query - * @return string - */ - public static function get_preload_record_columns_sql($tablealias) { - return "$tablealias.id AS ctxid, " . - "$tablealias.path AS ctxpath, " . - "$tablealias.depth AS ctxdepth, " . - "$tablealias.contextlevel AS ctxlevel, " . - "$tablealias.instanceid AS ctxinstance, " . - "$tablealias.locked AS ctxlocked"; - } - - /** - * Preloads context cache with information from db record and strips the cached info. - * - * The db request has to contain all columns from context_helper::get_preload_record_columns(). - * - * @static - * @param stdClass $rec - * @return void This is intentional. See MDL-37115. You will need to get the context - * in the normal way, but it is now cached, so that will be fast. - */ - public static function preload_from_record(stdClass $rec) { - context::preload_from_record($rec); - } - - /** - * Preload a set of contexts using their contextid. - * - * @param array $contextids - */ - public static function preload_contexts_by_id(array $contextids) { - global $DB; - - // Determine which contexts are not already cached. - $tofetch = []; - foreach ($contextids as $contextid) { - if (!self::cache_get_by_id($contextid)) { - $tofetch[] = $contextid; - } - } - - if (count($tofetch) > 1) { - // There are at least two to fetch. - // There is no point only fetching a single context as this would be no more efficient than calling the existing code. - list($insql, $inparams) = $DB->get_in_or_equal($tofetch, SQL_PARAMS_NAMED); - $ctxs = $DB->get_records_select('context', "id {$insql}", $inparams, '', - \context_helper::get_preload_record_columns_sql('{context}')); - foreach ($ctxs as $ctx) { - self::preload_from_record($ctx); - } - } - } - - /** - * Preload all contexts instances from course. - * - * To be used if you expect multiple queries for course activities... - * - * @static - * @param int $courseid - */ - public static function preload_course($courseid) { - // Users can call this multiple times without doing any harm - if (isset(context::$cache_preloaded[$courseid])) { - return; - } - $coursecontext = context_course::instance($courseid); - $coursecontext->get_child_contexts(); - - context::$cache_preloaded[$courseid] = true; - } - - /** - * Delete context instance - * - * @static - * @param int $contextlevel - * @param int $instanceid - * @return void - */ - public static function delete_instance($contextlevel, $instanceid) { - global $DB; - - // double check the context still exists - if ($record = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) { - $context = context::create_instance_from_record($record); - $context->delete(); - } else { - // we should try to purge the cache anyway - } - } - - /** - * Returns the name of specified context level - * - * @static - * @param int $contextlevel - * @return string name of the context level - */ - public static function get_level_name($contextlevel) { - $classname = context_helper::get_class_for_level($contextlevel); - return $classname::get_level_name(); - } - - /** - * Gets the current context to be used for navigation tree filtering. - * - * @param context|null $context The current context to be checked against. - * @return context|null the context that navigation tree filtering should use. - */ - public static function get_navigation_filter_context(?context $context): ?context { - global $CFG; - if (!empty($CFG->filternavigationwithsystemcontext)) { - return context_system::instance(); - } else { - return $context; - } - } - - /** - * not used - */ - public function get_url() { - } - - /** - * not used - * - * @param string $sort - */ - public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { - } -} - - -/** - * System context class - * - * @package core_access - * @category access - * @copyright Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -class context_system extends context { - /** - * Please use context_system::instance() if you need the instance of context. - * - * @param stdClass $record - */ - protected function __construct(stdClass $record) { - parent::__construct($record); - if ($record->contextlevel != CONTEXT_SYSTEM) { - throw new coding_exception('Invalid $record->contextlevel in context_system constructor.'); - } - } - - /** - * Returns human readable context level name. - * - * @static - * @return string the human readable context level name. - */ - public static function get_level_name() { - return get_string('coresystem'); - } - - /** - * Returns human readable context identifier. - * - * @param boolean $withprefix does not apply to system context - * @param boolean $short does not apply to system context - * @param boolean $escape does not apply to system context - * @return string the human readable context name. - */ - public function get_context_name($withprefix = true, $short = false, $escape = true) { - return self::get_level_name(); - } - - /** - * Returns the most relevant URL for this context. - * - * @return moodle_url - */ - public function get_url() { - return new moodle_url('/'); - } - - /** - * Returns array of relevant context capability records. - * - * @param string $sort - * @return array - */ - public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { - global $DB; - - return $DB->get_records('capabilities', [], $sort); - } - - /** - * Create missing context instances at system context - * @static - */ - protected static function create_level_instances() { - // nothing to do here, the system context is created automatically in installer - self::instance(0); - } - - /** - * Returns system context instance. - * - * @static - * @param int $instanceid should be 0 - * @param int $strictness - * @param bool $cache - * @return context_system context instance - */ - public static function instance($instanceid = 0, $strictness = MUST_EXIST, $cache = true) { - global $DB; - - if ($instanceid != 0) { - debugging('context_system::instance(): invalid $id parameter detected, should be 0'); - } - - // SYSCONTEXTID is cached in local cache to eliminate 1 query per page. - if (defined('SYSCONTEXTID') and $cache) { - if (!isset(context::$systemcontext)) { - $record = new stdClass(); - $record->id = SYSCONTEXTID; - $record->contextlevel = CONTEXT_SYSTEM; - $record->instanceid = 0; - $record->path = '/'.SYSCONTEXTID; - $record->depth = 1; - $record->locked = 0; - context::$systemcontext = new context_system($record); - } - return context::$systemcontext; - } - - try { - // We ignore the strictness completely because system context must exist except during install. - $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST); - } catch (dml_exception $e) { - //table or record does not exist - if (!during_initial_install()) { - // do not mess with system context after install, it simply must exist - throw $e; - } - $record = null; - } - - if (!$record) { - $record = new stdClass(); - $record->contextlevel = CONTEXT_SYSTEM; - $record->instanceid = 0; - $record->depth = 1; - $record->path = null; // Not known before insert. - $record->locked = 0; - - try { - if ($DB->count_records('context')) { - // contexts already exist, this is very weird, system must be first!!! - return null; - } - if (defined('SYSCONTEXTID')) { - // this would happen only in unittest on sites that went through weird 1.7 upgrade - $record->id = SYSCONTEXTID; - $DB->import_record('context', $record); - $DB->get_manager()->reset_sequence('context'); - } else { - $record->id = $DB->insert_record('context', $record); - } - } catch (dml_exception $e) { - // can not create context - table does not exist yet, sorry - return null; - } - } - - if ($record->instanceid != 0) { - // this is very weird, somebody must be messing with context table - debugging('Invalid system context detected'); - } - - if ($record->depth != 1 or $record->path != '/'.$record->id) { - // fix path if necessary, initial install or path reset - $record->depth = 1; - $record->path = '/'.$record->id; - $DB->update_record('context', $record); - } - - if (empty($record->locked)) { - $record->locked = 0; - } - - if (!defined('SYSCONTEXTID')) { - define('SYSCONTEXTID', $record->id); - } - - context::$systemcontext = new context_system($record); - return context::$systemcontext; - } - - /** - * Returns all site contexts except the system context, DO NOT call on production servers!! - * - * Contexts are not cached. - * - * @return array - */ - public function get_child_contexts() { - global $DB; - - debugging('Fetching of system context child courses is strongly discouraged on production servers (it may eat all available memory)!'); - - // Just get all the contexts except for CONTEXT_SYSTEM level - // and hope we don't OOM in the process - don't cache - $sql = "SELECT c.* - FROM {context} c - WHERE contextlevel > ".CONTEXT_SYSTEM; - $records = $DB->get_records_sql($sql); - - $result = array(); - foreach ($records as $record) { - $result[$record->id] = context::create_instance_from_record($record); - } - - return $result; - } - - /** - * Returns sql necessary for purging of stale context instances. - * - * @static - * @return string cleanup SQL - */ - protected static function get_cleanup_sql() { - $sql = " - SELECT c.* - FROM {context} c - WHERE 1=2 - "; - - return $sql; - } - - /** - * Rebuild context paths and depths at system context level. - * - * @static - * @param bool $force - */ - protected static function build_paths($force) { - global $DB; - - /* note: ignore $force here, we always do full test of system context */ - - // exactly one record must exist - $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST); - - if ($record->instanceid != 0) { - debugging('Invalid system context detected'); - } - - if (defined('SYSCONTEXTID') and $record->id != SYSCONTEXTID) { - debugging('Invalid SYSCONTEXTID detected'); - } - - if ($record->depth != 1 or $record->path != '/'.$record->id) { - // fix path if necessary, initial install or path reset - $record->depth = 1; - $record->path = '/'.$record->id; - $DB->update_record('context', $record); - } - } - - /** - * Set whether this context has been locked or not. - * - * @param bool $locked - * @return $this - */ - public function set_locked(bool $locked) { - throw new \coding_exception('It is not possible to lock the system context'); - - return $this; - } -} - - -/** - * User context class - * - * @package core_access - * @category access - * @copyright Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -class context_user extends context { - /** - * Please use context_user::instance($userid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) - * - * @param stdClass $record - */ - protected function __construct(stdClass $record) { - parent::__construct($record); - if ($record->contextlevel != CONTEXT_USER) { - throw new coding_exception('Invalid $record->contextlevel in context_user constructor.'); - } - } - - /** - * Returns human readable context level name. - * - * @static - * @return string the human readable context level name. - */ - public static function get_level_name() { - return get_string('user'); - } - - /** - * Returns human readable context identifier. - * - * @param boolean $withprefix whether to prefix the name of the context with User - * @param boolean $short does not apply to user context - * @param boolean $escape does not apply to user context - * @return string the human readable context name. - */ - public function get_context_name($withprefix = true, $short = false, $escape = true) { - global $DB; - - $name = ''; - if ($user = $DB->get_record('user', array('id'=>$this->_instanceid, 'deleted'=>0))) { - if ($withprefix){ - $name = get_string('user').': '; - } - $name .= fullname($user); - } - return $name; - } - - /** - * Returns the most relevant URL for this context. - * - * @return moodle_url - */ - public function get_url() { - global $COURSE; - - if ($COURSE->id == SITEID) { - $url = new moodle_url('/user/profile.php', array('id'=>$this->_instanceid)); - } else { - $url = new moodle_url('/user/view.php', array('id'=>$this->_instanceid, 'courseid'=>$COURSE->id)); - } - return $url; - } - - /** - * Returns array of relevant context capability records. - * - * @param string $sort - * @return array - */ - public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { - global $DB; - - $extracaps = array('moodle/grade:viewall'); - list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap'); - - return $DB->get_records_select('capabilities', "contextlevel = :level OR name {$extra}", - $params + ['level' => CONTEXT_USER], $sort); - } - - /** - * Returns user context instance. - * - * @static - * @param int $userid id from {user} table - * @param int $strictness - * @return context_user context instance - */ - public static function instance($userid, $strictness = MUST_EXIST) { - global $DB; - - if ($context = context::cache_get(CONTEXT_USER, $userid)) { - return $context; - } - - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_USER, 'instanceid' => $userid))) { - if ($user = $DB->get_record('user', array('id' => $userid, 'deleted' => 0), 'id', $strictness)) { - $record = context::insert_context_record(CONTEXT_USER, $user->id, '/'.SYSCONTEXTID, 0); - } - } - - if ($record) { - $context = new context_user($record); - context::cache_add($context); - return $context; - } - - return false; - } - - /** - * Create missing context instances at user context level - * @static - */ - protected static function create_level_instances() { - global $DB; - - $sql = "SELECT ".CONTEXT_USER.", u.id - FROM {user} u - WHERE u.deleted = 0 - AND NOT EXISTS (SELECT 'x' - FROM {context} cx - WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")"; - $contextdata = $DB->get_recordset_sql($sql); - foreach ($contextdata as $context) { - context::insert_context_record(CONTEXT_USER, $context->id, null); - } - $contextdata->close(); - } - - /** - * Returns sql necessary for purging of stale context instances. - * - * @static - * @return string cleanup SQL - */ - protected static function get_cleanup_sql() { - $sql = " - SELECT c.* - FROM {context} c - LEFT OUTER JOIN {user} u ON (c.instanceid = u.id AND u.deleted = 0) - WHERE u.id IS NULL AND c.contextlevel = ".CONTEXT_USER." - "; - - return $sql; - } - - /** - * Rebuild context paths and depths at user context level. - * - * @static - * @param bool $force - */ - protected static function build_paths($force) { - global $DB; - - // First update normal users. - $path = $DB->sql_concat('?', 'id'); - $pathstart = '/' . SYSCONTEXTID . '/'; - $params = array($pathstart); - - if ($force) { - $where = "depth <> 2 OR path IS NULL OR path <> ({$path})"; - $params[] = $pathstart; - } else { - $where = "depth = 0 OR path IS NULL"; - } - - $sql = "UPDATE {context} - SET depth = 2, - path = {$path} - WHERE contextlevel = " . CONTEXT_USER . " - AND ($where)"; - $DB->execute($sql, $params); - } -} - - -/** - * Course category context class - * - * @package core_access - * @category access - * @copyright Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -class context_coursecat extends context { - /** - * Please use context_coursecat::instance($coursecatid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) - * - * @param stdClass $record - */ - protected function __construct(stdClass $record) { - parent::__construct($record); - if ($record->contextlevel != CONTEXT_COURSECAT) { - throw new coding_exception('Invalid $record->contextlevel in context_coursecat constructor.'); - } - } - - /** - * Returns human readable context level name. - * - * @static - * @return string the human readable context level name. - */ - public static function get_level_name() { - return get_string('category'); - } - - /** - * Returns human readable context identifier. - * - * @param boolean $withprefix whether to prefix the name of the context with Category - * @param boolean $short does not apply to course categories - * @param boolean $escape Whether the returned name of the context 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) { - global $DB; - - $name = ''; - if ($category = $DB->get_record('course_categories', array('id'=>$this->_instanceid))) { - if ($withprefix){ - $name = get_string('category').': '; - } - if (!$escape) { - $name .= format_string($category->name, true, array('context' => $this, 'escape' => false)); - } else { - $name .= format_string($category->name, true, array('context' => $this)); - } - } - return $name; - } - - /** - * Returns the most relevant URL for this context. - * - * @return moodle_url - */ - public function get_url() { - return new moodle_url('/course/index.php', array('categoryid' => $this->_instanceid)); - } - - /** - * Returns array of relevant context capability records. - * - * @param string $sort - * @return array - */ - public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { - global $DB; - - return $DB->get_records_list('capabilities', 'contextlevel', [ - CONTEXT_COURSECAT, - CONTEXT_COURSE, - CONTEXT_MODULE, - CONTEXT_BLOCK, - ], $sort); - } - - /** - * Returns course category context instance. - * - * @static - * @param int $categoryid id from {course_categories} table - * @param int $strictness - * @return context_coursecat context instance - */ - public static function instance($categoryid, $strictness = MUST_EXIST) { - global $DB; - - if ($context = context::cache_get(CONTEXT_COURSECAT, $categoryid)) { - return $context; - } - - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSECAT, 'instanceid' => $categoryid))) { - if ($category = $DB->get_record('course_categories', array('id' => $categoryid), 'id,parent', $strictness)) { - if ($category->parent) { - $parentcontext = context_coursecat::instance($category->parent); - $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, $parentcontext->path); - } else { - $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, '/'.SYSCONTEXTID, 0); - } - } - } - - if ($record) { - $context = new context_coursecat($record); - context::cache_add($context); - return $context; - } - - return false; - } - - /** - * Returns immediate child contexts of category and all subcategories, - * children of subcategories and courses are not returned. - * - * @return array - */ - public function get_child_contexts() { - global $DB; - - if (empty($this->_path) or 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 ? AND (ctx.depth = ? OR ctx.contextlevel = ?)"; - $params = array($this->_path.'/%', $this->depth+1, CONTEXT_COURSECAT); - $records = $DB->get_records_sql($sql, $params); - - $result = array(); - foreach ($records as $record) { - $result[$record->id] = context::create_instance_from_record($record); - } - - return $result; - } - - /** - * Create missing context instances at course category context level - * @static - */ - protected static function create_level_instances() { - global $DB; - - $sql = "SELECT ".CONTEXT_COURSECAT.", cc.id - FROM {course_categories} cc - WHERE NOT EXISTS (SELECT 'x' - FROM {context} cx - WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")"; - $contextdata = $DB->get_recordset_sql($sql); - foreach ($contextdata as $context) { - context::insert_context_record(CONTEXT_COURSECAT, $context->id, null); - } - $contextdata->close(); - } - - /** - * Returns sql necessary for purging of stale context instances. - * - * @static - * @return string cleanup SQL - */ - protected static function get_cleanup_sql() { - $sql = " - SELECT c.* - FROM {context} c - LEFT OUTER JOIN {course_categories} cc ON c.instanceid = cc.id - WHERE cc.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT." - "; - - return $sql; - } - - /** - * Rebuild context paths and depths at course category context level. - * - * @static - * @param bool $force - */ - protected static function build_paths($force) { - global $DB; - - if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSECAT." AND (depth = 0 OR path IS NULL)")) { - if ($force) { - $ctxemptyclause = $emptyclause = ''; - } else { - $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; - $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; - } - - $base = '/'.SYSCONTEXTID; - - // Normal top level categories - $sql = "UPDATE {context} - SET depth=2, - path=".$DB->sql_concat("'$base/'", 'id')." - WHERE contextlevel=".CONTEXT_COURSECAT." - AND EXISTS (SELECT 'x' - FROM {course_categories} cc - WHERE cc.id = {context}.instanceid AND cc.depth=1) - $emptyclause"; - $DB->execute($sql); - - // Deeper categories - one query per depthlevel - $maxdepth = $DB->get_field_sql("SELECT MAX(depth) FROM {course_categories}"); - for ($n=2; $n<=$maxdepth; $n++) { - $sql = "INSERT INTO {context_temp} (id, path, depth, locked) - SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked - FROM {context} ctx - JOIN {course_categories} cc ON (cc.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT." AND cc.depth = $n) - JOIN {context} pctx ON (pctx.instanceid = cc.parent AND pctx.contextlevel = ".CONTEXT_COURSECAT.") - WHERE pctx.path IS NOT NULL AND pctx.depth > 0 - $ctxemptyclause"; - $trans = $DB->start_delegated_transaction(); - $DB->delete_records('context_temp'); - $DB->execute($sql); - context::merge_context_temp_table(); - $DB->delete_records('context_temp'); - $trans->allow_commit(); - - } - } - } -} - - -/** - * Course context class - * - * @package core_access - * @category access - * @copyright Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -class context_course extends context { - /** - * Please use context_course::instance($courseid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) - * - * @param stdClass $record - */ - protected function __construct(stdClass $record) { - parent::__construct($record); - if ($record->contextlevel != CONTEXT_COURSE) { - throw new coding_exception('Invalid $record->contextlevel in context_course constructor.'); - } - } - - /** - * Returns human readable context level name. - * - * @static - * @return string the human readable context level name. - */ - public static function get_level_name() { - return get_string('course'); - } - - /** - * Returns human readable context identifier. - * - * @param boolean $withprefix whether to prefix the name of the context with Course - * @param boolean $short whether to use the short name of the thing. - * @param bool $escape Whether the returned category name 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) { - global $DB; - - $name = ''; - if ($this->_instanceid == SITEID) { - $name = get_string('frontpage', 'admin'); - } else { - if ($course = $DB->get_record('course', array('id'=>$this->_instanceid))) { - if ($withprefix){ - $name = get_string('course').': '; - } - if ($short){ - if (!$escape) { - $name .= format_string($course->shortname, true, array('context' => $this, 'escape' => false)); - } else { - $name .= format_string($course->shortname, true, array('context' => $this)); - } - } else { - if (!$escape) { - $name .= format_string(get_course_display_name_for_list($course), true, array('context' => $this, - 'escape' => false)); - } else { - $name .= format_string(get_course_display_name_for_list($course), true, array('context' => $this)); - } - } - } - } - return $name; - } - - /** - * Returns the most relevant URL for this context. - * - * @return moodle_url - */ - public function get_url() { - if ($this->_instanceid != SITEID) { - return new moodle_url('/course/view.php', array('id'=>$this->_instanceid)); - } - - return new moodle_url('/'); - } - - /** - * Returns array of relevant context capability records. - * - * @param string $sort - * @return array - */ - public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { - global $DB; - - return $DB->get_records_list('capabilities', 'contextlevel', [ - CONTEXT_COURSE, - CONTEXT_MODULE, - CONTEXT_BLOCK, - ], $sort); - } - - /** - * 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 context of the enclosing course, null if not found or exception - */ - public function get_course_context($strict = true) { - return $this; - } - - /** - * Returns course context instance. - * - * @static - * @param int $courseid id from {course} table - * @param int $strictness - * @return context_course context instance - */ - public static function instance($courseid, $strictness = MUST_EXIST) { - global $DB; - - if ($context = context::cache_get(CONTEXT_COURSE, $courseid)) { - return $context; - } - - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSE, 'instanceid' => $courseid))) { - if ($course = $DB->get_record('course', array('id' => $courseid), 'id,category', $strictness)) { - if ($course->category) { - $parentcontext = context_coursecat::instance($course->category); - $record = context::insert_context_record(CONTEXT_COURSE, $course->id, $parentcontext->path); - } else { - $record = context::insert_context_record(CONTEXT_COURSE, $course->id, '/'.SYSCONTEXTID, 0); - } - } - } - - if ($record) { - $context = new context_course($record); - context::cache_add($context); - return $context; - } - - return false; - } - - /** - * Create missing context instances at course context level - * @static - */ - protected static function create_level_instances() { - global $DB; - - $sql = "SELECT ".CONTEXT_COURSE.", c.id - FROM {course} c - WHERE NOT EXISTS (SELECT 'x' - FROM {context} cx - WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")"; - $contextdata = $DB->get_recordset_sql($sql); - foreach ($contextdata as $context) { - context::insert_context_record(CONTEXT_COURSE, $context->id, null); - } - $contextdata->close(); - } - - /** - * Returns sql necessary for purging of stale context instances. - * - * @static - * @return string cleanup SQL - */ - protected static function get_cleanup_sql() { - $sql = " - SELECT c.* - FROM {context} c - LEFT OUTER JOIN {course} co ON c.instanceid = co.id - WHERE co.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE." - "; - - return $sql; - } - - /** - * Rebuild context paths and depths at course context level. - * - * @static - * @param bool $force - */ - protected static function build_paths($force) { - global $DB; - - if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSE." AND (depth = 0 OR path IS NULL)")) { - if ($force) { - $ctxemptyclause = $emptyclause = ''; - } else { - $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; - $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; - } - - $base = '/'.SYSCONTEXTID; - - // Standard frontpage - $sql = "UPDATE {context} - SET depth = 2, - path = ".$DB->sql_concat("'$base/'", 'id')." - WHERE contextlevel = ".CONTEXT_COURSE." - AND EXISTS (SELECT 'x' - FROM {course} c - WHERE c.id = {context}.instanceid AND c.category = 0) - $emptyclause"; - $DB->execute($sql); - - // standard courses - $sql = "INSERT INTO {context_temp} (id, path, depth, locked) - SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked - FROM {context} ctx - JOIN {course} c ON (c.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSE." AND c.category <> 0) - JOIN {context} pctx ON (pctx.instanceid = c.category AND pctx.contextlevel = ".CONTEXT_COURSECAT.") - WHERE pctx.path IS NOT NULL AND pctx.depth > 0 - $ctxemptyclause"; - $trans = $DB->start_delegated_transaction(); - $DB->delete_records('context_temp'); - $DB->execute($sql); - context::merge_context_temp_table(); - $DB->delete_records('context_temp'); - $trans->allow_commit(); - } - } -} - - -/** - * Course module context class - * - * @package core_access - * @category access - * @copyright Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -class context_module extends context { - /** - * Please use context_module::instance($cmid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) - * - * @param stdClass $record - */ - protected function __construct(stdClass $record) { - parent::__construct($record); - if ($record->contextlevel != CONTEXT_MODULE) { - throw new coding_exception('Invalid $record->contextlevel in context_module constructor.'); - } - } - - /** - * Returns human readable context level name. - * - * @static - * @return string the human readable context level name. - */ - public static function get_level_name() { - return get_string('activitymodule'); - } - - /** - * Returns human readable context identifier. - * - * @param boolean $withprefix whether to prefix the name of the context with the - * module name, e.g. Forum, Glossary, etc. - * @param boolean $short does not apply to module context - * @param boolean $escape Whether the returned name of the context 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) { - global $DB; - - $name = ''; - if ($cm = $DB->get_record_sql("SELECT cm.*, md.name AS modname - FROM {course_modules} cm - JOIN {modules} md ON md.id = cm.module - WHERE cm.id = ?", array($this->_instanceid))) { - if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) { - if ($withprefix){ - $name = get_string('modulename', $cm->modname).': '; - } - if (!$escape) { - $name .= format_string($mod->name, true, array('context' => $this, 'escape' => false)); - } else { - $name .= format_string($mod->name, true, array('context' => $this)); - } - } - } - return $name; - } - - /** - * Returns the most relevant URL for this context. - * - * @return moodle_url - */ - public function get_url() { - global $DB; - - if ($modname = $DB->get_field_sql("SELECT md.name AS modname - FROM {course_modules} cm - JOIN {modules} md ON md.id = cm.module - WHERE cm.id = ?", array($this->_instanceid))) { - return new moodle_url('/mod/' . $modname . '/view.php', array('id'=>$this->_instanceid)); - } - - return new moodle_url('/'); - } - - /** - * Returns array of relevant context capability records. - * - * @param string $sort - * @return array - */ - public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { - global $DB, $CFG; - - $cm = $DB->get_record('course_modules', array('id'=>$this->_instanceid)); - $module = $DB->get_record('modules', array('id'=>$cm->module)); - - $subcaps = array(); - - $modulepath = "{$CFG->dirroot}/mod/{$module->name}"; - if (file_exists("{$modulepath}/db/subplugins.json")) { - $subplugins = (array) json_decode(file_get_contents("{$modulepath}/db/subplugins.json"))->plugintypes; - } else if (file_exists("{$modulepath}/db/subplugins.php")) { - debugging('Use of subplugins.php has been deprecated. ' . - 'Please update your plugin to provide a subplugins.json file instead.', - DEBUG_DEVELOPER); - $subplugins = array(); // should be redefined in the file - include("{$modulepath}/db/subplugins.php"); - } - - if (!empty($subplugins)) { - foreach (array_keys($subplugins) as $subplugintype) { - foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) { - $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname))); - } - } - } - - $modfile = "{$modulepath}/lib.php"; - $extracaps = array(); - if (file_exists($modfile)) { - include_once($modfile); - $modfunction = $module->name.'_get_extra_capabilities'; - if (function_exists($modfunction)) { - $extracaps = $modfunction(); - } - } - - $extracaps = array_merge($subcaps, $extracaps); - $extra = ''; - list($extra, $params) = $DB->get_in_or_equal( - $extracaps, SQL_PARAMS_NAMED, 'cap0', true, ''); - if (!empty($extra)) { - $extra = "OR name $extra"; - } - - // Fetch the list of modules, and remove this one. - $components = \core_component::get_component_list(); - $componentnames = $components['mod']; - unset($componentnames["mod_{$module->name}"]); - $componentnames = array_keys($componentnames); - - // Exclude all other modules. - list($notcompsql, $notcompparams) = $DB->get_in_or_equal($componentnames, SQL_PARAMS_NAMED, 'notcomp', false); - $params = array_merge($params, $notcompparams); - - - // Exclude other component submodules. - $i = 0; - $ignorecomponents = []; - foreach ($componentnames as $mod) { - if ($subplugins = \core_component::get_subplugins($mod)) { - foreach (array_keys($subplugins) as $subplugintype) { - $paramname = "notlike{$i}"; - $ignorecomponents[] = $DB->sql_like('component', ":{$paramname}", true, true, true); - $params[$paramname] = "{$subplugintype}_%"; - $i++; - } - } - } - $notlikesql = "(" . implode(' AND ', $ignorecomponents) . ")"; - - $sql = "SELECT * - FROM {capabilities} - WHERE (contextlevel = ".CONTEXT_MODULE." - AND component {$notcompsql} - AND {$notlikesql}) - $extra - ORDER BY $sort"; - - return $DB->get_records_sql($sql, $params); - } - - /** - * 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 context of the enclosing course, null if not found or exception - */ - public function get_course_context($strict = true) { - return $this->get_parent_context(); - } - - /** - * Returns module context instance. - * - * @static - * @param int $cmid id of the record from {course_modules} table; pass cmid there, NOT id in the instance column - * @param int $strictness - * @return context_module context instance - */ - public static function instance($cmid, $strictness = MUST_EXIST) { - global $DB; - - if ($context = context::cache_get(CONTEXT_MODULE, $cmid)) { - return $context; - } - - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_MODULE, 'instanceid' => $cmid))) { - if ($cm = $DB->get_record('course_modules', array('id' => $cmid), 'id,course', $strictness)) { - $parentcontext = context_course::instance($cm->course); - $record = context::insert_context_record(CONTEXT_MODULE, $cm->id, $parentcontext->path); - } - } - - if ($record) { - $context = new context_module($record); - context::cache_add($context); - return $context; - } - - return false; - } - - /** - * Create missing context instances at module context level - * @static - */ - protected static function create_level_instances() { - global $DB; - - $sql = "SELECT ".CONTEXT_MODULE.", cm.id - FROM {course_modules} cm - WHERE NOT EXISTS (SELECT 'x' - FROM {context} cx - WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")"; - $contextdata = $DB->get_recordset_sql($sql); - foreach ($contextdata as $context) { - context::insert_context_record(CONTEXT_MODULE, $context->id, null); - } - $contextdata->close(); - } - - /** - * Returns sql necessary for purging of stale context instances. - * - * @static - * @return string cleanup SQL - */ - protected static function get_cleanup_sql() { - $sql = " - SELECT c.* - FROM {context} c - LEFT OUTER JOIN {course_modules} cm ON c.instanceid = cm.id - WHERE cm.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE." - "; - - return $sql; - } - - /** - * Rebuild context paths and depths at module context level. - * - * @static - * @param bool $force - */ - protected static function build_paths($force) { - global $DB; - - if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_MODULE." AND (depth = 0 OR path IS NULL)")) { - if ($force) { - $ctxemptyclause = ''; - } else { - $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; - } - - $sql = "INSERT INTO {context_temp} (id, path, depth, locked) - SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked - FROM {context} ctx - JOIN {course_modules} cm ON (cm.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_MODULE.") - JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = ".CONTEXT_COURSE.") - WHERE pctx.path IS NOT NULL AND pctx.depth > 0 - $ctxemptyclause"; - $trans = $DB->start_delegated_transaction(); - $DB->delete_records('context_temp'); - $DB->execute($sql); - context::merge_context_temp_table(); - $DB->delete_records('context_temp'); - $trans->allow_commit(); - } - } -} - - -/** - * Block context class - * - * @package core_access - * @category access - * @copyright Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - * @since Moodle 2.2 - */ -class context_block extends context { - /** - * Please use context_block::instance($blockinstanceid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) - * - * @param stdClass $record - */ - protected function __construct(stdClass $record) { - parent::__construct($record); - if ($record->contextlevel != CONTEXT_BLOCK) { - throw new coding_exception('Invalid $record->contextlevel in context_block constructor'); - } - } - - /** - * Returns human readable context level name. - * - * @static - * @return string the human readable context level name. - */ - public static function get_level_name() { - return get_string('block'); - } - - /** - * Returns human readable context identifier. - * - * @param boolean $withprefix whether to prefix the name of the context with Block - * @param boolean $short does not apply to block context - * @param boolean $escape does not apply to block context - * @return string the human readable context name. - */ - public function get_context_name($withprefix = true, $short = false, $escape = true) { - global $DB, $CFG; - - $name = ''; - if ($blockinstance = $DB->get_record('block_instances', array('id'=>$this->_instanceid))) { - global $CFG; - require_once("$CFG->dirroot/blocks/moodleblock.class.php"); - require_once("$CFG->dirroot/blocks/$blockinstance->blockname/block_$blockinstance->blockname.php"); - $blockname = "block_$blockinstance->blockname"; - if ($blockobject = new $blockname()) { - if ($withprefix){ - $name = get_string('block').': '; - } - $name .= $blockobject->title; - } - } - - return $name; - } - - /** - * Returns the most relevant URL for this context. - * - * @return moodle_url - */ - public function get_url() { - $parentcontexts = $this->get_parent_context(); - return $parentcontexts->get_url(); - } - - /** - * Returns array of relevant context capability records. - * - * @param string $sort - * @return array - */ - public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { - global $DB; - - $bi = $DB->get_record('block_instances', array('id' => $this->_instanceid)); - - $select = '(contextlevel = :level AND component = :component)'; - $params = [ - 'level' => CONTEXT_BLOCK, - 'component' => 'block_' . $bi->blockname, - ]; - - $extracaps = block_method_result($bi->blockname, 'get_extra_capabilities'); - if ($extracaps) { - list($extra, $extraparams) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap'); - $select .= " OR name $extra"; - $params = array_merge($params, $extraparams); - } - - return $DB->get_records_select('capabilities', $select, $params, $sort); - } - - /** - * 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 context of the enclosing course, null if not found or exception - */ - public function get_course_context($strict = true) { - $parentcontext = $this->get_parent_context(); - return $parentcontext->get_course_context($strict); - } - - /** - * Returns block context instance. - * - * @static - * @param int $blockinstanceid id from {block_instances} table. - * @param int $strictness - * @return context_block context instance - */ - public static function instance($blockinstanceid, $strictness = MUST_EXIST) { - global $DB; - - if ($context = context::cache_get(CONTEXT_BLOCK, $blockinstanceid)) { - return $context; - } - - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_BLOCK, 'instanceid' => $blockinstanceid))) { - if ($bi = $DB->get_record('block_instances', array('id' => $blockinstanceid), 'id,parentcontextid', $strictness)) { - $parentcontext = context::instance_by_id($bi->parentcontextid); - $record = context::insert_context_record(CONTEXT_BLOCK, $bi->id, $parentcontext->path); - } - } - - if ($record) { - $context = new context_block($record); - context::cache_add($context); - return $context; - } - - return false; - } - - /** - * Block do not have child contexts... - * @return array - */ - public function get_child_contexts() { - return array(); - } - - /** - * Create missing context instances at block context level - * @static - */ - protected static function create_level_instances() { - global $DB; - - $sql = <<execute($sql, [ - 'contextlevel' => CONTEXT_BLOCK, - 'existingcontextlevel' => CONTEXT_BLOCK, - ]); - } - - /** - * Returns sql necessary for purging of stale context instances. - * - * @static - * @return string cleanup SQL - */ - protected static function get_cleanup_sql() { - $sql = " - SELECT c.* - FROM {context} c - LEFT OUTER JOIN {block_instances} bi ON c.instanceid = bi.id - WHERE bi.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK." - "; - - return $sql; - } - - /** - * Rebuild context paths and depths at block context level. - * - * @static - * @param bool $force - */ - protected static function build_paths($force) { - global $DB; - - if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_BLOCK." AND (depth = 0 OR path IS NULL)")) { - if ($force) { - $ctxemptyclause = ''; - } else { - $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; - } - - // pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent - $sql = "INSERT INTO {context_temp} (id, path, depth, locked) - SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked - FROM {context} ctx - JOIN {block_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_BLOCK.") - JOIN {context} pctx ON (pctx.id = bi.parentcontextid) - WHERE (pctx.path IS NOT NULL AND pctx.depth > 0) - $ctxemptyclause"; - $trans = $DB->start_delegated_transaction(); - $DB->delete_records('context_temp'); - $DB->execute($sql); - context::merge_context_temp_table(); - $DB->delete_records('context_temp'); - $trans->allow_commit(); - } - } -} - - // ============== DEPRECATED FUNCTIONS ========================================== // Old context related functions were deprecated in 2.0, it is recommended // to use context classes in new code. Old function can be used when @@ -7813,6 +5033,18 @@ class context_block extends context { // then we will add error message and only after that we can remove the functions // completely. +// Renamed context class do not use lib/db/renamedclasses.php because we cannot +// ask everybody to update all code, so let's keep this here for the next few decades. +// Another benefit is that PHPStorm understands this and stops complaining. +class_alias(core\context_helper::class, 'context_helper', true); +class_alias(core\context::class, 'context', true); +class_alias(core\context\block::class, 'context_block'); +class_alias(core\context\course::class, 'context_course', true); +class_alias(core\context\coursecat::class, 'context_coursecat'); +class_alias(core\context\module::class, 'context_module', true); +class_alias(core\context\system::class, 'context_system', true); +class_alias(core\context\user::class, 'context_user', true); + /** * Runs get_records select on context table and returns the result * Does get_records_select on the context table, and returns the results ordered diff --git a/lib/classes/context.php b/lib/classes/context.php new file mode 100644 index 00000000000..0b71b0c89e5 --- /dev/null +++ b/lib/classes/context.php @@ -0,0 +1,1047 @@ +. + +namespace core; + +use stdClass, IteratorAggregate, ArrayIterator; +use coding_exception, moodle_url; +use core\context\system; + +/** + * 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 http://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 + */ +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 system The system context once initialised + */ + protected static $systemcontext = null; + + /** + * Resets the cache to remove all data. + * @static + */ + 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. + * + * @static + * @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. + * + * @static + * @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. + * + * @static + * @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. + * + * @static + * @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. + * + * @static + * @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 context::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. + * @static + * @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 = context::cache_get_by_id($record->id)) { + return $context; + } + + $context = new $classname($record); + context::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. + * @static + */ + 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' or $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. + * + * @static + * @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' and 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 system::instance(0, $strictness); + } + + if (is_array($id) or is_object($id) or empty($id)) { + throw new coding_exception('Invalid context id specified context::instance_by_id()'); + } + + if ($context = context::cache_get_by_id($id)) { + return $context; + } + + if ($record = $DB->get_record('context', array('id'=>$id), '*', $strictness)) { + return context::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(); + + context::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); + } + + context::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))) { + context::cache_remove($this); + return; + } + + $this->delete_content(); + $DB->delete_records('context', array('id'=>$this->_id)); + // purge static context cache if entry present + context::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 + * + * @static + * @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 + */ + public abstract function get_url(); + + /** + * Returns array of relevant context capability records. + * + * @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display + * @return array + */ + public abstract 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) or 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] = context::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) { + $parent = context::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) or $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 + + return context::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. + * + * @static + * @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. + * + * @static + * @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 + * + * @static + * @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() + } + } + } + } +} diff --git a/lib/classes/context/block.php b/lib/classes/context/block.php new file mode 100644 index 00000000000..c83e10eb869 --- /dev/null +++ b/lib/classes/context/block.php @@ -0,0 +1,246 @@ +. + +namespace core\context; + +use core\context; +use stdClass; +use coding_exception, moodle_url; + +/** + * Block context class + * + * @package core_access + * @category access + * @copyright Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.2 + */ +class block extends context { + /** + * Please use context_block::instance($blockinstanceid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_BLOCK) { + throw new coding_exception('Invalid $record->contextlevel in context_block constructor'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + public static function get_level_name() { + return get_string('block'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with Block + * @param boolean $short does not apply to block context + * @param boolean $escape does not apply to block context + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false, $escape = true) { + global $DB, $CFG; + + $name = ''; + if ($blockinstance = $DB->get_record('block_instances', array('id'=>$this->_instanceid))) { + global $CFG; + require_once("$CFG->dirroot/blocks/moodleblock.class.php"); + require_once("$CFG->dirroot/blocks/$blockinstance->blockname/block_$blockinstance->blockname.php"); + $blockname = "block_$blockinstance->blockname"; + if ($blockobject = new $blockname()) { + if ($withprefix){ + $name = get_string('block').': '; + } + $name .= $blockobject->title; + } + } + + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + $parentcontexts = $this->get_parent_context(); + return $parentcontexts->get_url(); + } + + /** + * Returns array of relevant context capability records. + * + * @param string $sort + * @return array + */ + public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { + global $DB; + + $bi = $DB->get_record('block_instances', array('id' => $this->_instanceid)); + + $select = '(contextlevel = :level AND component = :component)'; + $params = [ + 'level' => CONTEXT_BLOCK, + 'component' => 'block_' . $bi->blockname, + ]; + + $extracaps = block_method_result($bi->blockname, 'get_extra_capabilities'); + if ($extracaps) { + list($extra, $extraparams) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap'); + $select .= " OR name $extra"; + $params = array_merge($params, $extraparams); + } + + return $DB->get_records_select('capabilities', $select, $params, $sort); + } + + /** + * 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 course context of the enclosing course, null if not found or exception + */ + public function get_course_context($strict = true) { + $parentcontext = $this->get_parent_context(); + return $parentcontext->get_course_context($strict); + } + + /** + * Returns block context instance. + * + * @static + * @param int $blockinstanceid id from {block_instances} table. + * @param int $strictness + * @return block|false context instance + */ + public static function instance($blockinstanceid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_BLOCK, $blockinstanceid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_BLOCK, 'instanceid' => $blockinstanceid))) { + if ($bi = $DB->get_record('block_instances', array('id' => $blockinstanceid), 'id,parentcontextid', $strictness)) { + $parentcontext = context::instance_by_id($bi->parentcontextid); + $record = context::insert_context_record(CONTEXT_BLOCK, $bi->id, $parentcontext->path); + } + } + + if ($record) { + $context = new block($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Block do not have child contexts... + * @return array + */ + public function get_child_contexts() { + return array(); + } + + /** + * Create missing context instances at block context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $sql = <<execute($sql, [ + 'contextlevel' => CONTEXT_BLOCK, + 'existingcontextlevel' => CONTEXT_BLOCK, + ]); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {block_instances} bi ON c.instanceid = bi.id + WHERE bi.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at block context level. + * + * @static + * @param bool $force + */ + protected static function build_paths($force) { + global $DB; + + if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_BLOCK." AND (depth = 0 OR path IS NULL)")) { + if ($force) { + $ctxemptyclause = ''; + } else { + $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; + } + + // pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent + $sql = "INSERT INTO {context_temp} (id, path, depth, locked) + SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked + FROM {context} ctx + JOIN {block_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_BLOCK.") + JOIN {context} pctx ON (pctx.id = bi.parentcontextid) + WHERE (pctx.path IS NOT NULL AND pctx.depth > 0) + $ctxemptyclause"; + $trans = $DB->start_delegated_transaction(); + $DB->delete_records('context_temp'); + $DB->execute($sql); + context::merge_context_temp_table(); + $DB->delete_records('context_temp'); + $trans->allow_commit(); + } + } +} diff --git a/lib/classes/context/course.php b/lib/classes/context/course.php new file mode 100644 index 00000000000..e5559f2ea1d --- /dev/null +++ b/lib/classes/context/course.php @@ -0,0 +1,250 @@ +. + +namespace core\context; + +use core\context; +use stdClass; +use coding_exception, moodle_url; + +/** + * Course context class + * + * @package core_access + * @category access + * @copyright Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.2 + */ +class course extends context { + /** + * Please use context_course::instance($courseid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_COURSE) { + throw new coding_exception('Invalid $record->contextlevel in context_course constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + public static function get_level_name() { + return get_string('course'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with Course + * @param boolean $short whether to use the short name of the thing. + * @param bool $escape Whether the returned category name 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) { + global $DB; + + $name = ''; + if ($this->_instanceid == SITEID) { + $name = get_string('frontpage', 'admin'); + } else { + if ($course = $DB->get_record('course', array('id'=>$this->_instanceid))) { + if ($withprefix){ + $name = get_string('course').': '; + } + if ($short){ + if (!$escape) { + $name .= format_string($course->shortname, true, array('context' => $this, 'escape' => false)); + } else { + $name .= format_string($course->shortname, true, array('context' => $this)); + } + } else { + if (!$escape) { + $name .= format_string(get_course_display_name_for_list($course), true, array('context' => $this, + 'escape' => false)); + } else { + $name .= format_string(get_course_display_name_for_list($course), true, array('context' => $this)); + } + } + } + } + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + if ($this->_instanceid != SITEID) { + return new moodle_url('/course/view.php', array('id'=>$this->_instanceid)); + } + + return new moodle_url('/'); + } + + /** + * Returns array of relevant context capability records. + * + * @param string $sort + * @return array + */ + public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { + global $DB; + + return $DB->get_records_list('capabilities', 'contextlevel', [ + CONTEXT_COURSE, + CONTEXT_MODULE, + CONTEXT_BLOCK, + ], $sort); + } + + /** + * 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 course context of the enclosing course, null if not found or exception + */ + public function get_course_context($strict = true) { + return $this; + } + + /** + * Returns course context instance. + * + * @static + * @param int $courseid id from {course} table + * @param int $strictness + * @return course context instance + */ + public static function instance($courseid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_COURSE, $courseid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSE, 'instanceid' => $courseid))) { + if ($course = $DB->get_record('course', array('id' => $courseid), 'id,category', $strictness)) { + if ($course->category) { + $parentcontext = coursecat::instance($course->category); + $record = context::insert_context_record(CONTEXT_COURSE, $course->id, $parentcontext->path); + } else { + $record = context::insert_context_record(CONTEXT_COURSE, $course->id, '/'.SYSCONTEXTID, 0); + } + } + } + + if ($record) { + $context = new course($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Create missing context instances at course context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $sql = "SELECT ".CONTEXT_COURSE.", c.id + FROM {course} c + WHERE NOT EXISTS (SELECT 'x' + FROM {context} cx + WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")"; + $contextdata = $DB->get_recordset_sql($sql); + foreach ($contextdata as $context) { + context::insert_context_record(CONTEXT_COURSE, $context->id, null); + } + $contextdata->close(); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {course} co ON c.instanceid = co.id + WHERE co.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at course context level. + * + * @static + * @param bool $force + */ + protected static function build_paths($force) { + global $DB; + + if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSE." AND (depth = 0 OR path IS NULL)")) { + if ($force) { + $ctxemptyclause = $emptyclause = ''; + } else { + $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; + $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; + } + + $base = '/'.SYSCONTEXTID; + + // Standard frontpage + $sql = "UPDATE {context} + SET depth = 2, + path = ".$DB->sql_concat("'$base/'", 'id')." + WHERE contextlevel = ".CONTEXT_COURSE." + AND EXISTS (SELECT 'x' + FROM {course} c + WHERE c.id = {context}.instanceid AND c.category = 0) + $emptyclause"; + $DB->execute($sql); + + // standard courses + $sql = "INSERT INTO {context_temp} (id, path, depth, locked) + SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked + FROM {context} ctx + JOIN {course} c ON (c.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSE." AND c.category <> 0) + JOIN {context} pctx ON (pctx.instanceid = c.category AND pctx.contextlevel = ".CONTEXT_COURSECAT.") + WHERE pctx.path IS NOT NULL AND pctx.depth > 0 + $ctxemptyclause"; + $trans = $DB->start_delegated_transaction(); + $DB->delete_records('context_temp'); + $DB->execute($sql); + context::merge_context_temp_table(); + $DB->delete_records('context_temp'); + $trans->allow_commit(); + } + } +} diff --git a/lib/classes/context/coursecat.php b/lib/classes/context/coursecat.php new file mode 100644 index 00000000000..3a1d0e0c991 --- /dev/null +++ b/lib/classes/context/coursecat.php @@ -0,0 +1,256 @@ +. + +namespace core\context; + +use core\context; +use stdClass; +use coding_exception, moodle_url; + +/** + * Course category context class + * + * @package core_access + * @category access + * @copyright Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.2 + */ +class coursecat extends context { + /** + * Please use context_coursecat::instance($coursecatid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_COURSECAT) { + throw new coding_exception('Invalid $record->contextlevel in context_coursecat constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + public static function get_level_name() { + return get_string('category'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with Category + * @param boolean $short does not apply to course categories + * @param boolean $escape Whether the returned name of the context 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) { + global $DB; + + $name = ''; + if ($category = $DB->get_record('course_categories', array('id'=>$this->_instanceid))) { + if ($withprefix){ + $name = get_string('category').': '; + } + if (!$escape) { + $name .= format_string($category->name, true, array('context' => $this, 'escape' => false)); + } else { + $name .= format_string($category->name, true, array('context' => $this)); + } + } + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + return new moodle_url('/course/index.php', array('categoryid' => $this->_instanceid)); + } + + /** + * Returns array of relevant context capability records. + * + * @param string $sort + * @return array + */ + public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { + global $DB; + + return $DB->get_records_list('capabilities', 'contextlevel', [ + CONTEXT_COURSECAT, + CONTEXT_COURSE, + CONTEXT_MODULE, + CONTEXT_BLOCK, + ], $sort); + } + + /** + * Returns course category context instance. + * + * @static + * @param int $categoryid id from {course_categories} table + * @param int $strictness + * @return coursecat|false context instance + */ + public static function instance($categoryid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_COURSECAT, $categoryid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSECAT, 'instanceid' => $categoryid))) { + if ($category = $DB->get_record('course_categories', array('id' => $categoryid), 'id,parent', $strictness)) { + if ($category->parent) { + $parentcontext = coursecat::instance($category->parent); + $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, $parentcontext->path); + } else { + $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, '/'.SYSCONTEXTID, 0); + } + } + } + + if ($record) { + $context = new coursecat($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Returns immediate child contexts of category and all subcategories, + * children of subcategories and courses are not returned. + * + * @return array + */ + public function get_child_contexts() { + global $DB; + + if (empty($this->_path) or 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 ? AND (ctx.depth = ? OR ctx.contextlevel = ?)"; + $params = array($this->_path.'/%', $this->depth+1, CONTEXT_COURSECAT); + $records = $DB->get_records_sql($sql, $params); + + $result = array(); + foreach ($records as $record) { + $result[$record->id] = context::create_instance_from_record($record); + } + + return $result; + } + + /** + * Create missing context instances at course category context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $sql = "SELECT ".CONTEXT_COURSECAT.", cc.id + FROM {course_categories} cc + WHERE NOT EXISTS (SELECT 'x' + FROM {context} cx + WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")"; + $contextdata = $DB->get_recordset_sql($sql); + foreach ($contextdata as $context) { + context::insert_context_record(CONTEXT_COURSECAT, $context->id, null); + } + $contextdata->close(); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {course_categories} cc ON c.instanceid = cc.id + WHERE cc.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at course category context level. + * + * @static + * @param bool $force + */ + protected static function build_paths($force) { + global $DB; + + if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSECAT." AND (depth = 0 OR path IS NULL)")) { + if ($force) { + $ctxemptyclause = $emptyclause = ''; + } else { + $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; + $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; + } + + $base = '/'.SYSCONTEXTID; + + // Normal top level categories + $sql = "UPDATE {context} + SET depth=2, + path=".$DB->sql_concat("'$base/'", 'id')." + WHERE contextlevel=".CONTEXT_COURSECAT." + AND EXISTS (SELECT 'x' + FROM {course_categories} cc + WHERE cc.id = {context}.instanceid AND cc.depth=1) + $emptyclause"; + $DB->execute($sql); + + // Deeper categories - one query per depthlevel + $maxdepth = $DB->get_field_sql("SELECT MAX(depth) FROM {course_categories}"); + for ($n=2; $n<=$maxdepth; $n++) { + $sql = "INSERT INTO {context_temp} (id, path, depth, locked) + SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked + FROM {context} ctx + JOIN {course_categories} cc ON (cc.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT." AND cc.depth = $n) + JOIN {context} pctx ON (pctx.instanceid = cc.parent AND pctx.contextlevel = ".CONTEXT_COURSECAT.") + WHERE pctx.path IS NOT NULL AND pctx.depth > 0 + $ctxemptyclause"; + $trans = $DB->start_delegated_transaction(); + $DB->delete_records('context_temp'); + $DB->execute($sql); + context::merge_context_temp_table(); + $DB->delete_records('context_temp'); + $trans->allow_commit(); + + } + } + } +} diff --git a/lib/classes/context/module.php b/lib/classes/context/module.php new file mode 100644 index 00000000000..9f5e28096bd --- /dev/null +++ b/lib/classes/context/module.php @@ -0,0 +1,301 @@ +. + +namespace core\context; + +use core\context; +use stdClass; +use coding_exception, moodle_url; + +/** + * Course module context class + * + * @package core_access + * @category access + * @copyright Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.2 + */ +class module extends context { + /** + * Please use context_module::instance($cmid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_MODULE) { + throw new coding_exception('Invalid $record->contextlevel in context_module constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + public static function get_level_name() { + return get_string('activitymodule'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with the + * module name, e.g. Forum, Glossary, etc. + * @param boolean $short does not apply to module context + * @param boolean $escape Whether the returned name of the context 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) { + global $DB; + + $name = ''; + if ($cm = $DB->get_record_sql("SELECT cm.*, md.name AS modname + FROM {course_modules} cm + JOIN {modules} md ON md.id = cm.module + WHERE cm.id = ?", array($this->_instanceid))) { + if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) { + if ($withprefix){ + $name = get_string('modulename', $cm->modname).': '; + } + if (!$escape) { + $name .= format_string($mod->name, true, array('context' => $this, 'escape' => false)); + } else { + $name .= format_string($mod->name, true, array('context' => $this)); + } + } + } + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + global $DB; + + if ($modname = $DB->get_field_sql("SELECT md.name AS modname + FROM {course_modules} cm + JOIN {modules} md ON md.id = cm.module + WHERE cm.id = ?", array($this->_instanceid))) { + return new moodle_url('/mod/' . $modname . '/view.php', array('id'=>$this->_instanceid)); + } + + return new moodle_url('/'); + } + + /** + * Returns array of relevant context capability records. + * + * @param string $sort + * @return array + */ + public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { + global $DB, $CFG; + + $cm = $DB->get_record('course_modules', array('id'=>$this->_instanceid)); + $module = $DB->get_record('modules', array('id'=>$cm->module)); + + $subcaps = array(); + + $modulepath = "{$CFG->dirroot}/mod/{$module->name}"; + if (file_exists("{$modulepath}/db/subplugins.json")) { + $subplugins = (array) json_decode(file_get_contents("{$modulepath}/db/subplugins.json"))->plugintypes; + } else if (file_exists("{$modulepath}/db/subplugins.php")) { + debugging('Use of subplugins.php has been deprecated. ' . + 'Please update your plugin to provide a subplugins.json file instead.', + DEBUG_DEVELOPER); + $subplugins = array(); // should be redefined in the file + include("{$modulepath}/db/subplugins.php"); + } + + if (!empty($subplugins)) { + foreach (array_keys($subplugins) as $subplugintype) { + foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) { + $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname))); + } + } + } + + $modfile = "{$modulepath}/lib.php"; + $extracaps = array(); + if (file_exists($modfile)) { + include_once($modfile); + $modfunction = $module->name.'_get_extra_capabilities'; + if (function_exists($modfunction)) { + $extracaps = $modfunction(); + } + } + + $extracaps = array_merge($subcaps, $extracaps); + $extra = ''; + list($extra, $params) = $DB->get_in_or_equal( + $extracaps, SQL_PARAMS_NAMED, 'cap0', true, ''); + if (!empty($extra)) { + $extra = "OR name $extra"; + } + + // Fetch the list of modules, and remove this one. + $components = \core_component::get_component_list(); + $componentnames = $components['mod']; + unset($componentnames["mod_{$module->name}"]); + $componentnames = array_keys($componentnames); + + // Exclude all other modules. + list($notcompsql, $notcompparams) = $DB->get_in_or_equal($componentnames, SQL_PARAMS_NAMED, 'notcomp', false); + $params = array_merge($params, $notcompparams); + + + // Exclude other component submodules. + $i = 0; + $ignorecomponents = []; + foreach ($componentnames as $mod) { + if ($subplugins = \core_component::get_subplugins($mod)) { + foreach (array_keys($subplugins) as $subplugintype) { + $paramname = "notlike{$i}"; + $ignorecomponents[] = $DB->sql_like('component', ":{$paramname}", true, true, true); + $params[$paramname] = "{$subplugintype}_%"; + $i++; + } + } + } + $notlikesql = "(" . implode(' AND ', $ignorecomponents) . ")"; + + $sql = "SELECT * + FROM {capabilities} + WHERE (contextlevel = ".CONTEXT_MODULE." + AND component {$notcompsql} + AND {$notlikesql}) + $extra + ORDER BY $sort"; + + return $DB->get_records_sql($sql, $params); + } + + /** + * 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 course|false context of the enclosing course, null if not found or exception + */ + public function get_course_context($strict = true) { + return $this->get_parent_context(); + } + + /** + * Returns module context instance. + * + * @static + * @param int $cmid id of the record from {course_modules} table; pass cmid there, NOT id in the instance column + * @param int $strictness + * @return module|false context instance + */ + public static function instance($cmid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_MODULE, $cmid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_MODULE, 'instanceid' => $cmid))) { + if ($cm = $DB->get_record('course_modules', array('id' => $cmid), 'id,course', $strictness)) { + $parentcontext = course::instance($cm->course); + $record = context::insert_context_record(CONTEXT_MODULE, $cm->id, $parentcontext->path); + } + } + + if ($record) { + $context = new module($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Create missing context instances at module context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $sql = "SELECT ".CONTEXT_MODULE.", cm.id + FROM {course_modules} cm + WHERE NOT EXISTS (SELECT 'x' + FROM {context} cx + WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")"; + $contextdata = $DB->get_recordset_sql($sql); + foreach ($contextdata as $context) { + context::insert_context_record(CONTEXT_MODULE, $context->id, null); + } + $contextdata->close(); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {course_modules} cm ON c.instanceid = cm.id + WHERE cm.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at module context level. + * + * @static + * @param bool $force + */ + protected static function build_paths($force) { + global $DB; + + if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_MODULE." AND (depth = 0 OR path IS NULL)")) { + if ($force) { + $ctxemptyclause = ''; + } else { + $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; + } + + $sql = "INSERT INTO {context_temp} (id, path, depth, locked) + SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked + FROM {context} ctx + JOIN {course_modules} cm ON (cm.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_MODULE.") + JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = ".CONTEXT_COURSE.") + WHERE pctx.path IS NOT NULL AND pctx.depth > 0 + $ctxemptyclause"; + $trans = $DB->start_delegated_transaction(); + $DB->delete_records('context_temp'); + $DB->execute($sql); + context::merge_context_temp_table(); + $DB->delete_records('context_temp'); + $trans->allow_commit(); + } + } +} diff --git a/lib/classes/context/system.php b/lib/classes/context/system.php new file mode 100644 index 00000000000..1f885f35ede --- /dev/null +++ b/lib/classes/context/system.php @@ -0,0 +1,275 @@ +. + +namespace core\context; + +use core\context; +use stdClass; +use coding_exception, moodle_url; + +/** + * System context class + * + * @package core_access + * @category access + * @copyright Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.2 + */ +class system extends context { + /** + * Please use context_system::instance() if you need the instance of context. + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_SYSTEM) { + throw new coding_exception('Invalid $record->contextlevel in context_system constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + public static function get_level_name() { + return get_string('coresystem'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix does not apply to system context + * @param boolean $short does not apply to system context + * @param boolean $escape does not apply to system context + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false, $escape = true) { + return self::get_level_name(); + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + return new moodle_url('/'); + } + + /** + * Returns array of relevant context capability records. + * + * @param string $sort + * @return array + */ + public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { + global $DB; + + return $DB->get_records('capabilities', [], $sort); + } + + /** + * Create missing context instances at system context + * @static + */ + protected static function create_level_instances() { + // nothing to do here, the system context is created automatically in installer + self::instance(0); + } + + /** + * Returns system context instance. + * + * @static + * @param int $instanceid should be 0 + * @param int $strictness + * @param bool $cache + * @return system context instance + */ + public static function instance($instanceid = 0, $strictness = MUST_EXIST, $cache = true) { + global $DB; + + if ($instanceid != 0) { + debugging('context_system::instance(): invalid $id parameter detected, should be 0'); + } + + // SYSCONTEXTID is cached in local cache to eliminate 1 query per page. + if (defined('SYSCONTEXTID') and $cache) { + if (!isset(context::$systemcontext)) { + $record = new stdClass(); + $record->id = SYSCONTEXTID; + $record->contextlevel = CONTEXT_SYSTEM; + $record->instanceid = 0; + $record->path = '/'.SYSCONTEXTID; + $record->depth = 1; + $record->locked = 0; + context::$systemcontext = new system($record); + } + return context::$systemcontext; + } + + try { + // We ignore the strictness completely because system context must exist except during install. + $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST); + } catch (\dml_exception $e) { + //table or record does not exist + if (!during_initial_install()) { + // do not mess with system context after install, it simply must exist + throw $e; + } + $record = null; + } + + if (!$record) { + $record = new stdClass(); + $record->contextlevel = CONTEXT_SYSTEM; + $record->instanceid = 0; + $record->depth = 1; + $record->path = null; // Not known before insert. + $record->locked = 0; + + try { + if ($DB->count_records('context')) { + // contexts already exist, this is very weird, system must be first!!! + return null; + } + if (defined('SYSCONTEXTID')) { + // this would happen only in unittest on sites that went through weird 1.7 upgrade + $record->id = SYSCONTEXTID; + $DB->import_record('context', $record); + $DB->get_manager()->reset_sequence('context'); + } else { + $record->id = $DB->insert_record('context', $record); + } + } catch (\dml_exception $e) { + // can not create context - table does not exist yet, sorry + return null; + } + } + + if ($record->instanceid != 0) { + // this is very weird, somebody must be messing with context table + debugging('Invalid system context detected'); + } + + if ($record->depth != 1 or $record->path != '/'.$record->id) { + // fix path if necessary, initial install or path reset + $record->depth = 1; + $record->path = '/'.$record->id; + $DB->update_record('context', $record); + } + + if (empty($record->locked)) { + $record->locked = 0; + } + + if (!defined('SYSCONTEXTID')) { + define('SYSCONTEXTID', $record->id); + } + + context::$systemcontext = new system($record); + return context::$systemcontext; + } + + /** + * Returns all site contexts except the system context, DO NOT call on production servers!! + * + * Contexts are not cached. + * + * @return array + */ + public function get_child_contexts() { + global $DB; + + debugging('Fetching of system context child courses is strongly discouraged on production servers (it may eat all available memory)!'); + + // Just get all the contexts except for CONTEXT_SYSTEM level + // and hope we don't OOM in the process - don't cache + $sql = "SELECT c.* + FROM {context} c + WHERE contextlevel > ".CONTEXT_SYSTEM; + $records = $DB->get_records_sql($sql); + + $result = array(); + foreach ($records as $record) { + $result[$record->id] = context::create_instance_from_record($record); + } + + return $result; + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + WHERE 1=2 + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at system context level. + * + * @static + * @param bool $force + */ + protected static function build_paths($force) { + global $DB; + + /* note: ignore $force here, we always do full test of system context */ + + // exactly one record must exist + $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST); + + if ($record->instanceid != 0) { + debugging('Invalid system context detected'); + } + + if (defined('SYSCONTEXTID') and $record->id != SYSCONTEXTID) { + debugging('Invalid SYSCONTEXTID detected'); + } + + if ($record->depth != 1 or $record->path != '/'.$record->id) { + // fix path if necessary, initial install or path reset + $record->depth = 1; + $record->path = '/'.$record->id; + $DB->update_record('context', $record); + } + } + + /** + * Set whether this context has been locked or not. + * + * @param bool $locked + * @return $this + */ + public function set_locked(bool $locked) { + throw new \coding_exception('It is not possible to lock the system context'); + + return $this; + } +} diff --git a/lib/classes/context/user.php b/lib/classes/context/user.php new file mode 100644 index 00000000000..63778fddfe7 --- /dev/null +++ b/lib/classes/context/user.php @@ -0,0 +1,204 @@ +. + +namespace core\context; + +use core\context; +use stdClass; +use coding_exception, moodle_url; + +/** + * User context class + * + * @package core_access + * @category access + * @copyright Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.2 + */ +class user extends context { + /** + * Please use context_user::instance($userid) if you need the instance of context. + * Alternatively if you know only the context id use context::instance_by_id($contextid) + * + * @param stdClass $record + */ + protected function __construct(stdClass $record) { + parent::__construct($record); + if ($record->contextlevel != CONTEXT_USER) { + throw new coding_exception('Invalid $record->contextlevel in context_user constructor.'); + } + } + + /** + * Returns human readable context level name. + * + * @static + * @return string the human readable context level name. + */ + public static function get_level_name() { + return get_string('user'); + } + + /** + * Returns human readable context identifier. + * + * @param boolean $withprefix whether to prefix the name of the context with User + * @param boolean $short does not apply to user context + * @param boolean $escape does not apply to user context + * @return string the human readable context name. + */ + public function get_context_name($withprefix = true, $short = false, $escape = true) { + global $DB; + + $name = ''; + if ($user = $DB->get_record('user', array('id'=>$this->_instanceid, 'deleted'=>0))) { + if ($withprefix){ + $name = get_string('user').': '; + } + $name .= fullname($user); + } + return $name; + } + + /** + * Returns the most relevant URL for this context. + * + * @return moodle_url + */ + public function get_url() { + global $COURSE; + + if ($COURSE->id == SITEID) { + $url = new moodle_url('/user/profile.php', array('id'=>$this->_instanceid)); + } else { + $url = new moodle_url('/user/view.php', array('id'=>$this->_instanceid, 'courseid'=>$COURSE->id)); + } + return $url; + } + + /** + * Returns array of relevant context capability records. + * + * @param string $sort + * @return array + */ + public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { + global $DB; + + $extracaps = array('moodle/grade:viewall'); + list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap'); + + return $DB->get_records_select('capabilities', "contextlevel = :level OR name {$extra}", + $params + ['level' => CONTEXT_USER], $sort); + } + + /** + * Returns user context instance. + * + * @static + * @param int $userid id from {user} table + * @param int $strictness + * @return user|false context instance + */ + public static function instance($userid, $strictness = MUST_EXIST) { + global $DB; + + if ($context = context::cache_get(CONTEXT_USER, $userid)) { + return $context; + } + + if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_USER, 'instanceid' => $userid))) { + if ($user = $DB->get_record('user', array('id' => $userid, 'deleted' => 0), 'id', $strictness)) { + $record = context::insert_context_record(CONTEXT_USER, $user->id, '/'.SYSCONTEXTID, 0); + } + } + + if ($record) { + $context = new user($record); + context::cache_add($context); + return $context; + } + + return false; + } + + /** + * Create missing context instances at user context level + * @static + */ + protected static function create_level_instances() { + global $DB; + + $sql = "SELECT ".CONTEXT_USER.", u.id + FROM {user} u + WHERE u.deleted = 0 + AND NOT EXISTS (SELECT 'x' + FROM {context} cx + WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")"; + $contextdata = $DB->get_recordset_sql($sql); + foreach ($contextdata as $context) { + context::insert_context_record(CONTEXT_USER, $context->id, null); + } + $contextdata->close(); + } + + /** + * Returns sql necessary for purging of stale context instances. + * + * @static + * @return string cleanup SQL + */ + protected static function get_cleanup_sql() { + $sql = " + SELECT c.* + FROM {context} c + LEFT OUTER JOIN {user} u ON (c.instanceid = u.id AND u.deleted = 0) + WHERE u.id IS NULL AND c.contextlevel = ".CONTEXT_USER." + "; + + return $sql; + } + + /** + * Rebuild context paths and depths at user context level. + * + * @static + * @param bool $force + */ + protected static function build_paths($force) { + global $DB; + + // First update normal users. + $path = $DB->sql_concat('?', 'id'); + $pathstart = '/' . SYSCONTEXTID . '/'; + $params = array($pathstart); + + if ($force) { + $where = "depth <> 2 OR path IS NULL OR path <> ({$path})"; + $params[] = $pathstart; + } else { + $where = "depth = 0 OR path IS NULL"; + } + + $sql = "UPDATE {context} + SET depth = 2, + path = {$path} + WHERE contextlevel = " . CONTEXT_USER . " + AND ($where)"; + $DB->execute($sql, $params); + } +} diff --git a/lib/classes/context_helper.php b/lib/classes/context_helper.php new file mode 100644 index 00000000000..1cdadb473c3 --- /dev/null +++ b/lib/classes/context_helper.php @@ -0,0 +1,361 @@ +. + +namespace core; + +use stdClass; +use coding_exception; +use core\context\system; +use core\context\course; + +/** + * Context maintenance and helper methods. + * + * This is "extends context" is a bloody hack that tires to work around the deficiencies + * in the "protected" keyword in PHP, this helps us to hide all the internals of context + * level implementation from the rest of code, the code completion returns what developers need. + * + * Thank you Tim Hunt for helping me with this nasty trick. + * + * @package core_access + * @category access + * @copyright Petr Skoda + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @since Moodle 4.2 + */ +class context_helper extends context { + + /** + * @var array An array mapping context levels to classes + */ + private static $alllevels; + + /** + * Instance does not make sense here, only static use + */ + protected function __construct() { + } + + /** + * Reset internal context levels array. + */ + public static function reset_levels() { + self::$alllevels = null; + } + + /** + * Initialise context levels, call before using self::$alllevels. + */ + private static function init_levels() { + global $CFG; + + if (isset(self::$alllevels)) { + return; + } + self::$alllevels = array( + CONTEXT_SYSTEM => 'context_system', + CONTEXT_USER => 'context_user', + CONTEXT_COURSECAT => 'context_coursecat', + CONTEXT_COURSE => 'context_course', + CONTEXT_MODULE => 'context_module', + CONTEXT_BLOCK => 'context_block', + ); + + if (empty($CFG->custom_context_classes)) { + return; + } + + $levels = $CFG->custom_context_classes; + if (!is_array($levels)) { + $levels = @unserialize($levels); + } + if (!is_array($levels)) { + debugging('Invalid $CFG->custom_context_classes detected, value ignored.', DEBUG_DEVELOPER); + return; + } + + // Unsupported custom levels, use with care!!! + foreach ($levels as $level => $classname) { + self::$alllevels[$level] = $classname; + } + ksort(self::$alllevels); + } + + /** + * Returns a class name of the context level class + * + * @static + * @param int $contextlevel (CONTEXT_SYSTEM, etc.) + * @return string class name of the context class + */ + public static function get_class_for_level($contextlevel) { + self::init_levels(); + if (isset(self::$alllevels[$contextlevel])) { + return self::$alllevels[$contextlevel]; + } else { + throw new coding_exception('Invalid context level specified'); + } + } + + /** + * Returns a list of all context levels + * + * @static + * @return array int=>string (level=>level class name) + */ + public static function get_all_levels() { + self::init_levels(); + return self::$alllevels; + } + + /** + * Remove stale contexts that belonged to deleted instances. + * Ideally all code should cleanup contexts properly, unfortunately accidents happen... + * + * @static + * @return void + */ + public static function cleanup_instances() { + global $DB; + self::init_levels(); + + $sqls = array(); + foreach (self::$alllevels as $level=>$classname) { + $sqls[] = $classname::get_cleanup_sql(); + } + + $sql = implode(" UNION ", $sqls); + + // it is probably better to use transactions, it might be faster too + $transaction = $DB->start_delegated_transaction(); + + $rs = $DB->get_recordset_sql($sql); + foreach ($rs as $record) { + $context = context::create_instance_from_record($record); + $context->delete(); + } + $rs->close(); + + $transaction->allow_commit(); + } + + /** + * Create all context instances at the given level and above. + * + * @static + * @param int $contextlevel null means all levels + * @param bool $buildpaths + * @return void + */ + public static function create_instances($contextlevel = null, $buildpaths = true) { + self::init_levels(); + foreach (self::$alllevels as $level=>$classname) { + if ($contextlevel and $level > $contextlevel) { + // skip potential sub-contexts + continue; + } + $classname::create_level_instances(); + if ($buildpaths) { + $classname::build_paths(false); + } + } + } + + /** + * Rebuild paths and depths in all context levels. + * + * @static + * @param bool $force false means add missing only + * @return void + */ + public static function build_all_paths($force = false) { + self::init_levels(); + foreach (self::$alllevels as $classname) { + $classname::build_paths($force); + } + + // reset static course cache - it might have incorrect cached data + accesslib_clear_all_caches(true); + } + + /** + * Resets the cache to remove all data. + * @static + */ + public static function reset_caches() { + context::reset_caches(); + } + + /** + * Returns all fields necessary for context preloading from user $rec. + * + * This helps with performance when dealing with hundreds of contexts. + * + * @static + * @param string $tablealias context table alias in the query + * @return array (table.column=>alias, ...) + */ + public static function get_preload_record_columns($tablealias) { + return [ + "$tablealias.id" => "ctxid", + "$tablealias.path" => "ctxpath", + "$tablealias.depth" => "ctxdepth", + "$tablealias.contextlevel" => "ctxlevel", + "$tablealias.instanceid" => "ctxinstance", + "$tablealias.locked" => "ctxlocked", + ]; + } + + /** + * Returns all fields necessary for context preloading from user $rec. + * + * This helps with performance when dealing with hundreds of contexts. + * + * @static + * @param string $tablealias context table alias in the query + * @return string + */ + public static function get_preload_record_columns_sql($tablealias) { + return "$tablealias.id AS ctxid, " . + "$tablealias.path AS ctxpath, " . + "$tablealias.depth AS ctxdepth, " . + "$tablealias.contextlevel AS ctxlevel, " . + "$tablealias.instanceid AS ctxinstance, " . + "$tablealias.locked AS ctxlocked"; + } + + /** + * Preloads context cache with information from db record and strips the cached info. + * + * The db request has to contain all columns from context_helper::get_preload_record_columns(). + * + * @static + * @param stdClass $rec + * @return void This is intentional. See MDL-37115. You will need to get the context + * in the normal way, but it is now cached, so that will be fast. + */ + public static function preload_from_record(stdClass $rec) { + context::preload_from_record($rec); + } + + /** + * Preload a set of contexts using their contextid. + * + * @param array $contextids + */ + public static function preload_contexts_by_id(array $contextids) { + global $DB; + + // Determine which contexts are not already cached. + $tofetch = []; + foreach ($contextids as $contextid) { + if (!self::cache_get_by_id($contextid)) { + $tofetch[] = $contextid; + } + } + + if (count($tofetch) > 1) { + // There are at least two to fetch. + // There is no point only fetching a single context as this would be no more efficient than calling the existing code. + list($insql, $inparams) = $DB->get_in_or_equal($tofetch, SQL_PARAMS_NAMED); + $ctxs = $DB->get_records_select('context', "id {$insql}", $inparams, '', + context_helper::get_preload_record_columns_sql('{context}')); + foreach ($ctxs as $ctx) { + self::preload_from_record($ctx); + } + } + } + + /** + * Preload all contexts instances from course. + * + * To be used if you expect multiple queries for course activities... + * + * @static + * @param int $courseid + */ + public static function preload_course($courseid) { + // Users can call this multiple times without doing any harm + if (isset(context::$cache_preloaded[$courseid])) { + return; + } + $coursecontext = course::instance($courseid); + $coursecontext->get_child_contexts(); + + context::$cache_preloaded[$courseid] = true; + } + + /** + * Delete context instance + * + * @static + * @param int $contextlevel + * @param int $instanceid + * @return void + */ + public static function delete_instance($contextlevel, $instanceid) { + global $DB; + + // double check the context still exists + if ($record = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) { + $context = context::create_instance_from_record($record); + $context->delete(); + } else { + // we should try to purge the cache anyway + } + } + + /** + * Returns the name of specified context level + * + * @static + * @param int $contextlevel + * @return string name of the context level + */ + public static function get_level_name($contextlevel) { + $classname = context_helper::get_class_for_level($contextlevel); + return $classname::get_level_name(); + } + + /** + * Gets the current context to be used for navigation tree filtering. + * + * @param context|null $context The current context to be checked against. + * @return context|null the context that navigation tree filtering should use. + */ + public static function get_navigation_filter_context(?context $context): ?context { + global $CFG; + if (!empty($CFG->filternavigationwithsystemcontext)) { + return system::instance(); + } else { + return $context; + } + } + + /** + * not used + */ + public function get_url() { + } + + /** + * not used + * + * @param string $sort + */ + public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { + } +} From 176191dab54bce8d51787a76475b14b473d65088 Mon Sep 17 00:00:00 2001 From: Petr Skoda Date: Mon, 16 Jan 2023 10:50:07 +0100 Subject: [PATCH 2/2] MDL-74936 core: tidy up context API --- admin/roles/classes/preset.php | 18 +- admin/roles/tests/fixtures/mixed_levels.xml | 24 + admin/roles/tests/preset_test.php | 16 + .../dataprivacy/classes/data_registry.php | 6 + lib/accesslib.php | 18 +- lib/behat/classes/behat_session_trait.php | 49 +- lib/classes/context.php | 276 +++++---- lib/classes/context/block.php | 70 ++- lib/classes/context/course.php | 122 ++-- lib/classes/context/coursecat.php | 126 +++-- lib/classes/context/module.php | 108 +++- lib/classes/context/system.php | 128 +++-- lib/classes/context/user.php | 88 ++- lib/classes/context_helper.php | 253 ++++++--- lib/external/classes/external_api.php | 16 +- lib/external/tests/external_api_test.php | 25 +- lib/tests/accesslib_test.php | 2 +- lib/tests/context/block_test.php | 269 +++++++++ lib/tests/context/course_test.php | 291 ++++++++++ lib/tests/context/coursecat_test.php | 289 ++++++++++ lib/tests/context/module_test.php | 306 ++++++++++ lib/tests/context/system_test.php | 233 ++++++++ lib/tests/context/user_test.php | 268 +++++++++ lib/tests/context_helper_test.php | 530 ++++++++++++++++++ lib/tests/context_test.php | 106 ++++ lib/tests/customcontext_test.php | 33 +- lib/upgrade.txt | 3 + 27 files changed, 3210 insertions(+), 463 deletions(-) create mode 100644 admin/roles/tests/fixtures/mixed_levels.xml create mode 100644 lib/tests/context/block_test.php create mode 100644 lib/tests/context/course_test.php create mode 100644 lib/tests/context/coursecat_test.php create mode 100644 lib/tests/context/module_test.php create mode 100644 lib/tests/context/system_test.php create mode 100644 lib/tests/context/user_test.php create mode 100644 lib/tests/context_helper_test.php create mode 100644 lib/tests/context_test.php diff --git a/admin/roles/classes/preset.php b/admin/roles/classes/preset.php index b99434255ec..b6f6083e096 100644 --- a/admin/roles/classes/preset.php +++ b/admin/roles/classes/preset.php @@ -80,8 +80,15 @@ class core_role_preset { $top->appendChild($contextlevels); foreach (get_role_contextlevels($roleid) as $level) { $name = context_helper::get_class_for_level($level); - $name = preg_replace('/^context_/', '', $name); - $contextlevels->appendChild($dom->createElement('level', $name)); + if (strpos($name, 'core\\context\\') === 0) { + // Use short names of standard contexts for backwards compatibility. + $value = preg_replace('/^core\\\\context\\\\/', '', $name); + } else { + // Must be a custom plugin level, use numbers to work around + // potential duplicate short names of contexts in add-ons. + $value = $level; + } + $contextlevels->appendChild($dom->createElement('level', $value)); } foreach (array('assign', 'override', 'switch', 'view') as $type) { @@ -197,9 +204,10 @@ class core_role_preset { $info['contextlevels'] = array(); $levelmap = array_flip(context_helper::get_all_levels()); foreach ($values as $value) { - $level = 'context_'.$value; - if (isset($levelmap[$level])) { - $cl = $levelmap[$level]; + // Numbers and short names are supported since Moodle 4.2. + $classname = \core\context_helper::parse_external_level($value); + if ($classname) { + $cl = $classname::LEVEL; $info['contextlevels'][$cl] = $cl; } } diff --git a/admin/roles/tests/fixtures/mixed_levels.xml b/admin/roles/tests/fixtures/mixed_levels.xml new file mode 100644 index 00000000000..8ef8e2c8277 --- /dev/null +++ b/admin/roles/tests/fixtures/mixed_levels.xml @@ -0,0 +1,24 @@ + + + courseviewer + + + + + system + 40 + 50 + + + + + + student + + + moodle/category:viewhiddencategories + moodle/course:ignoreavailabilityrestrictions + moodle/course:viewhiddencourses + moodle/site:configview + + diff --git a/admin/roles/tests/preset_test.php b/admin/roles/tests/preset_test.php index 915565a6fc5..370a3e2de80 100644 --- a/admin/roles/tests/preset_test.php +++ b/admin/roles/tests/preset_test.php @@ -76,4 +76,20 @@ class preset_test extends \advanced_testcase { } } } + + /** + * Tests covered method. + * @covers \core_role_preset::parse_preset + */ + public function test_mixed_levels() { + // The problem here is that we cannot guarantee plugin contexts + // have unique short names, so we have to also support level numbers. + $xml = file_get_contents(__DIR__ . '/fixtures/mixed_levels.xml'); + $this->assertTrue(\core_role_preset::is_valid_preset($xml)); + + $preset = \core_role_preset::parse_preset($xml); + $expected = [\core\context\system::LEVEL, \core\context\coursecat::LEVEL, \core\context\course::LEVEL]; + $expected = array_combine($expected, $expected); + $this->assertSame($expected, $preset['contextlevels']); + } } diff --git a/admin/tool/dataprivacy/classes/data_registry.php b/admin/tool/dataprivacy/classes/data_registry.php index fcfe0d25e2c..cdc2e26e8e2 100644 --- a/admin/tool/dataprivacy/classes/data_registry.php +++ b/admin/tool/dataprivacy/classes/data_registry.php @@ -47,6 +47,12 @@ class data_registry { * @return string[] */ public static function var_names_from_context($classname, $pluginname = '') { + // Unfortunately authors of privacy API did not expect that we would be + // on day fixing auto-loading of context classes. + // The best way would have been probably level numbers at the end of vars, + // but it is probably too late to fix it. + $classname = preg_replace('/^[a-z0-9_]+\\\\context\\\\/', 'context_', $classname); + $pluginname = trim($pluginname ?? ''); if (!empty($pluginname)) { $categoryvar = $classname . '_' . $pluginname . '_category'; diff --git a/lib/accesslib.php b/lib/accesslib.php index 9326a794e1c..49b5ac874be 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -3549,21 +3549,7 @@ function get_roles_for_contextlevels($contextlevel) { * @return array list of the context levels at which this type of role may be assigned by default. */ function get_default_contextlevels($rolearchetype) { - static $defaults = array( - 'manager' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE), - 'coursecreator' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT), - 'editingteacher' => array(CONTEXT_COURSE, CONTEXT_MODULE), - 'teacher' => array(CONTEXT_COURSE, CONTEXT_MODULE), - 'student' => array(CONTEXT_COURSE, CONTEXT_MODULE), - 'guest' => array(), - 'user' => array(), - 'frontpage' => array()); - - if (isset($defaults[$rolearchetype])) { - return $defaults[$rolearchetype]; - } else { - return array(); - } + return \context_helper::get_compatible_levels($rolearchetype); } /** @@ -5024,7 +5010,7 @@ function role_change_permission($roleid, $context, $capname, $permission) { assign_capability($capname, $permission, $roleid, $context->id, true); } -// ============== DEPRECATED FUNCTIONS ========================================== +/* ============== DEPRECATED FUNCTIONS ========================================== */ // Old context related functions were deprecated in 2.0, it is recommended // to use context classes in new code. Old function can be used when // creating patches that are supposed to be backported to older stable branches. diff --git a/lib/behat/classes/behat_session_trait.php b/lib/behat/classes/behat_session_trait.php index f6ef429d44f..ae09ebe6e85 100644 --- a/lib/behat/classes/behat_session_trait.php +++ b/lib/behat/classes/behat_session_trait.php @@ -1148,53 +1148,12 @@ EOF; * @return context */ public static function get_context(string $levelname, string $contextref): context { - global $DB; - - // Getting context levels and names (we will be using the English ones as it is the test site language). - $contextlevels = context_helper::get_all_levels(); - $contextnames = array(); - foreach ($contextlevels as $level => $classname) { - $contextnames[context_helper::get_level_name($level)] = $level; + $context = \core\context_helper::resolve_behat_reference($levelname, $contextref); + if ($context) { + return $context; } - if (empty($contextnames[$levelname])) { - throw new Exception('The specified "' . $levelname . '" context level does not exist'); - } - $contextlevel = $contextnames[$levelname]; - - // Return it, we don't need to look for other internal ids. - if ($contextlevel == CONTEXT_SYSTEM) { - return context_system::instance(); - } - - switch ($contextlevel) { - - case CONTEXT_USER: - $instanceid = $DB->get_field('user', 'id', array('username' => $contextref)); - break; - - case CONTEXT_COURSECAT: - $instanceid = $DB->get_field('course_categories', 'id', array('idnumber' => $contextref)); - break; - - case CONTEXT_COURSE: - $instanceid = $DB->get_field('course', 'id', array('shortname' => $contextref)); - break; - - case CONTEXT_MODULE: - $instanceid = $DB->get_field('course_modules', 'id', array('idnumber' => $contextref)); - break; - - default: - break; - } - - $contextclass = $contextlevels[$contextlevel]; - if (!$context = $contextclass::instance($instanceid, IGNORE_MISSING)) { - throw new Exception('The specified "' . $contextref . '" context reference does not exist'); - } - - return $context; + throw new Exception("The specified context \"$levelname, $contextref\" does not exist"); } /** diff --git a/lib/classes/context.php b/lib/classes/context.php index 0b71b0c89e5..34f42fe8799 100644 --- a/lib/classes/context.php +++ b/lib/classes/context.php @@ -1,5 +1,5 @@ . +// along with Moodle. If not, see . namespace core; use stdClass, IteratorAggregate, ArrayIterator; use coding_exception, moodle_url; -use core\context\system; /** * Basic moodle context abstraction class. @@ -33,7 +32,7 @@ use core\context\system; * @package core_access * @category access * @copyright Petr Skoda - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 * * @property-read int $id context id @@ -41,6 +40,7 @@ use core\context\system; * @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 { @@ -99,34 +99,46 @@ abstract class context extends stdClass implements IteratorAggregate { /** * @var array Context caching info */ - private static $cache_contexts = array(); + 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; + protected static $cache_count = 0; /** * @var array Context caching info */ - protected static $cache_preloaded = array(); + protected static $cache_preloaded = array(); /** - * @var system The system context once initialised + * @var context\system The system context once initialised */ - protected static $systemcontext = null; + 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. - * @static */ protected static function reset_caches() { self::$cache_contextsbyid = array(); - self::$cache_contexts = array(); - self::$cache_count = 0; - self::$cache_preloaded = array(); + self::$cache_contexts = array(); + self::$cache_count = 0; + self::$cache_preloaded = array(); self::$systemcontext = null; } @@ -135,13 +147,12 @@ abstract class context extends stdClass implements IteratorAggregate { * Adds a context to the cache. If the cache is full, discards a batch of * older entries. * - * @static * @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 + // Already cached, no need to do anything - this is relatively cheap, we do all this because count() is slow. return; } @@ -150,11 +161,11 @@ abstract class context extends stdClass implements IteratorAggregate { 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 + // 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 + // We remove oldest third of the contexts to make room for more contexts. break; } unset(self::$cache_contextsbyid[$ctx->id]); @@ -171,13 +182,12 @@ abstract class context extends stdClass implements IteratorAggregate { /** * Removes a context from the cache. * - * @static * @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 + // 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]); @@ -193,7 +203,6 @@ abstract class context extends stdClass implements IteratorAggregate { /** * Gets a context from the cache. * - * @static * @param int $contextlevel Context level * @param int $instance Instance ID * @return context|bool Context or false if not in cache @@ -208,7 +217,6 @@ abstract class context extends stdClass implements IteratorAggregate { /** * Gets a context from the cache based on its id. * - * @static * @param int $id Context ID * @return context|bool Context or false if not in cache */ @@ -222,7 +230,6 @@ abstract class context extends stdClass implements IteratorAggregate { /** * Preloads context information from db record and strips the cached info. * - * @static * @param stdClass $rec * @return context|null (modifies $rec) */ @@ -243,12 +250,12 @@ abstract class context extends stdClass implements IteratorAggregate { } $record = (object) [ - 'id' => $rec->ctxid, - 'contextlevel' => $rec->ctxlevel, - 'instanceid' => $rec->ctxinstance, - 'path' => $rec->ctxpath, - 'depth' => $rec->ctxdepth, - 'locked' => $rec->ctxlocked, + 'id' => $rec->ctxid, + 'contextlevel' => $rec->ctxlevel, + 'instanceid' => $rec->ctxinstance, + 'path' => $rec->ctxpath, + 'depth' => $rec->ctxdepth, + 'locked' => $rec->ctxlocked, ]; unset($rec->ctxid); @@ -258,11 +265,11 @@ abstract class context extends stdClass implements IteratorAggregate { unset($rec->ctxdepth); unset($rec->ctxlocked); - return context::create_instance_from_record($record); + return self::create_instance_from_record($record); } - // ====== magic methods ======= + /* ====== magic methods ======= */ /** * Magic setter method, we do not want anybody to modify properties from the outside @@ -332,7 +339,7 @@ abstract class context extends stdClass implements IteratorAggregate { debugging('Can not unset context instance properties!'); } - // ====== implementing method from interface IteratorAggregate ====== + /* ====== implementing method from interface IteratorAggregate ====== */ /** * Create an iterator because magic vars can't be seen by 'foreach'. @@ -342,17 +349,17 @@ abstract class context extends stdClass implements IteratorAggregate { */ public function getIterator(): \Traversable { $ret = array( - 'id' => $this->id, + 'id' => $this->id, 'contextlevel' => $this->contextlevel, - 'instanceid' => $this->instanceid, - 'path' => $this->path, - 'depth' => $this->depth, - 'locked' => $this->locked, + 'instanceid' => $this->instanceid, + 'path' => $this->path, + 'depth' => $this->depth, + 'locked' => $this->locked, ); return new ArrayIterator($ret); } - // ====== general context methods ====== + /* ====== general context methods ====== */ /** * Constructor is protected so that devs are forced to @@ -361,11 +368,11 @@ abstract class context extends stdClass implements IteratorAggregate { * @param stdClass $record */ protected function __construct(stdClass $record) { - $this->_id = (int)$record->id; + $this->_id = (int)$record->id; $this->_contextlevel = (int)$record->contextlevel; - $this->_instanceid = $record->instanceid; - $this->_path = $record->path; - $this->_depth = $record->depth; + $this->_instanceid = $record->instanceid; + $this->_path = $record->path; + $this->_depth = $record->depth; if (isset($record->locked)) { $this->_locked = $record->locked; @@ -376,19 +383,19 @@ abstract class context extends stdClass implements IteratorAggregate { /** * This function is also used to work around 'protected' keyword problems in context_helper. - * @static + * * @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 = context::cache_get_by_id($record->id)) { + if ($context = self::cache_get_by_id($record->id)) { return $context; } $context = new $classname($record); - context::cache_add($context); + self::cache_add($context); return $context; } @@ -396,7 +403,6 @@ abstract class context extends stdClass implements IteratorAggregate { /** * Copy prepared new contexts from temp table to context table, * we do this in db specific way for perf reasons only. - * @static */ protected static function merge_context_temp_table() { global $DB; @@ -413,9 +419,9 @@ abstract class context extends stdClass implements IteratorAggregate { $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 + 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 @@ -426,19 +432,19 @@ abstract class context extends stdClass implements IteratorAggregate { WHERE EXISTS (SELECT 'x' FROM {context_temp} temp WHERE temp.id = ct.id)"; - } else if ($dbfamily == 'postgres' or $dbfamily == 'mssql') { + } else if ($dbfamily == 'postgres' || $dbfamily == 'mssql') { $updatesql = "UPDATE {context} - SET path = temp.path, - depth = temp.depth, - locked = temp.locked + SET path = temp.path, + depth = temp.depth, + locked = temp.locked FROM {context_temp} temp WHERE temp.id={context}.id"; } else { - // sqlite and others + // 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) + 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})"; } @@ -448,7 +454,6 @@ abstract class context extends stdClass implements IteratorAggregate { /** * Get a context instance as an object, from a given context id. * - * @static * @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 @@ -457,25 +462,25 @@ abstract class context extends stdClass implements IteratorAggregate { public static function instance_by_id($id, $strictness = MUST_EXIST) { global $DB; - if (get_called_class() !== 'core\context' and get_called_class() !== 'core\context_helper') { - // some devs might confuse context->id and instanceid, better prevent these mistakes completely + 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 system::instance(0, $strictness); + return context\system::instance(0, $strictness); } - if (is_array($id) or is_object($id) or empty($id)) { + if (is_array($id) || is_object($id) || empty($id)) { throw new coding_exception('Invalid context id specified context::instance_by_id()'); } - if ($context = context::cache_get_by_id($id)) { + if ($context = self::cache_get_by_id($id)) { return $context; } - if ($record = $DB->get_record('context', array('id'=>$id), '*', $strictness)) { - return context::create_instance_from_record($record); + if ($record = $DB->get_record('context', array('id' => $id), '*', $strictness)) { + return self::create_instance_from_record($record); } return false; @@ -491,12 +496,12 @@ abstract class context extends stdClass implements IteratorAggregate { global $DB; $frompath = $this->_path; - $newpath = $newparent->path . '/' . $this->_id; + $newpath = $newparent->path . '/' . $this->_id; $trans = $DB->start_delegated_transaction(); $setdepth = ''; - if (($newparent->depth +1) != $this->_depth) { + if (($newparent->depth + 1) != $this->_depth) { $diff = $newparent->depth - $this->_depth + 1; $setdepth = ", depth = depth + $diff"; } @@ -507,11 +512,11 @@ abstract class context extends stdClass implements IteratorAggregate { $params = array($newpath, $this->_id); $DB->execute($sql, $params); - $this->_path = $newpath; + $this->_path = $newpath; $this->_depth = $newparent->depth + 1; $sql = "UPDATE {context} - SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath)+1))." + SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath) + 1))." $setdepth WHERE path LIKE ?"; $params = array($newpath, "{$frompath}/%"); @@ -519,7 +524,7 @@ abstract class context extends stdClass implements IteratorAggregate { $this->mark_dirty(); - context::reset_caches(); + self::reset_caches(); $trans->allow_commit(); } @@ -567,10 +572,10 @@ abstract class context extends stdClass implements IteratorAggregate { $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/%'"); + $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)); + $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; } @@ -579,7 +584,7 @@ abstract class context extends stdClass implements IteratorAggregate { context_helper::build_all_paths(false); } - context::reset_caches(); + self::reset_caches(); } /** @@ -592,7 +597,7 @@ abstract class context extends stdClass implements IteratorAggregate { filter_delete_all_for_context($this->_id); require_once($CFG->dirroot . '/comment/lib.php'); - \comment::delete_comments(array('contextid'=>$this->_id)); + \comment::delete_comments(array('contextid' => $this->_id)); require_once($CFG->dirroot.'/rating/lib.php'); $delopt = new stdclass(); @@ -600,7 +605,7 @@ abstract class context extends stdClass implements IteratorAggregate { $rm = new \rating_manager(); $rm->delete_ratings($delopt); - // delete all files attached to this context + // Delete all files attached to this context. $fs = get_file_storage(); $fs->delete_area_files($this->_id); @@ -608,14 +613,14 @@ abstract class context extends stdClass implements IteratorAggregate { require_once($CFG->dirroot . '/repository/lib.php'); \repository::delete_all_for_context($this->_id); - // delete all advanced grading data attached to this context + // 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)); + // 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(); } @@ -644,27 +649,26 @@ abstract class context extends stdClass implements IteratorAggregate { throw new coding_exception('Cannot delete system context'); } - // double check the context still exists - if (!$DB->record_exists('context', array('id'=>$this->_id))) { - context::cache_remove($this); + // 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 - context::cache_remove($this); + $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 ====== + /* ====== context level related methods ====== */ /** * Utility method for context creation * - * @static * @param int $contextlevel * @param int $instanceid * @param string $parentpath @@ -675,14 +679,14 @@ abstract class context extends stdClass implements IteratorAggregate { $record = new stdClass(); $record->contextlevel = $contextlevel; - $record->instanceid = $instanceid; - $record->depth = 0; - $record->path = null; //not known before insert - $record->locked = 0; + $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 + // 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, '/'); @@ -704,7 +708,7 @@ abstract class context extends stdClass implements IteratorAggregate { * @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 + // Must be implemented in all context levels. throw new coding_exception('can not get name of abstract context'); } @@ -730,7 +734,50 @@ abstract class context extends stdClass implements IteratorAggregate { * * @return moodle_url */ - public abstract function get_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. @@ -738,7 +785,7 @@ abstract class context extends stdClass implements IteratorAggregate { * @param string $sort SQL order by snippet for sorting returned capabilities sensibly for display * @return array */ - public abstract function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT); + abstract public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT); /** * Recursive function which, given a context, find all its children context ids. @@ -758,7 +805,7 @@ abstract class context extends stdClass implements IteratorAggregate { public function get_child_contexts() { global $DB; - if (empty($this->_path) or empty($this->_depth)) { + if (empty($this->_path) || empty($this->_depth)) { debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths'); return array(); } @@ -771,7 +818,7 @@ abstract class context extends stdClass implements IteratorAggregate { $result = array(); foreach ($records as $record) { - $result[$record->id] = context::create_instance_from_record($record); + $result[$record->id] = self::create_instance_from_record($record); } return $result; @@ -818,7 +865,8 @@ abstract class context extends stdClass implements IteratorAggregate { $result = array(); foreach ($contextids as $contextid) { - $parent = context::instance_by_id($contextid, MUST_EXIST); + // Do NOT change this to self! + $parent = context_helper::instance_by_id($contextid, MUST_EXIST); $result[$parent->id] = $parent; } @@ -861,10 +909,10 @@ abstract class context extends stdClass implements IteratorAggregate { return array(); } - $parentcontexts = trim($this->_path, '/'); // kill leading slash + $parentcontexts = trim($this->_path, '/'); // Kill leading slash. $parentcontexts = explode('/', $parentcontexts); if (!$includeself) { - array_pop($parentcontexts); // and remove its own id + array_pop($parentcontexts); // And remove its own id. } return array_reverse($parentcontexts); @@ -905,16 +953,17 @@ abstract class context extends stdClass implements IteratorAggregate { * @return context|false */ public function get_parent_context() { - if (empty($this->_path) or $this->_id == SYSCONTEXTID) { + if (empty($this->_path) || $this->_id == SYSCONTEXTID) { return false; } - $parentcontexts = trim($this->_path, '/'); // kill leading slash + $parentcontexts = trim($this->_path, '/'); // Kill leading slash. $parentcontexts = explode('/', $parentcontexts); - array_pop($parentcontexts); // self - $contextid = array_pop($parentcontexts); // immediate parent + array_pop($parentcontexts); // Self. + $contextid = array_pop($parentcontexts); // Immediate parent. - return context::instance_by_id($contextid, MUST_EXIST); + // Do NOT change this to self! + return context_helper::instance_by_id($contextid, MUST_EXIST); } /** @@ -934,7 +983,6 @@ abstract class context extends stdClass implements IteratorAggregate { /** * Returns sql necessary for purging of stale context instances. * - * @static * @return string cleanup SQL */ protected static function get_cleanup_sql() { @@ -944,7 +992,6 @@ abstract class context extends stdClass implements IteratorAggregate { /** * Rebuild context paths and depths at context level. * - * @static * @param bool $force * @return void */ @@ -955,7 +1002,6 @@ abstract class context extends stdClass implements IteratorAggregate { /** * Create missing context instances at given level * - * @static * @return void */ protected static function create_level_instances() { @@ -969,10 +1015,10 @@ abstract class context extends stdClass implements IteratorAggregate { public function reload_if_dirty() { global $ACCESSLIB_PRIVATE, $USER; - // Load dirty contexts list if needed + // Load dirty contexts list if needed. if (CLI_SCRIPT) { if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) { - // we do not load dirty flags in CLI and cron + // We do not load dirty flags in CLI and cron. $ACCESSLIB_PRIVATE->dirtycontexts = array(); } } else { @@ -1025,9 +1071,9 @@ abstract class context extends stdClass implements IteratorAggregate { return; } - // only if it is a non-empty string + // 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); + set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time() + $CFG->sessiontimeout); if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) { $ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1; } else { @@ -1035,11 +1081,11 @@ abstract class context extends stdClass implements IteratorAggregate { $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); + $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() + // Flags not loaded yet, it will be done later in $context->reload_if_dirty(). } } } diff --git a/lib/classes/context/block.php b/lib/classes/context/block.php index c83e10eb869..0446abd5464 100644 --- a/lib/classes/context/block.php +++ b/lib/classes/context/block.php @@ -1,5 +1,5 @@ . +// along with Moodle. If not, see . namespace core\context; @@ -26,27 +26,40 @@ use coding_exception, moodle_url; * @package core_access * @category access * @copyright Petr Skoda - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 */ class block extends context { + /** @var int numeric context level value matching legacy CONTEXT_BLOCK */ + public const LEVEL = 80; + /** - * Please use context_block::instance($blockinstanceid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) + * Please use \core\context\block::instance($blockinstanceid) if you need the instance of context. + * Alternatively if you know only the context id use \core\context::instance_by_id($contextid) * * @param stdClass $record */ protected function __construct(stdClass $record) { parent::__construct($record); - if ($record->contextlevel != CONTEXT_BLOCK) { - throw new coding_exception('Invalid $record->contextlevel in context_block constructor'); + if ($record->contextlevel != self::LEVEL) { + throw new coding_exception('Invalid $record->contextlevel in core\context\block constructor'); } } + /** + * Returns short context name. + * + * @since Moodle 4.2 + * + * @return string + */ + public static function get_short_name(): string { + return 'block'; + } + /** * Returns human readable context level name. * - * @static * @return string the human readable context level name. */ public static function get_level_name() { @@ -65,13 +78,13 @@ class block extends context { global $DB, $CFG; $name = ''; - if ($blockinstance = $DB->get_record('block_instances', array('id'=>$this->_instanceid))) { + if ($blockinstance = $DB->get_record('block_instances', array('id' => $this->_instanceid))) { global $CFG; require_once("$CFG->dirroot/blocks/moodleblock.class.php"); require_once("$CFG->dirroot/blocks/$blockinstance->blockname/block_$blockinstance->blockname.php"); $blockname = "block_$blockinstance->blockname"; if ($blockobject = new $blockname()) { - if ($withprefix){ + if ($withprefix) { $name = get_string('block').': '; } $name .= $blockobject->title; @@ -91,6 +104,19 @@ class block extends context { return $parentcontexts->get_url(); } + /** + * Returns list of all possible parent context levels. + * @since Moodle 4.2 + * + * @return int[] + */ + public static function get_possible_parent_levels(): array { + // Blocks may be added to any other context instance. + $alllevels = \core\context_helper::get_all_levels(); + unset($alllevels[self::LEVEL]); + return array_keys($alllevels); + } + /** * Returns array of relevant context capability records. * @@ -104,7 +130,7 @@ class block extends context { $select = '(contextlevel = :level AND component = :component)'; $params = [ - 'level' => CONTEXT_BLOCK, + 'level' => self::LEVEL, 'component' => 'block_' . $bi->blockname, ]; @@ -132,7 +158,6 @@ class block extends context { /** * Returns block context instance. * - * @static * @param int $blockinstanceid id from {block_instances} table. * @param int $strictness * @return block|false context instance @@ -140,14 +165,14 @@ class block extends context { public static function instance($blockinstanceid, $strictness = MUST_EXIST) { global $DB; - if ($context = context::cache_get(CONTEXT_BLOCK, $blockinstanceid)) { + if ($context = context::cache_get(self::LEVEL, $blockinstanceid)) { return $context; } - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_BLOCK, 'instanceid' => $blockinstanceid))) { + if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $blockinstanceid))) { if ($bi = $DB->get_record('block_instances', array('id' => $blockinstanceid), 'id,parentcontextid', $strictness)) { $parentcontext = context::instance_by_id($bi->parentcontextid); - $record = context::insert_context_record(CONTEXT_BLOCK, $bi->id, $parentcontext->path); + $record = context::insert_context_record(self::LEVEL, $bi->id, $parentcontext->path); } } @@ -170,7 +195,6 @@ class block extends context { /** * Create missing context instances at block context level - * @static */ protected static function create_level_instances() { global $DB; @@ -189,15 +213,14 @@ class block extends context { EOF; $DB->execute($sql, [ - 'contextlevel' => CONTEXT_BLOCK, - 'existingcontextlevel' => CONTEXT_BLOCK, + 'contextlevel' => self::LEVEL, + 'existingcontextlevel' => self::LEVEL, ]); } /** * Returns sql necessary for purging of stale context instances. * - * @static * @return string cleanup SQL */ protected static function get_cleanup_sql() { @@ -205,7 +228,7 @@ class block extends context { SELECT c.* FROM {context} c LEFT OUTER JOIN {block_instances} bi ON c.instanceid = bi.id - WHERE bi.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK." + WHERE bi.id IS NULL AND c.contextlevel = ".self::LEVEL." "; return $sql; @@ -214,24 +237,23 @@ class block extends context { /** * Rebuild context paths and depths at block context level. * - * @static * @param bool $force */ protected static function build_paths($force) { global $DB; - if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_BLOCK." AND (depth = 0 OR path IS NULL)")) { + if ($force || $DB->record_exists_select('context', "contextlevel = ".self::LEVEL." AND (depth = 0 OR path IS NULL)")) { if ($force) { $ctxemptyclause = ''; } else { $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; } - // pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent + // The pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent. $sql = "INSERT INTO {context_temp} (id, path, depth, locked) SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked FROM {context} ctx - JOIN {block_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_BLOCK.") + JOIN {block_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = " . self::LEVEL . ") JOIN {context} pctx ON (pctx.id = bi.parentcontextid) WHERE (pctx.path IS NOT NULL AND pctx.depth > 0) $ctxemptyclause"; diff --git a/lib/classes/context/course.php b/lib/classes/context/course.php index e5559f2ea1d..74c4606a057 100644 --- a/lib/classes/context/course.php +++ b/lib/classes/context/course.php @@ -1,5 +1,5 @@ . +// along with Moodle. If not, see . namespace core\context; @@ -26,27 +26,40 @@ use coding_exception, moodle_url; * @package core_access * @category access * @copyright Petr Skoda - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 */ class course extends context { + /** @var int numeric context level value matching legacy CONTEXT_COURSE */ + public const LEVEL = 50; + /** - * Please use context_course::instance($courseid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) + * Please use \core\context\course::instance($courseid) if you need the instance of context. + * Alternatively if you know only the context id use \core\context::instance_by_id($contextid) * * @param stdClass $record */ protected function __construct(stdClass $record) { parent::__construct($record); - if ($record->contextlevel != CONTEXT_COURSE) { - throw new coding_exception('Invalid $record->contextlevel in context_course constructor.'); + if ($record->contextlevel != self::LEVEL) { + throw new coding_exception('Invalid $record->contextlevel in core\context\course constructor.'); } } + /** + * Returns short context name. + * + * @since Moodle 4.2 + * + * @return string + */ + public static function get_short_name(): string { + return 'course'; + } + /** * Returns human readable context level name. * - * @static * @return string the human readable context level name. */ public static function get_level_name() { @@ -68,11 +81,11 @@ class course extends context { if ($this->_instanceid == SITEID) { $name = get_string('frontpage', 'admin'); } else { - if ($course = $DB->get_record('course', array('id'=>$this->_instanceid))) { - if ($withprefix){ + if ($course = $DB->get_record('course', array('id' => $this->_instanceid))) { + if ($withprefix) { $name = get_string('course').': '; } - if ($short){ + if ($short) { if (!$escape) { $name .= format_string($course->shortname, true, array('context' => $this, 'escape' => false)); } else { @@ -98,12 +111,52 @@ class course extends context { */ public function get_url() { if ($this->_instanceid != SITEID) { - return new moodle_url('/course/view.php', array('id'=>$this->_instanceid)); + return new moodle_url('/course/view.php', array('id' => $this->_instanceid)); } return new moodle_url('/'); } + /** + * Returns context instance database name. + * + * @return string|null table name for all levels except system. + */ + protected static function get_instance_table(): ?string { + return 'course'; + } + + /** + * 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 ['shortname']; + } + + /** + * Returns list of all role archetypes that are compatible + * with role assignments in context level. + * @since Moodle 4.2 + * + * @return int[] + */ + protected static function get_compatible_role_archetypes(): array { + return ['manager', 'editingteacher', 'teacher', 'student']; + } + + /** + * Returns list of all possible parent context levels. + * @since Moodle 4.2 + * + * @return int[] + */ + public static function get_possible_parent_levels(): array { + return [coursecat::LEVEL]; + } + /** * Returns array of relevant context capability records. * @@ -113,11 +166,10 @@ class course extends context { public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { global $DB; - return $DB->get_records_list('capabilities', 'contextlevel', [ - CONTEXT_COURSE, - CONTEXT_MODULE, - CONTEXT_BLOCK, - ], $sort); + $levels = \core\context_helper::get_child_levels(self::LEVEL); + $levels[] = self::LEVEL; + + return $DB->get_records_list('capabilities', 'contextlevel', $levels, $sort); } /** @@ -133,25 +185,24 @@ class course extends context { /** * Returns course context instance. * - * @static * @param int $courseid id from {course} table * @param int $strictness - * @return course context instance + * @return course|false context instance */ public static function instance($courseid, $strictness = MUST_EXIST) { global $DB; - if ($context = context::cache_get(CONTEXT_COURSE, $courseid)) { + if ($context = context::cache_get(self::LEVEL, $courseid)) { return $context; } - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSE, 'instanceid' => $courseid))) { + if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $courseid))) { if ($course = $DB->get_record('course', array('id' => $courseid), 'id,category', $strictness)) { if ($course->category) { $parentcontext = coursecat::instance($course->category); - $record = context::insert_context_record(CONTEXT_COURSE, $course->id, $parentcontext->path); + $record = context::insert_context_record(self::LEVEL, $course->id, $parentcontext->path); } else { - $record = context::insert_context_record(CONTEXT_COURSE, $course->id, '/'.SYSCONTEXTID, 0); + $record = context::insert_context_record(self::LEVEL, $course->id, '/'.SYSCONTEXTID, 0); } } } @@ -167,19 +218,18 @@ class course extends context { /** * Create missing context instances at course context level - * @static */ protected static function create_level_instances() { global $DB; - $sql = "SELECT ".CONTEXT_COURSE.", c.id + $sql = "SELECT ".self::LEVEL.", c.id FROM {course} c WHERE NOT EXISTS (SELECT 'x' FROM {context} cx - WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")"; + WHERE c.id = cx.instanceid AND cx.contextlevel=".self::LEVEL.")"; $contextdata = $DB->get_recordset_sql($sql); foreach ($contextdata as $context) { - context::insert_context_record(CONTEXT_COURSE, $context->id, null); + context::insert_context_record(self::LEVEL, $context->id, null); } $contextdata->close(); } @@ -187,7 +237,6 @@ class course extends context { /** * Returns sql necessary for purging of stale context instances. * - * @static * @return string cleanup SQL */ protected static function get_cleanup_sql() { @@ -195,7 +244,7 @@ class course extends context { SELECT c.* FROM {context} c LEFT OUTER JOIN {course} co ON c.instanceid = co.id - WHERE co.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE." + WHERE co.id IS NULL AND c.contextlevel = ".self::LEVEL." "; return $sql; @@ -204,39 +253,38 @@ class course extends context { /** * Rebuild context paths and depths at course context level. * - * @static * @param bool $force */ protected static function build_paths($force) { global $DB; - if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSE." AND (depth = 0 OR path IS NULL)")) { + if ($force || $DB->record_exists_select('context', "contextlevel = ".self::LEVEL." AND (depth = 0 OR path IS NULL)")) { if ($force) { $ctxemptyclause = $emptyclause = ''; } else { $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; - $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; + $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; } $base = '/'.SYSCONTEXTID; - // Standard frontpage + // Standard frontpage. $sql = "UPDATE {context} SET depth = 2, path = ".$DB->sql_concat("'$base/'", 'id')." - WHERE contextlevel = ".CONTEXT_COURSE." + WHERE contextlevel = ".self::LEVEL." AND EXISTS (SELECT 'x' FROM {course} c WHERE c.id = {context}.instanceid AND c.category = 0) $emptyclause"; $DB->execute($sql); - // standard courses + // Standard courses. $sql = "INSERT INTO {context_temp} (id, path, depth, locked) SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked FROM {context} ctx - JOIN {course} c ON (c.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSE." AND c.category <> 0) - JOIN {context} pctx ON (pctx.instanceid = c.category AND pctx.contextlevel = ".CONTEXT_COURSECAT.") + JOIN {course} c ON (c.id = ctx.instanceid AND ctx.contextlevel = ".self::LEVEL." AND c.category <> 0) + JOIN {context} pctx ON (pctx.instanceid = c.category AND pctx.contextlevel = ".coursecat::LEVEL.") WHERE pctx.path IS NOT NULL AND pctx.depth > 0 $ctxemptyclause"; $trans = $DB->start_delegated_transaction(); diff --git a/lib/classes/context/coursecat.php b/lib/classes/context/coursecat.php index 3a1d0e0c991..43eb0a01959 100644 --- a/lib/classes/context/coursecat.php +++ b/lib/classes/context/coursecat.php @@ -1,5 +1,5 @@ . +// along with Moodle. If not, see . namespace core\context; @@ -26,27 +26,40 @@ use coding_exception, moodle_url; * @package core_access * @category access * @copyright Petr Skoda - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 */ class coursecat extends context { + /** @var int numeric context level value matching legacy CONTEXT_COURSECAT */ + public const LEVEL = 40; + /** - * Please use context_coursecat::instance($coursecatid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) + * Please use \core\context\coursecat::instance($coursecatid) if you need the instance of context. + * Alternatively if you know only the context id use \core\context::instance_by_id($contextid) * * @param stdClass $record */ protected function __construct(stdClass $record) { parent::__construct($record); - if ($record->contextlevel != CONTEXT_COURSECAT) { - throw new coding_exception('Invalid $record->contextlevel in context_coursecat constructor.'); + if ($record->contextlevel != self::LEVEL) { + throw new coding_exception('Invalid $record->contextlevel in core\context\coursecat constructor.'); } } + /** + * Returns short context name. + * + * @since Moodle 4.2 + * + * @return string + */ + public static function get_short_name(): string { + return 'coursecat'; + } + /** * Returns human readable context level name. * - * @static * @return string the human readable context level name. */ public static function get_level_name() { @@ -65,8 +78,8 @@ class coursecat extends context { global $DB; $name = ''; - if ($category = $DB->get_record('course_categories', array('id'=>$this->_instanceid))) { - if ($withprefix){ + if ($category = $DB->get_record('course_categories', array('id' => $this->_instanceid))) { + if ($withprefix) { $name = get_string('category').': '; } if (!$escape) { @@ -87,6 +100,46 @@ class coursecat extends context { return new moodle_url('/course/index.php', array('categoryid' => $this->_instanceid)); } + /** + * Returns context instance database name. + * + * @return string|null table name for all levels except system. + */ + protected static function get_instance_table(): ?string { + return 'course_categories'; + } + + /** + * 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 ['idnumber']; + } + + /** + * Returns list of all role archetypes that are compatible + * with role assignments in context level. + * @since Moodle 4.2 + * + * @return int[] + */ + protected static function get_compatible_role_archetypes(): array { + return ['manager', 'coursecreator']; + } + + /** + * Returns list of all possible parent context levels. + * @since Moodle 4.2 + * + * @return int[] + */ + public static function get_possible_parent_levels(): array { + return [system::LEVEL, self::LEVEL]; + } + /** * Returns array of relevant context capability records. * @@ -96,18 +149,15 @@ class coursecat extends context { public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { global $DB; - return $DB->get_records_list('capabilities', 'contextlevel', [ - CONTEXT_COURSECAT, - CONTEXT_COURSE, - CONTEXT_MODULE, - CONTEXT_BLOCK, - ], $sort); + $levels = \core\context_helper::get_child_levels(self::LEVEL); + $levels[] = self::LEVEL; + + return $DB->get_records_list('capabilities', 'contextlevel', $levels, $sort); } /** * Returns course category context instance. * - * @static * @param int $categoryid id from {course_categories} table * @param int $strictness * @return coursecat|false context instance @@ -115,17 +165,17 @@ class coursecat extends context { public static function instance($categoryid, $strictness = MUST_EXIST) { global $DB; - if ($context = context::cache_get(CONTEXT_COURSECAT, $categoryid)) { + if ($context = context::cache_get(self::LEVEL, $categoryid)) { return $context; } - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSECAT, 'instanceid' => $categoryid))) { + if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $categoryid))) { if ($category = $DB->get_record('course_categories', array('id' => $categoryid), 'id,parent', $strictness)) { if ($category->parent) { - $parentcontext = coursecat::instance($category->parent); - $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, $parentcontext->path); + $parentcontext = self::instance($category->parent); + $record = context::insert_context_record(self::LEVEL, $category->id, $parentcontext->path); } else { - $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, '/'.SYSCONTEXTID, 0); + $record = context::insert_context_record(self::LEVEL, $category->id, '/'.SYSCONTEXTID, 0); } } } @@ -148,7 +198,7 @@ class coursecat extends context { public function get_child_contexts() { global $DB; - if (empty($this->_path) or empty($this->_depth)) { + if (empty($this->_path) || empty($this->_depth)) { debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths'); return array(); } @@ -156,7 +206,7 @@ class coursecat extends context { $sql = "SELECT ctx.* FROM {context} ctx WHERE ctx.path LIKE ? AND (ctx.depth = ? OR ctx.contextlevel = ?)"; - $params = array($this->_path.'/%', $this->depth+1, CONTEXT_COURSECAT); + $params = array($this->_path.'/%', $this->depth + 1, self::LEVEL); $records = $DB->get_records_sql($sql, $params); $result = array(); @@ -169,19 +219,18 @@ class coursecat extends context { /** * Create missing context instances at course category context level - * @static */ protected static function create_level_instances() { global $DB; - $sql = "SELECT ".CONTEXT_COURSECAT.", cc.id + $sql = "SELECT ".self::LEVEL.", cc.id FROM {course_categories} cc WHERE NOT EXISTS (SELECT 'x' FROM {context} cx - WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")"; + WHERE cc.id = cx.instanceid AND cx.contextlevel=".self::LEVEL.")"; $contextdata = $DB->get_recordset_sql($sql); foreach ($contextdata as $context) { - context::insert_context_record(CONTEXT_COURSECAT, $context->id, null); + context::insert_context_record(self::LEVEL, $context->id, null); } $contextdata->close(); } @@ -189,7 +238,6 @@ class coursecat extends context { /** * Returns sql necessary for purging of stale context instances. * - * @static * @return string cleanup SQL */ protected static function get_cleanup_sql() { @@ -197,7 +245,7 @@ class coursecat extends context { SELECT c.* FROM {context} c LEFT OUTER JOIN {course_categories} cc ON c.instanceid = cc.id - WHERE cc.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT." + WHERE cc.id IS NULL AND c.contextlevel = ".self::LEVEL." "; return $sql; @@ -206,41 +254,41 @@ class coursecat extends context { /** * Rebuild context paths and depths at course category context level. * - * @static * @param bool $force */ protected static function build_paths($force) { global $DB; - if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSECAT." AND (depth = 0 OR path IS NULL)")) { + if ($force || $DB->record_exists_select('context', "contextlevel = ".self::LEVEL." AND (depth = 0 OR path IS NULL)")) { if ($force) { $ctxemptyclause = $emptyclause = ''; } else { $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; - $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; + $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)"; } $base = '/'.SYSCONTEXTID; - // Normal top level categories + // Normal top level categories. $sql = "UPDATE {context} SET depth=2, path=".$DB->sql_concat("'$base/'", 'id')." - WHERE contextlevel=".CONTEXT_COURSECAT." + WHERE contextlevel=".self::LEVEL." AND EXISTS (SELECT 'x' FROM {course_categories} cc WHERE cc.id = {context}.instanceid AND cc.depth=1) $emptyclause"; $DB->execute($sql); - // Deeper categories - one query per depthlevel + // Deeper categories - one query per depthlevel. $maxdepth = $DB->get_field_sql("SELECT MAX(depth) FROM {course_categories}"); - for ($n=2; $n<=$maxdepth; $n++) { + for ($n = 2; $n <= $maxdepth; $n++) { $sql = "INSERT INTO {context_temp} (id, path, depth, locked) SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked FROM {context} ctx - JOIN {course_categories} cc ON (cc.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT." AND cc.depth = $n) - JOIN {context} pctx ON (pctx.instanceid = cc.parent AND pctx.contextlevel = ".CONTEXT_COURSECAT.") + JOIN {course_categories} cc ON (cc.id = ctx.instanceid AND ctx.contextlevel = ".self::LEVEL." + AND cc.depth = $n) + JOIN {context} pctx ON (pctx.instanceid = cc.parent AND pctx.contextlevel = ".self::LEVEL.") WHERE pctx.path IS NOT NULL AND pctx.depth > 0 $ctxemptyclause"; $trans = $DB->start_delegated_transaction(); diff --git a/lib/classes/context/module.php b/lib/classes/context/module.php index 9f5e28096bd..e1db534868f 100644 --- a/lib/classes/context/module.php +++ b/lib/classes/context/module.php @@ -1,5 +1,5 @@ . +// along with Moodle. If not, see . namespace core\context; @@ -26,27 +26,40 @@ use coding_exception, moodle_url; * @package core_access * @category access * @copyright Petr Skoda - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 */ class module extends context { + /** @var int numeric context level value matching legacy CONTEXT_MODULE */ + public const LEVEL = 70; + /** - * Please use context_module::instance($cmid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) + * Please use \core\context\module::instance($cmid) if you need the instance of context. + * Alternatively if you know only the context id use \core\context::instance_by_id($contextid) * * @param stdClass $record */ protected function __construct(stdClass $record) { parent::__construct($record); - if ($record->contextlevel != CONTEXT_MODULE) { - throw new coding_exception('Invalid $record->contextlevel in context_module constructor.'); + if ($record->contextlevel != self::LEVEL) { + throw new coding_exception('Invalid $record->contextlevel in core\context\module constructor.'); } } + /** + * Returns short context name. + * + * @since Moodle 4.2 + * + * @return string + */ + public static function get_short_name(): string { + return 'module'; + } + /** * Returns human readable context level name. * - * @static * @return string the human readable context level name. */ public static function get_level_name() { @@ -71,7 +84,7 @@ class module extends context { JOIN {modules} md ON md.id = cm.module WHERE cm.id = ?", array($this->_instanceid))) { if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) { - if ($withprefix){ + if ($withprefix) { $name = get_string('modulename', $cm->modname).': '; } if (!$escape) { @@ -96,12 +109,52 @@ class module extends context { FROM {course_modules} cm JOIN {modules} md ON md.id = cm.module WHERE cm.id = ?", array($this->_instanceid))) { - return new moodle_url('/mod/' . $modname . '/view.php', array('id'=>$this->_instanceid)); + return new moodle_url('/mod/' . $modname . '/view.php', array('id' => $this->_instanceid)); } return new moodle_url('/'); } + /** + * Returns context instance database name. + * + * @return string|null table name for all levels except system. + */ + protected static function get_instance_table(): ?string { + return 'course_modules'; + } + + /** + * 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 ['idnumber']; + } + + /** + * Returns list of all role archetypes that are compatible + * with role assignments in context level. + * @since Moodle 4.2 + * + * @return int[] + */ + protected static function get_compatible_role_archetypes(): array { + return ['editingteacher', 'teacher', 'student']; + } + + /** + * Returns list of all possible parent context levels. + * @since Moodle 4.2 + * + * @return int[] + */ + public static function get_possible_parent_levels(): array { + return [course::LEVEL]; + } + /** * Returns array of relevant context capability records. * @@ -111,8 +164,8 @@ class module extends context { public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { global $DB, $CFG; - $cm = $DB->get_record('course_modules', array('id'=>$this->_instanceid)); - $module = $DB->get_record('modules', array('id'=>$cm->module)); + $cm = $DB->get_record('course_modules', array('id' => $this->_instanceid)); + $module = $DB->get_record('modules', array('id' => $cm->module)); $subcaps = array(); @@ -123,13 +176,13 @@ class module extends context { debugging('Use of subplugins.php has been deprecated. ' . 'Please update your plugin to provide a subplugins.json file instead.', DEBUG_DEVELOPER); - $subplugins = array(); // should be redefined in the file + $subplugins = array(); // Should be redefined in the file. include("{$modulepath}/db/subplugins.php"); } if (!empty($subplugins)) { foreach (array_keys($subplugins) as $subplugintype) { - foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) { + foreach (array_keys(\core_component::get_plugin_list($subplugintype)) as $subpluginname) { $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname))); } } @@ -163,7 +216,6 @@ class module extends context { list($notcompsql, $notcompparams) = $DB->get_in_or_equal($componentnames, SQL_PARAMS_NAMED, 'notcomp', false); $params = array_merge($params, $notcompparams); - // Exclude other component submodules. $i = 0; $ignorecomponents = []; @@ -181,7 +233,7 @@ class module extends context { $sql = "SELECT * FROM {capabilities} - WHERE (contextlevel = ".CONTEXT_MODULE." + WHERE (contextlevel = ".self::LEVEL." AND component {$notcompsql} AND {$notlikesql}) $extra @@ -203,7 +255,6 @@ class module extends context { /** * Returns module context instance. * - * @static * @param int $cmid id of the record from {course_modules} table; pass cmid there, NOT id in the instance column * @param int $strictness * @return module|false context instance @@ -211,14 +262,14 @@ class module extends context { public static function instance($cmid, $strictness = MUST_EXIST) { global $DB; - if ($context = context::cache_get(CONTEXT_MODULE, $cmid)) { + if ($context = context::cache_get(self::LEVEL, $cmid)) { return $context; } - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_MODULE, 'instanceid' => $cmid))) { + if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $cmid))) { if ($cm = $DB->get_record('course_modules', array('id' => $cmid), 'id,course', $strictness)) { $parentcontext = course::instance($cm->course); - $record = context::insert_context_record(CONTEXT_MODULE, $cm->id, $parentcontext->path); + $record = context::insert_context_record(self::LEVEL, $cm->id, $parentcontext->path); } } @@ -233,19 +284,18 @@ class module extends context { /** * Create missing context instances at module context level - * @static */ protected static function create_level_instances() { global $DB; - $sql = "SELECT ".CONTEXT_MODULE.", cm.id + $sql = "SELECT " . self::LEVEL . ", cm.id FROM {course_modules} cm WHERE NOT EXISTS (SELECT 'x' FROM {context} cx - WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")"; + WHERE cm.id = cx.instanceid AND cx.contextlevel=" . self::LEVEL . ")"; $contextdata = $DB->get_recordset_sql($sql); foreach ($contextdata as $context) { - context::insert_context_record(CONTEXT_MODULE, $context->id, null); + context::insert_context_record(self::LEVEL, $context->id, null); } $contextdata->close(); } @@ -253,7 +303,6 @@ class module extends context { /** * Returns sql necessary for purging of stale context instances. * - * @static * @return string cleanup SQL */ protected static function get_cleanup_sql() { @@ -261,7 +310,7 @@ class module extends context { SELECT c.* FROM {context} c LEFT OUTER JOIN {course_modules} cm ON c.instanceid = cm.id - WHERE cm.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE." + WHERE cm.id IS NULL AND c.contextlevel = " . self::LEVEL . " "; return $sql; @@ -270,13 +319,12 @@ class module extends context { /** * Rebuild context paths and depths at module context level. * - * @static * @param bool $force */ protected static function build_paths($force) { global $DB; - if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_MODULE." AND (depth = 0 OR path IS NULL)")) { + if ($force || $DB->record_exists_select('context', "contextlevel = " . self::LEVEL . " AND (depth = 0 OR path IS NULL)")) { if ($force) { $ctxemptyclause = ''; } else { @@ -286,8 +334,8 @@ class module extends context { $sql = "INSERT INTO {context_temp} (id, path, depth, locked) SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked FROM {context} ctx - JOIN {course_modules} cm ON (cm.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_MODULE.") - JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = ".CONTEXT_COURSE.") + JOIN {course_modules} cm ON (cm.id = ctx.instanceid AND ctx.contextlevel = " . self::LEVEL . ") + JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = " . course::LEVEL . ") WHERE pctx.path IS NOT NULL AND pctx.depth > 0 $ctxemptyclause"; $trans = $DB->start_delegated_transaction(); diff --git a/lib/classes/context/system.php b/lib/classes/context/system.php index 1f885f35ede..39c64e0a4dd 100644 --- a/lib/classes/context/system.php +++ b/lib/classes/context/system.php @@ -1,5 +1,5 @@ . +// along with Moodle. If not, see . namespace core\context; @@ -26,26 +26,39 @@ use coding_exception, moodle_url; * @package core_access * @category access * @copyright Petr Skoda - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 */ class system extends context { + /** @var int numeric context level value matching legacy CONTEXT_SYSTEM */ + public const LEVEL = 10; + /** - * Please use context_system::instance() if you need the instance of context. + * Please use \core\context\system::instance() if you need the instance of context. * * @param stdClass $record */ protected function __construct(stdClass $record) { parent::__construct($record); - if ($record->contextlevel != CONTEXT_SYSTEM) { - throw new coding_exception('Invalid $record->contextlevel in context_system constructor.'); + if ($record->contextlevel != self::LEVEL) { + throw new coding_exception('Invalid $record->contextlevel in core\context\system constructor.'); } } + /** + * Returns short context name. + * + * @since Moodle 4.2 + * + * @return string + */ + public static function get_short_name(): string { + return 'system'; + } + /** * Returns human readable context level name. * - * @static * @return string the human readable context level name. */ public static function get_level_name() { @@ -73,6 +86,27 @@ class system extends context { return new moodle_url('/'); } + /** + * Returns list of all role archetypes that are compatible + * with role assignments in context level. + * @since Moodle 4.2 + * + * @return int[] + */ + protected static function get_compatible_role_archetypes(): array { + return ['manager', 'coursecreator']; + } + + /** + * Returns list of all possible parent context levels. + * @since Moodle 4.2 + * + * @return int[] + */ + public static function get_possible_parent_levels(): array { + return []; + } + /** * Returns array of relevant context capability records. * @@ -87,17 +121,15 @@ class system extends context { /** * Create missing context instances at system context - * @static */ protected static function create_level_instances() { - // nothing to do here, the system context is created automatically in installer + // Nothing to do here, the system context is created automatically in installer. self::instance(0); } /** * Returns system context instance. * - * @static * @param int $instanceid should be 0 * @param int $strictness * @param bool $cache @@ -111,15 +143,15 @@ class system extends context { } // SYSCONTEXTID is cached in local cache to eliminate 1 query per page. - if (defined('SYSCONTEXTID') and $cache) { + if (defined('SYSCONTEXTID') && $cache) { if (!isset(context::$systemcontext)) { $record = new stdClass(); - $record->id = SYSCONTEXTID; - $record->contextlevel = CONTEXT_SYSTEM; - $record->instanceid = 0; - $record->path = '/'.SYSCONTEXTID; - $record->depth = 1; - $record->locked = 0; + $record->id = SYSCONTEXTID; + $record->contextlevel = self::LEVEL; + $record->instanceid = 0; + $record->path = '/'.SYSCONTEXTID; + $record->depth = 1; + $record->locked = 0; context::$systemcontext = new system($record); } return context::$systemcontext; @@ -127,11 +159,11 @@ class system extends context { try { // We ignore the strictness completely because system context must exist except during install. - $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST); + $record = $DB->get_record('context', array('contextlevel' => self::LEVEL), '*', MUST_EXIST); } catch (\dml_exception $e) { - //table or record does not exist + // Table or record does not exist. if (!during_initial_install()) { - // do not mess with system context after install, it simply must exist + // Do not mess with system context after install, it simply must exist. throw $e; } $record = null; @@ -139,19 +171,19 @@ class system extends context { if (!$record) { $record = new stdClass(); - $record->contextlevel = CONTEXT_SYSTEM; - $record->instanceid = 0; - $record->depth = 1; - $record->path = null; // Not known before insert. - $record->locked = 0; + $record->contextlevel = self::LEVEL; + $record->instanceid = 0; + $record->depth = 1; + $record->path = null; // Not known before insert. + $record->locked = 0; try { if ($DB->count_records('context')) { - // contexts already exist, this is very weird, system must be first!!! + // Contexts already exist, this is very weird, system must be first!!! return null; } if (defined('SYSCONTEXTID')) { - // this would happen only in unittest on sites that went through weird 1.7 upgrade + // This would happen only in unittest on sites that went through weird 1.7 upgrade. $record->id = SYSCONTEXTID; $DB->import_record('context', $record); $DB->get_manager()->reset_sequence('context'); @@ -159,20 +191,20 @@ class system extends context { $record->id = $DB->insert_record('context', $record); } } catch (\dml_exception $e) { - // can not create context - table does not exist yet, sorry + // Can not create context - table does not exist yet, sorry. return null; } } if ($record->instanceid != 0) { - // this is very weird, somebody must be messing with context table + // This is very weird, somebody must be messing with context table. debugging('Invalid system context detected'); } - if ($record->depth != 1 or $record->path != '/'.$record->id) { - // fix path if necessary, initial install or path reset + if ($record->depth != 1 || $record->path != '/'.$record->id) { + // Fix path if necessary, initial install or path reset. $record->depth = 1; - $record->path = '/'.$record->id; + $record->path = '/'.$record->id; $DB->update_record('context', $record); } @@ -198,13 +230,14 @@ class system extends context { public function get_child_contexts() { global $DB; - debugging('Fetching of system context child courses is strongly discouraged on production servers (it may eat all available memory)!'); + debugging('Fetching of system context child courses is strongly discouraged' + . ' on production servers (it may eat all available memory)!'); - // Just get all the contexts except for CONTEXT_SYSTEM level - // and hope we don't OOM in the process - don't cache + // Just get all the contexts except for system level + // and hope we don't OOM in the process - don't cache. $sql = "SELECT c.* FROM {context} c - WHERE contextlevel > ".CONTEXT_SYSTEM; + WHERE contextlevel > " . self::LEVEL; $records = $DB->get_records_sql($sql); $result = array(); @@ -218,7 +251,6 @@ class system extends context { /** * Returns sql necessary for purging of stale context instances. * - * @static * @return string cleanup SQL */ protected static function get_cleanup_sql() { @@ -234,7 +266,6 @@ class system extends context { /** * Rebuild context paths and depths at system context level. * - * @static * @param bool $force */ protected static function build_paths($force) { @@ -242,21 +273,21 @@ class system extends context { /* note: ignore $force here, we always do full test of system context */ - // exactly one record must exist - $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST); + // Exactly one record must exist. + $record = $DB->get_record('context', array('contextlevel' => self::LEVEL), '*', MUST_EXIST); if ($record->instanceid != 0) { debugging('Invalid system context detected'); } - if (defined('SYSCONTEXTID') and $record->id != SYSCONTEXTID) { + if (defined('SYSCONTEXTID') && $record->id != SYSCONTEXTID) { debugging('Invalid SYSCONTEXTID detected'); } - if ($record->depth != 1 or $record->path != '/'.$record->id) { - // fix path if necessary, initial install or path reset - $record->depth = 1; - $record->path = '/'.$record->id; + if ($record->depth != 1 || $record->path != '/'.$record->id) { + // Fix path if necessary, initial install or path reset. + $record->depth = 1; + $record->path = '/'.$record->id; $DB->update_record('context', $record); } } @@ -268,8 +299,9 @@ class system extends context { * @return $this */ public function set_locked(bool $locked) { - throw new \coding_exception('It is not possible to lock the system context'); - - return $this; + if ($locked) { + throw new \coding_exception('It is not possible to lock the system context'); + } + return parent::set_locked($locked); } } diff --git a/lib/classes/context/user.php b/lib/classes/context/user.php index 63778fddfe7..9d4cf95775f 100644 --- a/lib/classes/context/user.php +++ b/lib/classes/context/user.php @@ -1,5 +1,5 @@ . +// along with Moodle. If not, see . namespace core\context; @@ -26,27 +26,40 @@ use coding_exception, moodle_url; * @package core_access * @category access * @copyright Petr Skoda - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 */ class user extends context { + /** @var int numeric context level value matching legacy CONTEXT_USER */ + public const LEVEL = 30; + /** - * Please use context_user::instance($userid) if you need the instance of context. - * Alternatively if you know only the context id use context::instance_by_id($contextid) + * Please use \core\context\user::instance($userid) if you need the instance of context. + * Alternatively if you know only the context id use \core\context::instance_by_id($contextid) * * @param stdClass $record */ protected function __construct(stdClass $record) { parent::__construct($record); - if ($record->contextlevel != CONTEXT_USER) { - throw new coding_exception('Invalid $record->contextlevel in context_user constructor.'); + if ($record->contextlevel != self::LEVEL) { + throw new coding_exception('Invalid $record->contextlevel in core\context\user constructor.'); } } + /** + * Returns short context name. + * + * @since Moodle 4.2 + * + * @return string + */ + public static function get_short_name(): string { + return 'user'; + } + /** * Returns human readable context level name. * - * @static * @return string the human readable context level name. */ public static function get_level_name() { @@ -65,8 +78,8 @@ class user extends context { global $DB; $name = ''; - if ($user = $DB->get_record('user', array('id'=>$this->_instanceid, 'deleted'=>0))) { - if ($withprefix){ + if ($user = $DB->get_record('user', array('id' => $this->_instanceid, 'deleted' => 0))) { + if ($withprefix) { $name = get_string('user').': '; } $name .= fullname($user); @@ -83,13 +96,42 @@ class user extends context { global $COURSE; if ($COURSE->id == SITEID) { - $url = new moodle_url('/user/profile.php', array('id'=>$this->_instanceid)); + $url = new moodle_url('/user/profile.php', array('id' => $this->_instanceid)); } else { - $url = new moodle_url('/user/view.php', array('id'=>$this->_instanceid, 'courseid'=>$COURSE->id)); + $url = new moodle_url('/user/view.php', array('id' => $this->_instanceid, 'courseid' => $COURSE->id)); } return $url; } + /** + * Returns list of all possible parent context levels. + * @since Moodle 4.2 + * + * @return int[] + */ + public static function get_possible_parent_levels(): array { + return [system::LEVEL]; + } + + /** + * Returns context instance database name. + * + * @return string|null table name for all levels except system. + */ + protected static function get_instance_table(): ?string { + return 'user'; + } + + /** + * 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 ['username']; + } + /** * Returns array of relevant context capability records. * @@ -103,13 +145,12 @@ class user extends context { list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap'); return $DB->get_records_select('capabilities', "contextlevel = :level OR name {$extra}", - $params + ['level' => CONTEXT_USER], $sort); + $params + ['level' => self::LEVEL], $sort); } /** * Returns user context instance. * - * @static * @param int $userid id from {user} table * @param int $strictness * @return user|false context instance @@ -117,13 +158,13 @@ class user extends context { public static function instance($userid, $strictness = MUST_EXIST) { global $DB; - if ($context = context::cache_get(CONTEXT_USER, $userid)) { + if ($context = context::cache_get(self::LEVEL, $userid)) { return $context; } - if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_USER, 'instanceid' => $userid))) { + if (!$record = $DB->get_record('context', array('contextlevel' => self::LEVEL, 'instanceid' => $userid))) { if ($user = $DB->get_record('user', array('id' => $userid, 'deleted' => 0), 'id', $strictness)) { - $record = context::insert_context_record(CONTEXT_USER, $user->id, '/'.SYSCONTEXTID, 0); + $record = context::insert_context_record(self::LEVEL, $user->id, '/'.SYSCONTEXTID, 0); } } @@ -138,20 +179,19 @@ class user extends context { /** * Create missing context instances at user context level - * @static */ protected static function create_level_instances() { global $DB; - $sql = "SELECT ".CONTEXT_USER.", u.id + $sql = "SELECT " . self::LEVEL . ", u.id FROM {user} u WHERE u.deleted = 0 AND NOT EXISTS (SELECT 'x' FROM {context} cx - WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")"; + WHERE u.id = cx.instanceid AND cx.contextlevel=" . self::LEVEL . ")"; $contextdata = $DB->get_recordset_sql($sql); foreach ($contextdata as $context) { - context::insert_context_record(CONTEXT_USER, $context->id, null); + context::insert_context_record(self::LEVEL, $context->id, null); } $contextdata->close(); } @@ -159,7 +199,6 @@ class user extends context { /** * Returns sql necessary for purging of stale context instances. * - * @static * @return string cleanup SQL */ protected static function get_cleanup_sql() { @@ -167,7 +206,7 @@ class user extends context { SELECT c.* FROM {context} c LEFT OUTER JOIN {user} u ON (c.instanceid = u.id AND u.deleted = 0) - WHERE u.id IS NULL AND c.contextlevel = ".CONTEXT_USER." + WHERE u.id IS NULL AND c.contextlevel = " . self::LEVEL . " "; return $sql; @@ -176,7 +215,6 @@ class user extends context { /** * Rebuild context paths and depths at user context level. * - * @static * @param bool $force */ protected static function build_paths($force) { @@ -197,7 +235,7 @@ class user extends context { $sql = "UPDATE {context} SET depth = 2, path = {$path} - WHERE contextlevel = " . CONTEXT_USER . " + WHERE contextlevel = " . self::LEVEL . " AND ($where)"; $DB->execute($sql, $params); } diff --git a/lib/classes/context_helper.php b/lib/classes/context_helper.php index 1cdadb473c3..30a87368728 100644 --- a/lib/classes/context_helper.php +++ b/lib/classes/context_helper.php @@ -1,5 +1,5 @@ . +// along with Moodle. If not, see . namespace core; use stdClass; use coding_exception; -use core\context\system; -use core\context\course; /** * Context maintenance and helper methods. @@ -33,22 +31,16 @@ use core\context\course; * @package core_access * @category access * @copyright Petr Skoda - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later * @since Moodle 4.2 */ -class context_helper extends context { +abstract class context_helper extends context { /** - * @var array An array mapping context levels to classes + * @var array An array definitions of all context levels */ private static $alllevels; - /** - * Instance does not make sense here, only static use - */ - protected function __construct() { - } - /** * Reset internal context levels array. */ @@ -59,19 +51,19 @@ class context_helper extends context { /** * Initialise context levels, call before using self::$alllevels. */ - private static function init_levels() { + private static function init_levels():void { global $CFG; if (isset(self::$alllevels)) { return; } self::$alllevels = array( - CONTEXT_SYSTEM => 'context_system', - CONTEXT_USER => 'context_user', - CONTEXT_COURSECAT => 'context_coursecat', - CONTEXT_COURSE => 'context_course', - CONTEXT_MODULE => 'context_module', - CONTEXT_BLOCK => 'context_block', + CONTEXT_SYSTEM => \core\context\system::class, + CONTEXT_USER => \core\context\user::class, + CONTEXT_COURSECAT => \core\context\coursecat::class, + CONTEXT_COURSE => \core\context\course::class, + CONTEXT_MODULE => \core\context\module::class, + CONTEXT_BLOCK => \core\context\block::class, ); if (empty($CFG->custom_context_classes)) { @@ -94,14 +86,113 @@ class context_helper extends context { ksort(self::$alllevels); } + /** + * Converts legacy context_* class name to new class name. + * + * NOTE: this is needed for external API which uses short context names. + * @since Moodle 4.2 + * + * @param int|string $extlevel + * @return string|null context class name or null if not found + */ + public static function parse_external_level($extlevel): ?string { + self::init_levels(); + if (is_number($extlevel)) { + if (isset(self::$alllevels[$extlevel])) { + return self::$alllevels[$extlevel]; + } else { + return null; + } + } + if ($extlevel && is_string($extlevel)) { + $found = null; + foreach (self::$alllevels as $classname) { + if ($classname::get_short_name() === $extlevel) { + if ($found) { + debugging("Duplicate short context level name found '$extlevel', use numeric value instead", + DEBUG_DEVELOPER); + } else { + $found = $classname; + } + } + } + return $found; + } + return null; + } + + /** + * Resolve reference to context used in behat feature files. + * + * @param string $level + * @param string $reference + * @return context|null + */ + public static function resolve_behat_reference(string $level, string $reference): ?context { + global $DB; + + if (!PHPUNIT_TEST && !defined('BEHAT_SITE_RUNNING')) { + throw new coding_exception('resolve_behat_reference() cannot be used outside of tests'); + } + self::init_levels(); + + $classname = null; + if (is_number($level)) { + if (isset(self::$alllevels[$level])) { + $classname = self::$alllevels[$level]; + } + } else { + foreach (self::$alllevels as $levelclassname) { + if ($level === $levelclassname::get_level_name()) { + $classname = $levelclassname; + break; + } + if ($level === $levelclassname::get_short_name()) { + $classname = $levelclassname; + break; + } + } + } + if (!$classname) { + return null; + } + + if ($classname::LEVEL === context\system::LEVEL) { + return context\system::instance(); + } + + if (trim($reference) === '') { + return null; + } + + $table = $classname::get_instance_table(); + if (!$table) { + return null; + } + + $columns = $classname::get_behat_reference_columns(); + foreach ($columns as $column) { + $instance = $DB->get_record($table, [$column => $reference]); + if ($instance) { + $context = $classname::instance($instance->id, IGNORE_MISSING); + if ($context) { + return $context; + } + return null; + } + } + + return null; + } + /** * Returns a class name of the context level class * - * @static * @param int $contextlevel (CONTEXT_SYSTEM, etc.) * @return string class name of the context class + * @throws coding_exception if level does not exist */ - public static function get_class_for_level($contextlevel) { + public static function get_class_for_level(int $contextlevel): string { self::init_levels(); if (isset(self::$alllevels[$contextlevel])) { return self::$alllevels[$contextlevel]; @@ -113,19 +204,75 @@ class context_helper extends context { /** * Returns a list of all context levels * - * @static * @return array int=>string (level=>level class name) */ - public static function get_all_levels() { + public static function get_all_levels(): array { self::init_levels(); return self::$alllevels; } + /** + * Get list of possible child levels for given level. + * @since Moodle 4.2 + * + * @param int $parentlevel + * @return int[] list of context levels that my be children of given context level. + */ + public static function get_child_levels(int $parentlevel): array { + self::init_levels(); + $result = []; + $definitions = self::$alllevels; + + $recursion = function(int $pl) use (&$result, $definitions, &$recursion): void { + foreach ($definitions as $contextlevel => $classname) { + $parentlevels = $classname::get_possible_parent_levels(); + if (in_array($pl, $parentlevels)) { + if (isset($result[$contextlevel])) { + continue; + } + $result[$contextlevel] = $contextlevel; + $recursion($contextlevel); + } + } + }; + $recursion($parentlevel); + + $classname = self::get_class_for_level($parentlevel); + $parentlevels = $classname::get_possible_parent_levels(); + if (!in_array($parentlevel, $parentlevels)) { + unset($result[$parentlevel]); + } + + return array_values($result); + } + + /** + * Returns context levels that compatible with role archetype assignments. + * @since Moodle 4.2 + * + * @param string $archetype + * @return array + */ + public static function get_compatible_levels(string $archetype): array { + self::init_levels(); + $result = []; + + foreach (self::$alllevels as $contextlevel => $classname) { + $compatiblearchetypes = $classname::get_compatible_role_archetypes(); + foreach ($compatiblearchetypes as $at) { + if ($at === $archetype) { + $result[] = $contextlevel; + } + } + } + + return $result; + } + /** * Remove stale contexts that belonged to deleted instances. * Ideally all code should cleanup contexts properly, unfortunately accidents happen... * - * @static * @return void */ public static function cleanup_instances() { @@ -133,13 +280,13 @@ class context_helper extends context { self::init_levels(); $sqls = array(); - foreach (self::$alllevels as $level=>$classname) { + foreach (self::$alllevels as $classname) { $sqls[] = $classname::get_cleanup_sql(); } $sql = implode(" UNION ", $sqls); - // it is probably better to use transactions, it might be faster too + // It is probably better to use transactions, it might be faster too. $transaction = $DB->start_delegated_transaction(); $rs = $DB->get_recordset_sql($sql); @@ -155,16 +302,16 @@ class context_helper extends context { /** * Create all context instances at the given level and above. * - * @static * @param int $contextlevel null means all levels * @param bool $buildpaths * @return void */ public static function create_instances($contextlevel = null, $buildpaths = true) { self::init_levels(); - foreach (self::$alllevels as $level=>$classname) { - if ($contextlevel and $level > $contextlevel) { - // skip potential sub-contexts + foreach (self::$alllevels as $level => $classname) { + if ($contextlevel && $contextlevel != context\block::LEVEL && $level > $contextlevel) { + // Skip potential sub-contexts, + // in case of blocks build all contexts because plugin contexts may have higher levels. continue; } $classname::create_level_instances(); @@ -177,7 +324,6 @@ class context_helper extends context { /** * Rebuild paths and depths in all context levels. * - * @static * @param bool $force false means add missing only * @return void */ @@ -187,13 +333,12 @@ class context_helper extends context { $classname::build_paths($force); } - // reset static course cache - it might have incorrect cached data + // Reset static course cache - it might have incorrect cached data. accesslib_clear_all_caches(true); } /** * Resets the cache to remove all data. - * @static */ public static function reset_caches() { context::reset_caches(); @@ -204,7 +349,6 @@ class context_helper extends context { * * This helps with performance when dealing with hundreds of contexts. * - * @static * @param string $tablealias context table alias in the query * @return array (table.column=>alias, ...) */ @@ -224,7 +368,6 @@ class context_helper extends context { * * This helps with performance when dealing with hundreds of contexts. * - * @static * @param string $tablealias context table alias in the query * @return string */ @@ -242,12 +385,11 @@ class context_helper extends context { * * The db request has to contain all columns from context_helper::get_preload_record_columns(). * - * @static * @param stdClass $rec * @return void This is intentional. See MDL-37115. You will need to get the context * in the normal way, but it is now cached, so that will be fast. */ - public static function preload_from_record(stdClass $rec) { + public static function preload_from_record(stdClass $rec): void { context::preload_from_record($rec); } @@ -256,7 +398,7 @@ class context_helper extends context { * * @param array $contextids */ - public static function preload_contexts_by_id(array $contextids) { + public static function preload_contexts_by_id(array $contextids): void { global $DB; // Determine which contexts are not already cached. @@ -272,7 +414,7 @@ class context_helper extends context { // There is no point only fetching a single context as this would be no more efficient than calling the existing code. list($insql, $inparams) = $DB->get_in_or_equal($tofetch, SQL_PARAMS_NAMED); $ctxs = $DB->get_records_select('context', "id {$insql}", $inparams, '', - context_helper::get_preload_record_columns_sql('{context}')); + self::get_preload_record_columns_sql('{context}')); foreach ($ctxs as $ctx) { self::preload_from_record($ctx); } @@ -284,15 +426,14 @@ class context_helper extends context { * * To be used if you expect multiple queries for course activities... * - * @static * @param int $courseid */ public static function preload_course($courseid) { - // Users can call this multiple times without doing any harm + // Users can call this multiple times without doing any harm. if (isset(context::$cache_preloaded[$courseid])) { return; } - $coursecontext = course::instance($courseid); + $coursecontext = context\course::instance($courseid); $coursecontext->get_child_contexts(); context::$cache_preloaded[$courseid] = true; @@ -301,7 +442,6 @@ class context_helper extends context { /** * Delete context instance * - * @static * @param int $contextlevel * @param int $instanceid * @return void @@ -309,24 +449,21 @@ class context_helper extends context { public static function delete_instance($contextlevel, $instanceid) { global $DB; - // double check the context still exists - if ($record = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) { + // Double check the context still exists. + if ($record = $DB->get_record('context', array('contextlevel' => $contextlevel, 'instanceid' => $instanceid))) { $context = context::create_instance_from_record($record); $context->delete(); - } else { - // we should try to purge the cache anyway } } /** * Returns the name of specified context level * - * @static * @param int $contextlevel * @return string name of the context level */ public static function get_level_name($contextlevel) { - $classname = context_helper::get_class_for_level($contextlevel); + $classname = self::get_class_for_level($contextlevel); return $classname::get_level_name(); } @@ -339,23 +476,9 @@ class context_helper extends context { public static function get_navigation_filter_context(?context $context): ?context { global $CFG; if (!empty($CFG->filternavigationwithsystemcontext)) { - return system::instance(); + return context\system::instance(); } else { return $context; } } - - /** - * not used - */ - public function get_url() { - } - - /** - * not used - * - * @param string $sort - */ - public function get_capabilities(string $sort = self::DEFAULT_CAPABILITY_SORT) { - } } diff --git a/lib/external/classes/external_api.php b/lib/external/classes/external_api.php index 404ef75ae7a..5bd51bd93c7 100644 --- a/lib/external/classes/external_api.php +++ b/lib/external/classes/external_api.php @@ -519,7 +519,9 @@ class external_api { * The passed array must either contain a contextid or a combination of context level and instance id to fetch the context. * For example, the context level can be "course" and instanceid can be courseid. * - * See context_helper::get_all_levels() for a list of valid context levels. + * See context_helper::get_all_levels() for a list of valid numeric context levels, + * legacy short names such as 'system', 'user', 'course' are not supported in new + * plugin capabilities. * * @param array $param * @since Moodle 2.6 @@ -527,15 +529,15 @@ class external_api { * @return context */ protected static function get_context_from_params($param) { - $levels = context_helper::get_all_levels(); if (!empty($param['contextid'])) { return context::instance_by_id($param['contextid'], IGNORE_MISSING); } else if (!empty($param['contextlevel']) && isset($param['instanceid'])) { - $contextlevel = "context_{$param['contextlevel']}"; - if (!array_search($contextlevel, $levels)) { - throw new invalid_parameter_exception("Invalid context level = {$param['contextlevel']}"); + // Numbers and short names are supported since Moodle 4.2. + $classname = \core\context_helper::parse_external_level($param['contextlevel']); + if (!$classname) { + throw new invalid_parameter_exception('Invalid context level = '.$param['contextlevel']); } - return $contextlevel::instance($param['instanceid'], IGNORE_MISSING); + return $classname::instance($param['instanceid'], IGNORE_MISSING); } else { // No valid context info was found. throw new invalid_parameter_exception( @@ -556,7 +558,7 @@ class external_api { 0 ); $level = new external_value( - PARAM_ALPHA, + PARAM_ALPHANUM, // Since Moodle 4.2 numeric context level values are supported too. 'Context level. To be used with instanceid.', VALUE_DEFAULT, '' diff --git a/lib/external/tests/external_api_test.php b/lib/external/tests/external_api_test.php index f3a50ac3042..87365f0e48a 100644 --- a/lib/external/tests/external_api_test.php +++ b/lib/external/tests/external_api_test.php @@ -184,6 +184,11 @@ class external_api_test extends \advanced_testcase { $fetchedcontext = $this->get_context_from_params(["contextlevel" => "course", "instanceid" => $course->id]); $this->assertEquals($realcontext, $fetchedcontext); + // Use context level numbers instead of legacy short level names. + $fetchedcontext = $this->get_context_from_params( + ["contextlevel" => \core\context\course::LEVEL, "instanceid" => $course->id]); + $this->assertEquals($realcontext, $fetchedcontext); + // Passing empty values. try { $fetchedcontext = $this->get_context_from_params(["contextid" => 0]); @@ -211,9 +216,23 @@ class external_api_test extends \advanced_testcase { $fetchedcontext = $this->get_context_from_params(["contextlevel" => "system", "instanceid" => 0]); $this->assertEquals($realcontext, $fetchedcontext); - // Passing wrong level. - $this->expectException('invalid_parameter_exception'); - $fetchedcontext = $this->get_context_from_params(["contextlevel" => "random", "instanceid" => $course->id]); + // Passing wrong level name. + try { + $fetchedcontext = $this->get_context_from_params(["contextlevel" => "random", "instanceid" => $course->id]); + $this->fail('exception expected when level name is invalid'); + } catch (\moodle_exception $e) { + $this->assertInstanceOf('invalid_parameter_exception', $e); + $this->assertSame('Invalid parameter value detected (Invalid context level = random)', $e->getMessage()); + } + + // Passing wrong level number. + try { + $fetchedcontext = $this->get_context_from_params(["contextlevel" => -10, "instanceid" => $course->id]); + $this->fail('exception expected when level name is invalid'); + } catch (\moodle_exception $e) { + $this->assertInstanceOf('invalid_parameter_exception', $e); + $this->assertSame('Invalid parameter value detected (Invalid context level = -10)', $e->getMessage()); + } } /** diff --git a/lib/tests/accesslib_test.php b/lib/tests/accesslib_test.php index 1b87735661c..220a1a7bc6a 100644 --- a/lib/tests/accesslib_test.php +++ b/lib/tests/accesslib_test.php @@ -5190,7 +5190,7 @@ class accesslib_test extends advanced_testcase { /** * Context caching fixture */ -class context_inspection extends context_helper { +abstract class context_inspection extends \core\context_helper { public static function test_context_cache_size() { return self::$cache_count; } diff --git a/lib/tests/context/block_test.php b/lib/tests/context/block_test.php new file mode 100644 index 00000000000..1cc42bbe534 --- /dev/null +++ b/lib/tests/context/block_test.php @@ -0,0 +1,269 @@ +. + +namespace core\context; + +use core\context, core\context_helper; + +/** + * Unit tests for block context class. + * + * NOTE: more tests are in lib/tests/accesslib_test.php + * + * @package core + * @copyright Petr Skoda + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\context\block + */ +class block_test extends \advanced_testcase { + /** + * Tests legacy class name. + * @covers \context_block + */ + public function test_legacy_classname() { + $this->resetAfterTest(); + + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $this->assertInstanceOf(block::class, $context); + $this->assertInstanceOf(\context_block::class, $context); + } + + /** + * Tests covered methods. + * @covers ::instance + * @covers \core\context::instance_by_id + */ + public function test_factory_methods() { + $this->resetAfterTest(); + + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $this->assertInstanceOf(block::class, $context); + $this->assertSame((string)$block->id, $context->instanceid); + + $context = context::instance_by_id($context->id); + $this->assertInstanceOf(block::class, $context); + $this->assertSame((string)$block->id, $context->instanceid); + } + + /** + * Tests covered method. + * @covers ::get_short_name + */ + public function test_get_short_name() { + $this->assertSame('block', block::get_short_name()); + } + + /** + * Tests levels. + * @coversNothing + */ + public function test_level() { + $this->assertSame(80, block::LEVEL); + $this->assertSame(CONTEXT_BLOCK, block::LEVEL); + } + + /** + * Tests covered method. + * @covers ::get_level_name + */ + public function test_get_level_name() { + $this->assertSame('Block', block::get_level_name()); + } + + /** + * Tests covered method. + * @covers ::get_context_name + */ + public function test_get_context_name() { + $this->resetAfterTest(); + + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $this->assertSame('Block: Online users', $context->get_context_name()); + $this->assertSame('Block: Online users', $context->get_context_name(true)); + $this->assertSame('Online users', $context->get_context_name(false)); + $this->assertSame('Online users', $context->get_context_name(false, true)); + $this->assertSame('Block: Online users', $context->get_context_name(true, true, false)); + } + + /** + * Tests covered method. + * @covers ::get_url + */ + public function test_get_url() { + $this->resetAfterTest(); + + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $expected = new \moodle_url('/'); + $url = $context->get_url(); + $this->assertInstanceOf(\moodle_url::class, $url); + $this->assertSame($expected->out(), $url->out()); + } + + /** + * Tests covered method. + * @covers ::get_compatible_role_archetypes + */ + public function test_get_compatible_role_archetypes() { + global $DB; + + $allarchetypes = $DB->get_fieldset_select('role', 'DISTINCT archetype', 'archetype IS NOT NULL'); + foreach ($allarchetypes as $allarchetype) { + $levels = context_helper::get_compatible_levels($allarchetype); + $this->assertNotContains(block::LEVEL, $levels, "$allarchetype is not expected to be compatible with context"); + } + } + + /** + * Tests covered method. + * @covers ::get_possible_parent_levels + */ + public function test_get_possible_parent_levels() { + $result = block::get_possible_parent_levels(); + // All except itself. + $this->assertContains(system::LEVEL, $result); + $this->assertContains(user::LEVEL, $result); + $this->assertContains(coursecat::LEVEL, $result); + $this->assertContains(course::LEVEL, $result); + $this->assertContains(module::LEVEL, $result); + $this->assertNotContains(block::LEVEL, $result); + + // Make sure plugin contexts are covered too. + $all = \core\context_helper::get_all_levels(); + $this->assertCount(count($all) - 1, $result); + } + + /** + * Tests covered method. + * @covers ::get_capabilities + */ + public function test_get_capabilities() { + $this->resetAfterTest(); + + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $capabilities = $context->get_capabilities(); + $capabilities = convert_to_array($capabilities); + $capabilities = array_column($capabilities, 'name'); + $this->assertNotContains('moodle/site:config', $capabilities); + $this->assertNotContains('moodle/course:view', $capabilities); + $this->assertNotContains('moodle/category:manage', $capabilities); + $this->assertNotContains('moodle/user:viewalldetails', $capabilities); + $this->assertNotContains('mod/page:view', $capabilities); + $this->assertNotContains('mod/url:view', $capabilities); + } + + /** + * Tests covered method. + * @covers ::create_level_instances + */ + public function test_create_level_instances() { + global $DB; + $this->resetAfterTest(); + + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $DB->delete_records('context', ['id' => $context->id]); + context_helper::create_instances(block::LEVEL); + $record = $DB->get_record('context', ['contextlevel' => block::LEVEL, 'instanceid' => $block->id], '*', MUST_EXIST); + } + + /** + * Tests covered method. + * @covers ::get_child_contexts + */ + public function test_get_child_contexts() { + $this->resetAfterTest(); + + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $children = $context->get_child_contexts(); + $this->assertCount(0, $children); + } + + /** + * Tests covered method. + * @covers ::get_cleanup_sql + */ + public function test_get_cleanup_sql() { + global $DB; + $this->resetAfterTest(); + + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $DB->delete_records('block_instances', ['id' => $block->id]); + + context_helper::cleanup_instances(); + $this->assertFalse($DB->record_exists('context', ['contextlevel' => block::LEVEL, 'instanceid' => $block->id])); + } + + /** + * Tests covered method. + * @covers ::build_paths + */ + public function test_build_paths() { + global $DB; + $this->resetAfterTest(); + + $syscontext = system::instance(); + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $DB->set_field('context', 'depth', 1, ['id' => $context->id]); + $DB->set_field('context', 'path', '/0', ['id' => $context->id]); + + context_helper::build_all_paths(true); + + $record = $DB->get_record('context', ['id' => $context->id]); + $this->assertSame('2', $record->depth); + $this->assertSame('/' . $syscontext->id . '/' . $record->id, $record->path); + } + + /** + * Tests covered method. + * @covers ::set_locked + */ + public function test_set_locked() { + global $DB; + $this->resetAfterTest(); + + $block = $this->getDataGenerator()->create_block('online_users'); + $context = block::instance($block->id); + + $context->set_locked(true); + $context = block::instance($block->id); + $this->assertTrue($context->locked); + $record = $DB->get_record('context', ['id' => $context->id]); + $this->assertSame('1', $record->locked); + + $context->set_locked(false); + $context = block::instance($block->id); + $this->assertFalse($context->locked); + $record = $DB->get_record('context', ['id' => $context->id]); + $this->assertSame('0', $record->locked); + } +} diff --git a/lib/tests/context/course_test.php b/lib/tests/context/course_test.php new file mode 100644 index 00000000000..982098d7f92 --- /dev/null +++ b/lib/tests/context/course_test.php @@ -0,0 +1,291 @@ +. + +namespace core\context; + +use core\context, core\context_helper; + +/** + * Unit tests for course context class. + * + * NOTE: more tests are in lib/tests/accesslib_test.php + * + * @package core + * @copyright Petr Skoda + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\context\course + */ +class course_test extends \advanced_testcase { + /** + * Tests legacy class. + * @coversNothing + */ + public function test_legacy_classname() { + global $SITE; + $course = $SITE; + $context = \context_course::instance($course->id); + $this->assertInstanceOf(course::class, $context); + $this->assertInstanceOf(\context_course::class, $context); + } + + /** + * Tests covered methods. + * @covers ::instance + * @covers \core\context::instance_by_id + */ + public function test_factory_methods() { + global $SITE; + $course = $SITE; + $context = course::instance($course->id); + $this->assertInstanceOf(course::class, $context); + $this->assertSame($course->id, $context->instanceid); + + $context = context::instance_by_id($context->id); + $this->assertInstanceOf(course::class, $context); + $this->assertSame($course->id, $context->instanceid); + } + + /** + * Tests covered method. + * @covers ::get_short_name + */ + public function test_get_short_name() { + $this->assertSame('course', course::get_short_name()); + } + + /** + * Tests levels. + * @coversNothing + */ + public function test_level() { + $this->assertSame(50, course::LEVEL); + $this->assertSame(CONTEXT_COURSE, course::LEVEL); + } + + /** + * Tests covered method. + * @covers ::get_level_name + */ + public function test_get_level_name() { + $this->assertSame('Course', course::get_level_name()); + } + + /** + * Tests covered method. + * @covers ::get_context_name + */ + public function test_get_context_name() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(['fullname' => 'Test course', 'shortname' => 'TST']); + $context = course::instance($course->id); + + $this->assertSame('Course: Test course', $context->get_context_name()); + $this->assertSame('Course: Test course', $context->get_context_name(true)); + $this->assertSame('Test course', $context->get_context_name(false)); + $this->assertSame('TST', $context->get_context_name(false, true)); + $this->assertSame('Course: TST', $context->get_context_name(true, true, false)); + } + + /** + * Tests covered method. + * @covers ::get_url + */ + public function test_get_url() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $context = course::instance($course->id); + + $expected = new \moodle_url('/course/view.php', ['id' => $course->id]); + $url = $context->get_url(); + $this->assertInstanceOf(\moodle_url::class, $url); + $this->assertSame($expected->out(), $url->out()); + } + + /** + * Tests covered methods. + * @covers ::get_instance_table() + * @covers ::get_behat_reference_columns() + * @covers \core\context_helper::resolve_behat_reference + */ + public function test_resolve_behat_reference() { + $this->resetAfterTest(); + + $instance = $this->getDataGenerator()->create_course(['shortname' => 'xyz']); + $context = context\course::instance($instance->id); + + $result = context_helper::resolve_behat_reference('Course', $instance->shortname); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('course', $instance->shortname); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('50', $instance->shortname); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('Course', 'dshjkdshjkhjsadjhdsa'); + $this->assertNull($result); + + $result = context_helper::resolve_behat_reference('Course', ''); + $this->assertNull($result); + } + + /** + * Tests covered method. + * @covers ::get_compatible_role_archetypes + */ + public function test_get_compatible_role_archetypes() { + global $DB; + + $allarchetypes = $DB->get_fieldset_select('role', 'DISTINCT archetype', 'archetype IS NOT NULL'); + foreach ($allarchetypes as $allarchetype) { + $levels = context_helper::get_compatible_levels($allarchetype); + if ($allarchetype === 'editingteacher' || $allarchetype === 'teacher' + || $allarchetype === 'student' || $allarchetype === 'manager') { + $this->assertContains(course::LEVEL, $levels, "$allarchetype is expected to be compatible with context"); + } else { + $this->assertNotContains(course::LEVEL, $levels, "$allarchetype is not expected to be compatible with context"); + } + } + } + + /** + * Tests covered method. + * @covers ::get_possible_parent_levels + */ + public function test_get_possible_parent_levels() { + $this->assertSame([coursecat::LEVEL], course::get_possible_parent_levels()); + } + + /** + * Tests covered method. + * @covers ::get_capabilities + */ + public function test_get_capabilities() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $context = course::instance($course->id); + + $capabilities = $context->get_capabilities(); + $capabilities = convert_to_array($capabilities); + $capabilities = array_column($capabilities, 'name'); + $this->assertContains('moodle/course:view', $capabilities); + $this->assertContains('mod/page:view', $capabilities); + $this->assertContains('mod/url:view', $capabilities); + $this->assertNotContains('moodle/category:manage', $capabilities); + $this->assertNotContains('moodle/user:viewalldetails', $capabilities); + } + + /** + * Tests covered method. + * @covers ::create_level_instances + */ + public function test_create_level_instances() { + global $DB; + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = course::instance($course->id); + + $DB->delete_records('context', ['id' => $coursecontext->id]); + context_helper::create_instances(course::LEVEL); + $record = $DB->get_record('context', ['contextlevel' => course::LEVEL, 'instanceid' => $course->id], '*', MUST_EXIST); + } + + /** + * Tests covered method. + * @covers ::get_child_contexts + */ + public function test_get_child_contexts() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => 'Pokus']); + + $context = course::instance($course->id); + $children = $context->get_child_contexts(); + $this->assertCount(1, $children); + $childcontext = reset($children); + $this->assertInstanceOf(module::class, $childcontext); + $this->assertEquals($page->cmid, $childcontext->instanceid); + } + + /** + * Tests covered method. + * @covers ::get_cleanup_sql + */ + public function test_get_cleanup_sql() { + global $DB; + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = course::instance($course->id); + + $DB->delete_records('course', ['id' => $course->id]); + + context_helper::cleanup_instances(); + $this->assertFalse($DB->record_exists('context', ['contextlevel' => course::LEVEL, 'instanceid' => $course->id])); + } + + /** + * Tests covered method. + * @covers ::build_paths + */ + public function test_build_paths() { + global $DB; + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $coursecontext = course::instance($course->id); + $syscontext = system::instance(); + + $DB->set_field('context', 'depth', 1, ['id' => $coursecontext->id]); + $DB->set_field('context', 'path', '/0', ['id' => $coursecontext->id]); + + context_helper::build_all_paths(true); + + $record = $DB->get_record('context', ['id' => $coursecontext->id]); + $categorycontext = coursecat::instance($course->category); + $this->assertSame('3', $record->depth); + $this->assertSame('/' . $syscontext->id . '/' . $categorycontext->id . '/' . $record->id, $record->path); + } + + /** + * Tests covered method. + * @covers ::set_locked + */ + public function test_set_locked() { + global $DB; + $this->resetAfterTest(); + + $course1 = $this->getDataGenerator()->create_course(); + $context1 = course::instance($course1->id); + + $context1->set_locked(true); + $context1 = course::instance($course1->id); + $this->assertTrue($context1->locked); + $record = $DB->get_record('context', ['id' => $context1->id]); + $this->assertSame('1', $record->locked); + + $context1->set_locked(false); + $context1 = course::instance($course1->id); + $this->assertFalse($context1->locked); + $record = $DB->get_record('context', ['id' => $context1->id]); + $this->assertSame('0', $record->locked); + } +} diff --git a/lib/tests/context/coursecat_test.php b/lib/tests/context/coursecat_test.php new file mode 100644 index 00000000000..7fb5425c97a --- /dev/null +++ b/lib/tests/context/coursecat_test.php @@ -0,0 +1,289 @@ +. + +namespace core\context; + +use core\context, core\context_helper; + +/** + * Unit tests for coursecat context class. + * + * NOTE: more tests are in lib/tests/accesslib_test.php + * + * @package core + * @copyright Petr Skoda + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\context\coursecat + */ +class coursecat_test extends \advanced_testcase { + /** + * Tests legacy class name. + * @coversNothing + */ + public function test_legacy_classname() { + $category = \core_course_category::get_default(); + $context = \context_coursecat::instance($category->id); + $this->assertInstanceOf(coursecat::class, $context); + $this->assertInstanceOf(\context_coursecat::class, $context); + } + + /** + * Tests covered methods. + * @covers ::instance + * @covers \core\context::instance_by_id + */ + public function test_factory_methods() { + $category = \core_course_category::get_default(); + $context = coursecat::instance($category->id); + $this->assertInstanceOf(coursecat::class, $context); + $this->assertSame($category->id, $context->instanceid); + + $context = context::instance_by_id($context->id); + $this->assertInstanceOf(coursecat::class, $context); + $this->assertSame($category->id, $context->instanceid); + } + + /** + * Tests covered method. + * @covers ::get_short_name + */ + public function test_get_short_name() { + $this->assertSame('coursecat', coursecat::get_short_name()); + } + + /** + * Tests levels. + * @coversNothing + */ + public function test_level() { + $this->assertSame(40, coursecat::LEVEL); + $this->assertSame(CONTEXT_COURSECAT, coursecat::LEVEL); + } + + /** + * Tests covered method. + * @covers ::get_level_name + */ + public function test_get_level_name() { + $this->assertSame('Category', coursecat::get_level_name()); + } + + /** + * Tests covered method. + * @covers ::get_context_name + */ + public function test_get_context_name() { + $category = \core_course_category::get_default(); + $context = coursecat::instance($category->id); + $this->assertSame('Category: Category 1', $context->get_context_name()); + $this->assertSame('Category: Category 1', $context->get_context_name(true)); + $this->assertSame('Category 1', $context->get_context_name(false)); + $this->assertSame('Category 1', $context->get_context_name(false, true)); + $this->assertSame('Category: Category 1', $context->get_context_name(true, true, false)); + } + + /** + * Tests covered method. + * @covers ::get_url + */ + public function test_get_url() { + $category = \core_course_category::get_default(); + $context = coursecat::instance($category->id); + $expected = new \moodle_url('/course/index.php', ['categoryid' => $category->id]); + $url = $context->get_url(); + $this->assertInstanceOf(\moodle_url::class, $url); + $this->assertSame($expected->out(), $url->out()); + } + + /** + * Tests covered methods. + * @covers ::get_instance_table() + * @covers ::get_behat_reference_columns() + * @covers \core\context_helper::resolve_behat_reference + */ + public function test_resolve_behat_reference() { + $this->resetAfterTest(); + + $instance = $this->getDataGenerator()->create_category(['idnumber' => 'xyz']); + $context = context\coursecat::instance($instance->id); + + $result = context_helper::resolve_behat_reference('Category', $instance->idnumber); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('coursecat', $instance->idnumber); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('40', $instance->idnumber); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('Category', 'dshjkdshjkhjsadjhdsa'); + $this->assertNull($result); + + $result = context_helper::resolve_behat_reference('Category', ''); + $this->assertNull($result); + } + + /** + * Tests covered method. + * @covers ::get_compatible_role_archetypes + */ + public function test_get_compatible_role_archetypes() { + global $DB; + + $allarchetypes = $DB->get_fieldset_select('role', 'DISTINCT archetype', 'archetype IS NOT NULL'); + foreach ($allarchetypes as $allarchetype) { + $levels = context_helper::get_compatible_levels($allarchetype); + if ($allarchetype === 'manager' || $allarchetype === 'coursecreator') { + $this->assertContains(coursecat::LEVEL, $levels, "$allarchetype is expected to be compatible with context"); + } else { + $this->assertNotContains(coursecat::LEVEL, $levels, "$allarchetype is not expected to be compatible with context"); + } + } + } + + /** + * Tests covered method. + * @covers ::get_possible_parent_levels + */ + public function test_get_possible_parent_levels() { + $this->assertSame([system::LEVEL, coursecat::LEVEL], coursecat::get_possible_parent_levels()); + } + + /** + * Tests covered method. + * @covers ::get_capabilities + */ + public function test_get_capabilities() { + $category = \core_course_category::get_default(); + + $context = coursecat::instance($category->id); + $capabilities = $context->get_capabilities(); + $capabilities = convert_to_array($capabilities); + $capabilities = array_column($capabilities, 'name'); + $this->assertContains('moodle/category:manage', $capabilities); + $this->assertContains('moodle/course:view', $capabilities); + $this->assertNotContains('moodle/user:viewalldetails', $capabilities); + } + + /** + * Tests covered method. + * @covers ::create_level_instances + */ + public function test_create_level_instances() { + global $DB; + $this->resetAfterTest(); + + $coursecat = $this->getDataGenerator()->create_category(); + $coursecatcontext = coursecat::instance($coursecat->id); + + $DB->delete_records('context', ['id' => $coursecatcontext->id]); + context_helper::create_instances(coursecat::LEVEL); + $record = $DB->get_record('context', ['contextlevel' => coursecat::LEVEL, 'instanceid' => $coursecat->id], '*', MUST_EXIST); + } + + /** + * Tests covered method. + * @covers ::get_child_contexts + */ + public function test_get_child_contexts() { + $this->resetAfterTest(); + + $category = \core_course_category::get_default(); + + $context = coursecat::instance($category->id); + $children = $context->get_child_contexts(); + $this->assertCount(0, $children); + + $course = $this->getDataGenerator()->create_course(['categoryid' => $category->id]); + + // This may fail if some plugin auto-creates activities. + $children = $context->get_child_contexts(); + $this->assertCount(1, $children); + } + + /** + * Tests covered method. + * @covers ::get_cleanup_sql + */ + public function test_get_cleanup_sql() { + global $DB; + $this->resetAfterTest(); + + $coursecat = $this->getDataGenerator()->create_category(); + $coursecatcontext = coursecat::instance($coursecat->id); + + $DB->delete_records('course_categories', ['id' => $coursecat->id]); + + context_helper::cleanup_instances(); + $this->assertFalse($DB->record_exists('context', ['contextlevel' => coursecat::LEVEL, 'instanceid' => $coursecat->id])); + } + + /** + * Tests covered method. + * @covers ::build_paths + */ + public function test_build_paths() { + global $DB; + $this->resetAfterTest(); + + $coursecat = $this->getDataGenerator()->create_category(); + $coursecatcontext = coursecat::instance($coursecat->id); + $syscontext = system::instance(); + + $DB->set_field('context', 'depth', 1, ['id' => $coursecatcontext->id]); + $DB->set_field('context', 'path', '/0', ['id' => $coursecatcontext->id]); + + context_helper::build_all_paths(true); + + $record = $DB->get_record('context', ['id' => $coursecatcontext->id]); + $this->assertSame('2', $record->depth); + $this->assertSame('/' . $syscontext->id . '/' . $record->id, $record->path); + } + + /** + * Tests covered method. + * @covers ::set_locked + */ + public function test_set_locked() { + global $DB; + $this->resetAfterTest(); + + $category1 = $this->getDataGenerator()->create_category(); + $category2 = $this->getDataGenerator()->create_category(['parent' => $category1->id]); + $context1 = coursecat::instance($category1->id); + $context2 = coursecat::instance($category2->id); + + $context1->set_locked(true); + $context1 = coursecat::instance($category1->id); + $context2 = coursecat::instance($category2->id); + $this->assertTrue($context1->locked); + $this->assertTrue($context2->locked); + $record = $DB->get_record('context', ['id' => $context1->id]); + $this->assertSame('1', $record->locked); + $record = $DB->get_record('context', ['id' => $context2->id]); + $this->assertSame('0', $record->locked); + + $context1->set_locked(false); + $context1 = coursecat::instance($category1->id); + $context2 = coursecat::instance($category2->id); + $this->assertFalse($context1->locked); + $this->assertFalse($context2->locked); + $record = $DB->get_record('context', ['id' => $context1->id]); + $this->assertSame('0', $record->locked); + $record = $DB->get_record('context', ['id' => $context2->id]); + $this->assertSame('0', $record->locked); + } +} diff --git a/lib/tests/context/module_test.php b/lib/tests/context/module_test.php new file mode 100644 index 00000000000..a8fba3f200b --- /dev/null +++ b/lib/tests/context/module_test.php @@ -0,0 +1,306 @@ +. + +namespace core\context; + +use core\context, core\context_helper; + +/** + * Unit tests for module context class. + * + * NOTE: more tests are in lib/tests/accesslib_test.php + * + * @package core + * @copyright Petr Skoda + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\context\module + */ +class module_test extends \advanced_testcase { + /** + * Tests legacy class. + * @coversNothing + */ + public function test_legacy_classname() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id]); + $context = module::instance($page->cmid); + + $this->assertInstanceOf(module::class, $context); + $this->assertInstanceOf(\context_module::class, $context); + } + + /** + * Tests covered methods. + * @covers ::instance + * @covers \core\context::instance_by_id + */ + public function test_factory_methods() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id]); + $context = module::instance($page->cmid); + + $this->assertInstanceOf(module::class, $context); + $this->assertSame((string)$page->cmid, $context->instanceid); + + $context = context::instance_by_id($context->id); + $this->assertInstanceOf(module::class, $context); + $this->assertSame((string)$page->cmid, $context->instanceid); + } + + /** + * Tests covered method. + * @covers ::get_short_name + */ + public function test_get_short_name() { + $this->assertSame('module', module::get_short_name()); + } + + /** + * Tests context level. + * @coversNothing + */ + public function test_level() { + $this->assertSame(70, module::LEVEL); + $this->assertSame(CONTEXT_MODULE, module::LEVEL); + } + + /** + * Tests covered method. + * @covers ::get_level_name + */ + public function test_get_level_name() { + $this->assertSame('Activity module', module::get_level_name()); + } + + /** + * Tests covered method. + * @covers ::get_context_name + */ + public function test_get_context_name() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => 'Pokus']); + $context = module::instance($page->cmid); + + $this->assertSame('Page: Pokus', $context->get_context_name()); + $this->assertSame('Page: Pokus', $context->get_context_name(true)); + $this->assertSame('Pokus', $context->get_context_name(false)); + $this->assertSame('Pokus', $context->get_context_name(false, true)); + $this->assertSame('Page: Pokus', $context->get_context_name(true, true, false)); + } + + /** + * Tests covered method. + * @covers ::get_url + */ + public function test_get_url() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => 'Pokus']); + $context = module::instance($page->cmid); + + $expected = new \moodle_url('/mod/page/view.php', ['id' => $page->cmid]); + $url = $context->get_url(); + $this->assertInstanceOf(\moodle_url::class, $url); + $this->assertSame($expected->out(), $url->out()); + } + + /** + * Tests covered methods. + * @covers ::get_instance_table() + * @covers ::get_behat_reference_columns() + * @covers \core\context_helper::resolve_behat_reference + */ + public function test_resolve_behat_reference() { + global $DB; + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'idnumber' => 'xyz']); + $instance = $DB->get_record('course_modules', ['id' => $page->cmid], '*', MUST_EXIST); + $context = module::instance($instance->id); + + $result = context_helper::resolve_behat_reference('Activity module', $instance->idnumber); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('module', $instance->idnumber); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('70', $instance->idnumber); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('Activity module', 'dshjkdshjkhjsadjhdsa'); + $this->assertNull($result); + + $result = context_helper::resolve_behat_reference('Activity module', ''); + $this->assertNull($result); + } + + /** + * Tests covered method. + * @covers ::get_compatible_role_archetypes + */ + public function test_get_compatible_role_archetypes() { + global $DB; + + $allarchetypes = $DB->get_fieldset_select('role', 'DISTINCT archetype', 'archetype IS NOT NULL'); + foreach ($allarchetypes as $allarchetype) { + $levels = context_helper::get_compatible_levels($allarchetype); + if ($allarchetype === 'editingteacher' || $allarchetype === 'teacher' || $allarchetype === 'student') { + $this->assertContains(module::LEVEL, $levels, "$allarchetype is expected to be compatible with context"); + } else { + $this->assertNotContains(module::LEVEL, $levels, "$allarchetype is not expected to be compatible with context"); + } + } + } + + /** + * Tests covered method. + * @covers ::get_possible_parent_levels + */ + public function test_get_possible_parent_levels() { + $this->assertSame([course::LEVEL], module::get_possible_parent_levels()); + } + + /** + * Tests covered method. + * @covers ::get_capabilities + */ + public function test_get_capabilities() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => 'Pokus']); + $context = module::instance($page->cmid); + + $capabilities = $context->get_capabilities(); + $capabilities = convert_to_array($capabilities); + $capabilities = array_column($capabilities, 'name'); + $this->assertContains('mod/page:view', $capabilities); + $this->assertNotContains('mod/url:view', $capabilities); + $this->assertNotContains('moodle/course:view', $capabilities); + $this->assertNotContains('moodle/category:manage', $capabilities); + $this->assertNotContains('moodle/user:viewalldetails', $capabilities); + } + + /** + * Tests covered method. + * @covers ::create_level_instances + */ + public function test_create_level_instances() { + global $DB; + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => 'Pokus']); + $context = module::instance($page->cmid); + + $DB->delete_records('context', ['id' => $context->id]); + context_helper::create_instances(module::LEVEL); + $record = $DB->get_record('context', ['contextlevel' => module::LEVEL, 'instanceid' => $page->cmid], '*', MUST_EXIST); + } + + /** + * Tests covered method. + * @covers ::get_child_contexts + */ + public function test_get_child_contexts() { + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => 'Pokus']); + $context = module::instance($page->cmid); + + $children = $context->get_child_contexts(); + $this->assertCount(0, $children); + } + + /** + * Tests covered method. + * @covers ::get_cleanup_sql + */ + public function test_get_cleanup_sql() { + global $DB; + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => 'Pokus']); + $context = module::instance($page->cmid); + + $DB->delete_records('course_modules', ['id' => $page->cmid]); + $DB->delete_records('page', ['id' => $page->id]); + + context_helper::cleanup_instances(); + $this->assertFalse($DB->record_exists('context', ['contextlevel' => module::LEVEL, 'instanceid' => $page->cmid])); + } + + /** + * Tests covered method. + * @covers ::build_paths + */ + public function test_build_paths() { + global $DB; + $this->resetAfterTest(); + + $syscontext = system::instance(); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = course::instance($course->id); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => 'Pokus']); + $pagecontext = module::instance($page->cmid); + + $DB->set_field('context', 'depth', 1, ['id' => $pagecontext->id]); + $DB->set_field('context', 'path', '/0', ['id' => $pagecontext->id]); + + context_helper::build_all_paths(true); + + $record = $DB->get_record('context', ['id' => $pagecontext->id]); + $categorycontext = coursecat::instance($course->category); + $this->assertSame('4', $record->depth); + $this->assertSame('/' . $syscontext->id . '/' . $categorycontext->id . '/' + . $coursecontext->id . '/' . $record->id, $record->path); + } + + /** + * Tests covered method. + * @covers ::set_locked + */ + public function test_set_locked() { + global $DB; + $this->resetAfterTest(); + + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id, 'name' => 'Pokus']); + $context = module::instance($page->cmid); + + $context->set_locked(true); + $context = module::instance($page->cmid); + $this->assertTrue($context->locked); + $record = $DB->get_record('context', ['id' => $context->id]); + $this->assertSame('1', $record->locked); + + $context->set_locked(false); + $context = module::instance($page->cmid); + $this->assertFalse($context->locked); + $record = $DB->get_record('context', ['id' => $context->id]); + $this->assertSame('0', $record->locked); + } +} diff --git a/lib/tests/context/system_test.php b/lib/tests/context/system_test.php new file mode 100644 index 00000000000..ccf873186aa --- /dev/null +++ b/lib/tests/context/system_test.php @@ -0,0 +1,233 @@ +. + +namespace core\context; + +use core\context, core\context_helper; + +/** + * Unit tests for system context class. + * + * NOTE: more tests are in lib/tests/accesslib_test.php + * + * @package core + * @copyright Petr Skoda + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\context\system + */ +class system_test extends \advanced_testcase { + /** + * Tests legacy class. + * @coversNothing + */ + public function test_legacy_classname() { + $context = \context_system::instance(); + $this->assertInstanceOf(system::class, $context); + $this->assertInstanceOf(\context_system::class, $context); + } + + /** + * Tests covered methods. + * @covers ::instance + * @covers \core\context::instance_by_id + */ + public function test_factory_methods() { + $context = system::instance(); + $this->assertInstanceOf(system::class, $context); + $this->assertEquals(SYSCONTEXTID, $context->id); + + $context = context::instance_by_id($context->id); + $this->assertInstanceOf(system::class, $context); + $this->assertEquals(SYSCONTEXTID, $context->id); + } + + /** + * Tests covered method. + * @covers ::get_short_name + */ + public function test_get_short_name() { + $this->assertSame('system', system::get_short_name()); + } + + /** + * Tests context level. + * @coversNothing + */ + public function test_level() { + $this->assertSame(10, system::LEVEL); + $this->assertSame(CONTEXT_SYSTEM, system::LEVEL); + } + + /** + * Tests covered method. + * @covers ::get_level_name + */ + public function test_get_level_name() { + $this->assertSame('System', system::get_level_name()); + } + + /** + * Tests covered method. + * @covers ::get_context_name + */ + public function test_get_context_name() { + $context = system::instance(); + $this->assertSame('System', $context->get_context_name()); + $this->assertSame('System', $context->get_context_name(true)); + $this->assertSame('System', $context->get_context_name(false)); + $this->assertSame('System', $context->get_context_name(false, true)); + $this->assertSame('System', $context->get_context_name(true, true, false)); + } + + /** + * Tests covered method. + * @covers ::get_url + */ + public function test_get_url() { + $context = system::instance(); + $expected = new \moodle_url('/'); + $url = $context->get_url(); + $this->assertInstanceOf(\moodle_url::class, $url); + $this->assertSame($expected->out(), $url->out()); + } + + /** + * Tests covered method. + * @covers \core\context_helper::resolve_behat_reference + */ + public function test_resolve_behat_reference() { + $syscontext = context\system::instance(); + + $result = context_helper::resolve_behat_reference('System', ''); + $this->assertSame($syscontext->id, $result->id); + + $result = context_helper::resolve_behat_reference('System', '44'); + $this->assertSame($syscontext->id, $result->id); + + $result = context_helper::resolve_behat_reference('system', ''); + $this->assertSame($syscontext->id, $result->id); + + $result = context_helper::resolve_behat_reference('10', ''); + $this->assertSame($syscontext->id, $result->id); + } + + /** + * Tests covered method. + * @covers ::get_compatible_role_archetypes + */ + public function test_get_compatible_role_archetypes() { + global $DB; + + $allarchetypes = $DB->get_fieldset_select('role', 'DISTINCT archetype', 'archetype IS NOT NULL'); + foreach ($allarchetypes as $allarchetype) { + $levels = context_helper::get_compatible_levels($allarchetype); + if ($allarchetype === 'manager' || $allarchetype === 'coursecreator') { + $this->assertContains(system::LEVEL, $levels, "$allarchetype is expected to be compatible with context"); + } else { + $this->assertNotContains(system::LEVEL, $levels, "$allarchetype is not expected to be compatible with context"); + } + } + } + + /** + * Tests covered method. + * @covers ::get_possible_parent_levels + */ + public function test_get_possible_parent_levels() { + $this->assertSame([], system::get_possible_parent_levels()); + } + + /** + * Tests covered method. + * @covers ::get_capabilities + */ + public function test_get_capabilities() { + global $DB; + + $context = system::instance(); + $capabilities = $context->get_capabilities(); + $expected = $DB->count_records('capabilities', []); + $this->assertCount($expected, $capabilities); + } + + /** + * Tests covered method. + * @covers ::create_level_instances + */ + public function test_create_level_instances() { + context_helper::create_instances(system::LEVEL); + } + + /** + * Tests covered method. + * @covers ::get_child_contexts + */ + public function test_get_child_contexts() { + global $DB; + + $context = system::instance(); + $children = $context->get_child_contexts(); + $expected = $DB->count_records('context', []) - 1; + $this->assertCount($expected, $children); + $this->assertDebuggingCalled('Fetching of system context child courses is strongly ' + . 'discouraged on production servers (it may eat all available memory)!'); + } + + /** + * Tests covered method. + * @covers ::get_cleanup_sql + */ + public function test_get_cleanup_sql() { + // Nothing to clean up actually. + context_helper::cleanup_instances(); + } + + /** + * Tests covered method. + * @covers ::build_paths + */ + public function test_build_paths() { + global $DB; + $this->resetAfterTest(); + + $DB->set_field('context', 'depth', 2, ['id' => SYSCONTEXTID]); + $DB->set_field('context', 'path', '/0', ['id' => SYSCONTEXTID]); + + context_helper::build_all_paths(true); + + $record = $DB->get_record('context', ['id' => SYSCONTEXTID]); + $this->assertSame('1', $record->depth); + $this->assertSame('/' . $record->id, $record->path); + } + + /** + * Tests covered method. + * @covers ::set_locked + */ + public function test_set_locked() { + $context = system::instance(); + + $context->set_locked(false); + + try { + $context->set_locked(true); + } catch (\moodle_exception $e) { + $this->assertInstanceOf(\coding_exception::class, $e); + $this->assertSame('Coding error detected, it must be fixed by a programmer: ' + . 'It is not possible to lock the system context', $e->getMessage()); + } + } +} diff --git a/lib/tests/context/user_test.php b/lib/tests/context/user_test.php new file mode 100644 index 00000000000..d961337c182 --- /dev/null +++ b/lib/tests/context/user_test.php @@ -0,0 +1,268 @@ +. + +namespace core\context; + +use core\context, core\context_helper; + +/** + * Unit tests for user context class. + * + * NOTE: more tests are in lib/tests/accesslib_test.php + * + * @package core + * @copyright Petr Skoda + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\context\user + */ +class user_test extends \advanced_testcase { + /** + * Tests legacy class. + * @coversNothing + */ + public function test_legacy_classname() { + $admin = get_admin(); + $context = \context_user::instance($admin->id); + $this->assertInstanceOf(user::class, $context); + $this->assertInstanceOf(\context_user::class, $context); + } + + /** + * Tests covered methods. + * @covers ::instance + * @covers \core\context::instance_by_id + */ + public function test_factory_methods() { + $admin = get_admin(); + $context = user::instance($admin->id); + $this->assertInstanceOf(user::class, $context); + $this->assertSame($admin->id, $context->instanceid); + + $context = context::instance_by_id($context->id); + $this->assertInstanceOf(user::class, $context); + $this->assertSame($admin->id, $context->instanceid); + } + + /** + * Tests covered method. + * @covers ::get_short_name + */ + public function test_get_short_name() { + $this->assertSame('user', user::get_short_name()); + } + + /** + * Tests context level. + * @coversNothing + */ + public function test_level() { + $this->assertSame(30, user::LEVEL); + $this->assertSame(CONTEXT_USER, user::LEVEL); + } + + /** + * Tests covered method. + * @covers ::get_level_name + */ + public function test_get_level_name() { + $this->assertSame('User', user::get_level_name()); + } + + /** + * Tests covered method. + * @covers ::get_context_name + */ + public function test_get_context_name() { + $admin = get_admin(); + $context = user::instance($admin->id); + $this->assertSame('User: Admin User', $context->get_context_name()); + $this->assertSame('User: Admin User', $context->get_context_name(true)); + $this->assertSame('Admin User', $context->get_context_name(false)); + $this->assertSame('Admin User', $context->get_context_name(false, true)); + $this->assertSame('User: Admin User', $context->get_context_name(true, true, false)); + } + + /** + * Tests covered method. + * @covers ::get_url + */ + public function test_get_url() { + $admin = get_admin(); + $context = user::instance($admin->id); + $expected = new \moodle_url('/user/profile.php', ['id' => $admin->id]); + $url = $context->get_url(); + $this->assertInstanceOf(\moodle_url::class, $url); + $this->assertSame($expected->out(), $url->out()); + } + + /** + * Tests covered methods. + * @covers ::get_instance_table() + * @covers ::get_behat_reference_columns() + * @covers \core\context_helper::resolve_behat_reference + */ + public function test_resolve_behat_reference() { + $this->resetAfterTest(); + + $instance = $this->getDataGenerator()->create_user(); + $context = context\user::instance($instance->id); + + $result = context_helper::resolve_behat_reference('User', $instance->username); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('user', $instance->username); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('30', $instance->username); + $this->assertSame($context->id, $result->id); + + $result = context_helper::resolve_behat_reference('User', 'dshjkdshjkhjsadjhdsa'); + $this->assertNull($result); + + $result = context_helper::resolve_behat_reference('User', ''); + $this->assertNull($result); + } + + /** + * Tests covered method. + * @covers ::get_compatible_role_archetypes + */ + public function test_get_compatible_role_archetypes() { + global $DB; + + $allarchetypes = $DB->get_fieldset_select('role', 'DISTINCT archetype', 'archetype IS NOT NULL'); + foreach ($allarchetypes as $allarchetype) { + $levels = context_helper::get_compatible_levels($allarchetype); + $this->assertNotContains(user::LEVEL, $levels, "$allarchetype is not expected to be compatible with context"); + } + } + + /** + * Tests covered method. + * @covers ::get_possible_parent_levels + */ + public function test_get_possible_parent_levels() { + $this->assertSame([system::LEVEL], user::get_possible_parent_levels()); + } + + /** + * Tests covered method. + * @covers ::get_capabilities + */ + public function test_get_capabilities() { + $admin = get_admin(); + + $context = user::instance($admin->id); + $capabilities = $context->get_capabilities(); + $capabilities = convert_to_array($capabilities); + $capabilities = array_column($capabilities, 'name'); + + $this->assertContains('moodle/user:viewalldetails', $capabilities); + $this->assertContains('moodle/grade:viewall', $capabilities); + $this->assertNotContains('moodle/course:view', $capabilities); + $this->assertNotContains('moodle/site:config', $capabilities); + } + + /** + * Tests covered method. + * @covers ::create_level_instances + */ + public function test_create_level_instances() { + global $DB; + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $usercontext = user::instance($user->id); + + $DB->delete_records('context', ['id' => $usercontext->id]); + context_helper::create_instances(user::LEVEL); + $record = $DB->get_record('context', ['contextlevel' => user::LEVEL, 'instanceid' => $user->id], '*', MUST_EXIST); + } + + /** + * Tests covered method. + * @covers ::get_child_contexts + */ + public function test_get_child_contexts() { + $admin = get_admin(); + + $context = user::instance($admin->id); + $children = $context->get_child_contexts(); + $this->assertCount(0, $children); + } + + /** + * Tests covered method. + * @covers ::get_cleanup_sql + */ + public function test_get_cleanup_sql() { + global $DB; + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $usercontext = user::instance($user->id); + + $DB->set_field('user', 'deleted', 1, ['id' => $user->id]); + + context_helper::cleanup_instances(); + $this->assertFalse($DB->record_exists('context', ['contextlevel' => user::LEVEL, 'instanceid' => $user->id])); + } + + /** + * Tests covered method. + * @covers ::build_paths + */ + public function test_build_paths() { + global $DB; + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $usercontext = user::instance($user->id); + $syscontext = system::instance(); + + $DB->set_field('context', 'depth', 1, ['id' => $usercontext->id]); + $DB->set_field('context', 'path', '/0', ['id' => $usercontext->id]); + + context_helper::build_all_paths(true); + + $record = $DB->get_record('context', ['id' => $usercontext->id]); + $this->assertSame('2', $record->depth); + $this->assertSame('/' . $syscontext->id . '/' . $record->id, $record->path); + } + + /** + * Tests covered method. + * @covers ::set_locked + */ + public function test_set_locked() { + global $DB; + $this->resetAfterTest(); + + $admin = get_admin(); + $context = user::instance($admin->id); + $this->assertFalse($context->locked); + + $context->set_locked(true); + $this->assertTrue($context->locked); + $record = $DB->get_record('context', ['id' => $context->id]); + $this->assertSame('1', $record->locked); + + $context->set_locked(false); + $this->assertFalse($context->locked); + $record = $DB->get_record('context', ['id' => $context->id]); + $this->assertSame('0', $record->locked); + } +} diff --git a/lib/tests/context_helper_test.php b/lib/tests/context_helper_test.php new file mode 100644 index 00000000000..7033d52a2af --- /dev/null +++ b/lib/tests/context_helper_test.php @@ -0,0 +1,530 @@ +. + +namespace core; + +/** + * Unit tests for context helper class. + * + * NOTE: more tests are in lib/tests/accesslib_test.php + * + * @package core + * @copyright Petr Skoda + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\context_helper + */ +class context_helper_test extends \advanced_testcase { + /** + * Tests covered method. + * @covers ::parse_external_level + */ + public function test_parse_external_level() { + $this->assertSame(context\system::class, context_helper::parse_external_level('system')); + $this->assertSame(context\system::class, context_helper::parse_external_level(CONTEXT_SYSTEM)); + $this->assertSame(context\system::class, context_helper::parse_external_level((string)CONTEXT_SYSTEM)); + + $this->assertSame(context\user::class, context_helper::parse_external_level('user')); + $this->assertSame(context\user::class, context_helper::parse_external_level(CONTEXT_USER)); + $this->assertSame(context\user::class, context_helper::parse_external_level((string)CONTEXT_USER)); + + $this->assertSame(context\coursecat::class, context_helper::parse_external_level('coursecat')); + $this->assertSame(context\coursecat::class, context_helper::parse_external_level(CONTEXT_COURSECAT)); + $this->assertSame(context\coursecat::class, context_helper::parse_external_level((string)CONTEXT_COURSECAT)); + + $this->assertSame(context\course::class, context_helper::parse_external_level('course')); + $this->assertSame(context\course::class, context_helper::parse_external_level(CONTEXT_COURSE)); + $this->assertSame(context\course::class, context_helper::parse_external_level((string)CONTEXT_COURSE)); + + $this->assertSame(context\module::class, context_helper::parse_external_level('module')); + $this->assertSame(context\module::class, context_helper::parse_external_level(CONTEXT_MODULE)); + $this->assertSame(context\module::class, context_helper::parse_external_level((string)CONTEXT_MODULE)); + + $this->assertSame(context\block::class, context_helper::parse_external_level('block')); + $this->assertSame(context\block::class, context_helper::parse_external_level(CONTEXT_BLOCK)); + $this->assertSame(context\block::class, context_helper::parse_external_level((string)CONTEXT_BLOCK)); + + $this->assertNull(context_helper::parse_external_level('core_system')); + $this->assertNull(context_helper::parse_external_level('xsystem')); + $this->assertNull(context_helper::parse_external_level(1)); + $this->assertNull(context_helper::parse_external_level('')); + } + + /** + * Tests covered method. + * @covers ::resolve_behat_reference + */ + public function test_resolve_behat_reference() { + $this->assertNull(context_helper::resolve_behat_reference('blahbla', 'blahbla')); + $this->assertNull(context_helper::resolve_behat_reference('', '')); + $this->assertNull(context_helper::resolve_behat_reference('0', '')); + + $syscontext = context\system::instance(); + $result = context_helper::resolve_behat_reference('System', ''); + $this->assertSame($syscontext->id, $result->id); + + $syscontext = context\system::instance(); + $result = context_helper::resolve_behat_reference('10', ''); + $this->assertSame($syscontext->id, $result->id); + + // The rest is tested in each context class test. + } + + /** + * Tests covered method. + * @covers ::get_class_for_level + */ + public function test_get_class_for_level() { + $this->assertSame(context\system::class, context_helper::get_class_for_level(CONTEXT_SYSTEM)); + $this->assertSame(context\system::class, context_helper::get_class_for_level((string)CONTEXT_SYSTEM)); + + $this->assertSame(context\user::class, context_helper::get_class_for_level(CONTEXT_USER)); + $this->assertSame(context\user::class, context_helper::get_class_for_level((string)CONTEXT_USER)); + + $this->assertSame(context\coursecat::class, context_helper::get_class_for_level(CONTEXT_COURSECAT)); + $this->assertSame(context\coursecat::class, context_helper::get_class_for_level((string)CONTEXT_COURSECAT)); + + $this->assertSame(context\course::class, context_helper::get_class_for_level(CONTEXT_COURSE)); + $this->assertSame(context\course::class, context_helper::get_class_for_level((string)CONTEXT_COURSE)); + + $this->assertSame(context\module::class, context_helper::get_class_for_level(CONTEXT_MODULE)); + $this->assertSame(context\module::class, context_helper::get_class_for_level((string)CONTEXT_MODULE)); + + $this->assertSame(context\block::class, context_helper::get_class_for_level(CONTEXT_BLOCK)); + $this->assertSame(context\block::class, context_helper::get_class_for_level((string)CONTEXT_BLOCK)); + + try { + context_helper::get_class_for_level(1); + $this->fail('Exception expected if level does not exist'); + } catch (\moodle_exception $e) { + $this->assertInstanceOf(\coding_exception::class, $e); + $this->assertSame('Coding error detected, it must be fixed by a programmer: Invalid context level specified', + $e->getMessage()); + } + } + + /** + * Tests covered method. + * @covers ::get_all_levels + */ + public function test_get_all_levels() { + $levels = context_helper::get_all_levels(); + + $this->assertArrayHasKey(CONTEXT_SYSTEM, $levels); + $this->assertSame(context\system::class, $levels[CONTEXT_SYSTEM]); + + $this->assertArrayHasKey(CONTEXT_USER, $levels); + $this->assertSame(context\user::class, $levels[CONTEXT_USER]); + + $this->assertArrayHasKey(CONTEXT_COURSECAT, $levels); + $this->assertSame(context\coursecat::class, $levels[CONTEXT_COURSECAT]); + + $this->assertArrayHasKey(CONTEXT_COURSE, $levels); + $this->assertSame(context\course::class, $levels[CONTEXT_COURSE]); + + $this->assertArrayHasKey(CONTEXT_MODULE, $levels); + $this->assertSame(context\module::class, $levels[CONTEXT_MODULE]); + + $this->assertArrayHasKey(CONTEXT_BLOCK, $levels); + $this->assertSame(context\block::class, $levels[CONTEXT_BLOCK]); + + $sorted = $levels; + ksort($sorted, SORT_NUMERIC); + $block = $sorted[CONTEXT_BLOCK]; + unset($sorted[CONTEXT_BLOCK]); + $sorted[CONTEXT_BLOCK] = $block; + $this->assertSame(array_keys($sorted), array_keys($levels)); + + // Make sure level is set properly. + foreach ($levels as $level => $classname) { + $this->assertEquals($level, $classname::LEVEL); + if ($level != CONTEXT_SYSTEM) { + $this->assertGreaterThan(CONTEXT_SYSTEM, $level); + } + } + } + + /** + * Tests covered method. + * @covers ::get_child_levels + */ + public function test_get_child_levels() { + $alllevels = context_helper::get_all_levels(); + + $childlevels = context_helper::get_child_levels(CONTEXT_SYSTEM); + $this->assertSame(count($alllevels) - 1, count($childlevels)); + + $childlevels = context_helper::get_child_levels(CONTEXT_USER); + $this->assertNotContains(CONTEXT_SYSTEM, $childlevels); + $this->assertNotContains(CONTEXT_USER, $childlevels); + $this->assertNotContains(CONTEXT_COURSECAT, $childlevels); + $this->assertNotContains(CONTEXT_COURSE, $childlevels); + $this->assertNotContains(CONTEXT_MODULE, $childlevels); + $this->assertContains(CONTEXT_BLOCK, $childlevels); + + $childlevels = context_helper::get_child_levels(CONTEXT_COURSECAT); + $this->assertNotContains(CONTEXT_SYSTEM, $childlevels); + $this->assertNotContains(CONTEXT_USER, $childlevels); + $this->assertContains(CONTEXT_COURSECAT, $childlevels); + $this->assertContains(CONTEXT_COURSE, $childlevels); + $this->assertContains(CONTEXT_MODULE, $childlevels); + $this->assertContains(CONTEXT_BLOCK, $childlevels); + + $childlevels = context_helper::get_child_levels(CONTEXT_COURSE); + $this->assertNotContains(CONTEXT_SYSTEM, $childlevels); + $this->assertNotContains(CONTEXT_USER, $childlevels); + $this->assertNotContains(CONTEXT_COURSECAT, $childlevels); + $this->assertNotContains(CONTEXT_COURSE, $childlevels); + $this->assertContains(CONTEXT_MODULE, $childlevels); + $this->assertContains(CONTEXT_BLOCK, $childlevels); + + $childlevels = context_helper::get_child_levels(CONTEXT_MODULE); + $this->assertNotContains(CONTEXT_SYSTEM, $childlevels); + $this->assertNotContains(CONTEXT_USER, $childlevels); + $this->assertNotContains(CONTEXT_COURSECAT, $childlevels); + $this->assertNotContains(CONTEXT_COURSE, $childlevels); + $this->assertNotContains(CONTEXT_MODULE, $childlevels); + $this->assertContains(CONTEXT_BLOCK, $childlevels); + + $childlevels = context_helper::get_child_levels(CONTEXT_BLOCK); + $this->assertCount(0, $childlevels); + } + + /** + * Tests covered method. + * @covers ::get_compatible_levels + */ + public function test_get_compatible_levels() { + $levels = context_helper::get_compatible_levels('manager'); + $this->assertContains(CONTEXT_SYSTEM, $levels); + $this->assertNotContains(CONTEXT_USER, $levels); + $this->assertContains(CONTEXT_COURSECAT, $levels); + $this->assertContains(CONTEXT_COURSE, $levels); + $this->assertNotContains(CONTEXT_MODULE, $levels); + $this->assertNotContains(CONTEXT_BLOCK, $levels); + + $levels = context_helper::get_compatible_levels('coursecreator'); + $this->assertContains(CONTEXT_SYSTEM, $levels); + $this->assertNotContains(CONTEXT_USER, $levels); + $this->assertContains(CONTEXT_COURSECAT, $levels); + $this->assertNotContains(CONTEXT_COURSE, $levels); + $this->assertNotContains(CONTEXT_MODULE, $levels); + $this->assertNotContains(CONTEXT_BLOCK, $levels); + + $levels = context_helper::get_compatible_levels('editingteacher'); + $this->assertNotContains(CONTEXT_SYSTEM, $levels); + $this->assertNotContains(CONTEXT_USER, $levels); + $this->assertNotContains(CONTEXT_COURSECAT, $levels); + $this->assertContains(CONTEXT_COURSE, $levels); + $this->assertContains(CONTEXT_MODULE, $levels); + $this->assertNotContains(CONTEXT_BLOCK, $levels); + + $levels = context_helper::get_compatible_levels('teacher'); + $this->assertNotContains(CONTEXT_SYSTEM, $levels); + $this->assertNotContains(CONTEXT_USER, $levels); + $this->assertNotContains(CONTEXT_COURSECAT, $levels); + $this->assertContains(CONTEXT_COURSE, $levels); + $this->assertContains(CONTEXT_MODULE, $levels); + $this->assertNotContains(CONTEXT_BLOCK, $levels); + + $levels = context_helper::get_compatible_levels('student'); + $this->assertNotContains(CONTEXT_SYSTEM, $levels); + $this->assertNotContains(CONTEXT_USER, $levels); + $this->assertNotContains(CONTEXT_COURSECAT, $levels); + $this->assertContains(CONTEXT_COURSE, $levels); + $this->assertContains(CONTEXT_MODULE, $levels); + $this->assertNotContains(CONTEXT_BLOCK, $levels); + + $levels = context_helper::get_compatible_levels('user'); + $this->assertCount(0, $levels); + + $levels = context_helper::get_compatible_levels('guest'); + $this->assertCount(0, $levels); + + $levels = context_helper::get_compatible_levels('frontpage'); + $this->assertCount(0, $levels); + } + + /** + * Tests covered method. + * @covers ::cleanup_instances + */ + public function test_cleanup_instances() { + global $DB; + + $this->resetAfterTest(); + $this->preventResetByRollback(); + + $prevcount = $DB->count_records('context', []); + context_helper::cleanup_instances(); + $count = $DB->count_records('context', []); + $this->assertSame($prevcount, $count); + + // Insert bogus records for each level and see if they get removed, + // more test should be in tests for each context level. + $alllevels = context_helper::get_all_levels(); + foreach ($alllevels as $classname) { + if ($classname::LEVEL == CONTEXT_SYSTEM) { + continue; + } + $record = new \stdClass(); + $record->contextlevel = $classname::LEVEL; + $record->instanceid = 9999999999; + $record->path = null; + $record->depth = '2'; + $record->id = $DB->insert_record('context', $record); + $DB->set_field('context', 'path', SYSCONTEXTID . '/' . $record->id, ['id' => $record->id]); + } + context_helper::cleanup_instances(); + $count = $DB->count_records('context', []); + $this->assertSame($prevcount, $count); + } + + /** + * Tests covered method. + * @covers ::create_instances + */ + public function test_create_instances() { + global $DB; + + $this->resetAfterTest(); + $this->preventResetByRollback(); + + $user = $this->getDataGenerator()->create_user(); + $usercontext = context\user::instance($user->id); + $category = $this->getDataGenerator()->create_category(); + $categorycontext = context\coursecat::instance($category->id); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context\course::instance($course->id); + $page = $this->getDataGenerator()->create_module('page', ['course' => $course->id]); + $pagecontext = context\module::instance($page->cmid); + + $prevcount = $DB->count_records('context', []); + $DB->delete_records('context', ['id' => $usercontext->id]); + $DB->delete_records('context', ['id' => $categorycontext->id]); + $DB->delete_records('context', ['id' => $coursecontext->id]); + $DB->delete_records('context', ['id' => $pagecontext->id]); + + context_helper::create_instances(); + $count = $DB->count_records('context', []); + $this->assertSame($prevcount, $count); + } + + /** + * Tests covered method. + * @covers ::build_all_paths + */ + public function test_build_all_paths() { + $this->resetAfterTest(); + $this->preventResetByRollback(); + + // Just make sure there are no fatal errors for now. + context_helper::build_all_paths(true); + context_helper::build_all_paths(); + } + + /** + * Tests covered method. + * @covers ::reset_caches + */ + public function test_reset_caches() { + $this->resetAfterTest(); + $this->preventResetByRollback(); + + // Just make sure there are no fatal errors for now. + context_helper::reset_caches(); + } + + /** + * Tests covered method. + * @covers ::get_preload_record_columns + */ + public function test_get_preload_record_columns() { + $expected = array ( + 'testalias.id' => 'ctxid', + 'testalias.path' => 'ctxpath', + 'testalias.depth' => 'ctxdepth', + 'testalias.contextlevel' => 'ctxlevel', + 'testalias.instanceid' => 'ctxinstance', + 'testalias.locked' => 'ctxlocked', + ); + $result = context_helper::get_preload_record_columns('testalias'); + $this->assertSame($expected, $result); + } + + /** + * Tests covered method. + * @covers ::get_preload_record_columns_sql + */ + public function test_get_preload_record_columns_sql() { + global $DB; + + $result = context_helper::get_preload_record_columns_sql('testalias'); + $expected = 'testalias.id AS ctxid, testalias.path AS ctxpath, testalias.depth AS ctxdepth,' + .' testalias.contextlevel AS ctxlevel, testalias.instanceid AS ctxinstance, testalias.locked AS ctxlocked'; + $this->assertSame($expected, $result); + + $sql = "SELECT id, $result + FROM {context} testalias"; + $DB->get_records_sql($sql, []); + } + + /** + * Tests covered method. + * @covers ::preload_from_record + */ + public function test_preload_from_record() { + global $DB; + + $select = context_helper::get_preload_record_columns_sql('testalias'); + $sql = "SELECT id, $select + FROM {context} testalias"; + $records = $DB->get_records_sql($sql, []); + foreach ($records as $record) { + $this->assertNull(context_helper::preload_from_record($record)); + } + + $this->assertDebuggingNotCalled(); + $record = reset($records); + unset($record->ctxlevel); + $this->assertNull(context_helper::preload_from_record($record)); + } + + /** + * Tests covered method. + * @covers ::preload_contexts_by_id + */ + public function test_preload_contexts_by_id() { + global $DB; + + $contextids = $DB->get_fieldset_sql("SELECT id FROM {context}", []); + context_helper::preload_contexts_by_id($contextids); + context_helper::preload_contexts_by_id($contextids); + + context_helper::reset_caches(); + context_helper::preload_contexts_by_id($contextids); + } + + /** + * Tests covered method. + * @covers ::preload_course + */ + public function test_preload_course() { + global $SITE; + context_helper::preload_course($SITE->id); + } + + /** + * Tests covered method. + * @covers ::delete_instance + */ + public function test_delete_instance() { + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $category = $this->getDataGenerator()->create_category(); + $course = $this->getDataGenerator()->create_course(); + $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id)); + + // This is a bit silly test, it might start failing in the future + // because the instances are not deleted before deleting the contexts. + context_helper::delete_instance(CONTEXT_USER, $user->id); + context_helper::delete_instance(CONTEXT_COURSECAT, $category->id); + context_helper::delete_instance(CONTEXT_COURSE, $course->id); + context_helper::delete_instance(CONTEXT_MODULE, $page->cmid); + } + + /** + * Tests covered method. + * @covers ::get_level_name + */ + public function test_get_level_name() { + $allevels = context_helper::get_all_levels(); + foreach ($allevels as $level => $classname) { + $name = context_helper::get_level_name($level); + $this->assertIsString($name); + } + + $this->assertSame('System', context_helper::get_level_name(CONTEXT_SYSTEM)); + $this->assertSame('User', context_helper::get_level_name(CONTEXT_USER)); + $this->assertSame('Category', context_helper::get_level_name(CONTEXT_COURSECAT)); + $this->assertSame('Course', context_helper::get_level_name(CONTEXT_COURSE)); + $this->assertSame('Activity module', context_helper::get_level_name(CONTEXT_MODULE)); + $this->assertSame('Block', context_helper::get_level_name(CONTEXT_BLOCK)); + } + + /** + * Tests covered method. + * @covers ::get_navigation_filter_context + */ + public function test_get_navigation_filter_context() { + global $CFG; + $this->resetAfterTest(); + + $user = $this->getDataGenerator()->create_user(); + $usercontext = context\user::instance($user->id); + $category = $this->getDataGenerator()->create_category(); + $categorycontext = context\coursecat::instance($category->id); + $course = $this->getDataGenerator()->create_course(); + $coursecontext = context\course::instance($course->id); + $page = $this->getDataGenerator()->create_module('page', array('course' => $course->id)); + $pagecontext = context\module::instance($page->cmid); + $systemcontext = context\system::instance(); + + // Default is OFF. + $this->assertSame('0', $CFG->filternavigationwithsystemcontext); + + // First test passed values are returned if disabled. + set_config('filternavigationwithsystemcontext', '0'); + + $this->assertNull(context_helper::get_navigation_filter_context(null)); + + $filtercontext = context_helper::get_navigation_filter_context($systemcontext); + $this->assertSame($systemcontext, $filtercontext); + + $filtercontext = context_helper::get_navigation_filter_context($usercontext); + $this->assertSame($usercontext, $filtercontext); + + $filtercontext = context_helper::get_navigation_filter_context($categorycontext); + $this->assertSame($categorycontext, $filtercontext); + + $filtercontext = context_helper::get_navigation_filter_context($coursecontext); + $this->assertSame($coursecontext, $filtercontext); + + $filtercontext = context_helper::get_navigation_filter_context($pagecontext); + $this->assertSame($pagecontext, $filtercontext); + + // Now test that any input returns system context if enabled. + set_config('filternavigationwithsystemcontext', '1'); + + $filtercontext = context_helper::get_navigation_filter_context(null); + $this->assertSame($systemcontext->id, $filtercontext->id); + + $filtercontext = context_helper::get_navigation_filter_context($systemcontext); + $this->assertSame($systemcontext->id, $filtercontext->id); + + $filtercontext = context_helper::get_navigation_filter_context($usercontext); + $this->assertSame($systemcontext->id, $filtercontext->id); + + $filtercontext = context_helper::get_navigation_filter_context($categorycontext); + $this->assertSame($systemcontext->id, $filtercontext->id); + + $filtercontext = context_helper::get_navigation_filter_context($coursecontext); + $this->assertSame($systemcontext->id, $filtercontext->id); + + $filtercontext = context_helper::get_navigation_filter_context($pagecontext); + $this->assertSame($systemcontext->id, $filtercontext->id); + } +} diff --git a/lib/tests/context_test.php b/lib/tests/context_test.php new file mode 100644 index 00000000000..3fffc43cb01 --- /dev/null +++ b/lib/tests/context_test.php @@ -0,0 +1,106 @@ +. + +namespace core; + +/** + * Unit tests for base context class. + * + * NOTE: more tests are in lib/tests/accesslib_test.php + * + * @package core + * @copyright Petr Skoda + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \core\context + */ +class context_test extends \advanced_testcase { + /** + * Tests legacy class name. + * @coversNothing + */ + public function test_legacy_classname() { + $this->assertSame('core\context', context::class); + + $context = \context_system::instance(); + $this->assertInstanceOf(context::class, $context); + $this->assertInstanceOf('context', $context); + } + + /** + * Tests covered method. + * @covers ::instance_by_id + */ + public function test_factory_methods() { + $context = context::instance_by_id(SYSCONTEXTID); + $this->assertSame('core\\context\\system', get_class($context)); + } + + /** + * Tests covered methods. + * @covers ::__set + * @covers ::__unset + */ + public function test_propery_change_protection() { + $context = context\system::instance(); + + $context->contextlevel = -10; + $this->assertEquals($context::LEVEL, $context->contextlevel); + $this->assertDebuggingCalled('Can not change context instance properties!'); + + $context->instanceid = -10; + $this->assertEquals(0, $context->instanceid); + $this->assertDebuggingCalled('Can not change context instance properties!'); + + $context->id = -10; + $this->assertDebuggingCalled('Can not change context instance properties!'); + + $context->locked = -10; + $this->assertDebuggingCalled('Can not change context instance properties!'); + + unset($context->contextlevel); + $this->assertEquals($context::LEVEL, $context->contextlevel); + $this->assertDebuggingCalled('Can not unset context instance properties!'); + } + + /** + * Tests covered method. + * @covers ::__get + */ + public function test_incorrect_property() { + $context = context\system::instance(); + + $a = $context->whatever; + $this->assertDebuggingCalled('Invalid context property accessed! whatever'); + } + + /** + * Tests covered method. + * @covers ::getIterator + */ + public function test_iterator() { + $context = context\system::instance(); + $array = iterator_to_array($context->getIterator()); + $expected = [ + 'id' => $context->id, + 'contextlevel' => $context->contextlevel, + 'instanceid' => $context->instanceid, + 'path' => $context->path, + 'depth' => $context->depth, + 'locked' => false, + ]; + $this->assertSame($expected, $array); + } +} diff --git a/lib/tests/customcontext_test.php b/lib/tests/customcontext_test.php index d6a68e3e10e..26f00bf81f3 100644 --- a/lib/tests/customcontext_test.php +++ b/lib/tests/customcontext_test.php @@ -84,10 +84,19 @@ class customcontext_test extends \advanced_testcase { * Bogus custom context class for testing */ class context_bogus1 extends context { + /** + * Returns context shortname. + * + * @return string + */ + public static function get_short_name(): string { + return 'bogus1'; + } + /** * Returns the most relevant URL for this context. * - * @return moodle_url + * @return \moodle_url */ public function get_url() { global $ME; @@ -108,10 +117,19 @@ class context_bogus1 extends context { * Bogus custom context class for testing */ class context_bogus2 extends context { + /** + * Returns context shortname. + * + * @return string + */ + public static function get_short_name(): string { + return 'bogus2'; + } + /** * Returns the most relevant URL for this context. * - * @return moodle_url + * @return \moodle_url */ public function get_url() { global $ME; @@ -132,10 +150,19 @@ class context_bogus2 extends context { * Bogus custom context class for testing */ class context_bogus3 extends context { + /** + * Returns context shortname. + * + * @return string + */ + public static function get_short_name(): string { + return 'bogus3'; + } + /** * Returns the most relevant URL for this context. * - * @return moodle_url + * @return \moodle_url */ public function get_url() { global $ME; diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 0bf366ab15f..17689869a4a 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -3,6 +3,9 @@ information provided here is intended especially for developers. === 4.2 === +* All context classes were moved to \core\context namespace while keeping full backwards compatibility. + Please note it is not necessary to update plugins unless they rely on exact class names returned from + get_class() or context_helper. * Travis CI integration has been removed from core (MDLSITE-7135). Please, use GitHub Actions (GHA) instead. For more information visit https://moodledev.io/general/development/tools/gha * A new constant COMPLETION_COMPLETE_FAIL_HIDDEN is introduced to mark that user has received a failing grade