LTI Integration Library 4.10.3
PHP class library for building LTI integrations
 
Loading...
Searching...
No Matches
Tool.php
1<?php
2
3namespace ceLTIc\LTI;
4
6use ceLTIc\LTI\MediaType;
7use ceLTIc\LTI\Profile;
11use ceLTIc\LTI\OAuth;
15
23class Tool
24{
25 use System;
26 use ApiHook;
27
31 const CONNECTION_ERROR_MESSAGE = 'Sorry, there was an error connecting you to the application.';
32
36 const ID_SCOPE_ID_ONLY = 0;
37
41 const ID_SCOPE_GLOBAL = 1;
42
46 const ID_SCOPE_CONTEXT = 2;
47
51 const ID_SCOPE_RESOURCE = 3;
52
56 const ID_SCOPE_SEPARATOR = ':';
57
61 public static $MESSAGE_TYPES = array(
62 'basic-lti-launch-request',
63 'ConfigureLaunchRequest',
64 'DashboardRequest',
65 'ContentItemSelectionRequest',
66 'ContentItemUpdateRequest',
67 'LtiSubmissionReviewRequest',
68 'ToolProxyRegistrationRequest',
69 'LtiStartProctoring',
70 'LtiEndAssessment'
71 );
72
78 private static $LTI_CONSUMER_SETTING_NAMES = array('custom_tc_profile_url', 'custom_system_setting_url', 'custom_oauth2_access_token_url');
79
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'
90 );
91
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',
103 'custom_ap_acs_url'
104 );
105
111 private static $LTI_RETAIN_SETTING_NAMES = array('custom_lineitem_url');
112
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'
153 );
154
163 public $consumer = null;
164
170 public $platform = null;
171
177 public $returnUrl = null;
178
184 public $userResult = null;
185
191 public $resourceLink = null;
192
198 public $context = null;
199
205 public $defaultEmail = '';
206
213
219 public $allowSharing = false;
220
226 public $message = null;
227
233 public $baseUrl = null;
234
240 public $vendor = null;
241
247 public $product = null;
248
254 public $requiredServices = null;
255
261 public $optionalServices = null;
262
268 public $resourceHandlers = null;
269
275 public $messageUrl = null;
276
282 public $initiateLoginUrl = null;
283
289 public $redirectionUris = null;
290
296 public static $defaultTool = null;
297
303 public static $authenticateUsingGet = false;
304
310 public static $stateLife = 10;
311
317 public static $postMessageTimeoutDelay = 20;
318
324 protected $redirectUrl = null;
325
331 protected $mediaTypes = null;
332
338 protected $contentTypes = null;
339
345 protected $fileTypes = null;
346
352 protected $documentTargets = null;
353
359 protected $output = null;
360
366 protected $errorOutput = null;
367
373 private $constraints = null;
374
380 public function __construct($dataConnector = null)
381 {
382 $this->consumer = &$this->platform;
383 $this->initialize();
384 if (empty($dataConnector)) {
386 }
387 $this->dataConnector = $dataConnector;
388 }
389
393 public function initialize()
394 {
395 $this->id = null;
396 $this->key = null;
397 $this->name = 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();
414 $this->vendor = new Profile\Item();
415 $this->product = new Profile\Item();
416 $this->requiredServices = array();
417 $this->optionalServices = array();
418 $this->resourceHandlers = array();
419 }
420
426 public function save()
427 {
428 return $this->dataConnector->saveTool($this);
429 }
430
436 public function delete()
437 {
438 return $this->dataConnector->deleteTool($this);
439 }
440
450 public function getMessageParameters($strictMode = false, $disableCookieCheck = false, $generateWarnings = false)
451 {
452 if (is_null($this->messageParameters)) {
453 $this->parseMessage($strictMode, $disableCookieCheck, $generateWarnings);
454// Set debug mode
456 $this->debugMode = (isset($this->messageParameters['custom_debug']) &&
457 (strtolower($this->messageParameters['custom_debug']) === 'true'));
458 if ($this->debugMode) {
460 }
461 }
462// Set return URL if available
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'];
467 }
468 if (empty($this->returnUrl) && !empty($this->messageParameters['launch_presentation_return_url'])) {
469 $this->returnUrl = $this->messageParameters['launch_presentation_return_url'];
470 }
471 }
472
474 }
475
483 public function handleRequest($strictMode = false, $disableCookieCheck = false, $generateWarnings = false)
484 {
485 $parameters = Util::getRequestParameters();
486 if ($this->debugMode) {
488 }
489 if ($_SERVER['REQUEST_METHOD'] === 'HEAD') { // Ignore HEAD requests
490 Util::logRequest(true);
491 } elseif (isset($parameters['iss']) && (strlen($parameters['iss']) > 0)) { // Initiate login request
493 if (!isset($parameters['login_hint']) || (strlen($parameters['login_hint']) <= 0)) {
494 $this->ok = false;
495 $this->reason = 'Missing login_hint parameter.';
496 } elseif (!isset($parameters['target_link_uri']) || (strlen($parameters['target_link_uri']) <= 0)) {
497 $this->ok = false;
498 $this->reason = 'Missing target_link_uri parameter.';
499 } else {
500 $this->ok = $this->sendAuthenticationRequest($parameters, $disableCookieCheck);
501 }
502 } elseif (isset($parameters['openid_configuration']) && (strlen($parameters['openid_configuration']) > 0)) { // Dynamic registration request
504 $this->onRegistration();
505 } else { // LTI message
506 $this->getMessageParameters($strictMode, $disableCookieCheck, $generateWarnings);
508 if ($this->ok && $this->authenticate($strictMode, $disableCookieCheck, $generateWarnings)) {
509 if (empty($this->output)) {
510 $this->doCallback();
511 if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) {
512 $this->platform->save();
513 }
514 }
515 }
516 }
517 if (!$this->ok) {
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}";
523 }
524 }
525 Util::logError($errorMessage);
526 }
527 $this->result();
528 }
529
538 public function setParameterConstraint($name, $required = true, $maxLength = null, $messageTypes = null)
539 {
540 $name = trim($name);
541 if (!empty($name)) {
542 $this->constraints[$name] = array('required' => $required, 'max_length' => $maxLength, 'messages' => $messageTypes);
543 }
544 }
545
554 public function getConsumers()
555 {
556 Util::logDebug('Method ceLTIc\LTI\Tool::getConsumers() has been deprecated; please use ceLTIc\LTI\Tool::getPlatforms() instead.',
557 true);
558 return $this->getPlatforms();
559 }
560
566 public function getPlatforms()
567 {
568 return $this->dataConnector->getPlatforms();
569 }
570
579 public function findService($format, $methods)
580 {
581 $found = false;
582 $services = $this->platform->profile->service_offered;
583 if (is_array($services)) {
584 $n = -1;
585 foreach ($services as $service) {
586 $n++;
587 if (!is_array($service->format) || !in_array($format, $service->format)) {
588 continue;
589 }
590 $missing = array();
591 foreach ($methods as $method) {
592 if (!is_array($service->action) || !in_array($method, $service->action)) {
593 $missing[] = $method;
594 }
595 }
596 $methods = $missing;
597 if (count($methods) <= 0) {
598 $found = $service;
599 break;
600 }
601 }
602 }
603
604 return $found;
605 }
606
612 public function doToolProxyService()
613 {
614// Create tool proxy
615 $toolProxyService = $this->findService('application/vnd.ims.lti.v2.toolproxy+json', array('POST'));
617 $toolProxy = new MediaType\ToolProxy($this, $toolProxyService, $secret);
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);
621 if ($ok) {
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();
626 }
627
628 return $ok;
629 }
630
643 public static function sendForm($url, $params, $target = '')
644 {
645 Util::logDebug('Method ceLTIc\LTI\Tool::sendForm() has been deprecated; please use ceLTIc\LTI\Util::sendForm() instead.',
646 true);
647 return Util::sendForm($url, $params, $target);
648 }
649
650###
651### PROTECTED METHODS
652###
653
657 protected function onLaunch()
658 {
659 $this->reason = 'No onLaunch method found for tool.';
660 $this->onError();
661 }
662
666 protected function onConfigure()
667 {
668 $this->reason = 'No onConfigure method found for tool.';
669 $this->onError();
670 }
671
675 protected function onDashboard()
676 {
677 $this->reason = 'No onDashboard method found for tool.';
678 $this->onError();
679 }
680
684 protected function onContentItem()
685 {
686 $this->reason = 'No onContentItem method found for tool.';
687 $this->onError();
688 }
689
693 protected function onContentItemUpdate()
694 {
695 $this->reason = 'No onContentItemUpdate method found for tool.';
696 $this->onError();
697 }
698
702 protected function onSubmissionReview()
703 {
704 $this->reason = 'No onSubmissionReview method found for tool.';
705 $this->onError();
706 }
707
711 protected function onRegister()
712 {
713 $this->reason = 'No onRegister method found for tool.';
714 $this->onError();
715 }
716
720 protected function onRegistration()
721 {
722 $platformConfig = $this->getPlatformConfiguration();
723 if ($this->ok) {
724 $toolConfig = $this->getConfiguration($platformConfig);
725 $registrationConfig = $this->sendRegistration($platformConfig, $toolConfig);
726 if ($this->ok) {
727 $this->getPlatformToRegister($platformConfig, $registrationConfig);
728 }
729 }
730 $this->getRegistrationResponsePage($toolConfig);
731 $this->ok = true;
732 }
733
737 protected function onLtiStartProctoring()
738 {
739 $this->reason = 'No onLtiStartProctoring method found for tool.';
740 $this->onError();
741 }
742
746 protected function onLtiEndAssessment()
747 {
748 $this->reason = 'No onLtiEndAssessment method found for tool.';
749 $this->onError();
750 }
751
758 protected function onInitiateLogin($requestParameters, &$authParameters)
759 {
760 $hasSession = !empty(session_id());
761 if (!$hasSession) {
762 session_start();
763 }
764 $_SESSION['ceLTIc_lti_authentication_request'] = array(
765 'state' => $authParameters['state'],
766 'nonce' => $authParameters['nonce']
767 );
768 if (!$hasSession) {
769 session_write_close();
770 }
771 }
772
780 protected function onAuthenticate($state, $nonce, $usePlatformStorage)
781 {
782 $hasSession = !empty(session_id());
783 if (!$hasSession) {
784 session_start();
785 }
786 $parts = explode('.', $state);
787 if (!isset($this->rawParameters['_storage_check']) && $usePlatformStorage) { // Check browser storage
788 $this->rawParameters['_storage_check'] = '';
789 $javascript = $this->getStorageJS('lti.get_data', $state, '');
790 echo Util::sendForm($_SERVER['REQUEST_URI'], $this->rawParameters, '', $javascript);
791 exit;
792 } elseif (isset($this->rawParameters['_storage_check'])) {
793 if (!empty(($this->rawParameters['_storage_check']))) {
794 $state = $parts[0];
795 $parts = explode('.', $this->rawParameters['_storage_check']);
796 if ((count($parts) !== 2) || ($parts[0] !== $state) || ($parts[1] !== $nonce)) {
797 $this->ok = false;
798 $this->reason = 'Invalid state and/or nonce values';
799 }
800 } else {
801 $this->ok = false;
802 $this->reason = 'Error accessing platform storage';
803 }
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);
808 }
809 if (($state !== $auth['state']) || ($nonce !== $auth['nonce'])) {
810 $this->ok = false;
811 $this->reason = 'Invalid state parameter value and/or nonce claim value';
812 }
813 unset($_SESSION['ceLTIc_lti_authentication_request']);
814 }
815 if (!$hasSession) {
816 session_write_close();
817 }
818 }
819
823 protected function onResetSessionId()
824 {
825
826 }
827
831 protected function onError()
832 {
833 $this->ok = false;
834 }
835
841 protected function getPlatformConfiguration()
842 {
843 if ($this->ok) {
844 $parameters = Util::getRequestParameters();
845 $this->ok = !empty($parameters['openid_configuration']);
846 if ($this->ok) {
847 $http = new HttpMessage($parameters['openid_configuration']);
848 $this->ok = $http->send();
849 if ($this->ok) {
850 $platformConfig = Util::jsonDecode($http->response, true);
851 $this->ok = !empty($platformConfig);
852 }
853 if (!$this->ok) {
854 $this->reason = 'Unable to access platform configuration details.';
855 }
856 } else {
857 $this->reason = 'Invalid registration request: missing openid_configuration parameter.';
858 }
859 if ($this->ok) {
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']);
867 if (!$this->ok) {
868 $this->reason = 'Invalid platform configuration details.';
869 }
870 }
871 if ($this->ok) {
872 $jwtClient = Jwt::getJwtClient();
873 $algorithms = \array_intersect($jwtClient::getSupportedAlgorithms(),
874 $platformConfig['id_token_signing_alg_values_supported']);
875 $this->ok = !empty($algorithms);
876 if ($this->ok) {
877 rsort($platformConfig['id_token_signing_alg_values_supported']);
878 } else {
879 $this->reason = 'None of the signature algorithms offered by the platform is supported.';
880 }
881 }
882 }
883 if (!$this->ok) {
884 $platformConfig = null;
885 }
886
887 return $platformConfig;
888 }
889
897 protected function getConfiguration($platformConfig)
898 {
899 $claimsMapping = array(
900 'User.id' => 'sub',
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'
906 );
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'];
918 }
919 $scopesSupported = $platformConfig['scopes_supported'];
920 $iconUrl = null;
921 $messages = array();
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;
929 }
930 foreach (array_merge($resourceHandler->optionalMessages, $resourceHandler->requiredMessages) as $message) {
931 $type = $message->type;
932 if (array_key_exists($type, Util::MESSAGE_TYPE_MAPPING)) {
933 $type = Util::MESSAGE_TYPE_MAPPING[$type];
934 }
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);
947 $messages[] = array(
948 'type' => $type,
949 'target_link_uri' => "{$this->baseUrl}{$message->path}",
950 'label' => $toolName
951 );
952 }
953 foreach ($capabilities as $capability) {
954 if (array_key_exists($capability, $claimsMapping) && in_array($claimsMapping[$capability], $claimsSupported)) {
955 $claims[] = $claimsMapping[$capability];
956 }
957 }
958 }
959 }
960 if (empty($redirectUris)) {
961 $redirectUris = array($toolUrl);
962 } else {
963 $redirectUris = array_unique($redirectUris);
964 }
965 if (!empty($claims)) {
966 $claims = array_unique($claims);
967 }
968 $custom = array();
969 foreach ($constants as $name => $value) {
970 $custom[$name] = $value;
971 }
972 foreach ($variables as $name => $value) {
973 $custom[$name] = '$' . $value;
974 }
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;
982 $toolConfig['jwks_uri'] = $this->jku;
983 $toolConfig['token_endpoint_auth_method'] = 'private_key_jwt';
984 $toolConfig['https://purl.imsglobal.org/spec/lti-tool-configuration'] = array(
985 'domain' => $domain,
986 'target_link_uri' => $toolUrl,
987 'custom_parameters' => $custom,
988 'claims' => $claims,
989 'messages' => $messages,
990 'description' => $toolDescription
991 );
992 $toolConfig['scope'] = implode(' ', array_intersect($this->requiredScopes, $scopesSupported));
993 if (!empty($iconUrl)) {
994 $toolConfig['logo_uri'] = "{$this->baseUrl}{$iconUrl}";
995 }
996
997 return $toolConfig;
998 }
999
1008 protected function sendRegistration($platformConfig, $toolConfig)
1009 {
1010 if ($this->ok) {
1011 $parameters = Util::getRequestParameters();
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']}";
1016 }
1017 $http = new HttpMessage($platformConfig['registration_endpoint'], 'POST', $body, $headers);
1018 $this->ok = $http->send();
1019 if ($this->ok) {
1020 $registrationConfig = Util::jsonDecode($http->response, true);
1021 $this->ok = !empty($registrationConfig);
1022 }
1023 if (!$this->ok) {
1024 $this->reason = 'Unable to register with platform.';
1025 }
1026 }
1027 if (!$this->ok) {
1028 $registrationConfig = null;
1029 }
1030
1031 return $registrationConfig;
1032 }
1033
1043 protected function getPlatformToRegister($platformConfig, $registrationConfig, $doSave = true)
1044 {
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);
1052 }
1053 }
1054 $this->platform = new Platform($this->dataConnector);
1055 $this->platform->name = $domain;
1056 $this->platform->ltiVersion = Util::LTI_VERSION1P3;
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'];
1064 if ($doSave) {
1065 $this->ok = $this->platform->save();
1066 if (!$this->ok) {
1067 $this->reason = 'Sorry, an error occurred when saving the platform details.';
1068 }
1069 }
1070
1071 return $this->platform;
1072 }
1073
1079 protected function getRegistrationResponsePage($toolConfig)
1080 {
1081 $enabled = '';
1082 if (!empty($this->platform)) {
1083 $now = time();
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);
1090 }
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);
1094 } else {
1095 $enabled = ', but your access was set to end at ' . date('j F Y H:i T', $this->platform->enableUntil);
1096 }
1097 }
1098 }
1099 $html = <<< EOD
1100<!DOCTYPE html>
1101<html lang="en">
1102 <head>
1103 <meta http-equiv="content-type" content="text/html; charset=UTF-8">
1104 <title>LTI Tool registration</title>
1105 <style>
1106 h1 {
1107 font-soze: 110%;
1108 font-weight: bold;
1109 }
1110 .success {
1111 color: #155724;
1112 background-color: #d4edda;
1113 border-color: #c3e6cb;
1114 border: 1px solid;
1115 padding: .75rem 1.25rem;
1116 margin-bottom: 1rem;
1117 }
1118 .error {
1119 color: #721c24;
1120 background-color: #f8d7da;
1121 border-color: #f5c6cb;
1122 border: 1px solid;
1123 padding: .75rem 1.25rem;
1124 margin-bottom: 1rem;
1125 }
1126 .centre {
1127 text-align: center;
1128 }
1129 button {
1130 border: 1px solid transparent;
1131 padding: 0.375rem 0.75rem;
1132 font-size: 1rem;
1133 line-height: 1.5;
1134 border-radius: 0.25rem;
1135 color: #fff;
1136 background-color: #007bff;
1137 border-color: #007bff;
1138 text-align: center;
1139 text-decoration: none;
1140 display: inline-block;
1141 cursor: pointer;
1142 }
1143 </style>
1144 <script language="javascript" type="text/javascript">
1145 function doClose(el) {
1146 (window.opener || window.parent).postMessage({subject:'org.imsglobal.lti.close'}, '*');
1147 return true;
1148 }
1149 </script>
1150 </head>
1151 <body>
1152 <h1>{$toolConfig['client_name']} registration</h1>
1153
1154EOD;
1155 if ($this->ok) {
1156 $html .= <<< EOD
1157 <p class="success">
1158 The tool registration was successful{$enabled}.
1159 </p>
1160 <p class="centre">
1161 <button type="button" onclick="return doClose();">Close</button>
1162 </p>
1163
1164EOD;
1165 } else {
1166 $html .= <<< EOD
1167 <p class="error">
1168 Sorry, the registration was not successful: {$this->reason}
1169 </p>
1170
1171EOD;
1172 }
1173 $html .= <<< EOD
1174 </body>
1175</html>
1176EOD;
1177 $this->output = $html;
1178 }
1179
1189 public static function fromConsumerKey($key = null, $dataConnector = null, $autoEnable = false)
1190 {
1191 $tool = new static($dataConnector);
1192 $tool->key = $key;
1193 if (!empty($dataConnector)) {
1194 $ok = $dataConnector->loadTool($tool);
1195 if ($ok && $autoEnable) {
1196 $tool->enabled = true;
1197 }
1198 }
1199
1200 return $tool;
1201 }
1202
1212 public static function fromInitiateLoginUrl($initiateLoginUrl, $dataConnector = null, $autoEnable = false)
1213 {
1214 $tool = new static($dataConnector);
1215 $tool->initiateLoginUrl = $initiateLoginUrl;
1216 if ($dataConnector->loadTool($tool)) {
1217 if ($autoEnable) {
1218 $tool->enabled = true;
1219 }
1220 }
1221
1222 return $tool;
1223 }
1224
1233 public static function fromRecordId($id, $dataConnector)
1234 {
1235 $tool = new static($dataConnector);
1236 $tool->setRecordId($id);
1237 $dataConnector->loadTool($tool);
1238
1239 return $tool;
1240 }
1241
1242###
1243### PRIVATE METHODS
1244###
1245
1249 private function result()
1250 {
1251 if (!$this->ok) {
1252 $this->message = self::CONNECTION_ERROR_MESSAGE;
1253 $this->onError();
1254 }
1255 if (!$this->ok) {
1256// If not valid, return an error message to the platform if a return URL is provided
1257 if (!empty($this->returnUrl)) {
1258 $errorUrl = $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}";
1265 } else {
1266 $formParams['lti_errormsg'] = $this->message;
1267 if (!is_null($this->reason)) {
1268 $formParams['lti_errorlog'] = "Debug error: {$this->reason}";
1269 }
1270 }
1271 if (isset($this->messageParameters['data'])) {
1272 $formParams['data'] = $this->messageParameters['data'];
1273 }
1274 $this->ltiVersion = (isset($this->messageParameters['lti_version'])) ? $this->messageParameters['lti_version'] : Util::LTI_VERSION1;
1275 $page = $this->sendMessage($errorUrl, 'ContentItemSelection', $formParams);
1276 echo $page;
1277 } else {
1278 if (strpos($errorUrl, '?') === false) {
1279 $errorUrl .= '?';
1280 } else {
1281 $errorUrl .= '&';
1282 }
1283 if ($this->debugMode && !is_null($this->reason)) {
1284 $errorUrl .= 'lti_errormsg=' . urlencode("Debug error: {$this->reason}");
1285 } else {
1286 $errorUrl .= 'lti_errormsg=' . urlencode($this->message);
1287 if (!is_null($this->reason)) {
1288 $errorUrl .= '&lti_errorlog=' . urlencode("Debug error: {$this->reason}");
1289 }
1290 }
1291 header("Location: {$errorUrl}");
1292 }
1293 exit;
1294 } else {
1295 if (!is_null($this->errorOutput)) {
1296 echo $this->errorOutput;
1297 } elseif ($this->debugMode && !empty($this->reason)) {
1298 echo "Debug error: {$this->reason}";
1299 } else {
1300 echo "Error: {$this->message}";
1301 }
1302 exit;
1303 }
1304 } elseif (!is_null($this->redirectUrl)) {
1305 header("Location: {$this->redirectUrl}");
1306 exit;
1307 } elseif (!is_null($this->output)) {
1308 echo $this->output;
1309 exit;
1310 }
1311 }
1312
1324 private function authenticate($strictMode, $disableCookieCheck, $generateWarnings)
1325 {
1326 $doSavePlatform = false;
1327 $this->ok = $this->checkMessage();
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);
1331 }
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,
1335 $generateWarnings);
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);
1340 }
1341 }
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))) {
1345 $this->ok = false;
1346 $this->reason = 'Missing resource link ID.';
1347 }
1348 if ($this->messageParameters['lti_version'] === Util::LTI_VERSION1P3) {
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);
1354 }
1355 }
1356 } elseif (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') ||
1357 ($this->messageParameters['lti_message_type'] === 'ContentItemUpdateRequest')) {
1358 $isUpdate = ($this->messageParameters['lti_message_type'] === 'ContentItemUpdateRequest');
1359 $mediaTypes = array();
1360 $contentTypes = array();
1361 $fileTypes = array();
1362 $documentTargets = array();
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']));
1365 $mediaTypes = array_filter($mediaTypes);
1366 $mediaTypes = array_unique($mediaTypes);
1367 }
1368 if ((count($mediaTypes) <= 0) && ($this->messageParameters['lti_version'] !== Util::LTI_VERSION1P3)) {
1369 $this->setError('Missing or empty accept_media_types parameter.', $strictMode, $generateWarnings);
1370 }
1371 if ($isUpdate) {
1372 if ($this->messageParameters['lti_version'] !== Util::LTI_VERSION1P3) {
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)) {
1376 $this->ok = false;
1377 }
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)) {
1381 $this->ok = false;
1382 }
1383 }
1384 if ($this->ok) {
1385 foreach ($mediaTypes as $mediaType) {
1386 if (strpos($mediaType, 'application/vnd.ims.lti.') !== 0) {
1387 $fileTypes[] = $mediaType;
1388 }
1389 if (($mediaType === 'text/html') || ($mediaType === '*/*')) {
1390 $contentTypes[] = Item::TYPE_LINK;
1391 $contentTypes[] = Item::TYPE_HTML;
1392 } elseif ((strpos($mediaType, 'image/') === 0) || ($mediaType === '*/*')) {
1393 $contentTypes[] = Item::TYPE_IMAGE;
1394 } elseif ($mediaType === Item::LTI_LINK_MEDIA_TYPE) {
1395 $contentTypes[] = Item::TYPE_LTI_LINK;
1396 } elseif ($mediaType === Item::LTI_ASSIGNMENT_MEDIA_TYPE) {
1397 $contentTypes[] = Item::TYPE_LTI_ASSIGNMENT;
1398 }
1399 }
1400 if (!empty($fileTypes)) {
1401 $contentTypes[] = Item::TYPE_FILE;
1402 }
1403 $contentTypes = array_unique($contentTypes);
1404 }
1405 if (isset($this->messageParameters['accept_presentation_document_targets']) &&
1406 (strlen(trim($this->messageParameters['accept_presentation_document_targets'])) > 0)) {
1407 $documentTargets = array_map('trim',
1408 explode(',', $this->messageParameters['accept_presentation_document_targets']));
1409 $documentTargets = array_filter($documentTargets);
1410 $documentTargets = array_unique($documentTargets);
1411 if (count($documentTargets) <= 0) {
1412 $this->setError('Missing or empty accept_presentation_document_targets parameter.', $strictMode,
1413 $generateWarnings);
1414 } elseif (!empty($documentTargets)) {
1415 if (empty($this->jwt) || !$this->jwt->hasJwt()) {
1416 $permittedTargets = array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none');
1417 } else { // JWT
1418 $permittedTargets = array('embed', 'iframe', 'window');
1419 }
1420 foreach ($documentTargets as $documentTarget) {
1421 if (!$this->checkValue($documentTarget, $permittedTargets,
1422 'Invalid value in accept_presentation_document_targets parameter: \'%s\'.', $strictMode,
1423 $generateWarnings, true)) {
1424 $this->ok = false;
1425 }
1426 }
1427 }
1428 } else {
1429 $this->setError('No accept_presentation_document_targets parameter found.', $strictMode, $generateWarnings);
1430 }
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);
1434 }
1435 }
1436 if ($this->ok) {
1437 $this->mediaTypes = $mediaTypes;
1438 $this->contentTypes = $contentTypes;
1439 $this->fileTypes = $fileTypes;
1440 $this->documentTargets = $documentTargets;
1441 }
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);
1445 }
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);
1448 }
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);
1452 }
1453 }
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);
1464 }
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);
1468 }
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);
1472 }
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);
1476 }
1477 }
1478 }
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);
1484 }
1485 }
1486 }
1487// Check consumer key
1488 if (($this->ok || $generateWarnings) && ($this->messageParameters['lti_message_type'] !== 'ToolProxyRegistrationRequest')) {
1489 $now = time();
1490 if (!isset($this->messageParameters['oauth_consumer_key'])) {
1491 $this->setError('Missing consumer key.', true, $generateWarnings);
1492 }
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']}'.";
1496 } else {
1497 $reason = "Platform not recognised (Platform ID | Client ID | Deployment ID): '{$this->messageParameters['platform_id']}' | '{$this->messageParameters['oauth_consumer_key']}' | '{$this->messageParameters['deployment_id']}'.";
1498 }
1499 $this->setError($reason, true, $generateWarnings);
1500 }
1501 if ($this->ok) {
1502 if ($this->messageParameters['oauth_signature_method'] !== $this->platform->signatureMethod) {
1503 $this->platform->signatureMethod = $this->messageParameters['oauth_signature_method'];
1504 $doSavePlatform = true;
1505 }
1506 $today = date('Y-m-d', $now);
1507 if (is_null($this->platform->lastAccess)) {
1508 $doSavePlatform = true;
1509 } else {
1510 $last = date('Y-m-d', $this->platform->lastAccess);
1511 $doSavePlatform = $doSavePlatform || ($last !== $today);
1512 }
1513 $this->platform->lastAccess = $now;
1514 $this->ok = $this->verifySignature();
1515 }
1516 if ($this->ok) {
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']);
1520 if (!$this->ok) {
1521 $this->reason = 'Request is from an invalid platform.';
1522 }
1523 } else {
1524 $this->ok = isset($this->messageParameters['tool_consumer_instance_guid']);
1525 if (!$this->ok) {
1526 $this->reason = 'A platform GUID must be included in the launch request as this configuration is protected.';
1527 }
1528 }
1529 }
1530 if ($this->ok) {
1531 $this->ok = $this->platform->enabled;
1532 if (!$this->ok) {
1533 $this->reason = 'Platform has not been enabled by the tool.';
1534 }
1535 }
1536 if ($this->ok) {
1537 $this->ok = is_null($this->platform->enableFrom) || ($this->platform->enableFrom <= $now);
1538 if ($this->ok) {
1539 $this->ok = is_null($this->platform->enableUntil) || ($this->platform->enableUntil > $now);
1540 if (!$this->ok) {
1541 $this->reason = 'Platform access has expired.';
1542 }
1543 } else {
1544 $this->reason = 'Platform access is not yet available.';
1545 }
1546 }
1547 }
1548// Validate other message parameter values
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');
1554 if ($this->messageParameters['lti_version'] !== Util::LTI_VERSION1) {
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'));
1557 }
1558 $found = false;
1559 foreach ($context_types as $context_type) {
1560 $found = in_array(trim($context_type), $permitted_types);
1561 if ($found) {
1562 break;
1563 }
1564 }
1565 if (!$found) {
1566 $this->setError(sprintf('No valid value found in context_type parameter: \'%s\'.',
1567 $this->messageParameters['context_type']), $strictMode, $generateWarnings);
1568 }
1569 }
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)) {
1576 $this->ok = false;
1577 }
1578 if (isset($this->messageParameters['accept_multiple'])) {
1579 if (!$isUpdate) {
1580 if (!$this->checkValue($this->messageParameters['accept_multiple'], array('true', 'false'),
1581 'Invalid value for accept_multiple parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1582 $this->ok = false;
1583 }
1584 } elseif (!$this->checkValue($this->messageParameters['accept_multiple'], array('false'),
1585 'Invalid value for accept_multiple parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1586 $this->ok = false;
1587 }
1588 }
1589 if (isset($this->messageParameters['accept_copy_advice'])) {
1590 if (!$isUpdate) {
1591 if (!$this->checkValue($this->messageParameters['accept_copy_advice'], array('true', 'false'),
1592 'Invalid value for accept_copy_advice parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1593 $this->ok = false;
1594 }
1595 } elseif (!$this->checkValue($this->messageParameters['accept_copy_advice'], array('false'),
1596 'Invalid value for accept_copy_advice parameter: \'%s\'.', $strictMode, $generateWarnings)) {
1597 $this->ok = false;
1598 }
1599 }
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)) {
1603 $this->ok = false;
1604 }
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)) {
1608 $this->ok = false;
1609 }
1610 }
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)) {
1616 $this->ok = false;
1617 }
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);
1624 }
1625 }
1626 }
1627 }
1628 }
1629
1630 if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) {
1631 $this->ok = $this->messageParameters['lti_version'] === Util::LTI_VERSION2;
1632 if (!$this->ok) {
1633 $this->reason = 'Invalid lti_version parameter.';
1634 }
1635 if ($this->ok) {
1636 $url = $this->messageParameters['tc_profile_url'];
1637 if (strpos($url, '?') === false) {
1638 $url .= '?';
1639 } else {
1640 $url .= '&';
1641 }
1642 $url .= 'lti_version=' . Util::LTI_VERSION2;
1643 $http = new HttpMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');
1644 $this->ok = $http->send();
1645 if (!$this->ok) {
1646 $this->reason = 'Platform profile not accessible.';
1647 } else {
1648 $tcProfile = Util::jsonDecode($http->response);
1649 $this->ok = !is_null($tcProfile);
1650 if (!$this->ok) {
1651 $this->reason = 'Invalid JSON in platform profile.';
1652 }
1653 }
1654 }
1655// Check for required capabilities
1656 if ($this->ok) {
1657 $this->platform = Platform::fromConsumerKey($this->messageParameters['reg_key'], $this->dataConnector);
1658 $this->platform->profile = $tcProfile;
1659 $capabilities = $this->platform->profile->capability_offered;
1660 $missing = array();
1661 foreach ($this->resourceHandlers as $resourceHandler) {
1662 foreach ($resourceHandler->requiredMessages as $message) {
1663 if (!in_array($message->type, $capabilities)) {
1664 $missing[$message->type] = true;
1665 }
1666 }
1667 }
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;
1673 }
1674 }
1675 }
1676 if (!empty($missing)) {
1677 ksort($missing);
1678 $this->reason = 'Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\'.';
1679 $this->ok = false;
1680 }
1681 }
1682// Check for required services
1683 if ($this->ok) {
1684 foreach ($this->requiredServices as $service) {
1685 foreach ($service->formats as $format) {
1686 if (!$this->findService($format, $service->actions)) {
1687 if ($this->ok) {
1688 $this->reason = 'Required service(s) not offered - ';
1689 $this->ok = false;
1690 } else {
1691 $this->reason .= ', ';
1692 }
1693 $this->reason .= "'{$format}' [" . implode(', ', $service->actions) . '].';
1694 }
1695 }
1696 }
1697 }
1698 if ($this->ok) {
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;
1710 }
1711 }
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) {
1715 $url .= '?';
1716 } else {
1717 $url .= '&';
1718 }
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;
1726 }
1727 }
1728 }
1729
1730 if ($this->ok) {
1731
1732// Check if a relaunch is being requested
1733 if (isset($this->messageParameters['relaunch_url'])) {
1734 Util::logRequest();
1735 if (empty($this->messageParameters['platform_state'])) {
1736 $this->ok = false;
1737 $this->reason = 'Missing or empty platform_state parameter.';
1738 } else {
1739 $this->ok = $this->sendRelaunchRequest($disableCookieCheck);
1740 }
1741 } else {
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'])) {
1747 $ok = true;
1748 if ($constraint['required']) {
1749 if (!isset($this->messageParameters[$name]) || (strlen(trim($this->messageParameters[$name])) <= 0)) {
1750 $invalidParameters[] = "{$name} (missing)";
1751 $ok = false;
1752 }
1753 }
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)";
1757 }
1758 }
1759 }
1760 }
1761 if (count($invalidParameters) > 0) {
1762 $this->ok = false;
1763 if (empty($this->reason)) {
1764 $this->reason = 'Invalid parameter(s): ' . implode(', ', $invalidParameters) . '.';
1765 }
1766 }
1767
1768 if ($this->ok) {
1769
1770// Set the request context
1771 $contextId = '';
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();
1776 }
1777 if (empty($contextId) && isset($this->messageParameters['context_id'])) {
1778 $contextId = trim($this->messageParameters['context_id']);
1779 }
1780 if (!empty($contextId)) {
1781 $this->context = Context::fromPlatform($this->platform, $contextId);
1782 $title = '';
1783 if (isset($this->messageParameters['context_title'])) {
1784 $title = trim($this->messageParameters['context_title']);
1785 }
1786 if (empty($title)) {
1787 $title = "Course {$this->context->getId()}";
1788 }
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://purl.imsglobal.org/vocab/lis/v2/course#') === 0) {
1793 $this->context->type = substr($this->context->type, 46);
1794 }
1795 }
1796 }
1797
1798// Set the request resource link
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'];
1803 }
1804 if (empty($this->context)) {
1805 $this->resourceLink = ResourceLink::fromPlatform($this->platform,
1806 trim($this->messageParameters['resource_link_id']), $contentItemId);
1807 } else {
1808 $this->resourceLink = ResourceLink::fromContext($this->context,
1809 trim($this->messageParameters['resource_link_id']), $contentItemId);
1810 }
1811 $title = '';
1812 if (isset($this->messageParameters['resource_link_title'])) {
1813 $title = trim($this->messageParameters['resource_link_title']);
1814 }
1815 if (empty($title)) {
1816 $title = "Resource {$this->resourceLink->getId()}";
1817 }
1818 $this->resourceLink->title = $title;
1819 }
1820// Delete any existing custom parameters
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;
1825 }
1826 }
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);
1831 }
1832 }
1833 }
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);
1838 }
1839 }
1840 }
1841// Save LTI parameters
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);
1847 }
1848 }
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);
1855 }
1856 }
1857 }
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);
1864 }
1865 }
1866 }
1867// Save other custom parameters at all levels
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);
1875 }
1876 if (!empty($this->resourceLink)) {
1877 $this->resourceLink->setSetting($name, $value);
1878 }
1879 }
1880 }
1881
1882// Set the user instance
1883 $userId = '';
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();
1888 }
1889 if (empty($userId) && isset($this->messageParameters['user_id'])) {
1890 $userId = trim($this->messageParameters['user_id']);
1891 }
1892
1893 $this->userResult = UserResult::fromResourceLink($this->resourceLink, $userId);
1894
1895// Set the user name
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);
1901
1902// Set the sourcedId
1903 if (isset($this->messageParameters['lis_person_sourcedid'])) {
1904 $this->userResult->sourcedId = $this->messageParameters['lis_person_sourcedid'];
1905 }
1906
1907// Set the username
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'];
1918 }
1919
1920// Set the user email
1921 $email = (isset($this->messageParameters['lis_person_contact_email_primary'])) ? $this->messageParameters['lis_person_contact_email_primary'] : '';
1922 $this->userResult->setEmail($email, $this->defaultEmail);
1923
1924// Set the user image URI
1925 if (isset($this->messageParameters['user_image'])) {
1926 $this->userResult->image = $this->messageParameters['user_image'];
1927 }
1928
1929// Set the user roles
1930 if (isset($this->messageParameters['roles'])) {
1931 $this->userResult->roles = self::parseRoles($this->messageParameters['roles'],
1932 $this->messageParameters['lti_version']);
1933 }
1934
1935// Initialise the platform and check for changes
1936 $this->platform->defaultEmail = $this->defaultEmail;
1937 if ($this->platform->ltiVersion !== $this->messageParameters['lti_version']) {
1938 $this->platform->ltiVersion = $this->messageParameters['lti_version'];
1939 $doSavePlatform = true;
1940 }
1941 if (isset($this->messageParameters['deployment_id'])) {
1942 $this->platform->deploymentId = $this->messageParameters['deployment_id'];
1943 }
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;
1948 }
1949 }
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']}";
1954 }
1955// do not delete any existing consumer version if none is passed
1956 if ($this->platform->consumerVersion !== $version) {
1957 $this->platform->consumerVersion = $version;
1958 $doSavePlatform = true;
1959 }
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;
1963 }
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;
1971 }
1972 }
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;
1977 }
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;
1984 }
1985 }
1986
1987// Persist changes to platform
1988 if ($doSavePlatform) {
1989 $this->platform->save();
1990 }
1991
1992 if ($this->ok) {
1993
1994// Persist changes to cpntext
1995 if (isset($this->context)) {
1996 $this->context->save();
1997 }
1998
1999 if (isset($this->resourceLink)) {
2000// Persist changes to resource link
2001 $this->resourceLink->save();
2002
2003// Persist changes to user instnce
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();
2009 }
2010 } elseif ($this->userResult->isLearner()) { // Ensure all learners are recorded in case Assignment and Grade services are used
2011 $this->userResult->ltiResultSourcedId = '';
2012 $this->userResult->save();
2013 }
2014
2015// Check if a share arrangement is in place for this resource link
2016 $this->ok = $this->checkForShare();
2017 }
2018 }
2019 }
2020 }
2021
2022 return $this->ok;
2023 }
2024
2030 private function checkForShare()
2031 {
2032 $ok = true;
2033 $doSaveResourceLink = true;
2034
2035 $id = $this->resourceLink->primaryResourceLinkId;
2036
2037 $shareRequest = isset($this->messageParameters['custom_share_key']) && !empty($this->messageParameters['custom_share_key']);
2038 if ($shareRequest) {
2039 if (!$this->allowSharing) {
2040 $ok = false;
2041 $this->reason = 'Your sharing request has been refused because sharing is not being permitted.';
2042 } else {
2043// Check if this is a new share key
2044 $shareKey = new ResourceLinkShareKey($this->resourceLink, $this->messageParameters['custom_share_key']);
2045 if (!is_null($shareKey->resourceLinkId)) {
2046// Update resource link with sharing primary resource link details
2047 $id = $shareKey->resourceLinkId;
2048 $ok = ($id !== $this->resourceLink->getRecordId());
2049 if ($ok) {
2050 $this->resourceLink->primaryResourceLinkId = $id;
2051 $this->resourceLink->shareApproved = $shareKey->autoApprove;
2052 $ok = $this->resourceLink->save();
2053 if ($ok) {
2054 $doSaveResourceLink = false;
2055 $this->userResult->getResourceLink()->primaryResourceLinkId = $id;
2056 $this->userResult->getResourceLink()->shareApproved = $shareKey->autoApprove;
2057 $this->userResult->getResourceLink()->updated = time();
2058// Remove share key
2059 $shareKey->delete();
2060 } else {
2061 $this->reason = 'An error occurred initialising your share arrangement.';
2062 }
2063 } else {
2064 $this->reason = 'It is not possible to share your resource link with yourself.';
2065 }
2066 }
2067 if ($ok) {
2068 $ok = !is_null($id);
2069 if (!$ok) {
2070 $this->reason = 'You have requested to share a resource link but none is available.';
2071 } else {
2072 $ok = (!is_null($this->userResult->getResourceLink()->shareApproved) && $this->userResult->getResourceLink()->shareApproved);
2073 if (!$ok) {
2074 $this->reason = 'Your share request is waiting to be approved.';
2075 }
2076 }
2077 }
2078 }
2079 } else {
2080// Check no share is in place
2081 $ok = is_null($id);
2082 if (!$ok) {
2083 $this->reason = 'You have not requested to share a resource link but an arrangement is currently in place.';
2084 }
2085 }
2086
2087// Look up primary resource link
2088 if ($ok && !is_null($id)) {
2089 $resourceLink = ResourceLink::fromRecordId($id, $this->dataConnector);
2090 $ok = !is_null($resourceLink->created);
2091 if ($ok) {
2092 if ($doSaveResourceLink) {
2093 $this->resourceLink->save();
2094 }
2095 $this->resourceLink = $resourceLink;
2096 } else {
2097 $this->reason = 'Unable to load resource link being shared.';
2098 }
2099 }
2100
2101 return $ok;
2102 }
2103
2112 private function sendAuthenticationRequest($parameters, $disableCookieCheck)
2113 {
2114 $clientId = null;
2115 if (isset($parameters['client_id'])) {
2116 $clientId = $parameters['client_id'];
2117 }
2118 $deploymentId = null;
2119 if (isset($parameters['lti_deployment_id'])) {
2120 $deploymentId = $parameters['lti_deployment_id'];
2121 }
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;
2126 Util::logRequest();
2127 }
2128 $ok = !is_null($this->platform) && !empty($this->platform->authenticationUrl);
2129 if (!$ok) {
2130 $this->reason = 'Platform not found or no platform authentication request URL.';
2131 } else {
2132 $oauthRequest = OAuth\OAuthRequest::from_request();
2133 $usePlatformStorage = !empty($oauthRequest->get_parameter('lti_storage_target'));
2134 $session_id = '';
2135 if ($usePlatformStorage) {
2136 $usePlatformStorage = empty($_COOKIE[session_name()]) || ($_COOKIE[session_name()] !== session_id());
2137 }
2138 if (!$disableCookieCheck) {
2139 if (empty(session_id())) {
2140 if (empty($_COOKIE)) {
2141 Util::setTestCookie();
2142 }
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();
2147 }
2148 }
2149 }
2150 do {
2151 $state = Util::getRandomString();
2152 $nonce = new PlatformNonce($this->platform, "{$state}{$session_id}");
2153 $ok = !$nonce->load();
2154 } while (!$ok);
2155 $nonce->expires = time() + Tool::$stateLife;
2156 $ok = $nonce->save();
2157 if ($ok) {
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');
2162 } else { // Remove all parameters added by platform from query string
2163 $ignoreParams = array('iss', 'target_link_uri', 'login_hint', 'lti_message_hint', 'client_id', 'lti_deployment_id', 'lti_storage_target');
2164 }
2165 $queryString = '';
2166 $params = explode('&', $_SERVER['QUERY_STRING']);
2167 $ignore = false; // Only include those query parameters which come before any of the standard OpenID Connect ones
2168 foreach ($params as $param) {
2169 $parts = explode('=', $param, 2);
2170 if (in_array($parts[0], $ignoreParams)) {
2171 $ignore = true;
2172 } elseif (!$ignore) {
2173 if ((count($parts) <= 1) || empty($parts[1])) { // Drop equals sign for empty parameters to workaround Canvas bug
2174 $queryString .= "&{$parts[0]}";
2175 } else {
2176 $queryString .= "&{$parts[0]}={$parts[1]}";
2177 }
2178 }
2179 }
2180 if (!empty($queryString)) {
2181 $queryString = substr($queryString, 1);
2182 $redirectUri .= "?{$queryString}";
2183 }
2184 }
2185 $requestNonce = Util::getRandomString(32);
2186 $params = array(
2187 'client_id' => $this->platform->clientId,
2188 'login_hint' => $parameters['login_hint'],
2189 'nonce' => $requestNonce,
2190 'prompt' => 'none',
2191 'redirect_uri' => $redirectUri,
2192 'response_mode' => 'form_post',
2193 'response_type' => 'id_token',
2194 'scope' => 'openid',
2195 'state' => $nonce->getValue()
2196 );
2197 if (isset($parameters['lti_message_hint'])) {
2198 $params['lti_message_hint'] = $parameters['lti_message_hint'];
2199 }
2200 $this->onInitiateLogin($parameters, $params);
2201 $javascript = '';
2202 if ($usePlatformStorage) {
2203 $javascript = $this->getStorageJS('lti.put_data', $nonce->getValue(), $requestNonce);
2204 }
2205 if (!Tool::$authenticateUsingGet) {
2206 $this->output = Util::sendForm($this->platform->authenticationUrl, $params, '', $javascript);
2207 } else {
2208 Util::redirect($this->platform->authenticationUrl, $params, '', $javascript);
2209 }
2210 } else {
2211 $this->reason = 'Unable to generate a state value.';
2212 }
2213 }
2214
2215 return $ok;
2216 }
2217
2225 private function sendRelaunchRequest($disableCookieCheck)
2226 {
2227 $session_id = '';
2228 if (!$disableCookieCheck) {
2229 if (empty(session_id())) {
2230 if (empty($_COOKIE)) {
2231 Util::setTestCookie();
2232 }
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();
2237 }
2238 }
2239 }
2240 do {
2241 $state = Util::getRandomString();
2242 $nonce = new PlatformNonce($this->platform, "{$state}{$session_id}");
2243 $ok = !$nonce->load();
2244 } while (!$ok);
2245 $nonce->expires = time() + Tool::$stateLife;
2246 $this->ok = $nonce->save();
2247 if ($this->ok) {
2248 $params = array(
2249 'tool_state' => $nonce->getValue(),
2250 'platform_state' => $this->messageParameters['platform_state']
2251 );
2252 $params = $this->platform->addSignature($this->messageParameters['relaunch_url'], $params);
2253 $this->output = Util::sendForm($this->messageParameters['relaunch_url'], $params);
2254 } else {
2255 $this->reason = 'Unable to generate a state value.';
2256 }
2257
2258 return $this->ok;
2259 }
2260
2273 private function checkValue(&$value, $values, $reason, $strictMode, $generateWarnings, $ignoreInvalid = false)
2274 {
2275 $lookupValue = $value;
2276 if (!$strictMode) {
2277 $lookupValue = strtolower($value);
2278 }
2279 $ok = in_array($lookupValue, $values);
2280 if (!$ok) {
2281 if ($this->ok && $strictMode) {
2282 $this->reason = sprintf($reason, $value);
2283 } else {
2284 $ok = true;
2285 if ($generateWarnings) {
2286 $this->warnings[] = sprintf($reason, $value);
2287 }
2288 }
2289 } elseif ($lookupValue !== $value) {
2290 if ($generateWarnings) {
2291 $this->warnings[] = sprintf($reason, $value) . " [Changed to '{$lookupValue}']";
2292 }
2293 $value = $lookupValue;
2294 }
2295
2296 return $ok;
2297 }
2298
2306 private function setError($reason, $strictMode, $generateWarnings)
2307 {
2308 if ($strictMode && $this->ok) {
2309 $this->ok = false;
2310 $this->reason = $reason;
2311 } elseif ($generateWarnings) {
2312 $this->warnings[] = $reason;
2313 }
2314 }
2315
2325 private function getStorageJS($message, $state, $nonce)
2326 {
2327 $javascript = '';
2328 $timeoutDelay = static::$postMessageTimeoutDelay;
2329 $formSubmissionTimeout = Util::$formSubmissionTimeout;
2330 if ($timeoutDelay > 0) {
2331 $parts = explode('.', $state);
2332 $state = $parts[0];
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();
2344let timeout;
2345
2346window.addEventListener('message', function (event) {
2347 let ok = true;
2348 if (typeof event.data !== "object") {
2349 ok = false;
2350 console.log('Error \'response is not an object\': ' + event.data);
2351 }
2352 if (ok && event.data.error) {
2353 ok = false;
2354 if (event.data.error.code && event.data.error.message) {
2355 console.log('Error \'' + event.data.error.code + '\': ' + event.data.error.message);
2356 } else {
2357 console.log(event.data.error);
2358 }
2359 }
2360 if (ok && !event.data.subject) {
2361 ok = false;
2362 console.log('Error: There is no subject specified');
2363 }
2364 if (ok) {
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) {
2370 ok = false;
2371 console.log('Invalid message ID');
2372 } else {
2373 event.data.supported_messages.forEach(function(capability) {
2374 supported.set(capability.subject, (capability.frame) ? capability.frame : target);
2375 });
2376 if (supported.has('{$message}')) {
2377 sendMessage('{$message}');
2378 } else if (supported.has('org.imsglobal.{$message}')) {
2379 sendMessage('org.imsglobal.{$message}');
2380 } else {
2381 submitForm();
2382 }
2383 }
2384 break;
2385 case '{$message}.response':
2386 case 'org.imsglobal.{$message}.response':
2387 clearTimeout(timeout);
2388 if ((event.data.message_id !== messageid) || (event.origin !== origin)) {
2389 ok = false;
2390 console.log('Invalid message ID or origin');
2391 } else if (event.data.key !== state) {
2392 ok = false;
2393 console.log('Key not expected: ' + event.data.key);
2394 } else if (('{$message}' === 'lti.put_data') && (event.data.value != nonce)) {
2395 ok = false;
2396 console.log('Invalid value for key ' + event.data.key + ': ' + event.data.value + ' (expected ' + nonce + ')');
2397 } else {
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';
2402 }
2403 submitForm();
2404 }
2405 break;
2406 default:
2407 console.log('Subject \'' + event.data.subject + '\' not recognised');
2408 break;
2409 }
2410 } else {
2411 clearTimeout(timeout);
2412 }
2413 if (!ok) {
2414 submitForm();
2415 }
2416});
2417
2418function getTarget(frame = '') {
2419 let wdw = window.opener || window.parent;
2420 let targetframe = wdw;
2421 if (frame && (frame !== '_parent')) {
2422 try {
2423 targetframe = wdw.frames[frame];
2424 } catch(err) {
2425 targetframe = null;
2426 }
2427 if (!targetframe) {
2428 try {
2429 targetframe = window.top.frames[frame];
2430 } catch(err) {
2431 console.log('Cannot access storage frame (' + frame + '): ' + err.message);
2432 targetframe = null;
2433 }
2434 }
2435 }
2436 if (targetframe === window) {
2437 targetframe = null;
2438 }
2439 if (!targetframe) {
2440 console.log('No target frame found');
2441 }
2442
2443 return targetframe;
2444}
2445
2446
2447EOD;
2448 switch ($message) {
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);
2455 }
2456 let targetframe = getTarget(usetarget);
2457 if (targetframe) {
2458 try {
2459 targetframe.postMessage({
2460 'subject': subject,
2461 'message_id': messageid,
2462 'key': state,
2463 'value': nonce
2464 }, origin);
2465 } catch(err) {
2466 console.log(err.name + ': ' + err.message);
2467 }
2468 } else {
2469 saveData();
2470 }
2471}
2472
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
2477 submitForm();
2478 }, {$timeoutDelay});
2479 sendMessage('lti.put_data');
2480 }, {$timeoutDelay});
2481 checkCapabilities('org.imsglobal.lti.capabilities', true);
2482 }, {$timeoutDelay});
2483 checkCapabilities('lti.capabilities', false);
2484}
2485
2486EOD;
2487 break;
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);
2494 }
2495 let targetframe = getTarget(usetarget);
2496 if (targetframe) {
2497 try {
2498 targetframe.postMessage({
2499 'subject': subject,
2500 'message_id': messageid,
2501 'key': state
2502 }, origin);
2503 } catch(err) {
2504 console.log(err.name + ': ' + err.message);
2505 }
2506 }
2507}
2508
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
2513 submitForm();
2514 }, {$timeoutDelay});
2515 sendMessage('lti.get_data');
2516 }, {$timeoutDelay});
2517 checkCapabilities('org.imsglobal.lti.capabilities', true);
2518 }, {$timeoutDelay});
2519 checkCapabilities('lti.capabilities', false);
2520}
2521
2522EOD;
2523 break;
2524 }
2525
2526 $javascript .= <<< EOD
2527
2528function checkCapabilities(subject, checkparent) {
2529 let wdw = getTarget(target);
2530 if (wdw) {
2531 try {
2532 wdw.postMessage({
2533 'subject': subject,
2534 'message_id': capabilitiesid
2535 }, '*');
2536 if (checkparent && (wdw !== window.parent)) {
2537 window.parent.postMessage({
2538 'subject': subject,
2539 'message_id': capabilitiesid
2540 }, '*');
2541 }
2542 } catch(err) {
2543 console.log(err.name + ': ' + err.message);
2544 }
2545 }
2546}
2547
2548function doUnblock() {
2549 var el = document.getElementById('id_blocked');
2550 el.style.display = 'block';
2551}
2552
2553function submitForm() {
2554 if ((document.forms[0].target === '_blank') && (window.top === window.self)) {
2555 document.forms[0].target = '';
2556 }
2557 window.setTimeout(doUnblock, {$formSubmissionTimeout}000);
2558 document.forms[0].submit();
2559}
2560
2561window.onload=doOnLoad;
2562EOD;
2563 }
2564
2565 return $javascript;
2566 }
2567
2568}
Trait to handle API hook registrations.
Definition ApiHook.php:13
Class to represent a content-item object.
getDataConnector()
Get the data connector.
Definition Context.php:287
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.
Definition Jwt.php:15
Class to represent an LTI Tool Proxy media type.
Definition ToolProxy.php:16
Class to represent a platform.
Definition Platform.php:18
static fromConsumerKey($key=null, $dataConnector=null, $autoEnable=false)
Load the platform from the database by its consumer key.
Definition Platform.php:486
Class to represent a generic item object.
Class to represent an LTI system.
Definition System.php:22
sendMessage($url, $type, $messageParams, $target='', $userId=null, $hint=null)
Generate a web page containing an auto-submitted form of LTI message parameters.
Definition System.php:948
$reason
Error message for last request processed.
Definition System.php:109
$name
Local name of platform/tool.
Definition System.php:43
$enabled
Whether the system instance is enabled to accept connection requests.
Definition System.php:137
$secret
Shared secret.
Definition System.php:50
$dataConnector
Data connector object.
Definition System.php:71
verifySignature()
Verify the signature of a message.
Definition System.php:1095
$messageParameters
LTI message parameters.
Definition System.php:193
$jku
Endpoint for public key.
Definition System.php:102
$ok
True if the last request was successful.
Definition System.php:29
checkMessage()
Verify the required properties of an LTI message.
Definition System.php:1038
static parseRoles($roles, $ltiVersion=Util::LTI_VERSION1, $addPrincipalRole=false)
Parse a set of roles to comply with a specified version of LTI.
Definition System.php:568
Class to represent an LTI Tool.
Definition Tool.php:24
getMessageParameters($strictMode=false, $disableCookieCheck=false, $generateWarnings=false)
Get the message parameters.
Definition Tool.php:448
onRegister()
Process a valid tool proxy registration request.
Definition Tool.php:709
onError()
Process a response to an invalid request.
Definition Tool.php:829
onRegistration()
Process a dynamic registration request.
Definition Tool.php:718
$redirectionUris
Redirection URIs for Tool.
Definition Tool.php:287
getPlatformToRegister($platformConfig, $registrationConfig, $doSave=true)
Initialise the platform to be registered.
Definition Tool.php:1041
$documentTargets
Document targets accepted by the platform.
Definition Tool.php:350
const ID_SCOPE_RESOURCE
Prefix the ID with the consumer key and resource ID.
Definition Tool.php:49
$redirectUrl
URL to redirect user to on successful completion of the request.
Definition Tool.php:322
onSubmissionReview()
Process a valid submission review request.
Definition Tool.php:700
handleRequest($strictMode=false, $disableCookieCheck=false, $generateWarnings=false)
Process an incoming request.
Definition Tool.php:481
setParameterConstraint($name, $required=true, $maxLength=null, $messageTypes=null)
Add a parameter constraint to be checked on launch.
Definition Tool.php:536
sendRegistration($platformConfig, $toolConfig)
Send the tool registration to the platform.
Definition Tool.php:1006
static $MESSAGE_TYPES
List of supported incoming message types.
Definition Tool.php:59
$vendor
Vendor details.
Definition Tool.php:238
const ID_SCOPE_SEPARATOR
Character used to separate each element of an ID.
Definition Tool.php:54
onConfigure()
Process a valid configure request.
Definition Tool.php:664
onLtiEndAssessment()
Process a valid end assessment request.
Definition Tool.php:744
static fromInitiateLoginUrl($initiateLoginUrl, $dataConnector=null, $autoEnable=false)
Load the tool from the database by its initiate login URL.
Definition Tool.php:1210
const ID_SCOPE_ID_ONLY
Use ID value only.
Definition Tool.php:34
getConsumers()
Get an array of defined tool consumers.
Definition Tool.php:552
$errorOutput
HTML to be displayed on an unsuccessful completion of the request and no return URL is available.
Definition Tool.php:364
static $stateLife
Life in seconds for the state value issued during the OIDC login process.
Definition Tool.php:308
static $postMessageTimeoutDelay
Period in milliseconds to wait for a response to a postMessage.
Definition Tool.php:315
$returnUrl
Return URL provided by platform.
Definition Tool.php:175
$platform
Platform object.
Definition Tool.php:168
$contentTypes
Content item types accepted by the platform.
Definition Tool.php:336
$mediaTypes
Media types accepted by the platform.
Definition Tool.php:329
getRegistrationResponsePage($toolConfig)
Prepare the page to complete a registration request.
Definition Tool.php:1077
$defaultEmail
Default email domain.
Definition Tool.php:203
onContentItem()
Process a valid content-item request.
Definition Tool.php:682
$userResult
UserResult object.
Definition Tool.php:182
onLaunch()
Process a valid launch request.
Definition Tool.php:655
$messageUrl
Message URL for Tool.
Definition Tool.php:273
initialize()
Initialise the tool.
Definition Tool.php:391
onContentItemUpdate()
Process a valid content-item update request.
Definition Tool.php:691
getPlatformConfiguration()
Fetch a platform's configuration data.
Definition Tool.php:839
const ID_SCOPE_GLOBAL
Prefix an ID with the consumer key.
Definition Tool.php:39
static sendForm($url, $params, $target='')
Generate a web page containing an auto-submitted form of parameters.
Definition Tool.php:641
$context
Context object.
Definition Tool.php:196
getConfiguration($platformConfig)
Prepare the tool's configuration data.
Definition Tool.php:895
getPlatforms()
Get an array of defined platforms.
Definition Tool.php:564
$consumer
Tool consumer object.
Definition Tool.php:161
const ID_SCOPE_CONTEXT
Prefix the ID with the consumer key and context ID.
Definition Tool.php:44
$resourceHandlers
Resource handlers for Tool.
Definition Tool.php:266
onAuthenticate($state, $nonce, $usePlatformStorage)
Process response to an authentication request.
Definition Tool.php:778
onResetSessionId()
Process a change in the session ID.
Definition Tool.php:821
const CONNECTION_ERROR_MESSAGE
Default connection error message.
Definition Tool.php:29
$optionalServices
Optional services used by Tool.
Definition Tool.php:259
$baseUrl
Base URL for tool service.
Definition Tool.php:231
$output
Default HTML to be displayed on a successful completion of the request.
Definition Tool.php:357
$resourceLink
Resource link object.
Definition Tool.php:189
$initiateLoginUrl
Initiate Login request URL for Tool.
Definition Tool.php:280
findService($format, $methods)
Find an offered service based on a media type and HTTP action(s)
Definition Tool.php:577
onInitiateLogin($requestParameters, &$authParameters)
Process a login initiation request.
Definition Tool.php:756
$idScope
Scope to use for user IDs.
Definition Tool.php:210
$message
Message for last request processed.
Definition Tool.php:224
static $defaultTool
Default tool for use with service requests.
Definition Tool.php:294
doToolProxyService()
Send the tool proxy to the platform.
Definition Tool.php:610
static fromConsumerKey($key=null, $dataConnector=null, $autoEnable=false)
Load the tool from the database by its consumer key.
Definition Tool.php:1187
$requiredServices
Services required by Tool.
Definition Tool.php:252
$product
Product details.
Definition Tool.php:245
$fileTypes
File types accepted by the platform.
Definition Tool.php:343
onLtiStartProctoring()
Process a valid start proctoring request.
Definition Tool.php:735
onDashboard()
Process a valid dashboard request.
Definition Tool.php:673
static $authenticateUsingGet
Use GET method for authentication request messages when true.
Definition Tool.php:301
__construct($dataConnector=null)
Class constructor.
Definition Tool.php:378
save()
Save the tool to the database.
Definition Tool.php:424
static fromRecordId($id, $dataConnector)
Load the tool from the database by its record ID.
Definition Tool.php:1231
$allowSharing
Whether shared resource link arrangements are permitted.
Definition Tool.php:217
static fromResourceLink($resourceLink, $ltiUserId)
Class constructor from resource link.
Class to represent a platform user.
Definition User.php:13
const PRINCIPAL_ROLES
List of principal roles for LTI 1.3.
Definition User.php:18
Class to implement utility methods.
Definition Util.php:15
static sendForm($url, $params, $target='', $javascript='')
Generate a web page containing an auto-submitted form of parameters.
Definition Util.php:378
const LTI_VERSION1P3
LTI version 1.3 for messages.
Definition Util.php:25
const MESSAGE_TYPE_MAPPING
Mapping for standard message types.
Definition Util.php:40
static getRandomString($length=8)
Generate a random string.
Definition Util.php:523
static logRequest($debugLevel=false)
Log a request received.
Definition Util.php:286
static $logLevel
Current logging level.
Definition Util.php:202
static logError($message, $showSource=true)
Log an error message.
Definition Util.php:248
static getRequestParameters()
Return GET and POST request parameters (POST parameters take precedence).
Definition Util.php:233
const LTI_VERSION1
LTI version 1 for messages.
Definition Util.php:20
const LOGLEVEL_DEBUG
Log all messages.
Definition Util.php:159
static jsonDecode($str, $associative=false)
Decode a JSON string.
Definition Util.php:560
static logDebug($message, $showSource=false)
Log a debug message.
Definition Util.php:274
const LTI_VERSION2
LTI version 2 for messages.
Definition Util.php:30