From 0616f045c392f774812f3d79ca8936a0274296be Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 7 Jun 2018 14:35:26 +0800 Subject: [PATCH] MDL-53566 core: Add support for context locking This chagne adds support for a new feature known as Context Locking. This allows a context to be locked, thereby removing all write capabilities for all users (including admin) for that context, and all child contexts. --- cohort/lib.php | 2 +- course/classes/category.php | 3 + lang/en/role.php | 1 + lib/accesslib.php | 232 +++++++++++---- lib/classes/user.php | 2 +- lib/db/access.php | 7 + lib/db/install.xml | 2 + lib/db/upgrade.php | 27 +- lib/filebrowser/file_info_context_course.php | 17 +- lib/phpunit/classes/advanced_testcase.php | 3 + lib/setuplib.php | 2 +- lib/tests/accesslib_has_capability_test.php | 283 +++++++++++++++++++ lib/tests/moodlelib_test.php | 3 +- lib/tests/session_manager_test.php | 2 +- lib/upgrade.txt | 1 + version.php | 2 +- 16 files changed, 526 insertions(+), 63 deletions(-) create mode 100644 lib/tests/accesslib_has_capability_test.php diff --git a/cohort/lib.php b/cohort/lib.php index a76246ed6db..e5a991ab203 100644 --- a/cohort/lib.php +++ b/cohort/lib.php @@ -634,4 +634,4 @@ function cohort_get_list_of_themes() { } } return $themes; -} \ No newline at end of file +} diff --git a/course/classes/category.php b/course/classes/category.php index bf4bfd6eed5..28806759522 100644 --- a/course/classes/category.php +++ b/course/classes/category.php @@ -238,6 +238,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr $record->visible = 1; $record->depth = 0; $record->path = ''; + $record->locked = 0; self::$coursecat0 = new self($record); } return self::$coursecat0; @@ -2458,6 +2459,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr $context = $this->get_context(); $a['xi'] = $context->id; $a['xp'] = $context->path; + $a['xl'] = $context->locked; return $a; } @@ -2486,6 +2488,7 @@ class core_course_category implements renderable, cacheable_object, IteratorAggr $record->ctxdepth = $record->depth + 1; $record->ctxlevel = CONTEXT_COURSECAT; $record->ctxinstance = $record->id; + $record->ctxlocked = $a['xl']; return new self($record, true); } diff --git a/lang/en/role.php b/lang/en/role.php index d0d200347d8..ab7788937e5 100644 --- a/lang/en/role.php +++ b/lang/en/role.php @@ -411,6 +411,7 @@ $string['site:maintenanceaccess'] = 'Access site while in maintenance mode'; $string['site:manageallmessaging'] = 'Add, remove, block and unblock contacts for any user'; $string['site:manageblocks'] = 'Manage blocks on a page'; $string['site:messageanyuser'] = 'Bypass user privacy preferences for messaging any user'; +$string['site:managecontextlocks'] = 'Manage locking of site contexts'; $string['site:mnetloginfromremote'] = 'Login from a remote application via MNet'; $string['site:mnetlogintoremote'] = 'Roam to a remote application via MNet'; $string['site:readallmessages'] = 'Read all messages on site'; diff --git a/lib/accesslib.php b/lib/accesslib.php index ae631cf6c0e..c5a13fea1a2 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -478,6 +478,13 @@ function has_capability($capability, context $context, $user = null, $doanything } } + // Check whether context locking is enabled. + if (!empty($CFG->contextlocking)) { + if ($capinfo->captype === 'write' && $context->locked && $capinfo->name !== 'moodle/site:managecontextlocks') { + return false; + } + } + // somehow make sure the user is not deleted and actually exists if ($userid != 0) { if ($userid == $USER->id and isset($USER->deleted)) { @@ -4727,6 +4734,24 @@ abstract class context extends stdClass implements IteratorAggregate { */ protected $_depth; + /** + * Whether this context is locked or not. + * + * Can be accessed publicly through $context->locked. + * + * @var int + */ + protected $_locked; + + /** + * Whether any parent of the current context is locked. + * + * Can be accessed publicly through $context->ancestorlocked. + * + * @var int + */ + protected $_ancestorlocked; + /** * @var array Context caching info */ @@ -4862,22 +4887,40 @@ abstract class context extends stdClass implements IteratorAggregate { * @param stdClass $rec * @return void (modifies $rec) */ - protected static function preload_from_record(stdClass $rec) { - if (empty($rec->ctxid) or empty($rec->ctxlevel) or !isset($rec->ctxinstance) or empty($rec->ctxpath) or empty($rec->ctxdepth)) { - // $rec does not have enough data, passed here repeatedly or context does not exist yet - return; - } - - // note: in PHP5 the objects are passed by reference, no need to return $rec - $record = new stdClass(); - $record->id = $rec->ctxid; unset($rec->ctxid); - $record->contextlevel = $rec->ctxlevel; unset($rec->ctxlevel); - $record->instanceid = $rec->ctxinstance; unset($rec->ctxinstance); - $record->path = $rec->ctxpath; unset($rec->ctxpath); - $record->depth = $rec->ctxdepth; unset($rec->ctxdepth); - - return context::create_instance_from_record($record); - } + protected static function preload_from_record(stdClass $rec) { + $notenoughdata = false; + $notenoughdata = $notenoughdata || empty($rec->ctxid); + $notenoughdata = $notenoughdata || empty($rec->ctxlevel); + $notenoughdata = $notenoughdata || !isset($rec->ctxinstance); + $notenoughdata = $notenoughdata || empty($rec->ctxpath); + $notenoughdata = $notenoughdata || empty($rec->ctxdepth); + $notenoughdata = $notenoughdata || !isset($rec->ctxlocked); + if ($notenoughdata) { + // The record does not have enough data, passed here repeatedly or context does not exist yet. + if (isset($rec->ctxid) && !isset($rec->ctxlocked)) { + debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER); + } + return; + } + + $record = (object) [ + 'id' => $rec->ctxid, + 'contextlevel' => $rec->ctxlevel, + 'instanceid' => $rec->ctxinstance, + 'path' => $rec->ctxpath, + 'depth' => $rec->ctxdepth, + 'locked' => $rec->ctxlocked, + ]; + + unset($rec->ctxid); + unset($rec->ctxlevel); + unset($rec->ctxinstance); + unset($rec->ctxpath); + unset($rec->ctxdepth); + unset($rec->ctxlocked); + + return context::create_instance_from_record($record); + } // ====== magic methods ======= @@ -4898,11 +4941,18 @@ abstract class context extends stdClass implements IteratorAggregate { */ public function __get($name) { switch ($name) { - case 'id': return $this->_id; - case 'contextlevel': return $this->_contextlevel; - case 'instanceid': return $this->_instanceid; - case 'path': return $this->_path; - case 'depth': return $this->_depth; + case 'id': + return $this->_id; + case 'contextlevel': + return $this->_contextlevel; + case 'instanceid': + return $this->_instanceid; + case 'path': + return $this->_path; + case 'depth': + return $this->_depth; + case 'locked': + return $this->is_locked(); default: debugging('Invalid context property accessed! '.$name); @@ -4917,19 +4967,26 @@ abstract class context extends stdClass implements IteratorAggregate { */ public function __isset($name) { switch ($name) { - case 'id': return isset($this->_id); - case 'contextlevel': return isset($this->_contextlevel); - case 'instanceid': return isset($this->_instanceid); - case 'path': return isset($this->_path); - case 'depth': return isset($this->_depth); - - default: return false; + case 'id': + return isset($this->_id); + case 'contextlevel': + return isset($this->_contextlevel); + case 'instanceid': + return isset($this->_instanceid); + case 'path': + return isset($this->_path); + case 'depth': + return isset($this->_depth); + case 'locked': + // Locked is always set. + return true; + default: + return false; } - } /** - * ALl properties are read only, sorry. + * All properties are read only, sorry. * @param string $name */ public function __unset($name) { @@ -4950,7 +5007,8 @@ abstract class context extends stdClass implements IteratorAggregate { 'contextlevel' => $this->contextlevel, 'instanceid' => $this->instanceid, 'path' => $this->path, - 'depth' => $this->depth + 'depth' => $this->depth, + 'locked' => $this->locked, ); return new ArrayIterator($ret); } @@ -4969,6 +5027,12 @@ abstract class context extends stdClass implements IteratorAggregate { $this->_instanceid = $record->instanceid; $this->_path = $record->path; $this->_depth = $record->depth; + + if (isset($record->locked)) { + $this->_locked = $record->locked; + } else if (!during_initial_install() && !moodle_needs_upgrading()) { + debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER); + } } /** @@ -5011,12 +5075,13 @@ abstract class context extends stdClass implements IteratorAggregate { if ($dbfamily == 'mysql') { $updatesql = "UPDATE {context} ct, {context_temp} temp SET ct.path = temp.path, - ct.depth = temp.depth + ct.depth = temp.depth, + ct.locked = temp.locked WHERE ct.id = temp.id"; } else if ($dbfamily == 'oracle') { $updatesql = "UPDATE {context} ct - SET (ct.path, ct.depth) = - (SELECT temp.path, temp.depth + SET (ct.path, ct.depth, ct.locked) = + (SELECT temp.path, temp.depth, temp.locked FROM {context_temp} temp WHERE temp.id=ct.id) WHERE EXISTS (SELECT 'x' @@ -5025,14 +5090,16 @@ abstract class context extends stdClass implements IteratorAggregate { } else if ($dbfamily == 'postgres' or $dbfamily == 'mssql') { $updatesql = "UPDATE {context} SET path = temp.path, - depth = temp.depth + depth = temp.depth, + locked = temp.locked FROM {context_temp} temp WHERE temp.id={context}.id"; } else { // sqlite and others $updatesql = "UPDATE {context} SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id), - depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id) + 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})"; } @@ -5118,6 +5185,27 @@ abstract class context extends stdClass implements IteratorAggregate { $trans->allow_commit(); } + /** + * Set whether this context has been locked or not. + * + * @param bool $locked + * @return $this + */ + public function set_locked(bool $locked) { + global $DB; + + if ($this->_locked == $locked) { + return $this; + } + + $this->_locked = $locked; + $DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]); + $this->mark_dirty(); + self::reset_caches(); + + return $this; + } + /** * Remove all context path info and optionally rebuild it. * @@ -5239,6 +5327,7 @@ abstract class context extends stdClass implements IteratorAggregate { $record->instanceid = $instanceid; $record->depth = 0; $record->path = null; //not known before insert + $record->locked = 0; $record->id = $DB->insert_record('context', $record); @@ -5266,6 +5355,24 @@ abstract class context extends stdClass implements IteratorAggregate { throw new coding_exception('can not get name of abstract context'); } + /** + * Whether the current context is locked. + * + * @return bool + */ + public function is_locked() { + if ($this->_locked) { + return true; + } + + if ($parent = $this->get_parent_context()) { + $this->_ancestorlocked = $parent->is_locked(); + return $this->_ancestorlocked; + } + + return false; + } + /** * Returns the most relevant URL for this context. * @@ -5724,7 +5831,14 @@ class context_helper extends context { * @return array (table.column=>alias, ...) */ public static function get_preload_record_columns($tablealias) { - return array("$tablealias.id"=>"ctxid", "$tablealias.path"=>"ctxpath", "$tablealias.depth"=>"ctxdepth", "$tablealias.contextlevel"=>"ctxlevel", "$tablealias.instanceid"=>"ctxinstance"); + return [ + "$tablealias.id" => "ctxid", + "$tablealias.path" => "ctxpath", + "$tablealias.depth" => "ctxdepth", + "$tablealias.contextlevel" => "ctxlevel", + "$tablealias.instanceid" => "ctxinstance", + "$tablealias.locked" => "ctxlocked", + ]; } /** @@ -5737,7 +5851,12 @@ class context_helper extends context { * @return string */ public static function get_preload_record_columns_sql($tablealias) { - return "$tablealias.id AS ctxid, $tablealias.path AS ctxpath, $tablealias.depth AS ctxdepth, $tablealias.contextlevel AS ctxlevel, $tablealias.instanceid AS ctxinstance"; + return "$tablealias.id AS ctxid, " . + "$tablealias.path AS ctxpath, " . + "$tablealias.depth AS ctxdepth, " . + "$tablealias.contextlevel AS ctxlevel, " . + "$tablealias.instanceid AS ctxinstance, " . + "$tablealias.locked AS ctxlocked"; } /** @@ -5920,12 +6039,12 @@ class context_system extends context { $record->instanceid = 0; $record->path = '/'.SYSCONTEXTID; $record->depth = 1; + $record->locked = 0; context::$systemcontext = new context_system($record); } return context::$systemcontext; } - try { // We ignore the strictness completely because system context must exist except during install. $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST); @@ -5943,7 +6062,8 @@ class context_system extends context { $record->contextlevel = CONTEXT_SYSTEM; $record->instanceid = 0; $record->depth = 1; - $record->path = null; //not known before insert + $record->path = null; // Not known before insert. + $record->locked = 0; try { if ($DB->count_records('context')) { @@ -5976,6 +6096,10 @@ class context_system extends context { $DB->update_record('context', $record); } + if (empty($record->locked)) { + $record->locked = 0; + } + if (!defined('SYSCONTEXTID')) { define('SYSCONTEXTID', $record->id); } @@ -6056,6 +6180,18 @@ class context_system extends context { $DB->update_record('context', $record); } } + + /** + * Set whether this context has been locked or not. + * + * @param bool $locked + * @return $this + */ + public function set_locked(bool $locked) { + throw new \coding_exception('It is not possible to lock the system context'); + + return $this; + } } @@ -6458,8 +6594,8 @@ class context_coursecat extends context { // Deeper categories - one query per depthlevel $maxdepth = $DB->get_field_sql("SELECT MAX(depth) FROM {course_categories}"); for ($n=2; $n<=$maxdepth; $n++) { - $sql = "INSERT INTO {context_temp} (id, path, depth) - SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1 + $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.") @@ -6682,8 +6818,8 @@ class context_course extends context { $DB->execute($sql); // standard courses - $sql = "INSERT INTO {context_temp} (id, path, depth) - SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1 + $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.") @@ -6951,8 +7087,8 @@ class context_module extends context { $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)"; } - $sql = "INSERT INTO {context_temp} (id, path, depth) - SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1 + $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.") @@ -7172,8 +7308,8 @@ class context_block extends context { } // 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) - SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1 + $sql = "INSERT INTO {context_temp} (id, path, depth, locked) + SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked FROM {context} ctx JOIN {block_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_BLOCK.") JOIN {context} pctx ON (pctx.id = bi.parentcontextid) diff --git a/lib/classes/user.php b/lib/classes/user.php index cc1a1df0609..09a8053b891 100644 --- a/lib/classes/user.php +++ b/lib/classes/user.php @@ -382,7 +382,7 @@ class core_user { protected static function get_enrolled_sql_on_courses_with_capability($capability) { // Get all courses where user have the capability. $courses = get_user_capability_course($capability, null, true, - 'ctxid, ctxpath, ctxdepth, ctxlevel, ctxinstance'); + implode(',', array_values(context_helper::get_preload_record_columns('ctx')))); if (!$courses) { return [null, null]; } diff --git a/lib/db/access.php b/lib/db/access.php index 7ad389c0293..60b3659177f 100644 --- a/lib/db/access.php +++ b/lib/db/access.php @@ -2432,4 +2432,11 @@ $capabilities = array( ) ), + // Context locking/unlocking. + 'moodle/site:managecontextlocks' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + ], + ], ); diff --git a/lib/db/install.xml b/lib/db/install.xml index 7fd383d97c8..f2d08fec0e0 100644 --- a/lib/db/install.xml +++ b/lib/db/install.xml @@ -1130,6 +1130,7 @@ + @@ -1145,6 +1146,7 @@ + diff --git a/lib/db/upgrade.php b/lib/db/upgrade.php index 9b9fb47a21e..6c2d6fb503f 100644 --- a/lib/db/upgrade.php +++ b/lib/db/upgrade.php @@ -2658,10 +2658,6 @@ function xmldb_main_upgrade($oldversion) { $field = new xmldb_field('predictionsprocessor', XMLDB_TYPE_CHAR, '255', null, null, null, null, 'timesplitting'); // Conditionally launch add field predictionsprocessor. - if (!$dbman->field_exists($table, $field)) { - $dbman->add_field($table, $field); - } - // Main savepoint reached. upgrade_main_savepoint(true, 2018102900.00); } @@ -2771,5 +2767,28 @@ function xmldb_main_upgrade($oldversion) { upgrade_main_savepoint(true, 2018110700.01); } + if ($oldversion < 2018111300.00) { + // Define field locked to be added to context. + $table = new xmldb_table('context'); + $field = new xmldb_field('locked', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'depth'); + + // Conditionally launch add field locked. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Define field locked to be added to context_temp. + $table = new xmldb_table('context_temp'); + $field = new xmldb_field('locked', XMLDB_TYPE_INTEGER, '2', null, XMLDB_NOTNULL, null, '0', 'depth'); + + // Conditionally launch add field locked. + if (!$dbman->field_exists($table, $field)) { + $dbman->add_field($table, $field); + } + + // Note: This change also requires a bump in is_major_upgrade_required. + upgrade_main_savepoint(true, 2018111300.00); + } + return true; } diff --git a/lib/filebrowser/file_info_context_course.php b/lib/filebrowser/file_info_context_course.php index 4c8b7977075..74798f07a41 100644 --- a/lib/filebrowser/file_info_context_course.php +++ b/lib/filebrowser/file_info_context_course.php @@ -530,17 +530,24 @@ class file_info_context_course extends file_info { 'contextlevel' => CONTEXT_MODULE, 'depth' => $this->context->depth + 1, 'pathmask' => $this->context->path . '/%']; - $sql1 = "SELECT ctx.id AS contextid, f.component, f.filearea, f.itemid, ctx.instanceid AS cmid, " . - context_helper::get_preload_record_columns_sql('ctx') . " + $ctxfieldsas = context_helper::get_preload_record_columns_sql('ctx'); + $ctxfields = implode(', ', array_keys(context_helper::get_preload_record_columns('ctx'))); + $sql1 = "SELECT + ctx.id AS contextid, + f.component, + f.filearea, + f.itemid, + ctx.instanceid AS cmid, + {$ctxfieldsas} FROM {files} f INNER JOIN {context} ctx ON ctx.id = f.contextid WHERE f.filename <> :emptyfilename AND ctx.contextlevel = :contextlevel AND ctx.depth = :depth AND " . $DB->sql_like('ctx.path', ':pathmask') . " "; - $sql3 = ' GROUP BY ctx.id, f.component, f.filearea, f.itemid, ctx.instanceid, - ctx.path, ctx.depth, ctx.contextlevel - ORDER BY ctx.id, f.component, f.filearea, f.itemid'; + $sql3 = " + GROUP BY ctx.id, f.component, f.filearea, f.itemid, {$ctxfields} + ORDER BY ctx.id, f.component, f.filearea, f.itemid"; list($sql2, $params2) = $this->build_search_files_sql($extensions); $areas = []; if ($rs = $DB->get_recordset_sql($sql1. $sql2 . $sql3, array_merge($params1, $params2))) { diff --git a/lib/phpunit/classes/advanced_testcase.php b/lib/phpunit/classes/advanced_testcase.php index 546d11bee51..e18fddf3182 100644 --- a/lib/phpunit/classes/advanced_testcase.php +++ b/lib/phpunit/classes/advanced_testcase.php @@ -132,6 +132,9 @@ abstract class advanced_testcase extends base_testcase { self::resetAllData(true); } + // Reset context cache. + context_helper::reset_caches(); + // make sure test did not forget to close transaction if ($DB->is_transaction_started()) { self::resetAllData(); diff --git a/lib/setuplib.php b/lib/setuplib.php index 7a07e1d023f..1d5dd62bbb6 100644 --- a/lib/setuplib.php +++ b/lib/setuplib.php @@ -1395,7 +1395,7 @@ function disable_output_buffering() { */ function is_major_upgrade_required() { global $CFG; - $lastmajordbchanges = 2017092900.00; + $lastmajordbchanges = 2018111300.00; $required = empty($CFG->version); $required = $required || (float)$CFG->version < $lastmajordbchanges; diff --git a/lib/tests/accesslib_has_capability_test.php b/lib/tests/accesslib_has_capability_test.php new file mode 100644 index 00000000000..ccf6d44b534 --- /dev/null +++ b/lib/tests/accesslib_has_capability_test.php @@ -0,0 +1,283 @@ +. + +/** + * A collection of tests for accesslib::has_capability(). + * + * @package core + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Unit tests tests for has_capability. + * + * @package core + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class accesslib_has_capability_testcase extends \advanced_testcase { + + /** + * Unit tests to check the operation of locked contexts. + * + * Note: We only check the admin user here. + * If the admin cannot do it, then no-one can. + * + * @dataProvider locked_context_provider + * @param string[] $lockedcontexts The list of contexts, by name, to mark as locked + * @param string[] $blocked The list of contexts which will be 'blocked' by has_capability + */ + public function test_locked_contexts($lockedcontexts, $blocked) { + global $DB; + + $this->resetAfterTest(); + set_config('contextlocking', 1); + + $generator = $this->getDataGenerator(); + $otheruser = $generator->create_user(); + + // / (system) + // /Cat1 + // /Cat1/Block + // /Cat1/Course1 + // /Cat1/Course1/Block + // /Cat1/Course2 + // /Cat1/Course2/Block + // /Cat1/Cat1a + // /Cat1/Cat1a/Block + // /Cat1/Cat1a/Course1 + // /Cat1/Cat1a/Course1/Block + // /Cat1/Cat1a/Course2 + // /Cat1/Cat1a/Course2/Block + // /Cat1/Cat1b + // /Cat1/Cat1b/Block + // /Cat1/Cat1b/Course1 + // /Cat1/Cat1b/Course1/Block + // /Cat1/Cat1b/Course2 + // /Cat1/Cat1b/Course2/Block + // /Cat2 + // /Cat2/Block + // /Cat2/Course1 + // /Cat2/Course1/Block + // /Cat2/Course2 + // /Cat2/Course2/Block + // /Cat2/Cat2a + // /Cat2/Cat2a/Block + // /Cat2/Cat2a/Course1 + // /Cat2/Cat2a/Course1/Block + // /Cat2/Cat2a/Course2 + // /Cat2/Cat2a/Course2/Block + // /Cat2/Cat2b + // /Cat2/Cat2b/Block + // /Cat2/Cat2b/Course1 + // /Cat2/Cat2b/Course1/Block + // /Cat2/Cat2b/Course2 + // /Cat2/Cat2b/Course2/Block + + $adminuser = \core_user::get_user_by_username('admin'); + $contexts = (object) [ + 'system' => \context_system::instance(), + 'adminuser' => \context_user::instance($adminuser->id), + ]; + + $cat1 = $generator->create_category(); + $cat1a = $generator->create_category(['parent' => $cat1->id]); + $cat1b = $generator->create_category(['parent' => $cat1->id]); + + $contexts->cat1 = \context_coursecat::instance($cat1->id); + $contexts->cat1a = \context_coursecat::instance($cat1a->id); + $contexts->cat1b = \context_coursecat::instance($cat1b->id); + + $cat1course1 = $generator->create_course(['category' => $cat1->id]); + $cat1course2 = $generator->create_course(['category' => $cat1->id]); + $cat1acourse1 = $generator->create_course(['category' => $cat1a->id]); + $cat1acourse2 = $generator->create_course(['category' => $cat1a->id]); + $cat1bcourse1 = $generator->create_course(['category' => $cat1b->id]); + $cat1bcourse2 = $generator->create_course(['category' => $cat1b->id]); + + $contexts->cat1course1 = \context_course::instance($cat1course1->id); + $contexts->cat1acourse1 = \context_course::instance($cat1acourse1->id); + $contexts->cat1bcourse1 = \context_course::instance($cat1bcourse1->id); + $contexts->cat1course2 = \context_course::instance($cat1course2->id); + $contexts->cat1acourse2 = \context_course::instance($cat1acourse2->id); + $contexts->cat1bcourse2 = \context_course::instance($cat1bcourse2->id); + + $cat1block = $generator->create_block('online_users', ['parentcontextid' => $contexts->cat1->id]); + $cat1ablock = $generator->create_block('online_users', ['parentcontextid' => $contexts->cat1a->id]); + $cat1bblock = $generator->create_block('online_users', ['parentcontextid' => $contexts->cat1b->id]); + $cat1course1block = $generator->create_block('online_users', ['parentcontextid' => $contexts->cat1course1->id]); + $cat1course2block = $generator->create_block('online_users', ['parentcontextid' => $contexts->cat1course2->id]); + $cat1acourse1block = $generator->create_block('online_users', ['parentcontextid' => $contexts->cat1acourse1->id]); + $cat1acourse2block = $generator->create_block('online_users', ['parentcontextid' => $contexts->cat1acourse2->id]); + $cat1bcourse1block = $generator->create_block('online_users', ['parentcontextid' => $contexts->cat1bcourse1->id]); + $cat1bcourse2block = $generator->create_block('online_users', ['parentcontextid' => $contexts->cat1bcourse2->id]); + + $contexts->cat1block = \context_block::instance($cat1block->id); + $contexts->cat1ablock = \context_block::instance($cat1ablock->id); + $contexts->cat1bblock = \context_block::instance($cat1bblock->id); + $contexts->cat1course1block = \context_block::instance($cat1course1block->id); + $contexts->cat1course2block = \context_block::instance($cat1course2block->id); + $contexts->cat1acourse1block = \context_block::instance($cat1acourse1block->id); + $contexts->cat1acourse2block = \context_block::instance($cat1acourse2block->id); + $contexts->cat1bcourse1block = \context_block::instance($cat1bcourse1block->id); + $contexts->cat1bcourse2block = \context_block::instance($cat1bcourse2block->id); + + $writecapability = 'moodle/block:edit'; + $readcapability = 'moodle/block:view'; + $managecapability = 'moodle/site:managecontextlocks'; + + $this->setAdminUser(); + $totest = (array) $contexts; + foreach ($totest as $context) { + $this->assertTrue(has_capability($writecapability, $context)); + $this->assertTrue(has_capability($readcapability, $context)); + $this->assertTrue(has_capability($managecapability, $context)); + } + + // Lock the specified contexts. + foreach ($lockedcontexts as $contextname => $value) { + $contexts->$contextname->set_locked($value); + } + + // All read capabilities should remain. + foreach ((array) $contexts as $context) { + $this->assertTrue(has_capability($readcapability, $context)); + $this->assertTrue(has_capability($managecapability, $context)); + } + + // Check writes. + foreach ((array) $contexts as $contextname => $context) { + if (false !== array_search($contextname, $blocked)) { + $this->assertFalse(has_capability($writecapability, $context)); + } else { + $this->assertTrue(has_capability($writecapability, $context)); + } + } + + $this->setUser($otheruser); + // Check writes. + foreach ((array) $contexts as $contextname => $context) { + $this->assertFalse(has_capability($writecapability, $context)); + } + + // Disable the contextlocking experimental feature. + set_config('contextlocking', 0); + + $this->setAdminUser(); + // All read capabilities should remain. + foreach ((array) $contexts as $context) { + $this->assertTrue(has_capability($readcapability, $context)); + $this->assertTrue(has_capability($managecapability, $context)); + } + + // All write capabilities should now be present again. + foreach ((array) $contexts as $contextname => $context) { + $this->assertTrue(has_capability($writecapability, $context)); + } + + $this->setUser($otheruser); + // Check writes. + foreach ((array) $contexts as $contextname => $context) { + $this->assertFalse(has_capability($writecapability, $context)); + } + } + + /** + * Data provider for testing that has_capability() deals with locked contexts. + * + * @return array + */ + public function locked_context_provider() { + return [ + 'All unlocked' => [ + 'locked' => [ + ], + 'blockedwrites' => [ + ], + ], + 'User is locked (yes, this is weird)' => [ + 'locked' => [ + 'adminuser' => true, + ], + 'blockedwrites' => [ + 'adminuser', + ], + ], + 'Cat1/Block locked' => [ + 'locked' => [ + 'cat1block' => true, + ], + 'blockedwrites' => [ + 'cat1block', + ], + ], + 'Cat1' => [ + 'locked' => [ + 'cat1' => true, + ], + 'blockedwrites' => [ + 'cat1', + 'cat1block', + 'cat1a', + 'cat1ablock', + 'cat1b', + 'cat1bblock', + 'cat1course1', + 'cat1course1block', + 'cat1course2', + 'cat1course2block', + 'cat1acourse1', + 'cat1acourse1block', + 'cat1acourse2', + 'cat1acourse2block', + 'cat1bcourse1', + 'cat1bcourse1block', + 'cat1bcourse2', + 'cat1bcourse2block', + ], + ], + 'Cat1 locked and a child explicitly unlocked' => [ + 'locked' => [ + 'cat1' => true, + 'cat1a' => false, + ], + 'blockedwrites' => [ + 'cat1', + 'cat1block', + 'cat1a', + 'cat1ablock', + 'cat1b', + 'cat1bblock', + 'cat1course1', + 'cat1course1block', + 'cat1course2', + 'cat1course2block', + 'cat1acourse1', + 'cat1acourse1block', + 'cat1acourse2', + 'cat1acourse2block', + 'cat1bcourse1', + 'cat1bcourse1block', + 'cat1bcourse2', + 'cat1bcourse2block', + ], + ], + ]; + } +} diff --git a/lib/tests/moodlelib_test.php b/lib/tests/moodlelib_test.php index a2287541e9d..5f3eab48d97 100644 --- a/lib/tests/moodlelib_test.php +++ b/lib/tests/moodlelib_test.php @@ -2429,7 +2429,8 @@ class core_moodlelib_testcase extends advanced_testcase { 'contextlevel' => $obj->contextlevel, 'instanceid' => $obj->instanceid, 'path' => $obj->path, - 'depth' => $obj->depth + 'depth' => $obj->depth, + 'locked' => $obj->locked, ); $this->assertEquals(convert_to_array($obj), $ar); } diff --git a/lib/tests/session_manager_test.php b/lib/tests/session_manager_test.php index a5a15d904e2..b3b8c9d11c2 100644 --- a/lib/tests/session_manager_test.php +++ b/lib/tests/session_manager_test.php @@ -571,7 +571,7 @@ class core_session_manager_testcase extends advanced_testcase { \core\session\manager::loginas($user->id, context_system::instance()); $this->assertSame($user->id, $USER->id); - $this->assertSame(context_system::instance(), $USER->loginascontext); + $this->assertEquals(context_system::instance(), $USER->loginascontext); $this->assertSame($adminuser->id, $USER->realuser); $this->assertSame($GLOBALS['USER'], $_SESSION['USER']); $this->assertSame($GLOBALS['USER'], $USER); diff --git a/lib/upgrade.txt b/lib/upgrade.txt index 6bea9f1ab00..0b896f04e6c 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -165,6 +165,7 @@ the groupid field. until all settings have been set. The additional parameters are used recursively and shouldn't be need to be explicitly passed in when calling the function from other parts of Moodle. The return value: $settingsoutput is an array of setting names and the values that were set by the function. +* A new field has been added to the context table. Please ensure that any contxt preloading uses get_preload_record_columns_sql or get_preload_record_columns to fetch the list of columns. === 3.5 === diff --git a/version.php b/version.php index ebf9aea28d6..a1b2728a550 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018111000.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018111300.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes. -- 2.43.0