MDL-63712 core_message: Data should be in user context, not system
[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  * @copyright  2018 Mark Nelson <markn@moodle.com>
22  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23  */
24 namespace core_message\privacy;
26 use core_privacy\local\metadata\collection;
27 use core_privacy\local\request\approved_contextlist;
28 use core_privacy\local\request\contextlist;
29 use core_privacy\local\request\transform;
30 use core_privacy\local\request\writer;
32 defined('MOODLE_INTERNAL') || die();
34 /**
35  * Privacy Subsystem implementation for core_message.
36  *
37  * @copyright  2018 Mark Nelson <markn@moodle.com>
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class provider implements
41     \core_privacy\local\metadata\provider,
42     \core_privacy\local\request\subsystem\provider,
43     \core_privacy\local\request\user_preference_provider {
45     /**
46      * Return the fields which contain personal data.
47      *
48      * @param collection $items a reference to the collection to use to store the metadata.
49      * @return collection the updated collection of metadata items.
50      */
51     public static function get_metadata(collection $items) : collection {
52         $items->add_database_table(
53             'messages',
54             [
55                 'useridfrom' => 'privacy:metadata:messages:useridfrom',
56                 'conversationid' => 'privacy:metadata:messages:conversationid',
57                 'subject' => 'privacy:metadata:messages:subject',
58                 'fullmessage' => 'privacy:metadata:messages:fullmessage',
59                 'fullmessageformat' => 'privacy:metadata:messages:fullmessageformat',
60                 'fullmessagehtml' => 'privacy:metadata:messages:fullmessagehtml',
61                 'smallmessage' => 'privacy:metadata:messages:smallmessage',
62                 'timecreated' => 'privacy:metadata:messages:timecreated'
63             ],
64             'privacy:metadata:messages'
65         );
67         $items->add_database_table(
68             'message_user_actions',
69             [
70                 'userid' => 'privacy:metadata:message_user_actions:userid',
71                 'messageid' => 'privacy:metadata:message_user_actions:messageid',
72                 'action' => 'privacy:metadata:message_user_actions:action',
73                 'timecreated' => 'privacy:metadata:message_user_actions:timecreated'
74             ],
75             'privacy:metadata:message_user_actions'
76         );
78         $items->add_database_table(
79             'message_conversation_members',
80             [
81                 'conversationid' => 'privacy:metadata:message_conversation_members:conversationid',
82                 'userid' => 'privacy:metadata:message_conversation_members:userid',
83                 'timecreated' => 'privacy:metadata:message_conversation_members:timecreated',
84             ],
85             'privacy:metadata:message_conversation_members'
86         );
88         $items->add_database_table(
89             'message_contacts',
90             [
91                 'userid' => 'privacy:metadata:message_contacts:userid',
92                 'contactid' => 'privacy:metadata:message_contacts:contactid',
93                 'timecreated' => 'privacy:metadata:message_contacts:timecreated',
94             ],
95             'privacy:metadata:message_contacts'
96         );
98         $items->add_database_table(
99             'message_contact_requests',
100             [
101                 'userid' => 'privacy:metadata:message_contact_requests:userid',
102                 'requesteduserid' => 'privacy:metadata:message_contact_requests:requesteduserid',
103                 'timecreated' => 'privacy:metadata:message_contact_requests:timecreated',
104             ],
105             'privacy:metadata:message_contact_requests'
106         );
108         $items->add_database_table(
109             'message_users_blocked',
110             [
111                 'userid' => 'privacy:metadata:message_users_blocked:userid',
112                 'blockeduserid' => 'privacy:metadata:message_users_blocked:blockeduserid',
113                 'timecreated' => 'privacy:metadata:message_users_blocked:timecreated',
114             ],
115             'privacy:metadata:message_users_blocked'
116         );
118         $items->add_database_table(
119             'notifications',
120             [
121                 'useridfrom' => 'privacy:metadata:notifications:useridfrom',
122                 'useridto' => 'privacy:metadata:notifications:useridto',
123                 'subject' => 'privacy:metadata:notifications:subject',
124                 'fullmessage' => 'privacy:metadata:notifications:fullmessage',
125                 'fullmessageformat' => 'privacy:metadata:notifications:fullmessageformat',
126                 'fullmessagehtml' => 'privacy:metadata:notifications:fullmessagehtml',
127                 'smallmessage' => 'privacy:metadata:notifications:smallmessage',
128                 'component' => 'privacy:metadata:notifications:component',
129                 'eventtype' => 'privacy:metadata:notifications:eventtype',
130                 'contexturl' => 'privacy:metadata:notifications:contexturl',
131                 'contexturlname' => 'privacy:metadata:notifications:contexturlname',
132                 'timeread' => 'privacy:metadata:notifications:timeread',
133                 'timecreated' => 'privacy:metadata:notifications:timecreated',
134             ],
135             'privacy:metadata:notifications'
136         );
138         // Note - we are not adding the 'message' and 'message_read' tables
139         // as they are legacy tables. This information is moved to these
140         // new tables in a separate ad-hoc task. See MDL-61255.
142         // Now add that we also have user preferences.
143         $items->add_user_preference('core_message_messageprovider_settings',
144             'privacy:metadata:preference:core_message_settings');
146         return $items;
147     }
149     /**
150      * Store all user preferences for core message.
151      *
152      * @param  int $userid The userid of the user whose data is to be exported.
153      */
154     public static function export_user_preferences(int $userid) {
155         $preferences = get_user_preferences(null, null, $userid);
156         foreach ($preferences as $name => $value) {
157             if ((substr($name, 0, 16) == 'message_provider') || ($name == 'message_blocknoncontacts')) {
158                 writer::export_user_preference(
159                     'core_message',
160                     $name,
161                     $value,
162                     get_string('privacy:request:preference:set', 'core_message', (object) [
163                         'name' => $name,
164                         'value' => $value,
165                     ])
166                 );
167             }
168         }
169     }
171     /**
172      * Get the list of contexts that contain user information for the specified user.
173      *
174      * @param int $userid the userid.
175      * @return contextlist the list of contexts containing user info for the user.
176      */
177     public static function get_contexts_for_userid(int $userid) : contextlist {
178         global $DB;
180         $contextlist = new contextlist();
182         // Messages are in the user context.
183         // For the sake of performance, there is no need to call add_from_sql for each of the below cases.
184         // It is enough to add the user's context as soon as we come to the conclusion that the user has some data.
185         // Also, the order of checking is sorted by the probability of occurrence (just by guess).
186         // 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.
187         // So, checking messages table would suffice.
189         $hasdata = false;
190         $hasdata = $hasdata || $DB->record_exists_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
191         $hasdata = $hasdata || $DB->record_exists('message_conversation_members', ['userid' => $userid]);
192         $hasdata = $hasdata || $DB->record_exists('messages', ['useridfrom' => $userid]);
193         $hasdata = $hasdata || $DB->record_exists_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
194         $hasdata = $hasdata || $DB->record_exists_select('message_users_blocked', 'userid = ? OR blockeduserid = ?',
195                 [$userid, $userid]);
196         $hasdata = $hasdata || $DB->record_exists_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
197                 [$userid, $userid]);
199         if ($hasdata) {
200             $contextlist->add_user_context($userid);
201         }
203         return $contextlist;
204     }
206     /**
207      * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
208      *
209      * @param approved_contextlist $contextlist a list of contexts approved for export.
210      */
211     public static function export_user_data(approved_contextlist $contextlist) {
212         if (empty($contextlist->count())) {
213             return;
214         }
216         $userid = $contextlist->get_user()->id;
218         // Remove non-user and invalid contexts. If it ends up empty then early return.
219         $contexts = array_filter($contextlist->get_contexts(), function($context) use($userid) {
220             return $context->contextlevel == CONTEXT_USER && $context->instanceid == $userid;
221         });
223         if (empty($contexts)) {
224             return;
225         }
227         // Export the contacts.
228         self::export_user_data_contacts($userid);
230         // Export the contact requests.
231         self::export_user_data_contact_requests($userid);
233         // Export the blocked users.
234         self::export_user_data_blocked_users($userid);
236         // Export the notifications.
237         self::export_user_data_notifications($userid);
239         // Export the messages, with any related actions.
240         self::export_user_data_messages($userid);
241     }
243     /**
244      * Delete all data for all users in the specified context.
245      *
246      * @param \context $context the context to delete in.
247      */
248     public static function delete_data_for_all_users_in_context(\context $context) {
249         if ($context instanceof \context_user) {
250             static::delete_user_data($context->instanceid);
251         }
252     }
254     /**
255      * Delete all user data for the specified user, in the specified contexts.
256      *
257      * @param approved_contextlist $contextlist a list of contexts approved for deletion.
258      */
259     public static function delete_data_for_user(approved_contextlist $contextlist) {
260         if (empty($contextlist->count())) {
261             return;
262         }
264         $userid = $contextlist->get_user()->id;
266         // Remove non-user and invalid contexts. If it ends up empty then early return.
267         $contexts = array_filter($contextlist->get_contexts(), function($context) use($userid) {
268             return $context->contextlevel == CONTEXT_USER && $context->instanceid == $userid;
269         });
271         if (empty($contexts)) {
272             return;
273         }
275         static::delete_user_data($userid);
276     }
278     /**
279      * Delete all user data for the specified user.
280      *
281      * @param int $userid The user id
282      */
283     protected static function delete_user_data(int $userid) {
284         global $DB;
286         $DB->delete_records('messages', ['useridfrom' => $userid]);
287         $DB->delete_records('message_user_actions', ['userid' => $userid]);
288         $DB->delete_records('message_conversation_members', ['userid' => $userid]);
289         $DB->delete_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid]);
290         $DB->delete_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?', [$userid, $userid]);
291         $DB->delete_records_select('message_users_blocked', 'userid = ? OR blockeduserid = ?', [$userid, $userid]);
292         $DB->delete_records_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
293     }
295     /**
296      * Export the messaging contact data.
297      *
298      * @param int $userid
299      */
300     protected static function export_user_data_contacts(int $userid) {
301         global $DB;
303         $context = \context_user::instance($userid);
305         // Get the user's contacts.
306         if ($contacts = $DB->get_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid], 'id ASC')) {
307             $contactdata = [];
308             foreach ($contacts as $contact) {
309                 $contactdata[] = (object) [
310                     'contact' => transform::user($contact->contactid)
311                 ];
312             }
313             writer::with_context($context)->export_data([get_string('contacts', 'core_message')], (object) $contactdata);
314         }
315     }
317     /**
318      * Export the messaging contact requests data.
319      *
320      * @param int $userid
321      */
322     protected static function export_user_data_contact_requests(int $userid) {
323         global $DB;
325         $context = \context_user::instance($userid);
327         if ($contactrequests = $DB->get_records_select('message_contact_requests', 'userid = ? OR requesteduserid = ?',
328                 [$userid, $userid], 'id ASC')) {
329             $contactrequestsdata = [];
330             foreach ($contactrequests as $contactrequest) {
331                 if ($userid == $contactrequest->requesteduserid) {
332                     $maderequest = false;
333                     $contactid = $contactrequest->userid;
334                 } else {
335                     $maderequest = true;
336                     $contactid = $contactrequest->requesteduserid;
337                 }
339                 $contactrequestsdata[] = (object) [
340                     'contactrequest' => transform::user($contactid),
341                     'maderequest' => transform::yesno($maderequest)
342                 ];
343             }
344             writer::with_context($context)->export_data([get_string('contactrequests', 'core_message')],
345                 (object) $contactrequestsdata);
346         }
347     }
349     /**
350      * Export the messaging blocked users data.
351      *
352      * @param int $userid
353      */
354     protected static function export_user_data_blocked_users(int $userid) {
355         global $DB;
357         $context = \context_user::instance($userid);
359         if ($blockedusers = $DB->get_records('message_users_blocked', ['userid' => $userid], 'id ASC')) {
360             $blockedusersdata = [];
361             foreach ($blockedusers as $blockeduser) {
362                 $blockedusersdata[] = (object) [
363                     'blockeduser' => transform::user($blockeduser->blockeduserid)
364                 ];
365             }
366             writer::with_context($context)->export_data([get_string('blockedusers', 'core_message')], (object) $blockedusersdata);
367         }
368     }
370     /**
371      * Export the messaging data.
372      *
373      * @param int $userid
374      */
375     protected static function export_user_data_messages(int $userid) {
376         global $DB;
378         $context = \context_user::instance($userid);
380         $sql = "SELECT DISTINCT mcm.conversationid as id
381                   FROM {message_conversation_members} mcm
382                  WHERE mcm.userid = :userid";
383         if ($conversations = $DB->get_records_sql($sql, ['userid' => $userid])) {
384             // Ok, let's get the other users in the conversations.
385             $conversationids = array_keys($conversations);
386             list($conversationidsql, $conversationparams) = $DB->get_in_or_equal($conversationids, SQL_PARAMS_NAMED);
387             $userfields = \user_picture::fields('u');
388             $userssql = "SELECT mcm.conversationid, $userfields
389                            FROM {user} u
390                      INNER JOIN {message_conversation_members} mcm
391                              ON u.id = mcm.userid
392                           WHERE mcm.conversationid $conversationidsql
393                             AND mcm.userid != :userid
394                             AND u.deleted = 0";
395             $otherusers = $DB->get_records_sql($userssql, $conversationparams + ['userid' => $userid]);
396             foreach ($conversations as $conversation) {
397                 $otheruserfullname = get_string('unknownuser', 'core_message');
399                 // It's possible the other user has requested to be deleted, so might not exist
400                 // as a conversation member, or they have just been deleted.
401                 if (isset($otherusers[$conversation->id])) {
402                     $otheruserfullname = fullname($otherusers[$conversation->id]);
403                 }
405                 // Get all the messages for this conversation from start to finish.
406                 $sql = "SELECT m.*, muadelete.timecreated as timedeleted, muaread.timecreated as timeread
407                           FROM {messages} m
408                      LEFT JOIN {message_user_actions} muadelete
409                             ON m.id = muadelete.messageid AND muadelete.action = :deleteaction
410                      LEFT JOIN {message_user_actions} muaread
411                             ON m.id = muaread.messageid AND muaread.action = :readaction
412                          WHERE conversationid = :conversationid
413                       ORDER BY m.timecreated ASC";
414                 $messages = $DB->get_recordset_sql($sql, ['deleteaction' => \core_message\api::MESSAGE_ACTION_DELETED,
415                     'readaction' => \core_message\api::MESSAGE_ACTION_READ, 'conversationid' => $conversation->id]);
416                 $messagedata = [];
417                 foreach ($messages as $message) {
418                     $timeread = !is_null($message->timeread) ? transform::datetime($message->timeread) : '-';
419                     $issender = $userid == $message->useridfrom;
421                     $data = [
422                         'sender' => transform::yesno($issender),
423                         'message' => message_format_message_text($message),
424                         'timecreated' => transform::datetime($message->timecreated),
425                         'timeread' => $timeread
426                     ];
428                     if (!is_null($message->timedeleted)) {
429                         $data['timedeleted'] = transform::datetime($message->timedeleted);
430                     }
432                     $messagedata[] = (object) $data;
433                 }
434                 $messages->close();
436                 writer::with_context($context)->export_data([get_string('messages', 'core_message'), $otheruserfullname],
437                     (object) $messagedata);
438             }
439         }
440     }
442     /**
443      * Export the notification data.
444      *
445      * @param int $userid
446      */
447     protected static function export_user_data_notifications(int $userid) {
448         global $DB;
450         $context = \context_user::instance($userid);
452         $notificationdata = [];
453         $select = "useridfrom = ? OR useridto = ?";
454         $notifications = $DB->get_recordset_select('notifications', $select, [$userid, $userid], 'timecreated ASC');
455         foreach ($notifications as $notification) {
456             $timeread = !is_null($notification->timeread) ? transform::datetime($notification->timeread) : '-';
458             $data = (object) [
459                 'subject' => $notification->subject,
460                 'fullmessage' => $notification->fullmessage,
461                 'smallmessage' => $notification->smallmessage,
462                 'component' => $notification->component,
463                 'eventtype' => $notification->eventtype,
464                 'contexturl' => $notification->contexturl,
465                 'contexturlname' => $notification->contexturlname,
466                 'timeread' => $timeread,
467                 'timecreated' => transform::datetime($notification->timecreated)
468             ];
470             $notificationdata[] = $data;
471         }
472         $notifications->close();
474         writer::with_context($context)->export_data([get_string('notifications', 'core_message')], (object) $notificationdata);
475     }