mirror of
https://github.com/moodle/moodle.git
synced 2025-04-21 08:22:07 +02:00
MDL-69542 enrol_lti: add tool launch service
This commit is contained in:
parent
92195d97c0
commit
9d5264d500
@ -0,0 +1,384 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace enrol_lti\local\ltiadvantage\service;
|
||||
|
||||
use enrol_lti\helper;
|
||||
use enrol_lti\local\ltiadvantage\entity\context;
|
||||
use enrol_lti\local\ltiadvantage\entity\deployment;
|
||||
use enrol_lti\local\ltiadvantage\entity\migration_claim;
|
||||
use enrol_lti\local\ltiadvantage\entity\resource_link;
|
||||
use enrol_lti\local\ltiadvantage\entity\user;
|
||||
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\context_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\legacy_consumer_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\user_repository;
|
||||
use Packback\Lti1p3\LtiMessageLaunch;
|
||||
|
||||
/**
|
||||
* Class tool_launch_service.
|
||||
*
|
||||
* This class handles the launch of a resource by a user, using the LTI Advantage Resource Link Launch.
|
||||
*
|
||||
* See http://www.imsglobal.org/spec/lti/v1p3/#launch-from-a-resource-link
|
||||
*
|
||||
* @package enrol_lti
|
||||
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class tool_launch_service {
|
||||
|
||||
/** @var deployment_repository $deploymentrepo instance of a deployment repository. */
|
||||
private $deploymentrepo;
|
||||
|
||||
/** @var application_registration_repository instance of a application_registration repository */
|
||||
private $registrationrepo;
|
||||
|
||||
/** @var resource_link_repository instance of a resource_link repository */
|
||||
private $resourcelinkrepo;
|
||||
|
||||
/** @var user_repository instance of a user repository*/
|
||||
private $userrepo;
|
||||
|
||||
/** @var context_repository instance of a context repository */
|
||||
private $contextrepo;
|
||||
|
||||
/**
|
||||
* The tool_launch_service constructor.
|
||||
*
|
||||
* @param deployment_repository $deploymentrepo instance of a deployment_repository.
|
||||
* @param application_registration_repository $registrationrepo instance of an application_registration_repository.
|
||||
* @param resource_link_repository $resourcelinkrepo instance of a resource_link_repository.
|
||||
* @param user_repository $userrepo instance of a user_repository.
|
||||
* @param context_repository $contextrepo instance of a context_repository.
|
||||
*/
|
||||
public function __construct(deployment_repository $deploymentrepo,
|
||||
application_registration_repository $registrationrepo, resource_link_repository $resourcelinkrepo,
|
||||
user_repository $userrepo, context_repository $contextrepo) {
|
||||
|
||||
$this->deploymentrepo = $deploymentrepo;
|
||||
$this->registrationrepo = $registrationrepo;
|
||||
$this->resourcelinkrepo = $resourcelinkrepo;
|
||||
$this->userrepo = $userrepo;
|
||||
$this->contextrepo = $contextrepo;
|
||||
}
|
||||
|
||||
/** Get the launch data from the launch.
|
||||
*
|
||||
* @param LtiMessageLaunch $launch the launch instance.
|
||||
* @return \stdClass the launch data.
|
||||
*/
|
||||
private function get_launch_data(LtiMessageLaunch $launch): \stdClass {
|
||||
$launchdata = $launch->getLaunchData();
|
||||
$data = [
|
||||
'platform' => $launchdata['iss'],
|
||||
'clientid' => $launchdata['aud'], // See LTI_Message_Launch::validate_registration for details about aud.
|
||||
'exp' => $launchdata['exp'],
|
||||
'nonce' => $launchdata['nonce'],
|
||||
'sub' => $launchdata['sub'],
|
||||
'roles' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/roles'],
|
||||
'deploymentid' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/deployment_id'],
|
||||
'context' => !empty($launchdata['https://purl.imsglobal.org/spec/lti/claim/context']) ?
|
||||
$launchdata['https://purl.imsglobal.org/spec/lti/claim/context'] : null,
|
||||
'resourcelink' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/resource_link'],
|
||||
'targetlinkuri' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/target_link_uri'],
|
||||
'custom' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/custom'] ?? null,
|
||||
'launchid' => $launch->getLaunchId(),
|
||||
'user' => [
|
||||
'givenname' => !empty($launchdata['given_name']) ? $launchdata['given_name'] : null,
|
||||
'familyname' => !empty($launchdata['family_name']) ? $launchdata['family_name'] : null,
|
||||
'name' => !empty($launchdata['name']) ? $launchdata['name'] : null,
|
||||
'email' => !empty($launchdata['email']) ? $launchdata['email'] : null,
|
||||
'picture' => !empty($launchdata['picture']) ? $launchdata['picture'] : null,
|
||||
],
|
||||
'ags' => $launchdata['https://purl.imsglobal.org/spec/lti-ags/claim/endpoint'] ?? null,
|
||||
'nrps' => $launchdata['https://purl.imsglobal.org/spec/lti-nrps/claim/namesroleservice'] ?? null,
|
||||
'lti1p1' => $launchdata['https://purl.imsglobal.org/spec/lti/claim/lti1p1'] ?? null
|
||||
];
|
||||
|
||||
return (object) $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a context instance from the launch data.
|
||||
*
|
||||
* @param \stdClass $launchdata launch data.
|
||||
* @param deployment $deployment the deployment to which the context belongs.
|
||||
* @return context the context instance.
|
||||
*/
|
||||
private function context_from_launchdata(\stdClass $launchdata, deployment $deployment): context {
|
||||
if ($context = $this->contextrepo->find_by_contextid($launchdata->context['id'], $deployment->get_id())) {
|
||||
// The context has been mapped, just update it.
|
||||
$context->set_types($launchdata->context['type']);
|
||||
} else {
|
||||
// Map a new context.
|
||||
$context = $deployment->add_context($launchdata->context['id'], $launchdata->context['type']);
|
||||
}
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a resource_link from the launch data.
|
||||
*
|
||||
* @param \stdClass $launchdata the launch data.
|
||||
* @param \stdClass $resource the resource to which the resource link refers.
|
||||
* @param deployment $deployment the deployment to which the resource_link belongs.
|
||||
* @param context|null $context optional context in which the resource_link lives, null if not needed.
|
||||
* @return resource_link the resource_link instance.
|
||||
*/
|
||||
private function resource_link_from_launchdata(\stdClass $launchdata, \stdClass $resource, deployment $deployment,
|
||||
?context $context): resource_link {
|
||||
|
||||
if ($resourcelink = $this->resourcelinkrepo->find_by_deployment($deployment, $launchdata->resourcelink['id'])) {
|
||||
// Resource link exists, so update it.
|
||||
if (isset($context)) {
|
||||
$resourcelink->set_contextid($context->get_id());
|
||||
}
|
||||
// A resource link may have been updated, via content item selection, to refer to a different resource.
|
||||
if ($resourcelink->get_resourceid() != $resource->id) {
|
||||
$resourcelink->set_resourceid($resource->id);
|
||||
}
|
||||
} else {
|
||||
// Create a new resource link.
|
||||
$resourcelink = $deployment->add_resource_link(
|
||||
$launchdata->resourcelink['id'],
|
||||
$resource->id,
|
||||
$context ? $context->get_id() : null
|
||||
);
|
||||
}
|
||||
// AGS. If the lineitemsurl is missing, it means the tool has no access to the endpoint.
|
||||
// See: http://www.imsglobal.org/spec/lti-ags/v2p0#assignment-and-grade-service-claim.
|
||||
if ($launchdata->ags && $launchdata->ags['lineitems']) {
|
||||
$resourcelink->add_grade_service(
|
||||
new \moodle_url($launchdata->ags['lineitems']),
|
||||
isset($launchdata->ags['lineitem']) ? new \moodle_url($launchdata->ags['lineitem']) : null,
|
||||
$launchdata->ags['scope']
|
||||
);
|
||||
}
|
||||
|
||||
// NRPS.
|
||||
if ($launchdata->nrps) {
|
||||
$resourcelink->add_names_and_roles_service(
|
||||
new \moodle_url($launchdata->nrps['context_memberships_url']),
|
||||
$launchdata->nrps['service_versions']
|
||||
);
|
||||
}
|
||||
return $resourcelink;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an lti user instance from the launch data.
|
||||
*
|
||||
* @param \stdClass $user the moodle user object.
|
||||
* @param \stdClass $launchdata the launch data.
|
||||
* @param \stdClass $resource the resource to which the user belongs.
|
||||
* @param resource_link $resourcelink the resource_link from which the user originated.
|
||||
* @return user the user instance.
|
||||
*/
|
||||
private function lti_user_from_launchdata(\stdClass $user, \stdClass $launchdata, \stdClass $resource,
|
||||
resource_link $resourcelink): user {
|
||||
|
||||
// Find the user based on the unique-to-the-issuer 'sub' value.
|
||||
if ($ltiuser = $this->userrepo->find_single_user_by_resource($user->id, $resource->id)) {
|
||||
// User exists, so update existing based on resource data which may have changed.
|
||||
$ltiuser->set_resourcelinkid($resourcelink->get_id());
|
||||
$ltiuser->set_lang($resource->lang);
|
||||
$ltiuser->set_city($resource->city);
|
||||
$ltiuser->set_country($resource->country);
|
||||
$ltiuser->set_institution($resource->institution);
|
||||
$ltiuser->set_timezone($resource->timezone);
|
||||
$ltiuser->set_maildisplay($resource->maildisplay);
|
||||
} else {
|
||||
// Create the lti user.
|
||||
$ltiuser = $resourcelink->add_user(
|
||||
$user->id,
|
||||
$launchdata->sub,
|
||||
$resource->lang,
|
||||
$resource->city ?? '',
|
||||
$resource->country ?? '',
|
||||
$resource->institution ?? '',
|
||||
$resource->timezone ?? '',
|
||||
$resource->maildisplay ?? null,
|
||||
);
|
||||
}
|
||||
$ltiuser->set_lastaccess(time());
|
||||
return $ltiuser;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the migration claim from the launch data, or null if not found.
|
||||
*
|
||||
* @param \stdClass $launchdata the launch data.
|
||||
* @return migration_claim|null the claim instance if present in the launch data, else null.
|
||||
*/
|
||||
private function migration_claim_from_launchdata(\stdClass $launchdata): ?migration_claim {
|
||||
if (!isset($launchdata->lti1p1)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Despite the spec requiring the oauth_consumer_key field be present in the migration claim:
|
||||
// (see https://www.imsglobal.org/spec/lti/v1p3/migr#oauth_consumer_key),
|
||||
// Platforms may omit this field making migration impossible.
|
||||
// E.g. for Canvas launches taking place after an assignment_selection placement.
|
||||
if (empty($launchdata->lti1p1['oauth_consumer_key'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new migration_claim($launchdata->lti1p1, $launchdata->deploymentid,
|
||||
$launchdata->platform, $launchdata->clientid, $launchdata->exp, $launchdata->nonce,
|
||||
new legacy_consumer_repository());
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the launch user has an admin role.
|
||||
*
|
||||
* @param \stdClass $launchdata the launch data.
|
||||
* @return bool true if the user is admin, false otherwise.
|
||||
*/
|
||||
private function user_is_admin(\stdClass $launchdata): bool {
|
||||
// See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies.
|
||||
if ($launchdata->roles) {
|
||||
$adminroles = [
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/institution/person#Administrator',
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator'
|
||||
];
|
||||
|
||||
foreach ($adminroles as $validrole) {
|
||||
if (in_array($validrole, $launchdata->roles)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the launch user is an instructor.
|
||||
*
|
||||
* @param \stdClass $launchdata the launch data.
|
||||
* @param bool $includelegacy whether to also consider legacy simple names as valid roles.
|
||||
* @return bool true if the user is an instructor, false otherwise.
|
||||
*/
|
||||
private function user_is_staff(\stdClass $launchdata, bool $includelegacy = false): bool {
|
||||
// See: http://www.imsglobal.org/spec/lti/v1p3/#role-vocabularies.
|
||||
// This method also provides support for (legacy, deprecated) simple names for context roles.
|
||||
// I.e. 'ContentDeveloper' may be supported.
|
||||
if ($launchdata->roles) {
|
||||
$staffroles = [
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#ContentDeveloper',
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Instructor',
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership/Instructor#TeachingAssistant'
|
||||
];
|
||||
|
||||
if ($includelegacy) {
|
||||
$staffroles[] = 'ContentDeveloper';
|
||||
$staffroles[] = 'Instructor';
|
||||
$staffroles[] = 'Instructor#TeachingAssistant';
|
||||
}
|
||||
|
||||
foreach ($staffroles as $validrole) {
|
||||
if (in_array($validrole, $launchdata->roles)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the use case "A user launches the tool so they can view an external resource".
|
||||
*
|
||||
* @param \stdClass $user the Moodle user record, obtained via the auth_lti authentication process.
|
||||
* @param LtiMessageLaunch $launch the launch data.
|
||||
* @return array array containing [int $userid, \stdClass $resource]
|
||||
* @throws \moodle_exception if launch problems are encountered.
|
||||
*/
|
||||
public function user_launches_tool(\stdClass $user, LtiMessageLaunch $launch): array {
|
||||
|
||||
$launchdata = $this->get_launch_data($launch);
|
||||
|
||||
if (!$registration = $this->registrationrepo->find_by_platform($launchdata->platform, $launchdata->clientid)) {
|
||||
throw new \moodle_exception('ltiadvlauncherror:invalidregistration', 'enrol_lti', '',
|
||||
[$launchdata->platform, $launchdata->clientid]);
|
||||
}
|
||||
|
||||
if (!$deployment = $this->deploymentrepo->find_by_registration($registration->get_id(),
|
||||
$launchdata->deploymentid)) {
|
||||
throw new \moodle_exception('ltiadvlauncherror:invaliddeployment', 'enrol_lti', '',
|
||||
[$launchdata->deploymentid]);
|
||||
}
|
||||
|
||||
$resourceuuid = $launchdata->custom['id'] ?? null;
|
||||
if (empty($resourceuuid)) {
|
||||
throw new \moodle_exception('ltiadvlauncherror:missingid', 'enrol_lti');
|
||||
}
|
||||
|
||||
$resource = array_values(helper::get_lti_tools(['uuid' => $resourceuuid]));
|
||||
$resource = $resource[0] ?? null;
|
||||
if (empty($resource) || $resource->status != ENROL_INSTANCE_ENABLED) {
|
||||
throw new \moodle_exception('ltiadvlauncherror:invalidid', 'enrol_lti', '', $resourceuuid);
|
||||
}
|
||||
|
||||
// Update the deployment with the legacy consumer_key information, allowing migration of users to take place in future
|
||||
// names and roles syncs.
|
||||
if ($migrationclaim = $this->migration_claim_from_launchdata($launchdata)) {
|
||||
$deployment->set_legacy_consumer_key($migrationclaim->get_consumer_key());
|
||||
$this->deploymentrepo->save($deployment);
|
||||
}
|
||||
|
||||
// Save the context, if that claim is present.
|
||||
$context = null;
|
||||
if ($launchdata->context) {
|
||||
$context = $this->context_from_launchdata($launchdata, $deployment);
|
||||
$context = $this->contextrepo->save($context);
|
||||
}
|
||||
|
||||
// Save the resource link for the tool deployment.
|
||||
$resourcelink = $this->resource_link_from_launchdata($launchdata, $resource, $deployment, $context);
|
||||
$resourcelink = $this->resourcelinkrepo->save($resourcelink);
|
||||
|
||||
// Save the user launching the resource link.
|
||||
$ltiuser = $this->lti_user_from_launchdata($user, $launchdata, $resource, $resourcelink);
|
||||
$ltiuser = $this->userrepo->save($ltiuser);
|
||||
|
||||
// Set the frame embedding mode, which controls the display of blocks and nav when launching.
|
||||
global $SESSION;
|
||||
$context = \context::instance_by_id($resource->contextid);
|
||||
$isforceembed = $launchdata->custom['force_embed'] ?? false;
|
||||
$isinstructor = $this->user_is_staff($launchdata, true) || $this->user_is_admin($launchdata);
|
||||
$isforceembed = $isforceembed || ($context->contextlevel == CONTEXT_MODULE && !$isinstructor);
|
||||
if ($isforceembed) {
|
||||
$SESSION->forcepagelayout = 'embedded';
|
||||
} else {
|
||||
unset($SESSION->forcepagelayout);
|
||||
}
|
||||
|
||||
// Enrol the user in the course with no role.
|
||||
$result = helper::enrol_user($resource, $ltiuser->get_localid());
|
||||
if ($result !== helper::ENROLMENT_SUCCESSFUL) {
|
||||
throw new \moodle_exception($result, 'enrol_lti');
|
||||
}
|
||||
|
||||
// Give the user the role in the given context.
|
||||
$roleid = $isinstructor ? $resource->roleinstructor : $resource->rolelearner;
|
||||
role_assign($roleid, $ltiuser->get_localid(), $resource->contextid);
|
||||
|
||||
return [$ltiuser->get_localid(), $resource];
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,523 @@
|
||||
<?php
|
||||
// This file is part of Moodle - http://moodle.org/
|
||||
//
|
||||
// Moodle is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Moodle is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
namespace enrol_lti\local\ltiadvantage\service;
|
||||
|
||||
use core_availability\info_module;
|
||||
use enrol_lti\local\ltiadvantage\entity\resource_link;
|
||||
use enrol_lti\local\ltiadvantage\entity\user;
|
||||
use enrol_lti\local\ltiadvantage\entity\context;
|
||||
use enrol_lti\local\ltiadvantage\repository\application_registration_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\context_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\deployment_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\resource_link_repository;
|
||||
use enrol_lti\local\ltiadvantage\repository\user_repository;
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
require_once(__DIR__ . '/../lti_advantage_testcase.php');
|
||||
|
||||
/**
|
||||
* Tests for the tool_launch_service.
|
||||
*
|
||||
* @package enrol_lti
|
||||
* @copyright 2021 Jake Dallimore <jrhdallimore@gmail.com>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @coversDefaultClass \enrol_lti\local\ltiadvantage\service\tool_launch_service
|
||||
*/
|
||||
class tool_launch_service_test extends \lti_advantage_testcase {
|
||||
|
||||
/**
|
||||
* Test the use case "A user launches a tool so they can view an external resource/activity".
|
||||
*
|
||||
* @dataProvider user_launch_provider
|
||||
* @param array|null $legacydata array detailing what legacy information to create, or null if not required.
|
||||
* @param array|null $launchdata array containing details of the launch, including user and migration claim.
|
||||
* @param array $expected the array detailing expectations.
|
||||
* @covers ::user_launches_tool
|
||||
*/
|
||||
public function test_user_launches_tool(?array $legacydata, ?array $launchdata, array $expected) {
|
||||
$this->resetAfterTest();
|
||||
// Setup.
|
||||
$contextrepo = new context_repository();
|
||||
$resourcelinkrepo = new resource_link_repository();
|
||||
$deploymentrepo = new deployment_repository();
|
||||
$userrepo = new user_repository();
|
||||
[
|
||||
$course,
|
||||
$modresource,
|
||||
$modresource2,
|
||||
$courseresource,
|
||||
$registration,
|
||||
$deployment
|
||||
] = $this->create_test_environment();
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Generate the legacy data, on which the user migration is based.
|
||||
if ($legacydata) {
|
||||
[$legacytools, $legacyconsumer, $legacyusers] = $this->setup_legacy_data($course, $legacydata);
|
||||
}
|
||||
|
||||
// Get a mock 1.3 launch, optionally including the lti1p1 migration claim based on a legacy tool secret.
|
||||
$mocklaunch = $this->get_mock_launch($modresource, $launchdata['user'], null, true, true,
|
||||
$launchdata['launch_migration_claim']);
|
||||
|
||||
// Call the service.
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
if (isset($expected['exception'])) {
|
||||
$this->expectException($expected['exception']);
|
||||
$this->expectExceptionMessage($expected['exception_message']);
|
||||
}
|
||||
[$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
|
||||
|
||||
// As part of the launch, we expect to now have an lti-enrolled user who is recorded against the deployment.
|
||||
$users = $userrepo->find_by_resource($resource->id);
|
||||
$this->assertCount(1, $users);
|
||||
$user = array_pop($users);
|
||||
$this->assertInstanceOf(user::class, $user);
|
||||
$this->assertEquals($deployment->get_id(), $user->get_deploymentid());
|
||||
|
||||
// Deployment should be mapped to the legacy consumer key even if the user wasn't matched and migrated.
|
||||
$updateddeployment = $deploymentrepo->find($deployment->get_id());
|
||||
$this->assertEquals($expected['deployment_consumer_key'], $updateddeployment->get_legacy_consumer_key());
|
||||
|
||||
// The user comes from a resource_link, details of which should also be saved and linked to the deployment.
|
||||
$resourcelinks = $resourcelinkrepo->find_by_resource_and_user($resource->id, $user->get_id());
|
||||
$this->assertCount(1, $resourcelinks);
|
||||
$resourcelink = array_pop($resourcelinks);
|
||||
$this->assertInstanceOf(resource_link::class, $resourcelink);
|
||||
$this->assertEquals($deployment->get_id(), $resourcelink->get_deploymentid());
|
||||
|
||||
// The resourcelink should have a context, which should also be saved and linked to the deployment.
|
||||
$context = $contextrepo->find($resourcelink->get_contextid());
|
||||
$this->assertInstanceOf(context::class, $context);
|
||||
$this->assertEquals($deployment->get_id(), $context->get_deploymentid());
|
||||
|
||||
$enrolledusers = get_enrolled_users(\context_course::instance($course->id));
|
||||
$this->assertCount(1, $enrolledusers);
|
||||
|
||||
// Verify the module is visible to the user.
|
||||
$cmcontext = \context::instance_by_id($modresource->contextid);
|
||||
$this->assertTrue(info_module::is_user_visible($cmcontext->instanceid, $userid));
|
||||
|
||||
// And that other published modules are not yet visible to the user.
|
||||
$cmcontext = \context::instance_by_id($modresource2->contextid);
|
||||
$this->assertFalse(info_module::is_user_visible($cmcontext->instanceid, $userid));
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider for user launch testing.
|
||||
*
|
||||
* @return array the test case data.
|
||||
*/
|
||||
public function user_launch_provider(): array {
|
||||
return [
|
||||
'New tool: no legacy data, no migration claim sent' => [
|
||||
'legacy_data' => null,
|
||||
'launch_data' => [
|
||||
'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
|
||||
'launch_migration_claim' => null,
|
||||
],
|
||||
'expected' => [
|
||||
'deployment_consumer_key' => null,
|
||||
]
|
||||
],
|
||||
'Migrated tool: Legacy data exists, no change in user_id so omitted from claim' => [
|
||||
'legacy_data' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'tools' => [
|
||||
['secret' => 'toolsecret1'],
|
||||
['secret' => 'toolsecret2'],
|
||||
]
|
||||
],
|
||||
'launch_data' => [
|
||||
'user' => $this->get_mock_launch_users_with_ids(['1'])[0],
|
||||
'launch_migration_claim' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'signing_secret' => 'toolsecret1',
|
||||
'context_id' => 'd345b',
|
||||
'tool_consumer_instance_guid' => '12345-123',
|
||||
'resource_link_id' => '4b6fa'
|
||||
],
|
||||
],
|
||||
'expected' => [
|
||||
'deployment_consumer_key' => 'CONSUMER_1',
|
||||
]
|
||||
],
|
||||
|
||||
'Migrated tool: Legacy data exists, platform signs with different valid secret' => [
|
||||
'legacy_data' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'tools' => [
|
||||
['secret' => 'toolsecret1'],
|
||||
['secret' => 'toolsecret2'],
|
||||
]
|
||||
],
|
||||
'launch_data' => [
|
||||
'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
|
||||
'launch_migration_claim' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'signing_secret' => 'toolsecret2',
|
||||
'context_id' => 'd345b',
|
||||
'tool_consumer_instance_guid' => '12345-123',
|
||||
'resource_link_id' => '4b6fa'
|
||||
],
|
||||
],
|
||||
'expected' => [
|
||||
'deployment_consumer_key' => 'CONSUMER_1',
|
||||
]
|
||||
],
|
||||
'Migrated tool: Legacy data exists, no migration claim sent' => [
|
||||
'legacy_data' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'tools' => [
|
||||
['secret' => 'toolsecret1'],
|
||||
['secret' => 'toolsecret2'],
|
||||
]
|
||||
],
|
||||
'launch_data' => [
|
||||
'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
|
||||
'launch_migration_claim' => null,
|
||||
],
|
||||
'expected' => [
|
||||
'deployment_consumer_key' => null,
|
||||
]
|
||||
],
|
||||
'Migrated tool: Legacy data exists, migration claim signature generated using invalid secret' => [
|
||||
'legacy_data' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'tools' => [
|
||||
['secret' => 'toolsecret1'],
|
||||
['secret' => 'toolsecret2'],
|
||||
]
|
||||
],
|
||||
'launch_data' => [
|
||||
'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
|
||||
'launch_migration_claim' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'signing_secret' => 'secret-not-mapped-to-consumer',
|
||||
'user_id' => 'user-id-123',
|
||||
'context_id' => 'd345b',
|
||||
'tool_consumer_instance_guid' => '12345-123',
|
||||
'resource_link_id' => '4b6fa'
|
||||
],
|
||||
],
|
||||
'expected' => [
|
||||
'exception' => \coding_exception::class,
|
||||
'exception_message' => "Invalid 'oauth_consumer_key_sign' signature in lti1p1 claim"
|
||||
]
|
||||
],
|
||||
'Migrated tool: Legacy data exists, migration claim signature omitted' => [
|
||||
'legacy_data' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'tools' => [
|
||||
['secret' => 'toolsecret1'],
|
||||
['secret' => 'toolsecret2'],
|
||||
]
|
||||
],
|
||||
'launch_data' => [
|
||||
'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
|
||||
'launch_migration_claim' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'user_id' => 'user-id-123',
|
||||
'context_id' => 'd345b',
|
||||
'tool_consumer_instance_guid' => '12345-123',
|
||||
'resource_link_id' => '4b6fa'
|
||||
],
|
||||
],
|
||||
'expected' => [
|
||||
'exception' => \coding_exception::class,
|
||||
'exception_message' => "Missing 'oauth_consumer_key_sign' property in lti1p1 migration claim."
|
||||
]
|
||||
],
|
||||
'Migrated tool: Legacy data exists, migration claim missing oauth_consumer_key' => [
|
||||
'legacy_data' => [
|
||||
'consumer_key' => 'CONSUMER_1',
|
||||
'tools' => [
|
||||
['secret' => 'toolsecret1'],
|
||||
['secret' => 'toolsecret2'],
|
||||
]
|
||||
],
|
||||
'launch_data' => [
|
||||
'user' => $this->get_mock_launch_users_with_ids(['1p3_1'])[0],
|
||||
'launch_migration_claim' => [
|
||||
'user_id' => 'user-id-123',
|
||||
'context_id' => 'd345b',
|
||||
'tool_consumer_instance_guid' => '12345-123',
|
||||
'resource_link_id' => '4b6fa'
|
||||
],
|
||||
],
|
||||
'expected' => [
|
||||
'deployment_consumer_key' => null
|
||||
]
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test confirming that an exception is thrown if trying to launch a published resource without a custom id.
|
||||
*
|
||||
* @covers ::user_launches_tool
|
||||
*/
|
||||
public function test_user_launches_tool_missing_custom_id() {
|
||||
$this->resetAfterTest();
|
||||
[$course, $modresource] = $this->create_test_environment();
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$mockuser = $this->get_mock_launch_users_with_ids(['1p3_1'])[0];
|
||||
$mocklaunch = $this->get_mock_launch($modresource, $mockuser, null, false, false, null, []);
|
||||
|
||||
$this->expectException(\moodle_exception::class);
|
||||
$this->expectExceptionMessage(get_string('ltiadvlauncherror:missingid', 'enrol_lti'));
|
||||
[$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test confirming that an exception is thrown if trying to launch a published resource that doesn't exist.
|
||||
*
|
||||
* @covers ::user_launches_tool
|
||||
*/
|
||||
public function test_user_launches_tool_invalid_custom_id() {
|
||||
$this->resetAfterTest();
|
||||
[$course, $modresource] = $this->create_test_environment();
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$mockuser = $this->get_mock_launch_users_with_ids(['1p3_1'])[0];
|
||||
$mocklaunch = $this->get_mock_launch($modresource, $mockuser, null, false, false, null, ['id' => 999999]);
|
||||
|
||||
$this->expectException(\moodle_exception::class);
|
||||
$this->expectExceptionMessage(get_string('ltiadvlauncherror:invalidid', 'enrol_lti', 999999));
|
||||
[$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test confirming that an exception is thrown if trying to launch the tool where no application can be found.
|
||||
*
|
||||
* @covers ::user_launches_tool
|
||||
*/
|
||||
public function test_user_launches_tool_missing_registration() {
|
||||
$this->resetAfterTest();
|
||||
// Setup.
|
||||
[
|
||||
$course,
|
||||
$modresource,
|
||||
$modresource2,
|
||||
$courseresource,
|
||||
$registration,
|
||||
$deployment
|
||||
] = $this->create_test_environment();
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Delete the registration before trying to launch.
|
||||
$appregrepo = new application_registration_repository();
|
||||
$appregrepo->delete($registration->get_id());
|
||||
|
||||
// Call the service.
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$mockuser = $this->get_mock_launch_users_with_ids(['1p3_1'])[0];
|
||||
$mocklaunch = $this->get_mock_launch($modresource, $mockuser);
|
||||
|
||||
$this->expectException(\moodle_exception::class);
|
||||
$this->expectExceptionMessage(get_string('ltiadvlauncherror:invalidregistration', 'enrol_lti',
|
||||
[$registration->get_platformid(), $registration->get_clientid()]));
|
||||
[$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test confirming that an exception is thrown if trying to launch the tool where no deployment can be found.
|
||||
*
|
||||
* @covers ::user_launches_tool
|
||||
*/
|
||||
public function test_user_launches_tool_missing_deployment() {
|
||||
$this->resetAfterTest();
|
||||
// Setup.
|
||||
[
|
||||
$course,
|
||||
$modresource,
|
||||
$modresource2,
|
||||
$courseresource,
|
||||
$registration,
|
||||
$deployment
|
||||
] = $this->create_test_environment();
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
|
||||
// Delete the deployment before trying to launch.
|
||||
$deploymentrepo = new deployment_repository();
|
||||
$deploymentrepo->delete($deployment->get_id());
|
||||
|
||||
// Call the service.
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$mockuser = $this->get_mock_launch_users_with_ids(['1p3_1'])[0];
|
||||
$mocklaunch = $this->get_mock_launch($modresource, $mockuser);
|
||||
|
||||
$this->expectException(\moodle_exception::class);
|
||||
$this->expectExceptionMessage(get_string('ltiadvlauncherror:invaliddeployment', 'enrol_lti',
|
||||
[$deployment->get_deploymentid()]));
|
||||
[$userid, $resource] = $launchservice->user_launches_tool($instructoruser, $mocklaunch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the mapping from IMS roles to Moodle roles during a launch.
|
||||
*
|
||||
* @covers ::user_launches_tool
|
||||
*/
|
||||
public function test_user_launches_tool_role_mapping() {
|
||||
$this->resetAfterTest();
|
||||
// Create mock launches for 3 different user types: instructor, admin, learner.
|
||||
[$course, $modresource] = $this->create_test_environment();
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
$instructor2user = $this->getDataGenerator()->create_user();
|
||||
$adminuser = $this->getDataGenerator()->create_user();
|
||||
$learneruser = $this->getDataGenerator()->create_user();
|
||||
$mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
|
||||
$mockadminuser = $this->get_mock_launch_users_with_ids(
|
||||
['2'],
|
||||
false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/system/person#Administrator'
|
||||
)[0];
|
||||
$mocklearneruser = $this->get_mock_launch_users_with_ids(
|
||||
['3'],
|
||||
false,
|
||||
'http://purl.imsglobal.org/vocab/lis/v2/membership#Learner'
|
||||
)[0];
|
||||
$mockinstructor2user = $this->get_mock_launch_users_with_ids(
|
||||
['3'],
|
||||
false,
|
||||
'Instructor' // Using the legacy (deprecated in 1.3) simple name.
|
||||
)[0];
|
||||
$mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser);
|
||||
$mockadminlaunch = $this->get_mock_launch($modresource, $mockadminuser);
|
||||
$mocklearnerlaunch = $this->get_mock_launch($modresource, $mocklearneruser);
|
||||
$mockinstructor2launch = $this->get_mock_launch($modresource, $mockinstructor2user);
|
||||
|
||||
// Launch and confirm the role assignment.
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$modulecontext = \context::instance_by_id($modresource->contextid);
|
||||
|
||||
[$instructorid] = $launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch);
|
||||
[$instructorrole] = array_slice(get_user_roles($modulecontext, $instructorid), 0, 1);
|
||||
$this->assertEquals('teacher', $instructorrole->shortname);
|
||||
|
||||
[$adminid] = $launchservice->user_launches_tool($adminuser, $mockadminlaunch);
|
||||
[$adminrole] = array_slice(get_user_roles($modulecontext, $adminid), 0, 1);
|
||||
$this->assertEquals('teacher', $adminrole->shortname);
|
||||
|
||||
[$learnerid] = $launchservice->user_launches_tool($learneruser, $mocklearnerlaunch);
|
||||
[$learnerrole] = array_slice(get_user_roles($modulecontext, $learnerid), 0, 1);
|
||||
$this->assertEquals('student', $learnerrole->shortname);
|
||||
|
||||
[$instructor2id] = $launchservice->user_launches_tool($instructor2user, $mockinstructor2launch);
|
||||
[$instructor2role] = array_slice(get_user_roles($modulecontext, $instructor2id), 0, 1);
|
||||
$this->assertEquals('teacher', $instructor2role->shortname);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test verifying that a user launch can result in updates to some user fields.
|
||||
*
|
||||
* @covers ::user_launches_tool
|
||||
*/
|
||||
public function test_user_launches_tool_user_fields_updated() {
|
||||
$this->resetAfterTest();
|
||||
[$course, $modresource] = $this->create_test_environment();
|
||||
$user = $this->getDataGenerator()->create_user();
|
||||
$mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
$userrepo = new user_repository();
|
||||
|
||||
// Launch once, verifying the user details.
|
||||
$mocklaunch = $this->get_mock_launch($modresource, $mockinstructoruser);
|
||||
$launchservice->user_launches_tool($user, $mocklaunch);
|
||||
$createduser = $userrepo->find_single_user_by_resource(
|
||||
$user->id,
|
||||
$modresource->id
|
||||
);
|
||||
$this->assertEquals($modresource->lang, $createduser->get_lang());
|
||||
$this->assertEquals($modresource->city, $createduser->get_city());
|
||||
$this->assertEquals($modresource->country, $createduser->get_country());
|
||||
$this->assertEquals($modresource->institution, $createduser->get_institution());
|
||||
$this->assertEquals($modresource->timezone, $createduser->get_timezone());
|
||||
$this->assertEquals($modresource->maildisplay, $createduser->get_maildisplay());
|
||||
|
||||
// Change the resource's defaults and relaunch, verifying the relevant fields are updated for the launch user.
|
||||
// Note: lang change can't be tested without installation of another language pack.
|
||||
$modresource->city = 'Paris';
|
||||
$modresource->country = 'FR';
|
||||
$modresource->institution = 'Updated institution name';
|
||||
$modresource->timezone = 'UTC';
|
||||
$modresource->maildisplay = '1';
|
||||
global $DB;
|
||||
$DB->update_record('enrol_lti_tools', $modresource);
|
||||
|
||||
$mocklaunch = $this->get_mock_launch($modresource, $mockinstructoruser);
|
||||
$launchservice->user_launches_tool($user, $mocklaunch);
|
||||
$createduser = $userrepo->find($createduser->get_id());
|
||||
$this->assertEquals($modresource->city, $createduser->get_city());
|
||||
$this->assertEquals($modresource->country, $createduser->get_country());
|
||||
$this->assertEquals($modresource->institution, $createduser->get_institution());
|
||||
$this->assertEquals($modresource->timezone, $createduser->get_timezone());
|
||||
$this->assertEquals($modresource->maildisplay, $createduser->get_maildisplay());
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the launch when a module has an enrolment start date.
|
||||
*
|
||||
* @covers ::user_launches_tool
|
||||
*/
|
||||
public function test_user_launches_tool_max_enrolment_start_restriction() {
|
||||
$this->resetAfterTest();
|
||||
[$course, $modresource] = $this->create_test_environment(true, true, false,
|
||||
\enrol_lti\helper::MEMBER_SYNC_ENROL_NEW, false, false, time() + DAYSECS);
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
$mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
|
||||
$mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser);
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
|
||||
$this->expectException(\moodle_exception::class);
|
||||
$launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch);
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the Moodle-specific custom param 'forceembed' during user launches.
|
||||
*
|
||||
* @covers ::user_launches_tool
|
||||
*/
|
||||
public function test_user_launches_tool_force_embedding_custom_param() {
|
||||
$this->resetAfterTest();
|
||||
[$course, $modresource] = $this->create_test_environment();
|
||||
$instructoruser = $this->getDataGenerator()->create_user();
|
||||
$learneruser = $this->getDataGenerator()->create_user();
|
||||
$mockinstructoruser = $this->get_mock_launch_users_with_ids(['1'])[0];
|
||||
$mocklearneruser = $this->get_mock_launch_users_with_ids(['1'], false, '')[0];
|
||||
$mockinstructorlaunch = $this->get_mock_launch($modresource, $mockinstructoruser, null, false, false, null, [
|
||||
'id' => $modresource->uuid,
|
||||
'forcedembed' => true
|
||||
]);
|
||||
$mocklearnerlaunch = $this->get_mock_launch($modresource, $mocklearneruser, null, false, false, null, [
|
||||
'id' => $modresource->uuid,
|
||||
'forcedembed' => true
|
||||
]);
|
||||
$launchservice = $this->get_tool_launch_service();
|
||||
global $SESSION;
|
||||
|
||||
// Instructors aren't subject to forceembed.
|
||||
$launchservice->user_launches_tool($instructoruser, $mockinstructorlaunch);
|
||||
$this->assertObjectNotHasAttribute('forcepagelayout', $SESSION);
|
||||
|
||||
// Learners are.
|
||||
$launchservice->user_launches_tool($learneruser, $mocklearnerlaunch);
|
||||
$this->assertEquals('embedded', $SESSION->forcepagelayout);
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user