Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing
This commit is contained in:
commit
8ad7629422
|
@ -1,7 +1,9 @@
|
||||||
<?php
|
<?php
|
||||||
/*
|
/**
|
||||||
* StatusNet - the distributed open-source microblogging tool
|
* 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
|
* 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
|
* 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
|
* 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/>.
|
* 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
|
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()) {
|
// Only allow POST requests
|
||||||
$this->clientError(_('Not logged in.'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = common_current_user();
|
|
||||||
|
|
||||||
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
|
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
|
||||||
common_redirect(common_local_url('subscriptions', array('nickname' => $user->nickname)));
|
$this->clientError(_('This action only accepts POST requests.'));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
# CSRF protection
|
// CSRF protection
|
||||||
|
|
||||||
$token = $this->trimmed('token');
|
$token = $this->trimmed('token');
|
||||||
|
|
||||||
if (!$token || $token != common_session_token()) {
|
if (!$token || $token != common_session_token()) {
|
||||||
$this->clientError(_('There was a problem with your session token. Try again, please.'));
|
$this->clientError(_('There was a problem with your session token.'.
|
||||||
return;
|
' 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_id = $this->arg('subscribeto');
|
||||||
|
|
||||||
$other = User::staticGet('id', $other_id);
|
$this->other = Profile::staticGet('id', $other_id);
|
||||||
|
|
||||||
if (!$other) {
|
if (empty($this->other)) {
|
||||||
$this->clientError(_('Not a local user.'));
|
$this->clientError(_('No such profile.'));
|
||||||
return;
|
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)) {
|
$omb01 = Remote_profile::staticGet('id', $other_id);
|
||||||
$this->clientError($result);
|
|
||||||
return;
|
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')) {
|
if ($this->boolean('ajax')) {
|
||||||
$this->startHTML('text/xml;charset=utf-8');
|
$this->startHTML('text/xml;charset=utf-8');
|
||||||
$this->elementStart('head');
|
$this->elementStart('head');
|
||||||
$this->element('title', null, _('Subscribed'));
|
$this->element('title', null, _('Subscribed'));
|
||||||
$this->elementEnd('head');
|
$this->elementEnd('head');
|
||||||
$this->elementStart('body');
|
$this->elementStart('body');
|
||||||
$unsubscribe = new UnsubscribeForm($this, $other->getProfile());
|
$unsubscribe = new UnsubscribeForm($this, $this->other->getProfile());
|
||||||
$unsubscribe->show();
|
$unsubscribe->show();
|
||||||
$this->elementEnd('body');
|
$this->elementEnd('body');
|
||||||
$this->elementEnd('html');
|
$this->elementEnd('html');
|
||||||
} else {
|
} else {
|
||||||
common_redirect(common_local_url('subscriptions', array('nickname' =>
|
$url = common_local_url('subscriptions',
|
||||||
$user->nickname)),
|
array('nickname' => $this->user->nickname));
|
||||||
303);
|
common_redirect($url, 303);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -501,7 +501,11 @@ class Memcached_DataObject extends Safe_DataObject
|
||||||
|
|
||||||
function raiseError($message, $type = null, $behaviour = null)
|
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)
|
static function cacheGet($keyPart)
|
||||||
|
|
|
@ -104,6 +104,7 @@ class PoCo
|
||||||
function __construct($profile)
|
function __construct($profile)
|
||||||
{
|
{
|
||||||
$this->preferredUsername = $profile->nickname;
|
$this->preferredUsername = $profile->nickname;
|
||||||
|
$this->displayName = $profile->getBestName();
|
||||||
|
|
||||||
$this->note = $profile->bio;
|
$this->note = $profile->bio;
|
||||||
$this->address = new PoCoAddress($profile->location);
|
$this->address = new PoCoAddress($profile->location);
|
||||||
|
@ -129,6 +130,12 @@ class PoCo
|
||||||
$this->preferredUsername
|
$this->preferredUsername
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$xs->element(
|
||||||
|
'poco:displayName',
|
||||||
|
null,
|
||||||
|
$this->displayName
|
||||||
|
);
|
||||||
|
|
||||||
if (!empty($this->note)) {
|
if (!empty($this->note)) {
|
||||||
$xs->element('poco:note', null, $this->note);
|
$xs->element('poco:note', null, $this->note);
|
||||||
}
|
}
|
||||||
|
@ -823,7 +830,9 @@ class Activity
|
||||||
if ($namespace) {
|
if ($namespace) {
|
||||||
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
|
$attrs = array('xmlns' => 'http://www.w3.org/2005/Atom',
|
||||||
'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
|
'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 {
|
} else {
|
||||||
$attrs = array();
|
$attrs = array();
|
||||||
}
|
}
|
||||||
|
|
|
@ -438,14 +438,15 @@ class NoticeListItem extends Widget
|
||||||
$this->out->text(_('at'));
|
$this->out->text(_('at'));
|
||||||
$this->out->text(' ');
|
$this->out->text(' ');
|
||||||
if (empty($url)) {
|
if (empty($url)) {
|
||||||
$this->out->element('span', array('class' => 'geo',
|
$this->out->element('abbr', array('class' => 'geo',
|
||||||
'title' => $latlon),
|
'title' => $latlon),
|
||||||
$name);
|
$name);
|
||||||
} else {
|
} else {
|
||||||
$this->out->element('a', array('class' => 'geo',
|
$this->out->elementStart('a', array('href' => $url));
|
||||||
'title' => $latlon,
|
$this->out->element('abbr', array('class' => 'geo',
|
||||||
'href' => $url),
|
'title' => $latlon),
|
||||||
$name);
|
$name);
|
||||||
|
$this->out->elementEnd('a');
|
||||||
}
|
}
|
||||||
$this->out->elementEnd('span');
|
$this->out->elementEnd('span');
|
||||||
}
|
}
|
||||||
|
|
|
@ -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+)/',
|
preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/',
|
||||||
$text,
|
$text,
|
||||||
|
@ -251,58 +251,6 @@ class OStatusPlugin extends Plugin
|
||||||
return true;
|
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.
|
* Make sure necessary tables are filled out.
|
||||||
*/
|
*/
|
||||||
|
@ -312,6 +260,7 @@ class OStatusPlugin extends Plugin
|
||||||
$schema->ensureTable('ostatus_source', Ostatus_source::schemaDef());
|
$schema->ensureTable('ostatus_source', Ostatus_source::schemaDef());
|
||||||
$schema->ensureTable('feedsub', FeedSub::schemaDef());
|
$schema->ensureTable('feedsub', FeedSub::schemaDef());
|
||||||
$schema->ensureTable('hubsub', HubSub::schemaDef());
|
$schema->ensureTable('hubsub', HubSub::schemaDef());
|
||||||
|
$schema->ensureTable('magicsig', Magicsig::schemaDef());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,13 +287,19 @@ class OStatusPlugin extends Plugin
|
||||||
function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
|
function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
|
||||||
{
|
{
|
||||||
if ($notice->source == 'ostatus') {
|
if ($notice->source == 'ostatus') {
|
||||||
$bits = parse_url($notice->uri);
|
if ($notice->url) {
|
||||||
$domain = $bits['host'];
|
$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->url;
|
||||||
$url = $notice->uri;
|
$title = sprintf(_m("Sent from %s via OStatus"), $domain);
|
||||||
$title = sprintf(_m("Sent from %s via OStatus"), $domain);
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -359,12 +314,56 @@ class OStatusPlugin extends Plugin
|
||||||
{
|
{
|
||||||
$oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri);
|
$oprofile = Ostatus_profile::staticGet('feeduri', $feedsub->uri);
|
||||||
if ($oprofile) {
|
if ($oprofile) {
|
||||||
$oprofile->processFeed($feed);
|
$oprofile->processFeed($feed, 'push');
|
||||||
} else {
|
} else {
|
||||||
common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
|
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)
|
function onEndSubscribe($subscriber, $other)
|
||||||
{
|
{
|
||||||
$user = User::staticGet('id', $subscriber->id);
|
$user = User::staticGet('id', $subscriber->id);
|
||||||
|
@ -402,6 +401,54 @@ class OStatusPlugin extends Plugin
|
||||||
return true;
|
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,
|
* When one of our local users tries to join a remote group,
|
||||||
* notify the remote server. If the notification is rejected,
|
* 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);
|
$oprofile = Ostatus_profile::staticGet('group_id', $group->id);
|
||||||
if ($oprofile) {
|
if ($oprofile) {
|
||||||
|
if (!$oprofile->subscribe()) {
|
||||||
|
throw new Exception(_m('Could not set up remote group membership.'));
|
||||||
|
}
|
||||||
|
|
||||||
$member = Profile::staticGet($user->id);
|
$member = Profile::staticGet($user->id);
|
||||||
|
|
||||||
$act = new Activity();
|
$act = new Activity();
|
||||||
|
@ -438,7 +489,8 @@ class OStatusPlugin extends Plugin
|
||||||
if ($oprofile->notifyActivity($act)) {
|
if ($oprofile->notifyActivity($act)) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} 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);
|
$oprofile = Ostatus_profile::staticGet('group_id', $group->id);
|
||||||
if ($oprofile) {
|
if ($oprofile) {
|
||||||
// Drop the PuSH subscription if there are no other subscribers.
|
// Drop the PuSH subscription if there are no other subscribers.
|
||||||
|
$oprofile->garbageCollect();
|
||||||
$members = $group->getMembers(0, 1);
|
|
||||||
if ($members->N == 0) {
|
|
||||||
common_log(LOG_INFO, "Unsubscribing from now-unused group feed $oprofile->feeduri");
|
|
||||||
$oprofile->unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
$member = Profile::staticGet($user->id);
|
$member = Profile::staticGet($user->id);
|
||||||
|
|
|
@ -87,53 +87,168 @@ class OStatusSubAction extends Action
|
||||||
*/
|
*/
|
||||||
function showPreviewForm()
|
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()) {
|
if ($this->oprofile->isGroup()) {
|
||||||
$this->previewGroup();
|
$ok = $this->previewGroup();
|
||||||
$this->submit('subscribe', _m('Join'));
|
|
||||||
} else {
|
} else {
|
||||||
$this->previewUser();
|
$ok = $this->previewUser();
|
||||||
$this->submit('subscribe', _m('Subscribe'));
|
}
|
||||||
|
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('fieldset');
|
||||||
|
|
||||||
$this->elementEnd('form');
|
$this->elementEnd('form');
|
||||||
|
$this->elementEnd('li');
|
||||||
|
$this->elementEnd('ul');
|
||||||
|
$this->elementEnd('div');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a preview for a remote user's profile
|
* Show a preview for a remote user's profile
|
||||||
|
* @return boolean true if we're ok to try subscribing
|
||||||
*/
|
*/
|
||||||
function previewUser()
|
function previewUser()
|
||||||
{
|
{
|
||||||
$oprofile = $this->oprofile;
|
$oprofile = $this->oprofile;
|
||||||
$profile = $oprofile->localProfile();
|
$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
|
* Show a preview for a remote group's profile
|
||||||
|
* @return boolean true if we're ok to try joining
|
||||||
*/
|
*/
|
||||||
function previewGroup()
|
function previewGroup()
|
||||||
{
|
{
|
||||||
$oprofile = $this->oprofile;
|
$oprofile = $this->oprofile;
|
||||||
$group = $oprofile->localGroup();
|
$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;
|
$this->profile_uri = $profile_uri;
|
||||||
|
|
||||||
// @fixme validate, normalize bla bla
|
|
||||||
try {
|
try {
|
||||||
$oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
|
if (Validate::email($this->profile_uri)) {
|
||||||
$this->oprofile = $oprofile;
|
$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;
|
return true;
|
||||||
} catch (FeedSubBadURLException $e) {
|
} catch (FeedSubBadURLException $e) {
|
||||||
$this->error = _m('Invalid URL or could not reach server.');
|
$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
|
// And subscribe the current user to the local profile
|
||||||
$user = common_current_user();
|
$user = common_current_user();
|
||||||
|
|
||||||
if (!$this->oprofile->subscribe()) {
|
|
||||||
$this->showForm(_m("Failed to set up server-to-server subscription."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($this->oprofile->isGroup()) {
|
if ($this->oprofile->isGroup()) {
|
||||||
$group = $this->oprofile->localGroup();
|
$group = $this->oprofile->localGroup();
|
||||||
if ($user->isMember($group)) {
|
if ($user->isMember($group)) {
|
||||||
|
@ -287,7 +402,7 @@ class OStatusSubAction extends Action
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($this->validateFeed()) {
|
if ($this->validateFeed()) {
|
||||||
if ($this->arg('subscribe')) {
|
if ($this->arg('submit')) {
|
||||||
$this->saveFeed();
|
$this->saveFeed();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -343,7 +458,7 @@ class OStatusSubAction extends Action
|
||||||
|
|
||||||
function showPageNotice()
|
function showPageNotice()
|
||||||
{
|
{
|
||||||
if ($this->error) {
|
if (!empty($this->error)) {
|
||||||
$this->element('p', 'error', $this->error);
|
$this->element('p', 'error', $this->error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -65,12 +65,38 @@ class WebfingerAction extends Action
|
||||||
'format' => 'atom')),
|
'format' => 'atom')),
|
||||||
'type' => 'application/atom+xml');
|
'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',
|
$salmon_url = common_local_url('salmon',
|
||||||
array('id' => $this->user->id));
|
array('id' => $this->user->id));
|
||||||
|
|
||||||
$xrd->links[] = array('rel' => 'salmon',
|
$xrd->links[] = array('rel' => 'salmon',
|
||||||
'href' => $salmon_url);
|
'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
|
// TODO - finalize where the redirect should go on the publisher
|
||||||
$url = common_local_url('ostatussub') . '?profile={uri}';
|
$url = common_local_url('ostatussub') . '?profile={uri}';
|
||||||
$xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
$xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe',
|
||||||
|
|
|
@ -29,45 +29,86 @@
|
||||||
|
|
||||||
require_once 'Crypt/RSA.php';
|
require_once 'Crypt/RSA.php';
|
||||||
|
|
||||||
interface Magicsig
|
class Magicsig extends Memcached_DataObject
|
||||||
{
|
{
|
||||||
|
|
||||||
public function sign($bytes);
|
const PUBLICKEYREL = 'magic-public-key';
|
||||||
|
|
||||||
public function verify($signed, $signature_b64);
|
public $__table = 'magicsig';
|
||||||
}
|
|
||||||
|
|
||||||
class MagicsigRsaSha256
|
|
||||||
{
|
|
||||||
|
|
||||||
|
public $user_id;
|
||||||
public $keypair;
|
public $keypair;
|
||||||
|
public $alg;
|
||||||
|
|
||||||
public function __construct($init = null)
|
private $_rsa;
|
||||||
|
|
||||||
|
public function __construct($alg = 'RSA-SHA256')
|
||||||
{
|
{
|
||||||
if (is_null($init)) {
|
$this->alg = $alg;
|
||||||
$this->generate();
|
}
|
||||||
} else {
|
|
||||||
$this->fromString($init);
|
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)
|
public function generate($key_length = 512)
|
||||||
{
|
{
|
||||||
|
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
|
||||||
|
|
||||||
$keypair = new Crypt_RSA_KeyPair($key_length);
|
$keypair = new Crypt_RSA_KeyPair($key_length);
|
||||||
$params['public_key'] = $keypair->getPublicKey();
|
$params['public_key'] = $keypair->getPublicKey();
|
||||||
$params['private_key'] = $keypair->getPrivateKey();
|
$params['private_key'] = $keypair->getPrivateKey();
|
||||||
|
|
||||||
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
|
$this->_rsa = new Crypt_RSA($params);
|
||||||
$this->keypair = new Crypt_RSA($params);
|
|
||||||
PEAR::popErrorHandling();
|
PEAR::popErrorHandling();
|
||||||
|
|
||||||
|
$this->insert();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public function toString($full_pair = true)
|
public function toString($full_pair = true)
|
||||||
{
|
{
|
||||||
$public_key = $this->keypair->_public_key;
|
$public_key = $this->_rsa->_public_key;
|
||||||
$private_key = $this->keypair->_private_key;
|
$private_key = $this->_rsa->_private_key;
|
||||||
|
|
||||||
$mod = base64_url_encode($public_key->getModulus());
|
$mod = base64_url_encode($public_key->getModulus());
|
||||||
$exp = base64_url_encode($public_key->getExponent());
|
$exp = base64_url_encode($public_key->getExponent());
|
||||||
|
@ -79,10 +120,12 @@ class MagicsigRsaSha256
|
||||||
return 'RSA.' . $mod . '.' . $exp . $private_exp;
|
return 'RSA.' . $mod . '.' . $exp . $private_exp;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function fromString($text)
|
public static function fromString($text)
|
||||||
{
|
{
|
||||||
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
|
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
|
||||||
|
|
||||||
|
$magic_sig = new Magicsig();
|
||||||
|
|
||||||
// remove whitespace
|
// remove whitespace
|
||||||
$text = preg_replace('/\s+/', '', $text);
|
$text = preg_replace('/\s+/', '', $text);
|
||||||
|
|
||||||
|
@ -100,33 +143,46 @@ class MagicsigRsaSha256
|
||||||
$params['public_key'] = new Crypt_RSA_KEY($mod, $exp, 'public');
|
$params['public_key'] = new Crypt_RSA_KEY($mod, $exp, 'public');
|
||||||
if ($params['public_key']->isError()) {
|
if ($params['public_key']->isError()) {
|
||||||
$error = $params['public_key']->getLastError();
|
$error = $params['public_key']->getLastError();
|
||||||
print $error->getMessage();
|
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
|
||||||
exit;
|
return false;
|
||||||
}
|
}
|
||||||
if ($private_exp) {
|
if ($private_exp) {
|
||||||
$params['private_key'] = new Crypt_RSA_KEY($mod, $private_exp, 'private');
|
$params['private_key'] = new Crypt_RSA_KEY($mod, $private_exp, 'private');
|
||||||
if ($params['private_key']->isError()) {
|
if ($params['private_key']->isError()) {
|
||||||
$error = $params['private_key']->getLastError();
|
$error = $params['private_key']->getLastError();
|
||||||
print $error->getMessage();
|
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
|
||||||
exit;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->keypair = new Crypt_RSA($params);
|
$magic_sig->_rsa = new Crypt_RSA($params);
|
||||||
PEAR::popErrorHandling();
|
PEAR::popErrorHandling();
|
||||||
|
|
||||||
|
return $magic_sig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getName()
|
public function getName()
|
||||||
{
|
{
|
||||||
return 'RSA-SHA256';
|
return $this->alg;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHash()
|
||||||
|
{
|
||||||
|
switch ($this->alg) {
|
||||||
|
|
||||||
|
case 'RSA-SHA256':
|
||||||
|
return 'sha256';
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function sign($bytes)
|
public function sign($bytes)
|
||||||
{
|
{
|
||||||
$sig = $this->keypair->createSign($bytes, null, 'sha256');
|
$sig = $this->_rsa->createSign($bytes, null, 'sha256');
|
||||||
if ($this->keypair->isError()) {
|
if ($this->_rsa->isError()) {
|
||||||
$error = $this->keypair->getLastError();
|
$error = $this->_rsa->getLastError();
|
||||||
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
|
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $sig;
|
return $sig;
|
||||||
|
@ -134,11 +190,11 @@ class MagicsigRsaSha256
|
||||||
|
|
||||||
public function verify($signed_bytes, $signature)
|
public function verify($signed_bytes, $signature)
|
||||||
{
|
{
|
||||||
$result = $this->keypair->validateSign($signed_bytes, $signature, null, 'sha256');
|
$result = $this->_rsa->validateSign($signed_bytes, $signature, null, 'sha256');
|
||||||
if ($this->keypair->isError()) {
|
if ($this->_rsa->isError()) {
|
||||||
$error = $this->keypair->getLastError();
|
$error = $this->keypair->getLastError();
|
||||||
//common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
|
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
|
||||||
print $error->getMessage();
|
return false;
|
||||||
}
|
}
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
|
@ -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,
|
* Send an Activity Streams notification to the remote Salmon endpoint,
|
||||||
* if so configured.
|
* if so configured.
|
||||||
|
@ -379,7 +402,8 @@ class Ostatus_profile extends Memcached_DataObject
|
||||||
'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
|
'xmlns:activity' => 'http://activitystrea.ms/spec/1.0/',
|
||||||
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
|
'xmlns:thr' => 'http://purl.org/syndication/thread/1.0',
|
||||||
'xmlns:georss' => 'http://www.georss.org/georss',
|
'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 = new XMLStringer();
|
||||||
$entry->elementStart('entry', $attributes);
|
$entry->elementStart('entry', $attributes);
|
||||||
|
@ -464,7 +488,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||||
*
|
*
|
||||||
* @param DOMDocument $feed
|
* @param DOMDocument $feed
|
||||||
*/
|
*/
|
||||||
public function processFeed($feed)
|
public function processFeed($feed, $source)
|
||||||
{
|
{
|
||||||
$entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
|
$entries = $feed->getElementsByTagNameNS(Activity::ATOM, 'entry');
|
||||||
if ($entries->length == 0) {
|
if ($entries->length == 0) {
|
||||||
|
@ -474,7 +498,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||||
|
|
||||||
for ($i = 0; $i < $entries->length; $i++) {
|
for ($i = 0; $i < $entries->length; $i++) {
|
||||||
$entry = $entries->item($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 $entry
|
||||||
* @param DOMElement $feed for context
|
* @param DOMElement $feed for context
|
||||||
*/
|
*/
|
||||||
protected function processEntry($entry, $feed)
|
public function processEntry($entry, $feed, $source)
|
||||||
{
|
{
|
||||||
$activity = new Activity($entry, $feed);
|
$activity = new Activity($entry, $feed);
|
||||||
|
|
||||||
$debug = var_export($activity, true);
|
|
||||||
common_log(LOG_DEBUG, $debug);
|
|
||||||
|
|
||||||
if ($activity->verb == ActivityVerb::POST) {
|
if ($activity->verb == ActivityVerb::POST) {
|
||||||
$this->processPost($activity);
|
$this->processPost($activity, $source);
|
||||||
} else {
|
} else {
|
||||||
common_log(LOG_INFO, "Ignoring activity with unrecognized verb $activity->verb");
|
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.
|
* Process an incoming post activity from this remote feed.
|
||||||
* @param Activity $activity
|
* @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
|
* @fixme break up this function, it's getting nasty long
|
||||||
*/
|
*/
|
||||||
protected function processPost($activity)
|
public function processPost($activity, $method)
|
||||||
{
|
{
|
||||||
if ($this->isGroup()) {
|
if ($this->isGroup()) {
|
||||||
|
// A group feed will contain posts from multiple authors.
|
||||||
// @fixme validate these profiles in some way!
|
// @fixme validate these profiles in some way!
|
||||||
$oprofile = self::ensureActorProfile($activity);
|
$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 {
|
} 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);
|
$actorUri = self::getActorProfileURI($activity);
|
||||||
if ($actorUri == $this->uri) {
|
if ($actorUri == $this->uri) {
|
||||||
// @fixme check if profile info has changed and update it
|
// @fixme check if profile info has changed and update it
|
||||||
} else {
|
} else {
|
||||||
// @fixme drop or reject the messages once we've got the canonical profile URI recorded sanely
|
common_log(LOG_WARNING, "OStatus: skipping post with bad author: got $actorUri expected $this->uri");
|
||||||
common_log(LOG_INFO, "OStatus: Warning: non-group post with unexpected author: $actorUri expected $this->uri");
|
return false;
|
||||||
//return;
|
|
||||||
}
|
}
|
||||||
$oprofile = $this;
|
$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;
|
$sourceUri = $activity->object->id;
|
||||||
|
|
||||||
$dupe = Notice::staticGet('uri', $sourceUri);
|
$dupe = Notice::staticGet('uri', $sourceUri);
|
||||||
|
|
||||||
if ($dupe) {
|
if ($dupe) {
|
||||||
common_log(LOG_INFO, "OStatus: ignoring duplicate post: $sourceUri");
|
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;
|
$sourceUrl = null;
|
||||||
|
|
||||||
if ($activity->object->link) {
|
if ($activity->object->link) {
|
||||||
$sourceUrl = $activity->object->link;
|
$sourceUrl = $activity->object->link;
|
||||||
|
} else if ($activity->link) {
|
||||||
|
$sourceUrl = $activity->link;
|
||||||
} else if (preg_match('!^https?://!', $activity->object->id)) {
|
} else if (preg_match('!^https?://!', $activity->object->id)) {
|
||||||
$sourceUrl = $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;
|
$options = array('is_local' => Notice::REMOTE_OMB,
|
||||||
|
|
||||||
$params = array('is_local' => Notice::REMOTE_OMB,
|
|
||||||
'url' => $sourceUrl,
|
'url' => $sourceUrl,
|
||||||
'uri' => $sourceUri);
|
'uri' => $sourceUri,
|
||||||
|
'rendered' => $rendered,
|
||||||
|
'replies' => array(),
|
||||||
|
'groups' => array());
|
||||||
|
|
||||||
$location = $activity->context->location;
|
// Check for optional attributes...
|
||||||
|
|
||||||
if ($location) {
|
if (!empty($activity->time)) {
|
||||||
$params['lat'] = $location->lat;
|
$options['created'] = common_sql_date($activity->time);
|
||||||
$params['lon'] = $location->lon;
|
|
||||||
if ($location->location_id) {
|
|
||||||
$params['location_ns'] = $location->location_ns;
|
|
||||||
$params['location_id'] = $location->location_id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$profile = $oprofile->localProfile();
|
|
||||||
$params['groups'] = array();
|
|
||||||
$params['replies'] = array();
|
|
||||||
if ($activity->context) {
|
if ($activity->context) {
|
||||||
foreach ($activity->context->attention as $recipient) {
|
// Any individual or group attn: targets?
|
||||||
$roprofile = Ostatus_profile::staticGet('uri', $recipient);
|
$replies = $activity->context->attention;
|
||||||
if ($roprofile) {
|
$options['groups'] = $this->filterReplies($oprofile, $replies);
|
||||||
if ($roprofile->isGroup()) {
|
$options['replies'] = $replies;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$user = User::staticGet('uri', $recipient);
|
// Maintain direct reply associations
|
||||||
if ($user) {
|
// @fixme what about conversation ID?
|
||||||
// An @-reply directed to a local user.
|
if (!empty($activity->context->replyToID)) {
|
||||||
// @fixme sender verification, spam etc?
|
$orig = Notice::staticGet('uri',
|
||||||
$params['replies'][] = $recipient;
|
$activity->context->replyToID);
|
||||||
continue;
|
if (!empty($orig)) {
|
||||||
|
$options['reply_to'] = $orig->id;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// @fixme we need a uri on user_group
|
$location = $activity->context->location;
|
||||||
// $group = User_group::staticGet('uri', $recipient);
|
if ($location) {
|
||||||
$template = common_local_url('groupbyid', array('id' => '31337'));
|
$options['lat'] = $location->lat;
|
||||||
$template = preg_quote($template, '/');
|
$options['lon'] = $location->lon;
|
||||||
$template = str_replace('31337', '(\d+)', $template);
|
if ($location->location_id) {
|
||||||
common_log(LOG_DEBUG, $template);
|
$options['location_ns'] = $location->location_ns;
|
||||||
if (preg_match("/$template/", $recipient, $matches)) {
|
$options['location_id'] = $location->location_id;
|
||||||
$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");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$saved = Notice::saveNew($profile->id,
|
$saved = Notice::saveNew($oprofile->profile_id,
|
||||||
$content,
|
$content,
|
||||||
'ostatus',
|
'ostatus',
|
||||||
$params);
|
$options);
|
||||||
|
if ($saved) {
|
||||||
|
Ostatus_source::saveNew($saved, $this, $method);
|
||||||
|
}
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
common_log(LOG_ERR, "Failed saving notice entry for $sourceUri: " . $e->getMessage());
|
common_log(LOG_ERR, "OStatus save of remote message $sourceUri failed: " . $e->getMessage());
|
||||||
return;
|
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 {
|
* Clean up HTML
|
||||||
Ostatus_source::saveNew($saved, $this, 'push');
|
*/
|
||||||
} catch (Exception $e) {
|
protected function purify($html)
|
||||||
common_log(LOG_ERR, "Failed saving ostatus_source entry for $saved->notice_id: " . $e->getMessage());
|
{
|
||||||
|
// @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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -27,8 +27,6 @@
|
||||||
* @link http://status.net/
|
* @link http://status.net/
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once 'magicsig.php';
|
|
||||||
|
|
||||||
class MagicEnvelope
|
class MagicEnvelope
|
||||||
{
|
{
|
||||||
const ENCODING = 'base64url';
|
const ENCODING = 'base64url';
|
||||||
|
@ -64,7 +62,7 @@ class MagicEnvelope
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
$signature_alg = new MagicsigRsaSha256($this->getKeyPair($signer_uri));
|
$signature_alg = Magicsig::fromString($this->getKeyPair($signer_uri));
|
||||||
$armored_text = base64_encode($text);
|
$armored_text = base64_encode($text);
|
||||||
|
|
||||||
return array(
|
return array(
|
||||||
|
@ -139,7 +137,7 @@ class MagicEnvelope
|
||||||
$text = base64_decode($env['data']);
|
$text = base64_decode($env['data']);
|
||||||
$signer_uri = $this->getAuthor($text);
|
$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']);
|
return $verifier->verify($env['data'], $env['sig']);
|
||||||
}
|
}
|
||||||
|
|
|
@ -185,54 +185,6 @@ class SalmonAction extends Action
|
||||||
function saveNotice()
|
function saveNotice()
|
||||||
{
|
{
|
||||||
$oprofile = $this->ensureProfile();
|
$oprofile = $this->ensureProfile();
|
||||||
|
return $oprofile->processPost($this->act, 'salmon');
|
||||||
// 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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,10 @@ class Webfinger
|
||||||
|
|
||||||
$content = $this->fetchURL($url);
|
$content = $this->fetchURL($url);
|
||||||
|
|
||||||
|
if (!$content) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return XRD::parse($content);
|
return XRD::parse($content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -184,6 +184,7 @@ button.close,
|
||||||
.form_user_unsubscribe input.submit,
|
.form_user_unsubscribe input.submit,
|
||||||
.form_group_join input.submit,
|
.form_group_join input.submit,
|
||||||
.form_user_subscribe input.submit,
|
.form_user_subscribe input.submit,
|
||||||
|
.form_remote_authorize input.submit,
|
||||||
.entity_subscribe a,
|
.entity_subscribe a,
|
||||||
.entity_moderation p,
|
.entity_moderation p,
|
||||||
.entity_sandbox input.submit,
|
.entity_sandbox input.submit,
|
||||||
|
@ -291,6 +292,7 @@ background-position:0 1px;
|
||||||
.form_group_leave input.submit,
|
.form_group_leave input.submit,
|
||||||
.form_user_subscribe input.submit,
|
.form_user_subscribe input.submit,
|
||||||
.form_user_unsubscribe input.submit,
|
.form_user_unsubscribe input.submit,
|
||||||
|
.form_remote_authorize input.submit,
|
||||||
.entity_subscribe a {
|
.entity_subscribe a {
|
||||||
background-color:#AAAAAA;
|
background-color:#AAAAAA;
|
||||||
color:#FFFFFF;
|
color:#FFFFFF;
|
||||||
|
@ -301,6 +303,7 @@ background-position:5px -1246px;
|
||||||
}
|
}
|
||||||
.form_group_join input.submit,
|
.form_group_join input.submit,
|
||||||
.form_user_subscribe input.submit,
|
.form_user_subscribe input.submit,
|
||||||
|
.form_remote_authorize input.submit,
|
||||||
.entity_subscribe a {
|
.entity_subscribe a {
|
||||||
background-position:5px -1181px;
|
background-position:5px -1181px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -184,6 +184,7 @@ button.close,
|
||||||
.form_user_unsubscribe input.submit,
|
.form_user_unsubscribe input.submit,
|
||||||
.form_group_join input.submit,
|
.form_group_join input.submit,
|
||||||
.form_user_subscribe input.submit,
|
.form_user_subscribe input.submit,
|
||||||
|
.form_remote_authorize input.submit,
|
||||||
.entity_subscribe a,
|
.entity_subscribe a,
|
||||||
.entity_moderation p,
|
.entity_moderation p,
|
||||||
.entity_sandbox input.submit,
|
.entity_sandbox input.submit,
|
||||||
|
@ -290,6 +291,7 @@ background-position:0 1px;
|
||||||
.form_group_leave input.submit,
|
.form_group_leave input.submit,
|
||||||
.form_user_subscribe input.submit,
|
.form_user_subscribe input.submit,
|
||||||
.form_user_unsubscribe input.submit,
|
.form_user_unsubscribe input.submit,
|
||||||
|
.form_remote_authorize input.submit,
|
||||||
.entity_subscribe a {
|
.entity_subscribe a {
|
||||||
background-color:#AAAAAA;
|
background-color:#AAAAAA;
|
||||||
color:#FFFFFF;
|
color:#FFFFFF;
|
||||||
|
@ -300,6 +302,7 @@ background-position:5px -1246px;
|
||||||
}
|
}
|
||||||
.form_group_join input.submit,
|
.form_group_join input.submit,
|
||||||
.form_user_subscribe input.submit,
|
.form_user_subscribe input.submit,
|
||||||
|
.form_remote_authorize input.submit,
|
||||||
.entity_subscribe a {
|
.entity_subscribe a {
|
||||||
background-position:5px -1181px;
|
background-position:5px -1181px;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user