Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is 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   * Adhoc task handling migrating data to the new messaging table schema.
  19   *
  20   * @package    core_message
  21   * @copyright  2018 Mark Nelson <markn@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  namespace core_message\task;
  26  
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  /**
  30   * Class handling migrating data to the new messaging table schema.
  31   *
  32   * @package    core_message
  33   * @copyright  2018 Mark Nelson <markn@moodle.com>
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class migrate_message_data extends \core\task\adhoc_task {
  37  
  38      /**
  39       * Run the migration task.
  40       */
  41      public function execute() {
  42          global $DB;
  43  
  44          $userid = $this->get_custom_data()->userid;
  45  
  46          // Get the user's preference.
  47          $hasbeenmigrated = get_user_preferences('core_message_migrate_data', false, $userid);
  48  
  49          if (!$hasbeenmigrated) {
  50              // To determine if we should update the preference.
  51              $updatepreference = true;
  52  
  53              // Get all the users the current user has received a message from.
  54              $sql = "SELECT DISTINCT(useridfrom)
  55                        FROM {message} m
  56                       WHERE useridto = ?
  57                       UNION
  58                      SELECT DISTINCT(useridfrom)
  59                        FROM {message_read} m
  60                       WHERE useridto = ?";
  61              $users = $DB->get_records_sql($sql, [$userid, $userid]);
  62  
  63              // Get all the users the current user has messaged.
  64              $sql = "SELECT DISTINCT(useridto)
  65                        FROM {message} m
  66                       WHERE useridfrom = ?
  67                       UNION
  68                      SELECT DISTINCT(useridto)
  69                        FROM {message_read} m
  70                       WHERE useridfrom = ?";
  71              $users = $users + $DB->get_records_sql($sql, [$userid, $userid]);
  72              if (!empty($users)) {
  73                  // Loop through each user and migrate the data.
  74                  foreach ($users as $otheruserid => $user) {
  75                      $ids = [$userid, $otheruserid];
  76                      sort($ids);
  77                      $key = implode('_', $ids);
  78  
  79                      // Set the lock data.
  80                      $timeout = 5; // In seconds.
  81                      $locktype = 'core_message_migrate_data';
  82  
  83                      // Get an instance of the currently configured lock factory.
  84                      $lockfactory = \core\lock\lock_config::get_lock_factory($locktype);
  85  
  86                      // See if we can grab this lock.
  87                      if ($lock = $lockfactory->get_lock($key, $timeout)) {
  88                          try {
  89                              $transaction = $DB->start_delegated_transaction();
  90                              $this->migrate_data($userid, $otheruserid);
  91                              $transaction->allow_commit();
  92                          } catch (\Throwable $e) {
  93                              throw $e;
  94                          } finally {
  95                              $lock->release();
  96                          }
  97                      } else {
  98                          // Couldn't get a lock, move on to next user but make sure we don't update user preference so
  99                          // we still try again.
 100                          $updatepreference = false;
 101                          continue;
 102                      }
 103                  }
 104              }
 105  
 106              if ($updatepreference) {
 107                  set_user_preference('core_message_migrate_data', true, $userid);
 108              } else {
 109                  // Throwing an exception in the task will mean that it isn't removed from the queue and is tried again.
 110                  throw new \moodle_exception('Task failed.');
 111              }
 112          }
 113      }
 114  
 115      /**
 116       * Helper function to deal with migrating the data.
 117       *
 118       * @param int $userid The current user id.
 119       * @param int $otheruserid The user id of the other user in the conversation.
 120       * @throws \dml_exception
 121       */
 122      private function migrate_data($userid, $otheruserid) {
 123          global $DB;
 124  
 125          if ($userid == $otheruserid) {
 126              // Since 3.7, pending self-conversations should be migrated during the upgrading process so shouldn't be any
 127              // self-conversations on the legacy tables. However, this extra-check has been added just in case.
 128              $conversation = \core_message\api::get_self_conversation($userid);
 129              if (empty($conversation)) {
 130                  $conversation = \core_message\api::create_conversation(
 131                      \core_message\api::MESSAGE_CONVERSATION_TYPE_SELF,
 132                      [$userid]
 133                  );
 134              }
 135              $conversationid = $conversation->id;
 136          } else if (!$conversationid = \core_message\api::get_conversation_between_users([$userid, $otheruserid])) {
 137              $conversation = \core_message\api::create_conversation(
 138                  \core_message\api::MESSAGE_CONVERSATION_TYPE_INDIVIDUAL,
 139                  [
 140                      $userid,
 141                      $otheruserid
 142                  ]
 143              );
 144              $conversationid = $conversation->id;
 145          }
 146  
 147          // First, get the rows from the 'message' table.
 148          $select = "(useridfrom = ? AND useridto = ?) OR (useridfrom = ? AND useridto = ?)";
 149          $params = [$userid, $otheruserid, $otheruserid, $userid];
 150          $messages = $DB->get_recordset_select('message', $select, $params, 'id ASC');
 151          foreach ($messages as $message) {
 152              if ($message->notification) {
 153                  $this->migrate_notification($message, false);
 154              } else {
 155                  $this->migrate_message($conversationid, $message);
 156              }
 157          }
 158          $messages->close();
 159  
 160          // Ok, all done, delete the records from the 'message' table.
 161          $DB->delete_records_select('message', $select, $params);
 162  
 163          // Now, get the rows from the 'message_read' table.
 164          $messages = $DB->get_recordset_select('message_read', $select, $params, 'id ASC');
 165          foreach ($messages as $message) {
 166              if ($message->notification) {
 167                  $this->migrate_notification($message, true);
 168              } else {
 169                  $this->migrate_message($conversationid, $message);
 170              }
 171          }
 172          $messages->close();
 173  
 174          // Ok, all done, delete the records from the 'message_read' table.
 175          $DB->delete_records_select('message_read', $select, $params);
 176      }
 177  
 178      /**
 179       * Helper function to deal with migrating an individual notification.
 180       *
 181       * @param \stdClass $notification
 182       * @param bool $isread Was the notification read?
 183       * @throws \dml_exception
 184       */
 185      private function migrate_notification($notification, $isread) {
 186          global $DB;
 187  
 188          $tabledata = new \stdClass();
 189          $tabledata->useridfrom = $notification->useridfrom;
 190          $tabledata->useridto = $notification->useridto;
 191          $tabledata->subject = $notification->subject;
 192          $tabledata->fullmessage = $notification->fullmessage;
 193          $tabledata->fullmessageformat = $notification->fullmessageformat ?? FORMAT_MOODLE;
 194          $tabledata->fullmessagehtml = $notification->fullmessagehtml;
 195          $tabledata->smallmessage = $notification->smallmessage;
 196          $tabledata->component = $notification->component;
 197          $tabledata->eventtype = $notification->eventtype;
 198          $tabledata->contexturl = $notification->contexturl;
 199          $tabledata->contexturlname = $notification->contexturlname;
 200          $tabledata->timeread = $notification->timeread ?? null;
 201          $tabledata->timecreated = $notification->timecreated;
 202  
 203          $newid = $DB->insert_record('notifications', $tabledata);
 204  
 205          // Check if there is a record to move to the new 'message_popup_notifications' table.
 206          if ($mp = $DB->get_record('message_popup', ['messageid' => $notification->id, 'isread' => (int) $isread])) {
 207              $mpn = new \stdClass();
 208              $mpn->notificationid = $newid;
 209              $DB->insert_record('message_popup_notifications', $mpn);
 210  
 211              $DB->delete_records('message_popup', ['id' => $mp->id]);
 212          }
 213      }
 214  
 215      /**
 216       * Helper function to deal with migrating an individual message.
 217       *
 218       * @param int $conversationid The conversation between the two users.
 219       * @param \stdClass $message The message from either the 'message' or 'message_read' table
 220       * @throws \dml_exception
 221       */
 222      private function migrate_message($conversationid, $message) {
 223          global $DB;
 224  
 225          // Create the object we will be inserting into the database.
 226          $tabledata = new \stdClass();
 227          $tabledata->useridfrom = $message->useridfrom;
 228          $tabledata->conversationid = $conversationid;
 229          $tabledata->subject = $message->subject;
 230          $tabledata->fullmessage = $message->fullmessage;
 231          $tabledata->fullmessageformat = $message->fullmessageformat ?? FORMAT_MOODLE;
 232          $tabledata->fullmessagehtml = $message->fullmessagehtml;
 233          $tabledata->smallmessage = $message->smallmessage;
 234          $tabledata->timecreated = $message->timecreated;
 235  
 236          $messageid = $DB->insert_record('messages', $tabledata);
 237  
 238          // Check if we need to mark this message as deleted for the user from.
 239          if ($message->timeuserfromdeleted) {
 240              $mua = new \stdClass();
 241              $mua->userid = $message->useridfrom;
 242              $mua->messageid = $messageid;
 243              $mua->action = \core_message\api::MESSAGE_ACTION_DELETED;
 244              $mua->timecreated = $message->timeuserfromdeleted;
 245  
 246              $DB->insert_record('message_user_actions', $mua);
 247          }
 248  
 249          // Check if we need to mark this message as deleted for the user to.
 250          if ($message->timeusertodeleted and ($message->useridfrom != $message->useridto)) {
 251              $mua = new \stdClass();
 252              $mua->userid = $message->useridto;
 253              $mua->messageid = $messageid;
 254              $mua->action = \core_message\api::MESSAGE_ACTION_DELETED;
 255              $mua->timecreated = $message->timeusertodeleted;
 256  
 257              $DB->insert_record('message_user_actions', $mua);
 258          }
 259  
 260          // Check if we need to mark this message as read for the user to (it is always read by the user from).
 261          // Note - we do an isset() check here because this column only exists in the 'message_read' table.
 262          if (isset($message->timeread)) {
 263              $mua = new \stdClass();
 264              $mua->userid = $message->useridto;
 265              $mua->messageid = $messageid;
 266              $mua->action = \core_message\api::MESSAGE_ACTION_READ;
 267              $mua->timecreated = $message->timeread;
 268  
 269              $DB->insert_record('message_user_actions', $mua);
 270          }
 271      }
 272  
 273      /**
 274       * Queues the task.
 275       *
 276       * @param int $userid
 277       */
 278      public static function queue_task($userid) {
 279          // Let's set up the adhoc task.
 280          $task = new \core_message\task\migrate_message_data();
 281          $task->set_custom_data(
 282              [
 283                  'userid' => $userid
 284              ]
 285          );
 286  
 287          // Queue it.
 288          \core\task\manager::queue_adhoc_task($task, true);
 289      }
 290  }