LTI Integration Library  3.1.0
PHP class library for building LTI integrations
ToolProvider.php
Go to the documentation of this file.
1 <?php
2 
3 namespace ceLTIc\LTI;
4 
11 
21 {
22  use ApiHook;
23 
27  const CONNECTION_ERROR_MESSAGE = 'Sorry, there was an error connecting you to the application.';
28 
32  const LTI_VERSION1 = 'LTI-1p0';
33 
37  const LTI_VERSION2 = 'LTI-2p0';
38 
42  const ID_SCOPE_ID_ONLY = 0;
43 
47  const ID_SCOPE_GLOBAL = 1;
48 
52  const ID_SCOPE_CONTEXT = 2;
53 
57  const ID_SCOPE_RESOURCE = 3;
58 
62  const ID_SCOPE_SEPARATOR = ':';
63 
67  private static $LTI_VERSIONS = array(self::LTI_VERSION1, self::LTI_VERSION2);
68 
72  private static $METHOD_NAMES = array('basic-lti-launch-request' => 'onLaunch',
73  'ConfigureLaunchRequest' => 'onConfigure',
74  'DashboardRequest' => 'onDashboard',
75  'ContentItemSelectionRequest' => 'onContentItem',
76  'ToolProxyRegistrationRequest' => 'onRegister'
77  );
78 
82  private static $LTI_CONSUMER_SETTING_NAMES = array('custom_tc_profile_url', 'custom_system_setting_url', 'custom_oauth2_access_token_url');
83 
87  private static $LTI_CONTEXT_SETTING_NAMES = array('custom_context_setting_url',
88  'custom_lineitems_url', 'custom_results_url',
89  'custom_context_memberships_url');
90 
94  private static $LTI_RESOURCE_LINK_SETTING_NAMES = array('lis_result_sourcedid', 'lis_outcome_service_url',
95  'ext_ims_lis_basic_outcome_url', 'ext_ims_lis_resultvalue_sourcedids',
96  'ext_ims_lis_memberships_id', 'ext_ims_lis_memberships_url',
97  'ext_ims_lti_tool_setting', 'ext_ims_lti_tool_setting_id', 'ext_ims_lti_tool_setting_url',
98  'custom_link_setting_url',
99  'custom_lineitem_url', 'custom_result_url');
100 
104  private static $CUSTOM_SUBSTITUTION_VARIABLES = array('User.id' => 'user_id',
105  'User.image' => 'user_image',
106  'User.username' => 'username',
107  'User.scope.mentor' => 'role_scope_mentor',
108  'Membership.role' => 'roles',
109  'Person.sourcedId' => 'lis_person_sourcedid',
110  'Person.name.full' => 'lis_person_name_full',
111  'Person.name.family' => 'lis_person_name_family',
112  'Person.name.given' => 'lis_person_name_given',
113  'Person.email.primary' => 'lis_person_contact_email_primary',
114  'Context.id' => 'context_id',
115  'Context.type' => 'context_type',
116  'Context.title' => 'context_title',
117  'Context.label' => 'context_label',
118  'CourseOffering.sourcedId' => 'lis_course_offering_sourcedid',
119  'CourseSection.sourcedId' => 'lis_course_section_sourcedid',
120  'CourseSection.label' => 'context_label',
121  'CourseSection.title' => 'context_title',
122  'ResourceLink.id' => 'resource_link_id',
123  'ResourceLink.title' => 'resource_link_title',
124  'ResourceLink.description' => 'resource_link_description',
125  'Result.sourcedId' => 'lis_result_sourcedid',
126  'BasicOutcome.url' => 'lis_outcome_service_url',
127  'ToolConsumerProfile.url' => 'custom_tc_profile_url',
128  'ToolProxy.url' => 'tool_proxy_url',
129  'ToolProxy.custom.url' => 'custom_system_setting_url',
130  'ToolProxyBinding.custom.url' => 'custom_context_setting_url',
131  'LtiLink.custom.url' => 'custom_link_setting_url',
132  'LineItems.url' => 'custom_lineitems_url',
133  'LineItem.url' => 'custom_lineitem_url',
134  'Results.url' => 'custom_results_url',
135  'Result.url' => 'custom_result_url',
136  'ToolProxyBinding.memberships.url' => 'custom_context_memberships_url',
137  'LtiLink.memberships.url' => 'custom_link_memberships_url');
138 
144  public $ok = true;
145 
151  public $consumer = null;
152 
158  public $returnUrl = null;
159 
165  public $userResult = null;
166 
172  public $resourceLink = null;
173 
179  public $context = null;
180 
186  public $dataConnector = null;
187 
193  public $defaultEmail = '';
194 
201 
207  public $allowSharing = false;
208 
215 
221  public $reason = null;
222 
228  public $details = array();
229 
235  public $baseUrl = null;
236 
242  public $vendor = null;
243 
249  public $product = null;
250 
256  public $requiredServices = null;
257 
263  public $optionalServices = null;
264 
270  public $resourceHandlers = null;
271 
277  protected $redirectUrl = null;
278 
284  protected $mediaTypes = null;
285 
291  protected $documentTargets = null;
292 
298  protected $output = null;
299 
305  protected $errorOutput = null;
306 
312  protected $debugMode = false;
313 
319  protected $messageParameters = null;
320 
326  private $constraints = null;
327 
333  function __construct($dataConnector)
334  {
335  $this->constraints = array();
336  $this->dataConnector = $dataConnector;
337  $this->ok = !is_null($this->dataConnector);
338  $this->vendor = new Profile\Item();
339  $this->product = new Profile\Item();
340  $this->requiredServices = array();
341  $this->optionalServices = array();
342  $this->resourceHandlers = array();
343  }
344 
348  public function handleRequest()
349  {
350  if ($this->ok) {
351  $this->getMessageParameters();
352  if ($this->authenticate()) {
353  $this->doCallback();
354  }
355  }
356  $this->result();
357  }
358 
367  public function setParameterConstraint($name, $required = true, $maxLength = null, $messageTypes = null)
368  {
369  $name = trim($name);
370  if (strlen($name) > 0) {
371  $this->constraints[$name] = array('required' => $required, 'max_length' => $maxLength, 'messages' => $messageTypes);
372  }
373  }
374 
380  public function getConsumers()
381  {
382  return $this->dataConnector->getToolConsumers();
383  }
384 
393  public function findService($format, $methods)
394  {
395  $found = false;
396  $services = $this->consumer->profile->service_offered;
397  if (is_array($services)) {
398  $n = -1;
399  foreach ($services as $service) {
400  $n++;
401  if (!is_array($service->format) || !in_array($format, $service->format)) {
402  continue;
403  }
404  $missing = array();
405  foreach ($methods as $method) {
406  if (!is_array($service->action) || !in_array($method, $service->action)) {
407  $missing[] = $method;
408  }
409  }
410  $methods = $missing;
411  if (count($methods) <= 0) {
412  $found = $service;
413  break;
414  }
415  }
416  }
417 
418  return $found;
419  }
420 
426  public function doToolProxyService()
427  {
428 // Create tool proxy
429  $toolProxyService = $this->findService('application/vnd.ims.lti.v2.toolproxy+json', array('POST'));
431  $toolProxy = new MediaType\ToolProxy($this, $toolProxyService, $secret);
432  $http = $this->consumer->doServiceRequest($toolProxyService, 'POST', 'application/vnd.ims.lti.v2.toolproxy+json',
433  json_encode($toolProxy));
434  $ok = $http->ok && ($http->status == 201) && isset($http->responseJson->tool_proxy_guid) && (strlen($http->responseJson->tool_proxy_guid) > 0);
435  if ($ok) {
436  $this->consumer->setKey($http->responseJson->tool_proxy_guid);
437  $this->consumer->secret = $toolProxy->security_contract->shared_secret;
438  $this->consumer->toolProxy = json_encode($toolProxy);
439  $this->consumer->save();
440  }
441 
442  return $ok;
443  }
444 
450  public function getMessageParameters()
451  {
452  if ($this->ok && is_null($this->messageParameters)) {
453  $this->messageParameters = OAuth\OAuthUtil::parse_parameters(file_get_contents(OAuth\OAuthRequest::$POST_INPUT));
454  if (!empty($this->messageParameters['oauth_consumer_key'])) {
455  $this->consumer = new ToolConsumer($this->messageParameters['oauth_consumer_key'], $this->dataConnector);
456  }
457 
458 // Set debug mode
459  $this->debugMode = isset($this->messageParameters['custom_debug']) && (strtolower($this->messageParameters['custom_debug']) === 'true');
460 // Set return URL if available
461  if (isset($this->messageParameters['launch_presentation_return_url'])) {
462  $this->returnUrl = $this->messageParameters['launch_presentation_return_url'];
463  } elseif (isset($this->messageParameters['content_item_return_url'])) {
464  $this->returnUrl = $this->messageParameters['content_item_return_url'];
465  }
466  }
467 
469  }
470 
479  public static function parseRoles($roles, $ltiVersion = self::LTI_VERSION1)
480  {
481  if (!is_array($roles)) {
482  $roles = explode(',', $roles);
483  }
484  $parsedRoles = array();
485  foreach ($roles as $role) {
486  $role = trim($role);
487  if (!empty($role)) {
488  if ($ltiVersion === self::LTI_VERSION1) {
489  if (substr($role, 0, 4) !== 'urn:') {
490  $role = 'urn:lti:role:ims/lis/' . $role;
491  }
492  } elseif ((substr($role, 0, 7) !== 'http://') && (substr($role, 0, 8) !== 'https://')) {
493  $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . $role;
494  }
495  $parsedRoles[] = $role;
496  }
497  }
498 
499  return $parsedRoles;
500  }
501 
511  public static function sendForm($url, $params, $target = '')
512  {
513  $page = <<< EOD
514 <html>
515 <head>
516 <title>IMS LTI message</title>
517 <script type="text/javascript">
518 //<![CDATA[
519 function doOnLoad() {
520  document.forms[0].submit();
521 }
522 
523 window.onload=doOnLoad;
524 //]]>
525 </script>
526 </head>
527 <body>
528 <form action="{$url}" method="post" target="" encType="application/x-www-form-urlencoded">
529 
530 EOD;
531  foreach ($params as $key => $value) {
532  $key = htmlentities($key, ENT_COMPAT | ENT_HTML401, 'UTF-8');
533  if (!is_array($value)) {
534  $value = htmlentities($value, ENT_COMPAT | ENT_HTML401, 'UTF-8');
535  $page .= <<< EOD
536  <input type="hidden" name="{$key}" value="{$value}" />
537 
538 EOD;
539  } else {
540  foreach ($value as $element) {
541  $element = htmlentities($element, ENT_COMPAT | ENT_HTML401, 'UTF-8');
542  $page .= <<< EOD
543  <input type="hidden" name="{$key}" value="{$element}" />
544 
545 EOD;
546  }
547  }
548  }
549 
550  $page .= <<< EOD
551 </form>
552 </body>
553 </html>
554 EOD;
555 
556  return $page;
557  }
558 
559 ###
560 ### PROTECTED METHODS
561 ###
562 
568  protected function onLaunch()
569  {
570  $this->onError();
571  }
572 
578  protected function onConfigure()
579  {
580  $this->onError();
581  }
582 
588  protected function onDashboard()
589  {
590  $this->onError();
591  }
592 
598  protected function onContentItem()
599  {
600  $this->onError();
601  }
602 
608  protected function onRegister()
609  {
610  $this->onError();
611  }
612 
618  protected function onError()
619  {
620  return false;
621  }
622 
623 ###
624 ### PRIVATE METHODS
625 ###
626 
634  private function doCallback($method = null)
635  {
636  $callback = $method;
637  if (is_null($callback)) {
638  $callback = self::$METHOD_NAMES[$this->messageParameters['lti_message_type']];
639  }
640  if (method_exists($this, $callback)) {
641  $this->$callback();
642  } elseif (is_null($method) && $this->ok) {
643  $this->ok = false;
644  $this->reason = "Message type not supported: {$this->messageParameters['lti_message_type']}";
645  }
646  if ($this->ok && ($this->messageParameters['lti_message_type'] == 'ToolProxyRegistrationRequest')) {
647  $this->consumer->save();
648  }
649  }
650 
658  private function result()
659  {
660  $ok = false;
661  if (!$this->ok) {
662  $this->onError();
663  }
664  if (!$ok) {
665  if (!$this->ok) {
666 
667 // If not valid, return an error message to the tool consumer if a return URL is provided
668  if (!empty($this->returnUrl)) {
669  $errorUrl = $this->returnUrl;
670  if (strpos($errorUrl, '?') === false) {
671  $errorUrl .= '?';
672  } else {
673  $errorUrl .= '&';
674  }
675  if ($this->debugMode && !is_null($this->reason)) {
676  $errorUrl .= 'lti_errormsg=' . urlencode("Debug error: $this->reason");
677  } else {
678  $errorUrl .= 'lti_errormsg=' . urlencode($this->message);
679  if (!is_null($this->reason)) {
680  $errorUrl .= '&lti_errorlog=' . urlencode("Debug error: $this->reason");
681  }
682  }
683  if (!is_null($this->consumer) && isset($this->messageParameters['lti_message_type']) && (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') ||
684  ($this->messageParameters['lti_message_type'] === 'LtiDeepLinkingRequest'))) {
685  $formParams = array();
686  if (isset($this->messageParameters['data'])) {
687  $formParams['data'] = $this->messageParameters['data'];
688  }
689  $version = (isset($this->messageParameters['lti_version'])) ? $this->messageParameters['lti_version'] : self::LTI_VERSION1;
690  $formParams = $this->consumer->signParameters($errorUrl, 'ContentItemSelection', $version, $formParams);
691  $page = self::sendForm($errorUrl, $formParams);
692  echo $page;
693  } else {
694  header("Location: {$errorUrl}");
695  }
696  exit;
697  } else {
698  if (!is_null($this->errorOutput)) {
699  echo $this->errorOutput;
700  } elseif ($this->debugMode && !empty($this->reason)) {
701  echo "Debug error: {$this->reason}";
702  } else {
703  echo "Error: {$this->message}";
704  }
705  }
706  } elseif (!is_null($this->redirectUrl)) {
707  header("Location: {$this->redirectUrl}");
708  exit;
709  } elseif (!is_null($this->output)) {
710  echo $this->output;
711  }
712  }
713  }
714 
722  private function authenticate()
723  {
724 // Get the consumer
725  $doSaveConsumer = false;
726  $this->ok = $_SERVER['REQUEST_METHOD'] === 'POST';
727  if (!$this->ok) {
728  $this->reason = 'Message should be an HTTP POST request';
729  }
730 // Check all required launch parameters
731  if ($this->ok) {
732  $this->ok = isset($this->messageParameters['lti_message_type']) && array_key_exists($this->messageParameters['lti_message_type'],
733  self::$METHOD_NAMES);
734  if (!$this->ok) {
735  $this->reason = 'Invalid or missing lti_message_type parameter.';
736  }
737  }
738  if ($this->ok) {
739  $this->ok = isset($this->messageParameters['lti_version']) && in_array($this->messageParameters['lti_version'],
740  self::$LTI_VERSIONS);
741  if (!$this->ok) {
742  $this->reason = 'Invalid or missing lti_version parameter.';
743  }
744  }
745  if ($this->ok) {
746  if (($this->messageParameters['lti_message_type'] === 'basic-lti-launch-request') || ($this->messageParameters['lti_message_type'] === 'LtiResourceLinkRequest') || ($this->messageParameters['lti_message_type'] === 'DashboardRequest')) {
747  $this->ok = isset($this->messageParameters['resource_link_id']) && (strlen(trim($this->messageParameters['resource_link_id'])) > 0);
748  if (!$this->ok) {
749  $this->reason = 'Missing resource link ID.';
750  }
751  } elseif (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') || ($this->messageParameters['lti_message_type'] === 'LtiDeepLinkingRequest')) {
752  if (isset($this->messageParameters['accept_media_types']) && (strlen(trim($this->messageParameters['accept_media_types'])) > 0)) {
753  $mediaTypes = array_filter(explode(',', str_replace(' ', '', $this->messageParameters['accept_media_types'])),
754  'strlen');
755  $mediaTypes = array_unique($mediaTypes);
756  $this->ok = count($mediaTypes) > 0;
757  if (!$this->ok) {
758  $this->reason = 'No accept_media_types found.';
759  } else {
760  $this->mediaTypes = $mediaTypes;
761  }
762  } else {
763  $this->ok = false;
764  }
765  if ($this->ok && isset($this->messageParameters['accept_presentation_document_targets']) && (strlen(trim($this->messageParameters['accept_presentation_document_targets'])) > 0)) {
766  $documentTargets = array_filter(explode(',',
767  str_replace(' ', '', $this->messageParameters['accept_presentation_document_targets'])), 'strlen');
768  $documentTargets = array_unique($documentTargets);
769  $this->ok = count($documentTargets) > 0;
770  if (!$this->ok) {
771  $this->reason = 'Missing or empty accept_presentation_document_targets parameter.';
772  } else {
773  foreach ($documentTargets as $documentTarget) {
774  $this->ok = $this->checkValue($documentTarget,
775  array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none'),
776  'Invalid value in accept_presentation_document_targets parameter: %s.');
777  if (!$this->ok) {
778  break;
779  }
780  }
781  if ($this->ok) {
782  $this->documentTargets = $documentTargets;
783  }
784  }
785  } else {
786  $this->ok = false;
787  }
788  if ($this->ok) {
789  $this->ok = isset($this->messageParameters['content_item_return_url']) && (strlen(trim($this->messageParameters['content_item_return_url'])) > 0);
790  if (!$this->ok) {
791  $this->reason = 'Missing content_item_return_url parameter.';
792  }
793  }
794  } elseif ($this->messageParameters['lti_message_type'] == 'ToolProxyRegistrationRequest') {
795  $this->ok = ((isset($this->messageParameters['reg_key']) && (strlen(trim($this->messageParameters['reg_key'])) > 0)) &&
796  (isset($this->messageParameters['reg_password']) && (strlen(trim($this->messageParameters['reg_password'])) > 0)) &&
797  (isset($this->messageParameters['tc_profile_url']) && (strlen(trim($this->messageParameters['tc_profile_url'])) > 0)) &&
798  (isset($this->messageParameters['launch_presentation_return_url']) && (strlen(trim($this->messageParameters['launch_presentation_return_url'])) > 0)));
799  if ($this->debugMode && !$this->ok) {
800  $this->reason = 'Missing message parameters.';
801  }
802  }
803  }
804  $now = time();
805 // Check consumer key
806  if ($this->ok && ($this->messageParameters['lti_message_type'] != 'ToolProxyRegistrationRequest')) {
807  $this->ok = isset($this->messageParameters['oauth_consumer_key']);
808  if (!$this->ok) {
809  $this->reason = 'Missing consumer key.';
810  }
811  if ($this->ok) {
812  $this->ok = !is_null($this->consumer->created);
813  if (!$this->ok) {
814  $this->reason = 'Invalid consumer key: ' . $this->messageParameters['oauth_consumer_key'];
815  }
816  }
817  if ($this->ok) {
818  $today = date('Y-m-d', $now);
819  if (is_null($this->consumer->lastAccess)) {
820  $doSaveConsumer = true;
821  } else {
822  $last = date('Y-m-d', $this->consumer->lastAccess);
823  $doSaveConsumer = $doSaveConsumer || ($last !== $today);
824  }
825  $this->consumer->lastAccess = $now;
826  $this->consumer->signatureMethod = isset($this->messageParameters['oauth_signature_method']) ? $this->messageParameters['oauth_signature_method'] :
827  $this->consumer->signatureMethod;
828  try {
829  $store = new OAuthDataStore($this);
830  $server = new OAuth\OAuthServer($store);
832  $server->add_signature_method($method);
834  $server->add_signature_method($method);
836  $server->add_signature_method($method);
838  $server->add_signature_method($method);
839  $method = new OAuth\OAuthSignatureMethod_HMAC_SHA1();
840  $server->add_signature_method($method);
842  $res = $server->verify_request($request);
843  } catch (\Exception $e) {
844  $this->ok = false;
845  if (empty($this->reason)) {
846  $consumer = new OAuth\OAuthConsumer($this->consumer->getKey(), $this->consumer->secret);
847  $signature = $request->build_signature($method, $consumer, false);
848  if ($this->debugMode) {
849  $this->reason = $e->getMessage();
850  }
851  if (empty($this->reason)) {
852  $this->reason = 'OAuth signature check failed - perhaps an incorrect secret or timestamp.';
853  }
854  $this->details[] = 'Current timestamp: ' . time();
855  $this->details[] = "Expected signature: {$signature}";
856  $this->details[] = "Base string: {$request->base_string}";
857  }
858  }
859  }
860  if ($this->ok) {
861  if ($this->consumer->protected) {
862  if (!is_null($this->consumer->consumerGuid)) {
863  $this->ok = empty($this->messageParameters['tool_consumer_instance_guid']) ||
864  ($this->consumer->consumerGuid === $this->messageParameters['tool_consumer_instance_guid']);
865  if (!$this->ok) {
866  $this->reason = 'Request is from an invalid tool consumer.';
867  }
868  } else {
869  $this->ok = isset($this->messageParameters['tool_consumer_instance_guid']);
870  if (!$this->ok) {
871  $this->reason = 'A tool consumer GUID must be included in the launch request.';
872  }
873  }
874  }
875  if ($this->ok) {
876  $this->ok = $this->consumer->enabled;
877  if (!$this->ok) {
878  $this->reason = 'Tool consumer has not been enabled by the tool provider.';
879  }
880  }
881  if ($this->ok) {
882  $this->ok = is_null($this->consumer->enableFrom) || ($this->consumer->enableFrom <= $now);
883  if ($this->ok) {
884  $this->ok = is_null($this->consumer->enableUntil) || ($this->consumer->enableUntil > $now);
885  if (!$this->ok) {
886  $this->reason = 'Tool consumer access has expired.';
887  }
888  } else {
889  $this->reason = 'Tool consumer access is not yet available.';
890  }
891  }
892  }
893 
894 // Validate other message parameter values
895  if ($this->ok) {
896  if (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') || ($this->messageParameters['lti_message_type'] === 'LtiDeepLinkingRequest')) {
897  if (isset($this->messageParameters['accept_unsigned'])) {
898  $this->ok = $this->checkValue($this->messageParameters['accept_unsigned'], array('true', 'false'),
899  'Invalid value for accept_unsigned parameter: %s.');
900  }
901  if ($this->ok && isset($this->messageParameters['accept_multiple'])) {
902  $this->ok = $this->checkValue($this->messageParameters['accept_multiple'], array('true', 'false'),
903  'Invalid value for accept_multiple parameter: %s.');
904  }
905  if ($this->ok && isset($this->messageParameters['accept_copy_advice'])) {
906  $this->ok = $this->checkValue($this->messageParameters['accept_copy_advice'], array('true', 'false'),
907  'Invalid value for accept_copy_advice parameter: %s.');
908  }
909  if ($this->ok && isset($this->messageParameters['auto_create'])) {
910  $this->ok = $this->checkValue($this->messageParameters['auto_create'], array('true', 'false'),
911  'Invalid value for auto_create parameter: %s.');
912  }
913  if ($this->ok && isset($this->messageParameters['can_confirm'])) {
914  $this->ok = $this->checkValue($this->messageParameters['can_confirm'], array('true', 'false'),
915  'Invalid value for can_confirm parameter: %s.');
916  }
917  } elseif (isset($this->messageParameters['launch_presentation_document_target'])) {
918  $this->ok = $this->checkValue($this->messageParameters['launch_presentation_document_target'],
919  array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay'),
920  'Invalid value for launch_presentation_document_target parameter: %s.');
921  }
922  }
923  }
924 
925  if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) {
926  $this->ok = $this->messageParameters['lti_version'] == self::LTI_VERSION2;
927  if (!$this->ok) {
928  $this->reason = 'Invalid lti_version parameter';
929  }
930  if ($this->ok) {
931  $url = $this->messageParameters['tc_profile_url'];
932  if (strpos($url, '?') === FALSE) {
933  $url .= '?';
934  } else {
935  $url .= '&';
936  }
937  $url .= 'lti_version=' . self::LTI_VERSION2;
938  $http = new HTTPMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');
939  $this->ok = $http->send();
940  if (!$this->ok) {
941  $this->reason = 'Tool consumer profile not accessible.';
942  } else {
943  $tcProfile = json_decode($http->response);
944  $this->ok = !is_null($tcProfile);
945  if (!$this->ok) {
946  $this->reason = 'Invalid JSON in tool consumer profile.';
947  }
948  }
949  }
950 // Check for required capabilities
951  if ($this->ok) {
952  $this->consumer = new ToolConsumer($this->messageParameters['reg_key'], $this->dataConnector);
953  $this->consumer->profile = $tcProfile;
954  $capabilities = $this->consumer->profile->capability_offered;
955  $missing = array();
956  foreach ($this->resourceHandlers as $resourceHandler) {
957  foreach ($resourceHandler->requiredMessages as $message) {
958  if (!in_array($message->type, $capabilities)) {
959  $missing[$message->type] = true;
960  }
961  }
962  }
963  foreach ($this->constraints as $name => $constraint) {
964  if ($constraint['required']) {
965  if (!in_array($name, $capabilities)) {
966  $missing[$name] = true;
967  }
968  }
969  }
970  if (!empty($missing)) {
971  ksort($missing);
972  $this->reason = 'Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\'';
973  $this->ok = false;
974  }
975  }
976 // Check for required services
977  if ($this->ok) {
978  foreach ($this->requiredServices as $service) {
979  foreach ($service->formats as $format) {
980  if (!$this->findService($format, $service->actions)) {
981  if ($this->ok) {
982  $this->reason = 'Required service(s) not offered - ';
983  $this->ok = false;
984  } else {
985  $this->reason .= ', ';
986  }
987  $this->reason .= "'{$format}' [" . implode(', ', $service->actions) . ']';
988  }
989  }
990  }
991  }
992  if ($this->ok) {
993  if ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest') {
994  $this->consumer->profile = $tcProfile;
995  $this->consumer->secret = $this->messageParameters['reg_password'];
996  $this->consumer->ltiVersion = $this->messageParameters['lti_version'];
997  $this->consumer->name = $tcProfile->product_instance->service_owner->service_owner_name->default_value;
998  $this->consumer->consumerName = $this->consumer->name;
999  $this->consumer->consumerVersion = "{$tcProfile->product_instance->product_info->product_family->code}-{$tcProfile->product_instance->product_info->product_version}";
1000  $this->consumer->consumerGuid = $tcProfile->product_instance->guid;
1001  $this->consumer->enabled = true;
1002  $this->consumer->protected = true;
1003  $doSaveConsumer = true;
1004  }
1005  }
1006  } elseif ($this->ok && !empty($this->messageParameters['custom_tc_profile_url']) && empty($this->consumer->profile)) {
1007  $url = $this->messageParameters['custom_tc_profile_url'];
1008  if (strpos($url, '?') === FALSE) {
1009  $url .= '?';
1010  } else {
1011  $url .= '&';
1012  }
1013  $url .= 'lti_version=' . $this->consumer->ltiVersion;
1014  $http = new HTTPMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json');
1015  if ($http->send()) {
1016  $tcProfile = json_decode($http->response);
1017  if (!is_null($tcProfile)) {
1018  $this->consumer->profile = $tcProfile;
1019  $doSaveConsumer = true;
1020  }
1021  }
1022  }
1023 
1024 // Validate message parameter constraints
1025  if ($this->ok) {
1026  $invalidParameters = array();
1027  foreach ($this->constraints as $name => $constraint) {
1028  if (empty($constraint['messages']) || in_array($this->messageParameters['lti_message_type'], $constraint['messages'])) {
1029  $ok = true;
1030  if ($constraint['required']) {
1031  if (!isset($this->messageParameters[$name]) || (strlen(trim($this->messageParameters[$name])) <= 0)) {
1032  $invalidParameters[] = "{$name} (missing)";
1033  $ok = false;
1034  }
1035  }
1036  if ($ok && !is_null($constraint['max_length']) && isset($this->messageParameters[$name])) {
1037  if (strlen(trim($this->messageParameters[$name])) > $constraint['max_length']) {
1038  $invalidParameters[] = "{$name} (too long)";
1039  }
1040  }
1041  }
1042  }
1043  if (count($invalidParameters) > 0) {
1044  $this->ok = false;
1045  if (empty($this->reason)) {
1046  $this->reason = 'Invalid parameter(s): ' . implode(', ', $invalidParameters) . '.';
1047  }
1048  }
1049  }
1050 
1051  if ($this->ok) {
1052 
1053 // Set the request context
1054  $contextId = '';
1055  if ($this->hasApiHook(self::$CONTEXT_ID_HOOK, $this->consumer->getFamilyCode())) {
1056  $className = $this->getApiHook(self::$CONTEXT_ID_HOOK, $this->consumer->getFamilyCode());
1057  $tpHook = new $className($this);
1058  $contextId = $tpHook->getContextId();
1059  }
1060  if (empty($contextId) && isset($this->messageParameters['context_id'])) {
1061  $contextId = trim($this->messageParameters['context_id']);
1062  }
1063  if (!empty($contextId)) {
1064  $this->context = Context::fromConsumer($this->consumer, $contextId);
1065  $title = '';
1066  if (isset($this->messageParameters['context_title'])) {
1067  $title = trim($this->messageParameters['context_title']);
1068  }
1069  if (empty($title)) {
1070  $title = "Course {$this->context->getId()}";
1071  }
1072  $this->context->title = $title;
1073  if (isset($this->messageParameters['context_type'])) {
1074  $this->context->type = trim($this->messageParameters['context_type']);
1075  }
1076  }
1077 
1078 // Set the request resource link
1079  if (isset($this->messageParameters['resource_link_id'])) {
1080  $contentItemId = '';
1081  if (isset($this->messageParameters['custom_content_item_id'])) {
1082  $contentItemId = $this->messageParameters['custom_content_item_id'];
1083  }
1084  $this->resourceLink = ResourceLink::fromConsumer($this->consumer,
1085  trim($this->messageParameters['resource_link_id']), $contentItemId);
1086  if (!empty($this->context)) {
1087  $this->resourceLink->setContextId($this->context->getRecordId());
1088  }
1089  $title = '';
1090  if (isset($this->messageParameters['resource_link_title'])) {
1091  $title = trim($this->messageParameters['resource_link_title']);
1092  }
1093  if (empty($title)) {
1094  $title = "Resource {$this->resourceLink->getId()}";
1095  }
1096  $this->resourceLink->title = $title;
1097 // Delete any existing custom parameters
1098  foreach ($this->consumer->getSettings() as $name => $value) {
1099  if (strpos($name, 'custom_') === 0) {
1100  $this->consumer->setSetting($name);
1101  $doSaveConsumer = true;
1102  }
1103  }
1104  if (!empty($this->context)) {
1105  foreach ($this->context->getSettings() as $name => $value) {
1106  if (strpos($name, 'custom_') === 0) {
1107  $this->context->setSetting($name);
1108  }
1109  }
1110  }
1111  foreach ($this->resourceLink->getSettings() as $name => $value) {
1112  if (strpos($name, 'custom_') === 0) {
1113  $this->resourceLink->setSetting($name);
1114  }
1115  }
1116 // Save LTI parameters
1117  foreach (self::$LTI_CONSUMER_SETTING_NAMES as $name) {
1118  if (isset($this->messageParameters[$name])) {
1119  $this->consumer->setSetting($name, $this->messageParameters[$name]);
1120  } else {
1121  $this->consumer->setSetting($name);
1122  }
1123  }
1124  if (!empty($this->context)) {
1125  foreach (self::$LTI_CONTEXT_SETTING_NAMES as $name) {
1126  if (isset($this->messageParameters[$name])) {
1127  $this->context->setSetting($name, $this->messageParameters[$name]);
1128  } else {
1129  $this->context->setSetting($name);
1130  }
1131  }
1132  }
1133  foreach (self::$LTI_RESOURCE_LINK_SETTING_NAMES as $name) {
1134  if (isset($this->messageParameters[$name])) {
1135  $this->resourceLink->setSetting($name, $this->messageParameters[$name]);
1136  } else {
1137  $this->resourceLink->setSetting($name);
1138  }
1139  }
1140 // Save other custom parameters at all levels
1141  foreach ($this->messageParameters as $name => $value) {
1142  if ((strpos($name, 'custom_') === 0) &&
1143  !in_array($name,
1144  array_merge(self::$LTI_CONSUMER_SETTING_NAMES, self::$LTI_CONTEXT_SETTING_NAMES,
1145  self::$LTI_RESOURCE_LINK_SETTING_NAMES))) {
1146  $this->consumer->setSetting($name, $value);
1147  if (!empty($this->context)) {
1148  $this->context->setSetting($name, $value);
1149  }
1150  $this->resourceLink->setSetting($name, $value);
1151  }
1152  }
1153  }
1154 
1155 // Set the user instance
1156  $userId = '';
1157  if ($this->hasApiHook(self::$USER_ID_HOOK, $this->consumer->getFamilyCode())) {
1158  $className = $this->getApiHook(self::$USER_ID_HOOK, $this->consumer->getFamilyCode());
1159  $tpHook = new $className($this);
1160  $userId = $tpHook->getUserId();
1161  }
1162  if (empty($userId) && isset($this->messageParameters['user_id'])) {
1163  $userId = trim($this->messageParameters['user_id']);
1164  }
1165 
1166  $this->userResult = UserResult::fromResourceLink($this->resourceLink, $userId);
1167 
1168 // Set the user name
1169  $firstname = (isset($this->messageParameters['lis_person_name_given'])) ? $this->messageParameters['lis_person_name_given'] : '';
1170  $lastname = (isset($this->messageParameters['lis_person_name_family'])) ? $this->messageParameters['lis_person_name_family'] : '';
1171  $fullname = (isset($this->messageParameters['lis_person_name_full'])) ? $this->messageParameters['lis_person_name_full'] : '';
1172  $this->userResult->setNames($firstname, $lastname, $fullname);
1173 
1174 // Set the user email
1175  $email = (isset($this->messageParameters['lis_person_contact_email_primary'])) ? $this->messageParameters['lis_person_contact_email_primary'] : '';
1176  $this->userResult->setEmail($email, $this->defaultEmail);
1177 
1178 // Set the user image URI
1179  if (isset($this->messageParameters['user_image'])) {
1180  $this->userResult->image = $this->messageParameters['user_image'];
1181  }
1182 
1183 // Set the user roles
1184  if (isset($this->messageParameters['roles'])) {
1185  $this->userResult->roles = self::parseRoles($this->messageParameters['roles'], $this->consumer->ltiVersion);
1186  }
1187 
1188 // Initialise the consumer and check for changes
1189  $this->consumer->defaultEmail = $this->defaultEmail;
1190  if ($this->consumer->ltiVersion !== $this->messageParameters['lti_version']) {
1191  $this->consumer->ltiVersion = $this->messageParameters['lti_version'];
1192  $doSaveConsumer = true;
1193  }
1194  if (isset($this->messageParameters['tool_consumer_instance_name'])) {
1195  if ($this->consumer->consumerName !== $this->messageParameters['tool_consumer_instance_name']) {
1196  $this->consumer->consumerName = $this->messageParameters['tool_consumer_instance_name'];
1197  $doSaveConsumer = true;
1198  }
1199  }
1200  if (isset($this->messageParameters['tool_consumer_info_product_family_code'])) {
1201  $version = $this->messageParameters['tool_consumer_info_product_family_code'];
1202  if (isset($this->messageParameters['tool_consumer_info_version'])) {
1203  $version .= "-{$this->messageParameters['tool_consumer_info_version']
1204  }";
1205  }
1206 // do not delete any existing consumer version if none is passed
1207  if ($this->consumer->consumerVersion !== $version) {
1208  $this->consumer->consumerVersion = $version;
1209  $doSaveConsumer = true;
1210  }
1211  } elseif (isset($this->messageParameters['ext_lms']) && ($this->consumer->consumerName !== $this->messageParameters['ext_lms'])) {
1212  $this->consumer->consumerVersion = $this->messageParameters['ext_lms'];
1213  $doSaveConsumer = true;
1214  }
1215  if (isset($this->messageParameters['tool_consumer_instance_guid'])) {
1216  if (is_null($this->consumer->consumerGuid)) {
1217  $this->consumer->consumerGuid = $this->messageParameters['tool_consumer_instance_guid'];
1218  $doSaveConsumer = true;
1219  } elseif (!$this->consumer->protected) {
1220  $doSaveConsumer = ($this->consumer->consumerGuid !== $this->messageParameters['tool_consumer_instance_guid']);
1221  if ($doSaveConsumer) {
1222  $this->consumer->consumerGuid = $this->messageParameters['tool_consumer_instance_guid'];
1223  }
1224  }
1225  }
1226  if (isset($this->messageParameters['launch_presentation_css_url'])) {
1227  if ($this->consumer->cssPath !== $this->messageParameters['launch_presentation_css_url']) {
1228  $this->consumer->cssPath = $this->messageParameters['launch_presentation_css_url'];
1229  $doSaveConsumer = true;
1230  }
1231  } elseif (isset($this->messageParameters['ext_launch_presentation_css_url']) &&
1232  ($this->consumer->cssPath !== $this->messageParameters['ext_launch_presentation_css_url'])) {
1233  $this->consumer->cssPath = $this->messageParameters['ext_launch_presentation_css_url'];
1234  $doSaveConsumer = true;
1235  } elseif (!empty($this->consumer->cssPath)) {
1236  $this->consumer->cssPath = null;
1237  $doSaveConsumer = true;
1238  }
1239  }
1240 
1241 // Persist changes to consumer
1242  if ($doSaveConsumer) {
1243  $this->consumer->save();
1244  }
1245  if ($this->ok && isset($this->context)) {
1246  $this->context->save();
1247  }
1248  if ($this->ok && isset($this->resourceLink)) {
1249 
1250 // Check if a share arrangement is in place for this resource link
1251  $this->ok = $this->checkForShare();
1252 
1253 // Persist changes to resource link
1254  $this->resourceLink->save();
1255 
1256 // Save the user instance
1257  $this->userResult->setResourceLinkId($this->resourceLink->getRecordId());
1258  if (isset($this->messageParameters['lis_result_sourcedid'])) {
1259  if ($this->userResult->ltiResultSourcedId !== $this->messageParameters['lis_result_sourcedid']) {
1260  $this->userResult->ltiResultSourcedId = $this->messageParameters['lis_result_sourcedid'];
1261  $this->userResult->save();
1262  }
1263  } elseif (!empty($this->userResult->ltiResultSourcedId)) {
1264  $this->userResult->ltiResultSourcedId = '';
1265  $this->userResult->save();
1266  }
1267  }
1268 
1269  return $this->ok;
1270  }
1271 
1277  private function checkForShare()
1278  {
1279  $ok = true;
1280  $doSaveResourceLink = true;
1281 
1282  $id = $this->resourceLink->primaryResourceLinkId;
1283 
1284  $shareRequest = isset($this->messageParameters['custom_share_key']) && !empty($this->messageParameters['custom_share_key']);
1285  if ($shareRequest) {
1286  if (!$this->allowSharing) {
1287  $ok = false;
1288  $this->reason = 'Your sharing request has been refused because sharing is not being permitted.';
1289  } else {
1290 // Check if this is a new share key
1291  $shareKey = new ResourceLinkShareKey($this->resourceLink, $this->messageParameters['custom_share_key']);
1292  if (!is_null($shareKey->primaryConsumerKey) && !is_null($shareKey->primaryResourceLinkId)) {
1293 // Update resource link with sharing primary resource link details
1294  $key = $shareKey->primaryConsumerKey;
1295  $id = $shareKey->primaryResourceLinkId;
1296  $ok = ($key !== $this->consumer->getKey()) || ($id != $this->resourceLink->getId());
1297  if ($ok) {
1298  $this->resourceLink->primaryConsumerKey = $key;
1299  $this->resourceLink->primaryResourceLinkId = $id;
1300  $this->resourceLink->shareApproved = $shareKey->autoApprove;
1301  $ok = $this->resourceLink->save();
1302  if ($ok) {
1303  $doSaveResourceLink = false;
1304  $this->userResult->getResourceLink()->primaryConsumerKey = $key;
1305  $this->userResult->getResourceLink()->primaryResourceLinkId = $id;
1306  $this->userResult->getResourceLink()->shareApproved = $shareKey->autoApprove;
1307  $this->userResult->getResourceLink()->updated = time();
1308 // Remove share key
1309  $shareKey->delete();
1310  } else {
1311  $this->reason = 'An error occurred initialising your share arrangement.';
1312  }
1313  } else {
1314  $this->reason = 'It is not possible to share your resource link with yourself.';
1315  }
1316  }
1317  if ($ok) {
1318  $ok = !is_null($key);
1319  if (!$ok) {
1320  $this->reason = 'You have requested to share a resource link but none is available.';
1321  } else {
1322  $ok = (!is_null($this->userResult->getResourceLink()->shareApproved) && $this->userResult->getResourceLink()->shareApproved);
1323  if (!$ok) {
1324  $this->reason = 'Your share request is waiting to be approved.';
1325  }
1326  }
1327  }
1328  }
1329  } else {
1330 // Check no share is in place
1331  $ok = is_null($id);
1332  if (!$ok) {
1333  $this->reason = 'You have not requested to share a resource link but an arrangement is currently in place.';
1334  }
1335  }
1336 
1337 // Look up primary resource link
1338  if ($ok && !is_null($id)) {
1339  $consumer = new ToolConsumer($key, $this->dataConnector);
1340  $ok = !is_null($consumer->created);
1341  if ($ok) {
1343  $ok = !is_null($resourceLink->created);
1344  }
1345  if ($ok) {
1346  if ($doSaveResourceLink) {
1347  $this->resourceLink->save();
1348  }
1349  $this->resourceLink = $resourceLink;
1350  } else {
1351  $this->reason = 'Unable to load resource link being shared.';
1352  }
1353  }
1354 
1355  return $ok;
1356  }
1357 
1367  private function checkValue($value, $values, $reason)
1368  {
1369  $ok = in_array($value, $values);
1370  if (!$ok && !empty($reason)) {
1371  $this->reason = sprintf($reason, $value);
1372  }
1373 
1374  return $ok;
1375  }
1376 
1377 }
onRegister()
Process a valid tool proxy registration request.
Class to represent an LTI Tool Proxy media type.
Definition: ToolProxy.php:16
$ok
True if the last request was successful.
$vendor
Vendor details.
$allowSharing
Whether shared resource link arrangements are permitted.
static fromResourceLink($resourceLink, $ltiUserId)
Class constructor from resource link.
Definition: UserResult.php:255
$optionalServices
Optional services used by Tool Provider.
findService($format, $methods)
Find an offered service based on a media type and HTTP action(s)
setParameterConstraint($name, $required=true, $maxLength=null, $messageTypes=null)
Add a parameter constraint to be checked on launch.
$redirectUrl
URL to redirect user to on successful completion of the request.
getMessageParameters()
Get the message parameters.
onLaunch()
Process a valid launch request.
Class to represent an OAuth Consumer.
$message
Message for last request processed.
$output
Default HTML to be displayed on a successful completion of the request.
const ID_SCOPE_ID_ONLY
Use ID value only.
$defaultEmail
Default email domain.
Class to represent a tool consumer.
onError()
Process a response to an invalid request.
const LTI_VERSION2
LTI version 2 for messages.
Class to represent a generic item object.
Definition: Item.php:13
$dataConnector
Data connector object.
$product
Product details.
const ID_SCOPE_SEPARATOR
Character used to separate each element of an ID.
Class to represent an OAuth HMAC_SHA256 signature method.
$errorOutput
HTML to be displayed on an unsuccessful completion of the request and no return URL is available.
$mediaTypes
Media types accepted by the Tool Consumer.
$messageParameters
LTI message parameters.
$documentTargets
Document targets accepted by the Tool Consumer.
onContentItem()
Process a valid content-item request.
Class to represent an OAuth HMAC_SHA384 signature method.
const ID_SCOPE_CONTEXT
Prefix the ID with the consumer key and context ID.
const ID_SCOPE_GLOBAL
Prefix an ID with the consumer key.
$resourceHandlers
Resource handlers for Tool Provider.
Class to represent an OAuth HMAC_SHA224 signature method.
Class to represent an OAuth HMAC_SHA1 signature method.
getConsumers()
Get an array of defined tool consumers.
$debugMode
Whether debug messages explaining the cause of errors are to be returned to the tool consumer.
Class to represent an HTTP message request.
Definition: HTTPMessage.php:16
$requiredServices
Services required by Tool Provider.
__construct($dataConnector)
Class constructor.
$baseUrl
Base URL for tool provider service.
const ID_SCOPE_RESOURCE
Prefix the ID with the consumer key and resource ID.
handleRequest()
Process an incoming request.
onDashboard()
Process a valid dashboard request.
$idScope
Scope to use for user IDs.
static from_request($http_method=null, $http_url=null, $parameters=null)
attempt to build up a request from what was passed to the server
$reason
Error message for last request processed.
$consumer
Tool Consumer object.
Trait to handle API hook registrations.
Definition: ApiHook.php:13
Class to represent a tool consumer resource link share key.
static parse_parameters($input)
Definition: OAuthUtil.php:97
Class to represent an OAuth Server.
Definition: OAuthServer.php:12
static sendForm($url, $params, $target='')
Generate a web page containing an auto-submitted form of parameters.
Class to represent an OAuth Data Store.
$resourceLink
Resource link object.
$context
Context object.
$returnUrl
Return URL provided by tool consumer.
static getRandomString($length=8)
Generate a random string.
const LTI_VERSION1
LTI version 1 for messages.
$details
Details for error message relating to last request processed.
onConfigure()
Process a valid configure request.
static fromConsumer($consumer, $ltiContextId)
Class constructor from consumer.
Definition: Context.php:484
$userResult
UserResult object.
static parseRoles($roles, $ltiVersion=self::LTI_VERSION1)
Get an array of fully qualified user roles.
Class to represent an LTI Tool Provider.
Class to represent an OAuth HMAC_SHA512 signature method.
doToolProxyService()
Send the tool proxy to the Tool Consumer.
const CONNECTION_ERROR_MESSAGE
Default connection error message.