See Release Notes
Long Term Support Release
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body