Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
41d8725
[FIX] LTI: Add AGS consumer line item and score services
Saaweel May 22, 2026
e6d13e9
[FIX] LTI: Publish AGS scores in provider mode
Saaweel May 22, 2026
df58abf
[FIX] LTI: Update language labels
Saaweel May 22, 2026
7e4da05
Merge branch 'ilias11_LTI_fix_deep_linking' into ilias11_LTI_ags_deep…
Saaweel May 22, 2026
35e90a9
[FIX] LTI: Add content selection language labels
Saaweel May 27, 2026
0723440
[FIX] LTI: Add lost $logger var
Saaweel Jun 1, 2026
089b001
[FIX] LTI: Fix deep linking user selection
Saaweel Jun 2, 2026
bd46d97
[FIX] LTI: Restore attended setter on consumer result
Saaweel Jun 3, 2026
06728aa
[FIX] LTI: Fix deep linking redirect handling
Saaweel Jun 3, 2026
b6fdc47
[FIX] LTI: Preserve deep linking target link URI
Saaweel Jun 3, 2026
ac965ac
[FIX] LTI: Resolve AGS score user identifiers
Saaweel Jun 11, 2026
d0ae088
[FIX] LTI: Accept AGS score denominator
Saaweel Jun 12, 2026
6cf5502
[FIX] LTI: Share AGS user identifier matching
Saaweel Jun 16, 2026
59ca25d
[FIX] LTI: Allow deep linking content selection
Saaweel Jun 18, 2026
1a72920
[FIX] LTI: Improve AGS gradebook results
Saaweel Jun 19, 2026
019aa21
[FIX] LTI: Return correct userId and score scale in AGS results
Saaweel Jun 25, 2026
471e2f8
[FIX] LTI: Improve consumer creation error handling
Saaweel Jun 30, 2026
a62b2ae
[FIX] LTI: Complete fully graded submitted activities
Saaweel Jul 24, 2026
b826a58
[FIX] LTI: Format consumer provider form
Saaweel Jul 29, 2026
4584211
[FIX] LTI: Sort language entries
Saaweel Jul 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -256,4 +256,60 @@ public function step_16(): void
$this->db->manipulate($query);
}
}

public function step_17(): void
{
if (!$this->db->tableColumnExists('lti_consumer_settings', 'score_maximum')) {
$this->db->addTableColumn('lti_consumer_settings', 'score_maximum', [
'type' => 'float',
'notnull' => false,
'default' => 1
]);
}
}

public function step_18(): void
{
if (!$this->db->tableExists('lti_consumer_lineitems')) {
$values = [
'id' => ['type' => 'integer', 'length' => 4, 'notnull' => true],
'context_id' => ['type' => 'integer', 'length' => 4, 'notnull' => true],
'obj_id' => ['type' => 'integer', 'length' => 4, 'notnull' => false],
'client_id' => ['type' => 'text', 'length' => 255, 'notnull' => false],
'label' => ['type' => 'text', 'length' => 255, 'notnull' => false],
'score_maximum' => ['type' => 'float', 'notnull' => false, 'default' => 1],
'resource_id' => ['type' => 'text', 'length' => 255, 'notnull' => false],
'resource_link_id' => ['type' => 'text', 'length' => 255, 'notnull' => false],
'tag' => ['type' => 'text', 'length' => 255, 'notnull' => false],
'enabled' => ['type' => 'integer', 'length' => 1, 'notnull' => true, 'default' => 1]
];
$this->db->createTable('lti_consumer_lineitems', $values);
$this->db->addPrimaryKey('lti_consumer_lineitems', ['id']);
$this->db->createSequence('lti_consumer_lineitems');
}
}

public function step_19(): void
{
if ($this->db->tableExists('lti_consumer_lineitems') &&
!$this->db->tableColumnExists('lti_consumer_lineitems', 'resource_link_id')) {
$this->db->addTableColumn('lti_consumer_lineitems', 'resource_link_id', [
'type' => 'text',
'length' => 255,
'notnull' => false
]);
}
}

public function step_20(): void
{
if ($this->db->tableExists('lti_consumer_lineitems') &&
!$this->db->tableColumnExists('lti_consumer_lineitems', 'client_id')) {
$this->db->addTableColumn('lti_consumer_lineitems', 'client_id', [
'type' => 'text',
'length' => 255,
'notnull' => false
]);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,46 @@ public function __construct(?int $providerId = null)

private function preventClientIdInUrl(string $url): string
{
return preg_replace('/(\?|&)?client_id=[^&]*/', '', $url);
if (str_contains($url, ',')) {
return implode(',', array_map([$this, 'preventClientIdInUrl'], explode(',', $url)));
}

$parts = parse_url($url);
if ($parts === false || !isset($parts['query'])) {
return $url;
}

parse_str($parts['query'], $query);
unset($query['client_id']);

$result = '';
if (isset($parts['scheme'])) {
$result .= $parts['scheme'] . '://';
}
if (isset($parts['user'])) {
$result .= $parts['user'];
if (isset($parts['pass'])) {
$result .= ':' . $parts['pass'];
}
$result .= '@';
}
if (isset($parts['host'])) {
$result .= $parts['host'];
}
if (isset($parts['port'])) {
$result .= ':' . $parts['port'];
}
$result .= $parts['path'] ?? '';

$queryString = http_build_query($query, '', '&', PHP_QUERY_RFC3986);
if ($queryString !== '') {
$result .= '?' . $queryString;
}
if (isset($parts['fragment'])) {
$result .= '#' . $parts['fragment'];
}

return $result;
}

/**
Expand Down
97 changes: 93 additions & 4 deletions components/ILIAS/LTIConsumer/classes/class.ilLTIConsumeProviderFormGUI.php
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,88 @@ public function setAdminContext(bool $adminContext): void
$this->adminContext = $adminContext;
}

/**
* Build the "Deep linking" UI (button + modal with iframe).
*
* @param string $target_gui_class GUI class that has startDeepLinkingCmd()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a contradiction to the explicitly used PHP type.

* @param string $cmd command name, default 'startDeepLinking'
* @return string rendered HTML to embed in a form/custom input
*/
private function buildDlUiParts(
array $target_gui_class = [ilLTIConsumeProviderSettingsGUI::class],
string $cmd = 'startDeepLinking'
): string {
global $DIC;

$factory = $DIC->ui()->factory();
$renderer = $DIC->ui()->renderer();
$ctrl = $DIC->ctrl();

if ($this->isAdminContext()) {
$target_gui_class = [
ilAdministrationGUI::class,
ilObjLTIConsumerGUI::class,
ilLTIConsumerSettingsGUI::class,
ilLTIConsumeProviderSettingsGUI::class
];

$ctrl->setParameterByClass(ilLTIConsumerSettingsGUI::class, 'ref_id', ilObjLTIConsumer::getRefIdOfConsumerByDeploymentId((string) $this->getProvider()->getId()));
}

$iframe_url = $ctrl->getLinkTargetByClass(
$target_gui_class,
$cmd,
'',
true
);

$iframe_html = sprintf(
'<iframe src="%s" style="width:100%%;height:70vh;border:0;" allow="fullscreen"></iframe>',
htmlspecialchars($iframe_url, ENT_QUOTES)
);

$content = $factory->legacy()->content($iframe_html);

$modal = $factory
->modal()
->roundtrip($this->lng->txt('subtab_provider_settings'), $content);

$button = $factory
->button()
->standard($this->lng->txt('select'), '')
->withOnClick($modal->getShowSignal());

$html = $renderer->render([$button, $modal]);

$html .= <<<HTML
<script>
function closeDialog() {
let dlg = document.querySelector('dialog.c-modal.il-modal-roundtrip[open], dialog.c-modal[open]');
if (dlg && typeof dlg.close === 'function') {
dlg.close();
}
let backdrop = document.querySelector('.c-modal__backdrop, dialog + .c-modal__backdrop');
if (backdrop) {
backdrop.remove();
}

}
document.addEventListener('click', function (e) {
closeDialog();
}, true);

window.onLtiDeepLinkDone = function (url) {
closeDialog();
if(url) {
window.location.href = url;
}
};
</script>
HTML;

return $html;
}

public function initForm(string $formaction, string $saveCmd, string $cancelCmd): void
{
global $DIC;
Expand Down Expand Up @@ -126,6 +208,15 @@ public function initForm(string $formaction, string $saveCmd, string $cancelCmd)
if ($this->provider->getId() == 0) {
$lti13->setInfo($lng->txt('lti_con_version_1.3_before_id'));
}

if (!empty($this->provider->isContentItem())) {

$dl_html = $this->buildDlUiParts();
$dl_input = new ilCustomInputGUI($this->lng->txt('tab_content'));
$dl_input->setHTML($dl_html);
$lti13->addSubItem($dl_input);
}

$versionInp->addOption($lti13);
$providerUrlInp = new ilTextInputGUI($lng->txt('lti_con_tool_url'), 'provider_url13');
$providerUrlInp->setValue($this->provider->getProviderUrl());
Expand Down Expand Up @@ -885,7 +976,7 @@ public function initProvider(ilLTIConsumeProvider $provider): void
if (preg_match_all('/\S+/sm', $this->getInput('redirection_uris'), $redirect_uris_matches)) {
$provider->setRedirectionUris(implode(",", $redirect_uris_matches[0]));
} else {
$provider->setRedirectionUris($this->provider->getInitiateLogin());
$provider->setRedirectionUris($this->provider->getRedirectionUris());
}
$provider->setKeyType($this->getInput('key_type'));
if ($provider->getKeyType() == 'RSA_KEY') {
Expand Down Expand Up @@ -976,10 +1067,8 @@ public function getDynRegRequest(): string
}
$showToolConfigUrl = $DIC->ctrl()->getLinkTargetByClass([ilRepositoryGUI::class, ilObjLTIConsumerGUI::class], 'showToolConfig');
$regErrorUrl = $DIC->ctrl()->getLinkTargetByClass([ilRepositoryGUI::class, ilObjLTIConsumerGUI::class], 'addDynReg');
$this->getItemByPostVar('lti_dyn_reg_url')->setDisabled(true);
$this->getItemByPostVar('lti_dyn_reg_custom_params')->setDisabled(true);
$this->clearCommandButtons();
//$this->addCommandButton("cancelDynReg", $DIC->language()->txt('cancel'));
$this->addCommandButton("addDynReg", $DIC->language()->txt('save'));
$template = new ilTemplate('tpl.lti_dyn_reg_request.html', true, true, "components/ILIAS/LTIConsumer");
$template->setVariable('LTI_TOOL_REG_URL', $toolRegUrl);
$template->setVariable('LTI_DYN_REG_URL', $regUrl);
Expand Down
44 changes: 44 additions & 0 deletions components/ILIAS/LTIConsumer/classes/class.ilLTIConsumeProviderSettingsGUI.php
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,50 @@ private function getClientIdFromProviderUrl(string $providerUrl): string
return $res;
}

protected function startDeepLinkingCmd()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
protected function startDeepLinkingCmd()
protected function startDeepLinkingCmd(): never

{
global $DIC;
$provider = $this->object->getProvider();

$platform_client_id = $provider->getClientId();
$deployment_id = $provider->getId();
$user_id = ilObjLTIConsumer::getDeepLinkingUserIdentifier($provider, $DIC->user()); // or whatever you used as login_hint

$lti_message_hint = json_encode([
'deployment_id' => $deployment_id,
]);
$state = bin2hex(random_bytes(16));


$params = [
'login_hint' => $user_id,
'iss' => ilObjLTIConsumer::getPlattformId(),
'lti_message_hint' => $lti_message_hint,
'target_link_uri' => $provider->getContentItemUrl(),
'id' => $platform_client_id, // Instead of client_id due to ILIAS redirection system
'lti_deployment_id' => $deployment_id,
'state' => $state,
];


$join = (str_contains($provider->getInitiateLogin(), '?')) ? '&' : '?';
$url = $provider->getInitiateLogin() . $join . http_build_query($params);
$urlSafe = htmlspecialchars($url, ENT_QUOTES);
echo <<<HTML

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are using the HTTP component in other newly introduced code to handle HTTP responses, so please check if if could also be used here to build/send the response and close the connection.

You should consider using the HTTP component wherever possible when sending HTTP responses to the client, even in the existing code not touched by this PR:

<!doctype html>
<html>
<body onload="window.location.href='{$urlSafe}'">
<noscript>
<p>Continue to deep linking...</p>
<a href="{$urlSafe}">Continue</a>
</noscript>
</body>
</html>
HTML;
exit;

}

/**
* @throws ilMDServicesException
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,8 @@ protected function getLoginLTI13Form(): ?string
return null;
} else {
$this->initCmixUser();
$params = $this->getLaunchParametersLTI13($loginData['redirect_uri'], $this->object->getProvider()->getClientId(), $this->object->getProvider()->getId(), $loginData['nonce']);
$targetLinkUri = $loginData['target_link_uri'] ?? $this->getTargetLinkUri();
$params = $this->getLaunchParametersLTI13($targetLinkUri, $this->object->getProvider()->getClientId(), $this->object->getProvider()->getId(), $loginData['nonce']);
if (isset($loginData['state'])) {
$params['state'] = $loginData['state'];
}
Expand Down Expand Up @@ -256,7 +257,7 @@ protected function getStartButtonTxt13(): string
$output = '<form id="lti_launch_form" name="lti_launch_form" action="' . $this->object->getProvider()->getInitiateLogin() . '" method="post" target="' . $target . '" encType="application/x-www-form-urlencoded">';

$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'iss', ilObjLTIConsumer::getIliasHttpPath()) . "\n";
$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'target_link_uri', $this->object->getProvider()->getProviderUrl()) . "\n";
$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'target_link_uri', htmlspecialchars($this->getTargetLinkUri(), ENT_QUOTES)) . "\n";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use a Refinery transformation instead?

$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'login_hint', $user_ident) . "\n";
$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'lti_message_hint', $ltiMessageHint) . "\n";
$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'client_id', $this->object->getProvider()->getClientId()) . "\n";
Expand Down Expand Up @@ -292,7 +293,7 @@ protected function getEmbeddedAutoStartFormular(): string
ilSession::set('lti_message_hint', $ltiMessageHint);
$output = '<form id="lti_launch_form" name="lti_launch_form" action="' . $this->object->getProvider()->getInitiateLogin() . '" method="post" target="' . $target . '" encType="application/x-www-form-urlencoded">';
$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'iss', ilObjLTIConsumer::getIliasHttpPath()) . "\n";
$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'target_link_uri', $this->object->getProvider()->getProviderUrl()) . "\n";
$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'target_link_uri', htmlspecialchars($this->getTargetLinkUri(), ENT_QUOTES)) . "\n";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can't we use a Refinery transformation instead?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it ensured that all the existing values here surrounding the new lines are already trustworthy (checked, escaped)?

$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'login_hint', $user_ident) . "\n";
$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'lti_message_hint', $ltiMessageHint) . "\n";
$output .= sprintf('<input type="hidden" name="%s" value="%s" />', 'client_id', $this->object->getProvider()->getClientId()) . "\n";
Expand Down Expand Up @@ -404,6 +405,18 @@ protected function getLaunchParametersLTI13(string $endpoint, string $clientId,
);
}

private function getTargetLinkUri(): string
{
foreach (explode(';', $this->object->getCustomParams()) as $param) {
$parts = explode('=', $param, 2);
if (count($parts) === 2 && trim($parts[0]) === 'target_link_uri') {
return trim($parts[1]);
}
}

return $this->object->getProvider()->getProviderUrl();
}

public static function isEmbeddedLaunchRequest(): bool
{
global $DIC; /* @var \ILIAS\DI\Container $DIC */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,12 @@ public function getResources(): array
*/
public function getPermittedScopes(): array
{
$scopes = array();
$scopes[] = self::SCOPE_GRADESERVICE_LINEITEM;
$scopes[] = self::SCOPE_GRADESERVICE_LINEITEM_READ;
$scopes[] = self::SCOPE_GRADESERVICE_RESULT_READ;
$scopes[] = self::SCOPE_GRADESERVICE_SCORE;
return $scopes;
return [
self::SCOPE_GRADESERVICE_LINEITEM,
self::SCOPE_GRADESERVICE_LINEITEM_READ,
self::SCOPE_GRADESERVICE_RESULT_READ,
self::SCOPE_GRADESERVICE_SCORE
];
}

/**
Expand Down
Loading
Loading