full interface for userauthorization

darcs-hash:20080604185131-84dde-2ff45e07ebba18c97803ed4a99121a6244ef1158.gz
This commit is contained in:
Evan Prodromou 2008-06-04 14:51:31 -04:00
parent d266ab8c2d
commit d251e624a9
4 changed files with 439 additions and 34 deletions

View File

@ -20,28 +20,29 @@
if (!defined('LACONICA')) { exit(1); }
require_once(INSTALLDIR.'/lib/omb.php');
define('TIMESTAMP_THRESHOLD', 300);
class UserauthorizationAction extends Action {
function handle($args) {
parent::handle($args);
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
# We've shown the form, now post user's choice
$this->send_authorization();
} else {
try {
common_debug('userauthorization.php - fetching request');
$req = $this->get_request();
# We get called after login if we have a stored request
$req = $this->get_stored_request();
if (!$req) {
common_server_error(_t('Cannot find request'));
# this must be a new request
$req = $this->get_new_request();
# XXX: only validate new requests, since nonce is one-time use
$this->validate_request($req);
}
if (!$req) {
common_server_error(_t('No request found!'));
}
common_debug('userauthorization.php - $req = "'.print_r($req,TRUE).'"');
$server = omb_oauth_server();
common_debug('userauthorization.php - getting the consumer');
$consumer = $server->get_consumer($req);
common_debug('userauthorization.php - $consumer = "'.print_r($consumer,TRUE).'"');
$token = $server->get_token($req, $consumer, "request");
common_debug('userauthorization.php - $token = "'.print_r($token,TRUE).'"');
$server->check_signature($req, $consumer, $token);
} catch (OAuthException $e) {
$this->clear_request();
common_server_error($e->getMessage());
@ -52,46 +53,426 @@ class UserauthorizationAction extends Action {
$this->show_form($req);
} else {
# Go log in, and then come back
$this->store_request($req);
common_set_returnto(common_local_url('userauthorization'));
common_redirect(common_local_url('login'));
}
}
}
function show_form($req) {
$nickname = $req->get_parameter('omb_listenee_nickname');
$profile = $req->get_parameter('omb_listenee_profile');
$license = $req->get_parameter('omb_listenee_license');
$fullname = $req->get_parameter('omb_listenee_fullname');
$homepage = $req->get_parameter('omb_listenee_homepage');
$bio = $req->get_parameter('omb_listenee_bio');
$location = $req->get_parameter('omb_listenee_location');
$avatar = $req->get_parameter('omb_listenee_avatar');
common_show_header(_t('Authorize subscription'));
common_element('p', _t('Please check these details to make sure '.
'that you want to subscribe to this user\'s notices. '.
'If you didn\'t just ask to subscribe to someone\'s notices, '.
'click "Cancel".'));
common_element_start('div', 'profile');
if ($avatar) {
common_element('img', array('src' => $avatar,
'class' => 'avatar profile',
'width' => AVATAR_PROFILE_SIZE,
'height' => AVATAR_PROFILE_SIZE,
'alt' => $nickname));
}
common_element('a', array('href' => $profile,
'class' => 'external profile nickname'),
$nickname);
if ($fullname) {
common_element_start('div', 'fullname');
if ($homepage) {
common_element('a', array('href' => $homepage),
$fullname);
} else {
common_text($fullname);
}
common_element_end('div');
}
if ($location) {
common_element('div', 'location', $location);
}
if ($bio) {
common_element('div', 'bio', $bio);
}
common_element_start('div', 'license');
common_element('a', array('href' => $license,
'class' => 'license'),
$license);
common_element_end('div');
common_element_end('div');
common_element_start('form', array('method' => 'POST',
'id' => 'userauthorization',
'name' => 'userauthorization',
'action' => common_local_url('userauthorization')));
common_submit('accept', _t('Accept'));
common_submit('reject', _t('Reject'));
common_element_end('form');
common_show_footer();
}
function send_authorization() {
$req = $this->get_stored_request();
if (!$req) {
common_user_error(_t('No authorization request!'));
return;
}
$callback = $req->get_parameter('oauth_callback');
if ($this->arg('accept')) {
$this->authorize_token($req);
$this->save_remote_profile($req);
if (!$callback) {
$this->show_accept_message($req->get_parameter('oauth_token'));
} else {
$params = array();
$params['oauth_token'] = $req->get_parameter('oauth_token');
$params['omb_version'] = OMB_VERSION_01;
$user = User::staticGet('uri', $req->get_parameter('omb_listener'));
$profile = $user->getProfile();
$params['omb_listener_nickname'] = $user->nickname;
$params['omb_listener_profile'] = common_local_url('showstream',
array('nickname' => $user->nickname));
if ($profile->fullname) {
$params['omb_listener_fullname'] = $profile->fullname;
}
if ($profile->homepage) {
$params['omb_listener_homepage'] = $profile->homepage;
}
if ($profile->bio) {
$params['omb_listener_bio'] = $profile->bio;
}
if ($profile->location) {
$params['omb_listener_location'] = $profile->location;
}
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
if ($avatar) {
$params['omb_listener_avatar'] = $avatar->url;
}
$parts = array();
foreach ($params as $k => $v) {
$parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
}
$query_string = implode('&', $parts);
$parsed = parse_url($callback);
$url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
common_redirect($url, 303);
}
} else {
if (!$callback) {
$this->show_reject_message();
} else {
# XXX: not 100% sure how to signal failure... just redirect without token?
common_redirect($callback, 303);
}
}
}
function authorize_token(&$req) {
$consumer_key = @$req->get_parameter('oauth_consumer_key');
$token_field = @$req->get_parameter('oauth_token');
$rt = new Token();
$rt->consumer_key = $consumer_key;
$rt->tok = $token_field;
if ($rt->find(TRUE)) {
$orig_rt = clone($rt);
$rt->state = 1; # Authorized but not used
if ($rt->update($orig_rt)) {
return true;
}
}
return FALSE;
}
# XXX: refactor with similar code in finishremotesubscribe.php
function save_remote_profile(&$req) {
# FIXME: we should really do this when the consumer comes
# back for an access token. If they never do, we've got stuff in a
# weird state.
$fullname = $req->get_parameter('omb_listenee_fullname');
$profile_url = $req->get_parameter('omb_listenee_profile');
$homepage = $req->get_parameter('omb_listenee_homepage');
$bio = $req->get_parameter('omb_listenee_bio');
$location = $req->get_parameter('omb_listenee_location');
$avatar_url = $req->get_parameter('omb_listenee_avatar');
$listenee = $req->get_parameter('omb_listenee');
$remote = Remote_profile::staticGet('uri', $listenee);
if ($remote) {
$exists = true;
$profile = Profile::staticGet($remote->id);
$orig_remote = clone($remote);
$orig_profile = clone($profile);
} else {
$exists = false;
$remote = new Remote_profile();
$remote->uri = $omb['listener'];
$profile = new Profile();
}
$profile->nickname = $nickname;
$profile->profileurl = $profile_url;
if ($fullname) {
$profile->fullname = $fullname;
}
if ($homepage) {
$profile->homepage = $homepage;
}
if ($bio) {
$profile->bio = $bio;
}
if ($location) {
$profile->location = $location;
}
if ($exists) {
$profile->update($orig_profile);
} else {
$profile->created = DB_DataObject_Cast::dateTime(); # current time
$id = $profile->insert();
$remote->id = $id;
}
if ($avatar_url) {
$this->add_avatar($avatar_url);
}
if ($exists) {
$remote->update($orig_remote);
} else {
$remote->created = DB_DataObject_Cast::dateTime(); # current time
$remote->insert();
}
$user = common_current_user();
$datastore = omb_oauth_datastore();
$consumer = $this->get_consumer($datastore, $req);
$token = $this->get_token($datastore, $req, $consumer);
$sub = new Subscription();
$sub->subscriber = $user->id;
$sub->subscribed = $remote->id;
$sub->token = $token->key; # NOTE: request token, not valid for use!
$sub->created = DB_DataObject_Cast::dateTime(); # current time
if (!$sub->insert()) {
common_user_error(_t('Couldn\'t insert new subscription.'));
return;
}
}
function show_accept_message($tok) {
common_show_header(_t('Subscription authorized'));
common_element('p', NULL,
_t('The subscription has been authorized, but no '.
'callback URL was passed. Check with the site\'s instructions for '.
'details on how to authorize the subscription. Your subscription token is:'));
common_element('blockquote', 'token', $tok);
common_show_footer();
}
function show_reject_message($tok) {
common_show_header(_t('Subscription rejected'));
common_element('p', NULL,
_t('The subscription has been rejected, but no '.
'callback URL was passed. Check with the site\'s instructions for '.
'details on how to fully reject the subscription.'));
common_show_footer();
}
function store_request($req) {
common_ensure_session();
$_SESSION['userauthorizationrequest'] = $req;
}
function get_request() {
function clear_request($req) {
common_ensure_session();
unset($_SESSION['userauthorizationrequest']);
}
function get_stored_request() {
common_ensure_session();
$req = $_SESSION['userauthorizationrequest'];
if (!$req) {
# XXX: may have an uncaught exception
$req = OAuthRequest::from_request();
if ($req) {
$this->store_request($req);
}
}
return $req;
}
function show_form($req) {
common_show_header(_t('Authorize subscription'));
common_show_footer();
function get_new_request() {
$req = OAuthRequest::from_request();
}
function send_authorization() {
$req = $this->get_request();
# Throws an OAuthException if anything goes wrong
if (!$req) {
common_user_error(_t('No authorization request!'));
return;
function validate_request($req) {
# OAuth stuff -- have to copy from OAuth.php since they're
# all private methods, and there's no user-authentication method
$this->check_version($req);
$datastore = omb_oauth_datastore();
$consumer = $this->get_consumer($datastore, $req);
$token = $this->get_token($datastore, $req, $consumer);
$this->check_timestamp($req);
$this->check_nonce($datastore, $req, $consumer, $token);
$this->check_signature($req, $consumer, $token);
$this->validate_omb($req);
return true;
}
if ($this->boolean('authorize')) {
function validate_omb(&$req) {
foreach (array('omb_version', 'omb_listener', 'omb_listenee',
'omb_listenee_profile', 'omb_listenee_nickname',
'omb_listenee_license') as $param)
{
if (!$req->get_parameter($param)) {
throw new OAuthException("Required parameter '$param' not found");
}
}
# Now, OMB stuff
$version = $req->get_parameter('omb_version');
if ($version != OMB_VERSION_01) {
throw new OAuthException("OpenMicroBlogging version '$version' not supported");
}
$user = User::staticGet('uri', $req->get_parameter('omb_listener'));
if (!$user) {
throw new OAuthException("Listener URI '$listener' not found here");
}
$listenee = $req->get_parameter('omb_listenee');
if (!Validate::uri($listenee)) {
throw new OAuthException("Listenee URI '$listenee' not a valid URI");
} else if (strlen($listenee) > 255) {
throw new OAuthException("Listenee URI '$listenee' too long");
}
$nickname = $req->get_parameter('omb_listenee_nickname');
if (!Validate::string($nickname, array('min_length' => 1,
'max_length' => 64,
'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
}
$profile = $req->get_parameter('omb_listenee_profile');
if (!common_valid_http_url($profile)) {
throw new OAuthException("Invalid profile URL '$profile'.");
}
$license = $req->get_parameter('omb_listenee_license');
if (!common_valid_http_url($license)) {
throw new OAuthException("Invalid license URL '$license'.");
}
# optional stuff
$fullname = $req->get_parameter('omb_listenee_fullname');
if ($fullname && strlen($fullname) > 255) {
throw new OAuthException("Full name '$fullname' too long.");
}
$homepage = $req->get_parameter('omb_listenee_homepage');
if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
throw new OAuthException("Invalid homepage '$homepage'");
}
$bio = $req->get_parameter('omb_listenee_bio');
if ($bio && strlen($bio) > 140) {
throw new OAuthException("Bio too long '$bio'");
}
$location = $req->get_parameter('omb_listenee_location');
if ($location && strlen($location) > 255) {
throw new OAuthException("Location too long '$location'");
}
$avatar = $req->get_parameter('omb_listenee_avatar');
if ($avatar && (!common_valid_http_url($avatar) || strlen($avatar) > 255)) {
throw new OAuthException("Invalid avatar '$avatar'");
}
$callback = $req->get_parameter('oauth_callback');
if ($avatar && common_valid_http_url($callback)) {
throw new OAuthException("Invalid callback URL '$callback'");
}
}
# Snagged from OAuthServer
function check_version($req) {
$version = $req->get_parameter("oauth_version");
if (!$version) {
$version = 1.0;
}
if ($version != 1.0) {
throw new OAuthException("OAuth version '$version' not supported");
}
return $version;
}
# Snagged from OAuthServer
function get_consumer($datastore, $req) {
$consumer_key = @$req->get_parameter("oauth_consumer_key");
if (!$consumer_key) {
throw new OAuthException("Invalid consumer key");
}
$consumer = $datastore->lookup_consumer($consumer_key);
if (!$consumer) {
throw new OAuthException("Invalid consumer");
}
return $consumer;
}
# Mostly cadged from OAuthServer
function get_token(&$req, $consumer, $datastore) {/*{{{*/
$token_field = @$req->get_parameter('oauth_token');
$token = $datastore->lookup_token($consumer, 'request', $token_field);
if (!$token) {
throw new OAuthException("Invalid $token_type token: $token_field");
}
return $token;
}
function check_timestamp(&$req) {
$timestamp = @$req->get_parameter('oauth_timestamp');
$now = time();
if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
}
}
# NOTE: don't call twice on the same request; will fail!
function check_nonce(&$datastore, &$req, $consumer, $token) {
$timestamp = @$req->get_parameter('oauth_timestamp');
$nonce = @$req->get_parameter('oauth_nonce');
$found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
if ($found) {
throw new OAuthException("Nonce already used");
}
return true;
}
function check_signature(&$req, $consumer, $token) {
$signature_method = $this->get_signature_method($req);
$signature = $req->get_parameter('oauth_signature');
$valid_sig = $signature_method->check_signature($req,
$consumer,
$token,
$signature);
if (!$valid_sig) {
throw new OAuthException("Invalid signature");
}
}
function get_signature_method(&$req) {
$signature_method = @$req->get_parameter("oauth_signature_method");
if (!$signature_method) {
$signature_method = "PLAINTEXT";
}
if ($signature_method != 'HMAC-SHA1') {
throw new OAuthException("Signature method '$signature_method' not supported.");
}
return omb_hmac_sha1();
}
}

View File

@ -106,6 +106,18 @@ class LaconicaOAuthDataStore extends OAuthDataStore {
$rt->state = 2; # used
if (!$rt->update($orig_rt)) {
return NULL;
}
# Update subscription
# XXX: mixing levels here
$sub = Subscription::staticGet('token', $rt->tok);
if (!$sub) {
return NULL;
}
$orig_sub = clone($sub);
$sub->token = $at->tok;
$sub->secret = $at->secret;
if (!$sub->update($orig_sub)) {
return NULL;
} else {
return new OAuthToken($at->tok, $at->secret);
}

View File

@ -42,7 +42,7 @@ define('OAUTH_POST_BODY', OAUTH_NAMESPACE.'parameters/post-body');
define('OAUTH_HMAC_SHA1', OAUTH_NAMESPACE.'signature/HMAC-SHA1');
function omb_oauth_consumer() {
static $con = null;
static $con = NULL;
if (!$con) {
$con = new OAuthConsumer(common_root_url(), '');
}
@ -52,12 +52,20 @@ function omb_oauth_consumer() {
function omb_oauth_server() {
static $server = null;
if (!$server) {
$server = new OAuthServer(new LaconicaOAuthDataStore());
$server = new OAuthServer(omb_oauth_datastore());
$server->add_signature_method(omb_hmac_sha1());
}
return $server;
}
function omb_oauth_datastore() {
static $store = NULL;
if (!$store) {
$store = new LaconicaOAuthDataStore();
}
return $store;
}
function omb_hmac_sha1() {
static $hmac_method = NULL;
if (!$hmac_method) {

View File

@ -589,3 +589,7 @@ function common_log($priority, $msg) {
function common_debug($msg) {
common_log(LOG_DEBUG, $msg);
}
function common_valid_http_url($url) {
return Validate::uri($url, array('allowed_schemes' => array('http', 'https')));
}