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