From a54991797dc310bbdc7571f999dd006d8405a49e Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 23 Sep 2010 16:00:03 -0700 Subject: [PATCH 01/54] Upgrade OAuth.php to the latest version. --- extlib/OAuth.php | 954 ++++++++++++++++++++++++++--------------------- 1 file changed, 522 insertions(+), 432 deletions(-) diff --git a/extlib/OAuth.php b/extlib/OAuth.php index 04984d5fa0..e9c4bdfaec 100644 --- a/extlib/OAuth.php +++ b/extlib/OAuth.php @@ -3,26 +3,26 @@ /* Generic exception class */ -class OAuthException extends Exception {/*{{{*/ +class OAuthException extends Exception { // pass -}/*}}}*/ +} -class OAuthConsumer {/*{{{*/ +class OAuthConsumer { public $key; public $secret; - function __construct($key, $secret, $callback_url=NULL) {/*{{{*/ + function __construct($key, $secret, $callback_url=NULL) { $this->key = $key; $this->secret = $secret; $this->callback_url = $callback_url; - }/*}}}*/ + } - function __toString() {/*{{{*/ + function __toString() { return "OAuthConsumer[key=$this->key,secret=$this->secret]"; - }/*}}}*/ -}/*}}}*/ + } +} -class OAuthToken {/*{{{*/ +class OAuthToken { // access tokens and request tokens public $key; public $secret; @@ -31,56 +31,77 @@ class OAuthToken {/*{{{*/ * key = the token * secret = the token secret */ - function __construct($key, $secret) {/*{{{*/ + function __construct($key, $secret) { $this->key = $key; $this->secret = $secret; - }/*}}}*/ + } /** * generates the basic string serialization of a token that a server * would respond to request_token and access_token calls with */ - function to_string() {/*{{{*/ - return "oauth_token=" . OAuthUtil::urlencode_rfc3986($this->key) . - "&oauth_token_secret=" . OAuthUtil::urlencode_rfc3986($this->secret); - }/*}}}*/ + function to_string() { + return "oauth_token=" . + OAuthUtil::urlencode_rfc3986($this->key) . + "&oauth_token_secret=" . + OAuthUtil::urlencode_rfc3986($this->secret); + } - function __toString() {/*{{{*/ + function __toString() { return $this->to_string(); - }/*}}}*/ -}/*}}}*/ + } +} -class OAuthSignatureMethod {/*{{{*/ - public function check_signature(&$request, $consumer, $token, $signature) { +/** + * 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(); + + /** + * 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); + + /** + * 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; - - // Check for zero length, although unlikely here - if (strlen($built) == 0 || strlen($signature) == 0) { - return false; - } - - if (strlen($built) != strlen($signature)) { - return false; - } - - $result = 0; - - // Avoid a timing leak with a (hopefully) time insensitive compare - for ($i = 0; $i < strlen($signature); $i++) { - $result |= ord($built{$i}) ^ ord($signature{$i}); - } - - return $result == 0; } -}/*}}}*/ +} -class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {/*{{{*/ - function get_name() {/*{{{*/ +/** + * 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() { return "HMAC-SHA1"; - }/*}}}*/ + } - public function build_signature($request, $consumer, $token) {/*{{{*/ + public function build_signature($request, $consumer, $token) { $base_string = $request->get_signature_base_string(); $request->base_string = $base_string; @@ -92,61 +113,74 @@ class OAuthSignatureMethod_HMAC_SHA1 extends OAuthSignatureMethod {/*{{{*/ $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); $key = implode('&', $key_parts); - return base64_encode( hash_hmac('sha1', $base_string, $key, true)); - }/*}}}*/ -}/*}}}*/ + return base64_encode(hash_hmac('sha1', $base_string, $key, true)); + } +} -class OAuthSignatureMethod_PLAINTEXT extends OAuthSignatureMethod {/*{{{*/ - public function get_name() {/*{{{*/ +/** + * 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() { return "PLAINTEXT"; - }/*}}}*/ + } - public function build_signature($request, $consumer, $token) {/*{{{*/ - $sig = array( - OAuthUtil::urlencode_rfc3986($consumer->secret) + /** + * 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 : "" ); - if ($token) { - array_push($sig, OAuthUtil::urlencode_rfc3986($token->secret)); - } else { - array_push($sig, ''); - } + $key_parts = OAuthUtil::urlencode_rfc3986($key_parts); + $key = implode('&', $key_parts); + $request->base_string = $key; - $raw = implode("&", $sig); - // for debug purposes - $request->base_string = $raw; + return $key; + } +} - return OAuthUtil::urlencode_rfc3986($raw); - }/*}}}*/ -}/*}}}*/ - -class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {/*{{{*/ - public function get_name() {/*{{{*/ +/** + * 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() { return "RSA-SHA1"; - }/*}}}*/ + } - protected function fetch_public_cert(&$request) {/*{{{*/ - // not implemented yet, 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 - throw Exception("fetch_public_cert not implemented"); - }/*}}}*/ + // 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); - protected function fetch_private_cert(&$request) {/*{{{*/ - // not implemented yet, 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 - throw Exception("fetch_private_cert not implemented"); - }/*}}}*/ + // 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) {/*{{{*/ + public function build_signature($request, $consumer, $token) { $base_string = $request->get_signature_base_string(); $request->base_string = $base_string; - + // Fetch the private key cert based on the request $cert = $this->fetch_private_cert($request); @@ -154,19 +188,19 @@ class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {/*{{{*/ $privatekeyid = openssl_get_privatekey($cert); // Sign using the key - $ok = openssl_sign($base_string, $signature, $privatekeyid); + $ok = openssl_sign($base_string, $signature, $privatekeyid); // Release the key resource openssl_free_key($privatekeyid); - - return base64_encode($signature); - } /*}}}*/ - public function check_signature(&$request, $consumer, $token, $signature) {/*{{{*/ + return base64_encode($signature); + } + + public function check_signature($request, $consumer, $token, $signature) { $decoded_sig = base64_decode($signature); $base_string = $request->get_signature_base_string(); - + // Fetch the public key cert based on the request $cert = $this->fetch_public_cert($request); @@ -174,143 +208,145 @@ class OAuthSignatureMethod_RSA_SHA1 extends OAuthSignatureMethod {/*{{{*/ $publickeyid = openssl_get_publickey($cert); // Check the computed signature against the one passed in the query - $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); + $ok = openssl_verify($base_string, $decoded_sig, $publickeyid); // Release the key resource openssl_free_key($publickeyid); - - return $ok == 1; - } /*}}}*/ -}/*}}}*/ -class OAuthRequest {/*{{{*/ - private $parameters; - private $http_method; - private $http_url; + return $ok == 1; + } +} + +class OAuthRequest { + protected $parameters; + protected $http_method; + protected $http_url; // for debug purposes public $base_string; public static $version = '1.0'; + public static $POST_INPUT = 'php://input'; - function __construct($http_method, $http_url, $parameters=NULL) {/*{{{*/ - @$parameters or $parameters = array(); + 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); $this->parameters = $parameters; $this->http_method = $http_method; $this->http_url = $http_url; - }/*}}}*/ + } /** * attempt to build up a request from what was passed to the server */ - public static function from_request($http_method=NULL, $http_url=NULL, $parameters=NULL) {/*{{{*/ - $scheme = (!isset($_SERVER['HTTPS']) || $_SERVER['HTTPS'] != "on") ? 'http' : 'https'; - @$http_url or $http_url = $scheme . '://' . $_SERVER['HTTP_HOST'] . ':' . $_SERVER['SERVER_PORT'] . $_SERVER['REQUEST_URI']; - @$http_method or $http_method = $_SERVER['REQUEST_METHOD']; - - $request_headers = OAuthRequest::get_headers(); + 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']; - // let the library user override things however they'd like, if they know - // which parameters to use then go for it, for example XMLRPC might want to - // do this - if ($parameters) { - $req = new OAuthRequest($http_method, $http_url, $parameters); - } else { - // collect request parameters from query string (GET) and post-data (POST) if appropriate (note: POST vars have priority) - $req_parameters = $_GET; - if ($http_method == "POST" && - ( @strstr($request_headers["Content-Type"], "application/x-www-form-urlencoded") || @strstr($_ENV["CONTENT_TYPE"], "application/x-www-form-urlencoded") )) { - $req_parameters = array_merge($req_parameters, $_POST); + // 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); + } + + // 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); } - // next check for the auth header, we need to do some extra stuff - // if that is the case, namely suck in the parameters from GET or POST - // so that we can include them in the signature - if (@substr($request_headers['Authorization'], 0, 6) == "OAuth ") { - $header_parameters = OAuthRequest::split_header($request_headers['Authorization']); - $parameters = array_merge($req_parameters, $header_parameters); - $req = new OAuthRequest($http_method, $http_url, $parameters); - } else $req = new OAuthRequest($http_method, $http_url, $req_parameters); } - return $req; - }/*}}}*/ + return new OAuthRequest($http_method, $http_url, $parameters); + } /** * pretty much a helper function to set up the request */ - public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) {/*{{{*/ - @$parameters or $parameters = array(); + public static function from_consumer_and_token($consumer, $token, $http_method, $http_url, $parameters=NULL) { + $parameters = ($parameters) ? $parameters : array(); $defaults = array("oauth_version" => OAuthRequest::$version, "oauth_nonce" => OAuthRequest::generate_nonce(), "oauth_timestamp" => OAuthRequest::generate_timestamp(), "oauth_consumer_key" => $consumer->key); + if ($token) + $defaults['oauth_token'] = $token->key; + $parameters = array_merge($defaults, $parameters); - if ($token) { - $parameters['oauth_token'] = $token->key; - } return new OAuthRequest($http_method, $http_url, $parameters); - }/*}}}*/ + } - public function set_parameter($name, $value) {/*{{{*/ - $this->parameters[$name] = $value; - }/*}}}*/ + 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]); + } - public function get_parameter($name) {/*{{{*/ + $this->parameters[$name][] = $value; + } else { + $this->parameters[$name] = $value; + } + } + + public function get_parameter($name) { return isset($this->parameters[$name]) ? $this->parameters[$name] : null; - }/*}}}*/ + } - public function get_parameters() {/*{{{*/ + public function get_parameters() { return $this->parameters; - }/*}}}*/ + } + + public function unset_parameter($name) { + unset($this->parameters[$name]); + } /** - * Returns the normalized parameters of the request - * - * This will be all (except oauth_signature) parameters, - * sorted first by key, and if duplicate keys, then by - * value. - * - * The returned string will be all the key=value pairs - * concated by &. - * + * The request parameters, sorted and concatenated into a normalized string. * @return string */ - public function get_signable_parameters() {/*{{{*/ + public function get_signable_parameters() { // Grab all parameters $params = $this->parameters; - + // Remove oauth_signature if present + // Ref: Spec: 9.1.1 ("The oauth_signature parameter MUST be excluded.") if (isset($params['oauth_signature'])) { unset($params['oauth_signature']); } - - // Urlencode both keys and values - $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); - $values = OAuthUtil::urlencode_rfc3986(array_values($params)); - $params = array_combine($keys, $values); - // Sort by keys (natsort) - uksort($params, 'strcmp'); - - // Generate key=value pairs - $pairs = array(); - foreach ($params as $key=>$value ) { - if (is_array($value)) { - // If the value is an array, it's because there are multiple - // with the same key, sort them, then add all the pairs - natsort($value); - foreach ($value as $v2) { - $pairs[] = $key . '=' . $v2; - } - } else { - $pairs[] = $key . '=' . $value; - } - } - - // Return the pairs, concated with & - return implode('&', $pairs); - }/*}}}*/ + return OAuthUtil::build_http_query($params); + } /** * Returns the base string of this request @@ -319,7 +355,7 @@ class OAuthRequest {/*{{{*/ * and the parameters (normalized), each urlencoded * and the concated with &. */ - public function get_signature_base_string() {/*{{{*/ + public function get_signature_base_string() { $parts = array( $this->get_normalized_http_method(), $this->get_normalized_http_url(), @@ -329,185 +365,141 @@ class OAuthRequest {/*{{{*/ $parts = OAuthUtil::urlencode_rfc3986($parts); return implode('&', $parts); - }/*}}}*/ + } /** * just uppercases the http method */ - public function get_normalized_http_method() {/*{{{*/ + public function get_normalized_http_method() { return strtoupper($this->http_method); - }/*}}}*/ + } /** * parses the url and rebuilds it to be * scheme://host/path */ - public function get_normalized_http_url() {/*{{{*/ + public function get_normalized_http_url() { $parts = parse_url($this->http_url); - $port = isset($parts['port']) ? $parts['port'] : null; - $scheme = $parts['scheme']; - $host = $parts['host']; - $path = @$parts['path']; - - $port or $port = ($scheme == 'https') ? '443' : '80'; + $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'] : ''; if (($scheme == 'https' && $port != '443') || ($scheme == 'http' && $port != '80')) { $host = "$host:$port"; } return "$scheme://$host$path"; - }/*}}}*/ + } /** * builds a url usable for a GET request */ - public function to_url() {/*{{{*/ - $out = $this->get_normalized_http_url() . "?"; - $out .= $this->to_postdata(); + public function to_url() { + $post_data = $this->to_postdata(); + $out = $this->get_normalized_http_url(); + if ($post_data) { + $out .= '?'.$post_data; + } return $out; - }/*}}}*/ + } /** * builds the data one would send in a POST request - * - * TODO(morten.fangel): - * this function might be easily replaced with http_build_query() - * and corrections for rfc3986 compatibility.. but not sure */ - public function to_postdata() {/*{{{*/ - $total = array(); - foreach ($this->parameters as $k => $v) { - if (is_array($v)) { - foreach ($v as $va) { - $total[] = OAuthUtil::urlencode_rfc3986($k) . "[]=" . OAuthUtil::urlencode_rfc3986($va); - } - } else { - $total[] = OAuthUtil::urlencode_rfc3986($k) . "=" . OAuthUtil::urlencode_rfc3986($v); - } - } - $out = implode("&", $total); - return $out; - }/*}}}*/ + public function to_postdata() { + return OAuthUtil::build_http_query($this->parameters); + } /** * builds the Authorization: header */ - public function to_header() {/*{{{*/ - $out ='Authorization: OAuth realm=""'; + public function to_header($realm=null) { + $first = true; + if($realm) { + $out = 'Authorization: OAuth realm="' . OAuthUtil::urlencode_rfc3986($realm) . '"'; + $first = false; + } else + $out = 'Authorization: OAuth'; + $total = array(); foreach ($this->parameters as $k => $v) { if (substr($k, 0, 5) != "oauth") continue; - if (is_array($v)) throw new OAuthException('Arrays not supported in headers'); - $out .= ',' . OAuthUtil::urlencode_rfc3986($k) . '="' . OAuthUtil::urlencode_rfc3986($v) . '"'; + 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; } return $out; - }/*}}}*/ + } - public function __toString() {/*{{{*/ + public function __toString() { return $this->to_url(); - }/*}}}*/ + } - public function sign_request($signature_method, $consumer, $token) {/*{{{*/ - $this->set_parameter("oauth_signature_method", $signature_method->get_name()); + public function sign_request($signature_method, $consumer, $token) { + $this->set_parameter( + "oauth_signature_method", + $signature_method->get_name(), + false + ); $signature = $this->build_signature($signature_method, $consumer, $token); - $this->set_parameter("oauth_signature", $signature); - }/*}}}*/ + $this->set_parameter("oauth_signature", $signature, false); + } - public function build_signature($signature_method, $consumer, $token) {/*{{{*/ + public function build_signature($signature_method, $consumer, $token) { $signature = $signature_method->build_signature($this, $consumer, $token); return $signature; - }/*}}}*/ + } /** * util function: current timestamp */ - private static function generate_timestamp() {/*{{{*/ + private static function generate_timestamp() { return time(); - }/*}}}*/ + } /** * util function: current nonce */ - private static function generate_nonce() {/*{{{*/ + private static function generate_nonce() { $mt = microtime(); $rand = mt_rand(); return md5($mt . $rand); // md5s look nicer than numbers - }/*}}}*/ + } +} - /** - * util function for turning the Authorization: header into - * parameters, has to do some unescaping - */ - private static function split_header($header) {/*{{{*/ - $pattern = '/(([-_a-z]*)=("([^"]*)"|([^,]*)),?)/'; - $offset = 0; - $params = array(); - while (preg_match($pattern, $header, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { - $match = $matches[0]; - $header_name = $matches[2][0]; - $header_content = (isset($matches[5])) ? $matches[5][0] : $matches[4][0]; - $params[$header_name] = OAuthUtil::urldecode_rfc3986( $header_content ); - $offset = $match[1] + strlen($match[0]); - } - - if (isset($params['realm'])) { - unset($params['realm']); - } - - return $params; - }/*}}}*/ - - /** - * helper to try to sort out headers for people who aren't running apache - */ - private 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 - return apache_request_headers(); - } - // otherwise we don't have apache and are just going to have to hope - // that $_SERVER actually contains what we need - $out = array(); - 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; - } - } - return $out; - }/*}}}*/ -}/*}}}*/ - -class OAuthServer {/*{{{*/ +class OAuthServer { protected $timestamp_threshold = 300; // in seconds, five minutes - protected $version = 1.0; // hi blaine + protected $version = '1.0'; // hi blaine protected $signature_methods = array(); protected $data_store; - function __construct($data_store) {/*{{{*/ + function __construct($data_store) { $this->data_store = $data_store; - }/*}}}*/ + } + + public function add_signature_method($signature_method) { + $this->signature_methods[$signature_method->get_name()] = + $signature_method; + } - public function add_signature_method($signature_method) {/*{{{*/ - $this->signature_methods[$signature_method->get_name()] = - $signature_method; - }/*}}}*/ - // high level functions /** * process a request_token request * returns the request token on success */ - public function fetch_request_token(&$request) {/*{{{*/ + public function fetch_request_token(&$request) { $this->get_version($request); $consumer = $this->get_consumer($request); @@ -517,16 +509,18 @@ class OAuthServer {/*{{{*/ $this->check_signature($request, $consumer, $token); - $new_token = $this->data_store->new_request_token($consumer); + // Rev A change + $callback = $request->get_parameter('oauth_callback'); + $new_token = $this->data_store->new_request_token($consumer, $callback); return $new_token; - }/*}}}*/ + } /** * process an access_token request * returns the access token on success */ - public function fetch_access_token(&$request) {/*{{{*/ + public function fetch_access_token(&$request) { $this->get_version($request); $consumer = $this->get_consumer($request); @@ -534,63 +528,76 @@ class OAuthServer {/*{{{*/ // requires authorized request token $token = $this->get_token($request, $consumer, "request"); - $this->check_signature($request, $consumer, $token); - $new_token = $this->data_store->new_access_token($token, $consumer); + // Rev A change + $verifier = $request->get_parameter('oauth_verifier'); + $new_token = $this->data_store->new_access_token($token, $consumer, $verifier); return $new_token; - }/*}}}*/ + } /** * verify an api call, checks all the parameters */ - public function verify_request(&$request) {/*{{{*/ + public function verify_request(&$request) { $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); - }/*}}}*/ + } // Internals from here /** * version 1 */ - private function get_version(&$request) {/*{{{*/ + private function get_version(&$request) { $version = $request->get_parameter("oauth_version"); if (!$version) { - $version = 1.0; + // 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'; } - if ($version && $version != $this->version) { + if ($version !== $this->version) { throw new OAuthException("OAuth version '$version' not supported"); } return $version; - }/*}}}*/ + } /** * figure out the signature with some defaults */ - private function get_signature_method(&$request) {/*{{{*/ - $signature_method = - @$request->get_parameter("oauth_signature_method"); + private function get_signature_method($request) { + $signature_method = $request instanceof OAuthRequest + ? $request->get_parameter("oauth_signature_method") + : NULL; + if (!$signature_method) { - $signature_method = "PLAINTEXT"; + // 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'); } - if (!in_array($signature_method, + + if (!in_array($signature_method, array_keys($this->signature_methods))) { throw new OAuthException( - "Signature method '$signature_method' not supported try one of the following: " . implode(", ", array_keys($this->signature_methods)) - ); + "Signature method '$signature_method' not supported " . + "try one of the following: " . + implode(", ", array_keys($this->signature_methods)) + ); } return $this->signature_methods[$signature_method]; - }/*}}}*/ + } /** * try to find the consumer for the provided request's consumer key */ - private function get_consumer(&$request) {/*{{{*/ - $consumer_key = @$request->get_parameter("oauth_consumer_key"); + private function get_consumer($request) { + $consumer_key = $request instanceof OAuthRequest + ? $request->get_parameter("oauth_consumer_key") + : NULL; + if (!$consumer_key) { throw new OAuthException("Invalid consumer key"); } @@ -601,13 +608,16 @@ class OAuthServer {/*{{{*/ } return $consumer; - }/*}}}*/ + } /** * try to find the token for the provided request's token key */ - private function get_token(&$request, $consumer, $token_type="access") {/*{{{*/ - $token_field = @$request->get_parameter('oauth_token'); + private function get_token($request, $consumer, $token_type="access") { + $token_field = $request instanceof OAuthRequest + ? $request->get_parameter('oauth_token') + : NULL; + $token = $this->data_store->lookup_token( $consumer, $token_type, $token_field ); @@ -615,175 +625,255 @@ class OAuthServer {/*{{{*/ throw new OAuthException("Invalid $token_type token: $token_field"); } return $token; - }/*}}}*/ + } /** * all-in-one function to check the signature on a request * should guess the signature method appropriately */ - private function check_signature(&$request, $consumer, $token) {/*{{{*/ + private function check_signature($request, $consumer, $token) { // this should probably be in a different method - $timestamp = @$request->get_parameter('oauth_timestamp'); - $nonce = @$request->get_parameter('oauth_nonce'); + $timestamp = $request instanceof OAuthRequest + ? $request->get_parameter('oauth_timestamp') + : NULL; + $nonce = $request instanceof OAuthRequest + ? $request->get_parameter('oauth_nonce') + : NULL; $this->check_timestamp($timestamp); $this->check_nonce($consumer, $token, $nonce, $timestamp); $signature_method = $this->get_signature_method($request); - $signature = $request->get_parameter('oauth_signature'); + $signature = $request->get_parameter('oauth_signature'); $valid_sig = $signature_method->check_signature( - $request, - $consumer, - $token, + $request, + $consumer, + $token, $signature ); if (!$valid_sig) { throw new OAuthException("Invalid signature"); } - }/*}}}*/ + } /** * check that the timestamp is new enough */ - private function check_timestamp($timestamp) {/*{{{*/ + private function check_timestamp($timestamp) { + if( ! $timestamp ) + throw new OAuthException( + 'Missing timestamp parameter. The parameter is required' + ); + // verify that timestamp is recentish $now = time(); - if ($now - $timestamp > $this->timestamp_threshold) { - throw new OAuthException("Expired timestamp, yours $timestamp, ours $now"); + if (abs($now - $timestamp) > $this->timestamp_threshold) { + throw new OAuthException( + "Expired timestamp, yours $timestamp, ours $now" + ); } - }/*}}}*/ + } /** * check that the nonce is not repeated */ - private function check_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/ + private function check_nonce($consumer, $token, $nonce, $timestamp) { + if( ! $nonce ) + throw new OAuthException( + 'Missing nonce parameter. The parameter is required' + ); + // verify that the nonce is uniqueish - $found = $this->data_store->lookup_nonce($consumer, $token, $nonce, $timestamp); + $found = $this->data_store->lookup_nonce( + $consumer, + $token, + $nonce, + $timestamp + ); if ($found) { throw new OAuthException("Nonce already used: $nonce"); } - }/*}}}*/ + } +} - -}/*}}}*/ - -class OAuthDataStore {/*{{{*/ - function lookup_consumer($consumer_key) {/*{{{*/ +class OAuthDataStore { + function lookup_consumer($consumer_key) { // implement me - }/*}}}*/ + } - function lookup_token($consumer, $token_type, $token) {/*{{{*/ + function lookup_token($consumer, $token_type, $token) { // implement me - }/*}}}*/ + } - function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/ + function lookup_nonce($consumer, $token, $nonce, $timestamp) { // implement me - }/*}}}*/ + } - function new_request_token($consumer) {/*{{{*/ + function new_request_token($consumer, $callback = null) { // return a new token attached to this consumer - }/*}}}*/ + } - function new_access_token($token, $consumer) {/*{{{*/ + function new_access_token($token, $consumer, $verifier = null) { // 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 - }/*}}}*/ + } -}/*}}}*/ +} + +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 ''; + } +} -/* A very naive dbm-based oauth storage - */ -class SimpleOAuthDataStore extends OAuthDataStore {/*{{{*/ - private $dbh; - - function __construct($path = "oauth.gdbm") {/*{{{*/ - $this->dbh = dba_popen($path, 'c', 'gdbm'); - }/*}}}*/ - - function __destruct() {/*{{{*/ - dba_close($this->dbh); - }/*}}}*/ - - function lookup_consumer($consumer_key) {/*{{{*/ - $rv = dba_fetch("consumer_$consumer_key", $this->dbh); - if ($rv === FALSE) { - return NULL; - } - $obj = unserialize($rv); - if (!($obj instanceof OAuthConsumer)) { - return NULL; - } - return $obj; - }/*}}}*/ - - function lookup_token($consumer, $token_type, $token) {/*{{{*/ - $rv = dba_fetch("${token_type}_${token}", $this->dbh); - if ($rv === FALSE) { - return NULL; - } - $obj = unserialize($rv); - if (!($obj instanceof OAuthToken)) { - return NULL; - } - return $obj; - }/*}}}*/ - - function lookup_nonce($consumer, $token, $nonce, $timestamp) {/*{{{*/ - if (dba_exists("nonce_$nonce", $this->dbh)) { - return TRUE; - } else { - dba_insert("nonce_$nonce", "1", $this->dbh); - return FALSE; - } - }/*}}}*/ - - function new_token($consumer, $type="request") {/*{{{*/ - $key = md5(time()); - $secret = time() + time(); - $token = new OAuthToken($key, md5(md5($secret))); - if (!dba_insert("${type}_$key", serialize($token), $this->dbh)) { - throw new OAuthException("doooom!"); - } - return $token; - }/*}}}*/ - - function new_request_token($consumer) {/*{{{*/ - return $this->new_token($consumer, "request"); - }/*}}}*/ - - function new_access_token($token, $consumer) {/*{{{*/ - - $token = $this->new_token($consumer, 'access'); - dba_delete("request_" . $token->key, $this->dbh); - return $token; - }/*}}}*/ -}/*}}}*/ - -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 ''; - } - }/*}}}*/ - - - // This decode function isn't taking into consideration the above - // modifications to the encoding process. However, this method doesn't + // 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 rawurldecode($string); - }/*}}}*/ -}/*}}}*/ + public static function urldecode_rfc3986($string) { + return urldecode($string); + } + + // 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']); + } + } + return $params; + } + + // 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; + } + } else { + // 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; + } + } + } + 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; + } + } + return $parsed_parameters; + } + + public static function build_http_query($params) { + if (!$params) return ''; + + // Urlencode both keys and values + $keys = OAuthUtil::urlencode_rfc3986(array_keys($params)); + $values = OAuthUtil::urlencode_rfc3986(array_values($params)); + $params = array_combine($keys, $values); + + // Parameters are sorted by name, using lexicographical byte value ordering. + // Ref: Spec: 9.1.1 (1) + uksort($params, 'strcmp'); + + $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); + } +} ?> From 06d918d575cfb112b8719b0441548d55e679fe51 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 5 Oct 2010 01:21:50 +0000 Subject: [PATCH 02/54] Strip out the special 'p' paramter added by index.php from $_SERVER['QUERY_STRING'] before doing OAuth requests. Required by the latest version of the OAuth lib. --- lib/apioauth.php | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/apioauth.php b/lib/apioauth.php index 1c87e42324..3f71de0c35 100644 --- a/lib/apioauth.php +++ b/lib/apioauth.php @@ -86,11 +86,18 @@ class ApiOauthAction extends Action } // strip out the p param added in index.php - - // XXX: should we strip anything else? Or alternatively - // only allow a known list of params? unset($_GET['p']); unset($_POST['p']); + unset($_REQUEST['p']); + + $queryArray = explode('&', $_SERVER['QUERY_STRING']); + for ($i = 0; $i < sizeof($queryArray); $i++) { + if (substr($queryArray[$i], 0, 1) == 'p=') { + unset($queryArray[$i]); + } + } + + $_SERVER['QUERY_STRING'] = implode('&', $queryString); } function getCallback($url, $params) From 82f05d0a61752d0552bc8029b2a55ab7c5171b33 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 4 Oct 2010 16:47:20 -0700 Subject: [PATCH 03/54] Somewhat improved test script for fetching an OAuth request token --- tests/oauth/getrequesttoken.php | 43 +++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/tests/oauth/getrequesttoken.php b/tests/oauth/getrequesttoken.php index fc546a0f4c..7c08883542 100755 --- a/tests/oauth/getrequesttoken.php +++ b/tests/oauth/getrequesttoken.php @@ -24,47 +24,54 @@ require_once INSTALLDIR . '/scripts/commandline.inc'; require_once INSTALLDIR . '/extlib/OAuth.php'; $ini = parse_ini_file("oauth.ini"); - $test_consumer = new OAuthConsumer($ini['consumer_key'], $ini['consumer_secret']); - $rt_endpoint = $ini['apiroot'] . $ini['request_token_url']; - $parsed = parse_url($rt_endpoint); $params = array(); - parse_str($parsed['query'], $params); $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); -$req_req = OAuthRequest::from_consumer_and_token($test_consumer, NULL, "GET", $rt_endpoint, $params); -$req_req->sign_request($hmac_method, $test_consumer, NULL); - -$r = httpRequest($req_req->to_url()); +try { + $req_req = OAuthRequest::from_consumer_and_token($test_consumer, NULL, "GET", $rt_endpoint, $params); + $req_req->sign_request($hmac_method, $test_consumer, NULL); + $r = httpRequest($req_req->to_url()); +} catch (Exception $e) { + print $e->getMessage(); + var_dump($req_req); + exit(1); +} $body = $r->getBody(); - $token_stuff = array(); parse_str($body, $token_stuff); -$authurl = $ini['apiroot'] . $ini['authorize_url'] . '?oauth_token=' . $token_stuff['oauth_token']; +if (empty($token_stuff['oauth_token'])) { + print "Error: $body\n"; + exit(1); +} +$authurl = $ini['apiroot'] . $ini['authorize_url'] . '?oauth_token=' . $token_stuff['oauth_token']; +print "\nSuccess!\n\n"; print 'Request token : ' . $token_stuff['oauth_token'] . "\n"; print 'Request token secret : ' . $token_stuff['oauth_token_secret'] . "\n"; print "Authorize URL : $authurl\n"; -//var_dump($req_req); +print "\nNow paste the Authorize URL into your browser and authorize the request token.\n"; function httpRequest($url) { $request = HTTPClient::start(); - $request->setConfig(array( - 'follow_redirects' => true, - 'connect_timeout' => 120, - 'timeout' => 120, - 'ssl_verify_peer' => false, - 'ssl_verify_host' => false - )); + $request->setConfig( + array( + 'follow_redirects' => true, + 'connect_timeout' => 120, + 'timeout' => 120, + 'ssl_verify_peer' => false, + 'ssl_verify_host' => false + ) + ); return $request->get($url); } From 30537700786967f8fd3c91ff3a7b5fc1acf09fe8 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Mon, 4 Oct 2010 18:36:02 -0700 Subject: [PATCH 04/54] A bit more work on the request token fetching test script --- tests/oauth/getrequesttoken.php | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/oauth/getrequesttoken.php b/tests/oauth/getrequesttoken.php index 7c08883542..fc6f03379c 100755 --- a/tests/oauth/getrequesttoken.php +++ b/tests/oauth/getrequesttoken.php @@ -2,7 +2,7 @@ sign_request($hmac_method, $test_consumer, NULL); $r = httpRequest($req_req->to_url()); } catch (Exception $e) { + // oh noez print $e->getMessage(); var_dump($req_req); exit(1); From 83566f014c378bd1b6ebad936e98e4e76b3b731b Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 5 Oct 2010 16:13:07 -0700 Subject: [PATCH 05/54] Fix bad reference --- lib/apioauth.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/lib/apioauth.php b/lib/apioauth.php index 3f71de0c35..6a9ab63778 100644 --- a/lib/apioauth.php +++ b/lib/apioauth.php @@ -30,7 +30,7 @@ if (!defined('STATUSNET')) { exit(1); } - +require_once INSTALLDIR . '/lib/apiaction.php'; require_once INSTALLDIR . '/lib/apioauthstore.php'; /** @@ -44,7 +44,7 @@ require_once INSTALLDIR . '/lib/apioauthstore.php'; * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ -class ApiOauthAction extends Action +class ApiOauthAction extends ApiAction { /** * Is this a read-only action? @@ -91,13 +91,14 @@ class ApiOauthAction extends Action unset($_REQUEST['p']); $queryArray = explode('&', $_SERVER['QUERY_STRING']); + for ($i = 0; $i < sizeof($queryArray); $i++) { if (substr($queryArray[$i], 0, 1) == 'p=') { unset($queryArray[$i]); } } - $_SERVER['QUERY_STRING'] = implode('&', $queryString); + $_SERVER['QUERY_STRING'] = implode('&', $queryArray); } function getCallback($url, $params) @@ -120,4 +121,5 @@ class ApiOauthAction extends Action return ($url . '&' . $k . '=' . $v); } } + } From 4247be51164706af31b2d2a55807a05d89d31fb1 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 5 Oct 2010 17:09:57 -0700 Subject: [PATCH 06/54] Add plain text error format to clientError() --- lib/apiaction.php | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/apiaction.php b/lib/apiaction.php index 0ebf88282a..afba8ab634 100644 --- a/lib/apiaction.php +++ b/lib/apiaction.php @@ -1244,23 +1244,29 @@ class ApiAction extends Action // Do not emit error header for JSONP if (!isset($this->callback)) { - header('HTTP/1.1 '.$code.' '.$status_string); + header('HTTP/1.1 ' . $code . ' ' . $status_string); } - if ($format == 'xml') { + switch($format) { + case 'xml': $this->initDocument('xml'); $this->elementStart('hash'); $this->element('error', null, $msg); $this->element('request', null, $_SERVER['REQUEST_URI']); $this->elementEnd('hash'); $this->endDocument('xml'); - } elseif ($format == 'json'){ + break; + case 'json': $this->initDocument('json'); $error_array = array('error' => $msg, 'request' => $_SERVER['REQUEST_URI']); print(json_encode($error_array)); $this->endDocument('json'); - } else { - + break; + case 'text': + header('Content-Type: text/plain; charset=utf-8'); + print $msg; + break; + default: // If user didn't request a useful format, throw a regular client error throw new ClientException($msg, $code); } From 63663dbd0e4e5c3dd1139b7bdc82ec46a9f27525 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 5 Oct 2010 17:27:55 -0700 Subject: [PATCH 07/54] Stab that 'p' parameter! --- lib/apioauth.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/apioauth.php b/lib/apioauth.php index 6a9ab63778..b2d8faa5a5 100644 --- a/lib/apioauth.php +++ b/lib/apioauth.php @@ -93,7 +93,7 @@ class ApiOauthAction extends ApiAction $queryArray = explode('&', $_SERVER['QUERY_STRING']); for ($i = 0; $i < sizeof($queryArray); $i++) { - if (substr($queryArray[$i], 0, 1) == 'p=') { + if (substr($queryArray[$i], 0, 2) == 'p=') { unset($queryArray[$i]); } } From 73a73c936251f4f481eba2ca0264b49064797067 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 5 Oct 2010 17:38:03 -0700 Subject: [PATCH 08/54] - Update getrequesttoken test script to use 1.0a - Some cleanup --- tests/oauth/getrequesttoken.php | 39 +++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/tests/oauth/getrequesttoken.php b/tests/oauth/getrequesttoken.php index fc6f03379c..11ec126b79 100755 --- a/tests/oauth/getrequesttoken.php +++ b/tests/oauth/getrequesttoken.php @@ -2,7 +2,7 @@ sign_request($hmac_method, $test_consumer, NULL); - $r = httpRequest($req_req->to_url()); + $req = OAuthRequest::from_consumer_and_token( + $testConsumer, + null, + "GET", + $requestTokenUrl, + $params + ); + $req->sign_request($hmac_method, $testConsumer, NULL); + $r = httpRequest($req->to_url()); } catch (Exception $e) { // oh noez print $e->getMessage(); - var_dump($req_req); + var_dump($req); exit(1); } $body = $r->getBody(); -$token_stuff = array(); -parse_str($body, $token_stuff); +$tokenStuff = array(); +parse_str($body, $tokenStuff); -if (empty($token_stuff['oauth_token'])) { +if (empty($tokenStuff['oauth_token'])) { print "Error: $body\n"; exit(1); } -$authurl = $ini['apiroot'] . $ini['authorize_url'] . '?oauth_token=' . $token_stuff['oauth_token']; +$authurl = $ini['apiroot'] . $ini['authorize_url'] . '?oauth_token=' . $tokenStuff['oauth_token']; print "\nSuccess!\n\n"; -print 'Request token : ' . $token_stuff['oauth_token'] . "\n"; -print 'Request token secret : ' . $token_stuff['oauth_token_secret'] . "\n"; +print 'Request token : ' . $tokenStuff['oauth_token'] . "\n"; +print 'Request token secret : ' . $tokenStuff['oauth_token_secret'] . "\n"; print "Authorize URL : $authurl\n"; print "\nNow paste the Authorize URL into your browser and authorize the request token.\n"; @@ -72,7 +79,7 @@ print "\nNow paste the Authorize URL into your browser and authorize the request function httpRequest($url) { $request = HTTPClient::start(); - + $request->setConfig( array( 'follow_redirects' => true, @@ -82,7 +89,7 @@ function httpRequest($url) 'ssl_verify_host' => false ) ); - + return $request->get($url); } From f4f56eea3ae349ed7d47ae7385036107510bf5e5 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 5 Oct 2010 17:48:32 -0700 Subject: [PATCH 09/54] Override new_request_token() to store OAuth 1.0a verified callback URL --- lib/apioauthstore.php | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php index eca93866f0..620f0947fb 100644 --- a/lib/apioauthstore.php +++ b/lib/apioauthstore.php @@ -183,4 +183,30 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore throw new Exception(_('Failed to delete revoked token.')); } } + + /* + * Create a new request token. Overrided to support OAuth 1.0a callback + * + * @param OAuthConsumer $consumer the OAuth Consumer for this token + * @param string $callback the verified OAuth callback URL + * + * @return OAuthToken $token a new unauthorized OAuth request token + */ + + function new_request_token($consumer, $callback) + { + $t = new Token(); + $t->consumer_key = $consumer->key; + $t->tok = common_good_rand(16); + $t->secret = common_good_rand(16); + $t->type = 0; // request + $t->state = 0; // unauthorized + $t->verified_callback = $callback; + $t->created = DB_DataObject_Cast::dateTime(); + if (!$t->insert()) { + return null; + } else { + return new OAuthToken($t->tok, $t->secret); + } + } } From f97b863fd709135fb9f7bf7c756a1c6721e3e988 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 5 Oct 2010 17:53:50 -0700 Subject: [PATCH 10/54] Update ApiOauthRequestTokenAction to support OAuth 1.0a --- actions/apioauthrequesttoken.php | 74 ++++++++++++++++++++++++++------ lib/apioauth.php | 5 +++ 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/actions/apioauthrequesttoken.php b/actions/apioauthrequesttoken.php index 4fa626d866..4f4c2c8fb2 100644 --- a/actions/apioauthrequesttoken.php +++ b/actions/apioauthrequesttoken.php @@ -2,7 +2,7 @@ /** * StatusNet, the distributed open-source microblogging tool * - * Get an OAuth request token + * Issue temporary OAuth credentials (a request token) * * PHP version 5 * @@ -34,7 +34,7 @@ if (!defined('STATUSNET')) { require_once INSTALLDIR . '/lib/apioauth.php'; /** - * Get an OAuth request token + * Issue temporary OAuth credentials (a request token) * * @category API * @package StatusNet @@ -58,22 +58,23 @@ class ApiOauthRequestTokenAction extends ApiOauthAction { parent::prepare($args); - $this->callback = $this->arg('oauth_callback'); - - if (!empty($this->callback)) { - common_debug("callback: $this->callback"); - } + // XXX: support "force_login" parameter like Twitter? (Forces the user to enter + // their credentials to ensure the correct users account is authorized.) return true; } /** - * Class handler. + * Handle a request for temporary OAuth credentials + * + * Make sure the request is kosher, then emit a set of temporary + * credentials -- AKA an unauthorized request token. * * @param array $args array of arguments * * @return void */ + function handle($args) { parent::handle($args); @@ -85,14 +86,63 @@ class ApiOauthRequestTokenAction extends ApiOauthAction $server->add_signature_method($hmac_method); try { + $req = OAuthRequest::from_request(); + + // verify callback + if (!$this->verifyCallback($req->get_parameter('oauth_callback'))) { + throw new OAuthException( + "You must provide a valid URL or 'oob' in oauth_callback.", + 400 + ); + } + + // check signature and issue a new request token $token = $server->fetch_request_token($req); - print $token; + + // return token to the client + $this->showRequestToken($token); + } catch (OAuthException $e) { common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage()); - header('HTTP/1.1 401 Unauthorized'); - header('Content-Type: text/html; charset=utf-8'); - print $e->getMessage() . "\n"; + + // Return 401 for for bad credentials or signature problems, + // and 400 for missing or unsupported parameters + + $code = $e->getCode(); + $this->clientError($e->getMessage(), empty($code) ? 401 : $code, 'text'); + } + } + + /* + * Display temporary OAuth credentials + */ + + function showRequestToken($token) + { + header('Content-Type: application/x-www-form-urlencoded'); + print $token; + print '&oauth_callback_confirmed=true'; + } + + /* Make sure the callback parameter contains either a real URL + * or the string 'oob'. + * + * @todo Check for evil/banned URLs here + * + * @return boolean true or false + */ + + function verifyCallback($callback) + { + if ($callback == "oob") { + common_debug("OAuth request token requested for out of bounds client."); + return true; + } else { + return Validate::uri( + $callback, + array('allowed_schemes' => array('http', 'https')) + ); } } diff --git a/lib/apioauth.php b/lib/apioauth.php index b2d8faa5a5..75b0b3c576 100644 --- a/lib/apioauth.php +++ b/lib/apioauth.php @@ -77,6 +77,11 @@ class ApiOauthAction extends ApiAction self::cleanRequest(); } + /* + * Clean up the request so the OAuth library doesn't find + * any extra parameters or anything else it's not expecting. + * I'm looking at you, p parameter. + */ static function cleanRequest() { // kill evil effects of magical slashing From 5d5c4e8344ba8a16b7da36977693a3eec912880b Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 6 Oct 2010 19:05:31 -0700 Subject: [PATCH 11/54] Some more cleanup --- tests/oauth/getrequesttoken.php | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/tests/oauth/getrequesttoken.php b/tests/oauth/getrequesttoken.php index 11ec126b79..045d597166 100755 --- a/tests/oauth/getrequesttoken.php +++ b/tests/oauth/getrequesttoken.php @@ -33,12 +33,13 @@ foreach(array('consumer_key', 'consumer_secret', 'apiroot', 'request_token_url') } } -$testConsumer = new OAuthConsumer($ini['consumer_key'], $ini['consumer_secret']); +$testConsumer = new OAuthConsumer($ini['consumer_key'], $ini['consumer_secret']); $requestTokenUrl = $ini['apiroot'] . $ini['request_token_url']; -$parsed = parse_url($requestTokenUrl); -$params = array(); +$parsed = parse_url($requestTokenUrl); +$params = array(); + parse_str($parsed['query'], $params); -$params['oauth_callback'] = 'oob'; +$params['oauth_callback'] = 'oob'; // out-of-band $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); @@ -59,22 +60,24 @@ try { exit(1); } -$body = $r->getBody(); +$body = $r->getBody(); $tokenStuff = array(); + parse_str($body, $tokenStuff); -if (empty($tokenStuff['oauth_token'])) { +$tok = $tokenStuff['oauth_token']; +$confirmed = $tokenStuff['oauth_callback_confirmed']; + +if (empty($tokenStuff['oauth_token']) || empty($confirmed) || $confirmed != 'true') { print "Error: $body\n"; exit(1); } -$authurl = $ini['apiroot'] . $ini['authorize_url'] . '?oauth_token=' . $tokenStuff['oauth_token']; -print "\nSuccess!\n\n"; -print 'Request token : ' . $tokenStuff['oauth_token'] . "\n"; -print 'Request token secret : ' . $tokenStuff['oauth_token_secret'] . "\n"; -print "Authorize URL : $authurl\n"; +$authurl = $ini['apiroot'] . $ini['authorize_url'] . '?oauth_token=' . $tok; -print "\nNow paste the Authorize URL into your browser and authorize the request token.\n"; +print "\nSuccess! "; +print "Authorize URL:\n\n$authurl\n\n"; +print "Now paste the Authorize URL into your browser and authorize your temporary credentials.\n"; function httpRequest($url) { @@ -92,4 +95,3 @@ function httpRequest($url) return $request->get($url); } - From f71912440a17f468b1d60db2388fc6030631fce6 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 6 Oct 2010 19:06:57 -0700 Subject: [PATCH 12/54] - New base InfoAction for dialog box like msgs - Fix titles on error pages --- lib/clienterroraction.php | 26 ++++++++- lib/error.php | 73 ++++------------------- lib/info.php | 118 ++++++++++++++++++++++++++++++++++++++ lib/servererroraction.php | 23 ++++++++ 4 files changed, 176 insertions(+), 64 deletions(-) create mode 100644 lib/info.php diff --git a/lib/clienterroraction.php b/lib/clienterroraction.php index 08bced5bda..9233c9bde6 100644 --- a/lib/clienterroraction.php +++ b/lib/clienterroraction.php @@ -12,7 +12,7 @@ * @link http://status.net/ * * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2008, 2009, StatusNet, Inc. + * Copyright (C) 2008-2010 StatusNet, Inc. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -32,7 +32,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } -require_once INSTALLDIR.'/lib/error.php'; +require_once INSTALLDIR . '/lib/error.php'; /** * Class for displaying HTTP client errors @@ -90,4 +90,26 @@ class ClientErrorAction extends ErrorAction $this->showPage(); } + + /** + * To specify additional HTTP headers for the action + * + * @return void + */ + function extraHeaders() + { + $status_string = @self::$status[$this->code]; + header('HTTP/1.1 '.$this->code.' '.$status_string); + } + + /** + * Page title. + * + * @return page title + */ + + function title() + { + return @self::$status[$this->code]; + } } diff --git a/lib/error.php b/lib/error.php index a6a29119f7..762425dc44 100644 --- a/lib/error.php +++ b/lib/error.php @@ -33,6 +33,8 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +require_once INSTALLDIR . '/lib/info.php'; + /** * Base class for displaying HTTP errors * @@ -42,7 +44,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 * @link http://status.net/ */ -class ErrorAction extends Action +class ErrorAction extends InfoAction { static $status = array(); @@ -52,7 +54,7 @@ class ErrorAction extends Action function __construct($message, $code, $output='php://output', $indent=null) { - parent::__construct($output, $indent); + parent::__construct(null, $message, $output, $indent); $this->code = $code; $this->message = $message; @@ -64,43 +66,6 @@ class ErrorAction extends Action $this->prepare($_REQUEST); } - /** - * To specify additional HTTP headers for the action - * - * @return void - */ - function extraHeaders() - { - $status_string = @self::$status[$this->code]; - header('HTTP/1.1 '.$this->code.' '.$status_string); - } - - /** - * Display content. - * - * @return nothing - */ - function showContent() - { - $this->element('div', array('class' => 'error'), $this->message); - } - - /** - * Page title. - * - * @return page title - */ - - function title() - { - return @self::$status[$this->code]; - } - - function isReadOnly($args) - { - return true; - } - function showPage() { if ($this->minimal) { @@ -116,32 +81,16 @@ class ErrorAction extends Action exit(); } - // Overload a bunch of stuff so the page isn't too bloated - - function showBody() + /** + * Display content. + * + * @return nothing + */ + function showContent() { - $this->elementStart('body', array('id' => 'error')); - $this->elementStart('div', array('id' => 'wrap')); - $this->showHeader(); - $this->showCore(); - $this->showFooter(); - $this->elementEnd('div'); - $this->elementEnd('body'); + $this->element('div', array('class' => 'error'), $this->message); } - function showCore() - { - $this->elementStart('div', array('id' => 'core')); - $this->showContentBlock(); - $this->elementEnd('div'); - } - function showHeader() - { - $this->elementStart('div', array('id' => 'header')); - $this->showLogo(); - $this->showPrimaryNav(); - $this->elementEnd('div'); - } } diff --git a/lib/info.php b/lib/info.php new file mode 100644 index 0000000000..395c6522ec --- /dev/null +++ b/lib/info.php @@ -0,0 +1,118 @@ + + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2010, StatusNet, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +/** + * Base class for displaying dialog box like messages to the user + * + * @category Action + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3 + * @link http://status.net/ + * + * @see ErrorAction + */ + +class InfoAction extends Action +{ + var $message = null; + + function __construct($title, $message, $output='php://output', $indent=null) + { + parent::__construct($output, $indent); + + $this->message = $message; + $this->title = $title; + + // XXX: hack alert: usually we aren't going to + // call this page directly, but because it's + // an action it needs an args array anyway + $this->prepare($_REQUEST); + } + + /** + * Page title. + * + * @return page title + */ + + function title() + { + return empty($this->title) ? '' : $this->title; + } + + function isReadOnly($args) + { + return true; + } + + // Overload a bunch of stuff so the page isn't too bloated + + function showBody() + { + $this->elementStart('body', array('id' => 'error')); + $this->elementStart('div', array('id' => 'wrap')); + $this->showHeader(); + $this->showCore(); + $this->showFooter(); + $this->elementEnd('div'); + $this->elementEnd('body'); + } + + function showCore() + { + $this->elementStart('div', array('id' => 'core')); + $this->showContentBlock(); + $this->elementEnd('div'); + } + + function showHeader() + { + $this->elementStart('div', array('id' => 'header')); + $this->showLogo(); + $this->showPrimaryNav(); + $this->elementEnd('div'); + } + + /** + * Display content. + * + * @return nothing + */ + function showContent() + { + $this->element('div', array('class' => 'info'), $this->message); + } + +} diff --git a/lib/servererroraction.php b/lib/servererroraction.php index 9b5a553dc6..54cc99099a 100644 --- a/lib/servererroraction.php +++ b/lib/servererroraction.php @@ -96,4 +96,27 @@ class ServerErrorAction extends ErrorAction $this->showPage(); } + + /** + * To specify additional HTTP headers for the action + * + * @return void + */ + function extraHeaders() + { + $status_string = @self::$status[$this->code]; + header('HTTP/1.1 '.$this->code.' '.$status_string); + } + + /** + * Page title. + * + * @return page title + */ + + function title() + { + return @self::$status[$this->code]; + } + } From 69e621a3e882cd060eb4314554aada7167edd897 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Wed, 6 Oct 2010 19:20:47 -0700 Subject: [PATCH 13/54] - Update ApiOauthAuthorizeAction to 1.0a - Fix enumerable bugs - New page for displaying 1.0a verifier (still needs work) --- actions/apioauthauthorize.php | 231 ++++++++++++++++++++++++++-------- actions/apioauthpin.php | 69 ++++++++++ lib/apioauth.php | 27 +--- lib/apioauthstore.php | 8 ++ lib/oauthstore.php | 11 ++ lib/serverexception.php | 2 +- 6 files changed, 270 insertions(+), 78 deletions(-) create mode 100644 actions/apioauthpin.php diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index c2fbbcdd87..6772052f2c 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -32,6 +32,7 @@ if (!defined('STATUSNET')) { } require_once INSTALLDIR . '/lib/apioauth.php'; +require_once INSTALLDIR . '/lib/info.php'; /** * Authorize an OAuth request token @@ -43,9 +44,10 @@ require_once INSTALLDIR . '/lib/apioauth.php'; * @link http://status.net/ */ -class ApiOauthAuthorizeAction extends ApiOauthAction +class ApiOauthAuthorizeAction extends Action { - var $oauth_token; + var $oauthTokenParam; + var $reqToken; var $callback; var $app; var $nickname; @@ -67,12 +69,17 @@ class ApiOauthAuthorizeAction extends ApiOauthAction { parent::prepare($args); - $this->nickname = $this->trimmed('nickname'); - $this->password = $this->arg('password'); - $this->oauth_token = $this->arg('oauth_token'); - $this->callback = $this->arg('oauth_callback'); - $this->store = new ApiStatusNetOAuthDataStore(); - $this->app = $this->store->getAppByRequestToken($this->oauth_token); + $this->nickname = $this->trimmed('nickname'); + $this->password = $this->arg('password'); + $this->oauthTokenParam = $this->arg('oauth_token'); + $this->callback = $this->arg('oauth_callback'); + $this->store = new ApiStatusNetOAuthDataStore(); + + try { + $this->app = $this->store->getAppByRequestToken($this->oauthTokenParam); + } catch (Exception $e) { + $this->clientError($e->getMessage()); + } return true; } @@ -97,14 +104,30 @@ class ApiOauthAuthorizeAction extends ApiOauthAction } else { - if (empty($this->oauth_token)) { + // Make sure a oauth_token parameter was provided + if (empty($this->oauthTokenParam)) { $this->clientError(_('No oauth_token parameter provided.')); - return; + } else { + + // Check to make sure the token exists + $this->reqToken = $this->store->getTokenByKey($this->oauthTokenParam); + + if (empty($this->reqToken)) { + $this->serverError( + _('Invalid request token.') + ); + } else { + + // Check to make sure we haven't already authorized the token + if ($this->reqToken->state != 0) { + $this->clientError("Invalid request token."); + } + } } + // make sure there's an app associated with this token if (empty($this->app)) { - $this->clientError(_('Invalid token.')); - return; + $this->clientError(_('Invalid request token.')); } $name = $this->app->name; @@ -120,8 +143,8 @@ class ApiOauthAuthorizeAction extends ApiOauthAction $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { - $this->showForm(_('There was a problem with your session token. '. - 'Try again, please.')); + $this->showForm( + _('There was a problem with your session token. Try again, please.')); return; } @@ -130,6 +153,11 @@ class ApiOauthAuthorizeAction extends ApiOauthAction $user = null; if (!common_logged_in()) { + + // XXX Force credentials check? + + // XXX OpenID + $user = common_check_user($this->nickname, $this->password); if (empty($user)) { $this->showForm(_("Invalid nickname / password!")); @@ -141,9 +169,15 @@ class ApiOauthAuthorizeAction extends ApiOauthAction if ($this->arg('allow')) { - // mark the req token as authorized + // fetch the token + $this->reqToken = $this->store->getTokenByKey($this->oauthTokenParam); - $this->store->authorize_token($this->oauth_token); + // mark the req token as authorized + try { + $this->store->authorize_token($this->oauthTokenParam); + } catch (Exception $e) { + $this->serverError($e->getMessage()); + } // Check to see if there was a previous token associated // with this user/app and kill it. If the user is doing this she @@ -156,8 +190,7 @@ class ApiOauthAuthorizeAction extends ApiOauthAction if (!$result) { common_log_db_error($appUser, 'DELETE', __FILE__); - throw new ServerException(_('Database error deleting OAuth application user.')); - return; + $this->serverError(_('Database error deleting OAuth application user.')); } } @@ -175,20 +208,19 @@ class ApiOauthAuthorizeAction extends ApiOauthAction // granted. The OAuth app user record then gets updated // with the new access token and access type. - $appUser->token = $this->oauth_token; + $appUser->token = $this->oauthTokenParam; $appUser->created = common_sql_now(); $result = $appUser->insert(); if (!$result) { common_log_db_error($appUser, 'INSERT', __FILE__); - throw new ServerException(_('Database error inserting OAuth application user.')); - return; + $this->serverError(_('Database error inserting OAuth application user.')); } - // if we have a callback redirect and provide the token + // If we have a callback redirect and provide the token - // A callback specified in the app setup overrides whatever + // Note: A callback specified in the app setup overrides whatever // is passed in with the request. if (!empty($this->app->callback_url)) { @@ -197,40 +229,40 @@ class ApiOauthAuthorizeAction extends ApiOauthAction if (!empty($this->callback)) { - $target_url = $this->getCallback($this->callback, - array('oauth_token' => $this->oauth_token)); + $targetUrl = $this->getCallback( + $this->callback, + array( + 'oauth_token' => $this->oauthTokenParam, + 'oauth_verifier' => $this->reqToken->verifier // 1.0a + ) + ); + + // Redirect the user to the provided OAuth callback + common_redirect($targetUrl, 303); - common_redirect($target_url, 303); } else { - common_debug("callback was empty!"); + common_log( + LOG_INFO, + "No oauth_callback parameter provided for application ID " + . $this->app->id + . " when authorizing request token." + ); } - // otherwise inform the user that the rt was authorized + // Otherwise, inform the user that the rt was authorized + $this->showAuthorized(); - $this->elementStart('p'); + } else if ($this->arg('cancel')) { - // XXX: Do OAuth 1.0a verifier code + try { + $this->store->revoke_token($this->oauthTokenParam, 0); + $this->showCanceled(); + } catch (Exception $e) { + $this->ServerError($e->getMessage()); + } - $this->raw(sprintf(_("The request token %s has been authorized. " . - 'Please exchange it for an access token.'), - $this->oauth_token)); - - $this->elementEnd('p'); - - } else if ($this->arg('deny')) { - - $datastore = new ApiStatusNetOAuthDataStore(); - $datastore->revoke_token($this->oauth_token, 0); - - $this->elementStart('p'); - - $this->raw(sprintf(_("The request token %s has been denied and revoked."), - $this->oauth_token)); - - $this->elementEnd('p'); } else { $this->clientError(_('Unexpected form submission.')); - return; } } @@ -276,7 +308,7 @@ class ApiOauthAuthorizeAction extends ApiOauthAction _('Allow or deny access')); $this->hidden('token', common_session_token()); - $this->hidden('oauth_token', $this->oauth_token); + $this->hidden('oauth_token', $this->oauthTokenParam); $this->hidden('oauth_callback', $this->callback); $this->elementStart('ul', 'form_data'); @@ -321,11 +353,11 @@ class ApiOauthAuthorizeAction extends ApiOauthAction } - $this->element('input', array('id' => 'deny_submit', + $this->element('input', array('id' => 'cancel_submit', 'class' => 'submit submit form_action-primary', - 'name' => 'deny', + 'name' => 'cancel', 'type' => 'submit', - 'value' => _('Deny'))); + 'value' => _('Cancel'))); $this->element('input', array('id' => 'allow_submit', 'class' => 'submit submit form_action-secondary', @@ -348,7 +380,7 @@ class ApiOauthAuthorizeAction extends ApiOauthAction function getInstructions() { - return _('Allow or deny access to your account information.'); + return _('Authorize access to your account information.'); } /** @@ -388,4 +420,97 @@ class ApiOauthAuthorizeAction extends ApiOauthAction // NOP } + /* + * Show a nice message confirming the authorization + * operation was canceled. + * + * @return nothing + */ + + function showCanceled() + { + $info = new InfoAction( + _('Authorization canceled.'), + sprintf( + _('The request token %s has been revoked.'), + $this->oauthTokenParm + ) + ); + + $info->showPage(); + } + + /* + * Show a nice message that the authorization was successful. + * If the operation is out-of-band, show a pin. + * + * @return nothing + */ + + function showAuthorized() + { + + if ($this->reqToken->verified_callback == 'oob') { + + $pin = new ApiOauthPinAction($this->reqToken->verifier); + $pin->showPage(); + + } else { + + $info = new InfoAction( + _("Authorization succeeded."), + sprintf( + _('The request token %s has been authorized. Please exchange it for an access token using this verifier: %s'), + $this->oauthTokenParam, + $this->reqToken->verifier + ) + ); + + $info->showPage(); + } + } + + /* + * Properly format the callback URL and parameters so it's + * suitable for a redirect in the OAuth dance + * + * @param string $url the URL + * @param array $params an array of parameters + * + * @return string $url a URL to use for redirecting to + */ + + function getCallback($url, $params) + { + foreach ($params as $k => $v) { + $url = $this->appendQueryVar( + $url, + OAuthUtil::urlencode_rfc3986($k), + OAuthUtil::urlencode_rfc3986($v) + ); + } + + return $url; + } + + /* + * Append a new query parameter after any existing query + * parameters. + * + * @param string $url the URL + * @prarm string $k the parameter name + * @param string $v value of the paramter + * + * @return string $url the new URL with added parameter + */ + + function appendQueryVar($url, $k, $v) { + $url = preg_replace('/(.*)(\?|&)' . $k . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&'); + $url = substr($url, 0, -1); + if (strpos($url, '?') === false) { + return ($url . '?' . $k . '=' . $v); + } else { + return ($url . '&' . $k . '=' . $v); + } + } } diff --git a/actions/apioauthpin.php b/actions/apioauthpin.php new file mode 100644 index 0000000000..5a88b5e590 --- /dev/null +++ b/actions/apioauthpin.php @@ -0,0 +1,69 @@ +. + * + * @category Action + * @package StatusNet + * @author Zach Copley + * @copyright 2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { + exit(1); +} + +require_once INSTALLDIR . '/lib/info.php'; + +/** + * Class for displaying an OAuth verifier pin + * + * @category Action + * @package StatusNet + * @author Zach Copley + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + +class ApiOauthPinAction extends InfoAction +{ + + function __construct($verifier) + { + $this->verifier = $verifier; + $title = _('Authorization succeeded.'); + parent::__construct($title, $title); + } + + // TODO: Check for logged in state! + + /** + * Display content. + * + * @return nothing + */ + function showContent() + { + // XXX: make this much nicer + $this->element('div', array('class' => 'info'), $this->verifier); + } + +} diff --git a/lib/apioauth.php b/lib/apioauth.php index 75b0b3c576..54cecf92a8 100644 --- a/lib/apioauth.php +++ b/lib/apioauth.php @@ -34,9 +34,8 @@ require_once INSTALLDIR . '/lib/apiaction.php'; require_once INSTALLDIR . '/lib/apioauthstore.php'; /** - * Base action for API OAuth enpoints. Clean up the - * the request, and possibly some other common things - * here. + * Base action for API OAuth enpoints. Clean up the + * request. Some other common functions. * * @category API * @package StatusNet @@ -82,6 +81,7 @@ class ApiOauthAction extends ApiAction * any extra parameters or anything else it's not expecting. * I'm looking at you, p parameter. */ + static function cleanRequest() { // kill evil effects of magical slashing @@ -106,25 +106,4 @@ class ApiOauthAction extends ApiAction $_SERVER['QUERY_STRING'] = implode('&', $queryArray); } - function getCallback($url, $params) - { - foreach ($params as $k => $v) { - $url = $this->appendQueryVar($url, - OAuthUtil::urlencode_rfc3986($k), - OAuthUtil::urlencode_rfc3986($v)); - } - - return $url; - } - - function appendQueryVar($url, $k, $v) { - $url = preg_replace('/(.*)(\?|&)' . $k . '=[^&]+?(&)(.*)/i', '$1$2$4', $url . '&'); - $url = substr($url, 0, -1); - if (strpos($url, '?') === false) { - return ($url . '?' . $k . '=' . $v); - } else { - return ($url . '&' . $k . '=' . $v); - } - } - } diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php index 620f0947fb..4d141286bf 100644 --- a/lib/apioauthstore.php +++ b/lib/apioauthstore.php @@ -202,6 +202,14 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore $t->type = 0; // request $t->state = 0; // unauthorized $t->verified_callback = $callback; + + if ($callback === 'oob') { + // six digit pin + $t->verifier = mt_rand(0, 999999); + } else { + $t->verifier = common_good_rand(8); + } + $t->created = DB_DataObject_Cast::dateTime(); if (!$t->insert()) { return null; diff --git a/lib/oauthstore.php b/lib/oauthstore.php index f3ee629fd7..537667678b 100644 --- a/lib/oauthstore.php +++ b/lib/oauthstore.php @@ -55,6 +55,17 @@ class StatusNetOAuthDataStore extends OAuthDataStore } } + function getTokenByKey($token_key) + { + $t = new Token(); + $t->tok = $token_key; + if ($t->find(true)) { + return $t; + } else { + return null; + } + } + // http://oauth.net/core/1.0/#nonce // "The Consumer SHALL then generate a Nonce value that is unique for // all requests with that timestamp." diff --git a/lib/serverexception.php b/lib/serverexception.php index 7dc9765ad6..0dfbd04ffd 100644 --- a/lib/serverexception.php +++ b/lib/serverexception.php @@ -22,7 +22,7 @@ * @category Exception * @package StatusNet * @author Evan Prodromou - * @copyright 2008 StatusNet, Inc. + * @copyright 2008-2010 StatusNet, Inc. * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 * @link http://status.net/ */ From 8d2ccee3f62b06c4b8eb6fc15dd5a74d4211643d Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 7 Oct 2010 09:13:38 -0400 Subject: [PATCH 14/54] PHPCS-clean RequireValidateEmail --- .../RequireValidatedEmailPlugin.php | 121 ++++++++++++------ 1 file changed, 84 insertions(+), 37 deletions(-) diff --git a/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php b/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php index 719dba89cd..a7d7874f1f 100644 --- a/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php +++ b/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php @@ -2,7 +2,8 @@ /** * StatusNet, the distributed open-source microblogging tool * - * Plugin that requires the user to have a validated email address before they can post notices + * Plugin that requires the user to have a validated email address before they + * can post notices * * PHP version 5 * @@ -32,44 +33,64 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +/** + * Plugin for requiring a validated email before posting. + * + * Enable this plugin using addPlugin('RequireValidatedEmail'); + * + * @category Plugin + * @package StatusNet + * @author Craig Andrews + * @author Brion Vibber + * @author Evan Prodromou + * @copyright 2009 Free Software Foundation, Inc http://www.fsf.org + * @copyright 2009-2010 StatusNet, Inc. + * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0 + * @link http://status.net/ + */ + class RequireValidatedEmailPlugin extends Plugin { - // Users created before this time will be grandfathered in - // without the validation requirement. - public $grandfatherCutoff=null; + /** + * Users created before this time will be grandfathered in + * without the validation requirement. + */ - // If OpenID plugin is installed, users with a verified OpenID - // association whose provider URL matches one of these regexes - // will be considered to be sufficiently valid for our needs. - // - // For example, to trust WikiHow and Wikipedia OpenID users: - // - // addPlugin('RequireValidatedEmailPlugin', array( - // 'trustedOpenIDs' => array( - // '!^http://\w+\.wikihow\.com/!', - // '!^http://\w+\.wikipedia\.org/!', - // ), - // )); - public $trustedOpenIDs=array(); + public $grandfatherCutoff = null; - function __construct() - { - parent::__construct(); - } + /** + * If OpenID plugin is installed, users with a verified OpenID + * association whose provider URL matches one of these regexes + * will be considered to be sufficiently valid for our needs. + * + * For example, to trust WikiHow and Wikipedia OpenID users: + * + * addPlugin('RequireValidatedEmailPlugin', array( + * 'trustedOpenIDs' => array( + * '!^http://\w+\.wikihow\.com/!', + * '!^http://\w+\.wikipedia\.org/!', + * ), + * )); + */ + + public $trustedOpenIDs = array(); /** * Event handler for notice saves; rejects the notice * if user's address isn't validated. * - * @param Notice $notice + * @param Notice $notice The notice being saved + * * @return bool hook result code */ + function onStartNoticeSave($notice) { $user = User::staticGet('id', $notice->profile_id); if (!empty($user)) { // it's a remote notice if (!$this->validated($user)) { - throw new ClientException(_m("You must validate your email address before posting.")); + $msg = _m("You must validate your email address before posting."); + throw new ClientException($msg); } } return true; @@ -79,7 +100,8 @@ class RequireValidatedEmailPlugin extends Plugin * Event handler for registration attempts; rejects the registration * if email field is missing. * - * @param RegisterAction $action + * @param Action $action Action being executed + * * @return bool hook result code */ function onStartRegistrationTry($action) @@ -100,7 +122,8 @@ class RequireValidatedEmailPlugin extends Plugin * Check if a user has a validated email address or has been * otherwise grandfathered in. * - * @param User $user + * @param User $user User to valide + * * @return bool */ protected function validated($user) @@ -108,12 +131,16 @@ class RequireValidatedEmailPlugin extends Plugin // The email field is only stored after validation... // Until then you'll find them in confirm_address. $knownGood = !empty($user->email) || - $this->grandfathered($user) || - $this->hasTrustedOpenID($user); + $this->grandfathered($user) || + $this->hasTrustedOpenID($user); // Give other plugins a chance to override, if they can validate // that somebody's ok despite a non-validated email. - Event::handle('RequireValidatedEmailPlugin_Override', array($user, &$knownGood)); + + // FIXME: This isn't how to do it! Use Start*/End* instead + + Event::handle('RequireValidatedEmailPlugin_Override', + array($user, &$knownGood)); return $knownGood; } @@ -122,14 +149,15 @@ class RequireValidatedEmailPlugin extends Plugin * Check if a user was created before the grandfathering cutoff. * If so, we won't need to check for validation. * - * @param User $user - * @return bool + * @param User $user User to check + * + * @return bool true if user is grandfathered */ protected function grandfathered($user) { if ($this->grandfatherCutoff) { $created = strtotime($user->created . " GMT"); - $cutoff = strtotime($this->grandfatherCutoff); + $cutoff = strtotime($this->grandfatherCutoff); if ($created < $cutoff) { return true; } @@ -141,13 +169,20 @@ class RequireValidatedEmailPlugin extends Plugin * Override for RequireValidatedEmail plugin. If we have a user who's * not validated an e-mail, but did come from a trusted provider, * we'll consider them ok. + * + * @param User $user User to check + * + * @return bool true if user has a trusted OpenID. */ + function hasTrustedOpenID($user) { if ($this->trustedOpenIDs && class_exists('User_openid')) { foreach ($this->trustedOpenIDs as $regex) { $oid = new User_openid(); + $oid->user_id = $user->id; + $oid->find(); while ($oid->fetch()) { if (preg_match($regex, $oid->canonical)) { @@ -159,14 +194,26 @@ class RequireValidatedEmailPlugin extends Plugin return false; } + /** + * Add version information for this plugin. + * + * @param array &$versions Array of associative arrays of version data + * + * @return boolean hook value + */ + function onPluginVersion(&$versions) { - $versions[] = array('name' => 'Require Validated Email', - 'version' => STATUSNET_VERSION, - 'author' => 'Craig Andrews, Evan Prodromou, Brion Vibber', - 'homepage' => 'http://status.net/wiki/Plugin:RequireValidatedEmail', - 'rawdescription' => - _m('The Require Validated Email plugin disables posting for accounts that do not have a validated email address.')); + $versions[] = + array('name' => 'Require Validated Email', + 'version' => STATUSNET_VERSION, + 'author' => 'Craig Andrews, '. + 'Evan Prodromou, '. + 'Brion Vibber', + 'homepage' => + 'http://status.net/wiki/Plugin:RequireValidatedEmail', + 'rawdescription' => + _m('Disables posting without a validated email address.')); return true; } } From fa45805d6d7f42884b507361ba51028c5180f384 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 7 Oct 2010 10:22:57 -0400 Subject: [PATCH 15/54] Events for showing the notice form --- EVENTS.txt | 6 ++++++ lib/action.php | 5 ++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/EVENTS.txt b/EVENTS.txt index 2496416173..e5cafa8573 100644 --- a/EVENTS.txt +++ b/EVENTS.txt @@ -1136,3 +1136,9 @@ StartShowFeedLink: before showing an individual feed item EndShowFeedLink: after showing an individual feed - $action: action being executed - $feed: feed to show + +StartShowNoticeForm: before showing the notice form (before
) +- $action: action being executed + +EndShowNoticeForm: after showing the notice form (after ) +- $action: action being executed diff --git a/lib/action.php b/lib/action.php index ddc058d418..008f29d03e 100644 --- a/lib/action.php +++ b/lib/action.php @@ -397,7 +397,10 @@ class Action extends HTMLOutputter // lawsuit Event::handle('EndShowSiteNotice', array($this)); } if (common_logged_in()) { - $this->showNoticeForm(); + if (Event::handle('StartShowNoticeForm', array($this))) { + $this->showNoticeForm(); + Event::handle('EndShowNoticeForm', array($this)); + } } else { $this->showAnonymousMessage(); } From 1e3d5f80258811ce1c2154fcd971297e24264894 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 7 Oct 2010 10:32:29 -0400 Subject: [PATCH 16/54] hide notice form if not able to post --- .../RequireValidatedEmailPlugin.php | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php b/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php index a7d7874f1f..6c0ef37d51 100644 --- a/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php +++ b/plugins/RequireValidatedEmail/RequireValidatedEmailPlugin.php @@ -216,4 +216,23 @@ class RequireValidatedEmailPlugin extends Plugin _m('Disables posting without a validated email address.')); return true; } + + /** + * Hide the notice form if the user isn't able to post. + * + * @param Action $action action being shown + * + * @return boolean hook value + */ + + function onStartShowNoticeForm($action) + { + $user = common_current_user(); + if (!empty($user)) { // it's a remote notice + if (!$this->validated($user)) { + return false; + } + } + return true; + } } From 8658e4f8c4186574ac6503428be3ed534290387d Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 11:01:17 -0700 Subject: [PATCH 17/54] Use 7 digits for oob OAuth pin instead of 6 --- lib/apioauthstore.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php index 4d141286bf..7a4fe8db52 100644 --- a/lib/apioauthstore.php +++ b/lib/apioauthstore.php @@ -205,7 +205,7 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore if ($callback === 'oob') { // six digit pin - $t->verifier = mt_rand(0, 999999); + $t->verifier = mt_rand(0, 9999999); } else { $t->verifier = common_good_rand(8); } From 9d5224e2b4ae8dc8e8ac8b2328db77a6c01fc232 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 11:56:49 -0700 Subject: [PATCH 18/54] Change temp credential test script to use POST instead of GET (more useful for testing in general) --- tests/oauth/getrequesttoken.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/oauth/getrequesttoken.php b/tests/oauth/getrequesttoken.php index 045d597166..73f502af30 100755 --- a/tests/oauth/getrequesttoken.php +++ b/tests/oauth/getrequesttoken.php @@ -47,7 +47,7 @@ try { $req = OAuthRequest::from_consumer_and_token( $testConsumer, null, - "GET", + "POST", $requestTokenUrl, $params ); @@ -56,6 +56,7 @@ try { } catch (Exception $e) { // oh noez print $e->getMessage(); + print "OAuth Request:\n"; var_dump($req); exit(1); } @@ -93,5 +94,5 @@ function httpRequest($url) ) ); - return $request->get($url); + return $request->post($url); } From 82a0a1a74b9452fbc122fed9ee9c4e0a86b7a79e Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 12:01:00 -0700 Subject: [PATCH 19/54] More OAuthy name for temp credentials fetching test script --- tests/oauth/{getrequesttoken.php => fetch_temp_creds.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/oauth/{getrequesttoken.php => fetch_temp_creds.php} (100%) diff --git a/tests/oauth/getrequesttoken.php b/tests/oauth/fetch_temp_creds.php similarity index 100% rename from tests/oauth/getrequesttoken.php rename to tests/oauth/fetch_temp_creds.php From b8f2cc4e6f121f4ffacefb6fe632beb3b25eb126 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 13:51:47 -0700 Subject: [PATCH 20/54] Make the verifier pin display a little nicer --- actions/apioauthauthorize.php | 25 ++++++++++++++++--------- actions/apioauthpin.php | 16 +++++++--------- tests/oauth/fetch_temp_creds.php | 2 +- 3 files changed, 24 insertions(+), 19 deletions(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index 6772052f2c..d0b6211400 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -449,21 +449,28 @@ class ApiOauthAuthorizeAction extends Action function showAuthorized() { + $title = sprintf( + _("You have successfully authorized %s."), + $this->app->name + ); + + $msg = sprintf( + _('Please return to %s and enter the following security code to complete the process.'), + $this->app->name + ); if ($this->reqToken->verified_callback == 'oob') { - - $pin = new ApiOauthPinAction($this->reqToken->verifier); + $pin = new ApiOauthPinAction($title, $msg, $this->reqToken->verifier); $pin->showPage(); - } else { + // NOTE: This should probably never happen; trhow an error instead? + $info = new InfoAction( - _("Authorization succeeded."), - sprintf( - _('The request token %s has been authorized. Please exchange it for an access token using this verifier: %s'), - $this->oauthTokenParam, - $this->reqToken->verifier - ) + $title, + $msg, + $this->oauthTokenParam, + $this->reqToken->verifier ); $info->showPage(); diff --git a/actions/apioauthpin.php b/actions/apioauthpin.php index 5a88b5e590..5e6713a548 100644 --- a/actions/apioauthpin.php +++ b/actions/apioauthpin.php @@ -36,6 +36,8 @@ require_once INSTALLDIR . '/lib/info.php'; /** * Class for displaying an OAuth verifier pin * + * XXX: I'm pretty sure we don't need to check the logged in state here. -- Zach + * * @category Action * @package StatusNet * @author Zach Copley @@ -45,16 +47,13 @@ require_once INSTALLDIR . '/lib/info.php'; class ApiOauthPinAction extends InfoAction { - - function __construct($verifier) + function __construct($title, $message, $verifier) { $this->verifier = $verifier; - $title = _('Authorization succeeded.'); - parent::__construct($title, $title); + $this->title = $title; + parent::__construct($title, $message); } - // TODO: Check for logged in state! - /** * Display content. * @@ -62,8 +61,7 @@ class ApiOauthPinAction extends InfoAction */ function showContent() { - // XXX: make this much nicer - $this->element('div', array('class' => 'info'), $this->verifier); + $this->element('div', array('class' => 'info'), $this->message); + $this->element('div', array('id' => 'oauth_pin'), $this->verifier); } - } diff --git a/tests/oauth/fetch_temp_creds.php b/tests/oauth/fetch_temp_creds.php index 73f502af30..63ca351cd8 100755 --- a/tests/oauth/fetch_temp_creds.php +++ b/tests/oauth/fetch_temp_creds.php @@ -56,7 +56,7 @@ try { } catch (Exception $e) { // oh noez print $e->getMessage(); - print "OAuth Request:\n"; + print "\nOAuth Request:\n"; var_dump($req); exit(1); } From f8808b076108bbc80e2e23e795c34bcdf817a183 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 14:17:56 -0700 Subject: [PATCH 21/54] Added a comment about an open question: Should we allow pin-based workflow for clients registered as web applications? --- actions/apioauthauthorize.php | 5 ++++- actions/apioauthrequesttoken.php | 7 ++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/actions/apioauthauthorize.php b/actions/apioauthauthorize.php index d0b6211400..ea5c30c2ac 100644 --- a/actions/apioauthauthorize.php +++ b/actions/apioauthauthorize.php @@ -464,7 +464,10 @@ class ApiOauthAuthorizeAction extends Action $pin->showPage(); } else { - // NOTE: This should probably never happen; trhow an error instead? + // NOTE: This would only happen if an application registered as + // a web application but sent in 'oob' for the oauth_callback + // parameter. Usually web apps will send in a callback and + // not use the pin-based workflow. $info = new InfoAction( $title, diff --git a/actions/apioauthrequesttoken.php b/actions/apioauthrequesttoken.php index 4f4c2c8fb2..825460f93c 100644 --- a/actions/apioauthrequesttoken.php +++ b/actions/apioauthrequesttoken.php @@ -87,7 +87,7 @@ class ApiOauthRequestTokenAction extends ApiOauthAction try { - $req = OAuthRequest::from_request(); + $req = OAuthRequest::from_request(); // verify callback if (!$this->verifyCallback($req->get_parameter('oauth_callback'))) { @@ -137,6 +137,11 @@ class ApiOauthRequestTokenAction extends ApiOauthAction { if ($callback == "oob") { common_debug("OAuth request token requested for out of bounds client."); + + // XXX: Should we throw an error if a client is registered as a + // web application but requests the pin based workflow? For now I'm + // allowing the workflow to proceed and issuing a pin. --Zach + return true; } else { return Validate::uri( From 459727bd610df5e579311a1627ee9b0e93ac44b0 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 18:32:27 -0700 Subject: [PATCH 22/54] Update ApiOauthAccessTokenAction to OAuth 1.0a --- actions/apioauthaccesstoken.php | 56 ++++++++++++++++++++++++++------- lib/apioauthstore.php | 34 ++++++++++++-------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/actions/apioauthaccesstoken.php b/actions/apioauthaccesstoken.php index 887df4c20d..b8b188b1dc 100644 --- a/actions/apioauthaccesstoken.php +++ b/actions/apioauthaccesstoken.php @@ -2,7 +2,8 @@ /** * StatusNet, the distributed open-source microblogging tool * - * Exchange an authorized OAuth request token for an access token + * Action for getting OAuth token credentials (exchange an authorized + * request token for an access token) * * PHP version 5 * @@ -34,7 +35,8 @@ if (!defined('STATUSNET')) { require_once INSTALLDIR . '/lib/apioauth.php'; /** - * Exchange an authorized OAuth request token for an access token + * Action for getting OAuth token credentials (exchange an authorized + * request token for an access token) * * @category API * @package StatusNet @@ -45,6 +47,8 @@ require_once INSTALLDIR . '/lib/apioauth.php'; class ApiOauthAccessTokenAction extends ApiOauthAction { + protected $reqToken = null; + protected $verifier = null; /** * Class handler. @@ -65,30 +69,58 @@ class ApiOauthAccessTokenAction extends ApiOauthAction $atok = null; + // XXX: Insist that oauth_token and oauth_verifier be populated? + // Spec doesn't say they MUST be. + try { + $req = OAuthRequest::from_request(); + + $this->reqToken = $req->get_parameter('oauth_token'); + $this->verifier = $req->get_parameter('oauth_verifier'); + $atok = $server->fetch_access_token($req); } catch (OAuthException $e) { common_log(LOG_WARNING, 'API OAuthException - ' . $e->getMessage()); common_debug(var_export($req, true)); - $this->outputError($e->getMessage()); - return; + $code = $e->getCode(); + $this->clientError($e->getMessage(), empty($code) ? 401 : $code, 'text'); } if (empty($atok)) { - common_debug('couldn\'t get access token.'); - print "Token exchange failed. Has the request token been authorized?\n"; + + // Token exchange failed -- log it + + list($proxy, $ip) = common_client_ip(); + + $msg = sprintf( + 'API OAuth - Failure exchanging request token for access token, ' + . 'request token = %s, verifier = %s, IP = %s, proxy = %s', + $this->reqToken, + $this->verifier, + $ip, + $proxy + ); + + common_log(LOG_WARNING, $msg); + + print "Invalid request token or verifier."; + } else { - print $atok; + $this->showAccessToken($atok); } } - function outputError($msg) + /* + * Display OAuth token credentials + * + * @param OAuthToken token the access token + */ + + function showAccessToken($token) { - header('HTTP/1.1 401 Unauthorized'); - header('Content-Type: text/html; charset=utf-8'); - print $msg . "\n"; + header('Content-Type: application/x-www-form-urlencoded'); + print $token; } } - diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php index 7a4fe8db52..4b52ba1ba9 100644 --- a/lib/apioauthstore.php +++ b/lib/apioauthstore.php @@ -71,29 +71,33 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore } } - function new_access_token($token, $consumer) + function new_access_token($token, $consumer, $verifier) { - common_debug('new_access_token("'.$token->key.'","'.$consumer->key.'")', __FILE__); + common_debug( + 'new_access_token("' . $token->key . '","' . $consumer->key. '","' . $verifier . '")', + __FILE__ + ); $rt = new Token(); + $rt->consumer_key = $consumer->key; - $rt->tok = $token->key; - $rt->type = 0; // request + $rt->tok = $token->key; + $rt->type = 0; // request $app = Oauth_application::getByConsumerKey($consumer->key); + assert(!empty($app)); - if (empty($app)) { - common_debug("empty app!"); - } + if ($rt->find(true) && $rt->state == 1 && $rt->verifier == $verifier) { // authorized - if ($rt->find(true) && $rt->state == 1) { // authorized common_debug('request token found.', __FILE__); // find the associated user of the app $appUser = new Oauth_application_user(); + $appUser->application_id = $app->id; - $appUser->token = $rt->tok; + $appUser->token = $rt->tok; + $result = $appUser->find(true); if (!empty($result)) { @@ -106,10 +110,12 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore // go ahead and make the access token $at = new Token(); - $at->consumer_key = $consumer->key; - $at->tok = common_good_rand(16); - $at->secret = common_good_rand(16); - $at->type = 1; // access + $at->consumer_key = $consumer->key; + $at->tok = common_good_rand(16); + $at->secret = common_good_rand(16); + $at->type = 1; // access + $at->verifier = $verifier; + $at->verified_callback = $rt->verified_callback; // 1.0a $at->created = DB_DataObject_Cast::dateTime(); if (!$at->insert()) { @@ -217,4 +223,6 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore return new OAuthToken($t->tok, $t->secret); } } + + } From 70cad115734f0c34a5a2c7d6c8ce2492056a7a07 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 18:33:14 -0700 Subject: [PATCH 23/54] Update access token fetching test script to 1.0a --- tests/oauth/exchangetokens.php | 113 +++++++++++++++++++++---------- tests/oauth/fetch_temp_creds.php | 36 ++++++---- 2 files changed, 98 insertions(+), 51 deletions(-) diff --git a/tests/oauth/exchangetokens.php b/tests/oauth/exchangetokens.php index 2394826c7e..049c0cad07 100755 --- a/tests/oauth/exchangetokens.php +++ b/tests/oauth/exchangetokens.php @@ -24,82 +24,121 @@ require_once INSTALLDIR . '/extlib/OAuth.php'; $ini = parse_ini_file("oauth.ini"); -$test_consumer = new OAuthConsumer($ini['consumer_key'], $ini['consumer_secret']); +// Check to make sure we have everything we need from the ini file +foreach(array('consumer_key', 'consumer_secret', 'apiroot', 'access_token_url') as $inikey) { + if (empty($ini[$inikey])) { + print "You forgot to specify a $inikey in your oauth.ini file.\n"; + exit(1); + } +} -$at_endpoint = $ini['apiroot'] . $ini['access_token_url']; +$consumer = new OAuthConsumer($ini['consumer_key'], $ini['consumer_secret']); -$shortoptions = 't:s:'; -$longoptions = array('oauth_token=', 'token_secret='); +$endpoint = $ini['apiroot'] . $ini['access_token_url']; + +$shortoptions = 't:s:v:'; +$longoptions = array('oauth_token=', 'oauth_token_secret=', 'oauth_verifier='); $helptext = <<sign_request($hmac_method, $test_consumer, $rt); +try { -$r = httpRequest($req_req->to_url()); + $oauthReq = OAuthRequest::from_consumer_and_token( + $consumer, + $rtok, + "POST", + $endpoint, + $params + ); -common_debug("Exchange request token = " . var_export($rt, true)); -common_debug("Exchange tokens URL: " . $req_req->to_url()); + $oauthReq->sign_request($hmac_method, $consumer, $rtok); -$body = $r->getBody(); + $httpReq = httpRequest($endpoint, $oauthReq->to_postdata()); + $body = $httpReq->getBody(); -$token_stuff = array(); -parse_str($body, $token_stuff); +} catch (Exception $e) { + // oh noez + print $e->getMessage(); + print "\nOAuth Request:\n"; + var_dump($oauthReq); + exit(1); +} -print 'Access token : ' . $token_stuff['oauth_token'] . "\n"; -print 'Access token secret : ' . $token_stuff['oauth_token_secret'] . "\n"; +$tokenStuff = array(); +parse_str($body, $tokenStuff); -function httpRequest($url) +if (empty($tokenStuff['oauth_token']) || empty($tokenStuff['oauth_token_secret'])) { + print "Error! HTTP response body: $body\n"; + exit(1); +} + +print "Access Token\n"; +print ' - oauth_token = ' . $tokenStuff['oauth_token'] . "\n"; +print ' - oauth_token_secret = ' . $tokenStuff['oauth_token_secret'] . "\n"; + +function httpRequest($endpoint, $poststr) { $request = HTTPClient::start(); - $request->setConfig(array( - 'follow_redirects' => true, - 'connect_timeout' => 120, - 'timeout' => 120, - 'ssl_verify_peer' => false, - 'ssl_verify_host' => false - )); + $request->setConfig( + array( + 'follow_redirects' => true, + 'connect_timeout' => 120, + 'timeout' => 120, + 'ssl_verify_peer' => false, + 'ssl_verify_host' => false + ) + ); - return $request->get($url); + parse_str($poststr, $postdata); + return $request->post($endpoint, null, $postdata); } diff --git a/tests/oauth/fetch_temp_creds.php b/tests/oauth/fetch_temp_creds.php index 63ca351cd8..bea512a914 100755 --- a/tests/oauth/fetch_temp_creds.php +++ b/tests/oauth/fetch_temp_creds.php @@ -33,10 +33,10 @@ foreach(array('consumer_key', 'consumer_secret', 'apiroot', 'request_token_url') } } -$testConsumer = new OAuthConsumer($ini['consumer_key'], $ini['consumer_secret']); -$requestTokenUrl = $ini['apiroot'] . $ini['request_token_url']; -$parsed = parse_url($requestTokenUrl); -$params = array(); +$consumer = new OAuthConsumer($ini['consumer_key'], $ini['consumer_secret']); +$endpoint = $ini['apiroot'] . $ini['request_token_url']; +$parsed = parse_url($endpoint); +$params = array(); parse_str($parsed['query'], $params); $params['oauth_callback'] = 'oob'; // out-of-band @@ -45,14 +45,14 @@ $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); try { $req = OAuthRequest::from_consumer_and_token( - $testConsumer, + $consumer, null, "POST", - $requestTokenUrl, + $endpoint, $params ); - $req->sign_request($hmac_method, $testConsumer, NULL); - $r = httpRequest($req->to_url()); + $req->sign_request($hmac_method, $consumer, NULL); + $r = httpRequest($endpoint, $req->to_postdata()); } catch (Exception $e) { // oh noez print $e->getMessage(); @@ -69,18 +69,24 @@ parse_str($body, $tokenStuff); $tok = $tokenStuff['oauth_token']; $confirmed = $tokenStuff['oauth_callback_confirmed']; -if (empty($tokenStuff['oauth_token']) || empty($confirmed) || $confirmed != 'true') { - print "Error: $body\n"; +if (empty($tokenStuff['oauth_token']) + || empty($tokenStuff['oauth_token_secret']) + || empty($confirmed) + || $confirmed != 'true') +{ + print "Error! HTTP response body: $body\n"; exit(1); } $authurl = $ini['apiroot'] . $ini['authorize_url'] . '?oauth_token=' . $tok; -print "\nSuccess! "; -print "Authorize URL:\n\n$authurl\n\n"; +print "Request Token\n"; +print ' - oauth_token = ' . $tokenStuff['oauth_token'] . "\n"; +print ' - oauth_token_secret = ' . $tokenStuff['oauth_token_secret'] . "\n"; +print "Authorize URL\n $authurl\n\n"; print "Now paste the Authorize URL into your browser and authorize your temporary credentials.\n"; -function httpRequest($url) +function httpRequest($endpoint, $poststr) { $request = HTTPClient::start(); @@ -94,5 +100,7 @@ function httpRequest($url) ) ); - return $request->post($url); + // Turn signed request query string back into an array + parse_str($poststr, $postdata); + return $request->post($endpoint, null, $postdata); } From 46de847ce0b72a85b96fcbea94624db98c265d45 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 18:41:34 -0700 Subject: [PATCH 24/54] Rename OAuth token credential fetching script --- tests/oauth/{exchangetokens.php => fetch_token_creds.php} | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) rename tests/oauth/{exchangetokens.php => fetch_token_creds.php} (96%) diff --git a/tests/oauth/exchangetokens.php b/tests/oauth/fetch_token_creds.php similarity index 96% rename from tests/oauth/exchangetokens.php rename to tests/oauth/fetch_token_creds.php index 049c0cad07..a508c7240c 100755 --- a/tests/oauth/exchangetokens.php +++ b/tests/oauth/fetch_token_creds.php @@ -40,8 +40,10 @@ $shortoptions = 't:s:v:'; $longoptions = array('oauth_token=', 'oauth_token_secret=', 'oauth_verifier='); $helptext = << Date: Thu, 7 Oct 2010 19:23:43 -0700 Subject: [PATCH 25/54] Some fixups to this the OAuth verify credentials test script --- tests/oauth/verifycreds.php | 76 ++++++++++++++++++++----------------- 1 file changed, 41 insertions(+), 35 deletions(-) diff --git a/tests/oauth/verifycreds.php b/tests/oauth/verifycreds.php index 873bdb8bdd..7eea6e7e74 100755 --- a/tests/oauth/verifycreds.php +++ b/tests/oauth/verifycreds.php @@ -22,15 +22,15 @@ define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..')); require_once INSTALLDIR . '/extlib/OAuth.php'; -$shortoptions = 'o:s:'; -$longoptions = array('oauth_token=', 'token_secret='); +$shortoptions = 't:s:'; +$longoptions = array('oauth_token=', 'oauth_token_secret='); $helptext = <<sign_request($hmac_method, $test_consumer, $at); + $hmac_method = new OAuthSignatureMethod_HMAC_SHA1(); -$r = httpRequest($req_req->to_url()); + $oauthReq = OAuthRequest::from_consumer_and_token( + $consumer, + $atok, + "GET", + $endpoint, + $params + ); -$body = $r->getBody(); + $oauthReq->sign_request($hmac_method, $consumer, $atok); -print "$body\n"; + $httpReq = httpRequest($oauthReq->to_url()); -//print $req_req->to_url() . "\n\n"; +} catch (Exception $e) { + print "Error! HTTP response body: " . $httpReq->getBody(); + exit(1); +} + +print $httpReq->getBody(); function httpRequest($url) { $request = HTTPClient::start(); - $request->setConfig(array( - 'follow_redirects' => true, - 'connect_timeout' => 120, - 'timeout' => 120, - 'ssl_verify_peer' => false, - 'ssl_verify_host' => false - )); + $request->setConfig( + array( + 'follow_redirects' => true, + 'connect_timeout' => 120, + 'timeout' => 120, + 'ssl_verify_peer' => false, + 'ssl_verify_host' => false + ) + ); return $request->get($url); } - From be1668a1bd8436952bd9ee36ed710fae9834643f Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 19:24:24 -0700 Subject: [PATCH 26/54] Renamed the OAuth verify credentials test script --- tests/oauth/{verifycreds.php => oauth_verify_creds.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/oauth/{verifycreds.php => oauth_verify_creds.php} (100%) diff --git a/tests/oauth/verifycreds.php b/tests/oauth/oauth_verify_creds.php similarity index 100% rename from tests/oauth/verifycreds.php rename to tests/oauth/oauth_verify_creds.php From 3e0a1e3b884f31473383825fa9b54ab4e3e7b578 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 19:40:31 -0700 Subject: [PATCH 27/54] Some fixups --- tests/oauth/statusupdate.php | 75 ++++++++++++++++++++---------------- 1 file changed, 42 insertions(+), 33 deletions(-) diff --git a/tests/oauth/statusupdate.php b/tests/oauth/statusupdate.php index 4aa230e280..5e9d2a7ab0 100644 --- a/tests/oauth/statusupdate.php +++ b/tests/oauth/statusupdate.php @@ -22,16 +22,16 @@ define('INSTALLDIR', realpath(dirname(__FILE__) . '/../..')); require_once INSTALLDIR . '/extlib/OAuth.php'; -$shortoptions = 'o:s:u:'; +$shortoptions = 't:s:u:'; $longoptions = array('oauth_token=', 'token_secret=', 'update='); $helptext = <<sign_request($hmac_method, $test_consumer, $at); +try { -$r = httpRequest($req_req->to_url()); + $oauthReq = OAuthRequest::from_consumer_and_token( + $consumer, + $atok, + 'POST', + $endpoint, + $params + ); -$body = $r->getBody(); + $oauthReq->sign_request($hmac_method, $consumer, $atok); -print "$body\n"; + $httpReq = httpRequest($endpoint, $oauthReq->to_postdata()); -//print $req_req->to_url() . "\n\n"; + print $httpReq->getBody(); -function httpRequest($url) +} catch (Exception $e) { + print "Error! . $e->getMessage() . 'HTTP reponse body: " . $httpReq->getBody(); + exit(1); +} + +function httpRequest($endpoint, $poststr) { $request = HTTPClient::start(); - $request->setConfig(array( - 'follow_redirects' => true, - 'connect_timeout' => 120, - 'timeout' => 120, - 'ssl_verify_peer' => false, - 'ssl_verify_host' => false - )); + $request->setConfig( + array( + 'follow_redirects' => true, + 'connect_timeout' => 120, + 'timeout' => 120, + 'ssl_verify_peer' => false, + 'ssl_verify_host' => false + ) + ); - return $request->post($url); + // Turn signed request query string back into an array + parse_str($poststr, $postdata); + return $request->post($endpoint, null, $postdata); } From 626f3066002c707e11befcbba84aa7f3b372fd0c Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 19:41:05 -0700 Subject: [PATCH 28/54] Rename OAuth status update script --- tests/oauth/{statusupdate.php => oauth_post_notice.php} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/oauth/{statusupdate.php => oauth_post_notice.php} (100%) diff --git a/tests/oauth/statusupdate.php b/tests/oauth/oauth_post_notice.php similarity index 100% rename from tests/oauth/statusupdate.php rename to tests/oauth/oauth_post_notice.php From 590d96f70e79e371610633e48497e14b5e6bc445 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 19:43:55 -0700 Subject: [PATCH 29/54] Rename oauth.ini example to oauth.ini.sample --- tests/oauth/{oauth.ini => oauth.ini.sample} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/oauth/{oauth.ini => oauth.ini.sample} (100%) diff --git a/tests/oauth/oauth.ini b/tests/oauth/oauth.ini.sample similarity index 100% rename from tests/oauth/oauth.ini rename to tests/oauth/oauth.ini.sample From baa8ae778a01326927394818335d410233d24c49 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Thu, 7 Oct 2010 19:46:46 -0700 Subject: [PATCH 30/54] Update OAuth test script README --- tests/oauth/README | 162 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 150 insertions(+), 12 deletions(-) diff --git a/tests/oauth/README b/tests/oauth/README index dd76feb0c6..13f1d0c039 100644 --- a/tests/oauth/README +++ b/tests/oauth/README @@ -1,22 +1,160 @@ Some very rough test scripts for hitting up the OAuth endpoints. -Note: this works best if you register an OAuth application, leaving -the callback URL blank. +These instructions assume you understand the basics of how OAuth +works. You may want to read up about it first. Here are some good +resources for learning about OAuth: -Put your instance info and consumer key and secret in oauth.ini + http://hueniverse.com/oauth/ + http://tools.ietf.org/html/rfc5849 -Example usage: --------------- +To use these scripts (and OAuth in general) first you will need to +register and OAuth client application with your StatusNet instance: -php getrequesttoken.php + http://example.status.net/settings/oauthapps -Gets a request token, token secret and a url to authorize it. Once -you authorize the request token you can exchange it for an access token... +oauth.ini +--------- -php exchangetokens.php --oauth_token=b9a79548a88c1aa9a5bea73103c6d41d --token_secret=4a47d9337fc0202a14ab552e17a3b657 +Using oauth.ini.sample as a guide, put your StatusNet OAuth endpoints +and consumer key and secret in a file called oauth.ini and save it +in the same directory as these scripts. -Once you have your access token, go ahead and try a protected API -resource: +fetch_temp_creds.php +-------------------- -php verifycreds.php --oauth_token=cf2de7665f0dda0a82c2dc39b01be7f9 --token_secret=4524c3b712200138e1a4cff2e9ca83d8 +Will fetch a request token, token secret and a URL to authorize the +token. Once you authorize the request token, you can exchange it +for an access token. + +example usage: + + $ php fetch_temp_creds.php + Request Token + - oauth_token = 89d481e376edc622f08da5791e6a4446 + - oauth_token_secret = 6d028bcd1ea125cbed7da2f254219885 + Authorize URL + http://example.status.net/api/oauth/authorize?oauth_token=89d481e376edc622f08da5791e6a4446 + + Now paste the Authorize URL into your browser and authorize your temporary credentials. + +fetch_token_creds.php +--------------------- + +After you have authorized your request token, you will be presented +with a verifier code, or pin, in your browser, which you will need +to get an access token. Make sure you copy it into a text buffer +or write it down or something. Then call fetch_token_credentials.php +to exchange your temporary credentials for real token credentials. + +example usage: + + $ php fetch_token_creds.php -t 89d481e376edc622f08da5791e6a4446 -s 6d028bcd1ea125cbed7da2f254219885 -v 305162 + Access Token + - oauth_token = 9b354df102d8e2b4621122c85d8d045c + - oauth_token_secret = 1800a88f1574b47d595214a74e5b1ec5 + + +oauth_verify_credentials.php +---------------------------- + +Now you should have real token credentials (an OAuth access token) +and you can access protected API resources. This is an example +script that calls /api/account/verify_credentials.xml. + +example usage: + + $ php oauth_verify_creds.php -t 80305cd15c5c69834364ac02d7f9178c -s 673e3b2978b1b92c8edbfe172505fee1 + + + 23 + zach + zach + + + http://example.status.net/theme/default/default-avatar-stream.png + + false + 0 + + + + + + 0 + Thu Sep 30 23:11:00 +0000 2010 + 0 + 0 + UTC + + false + 4 + true + false + true + + gar + false + Wed Oct 06 23:40:14 +0000 2010 + + web + 7 + + + + false + gar + + http://example.status.net/statusnet/zach + + +oauth_post_notice.php +--------------------- + +This is another test script that lets you post a notice via OAuth. + +example usage: + + $ php oauth_post_notice.php -t 80305cd15c5c69834364ac02d7f9178c -s 673e3b2978b1b92c8edbfe172505fee1 -u 'Test test test...' + + + Test test test... + false + Fri Oct 08 02:37:35 +0000 2010 + + <a href="http://banana.com" rel="nofollow">Banana</a> + 8 + + + + false + + 23 + zach + zach + + + http://example.status.net/statusnet/theme/default/default-avatar-stream.png + + false + 0 + + + + + + 0 + Thu Sep 30 23:11:00 +0000 2010 + 0 + 0 + UTC + + false + 5 + true + false + true + http://example.status.net/statusnet/zach + + Test test test... + From a77bc113269e88d264075d331570dccf662b9640 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 12 Oct 2010 12:25:34 -0700 Subject: [PATCH 31/54] Output a log message when issuing a request token --- actions/apioauthrequesttoken.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/actions/apioauthrequesttoken.php b/actions/apioauthrequesttoken.php index 825460f93c..478d2dbfc4 100644 --- a/actions/apioauthrequesttoken.php +++ b/actions/apioauthrequesttoken.php @@ -100,6 +100,16 @@ class ApiOauthRequestTokenAction extends ApiOauthAction // check signature and issue a new request token $token = $server->fetch_request_token($req); + common_log( + LOG_INFO, + sprintf( + "API OAuth - Issued request token %s for consumer %s with oauth_callback %s", + $token->key, + $req->get_parameter('oauth_consumer_key'), + "'" . $req->get_parameter('oauth_callback') ."'" + ) + ); + // return token to the client $this->showRequestToken($token); From d8e06e66e9d4e3312f8d645a13680dc8046ca3a2 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 12 Oct 2010 16:19:53 -0700 Subject: [PATCH 32/54] Print a proper error message --- actions/apioauthaccesstoken.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/apioauthaccesstoken.php b/actions/apioauthaccesstoken.php index b8b188b1dc..663a7a2bb6 100644 --- a/actions/apioauthaccesstoken.php +++ b/actions/apioauthaccesstoken.php @@ -105,7 +105,7 @@ class ApiOauthAccessTokenAction extends ApiOauthAction common_log(LOG_WARNING, $msg); - print "Invalid request token or verifier."; + $this->clientError(_("Invalid request token or verifier.", 400, 'text')); } else { $this->showAccessToken($atok); From 5270e931311af0f644ebd4302833ad3bb89b81d4 Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 12 Oct 2010 16:20:09 -0700 Subject: [PATCH 33/54] Spelling - OAuth not Oath --- lib/apioauthstore.php | 2 +- lib/connectsettingsaction.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/apioauthstore.php b/lib/apioauthstore.php index 4b52ba1ba9..f3bf0b8576 100644 --- a/lib/apioauthstore.php +++ b/lib/apioauthstore.php @@ -101,7 +101,7 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore $result = $appUser->find(true); if (!empty($result)) { - common_debug("Oath app user found."); + common_debug("Ouath app user found."); } else { common_debug("Oauth app user not found. app id $app->id token $rt->tok"); return null; diff --git a/lib/connectsettingsaction.php b/lib/connectsettingsaction.php index bb2e86176a..325276c5fc 100644 --- a/lib/connectsettingsaction.php +++ b/lib/connectsettingsaction.php @@ -116,9 +116,9 @@ class ConnectSettingsNav extends Widget } $menu['oauthconnectionssettings'] = array( - // TRANS: Menu item for OAth connection settings. + // TRANS: Menu item for OuAth connection settings. _m('MENU','Connections'), - // TRANS: Tooltip for connected applications (Connections through OAth) menu item. + // TRANS: Tooltip for connected applications (Connections through OAuth) menu item. _('Authorized connected applications') ); From 2291d68e7019d04ae57f8cb408c42fc775a9a9ff Mon Sep 17 00:00:00 2001 From: Zach Copley Date: Tue, 12 Oct 2010 17:54:42 -0700 Subject: [PATCH 34/54] Default to ssl in oauth tests examples config --- tests/oauth/oauth.ini.sample | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/oauth/oauth.ini.sample b/tests/oauth/oauth.ini.sample index 16b747fe43..927620e2f8 100644 --- a/tests/oauth/oauth.ini.sample +++ b/tests/oauth/oauth.ini.sample @@ -1,5 +1,5 @@ ; Setup OAuth info here -apiroot = "http://YOURSTATUSNET/api" +apiroot = "https://YOURSTATUSNET/api" request_token_url = "/oauth/request_token" authorize_url = "/oauth/authorize" From 90e54f6cf08f74478d152c329363bcd77eea6329 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 13 Oct 2010 10:00:16 -0700 Subject: [PATCH 35/54] ModHelper -> silence only, don't let them sandbox (it's too confusing atm without a good audit trail) --- plugins/ModHelper/ModHelperPlugin.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/ModHelper/ModHelperPlugin.php b/plugins/ModHelper/ModHelperPlugin.php index 78818ebc87..2d0ae5b029 100644 --- a/plugins/ModHelper/ModHelperPlugin.php +++ b/plugins/ModHelper/ModHelperPlugin.php @@ -41,7 +41,7 @@ class ModHelperPlugin extends Plugin function onUserRightsCheck($profile, $right, &$result) { - if ($right == Right::SILENCEUSER || $right == Right::SANDBOXUSER) { + if ($right == Right::SILENCEUSER) { // Hrm.... really we should confirm that the *other* user isn't privleged. :) if ($profile->hasRole('modhelper')) { $result = true; From bca215563ff7aaee5c253b3a55cadc3b02116ef9 Mon Sep 17 00:00:00 2001 From: Brion Vibber Date: Wed, 13 Oct 2010 11:04:41 -0700 Subject: [PATCH 36/54] Clean up remote avatar temporary files if we fail before saving them into avatars directory (OMB core, OStatus, WikiHowProfile, YammerImport) --- lib/oauthstore.php | 19 +++++++---- plugins/OStatus/classes/Ostatus_profile.php | 33 +++++++++++-------- .../WikiHowProfile/WikiHowProfilePlugin.php | 31 +++++++++-------- plugins/YammerImport/lib/yammerimporter.php | 27 ++++++++------- 4 files changed, 65 insertions(+), 45 deletions(-) diff --git a/lib/oauthstore.php b/lib/oauthstore.php index 537667678b..1c8e725009 100644 --- a/lib/oauthstore.php +++ b/lib/oauthstore.php @@ -328,13 +328,18 @@ class StatusNetOAuthDataStore extends OAuthDataStore function add_avatar($profile, $url) { $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); - copy($url, $temp_filename); - $imagefile = new ImageFile($profile->id, $temp_filename); - $filename = Avatar::filename($profile->id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); + try { + copy($url, $temp_filename); + $imagefile = new ImageFile($profile->id, $temp_filename); + $filename = Avatar::filename($profile->id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + } catch (Exception $e) { + unlink($temp_filename); + throw $e; + } return $profile->setOriginal($filename); } diff --git a/plugins/OStatus/classes/Ostatus_profile.php b/plugins/OStatus/classes/Ostatus_profile.php index 47aee15f8a..03fcb71df0 100644 --- a/plugins/OStatus/classes/Ostatus_profile.php +++ b/plugins/OStatus/classes/Ostatus_profile.php @@ -1053,22 +1053,27 @@ class Ostatus_profile extends Memcached_DataObject // @fixme this should be better encapsulated // ripped from oauthstore.php (for old OMB client) $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); - if (!copy($url, $temp_filename)) { - throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url)); - } + try { + if (!copy($url, $temp_filename)) { + throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url)); + } - if ($this->isGroup()) { - $id = $this->group_id; - } else { - $id = $this->profile_id; + if ($this->isGroup()) { + $id = $this->group_id; + } else { + $id = $this->profile_id; + } + // @fixme should we be using different ids? + $imagefile = new ImageFile($id, $temp_filename); + $filename = Avatar::filename($id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + } catch (Exception $e) { + unlink($temp_filename); + throw $e; } - // @fixme should we be using different ids? - $imagefile = new ImageFile($id, $temp_filename); - $filename = Avatar::filename($id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); // @fixme hardcoded chmod is lame, but seems to be necessary to // keep from accidentally saving images from command-line (queues) // that can't be read from web server, which causes hard-to-notice diff --git a/plugins/WikiHowProfile/WikiHowProfilePlugin.php b/plugins/WikiHowProfile/WikiHowProfilePlugin.php index fa683c4836..753dff5a37 100644 --- a/plugins/WikiHowProfile/WikiHowProfilePlugin.php +++ b/plugins/WikiHowProfile/WikiHowProfilePlugin.php @@ -174,20 +174,25 @@ class WikiHowProfilePlugin extends Plugin // @fixme this should be better encapsulated // ripped from OStatus via oauthstore.php (for old OMB client) $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); - if (!copy($url, $temp_filename)) { - throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url)); + try { + if (!copy($url, $temp_filename)) { + throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url)); + } + + $profile = $user->getProfile(); + $id = $profile->id; + // @fixme should we be using different ids? + + $imagefile = new ImageFile($id, $temp_filename); + $filename = Avatar::filename($id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + } catch (Exception $e) { + unlink($temp_filename); + throw $e; } - - $profile = $user->getProfile(); - $id = $profile->id; - // @fixme should we be using different ids? - - $imagefile = new ImageFile($id, $temp_filename); - $filename = Avatar::filename($id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); $profile->setOriginal($filename); } } diff --git a/plugins/YammerImport/lib/yammerimporter.php b/plugins/YammerImport/lib/yammerimporter.php index 80cbcff8e7..93bc96d529 100644 --- a/plugins/YammerImport/lib/yammerimporter.php +++ b/plugins/YammerImport/lib/yammerimporter.php @@ -436,18 +436,23 @@ class YammerImporter // @fixme this should be better encapsulated // ripped from oauthstore.php (for old OMB client) $temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar'); - if (!copy($url, $temp_filename)) { - throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url)); - } + try { + if (!copy($url, $temp_filename)) { + throw new ServerException(sprintf(_m("Unable to fetch avatar from %s."), $url)); + } - $id = $dest->id; - // @fixme should we be using different ids? - $imagefile = new ImageFile($id, $temp_filename); - $filename = Avatar::filename($id, - image_type_to_extension($imagefile->type), - null, - common_timestamp()); - rename($temp_filename, Avatar::path($filename)); + $id = $dest->id; + // @fixme should we be using different ids? + $imagefile = new ImageFile($id, $temp_filename); + $filename = Avatar::filename($id, + image_type_to_extension($imagefile->type), + null, + common_timestamp()); + rename($temp_filename, Avatar::path($filename)); + } catch (Exception $e) { + unlink($temp_filename); + throw $e; + } // @fixme hardcoded chmod is lame, but seems to be necessary to // keep from accidentally saving images from command-line (queues) // that can't be read from web server, which causes hard-to-notice From 76038fe20c7ebd732ffbc659827ab812ee5a4b6e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Wed, 13 Oct 2010 22:44:06 -0400 Subject: [PATCH 37/54] better deletion of related objects in User_group::delete() --- classes/User_group.php | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/classes/User_group.php b/classes/User_group.php index 1f70057852..7d6e219148 100644 --- a/classes/User_group.php +++ b/classes/User_group.php @@ -559,16 +559,27 @@ class User_group extends Memcached_DataObject function delete() { if ($this->id) { + // Safe to delete in bulk for now + $related = array('Group_inbox', 'Group_block', 'Group_member', 'Related_group'); + Event::handle('UserGroupDeleteRelated', array($this, &$related)); + foreach ($related as $cls) { + $inst = new $cls(); $inst->group_id = $this->id; - $inst->delete(); + + if ($inst->find()) { + while ($inst->fetch()) { + $dup = clone($inst); + $dup->delete(); + } + } } // And related groups in the other direction... @@ -584,6 +595,10 @@ class User_group extends Memcached_DataObject if ($local) { $local->delete(); } + + // blow the cached ids + self::blow('user_group:notice_ids:%d', $this->id); + } else { common_log(LOG_WARN, "Ambiguous user_group->delete(); skipping related tables."); } From cef10c7167dd9c6183b52a09dc9baefbf0b433cc Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 00:16:23 -0400 Subject: [PATCH 38/54] add static method StatusNet::isHTTPS() --- lib/statusnet.php | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/lib/statusnet.php b/lib/statusnet.php index 7cb831696b..301994508d 100644 --- a/lib/statusnet.php +++ b/lib/statusnet.php @@ -169,7 +169,6 @@ class StatusNet return $sites; } - /** * Fire initialization events for all instantiated plugins. */ @@ -220,7 +219,7 @@ class StatusNet { return self::$is_api; } - + public function setApi($mode) { self::$is_api = $mode; @@ -368,6 +367,18 @@ class StatusNet } } } + + /** + * Are we running from the web with HTTPS? + * + * @return boolean true if we're running with HTTPS; else false + */ + + static function isHTTPS() + { + // There are some exceptions to this; add them here! + return $_SERVER['HTTPS']; + } } class NoConfigException extends Exception From 40c64388e6b0a768dcfa4aa003ba5114be070c2a Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 00:31:13 -0400 Subject: [PATCH 39/54] try and show an SSL image for the creative commons image --- lib/action.php | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/lib/action.php b/lib/action.php index 008f29d03e..b05770b152 100644 --- a/lib/action.php +++ b/lib/action.php @@ -894,8 +894,26 @@ class Action extends HTMLOutputter // lawsuit case 'cc': // fall through default: $this->elementStart('p'); + + $image = common_config('license', 'image'); + $sslimage = common_config('license', 'sslimage'); + + if (StatusNet::isHTTPS()) { + if (!empty($sslimage)) { + $url = $sslimage; + } else if (preg_match('#^http://i.creativecommons.org/#', $image)) { + // CC support HTTPS on their images + $url = preg_replace('/^http/', 'https', $image); + } else { + // Better to show mixed content than no content + $url = $image; + } + } else { + $url = $image; + } + $this->element('img', array('id' => 'license_cc', - 'src' => common_config('license', 'image'), + 'src' => $url, 'alt' => common_config('license', 'title'), 'width' => '80', 'height' => '15')); From d91f894ccbeed6612727c3fb3bffa3504a14ecea Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 00:46:32 -0400 Subject: [PATCH 40/54] try to show HTTPS-encrypted theme files for HTTPS-encrypted pages --- lib/theme.php | 93 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 38 deletions(-) diff --git a/lib/theme.php b/lib/theme.php index 992fce870e..500e168fb1 100644 --- a/lib/theme.php +++ b/lib/theme.php @@ -38,7 +38,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) { * Themes are directories with some expected sub-directories and files * in them. They're found in either local/theme (for locally-installed themes) * or theme/ subdir of installation dir. - * + * * Note that the 'local' directory can be overridden as $config['local']['path'] * and $config['local']['dir'] etc. * @@ -104,56 +104,73 @@ class Theme /** * Build a full URL to the given theme's base directory, possibly * using an offsite theme server path. - * + * * @param string $group configuration section name to pull paths from * @param string $fallbackSubdir default subdirectory under INSTALLDIR * @param string $name theme name - * + * * @return string URL - * + * * @todo consolidate code with that for other customizable paths */ protected function relativeThemePath($group, $fallbackSubdir, $name) { - $path = common_config($group, 'path'); + if (StatusNet::isHTTPS()) { - if (empty($path)) { - $path = common_config('site', 'path') . '/'; - if ($fallbackSubdir) { - $path .= $fallbackSubdir . '/'; - } - } + $sslserver = common_config($group, 'sslserver'); - if ($path[strlen($path)-1] != '/') { - $path .= '/'; - } - - if ($path[0] != '/') { - $path = '/'.$path; - } - - $server = common_config($group, 'server'); - - if (empty($server)) { - $server = common_config('site', 'server'); - } - - $ssl = common_config($group, 'ssl'); - - if (is_null($ssl)) { // null -> guess - if (common_config('site', 'ssl') == 'always' && - !common_config($group, 'server')) { - $ssl = true; + if (empty($sslserver)) { + $server = common_config('site', 'server'); + $path = common_config('site', 'path') . '/'; + if ($fallbackSubdir) { + $path .= $fallbackSubdir . '/'; + } } else { - $ssl = false; + $server = $sslserver; + $path = common_config($group, 'sslpath'); + if (empty($path)) { + $path = common_config($group, 'path'); + } } + + if ($path[strlen($path)-1] != '/') { + $path .= '/'; + } + + if ($path[0] != '/') { + $path = '/'.$path; + } + + return 'https://'.$server.$path.$name; + + } else { + + $path = common_config($group, 'path'); + + if (empty($path)) { + $path = common_config('site', 'path') . '/'; + if ($fallbackSubdir) { + $path .= $fallbackSubdir . '/'; + } + } + + if ($path[strlen($path)-1] != '/') { + $path .= '/'; + } + + if ($path[0] != '/') { + $path = '/'.$path; + } + + $server = common_config($group, 'server'); + + if (empty($server)) { + $server = common_config('site', 'server'); + } + + return 'http://'.$server.$path.$name; } - - $protocol = ($ssl) ? 'https' : 'http'; - - $path = $protocol . '://'.$server.$path.$name; - return $path; } /** @@ -221,7 +238,7 @@ class Theme /** * Pull data from the theme's theme.ini file. * @fixme calling getFile will fall back to default theme, this may be unsafe. - * + * * @return associative array of strings */ function getMetadata() From ca0323d01b9c38fe864c8f902d770061e893708b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 00:50:26 -0400 Subject: [PATCH 41/54] use HTTPS for favicon.ico if page is HTTPS --- lib/action.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/action.php b/lib/action.php index b05770b152..fcac0e0a7f 100644 --- a/lib/action.php +++ b/lib/action.php @@ -175,8 +175,9 @@ class Action extends HTMLOutputter // lawsuit $this->element('link', array('rel' => 'shortcut icon', 'href' => Theme::path('favicon.ico'))); } else { + // favicon.ico should be HTTPS if the rest of the page is $this->element('link', array('rel' => 'shortcut icon', - 'href' => common_path('favicon.ico'))); + 'href' => common_path('favicon.ico', StatusNet::isHTTPS()))); } if (common_config('site', 'mobile')) { From 74c5aa8f9a37b76f6bee571dd9fccf6ac4fc9e90 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 00:59:53 -0400 Subject: [PATCH 42/54] consolidate some theme path code between ssl and non-ssl --- lib/theme.php | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/lib/theme.php b/lib/theme.php index 500e168fb1..669d9a19fd 100644 --- a/lib/theme.php +++ b/lib/theme.php @@ -134,15 +134,7 @@ class Theme } } - if ($path[strlen($path)-1] != '/') { - $path .= '/'; - } - - if ($path[0] != '/') { - $path = '/'.$path; - } - - return 'https://'.$server.$path.$name; + $protocol = 'https'; } else { @@ -155,22 +147,24 @@ class Theme } } - if ($path[strlen($path)-1] != '/') { - $path .= '/'; - } - - if ($path[0] != '/') { - $path = '/'.$path; - } - $server = common_config($group, 'server'); if (empty($server)) { $server = common_config('site', 'server'); } - return 'http://'.$server.$path.$name; + $protocol = 'http'; } + + if ($path[strlen($path)-1] != '/') { + $path .= '/'; + } + + if ($path[0] != '/') { + $path = '/'.$path; + } + + return $protocol.'://'.$server.$path.$name; } /** From ac63f8baae281bff47f325005b6621dc61a1a71b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 01:00:13 -0400 Subject: [PATCH 43/54] show HTTPS urls for JavaScript if HTTPS used for page --- lib/htmloutputter.php | 70 ++++++++++++++++++++++++++----------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index 44b0296046..8d3e815d33 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -352,22 +352,55 @@ class HTMLOutputter extends XMLOutputter */ function script($src, $type='text/javascript') { - if(Event::handle('StartScriptElement', array($this,&$src,&$type))) { + if (Event::handle('StartScriptElement', array($this,&$src,&$type))) { $url = parse_url($src); - if( empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) - { + if (empty($url['scheme']) && empty($url['host']) && empty($url['query']) && empty($url['fragment'])) { + + // XXX: this seems like a big assumption + if (strpos($src, 'plugins/') === 0 || strpos($src, 'local/') === 0) { $src = common_path($src) . '?version=' . STATUSNET_VERSION; - }else{ + } else { - $path = common_config('javascript', 'path'); + if (StatusNet::isHTTPS()) { - if (empty($path)) { - $path = common_config('site', 'path') . '/js/'; + $sslserver = common_config('javascript', 'sslserver'); + + if (empty($sslserver)) { + $server = common_config('site', 'server'); + $path = common_config('site', 'path') . '/js/'; + } else { + $server = $sslserver; + $path = common_config('javascript', 'sslpath'); + if (empty($path)) { + $path = common_config('javascript', 'path'); + } + } + + $protocol = 'https'; + + } else { + + $path = common_config('javascript', 'path'); + + if (empty($path)) { + $path = common_config('site', 'path') . '/'; + if ($fallbackSubdir) { + $path .= $fallbackSubdir . '/'; + } + } + + $server = common_config('javascript', 'server'); + + if (empty($server)) { + $server = common_config('site', 'server'); + } + + $protocol = 'http'; } if ($path[strlen($path)-1] != '/') { @@ -378,32 +411,13 @@ class HTMLOutputter extends XMLOutputter $path = '/'.$path; } - $server = common_config('javascript', 'server'); - - if (empty($server)) { - $server = common_config('site', 'server'); - } - - $ssl = common_config('javascript', 'ssl'); - - if (is_null($ssl)) { // null -> guess - if (common_config('site', 'ssl') == 'always' && - !common_config('javascript', 'server')) { - $ssl = true; - } else { - $ssl = false; - } - } - - $protocol = ($ssl) ? 'https' : 'http'; - $src = $protocol.'://'.$server.$path.$src . '?version=' . STATUSNET_VERSION; } } $this->element('script', array('type' => $type, - 'src' => $src), - ' '); + 'src' => $src), + ' '); Event::handle('EndScriptElement', array($this,$src,$type)); } From 7436e5d13e6bc242a93e2f6dc561c59f88de60ee Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 01:09:02 -0400 Subject: [PATCH 44/54] use HTTPS for scripts and stylesheets if the current page is HTTPS --- lib/htmloutputter.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index 8d3e815d33..f01f1814f0 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -362,7 +362,7 @@ class HTMLOutputter extends XMLOutputter if (strpos($src, 'plugins/') === 0 || strpos($src, 'local/') === 0) { - $src = common_path($src) . '?version=' . STATUSNET_VERSION; + $src = common_path($src, StatusNet::isHTTPS()) . '?version=' . STATUSNET_VERSION; } else { @@ -467,7 +467,7 @@ class HTMLOutputter extends XMLOutputter if(file_exists(Theme::file($src,$theme))){ $src = Theme::path($src, $theme); }else{ - $src = common_path($src); + $src = common_path($src, StatusNet::isHTTPS()); } $src.= '?version=' . STATUSNET_VERSION; } From aafd95dc0c487fd9aaba3ce991d001acc0d2c6eb Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 01:18:19 -0400 Subject: [PATCH 45/54] Design::url() will use HTTPS if page is HTTPS --- classes/Design.php | 51 ++++++++++++++++++++++++++++------------------ 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/classes/Design.php b/classes/Design.php index ff44e01096..50712ce8b5 100644 --- a/classes/Design.php +++ b/classes/Design.php @@ -139,7 +139,37 @@ class Design extends Memcached_DataObject static function url($filename) { - $path = common_config('background', 'path'); + if (StatusNet::isHTTPS()) { + + $sslserver = common_config('background', 'sslserver'); + + if (empty($sslserver)) { + // XXX: this assumes that background dir == site dir + /background/ + // not true if there's another server + $server = common_config('site', 'server'); + $path = common_config('site', 'path') . '/background/'; + } else { + $server = $sslserver; + $path = common_config('background', 'sslpath'); + if (empty($path)) { + $path = common_config('background', 'path'); + } + } + + $protocol = 'https'; + + } else { + + $path = common_config('background', 'path'); + + $server = common_config('background', 'server'); + + if (empty($server)) { + $server = common_config('site', 'server'); + } + + $protocol = 'http'; + } if ($path[strlen($path)-1] != '/') { $path .= '/'; @@ -149,25 +179,6 @@ class Design extends Memcached_DataObject $path = '/'.$path; } - $server = common_config('background', 'server'); - - if (empty($server)) { - $server = common_config('site', 'server'); - } - - $ssl = common_config('background', 'ssl'); - - if (is_null($ssl)) { // null -> guess - if (common_config('site', 'ssl') == 'always' && - !common_config('background', 'server')) { - $ssl = true; - } else { - $ssl = false; - } - } - - $protocol = ($ssl) ? 'https' : 'http'; - return $protocol.'://'.$server.$path.$filename; } From 7fb765b2cb0be47236ad3fbf625f908dd5da8f47 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 01:23:12 -0400 Subject: [PATCH 46/54] document sslserver and sslpath configuration options --- README | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README b/README index c4d529f19d..f56c3a7994 100644 --- a/README +++ b/README @@ -1111,6 +1111,9 @@ path: Path part of theme URLs, before the theme name. Relative to the which means to use the site path + '/theme'. ssl: Whether to use SSL for theme elements. Default is null, which means guess based on site SSL settings. +sslserver: SSL server to use when page is HTTPS-encrypted. If + unspecified, site ssl server and so on will be used. +sslpath: If sslserver if defined, path to use when page is HTTPS-encrypted. javascript ---------- @@ -1122,6 +1125,9 @@ path: Path part of Javascript URLs. Defaults to null, which means to use the site path + '/js/'. ssl: Whether to use SSL for JavaScript files. Default is null, which means guess based on site SSL settings. +sslserver: SSL server to use when page is HTTPS-encrypted. If + unspecified, site ssl server and so on will be used. +sslpath: If sslserver if defined, path to use when page is HTTPS-encrypted. xmpp ---- @@ -1405,8 +1411,9 @@ dir: directory to write backgrounds too. Default is '/background/' subdir of install dir. path: path to backgrounds. Default is sub-path of install path; note that you may need to change this if you change site-path too. -ssl: Whether or not to use HTTPS for background files. Defaults to - null, meaning to guess from site-wide SSL settings. +sslserver: SSL server to use when page is HTTPS-encrypted. If + unspecified, site ssl server and so on will be used. +sslpath: If sslserver if defined, path to use when page is HTTPS-encrypted. ping ---- From 97a7fb246c8de9a2cf1bfc38ca275a13e9c40f58 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 01:35:11 -0400 Subject: [PATCH 47/54] correctly use sslserver if it is set --- classes/Design.php | 7 ++++++- lib/htmloutputter.php | 7 ++++++- lib/theme.php | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/classes/Design.php b/classes/Design.php index 50712ce8b5..a8fdb72191 100644 --- a/classes/Design.php +++ b/classes/Design.php @@ -146,7 +146,12 @@ class Design extends Memcached_DataObject if (empty($sslserver)) { // XXX: this assumes that background dir == site dir + /background/ // not true if there's another server - $server = common_config('site', 'server'); + if (is_string(common_config('site', 'sslserver')) && + mb_strlen(common_config('site', 'sslserver')) > 0) { + $server = common_config('site', 'sslserver'); + } else if (common_config('site', 'server')) { + $server = common_config('site', 'server'); + } $path = common_config('site', 'path') . '/background/'; } else { $server = $sslserver; diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index f01f1814f0..4a1b7db472 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -371,7 +371,12 @@ class HTMLOutputter extends XMLOutputter $sslserver = common_config('javascript', 'sslserver'); if (empty($sslserver)) { - $server = common_config('site', 'server'); + if (is_string(common_config('site', 'sslserver')) && + mb_strlen(common_config('site', 'sslserver')) > 0) { + $server = common_config('site', 'sslserver'); + } else if (common_config('site', 'server')) { + $server = common_config('site', 'server'); + } $path = common_config('site', 'path') . '/js/'; } else { $server = $sslserver; diff --git a/lib/theme.php b/lib/theme.php index 669d9a19fd..95b7c1de4b 100644 --- a/lib/theme.php +++ b/lib/theme.php @@ -121,7 +121,12 @@ class Theme $sslserver = common_config($group, 'sslserver'); if (empty($sslserver)) { - $server = common_config('site', 'server'); + if (is_string(common_config('site', 'sslserver')) && + mb_strlen(common_config('site', 'sslserver')) > 0) { + $server = common_config('site', 'sslserver'); + } else if (common_config('site', 'server')) { + $server = common_config('site', 'server'); + } $path = common_config('site', 'path') . '/'; if ($fallbackSubdir) { $path .= $fallbackSubdir . '/'; From 23ac9616245f8dfb11e2184f739059d22838010c Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 11:06:57 -0400 Subject: [PATCH 48/54] Show Webfinger, URI and profile page as subject and aliases --- plugins/OStatus/lib/xrdaction.php | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/plugins/OStatus/lib/xrdaction.php b/plugins/OStatus/lib/xrdaction.php index d1488dbdec..371c110800 100644 --- a/plugins/OStatus/lib/xrdaction.php +++ b/plugins/OStatus/lib/xrdaction.php @@ -36,7 +36,8 @@ class XrdAction extends Action function handle() { - $nick = $this->user->nickname; + $nick = $this->user->nickname; + $profile = $this->user->getProfile(); if (empty($this->xrd)) { $xrd = new XRD(); @@ -47,10 +48,28 @@ class XrdAction extends Action if (empty($xrd->subject)) { $xrd->subject = Discovery::normalize($this->uri); } - $xrd->alias[] = $this->user->uri; + + // Possible aliases for the user + + $uris = array($this->user->uri, $profile->profileurl); + + // FIXME: Webfinger generation code should live somewhere on its own + + $path = common_config('site', 'path'); + + if (empty($path)) { + $uris[] = sprintf('acct:%s@%s', $nick, common_config('site', 'server')); + } + + foreach ($uris as $uri) { + if ($uri != $xrd->subject) { + $xrd->alias[] = $uri; + } + } + $xrd->links[] = array('rel' => Discovery::PROFILEPAGE, 'type' => 'text/html', - 'href' => $this->user->uri); + 'href' => $profile->profileurl); $xrd->links[] = array('rel' => Discovery::UPDATESFROM, 'href' => common_local_url('ApiTimelineUser', @@ -66,7 +85,7 @@ class XrdAction extends Action // XFN $xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11', 'type' => 'text/html', - 'href' => $this->user->uri); + 'href' => $profile->profileurl); // FOAF $xrd->links[] = array('rel' => 'describedby', 'type' => 'application/rdf+xml', From ecb582e41900b9a8fe0cc01eac9c68b4da61d734 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 11:07:37 -0400 Subject: [PATCH 49/54] accept profile URL as a LRDD identifier --- plugins/OStatus/actions/userxrd.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/OStatus/actions/userxrd.php b/plugins/OStatus/actions/userxrd.php index c9b1d0a5be..575a07c409 100644 --- a/plugins/OStatus/actions/userxrd.php +++ b/plugins/OStatus/actions/userxrd.php @@ -46,7 +46,15 @@ class UserxrdAction extends XrdAction } } else { $this->user = User::staticGet('uri', $this->uri); + if (empty($this->user)) { + // try and get it by profile url + $profile = Profile::staticGet('profileurl', $this->uri); + if (!empty($profile)) { + $this->user = User::staticGet('id', $profile->id); + } + } } + if (!$this->user) { $this->clientError(_m('No such user.'), 404); return false; From b31c49c5d47e04c59bbbcf878ffd307fbf60b533 Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 14:22:17 -0400 Subject: [PATCH 50/54] Make HTTPS urls in File::url() if necessary --- README | 5 ++++ classes/File.php | 60 +++++++++++++++++++++++++++++++----------------- lib/default.php | 2 ++ 3 files changed, 46 insertions(+), 21 deletions(-) diff --git a/README b/README index f56c3a7994..70491c4c35 100644 --- a/README +++ b/README @@ -1355,6 +1355,11 @@ ssl: whether to use HTTPS for file URLs. Defaults to null, meaning to filecommand: command to use for determining the type of a file. May be skipped if fileinfo extension is installed. Defaults to '/usr/bin/file'. +sslserver: if specified, this server will be used when creating HTTPS + URLs. Otherwise, the site SSL server will be used, with /file/ path. +sslpath: if this and the sslserver are specified, this path will be used + when creating HTTPS URLs. Otherwise, the attachments|path value + will be used. group ----- diff --git a/classes/File.php b/classes/File.php index d457968b5e..da029e39b6 100644 --- a/classes/File.php +++ b/classes/File.php @@ -261,22 +261,41 @@ class File extends Memcached_DataObject // TRANS: Client exception thrown if a file upload does not have a valid name. throw new ClientException(_("Invalid filename.")); } - if(common_config('site','private')) { + + if (common_config('site','private')) { return common_local_url('getfile', array('filename' => $filename)); + } + + if (StatusNet::isHTTPS()) { + + $sslserver = common_config('attachments', 'sslserver'); + + if (empty($sslserver)) { + // XXX: this assumes that background dir == site dir + /file/ + // not true if there's another server + if (is_string(common_config('site', 'sslserver')) && + mb_strlen(common_config('site', 'sslserver')) > 0) { + $server = common_config('site', 'sslserver'); + } else if (common_config('site', 'server')) { + $server = common_config('site', 'server'); + } + $path = common_config('site', 'path') . '/file/'; + } else { + $server = $sslserver; + $path = common_config('attachments', 'sslpath'); + if (empty($path)) { + $path = common_config('attachments', 'path'); + } + } + + $protocol = 'https'; + } else { + $path = common_config('attachments', 'path'); - - if ($path[strlen($path)-1] != '/') { - $path .= '/'; - } - - if ($path[0] != '/') { - $path = '/'.$path; - } - $server = common_config('attachments', 'server'); if (empty($server)) { @@ -285,19 +304,18 @@ class File extends Memcached_DataObject $ssl = common_config('attachments', 'ssl'); - if (is_null($ssl)) { // null -> guess - if (common_config('site', 'ssl') == 'always' && - !common_config('attachments', 'server')) { - $ssl = true; - } else { - $ssl = false; - } - } - $protocol = ($ssl) ? 'https' : 'http'; - - return $protocol.'://'.$server.$path.$filename; } + + if ($path[strlen($path)-1] != '/') { + $path .= '/'; + } + + if ($path[0] != '/') { + $path = '/'.$path; + } + + return $protocol.'://'.$server.$path.$filename; } function getEnclosure(){ diff --git a/lib/default.php b/lib/default.php index 45e35e83d3..08ad811b61 100644 --- a/lib/default.php +++ b/lib/default.php @@ -210,6 +210,8 @@ $default = array('server' => null, 'dir' => INSTALLDIR . '/file/', 'path' => $_path . '/file/', + 'sslserver' => null, + 'sslpath' => null, 'ssl' => null, 'supported' => array('image/png', 'image/jpeg', From 72454db118ab4e54ba2d24a1cbd0e85da2a8413b Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 14:22:49 -0400 Subject: [PATCH 51/54] make the logo be compatible with HTTPS pages, if possible --- lib/action.php | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/lib/action.php b/lib/action.php index fcac0e0a7f..55ee83bde6 100644 --- a/lib/action.php +++ b/lib/action.php @@ -426,11 +426,35 @@ class Action extends HTMLOutputter // lawsuit } $this->elementStart('a', array('class' => 'url home bookmark', 'href' => $url)); - if (common_config('site', 'logo') || file_exists(Theme::file('logo.png'))) { + + if (StatusNet::isHTTPS()) { + $logoUrl = common_config('site', 'ssllogo'); + if (empty($logoUrl)) { + // if logo is an uploaded file, try to fall back to HTTPS file URL + $httpUrl = common_config('site', 'logo'); + if (!empty($httpUrl)) { + $f = File::staticGet('url', $httpUrl); + if (!empty($f) && !empty($f->filename)) { + // this will handle the HTTPS case + $logoUrl = File::url($f->filename); + } + } + } + } else { + $logoUrl = common_config('site', 'logo'); + } + + if (empty($logoUrl) && file_exists(Theme::file('logo.png'))) { + // This should handle the HTTPS case internally + $logoUrl = Theme::path('logo.png'); + } + + if (!empty($logoUrl)) { $this->element('img', array('class' => 'logo photo', - 'src' => (common_config('site', 'logo')) ? common_config('site', 'logo') : Theme::path('logo.png'), + 'src' => $logoUrl, 'alt' => common_config('site', 'name'))); } + $this->text(' '); $this->element('span', array('class' => 'fn org'), common_config('site', 'name')); $this->elementEnd('a'); From 8f3b18f27f1c8daee3f06f457eaf3587db22c96e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 14:53:20 -0400 Subject: [PATCH 52/54] fix copy-and-paste error in javascript url creation --- lib/htmloutputter.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lib/htmloutputter.php b/lib/htmloutputter.php index 4a1b7db472..42bff44908 100644 --- a/lib/htmloutputter.php +++ b/lib/htmloutputter.php @@ -393,10 +393,7 @@ class HTMLOutputter extends XMLOutputter $path = common_config('javascript', 'path'); if (empty($path)) { - $path = common_config('site', 'path') . '/'; - if ($fallbackSubdir) { - $path .= $fallbackSubdir . '/'; - } + $path = common_config('site', 'path') . '/js/'; } $server = common_config('javascript', 'server'); From 1a4dc03bfe9c02de04635411e1af78821adc4d5e Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 14:56:38 -0400 Subject: [PATCH 53/54] document and default for site|ssllogo --- README | 2 ++ lib/default.php | 1 + 2 files changed, 3 insertions(+) diff --git a/README b/README index 70491c4c35..6b351f143d 100644 --- a/README +++ b/README @@ -852,6 +852,8 @@ notice: A plain string that will appear on every page. A good place be escaped. logo: URL of an image file to use as the logo for the site. Overrides the logo in the theme, if any. +ssllogo: URL of an image file to use as the logo on SSL pages. If unset, + theme logo is used instead. ssl: Whether to use SSL and https:// URLs for some or all pages. Possible values are 'always' (use it for all pages), 'never' (don't use it for any pages), or 'sometimes' (use it for diff --git a/lib/default.php b/lib/default.php index 08ad811b61..fb032930b2 100644 --- a/lib/default.php +++ b/lib/default.php @@ -37,6 +37,7 @@ $default = 'path' => $_path, 'logfile' => null, 'logo' => null, + 'ssllogo' => null, 'logdebug' => false, 'fancy' => false, 'locale_path' => INSTALLDIR.'/locale', From fc6711327bcb2319139171ad3353603753f13eaa Mon Sep 17 00:00:00 2001 From: Evan Prodromou Date: Thu, 14 Oct 2010 15:06:11 -0400 Subject: [PATCH 54/54] let users set their SSL logo through the admin panel --- actions/designadminpanel.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/actions/designadminpanel.php b/actions/designadminpanel.php index 4285f7d731..587333e06a 100644 --- a/actions/designadminpanel.php +++ b/actions/designadminpanel.php @@ -140,7 +140,7 @@ class DesignadminpanelAction extends AdminPanelAction $themeChanged = ($this->trimmed('theme') != $oldtheme); } - static $settings = array('theme', 'logo'); + static $settings = array('theme', 'logo', 'ssllogo'); $values = array(); @@ -230,6 +230,7 @@ class DesignadminpanelAction extends AdminPanelAction function restoreDefaults() { $this->deleteSetting('site', 'logo'); + $this->deleteSetting('site', 'ssllogo'); $this->deleteSetting('site', 'theme'); $settings = array( @@ -293,7 +294,7 @@ class DesignadminpanelAction extends AdminPanelAction /** * Save the custom theme if the user uploaded one. - * + * * @return mixed custom theme name, if succesful, or null if no theme upload. * @throws ClientException for invalid theme archives * @throws ServerException if trouble saving the theme files @@ -331,6 +332,11 @@ class DesignadminpanelAction extends AdminPanelAction $this->clientError(_('Invalid logo URL.')); } + if (!empty($values['ssllogo']) && + !Validate::uri($values['ssllogo'], array('allowed_schemes' => array('https')))) { + $this->clientError(_('Invalid SSL logo URL.')); + } + if (!in_array($values['theme'], Theme::listAvailable())) { $this->clientError(sprintf(_("Theme not available: %s."), $values['theme'])); } @@ -444,6 +450,10 @@ class DesignAdminPanelForm extends AdminForm $this->input('logo', _('Site logo'), 'Logo for the site (full URL)'); $this->unli(); + $this->li(); + $this->input('ssllogo', _('SSL logo'), 'Logo to show on SSL pages'); + $this->unli(); + $this->out->elementEnd('ul'); $this->out->elementEnd('fieldset');