Merge branch 'MDL-61990' of https://github.com/timhunt/moodle
authorDavid Monllao <davidm@moodle.com>
Wed, 14 Nov 2018 11:09:34 +0000 (12:09 +0100)
committerDavid Monllao <davidm@moodle.com>
Wed, 14 Nov 2018 11:09:34 +0000 (12:09 +0100)
45 files changed:
admin/lock.php [new file with mode: 0644]
admin/settings/development.php
admin/tool/monitor/lib.php
badges/endorsement.php
badges/renderer.php
blocks/recentlyaccesseditems/classes/privacy/provider.php
blocks/recentlyaccesseditems/tests/privacy_test.php [new file with mode: 0644]
cohort/lib.php
course/classes/category.php
course/classes/management/helper.php
grade/edit/tree/index.php
grade/export/lib.php
grade/export/ods/grade_export_ods.php
grade/export/txt/grade_export_txt.php
grade/export/xls/grade_export_xls.php
grade/export/xml/grade_export_xml.php
grade/tests/export_test.php [new file with mode: 0644]
lang/en/admin.php
lang/en/message.php
lang/en/role.php
lib/accesslib.php
lib/blocklib.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/navigationlib.php
lib/phpunit/classes/advanced_testcase.php
lib/setuplib.php
lib/tests/accesslib_has_capability_test.php [new file with mode: 0644]
lib/tests/behat/locking.feature [new file with mode: 0644]
lib/tests/moodlelib_test.php
lib/tests/session_manager_test.php
lib/upgrade.txt
message/externallib.php
message/index.php
mod/forum/db/access.php
mod/forum/version.php
theme/boost/scss/moodle/blocks.scss
theme/boost/style/moodle.css
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/style/moodle.css
theme/bootstrapbase/templates/core_calendar/minicalendar_day_link.mustache
version.php

diff --git a/admin/lock.php b/admin/lock.php
new file mode 100644 (file)
index 0000000..3041ded
--- /dev/null
@@ -0,0 +1,96 @@
+<?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/>.
+
+/**
+ * This file is used to display a categories sub categories, external pages, and settings.
+ *
+ * @package    admin
+ * @copyright  2018 Andrew Nicols <andrew@nicols.co.uk>
+ * @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();
index cc2a620..61d82c3 100644 (file)
@@ -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));
 
index c7493ad..58078ec 100644 (file)
@@ -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);
index ec3e145..d7b2345 100644 (file)
@@ -63,6 +63,10 @@ $output = $PAGE->get_renderer('core', 'badges');
 $msg = optional_param('msg', '', PARAM_TEXT);
 $emsg = optional_param('emsg', '', PARAM_TEXT);
 
+if ($msg !== '') {
+    $msg = get_string($msg, 'badges');
+}
+
 echo $OUTPUT->header();
 echo $OUTPUT->heading(print_badge_image($badge, $context, 'small') . ' ' . $badge->name);
 
index 5810e57..d7c31e1 100644 (file)
@@ -150,8 +150,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $dl[get_string('description', 'badges')] = $badge->description;
         $dl[get_string('createdon', 'search')] = userdate($badge->timecreated);
         $dl[get_string('badgeimage', 'badges')] = print_badge_image($badge, $context, 'large');
-        $dl[get_string('imageauthorname', 'badges')] =
-            html_writer::link($badge->imageauthorname, $badge->imageauthorname, array('target' => '_blank'));
+        $dl[get_string('imageauthorname', 'badges')] = $badge->imageauthorname;
         $dl[get_string('imageauthoremail', 'badges')] =
             html_writer::tag('a', $badge->imageauthoremail, array('href' => 'mailto:' . $badge->imageauthoremail));
         $dl[get_string('imageauthorurl', 'badges')] =
@@ -352,8 +351,7 @@ class core_badges_renderer extends plugin_renderer_base {
         $dl[get_string('version', 'badges')] = $badge->version;
         $dl[get_string('language')] = $languages[$badge->language];
         $dl[get_string('description', 'badges')] = $badge->description;
-        $dl[get_string('imageauthorname', 'badges')] =
-            html_writer::link($badge->imageauthorname, $badge->imageauthorname, array('target' => '_blank'));
+        $dl[get_string('imageauthorname', 'badges')] = $badge->imageauthorname;
         $dl[get_string('imageauthoremail', 'badges')] =
             html_writer::tag('a', $badge->imageauthoremail, array('href' => 'mailto:' . $badge->imageauthoremail));
         $dl[get_string('imageauthorurl', 'badges')] =
@@ -1048,7 +1046,8 @@ class core_badges_renderer extends plugin_renderer_base {
         $output .= $this->heading(get_string('endorsement', 'badges'), 3);
         if (!empty($endorsement)) {
             $dl[get_string('issuername', 'badges')] = $endorsement->issuername;
-            $dl[get_string('issueremail', 'badges')] = $endorsement->issueremail;
+            $dl[get_string('issueremail', 'badges')] =
+                html_writer::tag('a', $endorsement->issueremail, array('href' => 'mailto:' . $endorsement->issueremail));
             $dl[get_string('issuerurl', 'badges')] = html_writer::link($endorsement->issuerurl, $endorsement->issuerurl,
                 array('target' => '_blank'));
             $dl[get_string('dateawarded', 'badges')] = date('c', $endorsement->dateissued);
index f56589b..7eb7235 100644 (file)
@@ -29,7 +29,9 @@ defined('MOODLE_INTERNAL') || die();
 use \core_privacy\local\metadata\collection;
 use \core_privacy\local\request\transform;
 use \core_privacy\local\request\contextlist;
+use \core_privacy\local\request\userlist;
 use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\approved_userlist;
 use \core_privacy\local\request\writer;
 
 /**
@@ -39,7 +41,10 @@ use \core_privacy\local\request\writer;
  * @copyright  2018 Victor Deniz <victor@moodle.com>
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-class provider implements \core_privacy\local\metadata\provider, \core_privacy\local\request\subsystem\provider {
+class provider implements
+        \core_privacy\local\metadata\provider,
+        \core_privacy\local\request\core_userlist_provider,
+        \core_privacy\local\request\plugin\provider {
 
     /**
      * Returns information about the user data stored in this component.
@@ -69,14 +74,36 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
      */
     public static function get_contexts_for_userid(int $userid) : contextlist {
         $params = ['userid' => $userid, 'contextuser' => CONTEXT_USER];
-        $sql = "SELECT id
-                  FROM {context}
-                 WHERE instanceid = :userid and contextlevel = :contextuser";
+        $sql = "SELECT c.id
+                  FROM {context} c
+                  JOIN {block_recentlyaccesseditems} b
+                    ON b.userid = c.instanceid
+                 WHERE c.instanceid = :userid
+                   AND c.contextlevel = :contextuser";
         $contextlist = new contextlist();
         $contextlist->add_from_sql($sql, $params);
         return $contextlist;
     }
 
+    /**
+     * Get the list of users within a specific context.
+     *
+     * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
+     */
+    public static function get_users_in_context(userlist $userlist) {
+        global $DB;
+
+        $context = $userlist->get_context();
+
+        if (!$context instanceof \context_user) {
+            return;
+        }
+
+        if ($DB->record_exists('block_recentlyaccesseditems', ['userid' => $context->instanceid])) {
+            $userlist->add_user($context->instanceid);
+        }
+    }
+
     /**
      * Export all user data for the specified user, in the specified contexts.
      *
@@ -148,4 +175,19 @@ class provider implements \core_privacy\local\metadata\provider, \core_privacy\l
             }
         }
     }
-}
\ No newline at end of file
+
+    /**
+     * Delete multiple users within a single context.
+     *
+     * @param approved_userlist $userlist The approved context and user information to delete information for.
+     */
+    public static function delete_data_for_users(approved_userlist $userlist) {
+        global $DB;
+
+        $context = $userlist->get_context();
+
+        if ($context instanceof \context_user && in_array($context->instanceid, $userlist->get_userids())) {
+            $DB->delete_records('block_recentlyaccesseditems', ['userid' => $context->instanceid]);
+        }
+    }
+}
diff --git a/blocks/recentlyaccesseditems/tests/privacy_test.php b/blocks/recentlyaccesseditems/tests/privacy_test.php
new file mode 100644 (file)
index 0000000..8c5ab69
--- /dev/null
@@ -0,0 +1,438 @@
+<?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/>.
+
+/**
+ * Block recentlyaccesseditems privacy provider tests.
+ *
+ * @package    block_recentlyaccesseditems
+ * @copyright  2018 Michael Hawkins <michaelh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.6
+ */
+defined('MOODLE_INTERNAL') || die();
+
+use \block_recentlyaccesseditems\privacy\provider;
+use \core_privacy\local\request\approved_contextlist;
+use \core_privacy\local\request\approved_userlist;
+
+/**
+ * Block Recently accessed items privacy provider tests.
+ *
+ * @package    block_recentlyaccesseditems
+ * @copyright  2018 Michael Hawkins <michaelh@moodle.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @since      Moodle 3.6
+ */
+class block_recentlyaccesseditems_privacy_testcase extends \core_privacy\tests\provider_testcase {
+
+    /**
+     * Test getting the context for the user ID related to this plugin.
+     */
+    public function test_get_contexts_for_userid() {
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+
+        $student = $generator->create_user();
+        $studentcontext = context_user::instance($student->id);
+        $teacher = $generator->create_user();
+        $teachercontext = context_user::instance($teacher->id);
+
+        // Enrol users in course and add course items.
+        $course = $generator->create_course();
+        $generator->enrol_user($student->id, $course->id, 'student');
+        $generator->enrol_user($teacher->id, $course->id, 'teacher');
+        $forum = $generator->create_module('forum', ['course' => $course]);
+        $chat = $generator->create_module('chat', ['course' => $course]);
+
+        // Check nothing is found before block is populated.
+        $contextlist1 = provider::get_contexts_for_userid($student->id);
+        $this->assertCount(0, $contextlist1);
+        $contextlist2 = provider::get_contexts_for_userid($teacher->id);
+        $this->assertCount(0, $contextlist2);
+
+        // Generate some recent activity for both users.
+        $this->setUser($student);
+        $event = \mod_forum\event\course_module_viewed::create(['context' => context_module::instance($forum->cmid),
+                    'objectid' => $forum->id]);
+        $event->trigger();
+
+        $this->setUser($teacher);
+        $event = \mod_chat\event\course_module_viewed::create(['context' => context_module::instance($chat->cmid),
+                    'objectid' => $chat->id]);
+        $event->trigger();
+
+        // Ensure provider only fetches the users's own context.
+        $contextlist1 = provider::get_contexts_for_userid($student->id);
+        $this->assertCount(1, $contextlist1);
+        $this->assertEquals($studentcontext, $contextlist1->current());
+
+        $contextlist2 = provider::get_contexts_for_userid($teacher->id);
+        $this->assertCount(1, $contextlist2);
+        $this->assertEquals($teachercontext, $contextlist2->current());
+    }
+
+    /**
+     * Test getting users in the context ID related to this plugin.
+     */
+    public function test_get_users_in_context() {
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+        $component = 'block_recentlyaccesseditems';
+
+        $student = $generator->create_user();
+        $studentcontext = context_user::instance($student->id);
+        $teacher = $generator->create_user();
+        $teachercontext = context_user::instance($teacher->id);
+
+        // Enrol users in course and add course items.
+        $course = $generator->create_course();
+        $generator->enrol_user($student->id, $course->id, 'student');
+        $generator->enrol_user($teacher->id, $course->id, 'teacher');
+        $forum = $generator->create_module('forum', ['course' => $course]);
+        $chat = $generator->create_module('chat', ['course' => $course]);
+
+        // Check nothing is found before block is populated.
+        $userlist1 = new \core_privacy\local\request\userlist($studentcontext, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(0, $userlist1);
+        $userlist2 = new \core_privacy\local\request\userlist($teachercontext, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(0, $userlist2);
+
+        // Generate some recent activity for both users.
+        $this->setUser($student);
+        $event = \mod_forum\event\course_module_viewed::create(['context' => context_module::instance($forum->cmid),
+                    'objectid' => $forum->id]);
+        $event->trigger();
+        $event = \mod_chat\event\course_module_viewed::create(['context' => context_module::instance($chat->cmid),
+                    'objectid' => $chat->id]);
+        $event->trigger();
+
+        $this->setUser($teacher);
+        $event = \mod_forum\event\course_module_viewed::create(['context' => context_module::instance($forum->cmid),
+                    'objectid' => $forum->id]);
+        $event->trigger();
+        $event = \mod_chat\event\course_module_viewed::create(['context' => context_module::instance($chat->cmid),
+                    'objectid' => $chat->id]);
+        $event->trigger();
+
+        // Ensure provider only fetches the user whose user context is checked.
+        $userlist1 = new \core_privacy\local\request\userlist($studentcontext, $component);
+        provider::get_users_in_context($userlist1);
+        $this->assertCount(1, $userlist1);
+        $this->assertEquals($student, $userlist1->current());
+
+        $userlist2 = new \core_privacy\local\request\userlist($teachercontext, $component);
+        provider::get_users_in_context($userlist2);
+        $this->assertCount(1, $userlist2);
+        $this->assertEquals($teacher, $userlist2->current());
+    }
+
+    /**
+     * Test fetching information about user data stored.
+     */
+    public function test_get_metadata() {
+        $collection = new \core_privacy\local\metadata\collection('block_recentlyaccesseditems');
+        $newcollection = provider::get_metadata($collection);
+        $itemcollection = $newcollection->get_collection();
+        $this->assertCount(1, $itemcollection);
+
+        $table = reset($itemcollection);
+        $this->assertEquals('block_recentlyaccesseditems', $table->get_name());
+
+        $privacyfields = $table->get_privacy_fields();
+        $this->assertCount(4, $privacyfields);
+        $this->assertArrayHasKey('userid', $privacyfields);
+        $this->assertArrayHasKey('courseid', $privacyfields);
+        $this->assertArrayHasKey('cmid', $privacyfields);
+        $this->assertArrayHasKey('timeaccess', $privacyfields);
+
+        $this->assertEquals('privacy:metadata:block_recentlyaccesseditemstablesummary', $table->get_summary());
+    }
+
+    /**
+     * Test exporting data for an approved contextlist.
+     */
+    public function test_export_user_data() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+        $component = 'block_recentlyaccesseditems';
+
+        $student = $generator->create_user();
+        $studentcontext = context_user::instance($student->id);
+
+        // Enrol user in course and add course items.
+        $course = $generator->create_course();
+        $generator->enrol_user($student->id, $course->id, 'student');
+        $forum = $generator->create_module('forum', ['course' => $course]);
+        $chat = $generator->create_module('chat', ['course' => $course]);
+
+        // Generate some recent activity.
+        $this->setUser($student);
+        $event = \mod_forum\event\course_module_viewed::create(['context' => context_module::instance($forum->cmid),
+                'objectid' => $forum->id]);
+        $event->trigger();
+        $event = \mod_chat\event\course_module_viewed::create(['context' => context_module::instance($chat->cmid),
+                'objectid' => $chat->id]);
+        $event->trigger();
+
+        // Confirm data is present.
+        $params = [
+            'courseid' => $course->id,
+            'userid' => $student->id,
+        ];
+
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+
+        // Export data for student.
+        $approvedlist = new approved_contextlist($student, $component, [$studentcontext->id]);
+        provider::export_user_data($approvedlist);
+
+        // Confirm student's data is exported.
+        $writer = \core_privacy\local\request\writer::with_context($studentcontext);
+        $this->assertTrue($writer->has_any_data());
+    }
+
+    /**
+     * Test deleting data for all users within an approved contextlist.
+     */
+    public function test_delete_data_for_all_users_in_context() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+
+        $student = $generator->create_user();
+        $studentcontext = context_user::instance($student->id);
+        $teacher = $generator->create_user();
+
+        // Enrol users in course and add course items.
+        $course = $generator->create_course();
+        $generator->enrol_user($student->id, $course->id, 'student');
+        $generator->enrol_user($teacher->id, $course->id, 'teacher');
+        $forum = $generator->create_module('forum', ['course' => $course]);
+        $chat = $generator->create_module('chat', ['course' => $course]);
+
+        // Generate some recent activity for both users.
+        $users = [$student, $teacher];
+        foreach ($users as $user) {
+            $this->setUser($user);
+            $event = \mod_forum\event\course_module_viewed::create(['context' => context_module::instance($forum->cmid),
+                        'objectid' => $forum->id]);
+            $event->trigger();
+            $event = \mod_chat\event\course_module_viewed::create(['context' => context_module::instance($chat->cmid),
+                        'objectid' => $chat->id]);
+            $event->trigger();
+        }
+
+        // Confirm data is present for both users.
+        $params = [
+            'courseid' => $course->id,
+            'userid' => $student->id,
+        ];
+
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+        $params['userid'] = $teacher->id;
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+
+        // Attempt system context deletion (should have no effect).
+        $systemcontext = context_system::instance();
+        provider::delete_data_for_all_users_in_context($systemcontext);
+
+        $params = ['courseid' => $course->id];
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(4, $result);
+
+        // Delete all data in student context.
+        provider::delete_data_for_all_users_in_context($studentcontext);
+
+        // Confirm only student data is deleted.
+        $params = [
+            'courseid' => $course->id,
+            'userid' => $student->id,
+        ];
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(0, $result);
+
+        $params['userid'] = $teacher->id;
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+    }
+
+    /**
+     * Test deleting data within an approved contextlist for a user.
+     */
+    public function test_delete_data_for_user() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+        $component = 'block_recentlyaccesseditems';
+
+        $student = $generator->create_user();
+        $studentcontext = context_user::instance($student->id);
+        $teacher = $generator->create_user();
+        $teachercontext = context_user::instance($teacher->id);
+
+        // Enrol users in course and add course items.
+        $course = $generator->create_course();
+        $generator->enrol_user($student->id, $course->id, 'student');
+        $generator->enrol_user($teacher->id, $course->id, 'teacher');
+        $forum = $generator->create_module('forum', ['course' => $course]);
+        $chat = $generator->create_module('chat', ['course' => $course]);
+
+        // Generate some recent activity for both users.
+        $users = [$student, $teacher];
+        foreach ($users as $user) {
+            $this->setUser($user);
+            $event = \mod_forum\event\course_module_viewed::create(['context' => context_module::instance($forum->cmid),
+                        'objectid' => $forum->id]);
+            $event->trigger();
+            $event = \mod_chat\event\course_module_viewed::create(['context' => context_module::instance($chat->cmid),
+                        'objectid' => $chat->id]);
+            $event->trigger();
+        }
+
+        // Confirm data is present for both users.
+        $params = [
+            'courseid' => $course->id,
+            'userid' => $student->id,
+        ];
+
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+        $params['userid'] = $teacher->id;
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+
+        // Attempt system context deletion (should have no effect).
+        $systemcontext = context_system::instance();
+        $approvedlist = new approved_contextlist($teacher, $component, [$systemcontext->id]);
+        provider::delete_data_for_user($approvedlist);
+
+        $params = ['courseid' => $course->id];
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(4, $result);
+
+        // Attempt to delete teacher data in student user context (should have no effect).
+        $approvedlist = new approved_contextlist($teacher, $component, [$studentcontext->id]);
+        provider::delete_data_for_user($approvedlist);
+
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(4, $result);
+
+        // Delete teacher data in their own user context.
+        $approvedlist = new approved_contextlist($teacher, $component, [$teachercontext->id]);
+        provider::delete_data_for_user($approvedlist);
+
+        // Confirm only teacher data is deleted.
+        $params = [
+            'courseid' => $course->id,
+            'userid' => $student->id,
+        ];
+
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+
+        $params['userid'] = $teacher->id;
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(0, $result);
+    }
+
+    /**
+     * Test deleting data within a context for an approved userlist.
+     */
+    public function test_delete_data_for_users() {
+        global $DB;
+
+        $this->resetAfterTest();
+        $generator = $this->getDataGenerator();
+        $component = 'block_recentlyaccesseditems';
+
+        $student = $generator->create_user();
+        $studentcontext = context_user::instance($student->id);
+        $teacher = $generator->create_user();
+        $teachercontext = context_user::instance($teacher->id);
+
+        // Enrol users in course and add course items.
+        $course = $generator->create_course();
+        $generator->enrol_user($student->id, $course->id, 'student');
+        $generator->enrol_user($teacher->id, $course->id, 'teacher');
+        $forum = $generator->create_module('forum', ['course' => $course]);
+        $chat = $generator->create_module('chat', ['course' => $course]);
+
+        // Generate some recent activity for all users.
+        $users = [$student, $teacher];
+        foreach ($users as $user) {
+            $this->setUser($user);
+            $event = \mod_forum\event\course_module_viewed::create(['context' => context_module::instance($forum->cmid),
+                        'objectid' => $forum->id]);
+            $event->trigger();
+            $event = \mod_chat\event\course_module_viewed::create(['context' => context_module::instance($chat->cmid),
+                        'objectid' => $chat->id]);
+            $event->trigger();
+        }
+
+        // Confirm data is present for all 3 users.
+        $params = [
+            'courseid' => $course->id,
+            'userid' => $student->id,
+        ];
+
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+        $params['userid'] = $teacher->id;
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+
+        // Attempt system context deletion (should have no effect).
+        $systemcontext = context_system::instance();
+        $approvedlist = new approved_userlist($systemcontext, $component, [$student->id, $teacher->id]);
+        provider::delete_data_for_users($approvedlist);
+
+        $params = ['courseid' => $course->id];
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(4, $result);
+
+        // Attempt to delete data in another user's context (should have no effect).
+        $approvedlist = new approved_userlist($studentcontext, $component, [$teacher->id]);
+        provider::delete_data_for_users($approvedlist);
+
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(4, $result);
+
+        // Delete users' data in teacher's context.
+        $approvedlist = new approved_userlist($teachercontext, $component, [$student->id, $teacher->id]);
+        provider::delete_data_for_users($approvedlist);
+
+        // Confirm only teacher data is deleted.
+        $params = [
+            'courseid' => $course->id,
+            'userid' => $student->id,
+        ];
+
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(2, $result);
+        $params['userid'] = $teacher->id;
+        $result = $DB->count_records('block_recentlyaccesseditems', $params);
+        $this->assertEquals(0, $result);
+    }
+}
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 2374883..0befd64 100644 (file)
@@ -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(
index b898b99..aa58497 100644 (file)
@@ -22,6 +22,8 @@
  * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
 
+define('NO_OUTPUT_BUFFERING', true); // The progress bar may be used here.
+
 require_once '../../../config.php';
 require_once $CFG->dirroot.'/grade/lib.php';
 require_once $CFG->dirroot.'/grade/report/lib.php'; // for preferences
index 6dabe7a..c05d31a 100644 (file)
@@ -282,10 +282,24 @@ abstract class grade_export {
     /**
      * Returns formatted grade feedback
      * @param object $feedback object with properties feedback and feedbackformat
+     * @param object $grade Grade object with grade properties
      * @return string
      */
-    public function format_feedback($feedback) {
-        return strip_tags(format_text($feedback->feedback, $feedback->feedbackformat));
+    public function format_feedback($feedback, $grade = null) {
+        $string = $feedback->feedback;
+        if (!empty($grade)) {
+            // Rewrite links to get the export working for 36, refer MDL-63488.
+            $string = file_rewrite_pluginfile_urls(
+                $feedback->feedback,
+                'pluginfile.php',
+                $grade->get_context()->id,
+                GRADE_FILE_COMPONENT,
+                GRADE_FEEDBACK_FILEAREA,
+                $grade->id
+            );
+        }
+
+        return strip_tags(format_text($string, $feedback->feedbackformat));
     }
 
     /**
index 5454456..40bbd90 100644 (file)
@@ -116,7 +116,7 @@ class grade_export_ods extends grade_export {
 
                 // writing feedback if requested
                 if ($this->export_feedback) {
-                    $myxls->write_string($i, $j++, $this->format_feedback($userdata->feedbacks[$itemid]));
+                    $myxls->write_string($i, $j++, $this->format_feedback($userdata->feedbacks[$itemid], $grade));
                 }
             }
             // Time exported.
index 7ec6aa4..e262d93 100644 (file)
@@ -109,7 +109,7 @@ class grade_export_txt extends grade_export {
                 }
 
                 if ($this->export_feedback) {
-                    $exportdata[] = $this->format_feedback($userdata->feedbacks[$itemid]);
+                    $exportdata[] = $this->format_feedback($userdata->feedbacks[$itemid], $grade);
                 }
             }
             // Time exported.
index 83e720d..f81b0e2 100644 (file)
@@ -110,7 +110,7 @@ class grade_export_xls extends grade_export {
                 }
                 // writing feedback if requested
                 if ($this->export_feedback) {
-                    $myxls->write_string($i, $j++, $this->format_feedback($userdata->feedbacks[$itemid]));
+                    $myxls->write_string($i, $j++, $this->format_feedback($userdata->feedbacks[$itemid], $grade));
                 }
             }
             // Time exported.
index c91122b..659fa6b 100644 (file)
@@ -101,7 +101,7 @@ class grade_export_xml extends grade_export {
                 }
 
                 if ($this->export_feedback) {
-                    $feedbackstr = $this->format_feedback($userdata->feedbacks[$itemid]);
+                    $feedbackstr = $this->format_feedback($userdata->feedbacks[$itemid], $grade);
                     fwrite($handle,  "\t\t<feedback>$feedbackstr</feedback>\n");
                 }
                 fwrite($handle,  "\t</result>\n");
diff --git a/grade/tests/export_test.php b/grade/tests/export_test.php
new file mode 100644 (file)
index 0000000..b2cc79a
--- /dev/null
@@ -0,0 +1,173 @@
+<?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/>.
+
+/**
+ * Unit tests for grade/report/lib.php.
+ *
+ * @package  core_grades
+ * @category phpunit
+ * @copyright   Andrew Nicols <andrew@nicols.co.uk>
+ * @license  http://www.gnu.org/copyleft/gpl.html GNU Public License
+ */
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+require_once($CFG->dirroot.'/grade/lib.php');
+require_once($CFG->dirroot.'/grade/export/lib.php');
+
+/**
+ * A test class used to test grade_report, the abstract grade report parent class
+ */
+class core_grade_export_test extends advanced_testcase {
+
+    /**
+     * Ensure that feedback is correct formatted. Test the default implementation of format_feedback
+     *
+     * @dataProvider    format_feedback_provider
+     * @param   string  $input The input string to test
+     * @param   int     $inputformat The format of the input string
+     * @param   string  $expected The expected result of the format.
+     */
+    public function test_format_feedback($input, $inputformat, $expected) {
+        $feedback = $this->getMockForAbstractClass(
+                \grade_export::class,
+                [],
+                '',
+                false
+            );
+
+        $this->assertEquals(
+            $expected,
+            $feedback->format_feedback((object) [
+                    'feedback' => $input,
+                    'feedbackformat' => $inputformat,
+                ])
+            );
+    }
+
+    /**
+     * Ensure that feedback is correctly formatted. Test augmented functionality to handle file links
+     */
+    public function test_format_feedback_with_grade() {
+        $this->resetAfterTest();
+        $dg = $this->getDataGenerator();
+        $c1 = $dg->create_course();
+        $u1 = $dg->create_user();
+        $gi1a = new grade_item($dg->create_grade_item(['courseid' => $c1->id]), false);
+        $gi1a->update_final_grade($u1->id, 1, 'test');
+        $contextid = $gi1a->get_context()->id;
+        $gradeid = $gi1a->id;
+
+        $tests = [
+            'Has server based image (HTML)' => [
+                '<p>See this reference: <img src="@@PLUGINFILE@@/test.img"></p>',
+                FORMAT_HTML,
+                "See this reference: "
+            ],
+            'Has server based image and more (HTML)' => [
+                '<p>See <img src="@@PLUGINFILE@@/test.img"> for <em>reference</em></p>',
+                FORMAT_HTML,
+                "See  for reference"
+            ],
+            'Has server based video and more (HTML)' => [
+                '<p>See <video src="@@PLUGINFILE@@/test.img">video of a duck</video> for <em>reference</em></p>',
+                FORMAT_HTML,
+                'See video of a duck for reference'
+            ],
+            'Has server based video with text and more (HTML)' => [
+                '<p>See <video src="@@PLUGINFILE@@/test.img">@@PLUGINFILE@@/test.img</video> for <em>reference</em></p>',
+                FORMAT_HTML,
+                "See https://www.example.com/moodle/pluginfile.php/$contextid/grade/feedback/$gradeid/test.img for reference"
+            ],
+            'Multiple videos (HTML)' => [
+                '<p>See <video src="@@PLUGINFILE@@/test.img">video of a duck</video> and '.
+                '<video src="http://example.com/myimage.jpg">video of a cat</video> for <em>reference</em></p>',
+                FORMAT_HTML,
+                'See video of a duck and video of a cat for reference'
+            ],
+        ];
+
+        $feedback = $this->getMockForAbstractClass(
+            \grade_export::class,
+            [],
+            '',
+            false
+        );
+
+        foreach ($tests as $key => $testdetails) {
+            $expected = $testdetails[2];
+            $input = $testdetails[0];
+            $inputformat = $testdetails[1];
+
+            $this->assertEquals(
+                $expected,
+                $feedback->format_feedback((object) [
+                    'feedback' => $input,
+                    'feedbackformat' => $inputformat,
+                ], $gi1a),
+                $key
+            );
+        }
+    }
+
+    /**
+     * Data provider for the format_feedback tests.
+     *
+     * @return  array
+     */
+    public function format_feedback_provider() : array {
+        return [
+            'Basic string (PLAIN)' => [
+                'This is an example string',
+                FORMAT_PLAIN,
+                'This is an example string',
+            ],
+            'Basic string (HTML)' => [
+                '<p>This is an example string</p>',
+                FORMAT_HTML,
+                'This is an example string',
+            ],
+            'Has image (HTML)' => [
+                '<p>See this reference: <img src="http://example.com/myimage.jpg"></p>',
+                FORMAT_HTML,
+                'See this reference: ',
+            ],
+            'Has image and more (HTML)' => [
+                '<p>See <img src="http://example.com/myimage.jpg"> for <em>reference</em></p>',
+                FORMAT_HTML,
+                'See  for reference',
+            ],
+            'Has video and more (HTML)' => [
+                '<p>See <video src="http://example.com/myimage.jpg">video of a duck</video> for <em>reference</em></p>',
+                FORMAT_HTML,
+                'See video of a duck for reference',
+            ],
+            'Multiple videos (HTML)' => [
+                '<p>See <video src="http://example.com/myimage.jpg">video of a duck</video> and '.
+                '<video src="http://example.com/myimage.jpg">video of a cat</video> for <em>reference</em></p>',
+                FORMAT_HTML,
+                'See video of a duck and video of a cat for reference'
+            ],
+            'HTML Looking tags in PLAIN' => [
+                'The way you have written the <img thing looks pretty fun >',
+                FORMAT_PLAIN,
+                'The way you have written the &lt;img thing looks pretty fun &gt;',
+            ],
+
+        ];
+    }
+}
index 11d79f3..e2f1ecd 100644 (file)
@@ -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 unfrozen. Freezing it will prevent any further changes. Are you sure you wish to continue?';
+$string['confirmcontextunlock'] = '{$a->contextname} is currently frozen. Unfreezing 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 freezing';
+$string['contextlocking_desc'] = 'This setting allows you to freeze categories, courses, activites, and blocks within the site by removing all write-access to those locations.';
+$string['contextlockappliestoadmin'] = 'Context freezing applies to administrators';
+$string['contextlockappliestoadmin_desc'] = 'This setting allows administrators to make changes in any context which is frozen.';
 $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'] = 'Freeze this context';
+$string['managecontextlocklocked'] = '{$a->contextname}, and all of its children are now frozen.';
+$string['managecontextlockunlocked'] = '{$a->contextname}, and all of its children are now unfrozen.';
+$string['managecontextunlock'] = 'Unfreeze this context';
 $string['manageformats'] = 'Manage course formats';
 $string['manageformatsgotosettings'] = 'Default format can be changed in {$a}';
 $string['managelang'] = 'Manage';
index 670c6c5..1c1f5ba 100644 (file)
@@ -38,7 +38,6 @@ $string['contactableprivacy_site'] = 'Anyone on the site';
 $string['contactblocked'] = 'Contact blocked';
 $string['contactrequests'] = 'Contact requests';
 $string['contacts'] = 'Contacts';
-$string['conversationdoesntexist'] = 'Conversation does not exist';
 $string['defaultmessageoutputs'] = 'Default message outputs';
 $string['defaults'] = 'Defaults';
 $string['deleteallconfirm'] = "Are you sure you would like to delete this entire conversation?";
index d0d2003..743bc48 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 freezing 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';
index ae631cf..9d6dc4f 100644 (file)
@@ -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;
-         }
-
-         // 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 +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)
index feaefb3..18172c2 100644 (file)
@@ -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;
     }
 
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..9ae57a9 100644 (file)
@@ -2432,4 +2432,11 @@ $capabilities = array(
         )
     ),
 
+    // Context locking/unlocking.
+    'moodle/site:managecontextlocks' => [
+        'captype' => 'write',
+        'contextlevel' => CONTEXT_MODULE,
+        '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..9e9cadd 100644 (file)
@@ -2771,5 +2771,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;
 }
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 94f995d..93d32b5 100644 (file)
@@ -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',
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..3bd14ae 100644 (file)
@@ -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 (file)
index 0000000..905932a
--- /dev/null
@@ -0,0 +1,445 @@
+<?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));
+        }
+    }
+
+    /**
+     * 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 (file)
index 0000000..d6f19c8
--- /dev/null
@@ -0,0 +1,205 @@
+@core
+Feature: Context freezing 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: Freeze course module module should freeze 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 "Freeze 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: Freeze course should freeze 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 "Freeze 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 "Unfreeze this context"
+    When I follow "faa1b"
+    Then I should not see "Add a new discussion topic"
+    And I should not see "Unfreeze 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: Freeze course category should freeze 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 "Unfreeze this context"
+    When I follow "faa1b"
+    Then I should not see "Add a new discussion topic"
+    And I should not see "Unfreeze 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 "Unfreeze 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"
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 c0f18c8..023fab1 100644 (file)
@@ -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 ===
 
index 8cc5d80..d200778 100644 (file)
@@ -274,17 +274,17 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = array('userids' => $userids, 'userid' => $userid);
+        $params = self::validate_parameters(self::create_contacts_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = array('userids' => $userids, 'userid' => $userid);
-        $params = self::validate_parameters(self::create_contacts_parameters(), $params);
-
         $warnings = array();
         foreach ($params['userids'] as $id) {
-            if (!message_add_contact($id, 0, $userid)) {
+            if (!message_add_contact($id, 0, $params['userid'])) {
                 $warnings[] = array(
                     'item' => 'user',
                     'itemid' => $id,
@@ -359,16 +359,16 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = array('userids' => $userids, 'userid' => $userid);
+        $params = self::validate_parameters(self::delete_contacts_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = array('userids' => $userids, 'userid' => $userid);
-        $params = self::validate_parameters(self::delete_contacts_parameters(), $params);
-
         foreach ($params['userids'] as $id) {
-            \core_message\api::remove_contact($userid, $id);
+            \core_message\api::remove_contact($params['userid'], $id);
         }
 
         return null;
@@ -417,14 +417,14 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = ['userid' => $userid, 'blockeduserid' => $blockeduserid];
+        $params = self::validate_parameters(self::block_user_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = ['userid' => $userid, 'blockeduserid' => $blockeduserid];
-        $params = self::validate_parameters(self::block_user_parameters(), $params);
-
         if (!\core_message\api::is_blocked($params['userid'], $params['blockeduserid'])) {
             \core_message\api::block_user($params['userid'], $params['blockeduserid']);
         }
@@ -473,14 +473,14 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = ['userid' => $userid, 'unblockeduserid' => $unblockeduserid];
+        $params = self::validate_parameters(self::unblock_user_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = ['userid' => $userid, 'unblockeduserid' => $unblockeduserid];
-        $params = self::validate_parameters(self::unblock_user_parameters(), $params);
-
         \core_message\api::unblock_user($params['userid'], $params['unblockeduserid']);
 
         return [];
@@ -540,17 +540,17 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = array('userids' => $userids, 'userid' => $userid);
+        $params = self::validate_parameters(self::block_contacts_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = array('userids' => $userids, 'userid' => $userid);
-        $params = self::validate_parameters(self::block_contacts_parameters(), $params);
-
         $warnings = array();
         foreach ($params['userids'] as $id) {
-            if (!message_block_contact($id, $userid)) {
+            if (!message_block_contact($id, $params['userid'])) {
                 $warnings[] = array(
                     'item' => 'user',
                     'itemid' => $id,
@@ -626,16 +626,16 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = array('userids' => $userids, 'userid' => $userid);
+        $params = self::validate_parameters(self::unblock_contacts_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = array('userids' => $userids, 'userid' => $userid);
-        $params = self::validate_parameters(self::unblock_contacts_parameters(), $params);
-
         foreach ($params['userids'] as $id) {
-            message_unblock_contact($id, $userid);
+            message_unblock_contact($id, $params['userid']);
         }
 
         return null;
@@ -700,11 +700,6 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
-        $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $userid) && !has_capability($capability, $context)) {
-            throw new required_capability_exception($context, $capability, 'nopermissions', '');
-        }
-
         $params = [
             'userid' => $userid,
             'limitfrom' => $limitfrom,
@@ -712,6 +707,11 @@ class core_message_external extends external_api {
         ];
         $params = self::validate_parameters(self::get_contact_requests_parameters(), $params);
 
+        $capability = 'moodle/site:manageallmessaging';
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
+            throw new required_capability_exception($context, $capability, 'nopermissions', '');
+        }
+
         return \core_message\api::get_contact_requests($params['userid'], $params['limitfrom'], $params['limitnum']);
     }
 
@@ -767,27 +767,28 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = [
+            'userid' => $userid,
+            'conversationid' => $conversationid,
+            'includecontactrequests' => $includecontactrequests,
+            'limitfrom' => $limitfrom,
+            'limitnum' => $limitnum
+        ];
+        $params = self::validate_parameters(self::get_conversation_members_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
         // The user needs to be a part of the conversation before querying who the members are.
-        if (!\core_message\api::is_user_in_conversation($userid, $conversationid)) {
+        if (!\core_message\api::is_user_in_conversation($params['userid'], $params['conversationid'])) {
             throw new moodle_exception('You are not a member of this conversation.');
         }
 
-        $params = [
-            'userid' => $userid,
-            'conversationid' => $conversationid,
-            'includecontactrequests' => $includecontactrequests,
-            'limitfrom' => $limitfrom,
-            'limitnum' => $limitnum
-        ];
-        self::validate_parameters(self::get_conversation_members_parameters(), $params);
 
-        return \core_message\api::get_conversation_members($userid, $conversationid, $includecontactrequests,
-            $limitfrom, $limitnum);
+        return \core_message\api::get_conversation_members($params['userid'], $params['conversationid'], $params['includecontactrequests'],
+            $params['limitfrom'], $params['limitnum']);
     }
 
     /**
@@ -833,14 +834,14 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
+        $params = self::validate_parameters(self::create_contact_request_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $userid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['userid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
-        $params = self::validate_parameters(self::create_contact_request_parameters(), $params);
-
         if (!\core_message\api::can_create_contact($params['userid'], $params['requesteduserid'])) {
             $warning[] = [
                 'item' => 'user',
@@ -899,14 +900,14 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
+        $params = self::validate_parameters(self::confirm_contact_request_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $requesteduserid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['requesteduserid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
-        $params = self::validate_parameters(self::confirm_contact_request_parameters(), $params);
-
         \core_message\api::confirm_contact_request($params['userid'], $params['requesteduserid']);
 
         return [];
@@ -953,14 +954,14 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
+        $params = self::validate_parameters(self::decline_contact_request_parameters(), $params);
+
         $capability = 'moodle/site:manageallmessaging';
-        if (($USER->id != $requesteduserid) && !has_capability($capability, $context)) {
+        if (($USER->id != $params['requesteduserid']) && !has_capability($capability, $context)) {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = ['userid' => $userid, 'requesteduserid' => $requesteduserid];
-        $params = self::validate_parameters(self::decline_contact_request_parameters(), $params);
-
         \core_message\api::decline_contact_request($params['userid'], $params['requesteduserid']);
 
         return [];
@@ -1182,14 +1183,20 @@ class core_message_external extends external_api {
             'limitfrom' => $limitfrom,
             'limitnum' => $limitnum
         );
-        self::validate_parameters(self::data_for_messagearea_search_users_in_course_parameters(), $params);
+        $params = self::validate_parameters(self::data_for_messagearea_search_users_in_course_parameters(), $params);
         self::validate_context($systemcontext);
 
-        if (($USER->id != $userid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['userid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
-        $users = \core_message\api::search_users_in_course($userid, $courseid, $search, $limitfrom, $limitnum);
+        $users = \core_message\api::search_users_in_course(
+            $params['userid'],
+            $params['courseid'],
+            $params['search'],
+            $params['limitfrom'],
+            $params['limitnum']
+        );
         $results = new \core_message\output\messagearea\user_search_results($users);
 
         $renderer = $PAGE->get_renderer('core_message');
@@ -1272,14 +1279,19 @@ class core_message_external extends external_api {
             'search' => $search,
             'limitnum' => $limitnum
         );
-        self::validate_parameters(self::data_for_messagearea_search_users_parameters(), $params);
+        $params = self::validate_parameters(self::data_for_messagearea_search_users_parameters(), $params);
         self::validate_context($systemcontext);
 
-        if (($USER->id != $userid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['userid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
-        list($contacts, $courses, $noncontacts) = \core_message\api::search_users($userid, $search, $limitnum);
+        list($contacts, $courses, $noncontacts) = \core_message\api::search_users(
+            $params['userid'],
+            $params['search'],
+            $params['limitnum']
+        );
+
         $search = new \core_message\output\messagearea\user_search_results($contacts, $courses, $noncontacts);
 
         $renderer = $PAGE->get_renderer('core_message');
@@ -1449,14 +1461,19 @@ class core_message_external extends external_api {
             'limitnum' => $limitnum
 
         );
-        self::validate_parameters(self::data_for_messagearea_search_messages_parameters(), $params);
+        $params = self::validate_parameters(self::data_for_messagearea_search_messages_parameters(), $params);
         self::validate_context($systemcontext);
 
-        if (($USER->id != $userid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['userid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
-        $messages = \core_message\api::search_messages($userid, $search, $limitfrom, $limitnum);
+        $messages = \core_message\api::search_messages(
+            $params['userid'],
+            $params['search'],
+            $params['limitfrom'],
+            $params['limitnum']
+        );
         $results = new \core_message\output\messagearea\message_search_results($messages);
 
         $renderer = $PAGE->get_renderer('core_message');
@@ -1527,16 +1544,23 @@ class core_message_external extends external_api {
             'type' => $type,
             'favourites' => $favourites
         );
-        self::validate_parameters(self::get_conversations_parameters(), $params);
+        $params = self::validate_parameters(self::get_conversations_parameters(), $params);
 
         $systemcontext = context_system::instance();
         self::validate_context($systemcontext);
 
-        if (($USER->id != $userid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['userid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
-        $conversations = \core_message\api::get_conversations($userid, $limitfrom, $limitnum, $type, $favourites);
+        $conversations = \core_message\api::get_conversations(
+            $params['userid'],
+            $params['limitfrom'],
+            $params['limitnum'],
+            $params['type'],
+            $params['favourites']
+        );
+
         return (object) ['conversations' => $conversations];
     }
 
@@ -1605,14 +1629,14 @@ class core_message_external extends external_api {
             'limitfrom' => $limitfrom,
             'limitnum' => $limitnum
         );
-        self::validate_parameters(self::data_for_messagearea_conversations_parameters(), $params);
+        $params = self::validate_parameters(self::data_for_messagearea_conversations_parameters(), $params);
         self::validate_context($systemcontext);
 
-        if (($USER->id != $userid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['userid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
-        $conversations = \core_message\api::get_conversations($userid, $limitfrom, $limitnum);
+        $conversations = \core_message\api::get_conversations($params['userid'], $params['limitfrom'], $params['limitnum']);
 
         // Format the conversations in the legacy style, as the get_conversations method has since been changed.
         $conversations = \core_message\helper::get_conversations_legacy_formatter($conversations);
@@ -1686,14 +1710,14 @@ class core_message_external extends external_api {
             'limitfrom' => $limitfrom,
             'limitnum' => $limitnum
         );
-        self::validate_parameters(self::data_for_messagearea_contacts_parameters(), $params);
+        $params = self::validate_parameters(self::data_for_messagearea_contacts_parameters(), $params);
         self::validate_context($systemcontext);
 
-        if (($USER->id != $userid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['userid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
-        $contacts = \core_message\api::get_contacts($userid, $limitfrom, $limitnum);
+        $contacts = \core_message\api::get_contacts($params['userid'], $params['limitfrom'], $params['limitnum']);
         $contacts = new \core_message\output\messagearea\contacts(null, $contacts);
 
         $renderer = $PAGE->get_renderer('core_message');
@@ -1773,14 +1797,14 @@ class core_message_external extends external_api {
             'newest' => $newest,
             'timefrom' => $timefrom,
         );
-        self::validate_parameters(self::data_for_messagearea_messages_parameters(), $params);
+        $params = self::validate_parameters(self::data_for_messagearea_messages_parameters(), $params);
         self::validate_context($systemcontext);
 
-        if (($USER->id != $currentuserid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['currentuserid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
-        if ($newest) {
+        if ($params['newest']) {
             $sort = 'timecreated DESC';
         } else {
             $sort = 'timecreated ASC';
@@ -1794,21 +1818,21 @@ class core_message_external extends external_api {
         // case those messages will be lost.
         //
         // Instead we ignore the current time in the result set to ensure that second is allowed to finish.
-        if (!empty($timefrom)) {
+        if (!empty($params['timefrom'])) {
             $timeto = time() - 1;
         } else {
             $timeto = 0;
         }
 
         // No requesting messages from the current time, as stated above.
-        if ($timefrom == time()) {
+        if ($params['timefrom'] == time()) {
             $messages = [];
         } else {
-            $messages = \core_message\api::get_messages($currentuserid, $otheruserid, $limitfrom,
-                                                        $limitnum, $sort, $timefrom, $timeto);
+            $messages = \core_message\api::get_messages($params['currentuserid'], $params['otheruserid'], $params['limitfrom'],
+                                                        $params['limitnum'], $sort, $params['timefrom'], $timeto);
         }
 
-        $messages = new \core_message\output\messagearea\messages($currentuserid, $otheruserid, $messages);
+        $messages = new \core_message\output\messagearea\messages($params['currentuserid'], $params['otheruserid'], $messages);
 
         $renderer = $PAGE->get_renderer('core_message');
         return $messages->export_for_template($renderer);
@@ -1900,10 +1924,10 @@ class core_message_external extends external_api {
             'newest' => $newest,
             'timefrom' => $timefrom,
         );
-        self::validate_parameters(self::get_conversation_messages_parameters(), $params);
+        $params = self::validate_parameters(self::get_conversation_messages_parameters(), $params);
         self::validate_context($systemcontext);
 
-        if (($USER->id != $currentuserid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['currentuserid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
@@ -1917,14 +1941,20 @@ class core_message_external extends external_api {
         // case those messages will be lost.
         //
         // Instead we ignore the current time in the result set to ensure that second is allowed to finish.
-        $timeto = empty($timefrom) ? 0 : time() - 1;
+        $timeto = empty($params['timefrom']) ? 0 : time() - 1;
 
         // No requesting messages from the current time, as stated above.
-        if ($timefrom == time()) {
+        if ($params['timefrom'] == time()) {
             $messages = [];
         } else {
-            $messages = \core_message\api::get_conversation_messages($currentuserid, $convid, $limitfrom,
-                                                        $limitnum, $sort, $timefrom, $timeto);
+            $messages = \core_message\api::get_conversation_messages(
+                $params['currentuserid'],
+                $params['convid'],
+                $params['limitfrom'],
+                $params['limitnum'],
+                $sort,
+                $params['timefrom'],
+                $timeto);
         }
 
         return $messages;
@@ -1990,14 +2020,14 @@ class core_message_external extends external_api {
             'currentuserid' => $currentuserid,
             'otheruserid' => $otheruserid
         );
-        self::validate_parameters(self::data_for_messagearea_get_most_recent_message_parameters(), $params);
+        $params = self::validate_parameters(self::data_for_messagearea_get_most_recent_message_parameters(), $params);
         self::validate_context($systemcontext);
 
-        if (($USER->id != $currentuserid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['currentuserid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
-        $message = \core_message\api::get_most_recent_message($currentuserid, $otheruserid);
+        $message = \core_message\api::get_most_recent_message($params['currentuserid'], $params['otheruserid']);
         $message = new \core_message\output\messagearea\message($message);
 
         $renderer = $PAGE->get_renderer('core_message');
@@ -2064,14 +2094,14 @@ class core_message_external extends external_api {
             'currentuserid' => $currentuserid,
             'otheruserid' => $otheruserid
         );
-        self::validate_parameters(self::data_for_messagearea_get_profile_parameters(), $params);
+        $params = self::validate_parameters(self::data_for_messagearea_get_profile_parameters(), $params);
         self::validate_context($systemcontext);
 
-        if (($USER->id != $currentuserid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['currentuserid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
-        $profile = \core_message\api::get_profile($currentuserid, $otheruserid);
+        $profile = \core_message\api::get_profile($params['currentuserid'], $params['otheruserid']);
         $profile = new \core_message\output\messagearea\profile($profile);
 
         $renderer = $PAGE->get_renderer('core_message');
@@ -3157,8 +3187,8 @@ class core_message_external extends external_api {
         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
         core_user::require_active_user($user);
 
-        if (\core_message\api::can_mark_all_messages_as_read($userid, $conversationid)) {
-            \core_message\api::mark_all_messages_as_read($userid, $conversationid);
+        if (\core_message\api::can_mark_all_messages_as_read($params['userid'], $params['conversationid'])) {
+            \core_message\api::mark_all_messages_as_read($params['userid'], $params['conversationid']);
         } else {
             throw new moodle_exception('accessdenied', 'admin');
         }
@@ -3225,7 +3255,7 @@ class core_message_external extends external_api {
         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
         core_user::require_active_user($user);
 
-        if (!$conversationid = \core_message\api::get_conversation_between_users([$userid, $otheruserid])) {
+        if (!$conversationid = \core_message\api::get_conversation_between_users([$params['userid'], $params['otheruserid']])) {
             return [];
         }
 
@@ -3318,7 +3348,7 @@ class core_message_external extends external_api {
         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
         core_user::require_active_user($user);
 
-        foreach ($conversationids as $conversationid) {
+        foreach ($params['conversationids'] as $conversationid) {
             if (\core_message\api::can_delete_conversation($user->id, $conversationid)) {
                 \core_message\api::delete_conversation_by_id($user->id, $conversationid);
             } else {
@@ -3391,8 +3421,8 @@ class core_message_external extends external_api {
         $user = core_user::get_user($params['userid'], '*', MUST_EXIST);
         core_user::require_active_user($user);
 
-        if (\core_message\api::can_delete_message($user->id, $messageid)) {
-            $status = \core_message\api::delete_message($user->id, $messageid);
+        if (\core_message\api::can_delete_message($user->id, $params['messageid'])) {
+            $status = \core_message\api::delete_message($user->id, $params['messageid']);
         } else {
             throw new moodle_exception('You do not have permission to delete this message');
         }
@@ -3473,11 +3503,11 @@ class core_message_external extends external_api {
 
         $user = self::validate_preferences_permissions($params['userid']);
 
-        $processor = get_message_processor($name);
+        $processor = get_message_processor($params['name']);
         $preferences = [];
         $form = new stdClass();
 
-        foreach ($formvalues as $formvalue) {
+        foreach ($params['formvalues'] as $formvalue) {
             // Curly braces to ensure interpretation is consistent between
             // php 5 and php 7.
             $form->{$formvalue['name']} = $formvalue['value'];
@@ -3486,7 +3516,7 @@ class core_message_external extends external_api {
         $processor->process_form($form, $preferences);
 
         if (!empty($preferences)) {
-            set_user_preferences($preferences, $userid);
+            set_user_preferences($preferences, $params['userid']);
         }
     }
 
@@ -3548,7 +3578,7 @@ class core_message_external extends external_api {
         core_user::require_active_user($user);
         self::validate_context(context_user::instance($params['userid']));
 
-        $processor = get_message_processor($name);
+        $processor = get_message_processor($params['name']);
 
         $processoroutput = new \core_message\output\processor($processor, $user);
         $renderer = $PAGE->get_renderer('core_message');
@@ -3850,7 +3880,7 @@ class core_message_external extends external_api {
         $systemcontext = context_system::instance();
         self::validate_context($systemcontext);
 
-        if (($USER->id != $userid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['userid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
@@ -3909,7 +3939,7 @@ class core_message_external extends external_api {
         $systemcontext = context_system::instance();
         self::validate_context($systemcontext);
 
-        if (($USER->id != $userid) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
+        if (($USER->id != $params['userid']) && !has_capability('moodle/site:readallmessages', $systemcontext)) {
             throw new moodle_exception('You do not have permission to perform this action.');
         }
 
index e3f1083..159ebfb 100644 (file)
@@ -128,10 +128,12 @@ if (!$user2realuser) {
 
 // Mark the conversation as read.
 if (!empty($user2->id)) {
+    $hasbeenreadallmessages = false;
     if ($currentuser && isset($conversations[$user2->id])) {
         // Mark the conversation we are loading as read.
         if ($conversationid = \core_message\api::get_conversation_between_users([$user1->id, $user2->id])) {
             \core_message\api::mark_all_messages_as_read($user1->id, $conversationid);
+            $hasbeenreadallmessages = true;
         }
 
         // Ensure the UI knows it's read as well.
@@ -140,52 +142,63 @@ if (!empty($user2->id)) {
 
     // Get the conversationid.
     if (!isset($conversationid)) {
-        if (!$conversationid = self::get_conversation_between_users($userids)) {
-            // If the conversationid doesn't exist, throw an exception.
-            throw new moodle_exception('conversationdoesntexist', 'core_message');
+        if (!$conversationid = \core_message\api::get_conversation_between_users([$user1->id, $user2->id])) {
+            // If the individual conversationid doesn't exist, create it.
+            $conversation = \core_message\api::create_conversation(
+                \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+                [$user1->id, $user2->id]
+            );
+            $conversationid = $conversation->id;
         }
     }
 
     $convmessages = \core_message\api::get_conversation_messages($user1->id, $conversationid, 0, 20, 'timecreated DESC');
-    $messages = $convmessages['messages'];
-
-    // Keeps track of the last day, month and year combo we were viewing.
-    $day = '';
-    $month = '';
-    $year = '';
-
-    // Parse the messages to add missing fields for backward compatibility.
-    $messages = array_reverse($messages);
-    $day = '';
-    $month = '';
-    $year = '';
-    foreach ($messages as $message) {
-        // Add useridto.
-        if (empty($message->useridto)) {
-            if ($message->useridfrom == $user1->id) {
-                $message->useridto = $user2->id;
-            } else {
-                $message->useridto = $user1->id;
+    $messages = [];
+    if (!empty($convmessages)) {
+        $messages = $convmessages['messages'];
+
+        // Parse the messages to add missing fields for backward compatibility.
+        $messages = array_reverse($messages);
+        // Keeps track of the last day, month and year combo we were viewing.
+        $day = '';
+        $month = '';
+        $year = '';
+        foreach ($messages as $message) {
+            // Add useridto.
+            if (empty($message->useridto)) {
+                if ($message->useridfrom == $user1->id) {
+                    $message->useridto = $user2->id;
+                } else {
+                    $message->useridto = $user1->id;
+                }
             }
-        }
 
-        // Add currentuserid.
-        $message->currentuserid = $USER->id;
-
-        // Check if we are now viewing a different block period.
-        $message->displayblocktime = false;
-        $date = usergetdate($message->timecreated);
-        if ($day != $date['mday'] || $month != $date['month'] || $year != $date['year']) {
-            $day = $date['mday'];
-            $month = $date['month'];
-            $year = $date['year'];
-            $message->displayblocktime = true;
-            $message->blocktime = userdate($message->timecreated, get_string('strftimedaydate'));
-        }
+            // Add currentuserid.
+            $message->currentuserid = $USER->id;
+
+            // Check if we are now viewing a different block period.
+            $message->displayblocktime = false;
+            $date = usergetdate($message->timecreated);
+            if ($day != $date['mday'] || $month != $date['month'] || $year != $date['year']) {
+                $day = $date['mday'];
+                $month = $date['month'];
+                $year = $date['year'];
+                $message->displayblocktime = true;
+                $message->blocktime = userdate($message->timecreated, get_string('strftimedaydate'));
+            }
 
-        // We don't have this information here so, for now, we leave an empty value.
-        // This is a temporary solution because a new UI is being built in MDL-63303.
-        $message->timeread = 0;
+            // We don't have this information here so, for now, we leave an empty value or the current time.
+            // This is a temporary solution because a new UI is being built in MDL-63303.
+            $message->timeread = 0;
+            if ($hasbeenreadallmessages && $message->useridfrom != $user1->id) {
+                // As all the messages sent by the other user have been marked as read previously, we will change
+                // timeread to the current time to avoid the last message will be duplicated after calling to the
+                // core_message_data_for_messagearea_messages via javascript.
+                // We only need to change that to the other user, because for the current user, messages are always
+                // marked as unread.
+                $message->timeread = time();
+            }
+        }
     }
 }
 
index 1dce7d4..e1c4f4e 100644 (file)
@@ -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,
index 7740358..45c53ce 100644 (file)
@@ -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)
index 4cfb711..3f7caf8 100644 (file)
@@ -146,22 +146,22 @@ $card-gutter : $card-deck-margin * 2;
 }
 
 .dashboard-card-deck {
-    @include media-breakpoint-up(sm) {
+    @media (min-width: 576px) {
         .dashboard-card {
             width: calc(50% - #{$card-gutter});
         }
     }
-    @include media-breakpoint-up(md) {
+    @media (min-width: 840px) {
         .dashboard-card {
             width: calc(33.33% - #{$card-gutter});
         }
     }
-    @include media-breakpoint-up(lg) {
+    @media (min-width: 1100px) {
         .dashboard-card {
             width: calc(25% - #{$card-gutter});
         }
     }
-    @include media-breakpoint-up(xl) {
+    @media (min-width: 1360px) {
         .dashboard-card {
             width: calc(20% - #{$card-gutter});
         }
@@ -170,12 +170,12 @@ $card-gutter : $card-deck-margin * 2;
 
 #region-main.has-blocks {
     .dashboard-card-deck {
-        @include media-breakpoint-up(lg) {
+        @media (min-width: 1200px) {
             .dashboard-card {
                 width: calc(33.33% - #{$card-gutter});
             }
         }
-        @include media-breakpoint-up(xl) {
+        @media (min-width: 1470px) {
             .dashboard-card {
                 width: calc(25% - #{$card-gutter});
             }
@@ -183,14 +183,24 @@ $card-gutter : $card-deck-margin * 2;
     }
 }
 
-body.drawer-open-left #region-main.has-blocks {
+body.drawer-open-left {
     .dashboard-card-deck {
-        @include media-breakpoint-up(lg) {
+        @media (min-width: 768px) {
+            .dashboard-card {
+                width: calc(100% - #{$card-gutter});
+            }
+        }
+        @media (min-width: 861px) {
+            .dashboard-card {
+                width: calc(50% - #{$card-gutter});
+            }
+        }
+        @media (min-width: 1122px) {
             .dashboard-card {
                 width: calc(33.33% - #{$card-gutter});
             }
         }
-        @media (min-width: 1400px) {
+        @media (min-width: 1381px) {
             .dashboard-card {
                 width: calc(25% - #{$card-gutter});
             }
@@ -198,14 +208,33 @@ body.drawer-open-left #region-main.has-blocks {
     }
 }
 
+body.drawer-open-left #region-main.has-blocks {
+    .dashboard-card-deck {
+        @media (min-width: 1200px) {
+            .dashboard-card {
+                width: calc(100% - #{$card-gutter});
+            }
+        }
+        @media (min-width: 1236px) {
+            .dashboard-card {
+                width: calc(50% - #{$card-gutter});
+            }
+        }
+        @media (min-width: 1497px) {
+            .dashboard-card {
+                width: calc(33.33% - #{$card-gutter});
+            }
+        }
+    }
+}
+
 @media (min-width: 1200px) {
     #block-region-side-pre {
         .dashboard-card-deck {
-            margin: 0;
+            margin-left: 0;
+            margin-right: 0;
             .dashboard-card {
-                width: 100% !important;
-                margin-left: 0;
-                margin-right: 0;
+                width: calc(100% - #{$card-gutter}) !important;
             }
         }
     }
index 0700407..44c7096 100644 (file)
@@ -11197,41 +11197,60 @@ div.editor_atto_toolbar button .icon {
   .dashboard-card-deck .dashboard-card {
     width: calc(50% - 0.5rem); } }
 
-@media (min-width: 768px) {
+@media (min-width: 840px) {
   .dashboard-card-deck .dashboard-card {
     width: calc(33.33% - 0.5rem); } }
 
-@media (min-width: 992px) {
+@media (min-width: 1100px) {
   .dashboard-card-deck .dashboard-card {
     width: calc(25% - 0.5rem); } }
 
-@media (min-width: 1200px) {
+@media (min-width: 1360px) {
   .dashboard-card-deck .dashboard-card {
     width: calc(20% - 0.5rem); } }
 
-@media (min-width: 992px) {
+@media (min-width: 1200px) {
   #region-main.has-blocks .dashboard-card-deck .dashboard-card {
     width: calc(33.33% - 0.5rem); } }
 
-@media (min-width: 1200px) {
+@media (min-width: 1470px) {
   #region-main.has-blocks .dashboard-card-deck .dashboard-card {
     width: calc(25% - 0.5rem); } }
 
-@media (min-width: 992px) {
-  body.drawer-open-left #region-main.has-blocks .dashboard-card-deck .dashboard-card {
+@media (min-width: 768px) {
+  body.drawer-open-left .dashboard-card-deck .dashboard-card {
+    width: calc(100% - 0.5rem); } }
+
+@media (min-width: 861px) {
+  body.drawer-open-left .dashboard-card-deck .dashboard-card {
+    width: calc(50% - 0.5rem); } }
+
+@media (min-width: 1122px) {
+  body.drawer-open-left .dashboard-card-deck .dashboard-card {
     width: calc(33.33% - 0.5rem); } }
 
-@media (min-width: 1400px) {
-  body.drawer-open-left #region-main.has-blocks .dashboard-card-deck .dashboard-card {
+@media (min-width: 1381px) {
+  body.drawer-open-left .dashboard-card-deck .dashboard-card {
     width: calc(25% - 0.5rem); } }
 
+@media (min-width: 1200px) {
+  body.drawer-open-left #region-main.has-blocks .dashboard-card-deck .dashboard-card {
+    width: calc(100% - 0.5rem); } }
+
+@media (min-width: 1236px) {
+  body.drawer-open-left #region-main.has-blocks .dashboard-card-deck .dashboard-card {
+    width: calc(50% - 0.5rem); } }
+
+@media (min-width: 1497px) {
+  body.drawer-open-left #region-main.has-blocks .dashboard-card-deck .dashboard-card {
+    width: calc(33.33% - 0.5rem); } }
+
 @media (min-width: 1200px) {
   #block-region-side-pre .dashboard-card-deck {
-    margin: 0; }
+    margin-left: 0;
+    margin-right: 0; }
     #block-region-side-pre .dashboard-card-deck .dashboard-card {
-      width: 100% !important;
-      margin-left: 0;
-      margin-right: 0; } }
+      width: calc(100% - 0.5rem) !important; } }
 
 .block_recentlyaccesseditems img.icon {
   height: auto;
index 46849d4..3ab0373 100644 (file)
     .dashboard-card {
         .border-radius(@baseBorderRadius);
         margin-bottom: 0.5rem;
+        margin-right: 0.25rem;
+        margin-left: 0.25rem;
         flex-grow: 0;
         flex-shrink: 0;
         min-width: 0;
-        width: 100%;
+        width: ~"calc(100% - 0.5rem)";
         flex-basis: auto;
     }
-    @media (min-width: 576px) {
+    @media (min-width: 647px) {
         .dashboard-card {
             display: flex;
             flex-direction: column;
-            margin-right: 0.25rem;
-            margin-left: 0.25rem;
+
             width: ~"calc(50% - 0.5rem)";
         }
     }
-
-    @media (min-width: 1200px) {
+    @media (min-width: 888px) {
         .dashboard-card {
             width: ~"calc(33.333% - 0.5rem)";
         }
     }
+    @media (min-width: 1147px) {
+        .dashboard-card {
+            width: ~"calc(25% - 0.5rem)";
+        }
+    }
+    @media (min-width: 1407px) {
+        .dashboard-card {
+            width: ~"calc(20% - 0.5rem)";
+        }
+    }
+}
+body.used-region-side-pre.empty-region-side-post,
+body.used-region-side-post.empty-region-side-pre {
+    .dashboard-card-deck {
+        @media (min-width: 768px) {
+            .dashboard-card {
+                width: ~"calc(100% - 0.5rem)";
+            }
+        }
+        @media (min-width: 815px) {
+            .dashboard-card {
+                width: ~"calc(50% - 0.5rem)";
+            }
+        }
+        @media (min-width: 1163px) {
+            .dashboard-card {
+                width: ~"calc(33.333% - 0.5rem)";
+            }
+        }
+        @media (min-width: 1514px) {
+            .dashboard-card {
+                width: ~"calc(25% - 0.5rem)";
+            }
+        }
+    }
+}
+body.used-region-side-pre.used-region-side-post {
+    .dashboard-card-deck {
+        @media (min-width: 768px) {
+            .dashboard-card {
+                width: ~"calc(100% - 0.5rem)";
+            }
+        }
+        @media (min-width: 1144px) {
+            .dashboard-card {
+                width: ~"calc(50% - 0.5rem)";
+            }
+        }
+        @media (min-width: 1680px) {
+            .dashboard-card {
+                width: ~"calc(33.333% - 0.5rem)";
+            }
+        }
+    }
 }
 
+/* stylelint-disable declaration-no-important */
 @media (min-width: 768px) {
     #block-region-side-post .dashboard-card-deck,
     #block-region-side-pre .dashboard-card-deck {
-        margin: 0;
-        height: unset;
         .dashboard-card {
-            width: 100%;
-            margin-left: 0;
-            margin-right: 0;
+            width: ~"calc(100% - 0.5rem)" !important;
         }
     }
 }
+.block_docked .dashboard-card {
+    width: ~"calc(100% - 0.5rem)" !important;
+}
+/* stylelint-enable */
 
 .dashboard-card-img {
     height: 7rem;
index 3e252bb..868a53c 100644 (file)
@@ -16266,39 +16266,86 @@ body {
   -moz-border-radius: 4px;
   border-radius: 4px;
   margin-bottom: 0.5rem;
+  margin-right: 0.25rem;
+  margin-left: 0.25rem;
   flex-grow: 0;
   flex-shrink: 0;
   min-width: 0;
-  width: 100%;
+  width: calc(100% - 0.5rem);
   flex-basis: auto;
 }
-@media (min-width: 576px) {
+@media (min-width: 647px) {
   .dashboard-card-deck .dashboard-card {
     display: flex;
     flex-direction: column;
-    margin-right: 0.25rem;
-    margin-left: 0.25rem;
     width: calc(50% - 0.5rem);
   }
 }
-@media (min-width: 1200px) {
+@media (min-width: 888px) {
   .dashboard-card-deck .dashboard-card {
     width: calc(33.333% - 0.5rem);
   }
 }
+@media (min-width: 1147px) {
+  .dashboard-card-deck .dashboard-card {
+    width: calc(25% - 0.5rem);
+  }
+}
+@media (min-width: 1407px) {
+  .dashboard-card-deck .dashboard-card {
+    width: calc(20% - 0.5rem);
+  }
+}
 @media (min-width: 768px) {
-  #block-region-side-post .dashboard-card-deck,
-  #block-region-side-pre .dashboard-card-deck {
-    margin: 0;
-    height: unset;
+  body.used-region-side-pre.empty-region-side-post .dashboard-card-deck .dashboard-card,
+  body.used-region-side-post.empty-region-side-pre .dashboard-card-deck .dashboard-card {
+    width: calc(100% - 0.5rem);
+  }
+}
+@media (min-width: 815px) {
+  body.used-region-side-pre.empty-region-side-post .dashboard-card-deck .dashboard-card,
+  body.used-region-side-post.empty-region-side-pre .dashboard-card-deck .dashboard-card {
+    width: calc(50% - 0.5rem);
+  }
+}
+@media (min-width: 1163px) {
+  body.used-region-side-pre.empty-region-side-post .dashboard-card-deck .dashboard-card,
+  body.used-region-side-post.empty-region-side-pre .dashboard-card-deck .dashboard-card {
+    width: calc(33.333% - 0.5rem);
+  }
+}
+@media (min-width: 1514px) {
+  body.used-region-side-pre.empty-region-side-post .dashboard-card-deck .dashboard-card,
+  body.used-region-side-post.empty-region-side-pre .dashboard-card-deck .dashboard-card {
+    width: calc(25% - 0.5rem);
   }
+}
+@media (min-width: 768px) {
+  body.used-region-side-pre.used-region-side-post .dashboard-card-deck .dashboard-card {
+    width: calc(100% - 0.5rem);
+  }
+}
+@media (min-width: 1144px) {
+  body.used-region-side-pre.used-region-side-post .dashboard-card-deck .dashboard-card {
+    width: calc(50% - 0.5rem);
+  }
+}
+@media (min-width: 1680px) {
+  body.used-region-side-pre.used-region-side-post .dashboard-card-deck .dashboard-card {
+    width: calc(33.333% - 0.5rem);
+  }
+}
+/* stylelint-disable declaration-no-important */
+@media (min-width: 768px) {
   #block-region-side-post .dashboard-card-deck .dashboard-card,
   #block-region-side-pre .dashboard-card-deck .dashboard-card {
-    width: 100%;
-    margin-left: 0;
-    margin-right: 0;
+    width: calc(100% - 0.5rem) !important;
   }
 }
+.block_docked .dashboard-card {
+  width: calc(100% - 0.5rem) !important;
+}
+/* stylelint-enable */
 .dashboard-card-img {
   height: 7rem;
   background-position: center;
index 56f0e7b..941d06b 100644 (file)
@@ -28,7 +28,7 @@
     }
 }}
 <a {{!
-    }} href="{{url}}"{{!
+    }} href="{{$url}}{{url}}{{/url}}"{{!
     }} id="calendar-day-popover-link-{{courseid}}-{{year}}-{{yday}}-{{uniqid}}"{{!
     }} data-container="body"{{!
     }} data-toggle="popover"{{!
index 6a8ab5b..93ff0a3 100644 (file)
@@ -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.