diff --git a/admin/lock.php b/admin/lock.php new file mode 100644 index 00000000000..3041ded67ae --- /dev/null +++ b/admin/lock.php @@ -0,0 +1,96 @@ +. + +/** + * This file is used to display a categories sub categories, external pages, and settings. + * + * @package admin + * @copyright 2018 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once('../config.php'); +require_once("{$CFG->libdir}/adminlib.php"); + +$contextid = required_param('id', PARAM_INT); +$confirm = optional_param('confirm', null, PARAM_INT); +$returnurl = optional_param('returnurl', null, PARAM_LOCALURL); + +$PAGE->set_url('/admin/lock.php', ['id' => $contextid]); + +list($context, $course, $cm) = get_context_info_array($contextid); + +$parentcontext = $context->get_parent_context(); +if ($parentcontext && !empty($parentcontext->locked)) { + // Can't make changes to a context whose parent is locked. + throw new \coding_exception('Not sure how you got here'); +} + +if ($course) { + $isfrontpage = ($course->id == SITEID); +} else { + $isfrontpage = false; + $course = $SITE; +} + +require_login($course, false, $cm); +require_capability('moodle/site:managecontextlocks', $context); + +$PAGE->set_pagelayout('admin'); +$PAGE->navigation->clear_cache(); + +$a = (object) [ + 'contextname' => $context->get_context_name(), +]; + +if (null !== $confirm && confirm_sesskey()) { + $context->set_locked(!empty($confirm)); + + if ($context->locked) { + $lockmessage = get_string('managecontextlocklocked', 'admin', $a); + } else { + $lockmessage = get_string('managecontextlockunlocked', 'admin', $a); + } + + if (empty($returnurl)) { + $returnurl = $context->get_url(); + } else { + $returnurl = new moodle_url($returnurl); + } + redirect($returnurl, $lockmessage); +} + +$heading = get_string('managecontextlock', 'admin'); +$PAGE->set_title($heading); +$PAGE->set_heading($heading); + +echo $OUTPUT->header(); + +if ($context->locked) { + $confirmstring = get_string('confirmcontextunlock', 'admin', $a); + $target = 0; +} else { + $confirmstring = get_string('confirmcontextlock', 'admin', $a); + $target = 1; +} + +$confirmurl = new \moodle_url($PAGE->url, ['confirm' => $target]); +if (!empty($returnurl)) { + $confirmurl->param('returnurl', $returnurl); +} + +echo $OUTPUT->confirm($confirmstring, $confirmurl, $context->get_url()); +echo $OUTPUT->footer(); diff --git a/admin/settings/development.php b/admin/settings/development.php index cc2a6203905..61d82c32d87 100644 --- a/admin/settings/development.php +++ b/admin/settings/development.php @@ -16,6 +16,16 @@ if ($hassiteconfig) { // speedup for non-admins, add all caps used on this page $temp->add(new admin_setting_configexecutable('pathtosassc', new lang_string('pathtosassc', 'admin'), new lang_string('pathtosassc_help', 'admin'), '')); + $temp->add(new admin_setting_configcheckbox('contextlocking', new lang_string('contextlocking', 'core_admin'), + new lang_string('contextlocking_desc', 'core_admin'), 0)); + + $temp->add(new admin_setting_configcheckbox( + 'contextlockappliestoadmin', + new lang_string('contextlockappliestoadmin', 'core_admin'), + new lang_string('contextlockappliestoadmin_desc', 'core_admin'), + 1 + )); + $temp->add(new admin_setting_configcheckbox('forceclean', new lang_string('forceclean', 'core_admin'), new lang_string('forceclean_desc', 'core_admin'), 0)); diff --git a/admin/tool/monitor/lib.php b/admin/tool/monitor/lib.php index c7493adedf1..58078ec2dc2 100644 --- a/admin/tool/monitor/lib.php +++ b/admin/tool/monitor/lib.php @@ -125,7 +125,15 @@ function tool_monitor_get_user_courses() { $options[0] = get_string('site'); } - $fields = 'fullname, visible, ctxid, ctxpath, ctxdepth, ctxlevel, ctxinstance'; + $fieldlist = array_merge( + [ + 'fullname', + 'visible', + ], + array_values(context_helper::get_preload_record_columns('c')) + ); + + $fields = implode(', ', $fieldlist); if ($courses = get_user_capability_course('tool/monitor:subscribe', null, true, $fields, $orderby)) { foreach ($courses as $course) { context_helper::preload_from_record($course); 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/course/classes/management/helper.php b/course/classes/management/helper.php index 2374883cadd..0befd6433df 100644 --- a/course/classes/management/helper.php +++ b/course/classes/management/helper.php @@ -168,6 +168,8 @@ class helper { * @return array */ public static function get_category_listitem_actions(\core_course_category $category) { + global $CFG; + $manageurl = new \moodle_url('/course/management.php', array('categoryid' => $category->id)); $baseurl = new \moodle_url($manageurl, array('sesskey' => \sesskey())); $actions = array(); @@ -280,6 +282,28 @@ class helper { ); } + // Context locking. + if (!empty($CFG->contextlocking) && has_capability('moodle/site:managecontextlocks', $category->get_context())) { + $parentcontext = $category->get_context()->get_parent_context(); + if (empty($parentcontext) || !$parentcontext->locked) { + if ($category->get_context()->locked) { + $lockicon = 'i/unlock'; + $lockstring = get_string('managecontextunlock', 'admin'); + } else { + $lockicon = 'i/lock'; + $lockstring = get_string('managecontextlock', 'admin'); + } + $actions['managecontextlock'] = [ + 'url' => new \moodle_url('/admin/lock.php', [ + 'id' => $category->get_context()->id, + 'returnurl' => $manageurl->out_as_local_url(false), + ]), + 'icon' => new \pix_icon($lockicon, $lockstring), + 'string' => $lockstring, + ]; + } + } + // Cohorts. if ($category->can_review_cohorts()) { $actions['cohorts'] = array( diff --git a/lang/en/admin.php b/lang/en/admin.php index 11d79f3c7ea..6219a099d7a 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -379,8 +379,14 @@ $string['configvisiblecourses'] = 'Display courses in hidden categories normally $string['configwarning'] = 'Be careful modifying these settings - strange values could cause problems.'; $string['configyuicomboloading'] = 'This options enables combined file loading optimisation for YUI libraries. This setting should be enabled on production sites for performance reasons.'; $string['confirmation'] = 'Confirmation'; +$string['confirmcontextlock'] = '{$a->contextname} is currently unlocked. Locking it will prevent any further changes. Are you sure you wish to continue?'; +$string['confirmcontextunlock'] = '{$a->contextname} is currently locked. Unlocking it will allow users to make changes. Are you sure you wish to continue?'; $string['confirmdeletecomments'] = 'You are about to delete comments, are you sure?'; $string['confirmed'] = 'Confirmed'; +$string['contextlocking'] = 'Context locking'; +$string['contextlocking_desc'] = 'This setting allows you to lock categories, courses, activites, and blocks within the site by removing all write-access to those locations.'; +$string['contextlockappliestoadmin'] = 'Context locking applies to administrators'; +$string['contextlockappliestoadmin_desc'] = 'This setting allows administrators to make changes in any context which is locked.'; $string['cookiehttponly'] = 'Only http cookies'; $string['cookiesecure'] = 'Secure cookies only'; $string['country'] = 'Default country'; @@ -718,6 +724,10 @@ $string['maintenancemodeisscheduled'] = 'This site will be switched to maintenan $string['maintenancemodeisscheduledlong'] = 'This site will be switched to maintenance mode in {$a->hour} hours {$a->min} mins {$a->sec} secs'; $string['maintfileopenerror'] = 'Error opening maintenance files!'; $string['maintinprogress'] = 'Maintenance is in progress...'; +$string['managecontextlock'] = 'Lock this context'; +$string['managecontextlocklocked'] = '{$a->contextname}, and all of its children are now locked.'; +$string['managecontextlockunlocked'] = '{$a->contextname}, and all of its children are now unlocked.'; +$string['managecontextunlock'] = 'Unlock this context'; $string['manageformats'] = 'Manage course formats'; $string['manageformatsgotosettings'] = 'Default format can be changed in {$a}'; $string['managelang'] = 'Manage'; diff --git a/lang/en/role.php b/lang/en/role.php index d0d200347d8..acb886dd50d 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 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..9d6dc4f2aa2 100644 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -478,6 +478,21 @@ 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) { + // Context locking applies to any write capability in a locked context. + // It does not apply to moodle/site:managecontextlocks - this is to allow context locking to be unlocked. + if ($capinfo->name !== 'moodle/site:managecontextlocks') { + // It applies to all users who are not site admins. + // It also applies to site admins when contextlockappliestoadmin is set. + if (!is_siteadmin($userid) || !empty($CFG->contextlockappliestoadmin)) { + 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 +4742,15 @@ 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; + /** * @var array Context caching info */ @@ -4862,22 +4886,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 +4940,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 +4966,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 +5006,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 +5026,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 +5074,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 +5089,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 +5184,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 +5326,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 +5354,23 @@ 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()) { + return $parent->is_locked(); + } + + return false; + } + /** * Returns the most relevant URL for this context. * @@ -5724,7 +5829,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 +5849,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 +6037,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 +6060,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 +6094,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 +6178,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 +6592,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 +6816,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 +7085,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 +7306,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/blocklib.php b/lib/blocklib.php index feaefb39e19..18172c2bc17 100644 --- a/lib/blocklib.php +++ b/lib/blocklib.php @@ -1378,6 +1378,30 @@ class block_manager { ); } + if (!empty($CFG->contextlocking) && has_capability('moodle/site:managecontextlocks', $block->context)) { + $parentcontext = $block->context->get_parent_context(); + if (empty($parentcontext) || empty($parentcontext->locked)) { + if ($block->context->locked) { + $lockicon = 'i/unlock'; + $lockstring = get_string('managecontextunlock', 'admin'); + } else { + $lockicon = 'i/lock'; + $lockstring = get_string('managecontextlock', 'admin'); + } + $controls[] = new action_menu_link_secondary( + new moodle_url( + '/admin/lock.php', + [ + 'id' => $block->context->id, + ] + ), + new pix_icon($lockicon, $lockstring, 'moodle', array('class' => 'iconsmall', 'title' => '')), + $lockstring, + ['class' => 'editing_lock'] + ); + } + } + return $controls; } 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..9ae57a90046 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_MODULE, + '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..b78d8d435cf 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 < 2018111301.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, 2018111301.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/navigationlib.php b/lib/navigationlib.php index 94f995d26fe..93d32b5d2bd 100644 --- a/lib/navigationlib.php +++ b/lib/navigationlib.php @@ -834,6 +834,43 @@ class navigation_node implements renderable { } return $this->action; } + + /** + * Add the menu item to handle locking and unlocking of a conext. + * + * @param \navigation_node $node Node to add + * @param \context $context The context to be locked + */ + protected function add_context_locking_node(\navigation_node $node, \context $context) { + global $CFG; + // Manage context locking. + if (!empty($CFG->contextlocking) && has_capability('moodle/site:managecontextlocks', $context)) { + $parentcontext = $context->get_parent_context(); + if (empty($parentcontext) || !$parentcontext->locked) { + if ($context->locked) { + $lockicon = 'i/unlock'; + $lockstring = get_string('managecontextunlock', 'admin'); + } else { + $lockicon = 'i/lock'; + $lockstring = get_string('managecontextlock', 'admin'); + } + $node->add( + $lockstring, + new moodle_url( + '/admin/lock.php', + [ + 'id' => $context->id, + ] + ), + self::TYPE_SETTING, + null, + 'contextlocking', + new pix_icon($lockicon, '') + ); + } + } + + } } /** @@ -4371,6 +4408,9 @@ class settings_navigation extends navigation_node { null, 'gradebooksetup', new pix_icon('i/settings', '')); } + // Add the context locking node. + $this->add_context_locking_node($coursenode, $coursecontext); + // Add outcome if permitted if ($adminoptions->outcomes) { $url = new moodle_url('/grade/edit/outcome/course.php', array('id'=>$course->id)); @@ -4507,6 +4547,10 @@ class settings_navigation extends navigation_node { $url = new moodle_url('/'.$CFG->admin.'/roles/check.php', array('contextid'=>$this->page->cm->context->id)); $modulenode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null, 'rolecheck'); } + + // Add the context locking node. + $this->add_context_locking_node($modulenode, $this->page->cm->context); + // Manage filters if (has_capability('moodle/filter:manage', $this->page->cm->context) && count(filter_get_available_in_context($this->page->cm->context))>0) { $url = new moodle_url('/filter/manage.php', array('contextid'=>$this->page->cm->context->id)); @@ -5087,6 +5131,9 @@ class settings_navigation extends navigation_node { 'checkpermissions', new pix_icon('i/checkpermissions', '')); } + // Add the context locking node. + $this->add_context_locking_node($blocknode, $this->context); + return $blocknode; } @@ -5149,6 +5196,9 @@ class settings_navigation extends navigation_node { $categorynode->add(get_string('checkpermissions', 'role'), $url, self::TYPE_SETTING, null, 'checkpermissions', new pix_icon('i/checkpermissions', '')); } + // Add the context locking node. + $this->add_context_locking_node($categorynode, $catcontext); + // Cohorts if (has_any_capability(array('moodle/cohort:view', 'moodle/cohort:manage'), $catcontext)) { $categorynode->add(get_string('cohorts', 'cohort'), new moodle_url('/cohort/index.php', 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..3bd14ae9041 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 = 2018111301.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..905932ae1db --- /dev/null +++ b/lib/tests/accesslib_has_capability_test.php @@ -0,0 +1,445 @@ +. + +/** + * 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)); + } + } + + /** + * 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_for_admin_with_config($lockedcontexts, $blocked) { + global $DB; + + $this->resetAfterTest(); + set_config('contextlocking', 1); + set_config('contextlockappliestoadmin', 0); + + $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) { + $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/behat/locking.feature b/lib/tests/behat/locking.feature new file mode 100644 index 00000000000..a1bebdedd25 --- /dev/null +++ b/lib/tests/behat/locking.feature @@ -0,0 +1,205 @@ +@core +Feature: Context locks apply to child contexts + In order to preserve content + As a manager + I can disbale writes at different areas + + Background: + Given the following config values are set as admin: + | contextlocking | 1 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher | Ateacher | Teacher | teacher@example.com | + | student1 | Astudent | Astudent | student1@example.com | + And the following "categories" exist: + | name | category | idnumber | + | cata | 0 | cata | + | cataa | cata | cataa | + | catb | 0 | catb | + And the following "courses" exist: + | fullname | shortname | category | + | courseaa1 | courseaa1 | cataa | + | courseaa2 | courseaa2 | cataa | + | courseb | courseb | catb | + And the following "activities" exist: + | activity | name | course | idnumber | + | forum | faa1 | courseaa1 | faa1 | + | forum | faa1b | courseaa1 | faa1b | + | forum | faa2 | courseaa2 | faa2 | + | forum | fb | courseb | fb | + And the following "course enrolments" exist: + | user | course | role | + | teacher | courseaa1 | editingteacher | + | student1 | courseaa1 | student | + | teacher | courseaa2 | editingteacher | + | student1 | courseaa2 | student | + | teacher | courseb | editingteacher | + | student1 | courseb | student | + + Scenario: Lock course module module should lock just that module + Given I log in as "admin" + And I am on "courseaa1" course homepage + And I follow "faa1" + And I should see "Add a new discussion topic" + When I follow "Lock this context" + And I click on "Continue" "button" + Then I should not see "Add a new discussion topic" + When I am on "courseaa1" course homepage + Then I should see "Turn editing on" + When I follow "faa1b" + Then I should see "Add a new discussion topic" + When I am on "courseaa2" course homepage + Then I should see "Turn editing on" + When I follow "faa2" + Then I should see "Add a new discussion topic" + When I am on "courseb" course homepage + Then I should see "Turn editing on" + When I follow "fb" + Then I should see "Add a new discussion topic" + + And I log out + When I log in as "teacher" + And I am on "courseaa1" course homepage + And I follow "faa1" + Then I should not see "Add a new discussion topic" + When I am on "courseaa1" course homepage + Then I should see "Turn editing on" + When I follow "faa1b" + Then I should see "Add a new discussion topic" + When I am on "courseaa2" course homepage + Then I should see "Turn editing on" + When I follow "faa2" + Then I should see "Add a new discussion topic" + When I am on "courseb" course homepage + Then I should see "Turn editing on" + When I follow "fb" + Then I should see "Add a new discussion topic" + + And I log out + When I log in as "student1" + And I am on "courseaa1" course homepage + And I follow "faa1" + Then I should not see "Add a new discussion topic" + When I am on "courseaa1" course homepage + When I follow "faa1b" + Then I should see "Add a new discussion topic" + When I am on "courseaa2" course homepage + When I follow "faa2" + Then I should see "Add a new discussion topic" + When I am on "courseb" course homepage + When I follow "fb" + Then I should see "Add a new discussion topic" + + Scenario: Lock course should lock all children + Given I log in as "admin" + And I am on "courseaa1" course homepage + And I should see "Turn editing on" + When I follow "Lock this context" + And I click on "Continue" "button" + Then I should not see "Turn editing on" + Then I should not see "Add a new discussion topic" + When I am on "courseaa1" course homepage + Then I should not see "Turn editing on" + And I should see "Unlock this context" + When I follow "faa1b" + Then I should not see "Add a new discussion topic" + And I should not see "Unlock this context" + When I am on "courseaa2" course homepage + Then I should see "Turn editing on" + When I follow "faa2" + Then I should see "Add a new discussion topic" + When I am on "courseb" course homepage + Then I should see "Turn editing on" + When I follow "fb" + Then I should see "Add a new discussion topic" + + And I log out + When I log in as "teacher" + And I am on "courseaa1" course homepage + And I follow "faa1" + Then I should not see "Add a new discussion topic" + When I am on "courseaa1" course homepage + Then I should not see "Turn editing on" + When I follow "faa1b" + Then I should not see "Add a new discussion topic" + When I am on "courseaa2" course homepage + Then I should see "Turn editing on" + When I follow "faa2" + Then I should see "Add a new discussion topic" + When I am on "courseb" course homepage + Then I should see "Turn editing on" + When I follow "fb" + Then I should see "Add a new discussion topic" + + And I log out + When I log in as "student1" + And I am on "courseaa1" course homepage + And I follow "faa1" + Then I should not see "Add a new discussion topic" + When I am on "courseaa1" course homepage + When I follow "faa1b" + Then I should not see "Add a new discussion topic" + When I am on "courseaa2" course homepage + When I follow "faa2" + Then I should see "Add a new discussion topic" + When I am on "courseb" course homepage + When I follow "fb" + Then I should see "Add a new discussion topic" + + Scenario: Lock course category should lock all children + Given I log in as "admin" + And I go to the courses management page + And I click on "managecontextlock" action for "cata" in management category listing + And I click on "Continue" "button" + And I am on "courseaa1" course homepage + And I should not see "Turn editing on" + Then I should not see "Add a new discussion topic" + When I am on "courseaa1" course homepage + Then I should not see "Turn editing on" + And I should not see "Unlock this context" + When I follow "faa1b" + Then I should not see "Add a new discussion topic" + And I should not see "Unlock this context" + When I am on "courseaa2" course homepage + Then I should not see "Turn editing on" + When I follow "faa2" + Then I should not see "Add a new discussion topic" + And I should not see "Unlock this context" + When I am on "courseb" course homepage + Then I should see "Turn editing on" + When I follow "fb" + Then I should see "Add a new discussion topic" + + And I log out + When I log in as "teacher" + And I am on "courseaa1" course homepage + Then I should not see "Turn editing on" + And I follow "faa1" + Then I should not see "Add a new discussion topic" + When I am on "courseaa1" course homepage + Then I should not see "Turn editing on" + When I follow "faa1b" + Then I should not see "Add a new discussion topic" + When I am on "courseaa2" course homepage + Then I should not see "Turn editing on" + When I follow "faa2" + Then I should not see "Add a new discussion topic" + When I am on "courseb" course homepage + Then I should see "Turn editing on" + When I follow "fb" + Then I should see "Add a new discussion topic" + + And I log out + When I log in as "student1" + And I am on "courseaa1" course homepage + And I follow "faa1" + Then I should not see "Add a new discussion topic" + When I am on "courseaa1" course homepage + When I follow "faa1b" + Then I should not see "Add a new discussion topic" + When I am on "courseaa2" course homepage + When I follow "faa2" + Then I should not see "Add a new discussion topic" + When I am on "courseb" course homepage + When I follow "fb" + Then I should see "Add a new discussion topic" 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 c0f18c8be0f..023fab1f398 100644 --- a/lib/upgrade.txt +++ b/lib/upgrade.txt @@ -166,6 +166,7 @@ the groupid field. 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. * Webservices no longer update the lastaccess time for a user in a course. Call core_course_view_course() manually if needed. +* 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/mod/forum/db/access.php b/mod/forum/db/access.php index 1dce7d453b3..e1c4f4e9c5d 100644 --- a/mod/forum/db/access.php +++ b/mod/forum/db/access.php @@ -181,7 +181,7 @@ $capabilities = array( 'mod/forum:deleteownpost' => array( - 'captype' => 'read', + 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'student' => CAP_ALLOW, @@ -193,7 +193,7 @@ $capabilities = array( 'mod/forum:deleteanypost' => array( - 'captype' => 'read', + 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, @@ -204,7 +204,7 @@ $capabilities = array( 'mod/forum:splitdiscussions' => array( - 'captype' => 'read', + 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, @@ -215,7 +215,7 @@ $capabilities = array( 'mod/forum:movediscussions' => array( - 'captype' => 'read', + 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, @@ -274,7 +274,7 @@ $capabilities = array( 'riskbitmask' => RISK_SPAM, - 'captype' => 'read', + 'captype' => 'write', 'contextlevel' => CONTEXT_MODULE, 'archetypes' => array( 'teacher' => CAP_ALLOW, diff --git a/mod/forum/version.php b/mod/forum/version.php index 7740358f5ff..45c53ce39ed 100644 --- a/mod/forum/version.php +++ b/mod/forum/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2018051400; // The current module version (Date: YYYYMMDDXX) +$plugin->version = 2018051401; // The current module version (Date: YYYYMMDDXX) $plugin->requires = 2018050800; // Requires this Moodle version $plugin->component = 'mod_forum'; // Full name of the plugin (used for diagnostics) diff --git a/version.php b/version.php index 6a8ab5b3d7b..93ff0a3fd83 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2018111300.00; // YYYYMMDD = weekly release date of this DEV branch. +$version = 2018111301.00; // YYYYMMDD = weekly release date of this DEV branch. // RR = release increments - 00 in DEV branches. // .XX = incremental changes.