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\service; 18 19 use enrol_lti\helper; 20 use enrol_lti\local\ltiadvantage\entity\context; 21 use enrol_lti\local\ltiadvantage\entity\deployment; 22 use enrol_lti\local\ltiadvantage\entity\migration_claim; 23 use enrol_lti\local\ltiadvantage\entity\resource_link; 24 use enrol_lti\local\ltiadvantage\entity\user; 25 use enrol_lti\local\ltiadvantage\repository\application_registration_repository; 26 use enrol_lti\local\ltiadvantage\repository\context_repository; 27 use enrol_lti\local\ltiadvantage\repository\deployment_repository; 28 use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository; 29 use enrol_lti\local\ltiadvantage\repository\resource_link_repository; 30 use enrol_lti\local\ltiadvantage\repository\user_repository; 31 use Packback\Lti1p3\LtiMessageLaunch; 32 33 /** 34 * Class tool_launch_service. 35 * 36 * This class handles the launch of a resource by a user, using the LTI Advantage Resource Link Launch. 37 * 38 * See http://www.imsglobal.org/spec/lti/v1p3/#launch-from-a-resource-link 39 * 40 * @package enrol_lti 41 * @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com> 42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 43 */ 44 class tool_launch_service { 45 46 /** @var deployment_repository $deploymentrepo instance of a deployment repository. */ 47 private $deploymentrepo; 48 49 /** @var application_registration_repository instance of a application_registration repository */ 50 private $registrationrepo; 51 52 /** @var resource_link_repository instance of a resource_link repository */ 53 private $resourcelinkrepo; 54 55 /** @var user_repository instance of a user repository*/ 56 private $userrepo; 57 58 /** @var context_repository instance of a context repository */ 59 private $contextrepo; 60 61 /** 62 * The tool_launch_service constructor. 63 * 64 * @param deployment_repository $deploymentrepo instance of a deployment_repository. 65 * @param application_registration_repository $registrationrepo instance of an application_registration_repository. 66 * @param resource_link_repository $resourcelinkrepo instance of a resource_link_repository. 67 * @param user_repository $userrepo instance of a user_repository. 68 * @param context_repository $contextrepo instance of a context_repository. 69 */ 70 public function __construct(deployment_repository $deploymentrepo, 71 application_registration_repository $registrationrepo, resource_link_repository $resourcelinkrepo, 72 user_repository $userrepo, context_repository $contextrepo) { 73 74 $this->deploymentrepo = $deploymentrepo; 75 $this->registrationrepo = $registrationrepo; 76 $this->resourcelinkrepo = $resourcelinkrepo; 77 $this->userrepo = $userrepo; 78 $this->contextrepo = $contextrepo; 79 } 80 81 /** Get the launch data from the launch. 82 * 83 * @param LtiMessageLaunch $launch the launch instance. 84 * @return \stdClass the launch data. 85 */ 86 private function get_launch_data(LtiMessageLaunch $launch): \stdClass { 87 $launchdata = $launch->getLaunchData(); 88 $data = [ 89 'platform' => $launchdata['iss'], 90 // The 'aud' property may be an array with one or more values, but can be a string if there is only one value. 91 // https://www.imsglobal.org/spec/security/v1p1#id-token. 92 'clientid' => is_array($launchdata['aud']) ? $launchdata['aud'][0] : $launchdata['aud'], 93 'exp' => $launchdata['exp'], 94 'nonce' => $launchdata['nonce'], 95 'sub' => $launchdata['sub'], 96 'roles' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/roles'], 97 'deploymentid' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/deployment_id'], 98 'context' => !empty($launchdata['https://purl.imsglobal.org/spec/lti/claim/context']) ? 99 $launchdata['https://purl.imsglobal.org/spec/lti/claim/context'] : null, 100 'resourcelink' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/resource_link'], 101 'targetlinkuri' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/target_link_uri'], 102 'custom' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/custom'] ?? null, 103 'launchid' => $launch->getLaunchId(), 104 'user' => [ 105 'givenname' => !empty($launchdata['given_name']) ? $launchdata['given_name'] : null, 106 'familyname' => !empty($launchdata['family_name']) ? $launchdata['family_name'] : null, 107 'name' => !empty($launchdata['name']) ? $launchdata['name'] : null, 108 'email' => !empty($launchdata['email']) ? $launchdata['email'] : null, 109 'picture' => !empty($launchdata['picture']) ? $launchdata['picture'] : null, 110 ], 111 'ags' => $launchdata['https://purl.imsglobal.org/spec/lti-ags/claim/endpoint'] ?? null, 112 'nrps' => $launchdata['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice'] ?? null, 113 'lti1p1' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] ?? null 114 ]; 115 116 return (object) $data; 117 } 118 119 /** 120 * Get a context instance from the launch data. 121 * 122 * @param \stdClass $launchdata launch data. 123 * @param deployment $deployment the deployment to which the context belongs. 124 * @return context the context instance. 125 */ 126 private function context_from_launchdata(\stdClass $launchdata, deployment $deployment): context { 127 if ($context = $this->contextrepo->find_by_contextid($launchdata->context['id'], $deployment->get_id())) { 128 // The context has been mapped, just update it. 129 $context->set_types($launchdata->context['type']); 130 } else { 131 // Map a new context. 132 $context = $deployment->add_context($launchdata->context['id'], $launchdata->context['type']); 133 } 134 return $context; 135 } 136 137 /** 138 * Get a resource_link from the launch data. 139 * 140 * @param \stdClass $launchdata the launch data. 141 * @param \stdClass $resource the resource to which the resource link refers. 142 * @param deployment $deployment the deployment to which the resource_link belongs. 143 * @param context|null $context optional context in which the resource_link lives, null if not needed. 144 * @return resource_link the resource_link instance. 145 */ 146 private function resource_link_from_launchdata(\stdClass $launchdata, \stdClass $resource, deployment $deployment, 147 ?context $context): resource_link { 148 149 if ($resourcelink = $this->resourcelinkrepo->find_by_deployment($deployment, $launchdata->resourcelink['id'])) { 150 // Resource link exists, so update it. 151 if (isset($context)) { 152 $resourcelink->set_contextid($context->get_id()); 153 } 154 // A resource link may have been updated, via content item selection, to refer to a different resource. 155 if ($resourcelink->get_resourceid() != $resource->id) { 156 $resourcelink->set_resourceid($resource->id); 157 } 158 } else { 159 // Create a new resource link. 160 $resourcelink = $deployment->add_resource_link( 161 $launchdata->resourcelink['id'], 162 $resource->id, 163 $context ? $context->get_id() : null 164 ); 165 } 166 // Add the AGS configuration for the resource link. 167 // See: http://www.imsglobal.org/spec/lti-ags/v2p0#assignment-and-grade-service-claim. 168 if ($launchdata->ags && (!empty($launchdata->ags['lineitems']) || !empty($launchdata->ags['lineitem']))) { 169 $resourcelink->add_grade_service( 170 !empty($launchdata->ags['lineitems']) ? new \moodle_url($launchdata->ags['lineitems']) : null, 171 !empty($launchdata->ags['lineitem']) ? new \moodle_url($launchdata->ags['lineitem']) : null, 172 $launchdata->ags['scope'] 173 ); 174 } 175 176 // NRPS. 177 if ($launchdata->nrps) { 178 $resourcelink->add_names_and_roles_service( 179 new \moodle_url($launchdata->nrps['context_memberships_url']), 180 $launchdata->nrps['service_versions'] 181 ); 182 } 183 return $resourcelink; 184 } 185 186 /** 187 * Get an lti user instance from the launch data. 188 * 189 * @param \stdClass $user the moodle user object. 190 * @param \stdClass $launchdata the launch data. 191 * @param \stdClass $resource the resource to which the user belongs. 192 * @param resource_link $resourcelink the resource_link from which the user originated. 193 * @return user the user instance. 194 */ 195 private function lti_user_from_launchdata(\stdClass $user, \stdClass $launchdata, \stdClass $resource, 196 resource_link $resourcelink): user { 197 198 // Find the user based on the unique-to-the-issuer 'sub' value. 199 if ($ltiuser = $this->userrepo->find_single_user_by_resource($user->id, $resource->id)) { 200 // User exists, so update existing based on resource data which may have changed. 201 $ltiuser->set_resourcelinkid($resourcelink->get_id()); 202 $ltiuser->set_lang($resource->lang); 203 $ltiuser->set_city($resource->city); 204 $ltiuser->set_country($resource->country); 205 $ltiuser->set_institution($resource->institution); 206 $ltiuser->set_timezone($resource->timezone); 207 $ltiuser->set_maildisplay($resource->maildisplay); 208 } else { 209 // Create the lti user. 210 $ltiuser = $resourcelink->add_user( 211 $user->id, 212 $launchdata->sub, 213 $resource->lang, 214 $resource->city ?? '', 215 $resource->country ?? '', 216 $resource->institution ?? '', 217 $resource->timezone ?? '', 218 $resource->maildisplay ?? null, 219 ); 220 } 221 $ltiuser->set_lastaccess(time()); 222 return $ltiuser; 223 } 224 225 /** 226 * Get the migration claim from the launch data, or null if not found. 227 * 228 * @param \stdClass $launchdata the launch data. 229 * @return migration_claim|null the claim instance if present in the launch data, else null. 230 */ 231 private function migration_claim_from_launchdata(\stdClass $launchdata): ?migration_claim { 232 if (!isset($launchdata->lti1p1)) { 233 return null; 234 } 235 236 // Despite the spec requiring the oauth_consumer_key field be present in the migration claim: 237 // (see https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key), 238 // Platforms may omit this field making migration impossible. 239 // E.g. for Canvas launches taking place after an assignment_selection placement. 240 if (empty($launchdata->lti1p1['oauth_consumer_key'])) { 241 return null; 242 } 243 244 return new migration_claim($launchdata->lti1p1, $launchdata->deploymentid, 245 $launchdata->platform, $launchdata->clientid, $launchdata->exp, $launchdata->nonce, 246 new legacy_consumer_repository()); 247 } 248 249 /** 250 * Check whether the launch user has an admin role. 251 * 252 * @param \stdClass $launchdata the launch data. 253 * @return bool true if the user is admin, false otherwise. 254 */ 255 private function user_is_admin(\stdClass $launchdata): bool { 256 // See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies. 257 if ($launchdata->roles) { 258 $adminroles = [ 259 'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator', 260 'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator' 261 ]; 262 263 foreach ($adminroles as $validrole) { 264 if (in_array($validrole, $launchdata->roles)) { 265 return true; 266 } 267 } 268 } 269 return false; 270 } 271 272 /** 273 * Check whether the launch user is an instructor. 274 * 275 * @param \stdClass $launchdata the launch data. 276 * @param bool $includelegacy whether to also consider legacy simple names as valid roles. 277 * @return bool true if the user is an instructor, false otherwise. 278 */ 279 private function user_is_staff(\stdClass $launchdata, bool $includelegacy = false): bool { 280 // See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies. 281 // This method also provides support for (legacy, deprecated) simple names for context roles. 282 // I.e. 'ContentDeveloper' may be supported. 283 if ($launchdata->roles) { 284 $staffroles = [ 285 'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper', 286 'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor', 287 'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant' 288 ]; 289 290 if ($includelegacy) { 291 $staffroles[] = 'ContentDeveloper'; 292 $staffroles[] = 'Instructor'; 293 $staffroles[] = 'Instructor#TeachingAssistant'; 294 } 295 296 foreach ($staffroles as $validrole) { 297 if (in_array($validrole, $launchdata->roles)) { 298 return true; 299 } 300 } 301 } 302 return false; 303 } 304 305 /** 306 * Handles the use case "A user launches the tool so they can view an external resource". 307 * 308 * @param \stdClass $user the Moodle user record, obtained via the auth_lti authentication process. 309 * @param LtiMessageLaunch $launch the launch data. 310 * @return array array containing [int $userid, \stdClass $resource] 311 * @throws \moodle_exception if launch problems are encountered. 312 */ 313 public function user_launches_tool(\stdClass $user, LtiMessageLaunch $launch): array { 314 315 $launchdata = $this->get_launch_data($launch); 316 317 if (!$registration = $this->registrationrepo->find_by_platform($launchdata->platform, $launchdata->clientid)) { 318 throw new \moodle_exception('ltiadvlauncherror:invalidregistration', 'enrol_lti', '', 319 [$launchdata->platform, $launchdata->clientid]); 320 } 321 322 if (!$deployment = $this->deploymentrepo->find_by_registration($registration->get_id(), 323 $launchdata->deploymentid)) { 324 throw new \moodle_exception('ltiadvlauncherror:invaliddeployment', 'enrol_lti', '', 325 [$launchdata->deploymentid]); 326 } 327 328 $resourceuuid = $launchdata->custom['id'] ?? null; 329 if (empty($resourceuuid)) { 330 throw new \moodle_exception('ltiadvlauncherror:missingid', 'enrol_lti'); 331 } 332 333 $resource = array_values(helper::get_lti_tools(['uuid' => $resourceuuid])); 334 $resource = $resource[0] ?? null; 335 if (empty($resource) || $resource->status != ENROL_INSTANCE_ENABLED) { 336 throw new \moodle_exception('ltiadvlauncherror:invalidid', 'enrol_lti', '', $resourceuuid); 337 } 338 339 // Update the deployment with the legacy consumer_key information, allowing migration of users to take place in future 340 // names and roles syncs. 341 if ($migrationclaim = $this->migration_claim_from_launchdata($launchdata)) { 342 $deployment->set_legacy_consumer_key($migrationclaim->get_consumer_key()); 343 $this->deploymentrepo->save($deployment); 344 } 345 346 // Save the context, if that claim is present. 347 $context = null; 348 if ($launchdata->context) { 349 $context = $this->context_from_launchdata($launchdata, $deployment); 350 $context = $this->contextrepo->save($context); 351 } 352 353 // Save the resource link for the tool deployment. 354 $resourcelink = $this->resource_link_from_launchdata($launchdata, $resource, $deployment, $context); 355 $resourcelink = $this->resourcelinkrepo->save($resourcelink); 356 357 // Save the user launching the resource link. 358 $ltiuser = $this->lti_user_from_launchdata($user, $launchdata, $resource, $resourcelink); 359 $ltiuser = $this->userrepo->save($ltiuser); 360 361 // Set the frame embedding mode, which controls the display of blocks and nav when launching. 362 global $SESSION; 363 $context = \context::instance_by_id($resource->contextid); 364 $isforceembed = $launchdata->custom['force_embed'] ?? false; 365 $isinstructor = $this->user_is_staff($launchdata, true) || $this->user_is_admin($launchdata); 366 $isforceembed = $isforceembed || ($context->contextlevel == CONTEXT_MODULE && !$isinstructor); 367 if ($isforceembed) { 368 $SESSION->forcepagelayout = 'embedded'; 369 } else { 370 unset($SESSION->forcepagelayout); 371 } 372 373 // Enrol the user in the course with no role. 374 $result = helper::enrol_user($resource, $ltiuser->get_localid()); 375 if ($result !== helper::ENROLMENT_SUCCESSFUL) { 376 throw new \moodle_exception($result, 'enrol_lti'); 377 } 378 379 // Give the user the role in the given context. 380 $roleid = $isinstructor ? $resource->roleinstructor : $resource->rolelearner; 381 role_assign($roleid, $ltiuser->get_localid(), $resource->contextid); 382 383 return [$ltiuser->get_localid(), $resource]; 384 } 385 386 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body