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  namespace enrol_lti\local\ltiadvantage\repository;
  18  use enrol_lti\local\ltiadvantage\entity\user;
  19  
  20  /**
  21   * Class user_repository.
  22   *
  23   * This class encapsulates persistence logic for \enrol_lti\local\entity\user type objects.
  24   *
  25   * @package enrol_lti
  26   * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
  27   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  class user_repository {
  30  
  31      /** @var string $ltiuserstable the name of the table to which the entity will be persisted.*/
  32      private $ltiuserstable = 'enrol_lti_users';
  33  
  34      /** @var string $userresourcelinkidtable the name of the join table mapping users to resource links.*/
  35      private $userresourcelinkidtable = 'enrol_lti_user_resource_link';
  36  
  37      /**
  38       * Convert a record into a user object and return it.
  39       *
  40       * @param \stdClass $userrecord the raw data from relevant tables required to instantiate a user.
  41       * @return user a user object.
  42       */
  43      private function user_from_record(\stdClass $userrecord): user {
  44          return user::create(
  45              $userrecord->toolid,
  46              $userrecord->localid,
  47              $userrecord->ltideploymentid,
  48              $userrecord->sourceid,
  49              $userrecord->lang,
  50              $userrecord->timezone,
  51              $userrecord->city,
  52              $userrecord->country,
  53              $userrecord->institution,
  54              $userrecord->maildisplay,
  55              $userrecord->lastgrade,
  56              $userrecord->lastaccess,
  57              $userrecord->resourcelinkid ?? null,
  58              (int) $userrecord->id
  59          );
  60      }
  61  
  62      /**
  63       * Create a list of user instances from a list of records.
  64       *
  65       * @param array $records the array of records.
  66       * @return array of user instances.
  67       */
  68      private function users_from_records(array $records): array {
  69          $users = [];
  70          foreach ($records as $record) {
  71              $users[] = $this->user_from_record($record);
  72          }
  73          return $users;
  74      }
  75  
  76      /**
  77       * Get a stdClass object ready for persisting, based on the supplied user object.
  78       *
  79       * @param user $user the user instance.
  80       * @return \stdClass the record.
  81       */
  82      private function user_record_from_user(user $user): \stdClass {
  83          return (object) [
  84              'id' => $user->get_localid(),
  85              'city' => $user->get_city(),
  86              'country' => $user->get_country(),
  87              'institution' => $user->get_institution(),
  88              'timezone' => $user->get_timezone(),
  89              'maildisplay' => $user->get_maildisplay(),
  90              'lang' => $user->get_lang()
  91          ];
  92      }
  93  
  94      /**
  95       * Create the corresponding enrol_lti_user record from a user instance.
  96       *
  97       * @param user $user the user instance.
  98       * @return \stdClass the record.
  99       */
 100      private function lti_user_record_from_user(user $user): \stdClass {
 101          $record = [
 102              'toolid' => $user->get_resourceid(),
 103              'ltideploymentid' => $user->get_deploymentid(),
 104              'sourceid' => $user->get_sourceid(),
 105              'lastgrade' => $user->get_lastgrade(),
 106              'lastaccess' => $user->get_lastaccess(),
 107          ];
 108          if ($user->get_id()) {
 109              $record['id'] = $user->get_id();
 110          }
 111  
 112          return (object) $record;
 113      }
 114  
 115      /**
 116       * Helper to validate user:tool uniqueness across a deployment.
 117       *
 118       * The DB cannot be relied on to do this uniqueness check, since the table is shared by LTI 1.1/2.0 data.
 119       *
 120       * @param user $user the user instance.
 121       * @return bool true if found, false otherwise.
 122       */
 123      private function user_exists_for_tool(user $user): bool {
 124          // Lack of an id doesn't preclude the object from existence in the store. It may be stale, without an id.
 125          // The user can still be found by checking their lti advantage user creds and correlating that to the relevant
 126          // lti_user entry (where tool matches the user object's resource).
 127          global $DB;
 128          $uniquesql = "SELECT lu.id
 129                          FROM {{$this->ltiuserstable}} lu
 130                         WHERE lu.toolid = :toolid
 131                           AND lu.userid = :userid";
 132          $params = ['toolid' => $user->get_resourceid(), 'userid' => $user->get_localid()];
 133          return $DB->record_exists_sql($uniquesql, $params);
 134      }
 135  
 136      /**
 137       * Save a user instance in the store.
 138       *
 139       * @param user $user the object to save.
 140       * @return user the saved object.
 141       */
 142      public function save(user $user): user {
 143          global $DB;
 144          $id = $user->get_id();
 145          $exists = !is_null($id) && $this->exists($id);
 146          if ($id && !$exists) {
 147              throw new \coding_exception("Cannot save lti user with id '{$id}'. The record does not exist.");
 148          }
 149  
 150          $userrecord = $this->user_record_from_user($user);
 151          $ltiuserrecord = $this->lti_user_record_from_user($user);
 152          $timenow = time();
 153          global $CFG;
 154          require_once($CFG->dirroot . '/user/lib.php');
 155          if ($exists) {
 156              $ltiuser = $DB->get_record($this->ltiuserstable, ['id' => $ltiuserrecord->id]);
 157              $userid = $ltiuser->userid;
 158              // Warn about localid vs ltiuser->userid mismatches here. Callers shouldn't be able to force updates using
 159              // localid. Only new user associations can be created that way.
 160              if (!empty($userrecord->id) && $userid != $userrecord->id) {
 161                  throw new \coding_exception("Cannot update user mapping. LTI user '{$ltiuser->id}' is already mapped " .
 162                      "to user '{$ltiuser->userid}' and can't be associated with another user '{$userrecord->id}'.");
 163              }
 164  
 165              // Only update the Moodle user record if something has changed.
 166              $rawuser = \core_user::get_user($userrecord->id);
 167              $userfieldstocompare = array_intersect_key(
 168                  (array) $rawuser,
 169                  (array) $userrecord
 170              );
 171              if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) {
 172                  \user_update_user($userrecord);
 173              }
 174              unset($userrecord->id);
 175  
 176              $ltiuserrecord->timemodified = $timenow;
 177              $DB->update_record($this->ltiuserstable, $ltiuserrecord);
 178          } else {
 179              // Validate uniqueness of the lti user, in the case of a stale object coming in to be saved.
 180              if ($this->user_exists_for_tool($user)) {
 181                  throw new \coding_exception("Cannot create duplicate LTI user '{$user->get_localid()}' for resource " .
 182                      "'{$user->get_resourceid()}'.");
 183              }
 184  
 185              // Only update the Moodle user record if something has changed.
 186              $userid = $userrecord->id;
 187              $rawuser = \core_user::get_user($userid);
 188              $userfieldstocompare = array_intersect_key(
 189                  (array) $rawuser,
 190                  (array) $userrecord
 191              );
 192              if (!empty(array_diff((array) $userrecord, $userfieldstocompare))) {
 193                  \user_update_user($userrecord);
 194              }
 195              unset($userrecord->id);
 196  
 197              // Create the lti_user record, holding details that have a lifespan equal to that of the enrolment instance.
 198              $ltiuserrecord->timecreated = $ltiuserrecord->timemodified = $timenow;
 199              $ltiuserrecord->userid = $userid;
 200              $ltiuserrecord->id = $DB->insert_record($this->ltiuserstable, $ltiuserrecord);
 201          }
 202  
 203          // If the user was created via a resource_link, create that association.
 204          if ($reslinkid = $user->get_resourcelinkid()) {
 205              $resourcelinkmap = ['ltiuserid' => $ltiuserrecord->id, 'resourcelinkid' => $reslinkid];
 206              if (!$DB->record_exists($this->userresourcelinkidtable, $resourcelinkmap)) {
 207                  $DB->insert_record($this->userresourcelinkidtable, $resourcelinkmap);
 208              }
 209          }
 210          $resourcelinkmap = $resourcelinkmap ?? [];
 211  
 212          // Transform the data into something that looks like a read and can be processed by user_from_record.
 213          $record = (object) array_merge(
 214              (array) $userrecord,
 215              (array) $ltiuserrecord,
 216              $resourcelinkmap,
 217              ['localid' => $userid]
 218          );
 219  
 220          return $this->user_from_record($record);
 221      }
 222  
 223      /**
 224       * Find and return a user by id.
 225       *
 226       * @param int $id the id of the user object.
 227       * @return user|null the user object, or null if the object cannot be found.
 228       */
 229      public function find(int $id): ?user {
 230          global $DB;
 231          try {
 232              $sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country,
 233                             u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade,
 234                             lu.lastaccess, lu.ltideploymentid
 235                        FROM {{$this->ltiuserstable}} lu
 236                        JOIN {user} u
 237                          ON (u.id = lu.userid)
 238                       WHERE lu.id = :id
 239                         AND lu.ltideploymentid IS NOT NULL";
 240  
 241              $record = $DB->get_record_sql($sql, ['id' => $id], MUST_EXIST);
 242              return $this->user_from_record($record);
 243          } catch (\dml_missing_record_exception $ex) {
 244              return null;
 245          }
 246      }
 247  
 248      /**
 249       * Find an lti user instance by resource.
 250       *
 251       * @param int $userid the id of the moodle user to look for.
 252       * @param int $resourceid the id of the published resource.
 253       * @return user|null the lti user instance, or null if not found.
 254       */
 255      public function find_single_user_by_resource(int $userid, int $resourceid): ?user {
 256          global $DB;
 257          try {
 258              // Find the lti advantage user record.
 259              $sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country,
 260                             u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade,
 261                             lu.lastaccess, lu.ltideploymentid
 262                        FROM {{$this->ltiuserstable}} lu
 263                        JOIN {user} u
 264                          ON (u.id = lu.userid)
 265                       WHERE lu.userid = :userid
 266                         AND lu.toolid = :resourceid
 267                         AND lu.ltideploymentid IS NOT NULL";
 268  
 269              $params = ['userid' => $userid, 'resourceid' => $resourceid];
 270              $record = $DB->get_record_sql($sql, $params, MUST_EXIST);
 271              return $this->user_from_record($record);
 272          } catch (\dml_missing_record_exception $ex) {
 273              return null;
 274          }
 275      }
 276  
 277      /**
 278       * Find all users for a particular shared resource.
 279       *
 280       * @param int $resourceid the id of the shared resource.
 281       * @return array the array of users, empty if none were found.
 282       */
 283      public function find_by_resource(int $resourceid): array {
 284          global $DB;
 285          $sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country,
 286                         u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade,
 287                         lu.lastaccess, lu.ltideploymentid
 288                    FROM {{$this->ltiuserstable}} lu
 289                    JOIN {user} u
 290                      ON (u.id = lu.userid)
 291                   WHERE lu.toolid = :resourceid
 292                     AND lu.ltideploymentid IS NOT NULL
 293                ORDER BY lu.lastaccess DESC";
 294  
 295          $records = $DB->get_records_sql($sql, ['resourceid' => $resourceid]);
 296          return $this->users_from_records($records);
 297      }
 298  
 299      /**
 300       * Get a list of users associated with the given resource link.
 301       *
 302       * @param int $resourcelinkid the id of the resource_link instance with which the users are associated.
 303       * @return array the array of users, empty if none were found.
 304       */
 305      public function find_by_resource_link(int $resourcelinkid) {
 306          global $DB;
 307          $sql = "SELECT lu.id, u.id as localid, u.username, u.firstname, u.lastname, u.email, u.city, u.country,
 308                         u.institution, u.timezone, u.maildisplay, u.lang, lu.sourceid, lu.toolid, lu.lastgrade,
 309                         lu.lastaccess, lu.ltideploymentid
 310                    FROM {{$this->ltiuserstable}} lu
 311                    JOIN {user} u
 312                      ON (u.id = lu.userid)
 313                    JOIN {{$this->userresourcelinkidtable}} url
 314                      ON (url.ltiuserid = lu.id)
 315                   WHERE url.resourcelinkid = :resourcelinkid
 316                ORDER BY lu.lastaccess DESC";
 317  
 318          $records = $DB->get_records_sql($sql, ['resourcelinkid' => $resourcelinkid]);
 319          return $this->users_from_records($records);
 320      }
 321  
 322      /**
 323       * Check whether or not the given user object exists.
 324       *
 325       * @param int $id the unique id the user.
 326       * @return bool true if found, false otherwise.
 327       */
 328      public function exists(int $id): bool {
 329          global $DB;
 330          return $DB->record_exists($this->ltiuserstable, ['id' => $id]);
 331      }
 332  
 333      /**
 334       * Delete a user based on id.
 335       *
 336       * @param int $id the id of the user to remove.
 337       */
 338      public function delete(int $id) {
 339          global $DB;
 340          $DB->delete_records($this->ltiuserstable, ['id' => $id]);
 341          $DB->delete_records($this->userresourcelinkidtable, ['ltiuserid' => $id]);
 342      }
 343  
 344      /**
 345       * Delete all lti user instances based on a given local deployment instance id.
 346       *
 347       * @param int $deploymentid the local id of the deployment instance to which the users belong.
 348       */
 349      public function delete_by_deployment(int $deploymentid): void {
 350          global $DB;
 351          $DB->delete_records($this->ltiuserstable, ['ltideploymentid' => $deploymentid]);
 352      }
 353  }