Commit ddb60a81 authored by Evan Prodromou's avatar Evan Prodromou

Merge remote branch 'gitorious/0.9.x' into 0.9.x

parents f11c1c77 6c77d86b
......@@ -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);
$this->clientError(_("Invalid request token or verifier.", 400, 'text'));
} 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;
}
}
This diff is collapsed.
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Action for displaying an OAuth verifier pin
*
* 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);
}
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 <zach@status.net>
* @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($title, $message, $verifier)
{
$this->verifier = $verifier;
$this->title = $title;
parent::__construct($title, $message);
}
/**
* Display content.
*
* @return nothing
*/
function showContent()
{
$this->element('div', array('class' => 'info'), $this->message);
$this->element('div', array('id' => 'oauth_pin'), $this->verifier);
}
}
......@@ -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,78 @@ class ApiOauthRequestTokenAction extends ApiOauthAction
$server->add_signature_method($hmac_method);
try {
$req = OAuthRequest::from_request();
$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;
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);
} 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.");
// 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(
$callback,
array('allowed_schemes' => array('http', 'https'))
);
}
}
......
<?php
/**
* StatusNet, the distributed open-source microblogging tool
*
* Delete a group
*
* 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 Group
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Brion Vibber <brion@status.net>
* @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/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) {
exit(1);
}
/**
* Delete a group
*
* This is the action for deleting a group.
*
* @category Group
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @author Brion Vibber <brion@status.net>
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
* @link http://status.net/
* @fixme merge more of this code with related variants
*/
class DeletegroupAction extends RedirectingAction
{
var $group = null;
/**
* Prepare to run
*
* @fixme merge common setup code with other group actions
* @fixme allow group admins to delete their own groups
*/
function prepare($args)
{
parent::prepare($args);
if (!common_logged_in()) {
$this->clientError(_('You must be logged in to delete a group.'));
return false;
}
$nickname_arg = $this->trimmed('nickname');
$id = intval($this->arg('id'));
if ($id) {
$this->group = User_group::staticGet('id', $id);
} else if ($nickname_arg) {
$nickname = common_canonical_nickname($nickname_arg);
// Permanent redirect on non-canonical nickname
if ($nickname_arg != $nickname) {
$args = array('nickname' => $nickname);
common_redirect(common_local_url('leavegroup', $args), 301);
return false;
}
$local = Local_group::staticGet('nickname', $nickname);
if (!$local) {
$this->clientError(_('No such group.'), 404);
return false;
}
$this->group = User_group::staticGet('id', $local->group_id);
} else {
$this->clientError(_('No nickname or ID.'), 404);
return false;
}
if (!$this->group) {
$this->clientError(_('No such group.'), 404);
return false;
}
$cur = common_current_user();
if (!$cur->hasRight(Right::DELETEGROUP)) {
$this->clientError(_('You are not allowed to delete this group.'), 403);
return false;
}
return true;
}
/**
* Handle the request
*
* On POST, delete the group.
*
* @param array $args unused
*
* @return void
*/
function handle($args)
{
parent::handle($args);
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
if ($this->arg('no')) {
$this->returnToPrevious();
return;
} elseif ($this->arg('yes')) {
$this->handlePost();
return;
}
}
$this->showPage();
}
function handlePost()
{
$cur = common_current_user();
try {
if (Event::handle('StartDeleteGroup', array($this->group))) {
$this->group->delete();
Event::handle('EndDeleteGroup', array($this->group));
}
} catch (Exception $e) {
$this->serverError(sprintf(_('Could not delete group %2$s.'),
$this->group->nickname));
}
if ($this->boolean('ajax')) {
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
$this->element('title', null, sprintf(_('Deleted group %2$s'),
$this->group->nickname));
$this->elementEnd('head');
$this->elementStart('body');
// @fixme add a sensible AJAX response form!
$this->elementEnd('body');
$this->elementEnd('html');
} else {
// @fixme if we could direct to the page on which this group
// would have shown... that would be awesome
common_redirect(common_local_url('groups'),
303);
}
}
function title() {
return _('Delete group');
}
function showContent() {
$this->areYouSureForm();
}
/**
* Confirm with user.
* Ripped from DeleteuserAction
*
* Shows a confirmation form.
*
* @fixme refactor common code for things like this
* @return void
*/
function areYouSureForm()
{
$id = $this->group->id;
$this->elementStart('form', array('id' => 'deletegroup-' . $id,
'method' => 'post',
'class' => 'form_settings form_entity_block',
'action' => common_local_url('deletegroup', array('id' => $this->group->id))));
$this->elementStart('fieldset');
$this->hidden('token', common_session_token());
$this->element('legend', _('Delete group'));
if (Event::handle('StartDeleteGroupForm', array($this, $this->group))) {
$this->element('p', null,
_('Are you sure you want to delete this group? '.
'This will clear all data about the group from the '.
'database, without a backup. ' .
'Public posts to this group will still appear in ' .
'individual timelines.'));
foreach ($this->args as $k => $v) {
if (substr($k, 0, 9) == 'returnto-') {
$this->hidden($k, $v);
}
}
Event::handle('EndDeleteGroupForm', array($this, $this->group));
}
$this->submit('form_action-no',
// TRANS: Button label on the delete group form.
_m('BUTTON','No'),
'submit form_action-primary',
'no',
// TRANS: Submit button title for 'No' when deleting a group.
_('Do not delete this group'));
$this->submit('form_action-yes',
// TRANS: Button label on the delete group form.
_m('BUTTON','Yes'),
'submit form_action-secondary',
'yes',
// TRANS: Submit button title for 'Yes' when deleting a group.
_('Delete this group'));
$this->elementEnd('fieldset');
$this->elementEnd('form');
}
}
\ No newline at end of file
......@@ -316,6 +316,12 @@ class ShowgroupAction extends GroupDesignAction
Event::handle('EndGroupSubscribe', array($this, $this->group));
}
$this->elementEnd('li');
if ($cur->hasRight(Right::DELETEGROUP)) {
$this->elementStart('li', 'entity_delete');
$df = new DeleteGroupForm($this, $this->group);
$df->show();
$this->elementEnd('li');
}
$this->elementEnd('ul');
$this->elementEnd('div');
}
......
......@@ -854,6 +854,7 @@ class Profile extends Memcached_DataObject
case Right::SANDBOXUSER:
case Right::SILENCEUSER:
case Right::DELETEUSER:
case Right::DELETEGROUP:
$result = $this->hasRole(Profile_role::MODERATOR);
break;
case Right::CONFIGURESITE:
......
......@@ -547,4 +547,46 @@ class User_group extends Memcached_DataObject
$group->query('COMMIT');
return $group;
}
/**
* Handle cascading deletion, on the model of notice and profile.
*
* This should handle freeing up cached entries for the group's
* id, nickname, URI, and aliases. There may be other areas that
* are not de-cached in the UI, including the sidebar lists on
* GroupsAction
*/
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();
}
// And related groups in the other direction...
$inst = new Related_group();
$inst->related_group_id = $this->id;
$inst->delete();
// Aliases and the local_group entry need to be cleared explicitly
// or we'll miss clearing some cache keys; that can make it hard
// to create a new group with one of those names or aliases.
$this->setAliases(array());
$local = Local_group::staticGet('group_id', $this->id);
if ($local) {
$local->delete();
}
} else {
common_log(LOG_WARN, "Ambiguous user_group->delete(); skipping related tables.");
}
parent::delete();
}
}
This diff is collapsed.
......@@ -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);
}
......
......@@ -30,13 +30,12 @@
if (!defined('STATUSNET')) {
exit(1);
}
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
......@@ -44,7 +43,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?
......@@ -77,6 +76,12 @@ class ApiOauthAction extends Action
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
......@@ -86,31 +91,19 @@ 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']);
function getCallback($url, $params)
{
foreach ($params as $k => $v) {
$url = $this->appendQueryVar($url,
OAuthUtil::urlencode_rfc3986($k),
OAuthUtil::urlencode_rfc3986($v));
$queryArray =