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 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 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   * Privacy Subsystem implementation for mod_lti.
  19   *
  20   * @package    mod_lti
  21   * @copyright  2018 Mark Nelson <markn@moodle.com>
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  namespace mod_lti\privacy;
  25  
  26  use core_privacy\local\metadata\collection;
  27  use core_privacy\local\request\approved_contextlist;
  28  use core_privacy\local\request\approved_userlist;
  29  use core_privacy\local\request\contextlist;
  30  use core_privacy\local\request\helper;
  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 mod_lti.
  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      \core_privacy\local\metadata\provider,
  45      \core_privacy\local\request\core_userlist_provider,
  46      \core_privacy\local\request\plugin\provider {
  47  
  48      /**
  49       * Return the fields which contain personal data.
  50       *
  51       * @param collection $items a reference to the collection to use to store the metadata.
  52       * @return collection the updated collection of metadata items.
  53       */
  54      public static function get_metadata(collection $items) : collection {
  55          $items->add_external_location_link(
  56              'lti_provider',
  57              [
  58                  'userid' => 'privacy:metadata:userid',
  59                  'username' => 'privacy:metadata:username',
  60                  'useridnumber' => 'privacy:metadata:useridnumber',
  61                  'firstname' => 'privacy:metadata:firstname',
  62                  'lastname' => 'privacy:metadata:lastname',
  63                  'fullname' => 'privacy:metadata:fullname',
  64                  'email' => 'privacy:metadata:email',
  65                  'role' => 'privacy:metadata:role',
  66                  'courseid' => 'privacy:metadata:courseid',
  67                  'courseidnumber' => 'privacy:metadata:courseidnumber',
  68                  'courseshortname' => 'privacy:metadata:courseshortname',
  69                  'coursefullname' => 'privacy:metadata:coursefullname',
  70              ],
  71              'privacy:metadata:externalpurpose'
  72          );
  73  
  74          $items->add_database_table(
  75              'lti_submission',
  76              [
  77                  'userid' => 'privacy:metadata:lti_submission:userid',
  78                  'datesubmitted' => 'privacy:metadata:lti_submission:datesubmitted',
  79                  'dateupdated' => 'privacy:metadata:lti_submission:dateupdated',
  80                  'gradepercent' => 'privacy:metadata:lti_submission:gradepercent',
  81                  'originalgrade' => 'privacy:metadata:lti_submission:originalgrade',
  82              ],
  83              'privacy:metadata:lti_submission'
  84          );
  85  
  86          $items->add_database_table(
  87              'lti_tool_proxies',
  88              [
  89                  'name' => 'privacy:metadata:lti_tool_proxies:name',
  90                  'createdby' => 'privacy:metadata:createdby',
  91                  'timecreated' => 'privacy:metadata:timecreated',
  92                  'timemodified' => 'privacy:metadata:timemodified'
  93              ],
  94              'privacy:metadata:lti_tool_proxies'
  95          );
  96  
  97          $items->add_database_table(
  98              'lti_types',
  99              [
 100                  'name' => 'privacy:metadata:lti_types:name',
 101                  'createdby' => 'privacy:metadata:createdby',
 102                  'timecreated' => 'privacy:metadata:timecreated',
 103                  'timemodified' => 'privacy:metadata:timemodified'
 104              ],
 105              'privacy:metadata:lti_types'
 106          );
 107  
 108          return $items;
 109      }
 110  
 111      /**
 112       * Get the list of contexts that contain user information for the specified user.
 113       *
 114       * @param int $userid the userid.
 115       * @return contextlist the list of contexts containing user info for the user.
 116       */
 117      public static function get_contexts_for_userid(int $userid) : contextlist {
 118          // Fetch all LTI submissions.
 119          $sql = "SELECT c.id
 120                    FROM {context} c
 121              INNER JOIN {course_modules} cm
 122                      ON cm.id = c.instanceid
 123                     AND c.contextlevel = :contextlevel
 124              INNER JOIN {modules} m
 125                      ON m.id = cm.module
 126                     AND m.name = :modname
 127              INNER JOIN {lti} lti
 128                      ON lti.id = cm.instance
 129              INNER JOIN {lti_submission} ltisub
 130                      ON ltisub.ltiid = lti.id
 131                   WHERE ltisub.userid = :userid";
 132  
 133          $params = [
 134              'modname' => 'lti',
 135              'contextlevel' => CONTEXT_MODULE,
 136              'userid' => $userid,
 137          ];
 138          $contextlist = new contextlist();
 139          $contextlist->add_from_sql($sql, $params);
 140  
 141          // Fetch all LTI types.
 142          $sql = "SELECT c.id
 143                   FROM {context} c
 144                   JOIN {course} course
 145                     ON c.contextlevel = :contextlevel
 146                    AND c.instanceid = course.id
 147                   JOIN {lti_types} ltit
 148                     ON ltit.course = course.id
 149                  WHERE ltit.createdby = :userid";
 150  
 151          $params = [
 152              'contextlevel' => CONTEXT_COURSE,
 153              'userid' => $userid
 154          ];
 155          $contextlist->add_from_sql($sql, $params);
 156  
 157          // The LTI tool proxies sit in the system context.
 158          $contextlist->add_system_context();
 159  
 160          return $contextlist;
 161      }
 162  
 163      /**
 164       * Get the list of users who have data within a context.
 165       *
 166       * @param   userlist    $userlist   The userlist containing the list of users who have data in this context/plugin combination.
 167       */
 168      public static function get_users_in_context(userlist $userlist) {
 169          $context = $userlist->get_context();
 170  
 171          if (!is_a($context, \context_module::class)) {
 172              return;
 173          }
 174  
 175          // Fetch all LTI submissions.
 176          $sql = "SELECT ltisub.userid
 177                    FROM {context} c
 178              INNER JOIN {course_modules} cm
 179                      ON cm.id = c.instanceid
 180                     AND c.contextlevel = :contextlevel
 181              INNER JOIN {modules} m
 182                      ON m.id = cm.module
 183                     AND m.name = :modname
 184              INNER JOIN {lti} lti
 185                      ON lti.id = cm.instance
 186              INNER JOIN {lti_submission} ltisub
 187                      ON ltisub.ltiid = lti.id
 188                   WHERE c.id = :contextid";
 189  
 190          $params = [
 191              'modname' => 'lti',
 192              'contextlevel' => CONTEXT_MODULE,
 193              'contextid' => $context->id,
 194          ];
 195  
 196          $userlist->add_from_sql('userid', $sql, $params);
 197  
 198          // Fetch all LTI types.
 199          $sql = "SELECT ltit.createdby AS userid
 200                   FROM {context} c
 201                   JOIN {course} course
 202                     ON c.contextlevel = :contextlevel
 203                    AND c.instanceid = course.id
 204                   JOIN {lti_types} ltit
 205                     ON ltit.course = course.id
 206                  WHERE c.id = :contextid";
 207  
 208          $params = [
 209              'contextlevel' => CONTEXT_COURSE,
 210              'contextid' => $context->id,
 211          ];
 212          $userlist->add_from_sql('userid', $sql, $params);
 213      }
 214  
 215      /**
 216       * Export personal data for the given approved_contextlist. User and context information is contained within the contextlist.
 217       *
 218       * @param approved_contextlist $contextlist a list of contexts approved for export.
 219       */
 220      public static function export_user_data(approved_contextlist $contextlist) {
 221          self::export_user_data_lti_submissions($contextlist);
 222  
 223          self::export_user_data_lti_types($contextlist);
 224  
 225          self::export_user_data_lti_tool_proxies($contextlist);
 226      }
 227  
 228      /**
 229       * Delete all data for all users in the specified context.
 230       *
 231       * @param \context $context the context to delete in.
 232       */
 233      public static function delete_data_for_all_users_in_context(\context $context) {
 234          global $DB;
 235  
 236          if (!$context instanceof \context_module) {
 237              return;
 238          }
 239  
 240          if ($cm = get_coursemodule_from_id('lti', $context->instanceid)) {
 241              $DB->delete_records('lti_submission', ['ltiid' => $cm->instance]);
 242          }
 243      }
 244  
 245      /**
 246       * Delete all user data for the specified user, in the specified contexts.
 247       *
 248       * @param approved_contextlist $contextlist a list of contexts approved for deletion.
 249       */
 250      public static function delete_data_for_user(approved_contextlist $contextlist) {
 251          global $DB;
 252  
 253          if (empty($contextlist->count())) {
 254              return;
 255          }
 256  
 257          $userid = $contextlist->get_user()->id;
 258          foreach ($contextlist->get_contexts() as $context) {
 259              if (!$context instanceof \context_module) {
 260                  continue;
 261              }
 262              $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
 263              $DB->delete_records('lti_submission', ['ltiid' => $instanceid, 'userid' => $userid]);
 264          }
 265      }
 266  
 267      /**
 268       * Delete multiple users within a single context.
 269       *
 270       * @param   approved_userlist       $userlist The approved context and user information to delete information for.
 271       */
 272      public static function delete_data_for_users(approved_userlist $userlist) {
 273          global $DB;
 274  
 275          $context = $userlist->get_context();
 276  
 277          if ($context instanceof \context_module) {
 278              $instanceid = $DB->get_field('course_modules', 'instance', ['id' => $context->instanceid], MUST_EXIST);
 279  
 280              list($insql, $inparams) = $DB->get_in_or_equal($userlist->get_userids(), SQL_PARAMS_NAMED);
 281              $sql = "ltiid = :instanceid AND userid {$insql}";
 282              $params = array_merge(['instanceid' => $instanceid], $inparams);
 283  
 284              $DB->delete_records_select('lti_submission', $sql, $params);
 285          }
 286      }
 287  
 288      /**
 289       * Export personal data for the given approved_contextlist related to LTI submissions.
 290       *
 291       * @param approved_contextlist $contextlist a list of contexts approved for export.
 292       */
 293      protected static function export_user_data_lti_submissions(approved_contextlist $contextlist) {
 294          global $DB;
 295  
 296          // Filter out any contexts that are not related to modules.
 297          $cmids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 298              if ($context->contextlevel == CONTEXT_MODULE) {
 299                  $carry[] = $context->instanceid;
 300              }
 301              return $carry;
 302          }, []);
 303  
 304          if (empty($cmids)) {
 305              return;
 306          }
 307  
 308          $user = $contextlist->get_user();
 309  
 310          // Get all the LTI activities associated with the above course modules.
 311          $ltiidstocmids = self::get_lti_ids_to_cmids_from_cmids($cmids);
 312          $ltiids = array_keys($ltiidstocmids);
 313  
 314          list($insql, $inparams) = $DB->get_in_or_equal($ltiids, SQL_PARAMS_NAMED);
 315          $params = array_merge($inparams, ['userid' => $user->id]);
 316          $recordset = $DB->get_recordset_select('lti_submission', "ltiid $insql AND userid = :userid", $params, 'dateupdated, id');
 317          self::recordset_loop_and_export($recordset, 'ltiid', [], function($carry, $record) use ($user, $ltiidstocmids) {
 318              $carry[] = [
 319                  'gradepercent' => $record->gradepercent,
 320                  'originalgrade' => $record->originalgrade,
 321                  'datesubmitted' => transform::datetime($record->datesubmitted),
 322                  'dateupdated' => transform::datetime($record->dateupdated)
 323              ];
 324              return $carry;
 325          }, function($ltiid, $data) use ($user, $ltiidstocmids) {
 326              $context = \context_module::instance($ltiidstocmids[$ltiid]);
 327              $contextdata = helper::get_context_data($context, $user);
 328              $finaldata = (object) array_merge((array) $contextdata, ['submissions' => $data]);
 329              helper::export_context_files($context, $user);
 330              writer::with_context($context)->export_data([], $finaldata);
 331          });
 332      }
 333  
 334      /**
 335       * Export personal data for the given approved_contextlist related to LTI types.
 336       *
 337       * @param approved_contextlist $contextlist a list of contexts approved for export.
 338       */
 339      protected static function export_user_data_lti_types(approved_contextlist $contextlist) {
 340          global $DB;
 341  
 342          // Filter out any contexts that are not related to courses.
 343          $courseids = array_reduce($contextlist->get_contexts(), function($carry, $context) {
 344              if ($context->contextlevel == CONTEXT_COURSE) {
 345                  $carry[] = $context->instanceid;
 346              }
 347              return $carry;
 348          }, []);
 349  
 350          if (empty($courseids)) {
 351              return;
 352          }
 353  
 354          $user = $contextlist->get_user();
 355  
 356          list($insql, $inparams) = $DB->get_in_or_equal($courseids, SQL_PARAMS_NAMED);
 357          $params = array_merge($inparams, ['userid' => $user->id]);
 358          $ltitypes = $DB->get_recordset_select('lti_types', "course $insql AND createdby = :userid", $params, 'timecreated ASC');
 359          self::recordset_loop_and_export($ltitypes, 'course', [], function($carry, $record) {
 360              $context = \context_course::instance($record->course);
 361              $options = ['context' => $context];
 362              $carry[] = [
 363                  'name' => format_string($record->name, true, $options),
 364                  'createdby' => transform::user($record->createdby),
 365                  'timecreated' => transform::datetime($record->timecreated),
 366                  'timemodified' => transform::datetime($record->timemodified)
 367              ];
 368              return $carry;
 369          }, function($courseid, $data) {
 370              $context = \context_course::instance($courseid);
 371              $finaldata = (object) ['lti_types' => $data];
 372              writer::with_context($context)->export_data([], $finaldata);
 373          });
 374      }
 375  
 376      /**
 377       * Export personal data for the given approved_contextlist related to LTI tool proxies.
 378       *
 379       * @param approved_contextlist $contextlist a list of contexts approved for export.
 380       */
 381      protected static function export_user_data_lti_tool_proxies(approved_contextlist $contextlist) {
 382          global $DB;
 383  
 384          // Filter out any contexts that are not related to system context.
 385          $systemcontexts = array_filter($contextlist->get_contexts(), function($context) {
 386              return $context->contextlevel == CONTEXT_SYSTEM;
 387          });
 388  
 389          if (empty($systemcontexts)) {
 390              return;
 391          }
 392  
 393          $user = $contextlist->get_user();
 394  
 395          $systemcontext = \context_system::instance();
 396  
 397          $data = [];
 398          $ltiproxies = $DB->get_recordset('lti_tool_proxies', ['createdby' => $user->id], 'timecreated ASC');
 399          foreach ($ltiproxies as $ltiproxy) {
 400              $data[] = [
 401                  'name' => format_string($ltiproxy->name, true, ['context' => $systemcontext]),
 402                  'createdby' => transform::user($ltiproxy->createdby),
 403                  'timecreated' => transform::datetime($ltiproxy->timecreated),
 404                  'timemodified' => transform::datetime($ltiproxy->timemodified)
 405              ];
 406          }
 407          $ltiproxies->close();
 408  
 409          $finaldata = (object) ['lti_tool_proxies' => $data];
 410          writer::with_context($systemcontext)->export_data([], $finaldata);
 411      }
 412  
 413      /**
 414       * Return a dict of LTI IDs mapped to their course module ID.
 415       *
 416       * @param array $cmids The course module IDs.
 417       * @return array In the form of [$ltiid => $cmid].
 418       */
 419      protected static function get_lti_ids_to_cmids_from_cmids(array $cmids) {
 420          global $DB;
 421  
 422          list($insql, $inparams) = $DB->get_in_or_equal($cmids, SQL_PARAMS_NAMED);
 423          $sql = "SELECT lti.id, cm.id AS cmid
 424                   FROM {lti} lti
 425                   JOIN {modules} m
 426                     ON m.name = :lti
 427                   JOIN {course_modules} cm
 428                     ON cm.instance = lti.id
 429                    AND cm.module = m.id
 430                  WHERE cm.id $insql";
 431          $params = array_merge($inparams, ['lti' => 'lti']);
 432  
 433          return $DB->get_records_sql_menu($sql, $params);
 434      }
 435  
 436      /**
 437       * Loop and export from a recordset.
 438       *
 439       * @param \moodle_recordset $recordset The recordset.
 440       * @param string $splitkey The record key to determine when to export.
 441       * @param mixed $initial The initial data to reduce from.
 442       * @param callable $reducer The function to return the dataset, receives current dataset, and the current record.
 443       * @param callable $export The function to export the dataset, receives the last value from $splitkey and the dataset.
 444       * @return void
 445       */
 446      protected static function recordset_loop_and_export(\moodle_recordset $recordset, $splitkey, $initial,
 447                                                          callable $reducer, callable $export) {
 448          $data = $initial;
 449          $lastid = null;
 450  
 451          foreach ($recordset as $record) {
 452              if ($lastid && $record->{$splitkey} != $lastid) {
 453                  $export($lastid, $data);
 454                  $data = $initial;
 455              }
 456              $data = $reducer($data, $record);
 457              $lastid = $record->{$splitkey};
 458          }
 459          $recordset->close();
 460  
 461          if (!empty($lastid)) {
 462              $export($lastid, $data);
 463          }
 464      }
 465  }