Search moodle.org's
Developer Documentation

  • Bug fixes for general core bugs in 3.6.x will end 11 November 2019 (12 months).
  • Bug fixes for security issues in 3.6.x will end 11 May 2020 (18 months) - Support has ended.
  • minimum PHP 7.0.0 Note: minimum PHP version has increased since Moodle 3.3. PHP 7.1.x and 7.2.x are supported too. PHP 7.3.x support is being implemented (@ MDL-63420) and not ready for production with this release.
  • Differences Between: [Versions 35 and 36]

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