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