Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

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