Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 402] [Versions 310 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  }