Merge branch 'MDL-63903-master' of https://github.com/snake/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Thu, 8 Nov 2018 06:02:10 +0000 (14:02 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Thu, 8 Nov 2018 06:02:10 +0000 (14:02 +0800)
55 files changed:
admin/tool/dataprivacy/classes/expired_contexts_manager.php
admin/tool/dataprivacy/tests/expired_contexts_test.php
admin/tool/mobile/classes/api.php
admin/tool/mobile/tests/externallib_test.php
auth/classes/output/login.php
auth/ldap/auth.php
auth/shibboleth/index.php
auth/shibboleth/index_form.html
auth/shibboleth/login.php
auth/upgrade.txt
blocks/login/block_login.php
blocks/starredcourses/db/services.php
config-dist.php
lib/classes/session/manager.php
lib/db/services.php
lib/lessphp/Visitor/toCSS.php
lib/lessphp/moodle_readme.txt
lib/moodlelib.php
lib/templates/loginform.mustache
lib/testing/generator/data_generator.php
lib/tests/authlib_test.php
login/change_password_form.php
login/index.php
login/token.php
message/classes/api.php
message/classes/helper.php
message/externallib.php
message/output/popup/amd/build/notification_popover_controller.min.js
message/output/popup/amd/src/notification_popover_controller.js
message/output/popup/tests/behat/message_popover_preferences.feature
message/output/popup/tests/behat/message_popover_unread.feature
message/output/popup/tests/behat/notification_popover_preferences.feature
message/output/popup/tests/behat/notification_popover_unread.feature
message/tests/api_test.php
message/tests/behat/delete_all_messages.feature
message/tests/behat/delete_messages.feature
message/tests/behat/manage_contacts.feature
message/tests/behat/reply_message.feature
message/tests/behat/search_messages.feature
message/tests/behat/search_users.feature
message/tests/behat/update_messaging_preferences.feature
message/tests/behat/view_messages.feature
message/tests/externallib_test.php
message/upgrade.txt
mod/assign/renderer.php
question/type/ddmarker/styles.css
search/engine/solr/classes/engine.php
theme/boost/scss/moodle/blocks.scss
theme/boost/style/moodle.css
theme/boost/templates/core/loginform.mustache
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/style/moodle.css
user/tests/behat/delete_users.feature
user/tests/behat/view_full_profile.feature
webservice/externallib.php

index c2dc169..fa63eb0 100644 (file)
@@ -786,11 +786,13 @@ class expired_contexts_manager {
         $parents = $context->get_parent_contexts(true);
         foreach ($parents as $parent) {
             if ($parent instanceof \context_course) {
+                // This is a context within a course. Check whether _this context_ is expired as a function of a course.
                 return self::is_course_context_expired($context);
             }
 
             if ($parent instanceof \context_user) {
-                return self::are_user_context_dependencies_expired($context);
+                // This is a context within a user. Check whether the _user_ has expired.
+                return self::are_user_context_dependencies_expired($parent);
             }
         }
 
@@ -810,12 +812,13 @@ class expired_contexts_manager {
     }
 
     /**
-     * Determine whether the supplied course context has expired.
+     * Determine whether the supplied course-related context has expired.
+     * Note: This is not necessarily a _course_ context, but a context which is _within_ a course.
      *
-     * @param   \context_course $context
+     * @param   \context        $context
      * @return  bool
      */
-    protected static function is_course_context_expired(\context_course $context) : bool {
+    protected static function is_course_context_expired(\context $context) : bool {
         $expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
 
         return !empty($expiryrecords[$context->path]) && $expiryrecords[$context->path]->info->is_fully_expired();
@@ -890,11 +893,13 @@ class expired_contexts_manager {
         $parents = $context->get_parent_contexts(true);
         foreach ($parents as $parent) {
             if ($parent instanceof \context_course) {
-                return self::is_course_context_expired_or_unprotected_for_user($parent, $user);
+                // This is a context within a course. Check whether _this context_ is expired as a function of a course.
+                return self::is_course_context_expired_or_unprotected_for_user($context, $user);
             }
 
             if ($parent instanceof \context_user) {
-                return self::are_user_context_dependencies_expired($context);
+                // This is a context within a user. Check whether the _user_ has expired.
+                return self::are_user_context_dependencies_expired($parent);
             }
         }
 
@@ -902,13 +907,14 @@ class expired_contexts_manager {
     }
 
     /**
-     * Determine whether the supplied course context has expired, or is unprotected.
+     * Determine whether the supplied course-related context has expired, or is unprotected.
+     * Note: This is not necessarily a _course_ context, but a context which is _within_ a course.
      *
-     * @param   \context_course $context
+     * @param   \context        $context
      * @param   \stdClass       $user
      * @return  bool
      */
-    protected static function is_course_context_expired_or_unprotected_for_user(\context_course $context, \stdClass $user) {
+    protected static function is_course_context_expired_or_unprotected_for_user(\context $context, \stdClass $user) {
         $expiryrecords = self::get_nested_expiry_info_for_courses($context->path);
 
         $info = $expiryrecords[$context->path]->info;
index 6058ac5..08ffb80 100644 (file)
@@ -97,7 +97,7 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
             api::set_contextlevel($record);
         } else {
             list($purposevar, ) = data_registry::var_names_from_context(
-                    \context_helper::get_class_for_level(CONTEXT_COURSE)
+                    \context_helper::get_class_for_level($contextlevel)
                 );
             set_config($purposevar, $purpose->get('id'), 'tool_dataprivacy');
         }
@@ -1719,18 +1719,17 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $manager->set_progress(new \null_progress_trace());
         $manager->method('get_privacy_manager')->willReturn($mockprivacymanager);
 
-        $purposes->course->set('retentionperiod', 'P5Y');
-        $purposes->course->save();
+        // Changing the retention period to a longer period will remove the expired_context record.
+        $purposes->activity->set('retentionperiod', 'P5Y');
+        $purposes->activity->save();
 
         list($processedcourses, $processedusers) = $manager->process_approved_deletions();
 
         $this->assertEquals(0, $processedcourses);
         $this->assertEquals(0, $processedusers);
 
+        $this->expectException('dml_missing_record_exception');
         $updatedcontext = new expired_context($expiredcontext->get('id'));
-
-        // No change - we just can't process it until the children have finished.
-        $this->assertEquals(expired_context::STATUS_APPROVED, $updatedcontext->get('status'));
     }
 
     /**
@@ -2186,8 +2185,6 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
      * Test the is_context_expired functions when supplied with the system context.
      */
     public function test_is_context_expired_system() {
-        global $DB;
-
         $this->resetAfterTest();
         $this->setup_basics('PT1H', 'PT1H', 'P1D');
         $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
@@ -2197,12 +2194,38 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
                 expired_contexts_manager::is_context_expired_or_unprotected_for_user(\context_system::instance(), $user));
     }
 
+    /**
+     * Test the is_context_expired functions when supplied with a block in the user context.
+     *
+     * Children of a user context always follow the user expiry rather than any context level defaults (e.g. at the
+     * block level.
+     */
+    public function test_is_context_expired_user_block() {
+        $this->resetAfterTest();
+
+        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
+        $purposes->block = $this->create_and_set_purpose_for_contextlevel('P5Y', CONTEXT_BLOCK);
+
+        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+        $this->setUser($user);
+        $block = $this->create_user_block('Title', 'Content', FORMAT_PLAIN);
+        $blockcontext = \context_block::instance($block->instance->id);
+        $this->setUser();
+
+        // Protected flags have no bearing on expiry of user subcontexts.
+        $this->assertTrue(expired_contexts_manager::is_context_expired($blockcontext));
+
+        $purposes->block->set('protected', 1)->save();
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));
+
+        $purposes->block->set('protected', 0)->save();
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($blockcontext, $user));
+    }
+
     /**
      * Test the is_context_expired functions when supplied with an expired course.
      */
     public function test_is_context_expired_course_expired() {
-        global $DB;
-
         $this->resetAfterTest();
 
         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
@@ -2226,8 +2249,6 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
      * Test the is_context_expired functions when supplied with an unexpired course.
      */
     public function test_is_context_expired_course_unexpired() {
-        global $DB;
-
         $this->resetAfterTest();
 
         $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D');
@@ -2247,6 +2268,42 @@ class tool_dataprivacy_expired_contexts_testcase extends advanced_testcase {
         $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
     }
 
+    /**
+     * Test the is_context_expired functions when supplied with an unexpired course and a child context in the course which is protected.
+     *
+     * When a child context has a specific purpose set, then that purpose should be respected with respect to the
+     * course.
+     *
+     * If the course is still within the expiry period for the child context, then that child's protected flag should be
+     * respected, even when the course may have expired.
+     */
+    public function test_is_child_context_expired_course_unexpired_with_child() {
+        $this->resetAfterTest();
+
+        $purposes = $this->setup_basics('PT1H', 'PT1H', 'P1D', 'P1D');
+        $purposes->course->set('protected', 0)->save();
+        $purposes->activity->set('protected', 1)->save();
+
+        $user = $this->getDataGenerator()->create_user(['lastaccess' => time() - YEARSECS]);
+        $course = $this->getDataGenerator()->create_course(['startdate' => time() - YEARSECS, 'enddate' => time() + WEEKSECS]);
+        $forum = $this->getDataGenerator()->create_module('forum', ['course' => $course->id]);
+
+        $coursecontext = \context_course::instance($course->id);
+        $cm = get_coursemodule_from_instance('forum', $forum->id);
+        $forumcontext = \context_module::instance($cm->id);
+
+        $this->getDataGenerator()->enrol_user($user->id, $course->id, 'student');
+
+        $this->assertFalse(expired_contexts_manager::is_context_expired($coursecontext));
+        $this->assertFalse(expired_contexts_manager::is_context_expired($forumcontext));
+
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($coursecontext, $user));
+        $this->assertFalse(expired_contexts_manager::is_context_expired_or_unprotected_for_user($forumcontext, $user));
+
+        $purposes->activity->set('protected', 0)->save();
+        $this->assertTrue(expired_contexts_manager::is_context_expired_or_unprotected_for_user($forumcontext, $user));
+    }
+
     /**
      * Test the is_context_expired functions when supplied with an expired course which has role overrides.
      */
index 5fcd7f7..71cf2bb 100644 (file)
@@ -336,6 +336,7 @@ class api {
         $mainmenu = new lang_string('mainmenu', 'tool_mobile');
         $course = new lang_string('course');
         $modules = new lang_string('managemodules');
+        $blocks = new lang_string('blocks');
         $user = new lang_string('user');
         $files = new lang_string('files');
         $remoteaddons = new lang_string('remoteaddons', 'tool_mobile');
@@ -359,6 +360,25 @@ class api {
 
         }
 
+        // Display blocks.
+        $availableblocks = core_plugin_manager::instance()->get_plugins_of_type('block');
+        $courseblocks = array();
+        $appsupportedblocks = array(
+            'activity_modules' => 'CoreBlockDelegate_AddonBlockActivityModules',
+            'site_main_menu' => 'CoreBlockDelegate_AddonBlockSiteMainMenu',
+            'myoverview' => 'CoreBlockDelegate_AddonBlockMyOverview',
+            'timeline' => 'CoreBlockDelegate_AddonBlockTimeline',
+            'recentlyaccessedcourses' => 'CoreBlockDelegate_AddonBlockRecentlyAccessedCourses',
+            'starredcourses' => 'CoreBlockDelegate_AddonBlockStarredCourses',
+            'recentlyaccesseditems' => 'CoreBlockDelegate_AddonBlockRecentlyAccessedItems',
+        );
+
+        foreach ($availableblocks as $block) {
+            if (isset($appsupportedblocks[$block->name])) {
+                $courseblocks[$appsupportedblocks[$block->name]] = $block->displayname;
+            }
+        }
+
         $features = array(
             'NoDelegate_CoreOffline' => new lang_string('offlineuse', 'tool_mobile'),
             '$mmLoginEmailSignup' => new lang_string('startsignup'),
@@ -401,6 +421,7 @@ class api {
                 'files_upload' => new lang_string('upload'),
             ),
             "$modules" => $coursemodules,
+            "$blocks" => $courseblocks,
         );
 
         if (!empty($remoteaddonslist)) {
index d5a8a34..00db97e 100644 (file)
@@ -109,6 +109,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         set_config('agedigitalconsentverification', 1);
         set_config('autolang', 1);
         set_config('lang', 'a_b');  // Set invalid lang.
+        set_config('disabledfeatures', 'myoverview', 'tool_mobile');
 
         list($authinstructions, $notusedformat) = external_format_text($authinstructions, FORMAT_MOODLE, $context->id);
         $expected['registerauth'] = 'email';
@@ -120,6 +121,7 @@ class tool_mobile_external_testcase extends externallib_advanced_testcase {
         $expected['supportemail'] = $CFG->supportemail;
         $expected['autolang'] = '1';
         $expected['lang'] = ''; // Expect empty because it was set to an invalid lang.
+        $expected['tool_mobile_disabledfeatures'] = 'myoverview';
 
         if ($logourl = $OUTPUT->get_logo_url()) {
             $expected['logourl'] = $logourl->out(false);
index 584abb8..7034e97 100644 (file)
@@ -70,6 +70,8 @@ class login implements renderable, templatable {
     public $signupurl;
     /** @var string The user name to pre-fill the form with. */
     public $username;
+    /** @var string The csrf token to limit login to requests that come from the login form. */
+    public $logintoken;
 
     /**
      * Constructor.
@@ -109,6 +111,7 @@ class login implements renderable, templatable {
 
         // Identity providers.
         $this->identityproviders = \auth_plugin_base::get_identity_providers($authsequence);
+        $this->logintoken = \core\session\manager::get_login_token();
     }
 
     /**
@@ -141,6 +144,7 @@ class login implements renderable, templatable {
         $data->rememberusername = $this->rememberusername;
         $data->signupurl = $this->signupurl->out(false);
         $data->username = $this->username;
+        $data->logintoken = $this->logintoken;
 
         return $data;
     }
index 4c0ca9e..cd9e347 100644 (file)
@@ -1725,7 +1725,8 @@ class auth_plugin_ldap extends auth_plugin_base {
 
         // Here we want to trigger the whole authentication machinery
         // to make sure no step is bypassed...
-        $user = authenticate_user_login($username, $key);
+        $reason = null;
+        $user = authenticate_user_login($username, $key, false, $reason, false);
         if ($user) {
             complete_user_login($user);
 
index 07b5ce7..b177f7c 100644 (file)
         $frm->password = generate_password(8);
 
     /// Check if the user has actually submitted login data to us
+        $reason = null;
 
         if ($shibbolethauth->user_login($frm->username, $frm->password)
-                && $user = authenticate_user_login($frm->username, $frm->password)) {
+                && $user = authenticate_user_login($frm->username, $frm->password, false, $reason, false)) {
             complete_user_login($user);
 
             if (user_not_fully_set_up($USER, true)) {
index 144c1fd..9f1e23e 100644 (file)
@@ -54,6 +54,7 @@ if ($show_instructions) {
         </div>
         <form action="../../login/index.php" method="post" id="guestlogin">
           <div class="guestform">
+            <input type="hidden" name="logintoken" value="<?php echo s(\core\session\manager::get_login_token()); ?>" />
             <input type="hidden" name="username" value="guest" />
             <input type="hidden" name="password" value="guest" />
             <input type="submit" value="<?php print_string("loginguest") ?>" />
index 2ed931a..6877fcb 100644 (file)
     $PAGE->set_heading($site->fullname);
 
     echo $OUTPUT->header();
-    include("index_form.html");
+
+    if (isloggedin() and !isguestuser()) {
+        // Prevent logging when already logged in, we do not want them to relogin by accident because sesskey would be changed.
+        echo $OUTPUT->box_start();
+        $params = array('sesskey' => sesskey(), 'loginpage' => 1);
+        $logout = new single_button(new moodle_url('/login/logout.php', $params), get_string('logout'), 'post');
+        $continue = new single_button(new moodle_url('/'), get_string('cancel'), 'get');
+        echo $OUTPUT->confirm(get_string('alreadyloggedin', 'error', fullname($USER)), $logout, $continue);
+        echo $OUTPUT->box_end();
+    } else {
+        include("index_form.html");
+    }
+
     echo $OUTPUT->footer();
 
 
index 138cbbc..4e0502b 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /auth/* - plugins,
 information provided here is intended especially for developers.
 
+=== 3.6 ===
+
+* Login forms generated from Moodle must include a login token to protect automated logins. See \core\session\manager::get_login_token().
+
 === 3.5 ===
 
 * The auth_db and auth_ldap plugins' implementations of update_user_record() have been removed and both now
index e17aea1..67d0d03 100644 (file)
@@ -86,6 +86,7 @@ class block_login extends block_base {
             $this->content->text .= '<div class="form-group">';
             $this->content->text .= '<input type="submit" class="btn btn-primary btn-block" value="'.get_string('login').'" />';
             $this->content->text .= '</div>';
+            $this->content->text .= '<input type="hidden" name="logintoken" value="'.s(\core\session\manager::get_login_token()).'" />';
 
             $this->content->text .= "</form>\n";
 
index fc3ebb9..cd5edb4 100644 (file)
@@ -33,6 +33,7 @@ $functions = array(
         'description' => 'Get users starred courses.',
         'type'        => 'read',
         'ajax'        => true,
+        'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
 );
 
index d48ecab..18cbe31 100644 (file)
@@ -594,6 +594,11 @@ $CFG->admin = 'admin';
 //
 //      $CFG->keepmessagingallusersenabled = true;
 //
+// Disable login token validation for login pages. Login token validation is enabled
+// by default unless $CFG->alternateloginurl is set.
+//
+//      $CFG->disablelogintoken = true;
+//
 //=========================================================================
 // 7. SETTINGS FOR DEVELOPMENT SERVERS - not intended for production use!!!
 //=========================================================================
index 6568e4b..a84424c 100644 (file)
@@ -46,6 +46,9 @@ class manager {
     /** @var bool $sessionactive Is the session active? */
     protected static $sessionactive = null;
 
+    /** @var string $logintokenkey Key used to get and store request protection for login form. */
+    protected static $logintokenkey = 'core_auth_login';
+
     /**
      * Start user session.
      *
@@ -923,4 +926,102 @@ class manager {
         )));
     }
 
+    /**
+     * Generate a new login token and store it in the session.
+     *
+     * @return array The current login state.
+     */
+    private static function create_login_token() {
+        global $SESSION;
+
+        $state = [
+            'token' => random_string(32),
+            'created' => time() // Server time - not user time.
+        ];
+
+        if (!isset($SESSION->logintoken)) {
+            $SESSION->logintoken = [];
+        }
+
+        // Overwrite any previous values.
+        $SESSION->logintoken[self::$logintokenkey] = $state;
+
+        return $state;
+    }
+
+    /**
+     * Get the current login token or generate a new one.
+     *
+     * All login forms generated from Moodle must include a login token
+     * named "logintoken" with the value being the result of this function.
+     * Logins will be rejected if they do not include this token as well as
+     * the username and password fields.
+     *
+     * @return string The current login token.
+     */
+    public static function get_login_token() {
+        global $CFG, $SESSION;
+
+        $state = false;
+
+        if (!isset($SESSION->logintoken)) {
+            $SESSION->logintoken = [];
+        }
+
+        if (array_key_exists(self::$logintokenkey, $SESSION->logintoken)) {
+            $state = $SESSION->logintoken[self::$logintokenkey];
+        }
+        if (empty($state)) {
+            $state = self::create_login_token();
+        }
+
+        // Check token lifespan.
+        if ($state['created'] < (time() - $CFG->sessiontimeout)) {
+            $state = self::create_login_token();
+        }
+
+        // Return the current session login token.
+        if (array_key_exists('token', $state)) {
+            return $state['token'];
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Check the submitted value against the stored login token.
+     *
+     * @param mixed $token The value submitted in the login form that we are validating.
+     *                     If false is passed for the token, this function will always return true.
+     * @return boolean If the submitted token is valid.
+     */
+    public static function validate_login_token($token = false) {
+        global $CFG;
+
+        if (!empty($CFG->alternateloginurl) || !empty($CFG->disablelogintoken)) {
+            // An external login page cannot generate the login token we need to protect CSRF on
+            // login requests.
+            // Other custom login workflows may skip this check by setting disablelogintoken in config.
+            return true;
+        }
+        if ($token === false) {
+            // authenticate_user_login is a core function was extended to validate tokens.
+            // For existing uses other than the login form it does not
+            // validate that a token was generated.
+            // Some uses that do not validate the token are login/token.php,
+            // or an auth plugin like auth/ldap/auth.php.
+            return true;
+        }
+
+        $currenttoken = self::get_login_token();
+
+        // We need to clean the login token so the old one is not valid again.
+        self::create_login_token();
+
+        if ($currenttoken !== $token) {
+            // Fail the login.
+            return false;
+        }
+        return true;
+    }
 }
index 53cc5eb..346ea3c 100644 (file)
@@ -542,7 +542,8 @@ $functions = array(
         'classpath' => 'course/externallib.php',
         'description' => 'List of enrolled courses for the given timeline classification (past, inprogress, or future).',
         'type' => 'read',
-        'ajax' => true
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_course_get_recent_courses' => array(
         'classname' => 'core_course_external',
@@ -550,7 +551,17 @@ $functions = array(
         'classpath' => 'course/externallib.php',
         'description' => 'List of courses a user has accessed most recently.',
         'type' => 'read',
-        'ajax' => true
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
+    'core_course_set_favourite_courses' => array(
+        'classname' => 'core_course_external',
+        'methodname' => 'set_favourite_courses',
+        'classpath' => 'course/externallib.php',
+        'description' => 'Add a list of courses to the list of favourite courses.',
+        'type' => 'read',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_enrol_get_course_enrolment_methods' => array(
         'classname' => 'core_enrol_external',
@@ -989,7 +1000,8 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_search_users',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for searching for people',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for searching for people',
         'type' => 'read',
         'ajax' => true,
     ),
@@ -997,9 +1009,19 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_search_users_in_course',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for searching for people in a course',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for searching for people in a course',
+        'type' => 'read',
+        'ajax' => true,
+    ),
+    'core_message_message_search_users' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'message_search_users',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Retrieve the data for searching for people',
         'type' => 'read',
         'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_message_data_for_messagearea_conversations' => array(
         'classname' => 'core_message_external',
@@ -2319,14 +2341,6 @@ $functions = array(
         'type'        => 'read',
         'services'    => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
-    'core_course_set_favourite_courses' => array(
-        'classname' => 'core_course_external',
-        'methodname' => 'set_favourite_courses',
-        'classpath' => 'course/externallib.php',
-        'description' => 'Add a list of courses to the list of favourite courses.',
-        'type' => 'read',
-        'ajax' => true
-    )
 );
 
 $services = array(
index 18d328f..8aaca96 100644 (file)
@@ -110,7 +110,7 @@ class Less_Visitor_toCSS extends Less_VisitorReplacing{
 
 
                // Compile rules and rulesets
-               $nodeRuleCnt = count($rulesetNode->rules);
+               $nodeRuleCnt = $rulesetNode->rules?count($rulesetNode->rules):0;
                for( $i = 0; $i < $nodeRuleCnt; ){
                        $rule = $rulesetNode->rules[$i];
 
index fcfaf4b..2b9274d 100644 (file)
@@ -8,9 +8,13 @@ All the files from the folder lib/Less are copied in
 this directory. Only exception made for the directory
 '.easymin' which is not included.
 
-Verify that https://github.com/oyejorge/less.php/pull/367 has been applied to the imported version or apply it locally.
-
 Also copy the license file from the project root.
 
 Licensed under the Apache license 2.0.
 
+Modifications:
+* MDL-63422 - Verify that https://github.com/oyejorge/less.php/pull/367 has been applied to the
+    imported version or apply it locally. PHP 7.3 compatibility.
+* MDL-62294 - Cherry-picked upstream commit to fix PHP 7.2 compatibility when counting ruleset rules.
+    https://github.com/oyejorge/less.php/commit/669acc51817a8da162b5f1b7137e79f0e4acc636
+    TODO: Remove this note when this library gets upgraded to the latest release that already includes this fix.
index 6c760dd..faa4580 100644 (file)
@@ -2548,6 +2548,9 @@ function dayofweek($day, $month, $year) {
 /**
  * Returns full login url.
  *
+ * Any form submissions for authentication to this URL must include username,
+ * password as well as a logintoken generated by \core\session\manager::get_login_token().
+ *
  * @return string login url
  */
 function get_login_url() {
@@ -4286,9 +4289,10 @@ function guest_user() {
  * @param string $password  User's password
  * @param bool $ignorelockout useful when guessing is prevented by other mechanism such as captcha or SSO
  * @param int $failurereason login failure reason, can be used in renderers (it may disclose if account exists)
+ * @param mixed logintoken If this is set to a string it is validated against the login token for the session.
  * @return stdClass|false A {@link $USER} object or false if error
  */
-function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null) {
+function authenticate_user_login($username, $password, $ignorelockout=false, &$failurereason=null, $logintoken=false) {
     global $CFG, $DB;
     require_once("$CFG->libdir/authlib.php");
 
@@ -4310,6 +4314,18 @@ function authenticate_user_login($username, $password, $ignorelockout=false, &$f
         }
     }
 
+    // Make sure this request came from the login form.
+    if (!\core\session\manager::validate_login_token($logintoken)) {
+        $failurereason = AUTH_LOGIN_FAILED;
+
+        // Trigger login failed event.
+        $event = \core\event\user_login_failed::create(array('userid' => $user->id,
+                'other' => array('username' => $username, 'reason' => $failurereason)));
+        $event->trigger();
+        error_log('[client '.getremoteaddr()."]  $CFG->wwwroot  Invalid Login Token:  $username  ".$_SERVER['HTTP_USER_AGENT']);
+        return false;
+    }
+
     $authsenabled = get_enabled_auth_plugins();
 
     if ($user) {
index 616e860..7aa51a6 100644 (file)
@@ -35,7 +35,8 @@
         "rememberusername": true,
         "signupurl": "http://localhost/stable_master/login/signup.php",
         "cookieshelpiconformatted": "",
-        "username": ""
+        "username": "",
+        "logintoken": "randomstring"
     }
 }}
 {{#hasinstructions}}
@@ -95,6 +96,7 @@
                 <div class="clearer"><!-- --></div>
                 <input id="anchor" type="hidden" name="anchor" value="" />
                 <script>document.getElementById('anchor').value = location.hash;</script>
+                <input type="hidden" name="logintoken" value="{{logintoken}}">
                 <input type="submit" id="loginbtn" value={{#quote}}{{#str}} login {{/str}}{{/quote}} />
                 <div class="forgetpass">
                     <a href="{{forgotpasswordurl}}">{{#str}} forgotten {{/str}}</a>
                 <div class="desc">{{#str}} someallowguest {{/str}}</div>
                 <form action="{{loginurl}}" method="post" id="guestlogin">
                     <div class="guestform">
+                        <input type="hidden" name="logintoken" value="{{logintoken}}">
                         <input type="hidden" name="username" value="guest" />
                         <input type="hidden" name="password" value="guest" />
                         <input type="submit" value={{#quote}}{{#str}} loginguest {{/str}}{{/quote}} />
index 23b5042..961d669 100644 (file)
@@ -542,6 +542,18 @@ EOD;
 
         $id = groups_create_group((object)$record);
 
+        // Allow tests to set group pictures.
+        if (!empty($record['picturepath'])) {
+            require_once($CFG->dirroot . '/lib/gdlib.php');
+            $grouppicture = process_new_icon(\context_course::instance($record['courseid']), 'group', 'icon', $id,
+                $record['picturepath']);
+
+            $DB->set_field('groups', 'picture', $grouppicture, ['id' => $id]);
+
+            // Invalidate the group data as we've updated the group record.
+            cache_helper::invalidate_by_definition('core', 'groupdata', array(), [$record['courseid']]);
+        }
+
         return $DB->get_record('groups', array('id'=>$id));
     }
 
index 53ad307..e59e383 100644 (file)
@@ -200,6 +200,59 @@ class core_authlib_testcase extends advanced_testcase {
         $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED);
         $this->assertEventContextNotUsed($event);
 
+        // Capture failed login token.
+        unset($CFG->alternateloginurl);
+        unset($CFG->disablelogintoken);
+        $sink = $this->redirectEvents();
+        $result = authenticate_user_login('username1', 'password1', false, $reason, 'invalidtoken');
+        $events = $sink->get_events();
+        $sink->close();
+        $event = array_pop($events);
+
+        $this->assertFalse($result);
+        $this->assertEquals(AUTH_LOGIN_FAILED, $reason);
+        // Test Event.
+        $this->assertInstanceOf('\core\event\user_login_failed', $event);
+        $expectedlogdata = array(SITEID, 'login', 'error', 'index.php', 'username1');
+        $this->assertEventLegacyLogData($expectedlogdata, $event);
+        $eventdata = $event->get_data();
+        $this->assertSame($eventdata['other']['username'], 'username1');
+        $this->assertSame($eventdata['other']['reason'], AUTH_LOGIN_FAILED);
+        $this->assertEventContextNotUsed($event);
+
+        // Login should work with invalid token if CFG login token settings override it.
+        $CFG->alternateloginurl = 'http://localhost/';
+        $sink = $this->redirectEvents();
+        $result = authenticate_user_login('username1', 'password1', false, $reason, 'invalidtoken');
+        $events = $sink->get_events();
+        $sink->close();
+        $this->assertEmpty($events);
+        $this->assertInstanceOf('stdClass', $result);
+        $this->assertEquals(AUTH_LOGIN_OK, $reason);
+
+        unset($CFG->alternateloginurl);
+        $CFG->disablelogintoken = true;
+
+        $sink = $this->redirectEvents();
+        $result = authenticate_user_login('username1', 'password1', false, $reason, 'invalidtoken');
+        $events = $sink->get_events();
+        $sink->close();
+        $this->assertEmpty($events);
+        $this->assertInstanceOf('stdClass', $result);
+        $this->assertEquals(AUTH_LOGIN_OK, $reason);
+
+        unset($CFG->disablelogintoken);
+        // Normal login with valid token.
+        $reason = null;
+        $token = \core\session\manager::get_login_token();
+        $sink = $this->redirectEvents();
+        $result = authenticate_user_login('username1', 'password1', false, $reason, $token);
+        $events = $sink->get_events();
+        $sink->close();
+        $this->assertEmpty($events);
+        $this->assertInstanceOf('stdClass', $result);
+        $this->assertEquals(AUTH_LOGIN_OK, $reason);
+
         $reason = null;
         // Capture failed login event.
         $sink = $this->redirectEvents();
index 66654f3..5064690 100644 (file)
@@ -85,9 +85,10 @@ class login_change_password_form extends moodleform {
     function validation($data, $files) {
         global $USER;
         $errors = parent::validation($data, $files);
+        $reason = null;
 
         // ignore submitted username
-        if (!$user = authenticate_user_login($USER->username, $data['password'], true)) {
+        if (!$user = authenticate_user_login($USER->username, $data['password'], true, $reason, false)) {
             $errors['password'] = get_string('invalidlogin');
             return $errors;
         }
index 923cb43..d62d7e1 100644 (file)
@@ -31,6 +31,8 @@ redirect_if_major_upgrade_required();
 
 $testsession = optional_param('testsession', 0, PARAM_INT); // test session works properly
 $anchor      = optional_param('anchor', '', PARAM_RAW);      // Used to restore hash anchor to wantsurl.
+$logintoken  = optional_param('logintoken', '', PARAM_RAW);       // Used to validate the request.
+
 $resendconfirmemail = optional_param('resendconfirmemail', false, PARAM_BOOL);
 
 $context = context_system::instance();
@@ -138,7 +140,7 @@ if ($frm and isset($frm->username)) {                             // Login WITH
         $frm = false;
     } else {
         if (empty($errormsg)) {
-            $user = authenticate_user_login($frm->username, $frm->password, false, $errorcode);
+            $user = authenticate_user_login($frm->username, $frm->password, false, $errorcode, $logintoken);
         }
     }
 
index a67b4b1..bf6fec5 100644 (file)
@@ -47,7 +47,8 @@ if (is_restored_user($username)) {
 
 $systemcontext = context_system::instance();
 
-$user = authenticate_user_login($username, $password);
+$reason = null;
+$user = authenticate_user_login($username, $password, false, $reason, false);
 if (!empty($user)) {
 
     // Cannot authenticate unless maintenance access is granted.
index b732e03..62995e3 100644 (file)
@@ -151,6 +151,11 @@ 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.
+     * 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
+     *
      * @param int $userid The user id doing the searching
      * @param int $courseid The id of the course we are searching in
      * @param string $search The string the user is searching
@@ -192,6 +197,11 @@ 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.
+     * 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
+     *
      * @param int $userid The user id doing the searching
      * @param string $search The string the user is searching
      * @param int $limitnum
@@ -208,22 +218,23 @@ class api {
         $excludeusers = array($userid, $CFG->siteguest);
         list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
 
+        $params = array('search' => '%' . $search . '%', 'userid1' => $userid, 'userid2' => $userid, 'userid3' => $userid);
+
         // Ok, let's search for contacts first.
         $contacts = array();
         $sql = "SELECT $ufields, mub.id as isuserblocked
-                  FROM {user} u
-                  JOIN {message_contacts} mc
-                    ON u.id = mc.contactid
-             LEFT JOIN {message_users_blocked} mub
-                    ON (mub.userid = :userid2 AND mub.blockeduserid = u.id)
-                 WHERE mc.userid = :userid
-                   AND u.deleted = 0
-                   AND u.confirmed = 1
-                   AND " . $DB->sql_like($fullname, ':search', false) . "
-                   AND u.id $exclude
-              ORDER BY " . $DB->sql_fullname();
-        if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'userid2' => $userid,
-                'search' => '%' . $search . '%') + $excludeparams, 0, $limitnum)) {
+                FROM {user} u
+                JOIN {message_contacts} mc
+                  ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)
+           LEFT JOIN {message_users_blocked} mub
+                  ON (mub.userid = :userid3 AND mub.blockeduserid = u.id)
+               WHERE u.deleted = 0
+                 AND u.confirmed = 1
+                 AND " . $DB->sql_like($fullname, ':search', false) . "
+                 AND u.id $exclude
+            ORDER BY " . $DB->sql_fullname();
+
+        if ($users = $DB->get_records_sql($sql, $params + $excludeparams, 0, $limitnum)) {
             foreach ($users as $user) {
                 $user->blocked = $user->isuserblocked ? 1 : 0;
                 $contacts[] = helper::create_contact($user);
@@ -251,39 +262,162 @@ class api {
             }
         }
 
-        // Let's get those non-contacts. Toast them gears boi.
-        // Note - you can only block contacts, so these users will not be blocked, so no need to get that
-        // extra detail from the database.
+        // Let's get those non-contacts.
         $noncontacts = array();
-        $sql = "SELECT $ufields
+        if ($CFG->messagingallusers) {
+            // In case $CFG->messagingallusers is enabled, search for all users site-wide but are not user's contact.
+            $sql = "SELECT $ufields
+                      FROM {user} u
+                 LEFT JOIN {message_users_blocked} mub
+                        ON (mub.userid = :userid1 AND mub.blockeduserid = u.id)
+                     WHERE u.deleted = 0
+                       AND u.confirmed = 1
+                       AND " . $DB->sql_like($fullname, ':search', false) . "
+                       AND u.id $exclude
+                       AND NOT EXISTS (SELECT mc.id
+                                         FROM {message_contacts} mc
+                                        WHERE (mc.userid = u.id AND mc.contactid = :userid2)
+                                           OR (mc.userid = :userid3 AND mc.contactid = u.id))
+                  ORDER BY " . $DB->sql_fullname();
+        } else {
+            // In case $CFG->messagingallusers is disabled, search for users you have a conversation with.
+            // Messaging setting could change, so could exist an old conversation with users you cannot message anymore.
+            $sql = "SELECT $ufields, mub.id as isuserblocked
+                      FROM {user} u
+                 LEFT JOIN {message_users_blocked} mub
+                        ON (mub.userid = :userid1 AND mub.blockeduserid = u.id)
+                INNER JOIN {message_conversation_members} cm
+                        ON u.id = cm.userid
+                INNER JOIN {message_conversation_members} cm2
+                        ON cm.conversationid = cm2.conversationid AND cm2.userid = :userid
+                     WHERE u.deleted = 0
+                       AND u.confirmed = 1
+                       AND " . $DB->sql_like($fullname, ':search', false) . "
+                       AND u.id $exclude
+                       AND NOT EXISTS (SELECT mc.id
+                                         FROM {message_contacts} mc
+                                        WHERE (mc.userid = u.id AND mc.contactid = :userid2)
+                                           OR (mc.userid = :userid3 AND mc.contactid = u.id))
+                  ORDER BY " . $DB->sql_fullname();
+            $params['userid'] = $userid;
+        }
+        if ($users = $DB->get_records_sql($sql,  $params + $excludeparams, 0, $limitnum)) {
+            foreach ($users as $user) {
+                $noncontacts[] = helper::create_contact($user);
+            }
+        }
+
+        return array($contacts, $courses, $noncontacts);
+    }
+
+    /**
+     * Handles searching for user.
+     *
+     * @param int $userid The user id doing the searching
+     * @param string $search The string the user is searching
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return array
+     */
+    public static function message_search_users(int $userid, string $search, int $limitfrom = 0, int $limitnum = 1000) : array {
+        global $CFG, $DB;
+
+        // Used to search for contacts.
+        $fullname = $DB->sql_fullname();
+
+        // Users not to include.
+        $excludeusers = array($userid, $CFG->siteguest);
+        list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
+
+        $params = array('search' => '%' . $search . '%', 'userid1' => $userid, 'userid2' => $userid);
+
+        // Ok, let's search for contacts first.
+        $sql = "SELECT u.id
                   FROM {user} u
+                  JOIN {message_contacts} mc
+                    ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)
                  WHERE u.deleted = 0
                    AND u.confirmed = 1
                    AND " . $DB->sql_like($fullname, ':search', false) . "
                    AND u.id $exclude
-                   AND u.id NOT IN (SELECT contactid
-                                      FROM {message_contacts}
-                                     WHERE userid = :userid)
               ORDER BY " . $DB->sql_fullname();
-        if ($users = $DB->get_records_sql($sql,  array('userid' => $userid, 'search' => '%' . $search . '%') + $excludeparams,
-                0, $limitnum)) {
-            foreach ($users as $user) {
-                $noncontacts[] = helper::create_contact($user);
+        $foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum);
+
+        $orderedcontacs = array();
+        if (!empty($foundusers)) {
+            $contacts = helper::get_member_info($userid, array_keys($foundusers));
+            // The get_member_info returns an associative array, so is not ordered in the same way.
+            // We need to reorder it again based on query's result.
+            foreach ($foundusers as $key => $value) {
+                $contact = $contacts[$key];
+                $contact->conversations = self::get_conversations_between_users($userid, $key, 0, 1000);
+                $orderedcontacs[] = $contact;
             }
         }
 
-        return array($contacts, $courses, $noncontacts);
+        // Let's get those non-contacts.
+        if ($CFG->messagingallusers) {
+            // In case $CFG->messagingallusers is enabled, search for all users site-wide but are not user's contact.
+            $sql = "SELECT u.id
+                      FROM {user} u
+                     WHERE u.deleted = 0
+                       AND u.confirmed = 1
+                       AND " . $DB->sql_like($fullname, ':search', false) . "
+                       AND u.id $exclude
+                       AND NOT EXISTS (SELECT mc.id
+                                         FROM {message_contacts} mc
+                                        WHERE (mc.userid = u.id AND mc.contactid = :userid1)
+                                           OR (mc.userid = :userid2 AND mc.contactid = u.id))
+                  ORDER BY " . $DB->sql_fullname();
+        } else {
+            // In case $CFG->messagingallusers is disabled, search for users you have a conversation with.
+            // Messaging setting could change, so could exist an old conversation with users you cannot message anymore.
+            $sql = "SELECT u.id
+                      FROM {user} u
+                INNER JOIN {message_conversation_members} cm
+                        ON u.id = cm.userid
+                INNER JOIN {message_conversation_members} cm2
+                        ON cm.conversationid = cm2.conversationid AND cm2.userid = :userid
+                     WHERE u.deleted = 0
+                       AND u.confirmed = 1
+                       AND " . $DB->sql_like($fullname, ':search', false) . "
+                       AND u.id $exclude
+                       AND NOT EXISTS (SELECT mc.id
+                                         FROM {message_contacts} mc
+                                        WHERE (mc.userid = u.id AND mc.contactid = :userid1)
+                                           OR (mc.userid = :userid2 AND mc.contactid = u.id))
+                  ORDER BY " . $DB->sql_fullname();
+            $params['userid'] = $userid;
+        }
+        $foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum);
+
+        $orderednoncontacs = array();
+        if (!empty($foundusers)) {
+            $noncontacts = helper::get_member_info($userid, array_keys($foundusers));
+            // The get_member_info returns an associative array, so is not ordered in the same way.
+            // We need to reorder it again based on query's result.
+            foreach ($foundusers as $key => $value) {
+                $contact = $noncontacts[$key];
+                $contact->conversations = self::get_conversations_between_users($userid, $key, 0, 1000);
+                $orderednoncontacs[] = $contact;
+            }
+        }
+
+        return array($orderedcontacs, $orderednoncontacs);
     }
 
     /**
-     * Gets the subnames for any conversations linked to components.
+     * Gets extra fields, like image url and subname for any conversations linked to components.
      *
      * The subname is like a subtitle for the conversation, to compliment it's name.
+     * The imageurl is the location of the image for the conversation, as might be seen on a listing of conversations for a user.
      *
      * @param array $conversations a list of conversations records.
      * @return array the array of subnames, index by conversation id.
+     * @throws \coding_exception
+     * @throws \dml_exception
      */
-    protected static function get_linked_conversation_subnames(array $conversations) {
+    protected static function get_linked_conversation_extra_fields(array $conversations) : array {
         global $DB;
 
         $linkedconversations = [];
@@ -299,11 +433,11 @@ class api {
 
         // TODO: MDL-63814: Working out the subname for linked conversations should be done in a generic way.
         // Get the itemid, but only for course group linked conversation for now.
-        $convsubnames = [];
+        $extrafields = [];
         if (!empty($linkeditems = $linkedconversations['core_group']['groups'])) { // Format: [conversationid => itemid].
             // Get the name of the course to which the group belongs.
             list ($groupidsql, $groupidparams) = $DB->get_in_or_equal(array_values($linkeditems), SQL_PARAMS_NAMED, 'groupid');
-            $sql = "SELECT g.id, c.shortname
+            $sql = "SELECT g.*, c.shortname as courseshortname
                       FROM {groups} g
                       JOIN {course} c
                         ON g.courseid = c.id
@@ -311,11 +445,16 @@ class api {
             $courseinfo = $DB->get_records_sql($sql, $groupidparams);
             foreach ($linkeditems as $convid => $groupid) {
                 if (array_key_exists($groupid, $courseinfo)) {
-                    $convsubnames[$convid] = format_string($courseinfo[$groupid]->shortname);
+                    $group = $courseinfo[$groupid];
+                    // Subname.
+                    $extrafields[$convid]['subname'] = format_string($courseinfo[$groupid]->courseshortname);
+
+                    // Imageurl.
+                    $extrafields[$convid]['imageurl'] = get_group_picture_url($group, $group->courseid, true)->out(false);
                 }
             }
         }
-        return $convsubnames;
+        return $extrafields;
     }
 
 
@@ -386,7 +525,8 @@ class api {
         $typesql = !is_null($type) ? " AND mc.type = :convtype " : "";
 
         $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
-                       m.smallmessage, m.timecreated, mc.component, mc.itemtype, mc.itemid
+                       m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagehtml, m.timecreated, mc.component,
+                       mc.itemtype, mc.itemid
                   FROM {message_conversations} mc
             INNER JOIN {message_conversation_members} mcm
                     ON (mcm.conversationid = mc.id AND mcm.userid = :userid3)
@@ -431,10 +571,12 @@ class api {
             return [];
         }
 
-        // COMPONENT-LINKED CONVERSATION SUBNAME.
-        // This subname will vary, depending on the component which created the linked conversation.
+        // COMPONENT-LINKED CONVERSATION FIELDS.
+        // Conversations linked to components may have extra information, such as:
+        // - subname: Essentially a subtitle for the conversation. So you'd have "name: subname".
+        // - imageurl: A URL to the image for the linked conversation.
         // For now, this is ONLY course groups.
-        $convsubnames = self::get_linked_conversation_subnames($conversations);
+        $convextrafields = self::get_linked_conversation_extra_fields($conversations);
 
         // MEMBERS.
         // Ideally, we want to get 1 member for each conversation, but this depends on the type and whether there is a recent
@@ -555,7 +697,8 @@ class api {
             $conv = new \stdClass();
             $conv->id = $conversation->id;
             $conv->name = $conversation->conversationname;
-            $conv->subname = $convsubnames[$conv->id] ?? null;
+            $conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
+            $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
             $conv->type = $conversation->conversationtype;
             $conv->membercount = $membercounts[$conv->id]->membercount;
             $conv->isfavourite = in_array($conv->id, $favouriteconversationids);
@@ -568,7 +711,7 @@ class api {
             if ($conversation->smallmessage) {
                 $msg = new \stdClass();
                 $msg->id = $conversation->messageid;
-                $msg->text = clean_param($conversation->smallmessage, PARAM_NOTAGS);
+                $msg->text = message_format_message_text($conversation);
                 $msg->useridfrom = $conversation->useridfrom;
                 $msg->timecreated = $conversation->timecreated;
                 $conv->messages[] = $msg;
@@ -579,6 +722,41 @@ class api {
         return $arrconversations;
     }
 
+    /**
+     * Returns all conversations between two users
+     *
+     * @param int $userid1 One of the user's id
+     * @param int $userid2 The other user's id
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return array
+     * @throws \dml_exception
+     */
+    public static function get_conversations_between_users(int $userid1, int $userid2,
+                                                           int $limitfrom = 0, int $limitnum = 20) : array {
+
+        global $DB;
+
+        if ($userid1 == $userid2) {
+            return array();
+        }
+
+        // Get all conversation where both user1 and user2 are members.
+        // TODO: Add subname value. Waiting for definite table structure.
+        $sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated
+                  FROM {message_conversations} mc
+            INNER JOIN {message_conversation_members} mcm1
+                    ON mc.id = mcm1.conversationid
+            INNER JOIN {message_conversation_members} mcm2
+                    ON mc.id = mcm2.conversationid
+                 WHERE mcm1.userid = :userid1
+                   AND mcm2.userid = :userid2
+                   AND mc.enabled != 0
+              ORDER BY mc.timecreated DESC";
+
+        return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
+    }
+
     /**
      * Mark a conversation as a favourite for the given user.
      *
index 67dcf87..1927b64 100644 (file)
@@ -552,7 +552,7 @@ class helper {
             $data->profileimageurl = $conv->members[$otheruser->id]->profileimageurl;
             $data->profileimageurlsmall = $conv->members[$otheruser->id]->profileimageurlsmall;
             $data->ismessaging = isset($conv->messages[0]->text) ? true : false;
-            $data->lastmessage = $conv->messages[0]->text ?? null;
+            $data->lastmessage = $conv->messages[0]->text ? clean_param($conv->messages[0]->text, PARAM_NOTAGS) : null;
             $data->messageid = $conv->messages[0]->id ?? null;
             $data->isonline = $conv->members[$otheruser->id]->isonline ?? null;
             $data->isblocked = $conv->members[$otheruser->id]->isblocked ?? null;
index c0c55f0..af3740a 100644 (file)
@@ -949,12 +949,14 @@ class core_message_external extends external_api {
      * @return external_single_structure
      * @since Moodle 3.6
      */
+
     private static function get_conversation_structure() {
         return new external_single_structure(
             array(
                 'id' => new external_value(PARAM_INT, 'The conversation id'),
                 'name' => new external_value(PARAM_NOTAGS, 'The conversation name, if set', VALUE_DEFAULT, null),
                 'subname' => new external_value(PARAM_NOTAGS, 'A subtitle for the conversation name, if set', VALUE_DEFAULT, null),
+                'imageurl' => new external_value(PARAM_URL, 'A link to the conversation picture, if set', VALUE_DEFAULT, null),
                 'type' => new external_value(PARAM_INT, 'The type of the conversation (1=individual,2=group)'),
                 'membercount' => new external_value(PARAM_INT, 'Total number of conversation members'),
                 'isfavourite' => new external_value(PARAM_BOOL, 'If the user marked conversation this conversation as a favourite'),
@@ -975,10 +977,12 @@ class core_message_external extends external_api {
      * Return the structure of a conversation member.
      *
      * @param bool $includecontactrequests Are we including contact requests?
+     * @param bool $includeconversations Are we including conversations?
      * @return external_single_structure
      * @since Moodle 3.6
      */
-    private static function get_conversation_member_structure(bool $includecontactrequests = false) {
+    private static function get_conversation_member_structure(bool $includecontactrequests = false,
+                                                              bool $includeconversations = false) {
         $result = [
             'id' => new external_value(PARAM_INT, 'The user id'),
             'fullname' => new external_value(PARAM_NOTAGS, 'The user\'s name'),
@@ -1003,6 +1007,18 @@ class core_message_external extends external_api {
             );
         }
 
+        if ($includeconversations) {
+            $result['conversations'] = new external_multiple_structure(new external_single_structure(
+                array(
+                    'id' => new external_value(PARAM_INT, 'Conversations id'),
+                    'type' => new external_value(PARAM_INT, 'Conversation type: private or public'),
+                    'name' => new external_value(PARAM_TEXT, 'Multilang compatible conversation name'. VALUE_OPTIONAL),
+                    'timecreated' => new external_value(PARAM_INT, 'The timecreated timestamp for the conversation'),
+                ), 'information about conversation', VALUE_OPTIONAL),
+                'Conversations between users', VALUE_OPTIONAL
+            );
+        }
+
         return new external_single_structure(
             $result
         );
@@ -1051,6 +1067,8 @@ class core_message_external extends external_api {
     /**
      * Get messagearea search users in course parameters.
      *
+     * @deprecated since 3.6
+     *
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1069,6 +1087,12 @@ class core_message_external extends external_api {
     /**
      * Get messagearea search users in course results.
      *
+     * @deprecated since 3.6
+     *
+     * NOTE: We are deprecating this function but not search_users_in_course API function for backwards compatibility
+     * with messaging UI. But should be removed once new group messaging UI is in place and old messaging UI is removed.
+     * Followup: MDL-63915
+     *
      * @param int $userid The id of the user who is performing the search
      * @param int $courseid The id of the course
      * @param string $search The string being searched
@@ -1113,6 +1137,8 @@ class core_message_external extends external_api {
     /**
      * Get messagearea search users in course returns.
      *
+     * @deprecated since 3.6
+     *
      * @return external_single_structure
      * @since 3.2
      */
@@ -1126,9 +1152,20 @@ class core_message_external extends external_api {
         );
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_search_users_in_course_is_deprecated() {
+        return true;
+    }
+
     /**
      * Get messagearea search users parameters.
      *
+     * @deprecated since 3.6
+     *
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1145,6 +1182,12 @@ class core_message_external extends external_api {
     /**
      * Get messagearea search users results.
      *
+     * @deprecated since 3.6
+     *
+     * NOTE: We are deprecating this function but not search_users API function for backwards compatibility
+     * with messaging UI. But should be removed once new group messaging UI is in place and old messaging UI is removed.
+     * Followup: MDL-63915
+     *
      * @param int $userid The id of the user who is performing the search
      * @param string $search The string being searched
      * @param int $limitnum
@@ -1184,6 +1227,8 @@ class core_message_external extends external_api {
     /**
      * Get messagearea search users returns.
      *
+     * @deprecated since 3.6
+     *
      * @return external_single_structure
      * @since 3.2
      */
@@ -1209,6 +1254,94 @@ class core_message_external extends external_api {
         );
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_search_users_is_deprecated() {
+        return true;
+    }
+
+    /**
+     * Get messagearea message search users parameters.
+     *
+     * @return external_function_parameters
+     * @since 3.6
+     */
+    public static function message_search_users_parameters() {
+        return new external_function_parameters(
+            array(
+                'userid' => new external_value(PARAM_INT, 'The id of the user who is performing the search'),
+                'search' => new external_value(PARAM_RAW, 'The string being searched'),
+                'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0),
+                'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0),
+            )
+        );
+    }
+
+    /**
+     * Get search users results.
+     *
+     * @param int $userid The id of the user who is performing the search
+     * @param string $search The string being searched
+     * @param int $limitfrom
+     * @param int $limitnum
+     * @return array
+     * @throws moodle_exception
+     * @since 3.6
+     */
+    public static function message_search_users($userid, $search, $limitfrom = 0, $limitnum = 0) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        $systemcontext = context_system::instance();
+
+        $params = array(
+            'userid' => $userid,
+            'search' => $search,
+            'limitfrom' => $limitfrom,
+            'limitnum' => $limitnum
+        );
+        $params = self::validate_parameters(self::message_search_users_parameters(), $params);
+        self::validate_context($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, $noncontacts) = \core_message\api::message_search_users(
+            $params['userid'],
+            $params['search'],
+            $params['limitfrom'],
+            $params['limitnum']);
+
+        return array('contacts' => $contacts, 'noncontacts' => $noncontacts);
+    }
+
+    /**
+     * Get messagearea message search users returns.
+     *
+     * @return external_single_structure
+     * @since 3.2
+     */
+    public static function message_search_users_returns() {
+        return new external_single_structure(
+            array(
+                'contacts' => new external_multiple_structure(
+                    self::get_conversation_member_structure(false, true)
+                ),
+                'noncontacts' => new external_multiple_structure(
+                    self::get_conversation_member_structure(false, true)
+                )
+            )
+        );
+    }
+
     /**
      * Get messagearea search messages parameters.
      *
index 2222a85..9dcc43c 100644 (file)
Binary files a/message/output/popup/amd/build/notification_popover_controller.min.js and b/message/output/popup/amd/build/notification_popover_controller.min.js differ
index 944f8a9..8a6b04f 100644 (file)
@@ -227,7 +227,7 @@ define(['jquery', 'core/ajax', 'core/templates', 'core/str', 'core/url',
                 notificationid: notification.id
             };
             if (notification.contexturl) {
-                notificationurlparams.redirecturl = notification.contexturl;
+                notificationurlparams.redirecturl = encodeURIComponent(notification.contexturl);
             }
             notification.contexturl = URL.relativeUrl('message/output/popup/mark_notification_read.php', notificationurlparams);
 
index dc8d49c..d9b050f 100644 (file)
@@ -1,4 +1,4 @@
-@message @message_popup
+@core_message @message_popup
 Feature: Message popover preferences
   In order to modify my message preferences
   As a user
index b27e22e..9706aa9 100644 (file)
@@ -1,4 +1,4 @@
-@message @message_popup @javascript
+@core_message @message_popup @javascript
 Feature: Message popover unread messages
   In order to be kept informed
   As a user
@@ -16,6 +16,9 @@ Feature: Message popover unread messages
       | user     | course | role           |
       | student1 | C1     | student        |
       | student2 | C1     | student        |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
     And I log in as "student2"
     And I send "Test message" message to "Student 1" user
     And I log out
index d4743da..1ec46af 100644 (file)
@@ -1,4 +1,4 @@
-@message @message_popup
+@core_message @message_popup
 Feature: Notification popover preferences
   In order to modify my notification preferences
   As a user
index e055d04..bf1d8fd 100644 (file)
@@ -1,4 +1,4 @@
-@message @message_popup @javascript
+@core_message @message_popup @javascript
 Feature: Notification popover unread notifications
   In order to be kept informed
   As a user
index 89567e2..76f2777 100644 (file)
@@ -258,7 +258,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $course5context = context_course::instance($course5->id);
         assign_capability('moodle/course:viewparticipants', CAP_PROHIBIT, $role->id, $course5context->id);
 
-        // Perform a search.
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
         list($contacts, $courses, $noncontacts) = \core_message\api::search_users($user1->id, 'search');
 
         // Check that we retrieved the correct contacts.
@@ -276,6 +277,277 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($user5->id, $noncontacts[0]->userid);
     }
 
+    /**
+     * Tests searching users with empty result.
+     */
+    public function test_search_users_with_empty_result() {
+
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User';
+        $user1->lastname = 'One';
+        $user1 = self::getDataGenerator()->create_user($user1);
+
+        // Set as the user performing the search.
+        $this->setUser($user1);
+
+        $user2 = new stdClass();
+        $user2->firstname = 'User';
+        $user2->lastname = 'Two';
+        $user2 = self::getDataGenerator()->create_user($user2);
+
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
+        list($contacts, $courses, $noncontacts) = \core_message\api::search_users($user1->id, 'search');
+
+        // Check results are empty.
+        $this->assertEquals(0, count($contacts));
+        $this->assertEquals(0, count($courses));
+        $this->assertEquals(0, count($noncontacts));
+    }
+
+    /**
+     * Tests searching users.
+     */
+    public function test_message_search_users() {
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User search';
+        $user1->lastname = 'One';
+        $user1 = self::getDataGenerator()->create_user($user1);
+
+        // Set as the user performing the search.
+        $this->setUser($user1);
+
+        $user2 = new stdClass();
+        $user2->firstname = 'User search';
+        $user2->lastname = 'Two';
+        $user2 = self::getDataGenerator()->create_user($user2);
+
+        $user3 = new stdClass();
+        $user3->firstname = 'User search';
+        $user3->lastname = 'Three';
+        $user3 = self::getDataGenerator()->create_user($user3);
+
+        $user4 = new stdClass();
+        $user4->firstname = 'User';
+        $user4->lastname = 'Four';
+        $user4 = self::getDataGenerator()->create_user($user4);
+
+        $user5 = new stdClass();
+        $user5->firstname = 'User search';
+        $user5->lastname = 'Five';
+        $user5 = self::getDataGenerator()->create_user($user5);
+
+        $user6 = new stdClass();
+        $user6->firstname = 'User search';
+        $user6->lastname = 'Six';
+        $user6 = self::getDataGenerator()->create_user($user6);
+
+        $user7 = new stdClass();
+        $user7->firstname = 'User search';
+        $user7->lastname = 'Seven';
+        $user7 = self::getDataGenerator()->create_user($user7);
+
+        // Add some users as contacts.
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user3->id, $user1->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+
+        // Create private conversations with some users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user1->id, $user6->id));
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user7->id, $user1->id));
+
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
+        list($contacts, $noncontacts) = \core_message\api::message_search_users($user1->id, 'search');
+
+        // Check that we retrieved the correct contacts.
+        $this->assertCount(2, $contacts);
+        $this->assertEquals($user3->id, $contacts[0]->id);
+        $this->assertEquals($user2->id, $contacts[1]->id);
+
+        // Check that we retrieved the correct non-contacts.
+        $this->assertCount(3, $noncontacts);
+        $this->assertEquals($user5->id, $noncontacts[0]->id);
+        $this->assertEquals($user7->id, $noncontacts[1]->id);
+        $this->assertEquals($user6->id, $noncontacts[2]->id);
+
+        // Perform a search $CFG->messagingallusers setting disabled.
+        set_config('messagingallusers', 0);
+        list($contacts, $noncontacts) = \core_message\api::message_search_users($user1->id, 'search');
+
+        // Check that we retrieved the correct contacts.
+        $this->assertCount(2, $contacts);
+        $this->assertEquals($user3->id, $contacts[0]->id);
+        $this->assertEquals($user2->id, $contacts[1]->id);
+
+        // Check that we retrieved the correct non-contacts.
+        $this->assertCount(2, $noncontacts);
+        $this->assertEquals($user7->id, $noncontacts[0]->id);
+        $this->assertEquals($user6->id, $noncontacts[1]->id);
+    }
+
+    /**
+     * Tests getting conversations between 2 users.
+     */
+    public function test_get_conversations_between_users() {
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User';
+        $user1->lastname = 'One';
+        $user1 = self::getDataGenerator()->create_user($user1);
+
+        $user2 = new stdClass();
+        $user2->firstname = 'User';
+        $user2->lastname = 'Two';
+        $user2 = self::getDataGenerator()->create_user($user2);
+
+        $user3 = new stdClass();
+        $user3->firstname = 'User search';
+        $user3->lastname = 'Three';
+        $user3 = self::getDataGenerator()->create_user($user3);
+
+        $user4 = new stdClass();
+        $user4->firstname = 'User';
+        $user4->lastname = 'Four';
+        $user4 = self::getDataGenerator()->create_user($user4);
+
+        $user5 = new stdClass();
+        $user5->firstname = 'User';
+        $user5->lastname = 'Five';
+        $user5 = self::getDataGenerator()->create_user($user5);
+
+        $user6 = new stdClass();
+        $user6->firstname = 'User search';
+        $user6->lastname = 'Six';
+        $user6 = self::getDataGenerator()->create_user($user6);
+
+        // Add some users as contacts.
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user6->id, $user1->id);
+
+        // Create private conversations with some users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user1->id, $user2->id));
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user3->id, $user1->id));
+
+        // Create a group conversation with users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            array($user1->id, $user2->id, $user3->id, $user4->id),
+            'Project chat');
+
+        // Check that we retrieved the correct conversations.
+        $this->assertCount(2, \core_message\api::get_conversations_between_users($user1->id, $user2->id));
+        $this->assertCount(2, \core_message\api::get_conversations_between_users($user2->id, $user1->id));
+        $this->assertCount(2, \core_message\api::get_conversations_between_users($user1->id, $user3->id));
+        $this->assertCount(2, \core_message\api::get_conversations_between_users($user3->id, $user1->id));
+        $this->assertCount(1, \core_message\api::get_conversations_between_users($user1->id, $user4->id));
+        $this->assertCount(1, \core_message\api::get_conversations_between_users($user4->id, $user1->id));
+        $this->assertCount(0, \core_message\api::get_conversations_between_users($user1->id, $user5->id));
+        $this->assertCount(0, \core_message\api::get_conversations_between_users($user5->id, $user1->id));
+        $this->assertCount(0, \core_message\api::get_conversations_between_users($user1->id, $user6->id));
+        $this->assertCount(0, \core_message\api::get_conversations_between_users($user6->id, $user1->id));
+    }
+
+    /**
+     * Tests searching users with and without conversations.
+     */
+    public function test_message_search_users_with_and_without_conversations() {
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User search';
+        $user1->lastname = 'One';
+        $user1 = self::getDataGenerator()->create_user($user1);
+
+        // Set as the user performing the search.
+        $this->setUser($user1);
+
+        $user2 = new stdClass();
+        $user2->firstname = 'User search';
+        $user2->lastname = 'Two';
+        $user2 = self::getDataGenerator()->create_user($user2);
+
+        $user3 = new stdClass();
+        $user3->firstname = 'User search';
+        $user3->lastname = 'Three';
+        $user3 = self::getDataGenerator()->create_user($user3);
+
+        $user4 = new stdClass();
+        $user4->firstname = 'User';
+        $user4->lastname = 'Four';
+        $user4 = self::getDataGenerator()->create_user($user4);
+
+        $user5 = new stdClass();
+        $user5->firstname = 'User search';
+        $user5->lastname = 'Five';
+        $user5 = self::getDataGenerator()->create_user($user5);
+
+        // Add a user as contact.
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        // Create private conversations with some users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user1->id, $user2->id));
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user3->id, $user1->id));
+
+        // Create a group conversation with users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            array($user1->id, $user2->id, $user4->id),
+            'Project chat');
+
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
+        list($contacts, $noncontacts) = \core_message\api::message_search_users($user1->id, 'search');
+
+        // Check that we retrieved the correct contacts.
+        $this->assertCount(1, $contacts);
+
+        // Check that we retrieved the correct conversations for contacts.
+        $this->assertCount(2, $contacts[0]->conversations);
+
+        // Check that we retrieved the correct non-contacts.
+        $this->assertCount(2, $noncontacts);
+        $this->assertEquals($user5->id, $noncontacts[0]->id);
+        $this->assertEquals($user3->id, $noncontacts[1]->id);
+
+        // Check that we retrieved the correct conversations for non-contacts.
+        $this->assertCount(0, $noncontacts[0]->conversations);
+        $this->assertCount(1, $noncontacts[1]->conversations);
+    }
+
+    /**
+     * Tests searching users with empty result.
+     */
+    public function test_message_search_users_with_empty_result() {
+
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User';
+        $user1->lastname = 'One';
+        $user1 = self::getDataGenerator()->create_user($user1);
+
+        // Set as the user performing the search.
+        $this->setUser($user1);
+
+        $user2 = new stdClass();
+        $user2->firstname = 'User';
+        $user2->lastname = 'Two';
+        $user2 = self::getDataGenerator()->create_user($user2);
+
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
+        list($contacts, $noncontacts) = \core_message\api::message_search_users($user1->id, 'search');
+
+        // Check results are empty.
+        $this->assertEquals(0, count($contacts));
+        $this->assertEquals(0, count($noncontacts));
+    }
+
     /**
      * Tests searching messages.
      */
@@ -715,6 +987,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
      * Test verifying get_conversations when no limits, offsets, type filters or favourite restrictions are used.
      */
     public function test_get_conversations_no_restrictions() {
+        global $DB;
         // No conversations should exist yet.
         $user1 = self::getDataGenerator()->create_user();
         $this->assertEquals([], \core_message\api::get_conversations($user1->id));
@@ -741,7 +1014,9 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(1, $conversations[0]->members);
         $this->assertEquals(4, $conversations[0]->membercount);
         $this->assertCount(1, $conversations[0]->messages);
-        $this->assertEquals("Message 14", $conversations[0]->messages[0]->text);
+        $message = $DB->get_record('messages', ['id' => $conversations[0]->messages[0]->id]);
+        $expectedmessagetext = message_format_message_text($message);
+        $this->assertEquals($expectedmessagetext, $conversations[0]->messages[0]->text);
         $this->assertEquals($user1->id, $conversations[0]->messages[0]->useridfrom);
 
         $this->assertEquals($gc2->id, $conversations[1]->id);
@@ -750,7 +1025,9 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(1, $conversations[1]->members);
         $this->assertEquals(3, $conversations[1]->membercount);
         $this->assertCount(1, $conversations[1]->messages);
-        $this->assertEquals("Message 11", $conversations[1]->messages[0]->text);
+        $message = $DB->get_record('messages', ['id' => $conversations[1]->messages[0]->id]);
+        $expectedmessagetext = message_format_message_text($message);
+        $this->assertEquals($expectedmessagetext, $conversations[1]->messages[0]->text);
         $this->assertEquals($user4->id, $conversations[1]->messages[0]->useridfrom);
 
         $this->assertEquals($ic2->id, $conversations[2]->id);
@@ -760,7 +1037,9 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($user3->id, $conversations[2]->members[$user3->id]->id);
         $this->assertEquals(2, $conversations[2]->membercount);
         $this->assertCount(1, $conversations[2]->messages);
-        $this->assertEquals("Message 4", $conversations[2]->messages[0]->text);
+        $message = $DB->get_record('messages', ['id' => $conversations[2]->messages[0]->id]);
+        $expectedmessagetext = message_format_message_text($message);
+        $this->assertEquals($expectedmessagetext, $conversations[2]->messages[0]->text);
         $this->assertEquals($user1->id, $conversations[2]->messages[0]->useridfrom);
 
         $this->assertEquals($ic1->id, $conversations[3]->id);
@@ -769,7 +1048,9 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(1, $conversations[3]->members);
         $this->assertEquals(2, $conversations[3]->membercount);
         $this->assertCount(1, $conversations[3]->messages);
-        $this->assertEquals("Message 2", $conversations[3]->messages[0]->text);
+        $message = $DB->get_record('messages', ['id' => $conversations[3]->messages[0]->id]);
+        $expectedmessagetext = message_format_message_text($message);
+        $this->assertEquals($expectedmessagetext, $conversations[3]->messages[0]->text);
         $this->assertEquals($user2->id, $conversations[3]->messages[0]->useridfrom);
 
         // Of the groups without messages, we expect to see the most recently created first.
@@ -792,6 +1073,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
             $this->assertObjectHasAttribute('id', $conv);
             $this->assertObjectHasAttribute('name', $conv);
             $this->assertObjectHasAttribute('subname', $conv);
+            $this->assertObjectHasAttribute('imageurl', $conv);
             $this->assertObjectHasAttribute('type', $conv);
             $this->assertObjectHasAttribute('isfavourite', $conv);
             $this->assertObjectHasAttribute('membercount', $conv);
@@ -818,6 +1100,34 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         }
     }
 
+    /**
+     * Test verifying that html format messages are supported, and that message_format_message_text() is being called appropriately.
+     */
+    public function test_get_conversations_message_format() {
+        global $DB;
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // Create conversation.
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]
+        );
+
+        // Send some messages back and forth.
+        $time = 1;
+        testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Sup mang?', $time + 1);
+        $mid = testhelper::send_fake_message_to_conversation($user1, $conversation->id, '<a href="#">A link</a>', $time + 2);
+
+        // Verify the format of the html message.
+        $message = $DB->get_record('messages', ['id' => $mid]);
+        $expectedmessagetext = message_format_message_text($message);
+        $conversations = \core_message\api::get_conversations($user1->id);
+        $messages = $conversations[0]->messages;
+        $this->assertEquals($expectedmessagetext, $messages[0]->text);
+    }
+
     /**
      * Tests retrieving conversations with a limit and offset to ensure pagination works correctly.
      */
@@ -1016,6 +1326,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
      * Test verifying that group linked conversations are returned and contain a subname matching the course name.
      */
     public function test_get_conversations_group_linked() {
+        global $CFG;
+
         // Create some users.
         $user1 = self::getDataGenerator()->create_user();
         $user2 = self::getDataGenerator()->create_user();
@@ -1028,14 +1340,21 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
         $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
         $this->getDataGenerator()->enrol_user($user3->id, $course1->id);
-        $group1 = $this->getDataGenerator()->create_group(array('courseid' => $course1->id, 'enablemessaging' => 1));
+        $group1 = $this->getDataGenerator()->create_group([
+            'courseid' => $course1->id,
+            'enablemessaging' => 1,
+            'picturepath' => $CFG->dirroot . '/lib/tests/fixtures/gd-logo.png'
+        ]);
 
         // Add users to group1.
         $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id));
         $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id));
 
         $conversations = \core_message\api::get_conversations($user1->id);
+        $this->assertEquals(2, $conversations[0]->membercount);
         $this->assertEquals($course1->shortname, $conversations[0]->subname);
+        $groupimageurl = get_group_picture_url($group1, $group1->courseid, true);
+        $this->assertEquals($groupimageurl, $conversations[0]->imageurl);
     }
 
    /**
@@ -1102,14 +1421,14 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                         array(
                             'messageposition'   => 0,
                             'with'              => 'user3',
-                            'subject'           => 'S5',
+                            'subject'           => '<p>S5</p>',
                             'unreadcount'       => 0,
                         ),
                         // User1 has also conversed with user2. The most recent message is S2.
                         array(
                             'messageposition'   => 1,
                             'with'              => 'user2',
-                            'subject'           => 'S2',
+                            'subject'           => '<p>S2</p>',
                             'unreadcount'       => 1,
                         ),
                     ),
@@ -1118,7 +1437,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                         array(
                             'messageposition'   => 0,
                             'with'              => 'user1',
-                            'subject'           => 'S2',
+                            'subject'           => '<p>S2</p>',
                             'unreadcount'       => 2,
                         ),
                     ),
@@ -1127,7 +1446,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                         array(
                             'messageposition'   => 0,
                             'with'              => 'user1',
-                            'subject'           => 'S5',
+                            'subject'           => '<p>S5</p>',
                             'unreadcount'       => 0,
                         ),
                     ),
@@ -1174,7 +1493,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                         array(
                             'messageposition'   => 0,
                             'with'              => 'user2',
-                            'subject'           => 'S4',
+                            'subject'           => '<p>S4</p>',
                             'unreadcount'       => 0,
                         ),
                     ),
@@ -1183,7 +1502,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                         array(
                             'messageposition'   => 0,
                             'with'              => 'user1',
-                            'subject'           => 'S4',
+                            'subject'           => '<p>S4</p>',
                             'unreadcount'       => 2,
                         ),
                     ),
@@ -1233,7 +1552,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                         array(
                             'messageposition'   => 0,
                             'with'              => 'user2',
-                            'subject'           => 'S2',
+                            'subject'           => '<p>S2</p>',
                             'unreadcount'       => 0,
                         ),
                     ),
@@ -1241,7 +1560,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                         array(
                             'messageposition'   => 0,
                             'with'              => 'user1',
-                            'subject'           => 'S2',
+                            'subject'           => '<p>S2</p>',
                             'unreadcount'       => 2
                         ),
                     ),
@@ -1318,7 +1637,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                         array(
                             'messageposition'   => 0,
                             'with'              => 'user2',
-                            'subject'           => 'S8',
+                            'subject'           => '<p>S8</p>',
                             'unreadcount'       => 1,
                         ),
                     ),
@@ -1326,7 +1645,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                         array(
                             'messageposition'   => 0,
                             'with'              => 'user1',
-                            'subject'           => 'S8',
+                            'subject'           => '<p>S8</p>',
                             'unreadcount'       => 3,
                         ),
                     ),
index c6f3546..2aa7b26 100644 (file)
@@ -1,4 +1,4 @@
-@core @message @javascript
+@core @core_message @javascript
 Feature: Delete all messages
   In order to communicate with fellow users
   As a user
@@ -16,6 +16,9 @@ Feature: Delete all messages
       | user     | course | role        |
       | user1    | C1     | student     |
       | user2    | C1     | student     |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
     And I log in as "user2"
     And I send "User 2 to User 1 message 1" message to "User 1" user
     And I send "User 2 to User 1 message 2" message in the message area
index a97087d..ec21545 100644 (file)
@@ -1,4 +1,4 @@
-@core @message @javascript
+@core @core_message @javascript
 Feature: Delete messages
   In order to communicate with fellow users
   As a user
@@ -16,6 +16,9 @@ Feature: Delete messages
       | user     | course | role           |
       | user1    | C1     | student        |
       | user2    | C1     | student        |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
     And I log in as "user2"
     And I send "User 2 to User 1 message 1" message to "User 1" user
     And I send "User 2 to User 1 message 2" message in the message area
index 135e154..278b115 100644 (file)
@@ -1,4 +1,4 @@
-@core @message @javascript
+@core @core_message @javascript
 Feature: Manage contacts
   In order to communicate with fellow users
   As a user
@@ -10,6 +10,9 @@ Feature: Manage contacts
       | user1    | User      | 1        | user1@example.com    |
       | user2    | User      | 2        | user2@example.com    |
       | user3    | User      | 3        | user3@example.com    |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
     And I log in as "admin"
     And I set the following administration settings values:
       | messagingallusers | 1 |
index a237ac3..5f126cb 100644 (file)
@@ -1,4 +1,4 @@
-@core @message @javascript
+@core @core_message @javascript
 Feature: Reply message
   In order to communicate with fellow users
   As a user
@@ -9,6 +9,9 @@ Feature: Reply message
       | username | firstname | lastname | email            |
       | user1    | User      | 1        | user1@example.com    |
       | user2    | User      | 2        | user2@example.com    |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
     And the following "courses" exist:
       | fullname | shortname |
       | Course 1 | C1        |
@@ -16,6 +19,9 @@ Feature: Reply message
       | user     | course | role           |
       | user1    | C1     | student        |
       | user2    | C1     | student        |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
     And I log in as "user2"
     And I send "User 2 to User 1" message to "User 1" user
     And I log out
index 5c7d3e9..2925860 100644 (file)
@@ -1,4 +1,4 @@
-@core @message @javascript
+@core @core_message @javascript
 Feature: Search messages
   In order to communicate with fellow users
   As a user
@@ -18,6 +18,9 @@ Feature: Search messages
       | user1    | C1     | student        |
       | user2    | C1     | student        |
       | user3    | C1     | student        |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
     And I log in as "user2"
     And I send "User 2 to User 1" message to "User 1" user
     And I log out
index e8c0114..74a0d49 100644 (file)
@@ -1,4 +1,4 @@
-@core @message @javascript
+@core @core_message @javascript
 Feature: Search users
   In order to communicate with fellow users
   As a user
@@ -10,6 +10,9 @@ Feature: Search users
       | user1    | User      | 1        | user1@example.com    |
       | user2    | User      | 2        | user2@example.com    |
       | user3    | User      | 3        | user3@example.com    |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
 
   Scenario: Search for single user
     When I log in as "user1"
index c03d2a3..6bddb3f 100644 (file)
@@ -1,4 +1,4 @@
-@core @message @javascript
+@core @core_message @javascript
 Feature: Messaging preferences
   In order to be notified of messages
   As a user
@@ -8,6 +8,9 @@ Feature: Messaging preferences
     Given I log in as "admin"
     And I navigate to "Plugins > Message outputs > Manage message outputs" in site administration
     And I click on "//table[contains(@class, 'admintable')]/tbody/tr/td[contains(text(), 'Email')]/following-sibling::td[1]/a" "xpath_element"
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
 
   Scenario: Alter my message preferences
     Given I follow "Preferences" in the user menu
index f979bd9..30ace15 100644 (file)
@@ -1,4 +1,4 @@
-@core @message @javascript
+@core @core_message @javascript
 Feature: View messages
   In order to communicate with fellow users
   As a user
@@ -18,6 +18,9 @@ Feature: View messages
       | user1    | C1     | student        |
       | user2    | C1     | student        |
       | user3    | C1     | student        |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
     And I log in as "user2"
     And I send "User 2 to User 1" message to "User 1" user
     And I log out
index 1d2667d..0781adc 100644 (file)
@@ -1821,7 +1821,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
     /**
      * Tests searching users in a course.
      */
-    public function test_messagearea_search_users_in_course() {
+    public function test_data_for_messagearea_search_users_in_course() {
         $this->resetAfterTest(true);
 
         // Create some users.
@@ -1885,7 +1885,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
     /**
      * Tests searching users in course as another user.
      */
-    public function test_messagearea_search_users_in_course_as_other_user() {
+    public function test_data_for_messagearea_search_users_in_course_as_other_user() {
         $this->resetAfterTest(true);
 
         // The person doing the search for another user.
@@ -1944,7 +1944,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
     /**
      * Tests searching users in course as another user without the proper capabilities.
      */
-    public function test_messagearea_search_users_in_course_as_other_user_without_cap() {
+    public function test_data_for_messagearea_search_users_in_course_as_other_user_without_cap() {
         $this->resetAfterTest(true);
 
         // Create some users.
@@ -1960,12 +1960,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         // Ensure an exception is thrown.
         $this->expectException('moodle_exception');
         core_message_external::data_for_messagearea_search_users_in_course($user2->id, $course->id, 'User');
+        $this->assertDebuggingCalled();
     }
 
     /**
      * Tests searching users in course with messaging disabled.
      */
-    public function test_messagearea_search_users_in_course_messaging_disabled() {
+    public function test_data_for_messagearea_search_users_in_course_messaging_disabled() {
         global $CFG;
 
         $this->resetAfterTest(true);
@@ -1983,12 +1984,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         // Ensure an exception is thrown.
         $this->expectException('moodle_exception');
         core_message_external::data_for_messagearea_search_users_in_course($user->id, $course->id, 'User');
+        $this->assertDebuggingCalled();
     }
 
     /**
      * Tests searching users.
      */
-    public function test_messagearea_search_users() {
+    public function test_data_for_messagearea_search_users() {
         $this->resetAfterTest(true);
 
         // Create some users.
@@ -2055,7 +2057,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         \core_message\api::add_contact($user1->id, $user3->id);
         \core_message\api::add_contact($user1->id, $user4->id);
 
-        // Perform a search.
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
         $result = core_message_external::data_for_messagearea_search_users($user1->id, 'search');
 
         // We need to execute the return values cleaning process to simulate the web service server.
@@ -2085,7 +2088,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
     /**
      * Tests searching users as another user.
      */
-    public function test_messagearea_search_users_as_other_user() {
+    public function test_data_for_messagearea_search_users_as_other_user() {
         $this->resetAfterTest(true);
 
         // The person doing the search.
@@ -2143,7 +2146,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         \core_message\api::add_contact($user1->id, $user3->id);
         \core_message\api::add_contact($user1->id, $user4->id);
 
-        // Perform a search.
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
         $result = core_message_external::data_for_messagearea_search_users($user1->id, 'search');
 
         // We need to execute the return values cleaning process to simulate the web service server.
@@ -2171,7 +2175,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
     /**
      * Tests searching users as another user without the proper capabilities.
      */
-    public function test_messagearea_search_users_as_other_user_without_cap() {
+    public function test_data_for_messagearea_search_users_as_other_user_without_cap() {
         $this->resetAfterTest(true);
 
         // Create some users.
@@ -2184,12 +2188,13 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         // Ensure an exception is thrown.
         $this->expectException('moodle_exception');
         core_message_external::data_for_messagearea_search_users($user2->id, 'User');
+        $this->assertDebuggingCalled();
     }
 
     /**
      * Tests searching users with messaging disabled.
      */
-    public function test_messagearea_search_users_messaging_disabled() {
+    public function test_data_for_messagearea_search_users_messaging_disabled() {
         global $CFG;
 
         $this->resetAfterTest(true);
@@ -2206,6 +2211,327 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         // Ensure an exception is thrown.
         $this->expectException('moodle_exception');
         core_message_external::data_for_messagearea_search_users($user->id, 'User');
+        $this->assertDebuggingCalled();
+    }
+
+    /**
+     * Tests searching users.
+     */
+    public function test_message_search_users() {
+        $this->resetAfterTest(true);
+
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User search';
+        $user1->lastname = 'One';
+        $user1 = self::getDataGenerator()->create_user($user1);
+        // Set as the user performing the search.
+        $this->setUser($user1);
+
+        $user2 = new stdClass();
+        $user2->firstname = 'User search';
+        $user2->lastname = 'Two';
+        $user2 = self::getDataGenerator()->create_user($user2);
+
+        $user3 = new stdClass();
+        $user3->firstname = 'User search';
+        $user3->lastname = 'Three';
+        $user3 = self::getDataGenerator()->create_user($user3);
+
+        $user4 = new stdClass();
+        $user4->firstname = 'User';
+        $user4->lastname = 'Four';
+        $user4 = self::getDataGenerator()->create_user($user4);
+
+        $user5 = new stdClass();
+        $user5->firstname = 'User search';
+        $user5->lastname = 'Five';
+        $user5 = self::getDataGenerator()->create_user($user5);
+
+        $user6 = new stdClass();
+        $user6->firstname = 'User search';
+        $user6->lastname = 'Six';
+        $user6 = self::getDataGenerator()->create_user($user6);
+
+        $user7 = new stdClass();
+        $user7->firstname = 'User search';
+        $user7->lastname = 'Seven';
+        $user7 = self::getDataGenerator()->create_user($user7);
+
+        // Add some users as contacts.
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user3->id, $user1->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+
+        // Create private conversations with some users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user1->id, $user6->id));
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user7->id, $user1->id));
+
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
+        $result = core_message_external::message_search_users($user1->id, 'search');
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(core_message_external::message_search_users_returns(),
+            $result);
+
+        // Confirm that we returns contacts and non-contacts.
+        $contacts = $result['contacts'];
+        $noncontacts = $result['noncontacts'];
+
+        // Check that we retrieved the correct contacts.
+        $this->assertCount(2, $contacts);
+        $this->assertEquals($user3->id, $contacts[0]['id']);
+        $this->assertEquals($user2->id, $contacts[1]['id']);
+
+        // Check that we retrieved the correct non-contacts.
+        $this->assertCount(3, $noncontacts);
+        $this->assertEquals($user5->id, $noncontacts[0]['id']);
+        $this->assertEquals($user7->id, $noncontacts[1]['id']);
+        $this->assertEquals($user6->id, $noncontacts[2]['id']);
+
+        // Perform a search $CFG->messagingallusers setting disabled.
+        set_config('messagingallusers', 0);
+        $result = core_message_external::message_search_users($user1->id, 'search');
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(core_message_external::message_search_users_returns(),
+            $result);
+
+        // Confirm that we returns contacts and non-contacts.
+        $contacts = $result['contacts'];
+        $noncontacts = $result['noncontacts'];
+
+        // Check that we retrieved the correct contacts.
+        $this->assertCount(2, $contacts);
+        $this->assertEquals($user3->id, $contacts[0]['id']);
+        $this->assertEquals($user2->id, $contacts[1]['id']);
+
+        // Check that we retrieved the correct non-contacts.
+        $this->assertCount(2, $noncontacts);
+        $this->assertEquals($user7->id, $noncontacts[0]['id']);
+        $this->assertEquals($user6->id, $noncontacts[1]['id']);
+    }
+
+    /**
+     * Tests searching users as another user.
+     */
+    public function test_message_search_users_as_other_user() {
+        $this->resetAfterTest(true);
+
+        // The person doing the search.
+        $this->setAdminUser();
+
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User search';
+        $user1->lastname = 'One';
+        $user1 = self::getDataGenerator()->create_user($user1);
+
+        $user2 = new stdClass();
+        $user2->firstname = 'User search';
+        $user2->lastname = 'Two';
+        $user2 = self::getDataGenerator()->create_user($user2);
+
+        $user3 = new stdClass();
+        $user3->firstname = 'User search';
+        $user3->lastname = 'Three';
+        $user3 = self::getDataGenerator()->create_user($user3);
+
+        $user4 = new stdClass();
+        $user4->firstname = 'User';
+        $user4->lastname = 'Four';
+        $user4 = self::getDataGenerator()->create_user($user4);
+
+        $user5 = new stdClass();
+        $user5->firstname = 'User search';
+        $user5->lastname = 'Five';
+        $user5 = self::getDataGenerator()->create_user($user5);
+
+        $user6 = new stdClass();
+        $user6->firstname = 'User search';
+        $user6->lastname = 'Six';
+        $user6 = self::getDataGenerator()->create_user($user6);
+
+        $user7 = new stdClass();
+        $user7->firstname = 'User search';
+        $user7->lastname = 'Seven';
+        $user7 = self::getDataGenerator()->create_user($user7);
+
+        // Add some users as contacts.
+        \core_message\api::add_contact($user1->id, $user2->id);
+        \core_message\api::add_contact($user3->id, $user1->id);
+        \core_message\api::add_contact($user1->id, $user4->id);
+
+        // Create private conversations with some users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user1->id, $user6->id));
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user7->id, $user1->id));
+
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
+        $result = core_message_external::message_search_users($user1->id, 'search');
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(core_message_external::message_search_users_returns(),
+            $result);
+
+        // Confirm that we returns contacts and non-contacts.
+        $contacts = $result['contacts'];
+        $noncontacts = $result['noncontacts'];
+
+        // Check that we retrieved the correct contacts.
+        $this->assertCount(2, $contacts);
+        $this->assertEquals($user3->id, $contacts[0]['id']);
+        $this->assertEquals($user2->id, $contacts[1]['id']);
+
+        // Check that we retrieved the correct non-contacts.
+        $this->assertCount(3, $noncontacts);
+        $this->assertEquals($user5->id, $noncontacts[0]['id']);
+        $this->assertEquals($user7->id, $noncontacts[1]['id']);
+        $this->assertEquals($user6->id, $noncontacts[2]['id']);
+
+        // Perform a search $CFG->messagingallusers setting disabled.
+        set_config('messagingallusers', 0);
+        $result = core_message_external::message_search_users($user1->id, 'search');
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(core_message_external::message_search_users_returns(),
+            $result);
+
+        // Confirm that we returns contacts and non-contacts.
+        $contacts = $result['contacts'];
+        $noncontacts = $result['noncontacts'];
+
+        // Check that we retrieved the correct contacts.
+        $this->assertCount(2, $contacts);
+        $this->assertEquals($user3->id, $contacts[0]['id']);
+        $this->assertEquals($user2->id, $contacts[1]['id']);
+
+        // Check that we retrieved the correct non-contacts.
+        $this->assertCount(2, $noncontacts);
+        $this->assertEquals($user7->id, $noncontacts[0]['id']);
+        $this->assertEquals($user6->id, $noncontacts[1]['id']);
+    }
+
+    /**
+     * Tests searching users as another user without the proper capabilities.
+     */
+    public function test_message_search_users_as_other_user_without_cap() {
+        $this->resetAfterTest(true);
+
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // The person doing the search for another user.
+        $this->setUser($user1);
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::message_search_users($user2->id, 'User');
+        $this->assertDebuggingCalled();
+    }
+
+    /**
+     * Tests searching users with and without conversations.
+     */
+    public function test_message_search_users_with_and_without_conversations() {
+        $this->resetAfterTest(true);
+
+        // Create some users.
+        $user1 = new stdClass();
+        $user1->firstname = 'User search';
+        $user1->lastname = 'One';
+        $user1 = self::getDataGenerator()->create_user($user1);
+
+        // Set as the user performing the search.
+        $this->setUser($user1);
+
+        $user2 = new stdClass();
+        $user2->firstname = 'User search';
+        $user2->lastname = 'Two';
+        $user2 = self::getDataGenerator()->create_user($user2);
+
+        $user3 = new stdClass();
+        $user3->firstname = 'User search';
+        $user3->lastname = 'Three';
+        $user3 = self::getDataGenerator()->create_user($user3);
+
+        $user4 = new stdClass();
+        $user4->firstname = 'User';
+        $user4->lastname = 'Four';
+        $user4 = self::getDataGenerator()->create_user($user4);
+
+        $user5 = new stdClass();
+        $user5->firstname = 'User search';
+        $user5->lastname = 'Five';
+        $user5 = self::getDataGenerator()->create_user($user5);
+
+        // Add a user as contact.
+        \core_message\api::add_contact($user1->id, $user2->id);
+
+        // Create private conversations with some users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user1->id, $user2->id));
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            array($user3->id, $user1->id));
+
+        // Create a group conversation with users.
+        \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            array($user1->id, $user2->id, $user4->id),
+            'Project chat');
+
+        // Perform a search $CFG->messagingallusers setting enabled.
+        set_config('messagingallusers', 1);
+        $result = core_message_external::message_search_users($user1->id, 'search');
+
+        // We need to execute the return values cleaning process to simulate the web service server.
+        $result = external_api::clean_returnvalue(core_message_external::message_search_users_returns(),
+            $result);
+
+        // Confirm that we returns contacts and non-contacts.
+        $contacts = $result['contacts'];
+        $noncontacts = $result['noncontacts'];
+
+        // Check that we retrieved the correct contacts.
+        $this->assertCount(1, $contacts);
+
+        // Check that we retrieved the correct conversations for contacts.
+        $this->assertCount(2, $contacts[0]['conversations']);
+
+        // Check that we retrieved the correct non-contacts.
+        $this->assertCount(2, $noncontacts);
+        $this->assertEquals($user5->id, $noncontacts[0]['id']);
+        $this->assertEquals($user3->id, $noncontacts[1]['id']);
+
+        // Check that we retrieved the correct conversations for non-contacts.
+        $this->assertCount(0, $noncontacts[0]['conversations']);
+        $this->assertCount(1, $noncontacts[1]['conversations']);
+    }
+
+    /**
+     * Tests searching users with messaging disabled.
+     */
+    public function test_message_search_users_messaging_disabled() {
+        $this->resetAfterTest(true);
+
+        // Create some skeleton data just so we can call the WS.
+        $user = self::getDataGenerator()->create_user();
+
+        // The person doing the search.
+        $this->setUser($user);
+
+        // Disable messaging.
+        set_config('messaging', 0);
+
+        // Ensure an exception is thrown.
+        $this->expectException('moodle_exception');
+        core_message_external::message_search_users($user->id, 'User');
+        $this->assertDebuggingCalled();
     }
 
     /**
@@ -3022,14 +3348,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertContains($user1->id, $membersid);
         $this->assertContains($user2->id, $membersid);
         $this->assertContains($user3->id, $membersid);
-        $this->assertNotContains($user4->id, $membersid);
-        $this->assertNotContains($user5->id, $membersid);
+
         $membersfullnames = [$members[0]['fullname'], $members[1]['fullname'], $members[2]['fullname']];
         $this->assertContains(fullname($user1), $membersfullnames);
         $this->assertContains(fullname($user2), $membersfullnames);
         $this->assertContains(fullname($user3), $membersfullnames);
-        $this->assertNotContains(fullname($user4), $membersfullnames);
-        $this->assertNotContains(fullname($user5), $membersfullnames);
 
         // Confirm the messages data is correct.
         $messages = $result['messages'];
@@ -3154,7 +3477,6 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertContains($user1->id, $membersid);
         $this->assertContains($user2->id, $membersid);
         $this->assertContains($user3->id, $membersid);
-        $this->assertNotContains($user4->id, $membersid);
 
         // Confirm the message data is correct.
         $messages = $result['messages'];
@@ -4443,6 +4765,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
             $this->assertArrayHasKey('id', $conv);
             $this->assertArrayHasKey('name', $conv);
             $this->assertArrayHasKey('subname', $conv);
+            $this->assertArrayHasKey('imageurl', $conv);
             $this->assertArrayHasKey('type', $conv);
             $this->assertArrayHasKey('membercount', $conv);
             $this->assertArrayHasKey('isfavourite', $conv);
@@ -4469,6 +4792,41 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Test verifying that html format messages are supported, and that message_format_message_text() is being called appropriately.
+     */
+    public function test_get_conversations_message_format() {
+        $this->resetAfterTest();
+
+        global $DB;
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+
+        // Create conversation.
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id]
+        );
+
+        // Send some messages back and forth.
+        $time = 1;
+        testhelper::send_fake_message_to_conversation($user2, $conversation->id, 'Sup mang?', $time + 1);
+        $mid = testhelper::send_fake_message_to_conversation($user1, $conversation->id, '<a href="#">A link</a>', $time + 2);
+        $message = $DB->get_record('messages', ['id' => $mid]);
+
+        // The user in scope.
+        $this->setUser($user1);
+
+        // Verify the format of the html message.
+        $expectedmessagetext = message_format_message_text($message);
+        $result = core_message_external::get_conversations($user1->id);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+        $messages = $conversations[0]['messages'];
+        $this->assertEquals($expectedmessagetext, $messages[0]['text']);
+    }
+
     /**
      * Tests retrieving conversations with a limit and offset to ensure pagination works correctly.
      */
@@ -4679,6 +5037,45 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         }
     }
 
+    /**
+     * Test verifying that group linked conversations are returned and contain a subname matching the course name.
+     */
+    public function test_get_conversations_group_linked() {
+        $this->resetAfterTest();
+        global $CFG;
+
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $course1 = $this->getDataGenerator()->create_course();
+
+        // Create a group with a linked conversation.
+        $this->setAdminUser();
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course1->id);
+        $group1 = $this->getDataGenerator()->create_group([
+            'courseid' => $course1->id,
+            'enablemessaging' => 1,
+            'picturepath' => $CFG->dirroot . '/lib/tests/fixtures/gd-logo.png'
+        ]);
+
+        // Add users to group1.
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user1->id));
+        $this->getDataGenerator()->create_group_member(array('groupid' => $group1->id, 'userid' => $user2->id));
+
+        $result = core_message_external::get_conversations($user1->id, 0, 20, null, false);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+
+        $this->assertEquals(2, $conversations[0]['membercount']);
+        $this->assertEquals($course1->shortname, $conversations[0]['subname']);
+        $groupimageurl = get_group_picture_url($group1, $group1->courseid, true);
+        $this->assertEquals($groupimageurl, $conversations[0]['imageurl']);
+    }
+
     /**
      * Test returning members in a conversation with no contact requests.
      */
index 5eaf792..eb7a159 100644 (file)
@@ -35,6 +35,7 @@ information provided here is intended especially for developers.
   - \core_message\api::get_most_recent_message()
   - \core_message\helper::get_messages()
   - \core_message\helper::create_messages()
+  - \core_message\api::search_users()
 * The method \core_message\api::can_delete_conversation() now expects a 'conversationid' to be passed
   as the second parameter.
 * The following web services have been deprecated. Please do not call these any more.
@@ -46,6 +47,9 @@ information provided here is intended especially for developers.
     core_message_external::core_message_mark_all_conversation_messages_as_read() instead.
   - core_message_external::data_for_messagearea_conversations(), please use core_message_external::get_conversations()
     instead
+  - core_message_external::data_for_messagearea_search_users_in_course().
+  - core_message_external::data_for_messagearea_search_users(),
+    please use core_message_external::message_search_users() instead.
 * The following function has been added for getting the privacy messaging preference:
   - get_user_privacy_messaging_preference()
 
index 6a1b81a..858f951 100644 (file)
@@ -1025,7 +1025,9 @@ class mod_assign_renderer extends plugin_renderer_base {
             $grade = null;
             foreach ($history->grades as $onegrade) {
                 if ($onegrade->attemptnumber == $submission->attemptnumber) {
-                    $grade = $onegrade;
+                    if ($onegrade->grade != ASSIGN_GRADE_NOT_SET) {
+                        $grade = $onegrade;
+                    }
                     break;
                 }
             }
index ccaafe5..b826e1d 100644 (file)
@@ -15,7 +15,9 @@
 }
 
 .que.ddmarker .dragitems .draghome {
+    display: inline-block;
     margin: 10px;
+    vertical-align: top;
 }
 
 .que.ddmarker .dragitems {
index 21199d4..74b2e08 100644 (file)
@@ -1257,7 +1257,13 @@ class engine extends \core_search\engine {
 
         if ($CFG->proxyhost && !is_proxybypass('http://' . $this->config->server_hostname . '/')) {
             $options['proxy_host'] = $CFG->proxyhost;
-            $options['proxy_port'] = $CFG->proxyport;
+            if (!empty($CFG->proxyport)) {
+                $options['proxy_port'] = $CFG->proxyport;
+            }
+            if (!empty($CFG->proxyuser) && !empty($CFG->proxypassword)) {
+                $options['proxy_login'] = $CFG->proxyuser;
+                $options['proxy_password'] = $CFG->proxypassword;
+            }
         }
 
         if (!class_exists('\SolrClient')) {
index dcbdfe4..b603dd2 100644 (file)
@@ -131,8 +131,10 @@ $card-gutter : $card-deck-margin * 2;
 
 .dashboard-card-deck .dashboard-card {
     margin-bottom: $card-gutter;
-    flex-basis: 100%;
+    flex-basis: auto;
+    width: 100%;
     flex-grow: 0;
+    flex-shrink: 0;
     .dashboard-card-img {
         height: 7rem;
         background-position: center;
@@ -146,22 +148,22 @@ $card-gutter : $card-deck-margin * 2;
 .dashboard-card-deck {
     @include media-breakpoint-up(sm) {
         .dashboard-card {
-            flex-basis: calc(50% - #{$card-gutter});
+            width: calc(50% - #{$card-gutter});
         }
     }
     @include media-breakpoint-up(md) {
         .dashboard-card {
-            flex-basis: calc(33.33% - #{$card-gutter});
+            width: calc(33.33% - #{$card-gutter});
         }
     }
     @include media-breakpoint-up(lg) {
         .dashboard-card {
-            flex-basis: calc(25% - #{$card-gutter});
+            width: calc(25% - #{$card-gutter});
         }
     }
     @include media-breakpoint-up(xl) {
         .dashboard-card {
-            flex-basis: calc(20% - #{$card-gutter});
+            width: calc(20% - #{$card-gutter});
         }
     }
 }
@@ -170,12 +172,12 @@ $card-gutter : $card-deck-margin * 2;
     .dashboard-card-deck {
         @include media-breakpoint-up(lg) {
             .dashboard-card {
-                flex-basis: calc(33.33% - #{$card-gutter});
+                width: calc(33.33% - #{$card-gutter});
             }
         }
         @include media-breakpoint-up(xl) {
             .dashboard-card {
-                flex-basis: calc(25% - #{$card-gutter});
+                width: calc(25% - #{$card-gutter});
             }
         }
     }
@@ -185,12 +187,12 @@ body.drawer-open-left #region-main.has-blocks {
     .dashboard-card-deck {
         @include media-breakpoint-up(lg) {
             .dashboard-card {
-                flex-basis: calc(33.33% - #{$card-gutter});
+                width: calc(33.33% - #{$card-gutter});
             }
         }
         @media (min-width: 1400px) {
             .dashboard-card {
-                flex-basis: calc(25% - #{$card-gutter});
+                width: calc(25% - #{$card-gutter});
             }
         }
     }
@@ -201,7 +203,7 @@ body.drawer-open-left #region-main.has-blocks {
         .dashboard-card-deck {
             margin: 0;
             .dashboard-card {
-                flex-basis: 100% !important;
+                width: 100% !important;
                 margin-left: 0;
                 margin-right: 0;
             }
index 4670378..68934ab 100644 (file)
@@ -11259,8 +11259,10 @@ div.editor_atto_toolbar button .icon {
 
 .dashboard-card-deck .dashboard-card {
   margin-bottom: 0.5rem;
-  flex-basis: 100%;
-  flex-grow: 0; }
+  flex-basis: auto;
+  width: 100%;
+  flex-grow: 0;
+  flex-shrink: 0; }
   .dashboard-card-deck .dashboard-card .dashboard-card-img {
     height: 7rem;
     background-position: center;
@@ -11270,41 +11272,41 @@ div.editor_atto_toolbar button .icon {
 
 @media (min-width: 576px) {
   .dashboard-card-deck .dashboard-card {
-    flex-basis: calc(50% - 0.5rem); } }
+    width: calc(50% - 0.5rem); } }
 
 @media (min-width: 768px) {
   .dashboard-card-deck .dashboard-card {
-    flex-basis: calc(33.33% - 0.5rem); } }
+    width: calc(33.33% - 0.5rem); } }
 
 @media (min-width: 992px) {
   .dashboard-card-deck .dashboard-card {
-    flex-basis: calc(25% - 0.5rem); } }
+    width: calc(25% - 0.5rem); } }
 
 @media (min-width: 1200px) {
   .dashboard-card-deck .dashboard-card {
-    flex-basis: calc(20% - 0.5rem); } }
+    width: calc(20% - 0.5rem); } }
 
 @media (min-width: 992px) {
   #region-main.has-blocks .dashboard-card-deck .dashboard-card {
-    flex-basis: calc(33.33% - 0.5rem); } }
+    width: calc(33.33% - 0.5rem); } }
 
 @media (min-width: 1200px) {
   #region-main.has-blocks .dashboard-card-deck .dashboard-card {
-    flex-basis: calc(25% - 0.5rem); } }
+    width: calc(25% - 0.5rem); } }
 
 @media (min-width: 992px) {
   body.drawer-open-left #region-main.has-blocks .dashboard-card-deck .dashboard-card {
-    flex-basis: calc(33.33% - 0.5rem); } }
+    width: calc(33.33% - 0.5rem); } }
 
 @media (min-width: 1400px) {
   body.drawer-open-left #region-main.has-blocks .dashboard-card-deck .dashboard-card {
-    flex-basis: calc(25% - 0.5rem); } }
+    width: calc(25% - 0.5rem); } }
 
 @media (min-width: 1200px) {
   #block-region-side-pre .dashboard-card-deck {
     margin: 0; }
     #block-region-side-pre .dashboard-card-deck .dashboard-card {
-      flex-basis: 100% !important;
+      width: 100% !important;
       margin-left: 0;
       margin-right: 0; } }
 
index 4f5168f..5450089 100644 (file)
@@ -39,6 +39,7 @@
     * errorformatted - Formatted error,
     * logourl - Flag, logo url,
     * sitename - Name of site.
+    * logintoken - Random token to protect login request.
 
     Example context (json):
     {
@@ -87,7 +88,8 @@
         "cookieshelpiconformatted": "",
         "errorformatted": "",
         "logourl": false,
-        "sitename": "Beer & Chips"
+        "sitename": "Beer & Chips",
+        "logintoken": "randomstring"
     }
 }}
 
                     <form class="mt-3" action="{{loginurl}}" method="post" id="login">
                         <input id="anchor" type="hidden" name="anchor" value="">
                         <script>document.getElementById('anchor').value = location.hash;</script>
+                        <input type="hidden" name="logintoken" value="{{logintoken}}">
                         <div class="form-group">
                             <label for="username" class="sr-only">
                                 {{^canloginbyemail}}
                         <div class="mt-2">
                             <p>{{#str}}someallowguest{{/str}}</p>
                             <form action="{{loginurl}}" method="post" id="guestlogin">
+                                <input type="hidden" name="logintoken" value="{{logintoken}}">
                                 <input type="hidden" name="username" value="guest" />
                                 <input type="hidden" name="password" value="guest" />
                                 <button class="btn btn-secondary btn-block" type="submit">{{#str}}loginguest{{/str}}</button>
index 7edeaa7..7492e5e 100644 (file)
                 img.actionmenu {
                     width: auto;
                 }
+                .toggle-display[role="menuitem"] img.icon {
+                    width: 22px;
+                    vertical-align: middle;
+                }
             }
         }
     }
         flex-grow: 0;
         flex-shrink: 0;
         min-width: 0;
-        flex-basis: 100%;
+        width: 100%;
+        flex-basis: auto;
     }
     @media (min-width: 576px) {
         .dashboard-card {
             flex-direction: column;
             margin-right: 0.25rem;
             margin-left: 0.25rem;
-            flex-basis: ~"calc(50% - 0.6rem)";
+            width: ~"calc(50% - 0.5rem)";
         }
     }
 
     @media (min-width: 1200px) {
         .dashboard-card {
-            flex-basis: ~"calc(33.333% - 0.5rem)";
+            width: ~"calc(33.333% - 0.5rem)";
         }
     }
 }
         margin: 0;
         height: unset;
         .dashboard-card {
-            flex-basis: 100%;
+            width: 100%;
             margin-left: 0;
             margin-right: 0;
         }
index 230c0b0..52266d1 100644 (file)
@@ -16084,6 +16084,10 @@ body {
 .editing .block .header .commands img.actionmenu {
   width: auto;
 }
+.editing .block .header .commands .toggle-display[role="menuitem"] img.icon {
+  width: 22px;
+  vertical-align: middle;
+}
 .jsenabled .block.hidden .content {
   display: none;
 }
@@ -16673,7 +16677,8 @@ body {
   flex-grow: 0;
   flex-shrink: 0;
   min-width: 0;
-  flex-basis: 100%;
+  width: 100%;
+  flex-basis: auto;
 }
 @media (min-width: 576px) {
   .dashboard-card-deck .dashboard-card {
@@ -16681,12 +16686,12 @@ body {
     flex-direction: column;
     margin-right: 0.25rem;
     margin-left: 0.25rem;
-    flex-basis: calc(50% - 0.6rem);
+    width: calc(50% - 0.5rem);
   }
 }
 @media (min-width: 1200px) {
   .dashboard-card-deck .dashboard-card {
-    flex-basis: calc(33.333% - 0.5rem);
+    width: calc(33.333% - 0.5rem);
   }
 }
 @media (min-width: 768px) {
@@ -16697,7 +16702,7 @@ body {
   }
   #block-region-side-post .dashboard-card-deck .dashboard-card,
   #block-region-side-pre .dashboard-card-deck .dashboard-card {
-    flex-basis: 100%;
+    width: 100%;
     margin-left: 0;
     margin-right: 0;
   }
index 5393486..0eb3b97 100644 (file)
@@ -20,6 +20,9 @@ Feature: Deleting users
       | user2    | C1     | student        |
       | user3    | C1     | student        |
       | user4    | C1     | student        |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
 
   @javascript
   Scenario: Deleting one user at a time
index bb75a3b..80da7d3 100644 (file)
@@ -21,6 +21,9 @@ Feature: Access to full profiles of users
       | student2 | C1 | student |
       | teacher1 | C1 | editingteacher |
       | student3 | C2 | student |
+    And the following config values are set as admin:
+      | messaging | 1 |
+      | messagingallusers | 1 |
 
   Scenario: Viewing full profiles with default settings
     When I log in as "student1"
index b0b71c3..94e3858 100644 (file)
@@ -167,7 +167,7 @@ class core_webservice_external extends external_api {
 
         // Retrieve some advanced features. Only enable/disable ones (bool).
         $advancedfeatures = array("usecomments", "usetags", "enablenotes", "messaging", "enableblogs",
-                                    "enablecompletion", "enablebadges");
+                                    "enablecompletion", "enablebadges", "messagingallusers");
         foreach ($advancedfeatures as $feature) {
             if (isset($CFG->{$feature})) {
                 $siteinfo['advancedfeatures'][] = array(