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 DOMDocument; 6 use DOMElement; 7 use IMSGlobal\LTI\ToolProvider\DataConnector\DataConnector; 8 use IMSGlobal\LTI\ToolProvider\Service; 9 use IMSGlobal\LTI\HTTPMessage; 10 use IMSGlobal\LTI\OAuth; 11 12 /** 13 * Class to represent a tool consumer resource link 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 http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0 20 */ 21 class ResourceLink 22 { 23 24 /** 25 * Read action. 26 */ 27 const EXT_READ = 1; 28 /** 29 * Write (create/update) action. 30 */ 31 const EXT_WRITE = 2; 32 /** 33 * Delete action. 34 */ 35 const EXT_DELETE = 3; 36 /** 37 * Create action. 38 */ 39 const EXT_CREATE = 4; 40 /** 41 * Update action. 42 */ 43 const EXT_UPDATE = 5; 44 45 /** 46 * Decimal outcome type. 47 */ 48 const EXT_TYPE_DECIMAL = 'decimal'; 49 /** 50 * Percentage outcome type. 51 */ 52 const EXT_TYPE_PERCENTAGE = 'percentage'; 53 /** 54 * Ratio outcome type. 55 */ 56 const EXT_TYPE_RATIO = 'ratio'; 57 /** 58 * Letter (A-F) outcome type. 59 */ 60 const EXT_TYPE_LETTER_AF = 'letteraf'; 61 /** 62 * Letter (A-F) with optional +/- outcome type. 63 */ 64 const EXT_TYPE_LETTER_AF_PLUS = 'letterafplus'; 65 /** 66 * Pass/fail outcome type. 67 */ 68 const EXT_TYPE_PASS_FAIL = 'passfail'; 69 /** 70 * Free text outcome type. 71 */ 72 const EXT_TYPE_TEXT = 'freetext'; 73 74 /** 75 * Context title. 76 * 77 * @var string $title 78 */ 79 public $title = null; 80 /** 81 * Resource link ID as supplied in the last connection request. 82 * 83 * @var string $ltiResourceLinkId 84 */ 85 public $ltiResourceLinkId = null; 86 /** 87 * User group sets (null if the consumer does not support the groups enhancement) 88 * 89 * @var array $groupSets 90 */ 91 public $groupSets = null; 92 /** 93 * User groups (null if the consumer does not support the groups enhancement) 94 * 95 * @var array $groups 96 */ 97 public $groups = null; 98 /** 99 * Request for last service request. 100 * 101 * @var string $extRequest 102 */ 103 public $extRequest = null; 104 /** 105 * Request headers for last service request. 106 * 107 * @var array $extRequestHeaders 108 */ 109 public $extRequestHeaders = null; 110 /** 111 * Response from last service request. 112 * 113 * @var string $extResponse 114 */ 115 public $extResponse = null; 116 /** 117 * Response header from last service request. 118 * 119 * @var array $extResponseHeaders 120 */ 121 public $extResponseHeaders = null; 122 /** 123 * Consumer key value for resource link being shared (if any). 124 * 125 * @var string $primaryResourceLinkId 126 */ 127 public $primaryResourceLinkId = null; 128 /** 129 * Whether the sharing request has been approved by the primary resource link. 130 * 131 * @var boolean $shareApproved 132 */ 133 public $shareApproved = null; 134 /** 135 * Date/time when the object was created. 136 * 137 * @var int $created 138 */ 139 public $created = null; 140 /** 141 * Date/time when the object was last updated. 142 * 143 * @var int $updated 144 */ 145 public $updated = null; 146 147 /** 148 * Record ID for this resource link. 149 * 150 * @var int $id 151 */ 152 private $id = null; 153 /** 154 * Tool Consumer for this resource link. 155 * 156 * @var ToolConsumer $consumer 157 */ 158 private $consumer = null; 159 /** 160 * Tool Consumer ID for this resource link. 161 * 162 * @var int $consumerId 163 */ 164 private $consumerId = null; 165 /** 166 * Context for this resource link. 167 * 168 * @var Context $context 169 */ 170 private $context = null; 171 /** 172 * Context ID for this resource link. 173 * 174 * @var int $contextId 175 */ 176 private $contextId = null; 177 /** 178 * Setting values (LTI parameters, custom parameters and local parameters). 179 * 180 * @var array $settings 181 */ 182 private $settings = null; 183 /** 184 * Whether the settings value have changed since last saved. 185 * 186 * @var boolean $settingsChanged 187 */ 188 private $settingsChanged = false; 189 /** 190 * XML document for the last extension service request. 191 * 192 * @var string $extDoc 193 */ 194 private $extDoc = null; 195 /** 196 * XML node array for the last extension service request. 197 * 198 * @var array $extNodes 199 */ 200 private $extNodes = null; 201 /** 202 * Data connector object or string. 203 * 204 * @var mixed $dataConnector 205 */ 206 private $dataConnector = null; 207 208 /** 209 * Class constructor. 210 */ 211 public function __construct() 212 { 213 214 $this->initialize(); 215 216 } 217 218 /** 219 * Initialise the resource link. 220 */ 221 public function initialize() 222 { 223 224 $this->title = ''; 225 $this->settings = array(); 226 $this->groupSets = null; 227 $this->groups = null; 228 $this->primaryResourceLinkId = null; 229 $this->shareApproved = null; 230 $this->created = null; 231 $this->updated = null; 232 233 } 234 235 /** 236 * Initialise the resource link. 237 * 238 * Pseudonym for initialize(). 239 */ 240 public function initialise() 241 { 242 243 $this->initialize(); 244 245 } 246 247 /** 248 * Save the resource link to the database. 249 * 250 * @return boolean True if the resource link was successfully saved. 251 */ 252 public function save() 253 { 254 255 $ok = $this->getDataConnector()->saveResourceLink($this); 256 if ($ok) { 257 $this->settingsChanged = false; 258 } 259 260 return $ok; 261 262 } 263 264 /** 265 * Delete the resource link from the database. 266 * 267 * @return boolean True if the resource link was successfully deleted. 268 */ 269 public function delete() 270 { 271 272 return $this->getDataConnector()->deleteResourceLink($this); 273 274 } 275 276 /** 277 * Get tool consumer. 278 * 279 * @return ToolConsumer Tool consumer object for this resource link. 280 */ 281 public function getConsumer() 282 { 283 284 if (is_null($this->consumer)) { 285 if (!is_null($this->context) || !is_null($this->contextId)) { 286 $this->consumer = $this->getContext()->getConsumer(); 287 } else { 288 $this->consumer = ToolConsumer::fromRecordId($this->consumerId, $this->getDataConnector()); 289 } 290 } 291 292 return $this->consumer; 293 294 } 295 296 /** 297 * Set tool consumer ID. 298 * 299 * @param int $consumerId Tool Consumer ID for this resource link. 300 */ 301 public function setConsumerId($consumerId) 302 { 303 304 $this->consumer = null; 305 $this->consumerId = $consumerId; 306 307 } 308 309 /** 310 * Get context. 311 * 312 * @return object LTIContext object for this resource link. 313 */ 314 public function getContext() 315 { 316 317 if (is_null($this->context) && !is_null($this->contextId)) { 318 $this->context = Context::fromRecordId($this->contextId, $this->getDataConnector()); 319 } 320 321 return $this->context; 322 323 } 324 325 /** 326 * Get context record ID. 327 * 328 * @return int Context record ID for this resource link. 329 */ 330 public function getContextId() 331 { 332 333 return $this->contextId; 334 335 } 336 337 /** 338 * Set context ID. 339 * 340 * @param int $contextId Context ID for this resource link. 341 */ 342 public function setContextId($contextId) 343 { 344 345 $this->context = null; 346 $this->contextId = $contextId; 347 348 } 349 350 /** 351 * Get tool consumer key. 352 * 353 * @return string Consumer key value for this resource link. 354 */ 355 public function getKey() 356 { 357 358 return $this->getConsumer()->getKey(); 359 360 } 361 362 /** 363 * Get resource link ID. 364 * 365 * @return string ID for this resource link. 366 */ 367 public function getId() 368 { 369 370 return $this->ltiResourceLinkId; 371 372 } 373 374 /** 375 * Get resource link record ID. 376 * 377 * @return int Record ID for this resource link. 378 */ 379 public function getRecordId() 380 { 381 382 return $this->id; 383 384 } 385 386 /** 387 * Set resource link record ID. 388 * 389 * @param int $id Record ID for this resource link. 390 */ 391 public function setRecordId($id) 392 { 393 394 $this->id = $id; 395 396 } 397 398 /** 399 * Get the data connector. 400 * 401 * @return mixed Data connector object or string 402 */ 403 public function getDataConnector() 404 { 405 406 return $this->dataConnector; 407 408 } 409 410 /** 411 * Get a setting value. 412 * 413 * @param string $name Name of setting 414 * @param string $default Value to return if the setting does not exist (optional, default is an empty string) 415 * 416 * @return string Setting value 417 */ 418 public function getSetting($name, $default = '') 419 { 420 421 if (array_key_exists($name, $this->settings)) { 422 $value = $this->settings[$name]; 423 } else { 424 $value = $default; 425 } 426 427 return $value; 428 429 } 430 431 /** 432 * Set a setting value. 433 * 434 * @param string $name Name of setting 435 * @param string $value Value to set, use an empty value to delete a setting (optional, default is null) 436 */ 437 public function setSetting($name, $value = null) 438 { 439 440 $old_value = $this->getSetting($name); 441 if ($value !== $old_value) { 442 if (!empty($value)) { 443 $this->settings[$name] = $value; 444 } else { 445 unset($this->settings[$name]); 446 } 447 $this->settingsChanged = true; 448 } 449 450 } 451 452 /** 453 * Get an array of all setting values. 454 * 455 * @return array Associative array of setting values 456 */ 457 public function getSettings() 458 { 459 460 return $this->settings; 461 462 } 463 464 /** 465 * Set an array of all setting values. 466 * 467 * @param array $settings Associative array of setting values 468 */ 469 public function setSettings($settings) 470 { 471 472 $this->settings = $settings; 473 474 } 475 476 /** 477 * Save setting values. 478 * 479 * @return boolean True if the settings were successfully saved 480 */ 481 public function saveSettings() 482 { 483 484 if ($this->settingsChanged) { 485 $ok = $this->save(); 486 } else { 487 $ok = true; 488 } 489 490 return $ok; 491 492 } 493 494 /** 495 * Check if the Outcomes service is supported. 496 * 497 * @return boolean True if this resource link supports the Outcomes service (either the LTI 1.1 or extension service) 498 */ 499 public function hasOutcomesService() 500 { 501 502 $url = $this->getSetting('ext_ims_lis_basic_outcome_url') . $this->getSetting('lis_outcome_service_url'); 503 504 return !empty($url); 505 506 } 507 508 /** 509 * Check if the Memberships extension service is supported. 510 * 511 * @return boolean True if this resource link supports the Memberships extension service 512 */ 513 public function hasMembershipsService() 514 { 515 516 $url = $this->getSetting('ext_ims_lis_memberships_url'); 517 518 return !empty($url); 519 520 } 521 522 /** 523 * Check if the Setting extension service is supported. 524 * 525 * @return boolean True if this resource link supports the Setting extension service 526 */ 527 public function hasSettingService() 528 { 529 530 $url = $this->getSetting('ext_ims_lti_tool_setting_url'); 531 532 return !empty($url); 533 534 } 535 536 /** 537 * Perform an Outcomes service request. 538 * 539 * @param int $action The action type constant 540 * @param Outcome $ltiOutcome Outcome object 541 * @param User $user User object 542 * 543 * @return boolean True if the request was successfully processed 544 */ 545 public function doOutcomesService($action, $ltiOutcome, $user) 546 { 547 548 $response = false; 549 $this->extResponse = null; 550 551 // Lookup service details from the source resource link appropriate to the user (in case the destination is being shared) 552 $sourceResourceLink = $user->getResourceLink(); 553 $sourcedId = $user->ltiResultSourcedId; 554 555 // Use LTI 1.1 service in preference to extension service if it is available 556 $urlLTI11 = $sourceResourceLink->getSetting('lis_outcome_service_url'); 557 $urlExt = $sourceResourceLink->getSetting('ext_ims_lis_basic_outcome_url'); 558 if ($urlExt || $urlLTI11) { 559 switch ($action) { 560 case self::EXT_READ: 561 if ($urlLTI11 && ($ltiOutcome->type === self::EXT_TYPE_DECIMAL)) { 562 $do = 'readResult'; 563 } else if ($urlExt) { 564 $urlLTI11 = null; 565 $do = 'basic-lis-readresult'; 566 } 567 break; 568 case self::EXT_WRITE: 569 if ($urlLTI11 && $this->checkValueType($ltiOutcome, array(self::EXT_TYPE_DECIMAL))) { 570 $do = 'replaceResult'; 571 } else if ($this->checkValueType($ltiOutcome)) { 572 $urlLTI11 = null; 573 $do = 'basic-lis-updateresult'; 574 } 575 break; 576 case self::EXT_DELETE: 577 if ($urlLTI11 && ($ltiOutcome->type === self::EXT_TYPE_DECIMAL)) { 578 $do = 'deleteResult'; 579 } else if ($urlExt) { 580 $urlLTI11 = null; 581 $do = 'basic-lis-deleteresult'; 582 } 583 break; 584 } 585 } 586 if (isset($do)) { 587 $value = $ltiOutcome->getValue(); 588 if (is_null($value)) { 589 $value = ''; 590 } 591 if ($urlLTI11) { 592 $xml = ''; 593 if ($action === self::EXT_WRITE) { 594 $xml = <<<EOF 595 596 <result> 597 <resultScore> 598 <language>{$ltiOutcome->language}</language> 599 <textString>{$value}</textString> 600 </resultScore> 601 </result> 602 EOF; 603 } 604 $sourcedId = htmlentities($sourcedId); 605 $xml = <<<EOF 606 <resultRecord> 607 <sourcedGUID> 608 <sourcedId>{$sourcedId}</sourcedId> 609 </sourcedGUID>{$xml} 610 </resultRecord> 611 EOF; 612 if ($this->doLTI11Service($do, $urlLTI11, $xml)) { 613 switch ($action) { 614 case self::EXT_READ: 615 if (!isset($this->extNodes['imsx_POXBody']["{$do}Response"]['result']['resultScore']['textString'])) { 616 break; 617 } else { 618 $ltiOutcome->setValue($this->extNodes['imsx_POXBody']["{$do}Response"]['result']['resultScore']['textString']); 619 } 620 case self::EXT_WRITE: 621 case self::EXT_DELETE: 622 $response = true; 623 break; 624 } 625 } 626 } else { 627 $params = array(); 628 $params['sourcedid'] = $sourcedId; 629 $params['result_resultscore_textstring'] = $value; 630 if (!empty($ltiOutcome->language)) { 631 $params['result_resultscore_language'] = $ltiOutcome->language; 632 } 633 if (!empty($ltiOutcome->status)) { 634 $params['result_statusofresult'] = $ltiOutcome->status; 635 } 636 if (!empty($ltiOutcome->date)) { 637 $params['result_date'] = $ltiOutcome->date; 638 } 639 if (!empty($ltiOutcome->type)) { 640 $params['result_resultvaluesourcedid'] = $ltiOutcome->type; 641 } 642 if (!empty($ltiOutcome->data_source)) { 643 $params['result_datasource'] = $ltiOutcome->data_source; 644 } 645 if ($this->doService($do, $urlExt, $params)) { 646 switch ($action) { 647 case self::EXT_READ: 648 if (isset($this->extNodes['result']['resultscore']['textstring'])) { 649 $response = $this->extNodes['result']['resultscore']['textstring']; 650 } 651 break; 652 case self::EXT_WRITE: 653 case self::EXT_DELETE: 654 $response = true; 655 break; 656 } 657 } 658 } 659 if (is_array($response) && (count($response) <= 0)) { 660 $response = ''; 661 } 662 } 663 664 return $response; 665 666 } 667 668 /** 669 * Perform a Memberships service request. 670 * 671 * The user table is updated with the new list of user objects. 672 * 673 * @param boolean $withGroups True is group information is to be requested as well 674 * 675 * @return mixed Array of User objects or False if the request was not successful 676 */ 677 public function doMembershipsService($withGroups = false) 678 { 679 680 $users = array(); 681 $oldUsers = $this->getUserResultSourcedIDs(true, ToolProvider::ID_SCOPE_RESOURCE); 682 $this->extResponse = null; 683 $url = $this->getSetting('ext_ims_lis_memberships_url'); 684 $params = array(); 685 $params['id'] = $this->getSetting('ext_ims_lis_memberships_id'); 686 $ok = false; 687 if ($withGroups) { 688 $ok = $this->doService('basic-lis-readmembershipsforcontextwithgroups', $url, $params); 689 } 690 if ($ok) { 691 $this->groupSets = array(); 692 $this->groups = array(); 693 } else { 694 $ok = $this->doService('basic-lis-readmembershipsforcontext', $url, $params); 695 } 696 697 if ($ok) { 698 if (!isset($this->extNodes['memberships']['member'])) { 699 $members = array(); 700 } else if (!isset($this->extNodes['memberships']['member'][0])) { 701 $members = array(); 702 $members[0] = $this->extNodes['memberships']['member']; 703 } else { 704 $members = $this->extNodes['memberships']['member']; 705 } 706 707 for ($i = 0; $i < count($members); $i++) { 708 709 $user = User::fromResourceLink($this, $members[$i]['user_id']); 710 711 // Set the user name 712 $firstname = (isset($members[$i]['person_name_given'])) ? $members[$i]['person_name_given'] : ''; 713 $lastname = (isset($members[$i]['person_name_family'])) ? $members[$i]['person_name_family'] : ''; 714 $fullname = (isset($members[$i]['person_name_full'])) ? $members[$i]['person_name_full'] : ''; 715 $user->setNames($firstname, $lastname, $fullname); 716 717 // Set the user email 718 $email = (isset($members[$i]['person_contact_email_primary'])) ? $members[$i]['person_contact_email_primary'] : ''; 719 $user->setEmail($email, $this->getConsumer()->defaultEmail); 720 721 /// Set the user roles 722 if (isset($members[$i]['roles'])) { 723 $user->roles = ToolProvider::parseRoles($members[$i]['roles']); 724 } 725 726 // Set the user groups 727 if (!isset($members[$i]['groups']['group'])) { 728 $groups = array(); 729 } else if (!isset($members[$i]['groups']['group'][0])) { 730 $groups = array(); 731 $groups[0] = $members[$i]['groups']['group']; 732 } else { 733 $groups = $members[$i]['groups']['group']; 734 } 735 for ($j = 0; $j < count($groups); $j++) { 736 $group = $groups[$j]; 737 if (isset($group['set'])) { 738 $set_id = $group['set']['id']; 739 if (!isset($this->groupSets[$set_id])) { 740 $this->groupSets[$set_id] = array('title' => $group['set']['title'], 'groups' => array(), 741 'num_members' => 0, 'num_staff' => 0, 'num_learners' => 0); 742 } 743 $this->groupSets[$set_id]['num_members']++; 744 if ($user->isStaff()) { 745 $this->groupSets[$set_id]['num_staff']++; 746 } 747 if ($user->isLearner()) { 748 $this->groupSets[$set_id]['num_learners']++; 749 } 750 if (!in_array($group['id'], $this->groupSets[$set_id]['groups'])) { 751 $this->groupSets[$set_id]['groups'][] = $group['id']; 752 } 753 $this->groups[$group['id']] = array('title' => $group['title'], 'set' => $set_id); 754 } else { 755 $this->groups[$group['id']] = array('title' => $group['title']); 756 } 757 $user->groups[] = $group['id']; 758 } 759 760 // If a result sourcedid is provided save the user 761 if (isset($members[$i]['lis_result_sourcedid'])) { 762 $user->ltiResultSourcedId = $members[$i]['lis_result_sourcedid']; 763 $user->save(); 764 } 765 $users[] = $user; 766 767 // Remove old user (if it exists) 768 unset($oldUsers[$user->getId(ToolProvider::ID_SCOPE_RESOURCE)]); 769 } 770 771 // Delete any old users which were not in the latest list from the tool consumer 772 foreach ($oldUsers as $id => $user) { 773 $user->delete(); 774 } 775 } else { 776 $users = false; 777 } 778 779 return $users; 780 781 } 782 783 /** 784 * Perform a Setting service request. 785 * 786 * @param int $action The action type constant 787 * @param string $value The setting value (optional, default is null) 788 * 789 * @return mixed The setting value for a read action, true if a write or delete action was successful, otherwise false 790 */ 791 public function doSettingService($action, $value = null) 792 { 793 794 $response = false; 795 $this->extResponse = null; 796 switch ($action) { 797 case self::EXT_READ: 798 $do = 'basic-lti-loadsetting'; 799 break; 800 case self::EXT_WRITE: 801 $do = 'basic-lti-savesetting'; 802 break; 803 case self::EXT_DELETE: 804 $do = 'basic-lti-deletesetting'; 805 break; 806 } 807 if (isset($do)) { 808 809 $url = $this->getSetting('ext_ims_lti_tool_setting_url'); 810 $params = array(); 811 $params['id'] = $this->getSetting('ext_ims_lti_tool_setting_id'); 812 if (is_null($value)) { 813 $value = ''; 814 } 815 $params['setting'] = $value; 816 817 if ($this->doService($do, $url, $params)) { 818 switch ($action) { 819 case self::EXT_READ: 820 if (isset($this->extNodes['setting']['value'])) { 821 $response = $this->extNodes['setting']['value']; 822 if (is_array($response)) { 823 $response = ''; 824 } 825 } 826 break; 827 case self::EXT_WRITE: 828 $this->setSetting('ext_ims_lti_tool_setting', $value); 829 $this->saveSettings(); 830 $response = true; 831 break; 832 case self::EXT_DELETE: 833 $response = true; 834 break; 835 } 836 } 837 } 838 839 return $response; 840 841 } 842 843 /** 844 * Check if the Tool Settings service is supported. 845 * 846 * @return boolean True if this resource link supports the Tool Settings service 847 */ 848 public function hasToolSettingsService() 849 { 850 851 $url = $this->getSetting('custom_link_setting_url'); 852 853 return !empty($url); 854 855 } 856 857 /** 858 * Get Tool Settings. 859 * 860 * @param int $mode Mode for request (optional, default is current level only) 861 * @param boolean $simple True if all the simple media type is to be used (optional, default is true) 862 * 863 * @return mixed The array of settings if successful, otherwise false 864 */ 865 public function getToolSettings($mode = Service\ToolSettings::MODE_CURRENT_LEVEL, $simple = true) 866 { 867 868 $url = $this->getSetting('custom_link_setting_url'); 869 $service = new Service\ToolSettings($this, $url, $simple); 870 $response = $service->get($mode); 871 872 return $response; 873 874 } 875 876 /** 877 * Perform a Tool Settings service request. 878 * 879 * @param array $settings An associative array of settings (optional, default is none) 880 * 881 * @return boolean True if action was successful, otherwise false 882 */ 883 public function setToolSettings($settings = array()) 884 { 885 886 $url = $this->getSetting('custom_link_setting_url'); 887 $service = new Service\ToolSettings($this, $url); 888 $response = $service->set($settings); 889 890 return $response; 891 892 } 893 894 /** 895 * Check if the Membership service is supported. 896 * 897 * @return boolean True if this resource link supports the Membership service 898 */ 899 public function hasMembershipService() 900 { 901 902 $has = !empty($this->contextId); 903 if ($has) { 904 $has = !empty($this->getContext()->getSetting('custom_context_memberships_url')); 905 } 906 907 return $has; 908 909 } 910 911 /** 912 * Get Memberships. 913 * 914 * @return mixed The array of User objects if successful, otherwise false 915 */ 916 public function getMembership() 917 { 918 919 $response = false; 920 if (!empty($this->contextId)) { 921 $url = $this->getContext()->getSetting('custom_context_memberships_url'); 922 if (!empty($url)) { 923 $service = new Service\Membership($this, $url); 924 $response = $service->get(); 925 } 926 } 927 928 return $response; 929 930 } 931 932 /** 933 * Obtain an array of User objects for users with a result sourcedId. 934 * 935 * The array may include users from other resource links which are sharing this resource link. 936 * It may also be optionally indexed by the user ID of a specified scope. 937 * 938 * @param boolean $localOnly True if only users from this resource link are to be returned, not users from shared resource links (optional, default is false) 939 * @param int $idScope Scope to use for ID values (optional, default is null for consumer default) 940 * 941 * @return array Array of User objects 942 */ 943 public function getUserResultSourcedIDs($localOnly = false, $idScope = null) 944 { 945 946 return $this->getDataConnector()->getUserResultSourcedIDsResourceLink($this, $localOnly, $idScope); 947 948 } 949 950 /** 951 * Get an array of ResourceLinkShare objects for each resource link which is sharing this context. 952 * 953 * @return array Array of ResourceLinkShare objects 954 */ 955 public function getShares() 956 { 957 958 return $this->getDataConnector()->getSharesResourceLink($this); 959 960 } 961 962 /** 963 * Class constructor from consumer. 964 * 965 * @param ToolConsumer $consumer Consumer object 966 * @param string $ltiResourceLinkId Resource link ID value 967 * @param string $tempId Temporary Resource link ID value (optional, default is null) 968 * @return ResourceLink 969 */ 970 public static function fromConsumer($consumer, $ltiResourceLinkId, $tempId = null) 971 { 972 973 $resourceLink = new ResourceLink(); 974 $resourceLink->consumer = $consumer; 975 $resourceLink->dataConnector = $consumer->getDataConnector(); 976 $resourceLink->ltiResourceLinkId = $ltiResourceLinkId; 977 if (!empty($ltiResourceLinkId)) { 978 $resourceLink->load(); 979 if (is_null($resourceLink->id) && !empty($tempId)) { 980 $resourceLink->ltiResourceLinkId = $tempId; 981 $resourceLink->load(); 982 $resourceLink->ltiResourceLinkId = $ltiResourceLinkId; 983 } 984 } 985 986 return $resourceLink; 987 988 } 989 990 /** 991 * Class constructor from context. 992 * 993 * @param Context $context Context object 994 * @param string $ltiResourceLinkId Resource link ID value 995 * @param string $tempId Temporary Resource link ID value (optional, default is null) 996 * @return ResourceLink 997 */ 998 public static function fromContext($context, $ltiResourceLinkId, $tempId = null) 999 { 1000 1001 $resourceLink = new ResourceLink(); 1002 $resourceLink->setContextId($context->getRecordId()); 1003 $resourceLink->context = $context; 1004 $resourceLink->dataConnector = $context->getDataConnector(); 1005 $resourceLink->ltiResourceLinkId = $ltiResourceLinkId; 1006 if (!empty($ltiResourceLinkId)) { 1007 $resourceLink->load(); 1008 if (is_null($resourceLink->id) && !empty($tempId)) { 1009 $resourceLink->ltiResourceLinkId = $tempId; 1010 $resourceLink->load(); 1011 $resourceLink->ltiResourceLinkId = $ltiResourceLinkId; 1012 } 1013 } 1014 1015 return $resourceLink; 1016 1017 } 1018 1019 /** 1020 * Load the resource link from the database. 1021 * 1022 * @param int $id Record ID of resource link 1023 * @param DataConnector $dataConnector Database connection object 1024 * 1025 * @return ResourceLink ResourceLink object 1026 */ 1027 public static function fromRecordId($id, $dataConnector) 1028 { 1029 1030 $resourceLink = new ResourceLink(); 1031 $resourceLink->dataConnector = $dataConnector; 1032 $resourceLink->load($id); 1033 1034 return $resourceLink; 1035 1036 } 1037 1038 ### 1039 ### PRIVATE METHODS 1040 ### 1041 1042 /** 1043 * Load the resource link from the database. 1044 * 1045 * @param int $id Record ID of resource link (optional, default is null) 1046 * 1047 * @return boolean True if resource link was successfully loaded 1048 */ 1049 private function load($id = null) 1050 { 1051 1052 $this->initialize(); 1053 $this->id = $id; 1054 1055 return $this->getDataConnector()->loadResourceLink($this); 1056 1057 } 1058 1059 /** 1060 * Convert data type of value to a supported type if possible. 1061 * 1062 * @param Outcome $ltiOutcome Outcome object 1063 * @param string[] $supportedTypes Array of outcome types to be supported (optional, default is null to use supported types reported in the last launch for this resource link) 1064 * 1065 * @return boolean True if the type/value are valid and supported 1066 */ 1067 private function checkValueType($ltiOutcome, $supportedTypes = null) 1068 { 1069 1070 if (empty($supportedTypes)) { 1071 $supportedTypes = explode(',', str_replace(' ', '', strtolower($this->getSetting('ext_ims_lis_resultvalue_sourcedids', self::EXT_TYPE_DECIMAL)))); 1072 } 1073 $type = $ltiOutcome->type; 1074 $value = $ltiOutcome->getValue(); 1075 // Check whether the type is supported or there is no value 1076 $ok = in_array($type, $supportedTypes) || (strlen($value) <= 0); 1077 if (!$ok) { 1078 // Convert numeric values to decimal 1079 if ($type === self::EXT_TYPE_PERCENTAGE) { 1080 if (substr($value, -1) === '%') { 1081 $value = substr($value, 0, -1); 1082 } 1083 $ok = is_numeric($value) && ($value >= 0) && ($value <= 100); 1084 if ($ok) { 1085 $ltiOutcome->setValue($value / 100); 1086 $ltiOutcome->type = self::EXT_TYPE_DECIMAL; 1087 } 1088 } else if ($type === self::EXT_TYPE_RATIO) { 1089 $parts = explode('/', $value, 2); 1090 $ok = (count($parts) === 2) && is_numeric($parts[0]) && is_numeric($parts[1]) && ($parts[0] >= 0) && ($parts[1] > 0); 1091 if ($ok) { 1092 $ltiOutcome->setValue($parts[0] / $parts[1]); 1093 $ltiOutcome->type = self::EXT_TYPE_DECIMAL; 1094 } 1095 // Convert letter_af to letter_af_plus or text 1096 } else if ($type === self::EXT_TYPE_LETTER_AF) { 1097 if (in_array(self::EXT_TYPE_LETTER_AF_PLUS, $supportedTypes)) { 1098 $ok = true; 1099 $ltiOutcome->type = self::EXT_TYPE_LETTER_AF_PLUS; 1100 } else if (in_array(self::EXT_TYPE_TEXT, $supportedTypes)) { 1101 $ok = true; 1102 $ltiOutcome->type = self::EXT_TYPE_TEXT; 1103 } 1104 // Convert letter_af_plus to letter_af or text 1105 } else if ($type === self::EXT_TYPE_LETTER_AF_PLUS) { 1106 if (in_array(self::EXT_TYPE_LETTER_AF, $supportedTypes) && (strlen($value) === 1)) { 1107 $ok = true; 1108 $ltiOutcome->type = self::EXT_TYPE_LETTER_AF; 1109 } else if (in_array(self::EXT_TYPE_TEXT, $supportedTypes)) { 1110 $ok = true; 1111 $ltiOutcome->type = self::EXT_TYPE_TEXT; 1112 } 1113 // Convert text to decimal 1114 } else if ($type === self::EXT_TYPE_TEXT) { 1115 $ok = is_numeric($value) && ($value >= 0) && ($value <=1); 1116 if ($ok) { 1117 $ltiOutcome->type = self::EXT_TYPE_DECIMAL; 1118 } else if (substr($value, -1) === '%') { 1119 $value = substr($value, 0, -1); 1120 $ok = is_numeric($value) && ($value >= 0) && ($value <=100); 1121 if ($ok) { 1122 if (in_array(self::EXT_TYPE_PERCENTAGE, $supportedTypes)) { 1123 $ltiOutcome->type = self::EXT_TYPE_PERCENTAGE; 1124 } else { 1125 $ltiOutcome->setValue($value / 100); 1126 $ltiOutcome->type = self::EXT_TYPE_DECIMAL; 1127 } 1128 } 1129 } 1130 } 1131 } 1132 1133 return $ok; 1134 1135 } 1136 1137 /** 1138 * Send a service request to the tool consumer. 1139 * 1140 * @param string $type Message type value 1141 * @param string $url URL to send request to 1142 * @param array $params Associative array of parameter values to be passed 1143 * 1144 * @return boolean True if the request successfully obtained a response 1145 */ 1146 private function doService($type, $url, $params) 1147 { 1148 1149 $ok = false; 1150 $this->extRequest = null; 1151 $this->extRequestHeaders = ''; 1152 $this->extResponse = null; 1153 $this->extResponseHeaders = ''; 1154 if (!empty($url)) { 1155 $params = $this->getConsumer()->signParameters($url, $type, $this->getConsumer()->ltiVersion, $params); 1156 // Connect to tool consumer 1157 $http = new HTTPMessage($url, 'POST', $params); 1158 // Parse XML response 1159 if ($http->send()) { 1160 $this->extResponse = $http->response; 1161 $this->extResponseHeaders = $http->responseHeaders; 1162 try { 1163 $this->extDoc = new DOMDocument(); 1164 $this->extDoc->loadXML($http->response); 1165 $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement); 1166 if (isset($this->extNodes['statusinfo']['codemajor']) && ($this->extNodes['statusinfo']['codemajor'] === 'Success')) { 1167 $ok = true; 1168 } 1169 } catch (\Exception $e) { 1170 } 1171 } 1172 $this->extRequest = $http->request; 1173 $this->extRequestHeaders = $http->requestHeaders; 1174 } 1175 1176 return $ok; 1177 1178 } 1179 1180 /** 1181 * Send a service request to the tool consumer. 1182 * 1183 * @param string $type Message type value 1184 * @param string $url URL to send request to 1185 * @param string $xml XML of message request 1186 * 1187 * @return boolean True if the request successfully obtained a response 1188 */ 1189 private function doLTI11Service($type, $url, $xml) 1190 { 1191 1192 $ok = false; 1193 $this->extRequest = null; 1194 $this->extRequestHeaders = ''; 1195 $this->extResponse = null; 1196 $this->extResponseHeaders = ''; 1197 if (!empty($url)) { 1198 $id = uniqid(); 1199 $xmlRequest = <<< EOD 1200 <?xml version = "1.0" encoding = "UTF-8"?> 1201 <imsx_POXEnvelopeRequest xmlns = "http://www.imsglobal.org/services/ltiv1p1/xsd/imsoms_v1p0"> 1202 <imsx_POXHeader> 1203 <imsx_POXRequestHeaderInfo> 1204 <imsx_version>V1.0</imsx_version> 1205 <imsx_messageIdentifier>{$id}</imsx_messageIdentifier> 1206 </imsx_POXRequestHeaderInfo> 1207 </imsx_POXHeader> 1208 <imsx_POXBody> 1209 <{$type}Request> 1210 {$xml} 1211 </{$type}Request> 1212 </imsx_POXBody> 1213 </imsx_POXEnvelopeRequest> 1214 EOD; 1215 // Calculate body hash 1216 $hash = base64_encode(sha1($xmlRequest, true)); 1217 $params = array('oauth_body_hash' => $hash); 1218 1219 // Add OAuth signature 1220 $hmacMethod = new OAuth\OAuthSignatureMethod_HMAC_SHA1(); 1221 $consumer = new OAuth\OAuthConsumer($this->getConsumer()->getKey(), $this->getConsumer()->secret, null); 1222 $req = OAuth\OAuthRequest::from_consumer_and_token($consumer, null, 'POST', $url, $params); 1223 $req->sign_request($hmacMethod, $consumer, null); 1224 $params = $req->get_parameters(); 1225 $header = $req->to_header(); 1226 $header .= "\nContent-Type: application/xml"; 1227 // Connect to tool consumer 1228 $http = new HTTPMessage($url, 'POST', $xmlRequest, $header); 1229 // Parse XML response 1230 if ($http->send()) { 1231 $this->extResponse = $http->response; 1232 $this->extResponseHeaders = $http->responseHeaders; 1233 try { 1234 $this->extDoc = new DOMDocument(); 1235 $this->extDoc->loadXML($http->response); 1236 $this->extNodes = $this->domnodeToArray($this->extDoc->documentElement); 1237 if (isset($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor']) && 1238 ($this->extNodes['imsx_POXHeader']['imsx_POXResponseHeaderInfo']['imsx_statusInfo']['imsx_codeMajor'] === 'success')) { 1239 $ok = true; 1240 } 1241 } catch (\Exception $e) { 1242 } 1243 } 1244 $this->extRequest = $http->request; 1245 $this->extRequestHeaders = $http->requestHeaders; 1246 } 1247 1248 return $ok; 1249 1250 } 1251 1252 /** 1253 * Convert DOM nodes to array. 1254 * 1255 * @param DOMElement $node XML element 1256 * 1257 * @return array Array of XML document elements 1258 */ 1259 private function domnodeToArray($node) 1260 { 1261 1262 $output = ''; 1263 switch ($node->nodeType) { 1264 case XML_CDATA_SECTION_NODE: 1265 case XML_TEXT_NODE: 1266 $output = trim($node->textContent); 1267 break; 1268 case XML_ELEMENT_NODE: 1269 for ($i = 0; $i < $node->childNodes->length; $i++) { 1270 $child = $node->childNodes->item($i); 1271 $v = $this->domnodeToArray($child); 1272 if (isset($child->tagName)) { 1273 $t = $child->tagName; 1274 if (!isset($output[$t])) { 1275 $output[$t] = array(); 1276 } 1277 $output[$t][] = $v; 1278 } else { 1279 $s = (string) $v; 1280 if (strlen($s) > 0) { 1281 $output = $s; 1282 } 1283 } 1284 } 1285 if (is_array($output)) { 1286 if ($node->attributes->length) { 1287 $a = array(); 1288 foreach ($node->attributes as $attrName => $attrNode) { 1289 $a[$attrName] = (string) $attrNode->value; 1290 } 1291 $output['@attributes'] = $a; 1292 } 1293 foreach ($output as $t => $v) { 1294 if (is_array($v) && count($v)==1 && $t!='@attributes') { 1295 $output[$t] = $v[0]; 1296 } 1297 } 1298 } 1299 break; 1300 } 1301 1302 return $output; 1303 1304 } 1305 1306 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body