OAuth.php 26.4 KB
Newer Older
1 2 3 4 5
<?php
// vim: foldmethod=marker

/* Generic exception class
 */
6
class OAuthException extends Exception {
7
  // pass
8
}
9

10
class OAuthConsumer {
11 12 13
  public $key;
  public $secret;

14
  function __construct($key, $secret, $callback_url=NULL) {
15 16 17
    $this->key = $key;
    $this->secret = $secret;
    $this->callback_url = $callback_url;
18
  }
Evan Prodromou's avatar
Evan Prodromou committed
19

20
  function __toString() {
Evan Prodromou's avatar
Evan Prodromou committed
21
    return "OAuthConsumer[key=$this->key,secret=$this->secret]";
22 23
  }
}
24

25
class OAuthToken {
26 27 28 29 30 31 32 33
  // access tokens and request tokens
  public $key;
  public $secret;

  /**
   * key = the token
   * secret = the token secret
   */
34
  function __construct($key, $secret) {
35 36
    $this->key = $key;
    $this->secret = $secret;
37
  }
38 39 40 41 42

  /**
   * generates the basic string serialization of a token that a server
   * would respond to request_token and access_token calls with
   */
43 44 45 46 47 48
  function to_string() {
    return "oauth_token=" .
           OAuthUtil::urlencode_rfc3986($this->key) .
           "&oauth_token_secret=" .
           OAuthUtil::urlencode_rfc3986($this->secret);
  }
49

50
  function __toString() {
51
    return $this->to_string();
52 53
  }
}
54

55 56 57 58 59 60 61 62 63 64
/**
 * A class for implementing a Signature Method
 * See section 9 ("Signing Requests") in the spec
 */
abstract class OAuthSignatureMethod {
  /**
   * Needs to return the name of the Signature Method (ie HMAC-SHA1)
   * @return string
   */
  abstract public function get_name();
65

66 67 68 69 70 71 72 73 74 75 76
  /**
   * Build up the signature
   * NOTE: The output of this function MUST NOT be urlencoded.
   * the encoding is handled in OAuthRequest when the final
   * request is serialized
   * @param OAuthRequest $request
   * @param OAuthConsumer $consumer
   * @param OAuthToken $token
   * @return string
   */
  abstract public function build_signature($request, $consumer, $token);
77

78 79 80 81 82 83 84 85 86 87 88
  /**
   * Verifies that a given signature is correct
   * @param OAuthRequest $request
   * @param OAuthConsumer $consumer
   * @param OAuthToken $token
   * @param string $signature
   * @return bool
   */
  public function check_signature($request, $consumer, $token, $signature) {
    $built = $this->build_signature($request, $consumer, $token);
    return $built == $signature;
89
  }
90 91 92 93 94 95 96 97 98 99 100
}

/**
 * The HMAC-SHA1 signature method uses the HMAC-SHA1 signature algorithm as defined in [RFC2104] 
 * where the Signature Base String is the text and the key is the concatenated values (each first 
 * encoded per Parameter Encoding) of the Consumer Secret and Token Secret, separated by an '&' 
 * character (ASCII code 38) even if empty.
 *   - Chapter 9.2 ("HMAC-SHA1")
 */
class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {
  function get_name() {
101
    return "HMAC-SHA1";
102
  }
103

104
  public function build_signature($request, $consumer, $token) {
105 106 107 108 109 110 111 112
    $base_string = $request->get_signature_base_string();
    $request->base_string = $base_string;

    $key_parts = array(
      $consumer->secret,
      ($token) ? $token->secret : ""
    );

Evan Prodromou's avatar
Evan Prodromou committed
113
    $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
114 115
    $key = implode('&', $key_parts);

116 117 118
    return base64_encode(hash_hmac('sha1', $base_string, $key, true));
  }
}
119

120 121 122 123 124 125 126
/**
 * The PLAINTEXT method does not provide any security protection and SHOULD only be used 
 * over a secure channel such as HTTPS. It does not use the Signature Base String.
 *   - Chapter 9.4 ("PLAINTEXT")
 */
class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {
  public function get_name() {
127
    return "PLAINTEXT";
128
  }
129

130 131 132 133 134 135 136 137 138 139 140 141 142
  /**
   * oauth_signature is set to the concatenated encoded values of the Consumer Secret and 
   * Token Secret, separated by a '&' character (ASCII code 38), even if either secret is 
   * empty. The result MUST be encoded again.
   *   - Chapter 9.4.1 ("Generating Signatures")
   *
   * Please note that the second encoding MUST NOT happen in the SignatureMethod, as
   * OAuthRequest handles this!
   */
  public function build_signature($request, $consumer, $token) {
    $key_parts = array(
      $consumer->secret,
      ($token) ? $token->secret : ""
143 144
    );

145 146 147
    $key_parts = OAuthUtil::urlencode_rfc3986($key_parts);
    $key = implode('&', $key_parts);
    $request->base_string = $key;
148

149 150 151 152 153 154 155 156 157 158 159 160 161 162
    return $key;
  }
}

/**
 * The RSA-SHA1 signature method uses the RSASSA-PKCS1-v1_5 signature algorithm as defined in 
 * [RFC3447] section 8.2 (more simply known as PKCS#1), using SHA-1 as the hash function for 
 * EMSA-PKCS1-v1_5. It is assumed that the Consumer has provided its RSA public key in a 
 * verified way to the Service Provider, in a manner which is beyond the scope of this 
 * specification.
 *   - Chapter 9.3 ("RSA-SHA1")
 */
abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {
  public function get_name() {
163
    return "RSA-SHA1";
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180
  }

  // Up to the SP to implement this lookup of keys. Possible ideas are:
  // (1) do a lookup in a table of trusted certs keyed off of consumer
  // (2) fetch via http using a url provided by the requester
  // (3) some sort of specific discovery code based on request
  //
  // Either way should return a string representation of the certificate
  protected abstract function fetch_public_cert(&$request);

  // Up to the SP to implement this lookup of keys. Possible ideas are:
  // (1) do a lookup in a table of trusted certs keyed off of consumer
  //
  // Either way should return a string representation of the certificate
  protected abstract function fetch_private_cert(&$request);

  public function build_signature($request, $consumer, $token) {
181 182
    $base_string = $request->get_signature_base_string();
    $request->base_string = $base_string;
183

184 185 186 187 188 189 190
    // Fetch the private key cert based on the request
    $cert = $this->fetch_private_cert($request);

    // Pull the private key ID from the certificate
    $privatekeyid = openssl_get_privatekey($cert);

    // Sign using the key
191
    $ok = openssl_sign($base_string, $signature, $privatekeyid);
192 193 194

    // Release the key resource
    openssl_free_key($privatekeyid);
195

196
    return base64_encode($signature);
197
  }
198

199
  public function check_signature($request, $consumer, $token, $signature) {
200 201 202
    $decoded_sig = base64_decode($signature);

    $base_string = $request->get_signature_base_string();
203

204 205 206 207 208 209 210
    // Fetch the public key cert based on the request
    $cert = $this->fetch_public_cert($request);

    // Pull the public key ID from the certificate
    $publickeyid = openssl_get_publickey($cert);

    // Check the computed signature against the one passed in the query
211
    $ok = openssl_verify($base_string, $decoded_sig, $publickeyid);
212 213 214

    // Release the key resource
    openssl_free_key($publickeyid);
215

216
    return $ok == 1;
217 218
  }
}
219

220 221 222 223
class OAuthRequest {
  protected $parameters;
  protected $http_method;
  protected $http_url;
224 225 226
  // for debug purposes
  public $base_string;
  public static $version = '1.0';
227
  public static $POST_INPUT = 'php://input';
228

229 230 231
  function __construct($http_method, $http_url, $parameters=NULL) {
    $parameters = ($parameters) ? $parameters : array();
    $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters);
232 233 234
    $this->parameters = $parameters;
    $this->http_method = $http_method;
    $this->http_url = $http_url;
235
  }
236 237 238 239 240


  /**
   * attempt to build up a request from what was passed to the server
   */
241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274
  public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {
    $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on")
              ? 'http'
              : 'https';
    $http_url = ($http_url) ? $http_url : $scheme .
                              '://' . $_SERVER['HTTP_HOST'] .
                              ':' .
                              $_SERVER['SERVER_PORT'] .
                              $_SERVER['REQUEST_URI'];
    $http_method = ($http_method) ? $http_method : $_SERVER['REQUEST_METHOD'];

    // We weren't handed any parameters, so let's find the ones relevant to
    // this request.
    // If you run XML-RPC or similar you should use this to provide your own
    // parsed parameter-list
    if (!$parameters) {
      // Find request headers
      $request_headers = OAuthUtil::get_headers();

      // Parse the query-string to find GET parameters
      $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']);

      // It's a POST request of the proper content-type, so parse POST
      // parameters and add those overriding any duplicates from GET
      if ($http_method == "POST"
          &&  isset($request_headers['Content-Type'])
          && strstr($request_headers['Content-Type'],
                     'application/x-www-form-urlencoded')
          ) {
        $post_data = OAuthUtil::parse_parameters(
          file_get_contents(self::$POST_INPUT)
        );
        $parameters = array_merge($parameters, $post_data);
      }
275

276 277 278 279 280 281 282
      // We have a Authorization-header with OAuth data. Parse the header
      // and add those overriding any duplicates from GET or POST
      if (isset($request_headers['Authorization']) && substr($request_headers['Authorization'], 0, 6) == 'OAuth ') {
        $header_parameters = OAuthUtil::split_header(
          $request_headers['Authorization']
        );
        $parameters = array_merge($parameters, $header_parameters);
Evan Prodromou's avatar
Evan Prodromou committed
283 284
      }

285
    }
Evan Prodromou's avatar
Evan Prodromou committed
286

287 288
    return new OAuthRequest($http_method, $http_url, $parameters);
  }
289 290 291 292

  /**
   * pretty much a helper function to set up the request
   */
293 294
  public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {
    $parameters = ($parameters) ?  $parameters : array();
295 296 297 298
    $defaults = array("oauth_version" => OAuthRequest::$version,
                      "oauth_nonce" => OAuthRequest::generate_nonce(),
                      "oauth_timestamp" => OAuthRequest::generate_timestamp(),
                      "oauth_consumer_key" => $consumer->key);
299 300 301
    if ($token)
      $defaults['oauth_token'] = $token->key;

302 303 304
    $parameters = array_merge($defaults, $parameters);

    return new OAuthRequest($http_method, $http_url, $parameters);
305
  }
306

307 308 309 310 311 312 313 314
  public function set_parameter($name, $value, $allow_duplicates = true) {
    if ($allow_duplicates && isset($this->parameters[$name])) {
      // We have already added parameter(s) with this name, so add to the list
      if (is_scalar($this->parameters[$name])) {
        // This is the first duplicate, so transform scalar (string)
        // into an array so we can add the duplicates
        $this->parameters[$name] = array($this->parameters[$name]);
      }
315

316 317 318 319 320 321 322
      $this->parameters[$name][] = $value;
    } else {
      $this->parameters[$name] = $value;
    }
  }

  public function get_parameter($name) {
Evan Prodromou's avatar
Evan Prodromou committed
323
    return isset($this->parameters[$name]) ? $this->parameters[$name] : null;
324
  }
325

326
  public function get_parameters() {
327
    return $this->parameters;
328 329 330 331 332
  }

  public function unset_parameter($name) {
    unset($this->parameters[$name]);
  }
333 334

  /**
335
   * The request parameters, sorted and concatenated into a normalized string.
336 337
   * @return string
   */
338
  public function get_signable_parameters() {
339 340
    // Grab all parameters
    $params = $this->parameters;
341

342
    // Remove oauth_signature if present
343
    // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.")
344 345 346 347
    if (isset($params['oauth_signature'])) {
      unset($params['oauth_signature']);
    }

348 349
    return OAuthUtil::build_http_query($params);
  }
350 351 352 353 354 355 356 357

  /**
   * Returns the base string of this request
   *
   * The base string defined as the method, the url
   * and the parameters (normalized), each urlencoded
   * and the concated with &.
   */
358
  public function get_signature_base_string() {
359 360 361 362 363 364
    $parts = array(
      $this->get_normalized_http_method(),
      $this->get_normalized_http_url(),
      $this->get_signable_parameters()
    );

Evan Prodromou's avatar
Evan Prodromou committed
365
    $parts = OAuthUtil::urlencode_rfc3986($parts);
366 367

    return implode('&', $parts);
368
  }
369 370 371 372

  /**
   * just uppercases the http method
   */
373
  public function get_normalized_http_method() {
374
    return strtoupper($this->http_method);
375
  }
376 377 378 379 380

  /**
   * parses the url and rebuilds it to be
   * scheme://host/path
   */
381
  public function get_normalized_http_url() {
382 383
    $parts = parse_url($this->http_url);

384 385 386 387
    $scheme = (isset($parts['scheme'])) ? $parts['scheme'] : 'http';
    $port = (isset($parts['port'])) ? $parts['port'] : (($scheme == 'https') ? '443' : '80');
    $host = (isset($parts['host'])) ? $parts['host'] : '';
    $path = (isset($parts['path'])) ? $parts['path'] : '';
388 389 390 391 392 393

    if (($scheme == 'https' && $port != '443')
        || ($scheme == 'http' && $port != '80')) {
      $host = "$host:$port";
    }
    return "$scheme://$host$path";
394
  }
395 396 397 398

  /**
   * builds a url usable for a GET request
   */
399 400 401 402 403 404
  public function to_url() {
    $post_data = $this->to_postdata();
    $out = $this->get_normalized_http_url();
    if ($post_data) {
      $out .= '?'.$post_data;
    }
405
    return $out;
406
  }
407 408 409 410

  /**
   * builds the data one would send in a POST request
   */
411 412 413
  public function to_postdata() {
    return OAuthUtil::build_http_query($this->parameters);
  }
414 415 416 417

  /**
   * builds the Authorization: header
   */
418 419 420 421 422 423 424 425
  public function to_header($realm=null) {
    $first = true;
	if($realm) {
      $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"';
      $first = false;
    } else
      $out = 'Authorization: OAuth';

426 427 428
    $total = array();
    foreach ($this->parameters as $k => $v) {
      if (substr($k, 0, 5) != "oauth") continue;
429 430 431 432 433 434 435 436 437
      if (is_array($v)) {
        throw new OAuthException('Arrays not supported in headers');
      }
      $out .= ($first) ? ' ' : ',';
      $out .= OAuthUtil::urlencode_rfc3986($k) .
              '="' .
              OAuthUtil::urlencode_rfc3986($v) .
              '"';
      $first = false;
438 439
    }
    return $out;
440
  }
441

442
  public function __toString() {
443
    return $this->to_url();
444
  }
445 446


447 448 449 450 451 452
  public function sign_request($signature_method, $consumer, $token) {
    $this->set_parameter(
      "oauth_signature_method",
      $signature_method->get_name(),
      false
    );
453
    $signature = $this->build_signature($signature_method, $consumer, $token);
454 455
    $this->set_parameter("oauth_signature", $signature, false);
  }
456

457
  public function build_signature($signature_method, $consumer, $token) {
458 459
    $signature = $signature_method->build_signature($this, $consumer, $token);
    return $signature;
460
  }
461 462 463 464

  /**
   * util function: current timestamp
   */
465
  private static function generate_timestamp() {
466
    return time();
467
  }
468 469 470 471

  /**
   * util function: current nonce
   */
472
  private static function generate_nonce() {
473 474 475 476
    $mt = microtime();
    $rand = mt_rand();

    return md5($mt . $rand); // md5s look nicer than numbers
477 478
  }
}
479

480
class OAuthServer {
481
  protected $timestamp_threshold = 300; // in seconds, five minutes
482
  protected $version = '1.0';             // hi blaine
483 484 485 486
  protected $signature_methods = array();

  protected $data_store;

487
  function __construct($data_store) {
488
    $this->data_store = $data_store;
489 490 491 492 493 494
  }

  public function add_signature_method($signature_method) {
    $this->signature_methods[$signature_method->get_name()] =
      $signature_method;
  }
495 496 497 498 499 500 501

  // high level functions

  /**
   * process a request_token request
   * returns the request token on success
   */
502
  public function fetch_request_token(&$request) {
503 504 505 506 507 508 509 510 511
    $this->get_version($request);

    $consumer = $this->get_consumer($request);

    // no token required for the initial token request
    $token = NULL;

    $this->check_signature($request, $consumer, $token);

512 513 514
    // Rev A change
    $callback = $request->get_parameter('oauth_callback');
    $new_token = $this->data_store->new_request_token($consumer, $callback);
515 516

    return $new_token;
517
  }
518 519 520 521 522

  /**
   * process an access_token request
   * returns the access token on success
   */
523
  public function fetch_access_token(&$request) {
524 525 526 527 528 529 530 531 532
    $this->get_version($request);

    $consumer = $this->get_consumer($request);

    // requires authorized request token
    $token = $this->get_token($request, $consumer, "request");

    $this->check_signature($request, $consumer, $token);

533 534 535
    // Rev A change
    $verifier = $request->get_parameter('oauth_verifier');
    $new_token = $this->data_store->new_access_token($token, $consumer, $verifier);
536 537

    return $new_token;
538
  }
539 540 541 542

  /**
   * verify an api call, checks all the parameters
   */
543
  public function verify_request(&$request) {
544 545 546 547 548
    $this->get_version($request);
    $consumer = $this->get_consumer($request);
    $token = $this->get_token($request, $consumer, "access");
    $this->check_signature($request, $consumer, $token);
    return array($consumer, $token);
549
  }
550 551 552 553 554

  // Internals from here
  /**
   * version 1
   */
555
  private function get_version(&$request) {
556 557
    $version = $request->get_parameter("oauth_version");
    if (!$version) {
558 559 560
      // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present. 
      // Chapter 7.0 ("Accessing Protected Ressources")
      $version = '1.0';
561
    }
562
    if ($version !== $this->version) {
563 564 565
      throw new OAuthException("OAuth version '$version' not supported");
    }
    return $version;
566
  }
567 568 569 570

  /**
   * figure out the signature with some defaults
   */
571 572 573 574 575
  private function get_signature_method($request) {
    $signature_method = $request instanceof OAuthRequest 
        ? $request->get_parameter("oauth_signature_method")
        : NULL;

576
    if (!$signature_method) {
577 578 579
      // According to chapter 7 ("Accessing Protected Ressources") the signature-method
      // parameter is required, and we can't just fallback to PLAINTEXT
      throw new OAuthException('No signature method parameter. This parameter is required');
580
    }
581 582

    if (!in_array($signature_method,
583 584
                  array_keys($this->signature_methods))) {
      throw new OAuthException(
585 586 587 588
        "Signature method '$signature_method' not supported " .
        "try one of the following: " .
        implode(", ", array_keys($this->signature_methods))
      );
589 590
    }
    return $this->signature_methods[$signature_method];
591
  }
592 593 594 595

  /**
   * try to find the consumer for the provided request's consumer key
   */
596 597 598 599 600
  private function get_consumer($request) {
    $consumer_key = $request instanceof OAuthRequest 
        ? $request->get_parameter("oauth_consumer_key")
        : NULL;

601 602 603 604 605 606 607 608 609 610
    if (!$consumer_key) {
      throw new OAuthException("Invalid consumer key");
    }

    $consumer = $this->data_store->lookup_consumer($consumer_key);
    if (!$consumer) {
      throw new OAuthException("Invalid consumer");
    }

    return $consumer;
611
  }
612 613 614 615

  /**
   * try to find the token for the provided request's token key
   */
616 617 618 619 620
  private function get_token($request, $consumer, $token_type="access") {
    $token_field = $request instanceof OAuthRequest
         ? $request->get_parameter('oauth_token')
         : NULL;

621 622 623 624 625 626 627
    $token = $this->data_store->lookup_token(
      $consumer, $token_type, $token_field
    );
    if (!$token) {
      throw new OAuthException("Invalid $token_type token: $token_field");
    }
    return $token;
628
  }
629 630 631 632 633

  /**
   * all-in-one function to check the signature on a request
   * should guess the signature method appropriately
   */
634
  private function check_signature($request, $consumer, $token) {
635
    // this should probably be in a different method
636 637 638 639 640 641
    $timestamp = $request instanceof OAuthRequest
        ? $request->get_parameter('oauth_timestamp')
        : NULL;
    $nonce = $request instanceof OAuthRequest
        ? $request->get_parameter('oauth_nonce')
        : NULL;
642 643 644 645 646 647

    $this->check_timestamp($timestamp);
    $this->check_nonce($consumer, $token, $nonce, $timestamp);

    $signature_method = $this->get_signature_method($request);

648
    $signature = $request->get_parameter('oauth_signature');
649
    $valid_sig = $signature_method->check_signature(
650 651 652
      $request,
      $consumer,
      $token,
653 654 655 656 657 658
      $signature
    );

    if (!$valid_sig) {
      throw new OAuthException("Invalid signature");
    }
659
  }
660 661 662 663

  /**
   * check that the timestamp is new enough
   */
664 665 666 667 668 669
  private function check_timestamp($timestamp) {
    if( ! $timestamp )
      throw new OAuthException(
        'Missing timestamp parameter. The parameter is required'
      );
    
670 671
    // verify that timestamp is recentish
    $now = time();
672 673 674 675
    if (abs($now - $timestamp) > $this->timestamp_threshold) {
      throw new OAuthException(
        "Expired timestamp, yours $timestamp, ours $now"
      );
676
    }
677
  }
678 679 680 681

  /**
   * check that the nonce is not repeated
   */
682 683 684 685 686 687
  private function check_nonce($consumer, $token, $nonce, $timestamp) {
    if( ! $nonce )
      throw new OAuthException(
        'Missing nonce parameter. The parameter is required'
      );

688
    // verify that the nonce is uniqueish
689 690 691 692 693 694
    $found = $this->data_store->lookup_nonce(
      $consumer,
      $token,
      $nonce,
      $timestamp
    );
695 696 697
    if ($found) {
      throw new OAuthException("Nonce already used: $nonce");
    }
698
  }
699

700
}
701

702 703
class OAuthDataStore {
  function lookup_consumer($consumer_key) {
704
    // implement me
705
  }
706

707
  function lookup_token($consumer, $token_type, $token) {
708
    // implement me
709
  }
710

711
  function lookup_nonce($consumer, $token, $nonce, $timestamp) {
712
    // implement me
713
  }
714

715
  function new_request_token($consumer, $callback = null) {
716
    // return a new token attached to this consumer
717
  }
718

719
  function new_access_token($token, $consumer, $verifier = null) {
720 721 722 723
    // return a new access token attached to this consumer
    // for the user associated with this token if the request token
    // is authorized
    // should also invalidate the request token
724
  }
725

726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741
}

class OAuthUtil {
  public static function urlencode_rfc3986($input) {
  if (is_array($input)) {
    return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input);
  } else if (is_scalar($input)) {
    return str_replace(
      '+',
      ' ',
      str_replace('%7E', '~', rawurlencode($input))
    );
  } else {
    return '';
  }
}
742 743


744 745 746 747 748 749
  // This decode function isn't taking into consideration the above
  // modifications to the encoding process. However, this method doesn't
  // seem to be used anywhere so leaving it as is.
  public static function urldecode_rfc3986($string) {
    return urldecode($string);
  }
750

751 752 753 754 755 756 757 758 759 760 761 762 763 764
  // Utility function for turning the Authorization: header into
  // parameters, has to do some unescaping
  // Can filter out any non-oauth parameters if needed (default behaviour)
  // May 28th, 2010 - method updated to tjerk.meesters for a speed improvement.
  //                  see http://code.google.com/p/oauth/issues/detail?id=163
  public static function split_header($header, $only_allow_oauth_parameters = true) {
    $params = array();
    if (preg_match_all('/('.($only_allow_oauth_parameters ? 'oauth_' : '').'[a-z_-]*)=(:?"([^"]*)"|([^,]*))/', $header, $matches)) {
      foreach ($matches[1] as $i => $h) {
        $params[$h] = OAuthUtil::urldecode_rfc3986(empty($matches[3][$i]) ? $matches[4][$i] : $matches[3][$i]);
      }
      if (isset($params['realm'])) {
        unset($params['realm']);
      }
765
    }
766 767
    return $params;
  }
768

769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788
  // helper to try to sort out headers for people who aren't running apache
  public static function get_headers() {
    if (function_exists('apache_request_headers')) {
      // we need this to get the actual Authorization: header
      // because apache tends to tell us it doesn't exist
      $headers = apache_request_headers();

      // sanitize the output of apache_request_headers because
      // we always want the keys to be Cased-Like-This and arh()
      // returns the headers in the same case as they are in the
      // request
      $out = array();
      foreach ($headers AS $key => $value) {
        $key = str_replace(
            " ",
            "-",
            ucwords(strtolower(str_replace("-", " ", $key)))
          );
        $out[$key] = $value;
      }
789
    } else {
790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810
      // otherwise we don't have apache and are just going to have to hope
      // that $_SERVER actually contains what we need
      $out = array();
      if( isset($_SERVER['CONTENT_TYPE']) )
        $out['Content-Type'] = $_SERVER['CONTENT_TYPE'];
      if( isset($_ENV['CONTENT_TYPE']) )
        $out['Content-Type'] = $_ENV['CONTENT_TYPE'];

      foreach ($_SERVER as $key => $value) {
        if (substr($key, 0, 5) == "HTTP_") {
          // this is chaos, basically it is just there to capitalize the first
          // letter of every word that is not an initial HTTP and strip HTTP
          // code from przemek
          $key = str_replace(
            " ",
            "-",
            ucwords(strtolower(str_replace("_", " ", substr($key, 5))))
          );
          $out[$key] = $value;
        }
      }
811
    }
812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842
    return $out;
  }

  // This function takes a input like a=b&a=c&d=e and returns the parsed
  // parameters like this
  // array('a' => array('b','c'), 'd' => 'e')
  public static function parse_parameters( $input ) {
    if (!isset($input) || !$input) return array();

    $pairs = explode('&', $input);

    $parsed_parameters = array();
    foreach ($pairs as $pair) {
      $split = explode('=', $pair, 2);
      $parameter = OAuthUtil::urldecode_rfc3986($split[0]);
      $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : '';

      if (isset($parsed_parameters[$parameter])) {
        // We have already recieved parameter(s) with this name, so add to the list
        // of parameters with this name

        if (is_scalar($parsed_parameters[$parameter])) {
          // This is the first duplicate, so transform scalar (string) into an array
          // so we can add the duplicates
          $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]);
        }

        $parsed_parameters[$parameter][] = $value;
      } else {
        $parsed_parameters[$parameter] = $value;
      }
843
    }
844 845
    return $parsed_parameters;
  }
846

847 848
  public static function build_http_query($params) {
    if (!$params) return '';
849

850 851 852 853
    // Urlencode both keys and values
    $keys = OAuthUtil::urlencode_rfc3986(array_keys($params));
    $values = OAuthUtil::urlencode_rfc3986(array_values($params));
    $params = array_combine($keys, $values);
854

855 856 857
    // Parameters are sorted by name, using lexicographical byte value ordering.
    // Ref: Spec: 9.1.1 (1)
    uksort($params, 'strcmp');
858

859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877
    $pairs = array();
    foreach ($params as $parameter => $value) {
      if (is_array($value)) {
        // If two or more parameters share the same name, they are sorted by their value
        // Ref: Spec: 9.1.1 (1)
        // June 12th, 2010 - changed to sort because of issue 164 by hidetaka
        sort($value, SORT_STRING);
        foreach ($value as $duplicate_value) {
          $pairs[] = $parameter . '=' . $duplicate_value;
        }
      } else {
        $pairs[] = $parameter . '=' . $value;
      }
    }
    // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61)
    // Each name-value pair is separated by an '&' character (ASCII code 38)
    return implode('&', $pairs);
  }
}
878 879

?>