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