MDL-63712 core_message: support removal of multiple users in a context
[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     \core_privacy\local\metadata\provider,
45     \core_privacy\local\request\subsystem\provider,
46     \core_privacy\local\request\user_preference_provider,
47     \core_privacy\local\request\core_userlist_provider {
49     /**
50      * Return the fields which contain personal data.
51      *
52      * @param collection $items a reference to the collection to use to store the metadata.
53      * @return collection the updated collection of metadata items.
54      */
55     public static function get_metadata(collection $items) : collection {
56         $items->add_database_table(
57             'messages',
58             [
59                 'useridfrom' => 'privacy:metadata:messages:useridfrom',
60                 'conversationid' => 'privacy:metadata:messages:conversationid',
61                 'subject' => 'privacy:metadata:messages:subject',
62                 'fullmessage' => 'privacy:metadata:messages:fullmessage',
63                 'fullmessageformat' => 'privacy:metadata:messages:fullmessageformat',
64                 'fullmessagehtml' => 'privacy:metadata:messages:fullmessagehtml',
65                 'smallmessage' => 'privacy:metadata:messages:smallmessage',
66                 'timecreated' => 'privacy:metadata:messages:timecreated'
67             ],
68             'privacy:metadata:messages'
69         );
71         $items->add_database_table(
72             'message_user_actions',
73             [
74                 'userid' => 'privacy:metadata:message_user_actions:userid',
75                 'messageid' => 'privacy:metadata:message_user_actions:messageid',
76                 'action' => 'privacy:metadata:message_user_actions:action',
77                 'timecreated' => 'privacy:metadata:message_user_actions:timecreated'
78             ],
79             'privacy:metadata:message_user_actions'
80         );
82         $items->add_database_table(
83             'message_conversation_members',
84             [
85                 'conversationid' => 'privacy:metadata:message_conversation_members:conversationid',
86                 'userid' => 'privacy:metadata:message_conversation_members:userid',
87                 'timecreated' => 'privacy:metadata:message_conversation_members:timecreated',
88             ],
89             'privacy:metadata:message_conversation_members'
90         );
92         $items->add_database_table(
93             'message_contacts',
94             [
95                 'userid' => 'privacy:metadata:message_contacts:userid',
96                 'contactid' => 'privacy:metadata:message_contacts:contactid',
97                 'timecreated' => 'privacy:metadata:message_contacts:timecreated',
98             ],
99             'privacy:metadata:message_contacts'
100         );
102         $items->add_database_table(
103             'message_contact_requests',
104             [
105                 'userid' => 'privacy:metadata:message_contact_requests:userid',
106                 'requesteduserid' => 'privacy:metadata:message_contact_requests:requesteduserid',
107                 'timecreated' => 'privacy:metadata:message_contact_requests:timecreated',
108             ],
109             'privacy:metadata:message_contact_requests'
110         );
112         $items->add_database_table(
113             'message_users_blocked',
114             [
115                 'userid' => 'privacy:metadata:message_users_blocked:userid',
116                 'blockeduserid' => 'privacy:metadata:message_users_blocked:blockeduserid',
117                 'timecreated' => 'privacy:metadata:message_users_blocked:timecreated',
118             ],
119             'privacy:metadata:message_users_blocked'
120         );
122         $items->add_database_table(
123             'notifications',
124             [
125                 'useridfrom' => 'privacy:metadata:notifications:useridfrom',
126                 'useridto' => 'privacy:metadata:notifications:useridto',
127                 'subject' => 'privacy:metadata:notifications:subject',
128                 'fullmessage' => 'privacy:metadata:notifications:fullmessage',
129                 'fullmessageformat' => 'privacy:metadata:notifications:fullmessageformat',
130                 'fullmessagehtml' => 'privacy:metadata:notifications:fullmessagehtml',
131                 'smallmessage' => 'privacy:metadata:notifications:smallmessage',
132                 'component' => 'privacy:metadata:notifications:component',
133                 'eventtype' => 'privacy:metadata:notifications:eventtype',
134                 'contexturl' => 'privacy:metadata:notifications:contexturl',
135                 'contexturlname' => 'privacy:metadata:notifications:contexturlname',
136                 'timeread' => 'privacy:metadata:notifications:timeread',
137                 'timecreated' => 'privacy:metadata:notifications:timecreated',
138             ],
139             'privacy:metadata:notifications'
140         );
142         // Note - we are not adding the 'message' and 'message_read' tables
143         // as they are legacy tables. This information is moved to these
144         // new tables in a separate ad-hoc task. See MDL-61255.
146         // Now add that we also have user preferences.
147         $items->add_user_preference('core_message_messageprovider_settings',
148             'privacy:metadata:preference:core_message_settings');
150         return $items;
151     }
153     /**
154      * Store all user preferences for core message.
155      *
156      * @param  int $userid The userid of the user whose data is to be exported.
157      */
158     public static function export_user_preferences(int $userid) {
159         $preferences = get_user_preferences(null, null, $userid);
160         foreach ($preferences as $name => $value) {
161             if ((substr($name, 0, 16) == 'message_provider') || ($name == 'message_blocknoncontacts')) {
162                 writer::export_user_preference(
163                     'core_message',
164                     $name,
165                     $value,
166                     get_string('privacy:request:preference:set', 'core_message', (object) [
167                         'name' => $name,
168                         'value' => $value,
169                     ])
170                 );
171             }
172         }
173     }
175     /**
176      * Get the list of contexts that contain user information for the specified user.
177      *
178      * @param int $userid the userid.
179      * @return contextlist the list of contexts containing user info for the user.
180      */
181     public static function get_contexts_for_userid(int $userid) : contextlist {
182         global $DB;
184         $contextlist = new contextlist();
186         // Messages are in the user context.
187         // For the sake of performance, there is no need to call add_from_sql for each of the below cases.
188         // It is enough to add the user's context as soon as we come to the conclusion that the user has some data.
189         // Also, the order of checking is sorted by the probability of occurrence (just by guess).
190         // 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.
191         // So, checking messages table would suffice.
193         $hasdata = false;
194         $hasdata = $hasdata || $DB->record_exists_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
195         $hasdata = $hasdata || $DB->record_exists('message_conversation_members', ['userid' => $userid]);
196         $hasdata = $hasdata || $DB->record_exists('messages', ['useridfrom' => $userid]);
197         $hasdata = $hasdata || $DB->record_exists_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
198         $hasdata = $hasdata || $DB->record_exists_select('message_users_blocked', 'userid = ? OR blockeduserid = ?',
199                 [$userid, $userid]);
200         $hasdata = $hasdata || $DB->record_exists_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
201                 [$userid, $userid]);
203         if ($hasdata) {
204             $contextlist->add_user_context($userid);
205         }
207         return $contextlist;
208     }
210     /**
211      * Get the list of users who have data within a context.
212      *
213      * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
214      */
215     public static function get_users_in_context(userlist $userlist) {
216         global $DB;
218         $context = $userlist->get_context();
220         if (!$context instanceof \context_user) {
221             return;
222         }
224         $userid = $context->instanceid;
226         // Messages are in the user context.
227         // For the sake of performance, there is no need to call add_from_sql for each of the below cases.
228         // It is enough to add the user's context as soon as we come to the conclusion that the user has some data.
229         // Also, the order of checking is sorted by the probability of occurrence (just by guess).
230         // 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.
231         // So, checking messages table would suffice.
233         $hasdata = false;
234         $hasdata = $hasdata || $DB->record_exists_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
235         $hasdata = $hasdata || $DB->record_exists('message_conversation_members', ['userid' => $userid]);
236         $hasdata = $hasdata || $DB->record_exists('messages', ['useridfrom' => $userid]);
237         $hasdata = $hasdata || $DB->record_exists_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
238         $hasdata = $hasdata || $DB->record_exists_select('message_users_blocked', 'userid = ? OR blockeduserid = ?',
239                         [$userid, $userid]);
240         $hasdata = $hasdata || $DB->record_exists_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
241                         [$userid, $userid]);
243         if ($hasdata) {
244             $userlist->add_user($userid);
245         }
246     }
248     /**
249      * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
250      *
251      * @param approved_contextlist $contextlist a list of contexts approved for export.
252      */
253     public static function export_user_data(approved_contextlist $contextlist) {
254         if (empty($contextlist->count())) {
255             return;
256         }
258         $userid = $contextlist->get_user()->id;
260         // Remove non-user and invalid contexts. If it ends up empty then early return.
261         $contexts = array_filter($contextlist->get_contexts(), function($context) use($userid) {
262             return $context->contextlevel == CONTEXT_USER && $context->instanceid == $userid;
263         });
265         if (empty($contexts)) {
266             return;
267         }
269         // Export the contacts.
270         self::export_user_data_contacts($userid);
272         // Export the contact requests.
273         self::export_user_data_contact_requests($userid);
275         // Export the blocked users.
276         self::export_user_data_blocked_users($userid);
278         // Export the notifications.
279         self::export_user_data_notifications($userid);
281         // Export the messages, with any related actions.
282         self::export_user_data_messages($userid);
283     }
285     /**
286      * Delete all data for all users in the specified context.
287      *
288      * @param \context $context the context to delete in.
289      */
290     public static function delete_data_for_all_users_in_context(\context $context) {
291         if ($context instanceof \context_user) {
292             static::delete_user_data($context->instanceid);
293         }
294     }
296     /**
297      * Delete all user data for the specified user, in the specified contexts.
298      *
299      * @param approved_contextlist $contextlist a list of contexts approved for deletion.
300      */
301     public static function delete_data_for_user(approved_contextlist $contextlist) {
302         if (empty($contextlist->count())) {
303             return;
304         }
306         $userid = $contextlist->get_user()->id;
308         // Remove non-user and invalid contexts. If it ends up empty then early return.
309         $contexts = array_filter($contextlist->get_contexts(), function($context) use($userid) {
310             return $context->contextlevel == CONTEXT_USER && $context->instanceid == $userid;
311         });
313         if (empty($contexts)) {
314             return;
315         }
317         static::delete_user_data($userid);
318     }
320     /**
321      * Delete multiple users within a single context.
322      *
323      * @param   approved_userlist       $userlist The approved context and user information to delete information for.
324      */
325     public static function delete_data_for_users(approved_userlist $userlist) {
326         $context = $userlist->get_context();
328         if (!$context instanceof \context_user) {
329             return;
330         }
332         // Remove invalid users. If it ends up empty then early return.
333         $userids = array_filter($userlist->get_userids(), function($userid) use($context) {
334             return $context->instanceid == $userid;
335         });
337         if (empty($userids)) {
338             return;
339         }
341         static::delete_user_data($context->instanceid);
342     }
344     /**
345      * Delete all user data for the specified user.
346      *
347      * @param int $userid The user id
348      */
349     protected static function delete_user_data(int $userid) {
350         global $DB;
352         $DB->delete_records('messages', ['useridfrom' => $userid]);
353         $DB->delete_records('message_user_actions', ['userid' => $userid]);
354         $DB->delete_records('message_conversation_members', ['userid' => $userid]);
355         $DB->delete_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
356         $DB->delete_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?', [$userid, $userid]);
357         $DB->delete_records_select('message_users_blocked', 'userid = ? OR blockeduserid = ?', [$userid, $userid]);
358         $DB->delete_records_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
359     }
361     /**
362      * Export the messaging contact data.
363      *
364      * @param int $userid
365      */
366     protected static function export_user_data_contacts(int $userid) {
367         global $DB;
369         $context = \context_user::instance($userid);
371         // Get the user's contacts.
372         if ($contacts = $DB->get_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid], 'id ASC')) {
373             $contactdata = [];
374             foreach ($contacts as $contact) {
375                 $contactdata[] = (object) [
376                     'contact' => transform::user($contact->contactid)
377                 ];
378             }
379             writer::with_context($context)->export_data([get_string('contacts', 'core_message')], (object) $contactdata);
380         }
381     }
383     /**
384      * Export the messaging contact requests data.
385      *
386      * @param int $userid
387      */
388     protected static function export_user_data_contact_requests(int $userid) {
389         global $DB;
391         $context = \context_user::instance($userid);
393         if ($contactrequests = $DB->get_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
394                 [$userid, $userid], 'id ASC')) {
395             $contactrequestsdata = [];
396             foreach ($contactrequests as $contactrequest) {
397                 if ($userid == $contactrequest->requesteduserid) {
398                     $maderequest = false;
399                     $contactid = $contactrequest->userid;
400                 } else {
401                     $maderequest = true;
402                     $contactid = $contactrequest->requesteduserid;
403                 }
405                 $contactrequestsdata[] = (object) [
406                     'contactrequest' => transform::user($contactid),
407                     'maderequest' => transform::yesno($maderequest)
408                 ];
409             }
410             writer::with_context($context)->export_data([get_string('contactrequests', 'core_message')],
411                 (object) $contactrequestsdata);
412         }
413     }
415     /**
416      * Export the messaging blocked users data.
417      *
418      * @param int $userid
419      */
420     protected static function export_user_data_blocked_users(int $userid) {
421         global $DB;
423         $context = \context_user::instance($userid);
425         if ($blockedusers = $DB->get_records('message_users_blocked', ['userid' => $userid], 'id ASC')) {
426             $blockedusersdata = [];
427             foreach ($blockedusers as $blockeduser) {
428                 $blockedusersdata[] = (object) [
429                     'blockeduser' => transform::user($blockeduser->blockeduserid)
430                 ];
431             }
432             writer::with_context($context)->export_data([get_string('blockedusers', 'core_message')], (object) $blockedusersdata);
433         }
434     }
436     /**
437      * Export the messaging data.
438      *
439      * @param int $userid
440      */
441     protected static function export_user_data_messages(int $userid) {
442         global $DB;
444         $context = \context_user::instance($userid);
446         $sql = "SELECT DISTINCT mcm.conversationid as id
447                   FROM {message_conversation_members} mcm
448                  WHERE mcm.userid = :userid";
449         if ($conversations = $DB->get_records_sql($sql, ['userid' => $userid])) {
450             // Ok, let's get the other users in the conversations.
451             $conversationids = array_keys($conversations);
452             list($conversationidsql, $conversationparams) = $DB->get_in_or_equal($conversationids, SQL_PARAMS_NAMED);
453             $userfields = \user_picture::fields('u');
454             $userssql = "SELECT mcm.conversationid, $userfields
455                            FROM {user} u
456                      INNER JOIN {message_conversation_members} mcm
457                              ON u.id = mcm.userid
458                           WHERE mcm.conversationid $conversationidsql
459                             AND mcm.userid != :userid
460                             AND u.deleted = 0";
461             $otherusers = $DB->get_records_sql($userssql, $conversationparams + ['userid' => $userid]);
462             foreach ($conversations as $conversation) {
463                 $otheruserfullname = get_string('unknownuser', 'core_message');
465                 // It's possible the other user has requested to be deleted, so might not exist
466                 // as a conversation member, or they have just been deleted.
467                 if (isset($otherusers[$conversation->id])) {
468                     $otheruserfullname = fullname($otherusers[$conversation->id]);
469                 }
471                 // Get all the messages for this conversation from start to finish.
472                 $sql = "SELECT m.*, muadelete.timecreated as timedeleted, muaread.timecreated as timeread
473                           FROM {messages} m
474                      LEFT JOIN {message_user_actions} muadelete
475                             ON m.id = muadelete.messageid AND muadelete.action = :deleteaction
476                      LEFT JOIN {message_user_actions} muaread
477                             ON m.id = muaread.messageid AND muaread.action = :readaction
478                          WHERE conversationid = :conversationid
479                       ORDER BY m.timecreated ASC";
480                 $messages = $DB->get_recordset_sql($sql, ['deleteaction' => \core_message\api::MESSAGE_ACTION_DELETED,
481                     'readaction' => \core_message\api::MESSAGE_ACTION_READ, 'conversationid' => $conversation->id]);
482                 $messagedata = [];
483                 foreach ($messages as $message) {
484                     $timeread = !is_null($message->timeread) ? transform::datetime($message->timeread) : '-';
485                     $issender = $userid == $message->useridfrom;
487                     $data = [
488                         'sender' => transform::yesno($issender),
489                         'message' => message_format_message_text($message),
490                         'timecreated' => transform::datetime($message->timecreated),
491                         'timeread' => $timeread
492                     ];
494                     if (!is_null($message->timedeleted)) {
495                         $data['timedeleted'] = transform::datetime($message->timedeleted);
496                     }
498                     $messagedata[] = (object) $data;
499                 }
500                 $messages->close();
502                 writer::with_context($context)->export_data([get_string('messages', 'core_message'), $otheruserfullname],
503                     (object) $messagedata);
504             }
505         }
506     }
508     /**
509      * Export the notification data.
510      *
511      * @param int $userid
512      */
513     protected static function export_user_data_notifications(int $userid) {
514         global $DB;
516         $context = \context_user::instance($userid);
518         $notificationdata = [];
519         $select = "useridfrom = ? OR useridto = ?";
520         $notifications = $DB->get_recordset_select('notifications', $select, [$userid, $userid], 'timecreated ASC');
521         foreach ($notifications as $notification) {
522             $timeread = !is_null($notification->timeread) ? transform::datetime($notification->timeread) : '-';
524             $data = (object) [
525                 'subject' => $notification->subject,
526                 'fullmessage' => $notification->fullmessage,
527                 'smallmessage' => $notification->smallmessage,
528                 'component' => $notification->component,
529                 'eventtype' => $notification->eventtype,
530                 'contexturl' => $notification->contexturl,
531                 'contexturlname' => $notification->contexturlname,
532                 'timeread' => $timeread,
533                 'timecreated' => transform::datetime($notification->timecreated)
534             ];
536             $notificationdata[] = $data;
537         }
538         $notifications->close();
540         writer::with_context($context)->export_data([get_string('notifications', 'core_message')], (object) $notificationdata);
541     }