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