MDL-60680 notifications: Support privacy API
[moodle.git] / message / classes / privacy / provider.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 // GNU General Public License for more details.
13 //
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
17 /**
18  * Privacy Subsystem implementation for core_message.
19  *
20  * @package    core_message
21  * @category   privacy
22  * @copyright  2018 Mark Nelson <markn@moodle.com>
23  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24  */
25 namespace core_message\privacy;
27 use core_privacy\local\metadata\collection;
28 use core_privacy\local\request\approved_contextlist;
29 use core_privacy\local\request\approved_userlist;
30 use core_privacy\local\request\contextlist;
31 use core_privacy\local\request\transform;
32 use core_privacy\local\request\userlist;
33 use core_privacy\local\request\writer;
35 defined('MOODLE_INTERNAL') || die();
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  */
43 class provider implements
44     // The messaging subsystem contains data.
45     \core_privacy\local\metadata\provider,
47     // The messaging subsystem provides all the messages at user context - i.e. individual ones.
48     \core_privacy\local\request\subsystem\provider,
50     // This plugin has some sitewide user preferences to export.
51     \core_privacy\local\request\user_preference_provider,
53     // This plugin is capable of determining which users have data within it.
54     \core_privacy\local\request\core_userlist_provider,
56     // The messaging subsystem provides a data service to other components.
57     \core_privacy\local\request\subsystem\plugin_provider {
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',
76                 'timecreated' => 'privacy:metadata:messages:timecreated',
77                 'customdata' => 'privacy:metadata:messages:customdata',
78             ],
79             'privacy:metadata:messages'
80         );
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         );
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         );
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         );
113         $items->add_database_table(
114             'message_contacts',
115             [
116                 'userid' => 'privacy:metadata:message_contacts:userid',
117                 'contactid' => 'privacy:metadata:message_contacts:contactid',
118                 'timecreated' => 'privacy:metadata:message_contacts:timecreated',
119             ],
120             'privacy:metadata:message_contacts'
121         );
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         );
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         );
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',
159                 'customdata' => 'privacy:metadata:notifications:customdata',
160             ],
161             'privacy:metadata:notifications'
162         );
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.
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');
172         // Add favourite conversations.
173         $items->link_subsystem('core_favourites', 'privacy:metadata:core_favourites');
175         return $items;
176     }
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) {
186             if (
187                 (substr($name, 0, 16) == 'message_provider') ||
188                 ($name == 'message_blocknoncontacts') ||
189                 ($name == 'message_entertosend')
190             ) {
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     }
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 {
211         global $DB;
213         $contextlist = new contextlist();
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.
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.
222         // So, checking messages table would suffice.
224         $hasdata = false;
225         $hasdata = $hasdata || $DB->record_exists_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
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]);
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]);
244         if ($hasdata) {
245             $contextlist->add_user_context($userid);
246         }
248         // Add favourite conversations.
249         \core_favourites\privacy\provider::add_contexts_for_userid($contextlist, $userid, 'core_message', 'message_conversations');
251         return $contextlist;
252     }
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;
262         $context = $userlist->get_context();
264         if (!$context instanceof \context_user) {
265             return;
266         }
268         $userid = $context->instanceid;
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.
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.
277         // So, checking messages table would suffice.
279         $hasdata = false;
280         $hasdata = $hasdata || $DB->record_exists_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
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]);
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]);
299         if ($hasdata) {
300             $userlist->add_user($userid);
301         }
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         }
312     }
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         }
324         $userid = $contextlist->get_user()->id;
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;
329         });
331         if (empty($contexts)) {
332             return;
333         }
335         // Export the contacts.
336         self::export_user_data_contacts($userid);
338         // Export the contact requests.
339         self::export_user_data_contact_requests($userid);
341         // Export the blocked users.
342         self::export_user_data_blocked_users($userid);
344         // Export the notifications.
345         self::export_user_data_notifications($userid);
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);
350     }
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) {
358         if ($context instanceof \context_user) {
359             static::delete_user_data($context->instanceid);
360         }
361     }
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) {
369         if (empty($contextlist->count())) {
370             return;
371         }
373         $userid = $contextlist->get_user()->id;
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;
378         });
380         if (empty($contexts)) {
381             return;
382         }
384         static::delete_user_data($userid);
385     }
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();
395         if (!$context instanceof \context_user) {
396             return;
397         }
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         });
404         if (empty($userids)) {
405             return;
406         }
408         static::delete_user_data($context->instanceid);
409     }
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         ];
440         if (!empty($itemid)) {
441             $sql .= " AND itemid = :itemid";
442             $params['itemid'] = $itemid;
443         }
445         $contextlist->add_from_sql($sql, $params);
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');
450     }
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         ];
472         if (!empty($itemid)) {
473             $sql .= " AND itemid = :itemid";
474             $params['itemid'] = $itemid;
475         }
477         $userlist->add_from_sql('userid', $sql, $params);
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         }
488     }
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;
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         ];
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         }
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     }
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;
550         if (empty($context)) {
551             return;
552         }
554         $select = "contextid = :contextid AND component = :component AND itemtype = :itemtype";
555         $params = [
556             'contextid' => $context->id,
557             'component' => $component,
558             'itemtype' => $itemtype
559         ];
561         if (!empty($itemid)) {
562             $select .= " AND itemid = :itemid";
563             $params['itemid'] = $itemid;
564         }
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);
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             }
581             // Delete messages and user_actions.
582             $DB->delete_records_list('message_user_actions', 'messageid', $messageids);
583             $DB->delete_records_list('messages', 'id', $messageids);
585             // Delete members and conversations.
586             $DB->delete_records_list('message_conversation_members', 'conversationid', $conversationids);
587             $DB->delete_records_list('message_conversation_actions', 'conversationid', $conversationids);
588             $DB->delete_records_list('message_conversations', 'id', $conversationids);
589         }
590     }
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     }
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;
627         $userids = $userlist->get_userids();
628         if (empty($userids)) {
629             return;
630         }
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         }
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);
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             }
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);
683                 // Delete all the messages sent by any of these users.
684                 $DB->delete_records_list('messages', 'id', $messageids);
685             }
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);
693             // Delete any conversation actions.
694             $DB->delete_records_select('message_conversation_actions', $sql, $params);
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             );
702         }
703     }
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;
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         }
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);
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             }
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);
776                 // Delete all the messages sent by the userid.
777                 $DB->delete_records_list('messages', 'id', $messageids);
778             }
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);
786             // Delete any conversation actions.
787             $DB->delete_records_select('message_conversation_actions', $sql, $params);
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             );
806         }
807     }
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;
817         // Delete individual conversations information for this user.
818         self::delete_user_data_conversations($userid, [], '', '');
820         // Delete contacts, requests, users blocked and notifications.
821         $DB->delete_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
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]);
824         $DB->delete_records_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
825     }
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;
835         $context = \context_user::instance($userid);
837         // Get the user's contacts.
838         if ($contacts = $DB->get_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid], 'id ASC')) {
839             $contactdata = [];
840             foreach ($contacts as $contact) {
841                 $contactdata[] = (object) [
842                     'contact' => transform::user($contact->contactid)
843                 ];
844             }
845             writer::with_context($context)->export_data([get_string('contacts', 'core_message')], (object) $contactdata);
846         }
847     }
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;
857         $context = \context_user::instance($userid);
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                 }
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     }
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;
889         $context = \context_user::instance($userid);
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         }
900     }
902     /**
903      * Export conversation messages.
904      *
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.
909      */
910     protected static function export_user_data_conversation_messages(int $userid, \stdClass $conversation, \context $context,
911                                                                      array $subcontext = []) {
912         global $DB;
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;
931             $data = [
932                 'issender' => transform::yesno($issender),
933                 'message' => message_format_message_text($message),
934                 'timecreated' => transform::datetime($message->timecreated),
935                 'timeread' => $timeread,
936                 'customdata' => $message->customdata,
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             }
943             if (!is_null($message->timedeleted)) {
944                 $data['timedeleted'] = transform::datetime($message->timedeleted);
945             }
947             $messagedata[] = (object) $data;
948         }
949         $messages->close();
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;
963                 }
965                 $subcontext = array_merge(
966                     $subcontext,
967                     [get_string('messages', 'core_message'), $otherusertext]
968                 );
970                 // Get the context for the favourite conversation.
971                 $conversationctx = \context_user::instance($userid);
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;
979                 }
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                 );
987                 // Get the context for the favourite conversation.
988                 $conversationctx = \context::instance_by_id($conversation->contextid);
989             }
991             // Export the conversation messages.
992             writer::with_context($context)->export_data($subcontext, (object) $messagedata);
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);
1000             }
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             }
1015         }
1016     }
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;
1026         $context = \context_user::instance($userid);
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) : '-';
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,
1043                 'timecreated' => transform::datetime($notification->timecreated),
1044                 'customdata' => $notification->customdata,
1045             ];
1047             $notificationdata[] = $data;
1048         }
1049         $notifications->close();
1051         writer::with_context($context)->export_data([get_string('notifications', 'core_message')], (object) $notificationdata);
1052     }