From 8701aa13c4d98a8911c9b69da3eacd42ff653b3c Mon Sep 17 00:00:00 2001 From: Murtaza Sultani Date: Thu, 12 Mar 2026 14:29:10 +0100 Subject: AGS --- app/controllers/admin/lti/AdminBaseController.php | 100 ---------------- app/controllers/admin/lti/deployments.php | 3 +- app/controllers/admin/lti/publications.php | 7 +- app/controllers/admin/lti/registrations.php | 3 +- app/controllers/admin/lti/resources.php | 5 +- app/controllers/course/lti.php | 2 +- app/controllers/enroll/lti/LtiBaseController.php | 55 --------- app/controllers/enroll/lti/auth_init.php | 18 +-- app/controllers/enroll/lti/contents.php | 9 +- app/controllers/enroll/lti/jwks.php | 18 +-- app/controllers/enroll/lti/launch.php | 17 +-- app/controllers/enroll/lti/launch_deeplink.php | 41 +++---- app/controllers/enroll/lti/provisioning_modes.php | 5 +- app/controllers/lti/1p3/ags/line_item.php | 74 +++--------- app/controllers/lti/1p3/ags/line_items.php | 32 +---- app/controllers/lti/1p3/jwks.php | 20 +--- app/controllers/lti/1p3/login.php | 21 +--- app/controllers/lti/1p3/token.php | 67 ++--------- db/migrations/6.3.1_step_5405.php | 2 +- lib/activities/ActivityObserver.php | 2 +- lib/activities/LtiCallbackProcessor.php | 53 --------- lib/classes/Lti/Controller/AdminBaseController.php | 100 ++++++++++++++++ lib/classes/Lti/Controller/AgsBaseController.php | 30 +++++ .../Lti/Controller/EnrollBaseController.php | 56 +++++++++ .../Lti/Controller/PlatformBaseController.php | 13 ++ lib/classes/Lti/Enum/ConfigurableType.php | 10 +- lib/classes/Lti/Enum/GradeSynchronization.php | 3 +- lib/classes/Lti/Enum/LtiVersion.php | 3 +- lib/classes/Lti/Enum/PublicationStatus.php | 5 +- lib/classes/Lti/Enum/RegistrationStatus.php | 5 +- lib/classes/Lti/Enum/ResourceLaunchContainer.php | 3 +- lib/classes/Lti/Enum/StatusHelpers.php | 38 ------ .../Lti/Enum/UserIdentityMappingContext.php | 3 +- lib/classes/Lti/Enum/UserProvisioningMode.php | 3 +- .../Lti/LTI1p3/AccessTokenResponseGenerator.php | 42 +++++++ lib/classes/Lti/LTI1p3/Helper.php | 17 +++ lib/classes/Lti/LTI1p3/Identity.php | 98 --------------- lib/classes/Lti/LTI1p3/KeyChainFactory.php | 43 ------- lib/classes/Lti/LTI1p3/KeyChainRepository.php | 14 +++ lib/classes/Lti/LTI1p3/KeyManager.php | 24 ---- lib/classes/Lti/LTI1p3/LineItemRepository.php | 131 ++++++--------------- lib/classes/Lti/LTI1p3/PlatformManager.php | 2 +- lib/classes/Lti/LTI1p3/RepositoryRegistry.php | 14 +-- lib/classes/Lti/LTI1p3/ResourceLinkRepository.php | 24 +++- lib/classes/Lti/LTI1p3/ResultRepository.php | 57 ++++----- lib/classes/Lti/LTI1p3/ScoreRepository.php | 31 ++--- lib/classes/Lti/LTI1p3/UserAuthenticator.php | 2 +- lib/classes/Lti/LTI1p3/UserIdentity.php | 71 +++++++++++ lib/classes/Lti/Observer/EnrollCallback.php | 53 +++++++++ lib/classes/Lti/Trait/StatusHelpers.php | 39 ++++++ lib/models/Grading/Definition.php | 32 +++-- lib/models/Grading/Instance.php | 17 ++- 52 files changed, 693 insertions(+), 844 deletions(-) delete mode 100644 app/controllers/admin/lti/AdminBaseController.php delete mode 100644 app/controllers/enroll/lti/LtiBaseController.php delete mode 100644 lib/activities/LtiCallbackProcessor.php create mode 100644 lib/classes/Lti/Controller/AdminBaseController.php create mode 100644 lib/classes/Lti/Controller/AgsBaseController.php create mode 100644 lib/classes/Lti/Controller/EnrollBaseController.php create mode 100644 lib/classes/Lti/Controller/PlatformBaseController.php delete mode 100644 lib/classes/Lti/Enum/StatusHelpers.php create mode 100644 lib/classes/Lti/LTI1p3/AccessTokenResponseGenerator.php create mode 100644 lib/classes/Lti/LTI1p3/Helper.php delete mode 100644 lib/classes/Lti/LTI1p3/Identity.php delete mode 100644 lib/classes/Lti/LTI1p3/KeyChainFactory.php create mode 100644 lib/classes/Lti/LTI1p3/KeyChainRepository.php delete mode 100644 lib/classes/Lti/LTI1p3/KeyManager.php create mode 100644 lib/classes/Lti/LTI1p3/UserIdentity.php create mode 100644 lib/classes/Lti/Observer/EnrollCallback.php create mode 100644 lib/classes/Lti/Trait/StatusHelpers.php diff --git a/app/controllers/admin/lti/AdminBaseController.php b/app/controllers/admin/lti/AdminBaseController.php deleted file mode 100644 index 1790a5c..0000000 --- a/app/controllers/admin/lti/AdminBaseController.php +++ /dev/null @@ -1,100 +0,0 @@ -range_id = Context::getId(); - $this->isModerator = LtiToolModule::isModerator($this->range_id); - - if (!$this->isModerator) { - throw new AccessDeniedException(); - } - - $this->isToolSharingEnabled = LtiToolModule::isToolSharingEnabled(); - } - - protected function buildRegistrationsSidebar(): void - { - // views: - $viewWidget = new ViewsWidget(); - - $viewWidget->addLink( - _('LTI-Tools'), - $this->url_for('admin/lti/registrations', ['role' => 'tool']), - )->setActive($this->ltiRole !== 'platform'); - - if ($this->isToolSharingEnabled) { - $viewWidget->addLink( - _('LTI-Platforms'), - $this->url_for('admin/lti/registrations', ['role' => 'platform']) - )->setActive($this->ltiRole === 'platform'); - } - - Sidebar::Get()->addWidget($viewWidget); - - // actions: - $actions = new ActionsWidget(); - $actions->addLink( - _('Neues LTI-Tool registrieren'), - $this->url_for('admin/lti/registrations/create', ['role' => 'tool']), - Icon::create('add') - )->asDialog('width=900;height=700'); - - if ($this->isToolSharingEnabled) { - $actions->addLink( - _('Neues LTI-Platform registrieren'), - $this->url_for('admin/lti/registrations/create', ['role' => 'platform']), - Icon::create('add') - )->asDialog('width=900;height=700'); - } - - $actions->addLink( - _('Daten zur LTI-Plattform anzeigen'), - $this->url_for('admin/lti/registrations/platform_data'), - Icon::create('info') - )->asDialog('width=900;height=700'); - - if ($this->isToolSharingEnabled) { - $actions->addLink( - _('Daten zur LTI-Tool anzeigen'), - $this->url_for('admin/lti/registrations/tool_data'), - Icon::create('info') - )->asDialog('width=900;height=700'); - } - - Sidebar::get()->addWidget($actions); - } - - protected function buildPublicationsSidebar(): void - { - if($this->isToolSharingEnabled) { - // actions: - $actions = new ActionsWidget(); - $actions->addLink( - _('Neue Veröffentlichung anlegen'), - $this->url_for('admin/lti/publications/create'), - Icon::create('add') - )->asDialog('width=700'); - - Sidebar::get()->addWidget($actions); - } - } - -} diff --git a/app/controllers/admin/lti/deployments.php b/app/controllers/admin/lti/deployments.php index 3a39d69..012c12e 100644 --- a/app/controllers/admin/lti/deployments.php +++ b/app/controllers/admin/lti/deployments.php @@ -1,10 +1,9 @@ preferred_language = 'en_GB'; - $user->store(); - } - - return $this; - } - - protected function validateCallbackData(string $callbackId): array - { - if (empty($_SESSION['callbacks'][$callbackId])) { - throw new AccessDeniedException('Missing or invalid callback ID'); - } - - $callbackData = $_SESSION['callbacks'][$callbackId]; - if ( - $callbackData['context'] !== 'lti' - || $callbackData['expires_at'] < time() - ) { - throw new AccessDeniedException('Invalid or expired callback data'); - } - - return $callbackData; - } -} diff --git a/app/controllers/enroll/lti/auth_init.php b/app/controllers/enroll/lti/auth_init.php index 46d3b61..0dca593 100644 --- a/app/controllers/enroll/lti/auth_init.php +++ b/app/controllers/enroll/lti/auth_init.php @@ -1,26 +1,18 @@ get(OidcInitiationRequestHandler::class); + $this->renderPsrResponse( - $this->oidcInitHandler->handle($this->getPsrRequest()) + $oidcInitHandler->handle($this->getPsrRequest()) ); } } diff --git a/app/controllers/enroll/lti/contents.php b/app/controllers/enroll/lti/contents.php index 4ccefd4..aef9cf1 100644 --- a/app/controllers/enroll/lti/contents.php +++ b/app/controllers/enroll/lti/contents.php @@ -1,9 +1,8 @@ have_perm('tutor')) { - $this->errors[] = _('Sie haben nicht die Berechtigung, diese Aktion auszuführen.'); + $this->errors = [ + _('Sie haben nicht die Berechtigung, diese Aktion auszuführen.') + ]; return; } diff --git a/app/controllers/enroll/lti/jwks.php b/app/controllers/enroll/lti/jwks.php index 9027a24..e0910a9 100644 --- a/app/controllers/enroll/lti/jwks.php +++ b/app/controllers/enroll/lti/jwks.php @@ -1,29 +1,21 @@ get(JwksRequestHandler::class); + $this->renderPsrResponse( - $this->jwksRequestHandler->handle($toolKeyring->getKeySetName()) + $jwksRequestHandler->handle($toolKeyring->getKeySetName()) ); } } diff --git a/app/controllers/enroll/lti/launch.php b/app/controllers/enroll/lti/launch.php index 3676eca..09dbff9 100644 --- a/app/controllers/enroll/lti/launch.php +++ b/app/controllers/enroll/lti/launch.php @@ -1,34 +1,27 @@ launchValidator->validatePlatformOriginatingLaunch($this->getPsrRequest()); + $launchValidator = app()->get(ToolLaunchValidatorInterface::class); + + $result = $launchValidator->validatePlatformOriginatingLaunch($this->getPsrRequest()); if ($result->hasError()) { throw new LtiException($result->getError()); diff --git a/app/controllers/enroll/lti/launch_deeplink.php b/app/controllers/enroll/lti/launch_deeplink.php index 607456f..b1735f7 100644 --- a/app/controllers/enroll/lti/launch_deeplink.php +++ b/app/controllers/enroll/lti/launch_deeplink.php @@ -1,46 +1,39 @@ launchValidator->validatePlatformOriginatingLaunch($this->getPsrRequest()); + $launchValidator = app()->get(ToolLaunchValidatorInterface::class); + + $result = $launchValidator->validatePlatformOriginatingLaunch($this->getPsrRequest()); - if ($request->hasError()) { - throw new LtiException($request->getError()); + if ($result->hasError()) { + throw new LtiException($result->getError()); } - $localRoles = RoleMapper::toLocal($request->getPayload()->getRoles()); + $localRoles = RoleMapper::toLocal($result->getPayload()->getRoles()); if(!in_array($localRoles['course'], ['dozent', 'tutor'])) { throw new AccessDeniedException(); } - $this->resolveDeeplinkProvisioningMode($request); + $this->resolveDeeplinkProvisioningMode($result); } public function callback_action(): void @@ -88,16 +81,16 @@ final class Enroll_Lti_LaunchDeeplinkController extends LtiBaseController $this->render_text($message->toHtmlRedirectForm()); } - private function resolveDeeplinkProvisioningMode(LaunchValidationResultInterface $request): void + private function resolveDeeplinkProvisioningMode(LaunchValidationResultInterface $result): void { - $userLocale = $request->getPayload()->getLaunchPresentation()?->getLocale(); + $userLocale = $result->getPayload()->getLaunchPresentation()?->getLocale(); $callbackId = Uuid::uuid4()->toString(); $_SESSION['callbacks'][$callbackId] = [ - 'user_identity' => $request->getPayload()->getUserIdentity(), - 'deployment_key' => $request->getPayload()->getDeploymentId(), - 'registration_id' => $request->getRegistration()->getIdentifier(), - 'settings_claim' => $request->getPayload()->getDeepLinkingSettings(), + 'user_identity' => $result->getPayload()->getUserIdentity(), + 'deployment_key' => $result->getPayload()->getDeploymentId(), + 'registration_id' => $result->getRegistration()->getIdentifier(), + 'settings_claim' => $result->getPayload()->getDeepLinkingSettings(), 'provisioning_mode' => UserProvisioningMode::ExistingAccountsOnly->value, 'context' => 'lti', 'action' => 'deeplink_callback', @@ -119,7 +112,7 @@ final class Enroll_Lti_LaunchDeeplinkController extends LtiBaseController return; } - $payload = $request->getPayload(); + $payload = $result->getPayload(); $userIdentityMapping = UserIdentityMapping::findOneBySQL( "context = :context AND external_email = :external_email AND external_user_id = :external_user_id AND registration_id = :registration_id", @@ -127,7 +120,7 @@ final class Enroll_Lti_LaunchDeeplinkController extends LtiBaseController 'context' => UserIdentityMappingContext::DeepLink->value, 'external_email' => $payload->getUserIdentity()->getEmail(), 'external_user_id' => $payload->getUserIdentity()->getIdentifier(), - 'registration_id' => $request->getRegistration()->getIdentifier() + 'registration_id' => $result->getRegistration()->getIdentifier() ] ); diff --git a/app/controllers/enroll/lti/provisioning_modes.php b/app/controllers/enroll/lti/provisioning_modes.php index 7dd54cc..e30ac63 100644 --- a/app/controllers/enroll/lti/provisioning_modes.php +++ b/app/controllers/enroll/lti/provisioning_modes.php @@ -1,13 +1,12 @@ new UpdateLineItemServiceServerRequestHandler($this->lineItemRepo), - 'DELETE' => new DeleteLineItemServiceServerRequestHandler($this->lineItemRepo), - 'GET' => new GetLineItemServiceServerRequestHandler($this->lineItemRepo), - default => throw new MethodNotAllowedException() - }; - - $this->renderAgsResponse($requestHandler); - - } catch (\Throwable $e) { - $requestBody = $this->getPsrRequest()->getBody()->getContents(); - - $response = new \Nyholm\Psr7\Response( - 500, - ['Content-Type' => 'application/json'], - json_encode([ - 'message' => $e->getMessage(), - 'request_body' => $requestBody, - ]) - ); - - $this->renderPsrResponse($response); - } - - $this->set_layout(null); + $requestHandler = match (Request::method()) { + 'PUT' => app()->get(UpdateLineItemServiceServerRequestHandler::class), + 'DELETE' => app()->get(DeleteLineItemServiceServerRequestHandler::class), + 'GET' => app()->get(GetLineItemServiceServerRequestHandler::class), + default => throw new MethodNotAllowedException() + }; + + $this->renderAgsResponse($requestHandler); } - public function results_action(): void + public function scores_action(): void { + $this->renderAgsResponse( + app()->get(ScoreServiceServerRequestHandler::class) + ); } - private function renderAgsResponse( - LtiServiceServerRequestHandlerInterface $requestHandler - ): void + public function results_action(): void { - $serviceServer = new LtiServiceServer($this->tokenValidator, $requestHandler); - - $this->renderPsrResponse( - $serviceServer->handle($this->getPsrRequest()) + $this->renderAgsResponse( + app()->get(ResultServiceServerRequestHandler::class) ); } } diff --git a/app/controllers/lti/1p3/ags/line_items.php b/app/controllers/lti/1p3/ags/line_items.php index 11d83de..e685557 100644 --- a/app/controllers/lti/1p3/ags/line_items.php +++ b/app/controllers/lti/1p3/ags/line_items.php @@ -1,41 +1,19 @@ new CreateLineItemServiceServerRequestHandler($this->lineItemRepo), - 'GET' => new ListLineItemsServiceServerRequestHandler($this->lineItemRepo), + 'POST' => app()->get(CreateLineItemServiceServerRequestHandler::class), + 'GET' => app()->get(ListLineItemsServiceServerRequestHandler::class), default => throw new MethodNotAllowedException() }; - $serviceServer = new LtiServiceServer($this->tokenValidator, $requestHandler); - - $this->renderPsrResponse( - $serviceServer->handle($this->getPsrRequest()) - ); + $this->renderAgsResponse($requestHandler); } - } diff --git a/app/controllers/lti/1p3/jwks.php b/app/controllers/lti/1p3/jwks.php index 9b78110..03c52f2 100644 --- a/app/controllers/lti/1p3/jwks.php +++ b/app/controllers/lti/1p3/jwks.php @@ -1,30 +1,18 @@ get(JwksRequestHandler::class); $this->renderPsrResponse( - $this->jwksRequestHandler->handle($platformKeyring->getKeySetName()) + $jwksRequestHandler->handle($platformKeyring->getKeySetName()) ); } } diff --git a/app/controllers/lti/1p3/login.php b/app/controllers/lti/1p3/login.php index c9d3e03..528732a 100644 --- a/app/controllers/lti/1p3/login.php +++ b/app/controllers/lti/1p3/login.php @@ -1,27 +1,16 @@ get(OidcAuthenticationRequestHandler::class); + $this->renderPsrResponse( - $this->oidcLoginHandler->handle($this->getPsrRequest()) + $oidcLoginHandler->handle($this->getPsrRequest()) ); } } diff --git a/app/controllers/lti/1p3/token.php b/app/controllers/lti/1p3/token.php index baea5db..2e0459f 100644 --- a/app/controllers/lti/1p3/token.php +++ b/app/controllers/lti/1p3/token.php @@ -1,68 +1,21 @@ getContent(); - $responseGenerator = new AccessTokenResponseGenerator( - new KeyManager(), - new AuthorizationServerFactory( - new ClientRepository(new RegistrationManager()), - new AccessTokenRepository(Factory::getCache()), - new ScopeRepository( - [ - new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/lineitem'), - new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/lineitem.readonly'), - new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/result.readonly'), - new ScopeEntity('https://purl.imsglobal.org/spec/lti-ags/scope/score') - ] - ), - $platformEncryptionKey - ) - ); - - $response = $responseGenerator->generate( - $this->getPsrRequest(), - $this->getPsrResponse(), - '1' - ); - - $this->renderPsrResponse($response); - } catch (\Throwable $e) { - $requestBody = $this->getPsrRequest()->getBody()->getContents(); - - $response = new \Nyholm\Psr7\Response( - 500, - ['Content-Type' => 'application/json'], - json_encode([ - 'message' => $e->getMessage(), - 'request_body' => $requestBody, - ]) - ); + $tokenGenerator = app()->get(AccessTokenResponseGeneratorInterface::class); - $this->renderPsrResponse($response); - } + $response = $tokenGenerator->generate( + $this->getPsrRequest(), + $this->getPsrResponse(), + PlatformManager::getKeyChain()->getIdentifier() + ); - $this->set_layout(null); + $this->renderPsrResponse($response); } } diff --git a/db/migrations/6.3.1_step_5405.php b/db/migrations/6.3.1_step_5405.php index 221800c..c414a44 100644 --- a/db/migrations/6.3.1_step_5405.php +++ b/db/migrations/6.3.1_step_5405.php @@ -155,7 +155,7 @@ final class Step5405 extends Migration { CREATE TABLE IF NOT EXISTS `lti_configs` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT, `configurable_id` INT UNSIGNED NOT NULL, - `configurable_type` ENUM('registration','publication', 'resource_link') NOT NULL DEFAULT 'registration', + `configurable_type` VARCHAR(255) NOT NULL, `name` VARCHAR(100) NOT NULL, `value` TEXT NOT NULL, `mkdate` INT UNSIGNED DEFAULT NULL, diff --git a/lib/activities/ActivityObserver.php b/lib/activities/ActivityObserver.php index 49516ac..3ee0c61 100644 --- a/lib/activities/ActivityObserver.php +++ b/lib/activities/ActivityObserver.php @@ -39,7 +39,7 @@ class ActivityObserver //Notifications for ScheduleProvider (Course) \NotificationCenter::addObserver('\Studip\Activity\ScheduleProvider', 'postActivity','CourseDidChangeSchedule'); - \NotificationCenter::addObserver('\Studip\Activity\LtiCallbackProcessor', 'handle','Authenticated'); + \NotificationCenter::addObserver('\Studip\Lti\Observer\EnrollCallback', 'handle','Authenticated'); // Notifications for CoursewareProvider diff --git a/lib/activities/LtiCallbackProcessor.php b/lib/activities/LtiCallbackProcessor.php deleted file mode 100644 index b370196..0000000 --- a/lib/activities/LtiCallbackProcessor.php +++ /dev/null @@ -1,53 +0,0 @@ -setUser($user) - ->setUserIdentity($ltiCallbackData['user_identity']); - - switch ($ltiCallbackData['action']) { - case 'enroll_user': - $publication = Publication::find($ltiCallbackData['publication_id']); - - $userManager->enroll($publication, $ltiCallbackData['local_roles'], $ltiCallbackData['registration_id']); - break; - case 'deeplink_callback': - $userManager - ->setRegistrationId($ltiCallbackData['registration_id']) - ->syncUserIdentityMapping(UserIdentityMappingContext::DeepLink->value); - break; - } - } - - private static function isValidEventData(array $eventData): bool - { - if (empty($_SESSION['callbacks'][$eventData['callback_id']])) { - return false; - } - - $ltiCallbackData = $_SESSION['callbacks'][$eventData['callback_id']]; - if ( - $ltiCallbackData['context'] !== 'lti' - || $ltiCallbackData['expires_at'] < time() - ) { - return false; - } - - return true; - } -} diff --git a/lib/classes/Lti/Controller/AdminBaseController.php b/lib/classes/Lti/Controller/AdminBaseController.php new file mode 100644 index 0000000..3cd0fce --- /dev/null +++ b/lib/classes/Lti/Controller/AdminBaseController.php @@ -0,0 +1,100 @@ +range_id = Context::getId(); + $this->isModerator = LtiToolModule::isModerator($this->range_id); + + if (!$this->isModerator) { + throw new AccessDeniedException(); + } + + $this->isToolSharingEnabled = LtiToolModule::isToolSharingEnabled(); + } + + protected function buildRegistrationsSidebar(): void + { + // views: + $viewWidget = new ViewsWidget(); + + $viewWidget->addLink( + _('LTI-Tools'), + $this->url_for('admin/lti/registrations', ['role' => 'tool']), + )->setActive($this->ltiRole !== 'platform'); + + if ($this->isToolSharingEnabled) { + $viewWidget->addLink( + _('LTI-Platforms'), + $this->url_for('admin/lti/registrations', ['role' => 'platform']) + )->setActive($this->ltiRole === 'platform'); + } + + Sidebar::Get()->addWidget($viewWidget); + + // actions: + $actions = new ActionsWidget(); + $actions->addLink( + _('Neues LTI-Tool registrieren'), + $this->url_for('admin/lti/registrations/create', ['role' => 'tool']), + Icon::create('add') + )->asDialog('width=900;height=700'); + + if ($this->isToolSharingEnabled) { + $actions->addLink( + _('Neues LTI-Platform registrieren'), + $this->url_for('admin/lti/registrations/create', ['role' => 'platform']), + Icon::create('add') + )->asDialog('width=900;height=700'); + } + + $actions->addLink( + _('Daten zur LTI-Plattform anzeigen'), + $this->url_for('admin/lti/registrations/platform_data'), + Icon::create('info') + )->asDialog('width=900;height=700'); + + if ($this->isToolSharingEnabled) { + $actions->addLink( + _('Daten zur LTI-Tool anzeigen'), + $this->url_for('admin/lti/registrations/tool_data'), + Icon::create('info') + )->asDialog('width=900;height=700'); + } + + Sidebar::get()->addWidget($actions); + } + + protected function buildPublicationsSidebar(): void + { + if($this->isToolSharingEnabled) { + // actions: + $actions = new ActionsWidget(); + $actions->addLink( + _('Neue Veröffentlichung anlegen'), + $this->url_for('admin/lti/publications/create'), + Icon::create('add') + )->asDialog('width=700'); + + Sidebar::get()->addWidget($actions); + } + } + +} diff --git a/lib/classes/Lti/Controller/AgsBaseController.php b/lib/classes/Lti/Controller/AgsBaseController.php new file mode 100644 index 0000000..37a126d --- /dev/null +++ b/lib/classes/Lti/Controller/AgsBaseController.php @@ -0,0 +1,30 @@ +get(RequestAccessTokenValidatorInterface::class), + $requestHandler + ); + + $this->renderPsrResponse( + $serviceServer->handle($this->getPsrRequest()) + ); + } +} diff --git a/lib/classes/Lti/Controller/EnrollBaseController.php b/lib/classes/Lti/Controller/EnrollBaseController.php new file mode 100644 index 0000000..0403daf --- /dev/null +++ b/lib/classes/Lti/Controller/EnrollBaseController.php @@ -0,0 +1,56 @@ +preferred_language = 'en_GB'; + $user->store(); + } + + return $this; + } + + protected function validateCallbackData(string $callbackId): array + { + if (empty($_SESSION['callbacks'][$callbackId])) { + throw new AccessDeniedException('Missing or invalid callback ID'); + } + + $callbackData = $_SESSION['callbacks'][$callbackId]; + if ( + $callbackData['context'] !== 'lti' + || $callbackData['expires_at'] < time() + ) { + throw new AccessDeniedException('Invalid or expired callback data'); + } + + return $callbackData; + } +} diff --git a/lib/classes/Lti/Controller/PlatformBaseController.php b/lib/classes/Lti/Controller/PlatformBaseController.php new file mode 100644 index 0000000..73e1709 --- /dev/null +++ b/lib/classes/Lti/Controller/PlatformBaseController.php @@ -0,0 +1,13 @@ +value => [ - 'value' => self::Active->value, - 'label' => _('Aktiv') - ], - self::Inactive->value => [ - 'value' => self::Inactive->value, - 'label' => _('Inaktiv') - ] - ]; - } - - public static function get(string $value): array - { - return static::all()[$value] ?? static::default(); - } - - public static function fromBoolean(bool $value): string - { - return $value ? self::Active->value : self::Inactive->value; - } - - public static function default(): array - { - return [ - 'value' => self::Inactive->value, - 'label' => _('Inaktiv') - ]; - } - -} diff --git a/lib/classes/Lti/Enum/UserIdentityMappingContext.php b/lib/classes/Lti/Enum/UserIdentityMappingContext.php index ac0d81b..e799c51 100644 --- a/lib/classes/Lti/Enum/UserIdentityMappingContext.php +++ b/lib/classes/Lti/Enum/UserIdentityMappingContext.php @@ -1,7 +1,8 @@ getScopeRepository(), + PlatformManager::getPrivateKey()->getContent() + ) + ); + } + + private function getScopeRepository(): ScopeRepository + { + $scopeEntities = array_map( + static fn(string $scope) => new ScopeEntity($scope), + self::SCOPES + ); + + return new ScopeRepository($scopeEntities); + + } +} diff --git a/lib/classes/Lti/LTI1p3/Helper.php b/lib/classes/Lti/LTI1p3/Helper.php new file mode 100644 index 0000000..69758c5 --- /dev/null +++ b/lib/classes/Lti/LTI1p3/Helper.php @@ -0,0 +1,17 @@ + $query['resource_link_id'] ?? null, + 'line_item_id' => $query['line_item_id'] ?? null + ]; + } + +} diff --git a/lib/classes/Lti/LTI1p3/Identity.php b/lib/classes/Lti/LTI1p3/Identity.php deleted file mode 100644 index e4e0826..0000000 --- a/lib/classes/Lti/LTI1p3/Identity.php +++ /dev/null @@ -1,98 +0,0 @@ - $registration->getIdentifier(), - 'user_id' => $user->id - ] - ); - - if ($privacySettings) { - $this->optionalFields = explode(',', $privacySettings->allowed_optional_fields); - } - } - - public function getIdentifier(): string - { - return $this->user->username; - } - - public function getName(): ?string - { - return $this->user->getFullName(); - } - - public function getEmail(): ?string - { - return $this->user->email; - } - - public function getGivenName(): ?string - { - return $this->user->vorname; - } - - public function getFamilyName(): ?string - { - return $this->user->nachname; - } - - public function getMiddleName(): ?string - { - return null; - } - - public function getLocale(): ?string - { - if (!in_array('lang', $this->optionalFields)) { - return ''; - } - return explode('_', $this->user->preferred_language)[0]; - } - - public function getPicture(): ?string - { - if (!in_array('avatar_url', $this->optionalFields)) { - return ''; - } - return Avatar::getAvatar($this->user->id)->getURL(Avatar::MEDIUM); - } - - public function getAdditionalProperties(): CollectionInterface - { - return new Collection(); - } - - public function normalize(): array - { - return [ - MessagePayloadInterface::CLAIM_SUB => $this->getIdentifier(), - MessagePayloadInterface::CLAIM_USER_NAME => $this->getName(), - MessagePayloadInterface::CLAIM_USER_EMAIL => $this->getEmail(), - MessagePayloadInterface::CLAIM_USER_GIVEN_NAME => $this->getGivenName(), - MessagePayloadInterface::CLAIM_USER_FAMILY_NAME => $this->getFamilyName(), - MessagePayloadInterface::CLAIM_USER_MIDDLE_NAME => $this->getMiddleName(), - MessagePayloadInterface::CLAIM_USER_LOCALE => $this->getLocale(), - MessagePayloadInterface::CLAIM_USER_PICTURE => $this->getPicture() - ]; - } -} diff --git a/lib/classes/Lti/LTI1p3/KeyChainFactory.php b/lib/classes/Lti/LTI1p3/KeyChainFactory.php deleted file mode 100644 index 9294a2d..0000000 --- a/lib/classes/Lti/LTI1p3/KeyChainFactory.php +++ /dev/null @@ -1,43 +0,0 @@ -toKeyChain(); - } - - $keyring = Keyring::findOneBySQL('range_id = :id', ['id' => $identifier]); - if (!$keyring) { - throw new KeyringException( - 'Keyring not found.', - KeyringException::NOT_FOUND - ); - } - - return $keyring->toKeyChain(); - } -} diff --git a/lib/classes/Lti/LTI1p3/KeyChainRepository.php b/lib/classes/Lti/LTI1p3/KeyChainRepository.php new file mode 100644 index 0000000..7aeedfd --- /dev/null +++ b/lib/classes/Lti/LTI1p3/KeyChainRepository.php @@ -0,0 +1,14 @@ +toKeyChain(); - } - - public function findByKeySetName(string $keySetName): array - { - $keyring = Keyring::findOneByRange_id($keySetName); - if ($keyring) { - return [$keyring->toKeyChain()]; - } - - return []; - } -} diff --git a/lib/classes/Lti/LTI1p3/LineItemRepository.php b/lib/classes/Lti/LTI1p3/LineItemRepository.php index 99c17e7..4efb1fb 100644 --- a/lib/classes/Lti/LTI1p3/LineItemRepository.php +++ b/lib/classes/Lti/LTI1p3/LineItemRepository.php @@ -11,63 +11,11 @@ use OAT\Library\Lti1p3Ags\Model\LineItem\LineItemCollectionInterface; final class LineItemRepository implements LineItemRepositoryInterface { - public static function getGradingToolName(string $toolId, string $deploymentId): string - { - return sprintf('lti-%s-%s', $toolId, $deploymentId); - } - - public static function getSearchParametersFromLineItemIdentifier(string $lineItemIdentifier): array - { - //$lineItemIdentifier contains the full URL to the line item. - //We must extract the course-ID, tool-ID and deployment-ID - //from the URL parameters first, before searching a grading definition. - $urlParts = parse_url($lineItemIdentifier); - $parameters = []; - if (empty($urlParts['query'])) { - //Nothing we can convert. - return []; - } - parse_str($urlParts['query'], $parameters); - if (empty($parameters)) { - //Same as above. - return []; - } - - $searchParameters = [ - 'course_id' => $parameters['cid'], - 'tool' => self::getGradingToolName($parameters['tool_id'], $parameters['deployment_id']) - ]; - if (!empty($parameters['definition_id'])) { - $searchParameters['definition_id'] = $parameters['definition_id']; - } - - return $searchParameters; - } - public function find(string $lineItemIdentifier): ?LineItemInterface { - $searchParameters = self::getSearchParametersFromLineItemIdentifier($lineItemIdentifier); - if (!$searchParameters) { - //Nothing we can search for. - return null; - } + $parameters = Helper::parseLineItemIdentifier($lineItemIdentifier); - $definition = null; - if (!empty($searchParameters['definition_id'])) { - $definition = Definition::find($searchParameters['definition_id']); - } else { - $definition = Definition::findOneBySQL( - "`course_id` = :course_id AND `tool` = :tool", - [ - 'course_id' => $searchParameters['course_id'], - 'tool' => $searchParameters['tool'] - ] - ); - } - if ($definition) { - return $definition->toLti1p3LineItem(); - } - return null; + return Definition::find($parameters['line_item_id'])?->toLti1p3LineItem(); } public function findCollection( @@ -79,43 +27,31 @@ final class LineItemRepository implements LineItemRepositoryInterface ): LineItemCollectionInterface { $result = new LineItemCollection(); + if (!$resourceLinkIdentifier) { - //Nothing we can search for. return $result; } - //Find the LTI resource link by its ID: - $resourceLink = ResourceLink::find($resourceLinkIdentifier); - if (!$resourceLink) { - throw new LTIException('Invalid resource link identifier.'); - } - $toolId = $resourceLink->deployment->tool_id ?? null; + $sqlQuery = 'id = :id OR tool = :tool_id ORDER BY mkdate DESC'; + $sqlParams = [ + 'id' => $resourceIdentifier, + 'tool_id' => $resourceLinkIdentifier + ]; - $sqlQuery = ['', []]; - if ($toolId && $resourceLink->course_id) { - $sqlQuery[0] .= "`tool` = :tool AND `course_id` = :course_id"; - $sqlQuery[1]['tool'] = self::getGradingToolName($toolId, $resourceLink->deployment_id); - $sqlQuery[1]['course_id'] = $resourceLink->course_id; - } else { - //No tool-ID means no line item collection can be found. - return $result; + if ($limit !== null) { + $sqlQuery .= ' LIMIT :limit'; + $sqlParams['limit'] = $limit; } - if ($limit) { - if (empty($sqlQuery[0])) { - $sqlQuery[0] .= "TRUE "; - } - $sqlQuery[0] .= "LIMIT :limit "; - $sqlQuery[1]['limit'] = $limit; - } - if ($offset) { - $sqlQuery[0] .= "OFFSET :offset"; - $sqlQuery[1]['offset'] = $offset; + if ($offset !== null) { + $sqlQuery .= ' OFFSET :offset'; + $sqlParams['offset'] = $offset; } - $definitions = Definition::findBySql(...$sqlQuery); - foreach ($definitions as $definition) { - $result->add($definition->toLti1p3LineItem()); + $gradeDefinitions = Definition::findBySql($sqlQuery, $sqlParams); + + foreach ($gradeDefinitions as $gradeDefinition) { + $result->add($gradeDefinition->toLti1p3LineItem()); } return $result; } @@ -123,28 +59,35 @@ final class LineItemRepository implements LineItemRepositoryInterface public function save(LineItemInterface $lineItem): LineItemInterface { $resourceLink = ResourceLink::find($lineItem->getResourceLinkIdentifier()); - if (!$resourceLink) { + if ($resourceLink === null) { throw new LTIException('Invalid resource link identifier.'); } - $definition = Definition::create([ - 'id' => $lineItem->getIdentifier(), - 'name' => $lineItem->getLabel(), - 'course_id' => $resourceLink->course_id, - 'tool' => $resourceLink->id, - 'weight' => '1.0' - ]); + // TODO:: normalize weight + $gradeDefinition = Definition::updateOrCreate( + [ + 'id' => $lineItem->getResourceIdentifier(), + 'tool' => $resourceLink->id, + 'course_id' => $resourceLink->course_id, + 'category' => 'LTI', + 'item' => ResourceLink::class + ], + [ + 'name' => $lineItem->getLabel(), + 'weight' => $lineItem->getScoreMaximum() / 100 + ] + ); - return $definition->toLti1p3LineItem(); + return $gradeDefinition->toLti1p3LineItem(); } public function delete(string $lineItemIdentifier): void { - $searchParameters = self::getSearchParametersFromLineItemIdentifier($lineItemIdentifier); + $parameters = Helper::parseLineItemIdentifier($lineItemIdentifier); Definition::deleteBySQL( - "`course_id` = :course_id AND `tool` = :tool", - $searchParameters + "id = ?", + [$parameters['line_item_id']] ); } } diff --git a/lib/classes/Lti/LTI1p3/PlatformManager.php b/lib/classes/Lti/LTI1p3/PlatformManager.php index 53e97d7..ea8caaa 100644 --- a/lib/classes/Lti/LTI1p3/PlatformManager.php +++ b/lib/classes/Lti/LTI1p3/PlatformManager.php @@ -3,9 +3,9 @@ namespace Studip\Lti\LTI1p3; use Config; use Keyring; -use OAT\Library\Lti1p3Core\Security\Key\KeyChain; use URLHelper; use OAT\Library\Lti1p3Core\Platform\Platform; +use OAT\Library\Lti1p3Core\Security\Key\KeyChain; use OAT\Library\Lti1p3Core\Security\Key\KeyInterface; use OAT\Library\Lti1p3Core\Platform\PlatformInterface; use OAT\Library\Lti1p3DeepLinking\Settings\DeepLinkingSettings; diff --git a/lib/classes/Lti/LTI1p3/RepositoryRegistry.php b/lib/classes/Lti/LTI1p3/RepositoryRegistry.php index 18abc4e..5058123 100644 --- a/lib/classes/Lti/LTI1p3/RepositoryRegistry.php +++ b/lib/classes/Lti/LTI1p3/RepositoryRegistry.php @@ -3,7 +3,8 @@ namespace Studip\Lti\LTI1p3; use DI; use OAT\Library\Lti1p3Core\Security\Nonce\NonceRepository; -use OAT\Library\Lti1p3Core\Security\Key\KeyChainRepository; +use OAT\Library\Lti1p3Ags\Repository\ScoreRepositoryInterface; +use OAT\Library\Lti1p3Ags\Repository\ResultRepositoryInterface; use OAT\Library\Lti1p3Ags\Repository\LineItemRepositoryInterface; use OAT\Library\Lti1p3Core\Security\Nonce\NonceRepositoryInterface; use OAT\Library\Lti1p3Core\Security\User\UserAuthenticatorInterface; @@ -14,6 +15,7 @@ use OAT\Library\Lti1p3Core\Security\OAuth2\Validator\RequestAccessTokenValidator use OAT\Library\Lti1p3Core\Message\Launch\Validator\Platform\PlatformLaunchValidator; use OAT\Library\Lti1p3Core\Message\Launch\Validator\Tool\ToolLaunchValidatorInterface; use OAT\Library\Lti1p3Core\Security\OAuth2\Validator\RequestAccessTokenValidatorInterface; +use OAT\Library\Lti1p3Core\Security\OAuth2\Generator\AccessTokenResponseGeneratorInterface; use OAT\Library\Lti1p3Core\Message\Launch\Validator\Platform\PlatformLaunchValidatorInterface; final class RepositoryRegistry @@ -27,13 +29,11 @@ final class RepositoryRegistry ToolLaunchValidatorInterface::class => DI\get(ToolLaunchValidator::class), UserAuthenticatorInterface::class => DI\get(UserAuthenticator::class), LineItemRepositoryInterface::class => DI\get(LineItemRepository::class), + ScoreRepositoryInterface::class => DI\get(ScoreRepository::class), + ResultRepositoryInterface::class => DI\get(ResultRepository::class), RequestAccessTokenValidatorInterface::class => DI\get(RequestAccessTokenValidator::class), - KeyChainRepositoryInterface::class => DI\factory(function() { - return new KeyChainRepository([ - PlatformManager::getKeyChain(), - ToolManager::getKeyChain() - ]); - }), + AccessTokenResponseGeneratorInterface::class => DI\get(AccessTokenResponseGenerator::class), + KeyChainRepositoryInterface::class => DI\get(KeyChainRepository::class) ]; } } diff --git a/lib/classes/Lti/LTI1p3/ResourceLinkRepository.php b/lib/classes/Lti/LTI1p3/ResourceLinkRepository.php index a188eac..4c93dda 100644 --- a/lib/classes/Lti/LTI1p3/ResourceLinkRepository.php +++ b/lib/classes/Lti/LTI1p3/ResourceLinkRepository.php @@ -3,6 +3,7 @@ namespace Studip\Lti\LTI1p3; use URLHelper; use Lti\ResourceLink; +use Grading\Definition; use Studip\Lti\Enum\GradeSynchronization; use OAT\Library\Lti1p3Core\Util\Collection\Collection; use OAT\Library\Lti1p3Core\Message\Payload\Claim\AgsClaim; @@ -129,8 +130,27 @@ final class ResourceLinkRepository implements LtiResourceLinkInterface public function getAgsClaim(): ?AgsClaim { - $lineItemsContainerUrl = URLHelper::getURL('dispatch.php/lti/1p3/ags/line_items', ['resource_link_id' => $this->resourceLink->id], true); - $lineItemURL = URLHelper::getURL('dispatch.php/lti/1p3/ags/line_item', ['resource_link_id' => $this->resourceLink->id], true); + $gradeDefinition = Definition::firstOrCreate( + [ + 'tool' => $this->resourceLink->id, + 'item' => ResourceLink::class, + 'course_id' => $this->resourceLink->course_id, + 'category' => 'LTI' + ], + [ + 'weight' => 1, + 'name' => $this->resourceLink->title + ] + ); + + $urlParams = [ + 'line_item_id' => $gradeDefinition->id, + 'resource_link_id' => $this->resourceLink->id + ]; + + $lineItemURL = URLHelper::getURL('dispatch.php/lti/1p3/ags/line_item', $urlParams, true); + $lineItemsContainerUrl = URLHelper::getURL('dispatch.php/lti/1p3/ags/line_items', $urlParams, true); + $gradeSynchronization = (int) $this->resourceLink->getConfigValues()['grade_synchronization']; return match ($gradeSynchronization) { diff --git a/lib/classes/Lti/LTI1p3/ResultRepository.php b/lib/classes/Lti/LTI1p3/ResultRepository.php index 2389132..ed5b6cc 100644 --- a/lib/classes/Lti/LTI1p3/ResultRepository.php +++ b/lib/classes/Lti/LTI1p3/ResultRepository.php @@ -15,29 +15,24 @@ final class ResultRepository implements ResultRepositoryInterface ?int $offset = null ): ResultCollectionInterface { - $sqlParams = LineItemRepository::getSearchParametersFromLineItemIdentifier($lineItemIdentifier); - if (!$sqlParams) { - //Nothing we can search for: - return new ResultCollection(); - } - $sql = 'JOIN `grading_definitions` gd - ON (`definition_id` = gd.`id`) - WHERE gd.`course_id` = :course_id - AND gd.`tool` = :tool'; - if ($limit) { - $sql .= 'LIMIT :limit '; + [$sqlQuery, $sqlParams] = $this->resolveBaseSqlQuery($lineItemIdentifier); + + if ($limit !== null) { + $sqlQuery .= ' LIMIT :limit'; $sqlParams['limit'] = $limit; } - if ($offset) { - $sql .= 'OFFSET :offset '; + if ($offset !== null) { + $sqlQuery .= ' OFFSET :offset'; $sqlParams['offset'] = $offset; } - $grades = Instance::findBySQL($sql, $sqlParams); + $gradeInstances = Instance::findBySQL($sqlQuery, $sqlParams); + $results = new ResultCollection(); - foreach ($grades as $grade) { - $results->add($grade->toResult()); + foreach ($gradeInstances as $gradeInstance) { + $results->add($gradeInstance->toLti1p3Result()); } + return $results; } @@ -46,18 +41,26 @@ final class ResultRepository implements ResultRepositoryInterface string $userIdentifier ): ?ResultInterface { - $searchParameters = LineItemRepository::getSearchParametersFromLineItemIdentifier($lineItemIdentifier); - - return Instance::findOneBySQL( - 'JOIN `grading_definitions` gd - ON (`definition_id` = gd.`id`) - WHERE gd.`course_id` = :course_id - AND gd.`tool` = :tool - AND `user_id` = :user_id', + [$sqlQuery, $sqlParams] = $this->resolveBaseSqlQuery($lineItemIdentifier); + + $sqlQuery .= ' AND grading_instances.user_id = :user_id'; + $sqlParams['user_id'] = $userIdentifier; + + return Instance::findOneBySQL($sqlQuery, $sqlParams)?->toLti1p3Result(); + } + + private function resolveBaseSqlQuery(string $lineItemIdentifier): array + { + $parameters = Helper::parseLineItemIdentifier($lineItemIdentifier); + + return [ + 'JOIN grading_definitions ON grading_instances.definition_id = grading_definitions.id + WHERE grading_instances.definition_id = :definition_id + AND grading_definitions.tool = :tool_id ', [ - ...$searchParameters, - 'user_id' => $userIdentifier + 'definition_id' => $parameters['line_item_id'], + 'tool_id' => $parameters['resource_link_id'] ] - )?->toResult(); + ]; } } diff --git a/lib/classes/Lti/LTI1p3/ScoreRepository.php b/lib/classes/Lti/LTI1p3/ScoreRepository.php index 794860f..a347e6d 100644 --- a/lib/classes/Lti/LTI1p3/ScoreRepository.php +++ b/lib/classes/Lti/LTI1p3/ScoreRepository.php @@ -9,21 +9,24 @@ final class ScoreRepository implements ScoreRepositoryInterface { public function save(ScoreInterface $score): ScoreInterface { - $userId = $score->getUserIdentifier(); - $definitionId = $score->getLineItemIdentifier(); - - $grade = Instance::findOneBySQL( - '`definition_id` = :definition_id AND `user_id` = :user_id', - ['definition_id' => $definitionId, 'user_id' => $userId] + Instance::updateOrCreate( + [ + 'definition_id' => $score->getLineItemIdentifier(), + 'user_id' => $score->getUserIdentifier() + ], + [ + 'rawgrade' => $score->getScoreGiven() / 100, + 'feedback' => $score->getComment(), + 'passed' => ScoreRepository::isPassed($score->getGradingProgressStatus()), + 'chdate' => $score->getTimestamp()->getTimestamp() + ] ); - if (!$grade) { - $grade = new Instance(); - $grade->definition_id = $definitionId; - $grade->user_id = $userId; - } - $grade->rawgrade = $score->getScoreGiven(); - $grade->feedback = $score->getComment(); - $grade->store(); + return $score; } + + private static function isPassed(string $gradingProgressStatus): bool + { + return $gradingProgressStatus === ScoreInterface::GRADING_PROGRESS_STATUS_FULLY_GRADED; + } } diff --git a/lib/classes/Lti/LTI1p3/UserAuthenticator.php b/lib/classes/Lti/LTI1p3/UserAuthenticator.php index 5955291..6d57309 100644 --- a/lib/classes/Lti/LTI1p3/UserAuthenticator.php +++ b/lib/classes/Lti/LTI1p3/UserAuthenticator.php @@ -15,7 +15,7 @@ final class UserAuthenticator implements UserAuthenticatorInterface return new UserAuthenticationResult( $user !== null, - new Identity($user, $registration) + new UserIdentity($user, $registration) ); } } diff --git a/lib/classes/Lti/LTI1p3/UserIdentity.php b/lib/classes/Lti/LTI1p3/UserIdentity.php new file mode 100644 index 0000000..75eb7fa --- /dev/null +++ b/lib/classes/Lti/LTI1p3/UserIdentity.php @@ -0,0 +1,71 @@ +optionalFields = $this->loadOptionalFields(); + + parent::__construct( + $this->user->username, + $this->user->getFullName(), + $this->user->email, + $this->user->vorname, + $this->user->nachname, + null, + $this->getUserLocale(), + $this->getUserProfilePicture() + ); + } + + private function getUserLocale(): ?string + { + if (!$this->hasOptionalField('lang')) { + return null; + } + + return explode('_', $this->user->preferred_language)[0] ?? null; + } + + private function getUserProfilePicture(): ?string + { + if (!$this->hasOptionalField('avatar_url')) { + return null; + } + + return Avatar::getAvatar($this->user->id)->getURL(Avatar::MEDIUM); + } + + private function loadOptionalFields(): array + { + $privacySettings = RegistrationPrivacySettings::findOneBySQL( + "`registration_id` = :registration_id AND `user_id` = :user_id", + [ + 'registration_id' => $this->registration->getIdentifier(), + 'user_id' => $this->user->id, + ] + ); + + if (!$privacySettings || empty($privacySettings->allowed_optional_fields)) { + return []; + } + + return explode(',', $privacySettings->allowed_optional_fields); + } + + private function hasOptionalField(string $field): bool + { + return in_array($field, $this->optionalFields, true); + } +} diff --git a/lib/classes/Lti/Observer/EnrollCallback.php b/lib/classes/Lti/Observer/EnrollCallback.php new file mode 100644 index 0000000..3670438 --- /dev/null +++ b/lib/classes/Lti/Observer/EnrollCallback.php @@ -0,0 +1,53 @@ +setUser($user) + ->setUserIdentity($ltiCallbackData['user_identity']); + + switch ($ltiCallbackData['action']) { + case 'enroll_user': + $publication = Publication::find($ltiCallbackData['publication_id']); + + $userManager->enroll($publication, $ltiCallbackData['local_roles'], $ltiCallbackData['registration_id']); + break; + case 'deeplink_callback': + $userManager + ->setRegistrationId($ltiCallbackData['registration_id']) + ->syncUserIdentityMapping(UserIdentityMappingContext::DeepLink->value); + break; + } + } + + private static function isValidEventData(array $eventData): bool + { + if (empty($_SESSION['callbacks'][$eventData['callback_id']])) { + return false; + } + + $ltiCallbackData = $_SESSION['callbacks'][$eventData['callback_id']]; + if ( + $ltiCallbackData['context'] !== 'lti' + || $ltiCallbackData['expires_at'] < time() + ) { + return false; + } + + return true; + } +} diff --git a/lib/classes/Lti/Trait/StatusHelpers.php b/lib/classes/Lti/Trait/StatusHelpers.php new file mode 100644 index 0000000..d732963 --- /dev/null +++ b/lib/classes/Lti/Trait/StatusHelpers.php @@ -0,0 +1,39 @@ +value => [ + 'value' => self::Active->value, + 'label' => _('Aktiv') + ], + self::Inactive->value => [ + 'value' => self::Inactive->value, + 'label' => _('Inaktiv') + ] + ]; + } + + public static function get(string $value): array + { + return static::all()[$value] ?? static::default(); + } + + public static function fromBoolean(bool $value): string + { + return $value ? self::Active->value : self::Inactive->value; + } + + public static function default(): array + { + return [ + 'value' => self::Inactive->value, + 'label' => _('Inaktiv') + ]; + } + +} diff --git a/lib/models/Grading/Definition.php b/lib/models/Grading/Definition.php index 4b7d8b7..210296a 100644 --- a/lib/models/Grading/Definition.php +++ b/lib/models/Grading/Definition.php @@ -2,6 +2,7 @@ namespace Grading; +use DateTime; use Lti\ResourceLink; use OAT\Library\Lti1p3Ags\Model\LineItem\LineItem; use OAT\Library\Lti1p3Ags\Model\LineItem\LineItemInterface; @@ -73,27 +74,24 @@ class Definition extends \SimpleORMap public function toLti1p3LineItem(): LineItemInterface { - $resourceLinkIdentifier = $this->tool ?? ''; - $deploymentId = ''; - if ($resourceLinkIdentifier) { - $ltiResourceLink = ResourceLink::find($resourceLinkIdentifier); - if ($ltiResourceLink) { - $deploymentId = $ltiResourceLink->deployment_id; - } - } + $ltiResourceLink = ResourceLink::find($this->tool); - $identifier = URLHelper::getURL(sprintf( - 'dispatch.php/lti/1p3/ags/server/line_item/%1$s/%2$s', - $resourceLinkIdentifier, - $this->id - )); + $lineItemURL = URLHelper::getURL( + 'dispatch.php/lti/1p3/ags/line_item', + [ + 'line_item_id' => $this->id, + 'resource_link_id' => $ltiResourceLink->id + ], + true + ); return new LineItem( - PHP_FLOAT_MAX, + $this->weight * 100, $this->name, - $identifier, - $deploymentId, - $resourceLinkIdentifier + $lineItemURL, + $this->id, + $ltiResourceLink->id, + $this->course->getFullName() ); } } diff --git a/lib/models/Grading/Instance.php b/lib/models/Grading/Instance.php index e152544..9930ac0 100644 --- a/lib/models/Grading/Instance.php +++ b/lib/models/Grading/Instance.php @@ -88,15 +88,20 @@ class Instance extends \SimpleORMap return $this->content['rawgrade'] = number_format($grade, 5, '.', ''); } - public function toResult() : Result + public function toLti1p3Result(): Result { + $gradeDefinition = $this->definition; + return new Result( $this->user_id, - $this->definition_id, - $this->user_id . '_' . $this->definition_id, - $this->rawgrade, - 9.99999, //see above - $this->feedback + $gradeDefinition->id, + $this->definition_id. ':' .$this->user_id, + $this->rawgrade * 100, + $gradeDefinition->weight * 100, + $this->feedback, + [ + 'isPassed' => (bool) $this->passed + ] ); } } -- cgit v1.0