Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
   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  }