Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 401 and 402] [Versions 401 and 403]

   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  }