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   * Data provider.
  19   *
  20   * @package    mod_chat
  21   * @copyright  2018 Frédéric Massart
  22   * @author     Frédéric Massart <fred@branchup.tech>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  namespace mod_chat\privacy;
  27  defined('MOODLE_INTERNAL') || die();
  28  
  29  use context;
  30  use context_helper;
  31  use context_module;
  32  use moodle_recordset;
  33  use stdClass;
  34  use core_privacy\local\metadata\collection;
  35  use core_privacy\local\request\approved_contextlist;
  36  use core_privacy\local\request\approved_userlist;
  37  use core_privacy\local\request\contextlist;
  38  use core_privacy\local\request\helper;
  39  use core_privacy\local\request\transform;
  40  use core_privacy\local\request\userlist;
  41  use core_privacy\local\request\writer;
  42  
  43  /**
  44   * Data provider class.
  45   *
  46   * @package    mod_chat
  47   * @copyright  2018 Frédéric Massart
  48   * @author     Frédéric Massart <fred@branchup.tech>
  49   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  50   */
  51  class provider implements
  52      \core_privacy\local\metadata\provider,
  53      \core_privacy\local\request\core_userlist_provider,
  54      \core_privacy\local\request\plugin\provider {
  55  
  56      /**
  57       * Returns metadata.
  58       *
  59       * @param collection $collection The initialised collection to add items to.
  60       * @return collection A listing of user data stored through this system.
  61       */
  62      public static function get_metadata(collection $collection) : collection {
  63  
  64          $collection->add_database_table('chat_messages', [
  65              'userid' => 'privacy:metadata:messages:userid',
  66              'message' => 'privacy:metadata:messages:message',
  67              'issystem' => 'privacy:metadata:messages:issystem',
  68              'timestamp' => 'privacy:metadata:messages:timestamp',
  69          ], 'privacy:metadata:messages');
  70  
  71          // The tables chat_messages_current and chat_users are not exported/deleted
  72          // because they are considered as short-lived data and are deleted on a
  73          // regular basis by cron, or during normal requests. TODO MDL-62006.
  74  
  75          $collection->add_database_table('chat_messages_current', [
  76              'userid' => 'privacy:metadata:messages:userid',
  77              'message' => 'privacy:metadata:messages:message',
  78              'issystem' => 'privacy:metadata:messages:issystem',
  79              'timestamp' => 'privacy:metadata:messages:timestamp'
  80          ], 'privacy:metadata:chat_messages_current');
  81  
  82          $collection->add_database_table('chat_users', [
  83              'userid' => 'privacy:metadata:chat_users:userid',
  84              'version' => 'privacy:metadata:chat_users:version',
  85              'ip' => 'privacy:metadata:chat_users:ip',
  86              'firstping' => 'privacy:metadata:chat_users:firstping',
  87              'lastping' => 'privacy:metadata:chat_users:lastping',
  88              'lastmessageping' => 'privacy:metadata:chat_users:lastmessageping',
  89              'lang' => 'privacy:metadata:chat_users:lang'
  90          ], 'privacy:metadata:chat_users');
  91  
  92          return $collection;
  93      }
  94  
  95      /**
  96       * Get the list of contexts that contain user information for the specified user.
  97       *
  98       * @param int $userid The user to search.
  99       * @return contextlist $contextlist The contextlist containing the list of contexts used in this plugin.
 100       */
 101      public static function get_contexts_for_userid(int $userid) : \core_privacy\local\request\contextlist {
 102          $contextlist = new \core_privacy\local\request\contextlist();
 103  
 104          $sql = "
 105              SELECT DISTINCT ctx.id
 106                FROM {chat} c
 107                JOIN {modules} m
 108                  ON m.name = :chat
 109                JOIN {course_modules} cm
 110                  ON cm.instance = c.id
 111                 AND cm.module = m.id
 112                JOIN {context} ctx
 113                  ON ctx.instanceid = cm.id
 114                 AND ctx.contextlevel = :modulelevel
 115                JOIN {chat_messages} chm
 116                  ON chm.chatid = c.id
 117               WHERE chm.userid = :userid";
 118  
 119          $params = [
 120              'chat' => 'chat',
 121              'modulelevel' => CONTEXT_MODULE,
 122              'userid' => $userid,
 123          ];
 124          $contextlist->add_from_sql($sql, $params);
 125  
 126          return $contextlist;
 127      }
 128  
 129      /**
 130       * Get the list of users who have data within a context.
 131       *
 132       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 133       */
 134      public static function get_users_in_context(userlist $userlist) {
 135          $context = $userlist->get_context();
 136  
 137          if (!is_a($context, \context_module::class)) {
 138              return;
 139          }
 140  
 141          $params = [
 142              'instanceid'    => $context->instanceid,
 143              'modulename'    => 'chat',
 144          ];
 145  
 146          $sql = "SELECT chm.userid
 147                    FROM {course_modules} cm
 148                    JOIN {modules} m ON m.id = cm.module AND m.name = :modulename
 149                    JOIN {chat} c ON c.id = cm.instance
 150                    JOIN {chat_messages} chm ON chm.chatid = c.id
 151                   WHERE cm.id = :instanceid";
 152  
 153          $userlist->add_from_sql('userid', $sql, $params);
 154      }
 155  
 156      /**
 157       * Export all user data for the specified user, in the specified contexts.
 158       *
 159       * @param approved_contextlist $contextlist The approved contexts to export information for.
 160       */
 161      public static function export_user_data(approved_contextlist $contextlist) {
 162          global $DB;
 163  
 164          $user = $contextlist->get_user();
 165          $userid = $user->id;
 166          $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 167              if ($context->contextlevel == CONTEXT_MODULE) {
 168                  $carry[] = $context->instanceid;
 169              }
 170              return $carry;
 171          }, []);
 172          if (empty($cmids)) {
 173              return;
 174          }
 175  
 176          $chatidstocmids = static::get_chat_ids_to_cmids_from_cmids($cmids);
 177          $chatids = array_keys($chatidstocmids);
 178  
 179          // Export the messages.
 180          list($insql, $inparams) = $DB->get_in_or_equal($chatids, SQL_PARAMS_NAMED);
 181          $params = array_merge($inparams, ['userid' => $userid]);
 182          $recordset = $DB->get_recordset_select('chat_messages', "chatid $insql AND userid = :userid", $params, 'timestamp, id');
 183          static::recordset_loop_and_export($recordset, 'chatid', [], function($carry, $record) use ($user, $chatidstocmids) {
 184              $message = $record->message;
 185              if ($record->issystem) {
 186                  $message = get_string('message' . $record->message, 'mod_chat', fullname($user));
 187              }
 188              $carry[] = [
 189                  'message' => $message,
 190                  'sent_at' => transform::datetime($record->timestamp),
 191                  'is_system_generated' => transform::yesno($record->issystem),
 192              ];
 193              return $carry;
 194  
 195          }, function($chatid, $data) use ($user, $chatidstocmids) {
 196              $context = context_module::instance($chatidstocmids[$chatid]);
 197              $contextdata = helper::get_context_data($context, $user);
 198              $finaldata = (object) array_merge((array) $contextdata, ['messages' => $data]);
 199              helper::export_context_files($context, $user);
 200              writer::with_context($context)->export_data([], $finaldata);
 201          });
 202      }
 203  
 204      /**
 205       * Delete all data for all users in the specified context.
 206       *
 207       * @param context $context The specific context to delete data for.
 208       */
 209      public static function delete_data_for_all_users_in_context(context $context) {
 210          global $DB;
 211  
 212          if ($context->contextlevel != CONTEXT_MODULE) {
 213              return;
 214          }
 215  
 216          $cm = get_coursemodule_from_id('chat', $context->instanceid);
 217          if (!$cm) {
 218              return;
 219          }
 220  
 221          $chatid = $cm->instance;
 222          $DB->delete_records_select('chat_messages', 'chatid = :chatid', ['chatid' => $chatid]);
 223          $DB->delete_records_select('chat_messages_current', 'chatid = :chatid', ['chatid' => $chatid]);
 224          $DB->delete_records_select('chat_users', 'chatid = :chatid', ['chatid' => $chatid]);
 225      }
 226  
 227      /**
 228       * Delete all user data for the specified user, in the specified contexts.
 229       *
 230       * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
 231       */
 232      public static function delete_data_for_user(approved_contextlist $contextlist) {
 233          global $DB;
 234  
 235          $userid = $contextlist->get_user()->id;
 236          $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 237              if ($context->contextlevel == CONTEXT_MODULE) {
 238                  $carry[] = $context->instanceid;
 239              }
 240              return $carry;
 241          }, []);
 242          if (empty($cmids)) {
 243              return;
 244          }
 245  
 246          $chatidstocmids = static::get_chat_ids_to_cmids_from_cmids($cmids);
 247          $chatids = array_keys($chatidstocmids);
 248  
 249          list($insql, $inparams) = $DB->get_in_or_equal($chatids, SQL_PARAMS_NAMED);
 250          $sql = "chatid $insql AND userid = :userid";
 251          $params = array_merge($inparams, ['userid' => $userid]);
 252  
 253          $DB->delete_records_select('chat_messages', $sql, $params);
 254          $DB->delete_records_select('chat_messages_current', $sql, $params);
 255          $DB->delete_records_select('chat_users', $sql, $params);
 256      }
 257  
 258  
 259      /**
 260       * Delete multiple users within a single context.
 261       *
 262       * @param   approved_userlist       $userlist The approved context and user information to delete information for.
 263       */
 264      public static function delete_data_for_users(approved_userlist $userlist) {
 265          global $DB;
 266  
 267          $context = $userlist->get_context();
 268          $cm = $DB->get_record('course_modules', ['id' => $context->instanceid]);
 269          $chat = $DB->get_record('chat', ['id' => $cm->instance]);
 270  
 271          list($userinsql, $userinparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
 272          $params = array_merge(['chatid' => $chat->id], $userinparams);
 273          $sql = "chatid = :chatid AND userid {$userinsql}";
 274  
 275          $DB->delete_records_select('chat_messages', $sql, $params);
 276          $DB->delete_records_select('chat_messages_current', $sql, $params);
 277          $DB->delete_records_select('chat_users', $sql, $params);
 278      }
 279  
 280      /**
 281       * Return a dict of chat IDs mapped to their course module ID.
 282       *
 283       * @param array $cmids The course module IDs.
 284       * @return array In the form of [$chatid => $cmid].
 285       */
 286      protected static function get_chat_ids_to_cmids_from_cmids(array $cmids) {
 287          global $DB;
 288          list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
 289          $sql = "
 290              SELECT c.id, cm.id AS cmid
 291                FROM {chat} c
 292                JOIN {modules} m
 293                  ON m.name = :chat
 294                JOIN {course_modules} cm
 295                  ON cm.instance = c.id
 296                 AND cm.module = m.id
 297               WHERE cm.id $insql";
 298          $params = array_merge($inparams, ['chat' => 'chat']);
 299          return $DB->get_records_sql_menu($sql, $params);
 300      }
 301  
 302      /**
 303       * Loop and export from a recordset.
 304       *
 305       * @param moodle_recordset $recordset The recordset.
 306       * @param string $splitkey The record key to determine when to export.
 307       * @param mixed $initial The initial data to reduce from.
 308       * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
 309       * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
 310       * @return void
 311       */
 312      protected static function recordset_loop_and_export(moodle_recordset $recordset, $splitkey, $initial,
 313              callable $reducer, callable $export) {
 314  
 315          $data = $initial;
 316          $lastid = null;
 317  
 318          foreach ($recordset as $record) {
 319              if ($lastid && $record->{$splitkey} != $lastid) {
 320                  $export($lastid, $data);
 321                  $data = $initial;
 322              }
 323              $data = $reducer($data, $record);
 324              $lastid = $record->{$splitkey};
 325          }
 326          $recordset->close();
 327  
 328          if (!empty($lastid)) {
 329              $export($lastid, $data);
 330          }
 331      }
 332  
 333  }