diff --git a/components/ILIAS/LTI/PRIVACY.md b/components/ILIAS/LTI/PRIVACY.md index e13756a37173..1a7e5492da63 100644 --- a/components/ILIAS/LTI/PRIVACY.md +++ b/components/ILIAS/LTI/PRIVACY.md @@ -44,6 +44,7 @@ In Administration > LTI, the privacy-related settings can be configured. - For LTI Advantage tools, the Names and Role Provisioning Services can be activated (“Names and Role Provisioning Services”). A tool then can read the members of the course or group an object of it is in, with their roles (instructor or learner) and their status (active or inactive). Each member is described as a launch of the object describes the user, under the same privacy settings: user id, name and email only as far as “User identification” and “User name” allow. With a random user id per object, only the members who launched the object are given to the tool. ILIAS does not store this list; it is sent to the tool on request. - An LTI Advantage tool can be registered through Dynamic Registration. ILIAS and the tool exchange their configuration (URLs, keys, client id, the scopes and messages the tool uses); no personal data is part of it. ILIAS stores the tool as an own tool of the user who registered it, as if that user had entered it. - When a tool supports Deep Linking, the user who creates an LTI object picks its content in the tool. The request describes the user as a launch does, under the same privacy settings, with the role of an instructor. Each resource link the tool sends back becomes an object with the title, description and URL the tool gave; they hold no personal data. +- While a launch, a Deep Linking request or a Dynamic Registration of an LTI Advantage tool waits for the tool to answer, ILIAS keeps it for the session of the user in the table `lti_pending_requests`. For a launch and a Deep Linking request this is the message for the tool, which describes the user as set out above; the session is only stored as a hash. A request is deleted when the tool answers it, and an expired one (after at most an hour) with the next request of the same kind. - If the provider or tool supports xAPI statements, refer to the PRIVACY.md file for xAPI. Typically, xAPI generates and transmits large amounts of behavioral data, often highly personalized. Proper privacy considerations must be taken into account when using xAPI. ### Data being stored by ILIAS as LTI Tool (Provider) @@ -73,7 +74,7 @@ The log of the component `lti` names the users of each launch and score by their ### Data being deleted -- As LTI Tool (Consumer), the scores and results of the users of an object are deleted with the object when it is deleted from trash. Personal data transferred to a tool can only be deleted within that tool; how much is transferred depends on the privacy settings described above. +- As LTI Tool (Consumer), the scores and results of the users of an object are deleted with the object when it is deleted from trash. The requests waiting for a tool to answer are deleted when it answers them, or with the next request of the same kind once they expired. Personal data transferred to a tool can only be deleted within that tool; how much is transferred depends on the privacy settings described above. - As LTI Tool (Provider), the accounts created for the users of a platform are deleted like any other account, in Administration > Users and Roles. Deleting a platform in Administration > LTI deletes its registration and the releases of objects to it, not the accounts. ### Data being exported diff --git a/components/ILIAS/LTI/README.md b/components/ILIAS/LTI/README.md index 5ca97a01edf2..e11374362ae9 100644 --- a/components/ILIAS/LTI/README.md +++ b/components/ILIAS/LTI/README.md @@ -40,7 +40,7 @@ LTI/ │ │ ├── Consumer/ │ │ └── Provider/ │ ├── LTIAdvantage/ LTI Advantage (LTI 1.3 and its services) -│ │ ├── Common/ Key pair, responses of the endpoints, requests waiting in the session +│ │ ├── Common/ Key pair, responses of the endpoints, requests a session waits for │ │ ├── Platform/ ILIAS launches tools: Launch, DynamicRegistration, DeepLinking, AGS, NRPS │ │ └── Tool/ Platforms launch ILIAS: DynamicRegistration, DeepLinking, NRPS │ └── Setup/ Setup agent and database update steps @@ -103,7 +103,7 @@ the endpoints: * **ILIAS as platform:** `ilLTIAdvantagePlatformConnection` sets up the library for one tool: ILIAS is the platform, with the client id it gave the tool and the id of the tool as deployment id, and the tool is the default tool of the library, with its PEM key or key set URL. A launch starts the OpenID Connect login at - the tool, which gets the id_token from `ltiauth.php`; the login waits in the session and is used once. + the tool, which gets the id_token from `ltiauth.php`; the login waits for the session and is used once. Tools often ask `ltiauth.php` with a POST from their own site, which does not carry a SameSite=Lax cookie, so the page that starts the launch sends the session cookie again as SameSite=None, as `ilLTILoginGUI` does for LTI sessions. An embedded launch also offers the platform storage of LTI (`Platform::getStorageJS()` @@ -119,8 +119,10 @@ the endpoints: URLs of `ltiservices.php` come from the configured HTTP path, because under that script the base URL of the request ends in the script and its path. `ilLTIResponse` ends the requests of the endpoints, of the pages that post a message to the other side and of the modals the tables load asynchronously, and - `ilLTIAdvantagePendingRequests` keeps in the session what waits to be answered: a login, a Deep Linking - request, a Dynamic Registration. + `ilLTIAdvantagePendingRequests` keeps for the session what waits to be answered: a login, a Deep Linking + request, a Dynamic Registration. They are kept in the database (`lti_pending_requests`), one row each, and + not in the session itself: ILIAS writes the whole session back when a request ends, so a request running in + parallel, like the asynchronous ones of the main bar during an embedded launch, would remove them again. * **ILIAS as tool:** `lti.php` takes the OpenID Connect login and the id_token of a platform as well as an LTI 1.1 launch. `ilLTIDataConnector` finds the platform by issuer, client id and deployment id, keeps the key the library fetched from the key set of the platform and the access tokens of its services. The diff --git a/components/ILIAS/LTI/classes/LTIAdvantage/Common/class.ilLTIAdvantagePendingRequestDBStorage.php b/components/ILIAS/LTI/classes/LTIAdvantage/Common/class.ilLTIAdvantagePendingRequestDBStorage.php new file mode 100644 index 000000000000..e08fefed596f --- /dev/null +++ b/components/ILIAS/LTI/classes/LTIAdvantage/Common/class.ilLTIAdvantagePendingRequestDBStorage.php @@ -0,0 +1,88 @@ + + */ +final readonly class ilLTIAdvantagePendingRequestDBStorage implements ilLTIAdvantagePendingRequestStorage +{ + public const string TABLE = 'lti_pending_requests'; + + public function __construct(private ilDBInterface $db) + { + } + + public function read(string $session, string $kind, string $key): ?array + { + $result = $this->db->queryF( + 'SELECT request_data, created FROM ' . self::TABLE . ' WHERE session_id = %s AND kind = %s AND request_key = %s', + [ilDBConstants::T_TEXT, ilDBConstants::T_TEXT, ilDBConstants::T_TEXT], + [$session, $kind, $key] + ); + $row = $this->db->fetchAssoc($result); + if ($row === null) { + return null; + } + $request = json_decode((string) $row['request_data'], true); + + return is_array($request) ? [$request, (int) $row['created']] : null; + } + + public function write(string $session, string $kind, string $key, array $request, int $created): void + { + $this->db->replace( + self::TABLE, + [ + 'session_id' => [ilDBConstants::T_TEXT, $session], + 'kind' => [ilDBConstants::T_TEXT, $kind], + 'request_key' => [ilDBConstants::T_TEXT, $key], + ], + [ + 'request_data' => [ + ilDBConstants::T_CLOB, + json_encode($request, JSON_THROW_ON_ERROR | JSON_INVALID_UTF8_SUBSTITUTE) + ], + 'created' => [ilDBConstants::T_INTEGER, $created], + ] + ); + } + + public function delete(string $session, string $kind, string $key): bool + { + return $this->db->manipulateF( + 'DELETE FROM ' . self::TABLE . ' WHERE session_id = %s AND kind = %s AND request_key = %s', + [ilDBConstants::T_TEXT, ilDBConstants::T_TEXT, ilDBConstants::T_TEXT], + [$session, $kind, $key] + ) > 0; + } + + public function deleteOlderThan(string $kind, int $created): void + { + $this->db->manipulateF( + 'DELETE FROM ' . self::TABLE . ' WHERE kind = %s AND created < %s', + [ilDBConstants::T_TEXT, ilDBConstants::T_INTEGER], + [$kind, $created] + ); + } +} diff --git a/components/ILIAS/LTI/classes/LTIAdvantage/Common/class.ilLTIAdvantagePendingRequests.php b/components/ILIAS/LTI/classes/LTIAdvantage/Common/class.ilLTIAdvantagePendingRequests.php index b38285458bab..6b12544718fa 100644 --- a/components/ILIAS/LTI/classes/LTIAdvantage/Common/class.ilLTIAdvantagePendingRequests.php +++ b/components/ILIAS/LTI/classes/LTIAdvantage/Common/class.ilLTIAdvantagePendingRequests.php @@ -23,17 +23,34 @@ * waiting for its authentication request, a Deep Linking request waiting for its response, a Dynamic * Registration waiting for the tool. Each one is kept by its key for a while, and an expired one is gone. * + * The requests belong to the session, but are not kept in it: ILIAS writes the whole session back when a + * request ends, so a request running in parallel in the same session would remove them again. + * * @author Saúl Díaz */ final readonly class ilLTIAdvantagePendingRequests { private const string CREATED = 'created'; + private ilLTIAdvantagePendingRequestStorage $storage; + private string $session; + /** - * @param int $lifetime seconds a request may wait + * @param string $kind what the requests are, which keeps them apart from the other ones of the session + * @param int $lifetime seconds a request may wait + * @param string|null $session_id the session the requests belong to, the current one by default */ - public function __construct(private string $session_key, private int $lifetime) - { + public function __construct( + private string $kind, + private int $lifetime, + ?ilLTIAdvantagePendingRequestStorage $storage = null, + ?string $session_id = null + ) { + global $DIC; + + $this->storage = $storage ?? new ilLTIAdvantagePendingRequestDBStorage($DIC->database()); + $session_id ??= session_id(); + $this->session = $session_id !== false && $session_id !== '' ? hash('sha256', $session_id) : ''; } /** @@ -41,9 +58,12 @@ public function __construct(private string $session_key, private int $lifetime) */ public function add(string $key, array $request): void { - $requests = $this->getAll(); - $requests[$key] = [self::CREATED => time()] + $request; - ilSession::set($this->session_key, $requests); + if ($this->session === '') { + return; + } + $now = time(); + $this->storage->deleteOlderThan($this->kind, $now - $this->lifetime); + $this->storage->write($this->session, $this->kind, $this->hashKey($key), $request, $now); } /** @@ -51,34 +71,42 @@ public function add(string $key, array $request): void */ public function get(string $key): ?array { - return $this->getAll()[$key] ?? null; + if ($this->session === '') { + return null; + } + $kept = $this->storage->read($this->session, $this->kind, $this->hashKey($key)); + + return $this->isWaiting($kept) ? [self::CREATED => $kept[1]] + $kept[0] : null; } /** - * Ends the request, answered or not. + * Ends the request, answered or not. Only one of several requests taking it at the same time gets it. * * @return array|null the request, null when it was not waiting */ public function take(string $key): ?array { - $requests = $this->getAll(); - $request = $requests[$key] ?? null; - unset($requests[$key]); - ilSession::set($this->session_key, $requests); + if ($this->session === '') { + return null; + } + $kept = $this->storage->read($this->session, $this->kind, $this->hashKey($key)); + if ($kept === null || !$this->storage->delete($this->session, $this->kind, $this->hashKey($key))) { + return null; + } - return $request; + return $this->isWaiting($kept) ? [self::CREATED => $kept[1]] + $kept[0] : null; + } + + private function hashKey(string $key): string + { + return hash('sha256', $key); } /** - * @return array the requests still waiting, by key + * @param array{0: array, 1: int}|null $kept */ - private function getAll(): array + private function isWaiting(?array $kept): bool { - $requests = ilSession::get($this->session_key); - - return array_filter( - is_array($requests) ? $requests : [], - fn($request): bool => is_array($request) && ($request[self::CREATED] ?? 0) >= time() - $this->lifetime - ); + return $kept !== null && $kept[1] >= time() - $this->lifetime; } } diff --git a/components/ILIAS/LTI/classes/LTIAdvantage/Common/interface.ilLTIAdvantagePendingRequestStorage.php b/components/ILIAS/LTI/classes/LTIAdvantage/Common/interface.ilLTIAdvantagePendingRequestStorage.php new file mode 100644 index 000000000000..37e7285bda94 --- /dev/null +++ b/components/ILIAS/LTI/classes/LTIAdvantage/Common/interface.ilLTIAdvantagePendingRequestStorage.php @@ -0,0 +1,50 @@ + + */ +interface ilLTIAdvantagePendingRequestStorage +{ + /** + * @return array{0: array, 1: int}|null the request and when it was kept, null when there is none + */ + public function read(string $session, string $kind, string $key): ?array; + + /** + * Keeps the request, replacing one with the same session, kind and key. + * + * @param array $request + */ + public function write(string $session, string $kind, string $key, array $request, int $created): void; + + /** + * @return bool whether there was a request to remove + */ + public function delete(string $session, string $kind, string $key): bool; + + /** + * Removes the requests of the kind kept before the given time, of all sessions. + */ + public function deleteOlderThan(string $kind, int $created): void; +} diff --git a/components/ILIAS/LTI/classes/LTIAdvantage/Platform/DeepLinking/class.ilLTIAdvantagePlatformDeepLinking.php b/components/ILIAS/LTI/classes/LTIAdvantage/Platform/DeepLinking/class.ilLTIAdvantagePlatformDeepLinking.php index 593cd2497f0a..f7fb2bf8e069 100644 --- a/components/ILIAS/LTI/classes/LTIAdvantage/Platform/DeepLinking/class.ilLTIAdvantagePlatformDeepLinking.php +++ b/components/ILIAS/LTI/classes/LTIAdvantage/Platform/DeepLinking/class.ilLTIAdvantagePlatformDeepLinking.php @@ -27,7 +27,7 @@ * LTI Advantage Deep Linking with ILIAS as platform: a user who creates LTI objects in a container picks the * content in the tool, and each resource link the tool sends back becomes an object. * - * A request waits in the session of the user, for that user and container, and is used once: its state goes + * A request waits for the session of the user, for that user and container, and is used once: its state goes * to the tool as the data of the request and has to come back with the response. The session cookie is sent * as SameSite=None when the request starts, because the tool posts the response from its own site. * diff --git a/components/ILIAS/LTI/classes/LTIAdvantage/Platform/DynamicRegistration/class.ilLTIAdvantagePlatformRegistration.php b/components/ILIAS/LTI/classes/LTIAdvantage/Platform/DynamicRegistration/class.ilLTIAdvantagePlatformRegistration.php index e878c723a924..77ade5465f4e 100644 --- a/components/ILIAS/LTI/classes/LTIAdvantage/Platform/DynamicRegistration/class.ilLTIAdvantagePlatformRegistration.php +++ b/components/ILIAS/LTI/classes/LTIAdvantage/Platform/DynamicRegistration/class.ilLTIAdvantagePlatformRegistration.php @@ -31,7 +31,7 @@ * ltiregistration.php, with the token, from its server: that request has no session of ILIAS, so the token * carries who registers and the client id the tool gets. The tool is stored as an own tool of that user, and * the token is used up with it. When the tool says it is done, the page of the user takes the tool the - * registration left in the session and creates the object for it. + * registration left for the session and creates the object for it. * * @author Saúl Díaz */ diff --git a/components/ILIAS/LTI/classes/LTIAdvantage/Platform/class.ilLTIAdvantagePlatformConnection.php b/components/ILIAS/LTI/classes/LTIAdvantage/Platform/class.ilLTIAdvantagePlatformConnection.php index 6f3b01818b45..91e2c9761c08 100644 --- a/components/ILIAS/LTI/classes/LTIAdvantage/Platform/class.ilLTIAdvantagePlatformConnection.php +++ b/components/ILIAS/LTI/classes/LTIAdvantage/Platform/class.ilLTIAdvantagePlatformConnection.php @@ -36,7 +36,7 @@ * the library, with its key and URLs. The library does the protocol: the OpenID Connect login, the * id_token, the check of the client assertion of a token request and the access token. * - * The login a launch starts is kept in the session of the user until the tool asks for the id_token at + * The login a launch starts is kept for the session of the user until the tool asks for the id_token at * ltiauth.php. Unlike the library, an authentication request without such a login is refused. * * An embedded launch offers the tool the platform storage of LTI (postMessage to the page around the @@ -166,11 +166,13 @@ public function verifyClientAssertion(string $assertion): bool /** * The page that starts the launch of an object: it sends the browser to the login initiation URL of - * the tool. The launch itself waits in the session for the authentication request of the tool. + * the tool. The launch itself waits for the authentication request of the tool, under a hint of its own, + * so launches of the same object running at the same time, in two tabs for one, do not replace each other. * * @param array $parameters see ilLTIAdvantagePlatformLaunchParameterBuilder * @param bool $embedded true when the tool opens in an iframe of the page that handles the platform storage * @param string $url the target link of the object, when it has its own instead of the one of the tool + * @throws RandomException */ public function getLaunchPage(int $ref_id, array $parameters, bool $embedded, string $url = ''): string { @@ -178,7 +180,7 @@ public function getLaunchPage(int $ref_id, array $parameters, bool $embedded, st $url !== '' ? $url : Tool::$defaultTool->messageUrl, self::MESSAGE_LAUNCH, $parameters, - (string) $ref_id, + $ref_id . '.' . bin2hex(random_bytes(8)), $embedded ); } diff --git a/components/ILIAS/LTI/classes/Setup/class.ilLTIDatabaseUpdateSteps.php b/components/ILIAS/LTI/classes/Setup/class.ilLTIDatabaseUpdateSteps.php index 89d8710a3f7c..e7cbd43045f9 100644 --- a/components/ILIAS/LTI/classes/Setup/class.ilLTIDatabaseUpdateSteps.php +++ b/components/ILIAS/LTI/classes/Setup/class.ilLTIDatabaseUpdateSteps.php @@ -517,4 +517,23 @@ public function step_34(): void ]); } } + + /** + * The requests of LTI Advantage a session waits to be answered: logins, Deep Linking requests and Dynamic + * Registrations. They are not kept in the session, which a parallel request of it writes back later. + */ + public function step_35(): void + { + if (!$this->db->tableExists('lti_pending_requests')) { + $this->db->createTable('lti_pending_requests', [ + 'session_id' => ['type' => 'text', 'length' => 64, 'notnull' => true], + 'kind' => ['type' => 'text', 'length' => 64, 'notnull' => true], + 'request_key' => ['type' => 'text', 'length' => 64, 'notnull' => true], + 'request_data' => ['type' => 'clob', 'notnull' => true], + 'created' => ['type' => 'integer', 'length' => 4, 'notnull' => true, 'default' => 0] + ]); + $this->db->addPrimaryKey('lti_pending_requests', ['session_id', 'kind', 'request_key']); + $this->db->addIndex('lti_pending_requests', ['kind', 'created'], 'i1'); + } + } } diff --git a/components/ILIAS/LTI/tests/ilLTIAdvantagePendingRequestsTest.php b/components/ILIAS/LTI/tests/ilLTIAdvantagePendingRequestsTest.php index 52a36ab3f6c2..1b26cdb45c15 100644 --- a/components/ILIAS/LTI/tests/ilLTIAdvantagePendingRequestsTest.php +++ b/components/ILIAS/LTI/tests/ilLTIAdvantagePendingRequestsTest.php @@ -25,16 +25,59 @@ */ final class ilLTIAdvantagePendingRequestsTest extends TestCase { - private const string SESSION_KEY = 'lti_test_pending'; + private const string KIND = 'lti_test_pending'; + private const string SESSION = 'session1'; + + private ilLTIAdvantagePendingRequestStorage $storage; protected function setUp(): void { - $_SESSION = []; + $this->storage = new class () implements ilLTIAdvantagePendingRequestStorage { + /** @var array */ + public array $rows = []; + + public function read(string $session, string $kind, string $key): ?array + { + return $this->rows[$session . '|' . $kind . '|' . $key] ?? null; + } + + public function write(string $session, string $kind, string $key, array $request, int $created): void + { + $this->rows[$session . '|' . $kind . '|' . $key] = [$request, $created]; + } + + public function delete(string $session, string $kind, string $key): bool + { + $exists = isset($this->rows[$session . '|' . $kind . '|' . $key]); + unset($this->rows[$session . '|' . $kind . '|' . $key]); + + return $exists; + } + + public function deleteOlderThan(string $kind, int $created): void + { + foreach ($this->rows as $id => $row) { + if (explode('|', $id)[1] === $kind && $row[1] < $created) { + unset($this->rows[$id]); + } + } + } + }; + } + + private function requests(string $kind = self::KIND, string $session = self::SESSION): ilLTIAdvantagePendingRequests + { + return new ilLTIAdvantagePendingRequests($kind, 600, $this->storage, $session); + } + + private function keep(string $key, array $request, int $created, string $session = self::SESSION): void + { + $this->storage->write(hash('sha256', $session), self::KIND, hash('sha256', $key), $request, $created); } public function testAddedRequestIsWaiting(): void { - $requests = new ilLTIAdvantagePendingRequests(self::SESSION_KEY, 600); + $requests = $this->requests(); $requests->add('state1', ['nonce' => 'n1', 'ref_id' => 42]); $request = $requests->get('state1'); @@ -45,7 +88,7 @@ public function testAddedRequestIsWaiting(): void public function testUnknownRequestIsNotWaiting(): void { - $requests = new ilLTIAdvantagePendingRequests(self::SESSION_KEY, 600); + $requests = $this->requests(); $requests->add('state1', ['nonce' => 'n1']); $this->assertNull($requests->get('state2')); @@ -53,7 +96,7 @@ public function testUnknownRequestIsNotWaiting(): void public function testTakenRequestIsNoLongerWaiting(): void { - $requests = new ilLTIAdvantagePendingRequests(self::SESSION_KEY, 600); + $requests = $this->requests(); $requests->add('state1', ['nonce' => 'n1']); $requests->add('state2', ['nonce' => 'n2']); @@ -65,50 +108,61 @@ public function testTakenRequestIsNoLongerWaiting(): void public function testExpiredRequestIsGone(): void { - $_SESSION[self::SESSION_KEY] = [ - 'old' => ['created' => time() - 601, 'nonce' => 'n1'], - 'new' => ['created' => time() - 10, 'nonce' => 'n2'], - ]; - $requests = new ilLTIAdvantagePendingRequests(self::SESSION_KEY, 600); + $this->keep('old', ['nonce' => 'n1'], time() - 601); + $this->keep('new', ['nonce' => 'n2'], time() - 10); + $requests = $this->requests(); $this->assertNull($requests->get('old')); $this->assertNull($requests->take('old')); $this->assertSame('n2', $requests->get('new')['nonce']); } - public function testExpiredRequestsAreRemovedFromTheSession(): void + public function testExpiredRequestsOfAllSessionsAreRemovedWhenOneIsAdded(): void { - $_SESSION[self::SESSION_KEY] = [ - 'old' => ['created' => time() - 601, 'nonce' => 'n1'], - ]; - $requests = new ilLTIAdvantagePendingRequests(self::SESSION_KEY, 600); - $requests->add('new', ['nonce' => 'n2']); + $this->keep('old', ['nonce' => 'n1'], time() - 601); + $this->keep('other', ['nonce' => 'n3'], time() - 601, 'session2'); + $this->requests()->add('new', ['nonce' => 'n2']); - $this->assertSame(['new'], array_keys($_SESSION[self::SESSION_KEY])); + $this->assertCount(1, $this->storage->rows); + $this->assertSame('n2', $this->requests()->get('new')['nonce']); } - public function testMalformedSessionDataIsIgnored(): void + public function testKindsKeepRequestsApart(): void { - $_SESSION[self::SESSION_KEY] = [ - 'string' => 'not a request', - 'undated' => ['nonce' => 'n1'], - ]; - $requests = new ilLTIAdvantagePendingRequests(self::SESSION_KEY, 600); + $logins = $this->requests(self::KIND . '_login'); + $deep_links = $this->requests(self::KIND . '_deep_linking'); + $logins->add('state1', ['nonce' => 'n1']); - $this->assertNull($requests->get('string')); - $this->assertNull($requests->get('undated')); + $this->assertNull($deep_links->get('state1')); + $this->assertSame('n1', $logins->get('state1')['nonce']); + } - $_SESSION[self::SESSION_KEY] = 'not a list'; - $this->assertNull($requests->get('string')); + public function testSessionsKeepRequestsApart(): void + { + $this->requests()->add('state1', ['nonce' => 'n1']); + + $this->assertNull($this->requests(self::KIND, 'session2')->get('state1')); + $this->assertNull($this->requests(self::KIND, 'session2')->take('state1')); + $this->assertSame('n1', $this->requests()->get('state1')['nonce']); } - public function testSessionKeysKeepRequestsApart(): void + public function testRequestsOfTheSameSessionDoNotReplaceEachOther(): void { - $logins = new ilLTIAdvantagePendingRequests(self::SESSION_KEY . '_login', 600); - $deep_links = new ilLTIAdvantagePendingRequests(self::SESSION_KEY . '_deep_linking', 600); - $logins->add('state1', ['nonce' => 'n1']); + // two requests of one session, each with its own instance as in two parallel requests of ILIAS + $this->requests()->add('launch1', ['nonce' => 'n1']); + $this->requests()->add('launch2', ['nonce' => 'n2']); - $this->assertNull($deep_links->get('state1')); - $this->assertSame('n1', $logins->get('state1')['nonce']); + $this->assertSame('n1', $this->requests()->take('launch1')['nonce']); + $this->assertSame('n2', $this->requests()->take('launch2')['nonce']); + } + + public function testNothingIsKeptWithoutSession(): void + { + $requests = $this->requests(self::KIND, ''); + $requests->add('state1', ['nonce' => 'n1']); + + $this->assertSame([], $this->storage->rows); + $this->assertNull($requests->get('state1')); + $this->assertNull($requests->take('state1')); } }