Campustream 1.0
A social network MQP for WPI
|
00001 <?php 00002 // vim: foldmethod=marker 00003 00004 /* Generic exception class 00005 */ 00006 class OAuthException extends Exception { 00007 // pass 00008 } 00009 00010 class OAuthConsumer { 00011 public $key; 00012 public $secret; 00013 00014 function __construct($key, $secret, $callback_url=NULL) { 00015 $this->key = $key; 00016 $this->secret = $secret; 00017 $this->callback_url = $callback_url; 00018 } 00019 00020 function __toString() { 00021 return "OAuthConsumer[key=$this->key,secret=$this->secret]"; 00022 } 00023 } 00024 00025 class OAuthToken { 00026 // access tokens and request tokens 00027 public $key; 00028 public $secret; 00029 00034 function __construct($key, $secret) { 00035 $this->key = $key; 00036 $this->secret = $secret; 00037 } 00038 00043 function to_string() { 00044 return "oauth_token=" . 00045 OAuthUtil::urlencode_rfc3986($this->key) . 00046 "&oauth_token_secret=" . 00047 OAuthUtil::urlencode_rfc3986($this->secret); 00048 } 00049 00050 function __toString() { 00051 return $this->to_string(); 00052 } 00053 } 00054 00059 abstract class OAuthSignatureMethod { 00064 abstract public function get_name(); 00065 00076 abstract public function build_signature($request, $consumer, $token); 00077 00086 public function check_signature($request, $consumer, $token, $signature) { 00087 $built = $this->build_signature($request, $consumer, $token); 00088 return $built == $signature; 00089 } 00090 } 00091 00099 class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod { 00100 function get_name() { 00101 return "HMAC-SHA1"; 00102 } 00103 00104 public function build_signature($request, $consumer, $token) { 00105 $base_string = $request->get_signature_base_string(); 00106 $request->base_string = $base_string; 00107 00108 $key_parts = array( 00109 $consumer->secret, 00110 ($token) ? $token->secret : "" 00111 ); 00112 00113 $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); 00114 $key = implode('&', $key_parts); 00115 00116 return base64_encode(hash_hmac('sha1', $base_string, $key, true)); 00117 } 00118 } 00119 00125 class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod { 00126 public function get_name() { 00127 return "PLAINTEXT"; 00128 } 00129 00139 public function build_signature($request, $consumer, $token) { 00140 $key_parts = array( 00141 $consumer->secret, 00142 ($token) ? $token->secret : "" 00143 ); 00144 00145 $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); 00146 $key = implode('&', $key_parts); 00147 $request->base_string = $key; 00148 00149 return $key; 00150 } 00151 } 00152 00161 abstract class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod { 00162 public function get_name() { 00163 return "RSA-SHA1"; 00164 } 00165 00166 // Up to the SP to implement this lookup of keys. Possible ideas are: 00167 // (1) do a lookup in a table of trusted certs keyed off of consumer 00168 // (2) fetch via http using a url provided by the requester 00169 // (3) some sort of specific discovery code based on request 00170 // 00171 // Either way should return a string representation of the certificate 00172 protected abstract function fetch_public_cert(&$request); 00173 00174 // Up to the SP to implement this lookup of keys. Possible ideas are: 00175 // (1) do a lookup in a table of trusted certs keyed off of consumer 00176 // 00177 // Either way should return a string representation of the certificate 00178 protected abstract function fetch_private_cert(&$request); 00179 00180 public function build_signature($request, $consumer, $token) { 00181 $base_string = $request->get_signature_base_string(); 00182 $request->base_string = $base_string; 00183 00184 // Fetch the private key cert based on the request 00185 $cert = $this->fetch_private_cert($request); 00186 00187 // Pull the private key ID from the certificate 00188 $privatekeyid = openssl_get_privatekey($cert); 00189 00190 // Sign using the key 00191 $ok = openssl_sign($base_string, $signature, $privatekeyid); 00192 00193 // Release the key resource 00194 openssl_free_key($privatekeyid); 00195 00196 return base64_encode($signature); 00197 } 00198 00199 public function check_signature($request, $consumer, $token, $signature) { 00200 $decoded_sig = base64_decode($signature); 00201 00202 $base_string = $request->get_signature_base_string(); 00203 00204 // Fetch the public key cert based on the request 00205 $cert = $this->fetch_public_cert($request); 00206 00207 // Pull the public key ID from the certificate 00208 $publickeyid = openssl_get_publickey($cert); 00209 00210 // Check the computed signature against the one passed in the query 00211 $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); 00212 00213 // Release the key resource 00214 openssl_free_key($publickeyid); 00215 00216 return $ok == 1; 00217 } 00218 } 00219 00220 class OAuthRequest { 00221 private $parameters; 00222 private $http_method; 00223 private $http_url; 00224 // for debug purposes 00225 public $base_string; 00226 public static $version = '1.0'; 00227 public static $POST_INPUT = 'php://input'; 00228 00229 function __construct($http_method, $http_url, $parameters=NULL) { 00230 @$parameters or $parameters = array(); 00231 $parameters = array_merge( OAuthUtil::parse_parameters(parse_url($http_url, PHP_URL_QUERY)), $parameters); 00232 $this->parameters = $parameters; 00233 $this->http_method = $http_method; 00234 $this->http_url = $http_url; 00235 } 00236 00237 00241 public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) { 00242 $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") 00243 ? 'http' 00244 : 'https'; 00245 @$http_url or $http_url = $scheme . 00246 '://' . $_SERVER['HTTP_HOST'] . 00247 ':' . 00248 $_SERVER['SERVER_PORT'] . 00249 $_SERVER['REQUEST_URI']; 00250 @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; 00251 00252 // We weren't handed any parameters, so let's find the ones relevant to 00253 // this request. 00254 // If you run XML-RPC or similar you should use this to provide your own 00255 // parsed parameter-list 00256 if (!$parameters) { 00257 // Find request headers 00258 $request_headers = OAuthUtil::get_headers(); 00259 00260 // Parse the query-string to find GET parameters 00261 $parameters = OAuthUtil::parse_parameters($_SERVER['QUERY_STRING']); 00262 00263 // It's a POST request of the proper content-type, so parse POST 00264 // parameters and add those overriding any duplicates from GET 00265 if ($http_method == "POST" 00266 && @strstr($request_headers["Content-Type"], 00267 "application/x-www-form-urlencoded") 00268 ) { 00269 $post_data = OAuthUtil::parse_parameters( 00270 file_get_contents(self::$POST_INPUT) 00271 ); 00272 $parameters = array_merge($parameters, $post_data); 00273 } 00274 00275 // We have a Authorization-header with OAuth data. Parse the header 00276 // and add those overriding any duplicates from GET or POST 00277 if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") { 00278 $header_parameters = OAuthUtil::split_header( 00279 $request_headers['Authorization'] 00280 ); 00281 $parameters = array_merge($parameters, $header_parameters); 00282 } 00283 00284 } 00285 00286 return new OAuthRequest($http_method, $http_url, $parameters); 00287 } 00288 00292 public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) { 00293 @$parameters or $parameters = array(); 00294 $defaults = array("oauth_version" => OAuthRequest::$version, 00295 "oauth_nonce" => OAuthRequest::generate_nonce(), 00296 "oauth_timestamp" => OAuthRequest::generate_timestamp(), 00297 "oauth_consumer_key" => $consumer->key); 00298 if ($token) 00299 $defaults['oauth_token'] = $token->key; 00300 00301 $parameters = array_merge($defaults, $parameters); 00302 00303 return new OAuthRequest($http_method, $http_url, $parameters); 00304 } 00305 00306 public function set_parameter($name, $value, $allow_duplicates = true) { 00307 if ($allow_duplicates && isset($this->parameters[$name])) { 00308 // We have already added parameter(s) with this name, so add to the list 00309 if (is_scalar($this->parameters[$name])) { 00310 // This is the first duplicate, so transform scalar (string) 00311 // into an array so we can add the duplicates 00312 $this->parameters[$name] = array($this->parameters[$name]); 00313 } 00314 00315 $this->parameters[$name][] = $value; 00316 } else { 00317 $this->parameters[$name] = $value; 00318 } 00319 } 00320 00321 public function get_parameter($name) { 00322 return isset($this->parameters[$name]) ? $this->parameters[$name] : null; 00323 } 00324 00325 public function get_parameters() { 00326 return $this->parameters; 00327 } 00328 00329 public function unset_parameter($name) { 00330 unset($this->parameters[$name]); 00331 } 00332 00337 public function get_signable_parameters() { 00338 // Grab all parameters 00339 $params = $this->parameters; 00340 00341 // Remove oauth_signature if present 00342 // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") 00343 if (isset($params['oauth_signature'])) { 00344 unset($params['oauth_signature']); 00345 } 00346 00347 return OAuthUtil::build_http_query($params); 00348 } 00349 00357 public function get_signature_base_string() { 00358 $parts = array( 00359 $this->get_normalized_http_method(), 00360 $this->get_normalized_http_url(), 00361 $this->get_signable_parameters() 00362 ); 00363 00364 $parts = OAuthUtil::urlencode_rfc3986($parts); 00365 00366 return implode('&', $parts); 00367 } 00368 00372 public function get_normalized_http_method() { 00373 return strtoupper($this->http_method); 00374 } 00375 00380 public function get_normalized_http_url() { 00381 $parts = parse_url($this->http_url); 00382 00383 $port = @$parts['port']; 00384 $scheme = $parts['scheme']; 00385 $host = $parts['host']; 00386 $path = @$parts['path']; 00387 00388 $port or $port = ($scheme == 'https') ? '443' : '80'; 00389 00390 if (($scheme == 'https' && $port != '443') 00391 || ($scheme == 'http' && $port != '80')) { 00392 $host = "$host:$port"; 00393 } 00394 return "$scheme://$host$path"; 00395 } 00396 00400 public function to_url() { 00401 $post_data = $this->to_postdata(); 00402 $out = $this->get_normalized_http_url(); 00403 if ($post_data) { 00404 $out .= '?'.$post_data; 00405 } 00406 return $out; 00407 } 00408 00412 public function to_postdata() { 00413 return OAuthUtil::build_http_query($this->parameters); 00414 } 00415 00419 public function to_header($realm=null) { 00420 $first = true; 00421 if($realm) { 00422 $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"'; 00423 $first = false; 00424 } else 00425 $out = 'Authorization: OAuth'; 00426 00427 $total = array(); 00428 foreach ($this->parameters as $k => $v) { 00429 if (substr($k, 0, 5) != "oauth") continue; 00430 if (is_array($v)) { 00431 throw new OAuthException('Arrays not supported in headers'); 00432 } 00433 $out .= ($first) ? ' ' : ','; 00434 $out .= OAuthUtil::urlencode_rfc3986($k) . 00435 '="' . 00436 OAuthUtil::urlencode_rfc3986($v) . 00437 '"'; 00438 $first = false; 00439 } 00440 return $out; 00441 } 00442 00443 public function __toString() { 00444 return $this->to_url(); 00445 } 00446 00447 00448 public function sign_request($signature_method, $consumer, $token) { 00449 $this->set_parameter( 00450 "oauth_signature_method", 00451 $signature_method->get_name(), 00452 false 00453 ); 00454 $signature = $this->build_signature($signature_method, $consumer, $token); 00455 $this->set_parameter("oauth_signature", $signature, false); 00456 } 00457 00458 public function build_signature($signature_method, $consumer, $token) { 00459 $signature = $signature_method->build_signature($this, $consumer, $token); 00460 return $signature; 00461 } 00462 00466 private static function generate_timestamp() { 00467 return time(); 00468 } 00469 00473 private static function generate_nonce() { 00474 $mt = microtime(); 00475 $rand = mt_rand(); 00476 00477 return md5($mt . $rand); // md5s look nicer than numbers 00478 } 00479 } 00480 00481 class OAuthServer { 00482 protected $timestamp_threshold = 300; // in seconds, five minutes 00483 protected $version = '1.0'; // hi blaine 00484 protected $signature_methods = array(); 00485 00486 protected $data_store; 00487 00488 function __construct($data_store) { 00489 $this->data_store = $data_store; 00490 } 00491 00492 public function add_signature_method($signature_method) { 00493 $this->signature_methods[$signature_method->get_name()] = 00494 $signature_method; 00495 } 00496 00497 // high level functions 00498 00503 public function fetch_request_token(&$request) { 00504 $this->get_version($request); 00505 00506 $consumer = $this->get_consumer($request); 00507 00508 // no token required for the initial token request 00509 $token = NULL; 00510 00511 $this->check_signature($request, $consumer, $token); 00512 00513 // Rev A change 00514 $callback = $request->get_parameter('oauth_callback'); 00515 $new_token = $this->data_store->new_request_token($consumer, $callback); 00516 00517 return $new_token; 00518 } 00519 00524 public function fetch_access_token(&$request) { 00525 $this->get_version($request); 00526 00527 $consumer = $this->get_consumer($request); 00528 00529 // requires authorized request token 00530 $token = $this->get_token($request, $consumer, "request"); 00531 00532 $this->check_signature($request, $consumer, $token); 00533 00534 // Rev A change 00535 $verifier = $request->get_parameter('oauth_verifier'); 00536 $new_token = $this->data_store->new_access_token($token, $consumer, $verifier); 00537 00538 return $new_token; 00539 } 00540 00544 public function verify_request(&$request) { 00545 $this->get_version($request); 00546 $consumer = $this->get_consumer($request); 00547 $token = $this->get_token($request, $consumer, "access"); 00548 $this->check_signature($request, $consumer, $token); 00549 return array($consumer, $token); 00550 } 00551 00552 // Internals from here 00556 private function get_version(&$request) { 00557 $version = $request->get_parameter("oauth_version"); 00558 if (!$version) { 00559 // Service Providers MUST assume the protocol version to be 1.0 if this parameter is not present. 00560 // Chapter 7.0 ("Accessing Protected Ressources") 00561 $version = '1.0'; 00562 } 00563 if ($version !== $this->version) { 00564 throw new OAuthException("OAuth version '$version' not supported"); 00565 } 00566 return $version; 00567 } 00568 00572 private function get_signature_method(&$request) { 00573 $signature_method = 00574 @$request->get_parameter("oauth_signature_method"); 00575 00576 if (!$signature_method) { 00577 // According to chapter 7 ("Accessing Protected Ressources") the signature-method 00578 // parameter is required, and we can't just fallback to PLAINTEXT 00579 throw new OAuthException('No signature method parameter. This parameter is required'); 00580 } 00581 00582 if (!in_array($signature_method, 00583 array_keys($this->signature_methods))) { 00584 throw new OAuthException( 00585 "Signature method '$signature_method' not supported " . 00586 "try one of the following: " . 00587 implode(", ", array_keys($this->signature_methods)) 00588 ); 00589 } 00590 return $this->signature_methods[$signature_method]; 00591 } 00592 00596 private function get_consumer(&$request) { 00597 $consumer_key = @$request->get_parameter("oauth_consumer_key"); 00598 if (!$consumer_key) { 00599 throw new OAuthException("Invalid consumer key"); 00600 } 00601 00602 $consumer = $this->data_store->lookup_consumer($consumer_key); 00603 if (!$consumer) { 00604 throw new OAuthException("Invalid consumer"); 00605 } 00606 00607 return $consumer; 00608 } 00609 00613 private function get_token(&$request, $consumer, $token_type="access") { 00614 $token_field = @$request->get_parameter('oauth_token'); 00615 $token = $this->data_store->lookup_token( 00616 $consumer, $token_type, $token_field 00617 ); 00618 if (!$token) { 00619 throw new OAuthException("Invalid $token_type token: $token_field"); 00620 } 00621 return $token; 00622 } 00623 00628 private function check_signature(&$request, $consumer, $token) { 00629 // this should probably be in a different method 00630 $timestamp = @$request->get_parameter('oauth_timestamp'); 00631 $nonce = @$request->get_parameter('oauth_nonce'); 00632 00633 $this->check_timestamp($timestamp); 00634 $this->check_nonce($consumer, $token, $nonce, $timestamp); 00635 00636 $signature_method = $this->get_signature_method($request); 00637 00638 $signature = $request->get_parameter('oauth_signature'); 00639 $valid_sig = $signature_method->check_signature( 00640 $request, 00641 $consumer, 00642 $token, 00643 $signature 00644 ); 00645 00646 if (!$valid_sig) { 00647 throw new OAuthException("Invalid signature"); 00648 } 00649 } 00650 00654 private function check_timestamp($timestamp) { 00655 if( ! $timestamp ) 00656 throw new OAuthException( 00657 'Missing timestamp parameter. The parameter is required' 00658 ); 00659 00660 // verify that timestamp is recentish 00661 $now = time(); 00662 if (abs($now - $timestamp) > $this->timestamp_threshold) { 00663 throw new OAuthException( 00664 "Expired timestamp, yours $timestamp, ours $now" 00665 ); 00666 } 00667 } 00668 00672 private function check_nonce($consumer, $token, $nonce, $timestamp) { 00673 if( ! $nonce ) 00674 throw new OAuthException( 00675 'Missing nonce parameter. The parameter is required' 00676 ); 00677 00678 // verify that the nonce is uniqueish 00679 $found = $this->data_store->lookup_nonce( 00680 $consumer, 00681 $token, 00682 $nonce, 00683 $timestamp 00684 ); 00685 if ($found) { 00686 throw new OAuthException("Nonce already used: $nonce"); 00687 } 00688 } 00689 00690 } 00691 00692 class OAuthDataStore { 00693 function lookup_consumer($consumer_key) { 00694 // implement me 00695 } 00696 00697 function lookup_token($consumer, $token_type, $token) { 00698 // implement me 00699 } 00700 00701 function lookup_nonce($consumer, $token, $nonce, $timestamp) { 00702 // implement me 00703 } 00704 00705 function new_request_token($consumer, $callback = null) { 00706 // return a new token attached to this consumer 00707 } 00708 00709 function new_access_token($token, $consumer, $verifier = null) { 00710 // return a new access token attached to this consumer 00711 // for the user associated with this token if the request token 00712 // is authorized 00713 // should also invalidate the request token 00714 } 00715 00716 } 00717 00718 class OAuthUtil { 00719 public static function urlencode_rfc3986($input) { 00720 if (is_array($input)) { 00721 return array_map(array('OAuthUtil', 'urlencode_rfc3986'), $input); 00722 } else if (is_scalar($input)) { 00723 return str_replace( 00724 '+', 00725 ' ', 00726 str_replace('%7E', '~', rawurlencode($input)) 00727 ); 00728 } else { 00729 return ''; 00730 } 00731 } 00732 00733 00734 // This decode function isn't taking into consideration the above 00735 // modifications to the encoding process. However, this method doesn't 00736 // seem to be used anywhere so leaving it as is. 00737 public static function urldecode_rfc3986($string) { 00738 return urldecode($string); 00739 } 00740 00741 // Utility function for turning the Authorization: header into 00742 // parameters, has to do some unescaping 00743 // Can filter out any non-oauth parameters if needed (default behaviour) 00744 public static function split_header($header, $only_allow_oauth_parameters = true) { 00745 $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/'; 00746 $offset = 0; 00747 $params = array(); 00748 while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { 00749 $match = $matches[0]; 00750 $header_name = $matches[2][0]; 00751 $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0]; 00752 if (preg_match('/^oauth_/', $header_name) || !$only_allow_oauth_parameters) { 00753 $params[$header_name] = OAuthUtil::urldecode_rfc3986($header_content); 00754 } 00755 $offset = $match[1] + strlen($match[0]); 00756 } 00757 00758 if (isset($params['realm'])) { 00759 unset($params['realm']); 00760 } 00761 00762 return $params; 00763 } 00764 00765 // helper to try to sort out headers for people who aren't running apache 00766 public static function get_headers() { 00767 if (function_exists('apache_request_headers')) { 00768 // we need this to get the actual Authorization: header 00769 // because apache tends to tell us it doesn't exist 00770 $headers = apache_request_headers(); 00771 00772 // sanitize the output of apache_request_headers because 00773 // we always want the keys to be Cased-Like-This and arh() 00774 // returns the headers in the same case as they are in the 00775 // request 00776 $out = array(); 00777 foreach( $headers AS $key => $value ) { 00778 $key = str_replace( 00779 " ", 00780 "-", 00781 ucwords(strtolower(str_replace("-", " ", $key))) 00782 ); 00783 $out[$key] = $value; 00784 } 00785 } else { 00786 // otherwise we don't have apache and are just going to have to hope 00787 // that $_SERVER actually contains what we need 00788 $out = array(); 00789 if( isset($_SERVER['CONTENT_TYPE']) ) 00790 $out['Content-Type'] = $_SERVER['CONTENT_TYPE']; 00791 if( isset($_ENV['CONTENT_TYPE']) ) 00792 $out['Content-Type'] = $_ENV['CONTENT_TYPE']; 00793 00794 foreach ($_SERVER as $key => $value) { 00795 if (substr($key, 0, 5) == "HTTP_") { 00796 // this is chaos, basically it is just there to capitalize the first 00797 // letter of every word that is not an initial HTTP and strip HTTP 00798 // code from przemek 00799 $key = str_replace( 00800 " ", 00801 "-", 00802 ucwords(strtolower(str_replace("_", " ", substr($key, 5)))) 00803 ); 00804 $out[$key] = $value; 00805 } 00806 } 00807 } 00808 return $out; 00809 } 00810 00811 // This function takes a input like a=b&a=c&d=e and returns the parsed 00812 // parameters like this 00813 // array('a' => array('b','c'), 'd' => 'e') 00814 public static function parse_parameters( $input ) { 00815 if (!isset($input) || !$input) return array(); 00816 00817 $pairs = explode('&', $input); 00818 00819 $parsed_parameters = array(); 00820 foreach ($pairs as $pair) { 00821 $split = explode('=', $pair, 2); 00822 $parameter = OAuthUtil::urldecode_rfc3986($split[0]); 00823 $value = isset($split[1]) ? OAuthUtil::urldecode_rfc3986($split[1]) : ''; 00824 00825 if (isset($parsed_parameters[$parameter])) { 00826 // We have already recieved parameter(s) with this name, so add to the list 00827 // of parameters with this name 00828 00829 if (is_scalar($parsed_parameters[$parameter])) { 00830 // This is the first duplicate, so transform scalar (string) into an array 00831 // so we can add the duplicates 00832 $parsed_parameters[$parameter] = array($parsed_parameters[$parameter]); 00833 } 00834 00835 $parsed_parameters[$parameter][] = $value; 00836 } else { 00837 $parsed_parameters[$parameter] = $value; 00838 } 00839 } 00840 return $parsed_parameters; 00841 } 00842 00843 public static function build_http_query($params) { 00844 if (!$params) return ''; 00845 00846 // Urlencode both keys and values 00847 $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); 00848 $values = OAuthUtil::urlencode_rfc3986(array_values($params)); 00849 $params = array_combine($keys, $values); 00850 00851 // Parameters are sorted by name, using lexicographical byte value ordering. 00852 // Ref: Spec: 9.1.1 (1) 00853 uksort($params, 'strcmp'); 00854 00855 $pairs = array(); 00856 foreach ($params as $parameter => $value) { 00857 if (is_array($value)) { 00858 // If two or more parameters share the same name, they are sorted by their value 00859 // Ref: Spec: 9.1.1 (1) 00860 natsort($value); 00861 foreach ($value as $duplicate_value) { 00862 $pairs[] = $parameter . '=' . $duplicate_value; 00863 } 00864 } else { 00865 $pairs[] = $parameter . '=' . $value; 00866 } 00867 } 00868 // For each parameter, the name is separated from the corresponding value by an '=' character (ASCII code 61) 00869 // Each name-value pair is separated by an '&' character (ASCII code 38) 00870 return implode('&', $pairs); 00871 } 00872 } 00873 00874 ?>