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