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