Merge branch 'oauth-continued' into 0.9.x

This commit is contained in:
Zach Copley 2010-02-02 08:48:52 +00:00
commit 2be00ce642
14 changed files with 406 additions and 39 deletions

View File

@ -67,8 +67,6 @@ class ApiOauthAuthorizeAction extends ApiOauthAction
{ {
parent::prepare($args); parent::prepare($args);
common_debug("apioauthauthorize");
$this->nickname = $this->trimmed('nickname'); $this->nickname = $this->trimmed('nickname');
$this->password = $this->arg('password'); $this->password = $this->arg('password');
$this->oauth_token = $this->arg('oauth_token'); $this->oauth_token = $this->arg('oauth_token');
@ -99,24 +97,17 @@ class ApiOauthAuthorizeAction extends ApiOauthAction
} else { } else {
// XXX: make better error messages
if (empty($this->oauth_token)) { if (empty($this->oauth_token)) {
$this->clientError(_('No oauth_token parameter provided.'));
common_debug("No request token found.");
$this->clientError(_('Bad request.'));
return; return;
} }
if (empty($this->app)) { if (empty($this->app)) {
common_debug('No app for that token.'); $this->clientError(_('Invalid token.'));
$this->clientError(_('Bad request.'));
return; return;
} }
$name = $this->app->name; $name = $this->app->name;
common_debug("Requesting auth for app: " . $name);
$this->showForm(); $this->showForm();
} }
@ -124,8 +115,6 @@ class ApiOauthAuthorizeAction extends ApiOauthAction
function handlePost() function handlePost()
{ {
common_debug("handlePost()");
// check session token for CSRF protection. // check session token for CSRF protection.
$token = $this->trimmed('token'); $token = $this->trimmed('token');
@ -202,21 +191,15 @@ class ApiOauthAuthorizeAction extends ApiOauthAction
// A callback specified in the app setup overrides whatever // A callback specified in the app setup overrides whatever
// is passed in with the request. // is passed in with the request.
common_debug("Req token is authorized - doing callback");
if (!empty($this->app->callback_url)) { if (!empty($this->app->callback_url)) {
$this->callback = $this->app->callback_url; $this->callback = $this->app->callback_url;
} }
if (!empty($this->callback)) { if (!empty($this->callback)) {
// XXX: Need better way to build this redirect url.
$target_url = $this->getCallback($this->callback, $target_url = $this->getCallback($this->callback,
array('oauth_token' => $this->oauth_token)); array('oauth_token' => $this->oauth_token));
common_debug("Doing callback to $target_url");
common_redirect($target_url, 303); common_redirect($target_url, 303);
} else { } else {
common_debug("callback was empty!"); common_debug("callback was empty!");
@ -236,9 +219,12 @@ class ApiOauthAuthorizeAction extends ApiOauthAction
} else if ($this->arg('deny')) { } else if ($this->arg('deny')) {
$datastore = new ApiStatusNetOAuthDataStore();
$datastore->revoke_token($this->oauth_token, 0);
$this->elementStart('p'); $this->elementStart('p');
$this->raw(sprintf(_("The request token %s has been denied."), $this->raw(sprintf(_("The request token %s has been denied and revoked."),
$this->oauth_token)); $this->oauth_token));
$this->elementEnd('p'); $this->elementEnd('p');
@ -305,12 +291,15 @@ class ApiOauthAuthorizeAction extends ApiOauthAction
$msg = _('The application <strong>%1$s</strong> by ' . $msg = _('The application <strong>%1$s</strong> by ' .
'<strong>%2$s</strong> would like the ability ' . '<strong>%2$s</strong> would like the ability ' .
'to <strong>%3$s</strong> your account data.'); 'to <strong>%3$s</strong> your %4$s account data. ' .
'You should only give access to your %4$s account ' .
'to third parties you trust.');
$this->raw(sprintf($msg, $this->raw(sprintf($msg,
$this->app->name, $this->app->name,
$this->app->organization, $this->app->organization,
$access)); $access,
common_config('site', 'name')));
$this->elementEnd('p'); $this->elementEnd('p');
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementEnd('ul'); $this->elementEnd('ul');
@ -372,6 +361,31 @@ class ApiOauthAuthorizeAction extends ApiOauthAction
function showLocalNav() function showLocalNav()
{ {
// NOP
}
/**
* Show site notice.
*
* @return nothing
*/
function showSiteNotice()
{
// NOP
}
/**
* Show notice form.
*
* Show the form for posting a new notice
*
* @return nothing
*/
function showNoticeForm()
{
// NOP
} }
} }

View File

@ -0,0 +1,176 @@
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Action class to delete an OAuth application
*
* PHP version 5
*
* LICENCE: 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 <http://www.gnu.org/licenses/>.
*
* @category Action
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @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);
}
/**
* Delete an OAuth appliction
*
* @category Action
* @package StatusNet
* @author Zach Copley <zach@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl.html AGPLv3
* @link http://status.net/
*/
class DeleteapplicationAction extends Action
{
var $app = null;
/**
* Take arguments for running
*
* @param array $args $_REQUEST args
*
* @return boolean success flag
*/
function prepare($args)
{
if (!parent::prepare($args)) {
return false;
}
if (!common_logged_in()) {
$this->clientError(_('You must be logged in to delete an application.'));
return false;
}
$id = (int)$this->arg('id');
$this->app = Oauth_application::staticGet('id', $id);
if (empty($this->app)) {
$this->clientError(_('Application not found.'));
return false;
}
$cur = common_current_user();
if ($cur->id != $this->app->owner) {
$this->clientError(_('You are not the owner of this application.'), 401);
return false;
}
return true;
}
/**
* Handle request
*
* Shows a page with list of favorite notices
*
* @param array $args $_REQUEST args; handled in prepare()
*
* @return void
*/
function handle($args)
{
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// CSRF protection
$token = $this->trimmed('token');
if (!$token || $token != common_session_token()) {
$this->clientError(_('There was a problem with your session token.'));
return;
}
if ($this->arg('no')) {
common_redirect(common_local_url('showapplication',
array('id' => $this->app->id)), 303);
} elseif ($this->arg('yes')) {
$this->handlePost();
common_redirect(common_local_url('oauthappssettings'), 303);
} else {
$this->showPage();
}
}
}
function showContent() {
$this->areYouSureForm();
}
function title() {
return _('Delete application');
}
function showNoticeForm() {
// nop
}
/**
* Confirm with user.
*
* Shows a confirmation form.
*
* @return void
*/
function areYouSureForm()
{
$id = $this->app->id;
$this->elementStart('form', array('id' => 'deleteapplication-' . $id,
'method' => 'post',
'class' => 'form_settings form_entity_block',
'action' => common_local_url('deleteapplication',
array('id' => $this->app->id))));
$this->elementStart('fieldset');
$this->hidden('token', common_session_token());
$this->element('legend', _('Delete application'));
$this->element('p', null,
_('Are you sure you want to delete this application? '.
'This will clear all data about the application from the '.
'database, including all existing user connections.'));
$this->submit('form_action-no',
_('No'),
'submit form_action-primary',
'no',
_("Do not delete this application"));
$this->submit('form_action-yes',
_('Yes'),
'submit form_action-secondary',
'yes', _('Delete this application'));
$this->elementEnd('fieldset');
$this->elementEnd('form');
}
/**
* Actually delete the app
*
* @return void
*/
function handlePost()
{
$this->app->delete();
}
}

View File

@ -179,6 +179,9 @@ class EditApplicationAction extends OwnerDesignAction
} elseif (mb_strlen($name) > 255) { } elseif (mb_strlen($name) > 255) {
$this->showForm(_('Name is too long (max 255 chars).')); $this->showForm(_('Name is too long (max 255 chars).'));
return; return;
} else if ($this->nameExists($name)) {
$this->showForm(_('Name already in use. Try another one.'));
return;
} elseif (empty($description)) { } elseif (empty($description)) {
$this->showForm(_('Description is required.')); $this->showForm(_('Description is required.'));
return; return;
@ -260,5 +263,26 @@ class EditApplicationAction extends OwnerDesignAction
common_redirect(common_local_url('oauthappssettings'), 303); common_redirect(common_local_url('oauthappssettings'), 303);
} }
/**
* Does the app name already exist?
*
* Checks the DB to see someone has already registered and app
* with the same name.
*
* @param string $name app name to check
*
* @return boolean true if the name already exists
*/
function nameExists($name)
{
$newapp = Oauth_application::staticGet('name', $name);
if (!$newapp) {
return false;
} else {
return $newapp->id != $this->app->id;
}
}
} }

View File

@ -158,6 +158,9 @@ class NewApplicationAction extends OwnerDesignAction
if (empty($name)) { if (empty($name)) {
$this->showForm(_('Name is required.')); $this->showForm(_('Name is required.'));
return; return;
} else if ($this->nameExists($name)) {
$this->showForm(_('Name already in use. Try another one.'));
return;
} elseif (mb_strlen($name) > 255) { } elseif (mb_strlen($name) > 255) {
$this->showForm(_('Name is too long (max 255 chars).')); $this->showForm(_('Name is too long (max 255 chars).'));
return; return;
@ -273,5 +276,22 @@ class NewApplicationAction extends OwnerDesignAction
} }
/**
* Does the app name already exist?
*
* Checks the DB to see someone has already registered and app
* with the same name.
*
* @param string $name app name to check
*
* @return boolean true if the name already exists
*/
function nameExists($name)
{
$app = Oauth_application::staticGet('name', $name);
return ($app !== false);
}
} }

View File

@ -33,6 +33,7 @@ if (!defined('STATUSNET') && !defined('LACONICA')) {
require_once INSTALLDIR . '/lib/connectsettingsaction.php'; require_once INSTALLDIR . '/lib/connectsettingsaction.php';
require_once INSTALLDIR . '/lib/applicationlist.php'; require_once INSTALLDIR . '/lib/applicationlist.php';
require_once INSTALLDIR . '/lib/apioauthstore.php';
/** /**
* Show connected OAuth applications * Show connected OAuth applications
@ -71,11 +72,6 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
return _('Connected applications'); return _('Connected applications');
} }
function isReadOnly($args)
{
return true;
}
/** /**
* Instructions for use * Instructions for use
* *
@ -153,6 +149,13 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
} }
} }
/**
* Revoke access to an authorized OAuth application
*
* @param int $appId the ID of the application
*
*/
function revokeAccess($appId) function revokeAccess($appId)
{ {
$cur = common_current_user(); $cur = common_current_user();
@ -164,6 +167,8 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
return false; return false;
} }
// XXX: Transaction here?
$appUser = Oauth_application_user::getByKeys($cur, $app); $appUser = Oauth_application_user::getByKeys($cur, $app);
if (empty($appUser)) { if (empty($appUser)) {
@ -171,12 +176,13 @@ class OauthconnectionssettingsAction extends ConnectSettingsAction
return false; return false;
} }
$orig = clone($appUser); $datastore = new ApiStatusNetOAuthDataStore();
$appUser->access_type = 0; // No access $datastore->revoke_token($appUser->token, 1);
$result = $appUser->update();
$result = $appUser->delete();
if (!$result) { if (!$result) {
common_log_db_error($orig, 'UPDATE', __FILE__); common_log_db_error($orig, 'DELETE', __FILE__);
$this->clientError(_('Unable to revoke access for app: ' . $app->id)); $this->clientError(_('Unable to revoke access for app: ' . $app->id));
return false; return false;
} }

View File

@ -222,18 +222,33 @@ class ShowApplicationAction extends OwnerDesignAction
$this->elementStart('li', 'entity_reset_keysecret'); $this->elementStart('li', 'entity_reset_keysecret');
$this->elementStart('form', array( $this->elementStart('form', array(
'id' => 'forma_reset_key', 'id' => 'form_reset_key',
'class' => 'form_reset_key', 'class' => 'form_reset_key',
'method' => 'POST', 'method' => 'POST',
'action' => common_local_url('showapplication', 'action' => common_local_url('showapplication',
array('id' => $this->application->id)))); array('id' => $this->application->id))));
$this->elementStart('fieldset'); $this->elementStart('fieldset');
$this->hidden('token', common_session_token()); $this->hidden('token', common_session_token());
$this->submit('reset', _('Reset key & secret')); $this->submit('reset', _('Reset key & secret'));
$this->elementEnd('fieldset'); $this->elementEnd('fieldset');
$this->elementEnd('form'); $this->elementEnd('form');
$this->elementEnd('li'); $this->elementEnd('li');
$this->elementStart('li', 'entity_delete');
$this->elementStart('form', array(
'id' => 'form_delete_application',
'class' => 'form_delete_application',
'method' => 'POST',
'action' => common_local_url('deleteapplication',
array('id' => $this->application->id))));
$this->elementStart('fieldset');
$this->hidden('token', common_session_token());
$this->submit('delete', _('Delete'));
$this->elementEnd('fieldset');
$this->elementEnd('form');
$this->elementEnd('li');
$this->elementEnd('ul'); $this->elementEnd('ul');
$this->elementEnd('div'); $this->elementEnd('div');

View File

@ -36,4 +36,34 @@ class Consumer extends Memcached_DataObject
return $cons; return $cons;
} }
/**
* Delete a Consumer and related tokens and nonces
*
* XXX: Should this happen in an OAuthDataStore instead?
*
*/
function delete()
{
// XXX: Is there any reason NOT to do this kind of cleanup?
$this->_deleteTokens();
$this->_deleteNonces();
parent::delete();
}
function _deleteTokens()
{
$token = new Token();
$token->consumer_key = $this->consumer_key;
$token->delete();
}
function _deleteNonces()
{
$nonce = new Nonce();
$nonce->consumer_key = $this->consumer_key;
$nonce->delete();
}
} }

View File

@ -137,4 +137,21 @@ class Oauth_application extends Memcached_DataObject
} }
} }
function delete()
{
$this->_deleteAppUsers();
$consumer = $this->getConsumer();
$consumer->delete();
parent::delete();
}
function _deleteAppUsers()
{
$oauser = new Oauth_application_user();
$oauser->application_id = $this->id;
$oauser->delete();
}
} }

View File

@ -353,7 +353,7 @@ notice_id = K
id = 129 id = 129
owner = 129 owner = 129
consumer_key = 130 consumer_key = 130
name = 130 name = 2
description = 2 description = 2
icon = 130 icon = 130
source_url = 2 source_url = 2
@ -367,6 +367,7 @@ modified = 384
[oauth_application__keys] [oauth_application__keys]
id = N id = N
name = U
[oauth_application_user] [oauth_application_user]
profile_id = 129 profile_id = 129

View File

@ -214,7 +214,7 @@ create table oauth_application (
id integer auto_increment primary key comment 'unique identifier', id integer auto_increment primary key comment 'unique identifier',
owner integer not null comment 'owner of the application' references profile (id), owner integer not null comment 'owner of the application' references profile (id),
consumer_key varchar(255) not null comment 'application consumer key' references consumer (consumer_key), consumer_key varchar(255) not null comment 'application consumer key' references consumer (consumer_key),
name varchar(255) not null comment 'name of the application', name varchar(255) unique key comment 'name of the application',
description varchar(255) comment 'description of the application', description varchar(255) comment 'description of the application',
icon varchar(255) not null comment 'application icon', icon varchar(255) not null comment 'application icon',
source_url varchar(255) comment 'application homepage - used for source link', source_url varchar(255) comment 'application homepage - used for source link',
@ -230,7 +230,7 @@ create table oauth_application (
create table oauth_application_user ( create table oauth_application_user (
profile_id integer not null comment 'user of the application' references profile (id), profile_id integer not null comment 'user of the application' references profile (id),
application_id integer not null comment 'id of the application' references oauth_application (id), application_id integer not null comment 'id of the application' references oauth_application (id),
access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write, bit 3 = revoked', access_type tinyint default 0 comment 'access type, bit 1 = read, bit 2 = write',
token varchar(255) comment 'request or access token', token varchar(255) comment 'request or access token',
created datetime not null comment 'date this record was created', created datetime not null comment 'date this record was created',
modified timestamp comment 'date this record was modified', modified timestamp comment 'date this record was modified',

View File

@ -1250,10 +1250,27 @@ class ApiAction extends Action
case 'api': case 'api':
break; break;
default: default:
$name = null;
$url = null;
$ns = Notice_source::staticGet($source); $ns = Notice_source::staticGet($source);
if ($ns) { if ($ns) {
$source_name = '<a href="' . $ns->url . '">' . $ns->name . '</a>'; $name = $ns->name;
$url = $ns->url;
} else {
$app = Oauth_application::staticGet('name', $source);
if ($app) {
$name = $app->name;
$url = $app->source_url;
}
} }
if (!empty($name) && !empty($url)) {
$source_name = '<a href="' . $url . '">' . $name . '</a>';
}
break; break;
} }
return $source_name; return $source_name;

View File

@ -159,5 +159,32 @@ class ApiStatusNetOAuthDataStore extends StatusNetOAuthDataStore
} }
} }
/**
* Revoke specified access token
*
* Revokes the token specified by $token_key.
* Throws exceptions in case of error.
*
* @param string $token_key the token to be revoked
* @param int $type type of token (0 = req, 1 = access)
*
* @access public
*
* @return void
*/
public function revoke_token($token_key, $type = 0) {
$rt = new Token();
$rt->tok = $token_key;
$rt->type = $type;
$rt->state = 0;
if (!$rt->find(true)) {
throw new Exception('Tried to revoke unknown token');
}
if (!$rt->delete()) {
throw new Exception('Failed to delete revoked token');
}
}
} }

View File

@ -486,12 +486,28 @@ class NoticeListItem extends Widget
$this->out->element('span', 'device', $source_name); $this->out->element('span', 'device', $source_name);
break; break;
default: default:
$name = null;
$url = null;
$ns = Notice_source::staticGet($this->notice->source); $ns = Notice_source::staticGet($this->notice->source);
if ($ns) { if ($ns) {
$name = $ns->name;
$url = $ns->url;
} else {
$app = Oauth_application::staticGet('name', $this->notice->source);
if ($app) {
$name = $app->name;
$url = $app->source_url;
}
}
if (!empty($name) && !empty($url)) {
$this->out->elementStart('span', 'device'); $this->out->elementStart('span', 'device');
$this->out->element('a', array('href' => $ns->url, $this->out->element('a', array('href' => $url,
'rel' => 'external'), 'rel' => 'external'),
$ns->name); $name);
$this->out->elementEnd('span'); $this->out->elementEnd('span');
} else { } else {
$this->out->element('span', 'device', $source_name); $this->out->element('span', 'device', $source_name);

View File

@ -152,6 +152,10 @@ class Router
array('action' => 'editapplication'), array('action' => 'editapplication'),
array('id' => '[0-9]+') array('id' => '[0-9]+')
); );
$m->connect('settings/oauthapps/delete/:id',
array('action' => 'deleteapplication'),
array('id' => '[0-9]+')
);
// search // search