1<?php 2 3namespace ceLTIc\LTI; 4 5use ceLTIc\LTI\DataConnector; 6use ceLTIc\LTI\MediaType; 7use ceLTIc\LTI\Profile; 8use ceLTIc\LTI\Http\HttpMessage; 9use ceLTIc\LTI\OAuth; 10use ceLTIc\LTI\ApiHook\ApiHook; 11 12/** 13 * Class to represent an LTI Tool Provider 14 * 15 * @author Stephen P Vickers <stephen@spvsoftwareproducts.com> 16 * @copyright SPV Software Products 17 * @license http://www.gnu.org/licenses/lgpl.html GNU Lesser General Public License, version 3 18 */ 19class ToolProvider 20{ 21 use ApiHook; 22 23 /** 24 * Default connection error message. 25 */ 26 const CONNECTION_ERROR_MESSAGE = 'Sorry, there was an error connecting you to the application.'; 27 28 /** 29 * LTI version 1 for messages. 30 */ 31 const LTI_VERSION1 = 'LTI-1p0'; 32 33 /** 34 * LTI version 2 for messages. 35 */ 36 const LTI_VERSION2 = 'LTI-2p0'; 37 38 /** 39 * Use ID value only. 40 */ 41 const ID_SCOPE_ID_ONLY = 0; 42 43 /** 44 * Prefix an ID with the consumer key. 45 */ 46 const ID_SCOPE_GLOBAL = 1; 47 48 /** 49 * Prefix the ID with the consumer key and context ID. 50 */ 51 const ID_SCOPE_CONTEXT = 2; 52 53 /** 54 * Prefix the ID with the consumer key and resource ID. 55 */ 56 const ID_SCOPE_RESOURCE = 3; 57 58 /** 59 * Character used to separate each element of an ID. 60 */ 61 const ID_SCOPE_SEPARATOR = ':'; 62 63 /** 64 * Permitted LTI versions for messages. 65 */ 66 private static $LTI_VERSIONS = array(self::LTI_VERSION1, self::LTI_VERSION2); 67 68 /** 69 * List of supported message types and associated class methods. 70 */ 71 private static $METHOD_NAMES = array('basic-lti-launch-request' => 'onLaunch', 72 'ConfigureLaunchRequest' => 'onConfigure', 73 'DashboardRequest' => 'onDashboard', 74 'ContentItemSelectionRequest' => 'onContentItem', 75 'ToolProxyRegistrationRequest' => 'onRegister' 76 ); 77 78 /** 79 * Names of LTI parameters to be retained in the consumer settings property. 80 */ 81 private static $LTI_CONSUMER_SETTING_NAMES = array('custom_tc_profile_url', 'custom_system_setting_url', 'custom_oauth2_access_token_url'); 82 83 /** 84 * Names of LTI parameters to be retained in the context settings property. 85 */ 86 private static $LTI_CONTEXT_SETTING_NAMES = array('custom_context_setting_url', 87 'custom_context_memberships_url', 'custom_context_memberships_v2_url', 'custom_lineitems_url'); 88 89 /** 90 * Names of LTI parameters to be retained in the resource link settings property. 91 */ 92 private static $LTI_RESOURCE_LINK_SETTING_NAMES = array('lis_result_sourcedid', 'lis_outcome_service_url', 93 'ext_ims_lis_basic_outcome_url', 'ext_ims_lis_resultvalue_sourcedids', 94 'ext_ims_lis_memberships_id', 'ext_ims_lis_memberships_url', 95 'ext_ims_lti_tool_setting', 'ext_ims_lti_tool_setting_id', 'ext_ims_lti_tool_setting_url', 96 'custom_link_setting_url', 'custom_link_memberships_url', 97 'custom_lineitems_url', 'custom_lineitem_url'); 98 99 /** 100 * Names of LTI parameters to be retained even when not passed. 101 */ 102 private static $LTI_RETAIN_SETTING_NAMES = array('custom_lineitem_url'); 103 104 /** 105 * Names of LTI custom parameter substitution variables (or capabilities) and their associated default message parameter names. 106 */ 107 private static $CUSTOM_SUBSTITUTION_VARIABLES = array('User.id' => 'user_id', 108 'User.image' => 'user_image', 109 'User.username' => 'username', 110 'User.scope.mentor' => 'role_scope_mentor', 111 'Membership.role' => 'roles', 112 'Person.sourcedId' => 'lis_person_sourcedid', 113 'Person.name.full' => 'lis_person_name_full', 114 'Person.name.family' => 'lis_person_name_family', 115 'Person.name.given' => 'lis_person_name_given', 116 'Person.email.primary' => 'lis_person_contact_email_primary', 117 'Context.id' => 'context_id', 118 'Context.type' => 'context_type', 119 'Context.title' => 'context_title', 120 'Context.label' => 'context_label', 121 'CourseOffering.sourcedId' => 'lis_course_offering_sourcedid', 122 'CourseSection.sourcedId' => 'lis_course_section_sourcedid', 123 'CourseSection.label' => 'context_label', 124 'CourseSection.title' => 'context_title', 125 'ResourceLink.id' => 'resource_link_id', 126 'ResourceLink.title' => 'resource_link_title', 127 'ResourceLink.description' => 'resource_link_description', 128 'Result.sourcedId' => 'lis_result_sourcedid', 129 'BasicOutcome.url' => 'lis_outcome_service_url', 130 'ToolConsumerProfile.url' => 'custom_tc_profile_url', 131 'ToolProxy.url' => 'tool_proxy_url', 132 'ToolProxy.custom.url' => 'custom_system_setting_url', 133 'ToolProxyBinding.custom.url' => 'custom_context_setting_url', 134 'LtiLink.custom.url' => 'custom_link_setting_url', 135 'LineItems.url' => 'custom_lineitems_url', 136 'LineItem.url' => 'custom_lineitem_url', 137 'ToolProxyBinding.memberships.url' => 'custom_context_memberships_url', 138 'LtiLink.memberships.url' => 'custom_link_memberships_url'); 139 140 /** 141 * True if the last request was successful. 142 * 143 * @var bool $ok 144 */ 145 public $ok = true; 146 147 /** 148 * Tool Consumer object. 149 * 150 * @var ToolConsumer|null $consumer 151 */ 152 public $consumer = null; 153 154 /** 155 * Return URL provided by tool consumer. 156 * 157 * @var string|null $returnUrl 158 */ 159 public $returnUrl = null; 160 161 /** 162 * UserResult object. 163 * 164 * @var UserResult|null $userResult 165 */ 166 public $userResult = null; 167 168 /** 169 * Resource link object. 170 * 171 * @var ResourceLink|null $resourceLink 172 */ 173 public $resourceLink = null; 174 175 /** 176 * Context object. 177 * 178 * @var Context|null $context 179 */ 180 public $context = null; 181 182 /** 183 * Data connector object. 184 * 185 * @var DataConnector|null $dataConnector 186 */ 187 public $dataConnector = null; 188 189 /** 190 * Default email domain. 191 * 192 * @var string $defaultEmail 193 */ 194 public $defaultEmail = ''; 195 196 /** 197 * Scope to use for user IDs. 198 * 199 * @var int $idScope 200 */ 201 public $idScope = self::ID_SCOPE_ID_ONLY; 202 203 /** 204 * Whether shared resource link arrangements are permitted. 205 * 206 * @var bool $allowSharing 207 */ 208 public $allowSharing = false; 209 210 /** 211 * Message for last request processed 212 * 213 * @var string $message 214 */ 215 public $message = self::CONNECTION_ERROR_MESSAGE; 216 217 /** 218 * Error message for last request processed. 219 * 220 * @var string|null $reason 221 */ 222 public $reason = null; 223 224 /** 225 * Details for error message relating to last request processed. 226 * 227 * @var array $details 228 */ 229 public $details = array(); 230 231 /** 232 * Base URL for tool provider service 233 * 234 * @var string|null $baseUrl 235 */ 236 public $baseUrl = null; 237 238 /** 239 * Vendor details 240 * 241 * @var Item|null $vendor 242 */ 243 public $vendor = null; 244 245 /** 246 * Product details 247 * 248 * @var Item|null $product 249 */ 250 public $product = null; 251 252 /** 253 * Services required by Tool Provider 254 * 255 * @var array|null $requiredServices 256 */ 257 public $requiredServices = null; 258 259 /** 260 * Optional services used by Tool Provider 261 * 262 * @var array|null $optionalServices 263 */ 264 public $optionalServices = null; 265 266 /** 267 * Resource handlers for Tool Provider 268 * 269 * @var array|null $resourceHandlers 270 */ 271 public $resourceHandlers = null; 272 273 /** 274 * URL to redirect user to on successful completion of the request. 275 * 276 * @var string|null $redirectUrl 277 */ 278 protected $redirectUrl = null; 279 280 /** 281 * Media types accepted by the Tool Consumer. 282 * 283 * @var array|null $mediaTypes 284 */ 285 protected $mediaTypes = null; 286 287 /** 288 * Document targets accepted by the Tool Consumer. 289 * 290 * @var array|null $documentTargets 291 */ 292 protected $documentTargets = null; 293 294 /** 295 * Default HTML to be displayed on a successful completion of the request. 296 * 297 * @var string|null $output 298 */ 299 protected $output = null; 300 301 /** 302 * HTML to be displayed on an unsuccessful completion of the request and no return URL is available. 303 * 304 * @var string|null $errorOutput 305 */ 306 protected $errorOutput = null; 307 308 /** 309 * Whether debug messages explaining the cause of errors are to be returned to the tool consumer. 310 * 311 * @var bool $debugMode 312 */ 313 protected $debugMode = false; 314 315 /** 316 * LTI message parameters. 317 * 318 * @var array|null $messageParameters 319 */ 320 protected $messageParameters = null; 321 322 /** 323 * LTI parameter constraints for auto validation checks. 324 * 325 * @var array|null $constraints 326 */ 327 private $constraints = null; 328 329 /** 330 * Class constructor 331 * 332 * @param DataConnector $dataConnector Object containing a database connection object 333 */ 334 function __construct($dataConnector) 335 { 336 $this->constraints = array(); 337 $this->dataConnector = $dataConnector; 338 $this->ok = !is_null($this->dataConnector); 339 $this->vendor = new Profile\Item(); 340 $this->product = new Profile\Item(); 341 $this->requiredServices = array(); 342 $this->optionalServices = array(); 343 $this->resourceHandlers = array(); 344 } 345 346 /** 347 * Process an incoming request 348 */ 349 public function handleRequest() 350 { 351 if ($this->debugMode) { 352 Util::$logLevel = Util::LOGLEVEL_DEBUG; 353 } 354 Util::logRequest(); 355 if ($this->ok) { 356 $this->getMessageParameters(); 357 if ($this->authenticate()) { 358 if (empty($this->output)) { 359 $this->doCallback(); 360 } 361 } 362 if (!$this->ok) { 363 Util::logError("Request failed with reason: '{$this->reason}'"); 364 } 365 } 366 $this->result(); 367 } 368 369 /** 370 * Add a parameter constraint to be checked on launch 371 * 372 * @param string $name Name of parameter to be checked 373 * @param bool $required True if parameter is required (optional, default is true) 374 * @param int $maxLength Maximum permitted length of parameter value (optional, default is null) 375 * @param array $messageTypes Array of message types to which the constraint applies (optional, default is all) 376 */ 377 public function setParameterConstraint($name, $required = true, $maxLength = null, $messageTypes = null) 378 { 379 $name = trim($name); 380 if (strlen($name) > 0) { 381 $this->constraints[$name] = array('required' => $required, 'max_length' => $maxLength, 'messages' => $messageTypes); 382 } 383 } 384 385 /** 386 * Get an array of defined tool consumers 387 * 388 * @return array Array of ToolConsumer objects 389 */ 390 public function getConsumers() 391 { 392 return $this->dataConnector->getToolConsumers(); 393 } 394 395 /** 396 * Find an offered service based on a media type and HTTP action(s) 397 * 398 * @param string $format Media type required 399 * @param array $methods Array of HTTP actions required 400 * 401 * @return object The service object 402 */ 403 public function findService($format, $methods) 404 { 405 $found = false; 406 $services = $this->consumer->profile->service_offered; 407 if (is_array($services)) { 408 $n = -1; 409 foreach ($services as $service) { 410 $n++; 411 if (!is_array($service->format) || !in_array($format, $service->format)) { 412 continue; 413 } 414 $missing = array(); 415 foreach ($methods as $method) { 416 if (!is_array($service->action) || !in_array($method, $service->action)) { 417 $missing[] = $method; 418 } 419 } 420 $methods = $missing; 421 if (count($methods) <= 0) { 422 $found = $service; 423 break; 424 } 425 } 426 } 427 428 return $found; 429 } 430 431 /** 432 * Send the tool proxy to the Tool Consumer 433 * 434 * @return bool True if the tool proxy was accepted 435 */ 436 public function doToolProxyService() 437 { 438// Create tool proxy 439 $toolProxyService = $this->findService('application/vnd.ims.lti.v2.toolproxy+json', array('POST')); 440 $secret = DataConnector\DataConnector::getRandomString(12); 441 $toolProxy = new MediaType\ToolProxy($this, $toolProxyService, $secret); 442 $http = $this->consumer->doServiceRequest($toolProxyService, 'POST', 'application/vnd.ims.lti.v2.toolproxy+json', 443 json_encode($toolProxy)); 444 $ok = $http->ok && ($http->status == 201) && isset($http->responseJson->tool_proxy_guid) && (strlen($http->responseJson->tool_proxy_guid) > 0); 445 if ($ok) { 446 $this->consumer->setKey($http->responseJson->tool_proxy_guid); 447 $this->consumer->secret = $toolProxy->security_contract->shared_secret; 448 $this->consumer->toolProxy = json_encode($toolProxy); 449 $this->consumer->save(); 450 } 451 452 return $ok; 453 } 454 455 /** 456 * Get the message parameters 457 * 458 * @return array The message parameter array 459 */ 460 public function getMessageParameters() 461 { 462 if ($this->ok && is_null($this->messageParameters)) { 463 $this->messageParameters = OAuth\OAuthUtil::parse_parameters(file_get_contents(OAuth\OAuthRequest::$POST_INPUT)); 464 if (!empty($this->messageParameters['oauth_consumer_key'])) { 465 $this->consumer = new ToolConsumer($this->messageParameters['oauth_consumer_key'], $this->dataConnector); 466 } 467// Set debug mode 468 if (!$this->debugMode) { 469 $this->debugMode = isset($this->messageParameters['custom_debug']) && (strtolower($this->messageParameters['custom_debug']) === 'true'); 470 if ($this->debugMode) { 471 $currentLogLevel = Util::$logLevel; 472 Util::$logLevel = Util::LOGLEVEL_DEBUG; 473 if ($currentLogLevel < Util::LOGLEVEL_INFO) { 474 Util::logRequest(); 475 } 476 } 477 } 478// Set return URL if available 479 if (isset($this->messageParameters['launch_presentation_return_url'])) { 480 $this->returnUrl = $this->messageParameters['launch_presentation_return_url']; 481 } elseif (isset($this->messageParameters['content_item_return_url'])) { 482 $this->returnUrl = $this->messageParameters['content_item_return_url']; 483 } 484 } 485 486 return $this->messageParameters; 487 } 488 489 /** 490 * Get an array of fully qualified user roles 491 * 492 * @param mixed $roles Comma-separated list of roles or array of roles 493 * @param string $ltiVersion LTI version (default is LTI-1p0) 494 * 495 * @return array Array of roles 496 */ 497 public static function parseRoles($roles, $ltiVersion = self::LTI_VERSION1) 498 { 499 if (!is_array($roles)) { 500 $roles = explode(',', $roles); 501 } 502 $parsedRoles = array(); 503 foreach ($roles as $role) { 504 $role = trim($role); 505 if (!empty($role)) { 506 if ($ltiVersion === self::LTI_VERSION1) { 507 if (substr($role, 0, 4) !== 'urn:') { 508 $role = 'urn:lti:role:ims/lis/' . $role; 509 } 510 } elseif ((substr($role, 0, 7) !== 'http://') && (substr($role, 0, 8) !== 'https://')) { 511 $role = 'http://purl.imsglobal.org/vocab/lis/v2/membership#' . $role; 512 } 513 $parsedRoles[] = $role; 514 } 515 } 516 517 return $parsedRoles; 518 } 519 520 /** 521 * Generate a web page containing an auto-submitted form of parameters. 522 * 523 * @param string $url URL to which the form should be submitted 524 * @param array $params Array of form parameters 525 * @param string $target Name of target (optional) 526 * 527 * @return string 528 */ 529 public static function sendForm($url, $params, $target = '') 530 { 531 $page = <<< EOD 532<html> 533<head> 534<title>IMS LTI message</title> 535<script type="text/javascript"> 536//<![CDATA[ 537function doOnLoad() { 538 document.forms[0].submit(); 539} 540 541window.onload=doOnLoad; 542//]]> 543</script> 544</head> 545<body> 546<form action="{$url}" method="post" target="" encType="application/x-www-form-urlencoded"> 547 548EOD; 549 foreach ($params as $key => $value) { 550 $key = htmlentities($key, ENT_COMPAT | ENT_HTML401, 'UTF-8'); 551 if (!is_array($value)) { 552 $value = htmlentities($value, ENT_COMPAT | ENT_HTML401, 'UTF-8'); 553 $page .= <<< EOD 554 <input type="hidden" name="{$key}" value="{$value}" /> 555 556EOD; 557 } else { 558 foreach ($value as $element) { 559 $element = htmlentities($element, ENT_COMPAT | ENT_HTML401, 'UTF-8'); 560 $page .= <<< EOD 561 <input type="hidden" name="{$key}" value="{$element}" /> 562 563EOD; 564 } 565 } 566 } 567 568 $page .= <<< EOD 569</form> 570</body> 571</html> 572EOD; 573 574 return $page; 575 } 576 577### 578### PROTECTED METHODS 579### 580 581 /** 582 * Process a valid launch request 583 * 584 * @return bool True if no error 585 */ 586 protected function onLaunch() 587 { 588 $this->onError(); 589 } 590 591 /** 592 * Process a valid configure request 593 * 594 * @return bool True if no error 595 */ 596 protected function onConfigure() 597 { 598 $this->onError(); 599 } 600 601 /** 602 * Process a valid dashboard request 603 * 604 * @return bool True if no error 605 */ 606 protected function onDashboard() 607 { 608 $this->onError(); 609 } 610 611 /** 612 * Process a valid content-item request 613 * 614 * @return bool True if no error 615 */ 616 protected function onContentItem() 617 { 618 $this->onError(); 619 } 620 621 /** 622 * Process a valid tool proxy registration request 623 * 624 * @return bool True if no error 625 */ 626 protected function onRegister() 627 { 628 $this->onError(); 629 } 630 631 /** 632 * Process a response to an invalid request 633 * 634 * @return bool True if no further error processing required 635 */ 636 protected function onError() 637 { 638 $this->ok = false; 639 } 640 641### 642### PRIVATE METHODS 643### 644 645 /** 646 * Call any callback function for the requested action. 647 * 648 * This function may set the redirect_url and output properties. 649 * 650 * @param string|null $method Name of method to be called (optional) 651 */ 652 private function doCallback($method = null) 653 { 654 $callback = $method; 655 if (is_null($callback)) { 656 $callback = self::$METHOD_NAMES[$this->messageParameters['lti_message_type']]; 657 } 658 if (method_exists($this, $callback)) { 659 $this->$callback(); 660 } elseif (is_null($method) && $this->ok) { 661 $this->ok = false; 662 $this->reason = "Message type not supported: {$this->messageParameters['lti_message_type']}"; 663 } 664 if ($this->ok && ($this->messageParameters['lti_message_type'] == 'ToolProxyRegistrationRequest')) { 665 $this->consumer->save(); 666 } 667 } 668 669 /** 670 * Perform the result of an action. 671 * 672 * This function may redirect the user to another URL rather than returning a value. 673 * 674 * @return string Output to be displayed (redirection, or display HTML or message) 675 */ 676 private function result() 677 { 678 if (!$this->ok) { 679 $this->onError(); 680 } 681 if (!$this->ok) { 682 683// If not valid, return an error message to the tool consumer if a return URL is provided 684 if (!empty($this->returnUrl)) { 685 $errorUrl = $this->returnUrl; 686 if (strpos($errorUrl, '?') === false) { 687 $errorUrl .= '?'; 688 } else { 689 $errorUrl .= '&'; 690 } 691 if ($this->debugMode && !is_null($this->reason)) { 692 $errorUrl .= 'lti_errormsg=' . urlencode("Debug error: $this->reason"); 693 } else { 694 $errorUrl .= 'lti_errormsg=' . urlencode($this->message); 695 if (!is_null($this->reason)) { 696 $errorUrl .= '<i_errorlog=' . urlencode("Debug error: $this->reason"); 697 } 698 } 699 if (!is_null($this->consumer) && isset($this->messageParameters['lti_message_type']) && (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') || 700 ($this->messageParameters['lti_message_type'] === 'LtiDeepLinkingRequest'))) { 701 $formParams = array(); 702 if (isset($this->messageParameters['data'])) { 703 $formParams['data'] = $this->messageParameters['data']; 704 } 705 $version = (isset($this->messageParameters['lti_version'])) ? $this->messageParameters['lti_version'] : self::LTI_VERSION1; 706 $formParams = $this->consumer->signParameters($errorUrl, 'ContentItemSelection', $version, $formParams); 707 $page = self::sendForm($errorUrl, $formParams); 708 echo $page; 709 } else { 710 header("Location: {$errorUrl}"); 711 } 712 exit; 713 } else { 714 if (!is_null($this->errorOutput)) { 715 echo $this->errorOutput; 716 } elseif ($this->debugMode && !empty($this->reason)) { 717 echo "Debug error: {$this->reason}"; 718 } else { 719 echo "Error: {$this->message}"; 720 } 721 } 722 } elseif (!is_null($this->redirectUrl)) { 723 header("Location: {$this->redirectUrl}"); 724 exit; 725 } elseif (!is_null($this->output)) { 726 echo $this->output; 727 exit; 728 } 729 } 730 731 /** 732 * Check the authenticity of the LTI launch request. 733 * 734 * The consumer, resource link and user objects will be initialised if the request is valid. 735 * 736 * @return bool True if the request has been successfully validated. 737 */ 738 private function authenticate() 739 { 740// Get the consumer 741 $doSaveConsumer = false; 742 $this->ok = $_SERVER['REQUEST_METHOD'] === 'POST'; 743 if (!$this->ok) { 744 $this->reason = 'Message should be an HTTP POST request'; 745 } 746// Check all required launch parameters 747 if ($this->ok) { 748 $this->ok = isset($this->messageParameters['lti_message_type']) && array_key_exists($this->messageParameters['lti_message_type'], 749 self::$METHOD_NAMES); 750 if (!$this->ok) { 751 $this->reason = 'Invalid or missing lti_message_type parameter.'; 752 } 753 } 754 if ($this->ok) { 755 $this->ok = isset($this->messageParameters['lti_version']) && in_array($this->messageParameters['lti_version'], 756 self::$LTI_VERSIONS); 757 if (!$this->ok) { 758 $this->reason = 'Invalid or missing lti_version parameter.'; 759 } 760 } 761 if ($this->ok) { 762 if (($this->messageParameters['lti_message_type'] === 'basic-lti-launch-request') || ($this->messageParameters['lti_message_type'] === 'LtiResourceLinkRequest') || ($this->messageParameters['lti_message_type'] === 'DashboardRequest')) { 763 $this->ok = isset($this->messageParameters['resource_link_id']) && (strlen(trim($this->messageParameters['resource_link_id'])) > 0); 764 if (!$this->ok) { 765 $this->reason = 'Missing resource link ID.'; 766 } 767 } elseif (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') || ($this->messageParameters['lti_message_type'] === 'LtiDeepLinkingRequest')) { 768 if (isset($this->messageParameters['accept_media_types']) && (strlen(trim($this->messageParameters['accept_media_types'])) > 0)) { 769 $mediaTypes = array_filter(explode(',', str_replace(' ', '', $this->messageParameters['accept_media_types'])), 770 'strlen'); 771 $mediaTypes = array_unique($mediaTypes); 772 $this->ok = count($mediaTypes) > 0; 773 if (!$this->ok) { 774 $this->reason = 'No accept_media_types found.'; 775 } else { 776 $this->mediaTypes = $mediaTypes; 777 } 778 } else { 779 $this->ok = false; 780 } 781 if ($this->ok && isset($this->messageParameters['accept_presentation_document_targets']) && (strlen(trim($this->messageParameters['accept_presentation_document_targets'])) > 0)) { 782 $documentTargets = array_filter(explode(',', 783 str_replace(' ', '', $this->messageParameters['accept_presentation_document_targets'])), 'strlen'); 784 $documentTargets = array_unique($documentTargets); 785 $this->ok = count($documentTargets) > 0; 786 if (!$this->ok) { 787 $this->reason = 'Missing or empty accept_presentation_document_targets parameter.'; 788 } else { 789 foreach ($documentTargets as $documentTarget) { 790 $this->ok = $this->checkValue($documentTarget, 791 array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay', 'none'), 792 'Invalid value in accept_presentation_document_targets parameter: %s.'); 793 if (!$this->ok) { 794 break; 795 } 796 } 797 if ($this->ok) { 798 $this->documentTargets = $documentTargets; 799 } 800 } 801 } else { 802 $this->ok = false; 803 } 804 if ($this->ok) { 805 $this->ok = isset($this->messageParameters['content_item_return_url']) && (strlen(trim($this->messageParameters['content_item_return_url'])) > 0); 806 if (!$this->ok) { 807 $this->reason = 'Missing content_item_return_url parameter.'; 808 } 809 } 810 } elseif ($this->messageParameters['lti_message_type'] == 'ToolProxyRegistrationRequest') { 811 $this->ok = ((isset($this->messageParameters['reg_key']) && (strlen(trim($this->messageParameters['reg_key'])) > 0)) && 812 (isset($this->messageParameters['reg_password']) && (strlen(trim($this->messageParameters['reg_password'])) > 0)) && 813 (isset($this->messageParameters['tc_profile_url']) && (strlen(trim($this->messageParameters['tc_profile_url'])) > 0)) && 814 (isset($this->messageParameters['launch_presentation_return_url']) && (strlen(trim($this->messageParameters['launch_presentation_return_url'])) > 0))); 815 if ($this->debugMode && !$this->ok) { 816 $this->reason = 'Missing message parameters.'; 817 } 818 } 819 } 820 $now = time(); 821// Check consumer key 822 if ($this->ok && ($this->messageParameters['lti_message_type'] != 'ToolProxyRegistrationRequest')) { 823 $this->ok = isset($this->messageParameters['oauth_consumer_key']); 824 if (!$this->ok) { 825 $this->reason = 'Missing consumer key.'; 826 } 827 if ($this->ok) { 828 $this->ok = !is_null($this->consumer->created); 829 if (!$this->ok) { 830 $this->reason = 'Invalid consumer key: ' . $this->messageParameters['oauth_consumer_key']; 831 } 832 } 833 if ($this->ok) { 834 $today = date('Y-m-d', $now); 835 if (is_null($this->consumer->lastAccess)) { 836 $doSaveConsumer = true; 837 } else { 838 $last = date('Y-m-d', $this->consumer->lastAccess); 839 $doSaveConsumer = $doSaveConsumer || ($last !== $today); 840 } 841 $this->consumer->lastAccess = $now; 842 $this->consumer->signatureMethod = isset($this->messageParameters['oauth_signature_method']) ? $this->messageParameters['oauth_signature_method'] : 843 $this->consumer->signatureMethod; 844 try { 845 $store = new OAuthDataStore($this); 846 $server = new OAuth\OAuthServer($store); 847 $method = new OAuth\OAuthSignatureMethod_HMAC_SHA224(); 848 $server->add_signature_method($method); 849 $method = new OAuth\OAuthSignatureMethod_HMAC_SHA256(); 850 $server->add_signature_method($method); 851 $method = new OAuth\OAuthSignatureMethod_HMAC_SHA384(); 852 $server->add_signature_method($method); 853 $method = new OAuth\OAuthSignatureMethod_HMAC_SHA512(); 854 $server->add_signature_method($method); 855 $method = new OAuth\OAuthSignatureMethod_HMAC_SHA1(); 856 $server->add_signature_method($method); 857 $request = OAuth\OAuthRequest::from_request(); 858 $res = $server->verify_request($request); 859 } catch (\Exception $e) { 860 $this->ok = false; 861 if (empty($this->reason)) { 862 $consumer = new OAuth\OAuthConsumer($this->consumer->getKey(), $this->consumer->secret); 863 $signature = $request->build_signature($method, $consumer, false); 864 if ($this->debugMode) { 865 $this->reason = $e->getMessage(); 866 } 867 if (empty($this->reason)) { 868 $this->reason = 'OAuth signature check failed - perhaps an incorrect secret or timestamp.'; 869 } 870 $this->details[] = 'Current timestamp: ' . time(); 871 $this->details[] = "Expected signature: {$signature}"; 872 $this->details[] = "Base string: {$request->base_string}"; 873 } 874 } 875 } 876 if ($this->ok) { 877 if ($this->consumer->protected) { 878 if (!is_null($this->consumer->consumerGuid)) { 879 $this->ok = empty($this->messageParameters['tool_consumer_instance_guid']) || 880 ($this->consumer->consumerGuid === $this->messageParameters['tool_consumer_instance_guid']); 881 if (!$this->ok) { 882 $this->reason = 'Request is from an invalid tool consumer.'; 883 } 884 } else { 885 $this->ok = isset($this->messageParameters['tool_consumer_instance_guid']); 886 if (!$this->ok) { 887 $this->reason = 'A tool consumer GUID must be included in the launch request.'; 888 } 889 } 890 } 891 if ($this->ok) { 892 $this->ok = $this->consumer->enabled; 893 if (!$this->ok) { 894 $this->reason = 'Tool consumer has not been enabled by the tool provider.'; 895 } 896 } 897 if ($this->ok) { 898 $this->ok = is_null($this->consumer->enableFrom) || ($this->consumer->enableFrom <= $now); 899 if ($this->ok) { 900 $this->ok = is_null($this->consumer->enableUntil) || ($this->consumer->enableUntil > $now); 901 if (!$this->ok) { 902 $this->reason = 'Tool consumer access has expired.'; 903 } 904 } else { 905 $this->reason = 'Tool consumer access is not yet available.'; 906 } 907 } 908 } 909 910// Validate other message parameter values 911 if ($this->ok) { 912 if (($this->messageParameters['lti_message_type'] === 'ContentItemSelectionRequest') || ($this->messageParameters['lti_message_type'] === 'LtiDeepLinkingRequest')) { 913 if (isset($this->messageParameters['accept_unsigned'])) { 914 $this->ok = $this->checkValue($this->messageParameters['accept_unsigned'], array('true', 'false'), 915 'Invalid value for accept_unsigned parameter: %s.'); 916 } 917 if ($this->ok && isset($this->messageParameters['accept_multiple'])) { 918 $this->ok = $this->checkValue($this->messageParameters['accept_multiple'], array('true', 'false'), 919 'Invalid value for accept_multiple parameter: %s.'); 920 } 921 if ($this->ok && isset($this->messageParameters['accept_copy_advice'])) { 922 $this->ok = $this->checkValue($this->messageParameters['accept_copy_advice'], array('true', 'false'), 923 'Invalid value for accept_copy_advice parameter: %s.'); 924 } 925 if ($this->ok && isset($this->messageParameters['auto_create'])) { 926 $this->ok = $this->checkValue($this->messageParameters['auto_create'], array('true', 'false'), 927 'Invalid value for auto_create parameter: %s.'); 928 } 929 if ($this->ok && isset($this->messageParameters['can_confirm'])) { 930 $this->ok = $this->checkValue($this->messageParameters['can_confirm'], array('true', 'false'), 931 'Invalid value for can_confirm parameter: %s.'); 932 } 933 } elseif (isset($this->messageParameters['launch_presentation_document_target'])) { 934 $this->ok = $this->checkValue($this->messageParameters['launch_presentation_document_target'], 935 array('embed', 'frame', 'iframe', 'window', 'popup', 'overlay'), 936 'Invalid value for launch_presentation_document_target parameter: %s.'); 937 } 938 } 939 } 940 941 if ($this->ok && ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest')) { 942 $this->ok = $this->messageParameters['lti_version'] == self::LTI_VERSION2; 943 if (!$this->ok) { 944 $this->reason = 'Invalid lti_version parameter'; 945 } 946 if ($this->ok) { 947 $url = $this->messageParameters['tc_profile_url']; 948 if (strpos($url, '?') === false) { 949 $url .= '?'; 950 } else { 951 $url .= '&'; 952 } 953 $url .= 'lti_version=' . self::LTI_VERSION2; 954 $http = new HttpMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json'); 955 $this->ok = $http->send(); 956 if (!$this->ok) { 957 $this->reason = 'Tool consumer profile not accessible.'; 958 } else { 959 $tcProfile = json_decode($http->response); 960 $this->ok = !is_null($tcProfile); 961 if (!$this->ok) { 962 $this->reason = 'Invalid JSON in tool consumer profile.'; 963 } 964 } 965 } 966// Check for required capabilities 967 if ($this->ok) { 968 $this->consumer = new ToolConsumer($this->messageParameters['reg_key'], $this->dataConnector); 969 $this->consumer->profile = $tcProfile; 970 $capabilities = $this->consumer->profile->capability_offered; 971 $missing = array(); 972 foreach ($this->resourceHandlers as $resourceHandler) { 973 foreach ($resourceHandler->requiredMessages as $message) { 974 if (!in_array($message->type, $capabilities)) { 975 $missing[$message->type] = true; 976 } 977 } 978 } 979 foreach ($this->constraints as $name => $constraint) { 980 if ($constraint['required']) { 981 if (empty(array_intersect($capabilities, 982 array_keys(array_intersect(self::$CUSTOM_SUBSTITUTION_VARIABLES, array($name)))))) { 983 $missing[$name] = true; 984 } 985 } 986 } 987 if (!empty($missing)) { 988 ksort($missing); 989 $this->reason = 'Required capability not offered - \'' . implode('\', \'', array_keys($missing)) . '\''; 990 $this->ok = false; 991 } 992 } 993// Check for required services 994 if ($this->ok) { 995 foreach ($this->requiredServices as $service) { 996 foreach ($service->formats as $format) { 997 if (!$this->findService($format, $service->actions)) { 998 if ($this->ok) { 999 $this->reason = 'Required service(s) not offered - '; 1000 $this->ok = false; 1001 } else { 1002 $this->reason .= ', '; 1003 } 1004 $this->reason .= "'{$format}' [" . implode(', ', $service->actions) . ']'; 1005 } 1006 } 1007 } 1008 } 1009 if ($this->ok) { 1010 if ($this->messageParameters['lti_message_type'] === 'ToolProxyRegistrationRequest') { 1011 $this->consumer->profile = $tcProfile; 1012 $this->consumer->secret = $this->messageParameters['reg_password']; 1013 $this->consumer->ltiVersion = $this->messageParameters['lti_version']; 1014 $this->consumer->name = $tcProfile->product_instance->service_owner->service_owner_name->default_value; 1015 $this->consumer->consumerName = $this->consumer->name; 1016 $this->consumer->consumerVersion = "{$tcProfile->product_instance->product_info->product_family->code}-{$tcProfile->product_instance->product_info->product_version}"; 1017 $this->consumer->consumerGuid = $tcProfile->product_instance->guid; 1018 $this->consumer->enabled = true; 1019 $this->consumer->protected = true; 1020 $doSaveConsumer = true; 1021 } 1022 } 1023 } elseif ($this->ok && !empty($this->messageParameters['custom_tc_profile_url']) && empty($this->consumer->profile)) { 1024 $url = $this->messageParameters['custom_tc_profile_url']; 1025 if (strpos($url, '?') === false) { 1026 $url .= '?'; 1027 } else { 1028 $url .= '&'; 1029 } 1030 $url .= 'lti_version=' . $this->messageParameters['lti_version']; 1031 $http = new HttpMessage($url, 'GET', null, 'Accept: application/vnd.ims.lti.v2.toolconsumerprofile+json'); 1032 if ($http->send()) { 1033 $tcProfile = json_decode($http->response); 1034 if (!is_null($tcProfile)) { 1035 $this->consumer->profile = $tcProfile; 1036 $doSaveConsumer = true; 1037 } 1038 } 1039 } 1040 1041 if ($this->ok) { 1042 1043// Check if a relaunch is being requested 1044 if (isset($this->messageParameters['relaunch_url'])) { 1045 if (empty($this->messageParameters['platform_state'])) { 1046 $this->ok = false; 1047 $this->reason = 'Missing or empty platform_state parameter'; 1048 } else { 1049 $this->sendRelaunchRequest(); 1050 } 1051 } else { 1052 1053// Validate message parameter constraints 1054 $invalidParameters = array(); 1055 foreach ($this->constraints as $name => $constraint) { 1056 if (empty($constraint['messages']) || in_array($this->messageParameters['lti_message_type'], $constraint['messages'])) { 1057 $ok = true; 1058 if ($constraint['required']) { 1059 if (!isset($this->messageParameters[$name]) || (strlen(trim($this->messageParameters[$name])) <= 0)) { 1060 $invalidParameters[] = "{$name} (missing)"; 1061 $ok = false; 1062 } 1063 } 1064 if ($ok && !is_null($constraint['max_length']) && isset($this->messageParameters[$name])) { 1065 if (strlen(trim($this->messageParameters[$name])) > $constraint['max_length']) { 1066 $invalidParameters[] = "{$name} (too long)"; 1067 } 1068 } 1069 } 1070 } 1071 if (count($invalidParameters) > 0) { 1072 $this->ok = false; 1073 if (empty($this->reason)) { 1074 $this->reason = 'Invalid parameter(s): ' . implode(', ', $invalidParameters) . '.'; 1075 } 1076 } 1077 1078 if ($this->ok) { 1079 1080// Set the request context 1081 $contextId = ''; 1082 if ($this->hasConfiguredApiHook(self::$CONTEXT_ID_HOOK, $this->consumer->getFamilyCode(), $this)) { 1083 $className = $this->getApiHook(self::$CONTEXT_ID_HOOK, $this->consumer->getFamilyCode()); 1084 $tpHook = new $className($this); 1085 $contextId = $tpHook->getContextId(); 1086 } 1087 if (empty($contextId) && isset($this->messageParameters['context_id'])) { 1088 $contextId = trim($this->messageParameters['context_id']); 1089 } 1090 if (!empty($contextId)) { 1091 $this->context = Context::fromConsumer($this->consumer, $contextId); 1092 $title = ''; 1093 if (isset($this->messageParameters['context_title'])) { 1094 $title = trim($this->messageParameters['context_title']); 1095 } 1096 if (empty($title)) { 1097 $title = "Course {$this->context->getId()}"; 1098 } 1099 $this->context->title = $title; 1100 if (isset($this->messageParameters['context_type'])) { 1101 $this->context->type = trim($this->messageParameters['context_type']); 1102 } 1103 } 1104 1105// Set the request resource link 1106 if (isset($this->messageParameters['resource_link_id'])) { 1107 $contentItemId = ''; 1108 if (isset($this->messageParameters['custom_content_item_id'])) { 1109 $contentItemId = $this->messageParameters['custom_content_item_id']; 1110 } 1111 if (empty($this->context)) { 1112 $this->resourceLink = ResourceLink::fromConsumer($this->consumer, 1113 trim($this->messageParameters['resource_link_id']), $contentItemId); 1114 } else { 1115 $this->resourceLink = ResourceLink::fromContext($this->context, 1116 trim($this->messageParameters['resource_link_id']), $contentItemId); 1117 } 1118 $title = ''; 1119 if (isset($this->messageParameters['resource_link_title'])) { 1120 $title = trim($this->messageParameters['resource_link_title']); 1121 } 1122 if (empty($title)) { 1123 $title = "Resource {$this->resourceLink->getId()}"; 1124 } 1125 $this->resourceLink->title = $title; 1126// Delete any existing custom parameters 1127 foreach ($this->consumer->getSettings() as $name => $value) { 1128 if ((strpos($name, 'custom_') === 0) && (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES))) { 1129 $this->consumer->setSetting($name); 1130 $doSaveConsumer = true; 1131 } 1132 } 1133 if (!empty($this->context)) { 1134 foreach ($this->context->getSettings() as $name => $value) { 1135 if ((strpos($name, 'custom_') === 0) && (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES))) { 1136 $this->context->setSetting($name); 1137 } 1138 } 1139 } 1140 foreach ($this->resourceLink->getSettings() as $name => $value) { 1141 if ((strpos($name, 'custom_') === 0) && (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES))) { 1142 $this->resourceLink->setSetting($name); 1143 } 1144 } 1145// Save LTI parameters 1146 foreach (self::$LTI_CONSUMER_SETTING_NAMES as $name) { 1147 if (isset($this->messageParameters[$name])) { 1148 $this->consumer->setSetting($name, $this->messageParameters[$name]); 1149 } else if (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES)) { 1150 $this->consumer->setSetting($name); 1151 } 1152 } 1153 if (!empty($this->context)) { 1154 foreach (self::$LTI_CONTEXT_SETTING_NAMES as $name) { 1155 if (isset($this->messageParameters[$name])) { 1156 $this->context->setSetting($name, $this->messageParameters[$name]); 1157 } else if (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES)) { 1158 $this->context->setSetting($name); 1159 } 1160 } 1161 } 1162 foreach (self::$LTI_RESOURCE_LINK_SETTING_NAMES as $name) { 1163 if (isset($this->messageParameters[$name])) { 1164 $this->resourceLink->setSetting($name, $this->messageParameters[$name]); 1165 } else if (!in_array($name, self::$LTI_RETAIN_SETTING_NAMES)) { 1166 $this->resourceLink->setSetting($name); 1167 } 1168 } 1169// Save other custom parameters at all levels 1170 foreach ($this->messageParameters as $name => $value) { 1171 if ((strpos($name, 'custom_') === 0) && 1172 !in_array($name, 1173 array_merge(self::$LTI_CONSUMER_SETTING_NAMES, self::$LTI_CONTEXT_SETTING_NAMES, 1174 self::$LTI_RESOURCE_LINK_SETTING_NAMES))) { 1175 $this->consumer->setSetting($name, $value); 1176 if (!empty($this->context)) { 1177 $this->context->setSetting($name, $value); 1178 } 1179 $this->resourceLink->setSetting($name, $value); 1180 } 1181 } 1182 } 1183 1184// Set the user instance 1185 $userId = ''; 1186 if ($this->hasConfiguredApiHook(self::$USER_ID_HOOK, $this->consumer->getFamilyCode(), $this)) { 1187 $className = $this->getApiHook(self::$USER_ID_HOOK, $this->consumer->getFamilyCode()); 1188 $tpHook = new $className($this); 1189 $userId = $tpHook->getUserId(); 1190 } 1191 if (empty($userId) && isset($this->messageParameters['user_id'])) { 1192 $userId = trim($this->messageParameters['user_id']); 1193 } 1194 1195 $this->userResult = UserResult::fromResourceLink($this->resourceLink, $userId); 1196 1197// Set the user name 1198 $firstname = (isset($this->messageParameters['lis_person_name_given'])) ? $this->messageParameters['lis_person_name_given'] : ''; 1199 $lastname = (isset($this->messageParameters['lis_person_name_family'])) ? $this->messageParameters['lis_person_name_family'] : ''; 1200 $fullname = (isset($this->messageParameters['lis_person_name_full'])) ? $this->messageParameters['lis_person_name_full'] : ''; 1201 $this->userResult->setNames($firstname, $lastname, $fullname); 1202 1203// Set the sourcedId 1204 if (isset($this->messageParameters['lis_person_sourcedid'])) { 1205 $this->userResult->sourcedId = $this->messageParameters['lis_person_sourcedid']; 1206 } 1207 1208// Set the username 1209 if (isset($this->messageParameters['ext_username'])) { 1210 $this->userResult->username = $this->messageParameters['ext_username']; 1211 } elseif (isset($this->messageParameters['ext_user_username'])) { 1212 $this->userResult->username = $this->messageParameters['ext_user_username']; 1213 } elseif (isset($this->messageParameters['custom_username'])) { 1214 $this->userResult->username = $this->messageParameters['custom_username']; 1215 } elseif (isset($this->messageParameters['custom_user_username'])) { 1216 $this->userResult->username = $this->messageParameters['custom_user_username']; 1217 } 1218 1219// Set the user email 1220 $email = (isset($this->messageParameters['lis_person_contact_email_primary'])) ? $this->messageParameters['lis_person_contact_email_primary'] : ''; 1221 $this->userResult->setEmail($email, $this->defaultEmail); 1222 1223// Set the user image URI 1224 if (isset($this->messageParameters['user_image'])) { 1225 $this->userResult->image = $this->messageParameters['user_image']; 1226 } 1227 1228// Set the user roles 1229 if (isset($this->messageParameters['roles'])) { 1230 $this->userResult->roles = self::parseRoles($this->messageParameters['roles'], $this->consumer->ltiVersion); 1231 } 1232 1233// Initialise the consumer and check for changes 1234 $this->consumer->defaultEmail = $this->defaultEmail; 1235 if ($this->consumer->ltiVersion !== $this->messageParameters['lti_version']) { 1236 $this->consumer->ltiVersion = $this->messageParameters['lti_version']; 1237 $doSaveConsumer = true; 1238 } 1239 if (isset($this->messageParameters['tool_consumer_instance_name'])) { 1240 if ($this->consumer->consumerName !== $this->messageParameters['tool_consumer_instance_name']) { 1241 $this->consumer->consumerName = $this->messageParameters['tool_consumer_instance_name']; 1242 $doSaveConsumer = true; 1243 } 1244 } 1245 if (isset($this->messageParameters['tool_consumer_info_product_family_code'])) { 1246 $version = $this->messageParameters['tool_consumer_info_product_family_code']; 1247 if (isset($this->messageParameters['tool_consumer_info_version'])) { 1248 $version .= "-{$this->messageParameters['tool_consumer_info_version'] 1249 }"; 1250 } 1251// do not delete any existing consumer version if none is passed 1252 if ($this->consumer->consumerVersion !== $version) { 1253 $this->consumer->consumerVersion = $version; 1254 $doSaveConsumer = true; 1255 } 1256 } elseif (isset($this->messageParameters['ext_lms']) && ($this->consumer->consumerName !== $this->messageParameters['ext_lms'])) { 1257 $this->consumer->consumerVersion = $this->messageParameters['ext_lms']; 1258 $doSaveConsumer = true; 1259 } 1260 if (isset($this->messageParameters['tool_consumer_instance_guid'])) { 1261 if (is_null($this->consumer->consumerGuid)) { 1262 $this->consumer->consumerGuid = $this->messageParameters['tool_consumer_instance_guid']; 1263 $doSaveConsumer = true; 1264 } elseif (!$this->consumer->protected) { 1265 $doSaveConsumer = ($this->consumer->consumerGuid !== $this->messageParameters['tool_consumer_instance_guid']); 1266 if ($doSaveConsumer) { 1267 $this->consumer->consumerGuid = $this->messageParameters['tool_consumer_instance_guid']; 1268 } 1269 } 1270 } 1271 if (isset($this->messageParameters['launch_presentation_css_url'])) { 1272 if ($this->consumer->cssPath !== $this->messageParameters['launch_presentation_css_url']) { 1273 $this->consumer->cssPath = $this->messageParameters['launch_presentation_css_url']; 1274 $doSaveConsumer = true; 1275 } 1276 } elseif (isset($this->messageParameters['ext_launch_presentation_css_url']) && 1277 ($this->consumer->cssPath !== $this->messageParameters['ext_launch_presentation_css_url'])) { 1278 $this->consumer->cssPath = $this->messageParameters['ext_launch_presentation_css_url']; 1279 $doSaveConsumer = true; 1280 } elseif (!empty($this->consumer->cssPath)) { 1281 $this->consumer->cssPath = null; 1282 $doSaveConsumer = true; 1283 } 1284 } 1285 1286// Persist changes to consumer 1287 if ($doSaveConsumer) { 1288 $this->consumer->save(); 1289 } 1290 if ($this->ok) { 1291 1292 if (isset($this->context)) { 1293 $this->context->save(); 1294 } 1295 1296 if (isset($this->resourceLink)) { 1297// Persist changes to resource link 1298 $this->resourceLink->save(); 1299 1300// Save the user instance 1301 $this->userResult->setResourceLinkId($this->resourceLink->getRecordId()); 1302 if (isset($this->messageParameters['lis_result_sourcedid'])) { 1303 if ($this->userResult->ltiResultSourcedId !== $this->messageParameters['lis_result_sourcedid']) { 1304 $this->userResult->ltiResultSourcedId = $this->messageParameters['lis_result_sourcedid']; 1305 $this->userResult->save(); 1306 } 1307 } elseif (!empty($this->userResult->ltiResultSourcedId)) { 1308 $this->userResult->ltiResultSourcedId = ''; 1309 $this->userResult->save(); 1310 } 1311 1312// Check if a share arrangement is in place for this resource link 1313 $this->ok = $this->checkForShare(); 1314 } 1315 } 1316 } 1317 } 1318 1319 return $this->ok; 1320 } 1321 1322 /** 1323 * Check if a share arrangement is in place. 1324 * 1325 * @return bool True if no error is reported 1326 */ 1327 private function checkForShare() 1328 { 1329 $ok = true; 1330 $doSaveResourceLink = true; 1331 1332 $id = $this->resourceLink->primaryResourceLinkId; 1333 1334 $shareRequest = isset($this->messageParameters['custom_share_key']) && !empty($this->messageParameters['custom_share_key']); 1335 if ($shareRequest) { 1336 if (!$this->allowSharing) { 1337 $ok = false; 1338 $this->reason = 'Your sharing request has been refused because sharing is not being permitted.'; 1339 } else { 1340// Check if this is a new share key 1341 $shareKey = new ResourceLinkShareKey($this->resourceLink, $this->messageParameters['custom_share_key']); 1342 if (!is_null($shareKey->resourceLinkId)) { 1343// Update resource link with sharing primary resource link details 1344 $id = $shareKey->resourceLinkId; 1345 $ok = ($id != $this->resourceLink->getRecordId()); 1346 if ($ok) { 1347 $this->resourceLink->primaryResourceLinkId = $id; 1348 $this->resourceLink->shareApproved = $shareKey->autoApprove; 1349 $ok = $this->resourceLink->save(); 1350 if ($ok) { 1351 $doSaveResourceLink = false; 1352 $this->userResult->getResourceLink()->primaryResourceLinkId = $id; 1353 $this->userResult->getResourceLink()->shareApproved = $shareKey->autoApprove; 1354 $this->userResult->getResourceLink()->updated = time(); 1355// Remove share key 1356 $shareKey->delete(); 1357 } else { 1358 $this->reason = 'An error occurred initialising your share arrangement.'; 1359 } 1360 } else { 1361 $this->reason = 'It is not possible to share your resource link with yourself.'; 1362 } 1363 } 1364 if ($ok) { 1365 $ok = !is_null($id); 1366 if (!$ok) { 1367 $this->reason = 'You have requested to share a resource link but none is available.'; 1368 } else { 1369 $ok = (!is_null($this->userResult->getResourceLink()->shareApproved) && $this->userResult->getResourceLink()->shareApproved); 1370 if (!$ok) { 1371 $this->reason = 'Your share request is waiting to be approved.'; 1372 } 1373 } 1374 } 1375 } 1376 } else { 1377// Check no share is in place 1378 $ok = is_null($id); 1379 if (!$ok) { 1380 $this->reason = 'You have not requested to share a resource link but an arrangement is currently in place.'; 1381 } 1382 } 1383 1384// Look up primary resource link 1385 if ($ok && !is_null($id)) { 1386 $resourceLink = ResourceLink::fromRecordId($id, $this->dataConnector); 1387 $ok = !is_null($resourceLink->created); 1388 if ($ok) { 1389 if ($doSaveResourceLink) { 1390 $this->resourceLink->save(); 1391 } 1392 $this->resourceLink = $resourceLink; 1393 } else { 1394 $this->reason = 'Unable to load resource link being shared.'; 1395 } 1396 } 1397 1398 return $ok; 1399 } 1400 1401 /** 1402 * Generate a form to perform a relaunch request. 1403 */ 1404 private function sendRelaunchRequest() 1405 { 1406 do { 1407 $nonce = new ConsumerNonce($this->consumer, DataConnector\DataConnector::getRandomString()); 1408 $ok = !$nonce->load(); 1409 } while (!$ok); 1410 $ok = $nonce->save(); 1411 if ($ok) { 1412 $params = array( 1413 'tool_state' => $nonce->getValue(), 1414 'platform_state' => $this->messageParameters['platform_state'] 1415 ); 1416 $params = $this->consumer->addSignature($this->messageParameters['relaunch_url'], $params); 1417 $this->output = static::sendForm($this->messageParameters['relaunch_url'], $params); 1418 } else { 1419 $this->reason = 'Unable to generate a state value'; 1420 } 1421 } 1422 1423 /** 1424 * Validate a parameter value from an array of permitted values. 1425 * 1426 * @param mixed $value Value to be checked 1427 * @param array $values Array of permitted values 1428 * @param string $reason Reason to generate when the value is not permitted 1429 * 1430 * @return bool True if value is valid 1431 */ 1432 private function checkValue($value, $values, $reason) 1433 { 1434 $ok = in_array($value, $values); 1435 if (!$ok && !empty($reason)) { 1436 $this->reason = sprintf($reason, $value); 1437 } 1438 1439 return $ok; 1440 } 1441 1442} 1443