Merge branch 'MDL-63716-master' of git://github.com/damyon/moodle
authorAndrew Nicols <andrew@nicols.co.uk>
Tue, 13 Nov 2018 03:20:54 +0000 (11:20 +0800)
committerAndrew Nicols <andrew@nicols.co.uk>
Tue, 13 Nov 2018 03:20:54 +0000 (11:20 +0800)
36 files changed:
admin/tool/lpimportcsv/index.php
badges/classes/external.php
badges/classes/external/competency_exporter.php [new file with mode: 0644]
badges/classes/external/endorsement_exporter.php [new file with mode: 0644]
badges/classes/external/related_info_exporter.php [new file with mode: 0644]
badges/classes/external/user_badge_exporter.php [new file with mode: 0644]
badges/tests/external_test.php
badges/upgrade.txt
blocks/recentlyaccesseditems/templates/recentlyaccesseditems-view.mustache
lang/en/message.php
lib/amd/src/sortable_list.js
lib/classes/event/group_message_sent.php [new file with mode: 0644]
lib/classes/message/manager.php
lib/db/services.php
lib/messagelib.php
lib/recaptchalib_v2.php
lib/tests/message_test.php
lib/tests/messagelib_test.php
message/classes/api.php
message/classes/helper.php
message/externallib.php
message/pendingcontactrequests.php
message/tests/api_test.php
message/tests/events_test.php
message/tests/externallib_test.php
message/upgrade.txt
mod/scorm/lib.php
mod/scorm/locallib.php
privacy/templates/htmlpage.mustache
theme/boost/scss/moodle/blocks.scss
theme/boost/style/moodle.css
theme/bootstrapbase/less/moodle/blocks.less
theme/bootstrapbase/style/moodle.css
user/externallib.php
user/tests/externallib_test.php
user/upgrade.txt

index fe14dac..2c541d3 100644 (file)
@@ -21,7 +21,7 @@
  * @copyright  2016 Damyon Wiese
  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  */
-
+define('NO_OUTPUT_BUFFERING', true);
 require_once(__DIR__ . '/../../../config.php');
 require_once($CFG->libdir.'/adminlib.php');
 
index e5b2de0..15437d0 100644 (file)
@@ -29,6 +29,8 @@ defined('MOODLE_INTERNAL') || die;
 require_once($CFG->libdir . '/externallib.php');
 require_once($CFG->libdir . '/badgeslib.php');
 
+use core_badges\external\user_badge_exporter;
+
 /**
  * Badges external functions
  *
@@ -73,7 +75,7 @@ class core_badges_external extends external_api {
      * @throws moodle_exception
      */
     public static function get_user_badges($userid = 0, $courseid = 0, $page = 0, $perpage = 0, $search = '', $onlypublic = false) {
-        global $CFG, $USER;
+        global $CFG, $USER, $PAGE;
 
         $warnings = array();
 
@@ -122,25 +124,64 @@ class core_badges_external extends external_api {
 
         foreach ($userbadges as $badge) {
             $context = ($badge->type == BADGE_TYPE_SITE) ? context_system::instance() : context_course::instance($badge->courseid);
-            $badge->badgeurl = moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/',
-                                                                            'f1')->out(false);
-            // Return all the information if we are requesting our own badges.
-            // Or, if we have permissions for configuring badges in the badge context.
-            if ($USER->id == $user->id or has_capability('moodle/badges:configuredetails', $context)) {
-                $result['badges'][] = (array) $badge;
-            } else {
-                $result['badges'][] = array(
+            $canconfiguredetails = has_capability('moodle/badges:configuredetails', $context);
+
+            // If the user is viewing another user's badge and doesn't have the right capability return only part of the data.
+            if ($USER->id != $user->id and !$canconfiguredetails) {
+                $badge = (object) array(
+                    'id' => $badge->id,
                     'name' => $badge->name,
                     'description' => $badge->description,
-                    'badgeurl' => $badge->badgeurl,
                     'issuername' => $badge->issuername,
                     'issuerurl' => $badge->issuerurl,
                     'issuercontact' => $badge->issuercontact,
                     'uniquehash' => $badge->uniquehash,
                     'dateissued' => $badge->dateissued,
                     'dateexpire' => $badge->dateexpire,
+                    'version' => $badge->version,
+                    'language' => $badge->language,
+                    'imageauthorname' => $badge->imageauthorname,
+                    'imageauthoremail' => $badge->imageauthoremail,
+                    'imageauthorurl' => $badge->imageauthorurl,
+                    'imagecaption' => $badge->imagecaption,
                 );
             }
+
+            // Create a badge instance to be able to get the endorsement and other info.
+            $badgeinstance = new badge($badge->id);
+            $endorsement = $badgeinstance->get_endorsement();
+            $competencies = $badgeinstance->get_alignment();
+            $relatedbadges = $badgeinstance->get_related_badges();
+
+            if (!$canconfiguredetails) {
+                // Return only the properties visible by the user.
+
+                if (!empty($competencies)) {
+                    foreach ($competencies as $competency) {
+                        unset($competency->targetdescription);
+                        unset($competency->targetframework);
+                        unset($competency->targetcode);
+                    }
+                }
+
+                if (!empty($relatedbadges)) {
+                    foreach ($relatedbadges as $relatedbadge) {
+                        unset($relatedbadge->version);
+                        unset($relatedbadge->language);
+                        unset($relatedbadge->type);
+                    }
+                }
+            }
+
+            $related = array(
+                'context' => $context,
+                'endorsement' => $endorsement ? $endorsement : null,
+                'competencies' => $competencies,
+                'relatedbadges' => $relatedbadges,
+            );
+
+            $exporter = new user_badge_exporter($badge, $related);
+            $result['badges'][] = $exporter->export($PAGE->get_renderer('core'));
         }
 
         return $result;
@@ -156,34 +197,7 @@ class core_badges_external extends external_api {
         return new external_single_structure(
             array(
                 'badges' => new external_multiple_structure(
-                    new external_single_structure(
-                        array(
-                            'id' => new external_value(PARAM_INT, 'Badge id.', VALUE_OPTIONAL),
-                            'name' => new external_value(PARAM_TEXT, 'Badge name.'),
-                            'description' => new external_value(PARAM_NOTAGS, 'Badge description.'),
-                            'badgeurl' => new external_value(PARAM_URL, 'Badge URL.'),
-                            'timecreated' => new external_value(PARAM_INT, 'Time created.', VALUE_OPTIONAL),
-                            'timemodified' => new external_value(PARAM_INT, 'Time modified.', VALUE_OPTIONAL),
-                            'usercreated' => new external_value(PARAM_INT, 'User created.', VALUE_OPTIONAL),
-                            'usermodified' => new external_value(PARAM_INT, 'User modified.', VALUE_OPTIONAL),
-                            'issuername' => new external_value(PARAM_NOTAGS, 'Issuer name.'),
-                            'issuerurl' => new external_value(PARAM_URL, 'Issuer URL.'),
-                            'issuercontact' => new external_value(PARAM_RAW, 'Issuer contact.'),
-                            'expiredate' => new external_value(PARAM_INT, 'Expire date.', VALUE_OPTIONAL),
-                            'expireperiod' => new external_value(PARAM_INT, 'Expire period.', VALUE_OPTIONAL),
-                            'type' => new external_value(PARAM_INT, 'Type.', VALUE_OPTIONAL),
-                            'courseid' => new external_value(PARAM_INT, 'Course id.', VALUE_OPTIONAL),
-                            'message' => new external_value(PARAM_RAW, 'Message.', VALUE_OPTIONAL),
-                            'messagesubject' => new external_value(PARAM_TEXT, 'Message subject.', VALUE_OPTIONAL),
-                            'attachment' => new external_value(PARAM_INT, 'Attachment.', VALUE_OPTIONAL),
-                            'status' => new external_value(PARAM_INT, 'Status.', VALUE_OPTIONAL),
-                            'issuedid' => new external_value(PARAM_INT, 'Issued id.', VALUE_OPTIONAL),
-                            'uniquehash' => new external_value(PARAM_ALPHANUM, 'Unique hash.'),
-                            'dateissued' => new external_value(PARAM_INT, 'Date issued.'),
-                            'dateexpire' => new external_value(PARAM_INT, 'Date expire.'),
-                            'visible' => new external_value(PARAM_INT, 'Visible.', VALUE_OPTIONAL),
-                        )
-                    )
+                    user_badge_exporter::get_read_structure()
                 ),
                 'warnings' => new external_warnings(),
             )
diff --git a/badges/classes/external/competency_exporter.php b/badges/classes/external/competency_exporter.php
new file mode 100644 (file)
index 0000000..3eca46a
--- /dev/null
@@ -0,0 +1,94 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains competency class for displaying a badge competency.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_badges\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+
+/**
+ * Class for displaying a badge competency.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class competency_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'id' => [
+                'type' => PARAM_INT,
+                'description' => 'Competency id',
+            ],
+            'badgeid' => [
+                'type' => PARAM_INT,
+                'description' => 'Badge id',
+            ],
+            'targetname' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Target name',
+            ],
+            'targeturl' => [
+                'type' => PARAM_URL,
+                'description' => 'Target URL',
+            ],
+            'targetdescription' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Target description',
+                'null' => NULL_ALLOWED,
+                'optional' => true,
+            ],
+            'targetframework' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Target framework',
+                'null' => NULL_ALLOWED,
+                'optional' => true,
+            ],
+            'targetcode' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Target code',
+                'null' => NULL_ALLOWED,
+                'optional' => true,
+            ]
+        ];
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return array(
+            'context' => 'context',
+        );
+    }
+}
diff --git a/badges/classes/external/endorsement_exporter.php b/badges/classes/external/endorsement_exporter.php
new file mode 100644 (file)
index 0000000..80e4dde
--- /dev/null
@@ -0,0 +1,95 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains endorsement class for displaying a badge endorsement.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_badges\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+
+/**
+ * Class for displaying a badge endorsement.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class endorsement_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'id' => [
+                'type' => PARAM_INT,
+                'description' => 'Endorsement id',
+            ],
+            'badgeid' => [
+                'type' => PARAM_INT,
+                'description' => 'Badge id',
+            ],
+            'issuername' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Endorsement issuer name',
+            ],
+            'issuerurl' => [
+                'type' => PARAM_URL,
+                'description' => 'Endorsement issuer URL',
+            ],
+            'issueremail' => [
+                'type' => PARAM_RAW,
+                'description' => 'Endorsement issuer email',
+            ],
+            'claimid' => [
+                'type' => PARAM_URL,
+                'description' => 'Claim URL',
+                'null' => NULL_ALLOWED,
+            ],
+            'claimcomment' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Claim comment',
+                'null' => NULL_ALLOWED,
+            ],
+            'dateissued' => [
+                'type' => PARAM_INT,
+                'description' => 'Date issued',
+                'default' => 0,
+            ]
+        ];
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return array(
+            'context' => 'context',
+        );
+    }
+}
diff --git a/badges/classes/external/related_info_exporter.php b/badges/classes/external/related_info_exporter.php
new file mode 100644 (file)
index 0000000..83bee1e
--- /dev/null
@@ -0,0 +1,85 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains related class for displaying information of a related badge.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_badges\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+
+/**
+ * Class for displaying information of a related badge.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class related_info_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'id' => [
+                'type' => PARAM_INT,
+                'description' => 'Badge id',
+            ],
+            'name' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Badge name',
+            ],
+            'version' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Version',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'language' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Language',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'type' => [
+                'type' => PARAM_INT,
+                'description' => 'Type',
+                'optional' => true,
+            ],
+        ];
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return array(
+            'context' => 'context',
+        );
+    }
+}
diff --git a/badges/classes/external/user_badge_exporter.php b/badges/classes/external/user_badge_exporter.php
new file mode 100644 (file)
index 0000000..0cc7269
--- /dev/null
@@ -0,0 +1,311 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Contains user badge class for displaying a badge issued to a user.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core_badges\external;
+
+defined('MOODLE_INTERNAL') || die();
+
+use core\external\exporter;
+use renderer_base;
+use moodle_url;
+use core_badges\external\endorsement_exporter;
+use core_badges\external\competency_exporter;
+use core_badges\external\related_info_exporter;
+
+/**
+ * Class for displaying a badge issued to a user.
+ *
+ * @package   core_badges
+ * @copyright 2018 Dani Palou <dani@moodle.com>
+ * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class user_badge_exporter extends exporter {
+
+    /**
+     * Return the list of properties.
+     *
+     * @return array
+     */
+    protected static function define_properties() {
+        return [
+            'id' => [
+                'type' => PARAM_INT,
+                'description' => 'Badge id',
+                'optional' => true,
+            ],
+            'name' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Badge name',
+            ],
+            'description' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Badge description',
+                'null' => NULL_ALLOWED,
+            ],
+            'timecreated' => [
+                'type' => PARAM_INT,
+                'description' => 'Time created',
+                'optional' => true,
+                'default' => 0,
+            ],
+            'timemodified' => [
+                'type' => PARAM_INT,
+                'description' => 'Time modified',
+                'optional' => true,
+                'default' => 0,
+            ],
+            'usercreated' => [
+                'type' => PARAM_INT,
+                'description' => 'User created',
+                'optional' => true,
+            ],
+            'usermodified' => [
+                'type' => PARAM_INT,
+                'description' => 'User modified',
+                'optional' => true,
+            ],
+            'issuername' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Issuer name',
+            ],
+            'issuerurl' => [
+                'type' => PARAM_URL,
+                'description' => 'Issuer URL',
+            ],
+            'issuercontact' => [
+                'type' => PARAM_RAW,
+                'description' => 'Issuer contact',
+                'null' => NULL_ALLOWED,
+            ],
+            'expiredate' => [
+                'type' => PARAM_INT,
+                'description' => 'Expire date',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'expireperiod' => [
+                'type' => PARAM_INT,
+                'description' => 'Expire period',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'type' => [
+                'type' => PARAM_INT,
+                'description' => 'Type',
+                'optional' => true,
+                'default' => 1,
+            ],
+            'courseid' => [
+                'type' => PARAM_INT,
+                'description' => 'Course id',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'message' => [
+                'type' => PARAM_RAW,
+                'description' => 'Message',
+                'optional' => true,
+            ],
+            'messagesubject' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Message subject',
+                'optional' => true,
+            ],
+            'attachment' => [
+                'type' => PARAM_INT,
+                'description' => 'Attachment',
+                'optional' => true,
+                'default' => 1,
+            ],
+            'notification' => [
+                'type' => PARAM_INT,
+                'description' => 'Whether to notify when badge is awarded',
+                'optional' => true,
+                'default' => 1,
+            ],
+            'nextcron' => [
+                'type' => PARAM_INT,
+                'description' => 'Next cron',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'status' => [
+                'type' => PARAM_INT,
+                'description' => 'Status',
+                'optional' => true,
+                'default' => 0,
+            ],
+            'issuedid' => [
+                'type' => PARAM_INT,
+                'description' => 'Issued id',
+                'optional' => true,
+            ],
+            'uniquehash' => [
+                'type' => PARAM_ALPHANUM,
+                'description' => 'Unique hash',
+            ],
+            'dateissued' => [
+                'type' => PARAM_INT,
+                'description' => 'Date issued',
+                'default' => 0,
+            ],
+            'dateexpire' => [
+                'type' => PARAM_INT,
+                'description' => 'Date expire',
+                'null' => NULL_ALLOWED,
+            ],
+            'visible' => [
+                'type' => PARAM_INT,
+                'description' => 'Visible',
+                'optional' => true,
+                'default' => 0,
+            ],
+            'email' => [
+                'type' => PARAM_TEXT,
+                'description' => 'User email',
+                'optional' => true,
+            ],
+            'version' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Version',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'language' => [
+                'type' => PARAM_NOTAGS,
+                'description' => 'Language',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'imageauthorname' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Name of the image author',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'imageauthoremail' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Email of the image author',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'imageauthorurl' => [
+                'type' => PARAM_URL,
+                'description' => 'URL of the image author',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+            'imagecaption' => [
+                'type' => PARAM_TEXT,
+                'description' => 'Caption of the image',
+                'optional' => true,
+                'null' => NULL_ALLOWED,
+            ],
+        ];
+    }
+
+    /**
+     * Returns a list of objects that are related.
+     *
+     * @return array
+     */
+    protected static function define_related() {
+        return array(
+            'context' => 'context',
+            'endorsement' => 'stdClass?',
+            'competencies' => 'stdClass[]',
+            'relatedbadges' => 'stdClass[]',
+        );
+    }
+
+    /**
+     * Return the list of additional properties.
+     *
+     * @return array
+     */
+    protected static function define_other_properties() {
+        return [
+            'badgeurl' => [
+                'type' => PARAM_URL,
+                'description' => 'Badge URL',
+            ],
+            'endorsement' => [
+                'type' => endorsement_exporter::read_properties_definition(),
+                'description' => 'Badge endorsement',
+                'optional' => true,
+            ],
+            'competencies' => [
+                'type' => competency_exporter::read_properties_definition(),
+                'description' => 'Badge competencies (alignment)',
+                'multiple' => true,
+            ],
+            'relatedbadges' => [
+                'type' => related_info_exporter::read_properties_definition(),
+                'description' => 'Related badges',
+                'multiple' => true,
+            ]
+        ];
+    }
+
+    /**
+     * Get the additional values to inject while exporting.
+     *
+     * @param renderer_base $output The renderer.
+     * @return array Keys are the property names, values are their values.
+     */
+    protected function get_other_values(renderer_base $output) {
+        $context = $this->related['context'];
+        $endorsement = $this->related['endorsement'];
+        $competencies = $this->related['competencies'];
+        $relatedbadges = $this->related['relatedbadges'];
+
+        $values = array(
+            'badgeurl' => moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $this->data->id, '/',
+                'f1')->out(false),
+            'competencies' => array(),
+            'relatedbadges' => array(),
+        );
+
+        if ($endorsement) {
+            $endorsementexporter = new endorsement_exporter($endorsement, array('context' => $context));
+            $values['endorsement'] = $endorsementexporter->export($output);
+        }
+
+        if (!empty($competencies)) {
+            foreach ($competencies as $competency) {
+                $competencyexporter = new competency_exporter($competency, array('context' => $context));
+                $values['competencies'][] = $competencyexporter->export($output);
+            }
+        }
+
+        if (!empty($relatedbadges)) {
+            foreach ($relatedbadges as $badge) {
+                $relatedexporter = new related_info_exporter($badge, array('context' => $context));
+                $values['relatedbadges'][] = $relatedexporter->export($output);
+            }
+        }
+
+        return $values;
+    }
+}
index 77db1a4..6cf9ad5 100644 (file)
@@ -85,6 +85,12 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
         $badge->attachment = 1;
         $badge->notification = 0;
         $badge->status = BADGE_STATUS_ACTIVE;
+        $badge->version = '1';
+        $badge->language = 'en';
+        $badge->imageauthorname = 'Image author';
+        $badge->imageauthoremail = 'imageauthor@example.com';
+        $badge->imageauthorurl = 'http://image-author-url.domain.co.nz';
+        $badge->imagecaption = 'Caption';
 
         $badgeid = $DB->insert_record('badge', $badge, true);
         $badge = new badge($badgeid);
@@ -93,6 +99,34 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
         // Hack the database to adjust the time each badge was issued.
         $DB->set_field('badge_issued', 'dateissued', $now - 11, array('userid' => $this->student->id, 'badgeid' => $badgeid));
 
+        // Add an endorsement for the badge.
+        $endorsement = new stdClass();
+        $endorsement->badgeid = $badgeid;
+        $endorsement->issuername = 'Issuer name';
+        $endorsement->issuerurl = 'http://endorsement-issuer-url.domain.co.nz';
+        $endorsement->issueremail = 'endorsementissuer@example.com';
+        $endorsement->claimid = 'http://claim-url.domain.co.nz';
+        $endorsement->claimcomment = 'Claim comment';
+        $endorsement->dateissued = $now;
+        $badge->save_endorsement($endorsement);
+
+        // Add 2 competencies.
+        $competency = new stdClass();
+        $competency->badgeid = $badgeid;
+        $competency->targetname = 'Competency 1';
+        $competency->targeturl = 'http://c1-target-url.domain.co.nz';
+        $competency->targetdescription = 'C1 target description';
+        $competency->targetframework = 'C1 framework';
+        $competency->targetcode = 'C1 code';
+        $badge->save_alignment($competency);
+
+        $competency->targetname = 'Competency 2';
+        $competency->targeturl = 'http://c2-target-url.domain.co.nz';
+        $competency->targetdescription = 'C2 target description';
+        $competency->targetframework = 'C2 framework';
+        $competency->targetcode = 'C2 code';
+        $badge->save_alignment($competency);
+
         // Now a course badge.
         $badge->id = null;
         $badge->name = "Test badge course";
@@ -100,12 +134,15 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
         $badge->type = BADGE_TYPE_COURSE;
         $badge->courseid = $this->course->id;
 
-        $badgeid = $DB->insert_record('badge', $badge, true);
-        $badge = new badge($badgeid);
+        $coursebadgeid = $DB->insert_record('badge', $badge, true);
+        $badge = new badge($coursebadgeid);
         $badge->issue($this->student->id, true);
 
         // Hack the database to adjust the time each badge was issued.
-        $DB->set_field('badge_issued', 'dateissued', $now - 11, array('userid' => $this->student->id, 'badgeid' => $badgeid));
+        $DB->set_field('badge_issued', 'dateissued', $now - 11, array('userid' => $this->student->id, 'badgeid' => $coursebadgeid));
+
+        // Make the site badge a related badge.
+        $badge->add_related_badges(array($badgeid));
     }
 
     /**
@@ -116,15 +153,60 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
 
         $this->setUser($this->student);
 
+        $badges = (array) badges_get_user_badges($this->student->id);
+        $expectedbadges = array();
+
+        foreach ($badges as $badge) {
+            $context = ($badge->type == BADGE_TYPE_SITE) ? context_system::instance() : context_course::instance($badge->courseid);
+            $badge->badgeurl = moodle_url::make_webservice_pluginfile_url($context->id, 'badges', 'badgeimage', $badge->id, '/',
+                                                                            'f1')->out(false);
+
+            // Get the endorsement, competencies and related badges.
+            $badgeinstance = new badge($badge->id);
+            $endorsement = $badgeinstance->get_endorsement();
+            $competencies = $badgeinstance->get_alignment();
+            $relatedbadges = $badgeinstance->get_related_badges();
+            $badge->competencies = array();
+            $badge->relatedbadges = array();
+
+            if ($endorsement) {
+                $badge->endorsement = (array) $endorsement;
+            }
+
+            if (!empty($competencies)) {
+                foreach ($competencies as $competency) {
+                    // Students cannot see some fields of the competencies.
+                    unset($competency->targetdescription);
+                    unset($competency->targetframework);
+                    unset($competency->targetcode);
+
+                    $badge->competencies[] = (array) $competency;
+                }
+            }
+
+            if (!empty($relatedbadges)) {
+                foreach ($relatedbadges as $relatedbadge) {
+                    // Students cannot see some fields of the related badges.
+                    unset($relatedbadge->version);
+                    unset($relatedbadge->language);
+                    unset($relatedbadge->type);
+
+                    $badge->relatedbadges[] = (array) $relatedbadge;
+                }
+            }
+
+            $expectedbadges[] = (array) $badge;
+        }
+
         $result = core_badges_external::get_user_badges();
         $result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
-        $this->assertCount(2, $result['badges']);
+        $this->assertEquals($expectedbadges, $result['badges']);
 
         // Pagination and filtering.
         $result = core_badges_external::get_user_badges(0, $this->course->id, 0, 1, '', true);
         $result = external_api::clean_returnvalue(core_badges_external::get_user_badges_returns(), $result);
         $this->assertCount(1, $result['badges']);
-        $this->assertEquals($this->course->id, $result['badges'][0]['courseid']);
+        $this->assertEquals($expectedbadges[1], $result['badges'][0]);
     }
 
     /**
@@ -143,6 +225,15 @@ class core_badges_external_testcase extends externallib_advanced_testcase {
         foreach ($result['badges'] as $badge) {
             if (isset($badge['type']) and $badge['type'] == BADGE_TYPE_COURSE) {
                 $this->assertTrue(isset($badge['message']));
+
+                // Check that we have permissions to see all the data in competencies and related badges.
+                foreach ($badge['competencies'] as $competency) {
+                    $this->assertTrue(isset($competency['targetdescription']));
+                }
+
+                foreach ($badge['relatedbadges'] as $relatedbadge) {
+                    $this->assertTrue(isset($relatedbadge['type']));
+                }
             } else {
                 $this->assertFalse(isset($badge['message']));
             }
index 8dd1f87..8b20394 100644 (file)
@@ -1,6 +1,10 @@
 This files describes API changes in /badges/*,
 information provided here is intended especially for developers.
 
+=== 3.6 ===
+
+* The WebService core_badges_get_user_badges now returns more information for each badge: notification, nextcron, email, version, language, imageauthorname, imageauthoremail, imageauthorurl, imagecaption, endorsement, competencies and relatedbadges.
+
 === 2.7 ===
 
 * get_completed_criteria_sql() - This method was added to award_criteria class and must be overriden
index 5b95e9c..c198dd0 100644 (file)
@@ -33,6 +33,8 @@
                 {{> block_recentlyaccesseditems/placeholder-item }}
                 {{> block_recentlyaccesseditems/placeholder-item }}
                 {{> block_recentlyaccesseditems/placeholder-item }}
+                {{> block_recentlyaccesseditems/placeholder-item }}
+                {{> block_recentlyaccesseditems/placeholder-item }}
             </div>
         </div>
     </div>
index 485cf39..670c6c5 100644 (file)
@@ -52,6 +52,7 @@ $string['emailtagline'] = 'This is a copy of a message sent to you at "{$a->site
 $string['enabled'] = 'Enabled';
 $string['errorcallingprocessor'] = 'Error calling defined output';
 $string['errortranslatingdefault'] = 'Error translating default setting provided by plugin, using system defaults instead.';
+$string['eventgroupmessagesent'] = 'Group message sent';
 $string['eventnotificationviewed'] = 'Notification viewed';
 $string['eventnotificationsent'] = 'Notification sent';
 $string['eventmessagecontactadded'] = 'Message contact added';
@@ -185,6 +186,7 @@ $string['touserdoesntexist'] = 'You can not send a message to a user id ({$a}) t
 $string['unblockcontact'] = 'Unblock contact';
 $string['unknownuser'] = 'Unknown user';
 $string['unreadnotification'] = 'Unread notification: {$a}';
+$string['unreadnewgroupconversationmessage'] = 'New message from {$a->name} in {$a->conversationname}';
 $string['unreadnewmessage'] = 'New message from {$a}';
 $string['usercantbemessaged'] = 'You can\'t message {$a} due to their message preferences. Try adding them as a contact.';
 $string['viewfullnotification'] = 'View full notification';
@@ -200,4 +202,4 @@ $string['eventmessagecontactblocked'] = 'Message contact blocked';
 $string['eventmessagecontactunblocked'] = 'Message contact unblocked';
 $string['messagingdisabled'] = 'Messaging is disabled on this site, emails will be sent instead';
 $string['userisblockingyou'] = 'This user has blocked you from sending messages to them.';
-$string['userisblockingyounoncontact'] = '{$a} only accepts messages from their contacts.';
\ No newline at end of file
+$string['userisblockingyounoncontact'] = '{$a} only accepts messages from their contacts.';
index 3806563..0f48780 100644 (file)
  *
  * Attach this JS module to this list:
  *
- * define(['jquery', 'core/sortable_list'], function($, SortableList) {
+ * Space between define and ( critical in comment but not allowed in code in order to function
+ * correctly with Moodle's requirejs.php
+ *
+ * define (['jquery', 'core/sortable_list'], function($, SortableList) {
  *     var list = new SortableList('ul.my-awesome-list'); // source list (usually <ul> or <tbody>) - selector or element
  *
  *     // Listen to the events when element is dragged.
diff --git a/lib/classes/event/group_message_sent.php b/lib/classes/event/group_message_sent.php
new file mode 100644 (file)
index 0000000..c02d12d
--- /dev/null
@@ -0,0 +1,157 @@
+<?php
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Group message sent event.
+ *
+ * @package    core
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\event;
+
+defined('MOODLE_INTERNAL') || die();
+
+/**
+ * Group message sent event class.
+ *
+ * @property-read array $other {
+ *      Extra information about event.
+ *
+ *      - int courseid: the id of the related course.
+ *      - int conversationid: the id of the conversation in which the message was sent.
+ * }
+ *
+ * @package    core
+ * @copyright  2018 Jake Dallimore <jrhdallimore@gmail.com>
+ * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class group_message_sent extends base {
+    /**
+     * Create event using ids.
+     * @param int $userfromid
+     * @param int $conversationid
+     * @param int $messageid
+     * @param int $courseid course id the event is related with.
+     * @return message_sent
+     */
+    public static function create_from_ids(int $userfromid, int $conversationid, int $messageid, int $courseid) {
+        // We may be sending a message from the 'noreply' address, which means we are not actually sending a
+        // message from a valid user. In this case, we will set the userid to 0.
+        // Check if the userid is valid.
+        if (!\core_user::is_real_user($userfromid)) {
+            $userfromid = 0;
+        }
+
+        $event = self::create([
+            'objectid' => $messageid,
+            'userid' => $userfromid,
+            'context' => \context_system::instance(),
+            'other' => [
+                'courseid' => $courseid,
+                'conversationid' => $conversationid
+            ]
+        ]);
+
+        return $event;
+    }
+
+    /**
+     * Init method.
+     */
+    protected function init() {
+        $this->data['objecttable'] = 'messages';
+        $this->data['crud'] = 'c';
+        $this->data['edulevel'] = self::LEVEL_OTHER;
+    }
+
+    /**
+     * Returns localised general event name.
+     *
+     * @return string
+     */
+    public static function get_name() {
+        return get_string('eventgroupmessagesent', 'message');
+    }
+
+    /**
+     * Returns relevant URL.
+     *
+     * @return \moodle_url
+     */
+    public function get_url() {
+        // There currently isn't a way to link back from a 'group message sent' event to a conversation.
+        // So, just return the user to the index page.
+        return new \moodle_url('/message/index.php');
+    }
+
+    /**
+     * Returns description of what happened.
+     *
+     * @return string
+     */
+    public function get_description() {
+        $conversationid = $this->other['conversationid'];
+
+        // Check if we are sending from a valid user.
+        if (\core_user::is_real_user($this->userid)) {
+
+            return "The user with id '$this->userid' sent a message with id '$this->objectid' to the conversation " .
+                   "with id '$conversationid'.";
+        }
+
+        return "A message with id '$this->objectid' was sent by the system to the conversation with id '$conversationid'.";
+    }
+
+    /**
+     * Custom validation.
+     *
+     * @throws \coding_exception
+     * @return void
+     */
+    protected function validate_data() {
+        parent::validate_data();
+
+        if (!isset($this->other['courseid'])) {
+            throw new \coding_exception('The \'courseid\' value must be set in other.');
+        }
+        if (!isset($this->other['conversationid'])) {
+            throw new \coding_exception('The \'conversationid\' value must be set in other.');
+        }
+    }
+
+    /**
+     * Get the object this event maps to.
+     *
+     * @return array|string object id mapping.
+     */
+    public static function get_objectid_mapping() {
+        return ['db' => 'messages', 'restore' => base::NOT_MAPPED];
+    }
+
+    /**
+     * Get the item mappings for the 'other' fields for this event.
+     *
+     * @return array the array of other fields, mapped.
+     */
+    public static function get_other_mapping() {
+        $othermapped = [];
+        $othermapped['courseid'] = ['db' => 'course', 'restore' => base::NOT_MAPPED];
+        $othermapped['conversationid'] = ['db' => 'message_conversations', 'restore' => base::NOT_MAPPED];
+        return $othermapped;
+    }
+}
index 17192f3..4ffc62e 100644 (file)
@@ -45,6 +45,247 @@ class manager {
     /** @var array buffer of pending messages */
     protected static $buffer = array();
 
+    /** @var array buffer of pending messages to conversations */
+    protected static $convmessagebuffer = array();
+
+    /**
+     * Used for calling processors, and generating event data when sending a message to a conversation.
+     *
+     * This is ONLY used for messages of type 'message' (notification=0), and is responsible for:
+     *
+     * 1. generation of per-user event data (to pass to processors)
+     * 2. generation of the processors for each recipient member of the conversation
+     * 3. calling said processors for each member, passing in the per-user (local) eventdata.
+     * 4. generation of an appropriate event for the message send, depending on the conversation type
+     *   - messages to individual conversations generate a 'message_sent' event (as per legacy send_message())
+     *   - messages to group conversations generate a 'group_message_sent' event.
+     *
+     * @param message $eventdata
+     * @param \stdClass $savemessage
+     * @return int
+     */
+    public static function send_message_to_conversation(message $eventdata, \stdClass $savemessage) : int {
+        global $DB, $CFG, $SITE;
+
+        if (empty($eventdata->convid)) {
+            throw new \moodle_exception("Message is not being sent to a conversation. Please check event data.");
+        }
+
+        // Fetch default (site) preferences.
+        $defaultpreferences = get_message_output_default_preferences();
+        $preferencebase = $eventdata->component.'_'.$eventdata->name;
+
+        // Because we're dealing with multiple recipients, we need to send a localised (per-user) version of the eventdata to each
+        // processor, because of things like the language-specific subject. We're going to modify this, for each recipient member.
+        // Any time we're modifying the event data here, we should be using the localised version.
+        // This localised version is based on the generic event data, but we should keep that object intact, so we clone it.
+        $localisedeventdata = clone $eventdata;
+
+        // Get user records for all members of the conversation.
+        $sql = "SELECT u.*
+                  FROM {message_conversation_members} mcm
+                  JOIN {user} u
+                    ON (mcm.conversationid = :convid AND u.id = mcm.userid)
+              ORDER BY u.id desc";
+        $members = $DB->get_records_sql($sql, ['convid' => $eventdata->convid]);
+        if (empty($members)) {
+            throw new \moodle_exception("Conversation has no members or does not exist.");
+        }
+
+        if (!is_object($localisedeventdata->userfrom)) {
+            $localisedeventdata->userfrom = $members[$localisedeventdata->userfrom];
+        }
+
+        // This should now hold only the other users (recipients).
+        unset($members[$localisedeventdata->userfrom->id]);
+        $otherusers = $members;
+
+        // Get conversation type and name. We'll use this to determine which message subject to generate, depending on type.
+        $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type, name');
+
+        // We treat individual conversations the same as any direct message with 'userfrom' and 'userto' specified.
+        // We know the other user, so set the 'userto' field so that the event code will get access to this field.
+        // If this was a legacy caller (eventdata->userto is set), then use that instead, as we want to use the fields specified
+        // in that object instead of using one fetched from the DB.
+        $legacymessage = false;
+        if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+            if (isset($eventdata->userto)) {
+                $legacymessage = true;
+            } else {
+                $otheruser = reset($otherusers);
+                $eventdata->userto = $otheruser;
+            }
+        }
+
+        // Fetch enabled processors.
+        // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
+        $processors = array_filter(get_message_processors(false), function($processor) {
+            if ($processor->object->force_process_messages()) {
+                return true;
+            }
+
+            return ($processor->enabled && $processor->configured);
+        });
+
+        // For each member of the conversation, other than the sender:
+        // 1. Set recipient specific event data (language specific, user prefs, etc)
+        // 2. Generate recipient specific processor list
+        // 3. Call send_message() to pass the message to processors and generate the relevant per-user events.
+        $eventprocmaps = []; // Init the event/processors buffer.
+        foreach ($otherusers as $recipient) {
+            // If this message was a legacy (1:1) message, then we use the userto.
+            if ($legacymessage) {
+                $recipient = $eventdata->userto;
+            }
+
+            $usertoisrealuser = (\core_user::is_real_user($recipient->id) != false);
+
+            // Using string manager directly so that strings in the message will be in the message recipients language rather than
+            // the sender's.
+            if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+                $localisedeventdata->subject = get_string_manager()->get_string('unreadnewmessage', 'message',
+                    fullname($localisedeventdata->userfrom), $recipient->lang);
+            } else if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
+                $stringdata = (object) ['name' => fullname($localisedeventdata->userfrom), 'conversationname' => $conv->name];
+                $localisedeventdata->subject = get_string_manager()->get_string('unreadnewgroupconversationmessage', 'message',
+                    $stringdata, $recipient->lang);
+            }
+
+            // Spoof the userto based on the current member id.
+            $localisedeventdata->userto = $recipient;
+
+            $s = new \stdClass();
+            $s->sitename = format_string($SITE->shortname, true, array('context' => \context_course::instance(SITEID)));
+            // When the new interface lands, the URL may be reintroduced, but for now it isn't supported, so just hit the index.
+            $s->url = $CFG->wwwroot.'/message/index.php';
+            $emailtagline = get_string_manager()->get_string('emailtagline', 'message', $s, $recipient->lang);
+
+            $localisedeventdata->fullmessage = $eventdata->fullmessage;
+            $localisedeventdata->fullmessagehtml = $eventdata->fullmessagehtml;
+            if (!empty($localisedeventdata->fullmessage)) {
+                $localisedeventdata->fullmessage .= "\n\n---------------------------------------------------------------------\n"
+                    . $emailtagline;
+            }
+            if (!empty($localisedeventdata->fullmessagehtml)) {
+                $localisedeventdata->fullmessagehtml .=
+                    "<br><br>---------------------------------------------------------------------<br>" . $emailtagline;
+            }
+
+            // If recipient is internal user (noreply user), and emailstop is set then don't send any msg.
+            if (!$usertoisrealuser && !empty($recipient->emailstop)) {
+                debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL);
+                return false;
+            }
+
+            // Set the online state.
+            if (isset($CFG->block_online_users_timetosee)) {
+                $timetoshowusers = $CFG->block_online_users_timetosee * 60;
+            } else {
+                $timetoshowusers = 300;
+            }
+
+            // Work out if the user is logged in or not.
+            $userstate = 'loggedoff';
+            if (!empty($localisedeventdata->userto->lastaccess)
+                    && (time() - $timetoshowusers) < $localisedeventdata->userto->lastaccess) {
+                $userstate = 'loggedin';
+            }
+
+            // Fill in the array of processors to be used based on default and user preferences.
+            $processorlist = [];
+            foreach ($processors as $processor) {
+                // Skip adding processors for internal user, if processor doesn't support sending message to internal user.
+                if (!$usertoisrealuser && !$processor->object->can_send_to_any_users()) {
+                    continue;
+                }
+
+                // First find out permissions.
+                $defaultpreference = $processor->name.'_provider_'.$preferencebase.'_permitted';
+                if (isset($defaultpreferences->{$defaultpreference})) {
+                    $permitted = $defaultpreferences->{$defaultpreference};
+                } else {
+                    // MDL-25114 They supplied an $eventdata->component $eventdata->name combination which doesn't
+                    // exist in the message_provider table (thus there is no default settings for them).
+                    $preferrormsg = "Could not load preference $defaultpreference. Make sure the component and name you supplied
+                    to message_send() are valid.";
+                    throw new coding_exception($preferrormsg);
+                }
+
+                // Find out if user has configured this output.
+                // Some processors cannot function without settings from the user.
+                $userisconfigured = $processor->object->is_user_configured($recipient);
+
+                // DEBUG: notify if we are forcing unconfigured output.
+                if ($permitted == 'forced' && !$userisconfigured) {
+                    debugging('Attempt to force message delivery to user who has "'.$processor->name.'" output unconfigured',
+                        DEBUG_NORMAL);
+                }
+
+                // Populate the list of processors we will be using.
+                if (!$eventdata->notification && $processor->object->force_process_messages()) {
+                    $processorlist[] = $processor->name;
+                } else if ($permitted == 'forced' && $userisconfigured) {
+                    // An admin is forcing users to use this message processor. Use this processor unconditionally.
+                    $processorlist[] = $processor->name;
+                } else if ($permitted == 'permitted' && $userisconfigured && !$recipient->emailstop) {
+                    // User has not disabled notifications.
+                    // See if user set any notification preferences, otherwise use site default ones.
+                    $userpreferencename = 'message_provider_'.$preferencebase.'_'.$userstate;
+                    if ($userpreference = get_user_preferences($userpreferencename, null, $recipient)) {
+                        if (in_array($processor->name, explode(',', $userpreference))) {
+                            $processorlist[] = $processor->name;
+                        }
+                    } else if (isset($defaultpreferences->{$userpreferencename})) {
+                        if (in_array($processor->name, explode(',', $defaultpreferences->{$userpreferencename}))) {
+                            $processorlist[] = $processor->name;
+                        }
+                    }
+                }
+            }
+            // Batch up the localised event data and processor list for all users into a local buffer.
+            $eventprocmaps[] = [clone($localisedeventdata), $processorlist];
+        }
+        // Then pass it off as one item of work, to be processed by send_conversation_message_to_processors(), which will
+        // handle all transaction buffering logic.
+        self::send_conversation_message_to_processors($eventprocmaps, $eventdata, $savemessage);
+
+        return $savemessage->id;
+    }
+
+    /**
+     * Takes a list of localised event data, and tries to send them to their respective member's message processors.
+     *
+     * Input format:
+     *  [CONVID => [$localisedeventdata, $savemessage, $processorlist], ].
+     *
+     * @param array $eventprocmaps the array of localised event data and processors for each member of the conversation.
+     * @param message $eventdata the original conversation message eventdata
+     * @param \stdClass $savemessage the saved message record.
+     * @throws \coding_exception
+     */
+    protected static function send_conversation_message_to_processors(array $eventprocmaps, message $eventdata,
+                                                                      \stdClass $savemessage) {
+        global $DB;
+
+        // We cannot communicate with external systems in DB transactions,
+        // buffer the messages if necessary.
+        if ($DB->is_transaction_started()) {
+            // Buffer this group conversation message and it's record.
+            self::$convmessagebuffer[] = [$eventprocmaps, $eventdata, $savemessage];
+            return;
+        }
+
+        // Send each localised version of the event data to each member's respective processors.
+        foreach ($eventprocmaps as $eventprocmap) {
+            $eventdata = $eventprocmap[0];
+            $processorlist = $eventprocmap[1];
+            self::call_processors($eventdata, $processorlist);
+        }
+
+        // Trigger event for sending a message or notification - we need to do this before marking as read!
+        self::trigger_message_events($eventdata, $savemessage);
+    }
+
     /**
      * Do the message sending.
      *
@@ -62,21 +303,7 @@ class manager {
 
         if (empty($processorlist)) {
             // Trigger event for sending a message or notification - we need to do this before marking as read!
-            if ($eventdata->notification) {
-                \core\event\notification_sent::create_from_ids(
-                    $eventdata->userfrom->id,
-                    $eventdata->userto->id,
-                    $savemessage->id,
-                    $eventdata->courseid
-                )->trigger();
-            } else { // Must be a message.
-                \core\event\message_sent::create_from_ids(
-                    $eventdata->userfrom->id,
-                    $eventdata->userto->id,
-                    $savemessage->id,
-                    $eventdata->courseid
-                )->trigger();
-            }
+            self::trigger_message_events($eventdata, $savemessage);
 
             if ($eventdata->notification or empty($CFG->messaging)) {
                 // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
@@ -110,7 +337,6 @@ class manager {
 
         // We cannot communicate with external systems in DB transactions,
         // buffer the messages if necessary.
-
         if ($DB->is_transaction_started()) {
             // We need to clone all objects so that devs may not modify it from outside later.
             $eventdata = clone($eventdata);
@@ -125,33 +351,11 @@ class manager {
             return $savemessage->id;
         }
 
-        foreach ($processorlist as $procname) {
-            // Let new messaging class add custom content based on the processor.
-            $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
-            $stdproc = new \stdClass();
-            $stdproc->name = $procname;
-            $processor = \core_message\api::get_processed_processor_object($stdproc);
-            if (!$processor->object->send_message($proceventdata)) {
-                debugging('Error calling message processor ' . $procname);
-            }
-        }
+        // Send the message to processors.
+        self::call_processors($eventdata, $processorlist);
 
         // Trigger event for sending a message or notification - we need to do this before marking as read!
-        if ($eventdata->notification) {
-            \core\event\notification_sent::create_from_ids(
-                $eventdata->userfrom->id,
-                $eventdata->userto->id,
-                $savemessage->id,
-                $eventdata->courseid
-            )->trigger();
-        } else { // Must be a message.
-            \core\event\message_sent::create_from_ids(
-                $eventdata->userfrom->id,
-                $eventdata->userto->id,
-                $savemessage->id,
-                $eventdata->courseid
-            )->trigger();
-        }
+        self::trigger_message_events($eventdata, $savemessage);
 
         if (empty($CFG->messaging)) {
             // If they have deselected all processors and its a notification mark it read. The user doesn't want to be bothered.
@@ -173,7 +377,7 @@ class manager {
      * Note: to be used from DML layer only.
      */
     public static function database_transaction_commited() {
-        if (!self::$buffer) {
+        if (!self::$buffer && !self::$convmessagebuffer) {
             return;
         }
         self::process_buffer();
@@ -186,19 +390,86 @@ class manager {
      */
     public static function database_transaction_rolledback() {
         self::$buffer = array();
+        self::$convmessagebuffer = array();
     }
 
     /**
      * Sent out any buffered messages if necessary.
      */
     protected static function process_buffer() {
-        // Reset the buffer first in case we get exception from processor.
+        // Reset the buffers first in case we get exception from processor.
         $messages = self::$buffer;
         self::$buffer = array();
+        $convmessages = self::$convmessagebuffer;
+        self::$convmessagebuffer = array();
 
         foreach ($messages as $message) {
             list($eventdata, $savemessage, $processorlist) = $message;
             self::send_message_to_processors($eventdata, $savemessage, $processorlist);
         }
+
+        foreach ($convmessages as $convmessage) {
+            list($eventprocmap, $eventdata, $savemessage) = $convmessage;
+            self::send_conversation_message_to_processors($eventprocmap, $eventdata, $savemessage);
+        }
+    }
+
+    /**
+     * Trigger an appropriate message creation event, based on the supplied $eventdata and $savemessage.
+     *
+     * @param message $eventdata the eventdata for the message.
+     * @param \stdClass $savemessage the message record.
+     * @throws \coding_exception
+     */
+    protected static function trigger_message_events(message $eventdata, \stdClass $savemessage) {
+        global $DB;
+        if ($eventdata->notification) {
+            \core\event\notification_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
+            )->trigger();
+        } else { // Must be a message.
+            // If the message is a group conversation, then trigger the 'group_message_sent' event.
+            if ($eventdata->convid) {
+                $conv = $DB->get_record('message_conversations', ['id' => $eventdata->convid], 'id, type');
+                if ($conv->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP) {
+                    \core\event\group_message_sent::create_from_ids(
+                        $eventdata->userfrom->id,
+                        $eventdata->convid,
+                        $savemessage->id,
+                        $eventdata->courseid
+                    )->trigger();
+                    return;
+                }
+                // Individual type conversations fall through to the default 'message_sent' event.
+            }
+            \core\event\message_sent::create_from_ids(
+                $eventdata->userfrom->id,
+                $eventdata->userto->id,
+                $savemessage->id,
+                $eventdata->courseid
+            )->trigger();
+        }
+    }
+
+    /**
+     * For each processor, call it's send_message() method.
+     *
+     * @param message $eventdata the message object.
+     * @param array $processorlist the list of processors for a single user.
+     */
+    protected static function call_processors(message $eventdata, array $processorlist) {
+        foreach ($processorlist as $procname) {
+            // Let new messaging class add custom content based on the processor.
+            $proceventdata = ($eventdata instanceof message) ? $eventdata->get_eventobject_for_processor($procname) : $eventdata;
+            $stdproc = new \stdClass();
+            $stdproc->name = $procname;
+            $processor = \core_message\api::get_processed_processor_object($stdproc);
+            if (!$processor->object->send_message($proceventdata)) {
+                debugging('Error calling message processor ' . $procname);
+            }
+        }
     }
 }
index 346ea3c..1f4483d 100644 (file)
@@ -1037,7 +1037,8 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_contacts',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for the contact list',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for the contact list',
         'type' => 'read',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
@@ -1046,7 +1047,8 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_messages',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for the messages',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for the messages',
         'type' => 'read',
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
@@ -1055,7 +1057,8 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_get_most_recent_message',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for the most recent message',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for the most recent message',
         'type' => 'read',
         'ajax' => true,
     ),
@@ -1063,7 +1066,8 @@ $functions = array(
         'classname' => 'core_message_external',
         'methodname' => 'data_for_messagearea_get_profile',
         'classpath' => 'message/externallib.php',
-        'description' => 'Retrieve the template data for the users\'s profile',
+        'description' => '** DEPRECATED ** Please do not call this function any more.
+                          Retrieve the template data for the users\'s profile',
         'type' => 'read',
         'ajax' => true,
     ),
@@ -1191,6 +1195,16 @@ $functions = array(
         'ajax' => true,
         'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
+    'core_message_send_messages_to_conversation' => array(
+        'classname' => 'core_message_external',
+        'methodname' => 'send_messages_to_conversation',
+        'classpath' => 'message/externallib.php',
+        'description' => 'Send messages to an existing conversation between users',
+        'type' => 'write',
+        'capabilities' => 'moodle/site:sendmessage',
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
+    ),
     'core_message_get_conversation_messages' => array(
         'classname' => 'core_message_external',
         'methodname' => 'get_conversation_messages',
@@ -1545,7 +1559,8 @@ $functions = array(
         'description' => 'Set user preferences.',
         'type' => 'write',
         'capabilities' => 'moodle/site:config',
-        'ajax' => true
+        'ajax' => true,
+        'services' => array(MOODLE_OFFICIAL_MOBILE_SERVICE),
     ),
     'core_user_agree_site_policy' => array(
         'classname' => 'core_user_external',
index 66b2002..95d97f2 100644 (file)
@@ -55,7 +55,7 @@ require_once(__DIR__ . '/../message/lib.php');
  * @return mixed the integer ID of the new message or false if there was a problem with submitted data
  */
 function message_send(\core\message\message $eventdata) {
-    global $CFG, $DB;
+    global $CFG, $DB, $SITE;
 
     //new message ID to return
     $messageid = false;
@@ -74,21 +74,114 @@ function message_send(\core\message\message $eventdata) {
         $eventdata->notification = 1;
     }
 
-    if (!is_object($eventdata->userto)) {
-        $eventdata->userto = core_user::get_user($eventdata->userto);
-    }
     if (!is_object($eventdata->userfrom)) {
         $eventdata->userfrom = core_user::get_user($eventdata->userfrom);
     }
-    if (!$eventdata->userto) {
-        debugging('Attempt to send msg to unknown user', DEBUG_NORMAL);
-        return false;
-    }
     if (!$eventdata->userfrom) {
         debugging('Attempt to send msg from unknown user', DEBUG_NORMAL);
         return false;
     }
 
+    // Legacy messages (FROM a single user TO a single user) must be converted into conversation messages.
+    // Then, these will be passed through the conversation messages code below.
+    if (!$eventdata->notification && !$eventdata->convid) {
+        // If messaging is disabled at the site level, then the 'instantmessage' provider is always disabled.
+        // Given this is the only 'message' type message provider, we can exit now if this is the case.
+        // Don't waste processing time trying to work out the other conversation member, if it's an individual
+        // conversation, just throw a generic debugging notice and return.
+        if (empty($CFG->messaging) || $eventdata->component !== 'moodle' || $eventdata->name !== 'instantmessage') {
+            debugging('Attempt to send msg from a provider '.$eventdata->component.'/'.$eventdata->name.
+                ' that is inactive or not allowed for the user id='.$eventdata->userto->id, DEBUG_NORMAL);
+            return false;
+        }
+
+        if (!is_object($eventdata->userto)) {
+            $eventdata->userto = core_user::get_user($eventdata->userto);
+        }
+        if (!$eventdata->userto) {
+            debugging('Attempt to send msg to unknown user', DEBUG_NORMAL);
+            return false;
+        }
+
+        // Verify all necessary data fields are present.
+        if (!isset($eventdata->userto->auth) or !isset($eventdata->userto->suspended)
+            or !isset($eventdata->userto->deleted) or !isset($eventdata->userto->emailstop)) {
+
+            debugging('Necessary properties missing in userto object, fetching full record', DEBUG_DEVELOPER);
+            $eventdata->userto = core_user::get_user($eventdata->userto->id);
+        }
+
+        $usertoisrealuser = (core_user::is_real_user($eventdata->userto->id) != false);
+        // If recipient is internal user (noreply user), and emailstop is set then don't send any msg.
+        if (!$usertoisrealuser && !empty($eventdata->userto->emailstop)) {
+            debugging('Attempt to send msg to internal (noreply) user', DEBUG_NORMAL);
+            return false;
+        }
+
+        if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id,
+                                                                                  $eventdata->userto->id])) {
+            $conversation = \core_message\api::create_conversation(
+                \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+                [
+                    $eventdata->userfrom->id,
+                    $eventdata->userto->id
+                ]
+            );
+        }
+        // We either have found a conversation, or created one.
+        $conversationid = $conversationid ? $conversationid : $conversation->id;
+        $eventdata->convid = $conversationid;
+    }
+
+    // This is a message directed to a conversation, not a specific user as was the way in legacy messaging.
+    // The above code has adapted the legacy messages into conversation messages.
+    // We must call send_message_to_conversation(), which handles per-member processor iteration and triggers
+    // a per-conversation event.
+    // All eventdata for messages should now have a convid, as we fixed this above.
+    if (!$eventdata->notification) {
+
+        // Only one message will be saved to the DB.
+        $conversationid = $eventdata->convid;
+        $table = 'messages';
+        $tabledata = new stdClass();
+        $tabledata->courseid = $eventdata->courseid;
+        $tabledata->useridfrom = $eventdata->userfrom->id;
+        $tabledata->conversationid = $conversationid;
+        $tabledata->subject = $eventdata->subject;
+        $tabledata->fullmessage = $eventdata->fullmessage;
+        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
+        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
+        $tabledata->smallmessage = $eventdata->smallmessage;
+        $tabledata->timecreated = time();
+
+        if ($messageid = message_handle_phpunit_redirection($eventdata, $table, $tabledata)) {
+            return $messageid;
+        }
+
+        // Cache messages.
+        if (!empty($eventdata->convid)) {
+            // Cache the timecreated value of the last message in this conversation.
+            $cache = cache::make('core', 'message_time_last_message_between_users');
+            $key = \core_message\helper::get_last_message_time_created_cache_key($eventdata->convid);
+            $cache->set($key, $tabledata->timecreated);
+        }
+
+        // Store unread message just in case we get a fatal error any time later.
+        $tabledata->id = $DB->insert_record($table, $tabledata);
+        $eventdata->savedmessageid = $tabledata->id;
+
+        return \core\message\manager::send_message_to_conversation($eventdata, $tabledata);
+    }
+
+    // Else the message is a notification.
+    if (!is_object($eventdata->userto)) {
+        $eventdata->userto = core_user::get_user($eventdata->userto);
+    }
+    if (!$eventdata->userto) {
+        debugging('Attempt to send msg to unknown user', DEBUG_NORMAL);
+        return false;
+    }
+
     // If the provider's component is disabled or the user can't receive messages from it, don't send the message.
     $isproviderallowed = false;
     foreach (message_get_providers_for_user($eventdata->userto->id) as $provider) {
@@ -133,110 +226,37 @@ function message_send(\core\message\message $eventdata) {
     }
 
     // Check if we are creating a notification or message.
-    if ($eventdata->notification) {
-        $table = 'notifications';
-
-        $tabledata = new stdClass();
-        $tabledata->useridfrom = $eventdata->userfrom->id;
-        $tabledata->useridto = $eventdata->userto->id;
-        $tabledata->subject = $eventdata->subject;
-        $tabledata->fullmessage = $eventdata->fullmessage;
-        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
-        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
-        $tabledata->smallmessage = $eventdata->smallmessage;
-        $tabledata->eventtype = $eventdata->name;
-        $tabledata->component = $eventdata->component;
-
-        if (!empty($eventdata->contexturl)) {
-            $tabledata->contexturl = (string)$eventdata->contexturl;
-        } else {
-            $tabledata->contexturl = null;
-        }
-
-        if (!empty($eventdata->contexturlname)) {
-            $tabledata->contexturlname = (string)$eventdata->contexturlname;
-        } else {
-            $tabledata->contexturlname = null;
-        }
+    $table = 'notifications';
+
+    $tabledata = new stdClass();
+    $tabledata->useridfrom = $eventdata->userfrom->id;
+    $tabledata->useridto = $eventdata->userto->id;
+    $tabledata->subject = $eventdata->subject;
+    $tabledata->fullmessage = $eventdata->fullmessage;
+    $tabledata->fullmessageformat = $eventdata->fullmessageformat;
+    $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
+    $tabledata->smallmessage = $eventdata->smallmessage;
+    $tabledata->eventtype = $eventdata->name;
+    $tabledata->component = $eventdata->component;
+    $tabledata->timecreated = time();
+    if (!empty($eventdata->contexturl)) {
+        $tabledata->contexturl = (string)$eventdata->contexturl;
     } else {
-        $table = 'messages';
-
-        if (!$conversationid = \core_message\api::get_conversation_between_users([$eventdata->userfrom->id,
-                $eventdata->userto->id])) {
-            $conversation = \core_message\api::create_conversation(
-                \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
-                [
-                    $eventdata->userfrom->id,
-                    $eventdata->userto->id
-                ]
-            );
-            $conversationid = $conversation->id;
-        }
-
-        $tabledata = new stdClass();
-        $tabledata->courseid = $eventdata->courseid;
-        $tabledata->useridfrom = $eventdata->userfrom->id;
-        $tabledata->conversationid = $conversationid;
-        $tabledata->subject = $eventdata->subject;
-        $tabledata->fullmessage = $eventdata->fullmessage;
-        $tabledata->fullmessageformat = $eventdata->fullmessageformat;
-        $tabledata->fullmessagehtml = $eventdata->fullmessagehtml;
-        $tabledata->smallmessage = $eventdata->smallmessage;
+        $tabledata->contexturl = null;
     }
 
-    $tabledata->timecreated = time();
-
-    if (PHPUNIT_TEST and class_exists('phpunit_util')) {
-        // Add some more tests to make sure the normal code can actually work.
-        $componentdir = core_component::get_component_directory($eventdata->component);
-        if (!$componentdir or !is_dir($componentdir)) {
-            throw new coding_exception('Invalid component specified in message-send(): '.$eventdata->component);
-        }
-        if (!file_exists("$componentdir/db/messages.php")) {
-            throw new coding_exception("$eventdata->component does not contain db/messages.php necessary for message_send()");
-        }
-        $messageproviders = null;
-        include("$componentdir/db/messages.php");
-        if (!isset($messageproviders[$eventdata->name])) {
-            throw new coding_exception("Missing messaging defaults for event '$eventdata->name' in '$eventdata->component' messages.php file");
-        }
-        unset($componentdir);
-        unset($messageproviders);
-        // Now ask phpunit if it wants to catch this message.
-        if (phpunit_util::is_redirecting_messages()) {
-            $messageid = $DB->insert_record($table, $tabledata);
-            $message = $DB->get_record($table, array('id' => $messageid));
-
-            // Add the useridto attribute for BC.
-            $message->useridto = $eventdata->userto->id;
-
-            // Mark the message/notification as read.
-            if ($eventdata->notification) {
-                \core_message\api::mark_notification_as_read($message);
-            } else {
-                \core_message\api::mark_message_as_read($eventdata->userto->id, $message);
-            }
+    if (!empty($eventdata->contexturlname)) {
+        $tabledata->contexturlname = (string)$eventdata->contexturlname;
+    } else {
+        $tabledata->contexturlname = null;
+    }
 
-            // Unit tests need this detail.
-            $message->notification = $eventdata->notification;
-            phpunit_util::message_sent($message);
-            return $messageid;
-        }
+    if ($messageid = message_handle_phpunit_redirection($eventdata, $table, $tabledata)) {
+        return $messageid;
     }
 
     // Fetch enabled processors.
-    // If we are dealing with a message some processors may want to handle it regardless of user and site settings.
-    if (!$eventdata->notification) {
-        $processors = array_filter(get_message_processors(false), function($processor) {
-            if ($processor->object->force_process_messages()) {
-                return true;
-            }
-
-            return ($processor->enabled && $processor->configured);
-        });
-    } else {
-        $processors = get_message_processors(true);
-    }
+    $processors = get_message_processors(true);
 
     // Preset variables
     $processorlist = array();
@@ -269,9 +289,7 @@ function message_send(\core\message\message $eventdata) {
         }
 
         // Populate the list of processors we will be using
-        if (!$eventdata->notification && $processor->object->force_process_messages()) {
-            $processorlist[] = $processor->name;
-        } else if ($permitted == 'forced' && $userisconfigured) {
+        if ($permitted == 'forced' && $userisconfigured) {
             // An admin is forcing users to use this message processor. Use this processor unconditionally.
             $processorlist[] = $processor->name;
         } else if ($permitted == 'permitted' && $userisconfigured && !$eventdata->userto->emailstop) {
@@ -290,16 +308,6 @@ function message_send(\core\message\message $eventdata) {
         }
     }
 
-    // Only cache messages, not notifications.
-    if (!$eventdata->notification) {
-        if (!empty($eventdata->convid)) {
-            // Cache the timecreated value of the last message in this conversation.
-            $cache = cache::make('core', 'message_time_last_message_between_users');
-            $key = \core_message\helper::get_last_message_time_created_cache_key($eventdata->convid);
-            $cache->set($key, $tabledata->timecreated);
-        }
-    }
-
     // Store unread message just in case we get a fatal error any time later.
     $tabledata->id = $DB->insert_record($table, $tabledata);
     $eventdata->savedmessageid = $tabledata->id;
@@ -308,6 +316,68 @@ function message_send(\core\message\message $eventdata) {
     return \core\message\manager::send_message($eventdata, $tabledata, $processorlist);
 }
 
+/**
+ * Helper method containing the PHPUnit specific code, used to redirect and capture messages/notifications.
+ *
+ * @param \core\message\message $eventdata the message object
+ * @param string $table the table to store the tabledata in, either messages or notifications.
+ * @param stdClass $tabledata the data to be stored when creating the message/notification.
+ * @return int the id of the stored message.
+ */
+function message_handle_phpunit_redirection(\core\message\message $eventdata, string $table, \stdClass $tabledata) {
+    global $DB;
+    if (PHPUNIT_TEST and class_exists('phpunit_util')) {
+        // Add some more tests to make sure the normal code can actually work.
+        $componentdir = core_component::get_component_directory($eventdata->component);
+        if (!$componentdir or !is_dir($componentdir)) {
+            throw new coding_exception('Invalid component specified in message-send(): '.$eventdata->component);
+        }
+        if (!file_exists("$componentdir/db/messages.php")) {
+            throw new coding_exception("$eventdata->component does not contain db/messages.php necessary for message_send()");
+        }
+        $messageproviders = null;
+        include("$componentdir/db/messages.php");
+        if (!isset($messageproviders[$eventdata->name])) {
+            throw new coding_exception("Missing messaging defaults for event '$eventdata->name' in '$eventdata->component' " .
+                "messages.php file");
+        }
+        unset($componentdir);
+        unset($messageproviders);
+        // Now ask phpunit if it wants to catch this message.
+        if (phpunit_util::is_redirecting_messages()) {
+            $messageid = $DB->insert_record($table, $tabledata);
+            $message = $DB->get_record($table, array('id' => $messageid));
+
+            if ($eventdata->notification) {
+                // Add the useridto attribute for BC.
+                $message->useridto = $eventdata->userto->id;
+
+                // Mark the notification as read.
+                \core_message\api::mark_notification_as_read($message);
+            } else {
+                // Add the useridto attribute for BC.
+                if (isset($eventdata->userto)) {
+                    $message->useridto = $eventdata->userto->id;
+                }
+                // Mark the message as read for each of the other users.
+                $sql = "SELECT u.*
+                  FROM {message_conversation_members} mcm
+                  JOIN {user} u
+                    ON (mcm.conversationid = :convid AND u.id = mcm.userid AND u.id != :userid)";
+                $otherusers = $DB->get_records_sql($sql, ['convid' => $eventdata->convid, 'userid' => $eventdata->userfrom->id]);
+                foreach ($otherusers as $othermember) {
+                    \core_message\api::mark_message_as_read($othermember->id, $message);
+                }
+            }
+
+            // Unit tests need this detail.
+            $message->notification = $eventdata->notification;
+            phpunit_util::message_sent($message);
+            return $messageid;
+        }
+    }
+}
+
 /**
  * Updates the message_providers table with the current set of message providers
  *
index b8e4837..8267fa7 100644 (file)
@@ -48,8 +48,8 @@ defined('MOODLE_INTERNAL') || die();
 /**
  * The reCAPTCHA URL's
  */
-define('RECAPTCHA_API_URL', 'https://www.google.com/recaptcha/api.js');
-define('RECAPTCHA_VERIFY_URL', 'https://www.google.com/recaptcha/api/siteverify');
+define('RECAPTCHA_API_URL', 'https://www.recaptcha.net/recaptcha/api.js');
+define('RECAPTCHA_VERIFY_URL', 'https://www.recaptcha.net/recaptcha/api/siteverify');
 
 /**
  * Returns the language code the reCAPTCHA element should use.
index 3f1d3de..d2e0265 100644 (file)
@@ -171,10 +171,10 @@ class core_message_testcase extends advanced_testcase {
         $this->assertSame(true, $recordexists);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
-        $this->assertSame($message->subject, $email->subject);
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
-        $this->assertRegExp('/test message body test/', $email->body);
+        $this->assertRegExp('/test message body.*test/s', $email->body);
         $sink->clear();
 
         // Test that event fired includes the courseid.
@@ -211,7 +211,7 @@ class core_message_testcase extends advanced_testcase {
         $this->assertSame(true, $recordexists);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
-        $this->assertSame($message->subject, $email->subject);
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $this->assertNotRegExp('/test message body test/', $email->body);
index 679175c..226de53 100644 (file)
@@ -65,7 +65,7 @@ class core_messagelib_testcase extends advanced_testcase {
         message_send($message);
         $emails = $sink->get_messages();
         $email = reset($emails);
-        $this->assertEquals($email->subject, 'message subject 1');
+        $this->assertEquals(get_string('unreadnewmessage', 'message', fullname(get_admin())), $email->subject);
     }
     public function test_message_get_providers_for_user() {
         global $CFG, $DB;
@@ -548,7 +548,7 @@ class core_messagelib_testcase extends advanced_testcase {
         $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
-        $this->assertSame($message->subject, $email->subject);
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $sink->clear();
@@ -581,7 +581,7 @@ class core_messagelib_testcase extends advanced_testcase {
         $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
         $this->assertSame($user1->email, $email->from);
         $this->assertSame($user2->email, $email->to);
-        $this->assertSame($message->subject, $email->subject);
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
         $this->assertNotEmpty($email->header);
         $this->assertNotEmpty($email->body);
         $sink->clear();
@@ -732,6 +732,255 @@ class core_messagelib_testcase extends advanced_testcase {
         $sink->clear();
     }
 
+    /**
+     * Tests calling message_send() with $eventdata representing a message to an individual conversation.
+     *
+     * This test will verify:
+     * - that the 'messages' record is created.
+     * - that the processors will be called for each conversation member, except the sender.
+     * - the a single event will be generated - 'message_sent'
+     *
+     * Note: We won't redirect/capture messages in this test because doing so causes message_send() to return early, before
+     * processors and events code is called. We need to test this code here, as we generally redirect messages elsewhere and we
+     * need to be sure this is covered.
+     */
+    public function test_message_send_to_conversation_individual() {
+        global $DB;
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        // Create some users and a conversation between them.
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [$user1->id, $user2->id], '1:1 project discussion');
+
+        // Generate the message.
+        $message = new \core\message\message();
+        $message->courseid          = 1;
+        $message->component         = 'moodle';
+        $message->name              = 'instantmessage';
+        $message->userfrom          = $user1;
+        $message->convid            = $conversation->id;
+        $message->subject           = 'message subject 1';
+        $message->fullmessage       = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = '<p>message body</p>';
+        $message->smallmessage      = 'small message';
+        $message->notification      = '0';
+
+        // Content specific to the email processor.
+        $content = array('*' => array('header' => ' test ', 'footer' => ' test '));
+        $message->set_additional_content('email', $content);
+
+        // Ensure we're going to hit the email processor for this user.
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+
+        // Now, send a message and verify the message processors (in this case, email) are hit.
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(1, $emails);
+        $email = reset($emails);
+
+        // Verify the record was created in 'messages'.
+        $recordexists = $DB->record_exists('messages', ['id' => $messageid]);
+        $this->assertTrue($recordexists);
+
+        // Verify the email information.
+        $this->assertSame($user1->email, $email->from);
+        $this->assertSame($user2->email, $email->to);
+
+        // The message subject is generated during the call for conversation messages,
+        // as the conversation may have many members having different lang preferences.
+        $this->assertSame(get_string('unreadnewmessage', 'message', fullname($user1)), $email->subject);
+
+        // The email content will have had an emailtagline appended to it, based on lang prefs,
+        // so verify the expected beginning and ends.
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/test message body.*test/s', $email->body);
+        $sink->clear();
+
+        // Now, send the message again, and verify that the event fired includes the courseid and conversationid.
+        $eventsink = $this->redirectEvents();
+        $messageid = message_send($message);
+        $events = $eventsink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf(\core\event\message_sent::class, $event);
+        $this->assertEquals($user1->id, $event->userid);
+        $this->assertEquals($user2->id, $event->relateduserid);
+        $this->assertEquals($message->courseid, $event->other['courseid']);
+
+        $eventsink->clear();
+        $sink->clear();
+    }
+
+    /**
+     * Tests calling message_send() with $eventdata representing a message to an group conversation.
+     *
+     * This test will verify:
+     * - that the 'messages' record is created.
+     * - that the processors will be called for each conversation member, except the sender.
+     * - the a single event will be generated - 'group_message_sent'
+     *
+     * Note: We won't redirect/capture messages in this test because doing so causes message_send() to return early, before
+     * processors and events code is called. We need to test this code here, as we generally redirect messages elsewhere and we
+     * need to be sure this is covered.
+     */
+    public function test_message_send_to_conversation_group() {
+        global $DB, $CFG, $SITE;
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        // Create some users and a conversation between them.
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id], 'Group project discussion');
+
+        // Generate the message.
+        $message = new \core\message\message();
+        $message->courseid          = 1;
+        $message->component         = 'moodle';
+        $message->name              = 'instantmessage';
+        $message->userfrom          = $user1;
+        $message->convid            = $conversation->id;
+        $message->subject           = 'message subject 1';
+        $message->fullmessage       = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = '<p>message body</p>';
+        $message->smallmessage      = 'small message';
+        $message->notification      = '0';
+
+        // Content specific to the email processor.
+        $content = array('*' => array('header' => ' test ', 'footer' => ' test '));
+        $message->set_additional_content('email', $content);
+
+        // Ensure we're going to hit the email processor for the recipient users.
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email'");
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user3);
+
+        // Now, send a message and verify the message processors (in this case, email) are hit.
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(2, $emails);
+
+        // Verify the record was created in 'messages'.
+        $recordexists = $DB->record_exists('messages', ['id' => $messageid]);
+        $this->assertTrue($recordexists);
+
+        // Verify the email information. Ordering is not guaranteed.
+        $members = [$user2->email => '', $user3->email => ''];
+        $email = $emails[0];
+        $this->assertSame($user1->email, $email->from);
+        $this->assertArrayHasKey($email->to, $members);
+        unset($members[$email->to]);
+
+        $email = $emails[1];
+        $this->assertSame($user1->email, $email->from);
+        $this->assertArrayHasKey($email->to, $members);
+        unset($members[$email->to]);
+
+        // The message subject is generated during the call for conversation messages,
+        // as the conversation may have many members having different lang preferences.
+        $tmp = (object) ['name' => fullname($user1), 'conversationname' => $conversation->name];
+        $this->assertSame(get_string('unreadnewgroupconversationmessage', 'message', $tmp), $email->subject);
+
+        // The email content will have had an emailtagline appended to it, based on lang prefs,
+        // so verify the expected beginning and ends.
+        $this->assertNotEmpty($email->header);
+        $this->assertNotEmpty($email->body);
+        $this->assertRegExp('/test message body.*test/s', $email->body);
+        $sink->clear();
+
+        // Now, send the message again, and verify that the event fired includes the courseid and conversationid.
+        $eventsink = $this->redirectEvents();
+        $messageid = message_send($message);
+        $events = $eventsink->get_events();
+        $this->assertCount(1, $events);
+        $event = reset($events);
+        $this->assertInstanceOf(\core\event\group_message_sent::class, $event);
+        $this->assertEquals($user1->id, $event->userid);
+        $this->assertNull($event->relateduserid);
+        $this->assertEquals($message->courseid, $event->other['courseid']);
+        $this->assertEquals($message->convid, $event->other['conversationid']);
+        $eventsink->clear();
+        $sink->clear();
+    }
+
+    /**
+     * Verify that sending a message to a conversation is an action which can be buffered by the manager if in a DB transaction.
+     *
+     * This should defer all processor calls (for 2 members in this case), and event creation (1 event).
+     */
+    public function test_send_message_to_conversation_group_with_buffering() {
+        global $DB, $CFG;
+        $this->preventResetByRollback();
+        $this->resetAfterTest();
+
+        $user1 = $this->getDataGenerator()->create_user(array('maildisplay' => 1));
+        $user2 = $this->getDataGenerator()->create_user();
+        $user3 = $this->getDataGenerator()->create_user();
+        set_config('allowedemaildomains', 'example.com');
+
+        // Create a conversation.
+        $conversation = \core_message\api::create_conversation(\core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [$user1->id, $user2->id, $user3->id], 'Group project discussion');
+
+        // Test basic email redirection.
+        $this->assertFileExists("$CFG->dirroot/message/output/email/version.php");
+        $this->assertFileExists("$CFG->dirroot/message/output/popup/version.php");
+
+        $DB->set_field_select('message_processors', 'enabled', 0, "name <> 'email' AND name <> 'popup'");
+        get_message_processors(true, true);
+
+        $eventsink = $this->redirectEvents();
+
+        // Will always use the pop-up processor.
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user2);
+        set_user_preference('message_provider_moodle_instantmessage_loggedoff', 'email', $user3);
+
+        $message = new \core\message\message();
+        $message->courseid          = 1;
+        $message->component         = 'moodle';
+        $message->name              = 'instantmessage';
+        $message->userfrom          = $user1;
+        $message->convid            = $conversation->id;
+        $message->subject           = 'message subject 1';
+        $message->fullmessage       = 'message body';
+        $message->fullmessageformat = FORMAT_MARKDOWN;
+        $message->fullmessagehtml   = '<p>message body</p>';
+        $message->smallmessage      = 'small message';
+        $message->notification      = '0';
+
+        $transaction = $DB->start_delegated_transaction();
+        $sink = $this->redirectEmails();
+        $messageid = message_send($message);
+        $emails = $sink->get_messages();
+        $this->assertCount(0, $emails);
+        $savedmessage = $DB->get_record('messages', array('id' => $messageid), '*', MUST_EXIST);
+        $sink->clear();
+        $this->assertFalse($DB->record_exists('message_user_actions', array()));
+        $DB->delete_records('messages', array());
+        $events = $eventsink->get_events();
+        $this->assertCount(0, $events);
+        $eventsink->clear();
+        $transaction->allow_commit();
+        $events = $eventsink->get_events();
+        $emails = $sink->get_messages();
+        $this->assertCount(2, $emails);
+        $this->assertCount(1, $events);
+        $this->assertInstanceOf('\core\event\group_message_sent', $events[0]);
+    }
+
     public function test_rollback() {
         global $DB;
 
index 43fa325..291e012 100644 (file)
@@ -151,7 +151,7 @@ class api {
     /**
      * Handles searching for user in a particular course in the message area.
      *
-     * TODO: This function should be removed once new group messaging UI is in place and old messaging UI is removed.
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
      * But we are deprecating data_for_messagearea_search_users_in_course external function.
      * Followup: MDL-63915
@@ -197,7 +197,7 @@ class api {
     /**
      * Handles searching for user in the message area.
      *
-     * TODO: This function should be removed once new group messaging UI is in place and old messaging UI is removed.
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
      * But we are deprecating data_for_messagearea_search_users external function.
      * Followup: MDL-63915
@@ -516,12 +516,10 @@ class api {
         // If we need to return ONLY favourites, or NO favourites, generate the SQL snippet.
         $favouritesql = "";
         $favouriteparams = [];
-        if (is_bool($favourites)) {
-            if (!empty($favouriteconversationids)) {
-                list ($insql, $inparams) = $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids');
-                $favouritesql = $favourites ? " AND mc.id {$insql} " : " AND mc.id NOT {$insql} ";
-                $favouriteparams = $inparams;
-            }
+        if (null !== $favourites && !empty($favouriteconversationids)) {
+            list ($insql, $favouriteparams) =
+                    $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites);
+            $favouritesql = " AND mc.id {$insql} ";
         }
 
         // If we need to restrict type, generate the SQL snippet.
@@ -561,10 +559,11 @@ class api {
         $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
 
         $conversations = [];
-        $uniquemembers = [];
         $members = [];
+        $individualmembers = [];
+        $groupmembers = [];
         foreach ($conversationset as $conversation) {
-            $conversations[] = $conversation;
+            $conversations[$conversation->id] = $conversation;
             $members[$conversation->id] = [];
         }
         $conversationset->close();
@@ -600,7 +599,7 @@ class api {
             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
                 if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) {
                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
-                    $uniquemembers[$conversation->useridfrom] = $conversation->useridfrom;
+                    $individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
                 } else {
                     $individualconversations[] = $conversation->id;
                 }
@@ -608,7 +607,7 @@ class api {
                 // If we have a recent message, the sender is our member.
                 if (!is_null($conversation->useridfrom)) {
                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
-                    $uniquemembers[$conversation->useridfrom] = $conversation->useridfrom;
+                    $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
                 }
             }
         }
@@ -626,10 +625,9 @@ class api {
 
             foreach ($conversationmembers as $mid => $member) {
                 $members[$member->conversationid][$member->userid] = $member->userid;
-                $uniquemembers[$member->userid] = $member->userid;
+                $individualmembers[$member->userid] = $member->userid;
             }
         }
-        $memberids = array_values($uniquemembers);
 
         // We could fail early here if we're sure that:
         // a) we have no otherusers for all the conversations (users may have been deleted)
@@ -639,8 +637,17 @@ class api {
         // needs to be done in a separate query to avoid doing a join on the messages tables and the user
         // tables because on large sites these tables are massive which results in extremely slow
         // performance (typically due to join buffer exhaustion).
-        if (!empty($memberids)) {
-            $memberinfo = helper::get_member_info($userid, $memberids);
+        if (!empty($individualmembers) || !empty($groupmembers)) {
+            // Now, we want to remove any duplicates from the group members array. For individual members we will
+            // be doing a more extensive call as we want their contact requests as well as privacy information,
+            // which is not necessary for group conversations.
+            $diffgroupmembers = array_diff($groupmembers, $individualmembers);
+
+            $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
+            $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
+
+            // Don't use array_merge, as we lose array keys.
+            $memberinfo = $individualmemberinfo + $groupmemberinfo;
 
             // Update the members array with the member information.
             $deletedmembers = [];
@@ -651,7 +658,15 @@ class api {
                         if ($memberinfo[$memberid]->isdeleted) {
                             $deletedmembers[$convid][] = $memberid;
                         }
-                        $members[$convid][$key] = $memberinfo[$memberid];
+
+                        $members[$convid][$key] = clone $memberinfo[$memberid];
+
+                        if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
+                            // Remove data we don't need for group.
+                            $members[$convid][$key]->requirescontact = null;
+                            $members[$convid][$key]->canmessage = null;
+                            $members[$convid][$key]->contactrequests = [];
+                        }
                     }
                 }
             }
@@ -791,6 +806,10 @@ class api {
     /**
      * Returns the contacts to display in the contacts area.
      *
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid The user id
      * @param int $limitfrom
      * @param int $limitnum
@@ -912,7 +931,10 @@ class api {
     /**
      * Returns the messages to display in the message area.
      *
-     * @deprecated since 3.6
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid the current user
      * @param int $otheruserid the other user
      * @param int $limitfrom
@@ -924,8 +946,6 @@ class api {
      */
     public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
             $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
-        debugging('\core_message\api::get_messages() is deprecated, please use ' .
-            '\core_message\api::get_conversation_messages() instead.', DEBUG_DEVELOPER);
 
         if (!empty($timefrom)) {
             // Get the conversation between userid and otheruserid.
@@ -996,15 +1016,15 @@ class api {
     /**
      * Returns the most recent message between two users.
      *
-     * @deprecated since 3.6
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid the current user
      * @param int $otheruserid the other user
      * @return \stdClass|null
      */
     public static function get_most_recent_message($userid, $otheruserid) {
-        debugging('\core_message\api::get_most_recent_message() is deprecated, please use ' .
-            '\core_message\api::get_most_recent_conversation_message() instead.', DEBUG_DEVELOPER);
-
         // We want two messages here so we get an accurate 'blocktime' value.
         if ($messages = helper::get_messages($userid, $otheruserid, 0, 0, 2, 'timecreated DESC')) {
             // Swap the order so we now have them in historical order.
@@ -1041,6 +1061,10 @@ class api {
     /**
      * Returns the profile information for a contact for a user.
      *
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid The user id
      * @param int $otheruserid The id of the user whose profile we want to view.
      * @return \stdClass
@@ -1428,7 +1452,103 @@ class api {
         }
 
         // Check if the recipient can be messaged by the sender.
-        return (self::can_contact_user($recipient, $sender));
+        return (self::can_contact_user($recipient->id, $sender->id));
+    }
+
+    /**
+     * Determines if a user is permitted to send a message to a given conversation.
+     * If no sender is provided then it defaults to the logged in user.
+     *
+     * @param int $userid the id of the user on which the checks will be applied.
+     * @param int $conversationid the id of the conversation we wish to check.
+     * @return bool true if the user can send a message to the conversation, false otherwise.
+     * @throws \moodle_exception
+     */
+    public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
+        global $DB;
+
+        $systemcontext = \context_system::instance();
+        if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
+            return false;
+        }
+
+        if (!self::is_user_in_conversation($userid, $conversationid)) {
+            return false;
+        }
+
+        // User can post messages and is in the conversation, but we need to check the conversation type to
+        // know whether or not to check the user privacy settings via can_contact_user().
+        $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
+        if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
+            return true;
+        } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
+            // Get the other user in the conversation.
+            $members = self::get_conversation_members($userid, $conversationid);
+            $otheruser = array_filter($members, function($member) use($userid) {
+                return $member->id != $userid;
+            });
+            $otheruser = reset($otheruser);
+
+            return self::can_contact_user($otheruser->id, $userid);
+        } else {
+            throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
+        }
+    }
+
+    /**
+     * Send a message from a user to a conversation.
+     *
+     * This method will create the basic eventdata and delegate to message creation to message_send.
+     * The message_send() method is responsible for event data that is specific to each recipient.
+     *
+     * @param int $userid the sender id.
+     * @param int $conversationid the conversation id.
+     * @param string $message the message to send.
+     * @param int $format the format of the message to send.
+     * @return \stdClass the message created.
+     * @throws \coding_exception
+     * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
+     */
+    public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
+                                                        int $format) : \stdClass {
+        global $DB;
+
+        if (!self::can_send_message_to_conversation($userid, $conversationid)) {
+            throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
+        }
+
+        $eventdata = new \core\message\message();
+        $eventdata->courseid         = 1;
+        $eventdata->component        = 'moodle';
+        $eventdata->name             = 'instantmessage';
+        $eventdata->userfrom         = $userid;
+        $eventdata->convid           = $conversationid;
+
+        if ($format == FORMAT_HTML) {
+            $eventdata->fullmessagehtml  = $message;
+            // Some message processors may revert to sending plain text even if html is supplied,
+            // so we keep both plain and html versions if we're intending to send html.
+            $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
+        } else {
+            $eventdata->fullmessage      = $message;
+            $eventdata->fullmessagehtml  = '';
+        }
+
+        $eventdata->fullmessageformat = $format;
+        $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
+
+        $eventdata->timecreated     = time();
+        $eventdata->notification    = 0;
+        $messageid = message_send($eventdata);
+
+        $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage, timecreated');
+        $message = (object) [
+            'id' => $messagerecord->id,
+            'useridfrom' => $messagerecord->useridfrom,
+            'text' => $messagerecord->fullmessage,
+            'timecreated' => $messagerecord->timecreated
+        ];
+        return $message;
     }
 
     /**
@@ -1862,6 +1982,15 @@ class api {
 
         global $DB;
 
+        $validtypes = [
+            self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            self::MESSAGE_CONVERSATION_TYPE_GROUP
+        ];
+
+        if (!in_array($type, $validtypes)) {
+            throw new \moodle_exception('An invalid conversation type was specified.');
+        }
+
         // Sanity check.
         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
             if (count($userids) > 2) {
@@ -2029,24 +2158,26 @@ class api {
      * It will not include blocked users.
      *
      * @param int $userid
+     * @param int $limitfrom
+     * @param int $limitnum
      * @return array The list of contact requests
      */
-    public static function get_contact_requests(int $userid) : array {
+    public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
         global $DB;
 
-        $ufields = \user_picture::fields('u');
-        $sql = "SELECT $ufields, mcr.id as contactrequestid
-                  FROM {user} u
-                  JOIN {message_contact_requests} mcr
-                    ON u.id = mcr.userid
+        $sql = "SELECT mcr.userid
+                  FROM {message_contact_requests} mcr
              LEFT JOIN {message_users_blocked} mub
-                    ON (mub.userid = ? AND mub.blockeduserid = u.id)
+                    ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
                  WHERE mcr.requesteduserid = ?
-                   AND u.deleted = 0
                    AND mub.id is NULL
-              ORDER BY mcr.timecreated DESC";
+              ORDER BY mcr.timecreated ASC";
+        if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
+            $userids = array_keys($contactrequests);
+            return helper::get_member_info($userid, $userids);
+        }
 
-        return $DB->get_records_sql($sql, [$userid, $userid]);
+        return [];
     }
 
     /**
@@ -2230,12 +2361,12 @@ class api {
     /**
      * Checks if the sender can message the recipient.
      *
-     * @param \stdClass $recipient The user object.
-     * @param \stdClass $sender The user object.
+     * @param int $recipientid
+     * @param int $senderid
      * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
      */
-    protected static function can_contact_user(\stdClass $recipient, \stdClass $sender) : bool {
-        if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $sender->id)) {
+    protected static function can_contact_user(int $recipientid, int $senderid) : bool {
+        if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid)) {
             // The sender has the ability to contact any user across the entire site.
             return true;
         }
@@ -2243,7 +2374,7 @@ class api {
         // The initial value of $cancontact is null to indicate that a value has not been determined.
         $cancontact = null;
 
-        if (self::is_blocked($recipient->id, $sender->id)) {
+        if (self::is_blocked($recipientid, $senderid)) {
             // The recipient has specifically blocked this sender.
             $cancontact = false;
         }
@@ -2257,7 +2388,7 @@ class api {
             //
             // The Site option is only possible when the messagingallusers site setting is also enabled.
 
-            $privacypreference = self::get_user_privacy_messaging_preference($recipient->id);
+            $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
             if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
                 // The user preference is to allow any user to contact them.
                 // No need to check anything else.
@@ -2265,12 +2396,12 @@ class api {
             } else {
                 // This user only allows their own contacts, and possibly course peers, to contact them.
                 // If the users are contacts then we can avoid the more expensive shared courses check.
-                $cancontact = self::is_contact($sender->id, $recipient->id);
+                $cancontact = self::is_contact($senderid, $recipientid);
 
                 if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
                     // The users are not contacts and the user allows course member messaging.
                     // Check whether these two users share any course together.
-                    $sharedcourses = enrol_get_shared_courses($recipient->id, $sender->id, true);
+                    $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
                     $cancontact = (!empty($sharedcourses));
                 }
             }
@@ -2283,12 +2414,12 @@ class api {
 
             // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
             if (null === $sharedcourses) {
-                $sharedcourses = enrol_get_shared_courses($recipient->id, $sender->id, true);
+                $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
             }
 
             foreach ($sharedcourses as $course) {
                 // Note: enrol_get_shared_courses will preload any shared context.
-                if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $sender->id)) {
+                if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
                     $cancontact = true;
                     break;
                 }
@@ -2490,25 +2621,7 @@ class api {
         if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
                 'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
             $userids = array_keys($members);
-            $members = helper::get_member_info($userid, $userids);
-
-            // Check if we want to include contact requests as well.
-            if ($includecontactrequests) {
-                list($useridsql, $usersparams) = $DB->get_in_or_equal($userids);
-
-                $wheresql = "(userid $useridsql OR requesteduserid $useridsql)";
-                if ($contactrequests = $DB->get_records_select('message_contact_requests', $wheresql,
-                        array_merge($usersparams, $usersparams), 'timecreated ASC, id ASC')) {
-                    foreach ($contactrequests as $contactrequest) {
-                        if (isset($members[$contactrequest->userid])) {
-                            $members[$contactrequest->userid]->contactrequests[] = $contactrequest;
-                        }
-                        if (isset($members[$contactrequest->requesteduserid])) {
-                            $members[$contactrequest->requesteduserid]->contactrequests[] = $contactrequest;
-                        }
-                    }
-                }
-            }
+            $members = helper::get_member_info($userid, $userids, $includecontactrequests);
 
             return $members;
         }
index 7322684..b83cd8e 100644 (file)
@@ -37,7 +37,10 @@ class helper {
     /**
      * Helper function to retrieve the messages between two users
      *
-     * @deprecated since 3.6
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid the current user
      * @param int $otheruserid the other user
      * @param int $timedeleted the time the message was deleted
@@ -50,9 +53,6 @@ class helper {
      */
     public static function get_messages($userid, $otheruserid, $timedeleted = 0, $limitfrom = 0, $limitnum = 0,
                                         $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
-        debugging('\core_message\helper::get_messages() is deprecated, please use ' .
-            '\core_message\helper::get_conversation_messages() instead.', DEBUG_DEVELOPER);
-
         global $DB;
 
         $hash = self::get_conversation_hash([$userid, $otheruserid]);
@@ -229,15 +229,15 @@ class helper {
     /**
      * Helper function to return an array of messages.
      *
-     * @deprecated since 3.6
+     * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
+     * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
+     * Followup: MDL-63915
+     *
      * @param int $userid
      * @param array $messages
      * @return array
      */
     public static function create_messages($userid, $messages) {
-        debugging('\core_message\helper::create_messages() is deprecated, please use ' .
-            '\core_message\helper::create_conversation_messages() instead.', DEBUG_DEVELOPER);
-
         // Store the messages.
         $arrmessages = array();
 
@@ -314,7 +314,8 @@ class helper {
             }
         }
         $data->isonline = null;
-        if (self::show_online_status($userfields)) {
+        $user = \core_user::get_user($data->userid);
+        if (self::show_online_status($user)) {
             $data->isonline = self::is_online($userfields->lastaccess);
         }
         $data->isblocked = isset($contact->blocked) ? (bool) $contact->blocked : false;
@@ -482,13 +483,22 @@ class helper {
      *
      * @param int $referenceuserid the id of the user which check contact and blocked status.
      * @param array $userids
+     * @param bool $includecontactrequests Do we want to include contact requests with this data?
+     * @param bool $includeprivacyinfo Do we want to include whether the user can message another, and if the user
+     *             requires a contact.
      * @return array the array of objects containing member info, indexed by userid.
      * @throws \coding_exception
      * @throws \dml_exception
      */
-    public static function get_member_info(int $referenceuserid, array $userids) : array {
+    public static function get_member_info(int $referenceuserid, array $userids, bool $includecontactrequests = false,
+                                           bool $includeprivacyinfo = false) : array {
         global $DB, $PAGE;
 
+        // Prevent exception being thrown when array is empty.
+        if (empty($userids)) {
+            return [];
+        }
+
         list($useridsql, $usersparams) = $DB->get_in_or_equal($userids);
         $userfields = \user_picture::fields('u', array('lastaccess'));
         $userssql = "SELECT $userfields, u.deleted, mc.id AS contactid, mub.id AS blockedid
@@ -525,8 +535,46 @@ class helper {
 
             $data->isdeleted = ($member->deleted) ? true : false;
 
+            $data->requirescontact = null;
+            $data->canmessage = null;
+            if ($includeprivacyinfo) {
+                $privacysetting = api::get_user_privacy_messaging_preference($member->id);
+                $data->requirescontact = $privacysetting == api::MESSAGE_PRIVACY_ONLYCONTACTS;
+
+                $recipient = new \stdClass();
+                $recipient->id = $member->id;
+
+                $sender = new \stdClass();
+                $sender->id = $referenceuserid;
+
+                $data->canmessage = api::can_post_message($recipient, $sender);
+            }
+
+            // Populate the contact requests, even if we don't need them.
+            $data->contactrequests = [];
+
             $members[$data->id] = $data;
         }
+
+        // Check if we want to include contact requests as well.
+        if (!empty($members) && $includecontactrequests) {
+            list($useridsql, $usersparams) = $DB->get_in_or_equal($userids);
+
+            $wheresql = "(userid $useridsql AND requesteduserid = ?) OR (userid = ? AND requesteduserid $useridsql)";
+            $params = array_merge($usersparams, [$referenceuserid, $referenceuserid], $usersparams);
+            if ($contactrequests = $DB->get_records_select('message_contact_requests', $wheresql, $params,
+                    'timecreated ASC, id ASC')) {
+                foreach ($contactrequests as $contactrequest) {
+                    if (isset($members[$contactrequest->userid])) {
+                        $members[$contactrequest->userid]->contactrequests[] = $contactrequest;
+                    }
+                    if (isset($members[$contactrequest->requesteduserid])) {
+                        $members[$contactrequest->requesteduserid]->contactrequests[] = $contactrequest;
+                    }
+                }
+            }
+        }
+
         return $members;
     }
 
index af3740a..8cc5d80 100644 (file)
@@ -39,6 +39,76 @@ require_once($CFG->dirroot . "/message/lib.php");
  * @since Moodle 2.2
  */
 class core_message_external extends external_api {
+    /**
+     * Returns description of method parameters
+     *
+     * @return external_function_parameters
+     * @since Moodle 3.6
+     */
+    public static function send_messages_to_conversation_parameters() {
+        return new external_function_parameters(
+            array(
+                'conversationid' => new external_value(PARAM_INT, 'id of the conversation'),
+                'messages' => new external_multiple_structure(
+                    new external_single_structure(
+                        array(
+                            'text' => new external_value(PARAM_RAW, 'the text of the message'),
+                            'textformat' => new external_format_value('text', VALUE_DEFAULT, FORMAT_MOODLE),
+                        )
+                    )
+                )
+            )
+        );
+    }
+
+    /**
+     * Send messages from the current USER to a conversation.
+     *
+     * This conversation may be any type of conversation, individual or group.
+     *
+     * @param int $conversationid the id of the conversation to which the messages will be sent.
+     * @param array $messages An array of message to send.
+     * @return array the array of messages which were sent (created).
+     * @since Moodle 3.6
+     */
+    public static function send_messages_to_conversation(int $conversationid, array $messages = []) {
+        global $CFG, $USER;
+
+        // Check if messaging is enabled.
+        if (empty($CFG->messaging)) {
+            throw new moodle_exception('disabled', 'message');
+        }
+
+        // Ensure the current user is allowed to run this function.
+        $context = context_system::instance();
+        self::validate_context($context);
+
+        $params = self::validate_parameters(self::send_messages_to_conversation_parameters(), [
+            'conversationid' => $conversationid,
+            'messages' => $messages
+        ]);
+
+        $messages = [];
+        foreach ($params['messages'] as $message) {
+            $messages[] = \core_message\api::send_message_to_conversation($USER->id, $params['conversationid'], $message['text'],
+                $message['textformat']);
+        }
+
+        return $messages;
+    }
+
+    /**
+     * Returns description of method result value.
+     *
+     * @return external_description
+     * @since Moodle 3.6
+     */
+    public static function send_messages_to_conversation_returns() {
+        return new external_multiple_structure(
+            self::get_conversation_message_structure()
+        );
+    }
+
 
     /**
      * Returns description of method parameters
@@ -599,7 +669,9 @@ class core_message_external extends external_api {
     public static function get_contact_requests_parameters() {
         return new external_function_parameters(
             [
-                'userid' => new external_value(PARAM_INT, 'The id of the user we want the requests for')
+                'userid' => new external_value(PARAM_INT, 'The id of the user we want the requests for'),
+                'limitfrom' => new external_value(PARAM_INT, 'Limit from', VALUE_DEFAULT, 0),
+                'limitnum' => new external_value(PARAM_INT, 'Limit number', VALUE_DEFAULT, 0)
             ]
         );
     }
@@ -613,8 +685,10 @@ class core_message_external extends external_api {
      * It will not include blocked users.
      *
      * @param int $userid The id of the user we want to get the contact requests for
+     * @param int $limitfrom
+     * @param int $limitnum
      */
-    public static function get_contact_requests(int $userid) {
+    public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) {
         global $CFG, $USER;
 
         // Check if messaging is enabled.
@@ -631,10 +705,14 @@ class core_message_external extends external_api {
             throw new required_capability_exception($context, $capability, 'nopermissions', '');
         }
 
-        $params = ['userid' => $userid];
+        $params = [
+            'userid' => $userid,
+            'limitfrom' => $limitfrom,
+            'limitnum' => $limitnum
+        ];
         $params = self::validate_parameters(self::get_contact_requests_parameters(), $params);
 
-        return \core_message\api::get_contact_requests($params['userid']);
+        return \core_message\api::get_contact_requests($params['userid'], $params['limitfrom'], $params['limitnum']);
     }
 
     /**
@@ -644,26 +722,7 @@ class core_message_external extends external_api {
      */
     public static function get_contact_requests_returns() {
         return new external_multiple_structure(
-            new external_single_structure(
-                [
-                    'id' => new external_value(core_user::get_property_type('id'), 'ID of the user'),
-                    'contactrequestid' => new external_value(PARAM_INT, 'The ID of the contact request'),
-                    'picture' => new external_value(core_user::get_property_type('picture'), 'The picture'),
-                    'firstname' => new external_value(core_user::get_property_type('firstname'),
-                        'The first name(s) of the user'),
-                    'lastname' => new external_value(core_user::get_property_type('lastname'),
-                        'The family name of the user'),
-                    'firstnamephonetic' => new external_value(core_user::get_property_type('firstnamephonetic'),
-                        'The phonetic first name of the user'),
-                    'lastnamephonetic' => new external_value(core_user::get_property_type('lastnamephonetic'),
-                        'The phonetic last name of the user'),
-                    'middlename' => new external_value(core_user::get_property_type('middlename'),
-                        'The middle name of the user'),
-                    'alternatename' => new external_value(core_user::get_property_type('alternatename'),
-                        'The alternate name of the user'),
-                    'email' => new external_value(core_user::get_property_type('email'), 'An email address')
-                ]
-            )
+            self::get_conversation_member_structure()
         );
     }
 
@@ -964,7 +1023,7 @@ class core_message_external extends external_api {
                 'unreadcount' => new external_value(PARAM_INT, 'The number of unread messages in this conversation',
                     VALUE_DEFAULT, null),
                 'members' => new external_multiple_structure(
-                    self::get_conversation_member_structure()
+                    self::get_conversation_member_structure(true)
                 ),
                 'messages' => new external_multiple_structure(
                     self::get_conversation_message_structure()
@@ -992,6 +1051,9 @@ class core_message_external extends external_api {
             'showonlinestatus' => new external_value(PARAM_BOOL, 'Show the user\'s online status?'),
             'isblocked' => new external_value(PARAM_BOOL, 'If the user has been blocked'),
             'iscontact' => new external_value(PARAM_BOOL, 'Is the user a contact?'),
+            'isdeleted' => new external_value(PARAM_BOOL, 'Is the user deleted?'),
+            'canmessage' => new external_value(PARAM_BOOL, 'If the user can be messaged'),
+            'requirescontact' => new external_value(PARAM_BOOL, 'If the user requires to be contacts'),
         ];
 
         if ($includecontactrequests) {
@@ -1590,6 +1652,7 @@ class core_message_external extends external_api {
     /**
      * The messagearea contacts return parameters.
      *
+     * @deprecated since 3.6
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1600,6 +1663,7 @@ class core_message_external extends external_api {
     /**
      * Get messagearea contacts parameters.
      *
+     * @deprecated since 3.6
      * @param int $userid The id of the user who we are viewing conversations for
      * @param int $limitfrom
      * @param int $limitnum
@@ -1639,6 +1703,7 @@ class core_message_external extends external_api {
     /**
      * The messagearea contacts return structure.
      *
+     * @deprecated since 3.6
      * @return external_single_structure
      * @since 3.2
      */
@@ -1646,9 +1711,19 @@ class core_message_external extends external_api {
         return self::data_for_messagearea_conversations_returns();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_contacts_is_deprecated() {
+        return true;
+    }
+
     /**
      * The messagearea messages parameters.
      *
+     * @deprecated since 3.6
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1669,6 +1744,7 @@ class core_message_external extends external_api {
     /**
      * Get messagearea messages.
      *
+     * @deprecated since 3.6
      * @param int $currentuserid The current user's id
      * @param int $otheruserid The other user's id
      * @param int $limitfrom
@@ -1741,6 +1817,7 @@ class core_message_external extends external_api {
     /**
      * The messagearea messages return structure.
      *
+     * @deprecated since 3.6
      * @return external_single_structure
      * @since 3.2
      */
@@ -1762,6 +1839,15 @@ class core_message_external extends external_api {
         );
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_messages_is_deprecated() {
+        return true;
+    }
+
     /**
      * The conversation messages parameters.
      *
@@ -1867,6 +1953,7 @@ class core_message_external extends external_api {
     /**
      * The get most recent message return parameters.
      *
+     * @deprecated since 3.6
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1882,6 +1969,7 @@ class core_message_external extends external_api {
     /**
      * Get the most recent message in a conversation.
      *
+     * @deprecated since 3.6
      * @param int $currentuserid The current user's id
      * @param int $otheruserid The other user's id
      * @return stdClass
@@ -1919,6 +2007,7 @@ class core_message_external extends external_api {
     /**
      * The get most recent message return structure.
      *
+     * @deprecated since 3.6
      * @return external_single_structure
      * @since 3.2
      */
@@ -1926,9 +2015,19 @@ class core_message_external extends external_api {
         return self::get_messagearea_message_structure();
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_get_most_recent_message_is_deprecated() {
+        return true;
+    }
+
     /**
      * The get profile parameters.
      *
+     * @deprecated since 3.6
      * @return external_function_parameters
      * @since 3.2
      */
@@ -1944,6 +2043,7 @@ class core_message_external extends external_api {
     /**
      * Get the profile information for a contact.
      *
+     * @deprecated since 3.6
      * @param int $currentuserid The current user's id
      * @param int $otheruserid The id of the user whose profile we are viewing
      * @return stdClass
@@ -1981,6 +2081,7 @@ class core_message_external extends external_api {
     /**
      * The get profile return structure.
      *
+     * @deprecated since 3.6
      * @return external_single_structure
      * @since 3.2
      */
@@ -2002,6 +2103,15 @@ class core_message_external extends external_api {
         );
     }
 
+    /**
+     * Marking the method as deprecated.
+     *
+     * @return bool
+     */
+    public static function data_for_messagearea_get_profile_is_deprecated() {
+        return true;
+    }
+
     /**
      * Get contacts parameters description.
      *
index cab15e9..f408ec2 100644 (file)
@@ -37,16 +37,16 @@ if (empty($CFG->messaging)) {
     print_error('disabled', 'message');
 }
 
-$id = optional_param('id', '', PARAM_INT); // The id of the request.
+$userid = optional_param('userid', '', PARAM_INT); // The userid of the request.
 $action = optional_param('action', '', PARAM_ALPHA);
 
 // Confirm the request is able to be approved/disapproved.
-if ($id) {
-    $request = $DB->get_record('message_contact_requests', ['id' => $id, 'requesteduserid' => $USER->id], '*', MUST_EXIST);
+if ($userid) {
+    $request = $DB->get_record('message_contact_requests', ['userid' => $userid, 'requesteduserid' => $USER->id], '*', MUST_EXIST);
 }
 
 // Use external functions as these are what we will be using in the new UI.
-if ($id && $action && confirm_sesskey()) {
+if ($userid && $action && confirm_sesskey()) {
     if ($action == 'approve') {
         core_message_external::confirm_contact_request($request->userid, $USER->id);
     } else if ($action == 'decline') {
@@ -67,13 +67,13 @@ $table->head = $headers;
 // Use external functions as these are what we will be using in the new UI.
 if ($contactrequests = core_message_external::get_contact_requests($USER->id)) {
     foreach ($contactrequests as $contactrequest) {
-        $approvelink = new moodle_url('/message/pendingcontactrequests.php', ['id' => $contactrequest->contactrequestid,
+        $approvelink = new moodle_url('/message/pendingcontactrequests.php', ['userid' => $contactrequest->id,
             'action' => 'approve', 'sesskey' => sesskey()]);
-        $declinelink = new moodle_url('/message/pendingcontactrequests.php', ['id' => $contactrequest->contactrequestid,
+        $declinelink = new moodle_url('/message/pendingcontactrequests.php', ['userid' => $contactrequest->id,
             'action' => 'decline', 'sesskey' => sesskey()]);
 
         $cells = array();
-        $cells[] = fullname($contactrequest);
+        $cells[] = $contactrequest->fullname;
         $cells[] = html_writer::link($approvelink, get_string('approve')) . " | " .
             html_writer::link($declinelink, get_string('cancel'));
         $table->data[] = new html_table_row($cells);
index dceda6f..145ec68 100644 (file)
@@ -1089,6 +1089,10 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
                 $this->assertObjectHasAttribute('showonlinestatus', $member);
                 $this->assertObjectHasAttribute('isblocked', $member);
                 $this->assertObjectHasAttribute('iscontact', $member);
+                $this->assertObjectHasAttribute('isdeleted', $member);
+                $this->assertObjectHasAttribute('canmessage', $member);
+                $this->assertObjectHasAttribute('requirescontact', $member);
+                $this->assertObjectHasAttribute('contactrequests', $member);
             }
             $this->assertObjectHasAttribute('messages', $conv);
             foreach ($conv->messages as $message) {
@@ -1271,6 +1275,51 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertNotContains($ic1->id, array_column($conversations, 'id'));
     }
 
+    /**
+     * Test verifying the behaviour of get_conversations() when fetching favourite conversations with only a single
+     * favourite.
+     */
+    public function test_get_conversations_favourite_conversations_single() {
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // Mark a single conversation as favourites.
+        \core_message\api::set_favourite_conversation($ic2->id, $user1->id);
+
+        // Get the conversation, first with no restrictions, confirming the favourite status of the conversations.
+        $conversations = \core_message\api::get_conversations($user1->id);
+        $this->assertCount(6, $conversations);
+        foreach ($conversations as $conv) {
+            if (in_array($conv->id, [$ic2->id])) {
+                $this->assertTrue($conv->isfavourite);
+            } else {
+                $this->assertFalse($conv->isfavourite);
+            }
+        }
+
+        // Now, get ONLY favourite conversations.
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20, null, true);
+        $this->assertCount(1, $conversations);
+        foreach ($conversations as $conv) {
+            $this->assertTrue($conv->isfavourite);
+            $this->assertEquals($ic2->id, $conv->id);
+        }
+
+        // Now, try ONLY favourites of type 'group'.
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20,
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP, true);
+        $this->assertEmpty($conversations);
+
+        // And NO favourite conversations.
+        $conversations = \core_message\api::get_conversations($user1->id, 0, 20, null, false);
+        $this->assertCount(5, $conversations);
+        foreach ($conversations as $conv) {
+            $this->assertFalse($conv->isfavourite);
+            $this->assertNotEquals($ic2, $conv->id);
+        }
+    }
+
     /**
      * Test verifying the behaviour of get_conversations() when fetching favourite conversations.
      */
@@ -1289,13 +1338,16 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         \core_message\api::set_favourite_conversation($ic1->id, $user1->id);
         \core_message\api::set_favourite_conversation($gc2->id, $user1->id);
         \core_message\api::set_favourite_conversation($gc5->id, $user1->id);
+        $favouriteids = [$ic1->id, $gc2->id, $gc5->id];
 
         // Get the conversations, first with no restrictions, confirming the favourite status of the conversations.
         $conversations = \core_message\api::get_conversations($user1->id);
         $this->assertCount(6, $conversations);
         foreach ($conversations as $conv) {
-            if (in_array($conv->id, [$ic1->id, $gc2->id, $gc5->id])) {
+            if (in_array($conv->id, $favouriteids)) {
                 $this->assertTrue($conv->isfavourite);
+            } else {
+                $this->assertFalse($conv->isfavourite);
             }
         }
 
@@ -1304,6 +1356,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(3, $conversations);
         foreach ($conversations as $conv) {
             $this->assertTrue($conv->isfavourite);
+            $this->assertNotFalse(array_search($conv->id, $favouriteids));
         }
 
         // Now, try ONLY favourites of type 'group'.
@@ -1312,6 +1365,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(2, $conversations);
         foreach ($conversations as $conv) {
             $this->assertTrue($conv->isfavourite);
+            $this->assertNotFalse(array_search($conv->id, [$gc2->id, $gc5->id]));
         }
 
         // And NO favourite conversations.
@@ -1319,9 +1373,69 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertCount(3, $conversations);
         foreach ($conversations as $conv) {
             $this->assertFalse($conv->isfavourite);
+            $this->assertFalse(array_search($conv->id, $favouriteids));
         }
     }
 
+    /**
+     * Test verifying get_conversations when there are users in a group and/or individual conversation. The reason this
+     * test is performed is because we do not need as much data for group conversations (saving DB calls), so we want
+     * to confirm this happens.
+     */
+    public function test_get_conversations_user_in_group_and_individual_chat() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ],
+            'Individual conversation'
+        );
+
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id);
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+            ],
+            'Group conversation'
+        );
+
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id);
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+        \core_message\api::create_contact_request($user1->id, $user3->id);
+
+        $conversations = \core_message\api::get_conversations($user2->id);
+
+        $groupconversation = array_shift($conversations);
+        $individualconversation = array_shift($conversations);
+
+        $this->assertEquals('Group conversation', $groupconversation->name);
+        $this->assertEquals('Individual conversation', $individualconversation->name);
+
+        $this->assertCount(1, $groupconversation->members);
+        $this->assertCount(1, $individualconversation->members);
+
+        $groupmember = reset($groupconversation->members);
+        $this->assertNull($groupmember->requirescontact);
+        $this->assertNull($groupmember->canmessage);
+        $this->assertEmpty($groupmember->contactrequests);
+
+        $individualmember = reset($individualconversation->members);
+        $this->assertNotNull($individualmember->requirescontact);
+        $this->assertNotNull($individualmember->canmessage);
+        $this->assertNotEmpty($individualmember->contactrequests);
+    }
+
     /**
      * Test verifying that group linked conversations are returned and contain a subname matching the course name.
      */
@@ -1836,7 +1950,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages.
         $messages = \core_message\api::get_messages($user1->id, $user2->id);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(4, count($messages));
@@ -2359,7 +2472,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the most recent messages.
         $message = \core_message\api::get_most_recent_message($user1->id, $user2->id);
-        $this->assertDebuggingCalledCount(3);
 
         // Check the results are correct.
         $this->assertEquals($user2->id, $message->useridfrom);
@@ -2877,6 +2989,380 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertTrue(\core_message\api::can_post_message($student2, $teacher1));
     }
 
+    /**
+     * Verify the expected behaviour of the can_send_message_to_conversation() method for authenticated users with default settings.
+     */
+    public function test_can_send_message_to_conversation_basic() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // For group conversations, there are no user privacy checks, so only membership in the conversation is needed.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // For individual conversations, the default privacy setting of 'only contacts and course members' applies.
+        // Users are not in the same course, nor are they contacts, so messages cannot be sent.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+
+        // After enrolling users to the course, they should be able to message them with the default privacy setting.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+    }
+
+    /**
+     * Verify the behaviour of can_send_message_to_conversation() for authenticated users without the sendmessage capability.
+     */
+    public function test_can_send_message_to_conversation_sendmessage_cap() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Default settings - user1 can send a message to both conversations.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Remove the capability to send a message.
+        $roleids = $DB->get_records_menu('role', null, '', 'shortname, id');
+        unassign_capability('moodle/site:sendmessage', $roleids['user'], context_system::instance());
+
+        // Verify that a user cannot send a message to either an individual or a group conversation.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+    }
+
+    /**
+     * Verify the behaviour of can_send_message_to_conversation() for authenticated users without the messageanyuser capability.
+     */
+    public function test_can_send_message_to_conversation_messageanyuser_cap() {
+        global $DB;
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Update the message preference for user2, so they can only be messaged by contacts.
+        set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS, $user2->id);
+
+        // Verify that the user cannot be contacted in the individual conversation and that groups are unaffected.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Assign the 'messageanyuser' capability to user1 at system context.
+        $systemcontext = context_system::instance();
+        $authenticateduser = $DB->get_record('role', ['shortname' => 'user']);
+        assign_capability('moodle/site:messageanyuser', CAP_ALLOW, $authenticateduser->id, $systemcontext->id);
+
+        // Check that user1 can now message user2 due to the capability, and that group conversations is again unaffected.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+    }
+
+    /**
+     * Test verifying that users cannot send messages to conversations they are not a part of.
+     */
+    public function test_can_post_message_to_conversation_non_member() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+        $user4 = self::getDataGenerator()->create_user();
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course->id);
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Verify, non members cannot send a message.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user4->id, $gc1->id));
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user4->id, $ic1->id));
+    }
+
+    /**
+     * Test verifying the behaviour of the can_send_message_to_conversation method when privacy is set to contacts only.
+     */
+    public function test_can_send_message_to_conversation_privacy_contacts_only() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Set the message privacy preference to 'contacts only' for user 2.
+        set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_ONLYCONTACTS, $user2->id);
+
+        // Verify that user1 cannot send a message to the individual conversation, but that the group conversation is unaffected.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Now, simulate a contact request (and approval) between user1 and user2.
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+        \core_message\api::confirm_contact_request($user1->id, $user2->id);
+
+        // Verify user1 can now message user2 again via their individual conversation.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+    }
+
+    /**
+     * Test verifying the behaviour of the can_send_message_to_conversation method when privacy is set to contacts / course members.
+     */
+    public function test_can_send_message_to_conversation_privacy_contacts_course() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Set the message privacy preference to 'contacts + course members' for user 2.
+        set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_COURSEMEMBER, $user2->id);
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Verify that users in a group conversation can message one another (i.e. privacy controls ignored).
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Verify that user1 can not message user2 unless they are either contacts, or share a course.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+
+        // Verify that user1 can send a message to user2, based on the shared course, without being a contact.
+        $this->assertFalse(\core_message\api::is_contact($user1->id, $user2->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+    }
+
+    /**
+     * Test verifying the behaviour of the can_send_message_to_conversation method when privacy is set to any user.
+     */
+    public function test_can_send_message_to_conversation_privacy_sitewide() {
+        // Create some users.
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // By default, the messaging privacy dictates that users can only be contacted by contacts, and members of their courses.
+        // Verify also, that groups are not restricted in this way.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Enable site-wide messagging privacy setting.
+        // This enables a privacy option for users, allowing them to choose to be contactable by anybody on the site.
+        set_config('messagingallusers', true);
+
+        // Set the second user's preference to receive messages from everybody.
+        set_user_preference('message_blocknoncontacts', \core_message\api::MESSAGE_PRIVACY_SITE, $user2->id);
+
+        // Check that user1 can send user2 a message, and that the group conversation is unaffected.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+
+        // Disable site-wide messagging privacy setting. The user will be able to receive messages from contacts
+        // and members sharing a course with her.
+        set_config('messagingallusers', false);
+
+        // As site-wide messaging setting is disabled, the value for user2 will be changed to MESSAGE_PRIVACY_COURSEMEMBER.
+        // Verify also that the group conversation is unaffected.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user1->id, $ic1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+    }
+
+    /**
+     * Test verifying the behaviour of the can_send_message_to_conversation method when a user is blocked.
+     */
+    public function test_can_send_message_to_conversation_when_blocked() {
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        // Create an individual conversation between user1 and user2.
+        $ic1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ]
+        );
+
+        // Create a group conversation between and users 1, 2 and 3.
+        $gc1 = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+                $user3->id
+            ]
+        );
+
+        // Enrol the users into the same course.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+
+        // Block the second user.
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        // Check that user2 can not send user1 a message in their individual conversation.
+        $this->assertFalse(\core_message\api::can_send_message_to_conversation($user2->id, $ic1->id));
+
+        // Verify that group conversations are unaffected.
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user1->id, $gc1->id));
+        $this->assertTrue(\core_message\api::can_send_message_to_conversation($user2->id, $gc1->id));
+    }
+
     /**
      * Tests get_user_privacy_messaging_preference method.
      */
@@ -3127,7 +3613,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages from $time, which should be all of them.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(4, count($messages));
@@ -3144,7 +3629,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages from $time + 3, which should only be the 2 last messages.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 3);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(2, count($messages));
@@ -3176,7 +3660,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages up until $time + 4, which should be all of them.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 4);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(4, count($messages));
@@ -3193,7 +3676,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages up until $time + 2, which should be the first two.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', 0, $time + 2);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(2, count($messages));
@@ -3225,7 +3707,6 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
 
         // Retrieve the messages from $time + 2 up until $time + 3, which should be 2nd and 3rd message.
         $messages = \core_message\api::get_messages($user1->id, $user2->id, 0, 0, 'timecreated ASC', $time + 2, $time + 3);
-        $this->assertDebuggingCalledCount(3);
 
         // Confirm the message data is correct.
         $this->assertEquals(2, count($messages));
@@ -4000,14 +4481,44 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $request = reset($requests);
 
         $this->assertEquals($user2->id, $request->id);
-        $this->assertEquals($user2->picture, $request->picture);
-        $this->assertEquals($user2->firstname, $request->firstname);
-        $this->assertEquals($user2->lastname, $request->lastname);
-        $this->assertEquals($user2->firstnamephonetic, $request->firstnamephonetic);
-        $this->assertEquals($user2->lastnamephonetic, $request->lastnamephonetic);
-        $this->assertEquals($user2->middlename, $request->middlename);
-        $this->assertEquals($user2->alternatename, $request->alternatename);
-        $this->assertEquals($user2->email, $request->email);
+        $this->assertEquals(fullname($user2), $request->fullname);
+        $this->assertObjectHasAttribute('profileimageurl', $request);
+        $this->assertObjectHasAttribute('profileimageurlsmall', $request);
+        $this->assertObjectHasAttribute('isonline', $request);
+        $this->assertObjectHasAttribute('showonlinestatus', $request);
+        $this->assertObjectHasAttribute('isblocked', $request);
+        $this->assertObjectHasAttribute('iscontact', $request);
+    }
+
+    /**
+     * Test getting contact requests when there are none.
+     */
+    public function test_get_contact_requests_no_requests() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+
+        $requests = \core_message\api::get_contact_requests($user1->id);
+
+        $this->assertEmpty($requests);
+    }
+
+    /**
+     * Test getting contact requests with limits.
+     */
+    public function test_get_contact_requests_with_limits() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+        \core_message\api::create_contact_request($user3->id, $user1->id);
+
+        $requests = \core_message\api::get_contact_requests($user1->id, 0, 1);
+
+        $this->assertCount(1, $requests);
     }
 
     /**
@@ -4282,6 +4793,14 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($conversation->id, $member3->conversationid);
     }
 
+    /**
+     * Test creating an invalid conversation.
+     */
+    public function test_create_conversation_invalid() {
+        $this->expectException('moodle_exception');
+        \core_message\api::create_conversation(3, [1, 2, 3]);
+    }
+
     /**
      * Test creating an individual conversation with too many members.
      */
@@ -4450,7 +4969,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member1->showonlinestatus);
         $this->assertEquals(false, $member1->iscontact);
         $this->assertEquals(false, $member1->isblocked);
-        $this->assertObjectNotHasAttribute('contactrequests', $member1);
+        $this->assertObjectHasAttribute('contactrequests', $member1);
+        $this->assertEmpty($member1->contactrequests);
 
         $this->assertEquals($user2->id, $member2->id);
         $this->assertEquals(fullname($user2), $member2->fullname);
@@ -4458,7 +4978,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member2->showonlinestatus);
         $this->assertEquals(true, $member2->iscontact);
         $this->assertEquals(false, $member2->isblocked);
-        $this->assertObjectNotHasAttribute('contactrequests', $member2);
+        $this->assertObjectHasAttribute('contactrequests', $member2);
+        $this->assertEmpty($member2->contactrequests);
 
         $this->assertEquals($user3->id, $member3->id);
         $this->assertEquals(fullname($user3), $member3->fullname);
@@ -4466,7 +4987,8 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member3->showonlinestatus);
         $this->assertEquals(false, $member3->iscontact);
         $this->assertEquals(true, $member3->isblocked);
-        $this->assertObjectNotHasAttribute('contactrequests', $member3);
+        $this->assertObjectHasAttribute('contactrequests', $member3);
+        $this->assertEmpty($member3->contactrequests);
     }
 
     /**
@@ -4521,7 +5043,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member1->showonlinestatus);
         $this->assertEquals(false, $member1->iscontact);
         $this->assertEquals(false, $member1->isblocked);
-        $this->assertCount(3, $member1->contactrequests);
+        $this->assertCount(2, $member1->contactrequests);
 
         $this->assertEquals($user2->id, $member2->id);
         $this->assertEquals(fullname($user2), $member2->fullname);
@@ -4529,7 +5051,7 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member2->showonlinestatus);
         $this->assertEquals(true, $member2->iscontact);
         $this->assertEquals(false, $member2->isblocked);
-        $this->assertCount(2, $member2->contactrequests);
+        $this->assertCount(1, $member2->contactrequests);
 
         $this->assertEquals($user3->id, $member3->id);
         $this->assertEquals(fullname($user3), $member3->fullname);
@@ -4537,12 +5059,11 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(true, $member3->showonlinestatus);
         $this->assertEquals(false, $member3->iscontact);
         $this->assertEquals(true, $member3->isblocked);
-        $this->assertCount(2, $member3->contactrequests);
+        $this->assertCount(1, $member3->contactrequests);
 
         // Confirm the contact requests are OK.
         $request1 = array_shift($member1->contactrequests);
         $request2 = array_shift($member1->contactrequests);
-        $request3 = array_shift($member1->contactrequests);
 
         $this->assertEquals($user1->id, $request1->userid);
         $this->assertEquals($user2->id, $request1->requesteduserid);
@@ -4550,26 +5071,155 @@ class core_message_api_testcase extends core_message_messagelib_testcase {
         $this->assertEquals($user1->id, $request2->userid);
         $this->assertEquals($user3->id, $request2->requesteduserid);
 
-        $this->assertEquals($user1->id, $request3->userid);
-        $this->assertEquals($user4->id, $request3->requesteduserid);
-
         $request1 = array_shift($member2->contactrequests);
-        $request2 = array_shift($member2->contactrequests);
 
         $this->assertEquals($user1->id, $request1->userid);
         $this->assertEquals($user2->id, $request1->requesteduserid);
 
-        $this->assertEquals($user2->id, $request2->userid);
-        $this->assertEquals($user3->id, $request2->requesteduserid);
-
         $request1 = array_shift($member3->contactrequests);
-        $request2 = array_shift($member3->contactrequests);
 
         $this->assertEquals($user1->id, $request1->userid);
         $this->assertEquals($user3->id, $request1->requesteduserid);
+    }
 
-        $this->assertEquals($user2->id, $request2->userid);
-        $this->assertEquals($user3->id, $request2->requesteduserid);
+    /**
+     * Test verifying that messages can be sent to existing individual conversations.
+     */
+    public function test_send_message_to_conversation_individual_conversation() {
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // Enrol the users into the same course so the privacy checks will pass using default (contact+course members) setting.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course->id);
+
+        // Redirect messages.
+        // This marks messages as read, but we can still observe and verify the number of conversation recipients,
+        // based on the message_viewed events generated as part of marking the message as read for each user.
+        $this->preventResetByRollback();
+        $sink = $this->redirectMessages();
+
+        // Send a message to an individual conversation.
+        $sink = $this->redirectEvents();
+        $message1 = \core_message\api::send_message_to_conversation($user1->id, $ic1->id, 'this is a message', FORMAT_MOODLE);
+        $events = $sink->get_events();
+
+        // Verify the message returned.
+        $this->assertInstanceOf(\stdClass::class, $message1);
+        $this->assertObjectHasAttribute('id', $message1);
+        $this->assertAttributeEquals($user1->id, 'useridfrom', $message1);
+        $this->assertAttributeEquals('this is a message', 'text', $message1);
+        $this->assertObjectHasAttribute('timecreated', $message1);
+
+        // Verify events. Note: the event is a message read event because of an if (PHPUNIT) conditional within message_send(),
+        // however, we can still determine the number and ids of any recipients this way.
+        $this->assertCount(1, $events);
+        $userids = array_column($events, 'userid');
+        $this->assertNotContains($user1->id, $userids);
+        $this->assertContains($user2->id, $userids);
+    }
+
+    /**
+     * Test verifying that messages can be sent to existing group conversations.
+     */
+    public function test_send_message_to_conversation_group_conversation() {
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // Enrol the users into the same course so the privacy checks will pass using default (contact+course members) setting.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course->id);
+
+        // Redirect messages.
+        // This marks messages as read, but we can still observe and verify the number of conversation recipients,
+        // based on the message_viewed events generated as part of marking the message as read for each user.
+        $this->preventResetByRollback();
+        $sink = $this->redirectMessages();
+
+        // Send a message to a group conversation.
+        $sink = $this->redirectEvents();
+        $message1 = \core_message\api::send_message_to_conversation($user1->id, $gc2->id, 'message to the group', FORMAT_MOODLE);
+        $events = $sink->get_events();
+
+        // Verify the message returned.
+        $this->assertInstanceOf(\stdClass::class, $message1);
+        $this->assertObjectHasAttribute('id', $message1);
+        $this->assertAttributeEquals($user1->id, 'useridfrom', $message1);
+        $this->assertAttributeEquals('message to the group', 'text', $message1);
+        $this->assertObjectHasAttribute('timecreated', $message1);
+
+        // Verify events. Note: the event is a message read event because of an if (PHPUNIT) conditional within message_send(),
+        // however, we can still determine the number and ids of any recipients this way.
+        $this->assertCount(2, $events);
+        $userids = array_column($events, 'userid');
+        $this->assertNotContains($user1->id, $userids);
+        $this->assertContains($user3->id, $userids);
+        $this->assertContains($user4->id, $userids);
+    }
+
+    /**
+     * Test verifying that messages cannot be sent to conversations that don't exist.
+     */
+    public function test_send_message_to_conversation_non_existent_conversation() {
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        $this->expectException(\moodle_exception::class);
+        \core_message\api::send_message_to_conversation($user1->id, 0, 'test', FORMAT_MOODLE);
+    }
+
+    /**
+     * Test verifying that messages cannot be sent to conversations by users who are not members.
+     */
+    public function test_send_message_to_conversation_non_member() {
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // Enrol the users into the same course so the privacy checks will pass using default (contact+course members) setting.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course->id);
+
+        $this->expectException(\moodle_exception::class);
+        \core_message\api::send_message_to_conversation($user3->id, $ic1->id, 'test', FORMAT_MOODLE);
+    }
+
+    /**
+     * Test verifying that messages cannot be sent to conversations by users who are not members.
+     */
+    public function test_send_message_to_conversation_blocked_user() {
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // Enrol the users into the same course so the privacy checks will pass using default (contact+course members) setting.
+        $course = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course->id);
+
+        // User 1 blocks user 2.
+        \core_message\api::block_user($user1->id, $user2->id);
+
+        // Verify that a message can be sent to any group conversation in which user1 and user2 are members.
+        $this->assertNotEmpty(\core_message\api::send_message_to_conversation($user1->id, $gc2->id, 'Hey guys', FORMAT_PLAIN));
+
+        // User 2 cannot send a message to the conversation with user 1.
+        $this->expectException(\moodle_exception::class);
+        \core_message\api::send_message_to_conversation($user2->id, $ic1->id, 'test', FORMAT_MOODLE);
     }
 
     /**
index 714461b..8960bed 100644 (file)
@@ -238,6 +238,101 @@ class core_message_events_testcase extends core_message_messagelib_testcase {
         $this->assertEquals(4, $event->other['courseid']);
     }
 
+    /**
+     * Test the group message sent event.
+     *
+     * We can't test events in any testing of the message_send() function as there is a conditional PHPUNIT check in message_send,
+     * resulting in fake messages being generated and captured under test. As a result, none of the events code, nor message
+     * processor code is called during testing.
+     */
+    public function test_group_message_sent() {
+        $event = \core\event\group_message_sent::create([
+            'objectid' => 3,
+            'userid' => 1,
+            'context'  => context_system::instance(),
+            'other' => [
+                'courseid' => 4,
+                'conversationid' => 54
+            ]
+        ]);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\group_message_sent', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+        $url = new moodle_url('/message/index.php');
+        $this->assertEquals($url, $event->get_url());
+        $this->assertEquals(3, $event->objectid);
+        $this->assertEquals(4, $event->other['courseid']);
+        $this->assertEquals(54, $event->other['conversationid']);
+    }
+
+    /**
+     * Test the group message sent event when created without a courseid.
+     */
+    public function test_group_message_sent_without_other_courseid() {
+        // Creating a message_sent event without other[courseid] leads to exception.
+        $this->expectException('coding_exception');
+        $this->expectExceptionMessage('The \'courseid\' value must be set in other');
+
+        $event = \core\event\group_message_sent::create([
+            'userid' => 1,
+            'objectid' => 3,
+            'context'  => context_system::instance(),
+            'relateduserid' => 2,
+            'other' => [
+                'conversationid' => 34
+            ]
+        ]);
+    }
+
+    /**
+     * Test the group message sent event when created without a conversationid.
+     */
+    public function test_group_message_sent_without_other_conversationid() {
+        // Creating a message_sent event without other[courseid] leads to exception.
+        $this->expectException('coding_exception');
+        $this->expectExceptionMessage('The \'conversationid\' value must be set in other');
+
+        $event = \core\event\group_message_sent::create([
+            'userid' => 1,
+            'objectid' => 3,
+            'context'  => context_system::instance(),
+            'relateduserid' => 2,
+            'other' => [
+                'courseid' => 44,
+            ]
+        ]);
+    }
+
+    /**
+     * Test the group message sent event using the create_from_ids() method.
+     */
+    public function test_group_message_sent_via_create_from_ids() {
+        // Fields are: userfromid, conversationid, messageid, courseid.
+        $event = \core\event\group_message_sent::create_from_ids(1, 2, 3, 4);
+
+        // Trigger and capture the event.
+        $sink = $this->redirectEvents();
+        $event->trigger();
+        $events = $sink->get_events();
+        $event = reset($events);
+
+        // Check that the event data is valid.
+        $this->assertInstanceOf('\core\event\group_message_sent', $event);
+        $this->assertEquals(context_system::instance(), $event->get_context());
+        $this->assertEquals(new moodle_url('/message/index.php'), $event->get_url());
+        $this->assertEquals(1, $event->userid);
+        $this->assertEquals(2, $event->other['conversationid']);
+        $this->assertEquals(3, $event->objectid);
+        $this->assertEquals(4, $event->other['courseid']);
+    }
+
     /**
      * Test the message viewed event.
      */
index 0781adc..54fe35d 100644 (file)
@@ -574,14 +574,50 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $request = reset($requests);
 
         $this->assertEquals($user2->id, $request['id']);
-        $this->assertEquals($user2->picture, $request['picture']);
-        $this->assertEquals($user2->firstname, $request['firstname']);
-        $this->assertEquals($user2->lastname, $request['lastname']);
-        $this->assertEquals($user2->firstnamephonetic, $request['firstnamephonetic']);
-        $this->assertEquals($user2->lastnamephonetic, $request['lastnamephonetic']);
-        $this->assertEquals($user2->middlename, $request['middlename']);
-        $this->assertEquals($user2->alternatename, $request['alternatename']);
-        $this->assertEquals($user2->email, $request['email']);
+        $this->assertEquals(fullname($user2), $request['fullname']);
+        $this->assertArrayHasKey('profileimageurl', $request);
+        $this->assertArrayHasKey('profileimageurlsmall', $request);
+        $this->assertArrayHasKey('isonline', $request);
+        $this->assertArrayHasKey('showonlinestatus', $request);
+        $this->assertArrayHasKey('isblocked', $request);
+        $this->assertArrayHasKey('iscontact', $request);
+    }
+
+    /**
+     * Test getting contact requests when there are none.
+     */
+    public function test_get_contact_requests_no_requests() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        $requests = core_message_external::get_contact_requests($user1->id);
+        $requests = external_api::clean_returnvalue(core_message_external::get_contact_requests_returns(), $requests);
+
+        $this->assertEmpty($requests);
+    }
+
+    /**
+     * Test getting contact requests with limits.
+     */
+    public function test_get_contact_requests_with_limits() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $this->setUser($user1);
+
+        \core_message\api::create_contact_request($user2->id, $user1->id);
+        \core_message\api::create_contact_request($user3->id, $user1->id);
+
+        $requests = core_message_external::get_contact_requests($user1->id, 0, 1);
+        $requests = external_api::clean_returnvalue(core_message_external::get_contact_requests_returns(), $requests);
+
+        $this->assertCount(1, $requests);
     }
 
     /**
@@ -3111,7 +3147,6 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Retrieve the messages.
         $result = core_message_external::data_for_messagearea_messages($user1->id, $user2->id);
-        $this->assertDebuggingCalledCount(3);
 
         // We need to execute the return values cleaning process to simulate the web service server.
         $result = external_api::clean_returnvalue(core_message_external::data_for_messagearea_messages_returns(),
@@ -3176,7 +3211,6 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Retrieve the messages from $time - 3, which should be the 3 most recent messages.
         $result = core_message_external::data_for_messagearea_messages($user1->id, $user2->id, 0, 0, false, $time - 3);
-        $this->assertDebuggingCalledCount(3);
 
         // We need to execute the return values cleaning process to simulate the web service server.
         $result = external_api::clean_returnvalue(core_message_external::data_for_messagearea_messages_returns(),
@@ -3217,7 +3251,6 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Retrieve the messages.
         $result = core_message_external::data_for_messagearea_messages($user1->id, $user2->id);
-        $this->assertDebuggingCalledCount(3);
 
         // We need to execute the return values cleaning process to simulate the web service server.
         $result = external_api::clean_returnvalue(core_message_external::data_for_messagearea_messages_returns(),
@@ -3577,7 +3610,6 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Get the most recent message.
         $result = core_message_external::data_for_messagearea_get_most_recent_message($user1->id, $user2->id);
-        $this->assertDebuggingCalledCount(3);
 
         // We need to execute the return values cleaning process to simulate the web service server.
         $result = external_api::clean_returnvalue(core_message_external::data_for_messagearea_get_most_recent_message_returns(),
@@ -3611,7 +3643,6 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
 
         // Get the most recent message.
         $result = core_message_external::data_for_messagearea_get_most_recent_message($user1->id, $user2->id);
-        $this->assertDebuggingCalledCount(3);
 
         // We need to execute the return values cleaning process to simulate the web service server.
         $result = external_api::clean_returnvalue(core_message_external::data_for_messagearea_get_most_recent_message_returns(),
@@ -4781,6 +4812,10 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
                 $this->assertArrayHasKey('showonlinestatus', $member);
                 $this->assertArrayHasKey('isblocked', $member);
                 $this->assertArrayHasKey('iscontact', $member);
+                $this->assertArrayHasKey('isdeleted', $member);
+                $this->assertArrayHasKey('canmessage', $member);
+                $this->assertArrayHasKey('requirescontact', $member);
+                $this->assertArrayHasKey('contactrequests', $member);
             }
             $this->assertArrayHasKey('messages', $conv);
             foreach ($conv['messages'] as $message) {
@@ -5076,6 +5111,68 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($groupimageurl, $conversations[0]['imageurl']);
     }
 
+    /**
+     * Test verifying get_conversations when there are users in a group and/or individual conversation. The reason this
+     * test is performed is because we do not need as much data for group conversations (saving DB calls), so we want
+     * to confirm this happens.
+     */
+    public function test_get_conversations_user_in_group_and_individual_chat() {
+        $this->resetAfterTest();
+
+        $user1 = self::getDataGenerator()->create_user();
+        $user2 = self::getDataGenerator()->create_user();
+        $user3 = self::getDataGenerator()->create_user();
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
+            [
+                $user1->id,
+                $user2->id
+            ],
+            'Individual conversation'
+        );
+
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id);
+
+        $conversation = \core_message\api::create_conversation(
+            \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP,
+            [
+                $user1->id,
+                $user2->id,
+            ],
+            'Group conversation'
+        );
+
+        testhelper::send_fake_message_to_conversation($user1, $conversation->id);
+
+        \core_message\api::create_contact_request($user1->id, $user2->id);
+        \core_message\api::create_contact_request($user1->id, $user3->id);
+
+        $this->setUser($user2);
+        $result = core_message_external::get_conversations($user2->id);
+        $result = external_api::clean_returnvalue(core_message_external::get_conversations_returns(), $result);
+        $conversations = $result['conversations'];
+
+        $groupconversation = array_shift($conversations);
+        $individualconversation = array_shift($conversations);
+
+        $this->assertEquals('Group conversation', $groupconversation['name']);
+        $this->assertEquals('Individual conversation', $individualconversation['name']);
+
+        $this->assertCount(1, $groupconversation['members']);
+        $this->assertCount(1, $individualconversation['members']);
+
+        $groupmember = reset($groupconversation['members']);
+        $this->assertNull($groupmember['requirescontact']);
+        $this->assertNull($groupmember['canmessage']);
+        $this->assertEmpty($groupmember['contactrequests']);
+
+        $individualmember = reset($individualconversation['members']);
+        $this->assertNotNull($individualmember['requirescontact']);
+        $this->assertNotNull($individualmember['canmessage']);
+        $this->assertNotEmpty($individualmember['contactrequests']);
+    }
+
     /**
      * Test returning members in a conversation with no contact requests.
      */
@@ -5161,7 +5258,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(true, $member1->showonlinestatus);
         $this->assertEquals(false, $member1->iscontact);
         $this->assertEquals(false, $member1->isblocked);
-        $this->assertObjectNotHasAttribute('contactrequests', $member1);
+        $this->assertObjectHasAttribute('contactrequests', $member1);
+        $this->assertEmpty($member1->contactrequests);
 
         $this->assertEquals($user2->id, $member2->id);
         $this->assertEquals(fullname($user2), $member2->fullname);
@@ -5169,7 +5267,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(true, $member2->showonlinestatus);
         $this->assertEquals(true, $member2->iscontact);
         $this->assertEquals(false, $member2->isblocked);
-        $this->assertObjectNotHasAttribute('contactrequests', $member2);
+        $this->assertObjectHasAttribute('contactrequests', $member2);
+        $this->assertEmpty($member2->contactrequests);
 
         $this->assertEquals($user3->id, $member3->id);
         $this->assertEquals(fullname($user3), $member3->fullname);
@@ -5177,7 +5276,8 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(true, $member3->showonlinestatus);
         $this->assertEquals(false, $member3->iscontact);
         $this->assertEquals(true, $member3->isblocked);
-        $this->assertObjectNotHasAttribute('contactrequests', $member3);
+        $this->assertObjectHasAttribute('contactrequests', $member3);
+        $this->assertEmpty($member3->contactrequests);
     }
 
     /**
@@ -5236,7 +5336,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(true, $member1->showonlinestatus);
         $this->assertEquals(false, $member1->iscontact);
         $this->assertEquals(false, $member1->isblocked);
-        $this->assertCount(3, $member1->contactrequests);
+        $this->assertCount(2, $member1->contactrequests);
 
         $this->assertEquals($user2->id, $member2->id);
         $this->assertEquals(fullname($user2), $member2->fullname);
@@ -5244,7 +5344,7 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(true, $member2->showonlinestatus);
         $this->assertEquals(true, $member2->iscontact);
         $this->assertEquals(false, $member2->isblocked);
-        $this->assertCount(2, $member2->contactrequests);
+        $this->assertCount(1, $member2->contactrequests);
 
         $this->assertEquals($user3->id, $member3->id);
         $this->assertEquals(fullname($user3), $member3->fullname);
@@ -5252,12 +5352,11 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals(true, $member3->showonlinestatus);
         $this->assertEquals(false, $member3->iscontact);
         $this->assertEquals(true, $member3->isblocked);
-        $this->assertCount(2, $member3->contactrequests);
+        $this->assertCount(1, $member3->contactrequests);
 
         // Confirm the contact requests are OK.
         $request1 = array_shift($member1->contactrequests);
         $request2 = array_shift($member1->contactrequests);
-        $request3 = array_shift($member1->contactrequests);
 
         $this->assertEquals($user1->id, $request1->userid);
         $this->assertEquals($user2->id, $request1->requesteduserid);
@@ -5265,26 +5364,15 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($user1->id, $request2->userid);
         $this->assertEquals($user3->id, $request2->requesteduserid);
 
-        $this->assertEquals($user1->id, $request3->userid);
-        $this->assertEquals($user4->id, $request3->requesteduserid);
-
         $request1 = array_shift($member2->contactrequests);
-        $request2 = array_shift($member2->contactrequests);
 
         $this->assertEquals($user1->id, $request1->userid);
         $this->assertEquals($user2->id, $request1->requesteduserid);
 
-        $this->assertEquals($user2->id, $request2->userid);
-        $this->assertEquals($user3->id, $request2->requesteduserid);
-
         $request1 = array_shift($member3->contactrequests);
-        $request2 = array_shift($member3->contactrequests);
 
         $this->assertEquals($user1->id, $request1->userid);
         $this->assertEquals($user3->id, $request1->requesteduserid);
-
-        $this->assertEquals($user2->id, $request2->userid);
-        $this->assertEquals($user3->id, $request2->requesteduserid);
     }
 
     /**
@@ -5313,4 +5401,171 @@ class core_message_externallib_testcase extends externallib_advanced_testcase {
         $this->expectException('moodle_exception');
         core_message_external::get_conversation_members($user3->id, $conversationid);
     }
+
+    /**
+     * Test verifying multiple messages can be sent to an individual conversation.
+     */
+    public function test_send_messages_to_conversation_individual() {
+        $this->resetAfterTest(true);
+
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // Enrol the users in the same course, so the default privacy controls (course + contacts) can be used.
+        $course1 = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course1->id);
+
+        // The user making the request.
+        $this->setUser($user1);
+
+        // Try to send a message as user1 to a conversation user1 is a a part of.
+        $messages = [
+            [
+                'text' => 'a message from user 1',
+                'textformat' => FORMAT_MOODLE
+            ],
+            [
+                'text' => 'another message from user 1',
+                'textformat' => FORMAT_MOODLE
+            ],
+        ];
+        // Redirect messages.
+        // This marks messages as read, but we can still observe and verify the number of conversation recipients,
+        // based on the message_viewed events generated as part of marking the message as read for each user.
+        $this->preventResetByRollback();
+        $sink = $this->redirectMessages();
+        $writtenmessages = core_message_external::send_messages_to_conversation($ic1->id, $messages);
+
+        external_api::clean_returnvalue(core_message_external::send_messages_to_conversation_returns(), $writtenmessages);
+
+        $this->assertCount(2, $writtenmessages);
+        $this->assertObjectHasAttribute('id', $writtenmessages[0]);
+        $this->assertEquals($user1->id, $writtenmessages[0]->useridfrom);
+        $this->assertEquals($messages[0]['text'], $writtenmessages[0]->text);
+        $this->assertNotEmpty($writtenmessages[0]->timecreated);
+
+        $this->assertObjectHasAttribute('id', $writtenmessages[1]);
+        $this->assertEquals($user1->id, $writtenmessages[1]->useridfrom);
+        $this->assertEquals($messages[1]['text'], $writtenmessages[1]->text);
+        $this->assertNotEmpty($writtenmessages[1]->timecreated);
+    }
+
+    /**
+     * Test verifying multiple messages can be sent to an group conversation.
+     */
+    public function test_send_messages_to_conversation_group() {
+        $this->resetAfterTest(true);
+
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // Enrol the users in the same course, so the default privacy controls (course + contacts) can be used.
+        $course1 = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course1->id);
+
+        // The user making the request.
+        $this->setUser($user1);
+
+        // Try to send a message as user1 to a conversation user1 is a a part of.
+        $messages = [
+            [
+                'text' => 'a message from user 1 to group conv',
+                'textformat' => FORMAT_MOODLE
+            ],
+            [
+                'text' => 'another message from user 1 to group conv',
+                'textformat' => FORMAT_MOODLE
+            ],
+        ];
+        // Redirect messages.
+        // This marks messages as read, but we can still observe and verify the number of conversation recipients,
+        // based on the message_viewed events generated as part of marking the message as read for each user.
+        $this->preventResetByRollback();
+        $sink = $this->redirectMessages();
+        $writtenmessages = core_message_external::send_messages_to_conversation($gc2->id, $messages);
+
+        external_api::clean_returnvalue(core_message_external::send_messages_to_conversation_returns(), $writtenmessages);
+
+        $this->assertCount(2, $writtenmessages);
+        $this->assertObjectHasAttribute('id', $writtenmessages[0]);
+        $this->assertEquals($user1->id, $writtenmessages[0]->useridfrom);
+        $this->assertEquals($messages[0]['text'], $writtenmessages[0]->text);
+        $this->assertNotEmpty($writtenmessages[0]->timecreated);
+
+        $this->assertObjectHasAttribute('id', $writtenmessages[1]);
+        $this->assertEquals($user1->id, $writtenmessages[1]->useridfrom);
+        $this->assertEquals($messages[1]['text'], $writtenmessages[1]->text);
+        $this->assertNotEmpty($writtenmessages[1]->timecreated);
+    }
+
+    /**
+     * Test verifying multiple messages can not be sent to a non existent conversation.
+     */
+    public function test_send_messages_to_conversation_non_existent_conversation() {
+        $this->resetAfterTest(true);
+
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // The user making the request.
+        $this->setUser($user1);
+
+        // Try to send a message as user1 to a conversation user1 is a a part of.
+        $messages = [
+            [
+                'text' => 'a message from user 1',
+                'textformat' => FORMAT_MOODLE
+            ],
+            [
+                'text' => 'another message from user 1',
+                'textformat' => FORMAT_MOODLE
+            ],
+        ];
+        $this->expectException(\moodle_exception::class);
+        $writtenmessages = core_message_external::send_messages_to_conversation(0, $messages);
+    }
+
+    /**
+     * Test verifying multiple messages can not be sent to a conversation by a non-member.
+     */
+    public function test_send_messages_to_conversation_non_member() {
+        $this->resetAfterTest(true);
+
+        // Get a bunch of conversations, some group, some individual and in different states.
+        list($user1, $user2, $user3, $user4, $ic1, $ic2, $ic3,
+            $gc1, $gc2, $gc3, $gc4, $gc5, $gc6) = $this->create_conversation_test_data();
+
+        // Enrol the users in the same course, so the default privacy controls (course + contacts) can be used.
+        $course1 = $this->getDataGenerator()->create_course();
+        $this->getDataGenerator()->enrol_user($user1->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user2->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user3->id, $course1->id);
+        $this->getDataGenerator()->enrol_user($user4->id, $course1->id);
+
+        // The user making the request. This user is not a member of group conversation 1 (gc1).
+        $this->setUser($user1);
+
+        // Try to send a message as user1 to a conversation user1 is a a part of.
+        $messages = [
+            [
+                'text' => 'a message from user 1 to group conv',
+                'textformat' => FORMAT_MOODLE
+            ],
+            [
+                'text' => 'another message from user 1 to group conv',
+                'textformat' => FORMAT_MOODLE
+            ],
+        ];
+        $this->expectException(\moodle_exception::class);
+        $writtenmessages = core_message_external::send_messages_to_conversation($gc1->id, $messages);
+    }
 }
index eb7a159..868178c 100644 (file)
@@ -31,11 +31,6 @@ information provided here is intended especially for developers.
   - \core_message\api::delete_conversation()
   - \core_message\api::is_user_non_contact_blocked()
   - \core_message\api::create_conversation_between_users()
-  - \core_message\api::get_messages()
-  - \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.
@@ -50,6 +45,10 @@ information provided here is intended especially for developers.
   - 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.
+  - core_message_external::data_for_messagearea_contacts().
+  - core_message_external::data_for_messagearea_messages().
+  - core_message_external::data_for_messagearea_get_most_recent_message().
+  - core_message_external::data_for_messagearea_get_profile().
 * The following function has been added for getting the privacy messaging preference:
   - get_user_privacy_messaging_preference()
 
index 6b74c23..f79e724 100644 (file)
@@ -1453,9 +1453,18 @@ function scorm_check_mode($scorm, &$newattempt, &$attempt, $userid, &$mode) {
 
     if ($scorm->forcenewattempt == SCORM_FORCEATTEMPT_ALWAYS) {
         // This SCORM is configured to force a new attempt on every re-entry.
-        $attempt++;
         $newattempt = 'on';
         $mode = 'normal';
+        if ($attempt == 1) {
+            // Check if the user has any existing data or if this is really the first attempt.
+            $exists = $DB->record_exists('scorm_scoes_track', array('userid' => $userid, 'scormid' => $scorm->id));
+            if (!$exists) {
+                // No records yet - Attempt should == 1.
+                return;
+            }
+        }
+        $attempt++;
+
         return;
     }
     // Check if the scorm module is incomplete (used to validate user request to start a new attempt).
index cf61bb0..1793a95 100644 (file)
@@ -780,7 +780,7 @@ function scorm_grade_user($scorm, $userid) {
 
     switch ($scorm->whatgrade) {
         case FIRSTATTEMPT:
-            return scorm_grade_user_attempt($scorm, $userid, 1);
+            return scorm_grade_user_attempt($scorm, $userid, scorm_get_first_attempt($scorm->id, $userid));
         break;
         case LASTATTEMPT:
             return scorm_grade_user_attempt($scorm, $userid, scorm_get_last_completed_attempt($scorm->id, $userid));
@@ -850,6 +850,30 @@ function scorm_get_last_attempt($scormid, $userid) {
     }
 }
 
+/**
+ * Returns the first attempt used - if no attempts yet, returns 1 for first attempt.
+ *
+ * @param int $scormid the id of the scorm.
+ * @param int $userid the id of the user.
+ *
+ * @return int The first attempt number.
+ */
+function scorm_get_first_attempt($scormid, $userid) {
+    global $DB;
+
+    // Find the first attempt number for the given user id and scorm id.
+    $sql = "SELECT MIN(attempt)
+              FROM {scorm_scoes_track}
+             WHERE userid = ? AND scormid = ?";
+
+    $lastattempt = $DB->get_field_sql($sql, array($userid, $scormid));
+    if (empty($lastattempt)) {
+        return '1';
+    } else {
+        return $lastattempt;
+    }
+}
+
 /**
  * Returns the last completed attempt used - if no completed attempts yet, returns 1 for first attempt
  *
index 163dd3e..5e4a272 100644 (file)
@@ -59,7 +59,7 @@
 </head>
 <body>
     <nav class="navbar navbar-light bg-light border-bottom">
-        <a class="navbar-brand" href="https://www.moodle.org" title="Moodle">
+        <a class="navbar-brand" href="https://moodle.org" title="Moodle">
             <img class="moodle-logo" src="pix/moodlelogo.svg" alt="Moodle logo" />
         </a>
         <span class="navbar-brand">{{#str}}broughtbymoodle, core_privacy{{/str}}</span>
         </footer>
     </div>
 </body>
-</html>
\ No newline at end of file
+</html>
index 19d6ec8..4cfb711 100644 (file)
@@ -119,7 +119,7 @@ $card-gutter : $card-deck-margin * 2;
 
 .dashboard-card-deck.one-row {
     flex-flow: nowrap;
-    overflow-y: scroll;
+    overflow-x: scroll;
 }
 
 .summaryimage {
index a0250ee..0700407 100644 (file)
@@ -11172,7 +11172,7 @@ div.editor_atto_toolbar button .icon {
 
 .dashboard-card-deck.one-row {
   flex-flow: nowrap;
-  overflow-y: scroll; }
+  overflow-x: scroll; }
 
 .summaryimage {
   height: 7rem;
index be6af72..46849d4 100644 (file)
 
 .dashboard-card-deck.one-row {
     flex-flow: nowrap;
-    overflow-y: scroll;
+    overflow-x: scroll;
 }
 
 .block_recentlyaccesseditems {
index 62755b1..3e252bb 100644 (file)
@@ -16230,7 +16230,7 @@ body {
 }
 .dashboard-card-deck.one-row {
   flex-flow: nowrap;
-  overflow-y: scroll;
+  overflow-x: scroll;
 }
 .block_recentlyaccesseditems img.icon {
   height: auto;
index e6794cf..2023cd8 100644 (file)
@@ -366,7 +366,8 @@ class core_user_external extends external_api {
                     new external_single_structure(
                         array(
                             'type'  => new external_value(PARAM_RAW, 'The name of the preference'),
-                            'value' => new external_value(PARAM_RAW, 'The value of the preference')
+                            'value' => new external_value(PARAM_RAW, 'The value of the preference, do not set this field if you
+                                want to remove (unset) the current value.', VALUE_DEFAULT, null),
                         )
                     ), 'User preferences', VALUE_DEFAULT, array()
                 )
@@ -383,7 +384,7 @@ class core_user_external extends external_api {
      * @return null
      * @since Moodle 3.2
      */
-    public static function update_user_preferences($userid, $emailstop = null, $preferences = array()) {
+    public static function update_user_preferences($userid = 0, $emailstop = null, $preferences = array()) {
         global $USER, $CFG;
 
         require_once($CFG->dirroot . '/user/lib.php');
@@ -401,7 +402,8 @@ class core_user_external extends external_api {
             'emailstop' => $emailstop,
             'preferences' => $preferences
         );
-        self::validate_parameters(self::update_user_preferences_parameters(), $params);
+        $params = self::validate_parameters(self::update_user_preferences_parameters(), $params);
+        $preferences = $params['preferences'];
 
         // Preferences.
         if (!empty($preferences)) {
index 0735aea..36fa7b5 100644 (file)
@@ -1239,6 +1239,40 @@ class core_user_externallib_testcase extends externallib_advanced_testcase {
         $this->assertEquals($user2->id, $result['warnings'][0]['itemid']);
     }
 
+    /**
+     * Test update_user_preferences unsetting an existing preference.
+     */
+    public function test_update_user_preferences_unset() {
+        global $DB;
+        $this->resetAfterTest(true);
+
+        $user = self::getDataGenerator()->create_user();
+
+        // Save users preferences.
+        $this->setAdminUser();
+        $preferences = array(
+            array(
+                'name' => 'htmleditor',
+                'value' => 'atto',
+                'userid' => $user->id,
+            )
+        );
+
+        $result = core_user_external::set_user_preferences($preferences);
+        $result = external_api::clean_returnvalue(core_user_external::set_user_preferences_returns(), $result);
+        $this->assertCount(0, $result['warnings']);
+        $this->assertCount(1, $result['saved']);
+
+        // Get preference from DB to avoid cache.
+        $this->assertEquals('atto', $DB->get_field('user_preferences', 'value',
+            array('userid' => $user->id, 'name' => 'htmleditor')));
+
+        // Now, unset.
+        $result = core_user_external::update_user_preferences($user->id, null, array(array('type' => 'htmleditor')));
+
+        $this->assertEquals(0, $DB->count_records('user_preferences', array('userid' => $user->id, 'name' => 'htmleditor')));
+    }
+
     /**
      * Test agree_site_policy
      */
index c82a29a..56e4dd9 100644 (file)
@@ -4,3 +4,5 @@ This files describes API changes for code that uses the user API.
 
 * The following functions have been finally deprecated and can not be used anymore:
   * useredit_update_picture()
+* core_user_external::update_user_preferences() now allows to unset existing preferences values.
+  If the preference value field is not set, the preference will be unset.