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