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.

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