Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing

This commit is contained in:
Zach Copley 2010-02-24 01:40:14 +00:00
commit 8ad7629422
14 changed files with 666 additions and 305 deletions

View File

@ -1,7 +1,9 @@
<?php
/*
/**
* StatusNet - the distributed open-source microblogging tool
* Copyright (C) 2008, 2009, StatusNet, Inc.
* Copyright (C) 2008-2010, StatusNet, Inc.
*
* Subscription action.
*
* 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
@ -15,68 +17,142 @@
*
* 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/>.
*
* PHP version 5
*
* @category Action
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2008-2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
if (!defined('STATUSNET')) {
exit(1);
}
/**
* Subscription action
*
* Subscribing to a profile. Does not work for OMB 0.1 remote subscriptions,
* but may work for other remote subscription protocols, like OStatus.
*
* Takes parameters:
*
* - subscribeto: a profile ID
* - token: session token to prevent CSRF attacks
* - ajax: boolean; whether to return Ajax or full-browser results
*
* Only works if the current user is logged in.
*
* @category Action
* @package StatusNet
* @author Evan Prodromou <evan@status.net>
* @copyright 2008-2010 StatusNet, Inc.
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
* @link http://status.net/
*/
class SubscribeAction extends Action
{
var $user;
var $other;
function handle($args)
/**
* Check pre-requisites and instantiate attributes
*
* @param Array $args array of arguments (URL, GET, POST)
*
* @return boolean success flag
*/
function prepare($args)
{
parent::handle($args);
parent::prepare($args);
if (!common_logged_in()) {
$this->clientError(_('Not logged in.'));
return;
}
$user = common_current_user();
// Only allow POST requests
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
common_redirect(common_local_url('subscriptions', array('nickname' => $user->nickname)));
return;
$this->clientError(_('This action only accepts POST requests.'));
return false;
}
# CSRF protection
// CSRF protection
$token = $this->trimmed('token');
if (!$token || $token != common_session_token()) {
$this->clientError(_('There was a problem with your session token. Try again, please.'));
return;
$this->clientError(_('There was a problem with your session token.'.
' Try again, please.'));
return false;
}
// Only for logged-in users
$this->user = common_current_user();
if (empty($this->user)) {
$this->clientError(_('Not logged in.'));
return false;
}
// Profile to subscribe to
$other_id = $this->arg('subscribeto');
$other = User::staticGet('id', $other_id);
$this->other = Profile::staticGet('id', $other_id);
if (!$other) {
$this->clientError(_('Not a local user.'));
return;
if (empty($this->other)) {
$this->clientError(_('No such profile.'));
return false;
}
$result = subs_subscribe_to($user, $other);
// OMB 0.1 doesn't have a mechanism for local-server-
// originated subscription.
if (is_string($result)) {
$this->clientError($result);
return;
$omb01 = Remote_profile::staticGet('id', $other_id);
if (!empty($omb01)) {
$this->clientError(_('You cannot subscribe to an OMB 0.1'.
' remote profile with this action.'));
return false;
}
return true;
}
/**
* Handle request
*
* Does the subscription and returns results.
*
* @param Array $args unused.
*
* @return void
*/
function handle($args)
{
// Throws exception on error
Subscription::start($this->user->getProfile(),
$this->other);
if ($this->boolean('ajax')) {
$this->startHTML('text/xml;charset=utf-8');
$this->elementStart('head');
$this->element('title', null, _('Subscribed'));
$this->elementEnd('head');
$this->elementStart('body');
$unsubscribe = new UnsubscribeForm($this, $other->getProfile());
$unsubscribe = new UnsubscribeForm($this, $this->other->getProfile());
$unsubscribe->show();
$this->elementEnd('body');
$this->elementEnd('html');
} else {
common_redirect(common_local_url('subscriptions', array('nickname' =>
$user->nickname)),
303);
$url = common_local_url('subscriptions',
array('nickname' => $this->user->nickname));
common_redirect($url, 303);
}
}
}

View File

@ -501,7 +501,11 @@ class Memcached_DataObject extends Safe_DataObject
function raiseError($message, $type = null, $behaviour = null)
{
throw new ServerException("DB_DataObject error [$type]: $message");
$id = get_class($this);
if ($this->id) {
$id .= ':' . $this->id;
}
throw new ServerException("[$id] DB_DataObject error [$type]: $message");
}
static function cacheGet($keyPart)

View File

@ -104,6 +104,7 @@ class PoCo
function __construct($profile)
{
$this->preferredUsername = $profile->nickname;
$this->displayName = $profile->getBestName();
$this->note = $profile->bio;
$this->address = new PoCoAddress($profile->location);
@ -129,6 +130,12 @@ class PoCo
$this->preferredUsername
);
$xs->element(
'poco:displayName',
null,
$this->displayName
);
if (!empty($this->note)) {
$xs->element('poco:note', null, $this->note);
}
@ -823,7 +830,9 @@ class Activity
if ($namespace) {
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
'xmlns:georss' => 'http://www.georss.org/georss',
'xmlns:ostatus' => 'http://ostatus.org/schema/1.0',
'xmlns:poco' => 'http://portablecontacts.net/spec/1.0');
} else {
$attrs = array();
}

View File

@ -438,14 +438,15 @@ class NoticeListItem extends Widget
$this->out->text(_('at'));
$this->out->text(' ');
if (empty($url)) {
$this->out->element('span', array('class' => 'geo',
$this->out->element('abbr', array('class' => 'geo',
'title' => $latlon),
$name);
} else {
$this->out->element('a', array('class' => 'geo',
'title' => $latlon,
'href' => $url),
$this->out->elementStart('a', array('href' => $url));
$this->out->element('abbr', array('class' => 'geo',
'title' => $latlon),
$name);
$this->out->elementEnd('a');
}
$this->out->elementEnd('span');
}

View File

@ -224,7 +224,7 @@ class OStatusPlugin extends Plugin
*
*/
function onEndFindMentions($sender, $text, &$mentions)
function onStartFindMentions($sender, $text, &$mentions)
{
preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/',
$text,
@ -251,58 +251,6 @@ class OStatusPlugin extends Plugin
return true;
}
/**
* Notify remote server and garbage collect unused feeds on unsubscribe.
* @fixme send these operations to background queues
*
* @param User $user
* @param Profile $other
* @return hook return value
*/
function onEndUnsubscribe($profile, $other)
{
$user = User::staticGet('id', $profile->id);
if (empty($user)) {
return true;
}
$oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
if (empty($oprofile)) {
return true;
}
// Drop the PuSH subscription if there are no other subscribers.
if ($other->subscriberCount() == 0) {
common_log(LOG_INFO, "Unsubscribing from now-unused feed $oprofile->feeduri");
$oprofile->unsubscribe();
}
$act = new Activity();
$act->verb = ActivityVerb::UNFOLLOW;
$act->id = TagURI::mint('unfollow:%d:%d:%s',
$profile->id,
$other->id,
common_date_iso8601(time()));
$act->time = time();
$act->title = _("Unfollow");
$act->content = sprintf(_("%s stopped following %s."),
$profile->getBestName(),
$other->getBestName());
$act->actor = ActivityObject::fromProfile($profile);
$act->object = ActivityObject::fromProfile($other);
$oprofile->notifyActivity($act);
return true;
}
/**
* Make sure necessary tables are filled out.
*/
@ -312,6 +260,7 @@ class OStatusPlugin extends Plugin
$schema->ensureTable('ostatus_source', Ostatus_source::schemaDef());
$schema->ensureTable('feedsub', FeedSub::schemaDef());
$schema->ensureTable('hubsub', HubSub::schemaDef());
$schema->ensureTable('magicsig', Magicsig::schemaDef());
return true;
}
@ -338,13 +287,19 @@ class OStatusPlugin extends Plugin
function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
{
if ($notice->source == 'ostatus') {
$bits = parse_url($notice->uri);
$domain = $bits['host'];
if ($notice->url) {
$bits = parse_url($notice->url);
$domain = $bits['host'];
if (substr($domain, 0, 4) == 'www.') {
$name = substr($domain, 4);
} else {
$name = $domain;
}
$name = $domain;
$url = $notice->uri;
$title = sprintf(_m("Sent from %s via OStatus"), $domain);
return false;
$url = $notice->url;
$title = sprintf(_m("Sent from %s via OStatus"), $domain);
return false;
}
}
}
@ -359,12 +314,56 @@ class OStatusPlugin extends Plugin
{
$oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri);
if ($oprofile) {
$oprofile->processFeed($feed);
$oprofile->processFeed($feed, 'push');
} else {
common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
}
}
/**
* When about to subscribe to a remote user, start a server-to-server
* PuSH subscription if needed. If we can't establish that, abort.
*
* @fixme If something else aborts later, we could end up with a stray
* PuSH subscription. This is relatively harmless, though.
*
* @param Profile $subscriber
* @param Profile $other
*
* @return hook return code
*
* @throws Exception
*/
function onStartSubscribe($subscriber, $other)
{
$user = User::staticGet('id', $subscriber->id);
if (empty($user)) {
return true;
}
$oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
if (empty($oprofile)) {
return true;
}
if (!$oprofile->subscribe()) {
throw new Exception(_m('Could not set up remote subscription.'));
}
}
/**
* Having established a remote subscription, send a notification to the
* remote OStatus profile's endpoint.
*
* @param Profile $subscriber
* @param Profile $other
*
* @return hook return code
*
* @throws Exception
*/
function onEndSubscribe($subscriber, $other)
{
$user = User::staticGet('id', $subscriber->id);
@ -402,6 +401,54 @@ class OStatusPlugin extends Plugin
return true;
}
/**
* Notify remote server and garbage collect unused feeds on unsubscribe.
* @fixme send these operations to background queues
*
* @param User $user
* @param Profile $other
* @return hook return value
*/
function onEndUnsubscribe($profile, $other)
{
$user = User::staticGet('id', $profile->id);
if (empty($user)) {
return true;
}
$oprofile = Ostatus_profile::staticGet('profile_id', $other->id);
if (empty($oprofile)) {
return true;
}
// Drop the PuSH subscription if there are no other subscribers.
$oprofile->garbageCollect();
$act = new Activity();
$act->verb = ActivityVerb::UNFOLLOW;
$act->id = TagURI::mint('unfollow:%d:%d:%s',
$profile->id,
$other->id,
common_date_iso8601(time()));
$act->time = time();
$act->title = _("Unfollow");
$act->content = sprintf(_("%s stopped following %s."),
$profile->getBestName(),
$other->getBestName());
$act->actor = ActivityObject::fromProfile($profile);
$act->object = ActivityObject::fromProfile($other);
$oprofile->notifyActivity($act);
return true;
}
/**
* When one of our local users tries to join a remote group,
* notify the remote server. If the notification is rejected,
@ -417,6 +464,10 @@ class OStatusPlugin extends Plugin
{
$oprofile = Ostatus_profile::staticGet('group_id', $group->id);
if ($oprofile) {
if (!$oprofile->subscribe()) {
throw new Exception(_m('Could not set up remote group membership.'));
}
$member = Profile::staticGet($user->id);
$act = new Activity();
@ -438,7 +489,8 @@ class OStatusPlugin extends Plugin
if ($oprofile->notifyActivity($act)) {
return true;
} else {
throw new ServerException(_m("Failed joining remote group."));
$oprofile->garbageCollect();
throw new Exception(_m("Failed joining remote group."));
}
}
}
@ -463,12 +515,7 @@ class OStatusPlugin extends Plugin
$oprofile = Ostatus_profile::staticGet('group_id', $group->id);
if ($oprofile) {
// Drop the PuSH subscription if there are no other subscribers.
$members = $group->getMembers(0, 1);
if ($members->N == 0) {
common_log(LOG_INFO, "Unsubscribing from now-unused group feed $oprofile->feeduri");
$oprofile->unsubscribe();
}
$oprofile->garbageCollect();
$member = Profile::staticGet($user->id);

View File

@ -87,53 +87,168 @@ class OStatusSubAction extends Action
*/
function showPreviewForm()
{
$this->elementStart('form', array('method' => 'post',
'id' => 'form_ostatus_sub',
'class' => 'form_settings',
'action' =>
common_local_url('ostatussub')));
$this->hidden('token', common_session_token());
$this->hidden('profile', $this->profile_uri);
$this->elementStart('fieldset', array('id' => 'settings_feeds'));
if ($this->oprofile->isGroup()) {
$this->previewGroup();
$this->submit('subscribe', _m('Join'));
$ok = $this->previewGroup();
} else {
$this->previewUser();
$this->submit('subscribe', _m('Subscribe'));
$ok = $this->previewUser();
}
if (!$ok) {
// @fixme maybe provide a cancel button or link back?
return;
}
$this->elementStart('div', 'entity_actions');
$this->elementStart('ul');
$this->elementStart('li', 'entity_subscribe');
$this->elementStart('form', array('method' => 'post',
'id' => 'form_ostatus_sub',
'class' => 'form_remote_authorize',
'action' =>
common_local_url('ostatussub')));
$this->elementStart('fieldset');
$this->hidden('token', common_session_token());
$this->hidden('profile', $this->profile_uri);
if ($this->oprofile->isGroup()) {
$this->submit('submit', _m('Join'), 'submit', null,
_m('Join this group'));
} else {
$this->submit('submit', _m('Subscribe'), 'submit', null,
_m('Subscribe to this user'));
}
$this->elementEnd('fieldset');
$this->elementEnd('form');
$this->elementEnd('li');
$this->elementEnd('ul');
$this->elementEnd('div');
}
/**
* Show a preview for a remote user's profile
* @return boolean true if we're ok to try subscribing
*/
function previewUser()
{
$oprofile = $this->oprofile;
$profile = $oprofile->localProfile();
$this->text(sprintf(_m("Remote user %s"), $profile->nickname));
// ...
$cur = common_current_user();
if ($cur->isSubscribed($profile)) {
$this->element('div', array('class' => 'error'),
_m("You are already subscribed to this user."));
$ok = false;
} else {
$ok = true;
}
$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
$avatarUrl = $avatar ? $avatar->displayUrl() : false;
$this->showEntity($profile,
$profile->profileurl,
$avatarUrl,
$profile->bio);
return $ok;
}
/**
* Show a preview for a remote group's profile
* @return boolean true if we're ok to try joining
*/
function previewGroup()
{
$oprofile = $this->oprofile;
$group = $oprofile->localGroup();
$this->text(sprintf(_m("Remote group %s"), $group->nickname));
// ..
$cur = common_current_user();
if ($cur->isMember($group)) {
$this->element('div', array('class' => 'error'),
_m("You are already a member of this group."));
$ok = false;
} else {
$ok = true;
}
$this->showEntity($group,
$group->getProfileUrl(),
$group->homepage_logo,
$group->description);
return $ok;
}
function showEntity($entity, $profile, $avatar, $note)
{
$nickname = $entity->nickname;
$fullname = $entity->fullname;
$homepage = $entity->homepage;
$location = $entity->location;
if (!$avatar) {
$avatar = Avatar::defaultImage(AVATAR_PROFILE_SIZE);
}
$this->elementStart('div', 'entity_profile vcard');
$this->elementStart('dl', 'entity_depiction');
$this->element('dt', null, _('Photo'));
$this->elementStart('dd');
$this->element('img', array('src' => $avatar,
'class' => 'photo avatar',
'width' => AVATAR_PROFILE_SIZE,
'height' => AVATAR_PROFILE_SIZE,
'alt' => $nickname));
$this->elementEnd('dd');
$this->elementEnd('dl');
$this->elementStart('dl', 'entity_nickname');
$this->element('dt', null, _('Nickname'));
$this->elementStart('dd');
$hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname';
$this->elementStart('a', array('href' => $profile,
'class' => 'url '.$hasFN));
$this->raw($nickname);
$this->elementEnd('a');
$this->elementEnd('dd');
$this->elementEnd('dl');
if (!is_null($fullname)) {
$this->elementStart('dl', 'entity_fn');
$this->elementStart('dd');
$this->elementStart('span', 'fn');
$this->raw($fullname);
$this->elementEnd('span');
$this->elementEnd('dd');
$this->elementEnd('dl');
}
if (!is_null($location)) {
$this->elementStart('dl', 'entity_location');
$this->element('dt', null, _('Location'));
$this->elementStart('dd', 'label');
$this->raw($location);
$this->elementEnd('dd');
$this->elementEnd('dl');
}
if (!is_null($homepage)) {
$this->elementStart('dl', 'entity_url');
$this->element('dt', null, _('URL'));
$this->elementStart('dd');
$this->elementStart('a', array('href' => $homepage,
'class' => 'url'));
$this->raw($homepage);
$this->elementEnd('a');
$this->elementEnd('dd');
$this->elementEnd('dl');
}
if (!is_null($note)) {
$this->elementStart('dl', 'entity_note');
$this->element('dt', null, _('Note'));
$this->elementStart('dd', 'note');
$this->raw($note);
$this->elementEnd('dd');
$this->elementEnd('dl');
}
$this->elementEnd('div');
}
/**
@ -173,10 +288,15 @@ class OStatusSubAction extends Action
}
$this->profile_uri = $profile_uri;
// @fixme validate, normalize bla bla
try {
$oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
$this->oprofile = $oprofile;
if (Validate::email($this->profile_uri)) {
$this->oprofile = Ostatus_profile::ensureWebfinger($this->profile_uri);
} else if (Validate::uri($this->profile_uri)) {
$this->oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
} else {
$this->error = _m("Invalid address format.");
return false;
}
return true;
} catch (FeedSubBadURLException $e) {
$this->error = _m('Invalid URL or could not reach server.');
@ -209,11 +329,6 @@ class OStatusSubAction extends Action
// And subscribe the current user to the local profile
$user = common_current_user();
if (!$this->oprofile->subscribe()) {
$this->showForm(_m("Failed to set up server-to-server subscription."));
return;
}
if ($this->oprofile->isGroup()) {
$group = $this->oprofile->localGroup();
if ($user->isMember($group)) {
@ -287,7 +402,7 @@ class OStatusSubAction extends Action
}
if ($this->validateFeed()) {
if ($this->arg('subscribe')) {
if ($this->arg('submit')) {
$this->saveFeed();
return;
}
@ -343,7 +458,7 @@ class OStatusSubAction extends Action
function showPageNotice()
{
if ($this->error) {
if (!empty($this->error)) {
$this->element('p', 'error', $this->error);
}
}

View File

@ -65,12 +65,38 @@ class WebfingerAction extends Action
'format' => 'atom')),
'type' => 'application/atom+xml');
// hCard
$xrd->links[] = array('rel' => 'http://microformats.org/profile/hcard',
'type' => 'text/html',
'href' => common_profile_url($nick));
// XFN
$xrd->links[] = array('rel' => 'http://gmpg.org/xfn/11',
'type' => 'text/html',
'href' => common_profile_url($nick));
// FOAF
$xrd->links[] = array('rel' => 'describedby',
'type' => 'application/rdf+xml',
'href' => common_local_url('foaf',
array('nickname' => $nick)));
$salmon_url = common_local_url('salmon',
array('id' => $this->user->id));
$xrd->links[] = array('rel' => 'salmon',
'href' => $salmon_url);
// Get this user's keypair
$magickey = Magicsig::staticGet('user_id', $this->user->id);
if (!$magickey) {
// No keypair yet, let's generate one.
$magickey = new Magicsig();
$magickey->generate();
}
$xrd->links[] = array('rel' => Magicsig::PUBLICKEYREL,
'href' => 'data:application/magic-public-key;'. $magickey->keypair);
// TODO - finalize where the redirect should go on the publisher
$url = common_local_url('ostatussub') . '?profile={uri}';
$xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',

View File

@ -29,45 +29,86 @@
require_once 'Crypt/RSA.php';
interface Magicsig
class Magicsig extends Memcached_DataObject
{
public function sign($bytes);
public function verify($signed, $signature_b64);
}
class MagicsigRsaSha256
{
public $keypair;
const PUBLICKEYREL = 'magic-public-key';
public function __construct($init = null)
public $__table = 'magicsig';
public $user_id;
public $keypair;
public $alg;
private $_rsa;
public function __construct($alg = 'RSA-SHA256')
{
if (is_null($init)) {
$this->generate();
} else {
$this->fromString($init);
}
$this->alg = $alg;
}
public /*static*/ function staticGet($k, $v=null)
{
return parent::staticGet(__CLASS__, $k, $v);
}
function table()
{
return array(
'user_id' => DB_DATAOBJECT_INT,
'keypair' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
'alg' => DB_DATAOBJECT_STR
);
}
static function schemaDef()
{
return array(new ColumnDef('user_id', 'integer',
null, true, 'PRI'),
new ColumnDef('keypair', 'varchar',
255, false),
new ColumnDef('alg', 'varchar',
64, false));
}
function keys()
{
return array_keys($this->keyTypes());
}
function keyTypes()
{
return array('user_id' => 'K');
}
function insert()
{
$this->keypair = $this->toString();
return parent::insert();
}
public function generate($key_length = 512)
{
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
$keypair = new Crypt_RSA_KeyPair($key_length);
$params['public_key'] = $keypair->getPublicKey();
$params['private_key'] = $keypair->getPrivateKey();
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
$this->keypair = new Crypt_RSA($params);
$this->_rsa = new Crypt_RSA($params);
PEAR::popErrorHandling();
$this->insert();
}
public function toString($full_pair = true)
{
$public_key = $this->keypair->_public_key;
$private_key = $this->keypair->_private_key;
$public_key = $this->_rsa->_public_key;
$private_key = $this->_rsa->_private_key;
$mod = base64_url_encode($public_key->getModulus());
$exp = base64_url_encode($public_key->getExponent());
@ -79,10 +120,12 @@ class MagicsigRsaSha256
return 'RSA.' . $mod . '.' . $exp . $private_exp;
}
public function fromString($text)
public static function fromString($text)
{
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
$magic_sig = new Magicsig();
// remove whitespace
$text = preg_replace('/\s+/', '', $text);
@ -100,33 +143,46 @@ class MagicsigRsaSha256
$params['public_key'] = new Crypt_RSA_KEY($mod, $exp, 'public');
if ($params['public_key']->isError()) {
$error = $params['public_key']->getLastError();
print $error->getMessage();
exit;
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
return false;
}
if ($private_exp) {
$params['private_key'] = new Crypt_RSA_KEY($mod, $private_exp, 'private');
if ($params['private_key']->isError()) {
$error = $params['private_key']->getLastError();
print $error->getMessage();
exit;
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
return false;
}
}
$this->keypair = new Crypt_RSA($params);
$magic_sig->_rsa = new Crypt_RSA($params);
PEAR::popErrorHandling();
return $magic_sig;
}
public function getName()
{
return 'RSA-SHA256';
return $this->alg;
}
public function getHash()
{
switch ($this->alg) {
case 'RSA-SHA256':
return 'sha256';
}
}
public function sign($bytes)
{
$sig = $this->keypair->createSign($bytes, null, 'sha256');
if ($this->keypair->isError()) {
$error = $this->keypair->getLastError();
$sig = $this->_rsa->createSign($bytes, null, 'sha256');
if ($this->_rsa->isError()) {
$error = $this->_rsa->getLastError();
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
return false;
}
return $sig;
@ -134,11 +190,11 @@ class MagicsigRsaSha256
public function verify($signed_bytes, $signature)
{
$result = $this->keypair->validateSign($signed_bytes, $signature, null, 'sha256');
if ($this->keypair->isError()) {
$result = $this->_rsa->validateSign($signed_bytes, $signature, null, 'sha256');
if ($this->_rsa->isError()) {
$error = $this->keypair->getLastError();
//common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
print $error->getMessage();
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
return false;
}
return $result;
}

View File

@ -346,6 +346,29 @@ class Ostatus_profile extends Memcached_DataObject
}
}
/**
* Check if this remote profile has any active local subscriptions, and
* if not drop the PuSH subscription feed.
*
* @return boolean
*/
public function garbageCollect()
{
if ($this->isGroup()) {
$members = $this->localGroup()->getMembers(0, 1);
$count = $members->N;
} else {
$count = $this->localProfile()->subscriberCount();
}
if ($count == 0) {
common_log(LOG_INFO, "Unsubscribing from now-unused remote feed $oprofile->feeduri");
$this->unsubscribe();
return true;
} else {
return false;
}
}
/**
* Send an Activity Streams notification to the remote Salmon endpoint,
* if so configured.
@ -379,7 +402,8 @@ class Ostatus_profile extends Memcached_DataObject
'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
'xmlns:georss' => 'http://www.georss.org/georss',
'xmlns:ostatus' => 'http://ostatus.org/schema/1.0');
'xmlns:ostatus' => 'http://ostatus.org/schema/1.0',
'xmlns:poco' => 'http://portablecontacts.net/spec/1.0');
$entry = new XMLStringer();
$entry->elementStart('entry', $attributes);
@ -464,7 +488,7 @@ class Ostatus_profile extends Memcached_DataObject
*
* @param DOMDocument $feed
*/
public function processFeed($feed)
public function processFeed($feed, $source)
{
$entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
if ($entries->length == 0) {
@ -474,7 +498,7 @@ class Ostatus_profile extends Memcached_DataObject
for ($i = 0; $i < $entries->length; $i++) {
$entry = $entries->item($i);
$this->processEntry($entry, $feed);
$this->processEntry($entry, $feed, $source);
}
}
@ -484,15 +508,12 @@ class Ostatus_profile extends Memcached_DataObject
* @param DOMElement $entry
* @param DOMElement $feed for context
*/
protected function processEntry($entry, $feed)
public function processEntry($entry, $feed, $source)
{
$activity = new Activity($entry, $feed);
$debug = var_export($activity, true);
common_log(LOG_DEBUG, $debug);
if ($activity->verb == ActivityVerb::POST) {
$this->processPost($activity);
$this->processPost($activity, $source);
} else {
common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb");
}
@ -501,130 +522,176 @@ class Ostatus_profile extends Memcached_DataObject
/**
* Process an incoming post activity from this remote feed.
* @param Activity $activity
* @param string $method 'push' or 'salmon'
* @return mixed saved Notice or false
* @fixme break up this function, it's getting nasty long
*/
protected function processPost($activity)
public function processPost($activity, $method)
{
if ($this->isGroup()) {
// A group feed will contain posts from multiple authors.
// @fixme validate these profiles in some way!
$oprofile = self::ensureActorProfile($activity);
if ($oprofile->isGroup()) {
// Groups can't post notices in StatusNet.
common_log(LOG_WARNING, "OStatus: skipping post with group listed as author: $oprofile->uri in feed from $this->uri");
return false;
}
} else {
// Individual user feeds may contain only posts from themselves.
// Authorship is validated against the profile URI on upper layers,
// through PuSH setup or Salmon signature checks.
$actorUri = self::getActorProfileURI($activity);
if ($actorUri == $this->uri) {
// @fixme check if profile info has changed and update it
} else {
// @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely
common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->uri");
//return;
common_log(LOG_WARNING, "OStatus: skipping post with bad author: got $actorUri expected $this->uri");
return false;
}
$oprofile = $this;
}
// The id URI will be used as a unique identifier for for the notice,
// protecting against duplicate saves. It isn't required to be a URL;
// tag: URIs for instance are found in Google Buzz feeds.
$sourceUri = $activity->object->id;
$dupe = Notice::staticGet('uri', $sourceUri);
if ($dupe) {
common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri");
return;
return false;
}
// We'll also want to save a web link to the original notice, if provided.
$sourceUrl = null;
if ($activity->object->link) {
$sourceUrl = $activity->object->link;
} else if ($activity->link) {
$sourceUrl = $activity->link;
} else if (preg_match('!^https?://!', $activity->object->id)) {
$sourceUrl = $activity->object->id;
}
// @fixme sanitize and save HTML content if available
// Get (safe!) HTML and text versions of the content
$rendered = $this->purify($activity->object->content);
$content = html_entity_decode(strip_tags($rendered));
$content = $activity->object->title;
$params = array('is_local' => Notice::REMOTE_OMB,
$options = array('is_local' => Notice::REMOTE_OMB,
'url' => $sourceUrl,
'uri' => $sourceUri);
'uri' => $sourceUri,
'rendered' => $rendered,
'replies' => array(),
'groups' => array());
$location = $activity->context->location;
// Check for optional attributes...
if ($location) {
$params['lat'] = $location->lat;
$params['lon'] = $location->lon;
if ($location->location_id) {
$params['location_ns'] = $location->location_ns;
$params['location_id'] = $location->location_id;
}
if (!empty($activity->time)) {
$options['created'] = common_sql_date($activity->time);
}
$profile = $oprofile->localProfile();
$params['groups'] = array();
$params['replies'] = array();
if ($activity->context) {
foreach ($activity->context->attention as $recipient) {
$roprofile = Ostatus_profile::staticGet('uri', $recipient);
if ($roprofile) {
if ($roprofile->isGroup()) {
// Deliver to local recipients of this remote group.
// @fixme sender verification?
$params['groups'][] = $roprofile->group_id;
continue;
} else {
// Delivery to remote users is the source service's job.
continue;
}
// Any individual or group attn: targets?
$replies = $activity->context->attention;
$options['groups'] = $this->filterReplies($oprofile, $replies);
$options['replies'] = $replies;
// Maintain direct reply associations
// @fixme what about conversation ID?
if (!empty($activity->context->replyToID)) {
$orig = Notice::staticGet('uri',
$activity->context->replyToID);
if (!empty($orig)) {
$options['reply_to'] = $orig->id;
}
$user = User::staticGet('uri', $recipient);
if ($user) {
// An @-reply directed to a local user.
// @fixme sender verification, spam etc?
$params['replies'][] = $recipient;
continue;
}
// @fixme we need a uri on user_group
// $group = User_group::staticGet('uri', $recipient);
$template = common_local_url('groupbyid', array('id' => '31337'));
$template = preg_quote($template, '/');
$template = str_replace('31337', '(\d+)', $template);
common_log(LOG_DEBUG, $template);
if (preg_match("/$template/", $recipient, $matches)) {
$id = $matches[1];
$group = User_group::staticGet('id', $id);
if ($group) {
// Deliver to all members of this local group.
// @fixme sender verification?
if ($profile->isMember($group)) {
common_log(LOG_DEBUG, "delivering to group $id $group->nickname");
$params['groups'][] = $group->id;
} else {
common_log(LOG_DEBUG, "not delivering to group $id $group->nickname because sender $profile->nickname is not a member");
}
continue;
} else {
common_log(LOG_DEBUG, "not delivering to missing group $id");
}
} else {
common_log(LOG_DEBUG, "not delivering to groups for $recipient");
}
$location = $activity->context->location;
if ($location) {
$options['lat'] = $location->lat;
$options['lon'] = $location->lon;
if ($location->location_id) {
$options['location_ns'] = $location->location_ns;
$options['location_id'] = $location->location_id;
}
}
}
try {
$saved = Notice::saveNew($profile->id,
$saved = Notice::saveNew($oprofile->profile_id,
$content,
'ostatus',
$params);
$options);
if ($saved) {
Ostatus_source::saveNew($saved, $this, $method);
}
} catch (Exception $e) {
common_log(LOG_ERR, "Failed saving notice entry for $sourceUri: " . $e->getMessage());
return;
common_log(LOG_ERR, "OStatus save of remote message $sourceUri failed: " . $e->getMessage());
throw $e;
}
common_log(LOG_INFO, "OStatus saved remote message $sourceUri as notice id $saved->id");
return $saved;
}
// Record which feed this came through...
try {
Ostatus_source::saveNew($saved, $this, 'push');
} catch (Exception $e) {
common_log(LOG_ERR, "Failed saving ostatus_source entry for $saved->notice_id: " . $e->getMessage());
/**
* Clean up HTML
*/
protected function purify($html)
{
// @fixme disable caching or set a sane temp dir
require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php');
$purifier = new HTMLPurifier();
return $purifier->purify($html);
}
/**
* Filters a list of recipient ID URIs to just those for local delivery.
* @param Ostatus_profile local profile of sender
* @param array in/out &$attention_uris set of URIs, will be pruned on output
* @return array of group IDs
*/
protected function filterReplies($sender, &$attention_uris)
{
$groups = array();
$replies = array();
foreach ($attention_uris as $recipient) {
// Is the recipient a local user?
$user = User::staticGet('uri', $recipient);
if ($user) {
// @fixme sender verification, spam etc?
$replies[] = $recipient;
continue;
}
// Is the recipient a remote group?
$oprofile = Ostatus_profile::staticGet('uri', $recipient);
if ($oprofile) {
if ($oprofile->isGroup()) {
// Deliver to local members of this remote group.
// @fixme sender verification?
$groups[] = $oprofile->group_id;
}
continue;
}
// Is the recipient a local group?
// @fixme we need a uri on user_group
// $group = User_group::staticGet('uri', $recipient);
$template = common_local_url('groupbyid', array('id' => '31337'));
$template = preg_quote($template, '/');
$template = str_replace('31337', '(\d+)', $template);
if (preg_match("/$template/", $recipient, $matches)) {
$id = $matches[1];
$group = User_group::staticGet('id', $id);
if ($group) {
// Deliver to all members of this local group if allowed.
if ($sender->localProfile()->isMember($group)) {
$groups[] = $group->id;
}
continue;
}
}
}
$attention_uris = $replies;
return $groups;
}
/**

View File

@ -27,8 +27,6 @@
* @link http://status.net/
*/
require_once 'magicsig.php';
class MagicEnvelope
{
const ENCODING = 'base64url';
@ -64,7 +62,7 @@ class MagicEnvelope
return false;
}
$signature_alg = new MagicsigRsaSha256($this->getKeyPair($signer_uri));
$signature_alg = Magicsig::fromString($this->getKeyPair($signer_uri));
$armored_text = base64_encode($text);
return array(
@ -139,7 +137,7 @@ class MagicEnvelope
$text = base64_decode($env['data']);
$signer_uri = $this->getAuthor($text);
$verifier = new MagicsigRsaSha256($this->getKeyPair($signer_uri));
$verifier = Magicsig::fromString($this->getKeyPair($signer_uri));
return $verifier->verify($env['data'], $env['sig']);
}

View File

@ -185,54 +185,6 @@ class SalmonAction extends Action
function saveNotice()
{
$oprofile = $this->ensureProfile();
// Get (safe!) HTML and text versions of the content
require_once(INSTALLDIR.'/extlib/HTMLPurifier/HTMLPurifier.auto.php');
$html = $this->act->object->content;
$purifier = new HTMLPurifier();
$rendered = $purifier->purify($html);
$content = html_entity_decode(strip_tags($rendered));
$options = array('is_local' => Notice::REMOTE_OMB,
'uri' => $this->act->object->id,
'url' => $this->act->object->link,
'rendered' => $rendered,
'replies' => $this->act->context->attention);
if (!empty($this->act->context->location)) {
$options['lat'] = $location->lat;
$options['lon'] = $location->lon;
if ($location->location_id) {
$options['location_ns'] = $location->location_ns;
$options['location_id'] = $location->location_id;
}
}
if (!empty($this->act->context->replyToID)) {
$orig = Notice::staticGet('uri',
$this->act->context->replyToID);
if (!empty($orig)) {
$options['reply_to'] = $orig->id;
}
}
if (!empty($this->act->time)) {
$options['created'] = common_sql_date($this->act->time);
}
$saved = Notice::saveNew($oprofile->profile_id,
$content,
'ostatus+salmon',
$options);
// Record that this was saved through a validated Salmon source
// @fixme actually do the signature validation!
Ostatus_source::saveNew($saved, $oprofile, 'salmon');
return $saved;
return $oprofile->processPost($this->act, 'salmon');
}
}

View File

@ -108,6 +108,10 @@ class Webfinger
$content = $this->fetchURL($url);
if (!$content) {
return false;
}
return XRD::parse($content);
}

View File

@ -184,6 +184,7 @@ button.close,
.form_user_unsubscribe input.submit,
.form_group_join input.submit,
.form_user_subscribe input.submit,
.form_remote_authorize input.submit,
.entity_subscribe a,
.entity_moderation p,
.entity_sandbox input.submit,
@ -291,6 +292,7 @@ background-position:0 1px;
.form_group_leave input.submit,
.form_user_subscribe input.submit,
.form_user_unsubscribe input.submit,
.form_remote_authorize input.submit,
.entity_subscribe a {
background-color:#AAAAAA;
color:#FFFFFF;
@ -301,6 +303,7 @@ background-position:5px -1246px;
}
.form_group_join input.submit,
.form_user_subscribe input.submit,
.form_remote_authorize input.submit,
.entity_subscribe a {
background-position:5px -1181px;
}

View File

@ -184,6 +184,7 @@ button.close,
.form_user_unsubscribe input.submit,
.form_group_join input.submit,
.form_user_subscribe input.submit,
.form_remote_authorize input.submit,
.entity_subscribe a,
.entity_moderation p,
.entity_sandbox input.submit,
@ -290,6 +291,7 @@ background-position:0 1px;
.form_group_leave input.submit,
.form_user_subscribe input.submit,
.form_user_unsubscribe input.submit,
.form_remote_authorize input.submit,
.entity_subscribe a {
background-color:#AAAAAA;
color:#FFFFFF;
@ -300,6 +302,7 @@ background-position:5px -1246px;
}
.form_group_join input.submit,
.form_user_subscribe input.submit,
.form_remote_authorize input.submit,
.entity_subscribe a {
background-position:5px -1181px;
}