6use ceLTIc\LTI\MediaType;
62 'basic-lti-launch-request',
63 'ConfigureLaunchRequest',
65 'ContentItemSelectionRequest',
66 'ContentItemUpdateRequest',
67 'LtiSubmissionReviewRequest',
68 'ToolProxyRegistrationRequest',
78 private static $LTI_CONSUMER_SETTING_NAMES = array(
'custom_tc_profile_url',
'custom_system_setting_url',
'custom_oauth2_access_token_url');
85 private static $LTI_CONTEXT_SETTING_NAMES = array(
'custom_context_setting_url',
86 'ext_ims_lis_memberships_id',
'ext_ims_lis_memberships_url',
87 'custom_context_memberships_url',
'custom_context_memberships_v2_url',
88 'custom_context_group_sets_url',
'custom_context_groups_url',
89 'custom_lineitems_url',
'custom_ags_scopes'
97 private static $LTI_RESOURCE_LINK_SETTING_NAMES = array(
'lis_result_sourcedid',
'lis_outcome_service_url',
98 'ext_ims_lis_basic_outcome_url',
'ext_ims_lis_resultvalue_sourcedids',
'ext_outcome_data_values_accepted',
99 'ext_ims_lis_memberships_id',
'ext_ims_lis_memberships_url',
100 'ext_ims_lti_tool_setting',
'ext_ims_lti_tool_setting_id',
'ext_ims_lti_tool_setting_url',
101 'custom_link_setting_url',
'custom_link_memberships_url',
102 'custom_lineitems_url',
'custom_lineitem_url',
'custom_ags_scopes',
111 private static $LTI_RETAIN_SETTING_NAMES = array(
'custom_lineitem_url');
118 private static $CUSTOM_SUBSTITUTION_VARIABLES = array(
'User.id' =>
'user_id',
119 'User.image' =>
'user_image',
120 'User.username' =>
'username',
121 'User.scope.mentor' =>
'role_scope_mentor',
122 'Membership.role' =>
'roles',
123 'Person.sourcedId' =>
'lis_person_sourcedid',
124 'Person.name.full' =>
'lis_person_name_full',
125 'Person.name.family' =>
'lis_person_name_family',
126 'Person.name.given' =>
'lis_person_name_given',
127 'Person.name.middle' =>
'lis_person_name_middle',
128 'Person.email.primary' =>
'lis_person_contact_email_primary',
129 'Context.id' =>
'context_id',
130 'Context.type' =>
'context_type',
131 'Context.title' =>
'context_title',
132 'Context.label' =>
'context_label',
133 'CourseOffering.sourcedId' =>
'lis_course_offering_sourcedid',
134 'CourseSection.sourcedId' =>
'lis_course_section_sourcedid',
135 'CourseSection.label' =>
'context_label',
136 'CourseSection.title' =>
'context_title',
137 'ResourceLink.id' =>
'resource_link_id',
138 'ResourceLink.title' =>
'resource_link_title',
139 'ResourceLink.description' =>
'resource_link_description',
140 'Result.sourcedId' =>
'lis_result_sourcedid',
141 'BasicOutcome.url' =>
'lis_outcome_service_url',
142 'ToolConsumerProfile.url' =>
'custom_tc_profile_url',
143 'ToolProxy.url' =>
'tool_proxy_url',
144 'ToolProxy.custom.url' =>
'custom_system_setting_url',
145 'ToolProxyBinding.custom.url' =>
'custom_context_setting_url',
146 'LtiLink.custom.url' =>
'custom_link_setting_url',
147 'LineItems.url' =>
'custom_lineitems_url',
148 'LineItem.url' =>
'custom_lineitem_url',
149 'ToolProxyBinding.memberships.url' =>
'custom_context_memberships_url',
150 'ToolProxyBinding.nrps.url' =>
'custom_context_memberships_v2_url',
151 'LtiLink.memberships.url' =>
'custom_link_memberships_url',
152 'LtiLink.acs.url' =>
'custom_ap_acs_url'
373 private $constraints =
null;
398 $this->secret =
null;
399 $this->messageUrl =
null;
400 $this->initiateLoginUrl =
null;
401 $this->redirectionUris =
null;
402 $this->rsaKey =
null;
403 $this->signatureMethod =
'HMAC-SHA1';
404 $this->encryptionMethod =
null;
405 $this->ltiVersion =
null;
406 $this->settings = array();
407 $this->enabled =
false;
408 $this->enableFrom =
null;
409 $this->enableUntil =
null;
410 $this->lastAccess =
null;
411 $this->created =
null;
412 $this->updated =
null;
413 $this->constraints = array();
416 $this->requiredServices = array();
417 $this->optionalServices = array();
418 $this->resourceHandlers = array();
426 public function save()
428 return $this->dataConnector->saveTool($this);
436 public function delete()
438 return $this->dataConnector->deleteTool($this);
450 public function getMessageParameters($strictMode =
false, $disableCookieCheck =
false, $generateWarnings =
false)
452 if (is_null($this->messageParameters)) {
453 $this->parseMessage($strictMode, $disableCookieCheck, $generateWarnings);
456 $this->debugMode = (isset($this->messageParameters[
'custom_debug']) &&
457 (strtolower($this->messageParameters[
'custom_debug']) ===
'true'));
458 if ($this->debugMode) {
463 if (!empty($this->messageParameters[
'lti_message_type']) &&
464 (($this->messageParameters[
'lti_message_type'] ===
'ContentItemSelectionRequest') || ($this->messageParameters[
'lti_message_type'] ===
'ContentItemUpdateRequest')) &&
465 !empty($this->messageParameters[
'content_item_return_url'])) {
466 $this->returnUrl = $this->messageParameters[
'content_item_return_url'];
468 if (empty($this->returnUrl) && !empty($this->messageParameters[
'launch_presentation_return_url'])) {
469 $this->returnUrl = $this->messageParameters[
'launch_presentation_return_url'];
483 public function handleRequest($strictMode =
false, $disableCookieCheck =
false, $generateWarnings =
false)
486 if ($this->debugMode) {
489 if ($_SERVER[
'REQUEST_METHOD'] ===
'HEAD') {
491 } elseif (isset($parameters[
'iss']) && (strlen($parameters[
'iss']) > 0)) {
493 if (!isset($parameters[
'login_hint']) || (strlen($parameters[
'login_hint']) <= 0)) {
495 $this->reason =
'Missing login_hint parameter.';
496 } elseif (!isset($parameters[
'target_link_uri']) || (strlen($parameters[
'target_link_uri']) <= 0)) {
498 $this->reason =
'Missing target_link_uri parameter.';
500 $this->ok = $this->sendAuthenticationRequest($parameters, $disableCookieCheck);
502 } elseif (isset($parameters[
'openid_configuration']) && (strlen($parameters[
'openid_configuration']) > 0)) {
508 if ($this->ok && $this->authenticate($strictMode, $disableCookieCheck, $generateWarnings)) {
509 if (empty($this->output)) {
511 if ($this->ok && ($this->messageParameters[
'lti_message_type'] ===
'ToolProxyRegistrationRequest')) {
512 $this->platform->save();
518 $errorMessage =
"Request failed with reason: '{$this->reason}'";
519 if (!empty($this->details)) {
520 $errorMessage .= PHP_EOL .
'Debug information:';
521 foreach ($this->details as $detail) {
522 $errorMessage .= PHP_EOL .
" {$detail}";
542 $this->constraints[
$name] = array(
'required' => $required,
'max_length' => $maxLength,
'messages' => $messageTypes);
556 Util::logDebug(
'Method ceLTIc\LTI\Tool::getConsumers() has been deprecated; please use ceLTIc\LTI\Tool::getPlatforms() instead.',
568 return $this->dataConnector->getPlatforms();
582 $services = $this->platform->profile->service_offered;
583 if (is_array($services)) {
585 foreach ($services as $service) {
587 if (!is_array($service->format) || !in_array($format, $service->format)) {
591 foreach ($methods as $method) {
592 if (!is_array($service->action) || !in_array($method, $service->action)) {
593 $missing[] = $method;
597 if (count($methods) <= 0) {
615 $toolProxyService = $this->
findService(
'application/vnd.ims.lti.v2.toolproxy+json', array(
'POST'));
618 $http = $this->platform->doServiceRequest($toolProxyService,
'POST',
'application/vnd.ims.lti.v2.toolproxy+json',
619 json_encode($toolProxy));
620 $ok = $http->ok && ($http->status === 201) && !empty($http->responseJson->tool_proxy_guid);
622 $this->platform->setKey($http->responseJson->tool_proxy_guid);
623 $this->platform->secret = $toolProxy->security_contract->shared_secret;
624 $this->platform->toolProxy = json_encode($toolProxy);
625 $this->platform->save();
643 public static function sendForm($url, $params, $target =
'')
645 Util::logDebug(
'Method ceLTIc\LTI\Tool::sendForm() has been deprecated; please use ceLTIc\LTI\Util::sendForm() instead.',
659 $this->reason =
'No onLaunch method found for tool.';
668 $this->reason =
'No onConfigure method found for tool.';
677 $this->reason =
'No onDashboard method found for tool.';
686 $this->reason =
'No onContentItem method found for tool.';
695 $this->reason =
'No onContentItemUpdate method found for tool.';
704 $this->reason =
'No onSubmissionReview method found for tool.';
713 $this->reason =
'No onRegister method found for tool.';
725 $registrationConfig = $this->
sendRegistration($platformConfig, $toolConfig);
739 $this->reason =
'No onLtiStartProctoring method found for tool.';
748 $this->reason =
'No onLtiEndAssessment method found for tool.';
758 protected function onInitiateLogin($requestParameters, &$authParameters)
760 $hasSession = !empty(session_id());
764 $_SESSION[
'ceLTIc_lti_authentication_request'] = array(
765 'state' => $authParameters[
'state'],
766 'nonce' => $authParameters[
'nonce']
769 session_write_close();
780 protected function onAuthenticate($state, $nonce, $usePlatformStorage)
782 $hasSession = !empty(session_id());
786 $parts = explode(
'.', $state);
787 if (!isset($this->rawParameters[
'_storage_check']) && $usePlatformStorage) {
788 $this->rawParameters[
'_storage_check'] =
'';
789 $javascript = $this->getStorageJS(
'lti.get_data', $state,
'');
790 echo
Util::sendForm($_SERVER[
'REQUEST_URI'], $this->rawParameters,
'', $javascript);
792 } elseif (isset($this->rawParameters[
'_storage_check'])) {
793 if (!empty(($this->rawParameters[
'_storage_check']))) {
795 $parts = explode(
'.', $this->rawParameters[
'_storage_check']);
796 if ((count($parts) !== 2) || ($parts[0] !== $state) || ($parts[1] !== $nonce)) {
798 $this->reason =
'Invalid state and/or nonce values';
802 $this->reason =
'Error accessing platform storage';
804 } elseif (isset($_SESSION[
'ceLTIc_lti_authentication_request'])) {
805 $auth = $_SESSION[
'ceLTIc_lti_authentication_request'];
806 if (substr($state, -16) ===
'.platformStorage') {
807 $state = substr($state, 0, -16);
809 if (($state !== $auth[
'state']) || ($nonce !== $auth[
'nonce'])) {
811 $this->reason =
'Invalid state parameter value and/or nonce claim value';
813 unset($_SESSION[
'ceLTIc_lti_authentication_request']);
816 session_write_close();
845 $this->ok = !empty($parameters[
'openid_configuration']);
847 $http =
new HttpMessage($parameters[
'openid_configuration']);
848 $this->ok = $http->send();
851 $this->ok = !empty($platformConfig);
854 $this->reason =
'Unable to access platform configuration details.';
857 $this->reason =
'Invalid registration request: missing openid_configuration parameter.';
860 $this->ok = !empty($platformConfig[
'registration_endpoint']) && !empty($platformConfig[
'jwks_uri']) && !empty($platformConfig[
'authorization_endpoint']) &&
861 !empty($platformConfig[
'token_endpoint']) && !empty($platformConfig[
'https://purl.imsglobal.org/spec/lti-platform-configuration']) &&
862 !empty($platformConfig[
'claims_supported']) && !empty($platformConfig[
'scopes_supported']) &&
863 !empty($platformConfig[
'id_token_signing_alg_values_supported']) &&
864 !empty($platformConfig[
'https://purl.imsglobal.org/spec/lti-platform-configuration'][
'product_family_code']) &&
865 !empty($platformConfig[
'https://purl.imsglobal.org/spec/lti-platform-configuration'][
'version']) &&
866 !empty($platformConfig[
'https://purl.imsglobal.org/spec/lti-platform-configuration'][
'messages_supported']);
868 $this->reason =
'Invalid platform configuration details.';
872 $jwtClient = Jwt::getJwtClient();
873 $algorithms = \array_intersect($jwtClient::getSupportedAlgorithms(),
874 $platformConfig[
'id_token_signing_alg_values_supported']);
875 $this->ok = !empty($algorithms);
877 rsort($platformConfig[
'id_token_signing_alg_values_supported']);
879 $this->reason =
'None of the signature algorithms offered by the platform is supported.';
884 $platformConfig =
null;
887 return $platformConfig;
899 $claimsMapping = array(
901 'Person.name.full' =>
'name',
902 'Person.name.given' =>
'given_name',
903 'Person.name.middle' =>
'middle_name',
904 'Person.name.family' =>
'family_name',
905 'Person.email.primary' =>
'email'
907 $toolName = (!empty($this->product->name)) ? $this->product->name :
'Unnamed tool';
908 $toolDescription = (!empty($this->product->description)) ? $this->product->description :
'';
909 $oauthRequest = OAuth\OAuthRequest::from_request();
910 $toolUrl = $oauthRequest->get_normalized_http_url();
911 $pos = strpos($toolUrl,
'//');
912 $domain = substr($toolUrl, $pos + 2);
913 $domain = substr($domain, 0, strpos($domain,
'/'));
914 $claimsSupported = $platformConfig[
'claims_supported'];
915 $messagesSupported = array();
916 foreach ($platformConfig[
'https://purl.imsglobal.org/spec/lti-platform-configuration'][
'messages_supported'] as
$message) {
917 $messagesSupported[] =
$message[
'type'];
919 $scopesSupported = $platformConfig[
'scopes_supported'];
922 $claims = array(
'iss');
923 $variables = array();
924 $constants = array();
925 $redirectUris = array();
926 foreach ($this->resourceHandlers as $resourceHandler) {
927 if (empty($iconUrl)) {
928 $iconUrl = $resourceHandler->icon;
930 foreach (array_merge($resourceHandler->optionalMessages, $resourceHandler->requiredMessages) as
$message) {
935 $capabilities = array();
936 if ($type ===
'LtiResourceLinkRequest') {
937 $toolUrl =
"{$this->baseUrl}{$message->path}";
938 $redirectUris[] = $toolUrl;
939 $capabilities =
$message->capabilities;
940 $variables = array_merge($variables,
$message->variables);
941 $constants = array_merge($constants,
$message->constants);
942 }
else if (in_array($type, $messagesSupported)) {
943 $redirectUris[] =
"{$this->baseUrl}{$message->path}";
944 $capabilities =
$message->capabilities;
945 $variables = array_merge(
$message->variables, $variables);
946 $constants = array_merge(
$message->constants, $constants);
949 'target_link_uri' =>
"{$this->baseUrl}{$message->path}",
953 foreach ($capabilities as $capability) {
954 if (array_key_exists($capability, $claimsMapping) && in_array($claimsMapping[$capability], $claimsSupported)) {
955 $claims[] = $claimsMapping[$capability];
960 if (empty($redirectUris)) {
961 $redirectUris = array($toolUrl);
963 $redirectUris = array_unique($redirectUris);
965 if (!empty($claims)) {
966 $claims = array_unique($claims);
969 foreach ($constants as
$name => $value) {
970 $custom[
$name] = $value;
972 foreach ($variables as
$name => $value) {
973 $custom[
$name] =
'$' . $value;
975 $toolConfig = array();
976 $toolConfig[
'application_type'] =
'web';
977 $toolConfig[
'client_name'] = $toolName;
978 $toolConfig[
'response_types'] = array(
'id_token');
979 $toolConfig[
'grant_types'] = array(
'implicit',
'client_credentials');
980 $toolConfig[
'initiate_login_uri'] = $toolUrl;
981 $toolConfig[
'redirect_uris'] = $redirectUris;
983 $toolConfig[
'token_endpoint_auth_method'] =
'private_key_jwt';
984 $toolConfig[
'https://purl.imsglobal.org/spec/lti-tool-configuration'] = array(
986 'target_link_uri' => $toolUrl,
987 'custom_parameters' => $custom,
989 'messages' => $messages,
990 'description' => $toolDescription
992 $toolConfig[
'scope'] = implode(
' ', array_intersect($this->requiredScopes, $scopesSupported));
993 if (!empty($iconUrl)) {
994 $toolConfig[
'logo_uri'] =
"{$this->baseUrl}{$iconUrl}";
1012 $body = json_encode($toolConfig);
1013 $headers =
"Content-type: application/json; charset=UTF-8";
1014 if (!empty($parameters[
'registration_token'])) {
1015 $headers .=
"\nAuthorization: Bearer {$parameters['registration_token']}";
1017 $http =
new HttpMessage($platformConfig[
'registration_endpoint'],
'POST', $body, $headers);
1018 $this->ok = $http->send();
1021 $this->ok = !empty($registrationConfig);
1024 $this->reason =
'Unable to register with platform.';
1028 $registrationConfig =
null;
1031 return $registrationConfig;
1045 $domain = $platformConfig[
'issuer'];
1046 $pos = strpos($domain,
'//');
1047 if ($pos !==
false) {
1048 $domain = substr($domain, $pos + 2);
1049 $pos = strpos($domain,
'/');
1050 if ($pos !==
false) {
1051 $domain = substr($domain, 0, $pos);
1054 $this->platform =
new Platform($this->dataConnector);
1055 $this->platform->name = $domain;
1057 $this->platform->signatureMethod = reset($platformConfig[
'id_token_signing_alg_values_supported']);
1058 $this->platform->platformId = $platformConfig[
'issuer'];
1059 $this->platform->clientId = $registrationConfig[
'client_id'];
1060 $this->platform->deploymentId = $registrationConfig[
'https://purl.imsglobal.org/spec/lti-tool-configuration'][
'deployment_id'];
1061 $this->platform->authenticationUrl = $platformConfig[
'authorization_endpoint'];
1062 $this->platform->accessTokenUrl = $platformConfig[
'token_endpoint'];
1063 $this->platform->jku = $platformConfig[
'jwks_uri'];
1065 $this->ok = $this->platform->save();
1067 $this->reason =
'Sorry, an error occurred when saving the platform details.';
1082 if (!empty($this->platform)) {
1084 if (!$this->platform->enabled) {
1085 $enabled =
', but it will need to be enabled by the tool provider before it can be used';
1086 }
else if (!empty($this->platform->enableFrom) && ($this->platform->enableFrom > $now)) {
1087 $enabled =
', but you will only have access from ' . date(
'j F Y H:i T', $this->platform->enableFrom);
1088 if (!empty($this->platform->enableUntil)) {
1089 $enabled .=
' until ' . date(
'j F Y H:i T', $this->platform->enableUntil);
1091 }
else if (!empty($this->platform->enableUntil)) {
1092 if ($this->platform->enableUntil > $now) {
1093 $enabled =
', but you will only have access until ' . date(
'j F Y H:i T', $this->platform->enableUntil);
1095 $enabled =
', but your access was set to end at ' . date(
'j F Y H:i T', $this->platform->enableUntil);
1103 <meta http-equiv=
"content-type" content=
"text/html; charset=UTF-8">
1104 <title>LTI
Tool registration</title>
1112 background-color: #d4edda;
1113 border-color: #c3e6cb;
1115 padding: .75rem 1.25rem;
1116 margin-bottom: 1rem;
1120 background-color: #f8d7da;
1121 border-color: #f5c6cb;
1123 padding: .75rem 1.25rem;
1124 margin-bottom: 1rem;
1130 border: 1px solid transparent;
1131 padding: 0.375rem 0.75rem;
1134 border-radius: 0.25rem;
1136 background-color: #007bff;
1137 border-color: #007bff;
1139 text-decoration: none;
1140 display:
inline-block;
1144 <script language=
"javascript" type=
"text/javascript">
1145 function doClose(el) {
1146 (window.opener || window.parent).postMessage({subject:
'org.imsglobal.lti.close'},
'*');
1152 <h1>{$toolConfig[
'client_name']} registration</h1>
1158 The tool registration was successful{
$enabled}.
1161 <button type=
"button" onclick=
"return doClose();">Close</button>
1168 Sorry, the registration was not successful: {$this->reason}
1177 $this->output = $html;
1195 if (
$ok && $autoEnable) {
1196 $tool->enabled =
true;
1218 $tool->enabled =
true;
1236 $tool->setRecordId($id);
1249 private function result()
1257 if (!empty($this->returnUrl)) {
1259 if (!is_null($this->platform) && isset($this->messageParameters[
'lti_message_type']) &&
1260 (($this->messageParameters[
'lti_message_type'] ===
'ContentItemSelectionRequest') ||
1261 ($this->messageParameters[
'lti_message_type'] ===
'ContentItemUpdateRequest'))) {
1262 $formParams = array();
1263 if ($this->debugMode && !is_null($this->reason)) {
1264 $formParams[
'lti_errormsg'] =
"Debug error: {$this->reason}";
1267 if (!is_null($this->reason)) {
1268 $formParams[
'lti_errorlog'] =
"Debug error: {$this->reason}";
1271 if (isset($this->messageParameters[
'data'])) {
1272 $formParams[
'data'] = $this->messageParameters[
'data'];
1274 $this->ltiVersion = (isset($this->messageParameters[
'lti_version'])) ? $this->messageParameters[
'lti_version'] :
Util::LTI_VERSION1;
1275 $page = $this->
sendMessage($errorUrl,
'ContentItemSelection', $formParams);
1278 if (strpos($errorUrl,
'?') ===
false) {
1283 if ($this->debugMode && !is_null($this->reason)) {
1284 $errorUrl .=
'lti_errormsg=' . urlencode(
"Debug error: {$this->reason}");
1286 $errorUrl .=
'lti_errormsg=' . urlencode($this->message);
1287 if (!is_null($this->reason)) {
1288 $errorUrl .=
'<i_errorlog=' . urlencode(
"Debug error: {$this->reason}");
1291 header(
"Location: {$errorUrl}");
1295 if (!is_null($this->errorOutput)) {
1297 } elseif ($this->debugMode && !empty($this->reason)) {
1298 echo
"Debug error: {$this->reason}";
1300 echo
"Error: {$this->message}";
1304 } elseif (!is_null($this->redirectUrl)) {
1305 header(
"Location: {$this->redirectUrl}");
1307 } elseif (!is_null($this->output)) {
1324 private function authenticate($strictMode, $disableCookieCheck, $generateWarnings)
1326 $doSavePlatform =
false;
1328 if (($this->ok || $generateWarnings) && !empty($this->jwt) && !empty($this->jwt->hasJwt())) {
1329 if ($this->jwt->hasClaim(
'sub') && (strlen($this->jwt->getClaim(
'sub')) <= 0)) {
1330 $this->setError(
'Empty sub claim', $strictMode, $generateWarnings);
1332 if (!empty($this->jwt->getClaim(
'https://purl.imsglobal.org/spec/lti/claim/context',
'')) &&
1333 empty($this->messageParameters[
'context_id'])) {
1334 $this->setError(
'Missing id property in https://purl.imsglobal.org/spec/lti/claim/context claim', $strictMode,
1336 } elseif (!empty($this->jwt->getClaim(
'https://purl.imsglobal.org/spec/lti/claim/tool_platform',
'')) &&
1337 empty($this->messageParameters[
'tool_consumer_instance_guid'])) {
1338 $this->setError(
'Missing guid property in https://purl.imsglobal.org/spec/lti/claim/tool_platform claim',
1339 $strictMode, $generateWarnings);
1342 if ($this->ok || $generateWarnings) {
1343 if ($this->messageParameters[
'lti_message_type'] ===
'basic-lti-launch-request') {
1344 if ($this->ok && (!isset($this->messageParameters[
'resource_link_id']) || (strlen(trim($this->messageParameters[
'resource_link_id'])) <= 0))) {
1346 $this->reason =
'Missing resource link ID.';
1349 if (!isset($this->messageParameters[
'roles'])) {
1350 $this->setError(
'Missing roles parameter.', $strictMode, $generateWarnings);
1351 } elseif (!empty($this->messageParameters[
'roles']) && empty(array_intersect(self::parseRoles($this->messageParameters[
'roles'],
1353 $this->setError(
'No principal role found in roles parameter.', $strictMode, $generateWarnings);
1356 } elseif (($this->messageParameters[
'lti_message_type'] ===
'ContentItemSelectionRequest') ||
1357 ($this->messageParameters[
'lti_message_type'] ===
'ContentItemUpdateRequest')) {
1358 $isUpdate = ($this->messageParameters[
'lti_message_type'] ===
'ContentItemUpdateRequest');
1363 if (isset($this->messageParameters[
'accept_media_types']) && (strlen(trim($this->messageParameters[
'accept_media_types'])) > 0)) {
1364 $mediaTypes = array_map(
'trim', explode(
',', $this->messageParameters[
'accept_media_types']));
1369 $this->setError(
'Missing or empty accept_media_types parameter.', $strictMode, $generateWarnings);
1373 if (!$this->checkValue($this->messageParameters[
'accept_media_types'],
1374 array(Item::LTI_LINK_MEDIA_TYPE, Item::LTI_ASSIGNMENT_MEDIA_TYPE),
1375 'Invalid value in accept_media_types parameter: \'%s\'.', $strictMode, $generateWarnings,
true)) {
1378 } elseif (!$this->checkValue($this->messageParameters[
'accept_types'],
1379 array(Item::TYPE_LTI_LINK, Item::TYPE_LTI_ASSIGNMENT),
1380 'Invalid value in accept_types parameter: \'%s\'.', $strictMode, $generateWarnings,
true)) {
1386 if (strpos($mediaType,
'application/vnd.ims.lti.') !== 0) {
1389 if (($mediaType ===
'text/html') || ($mediaType ===
'*/*')) {
1392 } elseif ((strpos($mediaType,
'image/') === 0) || ($mediaType ===
'*/*')) {
1394 } elseif ($mediaType === Item::LTI_LINK_MEDIA_TYPE) {
1396 } elseif ($mediaType === Item::LTI_ASSIGNMENT_MEDIA_TYPE) {
1405 if (isset($this->messageParameters[
'accept_presentation_document_targets']) &&
1406 (strlen(trim($this->messageParameters[
'accept_presentation_document_targets'])) > 0)) {
1408 explode(
',', $this->messageParameters[
'accept_presentation_document_targets']));
1412 $this->setError(
'Missing or empty accept_presentation_document_targets parameter.', $strictMode,
1415 if (empty($this->jwt) || !$this->jwt->hasJwt()) {
1416 $permittedTargets = array(
'embed',
'frame',
'iframe',
'window',
'popup',
'overlay',
'none');
1418 $permittedTargets = array(
'embed',
'iframe',
'window');
1421 if (!$this->checkValue($documentTarget, $permittedTargets,
1422 'Invalid value in accept_presentation_document_targets parameter: \'%s\'.', $strictMode,
1423 $generateWarnings,
true)) {
1429 $this->setError(
'No accept_presentation_document_targets parameter found.', $strictMode, $generateWarnings);
1431 if ($this->ok || $generateWarnings) {
1432 if (empty($this->messageParameters[
'content_item_return_url'])) {
1433 $this->setError(
'Missing content_item_return_url parameter.',
true, $generateWarnings);
1442 } elseif ($this->messageParameters[
'lti_message_type'] ===
'LtiSubmissionReviewRequest') {
1443 if (!isset($this->messageParameters[
'custom_lineitem_url']) && (strlen(trim($this->messageParameters[
'custom_lineitem_url'])) > 0)) {
1444 $this->setError(
'Missing LineItem service URL.',
true, $generateWarnings);
1446 if (!isset($this->messageParameters[
'for_user_id']) && (strlen(trim($this->messageParameters[
'for_user_id'])) > 0)) {
1447 $this->setError(
'Missing ID of \'for user\'',
true, $generateWarnings);
1449 if (($this->ok || $generateWarnings) && ($this->messageParameters[
'lti_version'] ===
Util::LTI_VERSION1P3)) {
1450 if (!isset($this->messageParameters[
'roles'])) {
1451 $this->setError(
'Missing roles parameter.', $strictMode, $generateWarnings);
1454 } elseif ($this->messageParameters[
'lti_message_type'] ===
'ToolProxyRegistrationRequest') {
1455 if ((!isset($this->messageParameters[
'reg_key']) ||
1456 (strlen(trim($this->messageParameters[
'reg_key'])) <= 0)) ||
1457 (!isset($this->messageParameters[
'reg_password']) ||
1458 (strlen(trim($this->messageParameters[
'reg_password'])) <= 0)) ||
1459 (!isset($this->messageParameters[
'tc_profile_url']) ||
1460 (strlen(trim($this->messageParameters[
'tc_profile_url'])) <= 0)) ||
1461 (!isset($this->messageParameters[
'launch_presentation_return_url']) ||
1462 (strlen(trim($this->messageParameters[
'launch_presentation_return_url'])) <= 0))) {
1463 $this->setError(
'Missing message parameters.',
true, $generateWarnings);
1465 } elseif ($this->messageParameters[
'lti_message_type'] ===
'LtiStartProctoring') {
1466 if (!isset($this->messageParameters[
'resource_link_id']) || (strlen(trim($this->messageParameters[
'resource_link_id'])) <= 0)) {
1467 $this->setError(
'Missing resource link ID.',
true, $generateWarnings);
1469 if (!isset($this->messageParameters[
'custom_ap_attempt_number']) || (strlen(trim($this->messageParameters[
'custom_ap_attempt_number'])) <= 0) ||
1470 !is_numeric($this->messageParameters[
'custom_ap_attempt_number'])) {
1471 $this->setError(
'Missing or invalid value for attempt number.',
true, $generateWarnings);
1473 if ((!isset($this->messageParameters[
'user_id']) || (strlen(trim($this->messageParameters[
'user_id'])) <= 0)) &&
1474 !isset($this->messageParameters[
'relaunch_url'])) {
1475 $this->setError(
'Empty user ID.',
true, $generateWarnings);
1479 if ($this->ok || $generateWarnings) {
1480 if (isset($this->messageParameters[
'role_scope_mentor'])) {
1481 if (!isset($this->messageParameters[
'roles']) ||
1482 !in_array(
'urn:lti:role:ims/lis/Mentor', self::parseRoles($this->messageParameters[
'roles']))) {
1483 $this->setError(
'Found role_scope_mentor parameter without a Mentor role.', $strictMode, $generateWarnings);
1488 if (($this->ok || $generateWarnings) && ($this->messageParameters[
'lti_message_type'] !==
'ToolProxyRegistrationRequest')) {
1490 if (!isset($this->messageParameters[
'oauth_consumer_key'])) {
1491 $this->setError(
'Missing consumer key.',
true, $generateWarnings);
1493 if (is_null($this->platform->created)) {
1494 if (empty($this->jwt) || !$this->jwt->hasJwt()) {
1495 $reason =
"Consumer key not recognised: '{$this->messageParameters['oauth_consumer_key']}'.";
1497 $reason =
"Platform not recognised (Platform ID | Client ID | Deployment ID): '{$this->messageParameters['platform_id']}' | '{$this->messageParameters['oauth_consumer_key']}' | '{$this->messageParameters['deployment_id']}'.";
1499 $this->setError(
$reason,
true, $generateWarnings);
1502 if ($this->messageParameters[
'oauth_signature_method'] !== $this->platform->signatureMethod) {
1503 $this->platform->signatureMethod = $this->messageParameters[
'oauth_signature_method'];
1504 $doSavePlatform =
true;
1506 $today = date(
'Y-m-d', $now);
1507 if (is_null($this->platform->lastAccess)) {
1508 $doSavePlatform =
true;
1510 $last = date(
'Y-m-d', $this->platform->lastAccess);
1511 $doSavePlatform = $doSavePlatform || ($last !== $today);
1513 $this->platform->lastAccess = $now;
1517 if ($this->platform->protected) {
1518 if (!is_null($this->platform->consumerGuid)) {
1519 $this->ok = empty($this->messageParameters[
'tool_consumer_instance_guid']) || ($this->platform->consumerGuid === $this->messageParameters[
'tool_consumer_instance_guid']);
1521 $this->reason =
'Request is from an invalid platform.';
1524 $this->ok = isset($this->messageParameters[
'tool_consumer_instance_guid']);
1526 $this->reason =
'A platform GUID must be included in the launch request as this configuration is protected.';
1531 $this->ok = $this->platform->enabled;
1533 $this->reason =
'Platform has not been enabled by the tool.';
1537 $this->ok = is_null($this->platform->enableFrom) || ($this->platform->enableFrom <= $now);
1539 $this->ok = is_null($this->platform->enableUntil) || ($this->platform->enableUntil > $now);
1541 $this->reason =
'Platform access has expired.';
1544 $this->reason =
'Platform access is not yet available.';
1549 if ($this->ok || $generateWarnings) {
1550 if (isset($this->messageParameters[
'context_type'])) {
1551 $context_types = explode(
',', $this->messageParameters[
'context_type']);
1552 $permitted_types = array(
'CourseTemplate',
'CourseOffering',
'CourseSection',
'Group',
1553 'urn:lti:context-type:ims/lis/CourseTemplate',
'urn:lti:context-type:ims/lis/CourseOffering',
'urn:lti:context-type:ims/lis/CourseSection',
'urn:lti:context-type:ims/lis/Group');
1555 $permitted_types = array_merge($permitted_types,
1556 array(
'http://purl.imsglobal.org/vocab/lis/v2/course#CourseTemplate',
'http://purl.imsglobal.org/vocab/lis/v2/course#CourseOffering',
'http://purl.imsglobal.org/vocab/lis/v2/course#CourseSection',
'http://purl.imsglobal.org/vocab/lis/v2/course#Group'));
1559 foreach ($context_types as $context_type) {
1560 $found = in_array(trim($context_type), $permitted_types);
1566 $this->setError(sprintf(
'No valid value found in context_type parameter: \'%s\'.',
1567 $this->messageParameters[
'context_type']), $strictMode, $generateWarnings);
1570 if (($this->ok || $generateWarnings) && (($this->messageParameters[
'lti_message_type'] ===
'ContentItemSelectionRequest') ||
1571 ($this->messageParameters[
'lti_message_type'] ===
'ContentItemUpdateRequest'))) {
1572 $isUpdate = ($this->messageParameters[
'lti_message_type'] ===
'ContentItemUpdateRequest');
1573 if (isset($this->messageParameters[
'accept_unsigned']) &&
1574 !$this->checkValue($this->messageParameters[
'accept_unsigned'], array(
'true',
'false'),
1575 'Invalid value for accept_unsigned parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1578 if (isset($this->messageParameters[
'accept_multiple'])) {
1580 if (!$this->checkValue($this->messageParameters[
'accept_multiple'], array(
'true',
'false'),
1581 'Invalid value for accept_multiple parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1584 } elseif (!$this->checkValue($this->messageParameters[
'accept_multiple'], array(
'false'),
1585 'Invalid value for accept_multiple parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1589 if (isset($this->messageParameters[
'accept_copy_advice'])) {
1591 if (!$this->checkValue($this->messageParameters[
'accept_copy_advice'], array(
'true',
'false'),
1592 'Invalid value for accept_copy_advice parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1595 } elseif (!$this->checkValue($this->messageParameters[
'accept_copy_advice'], array(
'false'),
1596 'Invalid value for accept_copy_advice parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1600 if (isset($this->messageParameters[
'auto_create']) &&
1601 !$this->checkValue($this->messageParameters[
'auto_create'], array(
'true',
'false'),
1602 'Invalid value for auto_create parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1605 if (isset($this->messageParameters[
'can_confirm']) &&
1606 !$this->checkValue($this->messageParameters[
'can_confirm'], array(
'true',
'false'),
1607 'Invalid value for can_confirm parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1611 if (isset($this->messageParameters[
'launch_presentation_document_target'])) {
1612 if (!$this->checkValue($this->messageParameters[
'launch_presentation_document_target'],
1613 array(
'embed',
'frame',
'iframe',
'window',
'popup',
'overlay'),
1614 'Invalid value for launch_presentation_document_target parameter: \'%s\'.', $strictMode,
1615 $generateWarnings,
true)) {
1618 if (($this->messageParameters[
'lti_message_type'] ===
'LtiStartProctoring') &&
1619 ($this->messageParameters[
'launch_presentation_document_target'] !==
'window')) {
1620 if (isset($this->messageParameters[
'launch_presentation_height']) ||
1621 isset($this->messageParameters[
'launch_presentation_width'])) {
1622 $this->setError(
'Height and width parameters must only be included for the window document target.',
1623 $strictMode, $generateWarnings);
1630 if ($this->ok && ($this->messageParameters[
'lti_message_type'] ===
'ToolProxyRegistrationRequest')) {
1633 $this->reason =
'Invalid lti_version parameter.';
1636 $url = $this->messageParameters[
'tc_profile_url'];
1637 if (strpos($url,
'?') ===
false) {
1643 $http =
new HttpMessage($url,
'GET',
null,
'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');
1644 $this->ok = $http->send();
1646 $this->reason =
'Platform profile not accessible.';
1649 $this->ok = !is_null($tcProfile);
1651 $this->reason =
'Invalid JSON in platform profile.';
1658 $this->platform->profile = $tcProfile;
1659 $capabilities = $this->platform->profile->capability_offered;
1661 foreach ($this->resourceHandlers as $resourceHandler) {
1662 foreach ($resourceHandler->requiredMessages as
$message) {
1663 if (!in_array(
$message->type, $capabilities)) {
1668 foreach ($this->constraints as
$name => $constraint) {
1669 if ($constraint[
'required']) {
1670 if (empty(array_intersect($capabilities,
1671 array_keys(array_intersect(self::$CUSTOM_SUBSTITUTION_VARIABLES, array(
$name)))))) {
1672 $missing[
$name] =
true;
1676 if (!empty($missing)) {
1678 $this->reason =
'Required capability not offered - \'' . implode(
'\', \
'', array_keys($missing)) .
'\'.
';
1682// Check for required services
1684 foreach ($this->requiredServices as $service) {
1685 foreach ($service->formats as $format) {
1686 if (!$this->findService($format, $service->actions)) {
1688 $this->reason = 'Required service(s) not offered -
';
1691 $this->reason .= ',
';
1693 $this->reason .= "'{$format}
' [" . implode(',
', $service->actions) . '].
';
1699 if ($this->messageParameters['lti_message_type
'] === 'ToolProxyRegistrationRequest
') {
1700 $this->platform->profile = $tcProfile;
1701 $this->platform->secret = $this->messageParameters['reg_password
'];
1702 $this->platform->ltiVersion = $this->messageParameters['lti_version
'];
1703 $this->platform->name = $tcProfile->product_instance->service_owner->service_owner_name->default_value;
1704 $this->platform->consumerName = $this->platform->name;
1705 $this->platform->consumerVersion = "{$tcProfile->product_instance->product_info->product_family->code}-{$tcProfile->product_instance->product_info->product_version}";
1706 $this->platform->consumerGuid = $tcProfile->product_instance->guid;
1707 $this->platform->enabled = true;
1708 $this->platform->protected = true;
1709 $doSavePlatform = true;
1712 } elseif ($this->ok && !empty($this->messageParameters['custom_tc_profile_url
']) && empty($this->platform->profile)) {
1713 $url = $this->messageParameters['custom_tc_profile_url
'];
1714 if (strpos($url, '?
') === false) {
1719 $url .= 'lti_version=
' . $this->messageParameters['lti_version
'];
1720 $http = new HttpMessage($url, 'GET
', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json
');
1721 if ($http->send()) {
1722 $tcProfile = Util::jsonDecode($http->response);
1723 if (!is_null($tcProfile)) {
1724 $this->platform->profile = $tcProfile;
1725 $doSavePlatform = true;
1732// Check if a relaunch is being requested
1733 if (isset($this->messageParameters['relaunch_url
'])) {
1735 if (empty($this->messageParameters['platform_state
'])) {
1737 $this->reason = 'Missing or empty platform_state parameter.
';
1739 $this->ok = $this->sendRelaunchRequest($disableCookieCheck);
1742// Validate message parameter constraints
1743 $invalidParameters = array();
1744 foreach ($this->constraints as $name => $constraint) {
1745 if (empty($constraint['messages
']) || in_array($this->messageParameters['lti_message_type
'],
1746 $constraint['messages
'])) {
1748 if ($constraint['required
']) {
1749 if (!isset($this->messageParameters[$name]) || (strlen(trim($this->messageParameters[$name])) <= 0)) {
1750 $invalidParameters[] = "{$name} (missing)";
1754 if ($ok && !is_null($constraint['max_length
']) && isset($this->messageParameters[$name])) {
1755 if (strlen(trim($this->messageParameters[$name])) > $constraint['max_length
']) {
1756 $invalidParameters[] = "{$name} (too long)";
1761 if (count($invalidParameters) > 0) {
1763 if (empty($this->reason)) {
1764 $this->reason = 'Invalid parameter(s):
' . implode(',
', $invalidParameters) . '.
';
1770// Set the request context
1772 if ($this->hasConfiguredApiHook(self::$CONTEXT_ID_HOOK, $this->platform->getFamilyCode(), $this)) {
1773 $className = $this->getApiHook(self::$CONTEXT_ID_HOOK, $this->platform->getFamilyCode());
1774 $tpHook = new $className($this);
1775 $contextId = $tpHook->getContextId();
1777 if (empty($contextId) && isset($this->messageParameters['context_id
'])) {
1778 $contextId = trim($this->messageParameters['context_id
']);
1780 if (!empty($contextId)) {
1781 $this->context = Context::fromPlatform($this->platform, $contextId);
1783 if (isset($this->messageParameters['context_title
'])) {
1784 $title = trim($this->messageParameters['context_title
']);
1786 if (empty($title)) {
1787 $title = "Course {$this->context->getId()}";
1789 $this->context->title = $title;
1790 if (isset($this->messageParameters['context_type
'])) {
1791 $this->context->type = trim($this->messageParameters['context_type
']);
1792 if (strpos($this->context->type, 'http:
1793 $this->context->type = substr($this->context->type, 46);
1799 if (isset($this->messageParameters[
'resource_link_id'])) {
1800 $contentItemId =
'';
1801 if (isset($this->messageParameters[
'custom_content_item_id'])) {
1802 $contentItemId = $this->messageParameters[
'custom_content_item_id'];
1804 if (empty($this->context)) {
1806 trim($this->messageParameters[
'resource_link_id']), $contentItemId);
1809 trim($this->messageParameters[
'resource_link_id']), $contentItemId);
1812 if (isset($this->messageParameters[
'resource_link_title'])) {
1813 $title = trim($this->messageParameters[
'resource_link_title']);
1815 if (empty($title)) {
1816 $title =
"Resource {$this->resourceLink->getId()}";
1818 $this->resourceLink->title = $title;
1821 foreach ($this->platform->getSettings() as
$name => $value) {
1822 if ((strpos(
$name,
'custom_') === 0) && (!in_array(
$name, self::$LTI_RETAIN_SETTING_NAMES))) {
1823 $this->platform->setSetting(
$name);
1824 $doSavePlatform =
true;
1827 if (!empty($this->context)) {
1828 foreach ($this->context->getSettings() as
$name => $value) {
1829 if ((strpos(
$name,
'custom_') === 0) && (!in_array(
$name, self::$LTI_RETAIN_SETTING_NAMES))) {
1830 $this->context->setSetting(
$name);
1834 if (!empty($this->resourceLink)) {
1835 foreach ($this->resourceLink->getSettings() as
$name => $value) {
1836 if ((strpos(
$name,
'custom_') === 0) && (!in_array(
$name, self::$LTI_RETAIN_SETTING_NAMES))) {
1837 $this->resourceLink->setSetting(
$name);
1842 foreach (self::$LTI_CONSUMER_SETTING_NAMES as
$name) {
1843 if (isset($this->messageParameters[
$name])) {
1844 $this->platform->setSetting(
$name, $this->messageParameters[
$name]);
1845 }
else if (!in_array(
$name, self::$LTI_RETAIN_SETTING_NAMES)) {
1846 $this->platform->setSetting(
$name);
1849 if (!empty($this->context)) {
1850 foreach (self::$LTI_CONTEXT_SETTING_NAMES as
$name) {
1851 if (isset($this->messageParameters[
$name])) {
1852 $this->context->setSetting(
$name, $this->messageParameters[
$name]);
1853 }
else if (!in_array(
$name, self::$LTI_RETAIN_SETTING_NAMES)) {
1854 $this->context->setSetting(
$name);
1858 if (!empty($this->resourceLink)) {
1859 foreach (self::$LTI_RESOURCE_LINK_SETTING_NAMES as
$name) {
1860 if (isset($this->messageParameters[
$name])) {
1861 $this->resourceLink->setSetting(
$name, $this->messageParameters[
$name]);
1862 }
else if (!in_array(
$name, self::$LTI_RETAIN_SETTING_NAMES)) {
1863 $this->resourceLink->setSetting(
$name);
1868 foreach ($this->messageParameters as
$name => $value) {
1869 if ((strpos(
$name,
'custom_') === 0) && !in_array(
$name,
1870 array_merge(self::$LTI_CONSUMER_SETTING_NAMES, self::$LTI_CONTEXT_SETTING_NAMES,
1871 self::$LTI_RESOURCE_LINK_SETTING_NAMES))) {
1872 $this->platform->setSetting(
$name, $value);
1873 if (!empty($this->context)) {
1874 $this->context->setSetting(
$name, $value);
1876 if (!empty($this->resourceLink)) {
1877 $this->resourceLink->setSetting(
$name, $value);
1884 if ($this->hasConfiguredApiHook(self::$USER_ID_HOOK, $this->platform->getFamilyCode(), $this)) {
1885 $className = $this->getApiHook(self::$USER_ID_HOOK, $this->platform->getFamilyCode());
1886 $tpHook =
new $className($this);
1887 $userId = $tpHook->getUserId();
1889 if (empty($userId) && isset($this->messageParameters[
'user_id'])) {
1890 $userId = trim($this->messageParameters[
'user_id']);
1896 $firstname = (isset($this->messageParameters[
'lis_person_name_given'])) ? $this->messageParameters[
'lis_person_name_given'] :
'';
1897 $middlename = (isset($this->messageParameters[
'lis_person_name_middle'])) ? $this->messageParameters[
'lis_person_name_middle'] :
'';
1898 $lastname = (isset($this->messageParameters[
'lis_person_name_family'])) ? $this->messageParameters[
'lis_person_name_family'] :
'';
1899 $fullname = (isset($this->messageParameters[
'lis_person_name_full'])) ? $this->messageParameters[
'lis_person_name_full'] :
'';
1900 $this->userResult->setNames($firstname, $lastname, $fullname);
1903 if (isset($this->messageParameters[
'lis_person_sourcedid'])) {
1904 $this->userResult->sourcedId = $this->messageParameters[
'lis_person_sourcedid'];
1908 if (isset($this->messageParameters[
'ext_username'])) {
1909 $this->userResult->username = $this->messageParameters[
'ext_username'];
1910 } elseif (isset($this->messageParameters[
'ext_user_username'])) {
1911 $this->userResult->username = $this->messageParameters[
'ext_user_username'];
1912 } elseif (isset($this->messageParameters[
'ext_d2l_username'])) {
1913 $this->userResult->username = $this->messageParameters[
'ext_d2l_username'];
1914 } elseif (isset($this->messageParameters[
'custom_username'])) {
1915 $this->userResult->username = $this->messageParameters[
'custom_username'];
1916 } elseif (isset($this->messageParameters[
'custom_user_username'])) {
1917 $this->userResult->username = $this->messageParameters[
'custom_user_username'];
1921 $email = (isset($this->messageParameters[
'lis_person_contact_email_primary'])) ? $this->messageParameters[
'lis_person_contact_email_primary'] :
'';
1922 $this->userResult->setEmail($email, $this->defaultEmail);
1925 if (isset($this->messageParameters[
'user_image'])) {
1926 $this->userResult->image = $this->messageParameters[
'user_image'];
1930 if (isset($this->messageParameters[
'roles'])) {
1931 $this->userResult->roles =
self::parseRoles($this->messageParameters[
'roles'],
1932 $this->messageParameters[
'lti_version']);
1937 if ($this->platform->ltiVersion !== $this->messageParameters[
'lti_version']) {
1938 $this->platform->ltiVersion = $this->messageParameters[
'lti_version'];
1939 $doSavePlatform =
true;
1941 if (isset($this->messageParameters[
'deployment_id'])) {
1942 $this->platform->deploymentId = $this->messageParameters[
'deployment_id'];
1944 if (isset($this->messageParameters[
'tool_consumer_instance_name'])) {
1945 if ($this->platform->consumerName !== $this->messageParameters[
'tool_consumer_instance_name']) {
1946 $this->platform->consumerName = $this->messageParameters[
'tool_consumer_instance_name'];
1947 $doSavePlatform =
true;
1950 if (isset($this->messageParameters[
'tool_consumer_info_product_family_code'])) {
1951 $version = $this->messageParameters[
'tool_consumer_info_product_family_code'];
1952 if (isset($this->messageParameters[
'tool_consumer_info_version'])) {
1953 $version .=
"-{$this->messageParameters['tool_consumer_info_version']}";
1956 if ($this->platform->consumerVersion !== $version) {
1957 $this->platform->consumerVersion = $version;
1958 $doSavePlatform =
true;
1960 } elseif (isset($this->messageParameters[
'ext_lms']) && ($this->platform->consumerName !== $this->messageParameters[
'ext_lms'])) {
1961 $this->platform->consumerVersion = $this->messageParameters[
'ext_lms'];
1962 $doSavePlatform =
true;
1964 if (isset($this->messageParameters[
'tool_consumer_instance_guid'])) {
1965 if (is_null($this->platform->consumerGuid)) {
1966 $this->platform->consumerGuid = $this->messageParameters[
'tool_consumer_instance_guid'];
1967 $doSavePlatform =
true;
1968 } elseif (!$this->platform->protected && ($this->platform->consumerGuid !== $this->messageParameters[
'tool_consumer_instance_guid'])) {
1969 $this->platform->consumerGuid = $this->messageParameters[
'tool_consumer_instance_guid'];
1970 $doSavePlatform =
true;
1973 if (isset($this->messageParameters[
'launch_presentation_css_url'])) {
1974 if ($this->platform->cssPath !== $this->messageParameters[
'launch_presentation_css_url']) {
1975 $this->platform->cssPath = $this->messageParameters[
'launch_presentation_css_url'];
1976 $doSavePlatform =
true;
1978 } elseif (isset($this->messageParameters[
'ext_launch_presentation_css_url']) && ($this->platform->cssPath !== $this->messageParameters[
'ext_launch_presentation_css_url'])) {
1979 $this->platform->cssPath = $this->messageParameters[
'ext_launch_presentation_css_url'];
1980 $doSavePlatform =
true;
1981 } elseif (!empty($this->platform->cssPath)) {
1982 $this->platform->cssPath =
null;
1983 $doSavePlatform =
true;
1988 if ($doSavePlatform) {
1989 $this->platform->save();
1995 if (isset($this->context)) {
1996 $this->context->save();
1999 if (isset($this->resourceLink)) {
2001 $this->resourceLink->save();
2004 $this->userResult->setResourceLinkId($this->resourceLink->getRecordId());
2005 if (isset($this->messageParameters[
'lis_result_sourcedid'])) {
2006 if ($this->userResult->ltiResultSourcedId !== $this->messageParameters[
'lis_result_sourcedid']) {
2007 $this->userResult->ltiResultSourcedId = $this->messageParameters[
'lis_result_sourcedid'];
2008 $this->userResult->save();
2010 } elseif ($this->userResult->isLearner()) {
2011 $this->userResult->ltiResultSourcedId =
'';
2012 $this->userResult->save();
2016 $this->ok = $this->checkForShare();
2030 private function checkForShare()
2033 $doSaveResourceLink =
true;
2035 $id = $this->resourceLink->primaryResourceLinkId;
2037 $shareRequest = isset($this->messageParameters[
'custom_share_key']) && !empty($this->messageParameters[
'custom_share_key']);
2038 if ($shareRequest) {
2039 if (!$this->allowSharing) {
2041 $this->reason =
'Your sharing request has been refused because sharing is not being permitted.';
2044 $shareKey =
new ResourceLinkShareKey($this->resourceLink, $this->messageParameters[
'custom_share_key']);
2045 if (!is_null($shareKey->resourceLinkId)) {
2047 $id = $shareKey->resourceLinkId;
2048 $ok = ($id !== $this->resourceLink->getRecordId());
2050 $this->resourceLink->primaryResourceLinkId = $id;
2051 $this->resourceLink->shareApproved = $shareKey->autoApprove;
2052 $ok = $this->resourceLink->save();
2054 $doSaveResourceLink =
false;
2055 $this->userResult->getResourceLink()->primaryResourceLinkId = $id;
2056 $this->userResult->getResourceLink()->shareApproved = $shareKey->autoApprove;
2057 $this->userResult->getResourceLink()->updated = time();
2059 $shareKey->delete();
2061 $this->reason =
'An error occurred initialising your share arrangement.';
2064 $this->reason =
'It is not possible to share your resource link with yourself.';
2068 $ok = !is_null($id);
2070 $this->reason =
'You have requested to share a resource link but none is available.';
2072 $ok = (!is_null($this->userResult->getResourceLink()->shareApproved) && $this->userResult->getResourceLink()->shareApproved);
2074 $this->reason =
'Your share request is waiting to be approved.';
2083 $this->reason =
'You have not requested to share a resource link but an arrangement is currently in place.';
2088 if ($ok && !is_null($id)) {
2089 $resourceLink = ResourceLink::fromRecordId($id, $this->dataConnector);
2090 $ok = !is_null($resourceLink->created);
2092 if ($doSaveResourceLink) {
2093 $this->resourceLink->save();
2095 $this->resourceLink = $resourceLink;
2097 $this->reason =
'Unable to load resource link being shared.';
2112 private function sendAuthenticationRequest($parameters, $disableCookieCheck)
2115 if (isset($parameters[
'client_id'])) {
2116 $clientId = $parameters[
'client_id'];
2118 $deploymentId =
null;
2119 if (isset($parameters[
'lti_deployment_id'])) {
2120 $deploymentId = $parameters[
'lti_deployment_id'];
2122 $currentLogLevel = Util::$logLevel;
2123 $this->platform = Platform::fromPlatformId($parameters[
'iss'], $clientId, $deploymentId, $this->dataConnector);
2124 if ($this->platform->debugMode && ($currentLogLevel < Util::LOGLEVEL_INFO)) {
2125 $this->debugMode =
true;
2128 $ok = !is_null($this->platform) && !empty($this->platform->authenticationUrl);
2130 $this->reason =
'Platform not found or no platform authentication request URL.';
2132 $oauthRequest = OAuth\OAuthRequest::from_request();
2133 $usePlatformStorage = !empty($oauthRequest->get_parameter(
'lti_storage_target'));
2135 if ($usePlatformStorage) {
2136 $usePlatformStorage = empty($_COOKIE[session_name()]) || ($_COOKIE[session_name()] !== session_id());
2138 if (!$disableCookieCheck) {
2139 if (empty(session_id())) {
2140 if (empty($_COOKIE)) {
2141 Util::setTestCookie();
2143 } elseif (empty($_COOKIE[session_name()]) || ($_COOKIE[session_name()] !== session_id())) {
2144 $session_id =
'.' . session_id();
2145 if (empty($_COOKIE[session_name()])) {
2146 Util::setTestCookie();
2151 $state = Util::getRandomString();
2152 $nonce =
new PlatformNonce($this->platform,
"{$state}{$session_id}");
2153 $ok = !$nonce->load();
2155 $nonce->expires = time() + Tool::$stateLife;
2156 $ok = $nonce->save();
2158 $redirectUri = $oauthRequest->get_normalized_http_url();
2159 if (!empty($_SERVER[
'QUERY_STRING'])) {
2160 if ($_SERVER[
'REQUEST_METHOD'] ===
'POST') {
2161 $ignoreParams = array(
'lti_storage_target');
2163 $ignoreParams = array(
'iss',
'target_link_uri',
'login_hint',
'lti_message_hint',
'client_id',
'lti_deployment_id',
'lti_storage_target');
2166 $params = explode(
'&', $_SERVER[
'QUERY_STRING']);
2168 foreach ($params as $param) {
2169 $parts = explode(
'=', $param, 2);
2170 if (in_array($parts[0], $ignoreParams)) {
2172 } elseif (!$ignore) {
2173 if ((count($parts) <= 1) || empty($parts[1])) {
2174 $queryString .=
"&{$parts[0]}";
2176 $queryString .=
"&{$parts[0]}={$parts[1]}";
2180 if (!empty($queryString)) {
2181 $queryString = substr($queryString, 1);
2182 $redirectUri .=
"?{$queryString}";
2185 $requestNonce = Util::getRandomString(32);
2187 'client_id' => $this->platform->clientId,
2188 'login_hint' => $parameters[
'login_hint'],
2189 'nonce' => $requestNonce,
2191 'redirect_uri' => $redirectUri,
2192 'response_mode' =>
'form_post',
2193 'response_type' =>
'id_token',
2194 'scope' =>
'openid',
2195 'state' => $nonce->getValue()
2197 if (isset($parameters[
'lti_message_hint'])) {
2198 $params[
'lti_message_hint'] = $parameters[
'lti_message_hint'];
2200 $this->onInitiateLogin($parameters, $params);
2202 if ($usePlatformStorage) {
2203 $javascript = $this->getStorageJS(
'lti.put_data', $nonce->getValue(), $requestNonce);
2205 if (!Tool::$authenticateUsingGet) {
2206 $this->output = Util::sendForm($this->platform->authenticationUrl, $params,
'', $javascript);
2208 Util::redirect($this->platform->authenticationUrl, $params,
'', $javascript);
2211 $this->reason =
'Unable to generate a state value.';
2225 private function sendRelaunchRequest($disableCookieCheck)
2228 if (!$disableCookieCheck) {
2229 if (empty(session_id())) {
2230 if (empty($_COOKIE)) {
2231 Util::setTestCookie();
2233 } elseif (empty($_COOKIE[session_name()]) || ($_COOKIE[session_name()] !== session_id())) {
2234 $session_id =
'.' . session_id();
2235 if (empty($_COOKIE[session_name()])) {
2236 Util::setTestCookie();
2241 $state = Util::getRandomString();
2242 $nonce =
new PlatformNonce($this->platform,
"{$state}{$session_id}");
2243 $ok = !$nonce->load();
2245 $nonce->expires = time() + Tool::$stateLife;
2246 $this->ok = $nonce->save();
2249 'tool_state' => $nonce->getValue(),
2250 'platform_state' => $this->messageParameters[
'platform_state']
2252 $params = $this->platform->addSignature($this->messageParameters[
'relaunch_url'], $params);
2253 $this->output = Util::sendForm($this->messageParameters[
'relaunch_url'], $params);
2255 $this->reason =
'Unable to generate a state value.';
2273 private function checkValue(&$value, $values, $reason, $strictMode, $generateWarnings, $ignoreInvalid =
false)
2275 $lookupValue = $value;
2277 $lookupValue = strtolower($value);
2279 $ok = in_array($lookupValue, $values);
2281 if ($this->ok && $strictMode) {
2282 $this->reason = sprintf($reason, $value);
2285 if ($generateWarnings) {
2286 $this->warnings[] = sprintf($reason, $value);
2289 } elseif ($lookupValue !== $value) {
2290 if ($generateWarnings) {
2291 $this->warnings[] = sprintf($reason, $value) .
" [Changed to '{$lookupValue}']";
2293 $value = $lookupValue;
2306 private function setError($reason, $strictMode, $generateWarnings)
2308 if ($strictMode && $this->ok) {
2310 $this->reason = $reason;
2311 } elseif ($generateWarnings) {
2312 $this->warnings[] = $reason;
2325 private function getStorageJS($message, $state, $nonce)
2328 $timeoutDelay = static::$postMessageTimeoutDelay;
2329 $formSubmissionTimeout = Util::$formSubmissionTimeout;
2330 if ($timeoutDelay > 0) {
2331 $parts = explode(
'.', $state);
2333 $capabilitiesId = Util::getRandomString();
2334 $messageId = Util::getRandomString();
2335 $javascript = <<< EOD
2336let origin =
new URL(
'{$this->platform->authenticationUrl}').origin;
2337let params =
new URLSearchParams(window.location.search);
2338let target = params.get(
'lti_storage_target');
2339let state =
'{$state}';
2340let nonce =
'{$nonce}';
2341let capabilitiesid =
'{$capabilitiesId}';
2342let messageid =
'{$messageId}';
2343let supported =
new Map();
2346window.addEventListener(
'message',
function (event) {
2348 if (typeof event.data !==
"object") {
2350 console.log(
'Error \'response is not an object\': ' + event.data);
2352 if (ok && event.data.error) {
2354 if (event.data.error.code && event.data.error.message) {
2355 console.log(
'Error \'' + event.data.error.code +
'\': ' + event.data.error.message);
2357 console.log(event.data.error);
2360 if (ok && !event.data.subject) {
2362 console.log(
'Error: There is no subject specified');
2365 switch (event.data.subject) {
2366 case
'lti.capabilities.response':
2367 case
'org.imsglobal.lti.capabilities.response':
2368 clearTimeout(timeout);
2369 if (event.data.message_id !== capabilitiesid) {
2371 console.log(
'Invalid message ID');
2373 event.data.supported_messages.forEach(function(capability) {
2374 supported.set(capability.subject, (capability.frame) ? capability.frame : target);
2376 if (supported.has(
'{$message}')) {
2377 sendMessage(
'{$message}');
2378 }
else if (supported.has(
'org.imsglobal.{$message}')) {
2379 sendMessage(
'org.imsglobal.{$message}');
2385 case '{$message}.response':
2386 case 'org.imsglobal.{$message}.response':
2387 clearTimeout(timeout);
2388 if ((event.data.message_id !== messageid) || (event.origin !== origin)) {
2390 console.log(
'Invalid message ID or origin');
2391 }
else if (event.data.key !== state) {
2393 console.log(
'Key not expected: ' + event.data.key);
2394 }
else if ((
'{$message}' ===
'lti.put_data') && (event.data.value != nonce)) {
2396 console.log(
'Invalid value for key ' + event.data.key +
': ' + event.data.value +
' (expected ' + nonce +
')');
2398 if (document.getElementById(
'id__storage_check')) {
2399 document.getElementById(
'id__storage_check').value = state +
'.' +
event.data.value;
2400 }
else if (document.getElementById(
'id_state')) {
2401 document.getElementById(
'id_state').value +=
'.platformStorage';
2407 console.log(
'Subject \'' + event.data.subject +
'\' not recognised
');
2411 clearTimeout(timeout);
2418function getTarget(frame = '') {
2419 let wdw = window.opener || window.parent;
2420 let targetframe = wdw;
2421 if (frame && (frame !== '_parent
')) {
2423 targetframe = wdw.frames[frame];
2429 targetframe = window.top.frames[frame];
2431 console.log('Cannot access storage frame (
' + frame + '):
' + err.message);
2436 if (targetframe === window) {
2440 console.log('No target frame found
');
2449 case 'lti.put_data
':
2450 $javascript .= <<< EOD
2451function sendMessage(subject) {
2452 let usetarget = target;
2453 if (supported.has(subject)) {
2454 usetarget = supported.get(subject);
2456 let targetframe = getTarget(usetarget);
2459 targetframe.postMessage({
2461 'message_id
': messageid,
2466 console.log(err.name + ':
' + err.message);
2473function doOnLoad() {
2474 timeout = setTimeout(function() { // Allow time to check platform capabilities
2475 timeout = setTimeout(function() { // Allow time to check platform capabilities
2476 timeout = setTimeout(function() { // Allow time to send postMessage
2478 }, {$timeoutDelay});
2479 sendMessage('lti.put_data
');
2480 }, {$timeoutDelay});
2481 checkCapabilities('org.imsglobal.lti.capabilities
', true);
2482 }, {$timeoutDelay});
2483 checkCapabilities('lti.capabilities
', false);
2488 case 'lti.get_data
':
2489 $javascript .= <<< EOD
2490function sendMessage(subject) {
2491 let usetarget = target;
2492 if (supported.has(subject)) {
2493 usetarget = supported.get(subject);
2495 let targetframe = getTarget(usetarget);
2498 targetframe.postMessage({
2500 'message_id
': messageid,
2504 console.log(err.name + ':
' + err.message);
2509function doOnLoad() {
2510 timeout = setTimeout(function() { // Allow time to check platform capabilities
2511 timeout = setTimeout(function() { // Allow time to check platform capabilities
2512 timeout = setTimeout(function() { // Allow time to send postMessage
2514 }, {$timeoutDelay});
2515 sendMessage('lti.get_data
');
2516 }, {$timeoutDelay});
2517 checkCapabilities('org.imsglobal.lti.capabilities
', true);
2518 }, {$timeoutDelay});
2519 checkCapabilities('lti.capabilities
', false);
2526 $javascript .= <<< EOD
2528function checkCapabilities(subject, checkparent) {
2529 let wdw = getTarget(target);
2534 'message_id
': capabilitiesid
2536 if (checkparent && (wdw !== window.parent)) {
2537 window.parent.postMessage({
2539 'message_id
': capabilitiesid
2543 console.log(err.name + ':
' + err.message);
2548function doUnblock() {
2549 var el = document.getElementById('id_blocked
');
2550 el.style.display = 'block
';
2553function submitForm() {
2554 if ((document.forms[0].target === '_blank
') && (window.top === window.self)) {
2555 document.forms[0].target = '';
2557 window.setTimeout(doUnblock, {$formSubmissionTimeout}000);
2558 document.forms[0].submit();
2561window.onload=doOnLoad;
Trait to handle API hook registrations.
Class to represent a content-item object.
getDataConnector()
Get the data connector.
Class to provide a connection to a persistent store for LTI objects.
Class to represent an HTTP message request.
Class to represent an HTTP message request.
Class to represent a generic item object.
static fromContext($context, $ltiResourceLinkId, $tempId=null)
Class constructor from context.
static fromPlatform($platform, $ltiResourceLinkId, $tempId=null)
Class constructor from platform.
Class to represent an LTI system.
sendMessage($url, $type, $messageParams, $target='', $userId=null, $hint=null)
Generate a web page containing an auto-submitted form of LTI message parameters.
$reason
Error message for last request processed.
$name
Local name of platform/tool.
$enabled
Whether the system instance is enabled to accept connection requests.
$dataConnector
Data connector object.
verifySignature()
Verify the signature of a message.
$messageParameters
LTI message parameters.
$jku
Endpoint for public key.
$ok
True if the last request was successful.
checkMessage()
Verify the required properties of an LTI message.
static parseRoles($roles, $ltiVersion=Util::LTI_VERSION1, $addPrincipalRole=false)
Parse a set of roles to comply with a specified version of LTI.
static fromResourceLink($resourceLink, $ltiUserId)
Class constructor from resource link.
Class to represent a platform user.
const PRINCIPAL_ROLES
List of principal roles for LTI 1.3.
Class to implement utility methods.
static sendForm($url, $params, $target='', $javascript='')
Generate a web page containing an auto-submitted form of parameters.
const LTI_VERSION1P3
LTI version 1.3 for messages.
const MESSAGE_TYPE_MAPPING
Mapping for standard message types.
static getRandomString($length=8)
Generate a random string.
static logRequest($debugLevel=false)
Log a request received.
static $logLevel
Current logging level.
static logError($message, $showSource=true)
Log an error message.
static getRequestParameters()
Return GET and POST request parameters (POST parameters take precedence).
const LTI_VERSION1
LTI version 1 for messages.
const LOGLEVEL_DEBUG
Log all messages.
static jsonDecode($str, $associative=false)
Decode a JSON string.
static logDebug($message, $showSource=false)
Log a debug message.
const LTI_VERSION2
LTI version 2 for messages.