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