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