MDL-53566 core: Add support for context locking
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 7 Jun 2018 06:35:26 +0000 (14:35 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 13 Nov 2018 13:17:40 +0000 (21:17 +0800)
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.

16 files changed:
cohort/lib.php
course/classes/category.php
lang/en/role.php
lib/accesslib.php
lib/classes/user.php
lib/db/access.php
lib/db/install.xml
lib/db/upgrade.php
lib/filebrowser/file_info_context_course.php
lib/phpunit/classes/advanced_testcase.php
lib/setuplib.php
lib/tests/accesslib_has_capability_test.php [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/session_manager_test.php
lib/upgrade.txt
version.php

index a76246e..e5a991a 100644 (file)
@@ -634,4 +634,4 @@ function cohort_get_list_of_themes() {
         }
     }
     return $themes;
-}
\ No newline at end of file
+}
index bf4bfd6..2880675 100644 (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);
     }
 
index d0d2003..ab77889 100644 (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';
index ae631cf..c5a13fe 100644 (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;
-         }
-
-         // 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)
index cc1a1df..09a8053 100644 (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];
         }
index 7ad389c..60b3659 100644 (file)
@@ -2432,4 +2432,11 @@ $capabilities = array(
         )
     ),
 
+    // Context locking/unlocking.
+    'moodle/site:managecontextlocks' => [
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_SYSTEM,
+        'archetypes' => [
+        ],
+    ],
 );
index 7fd383d..f2d08fe 100644 (file)
         <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"/>
         <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"/>
index 9b9fb47..6c2d6fb 100644 (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;
 }
index 4c8b797..74798f0 100644 (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))) {
index 546d11b..e18fddf 100644 (file)
@@ -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();
index 7a07e1d..1d5dd62 100644 (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;
diff --git a/lib/tests/accesslib_has_capability_test.php b/lib/tests/accesslib_has_capability_test.php
new file mode 100644 (file)
index 0000000..ccf6d44
--- /dev/null
@@ -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',
+                ],
+            ],
+        ];
+    }
+}
index a228754..5f3eab4 100644 (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);
     }
index a5a15d9..b3b8c9d 100644 (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);
index 6bea9f1..0b896f0 100644 (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 ===
 
index ebf9aea..a1b2728 100644 (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.