MDL-60680 notifications: Support privacy API
[moodle.git] / message / classes / privacy / provider.php
CommitLineData
04cd1b3f
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 * Privacy Subsystem implementation for core_message.
19 *
20 * @package core_message
ba81c26c 21 * @category privacy
04cd1b3f
MN
22 * @copyright 2018 Mark Nelson <markn@moodle.com>
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25namespace core_message\privacy;
26
27use core_privacy\local\metadata\collection;
28use core_privacy\local\request\approved_contextlist;
ba81c26c 29use core_privacy\local\request\approved_userlist;
04cd1b3f
MN
30use core_privacy\local\request\contextlist;
31use core_privacy\local\request\transform;
ba81c26c 32use core_privacy\local\request\userlist;
04cd1b3f
MN
33use core_privacy\local\request\writer;
34
35defined('MOODLE_INTERNAL') || die();
36
37/**
38 * Privacy Subsystem implementation for core_message.
39 *
40 * @copyright 2018 Mark Nelson <markn@moodle.com>
41 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
42 */
43class provider implements
201cebea 44 // The messaging subsystem contains data.
04cd1b3f 45 \core_privacy\local\metadata\provider,
201cebea
SA
46
47 // The messaging subsystem provides all the messages at user context - i.e. individual ones.
04cd1b3f 48 \core_privacy\local\request\subsystem\provider,
201cebea
SA
49
50 // This plugin has some sitewide user preferences to export.
ba81c26c 51 \core_privacy\local\request\user_preference_provider,
201cebea
SA
52
53 // This plugin is capable of determining which users have data within it.
54 \core_privacy\local\request\core_userlist_provider,
55
56 // The messaging subsystem provides a data service to other components.
57 \core_privacy\local\request\subsystem\plugin_provider {
04cd1b3f
MN
58
59 /**
60 * Return the fields which contain personal data.
61 *
62 * @param collection $items a reference to the collection to use to store the metadata.
63 * @return collection the updated collection of metadata items.
64 */
65 public static function get_metadata(collection $items) : collection {
66 $items->add_database_table(
67 'messages',
68 [
69 'useridfrom' => 'privacy:metadata:messages:useridfrom',
70 'conversationid' => 'privacy:metadata:messages:conversationid',
71 'subject' => 'privacy:metadata:messages:subject',
72 'fullmessage' => 'privacy:metadata:messages:fullmessage',
73 'fullmessageformat' => 'privacy:metadata:messages:fullmessageformat',
74 'fullmessagehtml' => 'privacy:metadata:messages:fullmessagehtml',
75 'smallmessage' => 'privacy:metadata:messages:smallmessage',
a129ba04
JL
76 'timecreated' => 'privacy:metadata:messages:timecreated',
77 'customdata' => 'privacy:metadata:messages:customdata',
04cd1b3f
MN
78 ],
79 'privacy:metadata:messages'
80 );
81
82 $items->add_database_table(
83 'message_user_actions',
84 [
85 'userid' => 'privacy:metadata:message_user_actions:userid',
86 'messageid' => 'privacy:metadata:message_user_actions:messageid',
87 'action' => 'privacy:metadata:message_user_actions:action',
88 'timecreated' => 'privacy:metadata:message_user_actions:timecreated'
89 ],
90 'privacy:metadata:message_user_actions'
91 );
92
93 $items->add_database_table(
94 'message_conversation_members',
95 [
96 'conversationid' => 'privacy:metadata:message_conversation_members:conversationid',
97 'userid' => 'privacy:metadata:message_conversation_members:userid',
98 'timecreated' => 'privacy:metadata:message_conversation_members:timecreated',
99 ],
100 'privacy:metadata:message_conversation_members'
101 );
102
1c36c11a
MN
103 $items->add_database_table(
104 'message_conversation_actions',
105 [
106 'conversationid' => 'privacy:metadata:message_conversation_actions:conversationid',
107 'userid' => 'privacy:metadata:message_conversation_actions:userid',
108 'timecreated' => 'privacy:metadata:message_conversation_actions:timecreated',
109 ],
110 'privacy:metadata:message_conversation_actions'
111 );
112
04cd1b3f
MN
113 $items->add_database_table(
114 'message_contacts',
115 [
116 'userid' => 'privacy:metadata:message_contacts:userid',
117 'contactid' => 'privacy:metadata:message_contacts:contactid',
742c27b3 118 'timecreated' => 'privacy:metadata:message_contacts:timecreated',
04cd1b3f
MN
119 ],
120 'privacy:metadata:message_contacts'
121 );
122
742c27b3
MN
123 $items->add_database_table(
124 'message_contact_requests',
125 [
126 'userid' => 'privacy:metadata:message_contact_requests:userid',
127 'requesteduserid' => 'privacy:metadata:message_contact_requests:requesteduserid',
128 'timecreated' => 'privacy:metadata:message_contact_requests:timecreated',
129 ],
130 'privacy:metadata:message_contact_requests'
131 );
132
133 $items->add_database_table(
134 'message_users_blocked',
135 [
136 'userid' => 'privacy:metadata:message_users_blocked:userid',
137 'blockeduserid' => 'privacy:metadata:message_users_blocked:blockeduserid',
138 'timecreated' => 'privacy:metadata:message_users_blocked:timecreated',
139 ],
140 'privacy:metadata:message_users_blocked'
141 );
142
04cd1b3f
MN
143 $items->add_database_table(
144 'notifications',
145 [
146 'useridfrom' => 'privacy:metadata:notifications:useridfrom',
147 'useridto' => 'privacy:metadata:notifications:useridto',
148 'subject' => 'privacy:metadata:notifications:subject',
149 'fullmessage' => 'privacy:metadata:notifications:fullmessage',
150 'fullmessageformat' => 'privacy:metadata:notifications:fullmessageformat',
151 'fullmessagehtml' => 'privacy:metadata:notifications:fullmessagehtml',
152 'smallmessage' => 'privacy:metadata:notifications:smallmessage',
153 'component' => 'privacy:metadata:notifications:component',
154 'eventtype' => 'privacy:metadata:notifications:eventtype',
155 'contexturl' => 'privacy:metadata:notifications:contexturl',
156 'contexturlname' => 'privacy:metadata:notifications:contexturlname',
157 'timeread' => 'privacy:metadata:notifications:timeread',
158 'timecreated' => 'privacy:metadata:notifications:timecreated',
a129ba04 159 'customdata' => 'privacy:metadata:notifications:customdata',
04cd1b3f
MN
160 ],
161 'privacy:metadata:notifications'
162 );
163
164 // Note - we are not adding the 'message' and 'message_read' tables
165 // as they are legacy tables. This information is moved to these
166 // new tables in a separate ad-hoc task. See MDL-61255.
167
168 // Now add that we also have user preferences.
169 $items->add_user_preference('core_message_messageprovider_settings',
170 'privacy:metadata:preference:core_message_settings');
171
16913e60
SA
172 // Add favourite conversations.
173 $items->link_subsystem('core_favourites', 'privacy:metadata:core_favourites');
174
04cd1b3f
MN
175 return $items;
176 }
177
178 /**
179 * Store all user preferences for core message.
180 *
181 * @param int $userid The userid of the user whose data is to be exported.
182 */
183 public static function export_user_preferences(int $userid) {
184 $preferences = get_user_preferences(null, null, $userid);
185 foreach ($preferences as $name => $value) {
8c8939c1
RW
186 if (
187 (substr($name, 0, 16) == 'message_provider') ||
188 ($name == 'message_blocknoncontacts') ||
189 ($name == 'message_entertosend')
190 ) {
04cd1b3f
MN
191 writer::export_user_preference(
192 'core_message',
193 $name,
194 $value,
195 get_string('privacy:request:preference:set', 'core_message', (object) [
196 'name' => $name,
197 'value' => $value,
198 ])
199 );
200 }
201 }
202 }
203
204 /**
205 * Get the list of contexts that contain user information for the specified user.
206 *
207 * @param int $userid the userid.
208 * @return contextlist the list of contexts containing user info for the user.
209 */
210 public static function get_contexts_for_userid(int $userid) : contextlist {
52334296
SR
211 global $DB;
212
04cd1b3f 213 $contextlist = new contextlist();
52334296
SR
214
215 // Messages are in the user context.
216 // For the sake of performance, there is no need to call add_from_sql for each of the below cases.
217 // It is enough to add the user's context as soon as we come to the conclusion that the user has some data.
218 // Also, the order of checking is sorted by the probability of occurrence (just by guess).
219 // There is no need to check the message_user_actions table, as there needs to be a message in order to be a message action.
1c36c11a
MN
220 // There is no need to check the message_conversation_actions table, as there needs to be a conversation in order to
221 // be a conversation action.
52334296
SR
222 // So, checking messages table would suffice.
223
224 $hasdata = false;
225 $hasdata = $hasdata || $DB->record_exists_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
201cebea
SA
226 $sql = "SELECT mc.id
227 FROM {message_conversations} mc
228 JOIN {message_conversation_members} mcm
229 ON (mcm.conversationid = mc.id AND mcm.userid = :userid)
230 WHERE mc.contextid IS NULL";
231 $hasdata = $hasdata || $DB->record_exists_sql($sql, ['userid' => $userid]);
232 $sql = "SELECT mc.id
233 FROM {message_conversations} mc
234 JOIN {messages} m
235 ON (m.conversationid = mc.id AND m.useridfrom = :useridfrom)
236 WHERE mc.contextid IS NULL";
237 $hasdata = $hasdata || $DB->record_exists_sql($sql, ['useridfrom' => $userid]);
52334296
SR
238 $hasdata = $hasdata || $DB->record_exists_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
239 $hasdata = $hasdata || $DB->record_exists_select('message_users_blocked', 'userid = ? OR blockeduserid = ?',
240 [$userid, $userid]);
241 $hasdata = $hasdata || $DB->record_exists_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
242 [$userid, $userid]);
243
244 if ($hasdata) {
245 $contextlist->add_user_context($userid);
246 }
04cd1b3f 247
16913e60
SA
248 // Add favourite conversations.
249 \core_favourites\privacy\provider::add_contexts_for_userid($contextlist, $userid, 'core_message', 'message_conversations');
250
04cd1b3f
MN
251 return $contextlist;
252 }
253
ba81c26c
SR
254 /**
255 * Get the list of users who have data within a context.
256 *
257 * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
258 */
259 public static function get_users_in_context(userlist $userlist) {
260 global $DB;
261
262 $context = $userlist->get_context();
263
264 if (!$context instanceof \context_user) {
265 return;
266 }
267
268 $userid = $context->instanceid;
269
270 // Messages are in the user context.
271 // For the sake of performance, there is no need to call add_from_sql for each of the below cases.
272 // It is enough to add the user's context as soon as we come to the conclusion that the user has some data.
273 // Also, the order of checking is sorted by the probability of occurrence (just by guess).
274 // There is no need to check the message_user_actions table, as there needs to be a message in order to be a message action.
1c36c11a
MN
275 // There is no need to check the message_conversation_actions table, as there needs to be a conversation in order to
276 // be a conversation action.
ba81c26c
SR
277 // So, checking messages table would suffice.
278
279 $hasdata = false;
280 $hasdata = $hasdata || $DB->record_exists_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
201cebea
SA
281 $sql = "SELECT mc.id
282 FROM {message_conversations} mc
283 JOIN {message_conversation_members} mcm
284 ON (mcm.conversationid = mc.id AND mcm.userid = :userid)
285 WHERE mc.contextid IS NULL";
286 $hasdata = $hasdata || $DB->record_exists_sql($sql, ['userid' => $userid]);
287 $sql = "SELECT mc.id
288 FROM {message_conversations} mc
289 JOIN {messages} m
290 ON (m.conversationid = mc.id AND m.useridfrom = :useridfrom)
291 WHERE mc.contextid IS NULL";
292 $hasdata = $hasdata || $DB->record_exists_sql($sql, ['useridfrom' => $userid]);
ba81c26c
SR
293 $hasdata = $hasdata || $DB->record_exists_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
294 $hasdata = $hasdata || $DB->record_exists_select('message_users_blocked', 'userid = ? OR blockeduserid = ?',
295 [$userid, $userid]);
296 $hasdata = $hasdata || $DB->record_exists_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
297 [$userid, $userid]);
298
299 if ($hasdata) {
300 $userlist->add_user($userid);
301 }
16913e60
SA
302
303 // Add favourite conversations.
304 $component = $userlist->get_component();
305 if ($component != 'core_message') {
306 $userlist->set_component('core_message');
307 }
308 \core_favourites\privacy\provider::add_userids_for_context($userlist, 'message_conversations');
309 if ($component != 'core_message') {
310 $userlist->set_component($component);
311 }
ba81c26c
SR
312 }
313
04cd1b3f
MN
314 /**
315 * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
316 *
317 * @param approved_contextlist $contextlist a list of contexts approved for export.
318 */
319 public static function export_user_data(approved_contextlist $contextlist) {
320 if (empty($contextlist->count())) {
321 return;
322 }
323
52334296
SR
324 $userid = $contextlist->get_user()->id;
325
326 // Remove non-user and invalid contexts. If it ends up empty then early return.
327 $contexts = array_filter($contextlist->get_contexts(), function($context) use($userid) {
328 return $context->contextlevel == CONTEXT_USER && $context->instanceid == $userid;
04cd1b3f
MN
329 });
330
331 if (empty($contexts)) {
332 return;
333 }
334
04cd1b3f
MN
335 // Export the contacts.
336 self::export_user_data_contacts($userid);
337
742c27b3
MN
338 // Export the contact requests.
339 self::export_user_data_contact_requests($userid);
340
341 // Export the blocked users.
342 self::export_user_data_blocked_users($userid);
343
04cd1b3f
MN
344 // Export the notifications.
345 self::export_user_data_notifications($userid);
346
201cebea
SA
347 // Conversations with empty contextid should be exported here because they are not related to any component/itemid.
348 $context = reset($contexts);
349 self::export_conversations($userid, '', '', $context);
04cd1b3f
MN
350 }
351
352 /**
353 * Delete all data for all users in the specified context.
354 *
355 * @param \context $context the context to delete in.
356 */
357 public static function delete_data_for_all_users_in_context(\context $context) {
52334296
SR
358 if ($context instanceof \context_user) {
359 static::delete_user_data($context->instanceid);
04cd1b3f 360 }
04cd1b3f
MN
361 }
362
363 /**
364 * Delete all user data for the specified user, in the specified contexts.
365 *
366 * @param approved_contextlist $contextlist a list of contexts approved for deletion.
367 */
368 public static function delete_data_for_user(approved_contextlist $contextlist) {
04cd1b3f
MN
369 if (empty($contextlist->count())) {
370 return;
371 }
372
52334296
SR
373 $userid = $contextlist->get_user()->id;
374
375 // Remove non-user and invalid contexts. If it ends up empty then early return.
376 $contexts = array_filter($contextlist->get_contexts(), function($context) use($userid) {
377 return $context->contextlevel == CONTEXT_USER && $context->instanceid == $userid;
04cd1b3f
MN
378 });
379
380 if (empty($contexts)) {
381 return;
382 }
383
52334296
SR
384 static::delete_user_data($userid);
385 }
386
ba81c26c
SR
387 /**
388 * Delete multiple users within a single context.
389 *
390 * @param approved_userlist $userlist The approved context and user information to delete information for.
391 */
392 public static function delete_data_for_users(approved_userlist $userlist) {
393 $context = $userlist->get_context();
394
395 if (!$context instanceof \context_user) {
396 return;
397 }
398
399 // Remove invalid users. If it ends up empty then early return.
400 $userids = array_filter($userlist->get_userids(), function($userid) use($context) {
401 return $context->instanceid == $userid;
402 });
403
404 if (empty($userids)) {
405 return;
406 }
407
408 static::delete_user_data($context->instanceid);
409 }
410
201cebea
SA
411 /**
412 * Provide a list of contexts which have conversations for the user, in the respective area (component/itemtype combination).
413 *
414 * This method is to be called by consumers of the messaging subsystem (plugins), in their get_contexts_for_userid() method,
415 * to add the contexts for items which may have any conversation, but would normally not be reported as having user data by the
416 * plugin responsible for them.
417 *
418 * @param contextlist $contextlist
419 * @param int $userid The id of the user in scope.
420 * @param string $component the frankenstyle component name.
421 * @param string $itemtype the type of the conversation items.
422 * @param int $itemid Optional itemid associated with component.
423 */
424 public static function add_contexts_for_conversations(contextlist $contextlist, int $userid, string $component,
425 string $itemtype, int $itemid = 0) {
426 // Search for conversations for this user in this area.
427 $sql = "SELECT mc.contextid
428 FROM {message_conversations} mc
429 JOIN {message_conversation_members} mcm
430 ON (mcm.conversationid = mc.id AND mcm.userid = :userid)
431 JOIN {context} ctx
432 ON mc.contextid = ctx.id
433 WHERE mc.component = :component AND mc.itemtype = :itemtype";
434 $params = [
435 'userid' => $userid,
436 'component' => $component,
437 'itemtype' => $itemtype,
438 ];
439
440 if (!empty($itemid)) {
441 $sql .= " AND itemid = :itemid";
442 $params['itemid'] = $itemid;
443 }
444
445 $contextlist->add_from_sql($sql, $params);
16913e60
SA
446
447 // Add favourite conversations. We don't need to filter by itemid because for now they are in the system context.
448 \core_favourites\privacy\provider::add_contexts_for_userid($contextlist, $userid, 'core_message', 'message_conversations');
449
201cebea
SA
450 }
451
452 /**
453 * Add the list of users who have a conversation in the specified area (component + itemtype + itemid).
454 *
455 * @param userlist $userlist The userlist to add the users to.
456 * @param string $component The component to check.
457 * @param string $itemtype The type of the conversation items.
458 * @param int $itemid Optional itemid associated with component.
459 */
460 public static function add_conversations_in_context(userlist $userlist, string $component, string $itemtype, int $itemid = 0) {
461 $sql = "SELECT mcm.userid
462 FROM {message_conversation_members} mcm
463 INNER JOIN {message_conversations} mc
464 ON mc.id = mcm.conversationid
465 WHERE mc.contextid = :contextid AND mc.component = :component AND mc.itemtype = :itemtype";
466 $params = [
467 'contextid' => $userlist->get_context()->id,
468 'component' => $component,
469 'itemtype' => $itemtype
470 ];
471
472 if (!empty($itemid)) {
473 $sql .= " AND itemid = :itemid";
474 $params['itemid'] = $itemid;
475 }
476
477 $userlist->add_from_sql('userid', $sql, $params);
16913e60
SA
478
479 // Add favourite conversations.
480 $component = $userlist->get_component();
481 if ($component != 'core_message') {
482 $userlist->set_component('core_message');
483 }
484 \core_favourites\privacy\provider::add_userids_for_context($userlist, 'message_conversations');
485 if ($component != 'core_message') {
486 $userlist->set_component($component);
487 }
201cebea
SA
488 }
489
490 /**
491 * Store all conversations which match the specified component, itemtype, and itemid.
492 *
493 * Conversations without context (for now, the private ones) are stored in '<$context> | Messages | <Other user id>'.
494 * Conversations with context are stored in '<$context> | Messages | <Conversation item type> | <Conversation name>'.
495 *
496 * @param int $userid The user whose information is to be exported.
497 * @param string $component The component to fetch data from.
498 * @param string $itemtype The itemtype that the data was exported in within the component.
499 * @param \context $context The context to export for.
500 * @param array $subcontext The sub-context in which to export this data.
501 * @param int $itemid Optional itemid associated with component.
502 */
503 public static function export_conversations(int $userid, string $component, string $itemtype, \context $context,
504 array $subcontext = [], int $itemid = 0) {
505 global $DB;
506
507 // Search for conversations for this user in this area.
508 $sql = "SELECT DISTINCT mc.*
509 FROM {message_conversations} mc
510 JOIN {message_conversation_members} mcm
511 ON (mcm.conversationid = mc.id AND mcm.userid = :userid)";
512 $params = [
513 'userid' => $userid
514 ];
515
516 // Get the conversations for the defined component and itemtype.
517 if (!empty($component) && !empty($itemtype)) {
518 $sql .= " WHERE mc.component = :component AND mc.itemtype = :itemtype";
519 $params['component'] = $component;
520 $params['itemtype'] = $itemtype;
521 if (!empty($itemid)) {
522 $sql .= " AND mc.itemid = :itemid";
523 $params['itemid'] = $itemid;
524 }
525 } else {
526 // Get all the conversations without any component and itemtype, so with null contextid.
527 $sql .= " WHERE mc.contextid IS NULL";
528 }
529
530 if ($conversations = $DB->get_records_sql($sql, $params)) {
531 // Export conversation messages.
532 foreach ($conversations as $conversation) {
533 self::export_user_data_conversation_messages($userid, $conversation, $context, $subcontext);
534 }
535 }
536 }
537
538 /**
539 * Deletes all group memberships for a specified context and component.
540 *
541 * @param \context $context Details about which context to delete group memberships for.
542 * @param string $component The component to delete. Empty string means no component.
543 * @param string $itemtype The itemtype of the component to delele. Empty string means no itemtype.
544 * @param int $itemid Optional itemid associated with component.
545 */
546 public static function delete_conversations_for_all_users(\context $context, string $component, string $itemtype,
547 int $itemid = 0) {
548 global $DB;
549
550 if (empty($context)) {
551 return;
552 }
553
554 $select = "contextid = :contextid AND component = :component AND itemtype = :itemtype";
555 $params = [
556 'contextid' => $context->id,
557 'component' => $component,
558 'itemtype' => $itemtype
559 ];
560
561 if (!empty($itemid)) {
562 $select .= " AND itemid = :itemid";
563 $params['itemid'] = $itemid;
564 }
565
566 // Get and remove all the conversations and messages for the specified context and area.
567 if ($conversationids = $DB->get_records_select('message_conversations', $select, $params, '', 'id')) {
568 $conversationids = array_keys($conversationids);
569 $messageids = $DB->get_records_list('messages', 'conversationid', $conversationids);
570 $messageids = array_keys($messageids);
571
16913e60
SA
572 // Delete these favourite conversations to all the users.
573 foreach ($conversationids as $conversationid) {
574 \core_favourites\privacy\provider::delete_favourites_for_all_users(
575 $context,
576 'core_message',
577 'message_conversations',
578 $conversationid);
579 }
580
201cebea
SA
581 // Delete messages and user_actions.
582 $DB->delete_records_list('message_user_actions', 'messageid', $messageids);
583 $DB->delete_records_list('messages', 'id', $messageids);
584
585 // Delete members and conversations.
586 $DB->delete_records_list('message_conversation_members', 'conversationid', $conversationids);
1c36c11a 587 $DB->delete_records_list('message_conversation_actions', 'conversationid', $conversationids);
201cebea
SA
588 $DB->delete_records_list('message_conversations', 'id', $conversationids);
589 }
590 }
591
592 /**
593 * Deletes all records for a user from a list of approved contexts.
594 *
595 * When the component and the itemtype are empty and there is only one user context in the list, all the
596 * conversations without contextid will be removed. However, if the component and itemtype are defined,
597 * only the conversations in these area for the contexts in $contextlist wil be deleted.
598 *
599 * @param approved_contextlist $contextlist Contains the user ID and a list of contexts to be deleted from.
600 * @param string $component The component to delete. Empty string means no component.
601 * @param string $itemtype The itemtype of the component to delele. Empty string means no itemtype.
602 * @param int $itemid Optional itemid associated with component.
603 */
604 public static function delete_conversations_for_user(approved_contextlist $contextlist, string $component, string $itemtype,
605 int $itemid = 0) {
606 self::delete_user_data_conversations(
607 $contextlist->get_user()->id,
608 $contextlist->get_contextids(),
609 $component,
610 $itemtype,
611 $itemid
612 );
613 }
614
615 /**
616 * Deletes all records for multiple users within a single context.
617 *
618 * @param approved_userlist $userlist The approved context and user information to delete information for.
619 * @param string $component The component to delete. Empty string means no component.
620 * @param string $itemtype The itemtype of the component to delele. Empty string means no itemtype.
621 * @param int $itemid Optional itemid associated with component.
622 */
623 public static function delete_conversations_for_users(approved_userlist $userlist, string $component, string $itemtype,
624 int $itemid = 0) {
625 global $DB;
626
627 $userids = $userlist->get_userids();
628 if (empty($userids)) {
629 return;
630 }
631
632 $context = $userlist->get_context();
633 $select = "mc.contextid = :contextid AND mc.component = :component AND mc.itemtype = :itemtype";
634 $params = [
635 'contextid' => $context->id,
636 'component' => $component,
637 'itemtype' => $itemtype
638 ];
639 if (!empty($itemid)) {
640 $select .= " AND itemid = :itemid";
641 $params['itemid'] = $itemid;
642 }
643
644 // Get conversations in this area where the specified users are a member of.
645 list($useridsql, $useridparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED);
646 $sql = "SELECT DISTINCT mcm.conversationid as id
647 FROM {message_conversation_members} mcm
648 INNER JOIN {message_conversations} mc
649 ON mc.id = mcm.conversationid
650 WHERE mcm.userid $useridsql AND $select";
651 $params += $useridparams;
652 $conversationids = array_keys($DB->get_records_sql($sql, $params));
653 if (!empty($conversationids)) {
654 list($conversationidsql, $conversationidparams) = $DB->get_in_or_equal($conversationids, SQL_PARAMS_NAMED);
655
656 // Get all the messages for these conversations which has some action stored for these users.
657 $sql = "SELECT DISTINCT m.id
658 FROM {messages} m
659 INNER JOIN {message_conversations} mc
660 ON mc.id = m.conversationid
661 INNER JOIN {message_user_actions} mua
662 ON mua.messageid = m.id
663 WHERE mua.userid $useridsql AND mc.id $conversationidsql";
664 $params = $useridparams + $conversationidparams;
665 $messageids = array_keys($DB->get_records_sql($sql, $params));
666 if (!empty($messageids)) {
667 // Delete all the user_actions for the messages on these conversations where the user has any action.
668 list($messageidsql, $messageidparams) = $DB->get_in_or_equal($messageids, SQL_PARAMS_NAMED);
669 $select = "messageid $messageidsql AND userid $useridsql";
670 $DB->delete_records_select('message_user_actions', $select, $messageidparams + $useridparams);
671 }
672
673 // Get all the messages for these conversations sent by these users.
674 $sql = "SELECT DISTINCT m.id
675 FROM {messages} m
676 WHERE m.useridfrom $useridsql AND m.conversationid $conversationidsql";
677 // Reuse the $params var because it contains the useridparams and the conversationids.
678 $messageids = array_keys($DB->get_records_sql($sql, $params));
679 if (!empty($messageids)) {
680 // Delete all the user_actions for the messages sent by any of these users.
681 $DB->delete_records_list('message_user_actions', 'messageid', $messageids);
682
683 // Delete all the messages sent by any of these users.
684 $DB->delete_records_list('messages', 'id', $messageids);
685 }
686
687 // In that case, conversations can't be removed, because they could have more members and messages.
688 // So, remove only users from the context conversations where they are member of.
689 $sql = "conversationid $conversationidsql AND userid $useridsql";
690 // Reuse the $params var because it contains the useridparams and the conversationids.
691 $DB->delete_records_select('message_conversation_members', $sql, $params);
16913e60 692
1c36c11a
MN
693 // Delete any conversation actions.
694 $DB->delete_records_select('message_conversation_actions', $sql, $params);
695
16913e60
SA
696 // Delete the favourite conversations.
697 $userlist = new \core_privacy\local\request\approved_userlist($context, 'core_message', $userids);
698 \core_favourites\privacy\provider::delete_favourites_for_userlist(
699 $userlist,
700 'message_conversations'
701 );
201cebea
SA
702 }
703 }
704
705 /**
706 * Deletes all records for multiple users within multiple contexts in a component area.
707 *
708 * @param int $userid The user identifier to delete information for.
709 * @param array $contextids The context identifiers to delete information for. Empty array means no context (for
710 * individual conversations).
711 * @param string $component The component to delete. Empty string means no component (for individual conversations).
712 * @param string $itemtype The itemtype of the component to delele. Empty string means no itemtype (for individual
713 * conversations).
714 * @param int $itemid Optional itemid associated with component.
715 */
716 protected static function delete_user_data_conversations(int $userid, array $contextids, string $component,
717 string $itemtype, int $itemid = 0) {
718 global $DB;
719
720 if (empty($contextids) && empty($component) && empty($itemtype) && empty($itemid)) {
721 // Individual conversations haven't context, component neither itemtype.
722 $select = "mc.contextid IS NULL";
723 $params = [];
724 } else {
725 list($contextidsql, $contextidparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED);
726 $select = "mc.contextid $contextidsql AND mc.component = :component AND mc.itemtype = :itemtype";
727 $params = [
728 'component' => $component,
729 'itemtype' => $itemtype
730 ];
731 $params += $contextidparams;
732 if (!empty($itemid)) {
733 $select .= " AND itemid = :itemid";
734 $params['itemid'] = $itemid;
735 }
736 }
737
738 // Get conversations in these contexts where the specified userid is a member of.
739 $sql = "SELECT DISTINCT mcm.conversationid as id
740 FROM {message_conversation_members} mcm
741 INNER JOIN {message_conversations} mc
742 ON mc.id = mcm.conversationid
743 WHERE mcm.userid = :userid AND $select";
744 $params['userid'] = $userid;
745 $conversationids = array_keys($DB->get_records_sql($sql, $params));
746 if (!empty($conversationids)) {
747 list($conversationidsql, $conversationidparams) = $DB->get_in_or_equal($conversationids, SQL_PARAMS_NAMED);
748
749 // Get all the messages for these conversations which has some action stored for the userid.
750 $sql = "SELECT DISTINCT m.id
751 FROM {messages} m
752 INNER JOIN {message_conversations} mc
753 ON mc.id = m.conversationid
754 INNER JOIN {message_user_actions} mua
755 ON mua.messageid = m.id
756 WHERE mua.userid = :userid AND mc.id $conversationidsql";
757 $params = ['userid' => $userid] + $conversationidparams;
758 $messageids = array_keys($DB->get_records_sql($sql, $params));
759 if (!empty($messageids)) {
760 // Delete all the user_actions for the messages on these conversations where the user has any action.
761 list($messageidsql, $messageidparams) = $DB->get_in_or_equal($messageids, SQL_PARAMS_NAMED);
762 $select = "messageid $messageidsql AND userid = :userid";
763 $DB->delete_records_select('message_user_actions', $select, $messageidparams + ['userid' => $userid]);
764 }
765
766 // Get all the messages for these conversations sent by the userid.
767 $sql = "SELECT DISTINCT m.id
768 FROM {messages} m
769 WHERE m.useridfrom = :userid AND m.conversationid $conversationidsql";
770 // Reuse the $params var because it contains the userid and the conversationids.
771 $messageids = array_keys($DB->get_records_sql($sql, $params));
772 if (!empty($messageids)) {
773 // Delete all the user_actions for the messages sent by the userid.
774 $DB->delete_records_list('message_user_actions', 'messageid', $messageids);
775
776 // Delete all the messages sent by the userid.
777 $DB->delete_records_list('messages', 'id', $messageids);
778 }
779
780 // In that case, conversations can't be removed, because they could have more members and messages.
781 // So, remove only userid from the context conversations where he/she is member of.
782 $sql = "conversationid $conversationidsql AND userid = :userid";
783 // Reuse the $params var because it contains the userid and the conversationids.
784 $DB->delete_records_select('message_conversation_members', $sql, $params);
16913e60 785
1c36c11a
MN
786 // Delete any conversation actions.
787 $DB->delete_records_select('message_conversation_actions', $sql, $params);
788
16913e60
SA
789 // Delete the favourite conversations.
790 if (empty($contextids) && empty($component) && empty($itemtype) && empty($itemid)) {
791 // Favourites for individual conversations are stored into the user context.
792 $favouritectxids = [\context_user::instance($userid)->id];
793 } else {
794 $favouritectxids = $contextids;
795 }
796 $contextlist = new \core_privacy\local\request\approved_contextlist(
797 \core_user::get_user($userid),
798 'core_message',
799 $favouritectxids
800 );
801 \core_favourites\privacy\provider::delete_favourites_for_user(
802 $contextlist,
803 'core_message',
804 'message_conversations'
805 );
201cebea
SA
806 }
807 }
808
52334296
SR
809 /**
810 * Delete all user data for the specified user.
811 *
812 * @param int $userid The user id
813 */
814 protected static function delete_user_data(int $userid) {
815 global $DB;
04cd1b3f 816
201cebea
SA
817 // Delete individual conversations information for this user.
818 self::delete_user_data_conversations($userid, [], '', '');
819
820 // Delete contacts, requests, users blocked and notifications.
04cd1b3f 821 $DB->delete_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
742c27b3
MN
822 $DB->delete_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?', [$userid, $userid]);
823 $DB->delete_records_select('message_users_blocked', 'userid = ? OR blockeduserid = ?', [$userid, $userid]);
04cd1b3f
MN
824 $DB->delete_records_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
825 }
826
827 /**
828 * Export the messaging contact data.
829 *
830 * @param int $userid
831 */
832 protected static function export_user_data_contacts(int $userid) {
833 global $DB;
834
52334296 835 $context = \context_user::instance($userid);
04cd1b3f
MN
836
837 // Get the user's contacts.
742c27b3 838 if ($contacts = $DB->get_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid], 'id ASC')) {
04cd1b3f
MN
839 $contactdata = [];
840 foreach ($contacts as $contact) {
841 $contactdata[] = (object) [
742c27b3 842 'contact' => transform::user($contact->contactid)
04cd1b3f
MN
843 ];
844 }
845 writer::with_context($context)->export_data([get_string('contacts', 'core_message')], (object) $contactdata);
846 }
742c27b3
MN
847 }
848
849 /**
850 * Export the messaging contact requests data.
851 *
852 * @param int $userid
853 */
854 protected static function export_user_data_contact_requests(int $userid) {
855 global $DB;
856
52334296 857 $context = \context_user::instance($userid);
742c27b3
MN
858
859 if ($contactrequests = $DB->get_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
860 [$userid, $userid], 'id ASC')) {
861 $contactrequestsdata = [];
862 foreach ($contactrequests as $contactrequest) {
863 if ($userid == $contactrequest->requesteduserid) {
864 $maderequest = false;
865 $contactid = $contactrequest->userid;
866 } else {
867 $maderequest = true;
868 $contactid = $contactrequest->requesteduserid;
869 }
870
871 $contactrequestsdata[] = (object) [
872 'contactrequest' => transform::user($contactid),
873 'maderequest' => transform::yesno($maderequest)
874 ];
875 }
876 writer::with_context($context)->export_data([get_string('contactrequests', 'core_message')],
877 (object) $contactrequestsdata);
878 }
879 }
880
881 /**
882 * Export the messaging blocked users data.
883 *
884 * @param int $userid
885 */
886 protected static function export_user_data_blocked_users(int $userid) {
887 global $DB;
888
52334296 889 $context = \context_user::instance($userid);
742c27b3
MN
890
891 if ($blockedusers = $DB->get_records('message_users_blocked', ['userid' => $userid], 'id ASC')) {
892 $blockedusersdata = [];
893 foreach ($blockedusers as $blockeduser) {
894 $blockedusersdata[] = (object) [
895 'blockeduser' => transform::user($blockeduser->blockeduserid)
896 ];
897 }
898 writer::with_context($context)->export_data([get_string('blockedusers', 'core_message')], (object) $blockedusersdata);
899 }
04cd1b3f
MN
900 }
901
902 /**
201cebea 903 * Export conversation messages.
04cd1b3f 904 *
201cebea
SA
905 * @param int $userid The user identifier.
906 * @param \stdClass $conversation The conversation to export the messages.
907 * @param \context $context The context to export for.
908 * @param array $subcontext The sub-context in which to export this data.
04cd1b3f 909 */
201cebea
SA
910 protected static function export_user_data_conversation_messages(int $userid, \stdClass $conversation, \context $context,
911 array $subcontext = []) {
04cd1b3f
MN
912 global $DB;
913
201cebea
SA
914 // Get all the messages for this conversation from start to finish.
915 $sql = "SELECT m.*, muadelete.timecreated as timedeleted, muaread.timecreated as timeread
916 FROM {messages} m
917 LEFT JOIN {message_user_actions} muadelete
918 ON m.id = muadelete.messageid AND muadelete.action = :deleteaction AND muadelete.userid = :deleteuserid
919 LEFT JOIN {message_user_actions} muaread
920 ON m.id = muaread.messageid AND muaread.action = :readaction AND muaread.userid = :readuserid
921 WHERE conversationid = :conversationid
922 ORDER BY m.timecreated ASC";
923 $messages = $DB->get_recordset_sql($sql, ['deleteaction' => \core_message\api::MESSAGE_ACTION_DELETED,
924 'readaction' => \core_message\api::MESSAGE_ACTION_READ, 'conversationid' => $conversation->id,
925 'deleteuserid' => $userid, 'readuserid' => $userid]);
926 $messagedata = [];
927 foreach ($messages as $message) {
928 $timeread = !is_null($message->timeread) ? transform::datetime($message->timeread) : '-';
929 $issender = $userid == $message->useridfrom;
930
931 $data = [
932 'issender' => transform::yesno($issender),
933 'message' => message_format_message_text($message),
934 'timecreated' => transform::datetime($message->timecreated),
a129ba04
JL
935 'timeread' => $timeread,
936 'customdata' => $message->customdata,
201cebea
SA
937 ];
938 if ($conversation->type == \core_message\api::MESSAGE_CONVERSATION_TYPE_GROUP && !$issender) {
939 // Only export sender for group conversations when is not the current user.
940 $data['sender'] = transform::user($message->useridfrom);
941 }
04cd1b3f 942
201cebea
SA
943 if (!is_null($message->timedeleted)) {
944 $data['timedeleted'] = transform::datetime($message->timedeleted);
945 }
04cd1b3f 946
201cebea
SA
947 $messagedata[] = (object) $data;
948 }
949 $messages->close();
950
951 if (!empty($messagedata)) {
952 // Get subcontext.
953 if (empty($conversation->contextid)) {
954 // Conversations without context are stored in 'Messages | <Other user id>'.
955 $members = $DB->get_records('message_conversation_members', ['conversationid' => $conversation->id]);
956 $members = array_filter($members, function ($member) use ($userid) {
957 return $member->userid != $userid;
958 });
959 if ($otheruser = reset($members)) {
960 $otherusertext = $otheruser->userid;
961 } else {
962 $otherusertext = get_string('unknownuser', 'core_message') . '_' . $conversation->id;
04cd1b3f
MN
963 }
964
201cebea
SA
965 $subcontext = array_merge(
966 $subcontext,
967 [get_string('messages', 'core_message'), $otherusertext]
968 );
16913e60
SA
969
970 // Get the context for the favourite conversation.
971 $conversationctx = \context_user::instance($userid);
201cebea
SA
972 } else {
973 // Conversations with context are stored in 'Messages | <Conversation item type> | <Conversation name>'.
974 if (get_string_manager()->string_exists($conversation->itemtype, $conversation->component)) {
975 $itemtypestring = get_string($conversation->itemtype, $conversation->component);
976 } else {
977 // If the itemtype doesn't exist in the component string file, the raw itemtype will be returned.
978 $itemtypestring = $conversation->itemtype;
04cd1b3f 979 }
04cd1b3f 980
201cebea
SA
981 $conversationname = get_string('privacy:export:conversationprefix', 'core_message') . $conversation->name;
982 $subcontext = array_merge(
983 $subcontext,
984 [get_string('messages', 'core_message'), $itemtypestring, $conversationname]
985 );
16913e60
SA
986
987 // Get the context for the favourite conversation.
988 $conversationctx = \context::instance_by_id($conversation->contextid);
04cd1b3f 989 }
201cebea
SA
990
991 // Export the conversation messages.
992 writer::with_context($context)->export_data($subcontext, (object) $messagedata);
16913e60
SA
993
994 // Get user's favourites information for the particular conversation.
995 $conversationfavourite = \core_favourites\privacy\provider::get_favourites_info_for_user($userid, $conversationctx,
996 'core_message', 'message_conversations', $conversation->id);
997 if ($conversationfavourite) {
998 // If the conversation has been favorited by the user, include it in the export.
999 writer::with_context($context)->export_related_data($subcontext, 'starred', (object) $conversationfavourite);
04cd1b3f 1000 }
1c36c11a
MN
1001
1002 // Check if the conversation was muted.
1003 $params = [
1004 'userid' => $userid,
1005 'conversationid' => $conversation->id,
1006 'action' => \core_message\api::CONVERSATION_ACTION_MUTED
1007 ];
1008 if ($mca = $DB->get_record('message_conversation_actions', $params)) {
1009 $mcatostore = [
1010 'muted' => transform::yesno(true),
1011 'timecreated' => transform::datetime($mca->timecreated),
1012 ];
1013 writer::with_context($context)->export_related_data($subcontext, 'muted', (object) $mcatostore);
1014 }
04cd1b3f
MN
1015 }
1016 }
1017
1018 /**
1019 * Export the notification data.
1020 *
1021 * @param int $userid
1022 */
1023 protected static function export_user_data_notifications(int $userid) {
1024 global $DB;
1025
52334296 1026 $context = \context_user::instance($userid);
04cd1b3f
MN
1027
1028 $notificationdata = [];
1029 $select = "useridfrom = ? OR useridto = ?";
1030 $notifications = $DB->get_recordset_select('notifications', $select, [$userid, $userid], 'timecreated ASC');
1031 foreach ($notifications as $notification) {
1032 $timeread = !is_null($notification->timeread) ? transform::datetime($notification->timeread) : '-';
1033
1034 $data = (object) [
1035 'subject' => $notification->subject,
1036 'fullmessage' => $notification->fullmessage,
1037 'smallmessage' => $notification->smallmessage,
1038 'component' => $notification->component,
1039 'eventtype' => $notification->eventtype,
1040 'contexturl' => $notification->contexturl,
1041 'contexturlname' => $notification->contexturlname,
1042 'timeread' => $timeread,
a129ba04
JL
1043 'timecreated' => transform::datetime($notification->timecreated),
1044 'customdata' => $notification->customdata,
04cd1b3f
MN
1045 ];
1046
1047 $notificationdata[] = $data;
1048 }
1049 $notifications->close();
1050
1051 writer::with_context($context)->export_data([get_string('notifications', 'core_message')], (object) $notificationdata);
1052 }
1053}