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