MDL-63712 core_message: Data should be in user context, not system
[moodle.git] / message / classes / privacy / provider.php
CommitLineData
04cd1b3f
MN
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17/**
18 * Privacy Subsystem implementation for core_message.
19 *
20 * @package core_message
21 * @copyright 2018 Mark Nelson <markn@moodle.com>
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24namespace core_message\privacy;
25
26use core_privacy\local\metadata\collection;
27use core_privacy\local\request\approved_contextlist;
28use core_privacy\local\request\contextlist;
29use core_privacy\local\request\transform;
30use core_privacy\local\request\writer;
31
32defined('MOODLE_INTERNAL') || die();
33
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 */
40class provider implements
41 \core_privacy\local\metadata\provider,
42 \core_privacy\local\request\subsystem\provider,
43 \core_privacy\local\request\user_preference_provider {
44
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 );
66
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 );
77
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 );
87
88 $items->add_database_table(
89 'message_contacts',
90 [
91 'userid' => 'privacy:metadata:message_contacts:userid',
92 'contactid' => 'privacy:metadata:message_contacts:contactid',
742c27b3 93 'timecreated' => 'privacy:metadata:message_contacts:timecreated',
04cd1b3f
MN
94 ],
95 'privacy:metadata:message_contacts'
96 );
97
742c27b3
MN
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 );
107
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 );
117
04cd1b3f
MN
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 );
137
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.
141
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');
145
146 return $items;
147 }
148
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 }
170
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 {
52334296
SR
178 global $DB;
179
04cd1b3f 180 $contextlist = new contextlist();
52334296
SR
181
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.
188
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]);
198
199 if ($hasdata) {
200 $contextlist->add_user_context($userid);
201 }
04cd1b3f
MN
202
203 return $contextlist;
204 }
205
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 }
215
52334296
SR
216 $userid = $contextlist->get_user()->id;
217
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;
04cd1b3f
MN
221 });
222
223 if (empty($contexts)) {
224 return;
225 }
226
04cd1b3f
MN
227 // Export the contacts.
228 self::export_user_data_contacts($userid);
229
742c27b3
MN
230 // Export the contact requests.
231 self::export_user_data_contact_requests($userid);
232
233 // Export the blocked users.
234 self::export_user_data_blocked_users($userid);
235
04cd1b3f
MN
236 // Export the notifications.
237 self::export_user_data_notifications($userid);
238
239 // Export the messages, with any related actions.
240 self::export_user_data_messages($userid);
241 }
242
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) {
52334296
SR
249 if ($context instanceof \context_user) {
250 static::delete_user_data($context->instanceid);
04cd1b3f 251 }
04cd1b3f
MN
252 }
253
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) {
04cd1b3f
MN
260 if (empty($contextlist->count())) {
261 return;
262 }
263
52334296
SR
264 $userid = $contextlist->get_user()->id;
265
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;
04cd1b3f
MN
269 });
270
271 if (empty($contexts)) {
272 return;
273 }
274
52334296
SR
275 static::delete_user_data($userid);
276 }
277
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;
04cd1b3f
MN
285
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]);
742c27b3
MN
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]);
04cd1b3f
MN
292 $DB->delete_records_select('notifications', 'useridfrom = ? OR useridto = ?', [$userid, $userid]);
293 }
294
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;
302
52334296 303 $context = \context_user::instance($userid);
04cd1b3f
MN
304
305 // Get the user's contacts.
742c27b3 306 if ($contacts = $DB->get_records_select('message_contacts', 'userid = ? OR contactid = ?', [$userid, $userid], 'id ASC')) {
04cd1b3f
MN
307 $contactdata = [];
308 foreach ($contacts as $contact) {
309 $contactdata[] = (object) [
742c27b3 310 'contact' => transform::user($contact->contactid)
04cd1b3f
MN
311 ];
312 }
313 writer::with_context($context)->export_data([get_string('contacts', 'core_message')], (object) $contactdata);
314 }
742c27b3
MN
315 }
316
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;
324
52334296 325 $context = \context_user::instance($userid);
742c27b3
MN
326
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 }
338
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 }
348
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;
356
52334296 357 $context = \context_user::instance($userid);
742c27b3
MN
358
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 }
04cd1b3f
MN
368 }
369
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;
377
52334296 378 $context = \context_user::instance($userid);
04cd1b3f
MN
379
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');
398
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 }
404
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;
420
421 $data = [
422 'sender' => transform::yesno($issender),
423 'message' => message_format_message_text($message),
424 'timecreated' => transform::datetime($message->timecreated),
425 'timeread' => $timeread
426 ];
427
428 if (!is_null($message->timedeleted)) {
429 $data['timedeleted'] = transform::datetime($message->timedeleted);
430 }
431
432 $messagedata[] = (object) $data;
433 }
434 $messages->close();
435
436 writer::with_context($context)->export_data([get_string('messages', 'core_message'), $otheruserfullname],
437 (object) $messagedata);
438 }
439 }
440 }
441
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;
449
52334296 450 $context = \context_user::instance($userid);
04cd1b3f
MN
451
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) : '-';
457
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 ];
469
470 $notificationdata[] = $data;
471 }
472 $notifications->close();
473
474 writer::with_context($context)->export_data([get_string('notifications', 'core_message')], (object) $notificationdata);
475 }
476}