Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

Differences Between: [Versions 310 and 403] [Versions 311 and 403] [Versions 39 and 403] [Versions 400 and 403] [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  #[\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  }