aboutsummaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorMurtaza Sultani <sultani@data-quest.de>2026-01-27 13:38:51 +0100
committerMurtaza Sultani <sultani@data-quest.de>2026-03-19 17:36:05 +0100
commit8eae6da6ebe7a2f201b956235854bfcaec361037 (patch)
treed66a80530db40a158768f261de532d1e4994ca60 /app
parent097a4f4244f6e491118dafdc2f24af4ad4410fda (diff)
Account provisioning modes
Diffstat (limited to 'app')
-rw-r--r--app/controllers/admin/lti/publications.php2
-rw-r--r--app/controllers/admin/lti/resources.php7
-rw-r--r--app/controllers/course/lti.php10
-rw-r--r--app/controllers/enrol/lti.php138
-rw-r--r--app/controllers/enroll/lti.php228
-rw-r--r--app/controllers/login.php1
-rw-r--r--app/views/enrol/lti/launch.php13
-rw-r--r--app/views/enroll/lti/_messages.php17
-rw-r--r--app/views/enroll/lti/create_new_account.php7
-rw-r--r--app/views/enroll/lti/deeplink_callback.php7
-rw-r--r--app/views/enroll/lti/launch.php7
-rw-r--r--app/views/enroll/lti/provisioning_modes.php74
-rw-r--r--app/views/lti/launch/index.php47
13 files changed, 402 insertions, 156 deletions
diff --git a/app/controllers/admin/lti/publications.php b/app/controllers/admin/lti/publications.php
index 384f253..b3e993a 100644
--- a/app/controllers/admin/lti/publications.php
+++ b/app/controllers/admin/lti/publications.php
@@ -37,7 +37,7 @@ class Admin_Lti_PublicationsController extends AdminBaseController
]
];
- if ($GLOBALS['perm']->have_perm('root')) {
+ if ($GLOBALS['perm']->have_perm('root') && !$this->range_id) {
$sqlQuery = [
"TRUE ORDER BY `mkdate`, `name`"
];
diff --git a/app/controllers/admin/lti/resources.php b/app/controllers/admin/lti/resources.php
index 5860fb1..8f6d9b0 100644
--- a/app/controllers/admin/lti/resources.php
+++ b/app/controllers/admin/lti/resources.php
@@ -89,6 +89,13 @@ class Admin_Lti_ResourcesController extends AdminBaseController
'icon' => Request::get('icon')
]);
+ if (Request::get('registration_id')) {
+ $deploymentId = Registration::find(Request::get('registration_id'))?->getDefaultDeployment()->id;
+ $resourceLink->setData([
+ 'deployment_id' => $deploymentId ?? $resourceLink->deployment_id
+ ]);
+ }
+
$resourceLink->store();
PageLayout::postSuccess(
diff --git a/app/controllers/course/lti.php b/app/controllers/course/lti.php
index 6ec281f..fe8c098 100644
--- a/app/controllers/course/lti.php
+++ b/app/controllers/course/lti.php
@@ -240,7 +240,9 @@ class Course_LtiController extends StudipController
$deployment->deployment_key,
RoleMapper::fromLocal($GLOBALS['perm']->get_studip_perm($this->range_id)),
[
- ...$resourceLinkRepo->getCustomLtiParameters(),
+ [
+ 'https://purl.imsglobal.org/spec/lti/claim/custom' => $resourceLinkRepo->getCustom()
+ ],
new ContextClaim(
$this->range_id,
['http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering'],
@@ -371,15 +373,15 @@ class Course_LtiController extends StudipController
$links = $ltiResources->getByType(LtiResourceLinkInterface::TYPE);
if (count($links) > 0) {
foreach ($links as $link) {
- $custom_parameters = '';
+ $customParameters = '';
foreach ($link->getCustom() as $key => $value) {
- $custom_parameters .= $key . '=' . $value . "\n";
+ $customParameters .= $key . '=' . $value . "\n";
}
ResourceLink::create([
'title' => $link->getTitle(),
'launch_url' => $link->getUrl(),
- 'custom_parameters' => $custom_parameters,
+ 'custom_parameters' => $customParameters,
'launch_container' => $link->getProperties()->get('presentation')['documentTarget'] ?? $resourceLink->launch_container,
'deployment_id' => $resourceLink->deployment_id,
'course_id' => $resourceLink->course_id,
diff --git a/app/controllers/enrol/lti.php b/app/controllers/enrol/lti.php
deleted file mode 100644
index 9dd1eee..0000000
--- a/app/controllers/enrol/lti.php
+++ /dev/null
@@ -1,138 +0,0 @@
-<?php
-
-use Lti\Publication;
-use OAT\Library\Lti1p3Core\Exception\LtiException;
-use OAT\Library\Lti1p3Core\Exception\LtiExceptionInterface;
-use OAT\Library\Lti1p3Core\Message\Launch\Validator\Result\LaunchValidationResultInterface;
-use Studip\Authentication\Manager as AuthenticationManager;
-use Studip\Cache\Factory;
-use Studip\LTI13a\PublicationValidator;
-use Studip\LTI13a\ToolManager;
-use Studip\LTI13a\RegistrationManager;
-use OAT\Library\Lti1p3Core\Security\Oidc\OidcInitiator;
-use OAT\Library\Lti1p3Core\Security\Nonce\NonceRepository;
-use OAT\Library\Lti1p3Core\Security\Key\KeyChainRepository;
-use OAT\Library\Lti1p3Core\Security\Jwks\Exporter\JwksExporter;
-use OAT\Library\Lti1p3Core\Security\Jwks\Server\JwksRequestHandler;
-use OAT\Library\Lti1p3Core\Message\Launch\Validator\Tool\ToolLaunchValidator;
-use OAT\Library\Lti1p3Core\Security\Oidc\Server\OidcInitiationRequestHandler;
-use Studip\LTI13a\UserEnrollment;
-use Studip\OAuth2\NegotiatesWithPsr7;
-use Trails\Dispatcher;
-
-class Enrol_LtiController extends AuthenticatedController
-{
- use NegotiatesWithPsr7;
- protected $allow_nobody = true;
- protected $with_session = false;
-
- public function __construct(Dispatcher $dispatcher)
- {
- $action = basename(get_route());
- if (in_array($action, ['launch'])) {
- $this->with_session = true;
- }
- parent::__construct($dispatcher);
- }
-
- public function jwks_action(): void
- {
- $keyChainRepo = new KeyChainRepository();
- $toolKeyring = ToolManager::getKeyring();
-
- $keyChainRepo->addKeyChain($toolKeyring->toKeyChain());
- $handler = new JwksRequestHandler(new JwksExporter($keyChainRepo));
- $this->renderPsrResponse($handler->handle($toolKeyring->range_id));
- }
-
- public function auth_init_action(): void
- {
- $oidcInitHandler = new OidcInitiationRequestHandler(
- new OidcInitiator(
- new RegistrationManager()
- )
- );
-
- $response = $oidcInitHandler->handle($this->getPsrRequest());
- $this->renderPsrResponse($response);
- }
-
- public function launch_action(): void
- {
- $validator = new ToolLaunchValidator(
- new RegistrationManager(),
- new NonceRepository(Factory::getCache())
- );
-
- $request = $validator->validatePlatformOriginatingLaunch($this->getPsrRequest());
-
- if ($request->hasError()) {
- dd($request->getError());
- }
-
- dd(Request::getInstance());
-
- try {
- $validator = new ToolLaunchValidator(
- new RegistrationManager(),
- new NonceRepository(Factory::getCache())
- );
-
- $request = $validator->validatePlatformOriginatingLaunch($this->getPsrRequest());
-
- if ($request->hasError()) {
- throw new LtiException($request->getError());
- }
-
- $publication = $this->getPublication($request);
-
- (new PublicationValidator($publication))->validateLaunch();
-
- $user = (new UserEnrollment($request, $publication))->enroll();
-
- auth()->setAuthenticatedUser($user);
- Metrics::increment('core.login.succeeded');
- sess()->regenerateId(AuthenticationManager::DEFAULT_KEPT_SESSION_VARIABLES);
-
- $this->redirect('course/overview?cid='.$publication->range->id);
- } catch (Throwable $exception) {
- $this->messages = [
- [
- 'type' => 'error',
- 'text' => $exception->getMessage()
- ]
- ];
-
- $this->set_layout($GLOBALS['template_factory']->open('lti/layout'));
- }
- }
-
- public function launch_deeplink_action(): void
- {
- dd('launch_deeplink_action: ', Request::getInstance());
- }
-
- /**
- * @throws LtiExceptionInterface
- */
- private function getPublication(LaunchValidationResultInterface $request): Publication
- {
- $customId = $request->getPayload()->getCustom()['id'] ?? null;
- if (!$customId) {
- throw new LtiException('Missing custom ID');
- }
-
- $publication = Publication::findOneBySQL("publication_key = ?", [$customId]);
- if (!$publication) {
- throw new LtiException('Invalid custom ID');
- }
-
- return $publication;
- }
-
- private function showMessages(array $messages): void
- {
- $this->messages = $messages;
- $this->set_layout($GLOBALS['template_factory']->open('lti/layout'));
- }
-}
diff --git a/app/controllers/enroll/lti.php b/app/controllers/enroll/lti.php
new file mode 100644
index 0000000..83a5cec
--- /dev/null
+++ b/app/controllers/enroll/lti.php
@@ -0,0 +1,228 @@
+<?php
+
+use Lti\Publication;
+use Lti\PublicationUser;
+use Ramsey\Uuid\Uuid;
+use Trails\Dispatcher;
+use Studip\Cache\Factory;
+use Studip\LTI13a\RoleMapper;
+use Studip\LTI13a\ToolManager;
+use Studip\LTI13a\UserEnrollment;
+use Lti\Enum\UserProvisioningMode;
+use Studip\OAuth2\NegotiatesWithPsr7;
+use Studip\LTI13a\PublicationValidator;
+use Studip\LTI13a\RegistrationManager;
+use OAT\Library\Lti1p3Core\Exception\LtiException;
+use OAT\Library\Lti1p3Core\Security\Oidc\OidcInitiator;
+use OAT\Library\Lti1p3Core\Security\Nonce\NonceRepository;
+use OAT\Library\Lti1p3Core\Security\Key\KeyChainRepository;
+use OAT\Library\Lti1p3Core\Exception\LtiExceptionInterface;
+use OAT\Library\Lti1p3Core\Security\Jwks\Exporter\JwksExporter;
+use OAT\Library\Lti1p3Core\Security\Jwks\Server\JwksRequestHandler;
+use OAT\Library\Lti1p3Core\Message\Payload\LtiMessagePayloadInterface;
+use OAT\Library\Lti1p3Core\Message\Launch\Validator\Tool\ToolLaunchValidator;
+use OAT\Library\Lti1p3Core\Security\Oidc\Server\OidcInitiationRequestHandler;
+use OAT\Library\Lti1p3Core\Message\Launch\Validator\Result\LaunchValidationResultInterface;
+
+class Enroll_LtiController extends AuthenticatedController
+{
+ use NegotiatesWithPsr7;
+ protected $allow_nobody = true;
+ protected $with_session = true;
+
+ public function __construct(Dispatcher $dispatcher)
+ {
+ $action = basename(get_route());
+ if (in_array($action, ['jwks', 'auth_init'])) {
+ $this->with_session = false;
+ }
+ parent::__construct($dispatcher);
+ }
+
+ public function jwks_action(): void
+ {
+ $keyChainRepo = new KeyChainRepository();
+ $toolKeyring = ToolManager::getKeyring();
+
+ $keyChainRepo->addKeyChain($toolKeyring->toKeyChain());
+ $handler = new JwksRequestHandler(new JwksExporter($keyChainRepo));
+ $this->renderPsrResponse($handler->handle($toolKeyring->range_id));
+ }
+
+ public function auth_init_action(): void
+ {
+ $oidcInitHandler = new OidcInitiationRequestHandler(
+ new OidcInitiator(
+ new RegistrationManager()
+ )
+ );
+
+ $response = $oidcInitHandler->handle($this->getPsrRequest());
+ $this->renderPsrResponse($response);
+ }
+
+ /**
+ * @throws LtiExceptionInterface
+ */
+ public function launch_action(): void
+ {
+ $validator = new ToolLaunchValidator(
+ new RegistrationManager(),
+ new NonceRepository(Factory::getCache())
+ );
+
+ $request = $validator->validatePlatformOriginatingLaunch($this->getPsrRequest());
+
+ if ($request->hasError()) {
+ throw new LtiException($request->getError());
+ }
+
+ $publication = $this->getPublication($request->getPayload());
+
+ if($publication === null) {
+ throw new LtiException('Missing or invalid custom ID');
+ }
+
+ (new PublicationValidator($publication))->validateLaunch();
+
+ $this->resolveProvisioningMode($request, $publication);
+ }
+
+
+ public function launch_deeplink_action(): void
+ {
+
+ }
+
+ public function provisioning_modes_action(): void
+ {
+ PageLayout::setTitle(_('Bereitstellungsmodus'));
+
+ $this->callbackId = Request::get('callback_id');
+
+ if (empty($_SESSION['callbacks'][$this->callbackId])) {
+ throw new AccessDeniedException('Missing or invalid callback ID');
+ }
+
+ $callbackData = $_SESSION['callbacks'][$this->callbackId];
+ if (
+ $callbackData['context'] !== 'lti'
+ || !isset($callbackData['provisioning_mode'])
+ || $callbackData['expires_at'] < time()
+ ) {
+ throw new AccessDeniedException('Invalid or expired callback data');
+ }
+
+ $this->provisioningMode = (int) $callbackData['provisioning_mode'];
+
+ PageLayout::disableSidebar();
+ PageLayout::setBodyElementId('lti');
+ }
+
+ public function create_new_account_action(): void
+ {
+ CSRFProtection::verifyUnsafeRequest();
+
+ $callbackId = Request::get('callback_id');
+ if (empty($_SESSION['callbacks'][$callbackId])) {
+ throw new AccessDeniedException('Missing or invalid callback ID');
+ }
+
+ $callbackData = $_SESSION['callbacks'][$callbackId];
+ if (
+ $callbackData['context'] !== 'lti'
+ || $callbackData['action'] !== 'enroll_user'
+ || $callbackData['expires_at'] < time()
+ ) {
+ throw new AccessDeniedException('Invalid or expired callback data');
+ }
+
+ $publication = Publication::find($callbackData['publication_id']);
+ $userEnrollment = new UserEnrollment($publication, $callbackData['local_roles'], $callbackData['registration_id']);
+ $userEnrollment
+ ->enroll($callbackData['user_identity'])
+ ->authenticate();
+
+ unset($_SESSION['callbacks'][$callbackId]);
+
+ $this->redirect('course/overview?cid='.$publication->range->id);
+ }
+
+ /**
+ * @throws LtiExceptionInterface
+ */
+ private function resolveProvisioningMode(LaunchValidationResultInterface $request, Publication $publication): void
+ {
+ $authUser = User::findCurrent();
+ if ($authUser) {
+ $this->redirect('course/overview?cid='.$publication->range->id);
+ return;
+ }
+
+ $payload = $request->getPayload();
+ $localRoles = RoleMapper::toLocal($payload->getRoles());
+ $userEnrollment = new UserEnrollment($publication, $localRoles, $request->getRegistration()->getIdentifier());
+
+ $publicationUser = PublicationUser::findOneBySQL(
+ "external_email = :external_email AND external_user_id = :external_user_id AND registration_id = :registration_id",
+ [
+ 'external_email' => $payload->getUserIdentity()->getEmail(),
+ 'external_user_id' => $payload->getUserIdentity()->getIdentifier(),
+ 'registration_id' => $request->getRegistration()->getIdentifier()
+ ]
+ );
+
+ if ($publicationUser) {
+ $userEnrollment
+ ->setUserIdentity($payload->getUserIdentity())
+ ->syncUser($publicationUser->user)
+ ->syncRangeMember()
+ ->authenticate();
+
+ $this->redirect('course/overview?cid='.$publication->range->id);
+ return;
+ }
+
+ // First launch:
+ $publicationConfigs = $publication->getConfigValues();
+ $provisioningMode = match ($localRoles['course'] ?? null) {
+ 'dozent' => (int) $publicationConfigs['provisioning_mode_instructor'],
+ 'autor' => (int) $publicationConfigs['provisioning_mode_student'],
+ default => throw new LtiException('Unsupported LTI role.')
+ };
+
+ if ($provisioningMode === UserProvisioningMode::NewAccountsOnly->value) {
+ $userEnrollment
+ ->enroll($payload->getUserIdentity())
+ ->authenticate();
+
+ $this->redirect('course/overview?cid='.$publication->range->id);
+ return;
+ }
+
+ $callbackId = Uuid::uuid4()->toString();
+ $_SESSION['callbacks'][$callbackId] = [
+ 'user_identity' => $payload->getUserIdentity(),
+ 'registration_id' => $request->getRegistration()->getIdentifier(),
+ 'publication_id' => $publication->id,
+ 'local_roles' => $localRoles,
+ 'provisioning_mode' => $provisioningMode,
+ 'context' => 'lti',
+ 'action' => 'enroll_user',
+ 'expires_at' => time() + 1800
+ ];
+ $_SESSION['redirect_after_login'] = URLHelper::getLink('dispatch.php/course/overview?cid='.$publication->range->id);
+
+ $this->redirect('enroll/lti/provisioning_modes?callback_id=' . $callbackId);
+ }
+
+
+ private function getPublication(LtiMessagePayloadInterface $payload): ?Publication
+ {
+ if (empty($payload->getCustom()['id'])) {
+ return null;
+ }
+
+ return Publication::findOneBySQL("publication_key = ?", [$payload->getCustom()['id']]);
+ }
+}
diff --git a/app/controllers/login.php b/app/controllers/login.php
index af8f3bd..0d42ef4 100644
--- a/app/controllers/login.php
+++ b/app/controllers/login.php
@@ -87,6 +87,7 @@ class LoginController extends AuthenticatedController
);
Metrics::increment('core.login.succeeded');
sess()->regenerateId(\Studip\Authentication\Manager::DEFAULT_KEPT_SESSION_VARIABLES);
+ NotificationCenter::postNotification('Authenticated', $check_auth['user'], ['callback_id' => Request::get('callback_id')]);
$this->redirect('start/index');
return;
}
diff --git a/app/views/enrol/lti/launch.php b/app/views/enrol/lti/launch.php
deleted file mode 100644
index 2a36a50..0000000
--- a/app/views/enrol/lti/launch.php
+++ /dev/null
@@ -1,13 +0,0 @@
-<?php
-/**
- * @var array $messages
- */
-?>
-
-<ul>
- <? foreach ($messages as $message): ?>
- <li>
- <?= $message['text'] ?>
- </li>
- <? endforeach ?>
-</ul>
diff --git a/app/views/enroll/lti/_messages.php b/app/views/enroll/lti/_messages.php
new file mode 100644
index 0000000..932dec6
--- /dev/null
+++ b/app/views/enroll/lti/_messages.php
@@ -0,0 +1,17 @@
+<?php
+/**
+ * @var array $messages
+ */
+?>
+
+<ul class="messages-container">
+ <? foreach ($messages as $message): ?>
+ <li>
+ <? if ($message['type'] == 'error'): ?>
+ <?= MessageBox::error($message['text']) ?>
+ <? else: ?>
+ <?= MessageBox::info($message['text']) ?>
+ <? endif; ?>
+ </li>
+ <? endforeach ?>
+</ul>
diff --git a/app/views/enroll/lti/create_new_account.php b/app/views/enroll/lti/create_new_account.php
new file mode 100644
index 0000000..40450f4
--- /dev/null
+++ b/app/views/enroll/lti/create_new_account.php
@@ -0,0 +1,7 @@
+<?php
+/**
+ * @var array $messages
+ */
+?>
+
+<?= $this->render_partial('enroll/lti/_messages', ['messages' => $messages ?? []]); ?>
diff --git a/app/views/enroll/lti/deeplink_callback.php b/app/views/enroll/lti/deeplink_callback.php
new file mode 100644
index 0000000..f4a5e8d
--- /dev/null
+++ b/app/views/enroll/lti/deeplink_callback.php
@@ -0,0 +1,7 @@
+<?php
+/**
+ * @var Enroll_LtiController $controller
+ */
+?>
+
+<?= $message->toHtmlRedirectForm() ?>
diff --git a/app/views/enroll/lti/launch.php b/app/views/enroll/lti/launch.php
new file mode 100644
index 0000000..40450f4
--- /dev/null
+++ b/app/views/enroll/lti/launch.php
@@ -0,0 +1,7 @@
+<?php
+/**
+ * @var array $messages
+ */
+?>
+
+<?= $this->render_partial('enroll/lti/_messages', ['messages' => $messages ?? []]); ?>
diff --git a/app/views/enroll/lti/provisioning_modes.php b/app/views/enroll/lti/provisioning_modes.php
new file mode 100644
index 0000000..e3809b7
--- /dev/null
+++ b/app/views/enroll/lti/provisioning_modes.php
@@ -0,0 +1,74 @@
+<?php
+/**
+ * @var Enroll_LtiController $controller
+ * @var int $provisioningMode
+ * @var string $callbackId
+ */
+
+use Lti\Enum\UserProvisioningMode;
+
+?>
+
+<?php
+/**
+ * @var array $messages
+ */
+?>
+
+<?= $this->render_partial('enroll/lti/_messages', ['messages' => $messages ?? []]); ?>
+
+<? if(empty($messages)): ?>
+<div class="provisioning-modes">
+ <h1><?= _('Willkommen!') ?></h1>
+ <?= MessageBox::info(_('Es sieht so aus, als wäre dies Ihr erstes Mal hier. Bitte wählen Sie eine der folgenden Kontooptionen aus.')) ?>
+ <br />
+ <ul class="studip-card-container">
+
+ <li class="studip-card">
+ <header class="studip-card__header">
+ <p class="studip-card__title">
+ <?= _('Ich habe bereits ein Konto') ?>
+ </p>
+ </header>
+ <div class="studip-card__body">
+ <?= Icon::create('role') ?>
+ <p class="studip-card__subtitle"><?= _('Bestehendes Konto verwenden') ?></p>
+ <p class="studip-card__description">
+ <?= _('Melde dich an, um dein bestehendes Konto zu verknüpfen.') ?>
+ </p>
+ </div>
+ <footer class="studip-card__footer">
+ <a href="<?= URLHelper::getLink('dispatch.php/login?callback_id=' . $callbackId) ?>" class="button">
+ <?= _('Anmelden') ?>
+ </a>
+ </footer>
+ </li>
+
+ <? if($provisioningMode !== UserProvisioningMode::ExistingAccountsOnly->value): ?>
+ <li class="studip-card">
+ <header class="studip-card__header">
+ <p class="studip-card__title">
+ <?= _('Ich möchte ein neues Konto erstellen') ?>
+ </p>
+ </header>
+ <div class="studip-card__body">
+ <?= Icon::create('add') ?>
+ <p class="studip-card__subtitle"><?= _('Konto erstellen') ?></p>
+ <p class="studip-card__description">
+ <?= _('Mit einem neuen Konto starten') ?>
+ </p>
+ </div>
+ <footer class="studip-card__footer">
+ <form action="<?= $controller->url_for('enroll/lti/create_new_account') ?>" method="post">
+ <?= CSRFProtection::tokenTag() ?>
+ <input type="hidden" name="callback_id" value="<?= $callbackId ?>" />
+ <button type="submit" class="button">
+ <?= _('Ein neues Konto erstellen') ?>
+ </button>
+ </form>
+ </footer>
+ </li>
+ <? endif ?>
+ </ul>
+</div>
+<? endif ?>
diff --git a/app/views/lti/launch/index.php b/app/views/lti/launch/index.php
new file mode 100644
index 0000000..c35b55d
--- /dev/null
+++ b/app/views/lti/launch/index.php
@@ -0,0 +1,47 @@
+<?php
+/**
+ * @var StudipController $controller
+ * @var ?Lti\ResourceLink $resourceLink
+ * @var string $launchUrl
+ * @var array $launchData
+ * @var string $signature
+ * @var string $version
+ * @var \OAT\Library\Lti1p3Core\Message\LtiMessage $message
+ */
+?>
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="UTF-8" />
+ <title data-original="<?= htmlReady(PageLayout::getTitle()) ?>">
+ <?= htmlReady(PageLayout::getTitle() . ' - ' . Config::get()->UNI_NAME_CLEAN) ?>
+ </title>
+ <? if ($version === '1.1') : ?>
+ <script type="text/javascript">
+ document.addEventListener("DOMContentLoaded", function () {
+ document.ltiLaunchForm.submit();
+ });
+ </script>
+ <? endif ?>
+ </head>
+ <body>
+ <? if ($version === '1.3a') : ?>
+ <? if ($message) : ?>
+ <?= $message->toHtmlRedirectForm(Request::submitted('do_not_send') ? false : true) ?>
+ <? else: ?>
+ <?= _('Das LTI-Tool kann nicht aufgerufen werden.') ?>
+ <? endif ?>
+ <? endif ?>
+ <? if ($version === '1.1'): ?>
+ <form name="ltiLaunchForm" method="post" action="<?= htmlReady($launchUrl) ?>">
+ <? foreach ($launchData as $key => $value): ?>
+ <input type="hidden" name="<?= htmlReady($key) ?>" value="<?= htmlReady($value, false) ?>" />
+ <? endforeach ?>
+ <input type="hidden" name="oauth_signature" value="<?= $signature ?>" />
+ <noscript>
+ <button><?= _('Anwendung starten') ?></button>
+ </noscript>
+ </form>
+ <? endif ?>
+ </body>
+</html>