Merge branch 'MDL-63923-master' of git://github.com/bmbrands/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Wed, 14 Nov 2018 01:54:40 +0000 (09:54 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Wed, 14 Nov 2018 01:54:40 +0000 (09:54 +0800)
66 files changed:
admin/lock.php [new file with mode: 0644]
admin/settings/development.php
admin/tool/generator/testplan.template.jmx
admin/tool/lpimportcsv/index.php
admin/tool/monitor/lib.php
badges/classes/external.php
badges/classes/external/competency_exporter.php [new file with mode: 0644]
badges/classes/external/endorsement_exporter.php [new file with mode: 0644]
badges/classes/external/related_info_exporter.php [new file with mode: 0644]
badges/classes/external/user_badge_exporter.php [new file with mode: 0644]
badges/tests/external_test.php
badges/upgrade.txt
blocks/recentlyaccesseditems/templates/recentlyaccesseditems-view.mustache
cohort/lib.php
course/classes/category.php
course/classes/management/helper.php
course/lib.php
install/lang/ca/error.php
lang/en/admin.php
lang/en/message.php
lang/en/role.php
lib/accesslib.php
lib/amd/src/sortable_list.js
lib/blocklib.php
lib/classes/event/group_message_sent.php [new file with mode: 0644]
lib/classes/message/manager.php
lib/classes/user.php
lib/db/access.php
lib/db/install.xml
lib/db/services.php
lib/db/upgrade.php
lib/filebrowser/file_info_context_course.php
lib/messagelib.php
lib/moodlelib.php
lib/navigationlib.php
lib/phpunit/classes/advanced_testcase.php
lib/recaptchalib_v2.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/message_test.php
lib/tests/messagelib_test.php
lib/tests/moodlelib_test.php
lib/tests/session_manager_test.php
lib/upgrade.txt
message/classes/api.php
message/classes/helper.php
message/externallib.php
message/pendingcontactrequests.php
message/tests/api_test.php
message/tests/events_test.php
message/tests/externallib_test.php
message/upgrade.txt
mod/forum/db/access.php
mod/forum/version.php
mod/scorm/lib.php
mod/scorm/locallib.php
privacy/templates/htmlpage.mustache
theme/boost/scss/moodle/blocks.scss
theme/boost/style/moodle.css
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/style/moodle.css
user/externallib.php
user/tests/externallib_test.php
user/upgrade.txt
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 ccd8cfb..b48b010 100644 (file)
             <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
           </HTTPSamplerProxy>
           <hashTree/>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="View login page" enabled="true">
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+              <collectionProp name="Arguments.arguments"/>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain"></stringProp>
+            <stringProp name="HTTPSampler.port"></stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">${sitepath}/login/index.php</stringProp>
+            <stringProp name="HTTPSampler.method">GET</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Get logintoken" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">LOGIN_TOKEN</stringProp>
+              <stringProp name="RegexExtractor.regex">type=&quot;hidden&quot;\sname=&quot;logintoken&quot;\svalue=&quot;(\w+)&quot;</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default">0</stringProp>
+              <stringProp name="RegexExtractor.match_number">1</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
           <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
             <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
               <collectionProp name="Arguments.arguments">
                   <boolProp name="HTTPArgument.use_equals">true</boolProp>
                   <stringProp name="Argument.name">password</stringProp>
                 </elementProp>
+                <elementProp name="logintoken" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">${LOGIN_TOKEN}</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                  <boolProp name="HTTPArgument.use_equals">true</boolProp>
+                  <stringProp name="Argument.name">logintoken</stringProp>
+                </elementProp>
               </collectionProp>
             </elementProp>
             <stringProp name="HTTPSampler.domain"></stringProp>
             <boolProp name="HTTPSampler.monitor">false</boolProp>
             <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
           </HTTPSamplerProxy>
-          <hashTree/>
+          <hashTree>
+            <ResponseAssertion guiclass="AssertionGui" testclass="ResponseAssertion" testname="Logged in" enabled="true">
+              <collectionProp name="Asserion.test_strings">
+                <stringProp name="615717117">&lt;div class=&quot;logininfo&quot;&gt;You are logged in as</stringProp>
+              </collectionProp>
+              <stringProp name="Assertion.test_field">Assertion.response_data</stringProp>
+              <boolProp name="Assertion.assume_success">false</boolProp>
+              <intProp name="Assertion.test_type">2</intProp>
+            </ResponseAssertion>
+            <hashTree/>
+          </hashTree>
           <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Frontpage logged" enabled="true">
             <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
               <collectionProp name="Arguments.arguments"/>
             <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
           </HTTPSamplerProxy>
           <hashTree/>
+          <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="View login page" enabled="true">
+            <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
+              <collectionProp name="Arguments.arguments"/>
+            </elementProp>
+            <stringProp name="HTTPSampler.domain"></stringProp>
+            <stringProp name="HTTPSampler.port"></stringProp>
+            <stringProp name="HTTPSampler.connect_timeout"></stringProp>
+            <stringProp name="HTTPSampler.response_timeout"></stringProp>
+            <stringProp name="HTTPSampler.protocol"></stringProp>
+            <stringProp name="HTTPSampler.contentEncoding"></stringProp>
+            <stringProp name="HTTPSampler.path">${sitepath}/login/index.php</stringProp>
+            <stringProp name="HTTPSampler.method">GET</stringProp>
+            <boolProp name="HTTPSampler.follow_redirects">true</boolProp>
+            <boolProp name="HTTPSampler.auto_redirects">false</boolProp>
+            <boolProp name="HTTPSampler.use_keepalive">true</boolProp>
+            <boolProp name="HTTPSampler.DO_MULTIPART_POST">false</boolProp>
+            <boolProp name="HTTPSampler.monitor">false</boolProp>
+            <stringProp name="HTTPSampler.embedded_url_re"></stringProp>
+          </HTTPSamplerProxy>
+          <hashTree>
+            <RegexExtractor guiclass="RegexExtractorGui" testclass="RegexExtractor" testname="Get logintoken" enabled="true">
+              <stringProp name="RegexExtractor.useHeaders">false</stringProp>
+              <stringProp name="RegexExtractor.refname">LOGIN_TOKEN</stringProp>
+              <stringProp name="RegexExtractor.regex">type=&quot;hidden&quot;\sname=&quot;logintoken&quot;\svalue=&quot;(\w+)&quot;</stringProp>
+              <stringProp name="RegexExtractor.template">$1$</stringProp>
+              <stringProp name="RegexExtractor.default">0</stringProp>
+              <stringProp name="RegexExtractor.match_number">1</stringProp>
+            </RegexExtractor>
+            <hashTree/>
+          </hashTree>
           <HTTPSamplerProxy guiclass="HttpTestSampleGui" testclass="HTTPSamplerProxy" testname="Login" enabled="true">
             <elementProp name="HTTPsampler.Arguments" elementType="Arguments" guiclass="HTTPArgumentsPanel" testclass="Arguments" testname="User Defined Variables" enabled="true">
               <collectionProp name="Arguments.arguments">
                   <boolProp name="HTTPArgument.use_equals">true</boolProp>
                   <stringProp name="Argument.name">password</stringProp>
                 </elementProp>
+                <elementProp name="logintoken" elementType="HTTPArgument">
+                  <boolProp name="HTTPArgument.always_encode">false</boolProp>
+                  <stringProp name="Argument.value">${LOGIN_TOKEN}</stringProp>
+                  <stringProp name="Argument.metadata">=</stringProp>
+                  <boolProp name="HTTPArgument.use_equals">true</boolProp>
+                  <stringProp name="Argument.name">logintoken</stringProp>
+                </elementProp>
               </collectionProp>
             </elementProp>
             <stringProp name="HTTPSampler.domain"></stringProp>
index fe14dac..2c541d3 100644 (file)
@@ -21,7 +21,7 @@
  * @copyright  2016 Damyon Wiese
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-
+define('NO_OUTPUT_BUFFERING', true);
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
 
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 e5b2de0..15437d0 100644 (file)
@@ -29,6 +29,8 @@ defined('MOODLE_INTERNAL') || die;
 require_once($CFG->libdir . '/externallib.php');
 require_once($CFG->libdir . '/badgeslib.php');
 
+use core_badges\external\user_badge_exporter;
+
 /**
  * Badges external functions
  *
@@ -73,7 +75,7 @@ class core_badges_external extends external_api {
      * @throws moodle_exception
      */
     public static function get_user_badges($userid = 0, $courseid = 0, $page = 0, $perpage = 0, $search = '', $onlypublic = false) {
-        global $CFG, $USER;
+        global $CFG, $USER, $PAGE;
 
         $warnings = array();
 
@@ -122,25 +124,64 @@ class core_badges_external extends external_api {
 
         foreach ($userbadges as $badge) {
             $context = ($badge->type == BADGE_TYPE_SITE) ? context_system::instance() : context_course::instance($badge->courseid);
-            $badge->badgeurl = moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/',
-                                                                            'f1')->out(false);
-            // Return all the information if we are requesting our own badges.
-            // Or, if we have permissions for configuring badges in the badge context.
-            if ($USER->id == $user->id or has_capability('moodle/badges:configuredetails', $context)) {
-                $result['badges'][] = (array) $badge;
-            } else {
-                $result['badges'][] = array(
+            $canconfiguredetails = has_capability('moodle/badges:configuredetails', $context);
+
+            // If the user is viewing another user's badge and doesn't have the right capability return only part of the data.
+            if ($USER->id != $user->id and !$canconfiguredetails) {
+                $badge = (object) array(
+                    'id' => $badge->id,
                     'name' => $badge->name,
                     'description' => $badge->description,
-                    'badgeurl' => $badge->badgeurl,
                     'issuername' => $badge->issuername,
                     'issuerurl' => $badge->issuerurl,
                     'issuercontact' => $badge->issuercontact,
                     'uniquehash' => $badge->uniquehash,
                     'dateissued' => $badge->dateissued,
                     'dateexpire' => $badge->dateexpire,
+                    'version' => $badge->version,
+                    'language' => $badge->language,
+                    'imageauthorname' => $badge->imageauthorname,
+                    'imageauthoremail' => $badge->imageauthoremail,
+                    'imageauthorurl' => $badge->imageauthorurl,
+                    'imagecaption' => $badge->imagecaption,
                 );
             }
+
+            // Create a badge instance to be able to get the endorsement and other info.
+            $badgeinstance = new badge($badge->id);
+            $endorsement = $badgeinstance->get_endorsement();
+            $competencies = $badgeinstance->get_alignment();
+            $relatedbadges = $badgeinstance->get_related_badges();
+
+            if (!$canconfiguredetails) {
+                // Return only the properties visible by the user.
+
+                if (!empty($competencies)) {
+                    foreach ($competencies as $competency) {
+                        unset($competency->targetdescription);
+                        unset($competency->targetframework);
+                        unset($competency->targetcode);
+                    }
+                }
+
+                if (!empty($relatedbadges)) {
+                    foreach ($relatedbadges as $relatedbadge) {
+                        unset($relatedbadge->version);
+                        unset($relatedbadge->language);
+                        unset($relatedbadge->type);
+                    }
+                }
+            }
+
+            $related = array(
+                'context' => $context,
+                'endorsement' => $endorsement ? $endorsement : null,
+                'competencies' => $competencies,
+                'relatedbadges' => $relatedbadges,
+            );
+
+            $exporter = new user_badge_exporter($badge, $related);
+            $result['badges'][] = $exporter->export($PAGE->get_renderer('core'));
         }
 
         return $result;
@@ -156,34 +197,7 @@ class core_badges_external extends external_api {
         return new external_single_structure(
             array(
                 'badges' => new external_multiple_structure(
-                    new external_single_structure(
-                        array(
-                            'id' => new external_value(PARAM_INT, 'Badge id.', VALUE_OPTIONAL),
-                            'name' => new external_value(PARAM_TEXT, 'Badge name.'),
-                            'description' => new external_value(PARAM_NOTAGS, 'Badge description.'),
-                            'badgeurl' => new external_value(PARAM_URL, 'Badge URL.'),
-                            'timecreated' => new external_value(PARAM_INT, 'Time created.', VALUE_OPTIONAL),
-                            'timemodified' => new external_value(PARAM_INT, 'Time modified.', VALUE_OPTIONAL),
-                            'usercreated' => new external_value(PARAM_INT, 'User created.', VALUE_OPTIONAL),
-                            'usermodified' => new external_value(PARAM_INT, 'User modified.', VALUE_OPTIONAL),
-                            'issuername' => new external_value(PARAM_NOTAGS, 'Issuer name.'),
-                            'issuerurl' => new external_value(PARAM_URL, 'Issuer URL.'),
-                            'issuercontact' => new external_value(PARAM_RAW, 'Issuer contact.'),
-                            'expiredate' => new external_value(PARAM_INT, 'Expire date.', VALUE_OPTIONAL),
-                            'expireperiod' => new external_value(PARAM_INT, 'Expire period.', VALUE_OPTIONAL),
-                            'type' => new external_value(PARAM_INT, 'Type.', VALUE_OPTIONAL),
-                            'courseid' => new external_value(PARAM_INT, 'Course id.', VALUE_OPTIONAL),
-                            'message' => new external_value(PARAM_RAW, 'Message.', VALUE_OPTIONAL),
-                            'messagesubject' => new external_value(PARAM_TEXT, 'Message subject.', VALUE_OPTIONAL),
-                            'attachment' => new external_value(PARAM_INT, 'Attachment.', VALUE_OPTIONAL),
-                            'status' => new external_value(PARAM_INT, 'Status.', VALUE_OPTIONAL),
-                            'issuedid' => new external_value(PARAM_INT, 'Issued id.', VALUE_OPTIONAL),
-                            'uniquehash' => new external_value(PARAM_ALPHANUM, 'Unique hash.'),
-                            'dateissued' => new external_value(PARAM_INT, 'Date issued.'),
-                            'dateexpire' => new external_value(PARAM_INT, 'Date expire.'),
-                            'visible' => new external_value(PARAM_INT, 'Visible.', VALUE_OPTIONAL),
-                        )
-                    )
+                    user_badge_exporter::get_read_structure()
                 ),
                 'warnings' => new external_warnings(),
             )
diff --git a/badges/classes/external/competency_exporter.php b/badges/classes/external/competency_exporter.php
new file mode 100644 (file)
index 0000000..3eca46a
--- /dev/null
@@ -0,0 +1,94 @@
+<?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/>.
+
+/**
+ * Contains competency class for displaying a badge competency.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_badges\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+
+/**
+ * Class for displaying a badge competency.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class competency_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'id' => [
+                'type' => PARAM_INT,
+                'description' => 'Competency id',
+            ],
+            'badgeid' => [
+                'type' => PARAM_INT,
+                'description' => 'Badge id',
+            ],
+            'targetname' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Target name',
+            ],
+            'targeturl' => [
+                'type' => PARAM_URL,
+                'description' => 'Target URL',
+            ],
+            'targetdescription' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Target description',
+                'null' => NULL_ALLOWED,
+                'optional' => true,
+            ],
+            'targetframework' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Target framework',
+                'null' => NULL_ALLOWED,
+                'optional' => true,
+            ],
+            'targetcode' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Target code',
+                'null' => NULL_ALLOWED,
+                'optional' => true,
+            ]
+        ];
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return array(
+            'context' => 'context',
+        );
+    }
+}
diff --git a/badges/classes/external/endorsement_exporter.php b/badges/classes/external/endorsement_exporter.php
new file mode 100644 (file)
index 0000000..80e4dde
--- /dev/null
@@ -0,0 +1,95 @@
+<?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/>.
+
+/**
+ * Contains endorsement class for displaying a badge endorsement.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_badges\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+
+/**
+ * Class for displaying a badge endorsement.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class endorsement_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'id' => [
+                'type' => PARAM_INT,
+                'description' => 'Endorsement id',
+            ],
+            'badgeid' => [
+                'type' => PARAM_INT,
+                'description' => 'Badge id',
+            ],
+            'issuername' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Endorsement issuer name',
+            ],
+            'issuerurl' => [
+                'type' => PARAM_URL,
+                'description' => 'Endorsement issuer URL',
+            ],
+            'issueremail' => [
+                'type' => PARAM_RAW,
+                'description' => 'Endorsement issuer email',
+            ],
+            'claimid' => [
+                'type' => PARAM_URL,
+                'description' => 'Claim URL',
+                'null' => NULL_ALLOWED,
+            ],
+            'claimcomment' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Claim comment',
+                'null' => NULL_ALLOWED,
+            ],
+            'dateissued' => [
+                'type' => PARAM_INT,
+                'description' => 'Date issued',
+                'default' => 0,
+            ]
+        ];
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return array(
+            'context' => 'context',
+        );
+    }
+}
diff --git a/badges/classes/external/related_info_exporter.php b/badges/classes/external/related_info_exporter.php
new file mode 100644 (file)
index 0000000..83bee1e
--- /dev/null
@@ -0,0 +1,85 @@
+<?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/>.
+
+/**
+ * Contains related class for displaying information of a related badge.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_badges\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+
+/**
+ * Class for displaying information of a related badge.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class related_info_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'id' => [
+                'type' => PARAM_INT,
+                'description' => 'Badge id',
+            ],
+            'name' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Badge name',
+            ],
+            'version' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Version',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'language' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Language',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'type' => [
+                'type' => PARAM_INT,
+                'description' => 'Type',
+                'optional' => true,
+            ],
+        ];
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return array(
+            'context' => 'context',
+        );
+    }
+}
diff --git a/badges/classes/external/user_badge_exporter.php b/badges/classes/external/user_badge_exporter.php
new file mode 100644 (file)
index 0000000..0cc7269
--- /dev/null
@@ -0,0 +1,311 @@
+<?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/>.
+
+/**
+ * Contains user badge class for displaying a badge issued to a user.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_badges\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use renderer_base;
+use moodle_url;
+use core_badges\external\endorsement_exporter;
+use core_badges\external\competency_exporter;
+use core_badges\external\related_info_exporter;
+
+/**
+ * Class for displaying a badge issued to a user.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_badge_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'id' => [
+                'type' => PARAM_INT,
+                'description' => 'Badge id',
+                'optional' => true,
+            ],
+            'name' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Badge name',
+            ],
+            'description' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Badge description',
+                'null' => NULL_ALLOWED,
+            ],
+            'timecreated' => [
+                'type' => PARAM_INT,
+                'description' => 'Time created',
+                'optional' => true,
+                'default' => 0,
+            ],
+            'timemodified' => [
+                'type' => PARAM_INT,
+                'description' => 'Time modified',
+                'optional' => true,
+                'default' => 0,
+            ],
+            'usercreated' => [
+                'type' => PARAM_INT,
+                'description' => 'User created',
+                'optional' => true,
+            ],
+            'usermodified' => [
+                'type' => PARAM_INT,
+                'description' => 'User modified',
+                'optional' => true,
+            ],
+            'issuername' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Issuer name',
+            ],
+            'issuerurl' => [
+                'type' => PARAM_URL,
+                'description' => 'Issuer URL',
+            ],
+            'issuercontact' => [
+                'type' => PARAM_RAW,
+                'description' => 'Issuer contact',
+                'null' => NULL_ALLOWED,
+            ],
+            'expiredate' => [
+                'type' => PARAM_INT,
+                'description' => 'Expire date',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'expireperiod' => [
+                'type' => PARAM_INT,
+                'description' => 'Expire period',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'type' => [
+                'type' => PARAM_INT,
+                'description' => 'Type',
+                'optional' => true,
+                'default' => 1,
+            ],
+            'courseid' => [
+                'type' => PARAM_INT,
+                'description' => 'Course id',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'message' => [
+                'type' => PARAM_RAW,
+                'description' => 'Message',
+                'optional' => true,
+            ],
+            'messagesubject' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Message subject',
+                'optional' => true,
+            ],
+            'attachment' => [
+                'type' => PARAM_INT,
+                'description' => 'Attachment',
+                'optional' => true,
+                'default' => 1,
+            ],
+            'notification' => [
+                'type' => PARAM_INT,
+                'description' => 'Whether to notify when badge is awarded',
+                'optional' => true,
+                'default' => 1,
+            ],
+            'nextcron' => [
+                'type' => PARAM_INT,
+                'description' => 'Next cron',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'status' => [
+                'type' => PARAM_INT,
+                'description' => 'Status',
+                'optional' => true,
+                'default' => 0,
+            ],
+            'issuedid' => [
+                'type' => PARAM_INT,
+                'description' => 'Issued id',
+                'optional' => true,
+            ],
+            'uniquehash' => [
+                'type' => PARAM_ALPHANUM,
+                'description' => 'Unique hash',
+            ],
+            'dateissued' => [
+                'type' => PARAM_INT,
+                'description' => 'Date issued',
+                'default' => 0,
+            ],
+            'dateexpire' => [
+                'type' => PARAM_INT,
+                'description' => 'Date expire',
+                'null' => NULL_ALLOWED,
+            ],
+            'visible' => [
+                'type' => PARAM_INT,
+                'description' => 'Visible',
+                'optional' => true,
+                'default' => 0,
+            ],
+            'email' => [
+                'type' => PARAM_TEXT,
+                'description' => 'User email',
+                'optional' => true,
+            ],
+            'version' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Version',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'language' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Language',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'imageauthorname' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Name of the image author',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'imageauthoremail' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Email of the image author',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'imageauthorurl' => [
+                'type' => PARAM_URL,
+                'description' => 'URL of the image author',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'imagecaption' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Caption of the image',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+        ];
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return array(
+            'context' => 'context',
+            'endorsement' => 'stdClass?',
+            'competencies' => 'stdClass[]',
+            'relatedbadges' => 'stdClass[]',
+        );
+    }
+
+    /**
+     * Return the list of additional properties.
+     *
+     * @return array
+     */
+    protected static function define_other_properties() {
+        return [
+            'badgeurl' => [
+                'type' => PARAM_URL,
+                'description' => 'Badge URL',
+            ],
+            'endorsement' => [
+                'type' => endorsement_exporter::read_properties_definition(),
+                'description' => 'Badge endorsement',
+                'optional' => true,
+            ],
+            'competencies' => [
+                'type' => competency_exporter::read_properties_definition(),
+                'description' => 'Badge competencies (alignment)',
+                'multiple' => true,
+            ],
+            'relatedbadges' => [
+                'type' => related_info_exporter::read_properties_definition(),
+                'description' => 'Related badges',
+                'multiple' => true,
+            ]
+        ];
+    }
+
+    /**
+     * Get the additional values to inject while exporting.
+     *
+     * @param renderer_base $output The renderer.
+     * @return array Keys are the property names, values are their values.
+     */
+    protected function get_other_values(renderer_base $output) {
+        $context = $this->related['context'];
+        $endorsement = $this->related['endorsement'];
+        $competencies = $this->related['competencies'];
+        $relatedbadges = $this->related['relatedbadges'];
+
+        $values = array(
+            'badgeurl' => moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $this->data->id, '/',
+                'f1')->out(false),
+            'competencies' => array(),
+            'relatedbadges' => array(),
+        );
+
+        if ($endorsement) {
+            $endorsementexporter = new endorsement_exporter($endorsement, array('context' => $context));
+            $values['endorsement'] = $endorsementexporter->export($output);
+        }
+
+        if (!empty($competencies)) {
+            foreach ($competencies as $competency) {
+                $competencyexporter = new competency_exporter($competency, array('context' => $context));
+                $values['competencies'][] = $competencyexporter->export($output);
+            }
+        }
+
+        if (!empty($relatedbadges)) {
+            foreach ($relatedbadges as $badge) {
+                $relatedexporter = new related_info_exporter($badge, array('context' => $context));
+                $values['relatedbadges'][] = $relatedexporter->export($output);
+            }
+        }
+
+        return $values;
+    }
+}
index 77db1a4..9ebf174 100644 (file)
@@ -85,6 +85,12 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
         $badge->attachment = 1;
         $badge->notification = 0;
         $badge->status = BADGE_STATUS_ACTIVE;
+        $badge->version = '1';
+        $badge->language = 'en';
+        $badge->imageauthorname = 'Image author';
+        $badge->imageauthoremail = 'imageauthor@example.com';
+        $badge->imageauthorurl = 'http://image-author-url.domain.co.nz';
+        $badge->imagecaption = 'Caption';
 
         $badgeid = $DB->insert_record('badge', $badge, true);
         $badge = new badge($badgeid);
@@ -93,6 +99,34 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
         // Hack the database to adjust the time each badge was issued.
         $DB->set_field('badge_issued', 'dateissued', $now - 11, array('userid' => $this->student->id, 'badgeid' => $badgeid));
 
+        // Add an endorsement for the badge.
+        $endorsement = new stdClass();
+        $endorsement->badgeid = $badgeid;
+        $endorsement->issuername = 'Issuer name';
+        $endorsement->issuerurl = 'http://endorsement-issuer-url.domain.co.nz';
+        $endorsement->issueremail = 'endorsementissuer@example.com';
+        $endorsement->claimid = 'http://claim-url.domain.co.nz';
+        $endorsement->claimcomment = 'Claim comment';
+        $endorsement->dateissued = $now;
+        $badge->save_endorsement($endorsement);
+
+        // Add 2 competencies.
+        $competency = new stdClass();
+        $competency->badgeid = $badgeid;
+        $competency->targetname = 'Competency 1';
+        $competency->targeturl = 'http://c1-target-url.domain.co.nz';
+        $competency->targetdescription = 'C1 target description';
+        $competency->targetframework = 'C1 framework';
+        $competency->targetcode = 'C1 code';
+        $badge->save_alignment($competency);
+
+        $competency->targetname = 'Competency 2';
+        $competency->targeturl = 'http://c2-target-url.domain.co.nz';
+        $competency->targetdescription = 'C2 target description';
+        $competency->targetframework = 'C2 framework';
+        $competency->targetcode = 'C2 code';
+        $badge->save_alignment($competency);
+
         // Now a course badge.
         $badge->id = null;
         $badge->name = "Test badge course";
@@ -100,12 +134,15 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
         $badge->type = BADGE_TYPE_COURSE;
         $badge->courseid = $this->course->id;
 
-        $badgeid = $DB->insert_record('badge', $badge, true);
-        $badge = new badge($badgeid);
+        $coursebadgeid = $DB->insert_record('badge', $badge, true);
+        $badge = new badge($coursebadgeid);
         $badge->issue($this->student->id, true);
 
         // Hack the database to adjust the time each badge was issued.
-        $DB->set_field('badge_issued', 'dateissued', $now - 11, array('userid' => $this->student->id, 'badgeid' => $badgeid));
+        $DB->set_field('badge_issued', 'dateissued', $now - 11, array('userid' => $this->student->id, 'badgeid' => $coursebadgeid));
+
+        // Make the site badge a related badge.
+        $badge->add_related_badges(array($badgeid));
     }
 
     /**
@@ -116,15 +153,65 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
 
         $this->setUser($this->student);
 
+        $badges = (array) badges_get_user_badges($this->student->id);
+        $expectedbadges = array();
+        $coursebadge = null;
+
+        foreach ($badges as $badge) {
+            $context = ($badge->type == BADGE_TYPE_SITE) ? context_system::instance() : context_course::instance($badge->courseid);
+            $badge->badgeurl = moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/',
+                                                                            'f1')->out(false);
+
+            // Get the endorsement, competencies and related badges.
+            $badgeinstance = new badge($badge->id);
+            $endorsement = $badgeinstance->get_endorsement();
+            $competencies = $badgeinstance->get_alignment();
+            $relatedbadges = $badgeinstance->get_related_badges();
+            $badge->competencies = array();
+            $badge->relatedbadges = array();
+
+            if ($endorsement) {
+                $badge->endorsement = (array) $endorsement;
+            }
+
+            if (!empty($competencies)) {
+                foreach ($competencies as $competency) {
+                    // Students cannot see some fields of the competencies.
+                    unset($competency->targetdescription);
+                    unset($competency->targetframework);
+                    unset($competency->targetcode);
+
+                    $badge->competencies[] = (array) $competency;
+                }
+            }
+
+            if (!empty($relatedbadges)) {
+                foreach ($relatedbadges as $relatedbadge) {
+                    // Students cannot see some fields of the related badges.
+                    unset($relatedbadge->version);
+                    unset($relatedbadge->language);
+                    unset($relatedbadge->type);
+
+                    $badge->relatedbadges[] = (array) $relatedbadge;
+                }
+            }
+
+            $expectedbadges[] = (array) $badge;
+            if (isset($badge->courseid)) {
+                // Save the course badge to be able to compare it in our tests.
+                $coursebadge = (array) $badge;
+            }
+        }
+
         $result = core_badges_external::get_user_badges();
         $result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
-        $this->assertCount(2, $result['badges']);
+        $this->assertEquals($expectedbadges, $result['badges']);
 
         // Pagination and filtering.
         $result = core_badges_external::get_user_badges(0, $this->course->id, 0, 1, '', true);
         $result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
         $this->assertCount(1, $result['badges']);
-        $this->assertEquals($this->course->id, $result['badges'][0]['courseid']);
+        $this->assertEquals($coursebadge, $result['badges'][0]);
     }
 
     /**
@@ -143,6 +230,15 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
         foreach ($result['badges'] as $badge) {
             if (isset($badge['type']) and $badge['type'] == BADGE_TYPE_COURSE) {
                 $this->assertTrue(isset($badge['message']));
+
+                // Check that we have permissions to see all the data in competencies and related badges.
+                foreach ($badge['competencies'] as $competency) {
+                    $this->assertTrue(isset($competency['targetdescription']));
+                }
+
+                foreach ($badge['relatedbadges'] as $relatedbadge) {
+                    $this->assertTrue(isset($relatedbadge['type']));
+                }
             } else {
                 $this->assertFalse(isset($badge['message']));
             }
index 8dd1f87..8b20394 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /badges/*,
 information provided here is intended especially for developers.
 
+=== 3.6 ===
+
+* The WebService core_badges_get_user_badges now returns more information for each badge: notification, nextcron, email, version, language, imageauthorname, imageauthoremail, imageauthorurl, imagecaption, endorsement, competencies and relatedbadges.
+
 === 2.7 ===
 
 * get_completed_criteria_sql() - This method was added to award_criteria class and must be overriden
index 5b95e9c..c198dd0 100644 (file)
@@ -33,6 +33,8 @@
                 {{> block_recentlyaccesseditems/placeholder-item }}
                 {{> block_recentlyaccesseditems/placeholder-item }}
                 {{> block_recentlyaccesseditems/placeholder-item }}
+                {{> block_recentlyaccesseditems/placeholder-item }}
+                {{> block_recentlyaccesseditems/placeholder-item }}
             </div>
         </div>
     </div>
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 b4ed03b..a409cf1 100644 (file)
@@ -26,6 +26,7 @@ defined('MOODLE_INTERNAL') || die;
 
 require_once($CFG->libdir.'/completionlib.php');
 require_once($CFG->libdir.'/filelib.php');
+require_once($CFG->libdir.'/datalib.php');
 require_once($CFG->dirroot.'/course/format/lib.php');
 
 define('COURSE_MAX_LOGS_PER_PAGE', 1000);       // Records.
@@ -3665,6 +3666,8 @@ function course_view($context, $sectionnumber = 0) {
 
     $event = \core\event\course_viewed::create($eventdata);
     $event->trigger();
+
+    user_accesstime_log($context->instanceid);
 }
 
 /**
index a94b903..626222c 100644 (file)
@@ -43,7 +43,7 @@ $string['cannotunzipfile'] = 'No s\'ha pogut descomprimir el fitxer';
 $string['componentisuptodate'] = 'El component està actualitzat';
 $string['dmlexceptiononinstall'] = '<p>S\'ha produït un error de la base de dades [{$a->errorcode}].<br />{$a->debuginfo}</p>';
 $string['downloadedfilecheckfailed'] = 'Ha fallat la comprovació del fitxer baixat';
-$string['invalidmd5'] = 'L\'md5 no és vàlid. Torneu-ho a provar';
+$string['invalidmd5'] = 'L\'md5 no és vàlid. Torneu a provar-ho';
 $string['missingrequiredfield'] = 'Falta algun camp necessari';
 $string['remotedownloaderror'] = '<p>No s\'ha pogut baixar el component al vostre servidor. Verifiqueu els paràmetres del servidor intermediari. Es recomana vivament l\'extensió cURL de PHP.</p>
 <p>Haureu de baixar manualment el fitxer <a href="{$a->url}">{$a->url}</a>, copiar-lo a la ubicació «{$a->dest}» del vostre servidor i descomprimir-lo allí.</p>';
index 11d79f3..6219a09 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 unlocked. Locking it will prevent any further changes. Are you sure you wish to continue?';
+$string['confirmcontextunlock'] = '{$a->contextname} is currently locked. Unlocking it will allow users to make changes. Are you sure you wish to continue?';
 $string['confirmdeletecomments'] = 'You are about to delete comments, are you sure?';
 $string['confirmed'] = 'Confirmed';
+$string['contextlocking'] = 'Context locking';
+$string['contextlocking_desc'] = 'This setting allows you to lock categories, courses, activites, and blocks within the site by removing all write-access to those locations.';
+$string['contextlockappliestoadmin'] = 'Context locking applies to administrators';
+$string['contextlockappliestoadmin_desc'] = 'This setting allows administrators to make changes in any context which is locked.';
 $string['cookiehttponly'] = 'Only http cookies';
 $string['cookiesecure'] = 'Secure cookies only';
 $string['country'] = 'Default country';
@@ -718,6 +724,10 @@ $string['maintenancemodeisscheduled'] = 'This site will be switched to maintenan
 $string['maintenancemodeisscheduledlong'] = 'This site will be switched to maintenance mode in {$a->hour} hours {$a->min} mins {$a->sec} secs';
 $string['maintfileopenerror'] = 'Error opening maintenance files!';
 $string['maintinprogress'] = 'Maintenance is in progress...';
+$string['managecontextlock'] = 'Lock this context';
+$string['managecontextlocklocked'] = '{$a->contextname}, and all of its children are now locked.';
+$string['managecontextlockunlocked'] = '{$a->contextname}, and all of its children are now unlocked.';
+$string['managecontextunlock'] = 'Unlock this context';
 $string['manageformats'] = 'Manage course formats';
 $string['manageformatsgotosettings'] = 'Default format can be changed in {$a}';
 $string['managelang'] = 'Manage';
index 485cf39..670c6c5 100644 (file)
@@ -52,6 +52,7 @@ $string['emailtagline'] = 'This is a copy of a message sent to you at "{$a->site
 $string['enabled'] = 'Enabled';
 $string['errorcallingprocessor'] = 'Error calling defined output';
 $string['errortranslatingdefault'] = 'Error translating default setting provided by plugin, using system defaults instead.';
+$string['eventgroupmessagesent'] = 'Group message sent';
 $string['eventnotificationviewed'] = 'Notification viewed';
 $string['eventnotificationsent'] = 'Notification sent';
 $string['eventmessagecontactadded'] = 'Message contact added';
@@ -185,6 +186,7 @@ $string['touserdoesntexist'] = 'You can not send a message to a user id ({$a}) t
 $string['unblockcontact'] = 'Unblock contact';
 $string['unknownuser'] = 'Unknown user';
 $string['unreadnotification'] = 'Unread notification: {$a}';
+$string['unreadnewgroupconversationmessage'] = 'New message from {$a->name} in {$a->conversationname}';
 $string['unreadnewmessage'] = 'New message from {$a}';
 $string['usercantbemessaged'] = 'You can\'t message {$a} due to their message preferences. Try adding them as a contact.';
 $string['viewfullnotification'] = 'View full notification';
@@ -200,4 +202,4 @@ $string['eventmessagecontactblocked'] = 'Message contact blocked';
 $string['eventmessagecontactunblocked'] = 'Message contact unblocked';
 $string['messagingdisabled'] = 'Messaging is disabled on this site, emails will be sent instead';
 $string['userisblockingyou'] = 'This user has blocked you from sending messages to them.';
-$string['userisblockingyounoncontact'] = '{$a} only accepts messages from their contacts.';
\ No newline at end of file
+$string['userisblockingyounoncontact'] = '{$a} only accepts messages from their contacts.';
index d0d2003..acb886d 100644 (file)
@@ -411,6 +411,7 @@ $string['site:maintenanceaccess'] = 'Access site while in maintenance mode';
 $string['site:manageallmessaging'] = 'Add, remove, block and unblock contacts for any user';
 $string['site:manageblocks'] = 'Manage blocks on a page';
 $string['site:messageanyuser'] = 'Bypass user privacy preferences for messaging any user';
+$string['site:managecontextlocks'] = 'Manage locking of 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 3806563..0f48780 100644 (file)
  *
  * Attach this JS module to this list:
  *
- * define(['jquery', 'core/sortable_list'], function($, SortableList) {
+ * Space between define and ( critical in comment but not allowed in code in order to function
+ * correctly with Moodle's requirejs.php
+ *
+ * define (['jquery', 'core/sortable_list'], function($, SortableList) {
  *     var list = new SortableList('ul.my-awesome-list'); // source list (usually <ul> or <tbody>) - selector or element
  *
  *     // Listen to the events when element is dragged.
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;
     }
 
diff --git a/lib/classes/event/group_message_sent.php b/lib/classes/event/group_message_sent.php
new file mode 100644 (file)
index 0000000..c02d12d
--- /dev/null
@@ -0,0 +1,157 @@
+<?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/>.
+
+/**
+ * Group message sent event.
+ *
+ * @package    core
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Group message sent event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - int courseid: the id of the related course.
+ *      - int conversationid: the id of the conversation in which the message was sent.
+ * }
+ *
+ * @package    core
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class group_message_sent extends base {
+    /**
+     * Create event using ids.
+     * @param int $userfromid
+     * @param int $conversationid
+     * @param int $messageid
+     * @param int $courseid course id the event is related with.
+     * @return message_sent
+     */
+    public static function create_from_ids(int $userfromid, int $conversationid, int $messageid, int $courseid) {
+        // We may be sending a message from the 'noreply' address, which means we are not actually sending a
+        // message from a valid user. In this case, we will set the userid to 0.
+        // Check if the userid is valid.
+        if (!\core_user::is_real_user($userfromid)) {
+            $userfromid = 0;
+        }
+
+        $event = self::create([
+            'objectid' => $messageid,
+            'userid' => $userfromid,
+            'context' => \context_system::instance(),
+            'other' => [
+                'courseid' => $courseid,
+                'conversationid' => $conversationid
+            ]
+        ]);
+
+        return $event;
+    }
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'messages';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventgroupmessagesent', 'message');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        // There currently isn't a way to link back from a 'group message sent' event to a conversation.
+        // So, just return the user to the index page.
+        return new \moodle_url('/message/index.php');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        $conversationid = $this->other['conversationid'];
+
+        // Check if we are sending from a valid user.
+        if (\core_user::is_real_user($this->userid)) {
+
+            return "The user with id '$this->userid' sent a message with id '$this->objectid' to the conversation " .
+                   "with id '$conversationid'.";
+        }
+
+        return "A message with id '$this->objectid' was sent by the system to the conversation with id '$conversationid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['courseid'])) {
+            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        }
+        if (!isset($this->other['conversationid'])) {
+            throw new \coding_exception('The \'conversationid\' value must be set in other.');
+        }
+    }
+
+    /**
+     * Get the object this event maps to.
+     *
+     * @return array|string object id mapping.
+     */
+    public static function get_objectid_mapping() {
+        return ['db' => 'messages', 'restore' => base::NOT_MAPPED];
+    }
+
+    /**
+     * Get the item mappings for the 'other' fields for this event.
+     *
+     * @return array the array of other fields, mapped.
+     */
+    public static function get_other_mapping() {
+        $othermapped = [];
+        $othermapped['courseid'] = ['db' => 'course', 'restore' => base::NOT_MAPPED];
+        $othermapped['conversationid'] = ['db' => 'message_conversations', 'restore' => base::NOT_MAPPED];
+        return $othermapped;
+    }
+}
index 17192f3..4ffc62e 100644 (file)
@@ -45,6 +45,247 @@ class manager {
     /** @var array buffer of pending messages */
     protected static $buffer = array();
 
+    /** @var array buffer of pending messages to conversations */
+    protected static $convmessagebuffer = array();
+
+    /**
+     * Used for calling processors, and generating event data when sending a message to a conversation.
+     *
+     * This is ONLY used for messages of type 'message' (notification=0), and is responsible for:
+     *
+     * 1. generation of per-user event data (to pass to processors)
+     * 2. generation of the processors for each recipient member of the conversation
+     * 3. calling said processors for each member, passing in the per-user (local) eventdata.
+     * 4. generation of an appropriate event for the message send, depending on the conversation type
+     *   - messages to individual conversations generate a 'message_sent' event (as per legacy send_message())
+     *   - messages to group conversations generate a 'group_message_sent' event.
+     *
+     * @param message $eventdata
+     * @param \stdClass $savemessage
+     * @return int
+     */
+    public static function send_message_to_conversation(message $eventdata, \stdClass $savemessage) : int {
+        global $DB, $CFG, $SITE;
+
+        if (empty($eventdata->convid)) {
+            throw new \moodle_exception("Message is not being sent to a conversation. Please check event data.");
+        }
+
+        // Fetch default (site) preferences.
+        $defaultpreferences = get_message_output_default_preferences();
+        $preferencebase = $eventdata->component.'_'.$eventdata->name;
+
+        // Because we're dealing with multiple recipients, we need to send a localised (per-user) version of the eventdata to each
+        // processor, because of things like the language-specific subject. We're going to modify this, for each recipient member.
+        // Any time we're modifying the event data here, we should be using the localised version.
+        // This localised version is based on the generic event data, but we should keep that object intact, so we clone it.
+        $localisedeventdata = clone $eventdata;
+
+        // Get user records for all members of the conversation.
+        $sql = "SELECT u.*
+                  FROM {message_conversation_members} mcm
+                  JOIN {user} u
+                    ON (mcm.conversationid = :convid AND u.id = mcm.userid)
+              ORDER BY u.id desc";
+        $members = $DB->get_records_sql($sql, ['convid' => $eventdata->convid]);
+        if (empty($members)) {
+            throw new \moodle_exception("Conversation has no members or does not exist.");
+        }
+
+        if (!is_object($localisedeventdata->userfrom)) {
+            $localisedeventdata->userfrom = $members[$localisedeventdata->userfrom];
+        }
+
+        // This should now hold only the other users (recipients).
+        unset($members[$localisedeventdata->userfrom->id]);
+        $otherusers = $members;
+
+        // Get conversation type and name. We'll use this to determine which message subject to generate, depending on type.
+        $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type, name');
+
+        // We treat individual conversations the same as any direct message with 'userfrom' and 'userto' specified.
+        // We know the other user, so set the 'userto' field so that the event code will get access to this field.
+        // If this was a legacy caller (eventdata->userto is set), then use that instead, as we want to use the fields specified
+        // in that object instead of using one fetched from the DB.
+        $legacymessage = false;
+        if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+            if (isset($eventdata->userto)) {
+                $legacymessage = true;
+            } else {
+                $otheruser = reset($otherusers);
+                $eventdata->userto = $otheruser;
+            }
+        }
+
+        // Fetch enabled processors.
+        // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
+        $processors = array_filter(get_message_processors(false), function($processor) {
+            if ($processor->object->force_process_messages()) {
+                return true;
+            }
+
+            return ($processor->enabled && $processor->configured);
+        });
+
+        // For each member of the conversation, other than the sender:
+        // 1. Set recipient specific event data (language specific, user prefs, etc)
+        // 2. Generate recipient specific processor list
+        // 3. Call send_message() to pass the message to processors and generate the relevant per-user events.
+        $eventprocmaps = []; // Init the event/processors buffer.
+        foreach ($otherusers as $recipient) {
+            // If this message was a legacy (1:1) message, then we use the userto.
+            if ($legacymessage) {
+                $recipient = $eventdata->userto;
+            }
+
+            $usertoisrealuser = (\core_user::is_real_user($recipient->id) != false);
+
+            // Using string manager directly so that strings in the message will be in the message recipients language rather than
+            // the sender's.
+            if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+                $localisedeventdata->subject = get_string_manager()->get_string('unreadnewmessage', 'message',
+                    fullname($localisedeventdata->userfrom), $recipient->lang);
+            } else if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
+                $stringdata = (object) ['name' => fullname($localisedeventdata->userfrom), 'conversationname' => $conv->name];
+                $localisedeventdata->subject = get_string_manager()->get_string('unreadnewgroupconversationmessage', 'message',
+                    $stringdata, $recipient->lang);
+            }
+
+            // Spoof the userto based on the current member id.
+            $localisedeventdata->userto = $recipient;
+
+            $s = new \stdClass();
+            $s->sitename = format_string($SITE->shortname, true, array('context' => \context_course::instance(SITEID)));
+            // When the new interface lands, the URL may be reintroduced, but for now it isn't supported, so just hit the index.
+            $s->url = $CFG->wwwroot.'/message/index.php';
+            $emailtagline = get_string_manager()->get_string('emailtagline', 'message', $s, $recipient->lang);
+
+            $localisedeventdata->fullmessage = $eventdata->fullmessage;
+            $localisedeventdata->fullmessagehtml = $eventdata->fullmessagehtml;
+            if (!empty($localisedeventdata->fullmessage)) {
+                $localisedeventdata->fullmessage .= "\n\n---------------------------------------------------------------------\n"
+                    . $emailtagline;
+            }
+            if (!empty($localisedeventdata->fullmessagehtml)) {
+                $localisedeventdata->fullmessagehtml .=
+                    "<br><br>---------------------------------------------------------------------<br>" . $emailtagline;
+            }
+
+            // If recipient is internal user (noreply user), and emailstop is set then don't send any msg.
+            if (!$usertoisrealuser && !empty($recipient->emailstop)) {
+                debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL);
+                return false;
+            }
+
+            // Set the online state.
+            if (isset($CFG->block_online_users_timetosee)) {
+                $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+            } else {
+                $timetoshowusers = 300;
+            }
+
+            // Work out if the user is logged in or not.
+            $userstate = 'loggedoff';
+            if (!empty($localisedeventdata->userto->lastaccess)
+                    && (time() - $timetoshowusers) < $localisedeventdata->userto->lastaccess) {
+                $userstate = 'loggedin';
+            }
+
+            // Fill in the array of processors to be used based on default and user preferences.
+            $processorlist = [];
+            foreach ($processors as $processor) {
+                // Skip adding processors for internal user, if processor doesn't support sending message to internal user.
+                if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) {
+                    continue;
+                }
+
+                // First find out permissions.
+                $defaultpreference = $processor->name.'_provider_'.$preferencebase.'_permitted';
+                if (isset($defaultpreferences->{$defaultpreference})) {
+                    $permitted = $defaultpreferences->{$defaultpreference};
+                } else {
+                    // MDL-25114 They supplied an $eventdata->component $eventdata->name combination which doesn't
+                    // exist in the message_provider table (thus there is no default settings for them).
+                    $preferrormsg = "Could not load preference $defaultpreference. Make sure the component and name you supplied
+                    to message_send() are valid.";
+                    throw new coding_exception($preferrormsg);
+                }
+
+                // Find out if user has configured this output.
+                // Some processors cannot function without settings from the user.
+                $userisconfigured = $processor->object->is_user_configured($recipient);
+
+                // DEBUG: notify if we are forcing unconfigured output.
+                if ($permitted == 'forced' && !$userisconfigured) {
+                    debugging('Attempt to force message delivery to user who has "'.$processor->name.'" output unconfigured',
+                        DEBUG_NORMAL);
+                }
+
+                // Populate the list of processors we will be using.
+                if (!$eventdata->notification && $processor->object->force_process_messages()) {
+                    $processorlist[] = $processor->name;
+                } else if ($permitted == 'forced' && $userisconfigured) {
+                    // An admin is forcing users to use this message processor. Use this processor unconditionally.
+                    $processorlist[] = $processor->name;
+                } else if ($permitted == 'permitted' && $userisconfigured && !$recipient->emailstop) {
+                    // User has not disabled notifications.
+                    // See if user set any notification preferences, otherwise use site default ones.
+                    $userpreferencename = 'message_provider_'.$preferencebase.'_'.$userstate;
+                    if ($userpreference = get_user_preferences($userpreferencename, null, $recipient)) {
+                        if (in_array($processor->name, explode(',', $userpreference))) {
+                            $processorlist[] = $processor->name;
+                        }
+                    } else if (isset($defaultpreferences->{$userpreferencename})) {
+                        if (in_array($processor->name, explode(',', $defaultpreferences->{$userpreferencename}))) {
+                            $processorlist[] = $processor->name;
+                        }
+                    }
+                }
+            }
+            // Batch up the localised event data and processor list for all users into a local buffer.
+            $eventprocmaps[] = [clone($localisedeventdata), $processorlist];
+        }
+        // Then pass it off as one item of work, to be processed by send_conversation_message_to_processors(), which will
+        // handle all transaction buffering logic.
+        self::send_conversation_message_to_processors($eventprocmaps, $eventdata, $savemessage);
+
+        return $savemessage->id;
+    }
+
+    /**
+     * Takes a list of localised event data, and tries to send them to their respective member's message processors.
+     *
+     * Input format:
+     *  [CONVID => [$localisedeventdata, $savemessage, $processorlist], ].
+     *
+     * @param array $eventprocmaps the array of localised event data and processors for each member of the conversation.
+     * @param message $eventdata the original conversation message eventdata
+     * @param \stdClass $savemessage the saved message record.
+     * @throws \coding_exception
+     */
+    protected static function send_conversation_message_to_processors(array $eventprocmaps, message $eventdata,
+                                                                      \stdClass $savemessage) {
+        global $DB;
+
+        // We cannot communicate with external systems in DB transactions,
+        // buffer the messages if necessary.
+        if ($DB->is_transaction_started()) {
+            // Buffer this group conversation message and it's record.
+            self::$convmessagebuffer[] = [$eventprocmaps, $eventdata, $savemessage];
+            return;
+        }
+
+        // Send each localised version of the event data to each member's respective processors.
+        foreach ($eventprocmaps as $eventprocmap) {
+            $eventdata = $eventprocmap[0];
+            $processorlist = $eventprocmap[1];
+            self::call_processors($eventdata, $processorlist);
+        }
+
+        // Trigger event for sending a message or notification - we need to do this before marking as read!
+        self::trigger_message_events($eventdata, $savemessage);
+    }
+
     /**
      * Do the message sending.
      *
@@ -62,21 +303,7 @@ class manager {
 
         if (empty($processorlist)) {
             // Trigger event for sending a message or notification - we need to do this before marking as read!
-            if ($eventdata->notification) {
-                \core\event\notification_sent::create_from_ids(
-                    $eventdata->userfrom->id,
-                    $eventdata->userto->id,
-                    $savemessage->id,
-                    $eventdata->courseid
-                )->trigger();
-            } else { // Must be a message.
-                \core\event\message_sent::create_from_ids(
-                    $eventdata->userfrom->id,
-                    $eventdata->userto->id,
-                    $savemessage->id,
-                    $eventdata->courseid
-                )->trigger();
-            }
+            self::trigger_message_events($eventdata, $savemessage);
 
             if ($eventdata->notification or empty($CFG->messaging)) {
                 // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
@@ -110,7 +337,6 @@ class manager {
 
         // We cannot communicate with external systems in DB transactions,
         // buffer the messages if necessary.
-
         if ($DB->is_transaction_started()) {
             // We need to clone all objects so that devs may not modify it from outside later.
             $eventdata = clone($eventdata);
@@ -125,33 +351,11 @@ class manager {
             return $savemessage->id;
         }
 
-        foreach ($processorlist as $procname) {
-            // Let new messaging class add custom content based on the processor.
-            $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
-            $stdproc = new \stdClass();
-            $stdproc->name = $procname;
-            $processor = \core_message\api::get_processed_processor_object($stdproc);
-            if (!$processor->object->send_message($proceventdata)) {
-                debugging('Error calling message processor ' . $procname);
-            }
-        }
+        // Send the message to processors.
+        self::call_processors($eventdata, $processorlist);
 
         // Trigger event for sending a message or notification - we need to do this before marking as read!
-        if ($eventdata->notification) {
-            \core\event\notification_sent::create_from_ids(
-                $eventdata->userfrom->id,
-                $eventdata->userto->id,
-                $savemessage->id,
-                $eventdata->courseid
-            )->trigger();
-        } else { // Must be a message.
-            \core\event\message_sent::create_from_ids(
-                $eventdata->userfrom->id,
-                $eventdata->userto->id,
-                $savemessage->id,
-                $eventdata->courseid
-            )->trigger();
-        }
+        self::trigger_message_events($eventdata, $savemessage);
 
         if (empty($CFG->messaging)) {
             // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
@@ -173,7 +377,7 @@ class manager {
      * Note: to be used from DML layer only.
      */
     public static function database_transaction_commited() {
-        if (!self::$buffer) {
+        if (!self::$buffer && !self::$convmessagebuffer) {
             return;
         }
         self::process_buffer();
@@ -186,19 +390,86 @@ class manager {
      */
     public static function database_transaction_rolledback() {
         self::$buffer = array();
+        self::$convmessagebuffer = array();
     }
 
     /**
      * Sent out any buffered messages if necessary.
      */
     protected static function process_buffer() {
-        // Reset the buffer first in case we get exception from processor.
+        // Reset the buffers first in case we get exception from processor.
         $messages = self::$buffer;
         self::$buffer = array();
+        $convmessages = self::$convmessagebuffer;
+        self::$convmessagebuffer = array();
 
         foreach ($messages as $message) {
             list($eventdata, $savemessage, $processorlist) = $message;
             self::send_message_to_processors($eventdata, $savemessage, $processorlist);
         }
+
+        foreach ($convmessages as $convmessage) {
+            list($eventprocmap, $eventdata, $savemessage) = $convmessage;
+            self::send_conversation_message_to_processors($eventprocmap, $eventdata, $savemessage);
+        }
+    }
+
+    /**
+     * Trigger an appropriate message creation event, based on the supplied $eventdata and $savemessage.
+     *
+     * @param message $eventdata the eventdata for the message.
+     * @param \stdClass $savemessage the message record.
+     * @throws \coding_exception
+     */
+    protected static function trigger_message_events(message $eventdata, \stdClass $savemessage) {
+        global $DB;
+        if ($eventdata->notification) {
+            \core\event\notification_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
+            )->trigger();
+        } else { // Must be a message.
+            // If the message is a group conversation, then trigger the 'group_message_sent' event.
+            if ($eventdata->convid) {
+                $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type');
+                if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
+                    \core\event\group_message_sent::create_from_ids(
+                        $eventdata->userfrom->id,
+                        $eventdata->convid,
+                        $savemessage->id,
+                        $eventdata->courseid
+                    )->trigger();
+                    return;
+                }
+                // Individual type conversations fall through to the default 'message_sent' event.
+            }
+            \core\event\message_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
+            )->trigger();
+        }
+    }
+
+    /**
+     * For each processor, call it's send_message() method.
+     *
+     * @param message $eventdata the message object.
+     * @param array $processorlist the list of processors for a single user.
+     */
+    protected static function call_processors(message $eventdata, array $processorlist) {
+        foreach ($processorlist as $procname) {
+            // Let new messaging class add custom content based on the processor.
+            $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
+            $stdproc = new \stdClass();
+            $stdproc->name = $procname;
+            $processor = \core_message\api::get_processed_processor_object($stdproc);
+            if (!$processor->object->send_message($proceventdata)) {
+                debugging('Error calling message processor ' . $procname);
+            }
+        }
     }
 }
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 346ea3c..1f4483d 100644 (file)
@@ -1037,7 +1037,8 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_contacts',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for the contact list',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for the contact list',
         'type' => 'read',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
@@ -1046,7 +1047,8 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_messages',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for the messages',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for the messages',
         'type' => 'read',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
@@ -1055,7 +1057,8 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_get_most_recent_message',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for the most recent message',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for the most recent message',
         'type' => 'read',
         'ajax' => true,
     ),
@@ -1063,7 +1066,8 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_get_profile',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for the users\'s profile',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for the users\'s profile',
         'type' => 'read',
         'ajax' => true,
     ),
@@ -1191,6 +1195,16 @@ $functions = array(
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_message_send_messages_to_conversation' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'send_messages_to_conversation',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Send messages to an existing conversation between users',
+        'type' => 'write',
+        'capabilities' => 'moodle/site:sendmessage',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_message_get_conversation_messages' => array(
         'classname' => 'core_message_external',
         'methodname' => 'get_conversation_messages',
@@ -1545,7 +1559,8 @@ $functions = array(
         'description' => 'Set user preferences.',
         'type' => 'write',
         'capabilities' => 'moodle/site:config',
-        'ajax' => true
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_user_agree_site_policy' => array(
         'classname' => 'core_user_external',
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 66b2002..95d97f2 100644 (file)
@@ -55,7 +55,7 @@ require_once(__DIR__ . '/../message/lib.php');
  * @return mixed the integer ID of the new message or false if there was a problem with submitted data
  */
 function message_send(\core\message\message $eventdata) {
-    global $CFG, $DB;
+    global $CFG, $DB, $SITE;
 
     //new message ID to return
     $messageid = false;
@@ -74,21 +74,114 @@ function message_send(\core\message\message $eventdata) {
         $eventdata->notification = 1;
     }
 
-    if (!is_object($eventdata->userto)) {
-        $eventdata->userto = core_user::get_user($eventdata->userto);
-    }
     if (!is_object($eventdata->userfrom)) {
         $eventdata->userfrom = core_user::get_user($eventdata->userfrom);
     }
-    if (!$eventdata->userto) {
-        debugging('Attempt to send msg to unknown user', DEBUG_NORMAL);
-        return false;
-    }
     if (!$eventdata->userfrom) {
         debugging('Attempt to send msg from unknown user', DEBUG_NORMAL);
         return false;
     }
 
+    // Legacy messages (FROM a single user TO a single user) must be converted into conversation messages.
+    // Then, these will be passed through the conversation messages code below.
+    if (!$eventdata->notification && !$eventdata->convid) {
+        // If messaging is disabled at the site level, then the 'instantmessage' provider is always disabled.
+        // Given this is the only 'message' type message provider, we can exit now if this is the case.
+        // Don't waste processing time trying to work out the other conversation member, if it's an individual
+        // conversation, just throw a generic debugging notice and return.
+        if (empty($CFG->messaging) || $eventdata->component !== 'moodle' || $eventdata->name !== 'instantmessage') {
+            debugging('Attempt to send msg from a provider '.$eventdata->component.'/'.$eventdata->name.
+                ' that is inactive or not allowed for the user id='.$eventdata->userto->id, DEBUG_NORMAL);
+            return false;
+        }
+
+        if (!is_object($eventdata->userto)) {
+            $eventdata->userto = core_user::get_user($eventdata->userto);
+        }
+        if (!$eventdata->userto) {
+            debugging('Attempt to send msg to unknown user', DEBUG_NORMAL);
+            return false;
+        }
+
+        // Verify all necessary data fields are present.
+        if (!isset($eventdata->userto->auth) or !isset($eventdata->userto->suspended)
+            or !isset($eventdata->userto->deleted) or !isset($eventdata->userto->emailstop)) {
+
+            debugging('Necessary properties missing in userto object, fetching full record', DEBUG_DEVELOPER);
+            $eventdata->userto = core_user::get_user($eventdata->userto->id);
+        }
+
+        $usertoisrealuser = (core_user::is_real_user($eventdata->userto->id) != false);
+        // If recipient is internal user (noreply user), and emailstop is set then don't send any msg.
+        if (!$usertoisrealuser && !empty($eventdata->userto->emailstop)) {
+            debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL);
+            return false;
+        }
+
+        if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id,
+                                                                                  $eventdata->userto->id])) {
+            $conversation = \core_message\api::create_conversation(
+                \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+                [
+                    $eventdata->userfrom->id,
+                    $eventdata->userto->id
+                ]
+            );
+        }
+        // We either have found a conversation, or created one.
+        $conversationid = $conversationid ? $conversationid : $conversation->id;
+        $eventdata->convid = $conversationid;
+    }
+
+    // This is a message directed to a conversation, not a specific user as was the way in legacy messaging.
+    // The above code has adapted the legacy messages into conversation messages.
+    // We must call send_message_to_conversation(), which handles per-member processor iteration and triggers
+    // a per-conversation event.
+    // All eventdata for messages should now have a convid, as we fixed this above.
+    if (!$eventdata->notification) {
+
+        // Only one message will be saved to the DB.
+        $conversationid = $eventdata->convid;
+        $table = 'messages';
+        $tabledata = new stdClass();
+        $tabledata->courseid = $eventdata->courseid;
+        $tabledata->useridfrom = $eventdata->userfrom->id;
+        $tabledata->conversationid = $conversationid;
+        $tabledata->subject = $eventdata->subject;
+        $tabledata->fullmessage = $eventdata->fullmessage;
+        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
+        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
+        $tabledata->smallmessage = $eventdata->smallmessage;
+        $tabledata->timecreated = time();
+
+        if ($messageid = message_handle_phpunit_redirection($eventdata, $table, $tabledata)) {
+            return $messageid;
+        }
+
+        // Cache messages.
+        if (!empty($eventdata->convid)) {
+            // Cache the timecreated value of the last message in this conversation.
+            $cache = cache::make('core', 'message_time_last_message_between_users');
+            $key = \core_message\helper::get_last_message_time_created_cache_key($eventdata->convid);
+            $cache->set($key, $tabledata->timecreated);
+        }
+
+        // Store unread message just in case we get a fatal error any time later.
+        $tabledata->id = $DB->insert_record($table, $tabledata);
+        $eventdata->savedmessageid = $tabledata->id;
+
+        return \core\message\manager::send_message_to_conversation($eventdata, $tabledata);
+    }
+
+    // Else the message is a notification.
+    if (!is_object($eventdata->userto)) {
+        $eventdata->userto = core_user::get_user($eventdata->userto);
+    }
+    if (!$eventdata->userto) {
+        debugging('Attempt to send msg to unknown user', DEBUG_NORMAL);
+        return false;
+    }
+
     // If the provider's component is disabled or the user can't receive messages from it, don't send the message.
     $isproviderallowed = false;
     foreach (message_get_providers_for_user($eventdata->userto->id) as $provider) {
@@ -133,110 +226,37 @@ function message_send(\core\message\message $eventdata) {
     }
 
     // Check if we are creating a notification or message.
-    if ($eventdata->notification) {
-        $table = 'notifications';
-
-        $tabledata = new stdClass();
-        $tabledata->useridfrom = $eventdata->userfrom->id;
-        $tabledata->useridto = $eventdata->userto->id;
-        $tabledata->subject = $eventdata->subject;
-        $tabledata->fullmessage = $eventdata->fullmessage;
-        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
-        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
-        $tabledata->smallmessage = $eventdata->smallmessage;
-        $tabledata->eventtype = $eventdata->name;
-        $tabledata->component = $eventdata->component;
-
-        if (!empty($eventdata->contexturl)) {
-            $tabledata->contexturl = (string)$eventdata->contexturl;
-        } else {
-            $tabledata->contexturl = null;
-        }
-
-        if (!empty($eventdata->contexturlname)) {
-            $tabledata->contexturlname = (string)$eventdata->contexturlname;
-        } else {
-            $tabledata->contexturlname = null;
-        }
+    $table = 'notifications';
+
+    $tabledata = new stdClass();
+    $tabledata->useridfrom = $eventdata->userfrom->id;
+    $tabledata->useridto = $eventdata->userto->id;
+    $tabledata->subject = $eventdata->subject;
+    $tabledata->fullmessage = $eventdata->fullmessage;
+    $tabledata->fullmessageformat = $eventdata->fullmessageformat;
+    $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
+    $tabledata->smallmessage = $eventdata->smallmessage;
+    $tabledata->eventtype = $eventdata->name;
+    $tabledata->component = $eventdata->component;
+    $tabledata->timecreated = time();
+    if (!empty($eventdata->contexturl)) {
+        $tabledata->contexturl = (string)$eventdata->contexturl;
     } else {
-        $table = 'messages';
-
-        if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id,
-                $eventdata->userto->id])) {
-            $conversation = \core_message\api::create_conversation(
-                \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
-                [
-                    $eventdata->userfrom->id,
-                    $eventdata->userto->id
-                ]
-            );
-            $conversationid = $conversation->id;
-        }
-
-        $tabledata = new stdClass();
-        $tabledata->courseid = $eventdata->courseid;
-        $tabledata->useridfrom = $eventdata->userfrom->id;
-        $tabledata->conversationid = $conversationid;
-        $tabledata->subject = $eventdata->subject;
-        $tabledata->fullmessage = $eventdata->fullmessage;
-        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
-        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
-        $tabledata->smallmessage = $eventdata->smallmessage;
+        $tabledata->contexturl = null;
     }
 
-    $tabledata->timecreated = time();
-
-    if (PHPUNIT_TEST and class_exists('phpunit_util')) {
-        // Add some more tests to make sure the normal code can actually work.
-        $componentdir = core_component::get_component_directory($eventdata->component);
-        if (!$componentdir or !is_dir($componentdir)) {
-            throw new coding_exception('Invalid component specified in message-send(): '.$eventdata->component);
-        }
-        if (!file_exists("$componentdir/db/messages.php")) {
-            throw new coding_exception("$eventdata->component does not contain db/messages.php necessary for message_send()");
-        }
-        $messageproviders = null;
-        include("$componentdir/db/messages.php");
-        if (!isset($messageproviders[$eventdata->name])) {
-            throw new coding_exception("Missing messaging defaults for event '$eventdata->name' in '$eventdata->component' messages.php file");
-        }
-        unset($componentdir);
-        unset($messageproviders);
-        // Now ask phpunit if it wants to catch this message.
-        if (phpunit_util::is_redirecting_messages()) {
-            $messageid = $DB->insert_record($table, $tabledata);
-            $message = $DB->get_record($table, array('id' => $messageid));
-
-            // Add the useridto attribute for BC.
-            $message->useridto = $eventdata->userto->id;
-
-            // Mark the message/notification as read.
-            if ($eventdata->notification) {
-                \core_message\api::mark_notification_as_read($message);
-            } else {
-                \core_message\api::mark_message_as_read($eventdata->userto->id, $message);
-            }
+    if (!empty($eventdata->contexturlname)) {
+        $tabledata->contexturlname = (string)$eventdata->contexturlname;
+    } else {
+        $tabledata->contexturlname = null;
+    }
 
-            // Unit tests need this detail.
-            $message->notification = $eventdata->notification;
-            phpunit_util::message_sent($message);
-            return $messageid;
-        }
+    if ($messageid = message_handle_phpunit_redirection($eventdata, $table, $tabledata)) {
+        return $messageid;
     }
 
     // Fetch enabled processors.
-    // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
-    if (!$eventdata->notification) {
-        $processors = array_filter(get_message_processors(false), function($processor) {
-            if ($processor->object->force_process_messages()) {
-                return true;
-            }
-
-            return ($processor->enabled && $processor->configured);
-        });
-    } else {
-        $processors = get_message_processors(true);
-    }
+    $processors = get_message_processors(true);
 
     // Preset variables
     $processorlist = array();
@@ -269,9 +289,7 @@ function message_send(\core\message\message $eventdata) {
         }
 
         // Populate the list of processors we will be using
-        if (!$eventdata->notification && $processor->object->force_process_messages()) {
-            $processorlist[] = $processor->name;
-        } else if ($permitted == 'forced' && $userisconfigured) {
+        if ($permitted == 'forced' && $userisconfigured) {
             // An admin is forcing users to use this message processor. Use this processor unconditionally.
             $processorlist[] = $processor->name;
         } else if ($permitted == 'permitted' && $userisconfigured && !$eventdata->userto->emailstop) {
@@ -290,16 +308,6 @@ function message_send(\core\message\message $eventdata) {
         }
     }
 
-    // Only cache messages, not notifications.
-    if (!$eventdata->notification) {
-        if (!empty($eventdata->convid)) {
-            // Cache the timecreated value of the last message in this conversation.
-            $cache = cache::make('core', 'message_time_last_message_between_users');
-            $key = \core_message\helper::get_last_message_time_created_cache_key($eventdata->convid);
-            $cache->set($key, $tabledata->timecreated);
-        }
-    }
-
     // Store unread message just in case we get a fatal error any time later.
     $tabledata->id = $DB->insert_record($table, $tabledata);
     $eventdata->savedmessageid = $tabledata->id;
@@ -308,6 +316,68 @@ function message_send(\core\message\message $eventdata) {
     return \core\message\manager::send_message($eventdata, $tabledata, $processorlist);
 }
 
+/**
+ * Helper method containing the PHPUnit specific code, used to redirect and capture messages/notifications.
+ *
+ * @param \core\message\message $eventdata the message object
+ * @param string $table the table to store the tabledata in, either messages or notifications.
+ * @param stdClass $tabledata the data to be stored when creating the message/notification.
+ * @return int the id of the stored message.
+ */
+function message_handle_phpunit_redirection(\core\message\message $eventdata, string $table, \stdClass $tabledata) {
+    global $DB;
+    if (PHPUNIT_TEST and class_exists('phpunit_util')) {
+        // Add some more tests to make sure the normal code can actually work.
+        $componentdir = core_component::get_component_directory($eventdata->component);
+        if (!$componentdir or !is_dir($componentdir)) {
+            throw new coding_exception('Invalid component specified in message-send(): '.$eventdata->component);
+        }
+        if (!file_exists("$componentdir/db/messages.php")) {
+            throw new coding_exception("$eventdata->component does not contain db/messages.php necessary for message_send()");
+        }
+        $messageproviders = null;
+        include("$componentdir/db/messages.php");
+        if (!isset($messageproviders[$eventdata->name])) {
+            throw new coding_exception("Missing messaging defaults for event '$eventdata->name' in '$eventdata->component' " .
+                "messages.php file");
+        }
+        unset($componentdir);
+        unset($messageproviders);
+        // Now ask phpunit if it wants to catch this message.
+        if (phpunit_util::is_redirecting_messages()) {
+            $messageid = $DB->insert_record($table, $tabledata);
+            $message = $DB->get_record($table, array('id' => $messageid));
+
+            if ($eventdata->notification) {
+                // Add the useridto attribute for BC.
+                $message->useridto = $eventdata->userto->id;
+
+                // Mark the notification as read.
+                \core_message\api::mark_notification_as_read($message);
+            } else {
+                // Add the useridto attribute for BC.
+                if (isset($eventdata->userto)) {
+                    $message->useridto = $eventdata->userto->id;
+                }
+                // Mark the message as read for each of the other users.
+                $sql = "SELECT u.*
+                  FROM {message_conversation_members} mcm
+                  JOIN {user} u
+                    ON (mcm.conversationid = :convid AND u.id = mcm.userid AND u.id != :userid)";
+                $otherusers = $DB->get_records_sql($sql, ['convid' => $eventdata->convid, 'userid' => $eventdata->userfrom->id]);
+                foreach ($otherusers as $othermember) {
+                    \core_message\api::mark_message_as_read($othermember->id, $message);
+                }
+            }
+
+            // Unit tests need this detail.
+            $message->notification = $eventdata->notification;
+            phpunit_util::message_sent($message);
+            return $messageid;
+        }
+    }
+}
+
 /**
  * Updates the message_providers table with the current set of message providers
  *
index faa4580..6d758b2 100644 (file)
@@ -2763,7 +2763,10 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
             $PAGE->set_course($course);
         }
         // Set accesstime or the user will appear offline which messes up messaging.
-        user_accesstime_log($course->id);
+        // Do not update access time for webservice or ajax requests.
+        if (!WS_SERVER && !AJAX_SCRIPT) {
+            user_accesstime_log($course->id);
+        }
         return;
     }
 
@@ -2972,7 +2975,10 @@ function require_login($courseorid = null, $autologinguest = true, $cm = null, $
     }
 
     // Finally access granted, update lastaccess times.
-    user_accesstime_log($course->id);
+    // Do not update access time for webservice or ajax requests.
+    if (!WS_SERVER && !AJAX_SCRIPT) {
+        user_accesstime_log($course->id);
+    }
 }
 
 
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 b8e4837..8267fa7 100644 (file)
@@ -48,8 +48,8 @@ defined('MOODLE_INTERNAL') || die();
 /**
  * The reCAPTCHA URL's
  */
-define('RECAPTCHA_API_URL', 'https://www.google.com/recaptcha/api.js');
-define('RECAPTCHA_VERIFY_URL', 'https://www.google.com/recaptcha/api/siteverify');
+define('RECAPTCHA_API_URL', 'https://www.recaptcha.net/recaptcha/api.js');
+define('RECAPTCHA_VERIFY_URL', 'https://www.recaptcha.net/recaptcha/api/siteverify');
 
 /**
  * Returns the language code the reCAPTCHA element should use.
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..a1bebde
--- /dev/null
@@ -0,0 +1,205 @@
+@core
+Feature: Context locks apply to child contexts
+  In order to preserve content
+  As a manager
+  I can disbale writes at different areas
+
+  Background:
+    Given the following config values are set as admin:
+      | contextlocking | 1 |
+    And the following "users" exist:
+      | username  | firstname | lastname | email                 |
+      | teacher   | Ateacher  | Teacher  | teacher@example.com   |
+      | student1  | Astudent  | Astudent | student1@example.com  |
+    And the following "categories" exist:
+      | name  | category | idnumber |
+      | cata  | 0        | cata     |
+      | cataa | cata     | cataa    |
+      | catb  | 0        | catb     |
+    And the following "courses" exist:
+      | fullname  | shortname | category  |
+      | courseaa1 | courseaa1 | cataa     |
+      | courseaa2 | courseaa2 | cataa     |
+      | courseb   | courseb   | catb      |
+    And the following "activities" exist:
+      | activity  | name    | course    | idnumber  |
+      | forum     | faa1    | courseaa1 | faa1      |
+      | forum     | faa1b   | courseaa1 | faa1b     |
+      | forum     | faa2    | courseaa2 | faa2      |
+      | forum     | fb      | courseb   | fb        |
+    And the following "course enrolments" exist:
+      | user      | course    | role           |
+      | teacher   | courseaa1 | editingteacher |
+      | student1  | courseaa1 | student        |
+      | teacher   | courseaa2 | editingteacher |
+      | student1  | courseaa2 | student        |
+      | teacher   | courseb   | editingteacher |
+      | student1  | courseb   | student        |
+
+  Scenario: Lock course module module should lock just that module
+    Given I log in as "admin"
+    And I am on "courseaa1" course homepage
+    And I follow "faa1"
+    And I should see "Add a new discussion topic"
+    When I follow "Lock this context"
+    And I click on "Continue" "button"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa1" course homepage
+    Then I should see "Turn editing on"
+    When I follow "faa1b"
+    Then I should see "Add a new discussion topic"
+    When I am on "courseaa2" course homepage
+    Then I should see "Turn editing on"
+    When I follow "faa2"
+    Then I should see "Add a new discussion topic"
+    When I am on "courseb" course homepage
+    Then I should see "Turn editing on"
+    When I follow "fb"
+    Then I should see "Add a new discussion topic"
+
+    And I log out
+    When I log in as "teacher"
+    And I am on "courseaa1" course homepage
+    And I follow "faa1"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa1" course homepage
+    Then I should see "Turn editing on"
+    When I follow "faa1b"
+    Then I should see "Add a new discussion topic"
+    When I am on "courseaa2" course homepage
+    Then I should see "Turn editing on"
+    When I follow "faa2"
+    Then I should see "Add a new discussion topic"
+    When I am on "courseb" course homepage
+    Then I should see "Turn editing on"
+    When I follow "fb"
+    Then I should see "Add a new discussion topic"
+
+    And I log out
+    When I log in as "student1"
+    And I am on "courseaa1" course homepage
+    And I follow "faa1"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa1" course homepage
+    When I follow "faa1b"
+    Then I should see "Add a new discussion topic"
+    When I am on "courseaa2" course homepage
+    When I follow "faa2"
+    Then I should see "Add a new discussion topic"
+    When I am on "courseb" course homepage
+    When I follow "fb"
+    Then I should see "Add a new discussion topic"
+
+  Scenario: Lock course should lock all children
+    Given I log in as "admin"
+    And I am on "courseaa1" course homepage
+    And I should see "Turn editing on"
+    When I follow "Lock this context"
+    And I click on "Continue" "button"
+    Then I should not see "Turn editing on"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa1" course homepage
+    Then I should not see "Turn editing on"
+    And I should see "Unlock this context"
+    When I follow "faa1b"
+    Then I should not see "Add a new discussion topic"
+    And I should not see "Unlock this context"
+    When I am on "courseaa2" course homepage
+    Then I should see "Turn editing on"
+    When I follow "faa2"
+    Then I should see "Add a new discussion topic"
+    When I am on "courseb" course homepage
+    Then I should see "Turn editing on"
+    When I follow "fb"
+    Then I should see "Add a new discussion topic"
+
+    And I log out
+    When I log in as "teacher"
+    And I am on "courseaa1" course homepage
+    And I follow "faa1"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa1" course homepage
+    Then I should not see "Turn editing on"
+    When I follow "faa1b"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa2" course homepage
+    Then I should see "Turn editing on"
+    When I follow "faa2"
+    Then I should see "Add a new discussion topic"
+    When I am on "courseb" course homepage
+    Then I should see "Turn editing on"
+    When I follow "fb"
+    Then I should see "Add a new discussion topic"
+
+    And I log out
+    When I log in as "student1"
+    And I am on "courseaa1" course homepage
+    And I follow "faa1"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa1" course homepage
+    When I follow "faa1b"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa2" course homepage
+    When I follow "faa2"
+    Then I should see "Add a new discussion topic"
+    When I am on "courseb" course homepage
+    When I follow "fb"
+    Then I should see "Add a new discussion topic"
+
+  Scenario: Lock course category should lock all children
+    Given I log in as "admin"
+    And I go to the courses management page
+    And I click on "managecontextlock" action for "cata" in management category listing
+    And I click on "Continue" "button"
+    And I am on "courseaa1" course homepage
+    And I should not see "Turn editing on"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa1" course homepage
+    Then I should not see "Turn editing on"
+    And I should not see "Unlock this context"
+    When I follow "faa1b"
+    Then I should not see "Add a new discussion topic"
+    And I should not see "Unlock this context"
+    When I am on "courseaa2" course homepage
+    Then I should not see "Turn editing on"
+    When I follow "faa2"
+    Then I should not see "Add a new discussion topic"
+    And I should not see "Unlock this context"
+    When I am on "courseb" course homepage
+    Then I should see "Turn editing on"
+    When I follow "fb"
+    Then I should see "Add a new discussion topic"
+
+    And I log out
+    When I log in as "teacher"
+    And I am on "courseaa1" course homepage
+    Then I should not see "Turn editing on"
+    And I follow "faa1"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa1" course homepage
+    Then I should not see "Turn editing on"
+    When I follow "faa1b"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa2" course homepage
+    Then I should not see "Turn editing on"
+    When I follow "faa2"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseb" course homepage
+    Then I should see "Turn editing on"
+    When I follow "fb"
+    Then I should see "Add a new discussion topic"
+
+    And I log out
+    When I log in as "student1"
+    And I am on "courseaa1" course homepage
+    And I follow "faa1"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa1" course homepage
+    When I follow "faa1b"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseaa2" course homepage
+    When I follow "faa2"
+    Then I should not see "Add a new discussion topic"
+    When I am on "courseb" course homepage
+    When I follow "fb"
+    Then I should see "Add a new discussion topic"
index 3f1d3de..d2e0265 100644 (file)
@@ -171,10 +171,10 @@ class core_message_testcase extends advanced_testcase {
         $this->assertSame(true, $recordexists);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
-        $this->assertSame($message->subject, $email->subject);
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
-        $this->assertRegExp('/test message body test/', $email->body);
+        $this->assertRegExp('/test message body.*test/s', $email->body);
         $sink->clear();
 
         // Test that event fired includes the courseid.
@@ -211,7 +211,7 @@ class core_message_testcase extends advanced_testcase {
         $this->assertSame(true, $recordexists);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
-        $this->assertSame($message->subject, $email->subject);
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $this->assertNotRegExp('/test message body test/', $email->body);
index 679175c..226de53 100644 (file)
@@ -65,7 +65,7 @@ class core_messagelib_testcase extends advanced_testcase {
         message_send($message);
         $emails = $sink->get_messages();
         $email = reset($emails);
-        $this->assertEquals($email->subject, 'message subject 1');
+        $this->assertEquals(get_string('unreadnewmessage', 'message', fullname(get_admin())), $email->subject);
     }
     public function test_message_get_providers_for_user() {
         global $CFG, $DB;
@@ -548,7 +548,7 @@ class core_messagelib_testcase extends advanced_testcase {
         $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
-        $this->assertSame($message->subject, $email->subject);
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $sink->clear();
@@ -581,7 +581,7 @@ class core_messagelib_testcase extends advanced_testcase {
         $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
-        $this->assertSame($message->subject, $email->subject);
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $sink->clear();
@@ -732,6 +732,255 @@ class core_messagelib_testcase extends advanced_testcase {
         $sink->clear();
     }
 
+    /**
+     * Tests calling message_send() with $eventdata representing a message to an individual conversation.
+     *
+     * This test will verify:
+     * - that the 'messages' record is created.
+     * - that the processors will be called for each conversation member, except the sender.
+     * - the a single event will be generated - 'message_sent'
+     *
+     * Note: We won't redirect/capture messages in this test because doing so causes message_send() to return early, before
+     * processors and events code is called. We need to test this code here, as we generally redirect messages elsewhere and we
+     * need to be sure this is covered.
+     */
+    public function test_message_send_to_conversation_individual() {
+        global $DB;
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        // Create some users and a conversation between them.
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id], '1:1 project discussion');
+
+        // Generate the message.
+        $message = new \core\message\message();
+        $message->courseid          = 1;
+        $message->component         = 'moodle';
+        $message->name              = 'instantmessage';
+        $message->userfrom          = $user1;
+        $message->convid            = $conversation->id;
+        $message->subject           = 'message subject 1';
+        $message->fullmessage       = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = '<p>message body</p>';
+        $message->smallmessage      = 'small message';
+        $message->notification      = '0';
+
+        // Content specific to the email processor.
+        $content = array('*' => array('header' => ' test ', 'footer' => ' test '));
+        $message->set_additional_content('email', $content);
+
+        // Ensure we're going to hit the email processor for this user.
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+
+        // Now, send a message and verify the message processors (in this case, email) are hit.
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+
+        // Verify the record was created in 'messages'.
+        $recordexists = $DB->record_exists('messages', ['id' => $messageid]);
+        $this->assertTrue($recordexists);
+
+        // Verify the email information.
+        $this->assertSame($user1->email, $email->from);
+        $this->assertSame($user2->email, $email->to);
+
+        // The message subject is generated during the call for conversation messages,
+        // as the conversation may have many members having different lang preferences.
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
+
+        // The email content will have had an emailtagline appended to it, based on lang prefs,
+        // so verify the expected beginning and ends.
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/test message body.*test/s', $email->body);
+        $sink->clear();
+
+        // Now, send the message again, and verify that the event fired includes the courseid and conversationid.
+        $eventsink = $this->redirectEvents();
+        $messageid = message_send($message);
+        $events = $eventsink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf(\core\event\message_sent::class, $event);
+        $this->assertEquals($user1->id, $event->userid);
+        $this->assertEquals($user2->id, $event->relateduserid);
+        $this->assertEquals($message->courseid, $event->other['courseid']);
+
+        $eventsink->clear();
+        $sink->clear();
+    }
+
+    /**
+     * Tests calling message_send() with $eventdata representing a message to an group conversation.
+     *
+     * This test will verify:
+     * - that the 'messages' record is created.
+     * - that the processors will be called for each conversation member, except the sender.
+     * - the a single event will be generated - 'group_message_sent'
+     *
+     * Note: We won't redirect/capture messages in this test because doing so causes message_send() to return early, before
+     * processors and events code is called. We need to test this code here, as we generally redirect messages elsewhere and we
+     * need to be sure this is covered.
+     */
+    public function test_message_send_to_conversation_group() {
+        global $DB, $CFG, $SITE;
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        // Create some users and a conversation between them.
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id], 'Group project discussion');
+
+        // Generate the message.
+        $message = new \core\message\message();
+        $message->courseid          = 1;
+        $message->component         = 'moodle';
+        $message->name              = 'instantmessage';
+        $message->userfrom          = $user1;
+        $message->convid            = $conversation->id;
+        $message->subject           = 'message subject 1';
+        $message->fullmessage       = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = '<p>message body</p>';
+        $message->smallmessage      = 'small message';
+        $message->notification      = '0';
+
+        // Content specific to the email processor.
+        $content = array('*' => array('header' => ' test ', 'footer' => ' test '));
+        $message->set_additional_content('email', $content);
+
+        // Ensure we're going to hit the email processor for the recipient users.
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user3);
+
+        // Now, send a message and verify the message processors (in this case, email) are hit.
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(2, $emails);
+
+        // Verify the record was created in 'messages'.
+        $recordexists = $DB->record_exists('messages', ['id' => $messageid]);
+        $this->assertTrue($recordexists);
+
+        // Verify the email information. Ordering is not guaranteed.
+        $members = [$user2->email => '', $user3->email => ''];
+        $email = $emails[0];
+        $this->assertSame($user1->email, $email->from);
+        $this->assertArrayHasKey($email->to, $members);
+        unset($members[$email->to]);
+
+        $email = $emails[1];
+        $this->assertSame($user1->email, $email->from);
+        $this->assertArrayHasKey($email->to, $members);
+        unset($members[$email->to]);
+
+        // The message subject is generated during the call for conversation messages,
+        // as the conversation may have many members having different lang preferences.
+        $tmp = (object) ['name' => fullname($user1), 'conversationname' => $conversation->name];
+        $this->assertSame(get_string('unreadnewgroupconversationmessage', 'message', $tmp), $email->subject);
+
+        // The email content will have had an emailtagline appended to it, based on lang prefs,
+        // so verify the expected beginning and ends.
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/test message body.*test/s', $email->body);
+        $sink->clear();
+
+        // Now, send the message again, and verify that the event fired includes the courseid and conversationid.
+        $eventsink = $this->redirectEvents();
+        $messageid = message_send($message);
+        $events = $eventsink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf(\core\event\group_message_sent::class, $event);
+        $this->assertEquals($user1->id, $event->userid);
+        $this->assertNull($event->relateduserid);
+        $this->assertEquals($message->courseid, $event->other['courseid']);
+        $this->assertEquals($message->convid, $event->other['conversationid']);
+        $eventsink->clear();
+        $sink->clear();
+    }
+
+    /**
+     * Verify that sending a message to a conversation is an action which can be buffered by the manager if in a DB transaction.
+     *
+     * This should defer all processor calls (for 2 members in this case), and event creation (1 event).
+     */
+    public function test_send_message_to_conversation_group_with_buffering() {
+        global $DB, $CFG;
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+
+        // Create a conversation.
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id], 'Group project discussion');
+
+        // Test basic email redirection.
+        $this->assertFileExists("$CFG->dirroot/message/output/email/version.php");
+        $this->assertFileExists("$CFG->dirroot/message/output/popup/version.php");
+
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email' AND name <> 'popup'");
+        get_message_processors(true, true);
+
+        $eventsink = $this->redirectEvents();
+
+        // Will always use the pop-up processor.
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user3);
+
+        $message = new \core\message\message();
+        $message->courseid          = 1;
+        $message->component         = 'moodle';
+        $message->name              = 'instantmessage';
+        $message->userfrom          = $user1;
+        $message->convid            = $conversation->id;
+        $message->subject           = 'message subject 1';
+        $message->fullmessage       = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = '<p>message body</p>';
+        $message->smallmessage      = 'small message';
+        $message->notification      = '0';
+
+        $transaction = $DB->start_delegated_transaction();
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(0, $emails);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
+        $sink->clear();
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
+        $DB->delete_records('messages', array());
+        $events = $eventsink->get_events();
+        $this->assertCount(0, $events);
+        $eventsink->clear();
+        $transaction->allow_commit();
+        $events = $eventsink->get_events();
+        $emails = $sink->get_messages();
+        $this->assertCount(2, $emails);
+        $this->assertCount(1, $events);
+        $this->assertInstanceOf('\core\event\group_message_sent', $events[0]);
+    }
+
     public function test_rollback() {
         global $DB;
 
index a228754..5f3eab4 100644 (file)
@@ -2429,7 +2429,8 @@ class core_moodlelib_testcase extends advanced_testcase {
             'contextlevel' => $obj->contextlevel,
             'instanceid'   => $obj->instanceid,
             'path'         => $obj->path,
-            'depth'        => $obj->depth
+            'depth'        => $obj->depth,
+            'locked'       => $obj->locked,
         );
         $this->assertEquals(convert_to_array($obj), $ar);
     }
index a5a15d9..b3b8c9d 100644 (file)
@@ -571,7 +571,7 @@ class core_session_manager_testcase extends advanced_testcase {
         \core\session\manager::loginas($user->id, context_system::instance());
 
         $this->assertSame($user->id, $USER->id);
-        $this->assertSame(context_system::instance(), $USER->loginascontext);
+        $this->assertEquals(context_system::instance(), $USER->loginascontext);
         $this->assertSame($adminuser->id, $USER->realuser);
         $this->assertSame($GLOBALS['USER'], $_SESSION['USER']);
         $this->assertSame($GLOBALS['USER'], $USER);
index 6bea9f1..023fab1 100644 (file)
@@ -165,6 +165,8 @@ the groupid field.
   until all settings have been set. The additional parameters are used recursively and shouldn't be need to be explicitly passed in when calling
   the function from other parts of Moodle.
   The return value: $settingsoutput is an array of setting names and the values that were set by the function.
+* 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 43fa325..291e012 100644 (file)
@@ -151,7 +151,7 @@ class api {
     /**
      * Handles searching for user in a particular course in the message area.
      *
-     * TODO: This function should be removed once new group messaging UI is in place and old messaging UI is removed.
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
      * But we are deprecating data_for_messagearea_search_users_in_course external function.
      * Followup: MDL-63915
@@ -197,7 +197,7 @@ class api {
     /**
      * Handles searching for user in the message area.
      *
-     * TODO: This function should be removed once new group messaging UI is in place and old messaging UI is removed.
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
      * But we are deprecating data_for_messagearea_search_users external function.
      * Followup: MDL-63915
@@ -516,12 +516,10 @@ class api {
         // If we need to return ONLY favourites, or NO favourites, generate the SQL snippet.
         $favouritesql = "";
         $favouriteparams = [];
-        if (is_bool($favourites)) {
-            if (!empty($favouriteconversationids)) {
-                list ($insql, $inparams) = $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids');
-                $favouritesql = $favourites ? " AND mc.id {$insql} " : " AND mc.id NOT {$insql} ";
-                $favouriteparams = $inparams;
-            }
+        if (null !== $favourites && !empty($favouriteconversationids)) {
+            list ($insql, $favouriteparams) =
+                    $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites);
+            $favouritesql = " AND mc.id {$insql} ";
         }
 
         // If we need to restrict type, generate the SQL snippet.
@@ -561,10 +559,11 @@ class api {
         $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
 
         $conversations = [];
-        $uniquemembers = [];
         $members = [];
+        $individualmembers = [];
+        $groupmembers = [];
         foreach ($conversationset as $conversation) {
-            $conversations[] = $conversation;
+            $conversations[$conversation->id] = $conversation;
             $members[$conversation->id] = [];
         }
         $conversationset->close();
@@ -600,7 +599,7 @@ class api {
             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
                 if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) {
                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
-                    $uniquemembers[$conversation->useridfrom] = $conversation->useridfrom;
+                    $individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
                 } else {
                     $individualconversations[] = $conversation->id;
                 }
@@ -608,7 +607,7 @@ class api {
                 // If we have a recent message, the sender is our member.
                 if (!is_null($conversation->useridfrom)) {
                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
-                    $uniquemembers[$conversation->useridfrom] = $conversation->useridfrom;
+                    $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
                 }
             }
         }
@@ -626,10 +625,9 @@ class api {
 
             foreach ($conversationmembers as $mid => $member) {
                 $members[$member->conversationid][$member->userid] = $member->userid;
-                $uniquemembers[$member->userid] = $member->userid;
+                $individualmembers[$member->userid] = $member->userid;
             }
         }
-        $memberids = array_values($uniquemembers);
 
         // We could fail early here if we're sure that:
         // a) we have no otherusers for all the conversations (users may have been deleted)
@@ -639,8 +637,17 @@ class api {
         // needs to be done in a separate query to avoid doing a join on the messages tables and the user
         // tables because on large sites these tables are massive which results in extremely slow
         // performance (typically due to join buffer exhaustion).
-        if (!empty($memberids)) {
-            $memberinfo = helper::get_member_info($userid, $memberids);
+        if (!empty($individualmembers) || !empty($groupmembers)) {
+            // Now, we want to remove any duplicates from the group members array. For individual members we will
+            // be doing a more extensive call as we want their contact requests as well as privacy information,
+            // which is not necessary for group conversations.
+            $diffgroupmembers = array_diff($groupmembers, $individualmembers);
+
+            $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
+            $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
+
+            // Don't use array_merge, as we lose array keys.
+            $memberinfo = $individualmemberinfo + $groupmemberinfo;
 
             // Update the members array with the member information.
             $deletedmembers = [];
@@ -651,7 +658,15 @@ class api {
                         if ($memberinfo[$memberid]->isdeleted) {
                             $deletedmembers[$convid][] = $memberid;
                         }
-                        $members[$convid][$key] = $memberinfo[$memberid];
+
+                        $members[$convid][$key] = clone $memberinfo[$memberid];
+
+                        if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
+                            // Remove data we don't need for group.
+                            $members[$convid][$key]->requirescontact = null;
+                            $members[$convid][$key]->canmessage = null;
+                            $members[$convid][$key]->contactrequests = [];
+                        }
                     }
                 }
             }
@@ -791,6 +806,10 @@ class api {
     /**
      * Returns the contacts to display in the contacts area.
      *
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid The user id
      * @param int $limitfrom
      * @param int $limitnum
@@ -912,7 +931,10 @@ class api {
     /**
      * Returns the messages to display in the message area.
      *
-     * @deprecated since 3.6
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid the current user
      * @param int $otheruserid the other user
      * @param int $limitfrom
@@ -924,8 +946,6 @@ class api {
      */
     public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
             $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
-        debugging('\core_message\api::get_messages() is deprecated, please use ' .
-            '\core_message\api::get_conversation_messages() instead.', DEBUG_DEVELOPER);
 
         if (!empty($timefrom)) {
             // Get the conversation between userid and otheruserid.
@@ -996,15 +1016,15 @@ class api {
     /**
      * Returns the most recent message between two users.
      *
-     * @deprecated since 3.6
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid the current user
      * @param int $otheruserid the other user
      * @return \stdClass|null
      */
     public static function get_most_recent_message($userid, $otheruserid) {
-        debugging('\core_message\api::get_most_recent_message() is deprecated, please use ' .
-            '\core_message\api::get_most_recent_conversation_message() instead.', DEBUG_DEVELOPER);
-
         // We want two messages here so we get an accurate 'blocktime' value.
         if ($messages = helper::get_messages($userid, $otheruserid, 0, 0, 2, 'timecreated DESC')) {
             // Swap the order so we now have them in historical order.
@@ -1041,6 +1061,10 @@ class api {
     /**
      * Returns the profile information for a contact for a user.
      *
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid The user id
      * @param int $otheruserid The id of the user whose profile we want to view.
      * @return \stdClass
@@ -1428,7 +1452,103 @@ class api {
         }
 
         // Check if the recipient can be messaged by the sender.
-        return (self::can_contact_user($recipient, $sender));
+        return (self::can_contact_user($recipient->id, $sender->id));
+    }
+
+    /**
+     * Determines if a user is permitted to send a message to a given conversation.
+     * If no sender is provided then it defaults to the logged in user.
+     *
+     * @param int $userid the id of the user on which the checks will be applied.
+     * @param int $conversationid the id of the conversation we wish to check.
+     * @return bool true if the user can send a message to the conversation, false otherwise.
+     * @throws \moodle_exception
+     */
+    public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
+        global $DB;
+
+        $systemcontext = \context_system::instance();
+        if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
+            return false;
+        }
+
+        if (!self::is_user_in_conversation($userid, $conversationid)) {
+            return false;
+        }
+
+        // User can post messages and is in the conversation, but we need to check the conversation type to
+        // know whether or not to check the user privacy settings via can_contact_user().
+        $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
+        if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
+            return true;
+        } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+            // Get the other user in the conversation.
+            $members = self::get_conversation_members($userid, $conversationid);
+            $otheruser = array_filter($members, function($member) use($userid) {
+                return $member->id != $userid;
+            });
+            $otheruser = reset($otheruser);
+
+            return self::can_contact_user($otheruser->id, $userid);
+        } else {
+            throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
+        }
+    }
+
+    /**
+     * Send a message from a user to a conversation.
+     *
+     * This method will create the basic eventdata and delegate to message creation to message_send.
+     * The message_send() method is responsible for event data that is specific to each recipient.
+     *
+     * @param int $userid the sender id.
+     * @param int $conversationid the conversation id.
+     * @param string $message the message to send.
+     * @param int $format the format of the message to send.
+     * @return \stdClass the message created.
+     * @throws \coding_exception
+     * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
+     */
+    public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
+                                                        int $format) : \stdClass {
+        global $DB;
+
+        if (!self::can_send_message_to_conversation($userid, $conversationid)) {
+            throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
+        }
+
+        $eventdata = new \core\message\message();
+        $eventdata->courseid         = 1;
+        $eventdata->component        = 'moodle';
+        $eventdata->name             = 'instantmessage';
+        $eventdata->userfrom         = $userid;
+        $eventdata->convid           = $conversationid;
+
+        if ($format == FORMAT_HTML) {
+            $eventdata->fullmessagehtml  = $message;
+            // Some message processors may revert to sending plain text even if html is supplied,
+            // so we keep both plain and html versions if we're intending to send html.
+            $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
+        } else {
+            $eventdata->fullmessage      = $message;
+            $eventdata->fullmessagehtml  = '';
+        }
+
+        $eventdata->fullmessageformat = $format;
+        $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
+
+        $eventdata->timecreated     = time();
+        $eventdata->notification    = 0;
+        $messageid = message_send($eventdata);
+
+        $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage, timecreated');
+        $message = (object) [
+            'id' => $messagerecord->id,
+            'useridfrom' => $messagerecord->useridfrom,
+            'text' => $messagerecord->fullmessage,
+            'timecreated' => $messagerecord->timecreated
+        ];
+        return $message;
     }
 
     /**
@@ -1862,6 +1982,15 @@ class api {
 
         global $DB;
 
+        $validtypes = [
+            self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            self::MESSAGE_CONVERSATION_TYPE_GROUP
+        ];
+
+        if (!in_array($type, $validtypes)) {
+            throw new \moodle_exception('An invalid conversation type was specified.');
+        }
+
         // Sanity check.
         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
             if (count($userids) > 2) {
@@ -2029,24 +2158,26 @@ class api {
      * It will not include blocked users.
      *
      * @param int $userid
+     * @param int $limitfrom
+     * @param int $limitnum
      * @return array The list of contact requests
      */
-    public static function get_contact_requests(int $userid) : array {
+    public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
         global $DB;
 
-        $ufields = \user_picture::fields('u');
-        $sql = "SELECT $ufields, mcr.id as contactrequestid
-                  FROM {user} u
-                  JOIN {message_contact_requests} mcr
-                    ON u.id = mcr.userid
+        $sql = "SELECT mcr.userid
+                  FROM {message_contact_requests} mcr
              LEFT JOIN {message_users_blocked} mub
-                    ON (mub.userid = ? AND mub.blockeduserid = u.id)
+                    ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
                  WHERE mcr.requesteduserid = ?
-                   AND u.deleted = 0
                    AND mub.id is NULL
-              ORDER BY mcr.timecreated DESC";
+              ORDER BY mcr.timecreated ASC";
+        if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
+            $userids = array_keys($contactrequests);
+            return helper::get_member_info($userid, $userids);
+        }
 
-        return $DB->get_records_sql($sql, [$userid, $userid]);
+        return [];
     }
 
     /**
@@ -2230,12 +2361,12 @@ class api {
     /**
      * Checks if the sender can message the recipient.
      *
-     * @param \stdClass $recipient The user object.
-     * @param \stdClass $sender The user object.
+     * @param int $recipientid
+     * @param int $senderid
      * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
      */
-    protected static function can_contact_user(\stdClass $recipient, \stdClass $sender) : bool {
-        if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $sender->id)) {
+    protected static function can_contact_user(int $recipientid, int $senderid) : bool {
+        if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid)) {
             // The sender has the ability to contact any user across the entire site.
             return true;
         }
@@ -2243,7 +2374,7 @@ class api {
         // The initial value of $cancontact is null to indicate that a value has not been determined.
         $cancontact = null;
 
-        if (self::is_blocked($recipient->id, $sender->id)) {
+        if (self::is_blocked($recipientid, $senderid)) {
             // The recipient has specifically blocked this sender.
             $cancontact = false;
         }
@@ -2257,7 +2388,7 @@ class api {
             //
             // The Site option is only possible when the messagingallusers site setting is also enabled.
 
-            $privacypreference = self::get_user_privacy_messaging_preference($recipient->id);
+            $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
             if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
                 // The user preference is to allow any user to contact them.
                 // No need to check anything else.
@@ -2265,12 +2396,12 @@ class api {
             } else {
                 // This user only allows their own contacts, and possibly course peers, to contact them.
                 // If the users are contacts then we can avoid the more expensive shared courses check.
-                $cancontact = self::is_contact($sender->id, $recipient->id);
+                $cancontact = self::is_contact($senderid, $recipientid);
 
                 if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
                     // The users are not contacts and the user allows course member messaging.
                     // Check whether these two users share any course together.
-                    $sharedcourses = enrol_get_shared_courses($recipient->id, $sender->id, true);
+                    $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
                     $cancontact = (!empty($sharedcourses));
                 }
             }
@@ -2283,12 +2414,12 @@ class api {
 
             // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
             if (null === $sharedcourses) {
-                $sharedcourses = enrol_get_shared_courses($recipient->id, $sender->id, true);
+                $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
             }
 
             foreach ($sharedcourses as $course) {
                 // Note: enrol_get_shared_courses will preload any shared context.
-                if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $sender->id)) {
+                if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
                     $cancontact = true;
                     break;
                 }
@@ -2490,25 +2621,7 @@ class api {
         if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
                 'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
             $userids = array_keys($members);
-            $members = helper::get_member_info($userid, $userids);
-
-            // Check if we want to include contact requests as well.
-            if ($includecontactrequests) {
-                list($useridsql, $usersparams) = $DB->get_in_or_equal($userids);
-
-                $wheresql = "(userid $useridsql OR requesteduserid $useridsql)";
-                if ($contactrequests = $DB->get_records_select('message_contact_requests', $wheresql,
-                        array_merge($usersparams, $usersparams), 'timecreated ASC, id ASC')) {
-                    foreach ($contactrequests as $contactrequest) {
-                        if (isset($members[$contactrequest->userid])) {
-                            $members[$contactrequest->userid]->contactrequests[] = $contactrequest;
-                        }
-                        if (isset($members[$contactrequest->requesteduserid])) {
-                            $members[$contactrequest->requesteduserid]->contactrequests[] = $contactrequest;
-                        }
-                    }
-                }
-            }
+            $members = helper::get_member_info($userid, $userids, $includecontactrequests);
 
             return $members;
         }
index 7322684..b83cd8e 100644 (file)
@@ -37,7 +37,10 @@ class helper {
     /**
      * Helper function to retrieve the messages between two users
      *
-     * @deprecated since 3.6
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid the current user
      * @param int $otheruserid the other user
      * @param int $timedeleted the time the message was deleted
@@ -50,9 +53,6 @@ class helper {
      */
     public static function get_messages($userid, $otheruserid, $timedeleted = 0, $limitfrom = 0, $limitnum = 0,
                                         $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
-        debugging('\core_message\helper::get_messages() is deprecated, please use ' .
-            '\core_message\helper::get_conversation_messages() instead.', DEBUG_DEVELOPER);
-
         global $DB;
 
         $hash = self::get_conversation_hash([$userid, $otheruserid]);
@@ -229,15 +229,15 @@ class helper {
     /**
      * Helper function to return an array of messages.
      *
-     * @deprecated since 3.6
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid
      * @param array $messages
      * @return array
      */
     public static function create_messages($userid, $messages) {
-        debugging('\core_message\helper::create_messages() is deprecated, please use ' .
-            '\core_message\helper::create_conversation_messages() instead.', DEBUG_DEVELOPER);
-
         // Store the messages.
         $arrmessages = array();
 
@@ -314,7 +314,8 @@ class helper {
             }
         }
         $data->isonline = null;
-        if (self::show_online_status($userfields)) {
+        $user = \core_user::get_user($data->userid);
+        if (self::show_online_status($user)) {
             $data->isonline = self::is_online($userfields->lastaccess);
         }
         $data->isblocked = isset($contact->blocked) ? (bool) $contact->blocked : false;
@@ -482,13 +483,22 @@ class helper {
      *
      * @param int $referenceuserid the id of the user which check contact and blocked status.
      * @param array $userids
+     * @param bool $includecontactrequests Do we want to include contact requests with this data?
+     * @param bool $includeprivacyinfo Do we want to include whether the user can message another, and if the user
+     *             requires a contact.
      * @return array the array of objects containing member info, indexed by userid.
      * @throws \coding_exception
      * @throws \dml_exception
      */
-    public static function get_member_info(int $referenceuserid, array $userids) : array {
+    public static function get_member_info(int $referenceuserid, array $userids, bool $includecontactrequests = false,
+                                           bool $includeprivacyinfo = false) : array {
         global $DB, $PAGE;
 
+        // Prevent exception being thrown when array is empty.
+        if (empty($userids)) {
+            return [];
+        }
+
         list($useridsql, $usersparams) = $DB->get_in_or_equal($userids);
         $userfields = \user_picture::fields('u', array('lastaccess'));
         $userssql = "SELECT $userfields, u.deleted, mc.id AS contactid, mub.id AS blockedid
@@ -525,8 +535,46 @@ class helper {
 
             $data->isdeleted = ($member->deleted) ? true : false;
 
+            $data->requirescontact = null;
+            $data->canmessage = null;
+            if ($includeprivacyinfo) {
+                $privacysetting = api::get_user_privacy_messaging_preference($member->id);
+                $data->requirescontact = $privacysetting == api::MESSAGE_PRIVACY_ONLYCONTACTS;
+
+                $recipient = new \stdClass();
+                $recipient->id = $member->id;
+
+                $sender = new \stdClass();
+                $sender->id = $referenceuserid;
+
+                $data->canmessage = api::can_post_message($recipient, $sender);
+            }
+
+            // Populate the contact requests, even if we don't need them.
+            $data->contactrequests = [];
+
             $members[$data->id] = $data;
         }
+
+        // Check if we want to include contact requests as well.
+        if (!empty($members) && $includecontactrequests) {
+            list($useridsql, $usersparams) = $DB->get_in_or_equal($userids);
+
+            $wheresql = "(userid $useridsql AND requesteduserid = ?) OR (userid = ? AND requesteduserid $useridsql)";
+            $params = array_merge($usersparams, [$referenceuserid, $referenceuserid], $usersparams);
+            if ($contactrequests = $DB->get_records_select('message_contact_requests', $wheresql, $params,
+                    'timecreated ASC, id ASC')) {
+                foreach ($contactrequests as $contactrequest) {
+                    if (isset($members[$contactrequest->userid])) {
+                        $members[$contactrequest->userid]->contactrequests[] = $contactrequest;
+                    }
+                    if (isset($members[$contactrequest->requesteduserid])) {
+                        $members[$contactrequest->requesteduserid]->contactrequests[] = $contactrequest;
+                    }
+                }
+            }
+        }
+
         return $members;
     }
 
index af3740a..d200778 100644 (file)
@@ -39,6 +39,76 @@ require_once($CFG->dirroot . "/message/lib.php");
  * @since Moodle 2.2
  */
 class core_message_external extends external_api {
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.6
+     */
+    public static function send_messages_to_conversation_parameters() {
+        return new external_function_parameters(
+            array(
+                'conversationid' => new external_value(PARAM_INT, 'id of the conversation'),
+                'messages' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'text' => new external_value(PARAM_RAW, 'the text of the message'),
+                            'textformat' => new external_format_value('text', VALUE_DEFAULT, FORMAT_MOODLE),
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Send messages from the current USER to a conversation.
+     *
+     * This conversation may be any type of conversation, individual or group.
+     *
+     * @param int $conversationid the id of the conversation to which the messages will be sent.
+     * @param array $messages An array of message to send.
+     * @return array the array of messages which were sent (created).
+     * @since Moodle 3.6
+     */
+    public static function send_messages_to_conversation(int $conversationid, array $messages = []) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Ensure the current user is allowed to run this function.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $params = self::validate_parameters(self::send_messages_to_conversation_parameters(), [
+            'conversationid' => $conversationid,
+            'messages' => $messages
+        ]);
+
+        $messages = [];
+        foreach ($params['messages'] as $message) {
+            $messages[] = \core_message\api::send_message_to_conversation($USER->id, $params['conversationid'], $message['text'],
+                $message['textformat']);
+        }
+
+        return $messages;
+    }
+
+    /**
+     * Returns description of method result value.
+     *
+     * @return external_description
+     * @since Moodle 3.6
+     */
+    public static function send_messages_to_conversation_returns() {
+        return new external_multiple_structure(
+            self::get_conversation_message_structure()
+        );
+    }
+
 
     /**
      * Returns description of method parameters
@@ -204,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,
@@ -289,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;
@@ -347,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']);
         }
@@ -403,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 [];
@@ -470,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,
@@ -556,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;
@@ -599,7 +669,9 @@ class core_message_external extends external_api {
     public static function get_contact_requests_parameters() {
         return new external_function_parameters(
             [
-                'userid' => new external_value(PARAM_INT, 'The id of the user we want the requests for')
+                'userid' => new external_value(PARAM_INT, 'The id of the user we want the requests for'),
+                'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0),
+                'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0)
             ]
         );
     }
@@ -613,8 +685,10 @@ class core_message_external extends external_api {
      * It will not include blocked users.
      *
      * @param int $userid The id of the user we want to get the contact requests for
+     * @param int $limitfrom
+     * @param int $limitnum
      */
-    public static function get_contact_requests(int $userid) {
+    public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) {
         global $CFG, $USER;
 
         // Check if messaging is enabled.
@@ -626,15 +700,19 @@ class core_message_external extends external_api {
         $context = context_system::instance();
         self::validate_context($context);
 
+        $params = [
+            'userid' => $userid,
+            'limitfrom' => $limitfrom,
+            'limitnum' => $limitnum
+        ];
+        $params = self::validate_parameters(self::get_contact_requests_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];
-        $params = self::validate_parameters(self::get_contact_requests_parameters(), $params);
-
-        return \core_message\api::get_contact_requests($params['userid']);
+        return \core_message\api::get_contact_requests($params['userid'], $params['limitfrom'], $params['limitnum']);
     }
 
     /**
@@ -644,26 +722,7 @@ class core_message_external extends external_api {
      */
     public static function get_contact_requests_returns() {
         return new external_multiple_structure(
-            new external_single_structure(
-                [
-                    'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'),
-                    'contactrequestid' => new external_value(PARAM_INT, 'The ID of the contact request'),
-                    'picture' => new external_value(core_user::get_property_type('picture'), 'The picture'),
-                    'firstname' => new external_value(core_user::get_property_type('firstname'),
-                        'The first name(s) of the user'),
-                    'lastname' => new external_value(core_user::get_property_type('lastname'),
-                        'The family name of the user'),
-                    'firstnamephonetic' => new external_value(core_user::get_property_type('firstnamephonetic'),
-                        'The phonetic first name of the user'),
-                    'lastnamephonetic' => new external_value(core_user::get_property_type('lastnamephonetic'),
-                        'The phonetic last name of the user'),
-                    'middlename' => new external_value(core_user::get_property_type('middlename'),
-                        'The middle name of the user'),
-                    'alternatename' => new external_value(core_user::get_property_type('alternatename'),
-                        'The alternate name of the user'),
-                    'email' => new external_value(core_user::get_property_type('email'), 'An email address')
-                ]
-            )
+            self::get_conversation_member_structure()
         );
     }
 
@@ -708,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']);
     }
 
     /**
@@ -774,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',
@@ -840,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 [];
@@ -894,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 [];
@@ -964,7 +1024,7 @@ class core_message_external extends external_api {
                 'unreadcount' => new external_value(PARAM_INT, 'The number of unread messages in this conversation',
                     VALUE_DEFAULT, null),
                 'members' => new external_multiple_structure(
-                    self::get_conversation_member_structure()
+                    self::get_conversation_member_structure(true)
                 ),
                 'messages' => new external_multiple_structure(
                     self::get_conversation_message_structure()
@@ -992,6 +1052,9 @@ class core_message_external extends external_api {
             'showonlinestatus' => new external_value(PARAM_BOOL, 'Show the user\'s online status?'),
             'isblocked' => new external_value(PARAM_BOOL, 'If the user has been blocked'),
             'iscontact' => new external_value(PARAM_BOOL, 'Is the user a contact?'),
+            'isdeleted' => new external_value(PARAM_BOOL, 'Is the user deleted?'),
+            'canmessage' => new external_value(PARAM_BOOL, 'If the user can be messaged'),
+            'requirescontact' => new external_value(PARAM_BOOL, 'If the user requires to be contacts'),
         ];
 
         if ($includecontactrequests) {
@@ -1120,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');
@@ -1210,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');
@@ -1387,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');
@@ -1465,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];
     }
 
@@ -1543,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);
@@ -1590,6 +1676,7 @@ class core_message_external extends external_api {
     /**
      * The messagearea contacts return parameters.
      *
+     * @deprecated since 3.6
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1600,6 +1687,7 @@ class core_message_external extends external_api {
     /**
      * Get messagearea contacts parameters.
      *
+     * @deprecated since 3.6
      * @param int $userid The id of the user who we are viewing conversations for
      * @param int $limitfrom
      * @param int $limitnum
@@ -1622,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');
@@ -1639,6 +1727,7 @@ class core_message_external extends external_api {
     /**
      * The messagearea contacts return structure.
      *
+     * @deprecated since 3.6
      * @return external_single_structure
      * @since 3.2
      */
@@ -1646,9 +1735,19 @@ class core_message_external extends external_api {
         return self::data_for_messagearea_conversations_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_contacts_is_deprecated() {
+        return true;
+    }
+
     /**
      * The messagearea messages parameters.
      *
+     * @deprecated since 3.6
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1669,6 +1768,7 @@ class core_message_external extends external_api {
     /**
      * Get messagearea messages.
      *
+     * @deprecated since 3.6
      * @param int $currentuserid The current user's id
      * @param int $otheruserid The other user's id
      * @param int $limitfrom
@@ -1697,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';
@@ -1718,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);
@@ -1741,6 +1841,7 @@ class core_message_external extends external_api {
     /**
      * The messagearea messages return structure.
      *
+     * @deprecated since 3.6
      * @return external_single_structure
      * @since 3.2
      */
@@ -1762,6 +1863,15 @@ class core_message_external extends external_api {
         );
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_messages_is_deprecated() {
+        return true;
+    }
+
     /**
      * The conversation messages parameters.
      *
@@ -1814,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.');
         }
 
@@ -1831,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;
@@ -1867,6 +1983,7 @@ class core_message_external extends external_api {
     /**
      * The get most recent message return parameters.
      *
+     * @deprecated since 3.6
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1882,6 +1999,7 @@ class core_message_external extends external_api {
     /**
      * Get the most recent message in a conversation.
      *
+     * @deprecated since 3.6
      * @param int $currentuserid The current user's id
      * @param int $otheruserid The other user's id
      * @return stdClass
@@ -1902,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');
@@ -1919,6 +2037,7 @@ class core_message_external extends external_api {
     /**
      * The get most recent message return structure.
      *
+     * @deprecated since 3.6
      * @return external_single_structure
      * @since 3.2
      */
@@ -1926,9 +2045,19 @@ class core_message_external extends external_api {
         return self::get_messagearea_message_structure();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_get_most_recent_message_is_deprecated() {
+        return true;
+    }
+
     /**
      * The get profile parameters.
      *
+     * @deprecated since 3.6
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1944,6 +2073,7 @@ class core_message_external extends external_api {
     /**
      * Get the profile information for a contact.
      *
+     * @deprecated since 3.6
      * @param int $currentuserid The current user's id
      * @param int $otheruserid The id of the user whose profile we are viewing
      * @return stdClass
@@ -1964,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');
@@ -1981,6 +2111,7 @@ class core_message_external extends external_api {
     /**
      * The get profile return structure.
      *
+     * @deprecated since 3.6
      * @return external_single_structure
      * @since 3.2
      */
@@ -2002,6 +2133,15 @@ class core_message_external extends external_api {
         );
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_get_profile_is_deprecated() {
+        return true;
+    }
+
     /**
      * Get contacts parameters description.
      *
@@ -3047,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');
         }
@@ -3115,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 [];
         }
 
@@ -3208,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 {
@@ -3281,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');
         }
@@ -3363,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'];
@@ -3376,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']);
         }
     }
 
@@ -3438,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');
@@ -3740,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.');
         }
 
@@ -3799,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 cab15e9..f408ec2 100644 (file)
@@ -37,16 +37,16 @@ if (empty($CFG->messaging)) {
     print_error('disabled', 'message');
 }
 
-$id = optional_param('id', '', PARAM_INT); // The id of the request.
+$userid = optional_param('userid', '', PARAM_INT); // The userid of the request.
 $action = optional_param('action', '', PARAM_ALPHA);
 
 // Confirm the request is able to be approved/disapproved.
-if ($id) {
-    $request = $DB->get_record('message_contact_requests', ['id' => $id, 'requesteduserid' => $USER->id], '*', MUST_EXIST);
+if ($userid) {
+    $request = $DB->get_record('message_contact_requests', ['userid' => $userid, 'requesteduserid' => $USER->id], '*', MUST_EXIST);
 }
 
 // Use external functions as these are what we will be using in the new UI.
-if ($id && $action && confirm_sesskey()) {
+if ($userid && $action && confirm_sesskey()) {
     if ($action == 'approve') {
         core_message_external::confirm_contact_request($request->userid, $USER->id);
     } else if ($action == 'decline') {
@@ -67,13 +67,13 @@ $table->head = $headers;
 // Use external functions as these are what we will be using in the new UI.
 if ($contactrequests = core_message_external::get_contact_requests($USER->id)) {
     foreach ($contactrequests as $contactrequest) {
-        $approvelink = new moodle_url('/message/pendingcontactrequests.php', ['id' => $contactrequest->contactrequestid,
+        $approvelink = new moodle_url('/message/pendingcontactrequests.php', ['userid' => $contactrequest->id,
             'action' => 'approve', 'sesskey' => sesskey()]);
-        $declinelink = new moodle_url('/message/pendingcontactrequests.php', ['id' => $contactrequest->contactrequestid,
+        $declinelink = new moodle_url('/message/pendingcontactrequests.php', ['userid' => $contactrequest->id,
             'action' => 'decline', 'sesskey' => sesskey()]);
 
         $cells = array();
-        $cells[] = fullname($contactrequest);
+        $cells[] = $contactrequest->fullname;
         $cells[] = html_writer::link($approvelink, get_string('approve')) . " | " .
             html_writer::link($declinelink, get_string('cancel'));
         $table->data[] = new html_table_row($cells);
index dceda6f..145ec68 100644 (file)
@@ -1089,6 +1089,10 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 $this->assertObjectHasAttribute('showonlinestatus', $member);
                 $this->assertObjectHasAttribute('isblocked', $member);
                 $this->assertObjectHasAttribute('iscontact', $member);
+                $this->assertObjectHasAttribute('isdeleted', $member);
+                $this->assertObjectHasAttribute('canmessage', $member);
+                $this->assertObjectHasAttribute('requirescontact', $member);
+                $this->assertObjectHasAttribute('contactrequests', $member);
             }
             $this->assertObjectHasAttribute('messages', $conv);
             foreach ($conv->messages as $message) {
@@ -1271,6 +1275,51 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertNotContains($ic1->id, array_column($conversations, 'id'));
     }
 
+    /**
+     * Test verifying the behaviour of get_conversations() when fetching favourite conversations with only a single
+     * favourite.
+     */
+    public function test_get_conversations_favourite_conversations_single() {
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // Mark a single conversation as favourites.
+        \core_message\api::set_favourite_conversation($ic2->id, $user1->id);
+
+        // Get the conversation, first with no restrictions, confirming the favourite status of the conversations.
+        $conversations = \core_message\api::get_conversations($user1->id);
+        $this->assertCount(6, $conversations);
+        foreach ($conversations as $conv) {
+            if (in_array($conv->id, [$ic2->id])) {
+                $this->assertTrue($conv->isfavourite);
+            } else {
+                $this->assertFalse($conv->isfavourite);
+            }
+        }
+
+        // Now, get ONLY favourite conversations.
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20, null, true);
+        $this->assertCount(1, $conversations);
+        foreach ($conversations as $conv) {
+            $this->assertTrue($conv->isfavourite);
+            $this->assertEquals($ic2->id, $conv->id);
+        }
+
+        // Now, try ONLY favourites of type 'group'.
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20,
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, true);
+        $this->assertEmpty($conversations);
+
+        // And NO favourite conversations.
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20, null, false);
+        $this->assertCount(5, $conversations);
+        foreach ($conversations as $conv) {
+            $this->assertFalse($conv->isfavourite);
+            $this->assertNotEquals($ic2, $conv->id);
+        }
+    }
+
     /**
      * Test verifying the behaviour of get_conversations() when fetching favourite conversations.
      */
@@ -1289,13 +1338,16 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         \core_message\api::set_favourite_conversation($ic1->id, $user1->id);
         \core_message\api::set_favourite_conversation($gc2->id, $user1->id);
         \core_message\api::set_favourite_conversation($gc5->id, $user1->id);
+        $favouriteids = [$ic1->id, $gc2->id, $gc5->id];
 
         // Get the conversations, first with no restrictions, confirming the favourite status of the conversations.
         $conversations = \core_message\api::get_conversations($user1->id);
         $this->assertCount(6, $conversations);
         foreach ($conversations as $conv) {
-            if (in_array($conv->id, [$ic1->id, $gc2->id, $gc5->id])) {
+            if (in_array($conv->id, $favouriteids)) {
                 $this->assertTrue($conv->isfavourite);
+            } else {
+                $this->assertFalse($conv->isfavourite);
             }
         }
 
@@ -1304,6 +1356,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(3, $conversations);
         foreach ($conversations as $conv) {
             $this->assertTrue($conv->isfavourite);
+            $this->assertNotFalse(array_search($conv->id, $favouriteids));
         }
 
         // Now, try ONLY favourites of type 'group'.
@@ -1312,6 +1365,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(2, $conversations);
         foreach ($conversations as $conv) {
             $this->assertTrue($conv->isfavourite);
+            $this->assertNotFalse(array_search($conv->id, [$gc2->id, $gc5->id]));
         }
 
         // And NO favourite conversations.
@@ -1319,9 +1373,69 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(3, $conversations);
         foreach ($conversations as $conv) {
             $this->assertFalse($conv->isfavourite);
+            $this->assertFalse(array_search($conv->id, $favouriteids));
         }
     }
 
+    /**
+     * Test verifying get_conversations when there are users in a group and/or individual conversation. The reason this
+     * test is performed is because we do not need as much data for group conversations (saving DB calls), so we want
+     * to confirm this happens.
+     */
+    public function test_get_conversations_user_in_group_and_individual_chat() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ],
+            'Individual conversation'
+        );
+
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id);
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+            ],
+            'Group conversation'
+        );
+
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id);
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+        \core_message\api::create_contact_request($user1->id, $user3->id);
+
+        $conversations = \core_message\api::get_conversations($user2->id);
+
+        $groupconversation = array_shift($conversations);
+        $individualconversation = array_shift($conversations);
+
+        $this->assertEquals('Group conversation', $groupconversation->name);
+        $this->assertEquals('Individual conversation', $individualconversation->name);
+
+        $this->assertCount(1, $groupconversation->members);
+        $this->assertCount(1, $individualconversation->members);
+
+        $groupmember = reset($groupconversation->members);
+        $this->assertNull($groupmember->requirescontact);
+        $this->assertNull($groupmember->canmessage);
+        $this->assertEmpty($groupmember->contactrequests);
+
+        $individualmember = reset($individualconversation->members);
+        $this->assertNotNull($individualmember->requirescontact);
+        $this->assertNotNull($individualmember->canmessage);
+        $this->assertNotEmpty($individualmember->contactrequests);
+    }
+
     /**
      * Test verifying that group linked conversations are returned and contain a subname matching the course name.
      */
@@ -1836,7 +1950,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages.
         $messages = \core_message\api::get_messages($user1->id, $user2->id);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(4, count($messages));
@@ -2359,7 +2472,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the most recent messages.
         $message = \core_message\api::get_most_recent_message($user1->id, $user2->id);
-        $this->assertDebuggingCalledCount(3);
 
         // Check the results are correct.
         $this->assertEquals($user2->id, $message->useridfrom);
@@ -2877,6 +2989,380 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertTrue(\core_message\api::can_post_message($student2, $teacher1));
     }
 
+    /**
+     * Verify the expected behaviour of the can_send_message_to_conversation() method for authenticated users with default settings.
+     */
+    public function test_can_send_message_to_conversation_basic() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // For group conversations, there are no user privacy checks, so only membership in the conversation is needed.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // For individual conversations, the default privacy setting of 'only contacts and course members' applies.
+        // Users are not in the same course, nor are they contacts, so messages cannot be sent.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+
+        // After enrolling users to the course, they should be able to message them with the default privacy setting.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+    }
+
+    /**
+     * Verify the behaviour of can_send_message_to_conversation() for authenticated users without the sendmessage capability.
+     */
+    public function test_can_send_message_to_conversation_sendmessage_cap() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Default settings - user1 can send a message to both conversations.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Remove the capability to send a message.
+        $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
+        unassign_capability('moodle/site:sendmessage', $roleids['user'], context_system::instance());
+
+        // Verify that a user cannot send a message to either an individual or a group conversation.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+    }
+
+    /**
+     * Verify the behaviour of can_send_message_to_conversation() for authenticated users without the messageanyuser capability.
+     */
+    public function test_can_send_message_to_conversation_messageanyuser_cap() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Update the message preference for user2, so they can only be messaged by contacts.
+        set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS, $user2->id);
+
+        // Verify that the user cannot be contacted in the individual conversation and that groups are unaffected.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Assign the 'messageanyuser' capability to user1 at system context.
+        $systemcontext = context_system::instance();
+        $authenticateduser = $DB->get_record('role', ['shortname' => 'user']);
+        assign_capability('moodle/site:messageanyuser', CAP_ALLOW, $authenticateduser->id, $systemcontext->id);
+
+        // Check that user1 can now message user2 due to the capability, and that group conversations is again unaffected.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+    }
+
+    /**
+     * Test verifying that users cannot send messages to conversations they are not a part of.
+     */
+    public function test_can_post_message_to_conversation_non_member() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+        $user4 = self::getDataGenerator()->create_user();
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course->id);
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Verify, non members cannot send a message.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user4->id, $gc1->id));
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user4->id, $ic1->id));
+    }
+
+    /**
+     * Test verifying the behaviour of the can_send_message_to_conversation method when privacy is set to contacts only.
+     */
+    public function test_can_send_message_to_conversation_privacy_contacts_only() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Set the message privacy preference to 'contacts only' for user 2.
+        set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS, $user2->id);
+
+        // Verify that user1 cannot send a message to the individual conversation, but that the group conversation is unaffected.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Now, simulate a contact request (and approval) between user1 and user2.
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+        \core_message\api::confirm_contact_request($user1->id, $user2->id);
+
+        // Verify user1 can now message user2 again via their individual conversation.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+    }
+
+    /**
+     * Test verifying the behaviour of the can_send_message_to_conversation method when privacy is set to contacts / course members.
+     */
+    public function test_can_send_message_to_conversation_privacy_contacts_course() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Set the message privacy preference to 'contacts + course members' for user 2.
+        set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_COURSEMEMBER, $user2->id);
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Verify that users in a group conversation can message one another (i.e. privacy controls ignored).
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Verify that user1 can not message user2 unless they are either contacts, or share a course.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+
+        // Verify that user1 can send a message to user2, based on the shared course, without being a contact.
+        $this->assertFalse(\core_message\api::is_contact($user1->id, $user2->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+    }
+
+    /**
+     * Test verifying the behaviour of the can_send_message_to_conversation method when privacy is set to any user.
+     */
+    public function test_can_send_message_to_conversation_privacy_sitewide() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // By default, the messaging privacy dictates that users can only be contacted by contacts, and members of their courses.
+        // Verify also, that groups are not restricted in this way.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Enable site-wide messagging privacy setting.
+        // This enables a privacy option for users, allowing them to choose to be contactable by anybody on the site.
+        set_config('messagingallusers', true);
+
+        // Set the second user's preference to receive messages from everybody.
+        set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_SITE, $user2->id);
+
+        // Check that user1 can send user2 a message, and that the group conversation is unaffected.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Disable site-wide messagging privacy setting. The user will be able to receive messages from contacts
+        // and members sharing a course with her.
+        set_config('messagingallusers', false);
+
+        // As site-wide messaging setting is disabled, the value for user2 will be changed to MESSAGE_PRIVACY_COURSEMEMBER.
+        // Verify also that the group conversation is unaffected.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+    }
+
+    /**
+     * Test verifying the behaviour of the can_send_message_to_conversation method when a user is blocked.
+     */
+    public function test_can_send_message_to_conversation_when_blocked() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+
+        // Block the second user.
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        // Check that user2 can not send user1 a message in their individual conversation.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user2->id, $ic1->id));
+
+        // Verify that group conversations are unaffected.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user2->id, $gc1->id));
+    }
+
     /**
      * Tests get_user_privacy_messaging_preference method.
      */
@@ -3127,7 +3613,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages from $time, which should be all of them.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(4, count($messages));
@@ -3144,7 +3629,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages from $time + 3, which should only be the 2 last messages.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 3);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(2, count($messages));
@@ -3176,7 +3660,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages up until $time + 4, which should be all of them.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 4);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(4, count($messages));
@@ -3193,7 +3676,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages up until $time + 2, which should be the first two.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 2);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(2, count($messages));
@@ -3225,7 +3707,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages from $time + 2 up until $time + 3, which should be 2nd and 3rd message.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 2, $time + 3);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(2, count($messages));
@@ -4000,14 +4481,44 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $request = reset($requests);
 
         $this->assertEquals($user2->id, $request->id);
-        $this->assertEquals($user2->picture, $request->picture);
-        $this->assertEquals($user2->firstname, $request->firstname);
-        $this->assertEquals($user2->lastname, $request->lastname);
-        $this->assertEquals($user2->firstnamephonetic, $request->firstnamephonetic);
-        $this->assertEquals($user2->lastnamephonetic, $request->lastnamephonetic);
-        $this->assertEquals($user2->middlename, $request->middlename);
-        $this->assertEquals($user2->alternatename, $request->alternatename);
-        $this->assertEquals($user2->email, $request->email);
+        $this->assertEquals(fullname($user2), $request->fullname);
+        $this->assertObjectHasAttribute('profileimageurl', $request);
+        $this->assertObjectHasAttribute('profileimageurlsmall', $request);
+        $this->assertObjectHasAttribute('isonline', $request);
+        $this->assertObjectHasAttribute('showonlinestatus', $request);
+        $this->assertObjectHasAttribute('isblocked', $request);
+        $this->assertObjectHasAttribute('iscontact', $request);
+    }
+
+    /**
+     * Test getting contact requests when there are none.
+     */
+    public function test_get_contact_requests_no_requests() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+
+        $requests = \core_message\api::get_contact_requests($user1->id);
+
+        $this->assertEmpty($requests);
+    }
+
+    /**
+     * Test getting contact requests with limits.
+     */
+    public function test_get_contact_requests_with_limits() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+        \core_message\api::create_contact_request($user3->id, $user1->id);
+
+        $requests = \core_message\api::get_contact_requests($user1->id, 0, 1);
+
+        $this->assertCount(1, $requests);
     }
 
     /**
@@ -4282,6 +4793,14 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($conversation->id, $member3->conversationid);
     }
 
+    /**
+     * Test creating an invalid conversation.
+     */
+    public function test_create_conversation_invalid() {
+        $this->expectException('moodle_exception');
+        \core_message\api::create_conversation(3, [1, 2, 3]);
+    }
+
     /**
      * Test creating an individual conversation with too many members.
      */
@@ -4450,7 +4969,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member1->showonlinestatus);
         $this->assertEquals(false, $member1->iscontact);
         $this->assertEquals(false, $member1->isblocked);
-        $this->assertObjectNotHasAttribute('contactrequests', $member1);
+        $this->assertObjectHasAttribute('contactrequests', $member1);
+        $this->assertEmpty($member1->contactrequests);
 
         $this->assertEquals($user2->id, $member2->id);
         $this->assertEquals(fullname($user2), $member2->fullname);
@@ -4458,7 +4978,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member2->showonlinestatus);
         $this->assertEquals(true, $member2->iscontact);
         $this->assertEquals(false, $member2->isblocked);
-        $this->assertObjectNotHasAttribute('contactrequests', $member2);
+        $this->assertObjectHasAttribute('contactrequests', $member2);
+        $this->assertEmpty($member2->contactrequests);
 
         $this->assertEquals($user3->id, $member3->id);
         $this->assertEquals(fullname($user3), $member3->fullname);
@@ -4466,7 +4987,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member3->showonlinestatus);
         $this->assertEquals(false, $member3->iscontact);
         $this->assertEquals(true, $member3->isblocked);
-        $this->assertObjectNotHasAttribute('contactrequests', $member3);
+        $this->assertObjectHasAttribute('contactrequests', $member3);
+        $this->assertEmpty($member3->contactrequests);
     }
 
     /**
@@ -4521,7 +5043,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member1->showonlinestatus);
         $this->assertEquals(false, $member1->iscontact);
         $this->assertEquals(false, $member1->isblocked);
-        $this->assertCount(3, $member1->contactrequests);
+        $this->assertCount(2, $member1->contactrequests);
 
         $this->assertEquals($user2->id, $member2->id);
         $this->assertEquals(fullname($user2), $member2->fullname);
@@ -4529,7 +5051,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member2->showonlinestatus);
         $this->assertEquals(true, $member2->iscontact);
         $this->assertEquals(false, $member2->isblocked);
-        $this->assertCount(2, $member2->contactrequests);
+        $this->assertCount(1, $member2->contactrequests);
 
         $this->assertEquals($user3->id, $member3->id);
         $this->assertEquals(fullname($user3), $member3->fullname);
@@ -4537,12 +5059,11 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member3->showonlinestatus);
         $this->assertEquals(false, $member3->iscontact);
         $this->assertEquals(true, $member3->isblocked);
-        $this->assertCount(2, $member3->contactrequests);
+        $this->assertCount(1, $member3->contactrequests);
 
         // Confirm the contact requests are OK.
         $request1 = array_shift($member1->contactrequests);
         $request2 = array_shift($member1->contactrequests);
-        $request3 = array_shift($member1->contactrequests);
 
         $this->assertEquals($user1->id,