MDL-64632 core_message: fixed PHP error when users are deleted from DB
[moodle.git] / message / classes / api.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Contains class used to return information to display for the message area.
19  *
20  * @package    core_message
21  * @copyright  2016 Mark Nelson <markn@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
25 namespace core_message;
27 use core_favourites\local\entity\favourite;
29 defined('MOODLE_INTERNAL') || die();
31 require_once($CFG->dirroot . '/lib/messagelib.php');
33 /**
34  * Class used to return information to display for the message area.
35  *
36  * @copyright  2016 Mark Nelson <markn@moodle.com>
37  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38  */
39 class api {
41     /**
42      * The action for reading a message.
43      */
44     const MESSAGE_ACTION_READ = 1;
46     /**
47      * The action for deleting a message.
48      */
49     const MESSAGE_ACTION_DELETED = 2;
51     /**
52      * The privacy setting for being messaged by anyone within courses user is member of.
53      */
54     const MESSAGE_PRIVACY_COURSEMEMBER = 0;
56     /**
57      * The privacy setting for being messaged only by contacts.
58      */
59     const MESSAGE_PRIVACY_ONLYCONTACTS = 1;
61     /**
62      * The privacy setting for being messaged by anyone on the site.
63      */
64     const MESSAGE_PRIVACY_SITE = 2;
66     /**
67      * An individual conversation.
68      */
69     const MESSAGE_CONVERSATION_TYPE_INDIVIDUAL = 1;
71     /**
72      * A group conversation.
73      */
74     const MESSAGE_CONVERSATION_TYPE_GROUP = 2;
76     /**
77      * The state for an enabled conversation area.
78      */
79     const MESSAGE_CONVERSATION_ENABLED = 1;
81     /**
82      * The state for a disabled conversation area.
83      */
84     const MESSAGE_CONVERSATION_DISABLED = 0;
86     /**
87      * Handles searching for messages in the message area.
88      *
89      * @param int $userid The user id doing the searching
90      * @param string $search The string the user is searching
91      * @param int $limitfrom
92      * @param int $limitnum
93      * @return array
94      */
95     public static function search_messages($userid, $search, $limitfrom = 0, $limitnum = 0) {
96         global $DB;
98         // Get the user fields we want.
99         $ufields = \user_picture::fields('u', array('lastaccess'), 'userfrom_id', 'userfrom_');
100         $ufields2 = \user_picture::fields('u2', array('lastaccess'), 'userto_id', 'userto_');
102         $sql = "SELECT m.id, m.useridfrom, mcm.userid as useridto, m.subject, m.fullmessage, m.fullmessagehtml, m.fullmessageformat,
103                        m.smallmessage, m.conversationid, m.timecreated, 0 as isread, $ufields, mub.id as userfrom_blocked,
104                        $ufields2, mub2.id as userto_blocked
105                   FROM {messages} m
106             INNER JOIN {user} u
107                     ON u.id = m.useridfrom
108             INNER JOIN {message_conversations} mc
109                     ON mc.id = m.conversationid
110             INNER JOIN {message_conversation_members} mcm
111                     ON mcm.conversationid = m.conversationid
112             INNER JOIN {user} u2
113                     ON u2.id = mcm.userid
114              LEFT JOIN {message_users_blocked} mub
115                     ON (mub.blockeduserid = u.id AND mub.userid = ?)
116              LEFT JOIN {message_users_blocked} mub2
117                     ON (mub2.blockeduserid = u2.id AND mub2.userid = ?)
118              LEFT JOIN {message_user_actions} mua
119                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
120                  WHERE (m.useridfrom = ? OR mcm.userid = ?)
121                    AND m.useridfrom != mcm.userid
122                    AND u.deleted = 0
123                    AND u2.deleted = 0
124                    AND mua.id is NULL
125                    AND " . $DB->sql_like('smallmessage', '?', false) . "
126               ORDER BY timecreated DESC";
128         $params = array($userid, $userid, $userid, self::MESSAGE_ACTION_DELETED, $userid, $userid, '%' . $search . '%');
130         // Convert the messages into searchable contacts with their last message being the message that was searched.
131         $conversations = array();
132         if ($messages = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
133             foreach ($messages as $message) {
134                 $prefix = 'userfrom_';
135                 if ($userid == $message->useridfrom) {
136                     $prefix = 'userto_';
137                     // If it from the user, then mark it as read, even if it wasn't by the receiver.
138                     $message->isread = true;
139                 }
140                 $blockedcol = $prefix . 'blocked';
141                 $message->blocked = $message->$blockedcol ? 1 : 0;
143                 $message->messageid = $message->id;
144                 $conversations[] = helper::create_contact($message, $prefix);
145             }
146         }
148         return $conversations;
149     }
151     /**
152      * Handles searching for user in a particular course in the message area.
153      *
154      * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
155      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
156      * But we are deprecating data_for_messagearea_search_users_in_course external function.
157      * Followup: MDL-63915
158      *
159      * @param int $userid The user id doing the searching
160      * @param int $courseid The id of the course we are searching in
161      * @param string $search The string the user is searching
162      * @param int $limitfrom
163      * @param int $limitnum
164      * @return array
165      */
166     public static function search_users_in_course($userid, $courseid, $search, $limitfrom = 0, $limitnum = 0) {
167         global $DB;
169         // Get all the users in the course.
170         list($esql, $params) = get_enrolled_sql(\context_course::instance($courseid), '', 0, true);
171         $sql = "SELECT u.*, mub.id as isblocked
172                   FROM {user} u
173                   JOIN ($esql) je
174                     ON je.id = u.id
175              LEFT JOIN {message_users_blocked} mub
176                     ON (mub.blockeduserid = u.id AND mub.userid = :userid)
177                  WHERE u.deleted = 0";
178         // Add more conditions.
179         $fullname = $DB->sql_fullname();
180         $sql .= " AND u.id != :userid2
181                   AND " . $DB->sql_like($fullname, ':search', false) . "
182              ORDER BY " . $DB->sql_fullname();
183         $params = array_merge(array('userid' => $userid, 'userid2' => $userid, 'search' => '%' . $search . '%'), $params);
185         // Convert all the user records into contacts.
186         $contacts = array();
187         if ($users = $DB->get_records_sql($sql, $params, $limitfrom, $limitnum)) {
188             foreach ($users as $user) {
189                 $user->blocked = $user->isblocked ? 1 : 0;
190                 $contacts[] = helper::create_contact($user);
191             }
192         }
194         return $contacts;
195     }
197     /**
198      * Handles searching for user in the message area.
199      *
200      * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
201      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
202      * But we are deprecating data_for_messagearea_search_users external function.
203      * Followup: MDL-63915
204      *
205      * @param int $userid The user id doing the searching
206      * @param string $search The string the user is searching
207      * @param int $limitnum
208      * @return array
209      */
210     public static function search_users($userid, $search, $limitnum = 0) {
211         global $CFG, $DB;
213         // Used to search for contacts.
214         $fullname = $DB->sql_fullname();
215         $ufields = \user_picture::fields('u', array('lastaccess'));
217         // Users not to include.
218         $excludeusers = array($userid, $CFG->siteguest);
219         list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
221         // Ok, let's search for contacts first.
222         $contacts = array();
223         $sql = "SELECT $ufields, mub.id as isuserblocked
224                   FROM {user} u
225                   JOIN {message_contacts} mc
226                     ON u.id = mc.contactid
227              LEFT JOIN {message_users_blocked} mub
228                     ON (mub.userid = :userid2 AND mub.blockeduserid = u.id)
229                  WHERE mc.userid = :userid
230                    AND u.deleted = 0
231                    AND u.confirmed = 1
232                    AND " . $DB->sql_like($fullname, ':search', false) . "
233                    AND u.id $exclude
234               ORDER BY " . $DB->sql_fullname();
235         if ($users = $DB->get_records_sql($sql, array('userid' => $userid, 'userid2' => $userid,
236                 'search' => '%' . $search . '%') + $excludeparams, 0, $limitnum)) {
237             foreach ($users as $user) {
238                 $user->blocked = $user->isuserblocked ? 1 : 0;
239                 $contacts[] = helper::create_contact($user);
240             }
241         }
243         // Now, let's get the courses.
244         // Make sure to limit searches to enrolled courses.
245         $enrolledcourses = enrol_get_my_courses(array('id', 'cacherev'));
246         $courses = array();
247         // Really we want the user to be able to view the participants if they have the capability
248         // 'moodle/course:viewparticipants' or 'moodle/course:enrolreview', but since the search_courses function
249         // only takes required parameters we can't. However, the chance of a user having 'moodle/course:enrolreview' but
250         // *not* 'moodle/course:viewparticipants' are pretty much zero, so it is not worth addressing.
251         if ($arrcourses = \core_course_category::search_courses(array('search' => $search), array('limit' => $limitnum),
252                 array('moodle/course:viewparticipants'))) {
253             foreach ($arrcourses as $course) {
254                 if (isset($enrolledcourses[$course->id])) {
255                     $data = new \stdClass();
256                     $data->id = $course->id;
257                     $data->shortname = $course->shortname;
258                     $data->fullname = $course->fullname;
259                     $courses[] = $data;
260                 }
261             }
262         }
264         // Let's get those non-contacts. Toast them gears boi.
265         // Note - you can only block contacts, so these users will not be blocked, so no need to get that
266         // extra detail from the database.
267         $noncontacts = array();
268         $sql = "SELECT $ufields
269                   FROM {user} u
270                  WHERE u.deleted = 0
271                    AND u.confirmed = 1
272                    AND " . $DB->sql_like($fullname, ':search', false) . "
273                    AND u.id $exclude
274                    AND u.id NOT IN (SELECT contactid
275                                       FROM {message_contacts}
276                                      WHERE userid = :userid)
277               ORDER BY " . $DB->sql_fullname();
278         if ($users = $DB->get_records_sql($sql,  array('userid' => $userid, 'search' => '%' . $search . '%') + $excludeparams,
279                 0, $limitnum)) {
280             foreach ($users as $user) {
281                 $noncontacts[] = helper::create_contact($user);
282             }
283         }
285         return array($contacts, $courses, $noncontacts);
286     }
288     /**
289      * Handles searching for user.
290      *
291      * @param int $userid The user id doing the searching
292      * @param string $search The string the user is searching
293      * @param int $limitfrom
294      * @param int $limitnum
295      * @return array
296      */
297     public static function message_search_users(int $userid, string $search, int $limitfrom = 0, int $limitnum = 20) : array {
298         global $CFG, $DB;
300         // Check if messaging is enabled.
301         if (empty($CFG->messaging)) {
302             throw new \moodle_exception('disabled', 'message');
303         }
305         // Used to search for contacts.
306         $fullname = $DB->sql_fullname();
308         // Users not to include.
309         $excludeusers = array($userid, $CFG->siteguest);
310         list($exclude, $excludeparams) = $DB->get_in_or_equal($excludeusers, SQL_PARAMS_NAMED, 'param', false);
312         $params = array('search' => '%' . $DB->sql_like_escape($search) . '%', 'userid1' => $userid, 'userid2' => $userid);
314         // Ok, let's search for contacts first.
315         $sql = "SELECT u.id
316                   FROM {user} u
317                   JOIN {message_contacts} mc
318                     ON (u.id = mc.contactid AND mc.userid = :userid1) OR (u.id = mc.userid AND mc.contactid = :userid2)
319                  WHERE u.deleted = 0
320                    AND u.confirmed = 1
321                    AND " . $DB->sql_like($fullname, ':search', false) . "
322                    AND u.id $exclude
323               ORDER BY " . $DB->sql_fullname();
324         $foundusers = $DB->get_records_sql_menu($sql, $params + $excludeparams, $limitfrom, $limitnum);
326         $contacts = [];
327         if (!empty($foundusers)) {
328             $contacts = helper::get_member_info($userid, array_keys($foundusers));
329             foreach ($contacts as $memberuserid => $memberinfo) {
330                 $contacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000);
331             }
332         }
334         // Let's get those non-contacts.
335         // If site wide messaging is enabled, we just fetch any matched users which are non-contacts.
336         if ($CFG->messagingallusers) {
337             $sql = "SELECT u.id
338                   FROM {user} u
339                  WHERE u.deleted = 0
340                    AND u.confirmed = 1
341                    AND " . $DB->sql_like($fullname, ':search', false) . "
342                    AND u.id $exclude
343                    AND NOT EXISTS (SELECT mc.id
344                                      FROM {message_contacts} mc
345                                     WHERE (mc.userid = u.id AND mc.contactid = :userid1)
346                                        OR (mc.userid = :userid2 AND mc.contactid = u.id))
347               ORDER BY " . $DB->sql_fullname();
349             $foundusers = $DB->get_records_sql($sql, $params + $excludeparams, $limitfrom, $limitnum);
350         } else {
351             require_once($CFG->dirroot . '/user/lib.php');
352             // If site-wide messaging is disabled, then we should only be able to search for users who we are allowed to see.
353             // Because we can't achieve all the required visibility checks in SQL, we'll iterate through the non-contact records
354             // and stop once we have enough matching the 'visible' criteria.
355             // TODO: MDL-63983 - Improve the performance of non-contact searches when site-wide messaging is disabled (default).
357             // Use a local generator to achieve this iteration.
358             $getnoncontactusers = function ($limitfrom = 0, $limitnum = 0) use($fullname, $exclude, $params, $excludeparams) {
359                 global $DB;
360                 $sql = "SELECT u.*
361                       FROM {user} u
362                      WHERE u.deleted = 0
363                        AND u.confirmed = 1
364                        AND " . $DB->sql_like($fullname, ':search', false) . "
365                        AND u.id $exclude
366                        AND NOT EXISTS (SELECT mc.id
367                                          FROM {message_contacts} mc
368                                         WHERE (mc.userid = u.id AND mc.contactid = :userid1)
369                                            OR (mc.userid = :userid2 AND mc.contactid = u.id))
370                   ORDER BY " . $DB->sql_fullname();
371                 while ($records = $DB->get_records_sql($sql, $params + $excludeparams, $limitfrom, $limitnum)) {
372                     yield $records;
373                     $limitfrom += $limitnum;
374                 }
375             };
377             // Fetch in batches of $limitnum * 2 to improve the chances of matching a user without going back to the DB.
378             // The generator cannot function without a sensible limiter, so set one if this is not set.
379             $batchlimit = ($limitnum == 0) ? 20 : $limitnum;
381             // We need to make the offset param work with the generator.
382             // Basically, if we want to get say 10 records starting at the 40th record, we need to see 50 records and return only
383             // those after the 40th record. We can never pass the method's offset param to the generator as we need to manage the
384             // position within those valid records ourselves.
385             // See MDL-63983 dealing with performance improvements to this area of code.
386             $noofvalidseenrecords = 0;
387             $returnedusers = [];
388             foreach ($getnoncontactusers(0, $batchlimit) as $users) {
389                 foreach ($users as $id => $user) {
390                     $userdetails = \user_get_user_details_courses($user);
392                     // Return the user only if the searched field is returned.
393                     // Otherwise it means that the $USER was not allowed to search the returned user.
394                     if (!empty($userdetails) and !empty($userdetails['fullname'])) {
395                         // We know we've matched, but only save the record if it's within the offset area we need.
396                         if ($limitfrom == 0) {
397                             // No offset specified, so just save.
398                             $returnedusers[$id] = $user;
399                         } else {
400                             // There is an offset in play.
401                             // If we've passed enough records already (> offset value), then we can save this one.
402                             if ($noofvalidseenrecords >= $limitfrom) {
403                                 $returnedusers[$id] = $user;
404                             }
405                         }
406                         if (count($returnedusers) == $limitnum) {
407                             break 2;
408                         }
409                         $noofvalidseenrecords++;
410                     }
411                 }
412             }
413             $foundusers = $returnedusers;
414         }
416         $noncontacts = [];
417         if (!empty($foundusers)) {
418             $noncontacts = helper::get_member_info($userid, array_keys($foundusers));
419             foreach ($noncontacts as $memberuserid => $memberinfo) {
420                 $noncontacts[$memberuserid]->conversations = self::get_conversations_between_users($userid, $memberuserid, 0, 1000);
421             }
422         }
424         return array(array_values($contacts), array_values($noncontacts));
425     }
427     /**
428      * Gets extra fields, like image url and subname for any conversations linked to components.
429      *
430      * The subname is like a subtitle for the conversation, to compliment it's name.
431      * The imageurl is the location of the image for the conversation, as might be seen on a listing of conversations for a user.
432      *
433      * @param array $conversations a list of conversations records.
434      * @return array the array of subnames, index by conversation id.
435      * @throws \coding_exception
436      * @throws \dml_exception
437      */
438     protected static function get_linked_conversation_extra_fields(array $conversations) : array {
439         global $DB, $PAGE;
441         $renderer = $PAGE->get_renderer('core');
443         $linkedconversations = [];
444         foreach ($conversations as $conversation) {
445             if (!is_null($conversation->component) && !is_null($conversation->itemtype)) {
446                 $linkedconversations[$conversation->component][$conversation->itemtype][$conversation->id]
447                     = $conversation->itemid;
448             }
449         }
450         if (empty($linkedconversations)) {
451             return [];
452         }
454         // TODO: MDL-63814: Working out the subname for linked conversations should be done in a generic way.
455         // Get the itemid, but only for course group linked conversation for now.
456         $extrafields = [];
457         if (!empty($linkeditems = $linkedconversations['core_group']['groups'])) { // Format: [conversationid => itemid].
458             // Get the name of the course to which the group belongs.
459             list ($groupidsql, $groupidparams) = $DB->get_in_or_equal(array_values($linkeditems), SQL_PARAMS_NAMED, 'groupid');
460             $sql = "SELECT g.*, c.shortname as courseshortname
461                       FROM {groups} g
462                       JOIN {course} c
463                         ON g.courseid = c.id
464                      WHERE g.id $groupidsql";
465             $courseinfo = $DB->get_records_sql($sql, $groupidparams);
466             foreach ($linkeditems as $convid => $groupid) {
467                 if (array_key_exists($groupid, $courseinfo)) {
468                     $group = $courseinfo[$groupid];
469                     // Subname.
470                     $extrafields[$convid]['subname'] = format_string($courseinfo[$groupid]->courseshortname);
472                     // Imageurl.
473                     $extrafields[$convid]['imageurl'] = $renderer->image_url('g/g1')->out(false); // default image.
474                     if ($url = get_group_picture_url($group, $group->courseid, true)) {
475                         $extrafields[$convid]['imageurl'] = $url->out(false);
476                     }
477                 }
478             }
479         }
480         return $extrafields;
481     }
484     /**
485      * Returns the contacts and their conversation to display in the contacts area.
486      *
487      * ** WARNING **
488      * It is HIGHLY recommended to use a sensible limit when calling this function. Trying
489      * to retrieve too much information in a single call will cause performance problems.
490      * ** WARNING **
491      *
492      * This function has specifically been altered to break each of the data sets it
493      * requires into separate database calls. This is to avoid the performance problems
494      * observed when attempting to join large data sets (e.g. the message tables and
495      * the user table).
496      *
497      * While it is possible to gather the data in a single query, and it may even be
498      * more efficient with a correctly tuned database, we have opted to trade off some of
499      * the benefits of a single query in order to ensure this function will work on
500      * most databases with default tunings and with large data sets.
501      *
502      * @param int $userid The user id
503      * @param int $limitfrom
504      * @param int $limitnum
505      * @param int $type the type of the conversation, if you wish to filter to a certain type (see api constants).
506      * @param bool $favourites whether to include NO favourites (false) or ONLY favourites (true), or null to ignore this setting.
507      * @return array the array of conversations
508      * @throws \moodle_exception
509      */
510     public static function get_conversations($userid, $limitfrom = 0, $limitnum = 20, int $type = null,
511             bool $favourites = null) {
512         global $DB;
514         if (!is_null($type) && !in_array($type, [self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
515                 self::MESSAGE_CONVERSATION_TYPE_GROUP])) {
516             throw new \moodle_exception("Invalid value ($type) for type param, please see api constants.");
517         }
519         // We need to know which conversations are favourites, so we can either:
520         // 1) Include the 'isfavourite' attribute on conversations (when $favourite = null and we're including all conversations)
521         // 2) Restrict the results to ONLY those conversations which are favourites (when $favourite = true)
522         // 3) Restrict the results to ONLY those conversations which are NOT favourites (when $favourite = false).
523         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
524         $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
525         $favouriteconversationids = array_column($favouriteconversations, 'itemid');
526         if ($favourites && empty($favouriteconversationids)) {
527             return []; // If we are aiming to return ONLY favourites, and we have none, there's nothing more to do.
528         }
530         // CONVERSATIONS AND MOST RECENT MESSAGE.
531         // Include those conversations with messages first (ordered by most recent message, desc), then add any conversations which
532         // don't have messages, such as newly created group conversations.
533         // Because we're sorting by message 'timecreated', those conversations without messages could be at either the start or the
534         // end of the results (behaviour for sorting of nulls differs between DB vendors), so we use the case to presort these.
536         // If we need to return ONLY favourites, or NO favourites, generate the SQL snippet.
537         $favouritesql = "";
538         $favouriteparams = [];
539         if (null !== $favourites && !empty($favouriteconversationids)) {
540             list ($insql, $favouriteparams) =
541                     $DB->get_in_or_equal($favouriteconversationids, SQL_PARAMS_NAMED, 'favouriteids', $favourites);
542             $favouritesql = " AND mc.id {$insql} ";
543         }
545         // If we need to restrict type, generate the SQL snippet.
546         $typesql = !is_null($type) ? " AND mc.type = :convtype " : "";
548         $sql = "SELECT m.id as messageid, mc.id as id, mc.name as conversationname, mc.type as conversationtype, m.useridfrom,
549                        m.smallmessage, m.fullmessage, m.fullmessageformat, m.fullmessagehtml, m.timecreated, mc.component,
550                        mc.itemtype, mc.itemid, mc.contextid
551                   FROM {message_conversations} mc
552             INNER JOIN {message_conversation_members} mcm
553                     ON (mcm.conversationid = mc.id AND mcm.userid = :userid3)
554             LEFT JOIN (
555                           SELECT m.conversationid, MAX(m.id) AS messageid
556                             FROM {messages} m
557                       INNER JOIN (
558                                       SELECT m.conversationid, MAX(m.timecreated) as maxtime
559                                         FROM {messages} m
560                                   INNER JOIN {message_conversation_members} mcm
561                                           ON mcm.conversationid = m.conversationid
562                                    LEFT JOIN {message_user_actions} mua
563                                           ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
564                                        WHERE mua.id is NULL
565                                          AND mcm.userid = :userid2
566                                     GROUP BY m.conversationid
567                                  ) maxmessage
568                                ON maxmessage.maxtime = m.timecreated AND maxmessage.conversationid = m.conversationid
569                          GROUP BY m.conversationid
570                        ) lastmessage
571                     ON lastmessage.conversationid = mc.id
572             LEFT JOIN {messages} m
573                    ON m.id = lastmessage.messageid
574                 WHERE mc.id IS NOT NULL
575                   AND mc.enabled = 1 $typesql $favouritesql
576               ORDER BY (CASE WHEN m.timecreated IS NULL THEN 0 ELSE 1 END) DESC, m.timecreated DESC, id DESC";
578         $params = array_merge($favouriteparams, ['userid' => $userid, 'action' => self::MESSAGE_ACTION_DELETED,
579             'userid2' => $userid, 'userid3' => $userid, 'convtype' => $type]);
580         $conversationset = $DB->get_recordset_sql($sql, $params, $limitfrom, $limitnum);
582         $conversations = [];
583         $selfconversations = []; // Used to track legacy conversations with one's self (both conv members the same user).
584         $members = [];
585         $individualmembers = [];
586         $groupmembers = [];
587         foreach ($conversationset as $conversation) {
588             $conversations[$conversation->id] = $conversation;
589             $members[$conversation->id] = [];
590         }
591         $conversationset->close();
593         // If there are no conversations found, then return early.
594         if (empty($conversations)) {
595             return [];
596         }
598         // COMPONENT-LINKED CONVERSATION FIELDS.
599         // Conversations linked to components may have extra information, such as:
600         // - subname: Essentially a subtitle for the conversation. So you'd have "name: subname".
601         // - imageurl: A URL to the image for the linked conversation.
602         // For now, this is ONLY course groups.
603         $convextrafields = self::get_linked_conversation_extra_fields($conversations);
605         // MEMBERS.
606         // Ideally, we want to get 1 member for each conversation, but this depends on the type and whether there is a recent
607         // message or not.
608         //
609         // For 'individual' type conversations between 2 users, regardless of who sent the last message,
610         // we want the details of the other member in the conversation (i.e. not the current user).
611         // The only exception to the 'not the current user' rule is for 'self' conversations - a legacy construct in which a user
612         // can message themselves via user bulk actions. Subsequently, there are 2 records for the same user created in the members
613         // table.
614         //
615         // For 'group' type conversations, we want the details of the member who sent the last message, if there is one.
616         // This can be the current user or another group member, but for groups without messages, this will be empty.
617         //
618         // This also means that if type filtering is specified and only group conversations are returned, we don't need this extra
619         // query to get the 'other' user as we already have that information.
621         // Work out which members we have already, and which ones we might need to fetch.
622         // If all the last messages were from another user, then we don't need to fetch anything further.
623         foreach ($conversations as $conversation) {
624             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
625                 if (!is_null($conversation->useridfrom) && $conversation->useridfrom != $userid) {
626                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
627                     $individualmembers[$conversation->useridfrom] = $conversation->useridfrom;
628                 } else {
629                     $individualconversations[] = $conversation->id;
630                 }
631             } else if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
632                 // If we have a recent message, the sender is our member.
633                 if (!is_null($conversation->useridfrom)) {
634                     $members[$conversation->id][$conversation->useridfrom] = $conversation->useridfrom;
635                     $groupmembers[$conversation->useridfrom] = $conversation->useridfrom;
636                 }
637             }
638         }
639         // If we need to fetch any member information for any of the individual conversations.
640         // This is the case if any of the individual conversations have a recent message sent by the current user.
641         if (!empty($individualconversations)) {
642             list ($icidinsql, $icidinparams) = $DB->get_in_or_equal($individualconversations, SQL_PARAMS_NAMED, 'convid');
643             $indmembersql = "SELECT mcm.id, mcm.conversationid, mcm.userid
644                         FROM {message_conversation_members} mcm
645                        WHERE mcm.conversationid $icidinsql
646                        AND mcm.userid != :userid
647                        ORDER BY mcm.id";
648             $indmemberparams = array_merge($icidinparams, ['userid' => $userid]);
649             $conversationmembers = $DB->get_records_sql($indmembersql, $indmemberparams);
651             foreach ($conversationmembers as $mid => $member) {
652                 $members[$member->conversationid][$member->userid] = $member->userid;
653                 $individualmembers[$member->userid] = $member->userid;
654             }
656             // Self conversations: If any of the individual conversations which were missing members are still missing members,
657             // we know these must be 'self' conversations. This is a legacy scenario, created via user bulk actions.
658             // In such cases, the member returned should be the current user.
659             //
660             // NOTE: Currently, these conversations are not returned by this method, however,
661             // identifying them is important for future reference.
662             foreach ($individualconversations as $indconvid) {
663                 if (empty($members[$indconvid])) {
664                     // Keep track of the self conversation (for future use).
665                     $selfconversations[$indconvid] = $indconvid;
667                     // Set the member to the current user.
668                     $members[$indconvid][$userid] = $userid;
669                     $individualmembers[$userid] = $userid;
670                 }
671             }
672         }
674         // We could fail early here if we're sure that:
675         // a) we have no otherusers for all the conversations (users may have been deleted)
676         // b) we're sure that all conversations are individual (1:1).
678         // We need to pull out the list of users info corresponding to the memberids in the conversations.This
679         // needs to be done in a separate query to avoid doing a join on the messages tables and the user
680         // tables because on large sites these tables are massive which results in extremely slow
681         // performance (typically due to join buffer exhaustion).
682         if (!empty($individualmembers) || !empty($groupmembers)) {
683             // Now, we want to remove any duplicates from the group members array. For individual members we will
684             // be doing a more extensive call as we want their contact requests as well as privacy information,
685             // which is not necessary for group conversations.
686             $diffgroupmembers = array_diff($groupmembers, $individualmembers);
688             $individualmemberinfo = helper::get_member_info($userid, $individualmembers, true, true);
689             $groupmemberinfo = helper::get_member_info($userid, $diffgroupmembers);
691             // Don't use array_merge, as we lose array keys.
692             $memberinfo = $individualmemberinfo + $groupmemberinfo;
694             if (empty($memberinfo)) {
695                 return [];
696             }
698             // Update the members array with the member information.
699             $deletedmembers = [];
700             foreach ($members as $convid => $memberarr) {
701                 foreach ($memberarr as $key => $memberid) {
702                     if (array_key_exists($memberid, $memberinfo)) {
703                         // If the user is deleted, remember that.
704                         if ($memberinfo[$memberid]->isdeleted) {
705                             $deletedmembers[$convid][] = $memberid;
706                         }
708                         $members[$convid][$key] = clone $memberinfo[$memberid];
710                         if ($conversations[$convid]->conversationtype == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
711                             // Remove data we don't need for group.
712                             $members[$convid][$key]->requirescontact = null;
713                             $members[$convid][$key]->canmessage = null;
714                             $members[$convid][$key]->contactrequests = [];
715                         }
716                     }
717                 }
718             }
719         }
721         // MEMBER COUNT.
722         $cids = array_column($conversations, 'id');
723         list ($cidinsql, $cidinparams) = $DB->get_in_or_equal($cids, SQL_PARAMS_NAMED, 'convid');
724         $membercountsql = "SELECT conversationid, count(DISTINCT userid) AS membercount
725                              FROM {message_conversation_members} mcm
726                             WHERE mcm.conversationid $cidinsql
727                          GROUP BY mcm.conversationid";
728         $membercounts = $DB->get_records_sql($membercountsql, $cidinparams);
730         // UNREAD MESSAGE COUNT.
731         // Finally, let's get the unread messages count for this user so that we can add it
732         // to the conversation. Remember we need to ignore the messages the user sent.
733         $unreadcountssql = 'SELECT m.conversationid, count(m.id) as unreadcount
734                               FROM {messages} m
735                         INNER JOIN {message_conversations} mc
736                                 ON mc.id = m.conversationid
737                         INNER JOIN {message_conversation_members} mcm
738                                 ON m.conversationid = mcm.conversationid
739                          LEFT JOIN {message_user_actions} mua
740                                 ON (mua.messageid = m.id AND mua.userid = ? AND
741                                    (mua.action = ? OR mua.action = ?))
742                              WHERE mcm.userid = ?
743                                AND m.useridfrom != ?
744                                AND mua.id is NULL
745                           GROUP BY m.conversationid';
746         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
747             $userid, $userid]);
749         // Because we'll be calling format_string on each conversation name and passing contexts, we preload them here.
750         // This warms the cache and saves potentially hitting the DB once for each context fetch below.
751         \context_helper::preload_contexts_by_id(array_column($conversations, 'contextid'));
753         // Now, create the final return structure.
754         $arrconversations = [];
755         foreach ($conversations as $conversation) {
756             // Do not include any individual conversations which do not contain a recent message for the user.
757             // This happens if the user has deleted all messages.
758             // Group conversations with deleted users or no messages are always returned.
759             if ($conversation->conversationtype == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL
760                     && (empty($conversation->messageid))) {
761                 continue;
762             }
764             // Exclude 'self' conversations for now.
765             if (isset($selfconversations[$conversation->id])) {
766                 continue;
767             }
769             $conv = new \stdClass();
770             $conv->id = $conversation->id;
772             // Name should be formatted and depends on the context the conversation resides in.
773             // If not set, the context is always context_user.
774             if (is_null($conversation->contextid)) {
775                 $convcontext = \context_user::instance($userid);
776             } else {
777                 $convcontext = \context::instance_by_id($conversation->contextid);
778             }
779             $conv->name = format_string($conversation->conversationname, true, ['context' => $convcontext]);
781             $conv->subname = $convextrafields[$conv->id]['subname'] ?? null;
782             $conv->imageurl = $convextrafields[$conv->id]['imageurl'] ?? null;
783             $conv->type = $conversation->conversationtype;
784             $conv->membercount = $membercounts[$conv->id]->membercount;
785             $conv->isfavourite = in_array($conv->id, $favouriteconversationids);
786             $conv->isread = isset($unreadcounts[$conv->id]) ? false : true;
787             $conv->unreadcount = isset($unreadcounts[$conv->id]) ? $unreadcounts[$conv->id]->unreadcount : null;
788             $conv->members = $members[$conv->id];
790             // Add the most recent message information.
791             $conv->messages = [];
792             if ($conversation->smallmessage) {
793                 $msg = new \stdClass();
794                 $msg->id = $conversation->messageid;
795                 $msg->text = message_format_message_text($conversation);
796                 $msg->useridfrom = $conversation->useridfrom;
797                 $msg->timecreated = $conversation->timecreated;
798                 $conv->messages[] = $msg;
799             }
801             $arrconversations[] = $conv;
802         }
803         return $arrconversations;
804     }
806     /**
807      * Returns all conversations between two users
808      *
809      * @param int $userid1 One of the user's id
810      * @param int $userid2 The other user's id
811      * @param int $limitfrom
812      * @param int $limitnum
813      * @return array
814      * @throws \dml_exception
815      */
816     public static function get_conversations_between_users(int $userid1, int $userid2,
817                                                            int $limitfrom = 0, int $limitnum = 20) : array {
819         global $DB;
821         if ($userid1 == $userid2) {
822             return array();
823         }
825         // Get all conversation where both user1 and user2 are members.
826         // TODO: Add subname value. Waiting for definite table structure.
827         $sql = "SELECT mc.id, mc.type, mc.name, mc.timecreated
828                   FROM {message_conversations} mc
829             INNER JOIN {message_conversation_members} mcm1
830                     ON mc.id = mcm1.conversationid
831             INNER JOIN {message_conversation_members} mcm2
832                     ON mc.id = mcm2.conversationid
833                  WHERE mcm1.userid = :userid1
834                    AND mcm2.userid = :userid2
835                    AND mc.enabled != 0
836               ORDER BY mc.timecreated DESC";
838         return $DB->get_records_sql($sql, array('userid1' => $userid1, 'userid2' => $userid2), $limitfrom, $limitnum);
839     }
841     /**
842      * Return a conversation.
843      *
844      * @param int $userid The user id to get the conversation for
845      * @param int $conversationid The id of the conversation to fetch
846      * @param bool $includecontactrequests Should contact requests be included between members
847      * @param bool $includeprivacyinfo Should privacy info be included between members
848      * @param int $memberlimit Limit number of members to load
849      * @param int $memberoffset Offset members by this amount
850      * @param int $messagelimit Limit number of messages to load
851      * @param int $messageoffset Offset the messages
852      * @param bool $newestmessagesfirst Order messages by newest first
853      * @return \stdClass
854      */
855     public static function get_conversation(
856         int $userid,
857         int $conversationid,
858         bool $includecontactrequests = false,
859         bool $includeprivacyinfo = false,
860         int $memberlimit = 0,
861         int $memberoffset = 0,
862         int $messagelimit = 0,
863         int $messageoffset = 0,
864         bool $newestmessagesfirst = true
865     ) {
866         global $USER, $DB;
868         $systemcontext = \context_system::instance();
869         $canreadallmessages = has_capability('moodle/site:readallmessages', $systemcontext);
870         if (($USER->id != $userid) && !$canreadallmessages) {
871             throw new \moodle_exception('You do not have permission to perform this action.');
872         }
874         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
875         if (!$conversation) {
876             return null;
877         }
879         // Get the context of the conversation. This will be used to check whether the conversation is a favourite.
880         // This will be either 'user' (for individual conversations) or, in the case of linked conversations,
881         // the context stored in the record.
882         $userctx = \context_user::instance($userid);
883         $conversationctx = empty($conversation->contextid) ? $userctx : \context::instance_by_id($conversation->contextid);
885         $isconversationmember = $DB->record_exists(
886             'message_conversation_members',
887             [
888                 'conversationid' => $conversationid,
889                 'userid' => $userid
890             ]
891         );
893         if (!$isconversationmember && !$canreadallmessages) {
894             throw new \moodle_exception('You do not have permission to view this conversation.');
895         }
897         $members = self::get_conversation_members(
898             $userid,
899             $conversationid,
900             $includecontactrequests,
901             $includeprivacyinfo,
902             $memberoffset,
903             $memberlimit
904         );
905         // Strip out the requesting user to match what get_conversations does.
906         $members = array_filter($members, function($member) use ($userid) {
907             return $member->id != $userid;
908         });
910         $messages = self::get_conversation_messages(
911             $userid,
912             $conversationid,
913             $messageoffset,
914             $messagelimit,
915             $newestmessagesfirst ? 'timecreated DESC' : 'timecreated ASC'
916         );
918         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
919         $isfavourite = $service->favourite_exists('core_message', 'message_conversations', $conversationid, $conversationctx);
921         $convextrafields = self::get_linked_conversation_extra_fields([$conversation]);
922         $subname = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['subname'] : null;
923         $imageurl = isset($convextrafields[$conversationid]) ? $convextrafields[$conversationid]['imageurl'] : null;
925         $unreadcountssql = 'SELECT count(m.id)
926                               FROM {messages} m
927                         INNER JOIN {message_conversations} mc
928                                 ON mc.id = m.conversationid
929                          LEFT JOIN {message_user_actions} mua
930                                 ON (mua.messageid = m.id AND mua.userid = ? AND
931                                    (mua.action = ? OR mua.action = ?))
932                              WHERE m.conversationid = ?
933                                AND m.useridfrom != ?
934                                AND mua.id is NULL';
935         $unreadcount = $DB->count_records_sql(
936             $unreadcountssql,
937             [
938                 $userid,
939                 self::MESSAGE_ACTION_READ,
940                 self::MESSAGE_ACTION_DELETED,
941                 $conversationid,
942                 $userid
943             ]
944         );
946         $membercount = $DB->count_records('message_conversation_members', ['conversationid' => $conversationid]);
948         return (object) [
949             'id' => $conversation->id,
950             'name' => $conversation->name,
951             'subname' => $subname,
952             'imageurl' => $imageurl,
953             'type' => $conversation->type,
954             'membercount' => $membercount,
955             'isfavourite' => $isfavourite,
956             'isread' => empty($unreadcount),
957             'unreadcount' => $unreadcount,
958             'members' => $members,
959             'messages' => $messages['messages']
960         ];
961     }
963     /**
964      * Mark a conversation as a favourite for the given user.
965      *
966      * @param int $conversationid the id of the conversation to mark as a favourite.
967      * @param int $userid the id of the user to whom the favourite belongs.
968      * @return favourite the favourite object.
969      * @throws \moodle_exception if the user or conversation don't exist.
970      */
971     public static function set_favourite_conversation(int $conversationid, int $userid) : favourite {
972         global $DB;
974         if (!self::is_user_in_conversation($userid, $conversationid)) {
975             throw new \moodle_exception("Conversation doesn't exist or user is not a member");
976         }
977         // Get the context for this conversation.
978         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
979         $userctx = \context_user::instance($userid);
980         if (empty($conversation->contextid)) {
981             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
982             $conversationctx = $userctx;
983         } else {
984             // If the contextid is defined, the favourite will be added there.
985             $conversationctx = \context::instance_by_id($conversation->contextid);
986         }
988         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
990         if ($favourite = $ufservice->get_favourite('core_message', 'message_conversations', $conversationid, $conversationctx)) {
991             return $favourite;
992         } else {
993             return $ufservice->create_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
994         }
995     }
997     /**
998      * Unset a conversation as a favourite for the given user.
999      *
1000      * @param int $conversationid the id of the conversation to unset as a favourite.
1001      * @param int $userid the id to whom the favourite belongs.
1002      * @throws \moodle_exception if the favourite does not exist for the user.
1003      */
1004     public static function unset_favourite_conversation(int $conversationid, int $userid) {
1005         global $DB;
1007         // Get the context for this conversation.
1008         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid]);
1009         $userctx = \context_user::instance($userid);
1010         if (empty($conversation->contextid)) {
1011             // When the conversation hasn't any contextid value defined, the favourite will be added to the user context.
1012             $conversationctx = $userctx;
1013         } else {
1014             // If the contextid is defined, the favourite will be added there.
1015             $conversationctx = \context::instance_by_id($conversation->contextid);
1016         }
1018         $ufservice = \core_favourites\service_factory::get_service_for_user_context($userctx);
1019         $ufservice->delete_favourite('core_message', 'message_conversations', $conversationid, $conversationctx);
1020     }
1022     /**
1023      * Returns the contacts to display in the contacts area.
1024      *
1025      * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
1026      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
1027      * Followup: MDL-63915
1028      *
1029      * @param int $userid The user id
1030      * @param int $limitfrom
1031      * @param int $limitnum
1032      * @return array
1033      */
1034     public static function get_contacts($userid, $limitfrom = 0, $limitnum = 0) {
1035         global $DB;
1037         $contactids = [];
1038         $sql = "SELECT mc.*
1039                   FROM {message_contacts} mc
1040                  WHERE mc.userid = ? OR mc.contactid = ?
1041               ORDER BY timecreated DESC";
1042         if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1043             foreach ($contacts as $contact) {
1044                 if ($userid == $contact->userid) {
1045                     $contactids[] = $contact->contactid;
1046                 } else {
1047                     $contactids[] = $contact->userid;
1048                 }
1049             }
1050         }
1052         if (!empty($contactids)) {
1053             list($insql, $inparams) = $DB->get_in_or_equal($contactids);
1055             $sql = "SELECT u.*, mub.id as isblocked
1056                       FROM {user} u
1057                  LEFT JOIN {message_users_blocked} mub
1058                         ON u.id = mub.blockeduserid
1059                      WHERE u.id $insql";
1060             if ($contacts = $DB->get_records_sql($sql, $inparams)) {
1061                 $arrcontacts = [];
1062                 foreach ($contacts as $contact) {
1063                     $contact->blocked = $contact->isblocked ? 1 : 0;
1064                     $arrcontacts[] = helper::create_contact($contact);
1065                 }
1067                 return $arrcontacts;
1068             }
1069         }
1071         return [];
1072     }
1074     /**
1075      * Get the contacts for a given user.
1076      *
1077      * @param int $userid
1078      * @param int $limitfrom
1079      * @param int $limitnum
1080      * @return array An array of contacts
1081      */
1082     public static function get_user_contacts(int $userid, int $limitfrom = 0, int $limitnum = 0) {
1083         global $DB;
1085         $sql = "SELECT *
1086                   FROM {message_contacts} mc
1087                  WHERE mc.userid = ? OR mc.contactid = ?
1088               ORDER BY timecreated DESC, id ASC";
1089         if ($contacts = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
1090             $userids = [];
1091             foreach ($contacts as $contact) {
1092                 if ($contact->userid == $userid) {
1093                     $userids[] = $contact->contactid;
1094                 } else {
1095                     $userids[] = $contact->userid;
1096                 }
1097             }
1098             return helper::get_member_info($userid, $userids);
1099         }
1101         return [];
1102     }
1104     /**
1105      * Returns the contacts count.
1106      *
1107      * @param int $userid The user id
1108      * @return array
1109      */
1110     public static function count_contacts(int $userid) : int {
1111         global $DB;
1113         $sql = "SELECT COUNT(id)
1114                   FROM {message_contacts}
1115                  WHERE userid = ? OR contactid = ?";
1116         return $DB->count_records_sql($sql, [$userid, $userid]);
1117     }
1119     /**
1120      * Returns the an array of the users the given user is in a conversation
1121      * with who are a contact and the number of unread messages.
1122      *
1123      * @param int $userid The user id
1124      * @param int $limitfrom
1125      * @param int $limitnum
1126      * @return array
1127      */
1128     public static function get_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1129         global $DB;
1131         $userfields = \user_picture::fields('u', array('lastaccess'));
1132         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1133                               FROM {message_contacts} mc
1134                         INNER JOIN {user} u
1135                                 ON (u.id = mc.contactid OR u.id = mc.userid)
1136                          LEFT JOIN {messages} m
1137                                 ON ((m.useridfrom = mc.contactid OR m.useridfrom = mc.userid) AND m.useridfrom != ?)
1138                          LEFT JOIN {message_conversation_members} mcm
1139                                 ON mcm.conversationid = m.conversationid AND mcm.userid = ? AND mcm.userid != m.useridfrom
1140                          LEFT JOIN {message_user_actions} mua
1141                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1142                          LEFT JOIN {message_users_blocked} mub
1143                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1144                              WHERE mua.id is NULL
1145                                AND mub.id is NULL
1146                                AND (mc.userid = ? OR mc.contactid = ?)
1147                                AND u.id != ?
1148                                AND u.deleted = 0
1149                           GROUP BY $userfields";
1151         return $DB->get_records_sql($unreadcountssql, [$userid, $userid, $userid, self::MESSAGE_ACTION_READ,
1152             $userid, $userid, $userid, $userid], $limitfrom, $limitnum);
1153     }
1155     /**
1156      * Returns the an array of the users the given user is in a conversation
1157      * with who are not a contact and the number of unread messages.
1158      *
1159      * @param int $userid The user id
1160      * @param int $limitfrom
1161      * @param int $limitnum
1162      * @return array
1163      */
1164     public static function get_non_contacts_with_unread_message_count($userid, $limitfrom = 0, $limitnum = 0) {
1165         global $DB;
1167         $userfields = \user_picture::fields('u', array('lastaccess'));
1168         $unreadcountssql = "SELECT $userfields, count(m.id) as messagecount
1169                               FROM {user} u
1170                         INNER JOIN {messages} m
1171                                 ON m.useridfrom = u.id
1172                         INNER JOIN {message_conversation_members} mcm
1173                                 ON mcm.conversationid = m.conversationid
1174                          LEFT JOIN {message_user_actions} mua
1175                                 ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1176                          LEFT JOIN {message_contacts} mc
1177                                 ON (mc.userid = ? AND mc.contactid = u.id)
1178                          LEFT JOIN {message_users_blocked} mub
1179                                 ON (mub.userid = ? AND mub.blockeduserid = u.id)
1180                              WHERE mcm.userid = ?
1181                                AND mcm.userid != m.useridfrom
1182                                AND mua.id is NULL
1183                                AND mub.id is NULL
1184                                AND mc.id is NULL
1185                                AND u.deleted = 0
1186                           GROUP BY $userfields";
1188         return $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, $userid, $userid, $userid],
1189             $limitfrom, $limitnum);
1190     }
1192     /**
1193      * Returns the messages to display in the message area.
1194      *
1195      * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
1196      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
1197      * Followup: MDL-63915
1198      *
1199      * @param int $userid the current user
1200      * @param int $otheruserid the other user
1201      * @param int $limitfrom
1202      * @param int $limitnum
1203      * @param string $sort
1204      * @param int $timefrom the time from the message being sent
1205      * @param int $timeto the time up until the message being sent
1206      * @return array
1207      */
1208     public static function get_messages($userid, $otheruserid, $limitfrom = 0, $limitnum = 0,
1209             $sort = 'timecreated ASC', $timefrom = 0, $timeto = 0) {
1211         if (!empty($timefrom)) {
1212             // Get the conversation between userid and otheruserid.
1213             $userids = [$userid, $otheruserid];
1214             if (!$conversationid = self::get_conversation_between_users($userids)) {
1215                 // This method was always used for individual conversations.
1216                 $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
1217                 $conversationid = $conversation->id;
1218             }
1220             // Check the cache to see if we even need to do a DB query.
1221             $cache = \cache::make('core', 'message_time_last_message_between_users');
1222             $key = helper::get_last_message_time_created_cache_key($conversationid);
1223             $lastcreated = $cache->get($key);
1225             // The last known message time is earlier than the one being requested so we can
1226             // just return an empty result set rather than having to query the DB.
1227             if ($lastcreated && $lastcreated < $timefrom) {
1228                 return [];
1229             }
1230         }
1232         $arrmessages = array();
1233         if ($messages = helper::get_messages($userid, $otheruserid, 0, $limitfrom, $limitnum,
1234                                              $sort, $timefrom, $timeto)) {
1235             $arrmessages = helper::create_messages($userid, $messages);
1236         }
1238         return $arrmessages;
1239     }
1241     /**
1242      * Returns the messages for the defined conversation.
1243      *
1244      * @param  int $userid The current user.
1245      * @param  int $convid The conversation where the messages belong. Could be an object or just the id.
1246      * @param  int $limitfrom Return a subset of records, starting at this point (optional).
1247      * @param  int $limitnum Return a subset comprising this many records in total (optional, required if $limitfrom is set).
1248      * @param  string $sort The column name to order by including optionally direction.
1249      * @param  int $timefrom The time from the message being sent.
1250      * @param  int $timeto The time up until the message being sent.
1251      * @return array of messages
1252      */
1253     public static function get_conversation_messages(int $userid, int $convid, int $limitfrom = 0, int $limitnum = 0,
1254         string $sort = 'timecreated ASC', int $timefrom = 0, int $timeto = 0) : array {
1256         if (!empty($timefrom)) {
1257             // Check the cache to see if we even need to do a DB query.
1258             $cache = \cache::make('core', 'message_time_last_message_between_users');
1259             $key = helper::get_last_message_time_created_cache_key($convid);
1260             $lastcreated = $cache->get($key);
1262             // The last known message time is earlier than the one being requested so we can
1263             // just return an empty result set rather than having to query the DB.
1264             if ($lastcreated && $lastcreated < $timefrom) {
1265                 return [];
1266             }
1267         }
1269         $messages = helper::get_conversation_messages($userid, $convid, 0, $limitfrom, $limitnum, $sort, $timefrom, $timeto);
1270         return helper::format_conversation_messages($userid, $convid, $messages);
1271     }
1273     /**
1274      * Returns the most recent message between two users.
1275      *
1276      * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
1277      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
1278      * Followup: MDL-63915
1279      *
1280      * @param int $userid the current user
1281      * @param int $otheruserid the other user
1282      * @return \stdClass|null
1283      */
1284     public static function get_most_recent_message($userid, $otheruserid) {
1285         // We want two messages here so we get an accurate 'blocktime' value.
1286         if ($messages = helper::get_messages($userid, $otheruserid, 0, 0, 2, 'timecreated DESC')) {
1287             // Swap the order so we now have them in historical order.
1288             $messages = array_reverse($messages);
1289             $arrmessages = helper::create_messages($userid, $messages);
1290             return array_pop($arrmessages);
1291         }
1293         return null;
1294     }
1296     /**
1297      * Returns the most recent message in a conversation.
1298      *
1299      * @param int $convid The conversation identifier.
1300      * @param int $currentuserid The current user identifier.
1301      * @return \stdClass|null The most recent message.
1302      */
1303     public static function get_most_recent_conversation_message(int $convid, int $currentuserid = 0) {
1304         global $USER;
1306         if (empty($currentuserid)) {
1307             $currentuserid = $USER->id;
1308         }
1310         if ($messages = helper::get_conversation_messages($currentuserid, $convid, 0, 0, 1, 'timecreated DESC')) {
1311             $convmessages = helper::format_conversation_messages($currentuserid, $convid, $messages);
1312             return array_pop($convmessages['messages']);
1313         }
1315         return null;
1316     }
1318     /**
1319      * Returns the profile information for a contact for a user.
1320      *
1321      * TODO: This function should be removed once the new group messaging UI is in place and the old messaging UI is removed.
1322      * For now we are not removing/deprecating this function for backwards compatibility with messaging UI.
1323      * Followup: MDL-63915
1324      *
1325      * @param int $userid The user id
1326      * @param int $otheruserid The id of the user whose profile we want to view.
1327      * @return \stdClass
1328      */
1329     public static function get_profile($userid, $otheruserid) {
1330         global $CFG, $PAGE;
1332         require_once($CFG->dirroot . '/user/lib.php');
1334         $user = \core_user::get_user($otheruserid, '*', MUST_EXIST);
1336         // Create the data we are going to pass to the renderable.
1337         $data = new \stdClass();
1338         $data->userid = $otheruserid;
1339         $data->fullname = fullname($user);
1340         $data->city = '';
1341         $data->country = '';
1342         $data->email = '';
1343         $data->isonline = null;
1344         // Get the user picture data - messaging has always shown these to the user.
1345         $userpicture = new \user_picture($user);
1346         $userpicture->size = 1; // Size f1.
1347         $data->profileimageurl = $userpicture->get_url($PAGE)->out(false);
1348         $userpicture->size = 0; // Size f2.
1349         $data->profileimageurlsmall = $userpicture->get_url($PAGE)->out(false);
1351         $userfields = user_get_user_details($user, null, array('city', 'country', 'email', 'lastaccess'));
1352         if ($userfields) {
1353             if (isset($userfields['city'])) {
1354                 $data->city = $userfields['city'];
1355             }
1356             if (isset($userfields['country'])) {
1357                 $data->country = $userfields['country'];
1358             }
1359             if (isset($userfields['email'])) {
1360                 $data->email = $userfields['email'];
1361             }
1362             if (isset($userfields['lastaccess'])) {
1363                 $data->isonline = helper::is_online($userfields['lastaccess']);
1364             }
1365         }
1367         $data->isblocked = self::is_blocked($userid, $otheruserid);
1368         $data->iscontact = self::is_contact($userid, $otheruserid);
1370         return $data;
1371     }
1373     /**
1374      * Checks if a user can delete messages they have either received or sent.
1375      *
1376      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1377      *  but will still seem as if it was by the user)
1378      * @param int $conversationid The id of the conversation
1379      * @return bool Returns true if a user can delete the conversation, false otherwise.
1380      */
1381     public static function can_delete_conversation(int $userid, int $conversationid = null) : bool {
1382         global $USER;
1384         if (is_null($conversationid)) {
1385             debugging('\core_message\api::can_delete_conversation() now expects a \'conversationid\' to be passed.',
1386                 DEBUG_DEVELOPER);
1387             return false;
1388         }
1390         $systemcontext = \context_system::instance();
1392         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
1393             return true;
1394         }
1396         if (!self::is_user_in_conversation($userid, $conversationid)) {
1397             return false;
1398         }
1400         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
1401                 $USER->id == $userid) {
1402             return true;
1403         }
1405         return false;
1406     }
1408     /**
1409      * Deletes a conversation.
1410      *
1411      * This function does not verify any permissions.
1412      *
1413      * @deprecated since 3.6
1414      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1415      *  but will still seem as if it was by the user)
1416      * @param int $otheruserid The id of the other user in the conversation
1417      * @return bool
1418      */
1419     public static function delete_conversation($userid, $otheruserid) {
1420         debugging('\core_message\api::delete_conversation() is deprecated, please use ' .
1421             '\core_message\api::delete_conversation_by_id() instead.', DEBUG_DEVELOPER);
1423         $conversationid = self::get_conversation_between_users([$userid, $otheruserid]);
1425         // If there is no conversation, there is nothing to do.
1426         if (!$conversationid) {
1427             return true;
1428         }
1430         self::delete_conversation_by_id($userid, $conversationid);
1432         return true;
1433     }
1435     /**
1436      * Deletes a conversation for a specified user.
1437      *
1438      * This function does not verify any permissions.
1439      *
1440      * @param int $userid The user id of who we want to delete the messages for (this may be done by the admin
1441      *  but will still seem as if it was by the user)
1442      * @param int $conversationid The id of the other user in the conversation
1443      */
1444     public static function delete_conversation_by_id(int $userid, int $conversationid) {
1445         global $DB, $USER;
1447         // Get all messages belonging to this conversation that have not already been deleted by this user.
1448         $sql = "SELECT m.*
1449                  FROM {messages} m
1450            INNER JOIN {message_conversations} mc
1451                    ON m.conversationid = mc.id
1452             LEFT JOIN {message_user_actions} mua
1453                    ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1454                 WHERE mua.id is NULL
1455                   AND mc.id = ?
1456              ORDER BY m.timecreated ASC";
1457         $messages = $DB->get_records_sql($sql, [$userid, self::MESSAGE_ACTION_DELETED, $conversationid]);
1459         // Ok, mark these as deleted.
1460         foreach ($messages as $message) {
1461             $mua = new \stdClass();
1462             $mua->userid = $userid;
1463             $mua->messageid = $message->id;
1464             $mua->action = self::MESSAGE_ACTION_DELETED;
1465             $mua->timecreated = time();
1466             $mua->id = $DB->insert_record('message_user_actions', $mua);
1468             \core\event\message_deleted::create_from_ids($userid, $USER->id,
1469                 $message->id, $mua->id)->trigger();
1470         }
1471     }
1473     /**
1474      * Returns the count of unread conversations (collection of messages from a single user) for
1475      * the given user.
1476      *
1477      * @param \stdClass $user the user who's conversations should be counted
1478      * @return int the count of the user's unread conversations
1479      */
1480     public static function count_unread_conversations($user = null) {
1481         global $USER, $DB;
1483         if (empty($user)) {
1484             $user = $USER;
1485         }
1487         $sql = "SELECT COUNT(DISTINCT(m.conversationid))
1488                   FROM {messages} m
1489             INNER JOIN {message_conversations} mc
1490                     ON m.conversationid = mc.id
1491             INNER JOIN {message_conversation_members} mcm
1492                     ON mc.id = mcm.conversationid
1493              LEFT JOIN {message_user_actions} mua
1494                     ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1495                  WHERE mcm.userid = ?
1496                    AND mc.enabled = ?
1497                    AND mcm.userid != m.useridfrom
1498                    AND mua.id is NULL";
1500         return $DB->count_records_sql($sql, [$user->id, self::MESSAGE_ACTION_READ, $user->id,
1501             self::MESSAGE_CONVERSATION_ENABLED]);
1502     }
1504     /**
1505      * Checks if a user can mark all messages as read.
1506      *
1507      * @param int $userid The user id of who we want to mark the messages for
1508      * @param int $conversationid The id of the conversation
1509      * @return bool true if user is permitted, false otherwise
1510      * @since 3.6
1511      */
1512     public static function can_mark_all_messages_as_read(int $userid, int $conversationid) : bool {
1513         global $USER;
1515         $systemcontext = \context_system::instance();
1517         if (has_capability('moodle/site:readallmessages', $systemcontext)) {
1518             return true;
1519         }
1521         if (!self::is_user_in_conversation($userid, $conversationid)) {
1522             return false;
1523         }
1525         if ($USER->id == $userid) {
1526             return true;
1527         }
1529         return false;
1530     }
1532     /**
1533      * Returns the count of conversations (collection of messages from a single user) for
1534      * the given user.
1535      *
1536      * @param int $userid The user whose conversations should be counted.
1537      * @return array the array of conversations counts, indexed by type.
1538      */
1539     public static function get_conversation_counts(int $userid) : array {
1540         global $DB;
1542         // Some restrictions we need to be aware of:
1543         // - Individual conversations containing soft-deleted user must be counted.
1544         // - Individual conversations containing only deleted messages must NOT be counted.
1545         // - Individual conversations which are legacy 'self' conversations (2 members, both the same user) must NOT be counted.
1546         // - Group conversations with 0 messages must be counted.
1547         // - Linked conversations which are disabled (enabled = 0) must NOT be counted.
1548         // - Any type of conversation can be included in the favourites count, however, the type counts and the favourites count
1549         // are mutually exclusive; any conversations which are counted in favourites cannot be counted elsewhere.
1551         // First, ask the favourites service to give us the join SQL for favourited conversations,
1552         // so we can include favourite information in the query.
1553         $usercontext = \context_user::instance($userid);
1554         $favservice = \core_favourites\service_factory::get_service_for_user_context($usercontext);
1555         list($favsql, $favparams) = $favservice->get_join_sql_by_type('core_message', 'message_conversations', 'fav', 'mc.id');
1557         $sql = "SELECT mc.type, fav.itemtype, COUNT(DISTINCT mc.id) as count
1558                   FROM {message_conversations} mc
1559             INNER JOIN {message_conversation_members} mcm
1560                     ON mcm.conversationid = mc.id
1561             INNER JOIN (
1562                               SELECT mcm.conversationid, count(distinct mcm.userid) as membercount
1563                                 FROM {message_conversation_members} mcm
1564                                WHERE mcm.conversationid IN (
1565                                         SELECT DISTINCT conversationid
1566                                           FROM {message_conversation_members} mcm2
1567                                          WHERE userid = :userid5
1568                                      )
1569                             GROUP BY mcm.conversationid
1570                        ) uniquemembercount
1571                     ON uniquemembercount.conversationid = mc.id
1572              LEFT JOIN (
1573                               SELECT m.conversationid as convid, MAX(m.timecreated) as maxtime
1574                                 FROM {messages} m
1575                           INNER JOIN {message_conversation_members} mcm
1576                                   ON mcm.conversationid = m.conversationid
1577                            LEFT JOIN {message_user_actions} mua
1578                                   ON (mua.messageid = m.id AND mua.userid = :userid AND mua.action = :action)
1579                                WHERE mua.id is NULL
1580                                  AND mcm.userid = :userid2
1581                             GROUP BY m.conversationid
1582                        ) maxvisibleconvmessage
1583                     ON maxvisibleconvmessage.convid = mc.id
1584                $favsql
1585                  WHERE mcm.userid = :userid3
1586                    AND mc.enabled = :enabled
1587                    AND (
1588                           (mc.type = :individualtype AND maxvisibleconvmessage.convid IS NOT NULL AND membercount > 1) OR
1589                           (mc.type = :grouptype)
1590                        )
1591               GROUP BY mc.type, fav.itemtype
1592               ORDER BY mc.type ASC";
1594         $params = [
1595             'userid' => $userid,
1596             'userid2' => $userid,
1597             'userid3' => $userid,
1598             'userid4' => $userid,
1599             'userid5' => $userid,
1600             'action' => self::MESSAGE_ACTION_DELETED,
1601             'enabled' => self::MESSAGE_CONVERSATION_ENABLED,
1602             'individualtype' => self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
1603             'grouptype' => self::MESSAGE_CONVERSATION_TYPE_GROUP,
1604         ] + $favparams;
1606         // Assemble the return array.
1607         $counts = [
1608             'favourites' => 0,
1609             'types' => [
1610                 self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
1611                 self::MESSAGE_CONVERSATION_TYPE_GROUP => 0
1612             ]
1613         ];
1615         $countsrs = $DB->get_recordset_sql($sql, $params);
1616         foreach ($countsrs as $key => $val) {
1617             if (!empty($val->itemtype)) {
1618                 $counts['favourites'] += $val->count;
1619                 continue;
1620             }
1621             $counts['types'][$val->type] = $val->count;
1622         }
1623         $countsrs->close();
1625         return $counts;
1626     }
1628     /**
1629      * Marks all messages being sent to a user in a particular conversation.
1630      *
1631      * If $conversationdid is null then it marks all messages as read sent to $userid.
1632      *
1633      * @param int $userid
1634      * @param int|null $conversationid The conversation the messages belong to mark as read, if null mark all
1635      */
1636     public static function mark_all_messages_as_read($userid, $conversationid = null) {
1637         global $DB;
1639         $messagesql = "SELECT m.*
1640                          FROM {messages} m
1641                    INNER JOIN {message_conversations} mc
1642                            ON mc.id = m.conversationid
1643                    INNER JOIN {message_conversation_members} mcm
1644                            ON mcm.conversationid = mc.id
1645                     LEFT JOIN {message_user_actions} mua
1646                            ON (mua.messageid = m.id AND mua.userid = ? AND mua.action = ?)
1647                         WHERE mua.id is NULL
1648                           AND mcm.userid = ?
1649                           AND m.useridfrom != ?";
1650         $messageparams = [];
1651         $messageparams[] = $userid;
1652         $messageparams[] = self::MESSAGE_ACTION_READ;
1653         $messageparams[] = $userid;
1654         $messageparams[] = $userid;
1655         if (!is_null($conversationid)) {
1656             $messagesql .= " AND mc.id = ?";
1657             $messageparams[] = $conversationid;
1658         }
1660         $messages = $DB->get_recordset_sql($messagesql, $messageparams);
1661         foreach ($messages as $message) {
1662             self::mark_message_as_read($userid, $message);
1663         }
1664         $messages->close();
1665     }
1667     /**
1668      * Marks all notifications being sent from one user to another user as read.
1669      *
1670      * If the from user is null then it marks all notifications as read sent to the to user.
1671      *
1672      * @param int $touserid the id of the message recipient
1673      * @param int|null $fromuserid the id of the message sender, null if all messages
1674      * @return void
1675      */
1676     public static function mark_all_notifications_as_read($touserid, $fromuserid = null) {
1677         global $DB;
1679         $notificationsql = "SELECT n.*
1680                               FROM {notifications} n
1681                              WHERE useridto = ?
1682                                AND timeread is NULL";
1683         $notificationsparams = [$touserid];
1684         if (!empty($fromuserid)) {
1685             $notificationsql .= " AND useridfrom = ?";
1686             $notificationsparams[] = $fromuserid;
1687         }
1689         $notifications = $DB->get_recordset_sql($notificationsql, $notificationsparams);
1690         foreach ($notifications as $notification) {
1691             self::mark_notification_as_read($notification);
1692         }
1693         $notifications->close();
1694     }
1696     /**
1697      * Marks ALL messages being sent from $fromuserid to $touserid as read.
1698      *
1699      * Can be filtered by type.
1700      *
1701      * @deprecated since 3.5
1702      * @param int $touserid the id of the message recipient
1703      * @param int $fromuserid the id of the message sender
1704      * @param string $type filter the messages by type, either MESSAGE_TYPE_NOTIFICATION, MESSAGE_TYPE_MESSAGE or '' for all.
1705      * @return void
1706      */
1707     public static function mark_all_read_for_user($touserid, $fromuserid = 0, $type = '') {
1708         debugging('\core_message\api::mark_all_read_for_user is deprecated. Please either use ' .
1709             '\core_message\api::mark_all_notifications_read_for_user or \core_message\api::mark_all_messages_read_for_user',
1710             DEBUG_DEVELOPER);
1712         $type = strtolower($type);
1714         $conversationid = null;
1715         $ignoremessages = false;
1716         if (!empty($fromuserid)) {
1717             $conversationid = self::get_conversation_between_users([$touserid, $fromuserid]);
1718             if (!$conversationid) { // If there is no conversation between the users then there are no messages to mark.
1719                 $ignoremessages = true;
1720             }
1721         }
1723         if (!empty($type)) {
1724             if ($type == MESSAGE_TYPE_NOTIFICATION) {
1725                 self::mark_all_notifications_as_read($touserid, $fromuserid);
1726             } else if ($type == MESSAGE_TYPE_MESSAGE) {
1727                 if (!$ignoremessages) {
1728                     self::mark_all_messages_as_read($touserid, $conversationid);
1729                 }
1730             }
1731         } else { // We want both.
1732             self::mark_all_notifications_as_read($touserid, $fromuserid);
1733             if (!$ignoremessages) {
1734                 self::mark_all_messages_as_read($touserid, $conversationid);
1735             }
1736         }
1737     }
1739     /**
1740      * Returns message preferences.
1741      *
1742      * @param array $processors
1743      * @param array $providers
1744      * @param \stdClass $user
1745      * @return \stdClass
1746      * @since 3.2
1747      */
1748     public static function get_all_message_preferences($processors, $providers, $user) {
1749         $preferences = helper::get_providers_preferences($providers, $user->id);
1750         $preferences->userdefaultemail = $user->email; // May be displayed by the email processor.
1752         // For every processors put its options on the form (need to get function from processor's lib.php).
1753         foreach ($processors as $processor) {
1754             $processor->object->load_data($preferences, $user->id);
1755         }
1757         // Load general messaging preferences.
1758         $preferences->blocknoncontacts = self::get_user_privacy_messaging_preference($user->id);
1759         $preferences->mailformat = $user->mailformat;
1760         $preferences->mailcharset = get_user_preferences('mailcharset', '', $user->id);
1762         return $preferences;
1763     }
1765     /**
1766      * Count the number of users blocked by a user.
1767      *
1768      * @param \stdClass $user The user object
1769      * @return int the number of blocked users
1770      */
1771     public static function count_blocked_users($user = null) {
1772         global $USER, $DB;
1774         if (empty($user)) {
1775             $user = $USER;
1776         }
1778         $sql = "SELECT count(mub.id)
1779                   FROM {message_users_blocked} mub
1780                  WHERE mub.userid = :userid";
1781         return $DB->count_records_sql($sql, array('userid' => $user->id));
1782     }
1784     /**
1785      * Determines if a user is permitted to send another user a private message.
1786      * If no sender is provided then it defaults to the logged in user.
1787      *
1788      * @param \stdClass $recipient The user object.
1789      * @param \stdClass|null $sender The user object.
1790      * @return bool true if user is permitted, false otherwise.
1791      */
1792     public static function can_post_message($recipient, $sender = null) {
1793         global $USER;
1795         if (is_null($sender)) {
1796             // The message is from the logged in user, unless otherwise specified.
1797             $sender = $USER;
1798         }
1800         $systemcontext = \context_system::instance();
1801         if (!has_capability('moodle/site:sendmessage', $systemcontext, $sender)) {
1802             return false;
1803         }
1805         if (has_capability('moodle/site:readallmessages', $systemcontext, $sender->id)) {
1806             return true;
1807         }
1809         // Check if the recipient can be messaged by the sender.
1810         return (self::can_contact_user($recipient->id, $sender->id));
1811     }
1813     /**
1814      * Determines if a user is permitted to send a message to a given conversation.
1815      * If no sender is provided then it defaults to the logged in user.
1816      *
1817      * @param int $userid the id of the user on which the checks will be applied.
1818      * @param int $conversationid the id of the conversation we wish to check.
1819      * @return bool true if the user can send a message to the conversation, false otherwise.
1820      * @throws \moodle_exception
1821      */
1822     public static function can_send_message_to_conversation(int $userid, int $conversationid) : bool {
1823         global $DB;
1825         $systemcontext = \context_system::instance();
1826         if (!has_capability('moodle/site:sendmessage', $systemcontext, $userid)) {
1827             return false;
1828         }
1830         if (!self::is_user_in_conversation($userid, $conversationid)) {
1831             return false;
1832         }
1834         // User can post messages and is in the conversation, but we need to check the conversation type to
1835         // know whether or not to check the user privacy settings via can_contact_user().
1836         $conversation = $DB->get_record('message_conversations', ['id' => $conversationid], '*', MUST_EXIST);
1837         if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_GROUP) {
1838             return true;
1839         } else if ($conversation->type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
1840             // Get the other user in the conversation.
1841             $members = self::get_conversation_members($userid, $conversationid);
1842             $otheruser = array_filter($members, function($member) use($userid) {
1843                 return $member->id != $userid;
1844             });
1845             $otheruser = reset($otheruser);
1847             return self::can_contact_user($otheruser->id, $userid);
1848         } else {
1849             throw new \moodle_exception("Invalid conversation type '$conversation->type'.");
1850         }
1851     }
1853     /**
1854      * Send a message from a user to a conversation.
1855      *
1856      * This method will create the basic eventdata and delegate to message creation to message_send.
1857      * The message_send() method is responsible for event data that is specific to each recipient.
1858      *
1859      * @param int $userid the sender id.
1860      * @param int $conversationid the conversation id.
1861      * @param string $message the message to send.
1862      * @param int $format the format of the message to send.
1863      * @return \stdClass the message created.
1864      * @throws \coding_exception
1865      * @throws \moodle_exception if the user is not permitted to send a message to the conversation.
1866      */
1867     public static function send_message_to_conversation(int $userid, int $conversationid, string $message,
1868                                                         int $format) : \stdClass {
1869         global $DB;
1871         if (!self::can_send_message_to_conversation($userid, $conversationid)) {
1872             throw new \moodle_exception("User $userid cannot send a message to conversation $conversationid");
1873         }
1875         $eventdata = new \core\message\message();
1876         $eventdata->courseid         = 1;
1877         $eventdata->component        = 'moodle';
1878         $eventdata->name             = 'instantmessage';
1879         $eventdata->userfrom         = $userid;
1880         $eventdata->convid           = $conversationid;
1882         if ($format == FORMAT_HTML) {
1883             $eventdata->fullmessagehtml  = $message;
1884             // Some message processors may revert to sending plain text even if html is supplied,
1885             // so we keep both plain and html versions if we're intending to send html.
1886             $eventdata->fullmessage = html_to_text($eventdata->fullmessagehtml);
1887         } else {
1888             $eventdata->fullmessage      = $message;
1889             $eventdata->fullmessagehtml  = '';
1890         }
1892         $eventdata->fullmessageformat = $format;
1893         $eventdata->smallmessage = $message; // Store the message unfiltered. Clean up on output.
1895         $eventdata->timecreated     = time();
1896         $eventdata->notification    = 0;
1897         $messageid = message_send($eventdata);
1899         $messagerecord = $DB->get_record('messages', ['id' => $messageid], 'id, useridfrom, fullmessage, timecreated');
1900         $message = (object) [
1901             'id' => $messagerecord->id,
1902             'useridfrom' => $messagerecord->useridfrom,
1903             'text' => $messagerecord->fullmessage,
1904             'timecreated' => $messagerecord->timecreated
1905         ];
1906         return $message;
1907     }
1909     /**
1910      * Get the messaging preference for a user.
1911      * If the user has not any messaging privacy preference:
1912      * - When $CFG->messagingallusers = false the default user preference is MESSAGE_PRIVACY_COURSEMEMBER.
1913      * - When $CFG->messagingallusers = true the default user preference is MESSAGE_PRIVACY_SITE.
1914      *
1915      * @param  int    $userid The user identifier.
1916      * @return int    The default messaging preference.
1917      */
1918     public static function get_user_privacy_messaging_preference(int $userid) : int {
1919         global $CFG, $USER;
1921         // When $CFG->messagingallusers is enabled, default value for the messaging preference will be "Anyone on the site";
1922         // otherwise, the default value will be "My contacts and anyone in my courses".
1923         if (empty($CFG->messagingallusers)) {
1924             $defaultprefvalue = self::MESSAGE_PRIVACY_COURSEMEMBER;
1925         } else {
1926             $defaultprefvalue = self::MESSAGE_PRIVACY_SITE;
1927         }
1928         if ($userid == $USER->id) {
1929             $user = $USER;
1930         } else {
1931             $user = $userid;
1932         }
1933         $privacypreference = get_user_preferences('message_blocknoncontacts', $defaultprefvalue, $user);
1935         // When the $CFG->messagingallusers privacy setting is disabled, MESSAGE_PRIVACY_SITE is
1936         // also disabled, so it has to be replaced to MESSAGE_PRIVACY_COURSEMEMBER.
1937         if (empty($CFG->messagingallusers) && $privacypreference == self::MESSAGE_PRIVACY_SITE) {
1938             $privacypreference = self::MESSAGE_PRIVACY_COURSEMEMBER;
1939         }
1941         return $privacypreference;
1942     }
1944     /**
1945      * Checks if the recipient is allowing messages from users that aren't a
1946      * contact. If not then it checks to make sure the sender is in the
1947      * recipient's contacts.
1948      *
1949      * @deprecated since 3.6
1950      * @param \stdClass $recipient The user object.
1951      * @param \stdClass|null $sender The user object.
1952      * @return bool true if $sender is blocked, false otherwise.
1953      */
1954     public static function is_user_non_contact_blocked($recipient, $sender = null) {
1955         debugging('\core_message\api::is_user_non_contact_blocked() is deprecated', DEBUG_DEVELOPER);
1957         global $USER, $CFG;
1959         if (is_null($sender)) {
1960             // The message is from the logged in user, unless otherwise specified.
1961             $sender = $USER;
1962         }
1964         $privacypreference = self::get_user_privacy_messaging_preference($recipient->id);
1965         switch ($privacypreference) {
1966             case self::MESSAGE_PRIVACY_SITE:
1967                 if (!empty($CFG->messagingallusers)) {
1968                     // Users can be messaged without being contacts or members of the same course.
1969                     break;
1970                 }
1971                 // When the $CFG->messagingallusers privacy setting is disabled, continue with the next
1972                 // case, because MESSAGE_PRIVACY_SITE is replaced to MESSAGE_PRIVACY_COURSEMEMBER.
1973             case self::MESSAGE_PRIVACY_COURSEMEMBER:
1974                 // Confirm the sender and the recipient are both members of the same course.
1975                 if (enrol_sharing_course($recipient, $sender)) {
1976                     // All good, the recipient and the sender are members of the same course.
1977                     return false;
1978                 }
1979             case self::MESSAGE_PRIVACY_ONLYCONTACTS:
1980                 // True if they aren't contacts (they can't send a message because of the privacy settings), false otherwise.
1981                 return !self::is_contact($sender->id, $recipient->id);
1982         }
1984         return false;
1985     }
1987     /**
1988      * Checks if the recipient has specifically blocked the sending user.
1989      *
1990      * Note: This function will always return false if the sender has the
1991      * readallmessages capability at the system context level.
1992      *
1993      * @deprecated since 3.6
1994      * @param int $recipientid User ID of the recipient.
1995      * @param int $senderid User ID of the sender.
1996      * @return bool true if $sender is blocked, false otherwise.
1997      */
1998     public static function is_user_blocked($recipientid, $senderid = null) {
1999         debugging('\core_message\api::is_user_blocked is deprecated and should not be used.',
2000             DEBUG_DEVELOPER);
2002         global $USER;
2004         if (is_null($senderid)) {
2005             // The message is from the logged in user, unless otherwise specified.
2006             $senderid = $USER->id;
2007         }
2009         $systemcontext = \context_system::instance();
2010         if (has_capability('moodle/site:readallmessages', $systemcontext, $senderid)) {
2011             return false;
2012         }
2014         if (self::is_blocked($recipientid, $senderid)) {
2015             return true;
2016         }
2018         return false;
2019     }
2021     /**
2022      * Get specified message processor, validate corresponding plugin existence and
2023      * system configuration.
2024      *
2025      * @param string $name  Name of the processor.
2026      * @param bool $ready only return ready-to-use processors.
2027      * @return mixed $processor if processor present else empty array.
2028      * @since Moodle 3.2
2029      */
2030     public static function get_message_processor($name, $ready = false) {
2031         global $DB, $CFG;
2033         $processor = $DB->get_record('message_processors', array('name' => $name));
2034         if (empty($processor)) {
2035             // Processor not found, return.
2036             return array();
2037         }
2039         $processor = self::get_processed_processor_object($processor);
2040         if ($ready) {
2041             if ($processor->enabled && $processor->configured) {
2042                 return $processor;
2043             } else {
2044                 return array();
2045             }
2046         } else {
2047             return $processor;
2048         }
2049     }
2051     /**
2052      * Returns weather a given processor is enabled or not.
2053      * Note:- This doesn't check if the processor is configured or not.
2054      *
2055      * @param string $name Name of the processor
2056      * @return bool
2057      */
2058     public static function is_processor_enabled($name) {
2060         $cache = \cache::make('core', 'message_processors_enabled');
2061         $status = $cache->get($name);
2063         if ($status === false) {
2064             $processor = self::get_message_processor($name);
2065             if (!empty($processor)) {
2066                 $cache->set($name, $processor->enabled);
2067                 return $processor->enabled;
2068             } else {
2069                 return false;
2070             }
2071         }
2073         return $status;
2074     }
2076     /**
2077      * Set status of a processor.
2078      *
2079      * @param \stdClass $processor processor record.
2080      * @param 0|1 $enabled 0 or 1 to set the processor status.
2081      * @return bool
2082      * @since Moodle 3.2
2083      */
2084     public static function update_processor_status($processor, $enabled) {
2085         global $DB;
2086         $cache = \cache::make('core', 'message_processors_enabled');
2087         $cache->delete($processor->name);
2088         return $DB->set_field('message_processors', 'enabled', $enabled, array('id' => $processor->id));
2089     }
2091     /**
2092      * Given a processor object, loads information about it's settings and configurations.
2093      * This is not a public api, instead use @see \core_message\api::get_message_processor()
2094      * or @see \get_message_processors()
2095      *
2096      * @param \stdClass $processor processor object
2097      * @return \stdClass processed processor object
2098      * @since Moodle 3.2
2099      */
2100     public static function get_processed_processor_object(\stdClass $processor) {
2101         global $CFG;
2103         $processorfile = $CFG->dirroot. '/message/output/'.$processor->name.'/message_output_'.$processor->name.'.php';
2104         if (is_readable($processorfile)) {
2105             include_once($processorfile);
2106             $processclass = 'message_output_' . $processor->name;
2107             if (class_exists($processclass)) {
2108                 $pclass = new $processclass();
2109                 $processor->object = $pclass;
2110                 $processor->configured = 0;
2111                 if ($pclass->is_system_configured()) {
2112                     $processor->configured = 1;
2113                 }
2114                 $processor->hassettings = 0;
2115                 if (is_readable($CFG->dirroot.'/message/output/'.$processor->name.'/settings.php')) {
2116                     $processor->hassettings = 1;
2117                 }
2118                 $processor->available = 1;
2119             } else {
2120                 print_error('errorcallingprocessor', 'message');
2121             }
2122         } else {
2123             $processor->available = 0;
2124         }
2125         return $processor;
2126     }
2128     /**
2129      * Retrieve users blocked by $user1
2130      *
2131      * @param int $userid The user id of the user whos blocked users we are returning
2132      * @return array the users blocked
2133      */
2134     public static function get_blocked_users($userid) {
2135         global $DB;
2137         $userfields = \user_picture::fields('u', array('lastaccess'));
2138         $blockeduserssql = "SELECT $userfields
2139                               FROM {message_users_blocked} mub
2140                         INNER JOIN {user} u
2141                                 ON u.id = mub.blockeduserid
2142                              WHERE u.deleted = 0
2143                                AND mub.userid = ?
2144                           GROUP BY $userfields
2145                           ORDER BY u.firstname ASC";
2146         return $DB->get_records_sql($blockeduserssql, [$userid]);
2147     }
2149     /**
2150      * Mark a single message as read.
2151      *
2152      * @param int $userid The user id who marked the message as read
2153      * @param \stdClass $message The message
2154      * @param int|null $timeread The time the message was marked as read, if null will default to time()
2155      */
2156     public static function mark_message_as_read($userid, $message, $timeread = null) {
2157         global $DB;
2159         if (is_null($timeread)) {
2160             $timeread = time();
2161         }
2163         $mua = new \stdClass();
2164         $mua->userid = $userid;
2165         $mua->messageid = $message->id;
2166         $mua->action = self::MESSAGE_ACTION_READ;
2167         $mua->timecreated = $timeread;
2168         $mua->id = $DB->insert_record('message_user_actions', $mua);
2170         // Get the context for the user who received the message.
2171         $context = \context_user::instance($userid, IGNORE_MISSING);
2172         // If the user no longer exists the context value will be false, in this case use the system context.
2173         if ($context === false) {
2174             $context = \context_system::instance();
2175         }
2177         // Trigger event for reading a message.
2178         $event = \core\event\message_viewed::create(array(
2179             'objectid' => $mua->id,
2180             'userid' => $userid, // Using the user who read the message as they are the ones performing the action.
2181             'context' => $context,
2182             'relateduserid' => $message->useridfrom,
2183             'other' => array(
2184                 'messageid' => $message->id
2185             )
2186         ));
2187         $event->trigger();
2188     }
2190     /**
2191      * Mark a single notification as read.
2192      *
2193      * @param \stdClass $notification The notification
2194      * @param int|null $timeread The time the message was marked as read, if null will default to time()
2195      */
2196     public static function mark_notification_as_read($notification, $timeread = null) {
2197         global $DB;
2199         if (is_null($timeread)) {
2200             $timeread = time();
2201         }
2203         if (is_null($notification->timeread)) {
2204             $updatenotification = new \stdClass();
2205             $updatenotification->id = $notification->id;
2206             $updatenotification->timeread = $timeread;
2208             $DB->update_record('notifications', $updatenotification);
2210             // Trigger event for reading a notification.
2211             \core\event\notification_viewed::create_from_ids(
2212                 $notification->useridfrom,
2213                 $notification->useridto,
2214                 $notification->id
2215             )->trigger();
2216         }
2217     }
2219     /**
2220      * Checks if a user can delete a message.
2221      *
2222      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2223      *  but will still seem as if it was by the user)
2224      * @param int $messageid The message id
2225      * @return bool Returns true if a user can delete the message, false otherwise.
2226      */
2227     public static function can_delete_message($userid, $messageid) {
2228         global $DB, $USER;
2230         $systemcontext = \context_system::instance();
2232         $conversationid = $DB->get_field('messages', 'conversationid', ['id' => $messageid], MUST_EXIST);
2234         if (has_capability('moodle/site:deleteanymessage', $systemcontext)) {
2235             return true;
2236         }
2238         if (!self::is_user_in_conversation($userid, $conversationid)) {
2239             return false;
2240         }
2242         if (has_capability('moodle/site:deleteownmessage', $systemcontext) &&
2243                 $USER->id == $userid) {
2244             return true;
2245         }
2247         return false;
2248     }
2250     /**
2251      * Deletes a message.
2252      *
2253      * This function does not verify any permissions.
2254      *
2255      * @param int $userid the user id of who we want to delete the message for (this may be done by the admin
2256      *  but will still seem as if it was by the user)
2257      * @param int $messageid The message id
2258      * @return bool
2259      */
2260     public static function delete_message($userid, $messageid) {
2261         global $DB, $USER;
2263         if (!$DB->record_exists('messages', ['id' => $messageid])) {
2264             return false;
2265         }
2267         // Check if the user has already deleted this message.
2268         if (!$DB->record_exists('message_user_actions', ['userid' => $userid,
2269                 'messageid' => $messageid, 'action' => self::MESSAGE_ACTION_DELETED])) {
2270             $mua = new \stdClass();
2271             $mua->userid = $userid;
2272             $mua->messageid = $messageid;
2273             $mua->action = self::MESSAGE_ACTION_DELETED;
2274             $mua->timecreated = time();
2275             $mua->id = $DB->insert_record('message_user_actions', $mua);
2277             // Trigger event for deleting a message.
2278             \core\event\message_deleted::create_from_ids($userid, $USER->id,
2279                 $messageid, $mua->id)->trigger();
2281             return true;
2282         }
2284         return false;
2285     }
2287     /**
2288      * Returns the conversation between two users.
2289      *
2290      * @param array $userids
2291      * @return int|bool The id of the conversation, false if not found
2292      */
2293     public static function get_conversation_between_users(array $userids) {
2294         global $DB;
2296         $conversations = self::get_individual_conversations_between_users([$userids]);
2297         $conversation = $conversations[0];
2299         if ($conversation) {
2300             return $conversation->id;
2301         }
2303         return false;
2304     }
2306     /**
2307      * Returns the conversations between sets of users.
2308      *
2309      * The returned array of results will be in the same order as the requested
2310      * arguments, null will be returned if there is no conversation for that user
2311      * pair.
2312      *
2313      * For example:
2314      * If we have 6 users with ids 1, 2, 3, 4, 5, 6 where only 2 conversations
2315      * exist. One between 1 and 2 and another between 5 and 6.
2316      *
2317      * Then if we call:
2318      * $conversations = get_individual_conversations_between_users([[1,2], [3,4], [5,6]]);
2319      *
2320      * The conversations array will look like:
2321      * [<conv_record>, null, <conv_record>];
2322      *
2323      * Where null is returned for the pairing of [3, 4] since no record exists.
2324      *
2325      * @param array $useridsets An array of arrays where the inner array is the set of user ids
2326      * @return stdClass[] Array of conversation records
2327      */
2328     public static function get_individual_conversations_between_users(array $useridsets) : array {
2329         global $DB;
2331         if (empty($useridsets)) {
2332             return [];
2333         }
2335         $hashes = array_map(function($userids) {
2336             return  helper::get_conversation_hash($userids);
2337         }, $useridsets);
2339         list($inorequalsql, $params) = $DB->get_in_or_equal($hashes);
2340         array_unshift($params, self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL);
2341         $where = "type = ? AND convhash ${inorequalsql}";
2342         $conversations = array_fill(0, count($hashes), null);
2343         $records = $DB->get_records_select('message_conversations', $where, $params);
2345         foreach (array_values($records) as $record) {
2346             $index = array_search($record->convhash, $hashes);
2347             if ($index !== false) {
2348                 $conversations[$index] = $record;
2349             }
2350         }
2352         return $conversations;
2353     }
2355     /**
2356      * Creates a conversation between two users.
2357      *
2358      * @deprecated since 3.6
2359      * @param array $userids
2360      * @return int The id of the conversation
2361      */
2362     public static function create_conversation_between_users(array $userids) {
2363         debugging('\core_message\api::create_conversation_between_users is deprecated, please use ' .
2364             '\core_message\api::create_conversation instead.', DEBUG_DEVELOPER);
2366         // This method was always used for individual conversations.
2367         $conversation = self::create_conversation(self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL, $userids);
2369         return $conversation->id;
2370     }
2372     /**
2373      * Creates a conversation with selected users and messages.
2374      *
2375      * @param int $type The type of conversation
2376      * @param int[] $userids The array of users to add to the conversation
2377      * @param string|null $name The name of the conversation
2378      * @param int $enabled Determines if the conversation is created enabled or disabled
2379      * @param string|null $component Defines the Moodle component which the conversation belongs to, if any
2380      * @param string|null $itemtype Defines the type of the component
2381      * @param int|null $itemid The id of the component
2382      * @param int|null $contextid The id of the context
2383      * @return \stdClass
2384      */
2385     public static function create_conversation(int $type, array $userids, string $name = null,
2386             int $enabled = self::MESSAGE_CONVERSATION_ENABLED, string $component = null,
2387             string $itemtype = null, int $itemid = null, int $contextid = null) {
2389         global $DB;
2391         $validtypes = [
2392             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
2393             self::MESSAGE_CONVERSATION_TYPE_GROUP
2394         ];
2396         if (!in_array($type, $validtypes)) {
2397             throw new \moodle_exception('An invalid conversation type was specified.');
2398         }
2400         // Sanity check.
2401         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
2402             if (count($userids) > 2) {
2403                 throw new \moodle_exception('An individual conversation can not have more than two users.');
2404             }
2405         }
2407         $conversation = new \stdClass();
2408         $conversation->type = $type;
2409         $conversation->name = $name;
2410         $conversation->convhash = null;
2411         if ($type == self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL) {
2412             $conversation->convhash = helper::get_conversation_hash($userids);
2413         }
2414         $conversation->component = $component;
2415         $conversation->itemtype = $itemtype;
2416         $conversation->itemid = $itemid;
2417         $conversation->contextid = $contextid;
2418         $conversation->enabled = $enabled;
2419         $conversation->timecreated = time();
2420         $conversation->timemodified = $conversation->timecreated;
2421         $conversation->id = $DB->insert_record('message_conversations', $conversation);
2423         // Add users to this conversation.
2424         $arrmembers = [];
2425         foreach ($userids as $userid) {
2426             $member = new \stdClass();
2427             $member->conversationid = $conversation->id;
2428             $member->userid = $userid;
2429             $member->timecreated = time();
2430             $member->id = $DB->insert_record('message_conversation_members', $member);
2432             $arrmembers[] = $member;
2433         }
2435         $conversation->members = $arrmembers;
2437         return $conversation;
2438     }
2440     /**
2441      * Checks if a user can create a group conversation.
2442      *
2443      * @param int $userid The id of the user attempting to create the conversation
2444      * @param \context $context The context they are creating the conversation from, most likely course context
2445      * @return bool
2446      */
2447     public static function can_create_group_conversation(int $userid, \context $context) : bool {
2448         global $CFG;
2450         // If we can't message at all, then we can't create a conversation.
2451         if (empty($CFG->messaging)) {
2452             return false;
2453         }
2455         // We need to check they have the capability to create the conversation.
2456         return has_capability('moodle/course:creategroupconversations', $context, $userid);
2457     }
2459     /**
2460      * Checks if a user can create a contact request.
2461      *
2462      * @param int $userid The id of the user who is creating the contact request
2463      * @param int $requesteduserid The id of the user being requested
2464      * @return bool
2465      */
2466     public static function can_create_contact(int $userid, int $requesteduserid) : bool {
2467         global $CFG;
2469         // If we can't message at all, then we can't create a contact.
2470         if (empty($CFG->messaging)) {
2471             return false;
2472         }
2474         // If we can message anyone on the site then we can create a contact.
2475         if ($CFG->messagingallusers) {
2476             return true;
2477         }
2479         // We need to check if they are in the same course.
2480         return enrol_sharing_course($userid, $requesteduserid);
2481     }
2483     /**
2484      * Handles creating a contact request.
2485      *
2486      * @param int $userid The id of the user who is creating the contact request
2487      * @param int $requesteduserid The id of the user being requested
2488      * @return \stdClass the request
2489      */
2490     public static function create_contact_request(int $userid, int $requesteduserid) : \stdClass {
2491         global $DB;
2493         $request = new \stdClass();
2494         $request->userid = $userid;
2495         $request->requesteduserid = $requesteduserid;
2496         $request->timecreated = time();
2498         $request->id = $DB->insert_record('message_contact_requests', $request);
2500         return $request;
2501     }
2504     /**
2505      * Handles confirming a contact request.
2506      *
2507      * @param int $userid The id of the user who created the contact request
2508      * @param int $requesteduserid The id of the user confirming the request
2509      */
2510     public static function confirm_contact_request(int $userid, int $requesteduserid) {
2511         global $DB;
2513         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2514                 'requesteduserid' => $requesteduserid])) {
2515             self::add_contact($userid, $requesteduserid);
2517             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2518         }
2519     }
2521     /**
2522      * Handles declining a contact request.
2523      *
2524      * @param int $userid The id of the user who created the contact request
2525      * @param int $requesteduserid The id of the user declining the request
2526      */
2527     public static function decline_contact_request(int $userid, int $requesteduserid) {
2528         global $DB;
2530         if ($request = $DB->get_record('message_contact_requests', ['userid' => $userid,
2531                 'requesteduserid' => $requesteduserid])) {
2532             $DB->delete_records('message_contact_requests', ['id' => $request->id]);
2533         }
2534     }
2536     /**
2537      * Handles returning the contact requests for a user.
2538      *
2539      * This also includes the user data necessary to display information
2540      * about the user.
2541      *
2542      * It will not include blocked users.
2543      *
2544      * @param int $userid
2545      * @param int $limitfrom
2546      * @param int $limitnum
2547      * @return array The list of contact requests
2548      */
2549     public static function get_contact_requests(int $userid, int $limitfrom = 0, int $limitnum = 0) : array {
2550         global $DB;
2552         $sql = "SELECT mcr.userid
2553                   FROM {message_contact_requests} mcr
2554              LEFT JOIN {message_users_blocked} mub
2555                     ON (mub.userid = ? AND mub.blockeduserid = mcr.userid)
2556                  WHERE mcr.requesteduserid = ?
2557                    AND mub.id is NULL
2558               ORDER BY mcr.timecreated ASC";
2559         if ($contactrequests = $DB->get_records_sql($sql, [$userid, $userid], $limitfrom, $limitnum)) {
2560             $userids = array_keys($contactrequests);
2561             return helper::get_member_info($userid, $userids);
2562         }
2564         return [];
2565     }
2567     /**
2568      * Returns the number of contact requests the user has received.
2569      *
2570      * @param int $userid The ID of the user we want to return the number of received contact requests for
2571      * @return int The count
2572      */
2573     public static function get_received_contact_requests_count(int $userid) : int {
2574         global $DB;
2575         $sql = "SELECT COUNT(mcr.id)
2576                   FROM {message_contact_requests} mcr
2577              LEFT JOIN {message_users_blocked} mub
2578                     ON mub.userid = mcr.requesteduserid AND mub.blockeduserid = mcr.userid
2579                  WHERE mcr.requesteduserid = :requesteduserid
2580                    AND mub.id IS NULL";
2581         $params = ['requesteduserid' => $userid];
2582         return $DB->count_records_sql($sql, $params);
2583     }
2585     /**
2586      * Handles adding a contact.
2587      *
2588      * @param int $userid The id of the user who requested to be a contact
2589      * @param int $contactid The id of the contact
2590      */
2591     public static function add_contact(int $userid, int $contactid) {
2592         global $DB;
2594         $messagecontact = new \stdClass();
2595         $messagecontact->userid = $userid;
2596         $messagecontact->contactid = $contactid;
2597         $messagecontact->timecreated = time();
2598         $messagecontact->id = $DB->insert_record('message_contacts', $messagecontact);
2600         $eventparams = [
2601             'objectid' => $messagecontact->id,
2602             'userid' => $userid,
2603             'relateduserid' => $contactid,
2604             'context' => \context_user::instance($userid)
2605         ];
2606         $event = \core\event\message_contact_added::create($eventparams);
2607         $event->add_record_snapshot('message_contacts', $messagecontact);
2608         $event->trigger();
2609     }
2611     /**
2612      * Handles removing a contact.
2613      *
2614      * @param int $userid The id of the user who is removing a user as a contact
2615      * @param int $contactid The id of the user to be removed as a contact
2616      */
2617     public static function remove_contact(int $userid, int $contactid) {
2618         global $DB;
2620         if ($contact = self::get_contact($userid, $contactid)) {
2621             $DB->delete_records('message_contacts', ['id' => $contact->id]);
2623             $event = \core\event\message_contact_removed::create(array(
2624                 'objectid' => $contact->id,
2625                 'userid' => $userid,
2626                 'relateduserid' => $contactid,
2627                 'context' => \context_user::instance($userid)
2628             ));
2629             $event->add_record_snapshot('message_contacts', $contact);
2630             $event->trigger();
2631         }
2632     }
2634     /**
2635      * Handles blocking a user.
2636      *
2637      * @param int $userid The id of the user who is blocking
2638      * @param int $usertoblockid The id of the user being blocked
2639      */
2640     public static function block_user(int $userid, int $usertoblockid) {
2641         global $DB;
2643         $blocked = new \stdClass();
2644         $blocked->userid = $userid;
2645         $blocked->blockeduserid = $usertoblockid;
2646         $blocked->timecreated = time();
2647         $blocked->id = $DB->insert_record('message_users_blocked', $blocked);
2649         // Trigger event for blocking a contact.
2650         $event = \core\event\message_user_blocked::create(array(
2651             'objectid' => $blocked->id,
2652             'userid' => $userid,
2653             'relateduserid' => $usertoblockid,
2654             'context' => \context_user::instance($userid)
2655         ));
2656         $event->add_record_snapshot('message_users_blocked', $blocked);
2657         $event->trigger();
2658     }
2660     /**
2661      * Handles unblocking a user.
2662      *
2663      * @param int $userid The id of the user who is unblocking
2664      * @param int $usertounblockid The id of the user being unblocked
2665      */
2666     public static function unblock_user(int $userid, int $usertounblockid) {
2667         global $DB;
2669         if ($blockeduser = $DB->get_record('message_users_blocked',
2670                 ['userid' => $userid, 'blockeduserid' => $usertounblockid])) {
2671             $DB->delete_records('message_users_blocked', ['id' => $blockeduser->id]);
2673             // Trigger event for unblocking a contact.
2674             $event = \core\event\message_user_unblocked::create(array(
2675                 'objectid' => $blockeduser->id,
2676                 'userid' => $userid,
2677                 'relateduserid' => $usertounblockid,
2678                 'context' => \context_user::instance($userid)
2679             ));
2680             $event->add_record_snapshot('message_users_blocked', $blockeduser);
2681             $event->trigger();
2682         }
2683     }
2685     /**
2686      * Checks if users are already contacts.
2687      *
2688      * @param int $userid The id of one of the users
2689      * @param int $contactid The id of the other user
2690      * @return bool Returns true if they are a contact, false otherwise
2691      */
2692     public static function is_contact(int $userid, int $contactid) : bool {
2693         global $DB;
2695         $sql = "SELECT id
2696                   FROM {message_contacts} mc
2697                  WHERE (mc.userid = ? AND mc.contactid = ?)
2698                     OR (mc.userid = ? AND mc.contactid = ?)";
2699         return $DB->record_exists_sql($sql, [$userid, $contactid, $contactid, $userid]);
2700     }
2702     /**
2703      * Returns the row in the database table message_contacts that represents the contact between two people.
2704      *
2705      * @param int $userid The id of one of the users
2706      * @param int $contactid The id of the other user
2707      * @return mixed A fieldset object containing the record, false otherwise
2708      */
2709     public static function get_contact(int $userid, int $contactid) {
2710         global $DB;
2712         $sql = "SELECT mc.*
2713                   FROM {message_contacts} mc
2714                  WHERE (mc.userid = ? AND mc.contactid = ?)
2715                     OR (mc.userid = ? AND mc.contactid = ?)";
2716         return $DB->get_record_sql($sql, [$userid, $contactid, $contactid, $userid]);
2717     }
2719     /**
2720      * Checks if a user is already blocked.
2721      *
2722      * @param int $userid
2723      * @param int $blockeduserid
2724      * @return bool Returns true if they are a blocked, false otherwise
2725      */
2726     public static function is_blocked(int $userid, int $blockeduserid) : bool {
2727         global $DB;
2729         return $DB->record_exists('message_users_blocked', ['userid' => $userid, 'blockeduserid' => $blockeduserid]);
2730     }
2732     /**
2733      * Get contact requests between users.
2734      *
2735      * @param int $userid The id of the user who is creating the contact request
2736      * @param int $requesteduserid The id of the user being requested
2737      * @return \stdClass[]
2738      */
2739     public static function get_contact_requests_between_users(int $userid, int $requesteduserid) : array {
2740         global $DB;
2742         $sql = "SELECT *
2743                   FROM {message_contact_requests} mcr
2744                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2745                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2746         return $DB->get_records_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2747     }
2749     /**
2750      * Checks if a contact request already exists between users.
2751      *
2752      * @param int $userid The id of the user who is creating the contact request
2753      * @param int $requesteduserid The id of the user being requested
2754      * @return bool Returns true if a contact request exists, false otherwise
2755      */
2756     public static function does_contact_request_exist(int $userid, int $requesteduserid) : bool {
2757         global $DB;
2759         $sql = "SELECT id
2760                   FROM {message_contact_requests} mcr
2761                  WHERE (mcr.userid = ? AND mcr.requesteduserid = ?)
2762                     OR (mcr.userid = ? AND mcr.requesteduserid = ?)";
2763         return $DB->record_exists_sql($sql, [$userid, $requesteduserid, $requesteduserid, $userid]);
2764     }
2766     /**
2767      * Checks if a user is already in a conversation.
2768      *
2769      * @param int $userid The id of the user we want to check if they are in a group
2770      * @param int $conversationid The id of the conversation
2771      * @return bool Returns true if a contact request exists, false otherwise
2772      */
2773     public static function is_user_in_conversation(int $userid, int $conversationid) : bool {
2774         global $DB;
2776         return $DB->record_exists('message_conversation_members', ['conversationid' => $conversationid,
2777             'userid' => $userid]);
2778     }
2780     /**
2781      * Checks if the sender can message the recipient.
2782      *
2783      * @param int $recipientid
2784      * @param int $senderid
2785      * @return bool true if recipient hasn't blocked sender and sender can contact to recipient, false otherwise.
2786      */
2787     protected static function can_contact_user(int $recipientid, int $senderid) : bool {
2788         if (has_capability('moodle/site:messageanyuser', \context_system::instance(), $senderid)) {
2789             // The sender has the ability to contact any user across the entire site.
2790             return true;
2791         }
2793         // The initial value of $cancontact is null to indicate that a value has not been determined.
2794         $cancontact = null;
2796         if (self::is_blocked($recipientid, $senderid)) {
2797             // The recipient has specifically blocked this sender.
2798             $cancontact = false;
2799         }
2801         $sharedcourses = null;
2802         if (null === $cancontact) {
2803             // There are three user preference options:
2804             // - Site: Allow anyone not explicitly blocked to contact me;
2805             // - Course members: Allow anyone I am in a course with to contact me; and
2806             // - Contacts: Only allow my contacts to contact me.
2807             //
2808             // The Site option is only possible when the messagingallusers site setting is also enabled.
2810             $privacypreference = self::get_user_privacy_messaging_preference($recipientid);
2811             if (self::MESSAGE_PRIVACY_SITE === $privacypreference) {
2812                 // The user preference is to allow any user to contact them.
2813                 // No need to check anything else.
2814                 $cancontact = true;
2815             } else {
2816                 // This user only allows their own contacts, and possibly course peers, to contact them.
2817                 // If the users are contacts then we can avoid the more expensive shared courses check.
2818                 $cancontact = self::is_contact($senderid, $recipientid);
2820                 if (!$cancontact && self::MESSAGE_PRIVACY_COURSEMEMBER === $privacypreference) {
2821                     // The users are not contacts and the user allows course member messaging.
2822                     // Check whether these two users share any course together.
2823                     $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2824                     $cancontact = (!empty($sharedcourses));
2825                 }
2826             }
2827         }
2829         if (false === $cancontact) {
2830             // At the moment the users cannot contact one another.
2831             // Check whether the messageanyuser capability applies in any of the shared courses.
2832             // This is intended to allow teachers to message students regardless of message settings.
2834             // Note: You cannot use empty($sharedcourses) here because this may be an empty array.
2835             if (null === $sharedcourses) {
2836                 $sharedcourses = enrol_get_shared_courses($recipientid, $senderid, true);
2837             }
2839             foreach ($sharedcourses as $course) {
2840                 // Note: enrol_get_shared_courses will preload any shared context.
2841                 if (has_capability('moodle/site:messageanyuser', \context_course::instance($course->id), $senderid)) {
2842                     $cancontact = true;
2843                     break;
2844                 }
2845             }
2846         }
2848         return $cancontact;
2849     }
2851     /**
2852      * Add some new members to an existing conversation.
2853      *
2854      * @param array $userids User ids array to add as members.
2855      * @param int $convid The conversation id. Must exists.
2856      * @throws \dml_missing_record_exception If convid conversation doesn't exist
2857      * @throws \dml_exception If there is a database error
2858      * @throws \moodle_exception If trying to add a member(s) to a non-group conversation
2859      */
2860     public static function add_members_to_conversation(array $userids, int $convid) {
2861         global $DB;
2863         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
2865         // We can only add members to a group conversation.
2866         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
2867             throw new \moodle_exception('You can not add members to a non-group conversation.');
2868         }
2870         // Be sure we are not trying to add a non existing user to the conversation. Work only with existing users.
2871         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
2872         $existingusers = $DB->get_fieldset_select('user', 'id', "id $useridcondition", $params);
2874         // Be sure we are not adding a user is already member of the conversation. Take all the members.
2875         $memberuserids = array_values($DB->get_records_menu(
2876             'message_conversation_members', ['conversationid' => $convid], 'id', 'id, userid')
2877         );
2879         // Work with existing new members.
2880         $members = array();
2881         $newuserids = array_diff($existingusers, $memberuserids);
2882         foreach ($newuserids as $userid) {
2883             $member = new \stdClass();
2884             $member->conversationid = $convid;
2885             $member->userid = $userid;
2886             $member->timecreated = time();
2887             $members[] = $member;
2888         }
2890         $DB->insert_records('message_conversation_members', $members);
2891     }
2893     /**
2894      * Remove some members from an existing conversation.
2895      *
2896      * @param array $userids The user ids to remove from conversation members.
2897      * @param int $convid The conversation id. Must exists.
2898      * @throws \dml_exception
2899      * @throws \moodle_exception If trying to remove a member(s) from a non-group conversation
2900      */
2901     public static function remove_members_from_conversation(array $userids, int $convid) {
2902         global $DB;
2904         $conversation = $DB->get_record('message_conversations', ['id' => $convid], '*', MUST_EXIST);
2906         if ($conversation->type != self::MESSAGE_CONVERSATION_TYPE_GROUP) {
2907             throw new \moodle_exception('You can not remove members from a non-group conversation.');
2908         }
2910         list($useridcondition, $params) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
2911         $params['convid'] = $convid;
2913         $DB->delete_records_select('message_conversation_members',
2914             "conversationid = :convid AND userid $useridcondition", $params);
2915     }
2917     /**
2918      * Count conversation members.
2919      *
2920      * @param int $convid The conversation id.
2921      * @return int Number of conversation members.
2922      * @throws \dml_exception
2923      */
2924     public static function count_conversation_members(int $convid) : int {
2925         global $DB;
2927         return $DB->count_records('message_conversation_members', ['conversationid' => $convid]);
2928     }
2930     /**
2931      * Checks whether or not a conversation area is enabled.
2932      *
2933      * @param string $component Defines the Moodle component which the area was added to.
2934      * @param string $itemtype Defines the type of the component.
2935      * @param int $itemid The id of the component.
2936      * @param int $contextid The id of the context.
2937      * @return bool Returns if a conversation area exists and is enabled, false otherwise
2938      */
2939     public static function is_conversation_area_enabled(string $component, string $itemtype, int $itemid, int $contextid) : bool {
2940         global $DB;
2942         return $DB->record_exists('message_conversations',
2943             [
2944                 'itemid' => $itemid,
2945                 'contextid' => $contextid,
2946                 'component' => $component,
2947                 'itemtype' => $itemtype,
2948                 'enabled' => self::MESSAGE_CONVERSATION_ENABLED
2949             ]
2950         );
2951     }
2953     /**
2954      * Get conversation by area.
2955      *
2956      * @param string $component Defines the Moodle component which the area was added to.
2957      * @param string $itemtype Defines the type of the component.
2958      * @param int $itemid The id of the component.
2959      * @param int $contextid The id of the context.
2960      * @return \stdClass
2961      */
2962     public static function get_conversation_by_area(string $component, string $itemtype, int $itemid, int $contextid) {
2963         global $DB;
2965         return $DB->get_record('message_conversations',
2966             [
2967                 'itemid' => $itemid,
2968                 'contextid' => $contextid,
2969                 'component' => $component,
2970                 'itemtype'  => $itemtype
2971             ]
2972         );
2973     }
2975     /**
2976      * Enable a conversation.
2977      *
2978      * @param int $conversationid The id of the conversation.
2979      * @return void
2980      */
2981     public static function enable_conversation(int $conversationid) {
2982         global $DB;
2984         $conversation = new \stdClass();
2985         $conversation->id = $conversationid;
2986         $conversation->enabled = self::MESSAGE_CONVERSATION_ENABLED;
2987         $conversation->timemodified = time();
2988         $DB->update_record('message_conversations', $conversation);
2989     }
2991     /**
2992      * Disable a conversation.
2993      *
2994      * @param int $conversationid The id of the conversation.
2995      * @return void
2996      */
2997     public static function disable_conversation(int $conversationid) {
2998         global $DB;
3000         $conversation = new \stdClass();
3001         $conversation->id = $conversationid;
3002         $conversation->enabled = self::MESSAGE_CONVERSATION_DISABLED;
3003         $conversation->timemodified = time();
3004         $DB->update_record('message_conversations', $conversation);
3005     }
3007     /**
3008      * Update the name of a conversation.
3009      *
3010      * @param int $conversationid The id of a conversation.
3011      * @param string $name The main name of the area
3012      * @return void
3013      */
3014     public static function update_conversation_name(int $conversationid, string $name) {
3015         global $DB;
3017         if ($conversation = $DB->get_record('message_conversations', array('id' => $conversationid))) {
3018             if ($name <> $conversation->name) {
3019                 $conversation->name = $name;
3020                 $conversation->timemodified = time();
3021                 $DB->update_record('message_conversations', $conversation);
3022             }
3023         }
3024     }
3026     /**
3027      * Returns a list of conversation members.
3028      *
3029      * @param int $userid The user we are returning the conversation members for, used by helper::get_member_info.
3030      * @param int $conversationid The id of the conversation
3031      * @param bool $includecontactrequests Do we want to include contact requests with this data?
3032      * @param bool $includeprivacyinfo Do we want to include privacy requests with this data?
3033      * @param int $limitfrom
3034      * @param int $limitnum
3035      * @return array
3036      */
3037     public static function get_conversation_members(int $userid, int $conversationid, bool $includecontactrequests = false,
3038                                                     bool $includeprivacyinfo = false, int $limitfrom = 0,
3039                                                     int $limitnum = 0) : array {
3040         global $DB;
3042         if ($members = $DB->get_records('message_conversation_members', ['conversationid' => $conversationid],
3043                 'timecreated ASC, id ASC', 'userid', $limitfrom, $limitnum)) {
3044             $userids = array_keys($members);
3045             $members = helper::get_member_info($userid, $userids, $includecontactrequests, $includeprivacyinfo);
3047             return $members;
3048         }
3050         return [];
3051     }
3053     /**
3054      * Get the unread counts for all conversations for the user, sorted by type, and including favourites.
3055      *
3056      * @param int $userid the id of the user whose conversations we'll check.
3057      * @return array the unread counts for each conversation, indexed by type.
3058      */
3059     public static function get_unread_conversation_counts(int $userid) : array {
3060         global $DB;
3062         // Get all conversations the user is in, and check unread.
3063         $unreadcountssql = 'SELECT conv.id, conv.type, indcounts.unreadcount
3064                               FROM {message_conversations} conv
3065                         INNER JOIN (
3066                                       SELECT m.conversationid, count(m.id) as unreadcount
3067                                         FROM {messages} m
3068                                   INNER JOIN {message_conversations} mc
3069                                           ON mc.id = m.conversationid
3070                                   INNER JOIN {message_conversation_members} mcm
3071                                           ON m.conversationid = mcm.conversationid
3072                                    LEFT JOIN {message_user_actions} mua
3073                                           ON (mua.messageid = m.id AND mua.userid = ? AND
3074                                              (mua.action = ? OR mua.action = ?))
3075                                        WHERE mcm.userid = ?
3076                                          AND m.useridfrom != ?
3077                                          AND mua.id is NULL
3078                                     GROUP BY m.conversationid
3079                                    ) indcounts
3080                                 ON indcounts.conversationid = conv.id
3081                              WHERE conv.enabled = 1';
3083         $unreadcounts = $DB->get_records_sql($unreadcountssql, [$userid, self::MESSAGE_ACTION_READ, self::MESSAGE_ACTION_DELETED,
3084             $userid, $userid]);
3086         // Get favourites, so we can track these separately.
3087         $service = \core_favourites\service_factory::get_service_for_user_context(\context_user::instance($userid));
3088         $favouriteconversations = $service->find_favourites_by_type('core_message', 'message_conversations');
3089         $favouriteconvids = array_flip(array_column($favouriteconversations, 'itemid'));
3091         // Assemble the return array.
3092         $counts = ['favourites' => 0, 'types' => [
3093             self::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL => 0,
3094             self::MESSAGE_CONVERSATION_TYPE_GROUP => 0
3095         ]];
3096         foreach ($unreadcounts as $convid => $info) {
3097             if (isset($favouriteconvids[$convid])) {
3098                 $counts['favourites']++;
3099                 continue;
3100             }
3101             $counts['types'][$info->type]++;
3102         }
3104         return $counts;
3105     }