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