Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 402 and 403]

   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  namespace core_external\privacy;
  18  
  19  use context;
  20  use context_user;
  21  use core_privacy\local\metadata\collection;
  22  use core_privacy\local\request\approved_contextlist;
  23  use core_privacy\local\request\transform;
  24  use core_privacy\local\request\writer;
  25  use core_privacy\local\request\userlist;
  26  use core_privacy\local\request\approved_userlist;
  27  
  28  /**
  29   * Data provider class.
  30   *
  31   * @package    core_external
  32   * @copyright  2018 Frédéric Massart
  33   * @author     Frédéric Massart <fred@branchup.tech>
  34   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  35   */
  36  class provider implements
  37      \core_privacy\local\metadata\provider,
  38      \core_privacy\local\request\core_userlist_provider,
  39      \core_privacy\local\request\subsystem\provider {
  40  
  41      /**
  42       * Returns metadata.
  43       *
  44       * @param collection $collection The initialised collection to add items to.
  45       * @return collection A listing of user data stored through this system.
  46       */
  47      public static function get_metadata(collection $collection) : collection {
  48  
  49          $collection->add_database_table('external_tokens', [
  50              'token' => 'privacy:metadata:tokens:token',
  51              'privatetoken' => 'privacy:metadata:tokens:privatetoken',
  52              'tokentype' => 'privacy:metadata:tokens:tokentype',
  53              'userid' => 'privacy:metadata:tokens:userid',
  54              'creatorid' => 'privacy:metadata:tokens:creatorid',
  55              'iprestriction' => 'privacy:metadata:tokens:iprestriction',
  56              'validuntil' => 'privacy:metadata:tokens:validuntil',
  57              'timecreated' => 'privacy:metadata:tokens:timecreated',
  58              'lastaccess' => 'privacy:metadata:tokens:lastaccess',
  59              'name' => 'privacy:metadata:tokens:name',
  60          ], 'privacy:metadata:tokens');
  61  
  62          $collection->add_database_table('external_services_users', [
  63              'userid' => 'privacy:metadata:serviceusers:userid',
  64              'iprestriction' => 'privacy:metadata:serviceusers:iprestriction',
  65              'validuntil' => 'privacy:metadata:serviceusers:validuntil',
  66              'timecreated' => 'privacy:metadata:serviceusers:timecreated',
  67          ], 'privacy:metadata:serviceusers');
  68  
  69          return $collection;
  70      }
  71  
  72      /**
  73       * Get the list of contexts that contain user information for the specified user.
  74       *
  75       * @param int $userid The user to search.
  76       * @return \core_privacy\local\request\contextlist $contextlist The contextlist containing the list of contexts
  77       *                                                              used in this plugin.
  78       */
  79      public static function get_contexts_for_userid(int $userid): \core_privacy\local\request\contextlist {
  80          $contextlist = new \core_privacy\local\request\contextlist();
  81  
  82          $sql = "
  83              SELECT ctx.id
  84                FROM {external_tokens} t
  85                JOIN {context} ctx
  86                  ON ctx.instanceid = t.userid
  87                 AND ctx.contextlevel = :userlevel
  88               WHERE t.userid = :userid1
  89                  OR t.creatorid = :userid2";
  90          $contextlist->add_from_sql($sql, ['userlevel' => CONTEXT_USER, 'userid1' => $userid, 'userid2' => $userid]);
  91  
  92          $sql = "
  93              SELECT ctx.id
  94                FROM {external_services_users} su
  95                JOIN {context} ctx
  96                  ON ctx.instanceid = su.userid
  97                 AND ctx.contextlevel = :userlevel
  98               WHERE su.userid = :userid";
  99          $contextlist->add_from_sql($sql, ['userlevel' => CONTEXT_USER, 'userid' => $userid]);
 100  
 101          return $contextlist;
 102      }
 103  
 104      /**
 105       * Get the list of users within a specific context.
 106       *
 107       * @param userlist $userlist The userlist containing the list of users who have data in this context/plugin combination.
 108       */
 109      public static function get_users_in_context(userlist $userlist) {
 110          global $DB;
 111  
 112          $context = $userlist->get_context();
 113  
 114          if (!$context instanceof \context_user) {
 115              return;
 116          }
 117  
 118          $userid = $context->instanceid;
 119  
 120          $hasdata = false;
 121          $hasdata = $hasdata || $DB->record_exists_select('external_tokens', 'userid = ? OR creatorid = ?', [$userid, $userid]);
 122          $hasdata = $hasdata || $DB->record_exists('external_services_users', ['userid' => $userid]);
 123  
 124          if ($hasdata) {
 125              $userlist->add_user($userid);
 126          }
 127      }
 128  
 129      /**
 130       * Export all user data for the specified user, in the specified contexts.
 131       *
 132       * @param approved_contextlist $contextlist The approved contexts to export information for.
 133       */
 134      public static function export_user_data(approved_contextlist $contextlist) {
 135          global $DB;
 136  
 137          $userid = $contextlist->get_user()->id;
 138          $contexts = array_reduce($contextlist->get_contexts(), function($carry, $context) use ($userid) {
 139              if ($context->contextlevel == CONTEXT_USER) {
 140                  if ($context->instanceid == $userid) {
 141                      $carry['has_mine'] = true;
 142                  } else {
 143                      $carry['others'][] = $context->instanceid;
 144                  }
 145              }
 146              return $carry;
 147          }, [
 148              'has_mine' => false,
 149              'others' => []
 150          ]);
 151  
 152          $path = [get_string('services', 'core_external')];
 153  
 154          // Exporting my stuff.
 155          if ($contexts['has_mine']) {
 156  
 157              $data = [];
 158  
 159              // Exporting my tokens.
 160              $sql = "
 161                  SELECT t.*, s.name as externalservicename
 162                    FROM {external_tokens} t
 163                    JOIN {external_services} s
 164                      ON s.id = t.externalserviceid
 165                   WHERE t.userid = :userid
 166                ORDER BY t.id";
 167              $recordset = $DB->get_recordset_sql($sql, ['userid' => $userid]);
 168              foreach ($recordset as $record) {
 169                  if (!isset($data['tokens'])) {
 170                      $data['tokens'] = [];
 171                  }
 172                  $data['tokens'][] = static::transform_token($record);
 173              }
 174              $recordset->close();
 175  
 176              // Exporting the services I have access to.
 177              $sql = "
 178                  SELECT su.*, s.name as externalservicename
 179                    FROM {external_services_users} su
 180                    JOIN {external_services} s
 181                      ON s.id = su.externalserviceid
 182                   WHERE su.userid = :userid
 183                ORDER BY su.id";
 184              $recordset = $DB->get_recordset_sql($sql, ['userid' => $userid]);
 185              foreach ($recordset as $record) {
 186                  if (!isset($data['services_user'])) {
 187                      $data['services_user'] = [];
 188                  }
 189                  $data['services_user'][] = [
 190                      'external_service' => $record->externalservicename,
 191                      'ip_restriction' => $record->iprestriction,
 192                      'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null,
 193                      'created_on' => transform::datetime($record->timecreated),
 194                  ];
 195              }
 196              $recordset->close();
 197  
 198              if (!empty($data)) {
 199                  writer::with_context(context_user::instance($userid))->export_data($path, (object) $data);
 200              };
 201          }
 202  
 203          // Exporting the tokens I created.
 204          if (!empty($contexts['others'])) {
 205              list($insql, $inparams) = $DB->get_in_or_equal($contexts['others'], SQL_PARAMS_NAMED);
 206              $sql = "
 207                  SELECT t.*, s.name as externalservicename
 208                    FROM {external_tokens} t
 209                    JOIN {external_services} s
 210                      ON s.id = t.externalserviceid
 211                   WHERE t.userid $insql
 212                     AND t.creatorid = :userid1
 213                     AND t.userid <> :userid2
 214                ORDER BY t.userid, t.id";
 215              $params = array_merge($inparams, ['userid1' => $userid, 'userid2' => $userid]);
 216              $recordset = $DB->get_recordset_sql($sql, $params);
 217              static::recordset_loop_and_export($recordset, 'userid', [], function($carry, $record) {
 218                  $carry[] = static::transform_token($record);
 219                  return $carry;
 220              }, function($userid, $data) use ($path) {
 221                  writer::with_context(context_user::instance($userid))->export_related_data($path, 'created_by_you', (object) [
 222                      'tokens' => $data
 223                  ]);
 224              });
 225          }
 226      }
 227  
 228      /**
 229       * Delete all data for all users in the specified context.
 230       *
 231       * @param context $context The specific context to delete data for.
 232       */
 233      public static function delete_data_for_all_users_in_context(context $context) {
 234          if ($context->contextlevel != CONTEXT_USER) {
 235              return;
 236          }
 237          static::delete_user_data($context->instanceid);
 238      }
 239  
 240      /**
 241       * Delete multiple users within a single context.
 242       *
 243       * @param approved_userlist $userlist The approved context and user information to delete information for.
 244       */
 245      public static function delete_data_for_users(approved_userlist $userlist) {
 246  
 247          $context = $userlist->get_context();
 248  
 249          if ($context instanceof \context_user) {
 250              static::delete_user_data($context->instanceid);
 251          }
 252      }
 253  
 254      /**
 255       * Delete all user data for the specified user, in the specified contexts.
 256       *
 257       * @param approved_contextlist $contextlist The approved contexts and user information to delete information for.
 258       */
 259      public static function delete_data_for_user(approved_contextlist $contextlist) {
 260          $userid = $contextlist->get_user()->id;
 261          foreach ($contextlist as $context) {
 262              if ($context->contextlevel == CONTEXT_USER && $context->instanceid == $userid) {
 263                  static::delete_user_data($context->instanceid);
 264                  break;
 265              }
 266          }
 267      }
 268  
 269      /**
 270       * Delete user data.
 271       *
 272       * @param int $userid The user ID.
 273       * @return void
 274       */
 275      protected static function delete_user_data($userid) {
 276          global $DB;
 277          $DB->delete_records('external_tokens', ['userid' => $userid]);
 278          $DB->delete_records('external_services_users', ['userid' => $userid]);
 279      }
 280  
 281      /**
 282       * Transform a token entry.
 283       *
 284       * @param object $record The token record.
 285       * @return array
 286       */
 287      protected static function transform_token($record) {
 288          $notexportedstr = get_string('privacy:request:notexportedsecurity', 'core_external');
 289          return [
 290              'external_service' => $record->externalservicename,
 291              'token' => $notexportedstr,
 292              'private_token' => $record->privatetoken ? $notexportedstr : null,
 293              'ip_restriction' => $record->iprestriction,
 294              'valid_until' => $record->validuntil ? transform::datetime($record->validuntil) : null,
 295              'created_on' => transform::datetime($record->timecreated),
 296              'last_access' => $record->lastaccess ? transform::datetime($record->lastaccess) : null,
 297              'name' => $record->name,
 298          ];
 299      }
 300  
 301      /**
 302       * Loop and export from a recordset.
 303       *
 304       * @param \moodle_recordset $recordset The recordset.
 305       * @param string $splitkey The record key to determine when to export.
 306       * @param mixed $initial The initial data to reduce from.
 307       * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
 308       * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
 309       * @return void
 310       */
 311      protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
 312              callable $reducer, callable $export) {
 313  
 314          $data = $initial;
 315          $lastid = null;
 316  
 317          foreach ($recordset as $record) {
 318              if ($lastid && $record->{$splitkey} != $lastid) {
 319                  $export($lastid, $data);
 320                  $data = $initial;
 321              }
 322              $data = $reducer($data, $record);
 323              $lastid = $record->{$splitkey};
 324          }
 325          $recordset->close();
 326  
 327          if (!empty($lastid)) {
 328              $export($lastid, $data);
 329          }
 330      }
 331  }