diff --git a/enrol/lti/classes/local/ltiadvantage/service/application_registration_service.php b/enrol/lti/classes/local/ltiadvantage/service/application_registration_service.php new file mode 100644 index 00000000000..9e13ae6f85e --- /dev/null +++ b/enrol/lti/classes/local/ltiadvantage/service/application_registration_service.php @@ -0,0 +1,176 @@ +. + +namespace enrol_lti\local\ltiadvantage\service; + +use enrol_lti\local\ltiadvantage\entity\application_registration; +use enrol_lti\local\ltiadvantage\entity\registration_url; +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\registration_url_repository; +use enrol_lti\local\ltiadvantage\repository\resource_link_repository; +use enrol_lti\local\ltiadvantage\repository\user_repository; + +/** + * Class application_registration_service. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class application_registration_service { + /** @var application_registration_repository repository to work with application_registration instances. */ + private $appregistrationrepo; + + /** @var deployment_repository repository to work with deployment instances. */ + private $deploymentrepo; + + /** @var resource_link_repository repository to work with resource link instances. */ + private $resourcelinkrepo; + + /** @var context_repository repository to work with context instances. */ + private $contextrepo; + + /** @var user_repository repository to work with user instances. */ + private $userrepo; + + /** + * The application_registration_service constructor. + * + * @param application_registration_repository $appregistrationrepo an application registration repository instance. + * @param deployment_repository $deploymentrepo a deployment repository instance. + * @param resource_link_repository $resourcelinkrepo a resource_link_repository instance. + * @param context_repository $contextrepo a context_repository instance. + * @param user_repository $userrepo a user_repository instance. + */ + public function __construct(application_registration_repository $appregistrationrepo, + deployment_repository $deploymentrepo, resource_link_repository $resourcelinkrepo, + context_repository $contextrepo, user_repository $userrepo) { + + $this->appregistrationrepo = $appregistrationrepo; + $this->deploymentrepo = $deploymentrepo; + $this->resourcelinkrepo = $resourcelinkrepo; + $this->contextrepo = $contextrepo; + $this->userrepo = $userrepo; + } + + /** + * Convert a DTO into a new application_registration domain object. + * + * @param \stdClass $dto the object containing information needed to register an application. + * @return application_registration the application_registration object + */ + private function registration_from_dto(\stdClass $dto): application_registration { + return application_registration::create( + $dto->name, + new \moodle_url($dto->platformid), + $dto->clientid, + new \moodle_url($dto->authenticationrequesturl), + new \moodle_url($dto->jwksurl), + new \moodle_url($dto->accesstokenurl), + $dto->id ?? null + ); + } + + /** + * Application service handling the use case "As an admin I can register an application as an LTI platform". + * + * @param \stdClass $appregdto details of the application to register. + * @return application_registration the application_registration domain object. + */ + public function create_application_registration(\stdClass $appregdto): application_registration { + if ($this->appregistrationrepo->find_by_platform($appregdto->platformid, $appregdto->clientid)) { + throw new \moodle_exception("A registration for issuer '$appregdto->platformid', " + ."clientid '$appregdto->clientid' already exists."); + } + return $this->appregistrationrepo->save($this->registration_from_dto($appregdto)); + } + + /** + * Application service handling the use case "As an admin I can update the registration of an LTI platform". + * + * @param \stdClass $appregdto details of the registration to update. + * @return application_registration the application_registration domain object. + */ + public function update_application_registration(\stdClass $appregdto): application_registration { + if (empty($appregdto->id)) { + throw new \coding_exception('Cannot update registration. Id is missing.'); + } + return $this->appregistrationrepo->save($this->registration_from_dto($appregdto)); + } + + /** + * Application service handling the use case "As an admin I can delete a registration of an LTI platform". + * + * @param int $registrationid id of the registration to delete. + */ + public function delete_application_registration(int $registrationid): void { + + $deployments = $this->deploymentrepo->find_all_by_registration($registrationid); + if ($deployments) { + $deploymentservice = new tool_deployment_service( + $this->appregistrationrepo, + $this->deploymentrepo, + $this->resourcelinkrepo, + $this->contextrepo, + $this->userrepo + ); + foreach ($deployments as $deployment) { + $deploymentservice->delete_tool_deployment($deployment->get_id()); + } + } + + $this->appregistrationrepo->delete($registrationid); + } + + /** + * Get a one-time-use dynamic registration URL which is valid only for the specified duration. + * + * @param int $durationsecs how long, in seconds, the registration URL will be valid for. + * @return registration_url the registration_url instnace. + */ + public function create_registration_url(int $durationsecs = 86400): registration_url { + if ($durationsecs <= 0) { + throw new \coding_exception('Invalid registration URL duration. Must be greater than 0.'); + } + + $regurl = new registration_url(time() + $durationsecs); + $regurlrepo = new registration_url_repository(); + return $regurlrepo->save($regurl); + } + + /** + * Get the current dynamic registration URL. + * + * Will return null if the URL doesn't exist, or if the URL has expired. + * + * @param string $token Used to get the registration url by token. + * @return registration_url|null the registration_url instance if valid, otherwise null. + */ + public function get_registration_url(string $token = ''): ?registration_url { + $regurlrepo = new registration_url_repository(); + return $token ? $regurlrepo->find_by_token($token) : $regurlrepo->find(); + } + + /** + * Delete the current dynamic registration URL. + */ + public function delete_registration_url(): void { + $regurlrepo = new registration_url_repository(); + $regurlrepo->delete(); + } +} diff --git a/enrol/lti/tests/local/ltiadvantage/service/application_registration_service_test.php b/enrol/lti/tests/local/ltiadvantage/service/application_registration_service_test.php new file mode 100644 index 00000000000..3c7439a69cd --- /dev/null +++ b/enrol/lti/tests/local/ltiadvantage/service/application_registration_service_test.php @@ -0,0 +1,324 @@ +. + +namespace enrol_lti\local\ltiadvantage\service; + +use enrol_lti\helper; +use enrol_lti\local\ltiadvantage\entity\registration_url; +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 application_registration_service. + * + * @package enrol_lti + * @copyright 2021 Jake Dallimore + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + * @coversDefaultClass \enrol_lti\local\ltiadvantage\service\application_registration_service + */ +class application_registration_service_test extends \lti_advantage_testcase { + /** + * Helper to get an application_registration_service instance. + * @return application_registration_service + */ + protected function get_application_registration_service(): application_registration_service { + return new application_registration_service( + new application_registration_repository(), + new deployment_repository(), + new resource_link_repository(), + new context_repository(), + new user_repository() + ); + } + + /** + * Test the use case "As an admin, I can register an application as an LTI consumer (platform)". + * + * @covers ::create_application_registration + */ + public function test_register_application() { + $this->resetAfterTest(); + $reg = (object) [ + 'name' => 'Example LMS application', + 'platformid' => 'https://lms.example.org', + 'clientid' => '123', + 'authenticationrequesturl' => new \moodle_url('https://example.org/authrequesturl'), + 'jwksurl' => new \moodle_url('https://example.org/jwksurl'), + 'accesstokenurl' => new \moodle_url('https://example.org/accesstokenurl') + ]; + + $service = $this->get_application_registration_service(); + $createdreg = $service->create_application_registration($reg); + + $regrepo = new application_registration_repository(); + $this->assertTrue($regrepo->exists($createdreg->get_id())); + } + + /** + * Test verifying that the service cannot save two identical (same issuer and clientid) application registrations. + * + * @covers ::create_application_registration + */ + public function test_register_application_unique_constraints() { + $this->resetAfterTest(); + $reg = (object) [ + 'name' => 'Example LMS application', + 'platformid' => 'https://lms.example.org', + 'clientid' => '123', + 'authenticationrequesturl' => new \moodle_url('https://example.org/authrequesturl'), + 'jwksurl' => new \moodle_url('https://example.org/jwksurl'), + 'accesstokenurl' => new \moodle_url('https://example.org/accesstokenurl') + ]; + + $service = $this->get_application_registration_service(); + $service->create_application_registration($reg); + + $this->expectException(\moodle_exception::class); + $service->create_application_registration($reg); + } + + /** + * Test the use case "As an admin, I can update an application registered as an LTI consumer (platform)". + * + * @covers ::update_application_registration + */ + public function test_update_application_registration() { + $this->resetAfterTest(); + $reg = (object) [ + 'name' => 'Example LMS application', + 'platformid' => 'https://lms.example.org', + 'clientid' => '123', + 'authenticationrequesturl' => new \moodle_url('https://example.org/authrequesturl'), + 'jwksurl' => new \moodle_url('https://example.org/jwksurl'), + 'accesstokenurl' => new \moodle_url('https://example.org/accesstokenurl') + ]; + + $service = $this->get_application_registration_service(); + $createdreg = $service->create_application_registration($reg); + + $reg->id = $createdreg->get_id(); + $reg->jwksurl = new \moodle_url('https://example.org/updated_jwksurl'); + + $updatedreg = $service->update_application_registration($reg); + $this->assertEquals($reg->name, $updatedreg->get_name()); + $this->assertEquals($reg->jwksurl, $updatedreg->get_jwksurl()); + } + + /** + * Test verifying that the service requires an object id. + * + * @covers ::update_application_registration + */ + public function test_update_application_registration_missing_id() { + $this->resetAfterTest(); + $reg = (object) [ + 'name' => 'Example LMS application', + 'platformid' => 'https://lms.example.org', + 'clientid' => '123', + 'authenticationrequesturl' => new \moodle_url('https://example.org/authrequesturl'), + 'jwksurl' => new \moodle_url('https://example.org/jwksurl'), + 'accesstokenurl' => new \moodle_url('https://example.org/accesstokenurl') + ]; + + $service = $this->get_application_registration_service(); + $service->create_application_registration($reg); + + $reg->jwksurl = new \moodle_url('https://example.org/updated_jwksurl'); + + $this->expectException(\coding_exception::class); + $service->update_application_registration($reg); + } + + /** + * Test that removing an application registration also removes all associated data. + * + * @covers ::delete_application_registration + */ + public function test_delete_application_registration() { + $this->resetAfterTest(); + // Setup. + $registrationrepo = new application_registration_repository(); + $deploymentrepo = new deployment_repository(); + $contextrepo = new context_repository(); + $resourcelinkrepo = new resource_link_repository(); + $userrepo = new user_repository(); + [$course, $resource] = $this->create_test_environment(); + + // Launch the tool for a user. + $mocklaunch = $this->get_mock_launch($resource, $this->get_mock_launch_users_with_ids(['1'])[0]); + $instructoruser = $this->getDataGenerator()->create_user(); + $launchservice = $this->get_tool_launch_service(); + $launchservice->user_launches_tool($instructoruser, $mocklaunch); + + // Check all the expected data exists for the deployment after setup. + $registrations = $registrationrepo->find_all(); + $this->assertCount(1, $registrations); + $registration = array_pop($registrations); + + $deployments = $deploymentrepo->find_all_by_registration($registration->get_id()); + $this->assertCount(1, $deployments); + $deployment = array_pop($deployments); + + $resourcelinks = $resourcelinkrepo->find_by_resource($resource->id); + $this->assertCount(1, $resourcelinks); + $resourcelink = array_pop($resourcelinks); + + $context = $contextrepo->find($resourcelink->get_contextid()); + $this->assertNotNull($context); + + $users = $userrepo->find_by_resource($resource->id); + $this->assertCount(1, $users); + $user = array_pop($users); + + $enrolledusers = get_enrolled_users(\context_course::instance($course->id)); + $this->assertCount(1, $enrolledusers); + + // Now delete the application_registration using the service. + $service = $this->get_application_registration_service(); + $service->delete_application_registration($registration->get_id()); + + // Verify that the context, resourcelink, user, deployment and registration instances are all deleted. + $this->assertFalse($registrationrepo->exists($registration->get_id())); + $this->assertFalse($deploymentrepo->exists($deployment->get_id())); + $this->assertFalse($contextrepo->exists($context->get_id())); + $this->assertFalse($resourcelinkrepo->exists($resourcelink->get_id())); + $this->assertFalse($userrepo->exists($user->get_id())); + + // Verify that all users are unenrolled. + $enrolledusers = get_enrolled_users(\context_course::instance($course->id)); + $this->assertCount(0, $enrolledusers); + + // Verify the tool record stays in place (I.e. the published resource is still available). + $this->assertNotEmpty(helper::get_lti_tool($resource->id)); + } + + /** + * Test creation of a dynamic registration url. + * @dataProvider registration_url_data_provider + * @param int|null $duration how long the URL is valid for, in seconds. + * @param array $expected the array of expected values/results. + * @covers ::create_registration_url + */ + public function test_create_registration_url(?int $duration, array $expected) { + $this->resetAfterTest(); + $appregservice = $this->get_application_registration_service(); + if ($expected['exception']) { + $this->expectException($expected['exception']); + } + if (!is_null($duration)) { + $regurl = $appregservice->create_registration_url($duration); + } else { + $regurl = $appregservice->create_registration_url(); + } + $this->assertInstanceOf(registration_url::class, $regurl); + $this->assertGreaterThanOrEqual($expected['expirytime'], $regurl->get_expiry_time()); + } + + /** + * Data provider for testing registration url creation. + * + * @return array the test data. + */ + public function registration_url_data_provider() { + return [ + 'no params' => [ + 'duration' => null, + 'expected' => [ + 'expirytime' => time() + 86400, + 'exception' => false, + ] + ], + 'expiry specified' => [ + 'duration' => 3600, + 'expected' => [ + 'expirytime' => time() + 3600, + 'exception' => false, + ] + ], + 'invalid expiry specified' => [ + 'duration' => -5, + 'expected' => [ + 'expirytime' => null, + 'exception' => \coding_exception::class, + ] + ] + ]; + } + + /** + * Test getting the current registration url. + * + * @covers ::get_registration_url + */ + public function test_get_registration_url() { + $this->resetAfterTest(); + $appregservice = $this->get_application_registration_service(); + + // Check when not existing. + $this->assertNull($appregservice->get_registration_url()); + + // Check after creation. + $appregservice->create_registration_url(); + $regurl = $appregservice->get_registration_url(); + $this->assertInstanceOf(registration_url::class, $regurl); + } + + /** + * Test getting a registration URL by its token. + * + * @covers ::get_registration_url + */ + public function test_get_registration_url_using_token() { + $this->resetAfterTest(); + $appregservice = $this->get_application_registration_service(); + $createdregurl = $appregservice->create_registration_url(); + + // Check valid token. + $token = $createdregurl->param('token'); + $regurl = $appregservice->get_registration_url($token); + $this->assertInstanceOf(registration_url::class, $regurl); + + // Check invalid token. + $this->assertNull($appregservice->get_registration_url('invalid_token')); + } + + /** + * Test deletion of the current registration URL. + * + * @covers ::delete_registration_url + */ + public function test_delete_registration_url() { + $this->resetAfterTest(); + $appregservice = $this->get_application_registration_service(); + + // Deletion when no URL exists. + $this->assertNull($appregservice->delete_registration_url()); + + // Deletion of a URL. + $appregservice->create_registration_url(); + $regurl = $appregservice->get_registration_url(); + $this->assertInstanceOf(registration_url::class, $regurl); + $appregservice->delete_registration_url(); + $this->assertNull($appregservice->get_registration_url()); + } +}