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.
This commit is contained in:
Andrew Nicols
2018-06-07 14:35:26 +08:00
parent 208950cff0
commit 0616f045c3
16 changed files with 524 additions and 61 deletions
+1 -1
View File
@@ -634,4 +634,4 @@ function cohort_get_list_of_themes() {
}
}
return $themes;
}
}
+3
View File
@@ -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);
}
+1
View File
@@ -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';
+182 -46
View File
@@ -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;
}
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;
}
// 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);
$record = (object) [
'id' => $rec->ctxid,
'contextlevel' => $rec->ctxlevel,
'instanceid' => $rec->ctxinstance,
'path' => $rec->ctxpath,
'depth' => $rec->ctxdepth,
'locked' => $rec->ctxlocked,
];
return context::create_instance_from_record($record);
}
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)
+1 -1
View File
@@ -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];
}
+7
View File
@@ -2432,4 +2432,11 @@ $capabilities = array(
)
),
// Context locking/unlocking.
'moodle/site:managecontextlocks' => [
'captype' => 'write',
'contextlevel' => CONTEXT_SYSTEM,
'archetypes' => [
],
],
);
+2
View File
@@ -1130,6 +1130,7 @@
<FIELD NAME="instanceid" TYPE="int" LENGTH="10" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="path" TYPE="char" LENGTH="255" NOTNULL="false" SEQUENCE="false"/>
<FIELD NAME="depth" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false"/>
<FIELD NAME="locked" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether this context and its children are locked"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
@@ -1145,6 +1146,7 @@
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" SEQUENCE="false" COMMENT="This id isn't autonumeric/sequence. It's the context-&gt;id"/>
<FIELD NAME="path" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="depth" TYPE="int" LENGTH="2" NOTNULL="true" SEQUENCE="false"/>
<FIELD NAME="locked" TYPE="int" LENGTH="2" NOTNULL="true" DEFAULT="0" SEQUENCE="false" COMMENT="Whether this context and its children are locked"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id"/>
+23 -4
View File
@@ -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;
}
+12 -5
View File
@@ -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))) {
@@ -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();
+1 -1
View File
@@ -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;
+283
View File
@@ -0,0 +1,283 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* A collection of tests for accesslib::has_capability().
*
* @package core
* @copyright 2018 Andrew Nicols <andrew@nicols.co.uk>
* @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 <andrew@nicols.co.uk>
* @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',
],
],
];
}
}
+2 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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);
+1
View File
@@ -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 ===
+1 -1
View File
@@ -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.