Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.
/mod/lti/ -> OAuth.php (source)

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

   1  <?php
   2  // This file is part of BasicLTI4Moodle
   3  //
   4  // BasicLTI4Moodle is an IMS BasicLTI (Basic Learning Tools for Interoperability)
   5  // consumer for Moodle 1.9 and Moodle 2.0. BasicLTI is a IMS Standard that allows web
   6  // based learning tools to be easily integrated in LMS as native ones. The IMS BasicLTI
   7  // specification is part of the IMS standard Common Cartridge 1.1 Sakai and other main LMS
   8  // are already supporting or going to support BasicLTI. This project Implements the consumer
   9  // for Moodle. Moodle is a Free Open source Learning Management System by Martin Dougiamas.
  10  // BasicLTI4Moodle is a project iniciated and leaded by Ludo(Marc Alier) and Jordi Piguillem
  11  // at the GESSI research group at UPC.
  12  // SimpleLTI consumer for Moodle is an implementation of the early specification of LTI
  13  // by Charles Severance (Dr Chuck) htp://dr-chuck.com , developed by Jordi Piguillem in a
  14  // Google Summer of Code 2008 project co-mentored by Charles Severance and Marc Alier.
  15  //
  16  // BasicLTI4Moodle is copyright 2009 by Marc Alier Forment, Jordi Piguillem and Nikolas Galanis
  17  // of the Universitat Politecnica de Catalunya http://www.upc.edu
  18  // Contact info: Marc Alier Forment granludo @ gmail.com or marc.alier @ upc.edu
  19  //
  20  // OAuth.php is distributed under the MIT License
  21  //
  22  // The MIT License
  23  //
  24  // Copyright (c) 2007 Andy Smith
  25  //
  26  // Permission is hereby granted, free of charge, to any person obtaining a copy
  27  // of this software and associated documentation files (the "Software"), to deal
  28  // in the Software without restriction, including without limitation the rights
  29  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  30  // copies of the Software, and to permit persons to whom the Software is
  31  // furnished to do so, subject to the following conditions:
  32  //
  33  // The above copyright notice and this permission notice shall be included in
  34  // all copies or substantial portions of the Software.
  35  //
  36  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  37  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  38  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  39  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  40  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  41  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  42  // THE SOFTWARE.
  43  //
  44  // Moodle is free software: you can redistribute it and/or modify
  45  // it under the terms of the GNU General Public License as published by
  46  // the Free Software Foundation, either version 3 of the License, or
  47  // (at your option) any later version.
  48  //
  49  // Moodle is distributed in the hope that it will be useful,
  50  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  51  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  52  // GNU General Public License for more details.
  53  //
  54  // You should have received a copy of the GNU General Public License
  55  // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  56  
  57  namespace moodle\mod\lti;//Using a namespace as the basicLTI module imports classes with the same names
  58  
  59  defined('MOODLE_INTERNAL') || die;
  60  
  61  $oauth_last_computed_signature = false;
  62  
  63  /* Generic exception class
  64   */
  65  class OAuthException extends \Exception {
  66      // pass
  67  }
  68  
  69  class OAuthConsumer {
  70      public $key;
  71      public $secret;
  72  
  73      function __construct($key, $secret, $callback_url = null) {
  74          $this->key = $key;
  75          $this->secret = $secret;
  76          $this->callback_url = $callback_url;
  77      }
  78  
  79      function __toString() {
  80          return "OAuthConsumer[key=$this->key,secret=$this->secret]";
  81      }
  82  }
  83  
  84  class OAuthToken {
  85      // access tokens and request tokens
  86      public $key;
  87      public $secret;
  88  
  89      /**
  90       * key = the token
  91       * secret = the token secret
  92       */
  93      function __construct($key, $secret) {
  94          $this->key = $key;
  95          $this->secret = $secret;
  96      }
  97  
  98      /**
  99       * generates the basic string serialization of a token that a server
 100       * would respond to request_token and access_token calls with
 101       */
 102      function to_string() {
 103          return "oauth_token=" .
 104          OAuthUtil::urlencode_rfc3986($this->key) .
 105          "&oauth_token_secret=" .
 106          OAuthUtil::urlencode_rfc3986($this->secret);
 107      }
 108  
 109      function __toString() {
 110          return $this->to_string();
 111      }
 112  }
 113  
 114  class OAuthSignatureMethod {
 115      public function check_signature(&$request, $consumer, $token, $signature) {
 116          $built = $this->build_signature($request, $consumer, $token);
 117          return $built == $signature;
 118      }
 119  }
 120  
 121  class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
 122      function get_name() {
 123          return "HMAC-SHA1";
 124      }
 125  
 126      public function build_signature($request, $consumer, $token) {
 127          global $oauth_last_computed_signature;
 128          $oauth_last_computed_signature = false;
 129  
 130          $base_string = $request->get_signature_base_string();
 131          $request->base_string = $base_string;
 132  
 133          $key_parts = array(
 134              $consumer->secret,
 135               ($token) ? $token->secret : ""
 136          );
 137  
 138          $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
 139          $key = implode('&', $key_parts);
 140  
 141          $computed_signature = base64_encode(hash_hmac('sha1', $base_string, $key, true));
 142          $oauth_last_computed_signature = $computed_signature;
 143          return $computed_signature;
 144      }
 145  
 146  }
 147  
 148  class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
 149      public function get_name() {
 150          return "PLAINTEXT";
 151      }
 152  
 153      public function build_signature($request, $consumer, $token) {
 154          $sig = array(
 155              OAuthUtil::urlencode_rfc3986($consumer->secret)
 156          );
 157  
 158          if ($token) {
 159              array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret));
 160          } else {
 161              array_push($sig, '');
 162          }
 163  
 164          $raw = implode("&", $sig);
 165          // for debug purposes
 166          $request->base_string = $raw;
 167  
 168          return OAuthUtil::urlencode_rfc3986($raw);
 169      }
 170  }
 171  
 172  class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
 173      public function get_name() {
 174          return "RSA-SHA1";
 175      }
 176  
 177      protected function fetch_public_cert(&$request) {
 178          // not implemented yet, ideas are:
 179          // (1) do a lookup in a table of trusted certs keyed off of consumer
 180          // (2) fetch via http using a url provided by the requester
 181          // (3) some sort of specific discovery code based on request
 182          //
 183          // either way should return a string representation of the certificate
 184          throw new OAuthException("fetch_public_cert not implemented");
 185      }
 186  
 187      protected function fetch_private_cert(&$request) {
 188          // not implemented yet, ideas are:
 189          // (1) do a lookup in a table of trusted certs keyed off of consumer
 190          //
 191          // either way should return a string representation of the certificate
 192          throw new OAuthException("fetch_private_cert not implemented");
 193      }
 194  
 195      public function build_signature(&$request, $consumer, $token) {
 196          $base_string = $request->get_signature_base_string();
 197          $request->base_string = $base_string;
 198  
 199          // Fetch the private key cert based on the request
 200          $cert = $this->fetch_private_cert($request);
 201  
 202          // Pull the private key ID from the certificate
 203          $privatekeyid = openssl_get_privatekey($cert);
 204  
 205          // Sign using the key
 206          $ok = openssl_sign($base_string, $signature, $privatekeyid);
 207  
 208          // Release the key resource
 209          openssl_free_key($privatekeyid);
 210  
 211          return base64_encode($signature);
 212      }
 213  
 214      public function check_signature(&$request, $consumer, $token, $signature) {
 215          $decoded_sig = base64_decode($signature);
 216  
 217          $base_string = $request->get_signature_base_string();
 218  
 219          // Fetch the public key cert based on the request
 220          $cert = $this->fetch_public_cert($request);
 221  
 222          // Pull the public key ID from the certificate
 223          $publickeyid = openssl_get_publickey($cert);
 224  
 225          // Check the computed signature against the one passed in the query
 226          $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
 227  
 228          // Release the key resource
 229          openssl_free_key($publickeyid);
 230  
 231          return $ok == 1;
 232      }
 233  }
 234  
 235  class OAuthRequest {
 236      private $parameters;
 237      private $http_method;
 238      private $http_url;
 239      // for debug purposes
 240      public $base_string;
 241      public static $version = '1.0';
 242      public static $POST_INPUT = 'php://input';
 243  
 244      function __construct($http_method, $http_url, $parameters = null) {
 245          @$parameters or $parameters = array();
 246          $this->parameters = $parameters;
 247          $this->http_method = $http_method;
 248          $this->http_url = $http_url;
 249      }
 250  
 251      /**
 252       * attempt to build up a request from what was passed to the server
 253       */
 254      public static function from_request($http_method = null, $http_url = null, $parameters = null) {
 255          $scheme = (!is_https()) ? 'http' : 'https';
 256          $port = "";
 257          if ($_SERVER['SERVER_PORT'] != "80" && $_SERVER['SERVER_PORT'] != "443" && strpos(':', $_SERVER['HTTP_HOST']) < 0) {
 258              $port = ':' . $_SERVER['SERVER_PORT'];
 259          }
 260          @$http_url or $http_url = $scheme .
 261          '://' . $_SERVER['HTTP_HOST'] .
 262          $port .
 263          $_SERVER['REQUEST_URI'];
 264          @$http_method or $http_method = $_SERVER['REQUEST_METHOD'];
 265  
 266          // We weren't handed any parameters, so let's find the ones relevant to
 267          // this request.
 268          // If you run XML-RPC or similar you should use this to provide your own
 269          // parsed parameter-list
 270          if (!$parameters) {
 271              // Find request headers
 272              $request_headers = OAuthUtil::get_headers();
 273  
 274              // Parse the query-string to find GET parameters
 275              $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);
 276  
 277              $ourpost = $_POST;
 278              // Add POST Parameters if they exist
 279              $parameters = array_merge($parameters, $ourpost);
 280  
 281              // We have a Authorization-header with OAuth data. Parse the header
 282              // and add those overriding any duplicates from GET or POST
 283              if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") {
 284                  $header_parameters = OAuthUtil::split_header($request_headers['Authorization']);
 285                  $parameters = array_merge($parameters, $header_parameters);
 286              }
 287  
 288          }
 289  
 290          return new OAuthRequest($http_method, $http_url, $parameters);
 291      }
 292  
 293      /**
 294       * pretty much a helper function to set up the request
 295       */
 296      public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters = null) {
 297          @$parameters or $parameters = array();
 298          $defaults = array(
 299              "oauth_version" => self::$version,
 300              "oauth_nonce" => self::generate_nonce(),
 301              "oauth_timestamp" => self::generate_timestamp(),
 302              "oauth_consumer_key" => $consumer->key
 303          );
 304          if ($token) {
 305              $defaults['oauth_token'] = $token->key;
 306          }
 307  
 308          $parameters = array_merge($defaults, $parameters);
 309  
 310          // Parse the query-string to find and add GET parameters
 311          $parts = parse_url($http_url);
 312          if (isset($parts['query'])) {
 313              $qparms = OAuthUtil::parse_parameters($parts['query']);
 314              $parameters = array_merge($qparms, $parameters);
 315          }
 316  
 317          return new OAuthRequest($http_method, $http_url, $parameters);
 318      }
 319  
 320      public function set_parameter($name, $value, $allow_duplicates = true) {
 321          if ($allow_duplicates && isset($this->parameters[$name])) {
 322              // We have already added parameter(s) with this name, so add to the list
 323              if (is_scalar($this->parameters[$name])) {
 324                  // This is the first duplicate, so transform scalar (string)
 325                  // into an array so we can add the duplicates
 326                  $this->parameters[$name] = array($this->parameters[$name]);
 327              }
 328  
 329              $this->parameters[$name][] = $value;
 330          } else {
 331              $this->parameters[$name] = $value;
 332          }
 333      }
 334  
 335      public function get_parameter($name) {
 336          return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
 337      }
 338  
 339      public function get_parameters() {
 340          return $this->parameters;
 341      }
 342  
 343      public function unset_parameter($name) {
 344          unset($this->parameters[$name]);
 345      }
 346  
 347      /**
 348       * The request parameters, sorted and concatenated into a normalized string.
 349       * @return string
 350       */
 351      public function get_signable_parameters() {
 352          // Grab all parameters
 353          $params = $this->parameters;
 354  
 355          // Remove oauth_signature if present
 356          // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
 357          if (isset($params['oauth_signature'])) {
 358              unset($params['oauth_signature']);
 359          }
 360  
 361          return OAuthUtil::build_http_query($params);
 362      }
 363  
 364      /**
 365       * Returns the base string of this request
 366       *
 367       * The base string defined as the method, the url
 368       * and the parameters (normalized), each urlencoded
 369       * and the concated with &.
 370       */
 371      public function get_signature_base_string() {
 372          $parts = array(
 373              $this->get_normalized_http_method(),
 374              $this->get_normalized_http_url(),
 375              $this->get_signable_parameters()
 376          );
 377  
 378          $parts = OAuthUtil::urlencode_rfc3986($parts);
 379  
 380          return implode('&', $parts);
 381      }
 382  
 383      /**
 384       * just uppercases the http method
 385       */
 386      public function get_normalized_http_method() {
 387          return strtoupper($this->http_method);
 388      }
 389  
 390      /**
 391       * parses the url and rebuilds it to be
 392       * scheme://host/path
 393       */
 394      public function get_normalized_http_url() {
 395          $parts = parse_url($this->http_url);
 396  
 397          $port = @$parts['port'];
 398          $scheme = $parts['scheme'];
 399          $host = $parts['host'];
 400          $path = @$parts['path'];
 401  
 402          $port or $port = ($scheme == 'https') ? '443' : '80';
 403  
 404          if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) {
 405              $host = "$host:$port";
 406          }
 407          return "$scheme://$host$path";
 408      }
 409  
 410      /**
 411       * builds a url usable for a GET request
 412       */
 413      public function to_url() {
 414          $post_data = $this->to_postdata();
 415          $out = $this->get_normalized_http_url();
 416          if ($post_data) {
 417              $out .= '?'.$post_data;
 418          }
 419          return $out;
 420      }
 421  
 422      /**
 423       * builds the data one would send in a POST request
 424       */
 425      public function to_postdata() {
 426          return OAuthUtil::build_http_query($this->parameters);
 427      }
 428  
 429      /**
 430       * builds the Authorization: header
 431       */
 432      public function to_header() {
 433          $out = 'Authorization: OAuth realm=""';
 434          $total = array();
 435          foreach ($this->parameters as $k => $v) {
 436              if (substr($k, 0, 5) != "oauth") {
 437                  continue;
 438              }
 439              if (is_array($v)) {
 440                  throw new OAuthException('Arrays not supported in headers');
 441              }
 442              $out .= ',' .
 443              OAuthUtil::urlencode_rfc3986($k) .
 444              '="' .
 445              OAuthUtil::urlencode_rfc3986($v) .
 446              '"';
 447          }
 448          return $out;
 449      }
 450  
 451      public function __toString() {
 452          return $this->to_url();
 453      }
 454  
 455      public function sign_request($signature_method, $consumer, $token) {
 456          $this->set_parameter("oauth_signature_method", $signature_method->get_name(), false);
 457          $signature = $this->build_signature($signature_method, $consumer, $token);
 458          $this->set_parameter("oauth_signature", $signature, false);
 459      }
 460  
 461      public function build_signature($signature_method, $consumer, $token) {
 462          $signature = $signature_method->build_signature($this, $consumer, $token);
 463          return $signature;
 464      }
 465  
 466      /**
 467       * util function: current timestamp
 468       */
 469      private static function generate_timestamp() {
 470          return time();
 471      }
 472  
 473      /**
 474       * util function: current nonce
 475       */
 476      private static function generate_nonce() {
 477          $mt = microtime();
 478          $rand = mt_rand();
 479  
 480          return md5($mt.$rand); // md5s look nicer than numbers
 481      }
 482  }
 483  
 484  class OAuthServer {
 485      protected $timestamp_threshold = 300; // in seconds, five minutes
 486      protected $version = 1.0; // hi blaine
 487      protected $signature_methods = array();
 488      protected $data_store;
 489  
 490      function __construct($data_store) {
 491          $this->data_store = $data_store;
 492      }
 493  
 494      public function add_signature_method($signature_method) {
 495          $this->signature_methods[$signature_method->get_name()] = $signature_method;
 496      }
 497  
 498      // high level functions
 499  
 500      /**
 501       * process a request_token request
 502       * returns the request token on success
 503       */
 504      public function fetch_request_token(&$request) {
 505          $this->get_version($request);
 506  
 507          $consumer = $this->get_consumer($request);
 508  
 509          // no token required for the initial token request
 510          $token = null;
 511  
 512          $this->check_signature($request, $consumer, $token);
 513  
 514          $new_token = $this->data_store->new_request_token($consumer);
 515  
 516          return $new_token;
 517      }
 518  
 519      /**
 520       * process an access_token request
 521       * returns the access token on success
 522       */
 523      public function fetch_access_token(&$request) {
 524          $this->get_version($request);
 525  
 526          $consumer = $this->get_consumer($request);
 527  
 528          // requires authorized request token
 529          $token = $this->get_token($request, $consumer, "request");
 530  
 531          $this->check_signature($request, $consumer, $token);
 532  
 533          $new_token = $this->data_store->new_access_token($token, $consumer);
 534  
 535          return $new_token;
 536      }
 537  
 538      /**
 539       * verify an api call, checks all the parameters
 540       */
 541      public function verify_request(&$request) {
 542          global $oauth_last_computed_signature;
 543          $oauth_last_computed_signature = false;
 544          $this->get_version($request);
 545          $consumer = $this->get_consumer($request);
 546          $token = $this->get_token($request, $consumer, "access");
 547          $this->check_signature($request, $consumer, $token);
 548          return array(
 549              $consumer,
 550              $token
 551          );
 552      }
 553  
 554      // Internals from here
 555      /**
 556       * version 1
 557       */
 558      private function get_version(&$request) {
 559          $version = $request->get_parameter("oauth_version");
 560          if (!$version) {
 561              $version = 1.0;
 562          }
 563          if ($version && $version != $this->version) {
 564              throw new OAuthException("OAuth version '$version' not supported");
 565          }
 566          return $version;
 567      }
 568  
 569      /**
 570       * figure out the signature with some defaults
 571       */
 572      private function get_signature_method(&$request) {
 573          $signature_method = @ $request->get_parameter("oauth_signature_method");
 574          if (!$signature_method) {
 575              $signature_method = "PLAINTEXT";
 576          }
 577          if (!in_array($signature_method, array_keys($this->signature_methods))) {
 578              throw new OAuthException("Signature method '$signature_method' not supported " .
 579              "try one of the following: " .
 580              implode(", ", array_keys($this->signature_methods)));
 581          }
 582          return $this->signature_methods[$signature_method];
 583      }
 584  
 585      /**
 586       * try to find the consumer for the provided request's consumer key
 587       */
 588      private function get_consumer(&$request) {
 589          $consumer_key = @ $request->get_parameter("oauth_consumer_key");
 590          if (!$consumer_key) {
 591              throw new OAuthException("Invalid consumer key");
 592          }
 593  
 594          $consumer = $this->data_store->lookup_consumer($consumer_key);
 595          if (!$consumer) {
 596              throw new OAuthException("Invalid consumer");
 597          }
 598  
 599          return $consumer;
 600      }
 601  
 602      /**
 603       * try to find the token for the provided request's token key
 604       */
 605      private function get_token(&$request, $consumer, $token_type = "access") {
 606          $token_field = @ $request->get_parameter('oauth_token');
 607          if (!$token_field) {
 608              return false;
 609          }
 610          $token = $this->data_store->lookup_token($consumer, $token_type, $token_field);
 611          if (!$token) {
 612              throw new OAuthException("Invalid $token_type token: $token_field");
 613          }
 614          return $token;
 615      }
 616  
 617      /**
 618       * all-in-one function to check the signature on a request
 619       * should guess the signature method appropriately
 620       */
 621      private function check_signature(&$request, $consumer, $token) {
 622          // this should probably be in a different method
 623          global $oauth_last_computed_signature;
 624          $oauth_last_computed_signature = false;
 625  
 626          $timestamp = @ $request->get_parameter('oauth_timestamp');
 627          $nonce = @ $request->get_parameter('oauth_nonce');
 628  
 629          $this->check_timestamp($timestamp);
 630          $this->check_nonce($consumer, $token, $nonce, $timestamp);
 631  
 632          $signature_method = $this->get_signature_method($request);
 633  
 634          $signature = $request->get_parameter('oauth_signature');
 635          $valid_sig = $signature_method->check_signature($request, $consumer, $token, $signature);
 636  
 637          if (!$valid_sig) {
 638              $ex_text = "Invalid signature";
 639              if ($oauth_last_computed_signature) {
 640                  $ex_text = $ex_text . " ours= $oauth_last_computed_signature yours=$signature";
 641              }
 642              throw new OAuthException($ex_text);
 643          }
 644      }
 645  
 646      /**
 647       * check that the timestamp is new enough
 648       */
 649      private function check_timestamp($timestamp) {
 650          // verify that timestamp is recentish
 651          $now = time();
 652          if ($now - $timestamp > $this->timestamp_threshold) {
 653              throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
 654          }
 655      }
 656  
 657      /**
 658       * check that the nonce is not repeated
 659       */
 660      private function check_nonce($consumer, $token, $nonce, $timestamp) {
 661          // verify that the nonce is uniqueish
 662          $found = $this->data_store->lookup_nonce($consumer, $token, $nonce, $timestamp);
 663          if ($found) {
 664              throw new OAuthException("Nonce already used: $nonce");
 665          }
 666      }
 667  
 668  }
 669  
 670  class OAuthDataStore {
 671      function lookup_consumer($consumer_key) {
 672          // implement me
 673      }
 674  
 675      function lookup_token($consumer, $token_type, $token) {
 676          // implement me
 677      }
 678  
 679      function lookup_nonce($consumer, $token, $nonce, $timestamp) {
 680          // implement me
 681      }
 682  
 683      function new_request_token($consumer) {
 684          // return a new token attached to this consumer
 685      }
 686  
 687      function new_access_token($token, $consumer) {
 688          // return a new access token attached to this consumer
 689          // for the user associated with this token if the request token
 690          // is authorized
 691          // should also invalidate the request token
 692      }
 693  
 694  }
 695  
 696  class OAuthUtil {
 697      public static function urlencode_rfc3986($input) {
 698          if (is_array($input)) {
 699              return array_map(array(
 700                  'moodle\mod\lti\OAuthUtil',
 701                  'urlencode_rfc3986'
 702              ), $input);
 703          } else {
 704              if (is_scalar($input)) {
 705                  return str_replace('+', ' ', str_replace('%7E', '~', rawurlencode($input)));
 706              } else {
 707                  return '';
 708              }
 709          }
 710      }
 711  
 712      // This decode function isn't taking into consideration the above
 713      // modifications to the encoding process. However, this method doesn't
 714      // seem to be used anywhere so leaving it as is.
 715      public static function urldecode_rfc3986($string) {
 716          return urldecode($string);
 717      }
 718  
 719      // Utility function for turning the Authorization: header into
 720      // parameters, has to do some unescaping
 721      // Can filter out any non-oauth parameters if needed (default behaviour)
 722      public static function split_header($header, $only_allow_oauth_parameters = true) {
 723          $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/';
 724          $offset = 0;
 725          $params = array();
 726          while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) {
 727              $match = $matches[0];
 728              $header_name = $matches[2][0];
 729              $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0];
 730              if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) {
 731                  $params[$header_name] = self::urldecode_rfc3986($header_content);
 732              }
 733              $offset = $match[1] + strlen($match[0]);
 734          }
 735  
 736          if (isset($params['realm'])) {
 737              unset($params['realm']);
 738          }
 739  
 740          return $params;
 741      }
 742  
 743      // helper to try to sort out headers for people who aren't running apache
 744      public static function get_headers() {
 745          if (function_exists('apache_request_headers')) {
 746              // we need this to get the actual Authorization: header
 747              // because apache tends to tell us it doesn't exist
 748              $in = apache_request_headers();
 749              $out = array();
 750              foreach ($in as $key => $value) {
 751                  $key = str_replace(" ", "-", ucwords(strtolower(str_replace("-", " ", $key))));
 752                  $out[$key] = $value;
 753              }
 754              return $out;
 755          }
 756          // otherwise we don't have apache and are just going to have to hope
 757          // that $_SERVER actually contains what we need
 758          $out = array();
 759          foreach ($_SERVER as $key => $value) {
 760              if (substr($key, 0, 5) == "HTTP_") {
 761                  // this is chaos, basically it is just there to capitalize the first
 762                  // letter of every word that is not an initial HTTP and strip HTTP
 763                  // code from przemek
 764                  $key = str_replace(" ", "-", ucwords(strtolower(str_replace("_", " ", substr($key, 5)))));
 765                  $out[$key] = $value;
 766              }
 767          }
 768          return $out;
 769      }
 770  
 771      // This function takes a input like a=b&a=c&d=e and returns the parsed
 772      // parameters like this
 773      // array('a' => array('b','c'), 'd' => 'e')
 774      public static function parse_parameters($input) {
 775          if (!isset($input) || !$input) {
 776              return array();
 777          }
 778  
 779          $pairs = explode('&', $input);
 780  
 781          $parsed_parameters = array();
 782          foreach ($pairs as $pair) {
 783              $split = explode('=', $pair, 2);
 784              $parameter = self::urldecode_rfc3986($split[0]);
 785              $value = isset($split[1]) ? self::urldecode_rfc3986($split[1]) : '';
 786  
 787              if (isset($parsed_parameters[$parameter])) {
 788                  // We have already recieved parameter(s) with this name, so add to the list
 789                  // of parameters with this name
 790  
 791                  if (is_scalar($parsed_parameters[$parameter])) {
 792                      // This is the first duplicate, so transform scalar (string) into an array
 793                      // so we can add the duplicates
 794                      $parsed_parameters[$parameter] = array(
 795                          $parsed_parameters[$parameter]
 796                      );
 797                  }
 798  
 799                  $parsed_parameters[$parameter][] = $value;
 800              } else {
 801                  $parsed_parameters[$parameter] = $value;
 802              }
 803          }
 804          return $parsed_parameters;
 805      }
 806  
 807      public static function build_http_query($params) {
 808          if (!$params) {
 809              return '';
 810          }
 811  
 812          // Urlencode both keys and values
 813          $keys = self::urlencode_rfc3986(array_keys($params));
 814          $values = self::urlencode_rfc3986(array_values($params));
 815          $params = array_combine($keys, $values);
 816  
 817          // Parameters are sorted by name, using lexicographical byte value ordering.
 818          // Ref: Spec: 9.1.1 (1)
 819          uksort($params, 'strcmp');
 820  
 821          $pairs = array();
 822          foreach ($params as $parameter => $value) {
 823              if (is_array($value)) {
 824                  // If two or more parameters share the same name, they are sorted by their value
 825                  // Ref: Spec: 9.1.1 (1)
 826                  natsort($value);
 827                  foreach ($value as $duplicate_value) {
 828                      $pairs[] = $parameter . '=' . $duplicate_value;
 829                  }
 830              } else {
 831                  $pairs[] = $parameter . '=' . $value;
 832              }
 833          }
 834          // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
 835          // Each name-value pair is separated by an '&' character (ASCII code 38)
 836          return implode('&', $pairs);
 837      }
 838  }