diff options
| author | Rasmus Fuhse <fuhse@data-quest.de> | 2025-07-04 11:26:38 +0000 |
|---|---|---|
| committer | Rasmus Fuhse <fuhse@data-quest.de> | 2025-07-04 11:26:38 +0000 |
| commit | f1613a24917bf32de90f042801d72b9fc18e43a2 (patch) | |
| tree | 8d298c4deb1b12e3a5104b311a144d21a57beb60 | |
| parent | 64a29f7621f459a444835ea85be204f7d12ed302 (diff) | |
course/lti/process_select_link: redirect on error with deployments, fixes #5488cherry-pick-d98e8f81
Closes #5488
Merge request studip/studip!4183
(cherry picked from commit d98e8f811ee969fc94f922a70ad270ff02d057fc)
6e3310be course/lti/process_select_link: redirect on error with deployments
b1f72a9f began rewriting course/lti/select_link and course/lti/process_select_link actions
74f13cb3 use LtiResourceLink instead of LtiDeployment
8c9e7707 course/lti/process_select_link: made lti resource link ID mandatory
5ee5de11 Revert "course/lti/process_select_link: made lti resource link ID mandatory"
c26e2605 Revert "use LtiResourceLink instead of LtiDeployment"
d466a82c Revert "began rewriting course/lti/select_link and course/lti/process_select_link actions"
ec9ebdd9 course/lti/process_select_link action: create LtiResourceLink objects
bb8aadb6 fixed typos
7c399153 fixed errors
c3d046b3 added redirects
6be413d1 Revert "added redirects"
3d4462f7 Revert "fixed errors"
adb61667 Revert "fixed typos"
fcd16406 Revert "course/lti/process_select_link action: create LtiResourceLink objects"
1f34f17d began rewriting course/lti/select_link and course/lti/process_select_link actions
5411e842 use LtiResourceLink instead of LtiDeployment
7d3b22fb course/lti/process_select_link: made lti resource link ID mandatory
a92bec66 added migration
6f1d77ec changed code
a08519ee changed more code, fixed errors
ecfab68d extended migration
4b441ca9 extended migration
18f6a0bb added columns to lti_deployments table, rewrote code for new database structure
40776ead replaced attribute access of LtiDeployment with LtiResourceLink
1150e772 continued moving attribute access
28ac0808 fixed course/lti/select_link action
a9189788 fixed lti/tool/index for tools in courses
14e29ec7 fixed errors
d1cf3aae fixed errors, added warning for more than one general LTI deployment per tool
46f0ea9a added deep link count to admin/lti/index
dc8058f3 removed unused code
d6d1491d fixed errors
a7f34412 removed debug code
944782dd fixed errors
7ee7716d fixed more errors
35563dc9 set course-ID as URL parameter for deep linking return URL
a1af83e5 fixed error
f2bd12cc course/lti/save_link: attempted to fix "no registration platform side" error
041d65de fixed error
d77844e0 fixed errors
581630b5 added debug code, allow setting link in registration manager
c9174799 removed debug code
b39ddf23 test
1f99d252 removed extra claim
75a25a15 made LTI requests have the same registration
680e9945 Prioritize JWKS URL over static key chain
46f3f6f4 Return LTi exception on public key failure
9d97dbbf Make public key unsetable
Co-authored-by: Moritz Strohm <strohm@data-quest.de>
22 files changed, 453 insertions, 287 deletions
diff --git a/app/controllers/course/lti.php b/app/controllers/course/lti.php index d167a9e..4658589 100644 --- a/app/controllers/course/lti.php +++ b/app/controllers/course/lti.php @@ -13,6 +13,7 @@ use OAT\Library\Lti1p3DeepLinking\Message\Launch\Builder\DeepLinkingLaunchReques use Studip\LTI13a\PlatformManager; use Studip\LTI13a\Registration; use Studip\LTI13a\RegistrationManager; +use OAT\Library\Lti1p3Core\Message\Payload\MessagePayloadInterface\MessagePayloadInterface; /** * course/lti.php - LTI consumer API for Stud.IP @@ -119,14 +120,23 @@ class Course_LtiController extends StudipController } Helpbar::get()->addPlainText('', _('Auf dieser Seite können Sie externe Anwendungen einbinden, sofern diese den LTI-Standard (Version 1.x) unterstützen.')); - if (Request::get('deployment_id')) { + + //Check for error messages: + if (Request::get('deployment_id') && (Request::submitted('lti_msg') || Request::submitted('lti_errormsg'))) { $deployment = LtiDeployment::find(Request::get('deployment_id')); if ($deployment) { - if (Request::get('lti_msg')) { - PageLayout::postInfo(htmlReady($deployment->title . ': ' . Request::get('lti_msg'))); - } - if (Request::get('lti_errormsg')) { - PageLayout::postError(htmlReady($deployment->title . ': ' . Request::get('lti_errormsg'))); + //Get the resource link for the deployment and display the messages: + $link = \LtiResourceLink::findOneBySQL( + "`deployment_id` = :deployment_id AND `course_id` = :course_id", + ['deployment_id' => $deployment->id, 'course_id' => $this->course_id] + ); + if ($link) { + if (Request::get('lti_msg')) { + PageLayout::postInfo(htmlReady($link->title . ': ' . Request::get('lti_msg'))); + } + if (Request::get('lti_errormsg')) { + PageLayout::postError(htmlReady($link->title . ': ' . Request::get('lti_errormsg'))); + } } } } @@ -138,7 +148,11 @@ class Course_LtiController extends StudipController $this->global_tool_deployments = LtiDeployment::findBySQL( "JOIN `lti_tools` ON `lti_deployments`.`tool_id` = `lti_tools`.`id` - WHERE `lti_tools`.`lti_version` = '1.3a' AND `lti_tools`.`range_id` = 'global' ORDER BY `lti_tools`.`name` ASC" + WHERE + `lti_deployments`.`purpose` = 'general' + AND `lti_tools`.`lti_version` = '1.3a' + AND `lti_tools`.`range_id` = 'global' + ORDER BY `lti_tools`.`name` ASC" ); if (!$this->global_tool_deployments) { @@ -267,11 +281,15 @@ class Course_LtiController extends StudipController //LTI 1.3a $this->lti13a_mode = true; - $return_url = !isset($this->resource_link->deployment->options['document_target']) ? - URLHelper::getURL($GLOBALS['ABSOLUTE_URI_STUDIP'] . 'dispatch.php/course/lti', ['deployment_id' => $this->resource_link->deployment_id]) : ''; - $document_target = isset($this->resource_link->deployment->options['document_target']) ? 'iframe' : 'window'; + $return_url = URLHelper::getURL($GLOBALS['ABSOLUTE_URI_STUDIP'] . 'dispatch.php/course/lti', ['deployment_id' => $this->resource_link->deployment_id]); + $document_target = 'window'; + if (!empty($this->resource_link->options['document_target'])) { + $return_url = $this->resource_link->options['document_target']; + $document_target = 'iframe'; + } + $locale = str_replace('_', '-', $_SESSION['_language']); - $registration = new Registration($this->resource_link->deployment->tool); + $registration = new Registration($this->resource_link->deployment->tool, $this->resource_link); $builder = new LtiResourceLinkLaunchRequestBuilder(); //The AGS URLs need several parameters: @@ -316,7 +334,7 @@ class Course_LtiController extends StudipController $this->url_for('lti/ags/line_item', $ags_url_parameters) ) ], - $this->resource_link->deployment->getCustomLtiParameterArray(), + $this->resource_link->getCustomLtiParameterArray(), ) ); } else { @@ -420,7 +438,7 @@ class Course_LtiController extends StudipController } /** - * Select a tool for adding a block via ContentItemSelectionRequest. + * Offers tool selection for LTI deep linking. */ public function add_link_action() { @@ -434,67 +452,69 @@ class Course_LtiController extends StudipController } /** - * Dispatch a ContentItemSelectionRequest to a specified LTI tool. + * Prepares the tool selected in the add_link action for being included in the course + * and displays the platform configuration that must be added in the LTI tool. */ - public function select_link_action($deployment_id = '') + public function select_link_action() { - $this->deployment = null; - if ($deployment_id) { - $this->deployment = LtiDeployment::find($deployment_id); - if (!$this->deployment) { - PageLayout::postError(_('Die Einbindung des LTI-Tools wurde nicht gefunden!')); - return; - } - if ($this->deployment->course_id !== $this->course_id) { - PageLayout::postError(_('Die Einbindung des LTI-Tools ist nicht für diese Veranstaltung bestimmt.')); - return; - } - if (empty($this->deployment->options['unfinished_deep_linking'])) { - PageLayout::postError(_('Die Einbindung des LTI-Tools ist bereits abgeschlossen.')); - return; - } - } - $this->tool = LtiTool::find(Request::int('tool_id')); if (!$this->tool) { PageLayout::postError(_('Das ausgewählte LTI-Tool wurde nicht gefunden.')); - $this->redirect('course/lti/add_link'); + $this->relocate('course/lti/add_link'); return; } if (!$this->tool->deep_linking) { PageLayout::postError(_('Das ausgewählte LTI-Tool unterstützt kein Deep Linking.')); - $this->redirect('course/lti/add_link'); + $this->relocate('course/lti/add_link'); return; } + + //Create a deployment for deep linking: + $this->deployment = new LtiDeployment(); + $this->deployment->tool_id = $this->tool->id; + $this->deployment->purpose = 'deep_linking'; + if ($this->deployment->store()) { + //Create an LTI resource link for the course: + $this->link = new \LtiResourceLink(); + $this->link->deployment_id = $this->deployment->id; + $this->link->course_id = $this->course_id; + $this->link->options = ['unfinished_deep_linking' => 'true']; + if (!$this->link->store()) { + PageLayout::postError(_('Die Einbindung des LTI-Tools in die Veranstaltung ist fehlgeschlagen.')); + $this->relocate('course/lti/add_link'); + } + } else { + PageLayout::postError(_('Es konnte kein LTI-Deployment für LTI Deep Linking erstellt werden.')); + $this->relocate('course/lti/add_link'); + } } - public function process_select_link_action($deployment_id = '') + /** + * Proceeds after the select_link action by switching to the LTI tool for + * selecting the items from the deep linked tool that shall be available in the Stud.IP course. + */ + public function process_select_link_action($link_id = '') { CSRFProtection::verifyUnsafeRequest(); - $this->deployment = null; - if ($deployment_id) { - $this->deployment = LtiDeployment::find($deployment_id); - if (!$this->deployment) { - PageLayout::postError(_('Die Einbindung des LTI-Tools wurde nicht gefunden!')); - return; - } - if ($this->deployment->course_id !== $this->course_id) { - PageLayout::postError(_('Die Einbindung des LTI-Tools ist nicht für diese Veranstaltung bestimmt.')); - return; - } - if (empty($this->deployment->options['unfinished_deep_linking'])) { - PageLayout::postError(_('Die Einbindung des LTI-Tools ist bereits abgeschlossen.')); - return; - } + $this->link = \LtiResourceLink::find($link_id); + if (!$this->link) { + PageLayout::postError(_('Die Einbindung des LTI-Tools wurde nicht gefunden.')); + $this->relocate('course/lti/add_link'); + return; } - - $this->tool = null; - if ($this->deployment) { - $this->tool = $this->deployment->tool; - } else { - $this->tool = LtiTool::find(Request::int('tool_id')); + if ($this->link->course_id !== $this->course_id) { + PageLayout::postError(_('Die Einbindung des LTI-Tools ist nicht für diese Veranstaltung bestimmt.')); + $this->relocate('course/lti/add_link'); + return; + } + if (empty($this->link->options['unfinished_deep_linking'])) { + PageLayout::postError(_('Die Einbindung des LTI-Tools ist bereits abgeschlossen.')); + $this->relocate('course/lti/add_link'); + return; } + + $this->tool = $this->link->deployment->tool ?? null; if (!$this->tool) { PageLayout::postError(_('Das ausgewählte LTI-Tool wurde nicht gefunden.')); $this->redirect('course/lti/add_link'); @@ -508,37 +528,20 @@ class Course_LtiController extends StudipController if ($this->tool->lti_version === '1.3a') { //LTI 1.3a - if ($this->deployment) { - $builder = new DeepLinkingLaunchRequestBuilder(); - $message = $builder->buildDeepLinkingLaunchRequest( - PlatformManager::getDeepLinkingConfiguration($this->tool->id), - new Registration($this->deployment->tool), - $GLOBALS['user']->id, - null, - $this->deployment->id, - [PlatformManager::getLtiRoleClaimForStudipRole($GLOBALS['perm']->get_studip_perm($this->course_id))] - ); - $this->render_text($message->toHtmlRedirectForm()); - } else { - //Build an LTI deployment object and mark it as not configured - //so that it can be displayed differently in the UI. - $this->deployment = new LtiDeployment(); - $this->deployment->tool_id = $this->tool->id; - $this->deployment->course_id = $this->course_id; - $this->deployment->title = $this->tool->name; - $this->deployment->options = ['unfinished_deep_linking' => true]; - if ($this->deployment->store() !== false) { - //Display the tool deployment data so that the user can enter - //them in the LTI tool. - PageLayout::postInfo( - _('Bitte tragen Sie die Daten zur Einbindung im LTI-Tool ein bevor sie fortfahren.') - ); - } - } + $builder = new DeepLinkingLaunchRequestBuilder(); + $message = $builder->buildDeepLinkingLaunchRequest( + PlatformManager::getDeepLinkingConfiguration($this->link->id, $this->course_id), + new Registration($this->tool, $this->link), + $GLOBALS['user']->id, + null, + $this->link->deployment_id, + [PlatformManager::getLtiRoleClaimForStudipRole($GLOBALS['perm']->get_studip_perm($this->course_id))] + ); + $this->render_text($message->toHtmlRedirectForm()); } else { //LTI 1.0/1.1 $custom_parameters = explode("\n", $this->tool->custom_parameters); - $content_item_return_url = $this->url_for('course/lti/save_link/' . $this->tool->id); + $content_item_return_url = $this->url_for('course/lti/save_link/' . $this->link->id); // set up ContentItemSelectionRequest $lti_link = new LtiLink($this->tool->launch_url, $this->tool->consumer_key, $this->tool->consumer_secret, $this->tool->oauth_signature_method); @@ -570,14 +573,31 @@ class Course_LtiController extends StudipController } /** - * Create a new LTI content block for the specified tool id. + * Handles the jump back from the LTI tool into Stud.IP and finishes the integration + * of a deep linked LTI tool into a Stud.IP course. * - * @param int $tool_id tool id + * @param int $link_id tool id */ - public function save_link_action($tool_id) + public function save_link_action($link_id) { - $tool = LtiTool::find($tool_id); + $this->link = \LtiResourceLink::find($link_id); + if (!$this->link) { + PageLayout::postError(_('Die Einbindung des LTI-Tools wurde nicht gefunden!')); + $this->relocate('course/lti/add_link'); + return; + } + if ($this->link->course_id !== $this->course_id) { + PageLayout::postError(_('Die Einbindung des LTI-Tools ist nicht für diese Veranstaltung bestimmt.')); + $this->relocate('course/lti/add_link'); + return; + } + if (empty($this->link->options['unfinished_deep_linking'])) { + PageLayout::postError(_('Die Einbindung des LTI-Tools ist bereits abgeschlossen.')); + $this->relocate('course/lti/add_link'); + return; + } + $tool = $this->link->deployment->tool ?? null; if (!$tool) { PageLayout::postError(_('Das ausgewählte LTI-Tool wurde nicht gefunden.')); $this->redirect('course/lti/add_link'); @@ -592,14 +612,17 @@ class Course_LtiController extends StudipController if ($tool->lti_version === '1.3a') { //LTI 1.3a + $reg_man = new RegistrationManager(); + $reg_man->setResourceLink($this->link); + $validator = new PlatformLaunchValidator( - new RegistrationManager(), + $reg_man, new NonceRepository(Studip\Cache\Factory::getCache()) ); $result = $validator->validateToolOriginatingLaunch($this->getPsrRequest()); if ($result->hasError()) { PageLayout::postError($result->getError()); - $this->redirect('course/lti/add_link'); + $this->redirect('course/lti/index'); return; } $all_lti_resources = (new ResourceCollectionFactory())->createFromClaim( @@ -608,35 +631,12 @@ class Course_LtiController extends StudipController $lti_resource_links = $all_lti_resources->getByType(LtiResourceLinkInterface::TYPE); if (count($lti_resource_links) > 0) { - $use_first_link = true; foreach ($lti_resource_links as $lti_resource_link) { - $deployment = LtiDeployment::findOneBySQL( - "JOIN `lti_resource_links` - ON `lti_deployments`.`id` = `lti_resource_links`.`deployment_id` - WHERE - `lti_deployments`.`tool_id` = :tool_id AND `lti_resource_links`.`course_id` = :course_id - AND `lti_deployments`.`options` LIKE '%unfinished_deep_linking%=%true'", - ['tool_id' => $this->tool->id, 'course_id' => $this->range_id] - ); - $use_first_link = false; - if (!$deployment) { - //If this is the first link, the deployment has been removed. - //In that case and if it is not the first link, a new deployment - //has to be created. - $deployment = new LtiDeployment(); - $deployment->tool_id = $tool->id; - $deployment->title = $tool->name; - } - $deployment->launch_url = $lti_resource_link->getUrl(); - if (!empty($deployment->options['unfinished_deep_linking'])) { - unset($deployment->options['unfinished_deep_linking']); - } - if ($deployment->store()) { - $link = new \LtiResourceLink(); - $link->deployment_id = $deployment->id; - $link->course_id = $this->range_id; - $link->store(); + $this->link->launch_url = $lti_resource_link->getUrl(); + if (!empty($this->link->options['unfinished_deep_linking'])) { + unset($this->link->options['unfinished_deep_linking']); } + $this->link->store(); } } } else { @@ -656,7 +656,7 @@ class Course_LtiController extends StudipController $lti_data = new LtiDeployment(); $lti_data->title = (string) $item['title']; $lti_data->description = Studip\Markup::purifyHtml(Studip\Markup::markAsHtml($item['text'])); - $lti_data->tool_id = $tool_id; + $lti_data->tool_id = $tool->id; $lti_data->launch_url = (string) ($item['url'] ?? ''); $options = []; if (is_array($item['custom'])) { @@ -682,7 +682,7 @@ class Course_LtiController extends StudipController } } - if ($lti_errormsg) { + if (!empty($lti_errormsg)) { PageLayout::postError($lti_errormsg); } @@ -819,9 +819,9 @@ class Course_LtiController extends StudipController */ public function outcome_action($id) { - $lti_data = LtiDeployment::find($id); + $lti_data = \LtiResourceLink::find($id); - if (!Studip\OAuth1::verifyRequest($this->getPsrRequest(), $lti_data->getConsumerSecret(), '')) { + if (!Studip\OAuth1::verifyRequest($this->getPsrRequest(), $lti_data->deployment->getConsumerSecret(), '')) { throw new Exception('Could not verify request.'); } @@ -880,21 +880,17 @@ class Course_LtiController extends StudipController Navigation::activateItem('/course/lti/grades'); if ($this->edit_perm) { - $this->lti_data_array = LtiDeployment::findBySQL( - "JOIN `lti_resource_links` - ON `lti_deployments`.`id` = `lti_resource_links`.`deployment_id` - WHERE `lti_resource_links`.`course_id` = :course_id - ORDER BY `lti_resource_links`.`position`", + $this->lti_data_array = \LtiResourceLink::findBySQL( + "`course_id` = :course_id + ORDER BY `position`", ['course_id' => $this->course_id] ); } else { //Only load those deployments that are fully configured: - $this->lti_data_array = LtiDeployment::findBySQL( - "JOIN `lti_resource_links` - ON `lti_deployments`.`id` = `lti_resource_links`.`deployment_id` - WHERE `lti_resource_links`.`course_id` = :course_id - AND (`lti_deployments`.`options` IS NULL OR `lti_deployments`.`options` NOT LIKE '%unfinished_deep_linking%') - ORDER BY `lti_resource_links`.`position`", + $this->lti_data_array = \LtiResourceLink::findBySQL( + "`course_id` = :course_id + AND (`options` IS NULL OR `options` NOT LIKE '%unfinished_deep_linking%') + ORDER BY `position`", ['course_id' => $this->course_id] ); } @@ -926,10 +922,10 @@ class Course_LtiController extends StudipController public function export_grades_action() { if ($this->edit_perm) { - $lti_data_array = LtiDeployment::findByCourse_id($this->course_id, 'ORDER BY position'); + $lti_data_array = \LtiResourceLink::findByCourse_id($this->course_id, 'ORDER BY position'); } else { //Only load those deployments that are fully configured: - $lti_data_array = LtiDeployment::findBySQL( + $lti_data_array = \LtiResourceLink::findBySQL( "`course_id` = :course_id AND (`options` IS NULL OR `options` NOT LIKE '%unfinished_deep_linking%') ORDER BY `position`", ['course_id' => $this->course_id] diff --git a/app/controllers/lti/tool.php b/app/controllers/lti/tool.php index fae276e..61ca37b 100644 --- a/app/controllers/lti/tool.php +++ b/app/controllers/lti/tool.php @@ -35,12 +35,13 @@ class Lti_ToolController extends AuthenticatedController { //$this->range_id and $this->tool are created in the before-filter. if ($this->range_id !== 'global') { - $this->deployment = LtiDeployment::findOneBySQL( - 'JOIN `lti_resource_links` + $link_id = Request::get('link_id'); + $this->link = \LtiResourceLink::findOneBySQL( + 'JOIN `lti_deployments` ON `lti_deployments`.`id` = `lti_resource_links`.`deployment_id` WHERE - `lti_deployments`.`tool_id` = :tool_id AND `lti_resource_links`.`course_id` = :range_id', - ['tool_id' => $this->tool->id, 'range_id' => $this->range_id] + `lti_deployments`.`tool_id` = :tool_id AND `lti_resource_links`.`id` = :link_id', + ['tool_id' => $this->tool->id, 'link_id' => $link_id] ); } } @@ -74,12 +75,9 @@ class Lti_ToolController extends AuthenticatedController PageLayout::postWarning(_('Bitte beachten Sie das geltende europäische Datenschutzrecht (DSGVO)!')); } elseif (!$this->tool->isEditableByUser()) { throw new AccessDeniedException(); - } else { - //The tool is old and editable by the user. Check if a deployment exists and load it. - $this->deployment = LtiDeployment::findOneBySQL( - "`tool_id` = :tool_id ORDER BY `mkdate` ASC", - ['tool_id' => $this->tool->id] - ); + } elseif (Request::get('link_id')) { + //The tool is old and editable by the user. Check if a link exists and load it. + $this->link = \LtiResourceLink::find(Request::get('link_id')); } if (Request::isPost()) { @@ -93,6 +91,10 @@ class Lti_ToolController extends AuthenticatedController protected function saveTool(): void { CSRFProtection::verifyUnsafeRequest(); + $this->link = null; + if (Request::get('link_id')) { + $this->link = \LtiResourceLink::find(Request::get('link_id')); + } //Note: $this->tool is created in the before_filter. $new_tool = $this->tool->isNew(); $this->tool->name = trim(Request::get('name')); @@ -118,10 +120,10 @@ class Lti_ToolController extends AuthenticatedController $this->tool->custom_parameters = trim(Request::get('custom_parameters')); $tool_public_key = trim(Request::get('tool_public_key')); - //Check if the tool has a deployment. If so, use it. Otherwise, create a new deployment. + //Check if the tool has a general deployment. If so, use it. Otherwise, create a new deployment. if (!$new_tool) { $this->deployment = LtiDeployment::findOneBySQL( - "`tool_id` = :tool_id ORDER BY `mkdate` ASC", + "`tool_id` = :tool_id AND `purpose` = 'general' ORDER BY `mkdate` ASC", ['tool_id' => $this->tool->id] ); } @@ -131,18 +133,6 @@ class Lti_ToolController extends AuthenticatedController $this->deployment->tool_id = $this->tool->id; } } - $this->deployment->description = trim(Request::get('description')); - $this->deployment->title = $this->tool->name; - $this->deployment->launch_url = $this->tool->launch_url; - $document_target = trim(Request::get('document_target')); - if ($document_target === 'iframe') { - if (!is_array($this->deployment->options)) { - $this->deployment->options = []; - } - $this->deployment->options['document_target'] = $document_target; - } elseif (isset($this->deployment->options['document_target'])) { - unset($this->deployment->options['document_target']); - } $errors = $this->tool->validate(); if ($errors) { @@ -166,22 +156,34 @@ class Lti_ToolController extends AuthenticatedController return; } if ($this->range_id !== 'global') { - $resource_link_exists = false; + $resource_link = null; if (!$new_tool) { //Create an LTI resource link, if it doesn't exist yet: - $resource_link_exists = \LtiResourceLink::countBySQL( + $resource_link = \LtiResourceLink::findOneBySQL( "`deployment_id` = :deployment_id AND `course_id` = :course_id", ['deployment_id' => $this->deployment->id, 'course_id' => $this->range_id] - ) > 0; + ); } - if (!$resource_link_exists) { + if (!$resource_link) { //Either the tool has just been created or the tool existed and it wasn't yet //linked to the course. In those both cases, we have to create a new LTI resource link. $resource_link = new \LtiResourceLink(); $resource_link->deployment_id = $this->deployment->id; $resource_link->course_id = $this->range_id; - $resource_link->store(); } + $resource_link->description = trim(Request::get('description')); + $resource_link->title = $this->tool->name; + $resource_link->launch_url = $this->tool->launch_url; + $document_target = trim(Request::get('document_target')); + if ($document_target === 'iframe') { + if (!is_array($resource_link->options)) { + $resource_link->options = []; + } + $resource_link->options['document_target'] = $document_target; + } elseif (isset($resource_link->options['document_target'])) { + unset($resource_link->options['document_target']); + } + $resource_link->store(); } if ($this->tool->lti_version === '1.3a' && $tool_public_key) { if (!$this->tool->updatePublicKey($tool_public_key)) { @@ -189,6 +191,8 @@ class Lti_ToolController extends AuthenticatedController _('Der öffentliche Schlüssel des LTI-Tools konnte nicht gespeichert werden.') ); } + } else { + Keyring::deleteBySQL("`range_type` = 'lti_tool' AND `range_id` = :tool_id", ['tool_id' => $this->tool->id]); } PageLayout::postSuccess(_('Das LTI-Tool wurde gespeichert.')); diff --git a/app/views/admin/lti/index.php b/app/views/admin/lti/index.php index f047add..83faf3a 100644 --- a/app/views/admin/lti/index.php +++ b/app/views/admin/lti/index.php @@ -17,6 +17,7 @@ <col style="width: 5%;"> <col style="width: 5%;"> <col style="width: 5%;"> + <col style="width: 5%;"> </colgroup> <thead> @@ -26,6 +27,7 @@ <th><?= _('Consumer-Key') ?></th> <th><?= _('LTI-Version') ?></th> <th><?= _('Deployment-ID') ?></th> + <th><?= _('Deep Links') ?></th> <th><?= _('Links') ?></th> <th class="actions"><?= _('Aktionen') ?></th> </tr> @@ -49,9 +51,25 @@ <td> <? //Each tool should only have one deployment-ID: - $deployment = LtiDeployment::findOneByTool_id($tool->id); + $deployments = LtiDeployment::findBySQL( + "`tool_id` = :tool_id AND `purpose` = 'general'", + ['tool_id' => $tool->id] + ); + $deployment_ids = []; + foreach ($deployments as $deployment) { + $deployment_ids[] = $deployment->id; + } ?> - <?= htmlReady($deployment->id ?? '') ?> + <?= htmlReady(implode(', ', $deployment_ids)) ?> + <? if (count($deployment_ids) > 1) : ?> + <?= tooltipIcon(_('Dieses Tool hat mehrere Deployment-IDs zur generellen Nutzung!')) ?> + <? endif ?> + </td> + <td> + <?= htmlReady(LtiDeployment::countBySQL( + "`tool_id` = :tool_id AND `purpose` = 'deep_linking'", + ['tool_id' => $tool->id] + )) ?> </td> <td> <?= \LtiResourceLink::countBySql( diff --git a/app/views/course/lti/add_link.php b/app/views/course/lti/add_link.php index dc479a2..c5fd555 100644 --- a/app/views/course/lti/add_link.php +++ b/app/views/course/lti/add_link.php @@ -3,7 +3,7 @@ <?= _('Auswahl des externen Tools') ?> <select name="tool_id"> <? foreach ($tools as $tool): ?> - <option value="<?= $tool->id ?>"><?= htmlReady($tool->name) ?></option> + <option value="<?= htmlReady($tool->id) ?>"><?= htmlReady($tool->name) ?></option> <? endforeach ?> </select> </label> diff --git a/app/views/course/lti/consent.php b/app/views/course/lti/consent.php index be78d5d..a6a07e9 100644 --- a/app/views/course/lti/consent.php +++ b/app/views/course/lti/consent.php @@ -53,7 +53,7 @@ <?= _('Ihr Profilbild') ?> </label> </fieldset> - <?= $this->render_partial('lti/_deployment_user_info', ['deployment' => $resource_link->deployment]) ?> + <?= $this->render_partial('lti/_link_user_info', ['link' => $resource_link]) ?> <fieldset> <legend><?= _('Bestätigung') ?></legend> <label> diff --git a/app/views/course/lti/grades.php b/app/views/course/lti/grades.php index e9b0b40..48df9e9 100644 --- a/app/views/course/lti/grades.php +++ b/app/views/course/lti/grades.php @@ -27,7 +27,7 @@ </td> <? foreach ($lti_data_array as $lti_data): ?> <td style="text-align: right;"> - <? if ($grade = $lti_data->grades->findOneBy('user_id', $member->user_id)): ?> + <? if ($grade = $lti_data->deployment->grades->findOneBy('user_id', $member->user_id)): ?> <?= sprintf('%.0f%%', $grade->score * 100) ?> <? else: ?> – diff --git a/app/views/course/lti/grades_user.php b/app/views/course/lti/grades_user.php index fb041c5..04ce429 100644 --- a/app/views/course/lti/grades_user.php +++ b/app/views/course/lti/grades_user.php @@ -16,7 +16,7 @@ <tr> <td><?= htmlReady($lti_data->title) ?></td> <td style="text-align: right;"> - <? if ($grade = LtiGrade::find([$lti_data->id, $GLOBALS['user']->id])): ?> + <? if ($grade = LtiGrade::find([$lti_data->deployment_id, $GLOBALS['user']->id])): ?> <?= sprintf('%.0f%%', $grade->score * 100) ?> <? else: ?> – diff --git a/app/views/course/lti/index.php b/app/views/course/lti/index.php index 76eb307..f28dc5e 100644 --- a/app/views/course/lti/index.php +++ b/app/views/course/lti/index.php @@ -11,8 +11,8 @@ <? foreach ($links as $link): ?> <? - $launch_url = $link->deployment->getLaunchURL(); - $unfinished_deep_linking = !empty($link->deployment->options['unfinished_deep_linking']); + $launch_url = $link->getLaunchURL(); + $unfinished_deep_linking = !empty($link->options['unfinished_deep_linking']); $no_consent = !LtiToolPrivacySettings::countBySql( '`tool_id` = :tool_id AND `user_id` = :user_id', ['tool_id' => $link->deployment->tool_id, 'user_id' => $GLOBALS['user']->id] @@ -22,7 +22,7 @@ <article class="studip"> <header> <h1> - <?= htmlReady($link->deployment->title) ?> + <?= htmlReady($link->title) ?> <?= $unfinished_deep_linking ? '(' . _('LTI Deep Linking noch nicht fertig eingerichtet') . ')' : '' ?> </h1> @@ -50,7 +50,7 @@ $show_admin_actions = $GLOBALS['perm']->have_studip_perm('tutor', $link->course_id); if ($show_admin_actions) { $menu->addLink( - $controller->url_for('lti/tool/index/' . $link->course_id . '/' . $link->deployment->tool->id), + $controller->url_for('lti/tool/index/' . $link->course_id . '/' . $link->deployment->tool_id, ['link_id' => $link->id]), _('Konfiguration des LTI-Tools anzeigen'), Icon::create('info-circle'), ['data-dialog' => 'size=default'] @@ -75,7 +75,7 @@ $menu->addLink( sprintf( 'javascript:void(STUDIP.Dialog.confirmAsPost(\'%s\', \'%s\'))', - sprintf(_('Wollen Sie das LTI-Tool "%s" wirklich entfernen?'), $link->deployment->title), + sprintf(_('Wollen Sie das LTI-Tool "%s" wirklich entfernen?'), $link->title), $controller->url_for('lti/tool/delete/' . $link->course_id . '/' . $link->deployment->tool_id) ), _('LTI-Tool entfernen'), @@ -92,11 +92,11 @@ <? if ($unfinished_deep_linking) : ?> <?= Studip\LinkButton::create( _('Einrichtung abschließen'), - $controller->url_for('course/lti/select_link/' . $link->id, ['tool_id' => $link->tool_id]), + $controller->url_for('course/lti/select_link/' . $link->id, ['tool_id' => $link->deployment->tool_id]), ['target' => '_blank'] ) ?> <? elseif ($no_consent) : ?> - <?= formatReady($link->deployment->description) ?> + <?= formatReady($link->description) ?> <p><?= _('Sie haben der Datenweitergabe an das LTI-Tool noch nicht zugestimmt und können es deswegen noch nicht nutzen.') ?></p> <?= Studip\LinkButton::create( _('Datenschutzeinstellungen öffnen'), @@ -105,9 +105,9 @@ ) ?> <? elseif ($launch_url) : ?> <? - $document_target = $link->deployment->options['document_target'] ?? ''; + $document_target = $link->options['document_target'] ?? ''; ?> - <?= formatReady($link->deployment->description) ?> + <?= formatReady($link->description) ?> <? if ($document_target === 'iframe') : ?> <iframe style="border: none; height: 640px; width: 100%;" src="<?= $controller->link_for('course/lti/iframe/' . $link->id) ?>"></iframe> diff --git a/app/views/course/lti/select_link.php b/app/views/course/lti/select_link.php index 370587e..b0a30f6 100644 --- a/app/views/course/lti/select_link.php +++ b/app/views/course/lti/select_link.php @@ -2,13 +2,13 @@ /** * @var AuthenticatedController $controller * @var LtiTool $tool - * @var LtiDeployment $deployment + * @var \LtiResourceLink $link */ ?> <form class="default" method="post" - action="<?= $controller->link_for('course/lti/process_select_link/' . htmlReady($deployment->id), ['tool_id' => $tool->id]) ?>"> + action="<?= $controller->link_for('course/lti/process_select_link/' . htmlReady($link->id ?? ''), ['tool_id' => $tool->id]) ?>"> <?= CSRFProtection::tokenTag() ?> - <?= $this->render_partial('lti/_tool_info', ['tool' => $tool, 'deployment' => $deployment]) ?> + <?= $this->render_partial('lti/_tool_info', ['tool' => $tool, 'deployment' => $link->deployment ?? null]) ?> <div data-dialog-button> <?= \Studip\Button::create(_('Weiter'), 'continue') ?> </div> diff --git a/app/views/course/lti/select_tool.php b/app/views/course/lti/select_tool.php index 118c1ae..d2af241 100644 --- a/app/views/course/lti/select_tool.php +++ b/app/views/course/lti/select_tool.php @@ -14,10 +14,10 @@ <select name="selected_deployment_id"> <? foreach ($global_tool_deployments as $deployment) : ?> <option value="<?= htmlReady($deployment->id) ?>"> - <? if ($deployment->title !== $deployment->tool->name) : ?> - <?= htmlReady(sprintf('%1$s (%2$s)', $deployment->tool->name, $deployment->title)) ?> + <? if ($deployment->name) : ?> + <?= htmlReady(sprintf('%1$s (%2$s)', $deployment->tool->name, $deployment->name)) ?> <? else : ?> - <?= htmlReady($deployment->title) ?> + <?= htmlReady($deployment->tool->name) ?> <? endif ?> </option> <? endforeach ?> diff --git a/app/views/lti/_deployment_user_info.php b/app/views/lti/_link_user_info.php index b43da8f..568aaf8 100644 --- a/app/views/lti/_deployment_user_info.php +++ b/app/views/lti/_link_user_info.php @@ -1,37 +1,37 @@ <? /** - * @var LtiDeployment $deployment + * @var \LtiResourceLink $link */ ?> -<? if (!empty($deployment)) : ?> +<? if (!empty($link)) : ?> <article class="studip"> - <header><h1><?= htmlReady($deployment->title) ?></h1></header> + <header><h1><?= htmlReady($link->title) ?></h1></header> <section> - <? if ($deployment->tool->range_id === 'global') : ?> + <? if ($link->deployment->tool->range_id === 'global') : ?> <p> <?= sprintf( 'Dies ist eine Einbindung des LTI-Tools „%s“.', - htmlReady($deployment->tool->name) + htmlReady($link->deployment->tool->name) ) ?> </p> <? endif ?> - <p><?= formatReady($deployment->description ?? '') ?></p> + <p><?= formatReady($link->description ?? '') ?></p> <? - $url_parts = parse_url($deployment->getLaunchURL()); + $url_parts = parse_url($link->getLaunchURL()); ?> <? if (!empty($url_parts['host'])) : ?> <p><?= _('Domain') ?>: <?= htmlReady($url_parts['host']) ?></p> <? endif ?> - <? if ($deployment->tool->terms_of_use_url || $deployment->tool->privacy_policy_url) : ?> + <? if ($link->deployment->tool->terms_of_use_url || $link->deployment->tool->privacy_policy_url) : ?> <p> - <? if ($deployment->tool->terms_of_use_url) : ?> - <a href="<?= htmlReady($deployment->tool->terms_of_use_url) ?>"> + <? if ($link->deployment->tool->terms_of_use_url) : ?> + <a href="<?= htmlReady($link->deployment->tool->terms_of_use_url) ?>"> <?= Icon::create('link-extern')->asImg(['class' => 'text-bottom']) ?> <?= _('Nutzungsbedingungen') ?> </a> <? endif ?> - <? if ($deployment->tool->privacy_policy_url) : ?> - <a href="<?= htmlReady($deployment->tool->privacy_policy_url) ?>"> + <? if ($link->deployment->tool->privacy_policy_url) : ?> + <a href="<?= htmlReady($link->deployment->tool->privacy_policy_url) ?>"> <?= Icon::create('link-extern')->asImg(['class' => 'text-bottom']) ?> <?= _('Datenschutzerklärung') ?> </a> diff --git a/app/views/lti/_tool_form_fields.php b/app/views/lti/_tool_form_fields.php index 0dcbd25..d1dccec 100644 --- a/app/views/lti/_tool_form_fields.php +++ b/app/views/lti/_tool_form_fields.php @@ -1,7 +1,7 @@ <?php /** * @var LtiTool $tool - * @var ?LtiDeployment $deployment + * @var ?\LtiResourceLink $link */ ?> <fieldset> @@ -10,11 +10,11 @@ <span class="textlabel"><?= _('Titel') ?></span> <span class="asterisk">*</span> <input type="text" name="name" required - value="<?= htmlReady($tool->name ?? '') ?>"> + value="<?= htmlReady($link->title ?? $tool->name ?? '') ?>"> </label> <label> <?= _('Beschreibung') ?> - <textarea name="description" class="wysiwyg"><?= wysiwygReady($deployment->description ?? '') ?></textarea> + <textarea name="description" class="wysiwyg"><?= wysiwygReady($link->description ?? '') ?></textarea> </label> <label> <?= _('Datenschutzhinweise') ?> @@ -114,8 +114,8 @@ <?= _('Zusätzliche LTI-Parameter') ?> <?= tooltipIcon(_('Ein Wert pro Zeile, Beispiel: Review:Chapter=1.2.56')) ?> <textarea name="custom_parameters"><?= htmlReady( - !empty($deployment->options['custom_parameters']) - ? $deployment->options['custom_parameters'] + !empty($link->options['custom_parameters']) + ? $link->options['custom_parameters'] : $tool->custom_parameters ?? '' ) ?></textarea> </label> @@ -123,7 +123,7 @@ <fieldset> <legend><?= _('Anzeigeeinstellungen') ?></legend> <label> - <input type="checkbox" name="document_target" value="iframe" <?= isset($deployment->options['document_target']) && $deployment->options['document_target'] === 'iframe' ? ' checked' : '' ?>> + <input type="checkbox" name="document_target" value="iframe" <?= isset($link->options['document_target']) && $link->options['document_target'] === 'iframe' ? ' checked' : '' ?>> <?= _('Anzeige im IFRAME auf der Seite') ?> <?= tooltipIcon(_('Normalerweise wird das externe Tool in einem neuen Fenster angezeigt. Aktivieren Sie diese Option, wenn die Anzeige stattdessen in einem IFRAME erfolgen soll.')) ?> </label> diff --git a/app/views/lti/_tool_info.php b/app/views/lti/_tool_info.php index 0fafc9d..fb9657e 100644 --- a/app/views/lti/_tool_info.php +++ b/app/views/lti/_tool_info.php @@ -1,15 +1,15 @@ <?php /** * @var LtiTool $tool - * @var LtiDeployment $deployment - * @var StudipControlle $controller + * @var \LtiResourceLink $link + * @var StudipController $controller */ ?> <? if (!empty($tool)) : ?> <article class="studip"> <header> - <? if ($deployment) : ?> - <h1><?= htmlReady($deployment->title) ?></h1> + <? if ($link) : ?> + <h1><?= htmlReady($link->title) ?></h1> <? else : ?> <h1><?= htmlReady($tool->name) ?></h1> <? endif ?> @@ -17,10 +17,10 @@ <dl> <dt><?= _('Launch-URL') ?></dt> <dd> - <? if ($deployment && $deployment->launch_url) : ?> - <a href="<?= htmlReady($deployment->launch_url) ?>"> + <? if ($link && $link->launch_url) : ?> + <a href="<?= htmlReady($link->launch_url) ?>"> <?= Icon::create('link-extern')->asImg(['class' => 'text-bottom']) ?> - <?= htmlReady($deployment->launch_url) ?> + <?= htmlReady($link->launch_url) ?> </a> <? else : ?> <a href="<?= htmlReady($tool->launch_url) ?>"> @@ -54,37 +54,32 @@ <dd><?= htmlReady($tool->id) ?></dd> <? endif ?> - <? if ($deployment) : ?> + <? if (!empty($link->deployment->id)) : ?> <dt><?= _('Deployment-ID') ?></dt> - <dd><?= htmlReady($deployment->id) ?></dd> + <dd><?= htmlReady($link->deployment->id) ?></dd> - <? if ($parameters = $deployment->getCustomParameters()) : ?> + <? if ($parameters = $link->getCustomParameters()) : ?> <dt><?= _('LTI custom parameters') ?></dt> <dd><?= htmlReady($parameters) ?></dd> <? endif ?> <? endif ?> - <dt><?= _('Direktlink zum LTI-Tool') ?></dt> - <dd> - <ul> - <? foreach ($tool->deployments as $deployment) : ?> - <? - $link = LtiResourceLink::findOneByDeployment_id($deployment->id); - ?> - <? if ($link) : ?> - <li> - <a href="<?= $controller->link_for('course/lti/iframe', $link->id) ?>"> - <?= Icon::create('link-extern')->asImg(['class' => 'text-bottom']) ?> - <?= $controller->link_for('course/lti/iframe', $link->id) ?> - </a> - </li> - <? endif ?> - <? endforeach ?> - </ul> - </dd> + <? if ($link) : ?> + <dt><?= _('Direktlink zum LTI-Tool') ?></dt> + <dd> + <ul> + <li> + <a href="<?= $controller->link_for('course/lti/iframe', $link->id) ?>"> + <?= Icon::create('link-extern')->asImg(['class' => 'text-bottom']) ?> + <?= $controller->link_for('course/lti/iframe', $link->id) ?> + </a> + </li> + </ul> + </dd> + <? endif ?> </dl> </article> <article class="studip"> <header><h1><?= _('Plattform-Konfiguration') ?></h1></header> - <?= $this->render_partial('lti/_platform_data', ['platform' => \Studip\LTI13a\PlatformManager::getPlatformConfiguration()]) ?> + <?= $this->render_partial('lti/_platform_data', ['platform' => \Studip\LTI13a\PlatformManager::getPlatformConfiguration($tool->id)]) ?> </article> <? endif ?> diff --git a/app/views/lti/tool/add.php b/app/views/lti/tool/add.php index fda0849..fd0c223 100644 --- a/app/views/lti/tool/add.php +++ b/app/views/lti/tool/add.php @@ -3,14 +3,14 @@ * @var AuthenticatedController $controller * @var string $range_id * @var LtiTool $tool - * @var LtiDeployment $deployment + * @var ?\LtiResourceLink $link */ ?> <form class="default" method="post" data-dialog="reload-on-close" - action="<?= $controller->link_for('lti/tool/add/' . $range_id . '/' . $tool->id) ?>"> + action="<?= $controller->link_for('lti/tool/add/' . $range_id . '/' . $tool->id, ['link_id' => $link->id ?? '']) ?>"> <?= CSRFProtection::tokenTag() ?> <?= $this->render_partial('lti/_tool_form_fields', [ 'tool' => $tool, - 'deployment' => $deployment, + 'link' => $link ?? null, ]) ?> </form> diff --git a/app/views/lti/tool/edit.php b/app/views/lti/tool/edit.php index 5b29dd3..74f40c3 100644 --- a/app/views/lti/tool/edit.php +++ b/app/views/lti/tool/edit.php @@ -3,16 +3,16 @@ * @var AuthenticatedController $controller * @var string $range_id * @var LtiTool $tool - * @var LtiDeployment $deployment + * @var ?\LtiResourceLink $link */ ?> <? if ($tool) : ?> <form class="default" method="post" data-dialog="reload-on-close" - action="<?= $controller->link_for('lti/tool/edit/' . $range_id . '/' . $tool->id) ?>"> + action="<?= $controller->link_for('lti/tool/edit/' . $range_id . '/' . $tool->id, ['link_id' => $link->id ?? '']) ?>"> <?= CSRFProtection::tokenTag() ?> <?= $this->render_partial('lti/_tool_form_fields', [ 'tool' => $tool, - 'deployment' => $deployment, + 'deployment' => $link ?? null, ]) ?> </form> <? endif ?> diff --git a/db/migrations/6.0.51_add_columns_to_lti_resource_links_table.php b/db/migrations/6.0.51_add_columns_to_lti_resource_links_table.php new file mode 100644 index 0000000..9385908 --- /dev/null +++ b/db/migrations/6.0.51_add_columns_to_lti_resource_links_table.php @@ -0,0 +1,79 @@ +<?php + + +class AddColumnsToLtiResourceLinksTable extends Migration +{ + public function description() + { + return 'Add missing columns to the lti_resource_links table.'; + } + + protected function up() + { + $db = DBManager::get(); + + //Clone the launch_url and options column from lti_deployments: + $db->exec( + "ALTER TABLE `lti_resource_links` + ADD COLUMN `title` VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN `description` TEXT NULL, + ADD COLUMN `launch_url` VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN `options` TEXT" + ); + + $db->exec( + "UPDATE `lti_resource_links` + JOIN `lti_deployments` + ON `lti_deployments`.`id` = `lti_resource_links`.`deployment_id` + SET + `lti_resource_links`.`title` = `lti_deployments`.`title`, + `lti_resource_links`.`description` = `lti_deployments`.`description`, + `lti_resource_links`.`launch_url` = `lti_deployments`.`launch_url`, + `lti_resource_links`.`options` = `lti_deployments`.`options`" + ); + + $db->exec( + "ALTER TABLE `lti_deployments` + DROP COLUMN `title`, + DROP COLUMN `description`, + DROP COLUMN `launch_url`, + DROP COLUMN `options`, + ADD COLUMN `purpose` ENUM ('general', 'deep_linking') NOT NULL DEFAULT 'general', + ADD COLUMN `name` VARCHAR(255) NOT NULL DEFAULT ''" + ); + } + + protected function down() + { + $db = DBManager::get(); + + $db->exec( + "ALTER TABLE `lti_deployments` + DROP COLUMN `name`, + DROP COLUMN `purpose`, + ADD COLUMN `title` VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN `description` TEXT NULL, + ADD COLUMN `launch_url` VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN `options` TEXT" + ); + + $db->exec( + "UPDATE `lti_deployments` + JOIN `lti_resource_links` + ON `lti_deployments`.`id` = `lti_resource_links`.`deployment_id` + SET + `lti_deployments`.`title` = `lti_resource_links`.`title`, + `lti_deployments`.`description` = `lti_resource_links`.`description`, + `lti_deployments`.`launch_url` = `lti_resource_links`.`launch_url`, + `lti_deployments`.`options` = `lti_resource_links`.`options`" + ); + + $db->exec( + "ALTER TABLE `lti_resource_links` + DROP COLUMN `title`, + DROP COLUMN `description`, + DROP COLUMN launch_url, + DROP COLUMN `options`" + ); + } +} diff --git a/lib/classes/LTI13a/PlatformManager.php b/lib/classes/LTI13a/PlatformManager.php index 10c7cb1..9017267 100644 --- a/lib/classes/LTI13a/PlatformManager.php +++ b/lib/classes/LTI13a/PlatformManager.php @@ -31,17 +31,20 @@ class PlatformManager * Generates an object containing the settings for using this Stud.IP * as a platform that connects to an LTI tool via Deep Linking. * - * @param string $tool_id An optional LTI tool ID that is used to construct + * @param string $link_id The Stud.IP LTI Resource Link ID that is used to construct * the platform return URL. * + * @param string $course_id An optional Stud.IP course for which to get + * the deep linking configuration. + * * @return DeepLinkingSettings The settings for deep linking. */ - public static function getDeepLinkingConfiguration(string $tool_id = '') : DeepLinkingSettings + public static function getDeepLinkingConfiguration(string $link_id, string $course_id = '') : DeepLinkingSettings { $c = \Config::get(); return new DeepLinkingSettings( - self::getDeepLinkingReturnUrl($tool_id), + self::getDeepLinkingReturnUrl($link_id, $course_id), [LtiResourceLinkInterface::TYPE], ['window', 'iframe'], 'text/html', @@ -85,13 +88,20 @@ class PlatformManager /** * Generates the URL for returning from the tool in an LTI deep linking process. * - * @param string $tool_id The optional LTI Tool-ID to append to the URL. + * @param string $link_id The Stud.IP LTI Resource Link ID to append to the URL. + * + * @param string $course_id An optional Stud.IP course for which to generate + * the deep linking return URL. * * @return string The URL for returning from an LTI deep linking process. */ - public static function getDeepLinkingReturnUrl(string $tool_id = '') : string + public static function getDeepLinkingReturnUrl(string $link_id, string $course_id = '') : string { - return \URLHelper::getURL('dispatch.php/course/lti/save_link/' . $tool_id, null, true); + $params = ['link_id' => $link_id]; + if ($course_id) { + $params['cid'] = $course_id; + } + return \URLHelper::getURL('dispatch.php/course/lti/save_link/' . $link_id, $params, true); } /** diff --git a/lib/classes/LTI13a/Registration.php b/lib/classes/LTI13a/Registration.php index c4b898f..5498ace 100644 --- a/lib/classes/LTI13a/Registration.php +++ b/lib/classes/LTI13a/Registration.php @@ -2,6 +2,7 @@ namespace Studip\LTI13a; +use OAT\Library\Lti1p3Core\Exception\LtiException; use OAT\Library\Lti1p3Core\Registration\RegistrationInterface; use OAT\Library\Lti1p3Core\Tool\ToolInterface; use OAT\Library\Lti1p3Core\Platform\PlatformInterface; @@ -10,7 +11,8 @@ use OAT\Library\Lti1p3Core\Security\Key\KeyChainInterface; class Registration implements RegistrationInterface { public function __construct( - protected ?\LtiTool $tool + protected ?\LtiTool $tool, + protected ?\LtiResourceLink $link = null ) { } @@ -24,13 +26,27 @@ class Registration implements RegistrationInterface return $this->tool; } + public function setLtiResourceLink(\LtiResourceLink $link) + { + $this->link = $link; + } + + public function getLtiResourceLink() : ?\LtiResourceLink + { + return $this->link; + } + #[\Override] public function getIdentifier(): string { if (!$this->tool) { return ''; } - return $this->tool->id; + if ($this->link) { + return $this->tool->id . '_' . $this->link->id; + } else { + return $this->tool->id; + } } #[\Override] @@ -63,7 +79,11 @@ class Registration implements RegistrationInterface if (!$this->tool) { return []; } - return \DBManager::get()->fetchFirst("SELECT `id` FROM `lti_deployments` WHERE `tool_id` = ?", [$this->tool->id]); + if ($this->link) { + return [$this->link->deployment_id]; + } else { + return \DBManager::get()->fetchFirst("SELECT `id` FROM `lti_deployments` WHERE `tool_id` = ?", [$this->tool->id]); + } } #[\Override] @@ -72,10 +92,14 @@ class Registration implements RegistrationInterface if (!$this->tool) { return false; } - return \LtiDeployment::countBySql( - "`tool_id` = :tool_id AND `id` = :deployment_id", - ['tool_id' => $this->tool->id, 'deployment_id' => $deploymentId] - ) > 0; + if ($this->link) { + return $this->link->deployment_id == $deploymentId; + } else { + return \LtiDeployment::countBySql( + "`tool_id` = :tool_id AND `id` = :deployment_id", + ['tool_id' => $this->tool->id, 'deployment_id' => $deploymentId] + ) > 0; + } } #[\Override] @@ -98,12 +122,13 @@ class Registration implements RegistrationInterface #[\Override] public function getToolKeyChain(): ?KeyChainInterface { - if (!$this->tool) { + if (!$this->tool || $this->tool->jwks_url) { return null; } + $keyring = $this->tool->getKeyring(); if (!$keyring) { - $keyring = $this->tool->getKeyring(true); + throw new LtiException('Failed to load public key for tool ' . $this->tool->id); } return $keyring->toKeyChain(); } diff --git a/lib/classes/LTI13a/RegistrationManager.php b/lib/classes/LTI13a/RegistrationManager.php index 922cb58..05bcc2e 100644 --- a/lib/classes/LTI13a/RegistrationManager.php +++ b/lib/classes/LTI13a/RegistrationManager.php @@ -7,15 +7,29 @@ use OAT\Library\Lti1p3Core\Registration\RegistrationInterface; class RegistrationManager implements RegistrationRepositoryInterface { + protected ?\LtiResourceLink $link = null; + + public function setResourceLink(\LtiResourceLink $link) + { + $this->link = $link; + } + #[\Override] public function find(string $identifier): ?RegistrationInterface { //The identifier is the ID of a tool. $tool = \LtiTool::find($identifier); + $link = null; + if (!$tool) { + //Attempt to find the tool and a resource link. + $id_parts = explode('_', $identifier); + $tool = \LtiTool::find($id_parts[0]); + $link = \LtiResourceLink::find($id_parts[1]); + } if (!$tool) { return null; } - return new Registration($tool); + return new Registration($tool, $link); } /** @@ -42,7 +56,7 @@ class RegistrationManager implements RegistrationRepositoryInterface } $tool = \LtiTool::find($clientId); if ($tool) { - return new Registration($tool); + return new Registration($tool, $this->link); } return null; } @@ -51,7 +65,8 @@ class RegistrationManager implements RegistrationRepositoryInterface public function findByPlatformIssuer(string $issuer, string $clientId = null): ?RegistrationInterface { //Only handle requests for registrations of this Stud.IP: - if ($issuer !== \Config::get()->STUDIP_INSTALLATION_ID) { + $platform_config = \Studip\LTI13a\PlatformManager::getPlatformConfiguration(); + if ($issuer !== $platform_config->getAudience()) { //Invalid issuer. return null; } diff --git a/lib/models/LtiDeployment.php b/lib/models/LtiDeployment.php index e59e738..86f18a0 100644 --- a/lib/models/LtiDeployment.php +++ b/lib/models/LtiDeployment.php @@ -12,15 +12,10 @@ * @license http://www.gnu.org/licenses/gpl-2.0.html GPL version 2 * * @property int $id database column - * @property string $title database column - * @property string $description database column * @property int $tool_id database column - * @property string $launch_url database column * @property int $mkdate database column * @property int $chdate database column - * @property JSONArrayObject|null $options database column * @property SimpleORMapCollection<LtiGrade> $grades has_many LtiGrade - * @property Course $course belongs_to Course * @property LtiTool $tool belongs_to LtiTool */ @@ -33,8 +28,6 @@ class LtiDeployment extends SimpleORMap { $config['db_table'] = 'lti_deployments'; - $config['serialized_fields']['options'] = JSONArrayObject::class; - $config['belongs_to']['tool'] = [ 'class_name' => LtiTool::class, 'foreign_key' => 'tool_id' @@ -61,6 +54,8 @@ class LtiDeployment extends SimpleORMap /** * Get the launch_url of this entry. + * + * @deprecated */ public function getLaunchURL() { @@ -72,6 +67,8 @@ class LtiDeployment extends SimpleORMap /** * Get the consumer_key of this entry. + * + * @deprecated */ public function getConsumerKey() { @@ -80,6 +77,8 @@ class LtiDeployment extends SimpleORMap /** * Get the consumer_secret of this entry. + * + * @deprecated */ public function getConsumerSecret() { @@ -88,6 +87,8 @@ class LtiDeployment extends SimpleORMap /** * Get the oauth_signature_method of this entry. + * + * @deprecated */ public function getOauthSignatureMethod() { @@ -96,6 +97,8 @@ class LtiDeployment extends SimpleORMap /** * Get the custom_parameters of this entry. + * + * @deprecated */ public function getCustomParameters() { @@ -107,23 +110,6 @@ class LtiDeployment extends SimpleORMap return $parameters; } - public function getCustomLtiParameterArray() : array - { - $parameter_str = $this->getCustomParameters(); - if (empty($parameter_str)) { - return []; - } - $parameters = explode("\n", $parameter_str); - $array = []; - foreach ($parameters as $parameter) { - $key_value_parts = explode('=', $parameter, 2); - if (count($key_value_parts) === 2) { - $array[trim($key_value_parts[0])] = trim($key_value_parts[1]); - } - } - return ['https://purl.imsglobal.org/spec/lti/claim/custom' => $array]; - } - /** * Get the send_lis_person attribute of this entry. */ diff --git a/lib/models/LtiGrade.php b/lib/models/LtiGrade.php index a7a5dbc..b037f30 100644 --- a/lib/models/LtiGrade.php +++ b/lib/models/LtiGrade.php @@ -18,6 +18,9 @@ * @property int $chdate database column * @property LtiDeployment $link belongs_to LtiDeployment * @property User $user belongs_to User + * + * NOTE: LtiGrade is only for the LTI 1.0/1.1 interface. + * The LTI 1.3A interface uses the grade book tables for storing grades. */ class LtiGrade extends SimpleORMap diff --git a/lib/models/LtiResourceLink.php b/lib/models/LtiResourceLink.php index 785c350..0ea15aa 100644 --- a/lib/models/LtiResourceLink.php +++ b/lib/models/LtiResourceLink.php @@ -23,7 +23,11 @@ use OAT\Library\Lti1p3Core\Util\Collection\CollectionInterface; * @property int $id database column * @property int $deployment_id database column * @property string $course_id database column + * @property string $title database column + * @property string $description database column * @property int $position database column + * @property string $launch_url database column + * @property JSONArrayObject|null $options database column * @property int $mkdate database column * @property int $chdate database column * @property ?LtiDeployment $deployment related object @@ -35,6 +39,8 @@ class LtiResourceLink extends \SimpleORMap implements LtiResourceLinkInterface { $config['db_table'] = 'lti_resource_links'; + $config['serialized_fields']['options'] = JSONArrayObject::class; + $config['belongs_to']['course'] = [ 'class_name' => Course::class, 'foreign_key' => 'course_id' @@ -89,14 +95,19 @@ class LtiResourceLink extends \SimpleORMap implements LtiResourceLinkInterface return self::findOneBySQL('course_id = ? AND position = ?', [$course_id, $position]); } + public function getLaunchURL() + { + if (!empty($this->deployment->tool) && empty($this->deployment->tool->allow_custom_url) && empty($this->deployment->tool->deep_linking) || empty($this->launch_url)) { + return $this->deployment->tool->launch_url; + } + return $this->launch_url; + } + //OAT library LtiResourceLinkInterface and ResourceInterface implementation: public function getUrl(): ?string { - if ($this->deployment) { - return $this->deployment->getLaunchURL(); - } - return null; + return $this->getLaunchURL(); } public function getIcon(): ?array @@ -154,10 +165,7 @@ class LtiResourceLink extends \SimpleORMap implements LtiResourceLinkInterface public function getTitle(): ?string { - if ($this->deployment) { - return $this->deployment->title; - } - return null; + return $this->title ?? $this->deployment->tool->name ?? null; } public function getText(): ?string @@ -184,4 +192,31 @@ class LtiResourceLink extends \SimpleORMap implements LtiResourceLinkInterface ) ); } + + public function getCustomParameters() + { + $parameters = ''; + if (!empty($this->deployment->tool)) { + $parameters = $this->deployment->tool->custom_parameters; + } + $parameters .= $this->options['custom_parameters'] ?? ''; + return $parameters; + } + + public function getCustomLtiParameterArray() : array + { + $parameter_str = $this->getCustomParameters(); + if (empty($parameter_str)) { + return []; + } + $parameters = explode("\n", $parameter_str); + $array = []; + foreach ($parameters as $parameter) { + $key_value_parts = explode('=', $parameter, 2); + if (count($key_value_parts) === 2) { + $array[trim($key_value_parts[0])] = trim($key_value_parts[1]); + } + } + return ['https://purl.imsglobal.org/spec/lti/claim/custom' => $array]; + } } |
