Skip to content

Commit

Permalink
Add support for Client Side postMessages
Browse files Browse the repository at this point in the history
  • Loading branch information
spvickers committed Nov 26, 2022
1 parent a23587e commit b96ff03
Show file tree
Hide file tree
Showing 4 changed files with 539 additions and 47 deletions.
164 changes: 164 additions & 0 deletions src/Platform.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ class Platform
'LtiStartAssessment'
);

/**
* Name of browser storage frame.
*
* @var string|null $browserStorageFrame
*/
public static $browserStorageFrame = null;

/**
* Local name of platform.
*
Expand Down Expand Up @@ -538,6 +545,155 @@ public static function fromRecordId($id, $dataConnector)
return $platform;
}

/**
* Get the JavaScript for handling storage postMessages from a tool.
*
* @return string The JavaScript to handle storage postMessages
*/
public static function getStorageJS()
{
$javascript = <<< EOD
(function () {
let storageData = {};
window.addEventListener('message', function (event) {
let ok = true;
if (typeof event.data !== "object") {
ok = false;
event.source.postMessage({
subject: '.response',
message_id: 0,
error: {
code: 'bad_request',
message: 'Event data is not an object'
}
}, event.origin);
}
let messageid = '';
if (event.data.message_id) {
messageid = event.data.message_id;
}
if (!event.data.subject) {
ok = false;
event.source.postMessage({
subject: '.response',
message_id: messageid,
error: {
code: 'bad_request',
message: 'There is no subject specified'
}
}, event.origin);
} else if (!event.data.message_id) {
ok = false;
event.source.postMessage({
subject: event.data.subject + '.response',
message_id: messageid,
error: {
code: 'bad_request',
message: 'There is no message ID specified'
}
}, event.origin);
}
if (ok) {
switch (event.data.subject) {
case 'lti.capabilities':
event.source.postMessage({
subject: 'lti.capabilities.response',
message_id: event.data.message_id,
supported_messages: [
{
subject: 'lti.capabilities'
},
{
subject: 'lti.get_data'
},
{
subject: 'lti.put_data'
}
]
}, event.origin);
break;
case 'lti.put_data':
if (!event.data.key) {
event.source.postMessage({
subject: event.data.subject + '.response',
message_id: messageid,
error: {
code: 'bad_request',
message: 'There is no key specified'
}
}, event.origin);
} else if (!event.data.value) {
event.source.postMessage({
subject: event.data.subject + '.response',
message_id: messageid,
error: {
code: 'bad_request',
message: 'There is no value specified'
}
}, event.origin);
} else {
if (!storageData[event.origin]) {
storageData[event.origin] = {};
}
storageData[event.origin][event.data.key] = event.data.value;
event.source.postMessage({
subject: 'lti.put_data.response',
message_id: event.data.message_id,
key: event.data.key,
value: event.data.value
}, event.origin);
}
break;
case 'lti.get_data':
if (!event.data.key) {
event.source.postMessage({
subject: event.data.subject + '.response',
message_id: messageid,
error: {
code: 'bad_request',
message: 'There is no key specified'
}
}, event.origin);
} else if (storageData[event.origin] && storageData[event.origin][event.data.key]) {
event.source.postMessage({
subject: 'lti.get_data.response',
message_id: event.data.message_id,
key: event.data.key,
value: storageData[event.origin][event.data.key]
}, event.origin);
} else {
console.log('There is no value stored with origin/key of \'' + event.origin + '/' + event.data.key + '\'');
event.source.postMessage({
subject: 'lti.get_data.response',
message_id: event.data.message_id,
error: {
code: 'bad_request',
message: 'There is no value stored for this key'
}
}, event.origin);
}
break;
default:
event.source.postMessage({
subject: event.data.subject + '.response',
message_id: event.data.message_id,
error: {
code: 'unsupported_subject',
message: 'Subject \'' + event.data.subject + '\' not recognised'
}
}, event.origin);
break;
}
}
}, false);
})();
EOD;

return $javascript;
}

###
### PROTECTED METHODS
###
Expand Down Expand Up @@ -709,6 +865,14 @@ private function handleAuthenticationRequest()
if (isset($parameters['state'])) {
$this->messageParameters['state'] = $parameters['state'];
}
if (!empty(static::$browserStorageFrame)) {
if (strpos($parameters['redirect_uri'], '?') === FALSE) {
$sep = '?';
} else {
$sep = '&';
}
$parameters['redirect_uri'] .= "{$sep}lti_storage_target=" . static::$browserStorageFrame;
}
$html = Util::sendForm($parameters['redirect_uri'], $this->messageParameters);
echo $html;
exit;
Expand Down
42 changes: 31 additions & 11 deletions src/System.php
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,14 @@ public function signMessage(&$url, $type, $version, $params, $loginHint = null,
if (!empty(Tool::$defaultTool)) {
$url = Tool::$defaultTool->initiateLoginUrl;
}
if (!empty(static::$browserStorageFrame)) {
if (strpos($url, '?') === FALSE) {
$sep = '?';
} else {
$sep = '&';
}
$url .= "{$sep}lti_storage_target=" . static::$browserStorageFrame;
}
} else {
$params = $this->signParameters($url, $type, $version, $params);
}
Expand Down Expand Up @@ -912,7 +920,7 @@ private function parseMessage($strictMode, $disableCookieCheck, $generateWarning
if (!$this->ok) {
$this->reason = 'Message does not contain a valid JWT';
} else {
$this->ok = $this->jwt->hasClaim('iss') && $this->jwt->hasClaim('aud') &&
$this->ok = $this->jwt->hasClaim('iss') && $this->jwt->hasClaim('aud') && $this->jwt->hasClaim('nonce') &&
$this->jwt->hasClaim(Util::JWT_CLAIM_PREFIX . '/claim/deployment_id');
if ($this->ok) {
$iss = $this->jwt->getClaim('iss');
Expand Down Expand Up @@ -955,21 +963,32 @@ private function parseMessage($strictMode, $disableCookieCheck, $generateWarning
$this->ok = !empty($this->rawParameters['state']);
if ($this->ok) {
$state = $this->rawParameters['state'];
$parts = explode('.', $state);
if (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1]) &&
($parts[1] !== 'platformStorage')) { // Reset to original session
session_abort();
session_id($parts[1]);
session_start();
$this->onResetSessionId();
}
$usePlatformStorage = (substr($state, -16) === '.platformStorage');
if ($usePlatformStorage) {
$state = substr($state, 0, -16);
}
$this->onAuthenticate($state, $this->jwt->getClaim('nonce'), $usePlatformStorage);
if (!$disableCookieCheck) {
$parts = explode('.', $state);
if (empty($_COOKIE) && !isset($_POST['_new_window'])) { // Reopen in a new window
Util::setTestCookie();
$_POST['_new_window'] = '';
echo Util::sendForm($_SERVER['REQUEST_URI'], $_POST, '_blank');
exit;
} elseif (!empty(session_id()) && (count($parts) > 1) && (session_id() !== $parts[1])) { // Reset to original session
session_abort();
session_id($parts[1]);
session_start();
$this->onResetSessionId();
}
Util::setTestCookie(true);
}
} else {
$this->reason = 'state parameter is missing';
}
if ($this->ok) {
$nonce = new PlatformNonce($this->platform, $state);
$this->ok = $nonce->load();
if (!$this->ok) {
Expand All @@ -985,20 +1004,21 @@ private function parseMessage($strictMode, $disableCookieCheck, $generateWarning
if ($this->ok) {
$this->ok = $nonce->delete();
}
if (!$this->ok) {
$this->reason = 'state parameter is invalid or has expired';
}
}
}
}
$this->messageParameters = array();
if ($this->ok) {
$this->messageParameters = array();
$this->messageParameters['oauth_consumer_key'] = $aud;
$this->messageParameters['oauth_signature_method'] = $this->jwt->getHeader('alg');
$this->parseClaims($strictMode, $generateWarnings);
} else {
$this->reason = 'state parameter is invalid or missing';
}
}
} else {
$this->reason = 'iss, aud and/or deployment_id claim not found';
$this->reason = 'iss, aud, deployment_id and/or nonce claim not found';
}
}
} catch (\Exception $e) {
Expand Down
Loading

0 comments on commit b96ff03

Please sign in to comment.