Search moodle.org's
Developer Documentation

See Release Notes

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

Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]

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