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