forked from GNUsocial/gnu-social
Merge branch 'testing' of gitorious.org:statusnet/mainline into 0.9.x
This commit is contained in:
@@ -24,6 +24,8 @@
|
||||
|
||||
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
|
||||
|
||||
set_include_path(get_include_path() . PATH_SEPARATOR . dirname(__FILE__) . '/extlib/');
|
||||
|
||||
class FeedSubException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -58,8 +60,6 @@ class OStatusPlugin extends Plugin
|
||||
$m->connect('main/push/callback/:feed',
|
||||
array('action' => 'pushcallback'),
|
||||
array('feed' => '[0-9]+'));
|
||||
$m->connect('settings/feedsub',
|
||||
array('action' => 'feedsubsettings'));
|
||||
|
||||
// Salmon endpoint
|
||||
$m->connect('main/salmon/user/:id',
|
||||
@@ -78,9 +78,18 @@ class OStatusPlugin extends Plugin
|
||||
*/
|
||||
function onEndInitializeQueueManager(QueueManager $qm)
|
||||
{
|
||||
$qm->connect('hubverify', 'HubVerifyQueueHandler');
|
||||
$qm->connect('hubdistrib', 'HubDistribQueueHandler');
|
||||
// Prepare outgoing distributions after notice save.
|
||||
$qm->connect('ostatus', 'OStatusQueueHandler');
|
||||
|
||||
// Outgoing from our internal PuSH hub
|
||||
$qm->connect('hubconf', 'HubConfQueueHandler');
|
||||
$qm->connect('hubout', 'HubOutQueueHandler');
|
||||
|
||||
// Outgoing Salmon replies (when we don't need a return value)
|
||||
$qm->connect('salmon', 'SalmonQueueHandler');
|
||||
|
||||
// Incoming from a foreign PuSH hub
|
||||
$qm->connect('pushin', 'PushInQueueHandler');
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -89,7 +98,7 @@ class OStatusPlugin extends Plugin
|
||||
*/
|
||||
function onStartEnqueueNotice($notice, &$transports)
|
||||
{
|
||||
$transports[] = 'hubdistrib';
|
||||
$transports[] = 'ostatus';
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -132,25 +141,6 @@ class OStatusPlugin extends Plugin
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the feed settings page to the Connect Settings menu
|
||||
*
|
||||
* @param Action &$action The calling page
|
||||
*
|
||||
* @return boolean hook return
|
||||
*/
|
||||
function onEndConnectSettingsNav(&$action)
|
||||
{
|
||||
$action_name = $action->trimmed('action');
|
||||
|
||||
$action->menuItem(common_local_url('feedsubsettings'),
|
||||
_m('Feeds'),
|
||||
_m('Feed subscription options'),
|
||||
$action_name === 'feedsubsettings');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically load the actions and libraries used by the plugin
|
||||
*
|
||||
@@ -211,94 +201,47 @@ class OStatusPlugin extends Plugin
|
||||
* @fixme push webfinger lookup & sending to a background queue
|
||||
* @fixme also detect short-form name for remote subscribees where not ambiguous
|
||||
*/
|
||||
|
||||
function onEndNoticeSave($notice)
|
||||
{
|
||||
$count = preg_match_all('/(\w+\.)*\w+@(\w+\.)*\w+(\w+\-\w+)*\.\w+/', $notice->content, $matches);
|
||||
if ($count) {
|
||||
foreach ($matches[0] as $webfinger) {
|
||||
|
||||
// FIXME: look up locally first
|
||||
|
||||
// Check to see if we've got an actual webfinger
|
||||
$w = new Webfinger;
|
||||
|
||||
$endpoint_uri = '';
|
||||
|
||||
$result = $w->lookup($webfinger);
|
||||
if (empty($result)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($result->links as $link) {
|
||||
if ($link['rel'] == 'salmon') {
|
||||
$endpoint_uri = $link['href'];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($endpoint_uri)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// FIXME: this needs to go out in a queue handler
|
||||
|
||||
$xml = '<?xml version="1.0" encoding="UTF-8" ?>';
|
||||
$xml .= $notice->asAtomEntry();
|
||||
|
||||
$salmon = new Salmon();
|
||||
$salmon->post($endpoint_uri, $xml);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
|
||||
function onStartFindMentions($sender, $text, &$mentions)
|
||||
{
|
||||
$user = User::staticGet('id', $profile->id);
|
||||
preg_match_all('/(?:^|\s+)@((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)/',
|
||||
$text,
|
||||
$wmatches,
|
||||
PREG_OFFSET_CAPTURE);
|
||||
|
||||
if (empty($user)) {
|
||||
return true;
|
||||
foreach ($wmatches[1] as $wmatch) {
|
||||
|
||||
$webfinger = $wmatch[0];
|
||||
|
||||
$this->log(LOG_INFO, "Checking Webfinger for address '$webfinger'");
|
||||
|
||||
$oprofile = Ostatus_profile::ensureWebfinger($webfinger);
|
||||
|
||||
if (empty($oprofile)) {
|
||||
|
||||
$this->log(LOG_INFO, "No Ostatus_profile found for address '$webfinger'");
|
||||
|
||||
} else {
|
||||
|
||||
$this->log(LOG_INFO, "Ostatus_profile found for address '$webfinger'");
|
||||
|
||||
$profile = $oprofile->localProfile();
|
||||
|
||||
$mentions[] = array('mentioned' => array($profile),
|
||||
'text' => $wmatch[0],
|
||||
'position' => $wmatch[1],
|
||||
'url' => $profile->profileurl);
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -308,8 +251,10 @@ class OStatusPlugin extends Plugin
|
||||
function onCheckSchema() {
|
||||
$schema = Schema::get();
|
||||
$schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
|
||||
$schema->ensureTable('ostatus_source', Ostatus_source::schemaDef());
|
||||
$schema->ensureTable('feedsub', FeedSub::schemaDef());
|
||||
$schema->ensureTable('hubsub', HubSub::schemaDef());
|
||||
$schema->ensureTable('magicsig', Magicsig::schemaDef());
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -336,13 +281,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -357,12 +308,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);
|
||||
@@ -400,6 +395,145 @@ 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,
|
||||
* deny the join.
|
||||
*
|
||||
* @param User_group $group
|
||||
* @param User $user
|
||||
*
|
||||
* @return mixed hook return value
|
||||
*/
|
||||
|
||||
function onStartJoinGroup($group, $user)
|
||||
{
|
||||
$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();
|
||||
$act->id = TagURI::mint('join:%d:%d:%s',
|
||||
$member->id,
|
||||
$group->id,
|
||||
common_date_iso8601(time()));
|
||||
|
||||
$act->actor = ActivityObject::fromProfile($member);
|
||||
$act->verb = ActivityVerb::JOIN;
|
||||
$act->object = $oprofile->asActivityObject();
|
||||
|
||||
$act->time = time();
|
||||
$act->title = _m("Join");
|
||||
$act->content = sprintf(_m("%s has joined group %s."),
|
||||
$member->getBestName(),
|
||||
$oprofile->getBestName());
|
||||
|
||||
if ($oprofile->notifyActivity($act)) {
|
||||
return true;
|
||||
} else {
|
||||
$oprofile->garbageCollect();
|
||||
throw new Exception(_m("Failed joining remote group."));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When one of our local users leaves a remote group, notify the remote
|
||||
* server.
|
||||
*
|
||||
* @fixme Might be good to schedule a resend of the leave notification
|
||||
* if it failed due to a transitory error. We've canceled the local
|
||||
* membership already anyway, but if the remote server comes back up
|
||||
* it'll be left with a stray membership record.
|
||||
*
|
||||
* @param User_group $group
|
||||
* @param User $user
|
||||
*
|
||||
* @return mixed hook return value
|
||||
*/
|
||||
|
||||
function onEndLeaveGroup($group, $user)
|
||||
{
|
||||
$oprofile = Ostatus_profile::staticGet('group_id', $group->id);
|
||||
if ($oprofile) {
|
||||
// Drop the PuSH subscription if there are no other subscribers.
|
||||
$oprofile->garbageCollect();
|
||||
|
||||
|
||||
$member = Profile::staticGet($user->id);
|
||||
|
||||
$act = new Activity();
|
||||
$act->id = TagURI::mint('leave:%d:%d:%s',
|
||||
$member->id,
|
||||
$group->id,
|
||||
common_date_iso8601(time()));
|
||||
|
||||
$act->actor = ActivityObject::fromProfile($member);
|
||||
$act->verb = ActivityVerb::LEAVE;
|
||||
$act->object = $oprofile->asActivityObject();
|
||||
|
||||
$act->time = time();
|
||||
$act->title = _m("Leave");
|
||||
$act->content = sprintf(_m("%s has left group %s."),
|
||||
$member->getBestName(),
|
||||
$oprofile->getBestName());
|
||||
|
||||
$oprofile->notifyActivity($act);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Notify remote users when their notices get favorited.
|
||||
*
|
||||
@@ -487,4 +621,94 @@ class OStatusPlugin extends Plugin
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function onStartGetProfileUri($profile, &$uri)
|
||||
{
|
||||
$oprofile = Ostatus_profile::staticGet('profile_id', $profile->id);
|
||||
if (!empty($oprofile)) {
|
||||
$uri = $oprofile->uri;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function onStartUserGroupHomeUrl($group, &$url)
|
||||
{
|
||||
return $this->onStartUserGroupPermalink($group, &$url);
|
||||
}
|
||||
|
||||
function onStartUserGroupPermalink($group, &$url)
|
||||
{
|
||||
$oprofile = Ostatus_profile::staticGet('group_id', $group->id);
|
||||
if ($oprofile) {
|
||||
// @fixme this should probably be in the user_group table
|
||||
// @fixme this uri not guaranteed to be a profile page
|
||||
$url = $oprofile->uri;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function onStartShowSubscriptionsContent($action)
|
||||
{
|
||||
$user = common_current_user();
|
||||
if ($user && ($user->id == $action->profile->id)) {
|
||||
$action->elementStart('div', 'entity_actions');
|
||||
$action->elementStart('p', array('id' => 'entity_remote_subscribe',
|
||||
'class' => 'entity_subscribe'));
|
||||
$action->element('a', array('href' => common_local_url('ostatussub'),
|
||||
'class' => 'entity_remote_subscribe')
|
||||
, _m('Subscribe to remote user'));
|
||||
$action->elementEnd('p');
|
||||
$action->elementEnd('div');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ping remote profiles with updates to this profile.
|
||||
* Salmon pings are queued for background processing.
|
||||
*/
|
||||
function onEndBroadcastProfile(Profile $profile)
|
||||
{
|
||||
$user = User::staticGet('id', $profile->id);
|
||||
|
||||
// Find foreign accounts I'm subscribed to that support Salmon pings.
|
||||
//
|
||||
// @fixme we could run updates through the PuSH feed too,
|
||||
// in which case we can skip Salmon pings to folks who
|
||||
// are also subscribed to me.
|
||||
$sql = "SELECT * FROM ostatus_profile " .
|
||||
"WHERE profile_id IN " .
|
||||
"(SELECT subscribed FROM subscription WHERE subscriber=%d) " .
|
||||
"OR group_id IN " .
|
||||
"(SELECT group_id FROM group_member WHERE profile_id=%d)";
|
||||
$oprofile = new Ostatus_profile();
|
||||
$oprofile->query(sprintf($sql, $profile->id, $profile->id));
|
||||
|
||||
if ($oprofile->N == 0) {
|
||||
common_log(LOG_DEBUG, "No OStatus remote subscribees for $profile->nickname");
|
||||
return true;
|
||||
}
|
||||
|
||||
$act = new Activity();
|
||||
|
||||
$act->verb = ActivityVerb::UPDATE_PROFILE;
|
||||
$act->id = TagURI::mint('update-profile:%d:%s',
|
||||
$profile->id,
|
||||
common_date_iso8601(time()));
|
||||
$act->time = time();
|
||||
$act->title = _m("Profile update");
|
||||
$act->content = sprintf(_m("%s has updated their profile page."),
|
||||
$profile->getBestName());
|
||||
|
||||
$act->actor = ActivityObject::fromProfile($profile);
|
||||
$act->object = $act->actor;
|
||||
|
||||
while ($oprofile->fetch()) {
|
||||
$oprofile->notifyDeferred($act);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -2,23 +2,33 @@ Plugin to support importing updates from external RSS and Atom feeds into your t
|
||||
|
||||
Uses PubSubHubbub for push feed updates; currently non-PuSH feeds cannot be subscribed.
|
||||
|
||||
Configuration options available:
|
||||
|
||||
$config['ostatus']['hub']
|
||||
(default internal hub)
|
||||
Set to URL of an external PuSH hub to use it instead of our internal hub.
|
||||
|
||||
$config['ostatus']['hub_retries']
|
||||
(default 0)
|
||||
Number of times to retry a PuSH send to consumers if using internal hub
|
||||
|
||||
|
||||
For testing, shouldn't be used in production:
|
||||
|
||||
$config['ostatus']['skip_signatures']
|
||||
(default use signatures)
|
||||
Disable generation and validation of Salmon magicenv signatures
|
||||
|
||||
$config['feedsub']['nohub']
|
||||
(default require hub)
|
||||
Allow low-level feed subscription setup for feeds without hubs.
|
||||
Not actually usable at this stage, OStatus will check for hubs too
|
||||
and we have no polling backend.
|
||||
|
||||
|
||||
Todo:
|
||||
* set feed icon avatar for actual profiles as well as for preview
|
||||
* use channel image and/or favicon for avatar?
|
||||
* garbage-collect subscriptions that are no longer being used
|
||||
* administrative way to kill feeds?
|
||||
* functional l10n
|
||||
* clean up subscription form look and workflow
|
||||
* use ajax for test/preview in subscription form
|
||||
* rssCloud support? (Does anything use it that doesn't support PuSH as well?)
|
||||
* possibly a polling daemon to support non-PuSH feeds?
|
||||
* likely problems with multiple feeds from the same site, such as category feeds on a blog
|
||||
(currently each feed would publish a separate notice on a separate profile, but pointing to the same post URI.)
|
||||
(could use the local URI I guess, but that's so icky!)
|
||||
* problems with Atom feeds that list <link rel="alternate" href="..."/> but don't have the type
|
||||
(such as http://atomgen.appspot.com/feed/5 demo feed); currently it's not recognized and we end up with the feed's master URI
|
||||
* make it easier to see what you're subscribed to and unsub from things
|
||||
* saner treatment of fullname/nickname?
|
||||
* fully functional l10n
|
||||
* redo non-OStatus feed support
|
||||
** rssCloud support?
|
||||
** possibly a polling daemon to support non-PuSH feeds?
|
||||
* make use of tags/categories from feeds
|
||||
* update feed profile data when it changes
|
||||
* XML_Feed_Parser has major problems with category and link tags; consider replacing?
|
||||
|
@@ -1,230 +0,0 @@
|
||||
<?php
|
||||
/*
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2009, StatusNet, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package FeedSubPlugin
|
||||
* @maintainer Brion Vibber <brion@status.net>
|
||||
*/
|
||||
|
||||
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
|
||||
|
||||
class FeedSubSettingsAction extends ConnectSettingsAction
|
||||
{
|
||||
protected $profile_uri;
|
||||
protected $preview;
|
||||
protected $munger;
|
||||
|
||||
/**
|
||||
* Title of the page
|
||||
*
|
||||
* @return string Title of the page
|
||||
*/
|
||||
|
||||
function title()
|
||||
{
|
||||
return _m('Feed subscriptions');
|
||||
}
|
||||
|
||||
/**
|
||||
* Instructions for use
|
||||
*
|
||||
* @return instructions for use
|
||||
*/
|
||||
|
||||
function getInstructions()
|
||||
{
|
||||
return _m('You can subscribe to feeds from other sites; ' .
|
||||
'updates will appear in your personal timeline.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Content area of the page
|
||||
*
|
||||
* Shows a form for associating a Twitter account with this
|
||||
* StatusNet account. Also lets the user set preferences.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
function showContent()
|
||||
{
|
||||
$user = common_current_user();
|
||||
|
||||
$profile = $user->getProfile();
|
||||
|
||||
$this->elementStart('form', array('method' => 'post',
|
||||
'id' => 'form_settings_feedsub',
|
||||
'class' => 'form_settings',
|
||||
'action' =>
|
||||
common_local_url('feedsubsettings')));
|
||||
|
||||
$this->hidden('token', common_session_token());
|
||||
|
||||
$this->elementStart('fieldset', array('id' => 'settings_feeds'));
|
||||
|
||||
$this->elementStart('ul', 'form_data');
|
||||
$this->elementStart('li', array('id' => 'settings_twitter_login_button'));
|
||||
$this->input('profile_uri',
|
||||
_m('Feed URL'),
|
||||
$this->profile_uri,
|
||||
_m('Enter the profile URL of a PubSubHubbub-enabled feed'));
|
||||
$this->elementEnd('li');
|
||||
$this->elementEnd('ul');
|
||||
|
||||
if ($this->preview) {
|
||||
$this->submit('subscribe', _m('Subscribe'));
|
||||
} else {
|
||||
$this->submit('validate', _m('Continue'));
|
||||
}
|
||||
|
||||
$this->elementEnd('fieldset');
|
||||
|
||||
$this->elementEnd('form');
|
||||
|
||||
if ($this->preview) {
|
||||
$this->previewFeed();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle posts to this form
|
||||
*
|
||||
* Based on the button that was pressed, muxes out to other functions
|
||||
* to do the actual task requested.
|
||||
*
|
||||
* All sub-functions reload the form with a message -- success or failure.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
function handlePost()
|
||||
{
|
||||
// CSRF protection
|
||||
$token = $this->trimmed('token');
|
||||
if (!$token || $token != common_session_token()) {
|
||||
$this->showForm(_('There was a problem with your session token. '.
|
||||
'Try again, please.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->arg('validate')) {
|
||||
$this->validateAndPreview();
|
||||
} else if ($this->arg('subscribe')) {
|
||||
$this->saveFeed();
|
||||
} else {
|
||||
$this->showForm(_('Unexpected form submission.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up and add a feed
|
||||
*
|
||||
* @return boolean true if feed successfully read
|
||||
* Sends you back to input form if not.
|
||||
*/
|
||||
function validateFeed()
|
||||
{
|
||||
$profile_uri = trim($this->arg('profile_uri'));
|
||||
|
||||
if ($profile_uri == '') {
|
||||
$this->showForm(_m('Empty remote profile URL!'));
|
||||
return;
|
||||
}
|
||||
$this->profile_uri = $profile_uri;
|
||||
|
||||
// @fixme validate, normalize bla bla
|
||||
try {
|
||||
$oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
|
||||
$this->oprofile = $oprofile;
|
||||
return true;
|
||||
} catch (FeedSubBadURLException $e) {
|
||||
$err = _m('Invalid URL or could not reach server.');
|
||||
} catch (FeedSubBadResponseException $e) {
|
||||
$err = _m('Cannot read feed; server returned error.');
|
||||
} catch (FeedSubEmptyException $e) {
|
||||
$err = _m('Cannot read feed; server returned an empty page.');
|
||||
} catch (FeedSubBadHTMLException $e) {
|
||||
$err = _m('Bad HTML, could not find feed link.');
|
||||
} catch (FeedSubNoFeedException $e) {
|
||||
$err = _m('Could not find a feed linked from this URL.');
|
||||
} catch (FeedSubUnrecognizedTypeException $e) {
|
||||
$err = _m('Not a recognized feed type.');
|
||||
} catch (FeedSubException $e) {
|
||||
// Any new ones we forgot about
|
||||
$err = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage());
|
||||
}
|
||||
|
||||
$this->showForm($err);
|
||||
return false;
|
||||
}
|
||||
|
||||
function saveFeed()
|
||||
{
|
||||
if ($this->validateFeed()) {
|
||||
$this->preview = true;
|
||||
|
||||
// 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)) {
|
||||
$this->showForm(_m('Already a member!'));
|
||||
} elseif (Group_member::join($this->profile->group_id, $user->id)) {
|
||||
$this->showForm(_m('Joined remote group!'));
|
||||
} else {
|
||||
$this->showForm(_m('Remote group join failed!'));
|
||||
}
|
||||
} else {
|
||||
$local = $this->oprofile->localProfile();
|
||||
if ($user->isSubscribed($local)) {
|
||||
$this->showForm(_m('Already subscribed!'));
|
||||
} elseif ($this->oprofile->subscribeLocalToRemote($user)) {
|
||||
$this->showForm(_m('Remote user subscribed!'));
|
||||
} else {
|
||||
$this->showForm(_m('Remote subscription failed!'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateAndPreview()
|
||||
{
|
||||
if ($this->validateFeed()) {
|
||||
$this->preview = true;
|
||||
$this->showForm(_m('Previewing feed:'));
|
||||
}
|
||||
}
|
||||
|
||||
function previewFeed()
|
||||
{
|
||||
$this->text('Profile preview should go here');
|
||||
}
|
||||
|
||||
function showScripts()
|
||||
{
|
||||
parent::showScripts();
|
||||
$this->autofocus('feedurl');
|
||||
}
|
||||
}
|
@@ -46,6 +46,11 @@ class GroupsalmonAction extends SalmonAction
|
||||
$this->clientError(_('No such group.'));
|
||||
}
|
||||
|
||||
$oprofile = Ostatus_profile::staticGet('group_id', $id);
|
||||
if ($oprofile) {
|
||||
$this->clientError(_m("Can't accept remote posts for a remote group."));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -74,13 +79,13 @@ class GroupsalmonAction extends SalmonAction
|
||||
throw new ClientException("Not to the attention of anyone.");
|
||||
} else {
|
||||
$uri = common_local_url('groupbyid', array('id' => $this->group->id));
|
||||
if (!in_array($context->attention, $uri)) {
|
||||
if (!in_array($uri, $context->attention)) {
|
||||
throw new ClientException("Not to the attention of this group.");
|
||||
}
|
||||
}
|
||||
|
||||
$profile = $this->ensureProfile();
|
||||
// @fixme save the post
|
||||
$this->saveNotice();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -88,21 +93,96 @@ class GroupsalmonAction extends SalmonAction
|
||||
* Save a subscription relationship for them.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Postel's law: consider a "follow" notification as a "join".
|
||||
*/
|
||||
function handleFollow()
|
||||
{
|
||||
$this->handleJoin(); // ???
|
||||
$this->handleJoin();
|
||||
}
|
||||
|
||||
/**
|
||||
* Postel's law: consider an "unfollow" notification as a "leave".
|
||||
*/
|
||||
function handleUnfollow()
|
||||
{
|
||||
$this->handleLeave();
|
||||
}
|
||||
|
||||
/**
|
||||
* A remote user joined our group.
|
||||
* @fixme move permission checks and event call into common code,
|
||||
* currently we're doing the main logic in joingroup action
|
||||
* and so have to repeat it here.
|
||||
*/
|
||||
|
||||
function handleJoin()
|
||||
{
|
||||
$oprofile = $this->ensureProfile();
|
||||
if (!$oprofile) {
|
||||
$this->clientError(_m("Can't read profile to set up group membership."));
|
||||
}
|
||||
if ($oprofile->isGroup()) {
|
||||
$this->clientError(_m("Groups can't join groups."));
|
||||
}
|
||||
|
||||
common_log(LOG_INFO, "Remote profile {$oprofile->uri} joining local group {$this->group->nickname}");
|
||||
$profile = $oprofile->localProfile();
|
||||
|
||||
if ($profile->isMember($this->group)) {
|
||||
// Already a member; we'll take it silently to aid in resolving
|
||||
// inconsistencies on the other side.
|
||||
return true;
|
||||
}
|
||||
|
||||
if (Group_block::isBlocked($this->group, $profile)) {
|
||||
$this->clientError(_('You have been blocked from that group by the admin.'), 403);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// @fixme that event currently passes a user from main UI
|
||||
// Event should probably move into Group_member::join
|
||||
// and take a Profile object.
|
||||
//
|
||||
//if (Event::handle('StartJoinGroup', array($this->group, $profile))) {
|
||||
Group_member::join($this->group->id, $profile->id);
|
||||
//Event::handle('EndJoinGroup', array($this->group, $profile));
|
||||
//}
|
||||
} catch (Exception $e) {
|
||||
$this->serverError(sprintf(_m('Could not join remote user %1$s to group %2$s.'),
|
||||
$oprofile->uri, $this->group->nickname));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A remote user left our group.
|
||||
*/
|
||||
|
||||
function handleLeave()
|
||||
{
|
||||
$oprofile = $this->ensureProfile();
|
||||
if (!$oprofile) {
|
||||
$this->clientError(_m("Can't read profile to cancel group membership."));
|
||||
}
|
||||
if ($oprofile->isGroup()) {
|
||||
$this->clientError(_m("Groups can't join groups."));
|
||||
}
|
||||
|
||||
common_log(LOG_INFO, "Remote profile {$oprofile->uri} leaving local group {$this->group->nickname}");
|
||||
$profile = $oprofile->localProfile();
|
||||
|
||||
try {
|
||||
// @fixme event needs to be refactored as above
|
||||
//if (Event::handle('StartLeaveGroup', array($this->group, $profile))) {
|
||||
Group_member::leave($this->group->id, $profile->id);
|
||||
//Event::handle('EndLeaveGroup', array($this->group, $profile));
|
||||
//}
|
||||
} catch (Exception $e) {
|
||||
$this->serverError(sprintf(_m('Could not remove remote user %1$s from group %2$s.'),
|
||||
$oprofile->uri, $this->group->nickname));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -29,7 +29,7 @@ class OStatusInitAction extends Action
|
||||
{
|
||||
|
||||
var $nickname;
|
||||
var $acct;
|
||||
var $profile;
|
||||
var $err;
|
||||
|
||||
function prepare($args)
|
||||
@@ -41,8 +41,11 @@ class OStatusInitAction extends Action
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->nickname = $this->trimmed('nickname');
|
||||
$this->acct = $this->trimmed('acct');
|
||||
// Local user the remote wants to subscribe to
|
||||
$this->nickname = $this->trimmed('nickname');
|
||||
|
||||
// Webfinger or profile URL of the remote user
|
||||
$this->profile = $this->trimmed('profile');
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -100,7 +103,7 @@ class OStatusInitAction extends Action
|
||||
_m('Nickname of the user you want to follow'));
|
||||
$this->elementEnd('li');
|
||||
$this->elementStart('li', array('id' => 'ostatus_profile'));
|
||||
$this->input('acct', _m('Profile Account'), $this->acct,
|
||||
$this->input('profile', _m('Profile Account'), $this->profile,
|
||||
_m('Your account id (i.e. user@identi.ca)'));
|
||||
$this->elementEnd('li');
|
||||
$this->elementEnd('ul');
|
||||
@@ -112,15 +115,17 @@ class OStatusInitAction extends Action
|
||||
function ostatusConnect()
|
||||
{
|
||||
$opts = array('allowed_schemes' => array('http', 'https', 'acct'));
|
||||
if (Validate::uri($this->acct, $opts)) {
|
||||
$bits = parse_url($this->acct);
|
||||
if (Validate::uri($this->profile, $opts)) {
|
||||
$bits = parse_url($this->profile);
|
||||
if ($bits['scheme'] == 'acct') {
|
||||
$this->connectWebfinger($bits['path']);
|
||||
} else {
|
||||
$this->connectProfile($this->acct);
|
||||
$this->connectProfile($this->profile);
|
||||
}
|
||||
} elseif (strpos('@', $this->acct) !== false) {
|
||||
$this->connectWebfinger($this->acct);
|
||||
} elseif (strpos($this->profile, '@') !== false) {
|
||||
$this->connectWebfinger($this->profile);
|
||||
} else {
|
||||
$this->clientError(_m("Must provide a remote profile."));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,13 +144,13 @@ class OStatusInitAction extends Action
|
||||
$user = User::staticGet('nickname', $this->nickname);
|
||||
$target_profile = common_local_url('userbyid', array('id' => $user->id));
|
||||
|
||||
$url = $w->applyTemplate($link['template'], $feed_url);
|
||||
|
||||
$url = $w->applyTemplate($link['template'], $target_profile);
|
||||
common_log(LOG_INFO, "Sending remote subscriber $acct to $url");
|
||||
common_redirect($url, 303);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
$this->clientError(_m("Couldn't confirm remote profile address."));
|
||||
}
|
||||
|
||||
function connectProfile($subscriber_profile)
|
||||
@@ -157,6 +162,7 @@ class OStatusInitAction extends Action
|
||||
$suburl = preg_replace('!^(.*)/(.*?)$!', '$1/main/ostatussub', $subscriber_profile);
|
||||
$suburl .= '?profile=' . urlencode($target_profile);
|
||||
|
||||
common_log(LOG_INFO, "Sending remote subscriber $subscriber_profile to $suburl");
|
||||
common_redirect($suburl, 303);
|
||||
}
|
||||
|
||||
|
@@ -24,11 +24,415 @@
|
||||
|
||||
if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); }
|
||||
|
||||
/**
|
||||
* Key UI methods:
|
||||
*
|
||||
* showInputForm() - form asking for a remote profile account or URL
|
||||
* We end up back here on errors
|
||||
*
|
||||
* showPreviewForm() - surrounding form for preview-and-confirm
|
||||
* previewUser() - display profile for a remote user
|
||||
* previewGroup() - display profile for a remote group
|
||||
*
|
||||
* successUser() - redirects to subscriptions page on subscribe
|
||||
* successGroup() - redirects to groups page on join
|
||||
*/
|
||||
class OStatusSubAction extends Action
|
||||
{
|
||||
protected $profile_uri;
|
||||
protected $preview;
|
||||
protected $munger;
|
||||
protected $profile_uri; // provided acct: or URI of remote entity
|
||||
protected $oprofile; // Ostatus_profile of remote entity, if valid
|
||||
|
||||
/**
|
||||
* Show the initial form, when we haven't yet been given a valid
|
||||
* remote profile.
|
||||
*/
|
||||
function showInputForm()
|
||||
{
|
||||
$user = common_current_user();
|
||||
|
||||
$profile = $user->getProfile();
|
||||
|
||||
$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->elementStart('fieldset', array('id' => 'settings_feeds'));
|
||||
|
||||
$this->elementStart('ul', 'form_data');
|
||||
$this->elementStart('li');
|
||||
$this->input('profile',
|
||||
_m('Address or profile URL'),
|
||||
$this->profile_uri,
|
||||
_m('Enter the profile URL of a PubSubHubbub-enabled feed'));
|
||||
$this->elementEnd('li');
|
||||
$this->elementEnd('ul');
|
||||
|
||||
$this->submit('validate', _m('Continue'));
|
||||
|
||||
$this->elementEnd('fieldset');
|
||||
|
||||
$this->elementEnd('form');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the preview-and-confirm form. We've got a valid remote
|
||||
* profile and are ready to poke it!
|
||||
*
|
||||
* This controls the wrapper form; actual profile display will
|
||||
* be in previewUser() or previewGroup() depending on the type.
|
||||
*/
|
||||
function showPreviewForm()
|
||||
{
|
||||
if ($this->oprofile->isGroup()) {
|
||||
$ok = $this->previewGroup();
|
||||
} else {
|
||||
$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();
|
||||
|
||||
$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();
|
||||
|
||||
$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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect on successful remote user subscription
|
||||
*/
|
||||
function successUser()
|
||||
{
|
||||
$cur = common_current_user();
|
||||
$url = common_local_url('subscriptions', array('nickname' => $cur->nickname));
|
||||
common_redirect($url, 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Redirect on successful remote group join
|
||||
*/
|
||||
function successGroup()
|
||||
{
|
||||
$cur = common_current_user();
|
||||
$url = common_local_url('usergroups', array('nickname' => $cur->nickname));
|
||||
common_redirect($url, 303);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull data for a remote profile and check if it's valid.
|
||||
* Fills out error UI string in $this->error
|
||||
* Fills out $this->oprofile on success.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
function validateFeed()
|
||||
{
|
||||
$profile_uri = trim($this->arg('profile'));
|
||||
|
||||
if ($profile_uri == '') {
|
||||
$this->showForm(_m('Empty remote profile URL!'));
|
||||
return;
|
||||
}
|
||||
$this->profile_uri = $profile_uri;
|
||||
|
||||
try {
|
||||
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.');
|
||||
} catch (FeedSubBadResponseException $e) {
|
||||
$this->error = _m('Cannot read feed; server returned error.');
|
||||
} catch (FeedSubEmptyException $e) {
|
||||
$this->error = _m('Cannot read feed; server returned an empty page.');
|
||||
} catch (FeedSubBadHTMLException $e) {
|
||||
$this->error = _m('Bad HTML, could not find feed link.');
|
||||
} catch (FeedSubNoFeedException $e) {
|
||||
$this->error = _m('Could not find a feed linked from this URL.');
|
||||
} catch (FeedSubUnrecognizedTypeException $e) {
|
||||
$this->error = _m('Not a recognized feed type.');
|
||||
} catch (FeedSubException $e) {
|
||||
// Any new ones we forgot about
|
||||
$this->error = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage());
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to finalize subscription.
|
||||
* validateFeed must have been run first.
|
||||
*
|
||||
* Calls showForm on failure or successUser/successGroup on success.
|
||||
*/
|
||||
function saveFeed()
|
||||
{
|
||||
// And subscribe the current user to the local profile
|
||||
$user = common_current_user();
|
||||
|
||||
if ($this->oprofile->isGroup()) {
|
||||
$group = $this->oprofile->localGroup();
|
||||
if ($user->isMember($group)) {
|
||||
$this->showForm(_m('Already a member!'));
|
||||
} elseif (Group_member::join($this->oprofile->group_id, $user->id)) {
|
||||
$this->successGroup();
|
||||
} else {
|
||||
$this->showForm(_m('Remote group join failed!'));
|
||||
}
|
||||
} else {
|
||||
$local = $this->oprofile->localProfile();
|
||||
if ($user->isSubscribed($local)) {
|
||||
$this->showForm(_m('Already subscribed!'));
|
||||
} elseif ($this->oprofile->subscribeLocalToRemote($user)) {
|
||||
$this->successUser();
|
||||
} else {
|
||||
$this->showForm(_m('Remote subscription failed!'));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function prepare($args)
|
||||
{
|
||||
parent::prepare($args);
|
||||
|
||||
if (!common_logged_in()) {
|
||||
// XXX: selfURL() didn't work. :<
|
||||
common_set_returnto($_SERVER['REQUEST_URI']);
|
||||
if (Event::handle('RedirectToLogin', array($this, null))) {
|
||||
common_redirect(common_local_url('login'), 303);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->profile_uri = $this->arg('profile');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the submission.
|
||||
*/
|
||||
function handle($args)
|
||||
{
|
||||
parent::handle($args);
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$this->handlePost();
|
||||
} else {
|
||||
if ($this->arg('profile')) {
|
||||
$this->validateFeed();
|
||||
}
|
||||
$this->showForm();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Handle posts to this form
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
function handlePost()
|
||||
{
|
||||
// CSRF protection
|
||||
$token = $this->trimmed('token');
|
||||
if (!$token || $token != common_session_token()) {
|
||||
$this->showForm(_('There was a problem with your session token. '.
|
||||
'Try again, please.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->validateFeed()) {
|
||||
if ($this->arg('submit')) {
|
||||
$this->saveFeed();
|
||||
return;
|
||||
}
|
||||
}
|
||||
$this->showForm();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the appropriate form based on our input state.
|
||||
*/
|
||||
function showForm($err=null)
|
||||
{
|
||||
if ($err) {
|
||||
$this->error = $err;
|
||||
}
|
||||
if ($this->boolean('ajax')) {
|
||||
header('Content-Type: text/xml;charset=utf-8');
|
||||
$this->xw->startDocument('1.0', 'UTF-8');
|
||||
$this->elementStart('html');
|
||||
$this->elementStart('head');
|
||||
$this->element('title', null, _m('Subscribe to user'));
|
||||
$this->elementEnd('head');
|
||||
$this->elementStart('body');
|
||||
$this->showContent();
|
||||
$this->elementEnd('body');
|
||||
$this->elementEnd('html');
|
||||
} else {
|
||||
$this->showPage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Title of the page
|
||||
@@ -52,10 +456,11 @@ class OStatusSubAction extends Action
|
||||
return _m('You can subscribe to users from other supported sites. Paste their address or profile URI below:');
|
||||
}
|
||||
|
||||
function showForm($error=null)
|
||||
function showPageNotice()
|
||||
{
|
||||
$this->error = $error;
|
||||
$this->showPage();
|
||||
if (!empty($this->error)) {
|
||||
$this->element('p', 'error', $this->error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -69,199 +474,11 @@ class OStatusSubAction extends Action
|
||||
|
||||
function showContent()
|
||||
{
|
||||
// @fixme is this right place?
|
||||
if ($this->error) {
|
||||
$this->text($this->error);
|
||||
}
|
||||
|
||||
$user = common_current_user();
|
||||
|
||||
$profile = $user->getProfile();
|
||||
|
||||
$this->elementStart('form', array('method' => 'post',
|
||||
'id' => 'ostatus_sub',
|
||||
'class' => 'form_settings',
|
||||
'action' =>
|
||||
common_local_url('ostatussub')));
|
||||
|
||||
$this->hidden('token', common_session_token());
|
||||
|
||||
$this->elementStart('fieldset', array('id' => 'settings_feeds'));
|
||||
|
||||
$this->elementStart('ul', 'form_data');
|
||||
$this->elementStart('li');
|
||||
$this->input('profile',
|
||||
_m('Address or profile URL'),
|
||||
$this->profile_uri,
|
||||
_m('Enter the profile URL of a PubSubHubbub-enabled feed'));
|
||||
$this->elementEnd('li');
|
||||
$this->elementEnd('ul');
|
||||
|
||||
if ($this->preview) {
|
||||
$this->submit('subscribe', _m('Subscribe'));
|
||||
if ($this->oprofile) {
|
||||
$this->showPreviewForm();
|
||||
} else {
|
||||
$this->submit('validate', _m('Continue'));
|
||||
$this->showInputForm();
|
||||
}
|
||||
|
||||
$this->elementEnd('fieldset');
|
||||
|
||||
$this->elementEnd('form');
|
||||
|
||||
if ($this->preview) {
|
||||
$this->previewFeed();
|
||||
}
|
||||
}
|
||||
|
||||
function prepare($args)
|
||||
{
|
||||
parent::prepare($args);
|
||||
|
||||
if (!common_logged_in()) {
|
||||
// XXX: selfURL() didn't work. :<
|
||||
common_set_returnto($_SERVER['REQUEST_URI']);
|
||||
if (Event::handle('RedirectToLogin', array($this, null))) {
|
||||
common_redirect(common_local_url('login'), 303);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
$this->profile_uri = $this->arg('profile');
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function handle($args)
|
||||
{
|
||||
parent::handle($args);
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$this->handlePost();
|
||||
} else {
|
||||
if ($this->profile_uri) {
|
||||
$this->validateAndPreview();
|
||||
} else {
|
||||
$this->showPage();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle posts to this form
|
||||
*
|
||||
* Based on the button that was pressed, muxes out to other functions
|
||||
* to do the actual task requested.
|
||||
*
|
||||
* All sub-functions reload the form with a message -- success or failure.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
|
||||
function handlePost()
|
||||
{
|
||||
// CSRF protection
|
||||
$token = $this->trimmed('token');
|
||||
if (!$token || $token != common_session_token()) {
|
||||
$this->showForm(_('There was a problem with your session token. '.
|
||||
'Try again, please.'));
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->arg('validate')) {
|
||||
$this->validateAndPreview();
|
||||
} else if ($this->arg('subscribe')) {
|
||||
$this->saveFeed();
|
||||
} else {
|
||||
$this->showForm(_('Unexpected form submission.'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up and add a feed
|
||||
*
|
||||
* @return boolean true if feed successfully read
|
||||
* Sends you back to input form if not.
|
||||
*/
|
||||
function validateFeed()
|
||||
{
|
||||
$profile_uri = trim($this->arg('profile'));
|
||||
|
||||
if ($profile_uri == '') {
|
||||
$this->showForm(_m('Empty remote profile URL!'));
|
||||
return;
|
||||
}
|
||||
$this->profile_uri = $profile_uri;
|
||||
|
||||
// @fixme validate, normalize bla bla
|
||||
try {
|
||||
$oprofile = Ostatus_profile::ensureProfile($this->profile_uri);
|
||||
$this->oprofile = $oprofile;
|
||||
return true;
|
||||
} catch (FeedSubBadURLException $e) {
|
||||
$err = _m('Invalid URL or could not reach server.');
|
||||
} catch (FeedSubBadResponseException $e) {
|
||||
$err = _m('Cannot read feed; server returned error.');
|
||||
} catch (FeedSubEmptyException $e) {
|
||||
$err = _m('Cannot read feed; server returned an empty page.');
|
||||
} catch (FeedSubBadHTMLException $e) {
|
||||
$err = _m('Bad HTML, could not find feed link.');
|
||||
} catch (FeedSubNoFeedException $e) {
|
||||
$err = _m('Could not find a feed linked from this URL.');
|
||||
} catch (FeedSubUnrecognizedTypeException $e) {
|
||||
$err = _m('Not a recognized feed type.');
|
||||
} catch (FeedSubException $e) {
|
||||
// Any new ones we forgot about
|
||||
$err = sprintf(_m('Bad feed URL: %s %s'), get_class($e), $e->getMessage());
|
||||
}
|
||||
|
||||
$this->showForm($err);
|
||||
return false;
|
||||
}
|
||||
|
||||
function saveFeed()
|
||||
{
|
||||
if ($this->validateFeed()) {
|
||||
$this->preview = true;
|
||||
|
||||
// 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)) {
|
||||
$this->showForm(_m('Already a member!'));
|
||||
} elseif (Group_member::join($this->profile->group_id, $user->id)) {
|
||||
$this->showForm(_m('Joined remote group!'));
|
||||
} else {
|
||||
$this->showForm(_m('Remote group join failed!'));
|
||||
}
|
||||
} else {
|
||||
$local = $this->oprofile->localProfile();
|
||||
if ($user->isSubscribed($local)) {
|
||||
$this->showForm(_m('Already subscribed!'));
|
||||
} elseif ($this->oprofile->subscribeLocalToRemote($user)) {
|
||||
$this->showForm(_m('Remote user subscribed!'));
|
||||
} else {
|
||||
$this->showForm(_m('Remote subscription failed!'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateAndPreview()
|
||||
{
|
||||
if ($this->validateFeed()) {
|
||||
$this->preview = true;
|
||||
$this->showForm(_m('Previewing feed:'));
|
||||
}
|
||||
}
|
||||
|
||||
function previewFeed()
|
||||
{
|
||||
$this->text('Profile preview should go here');
|
||||
}
|
||||
|
||||
function showScripts()
|
||||
|
@@ -29,6 +29,7 @@ class PushCallbackAction extends Action
|
||||
{
|
||||
function handle()
|
||||
{
|
||||
StatusNet::setApi(true); // Minimize error messages to aid in debugging
|
||||
parent::handle();
|
||||
if ($_SERVER['REQUEST_METHOD'] == 'POST') {
|
||||
$this->handlePost();
|
||||
@@ -60,13 +61,18 @@ class PushCallbackAction extends Action
|
||||
|
||||
$post = file_get_contents('php://input');
|
||||
|
||||
// @fixme Queue this to a background process; we should return
|
||||
// Queue this to a background process; we should return
|
||||
// as quickly as possible from a distribution POST.
|
||||
$feedsub->receive($post, $hmac);
|
||||
// If queues are disabled this'll process immediately.
|
||||
$data = array('feedsub_id' => $feedsub->id,
|
||||
'post' => $post,
|
||||
'hmac' => $hmac);
|
||||
$qm = QueueManager::get();
|
||||
$qm->enqueue($data, 'pushin');
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler for GET verification requests from the hub
|
||||
* Handler for GET verification requests from the hub.
|
||||
*/
|
||||
function handleGet()
|
||||
{
|
||||
@@ -75,31 +81,37 @@ class PushCallbackAction extends Action
|
||||
$challenge = $this->arg('hub_challenge');
|
||||
$lease_seconds = $this->arg('hub_lease_seconds');
|
||||
$verify_token = $this->arg('hub_verify_token');
|
||||
|
||||
|
||||
if ($mode != 'subscribe' && $mode != 'unsubscribe') {
|
||||
common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with mode \"$mode\"");
|
||||
throw new ServerException("Bogus hub callback: bad mode", 404);
|
||||
throw new ClientException("Bad hub.mode $mode", 404);
|
||||
}
|
||||
|
||||
|
||||
$feedsub = FeedSub::staticGet('uri', $topic);
|
||||
if (!$feedsub) {
|
||||
common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic");
|
||||
throw new ServerException("Bogus hub callback: unknown feed", 404);
|
||||
throw new ClientException("Bad hub.topic feed $topic", 404);
|
||||
}
|
||||
|
||||
if ($feedsub->verify_token !== $verify_token) {
|
||||
common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic");
|
||||
throw new ServerException("Bogus hub callback: bad token", 404);
|
||||
throw new ClientException("Bad hub.verify_token $token for $topic", 404);
|
||||
}
|
||||
|
||||
if ($mode != $feedsub->sub_state) {
|
||||
common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$feedsub->sub_state}\"");
|
||||
throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404);
|
||||
}
|
||||
|
||||
// OK!
|
||||
if ($mode == 'subscribe') {
|
||||
common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
|
||||
// We may get re-sub requests legitimately.
|
||||
if ($feedsub->sub_state != 'subscribe' && $feedsub->sub_state != 'active') {
|
||||
throw new ClientException("Unexpected subscribe request for $topic.", 404);
|
||||
}
|
||||
} else {
|
||||
if ($feedsub->sub_state != 'unsubscribe') {
|
||||
throw new ClientException("Unexpected unsubscribe request for $topic.", 404);
|
||||
}
|
||||
}
|
||||
|
||||
if ($mode == 'subscribe') {
|
||||
if ($feedsub->sub_state == 'active') {
|
||||
common_log(LOG_INFO, __METHOD__ . ': sub update confirmed');
|
||||
} else {
|
||||
common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
|
||||
}
|
||||
$feedsub->confirmSubscribe($lease_seconds);
|
||||
} else {
|
||||
common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
|
||||
|
@@ -59,102 +59,121 @@ class PushHubAction extends Action
|
||||
$mode = $this->trimmed('hub.mode');
|
||||
switch ($mode) {
|
||||
case "subscribe":
|
||||
$this->subscribe();
|
||||
break;
|
||||
case "unsubscribe":
|
||||
$this->unsubscribe();
|
||||
$this->subunsub($mode);
|
||||
break;
|
||||
case "publish":
|
||||
throw new ServerException("Publishing outside feeds not supported.", 400);
|
||||
throw new ClientException("Publishing outside feeds not supported.", 400);
|
||||
default:
|
||||
throw new ServerException("Unrecognized mode '$mode'.", 400);
|
||||
throw new ClientException("Unrecognized mode '$mode'.", 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a PuSH feed subscription request.
|
||||
* Process a request for a new or modified PuSH feed subscription.
|
||||
* If asynchronous verification is requested, updates won't be saved immediately.
|
||||
*
|
||||
* HTTP return codes:
|
||||
* 202 Accepted - request saved and awaiting verification
|
||||
* 204 No Content - already subscribed
|
||||
* 403 Forbidden - rejecting this (not specifically spec'd)
|
||||
* 400 Bad Request - rejecting this (not specifically spec'd)
|
||||
*/
|
||||
function subscribe()
|
||||
function subunsub($mode)
|
||||
{
|
||||
$feed = $this->argUrl('hub.topic');
|
||||
$callback = $this->argUrl('hub.callback');
|
||||
|
||||
$topic = $this->argUrl('hub.topic');
|
||||
if (!$this->recognizedFeed($topic)) {
|
||||
throw new ClientException("Unsupported hub.topic $topic; this hub only serves local user and group Atom feeds.");
|
||||
}
|
||||
|
||||
$verify = $this->arg('hub.verify'); // @fixme may be multiple
|
||||
if ($verify != 'sync' && $verify != 'async') {
|
||||
throw new ClientException("Invalid hub.verify $verify; must be sync or async.");
|
||||
}
|
||||
|
||||
$lease = $this->arg('hub.lease_seconds', null);
|
||||
if ($mode == 'subscribe' && $lease != '' && !preg_match('/^\d+$/', $lease)) {
|
||||
throw new ClientException("Invalid hub.lease $lease; must be empty or positive integer.");
|
||||
}
|
||||
|
||||
$token = $this->arg('hub.verify_token', null);
|
||||
|
||||
common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback");
|
||||
if ($this->getSub($feed, $callback)) {
|
||||
// Already subscribed; return 204 per spec.
|
||||
$secret = $this->arg('hub.secret', null);
|
||||
if ($secret != '' && strlen($secret) >= 200) {
|
||||
throw new ClientException("Invalid hub.secret $secret; must be under 200 bytes.");
|
||||
}
|
||||
|
||||
$sub = HubSub::staticGet($sub->topic, $sub->callback);
|
||||
if (!$sub) {
|
||||
// Creating a new one!
|
||||
$sub = new HubSub();
|
||||
$sub->topic = $topic;
|
||||
$sub->callback = $callback;
|
||||
}
|
||||
if ($mode == 'subscribe') {
|
||||
if ($secret) {
|
||||
$sub->secret = $secret;
|
||||
}
|
||||
if ($lease) {
|
||||
$sub->setLease(intval($lease));
|
||||
}
|
||||
}
|
||||
|
||||
if (!common_config('queue', 'enabled')) {
|
||||
// Won't be able to background it.
|
||||
$verify = 'sync';
|
||||
}
|
||||
if ($verify == 'async') {
|
||||
$sub->scheduleVerify($mode, $token);
|
||||
header('HTTP/1.1 202 Accepted');
|
||||
} else {
|
||||
$sub->verify($mode, $token);
|
||||
header('HTTP/1.1 204 No Content');
|
||||
common_log(LOG_DEBUG, __METHOD__ . ': already subscribed');
|
||||
return;
|
||||
}
|
||||
|
||||
common_log(LOG_DEBUG, __METHOD__ . ': setting up');
|
||||
$sub = new HubSub();
|
||||
$sub->topic = $feed;
|
||||
$sub->callback = $callback;
|
||||
$sub->secret = $this->arg('hub.secret', null);
|
||||
if (strlen($sub->secret) > 200) {
|
||||
throw new ClientException("hub.secret must be no longer than 200 chars", 400);
|
||||
}
|
||||
$sub->setLease(intval($this->arg('hub.lease_seconds')));
|
||||
|
||||
// @fixme check for feeds we don't manage
|
||||
// @fixme check the verification mode, might want a return immediately?
|
||||
|
||||
common_log(LOG_DEBUG, __METHOD__ . ': inserting');
|
||||
$ok = $sub->insert();
|
||||
|
||||
if (!$ok) {
|
||||
throw new ServerException("Failed to save subscription record", 500);
|
||||
}
|
||||
|
||||
// @fixme check errors ;)
|
||||
|
||||
$data = array('sub' => $sub, 'mode' => 'subscribe', 'token' => $token);
|
||||
$qm = QueueManager::get();
|
||||
$qm->enqueue($data, 'hubverify');
|
||||
|
||||
header('HTTP/1.1 202 Accepted');
|
||||
common_log(LOG_DEBUG, __METHOD__ . ': done');
|
||||
}
|
||||
|
||||
/**
|
||||
* Process a PuSH feed unsubscription request.
|
||||
* Check whether the given URL represents one of our canonical
|
||||
* user or group Atom feeds.
|
||||
*
|
||||
* HTTP return codes:
|
||||
* 202 Accepted - request saved and awaiting verification
|
||||
* 204 No Content - already subscribed
|
||||
* 400 Bad Request - invalid params or rejected feed
|
||||
*
|
||||
* @fixme background this
|
||||
* @param string $feed URL
|
||||
* @return boolean true if it matches
|
||||
*/
|
||||
function unsubscribe()
|
||||
function recognizedFeed($feed)
|
||||
{
|
||||
$feed = $this->argUrl('hub.topic');
|
||||
$callback = $this->argUrl('hub.callback');
|
||||
$sub = $this->getSub($feed, $callback);
|
||||
|
||||
if ($sub) {
|
||||
$token = $this->arg('hub.verify_token', null);
|
||||
if ($sub->verify('unsubscribe', $token)) {
|
||||
$sub->delete();
|
||||
common_log(LOG_INFO, "PuSH unsubscribed $feed for $callback");
|
||||
} else {
|
||||
throw new ServerException("Failed PuSH unsubscription: verification failed! $feed for $callback");
|
||||
$matches = array();
|
||||
if (preg_match('!/(\d+)\.atom$!', $feed, $matches)) {
|
||||
$id = $matches[1];
|
||||
$params = array('id' => $id, 'format' => 'atom');
|
||||
$userFeed = common_local_url('ApiTimelineUser', $params);
|
||||
$groupFeed = common_local_url('ApiTimelineGroup', $params);
|
||||
|
||||
if ($feed == $userFeed) {
|
||||
$user = User::staticGet('id', $id);
|
||||
if (!$user) {
|
||||
throw new ClientException("Invalid hub.topic $feed; user doesn't exist.");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new ServerException("Failed PuSH unsubscription: not subscribed! $feed for $callback");
|
||||
if ($feed == $groupFeed) {
|
||||
$user = User_group::staticGet('id', $id);
|
||||
if (!$user) {
|
||||
throw new ClientException("Invalid hub.topic $feed; group doesn't exist.");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
common_log(LOG_DEBUG, "Not a user or group feed? $feed $userFeed $groupFeed");
|
||||
}
|
||||
common_log(LOG_DEBUG, "LOST $feed");
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab and validate a URL from POST parameters.
|
||||
* @throws ServerException for malformed or non-http/https URLs
|
||||
* @throws ClientException for malformed or non-http/https URLs
|
||||
*/
|
||||
protected function argUrl($arg)
|
||||
{
|
||||
@@ -164,7 +183,7 @@ class PushHubAction extends Action
|
||||
if (Validate::uri($url, $params)) {
|
||||
return $url;
|
||||
} else {
|
||||
throw new ServerException("Invalid URL passed for $arg: '$url'", 400);
|
||||
throw new ClientException("Invalid URL passed for $arg: '$url'");
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -55,6 +55,8 @@ class UsersalmonAction extends SalmonAction
|
||||
*/
|
||||
function handlePost()
|
||||
{
|
||||
common_log(LOG_INFO, "Received post of '{$this->act->object->id}' from '{$this->act->actor->id}'");
|
||||
|
||||
switch ($this->act->object->type) {
|
||||
case ActivityObject::ARTICLE:
|
||||
case ActivityObject::BLOGENTRY:
|
||||
@@ -80,13 +82,21 @@ class UsersalmonAction extends SalmonAction
|
||||
throw new ClientException("In reply to a notice not by this user");
|
||||
}
|
||||
} else if (!empty($context->attention)) {
|
||||
if (!in_array($context->attention, $this->user->uri)) {
|
||||
if (!in_array($this->user->uri, $context->attention)) {
|
||||
common_log(LOG_ERR, "{$this->user->uri} not in attention list (".implode(',', $context->attention).")");
|
||||
throw new ClientException("To the attention of user(s) not including this one!");
|
||||
}
|
||||
} else {
|
||||
throw new ClientException("Not to anyone in reply to anything!");
|
||||
}
|
||||
|
||||
$existing = Notice::staticGet('uri', $this->act->object->id);
|
||||
|
||||
if (!empty($existing)) {
|
||||
common_log(LOG_ERR, "Not saving notice '{$existing->uri}'; already exists.");
|
||||
return;
|
||||
}
|
||||
|
||||
$this->saveNotice();
|
||||
}
|
||||
|
||||
|
@@ -37,7 +37,7 @@ class WebfingerAction extends Action
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function handle()
|
||||
{
|
||||
$acct = Webfinger::normalize($this->uri);
|
||||
@@ -55,15 +55,47 @@ class WebfingerAction extends Action
|
||||
|
||||
$xrd->subject = $this->uri;
|
||||
$xrd->alias[] = common_profile_url($nick);
|
||||
$xrd->links[] = array('rel' => 'http://webfinger.net/rel/profile-page',
|
||||
$xrd->links[] = array('rel' => Webfinger::PROFILEPAGE,
|
||||
'type' => 'text/html',
|
||||
'href' => common_profile_url($nick));
|
||||
|
||||
$xrd->links[] = array('rel' => Webfinger::UPDATESFROM,
|
||||
'href' => common_local_url('ApiTimelineUser',
|
||||
array('id' => $this->user->id,
|
||||
'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}';
|
||||
|
@@ -291,10 +291,9 @@ class FeedSub extends Memcached_DataObject
|
||||
$headers = array('Content-Type: application/x-www-form-urlencoded');
|
||||
$post = array('hub.mode' => $mode,
|
||||
'hub.callback' => $callback,
|
||||
'hub.verify' => 'async',
|
||||
'hub.verify' => 'sync',
|
||||
'hub.verify_token' => $this->verify_token,
|
||||
'hub.secret' => $this->secret,
|
||||
//'hub.lease_seconds' => 0,
|
||||
'hub.topic' => $this->uri);
|
||||
$client = new HTTPClient();
|
||||
$response = $client->post($this->huburi, $headers, $post);
|
||||
@@ -317,8 +316,8 @@ class FeedSub extends Memcached_DataObject
|
||||
common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub $this->huburi subscribing to $this->uri");
|
||||
|
||||
$orig = clone($this);
|
||||
$this->verify_token = null;
|
||||
$this->sub_state = null;
|
||||
$this->verify_token = '';
|
||||
$this->sub_state = 'inactive';
|
||||
$this->update($orig);
|
||||
unset($orig);
|
||||
|
||||
@@ -343,7 +342,7 @@ class FeedSub extends Memcached_DataObject
|
||||
} else {
|
||||
$this->sub_end = null;
|
||||
}
|
||||
$this->lastupdate = common_sql_now();
|
||||
$this->modified = common_sql_now();
|
||||
|
||||
return $this->update($original);
|
||||
}
|
||||
@@ -362,7 +361,7 @@ class FeedSub extends Memcached_DataObject
|
||||
$this->sub_state = '';
|
||||
$this->sub_start = '';
|
||||
$this->sub_end = '';
|
||||
$this->lastupdate = common_sql_now();
|
||||
$this->modified = common_sql_now();
|
||||
|
||||
return $this->update($original);
|
||||
}
|
||||
@@ -372,6 +371,12 @@ class FeedSub extends Memcached_DataObject
|
||||
* feed (as a DOMDocument) will be passed to the StartFeedSubHandleFeed
|
||||
* and EndFeedSubHandleFeed events for processing.
|
||||
*
|
||||
* Not guaranteed to be running in an immediate POST context; may be run
|
||||
* from a queue handler.
|
||||
*
|
||||
* Side effects: the feedsub record's lastupdate field will be updated
|
||||
* to the current time (not published time) if we got a legit update.
|
||||
*
|
||||
* @param string $post source of Atom or RSS feed
|
||||
* @param string $hmac X-Hub-Signature header, if present
|
||||
*/
|
||||
@@ -402,6 +407,10 @@ class FeedSub extends Memcached_DataObject
|
||||
return;
|
||||
}
|
||||
|
||||
$orig = clone($this);
|
||||
$this->last_update = common_sql_now();
|
||||
$this->update($orig);
|
||||
|
||||
Event::handle('StartFeedSubReceive', array($this, $feed));
|
||||
Event::handle('EndFeedSubReceive', array($this, $feed));
|
||||
}
|
||||
|
@@ -30,11 +30,11 @@ class HubSub extends Memcached_DataObject
|
||||
public $topic;
|
||||
public $callback;
|
||||
public $secret;
|
||||
public $challenge;
|
||||
public $lease;
|
||||
public $sub_start;
|
||||
public $sub_end;
|
||||
public $created;
|
||||
public $modified;
|
||||
|
||||
public /*static*/ function staticGet($topic, $callback)
|
||||
{
|
||||
@@ -61,11 +61,11 @@ class HubSub extends Memcached_DataObject
|
||||
'topic' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
|
||||
'callback' => DB_DATAOBJECT_STR + DB_DATAOBJECT_NOTNULL,
|
||||
'secret' => DB_DATAOBJECT_STR,
|
||||
'challenge' => DB_DATAOBJECT_STR,
|
||||
'lease' => DB_DATAOBJECT_INT,
|
||||
'sub_start' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
|
||||
'sub_end' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME,
|
||||
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
|
||||
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
|
||||
'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
|
||||
}
|
||||
|
||||
static function schemaDef()
|
||||
@@ -82,8 +82,6 @@ class HubSub extends Memcached_DataObject
|
||||
255, false),
|
||||
new ColumnDef('secret', 'text',
|
||||
null, true),
|
||||
new ColumnDef('challenge', 'varchar',
|
||||
32, true),
|
||||
new ColumnDef('lease', 'int',
|
||||
null, true),
|
||||
new ColumnDef('sub_start', 'datetime',
|
||||
@@ -91,6 +89,8 @@ class HubSub extends Memcached_DataObject
|
||||
new ColumnDef('sub_end', 'datetime',
|
||||
null, true),
|
||||
new ColumnDef('created', 'datetime',
|
||||
null, false),
|
||||
new ColumnDef('modified', 'datetime',
|
||||
null, false));
|
||||
}
|
||||
|
||||
@@ -148,84 +148,125 @@ class HubSub extends Memcached_DataObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a verification ping to subscriber
|
||||
* Schedule a future verification ping to the subscriber.
|
||||
* If queues are disabled, will be immediate.
|
||||
*
|
||||
* @param string $mode 'subscribe' or 'unsubscribe'
|
||||
* @param string $token hub.verify_token value, if provided by client
|
||||
*/
|
||||
function scheduleVerify($mode, $token=null, $retries=null)
|
||||
{
|
||||
if ($retries === null) {
|
||||
$retries = intval(common_config('ostatus', 'hub_retries'));
|
||||
}
|
||||
$data = array('sub' => clone($this),
|
||||
'mode' => $mode,
|
||||
'token' => $token,
|
||||
'retries' => $retries);
|
||||
$qm = QueueManager::get();
|
||||
$qm->enqueue($data, 'hubconf');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a verification ping to subscriber, and if confirmed apply the changes.
|
||||
* This may create, update, or delete the database record.
|
||||
*
|
||||
* @param string $mode 'subscribe' or 'unsubscribe'
|
||||
* @param string $token hub.verify_token value, if provided by client
|
||||
* @throws ClientException on failure
|
||||
*/
|
||||
function verify($mode, $token=null)
|
||||
{
|
||||
assert($mode == 'subscribe' || $mode == 'unsubscribe');
|
||||
|
||||
// Is this needed? data object fun...
|
||||
$clone = clone($this);
|
||||
$clone->challenge = common_good_rand(16);
|
||||
$clone->update($this);
|
||||
$this->challenge = $clone->challenge;
|
||||
unset($clone);
|
||||
|
||||
$challenge = common_good_rand(32);
|
||||
$params = array('hub.mode' => $mode,
|
||||
'hub.topic' => $this->topic,
|
||||
'hub.challenge' => $this->challenge);
|
||||
'hub.challenge' => $challenge);
|
||||
if ($mode == 'subscribe') {
|
||||
$params['hub.lease_seconds'] = $this->lease;
|
||||
}
|
||||
if ($token !== null) {
|
||||
$params['hub.verify_token'] = $token;
|
||||
}
|
||||
$url = $this->callback . '?' . http_build_query($params, '', '&'); // @fixme ugly urls
|
||||
|
||||
try {
|
||||
$request = new HTTPClient();
|
||||
$response = $request->get($url);
|
||||
$status = $response->getStatus();
|
||||
|
||||
if ($status >= 200 && $status < 300) {
|
||||
$fail = false;
|
||||
} else {
|
||||
// @fixme how can we schedule a second attempt?
|
||||
// Or should we?
|
||||
$fail = "Returned HTTP $status";
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
$fail = $e->getMessage();
|
||||
}
|
||||
if ($fail) {
|
||||
// @fixme how can we schedule a second attempt?
|
||||
// or save a fail count?
|
||||
// Or should we?
|
||||
common_log(LOG_ERR, "Failed to verify $mode for $this->topic at $this->callback: $fail");
|
||||
return false;
|
||||
// Any existing query string parameters must be preserved
|
||||
$url = $this->callback;
|
||||
if (strpos('?', $url) !== false) {
|
||||
$url .= '&';
|
||||
} else {
|
||||
if ($mode == 'subscribe') {
|
||||
// Establish or renew the subscription!
|
||||
// This seems unnecessary... dataobject fun!
|
||||
$clone = clone($this);
|
||||
$clone->challenge = null;
|
||||
$clone->setLease($this->lease);
|
||||
$clone->update($this);
|
||||
unset($clone);
|
||||
$url .= '?';
|
||||
}
|
||||
$url .= http_build_query($params, '', '&');
|
||||
|
||||
$this->challenge = null;
|
||||
$this->setLease($this->lease);
|
||||
common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic for $this->lease seconds");
|
||||
} else if ($mode == 'unsubscribe') {
|
||||
common_log(LOG_ERR, "Verified $mode of $this->callback:$this->topic");
|
||||
$this->delete();
|
||||
$request = new HTTPClient();
|
||||
$response = $request->get($url);
|
||||
$status = $response->getStatus();
|
||||
|
||||
if ($status >= 200 && $status < 300) {
|
||||
common_log(LOG_INFO, "Verified $mode of $this->callback:$this->topic");
|
||||
} else {
|
||||
throw new ClientException("Hub subscriber verification returned HTTP $status");
|
||||
}
|
||||
|
||||
$old = HubSub::staticGet($this->topic, $this->callback);
|
||||
if ($mode == 'subscribe') {
|
||||
if ($old) {
|
||||
$this->update($old);
|
||||
} else {
|
||||
$ok = $this->insert();
|
||||
}
|
||||
} else if ($mode == 'unsubscribe') {
|
||||
if ($old) {
|
||||
$old->delete();
|
||||
} else {
|
||||
// That's ok, we're already unsubscribed.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert wrapper; transparently set the hash key from topic and callback columns.
|
||||
* @return boolean success
|
||||
* @return mixed success
|
||||
*/
|
||||
function insert()
|
||||
{
|
||||
$this->hashkey = self::hashkey($this->topic, $this->callback);
|
||||
$this->created = common_sql_now();
|
||||
$this->modified = common_sql_now();
|
||||
return parent::insert();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update wrapper; transparently update modified column.
|
||||
* @return boolean success
|
||||
*/
|
||||
function update($old=null)
|
||||
{
|
||||
$this->modified = common_sql_now();
|
||||
return parent::update($old);
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule delivery of a 'fat ping' to the subscriber's callback
|
||||
* endpoint. If queues are disabled, this will run immediately.
|
||||
*
|
||||
* @param string $atom well-formed Atom feed
|
||||
* @param int $retries optional count of retries if POST fails; defaults to hub_retries from config or 0 if unset
|
||||
*/
|
||||
function distribute($atom, $retries=null)
|
||||
{
|
||||
if ($retries === null) {
|
||||
$retries = intval(common_config('ostatus', 'hub_retries'));
|
||||
}
|
||||
|
||||
$data = array('sub' => clone($this),
|
||||
'atom' => $atom,
|
||||
'retries' => $retries);
|
||||
$qm = QueueManager::get();
|
||||
$qm->enqueue($data, 'hubout');
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a 'fat ping' to the subscriber's callback endpoint
|
||||
* containing the given Atom feed chunk.
|
||||
@@ -234,6 +275,7 @@ class HubSub extends Memcached_DataObject
|
||||
* a higher level; don't just shove in a complete feed!
|
||||
*
|
||||
* @param string $atom well-formed Atom feed
|
||||
* @throws Exception (HTTP or general)
|
||||
*/
|
||||
function push($atom)
|
||||
{
|
||||
@@ -245,24 +287,18 @@ class HubSub extends Memcached_DataObject
|
||||
$hmac = '(none)';
|
||||
}
|
||||
common_log(LOG_INFO, "About to push feed to $this->callback for $this->topic, HMAC $hmac");
|
||||
try {
|
||||
$request = new HTTPClient();
|
||||
$request->setBody($atom);
|
||||
$response = $request->post($this->callback, $headers);
|
||||
|
||||
if ($response->isOk()) {
|
||||
return true;
|
||||
}
|
||||
common_log(LOG_ERR, "Error sending PuSH content " .
|
||||
"to $this->callback for $this->topic: " .
|
||||
$response->getStatus());
|
||||
return false;
|
||||
$request = new HTTPClient();
|
||||
$request->setBody($atom);
|
||||
$response = $request->post($this->callback, $headers);
|
||||
|
||||
} catch (Exception $e) {
|
||||
common_log(LOG_ERR, "Error sending PuSH content " .
|
||||
"to $this->callback for $this->topic: " .
|
||||
$e->getMessage());
|
||||
return false;
|
||||
if ($response->isOk()) {
|
||||
return true;
|
||||
} else {
|
||||
throw new Exception("Callback returned status: " .
|
||||
$response->getStatus() .
|
||||
"; body: " .
|
||||
trim($response->getBody()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
219
plugins/OStatus/classes/Magicsig.php
Normal file
219
plugins/OStatus/classes/Magicsig.php
Normal file
@@ -0,0 +1,219 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* A sample module to show best practices for StatusNet plugins
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @package StatusNet
|
||||
* @author James Walker <james@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
require_once 'Crypt/RSA.php';
|
||||
|
||||
class Magicsig extends Memcached_DataObject
|
||||
{
|
||||
|
||||
const PUBLICKEYREL = 'magic-public-key';
|
||||
|
||||
public $__table = 'magicsig';
|
||||
|
||||
public $user_id;
|
||||
public $keypair;
|
||||
public $alg;
|
||||
|
||||
private $_rsa;
|
||||
|
||||
public function __construct($alg = 'RSA-SHA256')
|
||||
{
|
||||
$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();
|
||||
|
||||
$this->_rsa = new Crypt_RSA($params);
|
||||
PEAR::popErrorHandling();
|
||||
|
||||
$this->insert();
|
||||
}
|
||||
|
||||
|
||||
public function toString($full_pair = true)
|
||||
{
|
||||
$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());
|
||||
$private_exp = '';
|
||||
if ($full_pair && $private_key->getExponent()) {
|
||||
$private_exp = '.' . base64_url_encode($private_key->getExponent());
|
||||
}
|
||||
|
||||
return 'RSA.' . $mod . '.' . $exp . $private_exp;
|
||||
}
|
||||
|
||||
public static function fromString($text)
|
||||
{
|
||||
PEAR::pushErrorHandling(PEAR_ERROR_RETURN);
|
||||
|
||||
$magic_sig = new Magicsig();
|
||||
|
||||
// remove whitespace
|
||||
$text = preg_replace('/\s+/', '', $text);
|
||||
|
||||
// parse components
|
||||
if (!preg_match('/RSA\.([^\.]+)\.([^\.]+)(.([^\.]+))?/', $text, $matches)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$mod = base64_url_decode($matches[1]);
|
||||
$exp = base64_url_decode($matches[2]);
|
||||
if ($matches[4]) {
|
||||
$private_exp = base64_url_decode($matches[4]);
|
||||
}
|
||||
|
||||
$params['public_key'] = new Crypt_RSA_KEY($mod, $exp, 'public');
|
||||
if ($params['public_key']->isError()) {
|
||||
$error = $params['public_key']->getLastError();
|
||||
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();
|
||||
common_log(LOG_DEBUG, 'RSA Error: '. $error->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$magic_sig->_rsa = new Crypt_RSA($params);
|
||||
PEAR::popErrorHandling();
|
||||
|
||||
return $magic_sig;
|
||||
}
|
||||
|
||||
public function getName()
|
||||
{
|
||||
return $this->alg;
|
||||
}
|
||||
|
||||
public function getHash()
|
||||
{
|
||||
switch ($this->alg) {
|
||||
|
||||
case 'RSA-SHA256':
|
||||
return 'sha256';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public function sign($bytes)
|
||||
{
|
||||
$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;
|
||||
}
|
||||
|
||||
public function verify($signed_bytes, $signature)
|
||||
{
|
||||
$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());
|
||||
return false;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Define a sha256 function for hashing
|
||||
// (Crypt_RSA should really be updated to use hash() )
|
||||
function sha256($bytes)
|
||||
{
|
||||
return hash('sha256', $bytes);
|
||||
}
|
||||
|
||||
function base64_url_encode($input)
|
||||
{
|
||||
return strtr(base64_encode($input), '+/', '-_');
|
||||
}
|
||||
|
||||
function base64_url_decode($input)
|
||||
{
|
||||
return base64_decode(strtr($input, '-_', '+/'));
|
||||
}
|
@@ -33,6 +33,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
|
||||
public $feeduri;
|
||||
public $salmonuri;
|
||||
public $avatar; // remote URL of the last avatar we saved
|
||||
|
||||
public $created;
|
||||
public $modified;
|
||||
@@ -58,6 +59,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
'group_id' => DB_DATAOBJECT_INT,
|
||||
'feeduri' => DB_DATAOBJECT_STR,
|
||||
'salmonuri' => DB_DATAOBJECT_STR,
|
||||
'avatar' => DB_DATAOBJECT_STR,
|
||||
'created' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL,
|
||||
'modified' => DB_DATAOBJECT_STR + DB_DATAOBJECT_DATE + DB_DATAOBJECT_TIME + DB_DATAOBJECT_NOTNULL);
|
||||
}
|
||||
@@ -74,6 +76,8 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
255, true, 'UNI'),
|
||||
new ColumnDef('salmonuri', 'text',
|
||||
null, true),
|
||||
new ColumnDef('avatar', 'text',
|
||||
null, true),
|
||||
new ColumnDef('created', 'datetime',
|
||||
null, false),
|
||||
new ColumnDef('modified', 'datetime',
|
||||
@@ -137,12 +141,49 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an ActivityObject describing this remote user or group profile.
|
||||
* Can then be used to generate Atom chunks.
|
||||
*
|
||||
* @return ActivityObject
|
||||
*/
|
||||
function asActivityObject()
|
||||
{
|
||||
if ($this->isGroup()) {
|
||||
$object = new ActivityObject();
|
||||
$object->type = 'http://activitystrea.ms/schema/1.0/group';
|
||||
$object->id = $this->uri;
|
||||
$self = $this->localGroup();
|
||||
|
||||
// @fixme put a standard getAvatar() interface on groups too
|
||||
if ($self->homepage_logo) {
|
||||
$object->avatar = $self->homepage_logo;
|
||||
$map = array('png' => 'image/png',
|
||||
'jpg' => 'image/jpeg',
|
||||
'jpeg' => 'image/jpeg',
|
||||
'gif' => 'image/gif');
|
||||
$extension = pathinfo(parse_url($avatarHref, PHP_URL_PATH), PATHINFO_EXTENSION);
|
||||
if (isset($map[$extension])) {
|
||||
// @fixme this ain't used/saved yet
|
||||
$object->avatarType = $map[$extension];
|
||||
}
|
||||
}
|
||||
|
||||
$object->link = $this->uri; // @fixme accurate?
|
||||
return $object;
|
||||
} else {
|
||||
return ActivityObject::fromProfile($this->localProfile());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an XML string fragment with profile information as an
|
||||
* Activity Streams noun object with the given element type.
|
||||
*
|
||||
* Assumes that 'activity' namespace has been previously defined.
|
||||
*
|
||||
* @fixme replace with wrappers on asActivityObject when it's got everything.
|
||||
*
|
||||
* @param string $element one of 'actor', 'subject', 'object', 'target'
|
||||
* @return string
|
||||
*/
|
||||
@@ -202,11 +243,19 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
}
|
||||
|
||||
/**
|
||||
* Damn dirty hack!
|
||||
* @return boolean true if this is a remote group
|
||||
*/
|
||||
function isGroup()
|
||||
{
|
||||
return (strpos($this->feeduri, '/groups/') !== false);
|
||||
if ($this->profile_id && !$this->group_id) {
|
||||
return false;
|
||||
} else if ($this->group_id && !$this->profile_id) {
|
||||
return true;
|
||||
} else if ($this->group_id && $this->profile_id) {
|
||||
throw new ServerException("Invalid ostatus_profile state: both group and profile IDs set for $this->uri");
|
||||
} else {
|
||||
throw new ServerException("Invalid ostatus_profile state: both group and profile IDs empty for $this->uri");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -250,18 +299,9 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
throw new ServerException("Remote groups can't subscribe to local users");
|
||||
}
|
||||
|
||||
// @fixme use regular channels for subbing, once they accept remote profiles
|
||||
$sub = new Subscription();
|
||||
$sub->subscriber = $this->profile_id;
|
||||
$sub->subscribed = $user->id;
|
||||
$sub->created = common_sql_now(); // current time
|
||||
Subscription::start($this->localProfile(), $user->getProfile());
|
||||
|
||||
if ($sub->insert()) {
|
||||
// @fixme use subs_notify() if refactored to take profiles?
|
||||
mail_subscribe_notify_profile($user, $this->localProfile());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -301,6 +341,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.
|
||||
@@ -334,7 +397,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);
|
||||
@@ -353,22 +417,60 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
common_log(LOG_INFO, "Posting to Salmon endpoint $this->salmonuri: $xml");
|
||||
|
||||
$salmon = new Salmon(); // ?
|
||||
$salmon->post($this->salmonuri, $xml);
|
||||
return $salmon->post($this->salmonuri, $xml);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function notifyActivity($activity)
|
||||
/**
|
||||
* Send a Salmon notification ping immediately, and confirm that we got
|
||||
* an acceptable response from the remote site.
|
||||
*
|
||||
* @param mixed $entry XML string, Notice, or Activity
|
||||
* @return boolean success
|
||||
*/
|
||||
public function notifyActivity($entry)
|
||||
{
|
||||
if ($this->salmonuri) {
|
||||
|
||||
$xml = $activity->asString();
|
||||
|
||||
$salmon = new Salmon(); // ?
|
||||
|
||||
$salmon->post($this->salmonuri, $xml);
|
||||
$salmon = new Salmon();
|
||||
return $salmon->post($this->salmonuri, $this->notifyPrepXml($entry));
|
||||
}
|
||||
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Queue a Salmon notification for later. If queues are disabled we'll
|
||||
* send immediately but won't get the return value.
|
||||
*
|
||||
* @param mixed $entry XML string, Notice, or Activity
|
||||
* @return boolean success
|
||||
*/
|
||||
public function notifyDeferred($entry)
|
||||
{
|
||||
if ($this->salmonuri) {
|
||||
$data = array('salmonuri' => $this->salmonuri,
|
||||
'entry' => $this->notifyPrepXml($entry));
|
||||
|
||||
$qm = QueueManager::get();
|
||||
return $qm->enqueue($data, 'salmon');
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected function notifyPrepXml($entry)
|
||||
{
|
||||
$preamble = '<?xml version="1.0" encoding="UTF-8" ?' . '>';
|
||||
if (is_string($entry)) {
|
||||
return $entry;
|
||||
} else if ($entry instanceof Activity) {
|
||||
return $preamble . $entry->asString(true);
|
||||
} else if ($entry instanceof Notice) {
|
||||
return $preamble . $entry->asAtomEntry(true, true);
|
||||
} else {
|
||||
throw new ServerException("Invalid type passed to Ostatus_profile::notify; must be XML string or Activity entry");
|
||||
}
|
||||
}
|
||||
|
||||
function getBestName()
|
||||
@@ -417,7 +519,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) {
|
||||
@@ -427,7 +529,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -437,15 +539,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");
|
||||
}
|
||||
@@ -454,67 +553,189 @@ 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
|
||||
// Check if profile info has changed and update it
|
||||
$this->updateFromActivityObject($activity->actor);
|
||||
} 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);
|
||||
}
|
||||
|
||||
if ($activity->context) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @fixme save detailed ostatus source info
|
||||
// @fixme ensure that groups get handled correctly
|
||||
try {
|
||||
$saved = Notice::saveNew($oprofile->profile_id,
|
||||
$content,
|
||||
'ostatus',
|
||||
$options);
|
||||
if ($saved) {
|
||||
Ostatus_source::saveNew($saved, $this, $method);
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
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;
|
||||
}
|
||||
|
||||
$saved = Notice::saveNew($oprofile->localProfile()->id,
|
||||
$content,
|
||||
'ostatus',
|
||||
$params);
|
||||
/**
|
||||
* Clean up HTML
|
||||
*/
|
||||
protected function purify($html)
|
||||
{
|
||||
require_once INSTALLDIR.'/extlib/htmLawed/htmLawed.php';
|
||||
$config = array('safe' => 1);
|
||||
return htmLawed($html, $config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
{
|
||||
common_log(LOG_DEBUG, "Original reply recipients: " . implode(', ', $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;
|
||||
} else {
|
||||
common_log(LOG_DEBUG, "Skipping reply to remote profile $recipient");
|
||||
}
|
||||
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.
|
||||
$profile = $sender->localProfile();
|
||||
if ($profile->isMember($group)) {
|
||||
$groups[] = $group->id;
|
||||
} else {
|
||||
common_log(LOG_DEBUG, "Skipping reply to local group $group->nickname as sender $profile->id is not a member");
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
common_log(LOG_DEBUG, "Skipping reply to bogus group $recipient");
|
||||
}
|
||||
}
|
||||
|
||||
common_log(LOG_DEBUG, "Skipping reply to unrecognized profile $recipient");
|
||||
|
||||
}
|
||||
$attention_uris = $replies;
|
||||
common_log(LOG_DEBUG, "Local reply recipients: " . implode(', ', $replies));
|
||||
common_log(LOG_DEBUG, "Local group recipients: " . implode(', ', $groups));
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -522,7 +743,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
* @return Ostatus_profile
|
||||
* @throws FeedSubException
|
||||
*/
|
||||
public static function ensureProfile($profile_uri)
|
||||
public static function ensureProfile($profile_uri, $hints=array())
|
||||
{
|
||||
// Get the canonical feed URI and check it
|
||||
$discover = new FeedDiscovery();
|
||||
@@ -545,7 +766,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
|
||||
if (!empty($subject)) {
|
||||
$subjObject = new ActivityObject($subject);
|
||||
return self::ensureActivityObjectProfile($subjObject, $feeduri, $salmonuri);
|
||||
return self::ensureActivityObjectProfile($subjObject, $feeduri, $salmonuri, $hints);
|
||||
}
|
||||
|
||||
// Otherwise, try the feed author
|
||||
@@ -554,7 +775,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
|
||||
if (!empty($author)) {
|
||||
$authorObject = new ActivityObject($author);
|
||||
return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri);
|
||||
return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri, $hints);
|
||||
}
|
||||
|
||||
// Sheesh. Not a very nice feed! Let's try fingerpoken in the
|
||||
@@ -570,7 +791,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
|
||||
if (!empty($actor)) {
|
||||
$actorObject = new ActivityObject($actor);
|
||||
return self::ensureActivityObjectProfile($actorObject, $feeduri, $salmonuri);
|
||||
return self::ensureActivityObjectProfile($actorObject, $feeduri, $salmonuri, $hints);
|
||||
|
||||
}
|
||||
|
||||
@@ -578,7 +799,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
|
||||
if (!empty($author)) {
|
||||
$authorObject = new ActivityObject($author);
|
||||
return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri);
|
||||
return self::ensureActivityObjectProfile($authorObject, $feeduri, $salmonuri, $hints);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -595,10 +816,28 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
*/
|
||||
protected function updateAvatar($url)
|
||||
{
|
||||
if ($url == $this->avatar) {
|
||||
// We've already got this one.
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->isGroup()) {
|
||||
$self = $this->localGroup();
|
||||
} else {
|
||||
$self = $this->localProfile();
|
||||
}
|
||||
if (!$self) {
|
||||
throw new ServerException(sprintf(
|
||||
_m("Tried to update avatar for unsaved remote profile %s"),
|
||||
$this->uri));
|
||||
}
|
||||
|
||||
// @fixme this should be better encapsulated
|
||||
// ripped from oauthstore.php (for old OMB client)
|
||||
$temp_filename = tempnam(sys_get_temp_dir(), 'listener_avatar');
|
||||
copy($url, $temp_filename);
|
||||
if (!copy($url, $temp_filename)) {
|
||||
throw new ServerException(sprintf(_m("Unable to fetch avatar from %s"), $url));
|
||||
}
|
||||
|
||||
if ($this->isGroup()) {
|
||||
$id = $this->group_id;
|
||||
@@ -612,19 +851,29 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
null,
|
||||
common_timestamp());
|
||||
rename($temp_filename, Avatar::path($filename));
|
||||
if ($this->isGroup()) {
|
||||
$group = $this->localGroup();
|
||||
$group->setOriginal($filename);
|
||||
} else {
|
||||
$profile = $this->localProfile();
|
||||
$profile->setOriginal($filename);
|
||||
}
|
||||
$self->setOriginal($filename);
|
||||
|
||||
$orig = clone($this);
|
||||
$this->avatar = $url;
|
||||
$this->update($orig);
|
||||
}
|
||||
|
||||
protected static function getActivityObjectAvatar($object)
|
||||
/**
|
||||
* Pull avatar URL from ActivityObject or profile hints
|
||||
*
|
||||
* @param ActivityObject $object
|
||||
* @param array $hints
|
||||
* @return mixed URL string or false
|
||||
*/
|
||||
|
||||
protected static function getActivityObjectAvatar($object, $hints=array())
|
||||
{
|
||||
// XXX: go poke around in the feed
|
||||
return $object->avatar;
|
||||
if ($object->avatar) {
|
||||
return $object->avatar;
|
||||
} else if (array_key_exists('avatar', $hints)) {
|
||||
return $hints['avatar'];
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -688,11 +937,13 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
return self::ensureActivityObjectProfile($activity->actor, $feeduri, $salmonuri);
|
||||
}
|
||||
|
||||
public static function ensureActivityObjectProfile($object, $feeduri=null, $salmonuri=null)
|
||||
public static function ensureActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array())
|
||||
{
|
||||
$profile = self::getActivityObjectProfile($object);
|
||||
if (!$profile) {
|
||||
$profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri);
|
||||
if ($profile) {
|
||||
$profile->updateFromActivityObject($object, $hints);
|
||||
} else {
|
||||
$profile = self::createActivityObjectProfile($object, $feeduri, $salmonuri, $hints);
|
||||
}
|
||||
return $profile;
|
||||
}
|
||||
@@ -701,7 +952,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
* @param Activity $activity
|
||||
* @return mixed matching Ostatus_profile or false if none known
|
||||
*/
|
||||
protected static function getActorProfile($activity)
|
||||
public static function getActorProfile($activity)
|
||||
{
|
||||
return self::getActivityObjectProfile($activity->actor);
|
||||
}
|
||||
@@ -709,7 +960,7 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
protected static function getActivityObjectProfile($object)
|
||||
{
|
||||
$uri = self::getActivityObjectProfileURI($object);
|
||||
return Ostatus_profile::staticGet('homeuri', $uri);
|
||||
return Ostatus_profile::staticGet('uri', $uri);
|
||||
}
|
||||
|
||||
protected static function getActorProfileURI($activity)
|
||||
@@ -745,17 +996,39 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
self::createActivityObjectProfile($actor, $feeduri, $salmonuri);
|
||||
}
|
||||
|
||||
protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null)
|
||||
/**
|
||||
* Create local ostatus_profile and profile/user_group entries for
|
||||
* the provided remote user or group.
|
||||
*
|
||||
* @param ActivityObject $object
|
||||
* @param string $feeduri
|
||||
* @param string $salmonuri
|
||||
* @param array $hints
|
||||
*
|
||||
* @fixme fold $feeduri/$salmonuri into $hints
|
||||
* @return Ostatus_profile
|
||||
*/
|
||||
protected static function createActivityObjectProfile($object, $feeduri=null, $salmonuri=null, $hints=array())
|
||||
{
|
||||
$homeuri = self::getActivityObjectProfileURI($object);
|
||||
$nickname = self::getActivityObjectNickname($object);
|
||||
$avatar = self::getActivityObjectAvatar($object);
|
||||
$homeuri = $object->id;
|
||||
|
||||
if (!$homeuri) {
|
||||
common_log(LOG_DEBUG, __METHOD__ . " empty actor profile URI: " . var_export($activity, true));
|
||||
throw new ServerException("No profile URI");
|
||||
}
|
||||
|
||||
if (empty($feeduri)) {
|
||||
if (array_key_exists('feedurl', $hints)) {
|
||||
$feeduri = $hints['feedurl'];
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($salmonuri)) {
|
||||
if (array_key_exists('salmon', $hints)) {
|
||||
$salmonuri = $hints['salmon'];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$feeduri || !$salmonuri) {
|
||||
// Get the canonical feed URI and check it
|
||||
$discover = new FeedDiscovery();
|
||||
@@ -770,11 +1043,79 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
}
|
||||
}
|
||||
|
||||
$profile = new Profile();
|
||||
$profile->nickname = $nickname;
|
||||
$profile->fullname = $object->title;
|
||||
$profile->profileurl = $object->link;
|
||||
$profile->created = common_sql_now();
|
||||
$oprofile = new Ostatus_profile();
|
||||
|
||||
$oprofile->uri = $homeuri;
|
||||
$oprofile->feeduri = $feeduri;
|
||||
$oprofile->salmonuri = $salmonuri;
|
||||
|
||||
$oprofile->created = common_sql_now();
|
||||
$oprofile->modified = common_sql_now();
|
||||
|
||||
if ($object->type == ActivityObject::PERSON) {
|
||||
$profile = new Profile();
|
||||
self::updateProfile($profile, $object, $hints);
|
||||
$profile->created = common_sql_now();
|
||||
|
||||
$oprofile->profile_id = $profile->insert();
|
||||
if (!$oprofile->profile_id) {
|
||||
throw new ServerException("Can't save local profile");
|
||||
}
|
||||
} else {
|
||||
$group = new User_group();
|
||||
$group->created = common_sql_now();
|
||||
self::updateGroup($group, $object, $hints);
|
||||
|
||||
$oprofile->group_id = $group->insert();
|
||||
if (!$oprofile->group_id) {
|
||||
throw new ServerException("Can't save local profile");
|
||||
}
|
||||
}
|
||||
|
||||
$ok = $oprofile->insert();
|
||||
|
||||
if ($ok) {
|
||||
$avatar = self::getActivityObjectAvatar($object, $hints);
|
||||
if ($avatar) {
|
||||
$oprofile->updateAvatar($avatar);
|
||||
}
|
||||
return $oprofile;
|
||||
} else {
|
||||
throw new ServerException("Can't save OStatus profile");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save any updated profile information to our local copy.
|
||||
* @param ActivityObject $object
|
||||
* @param array $hints
|
||||
*/
|
||||
public function updateFromActivityObject($object, $hints=array())
|
||||
{
|
||||
if ($this->isGroup()) {
|
||||
$group = $this->localGroup();
|
||||
self::updateGroup($group, $object, $hints);
|
||||
} else {
|
||||
$profile = $this->localProfile();
|
||||
self::updateProfile($profile, $object, $hints);
|
||||
}
|
||||
$avatar = self::getActivityObjectAvatar($object, $hints);
|
||||
if ($avatar) {
|
||||
$this->updateAvatar($avatar);
|
||||
}
|
||||
}
|
||||
|
||||
protected static function updateProfile($profile, $object, $hints=array())
|
||||
{
|
||||
$orig = clone($profile);
|
||||
|
||||
$profile->nickname = self::getActivityObjectNickname($object, $hints);
|
||||
$profile->fullname = $object->title;
|
||||
if (!empty($object->link)) {
|
||||
$profile->profileurl = $object->link;
|
||||
} else if (array_key_exists('profileurl', $hints)) {
|
||||
$profile->profileurl = $hints['profileurl'];
|
||||
}
|
||||
|
||||
// @fixme bio
|
||||
// @fixme tags/categories
|
||||
@@ -782,42 +1123,61 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
// @todo tags from categories
|
||||
// @todo lat/lon/location?
|
||||
|
||||
$ok = $profile->insert();
|
||||
|
||||
if (!$ok) {
|
||||
throw new ServerException("Can't save local profile");
|
||||
}
|
||||
|
||||
// @fixme either need to do feed discovery here
|
||||
// or need to split out some of the feed stuff
|
||||
// so we can leave it empty until later.
|
||||
|
||||
$oprofile = new Ostatus_profile();
|
||||
|
||||
$oprofile->uri = $homeuri;
|
||||
$oprofile->feeduri = $feeduri;
|
||||
$oprofile->salmonuri = $salmonuri;
|
||||
$oprofile->profile_id = $profile->id;
|
||||
|
||||
$oprofile->created = common_sql_now();
|
||||
$oprofile->modified = common_sql_now();
|
||||
|
||||
$ok = $oprofile->insert();
|
||||
|
||||
if ($ok) {
|
||||
$oprofile->updateAvatar($avatar);
|
||||
return $oprofile;
|
||||
} else {
|
||||
throw new ServerException("Can't save OStatus profile");
|
||||
if ($profile->id) {
|
||||
common_log(LOG_DEBUG, "Updating OStatus profile $profile->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true));
|
||||
$profile->update($orig);
|
||||
}
|
||||
}
|
||||
|
||||
protected static function getActivityObjectNickname($object)
|
||||
protected static function updateGroup($group, $object, $hints=array())
|
||||
{
|
||||
// XXX: check whatever PoCo calls a nickname first
|
||||
$orig = clone($group);
|
||||
|
||||
// @fixme need to make nick unique etc *hack hack*
|
||||
$group->nickname = self::getActivityObjectNickname($object, $hints);
|
||||
$group->fullname = $object->title;
|
||||
|
||||
// @fixme no canonical profileurl; using homepage instead for now
|
||||
$group->homepage = $object->id;
|
||||
|
||||
// @fixme homepage
|
||||
// @fixme bio
|
||||
// @fixme tags/categories
|
||||
// @fixme location?
|
||||
// @todo tags from categories
|
||||
// @todo lat/lon/location?
|
||||
|
||||
if ($group->id) {
|
||||
common_log(LOG_DEBUG, "Updating OStatus group $group->id from remote info $object->id: " . var_export($object, true) . var_export($hints, true));
|
||||
$group->update($orig);
|
||||
}
|
||||
}
|
||||
|
||||
protected static function getActivityObjectNickname($object, $hints=array())
|
||||
{
|
||||
if ($object->poco) {
|
||||
if (!empty($object->poco->preferredUsername)) {
|
||||
return common_nicknamize($object->poco->preferredUsername);
|
||||
}
|
||||
}
|
||||
if (!empty($object->nickname)) {
|
||||
return common_nicknamize($object->nickname);
|
||||
}
|
||||
|
||||
// Try the definitive ID
|
||||
|
||||
$nickname = self::nicknameFromURI($object->id);
|
||||
|
||||
// Try a Webfinger if one was passed (way) down
|
||||
|
||||
if (empty($nickname)) {
|
||||
if (array_key_exists('webfinger', $hints)) {
|
||||
$nickname = self::nicknameFromURI($hints['webfinger']);
|
||||
}
|
||||
}
|
||||
|
||||
// Try the name
|
||||
|
||||
if (empty($nickname)) {
|
||||
$nickname = common_nicknamize($object->title);
|
||||
}
|
||||
@@ -845,4 +1205,120 @@ class Ostatus_profile extends Memcached_DataObject
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static function ensureWebfinger($addr)
|
||||
{
|
||||
// First, look it up
|
||||
|
||||
$oprofile = Ostatus_profile::staticGet('uri', 'acct:'.$addr);
|
||||
|
||||
if (!empty($oprofile)) {
|
||||
return $oprofile;
|
||||
}
|
||||
|
||||
// Now, try some discovery
|
||||
|
||||
$wf = new Webfinger();
|
||||
|
||||
$result = $wf->lookup($addr);
|
||||
|
||||
if (!$result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($result->links as $link) {
|
||||
switch ($link['rel']) {
|
||||
case Webfinger::PROFILEPAGE:
|
||||
$profileUrl = $link['href'];
|
||||
break;
|
||||
case 'salmon':
|
||||
$salmonEndpoint = $link['href'];
|
||||
break;
|
||||
case Webfinger::UPDATESFROM:
|
||||
$feedUrl = $link['href'];
|
||||
break;
|
||||
default:
|
||||
common_log(LOG_NOTICE, "Don't know what to do with rel = '{$link['rel']}'");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$hints = array('webfinger' => $addr,
|
||||
'profileurl' => $profileUrl,
|
||||
'feedurl' => $feedUrl,
|
||||
'salmon' => $salmonEndpoint);
|
||||
|
||||
// If we got a feed URL, try that
|
||||
|
||||
if (isset($feedUrl)) {
|
||||
try {
|
||||
$oprofile = self::ensureProfile($feedUrl, $hints);
|
||||
return $oprofile;
|
||||
} catch (Exception $e) {
|
||||
common_log(LOG_WARNING, "Failed creating profile from feed URL '$feedUrl': " . $e->getMessage());
|
||||
// keep looking
|
||||
}
|
||||
}
|
||||
|
||||
// If we got a profile page, try that!
|
||||
|
||||
if (isset($profileUrl)) {
|
||||
try {
|
||||
$oprofile = self::ensureProfile($profileUrl, $hints);
|
||||
return $oprofile;
|
||||
} catch (Exception $e) {
|
||||
common_log(LOG_WARNING, "Failed creating profile from profile URL '$profileUrl': " . $e->getMessage());
|
||||
// keep looking
|
||||
}
|
||||
}
|
||||
|
||||
// XXX: try hcard
|
||||
// XXX: try FOAF
|
||||
|
||||
if (isset($salmonEndpoint)) {
|
||||
|
||||
// An account URL, a salmon endpoint, and a dream? Not much to go
|
||||
// on, but let's give it a try
|
||||
|
||||
$uri = 'acct:'.$addr;
|
||||
|
||||
$profile = new Profile();
|
||||
|
||||
$profile->nickname = self::nicknameFromUri($uri);
|
||||
$profile->created = common_sql_now();
|
||||
|
||||
if (isset($profileUrl)) {
|
||||
$profile->profileurl = $profileUrl;
|
||||
}
|
||||
|
||||
$profile_id = $profile->insert();
|
||||
|
||||
if (!$profile_id) {
|
||||
common_log_db_error($profile, 'INSERT', __FILE__);
|
||||
throw new Exception("Couldn't save profile for '$addr'");
|
||||
}
|
||||
|
||||
$oprofile = new Ostatus_profile();
|
||||
|
||||
$oprofile->uri = $uri;
|
||||
$oprofile->salmonuri = $salmonEndpoint;
|
||||
$oprofile->profile_id = $profile_id;
|
||||
$oprofile->created = common_sql_now();
|
||||
|
||||
if (isset($feedUrl)) {
|
||||
$profile->feeduri = $feedUrl;
|
||||
}
|
||||
|
||||
$result = $oprofile->insert();
|
||||
|
||||
if (!$result) {
|
||||
common_log_db_error($oprofile, 'INSERT', __FILE__);
|
||||
throw new Exception("Couldn't save ostatus_profile for '$addr'");
|
||||
}
|
||||
|
||||
return $oprofile;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
114
plugins/OStatus/classes/Ostatus_source.php
Normal file
114
plugins/OStatus/classes/Ostatus_source.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/*
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @package OStatusPlugin
|
||||
* @maintainer Brion Vibber <brion@status.net>
|
||||
*/
|
||||
|
||||
class Ostatus_source extends Memcached_DataObject
|
||||
{
|
||||
public $__table = 'ostatus_source';
|
||||
|
||||
public $notice_id; // notice we're referring to
|
||||
public $profile_uri; // uri of the ostatus_profile this came through -- may be a group feed
|
||||
public $method; // push or salmon
|
||||
|
||||
public /*static*/ function staticGet($k, $v=null)
|
||||
{
|
||||
return parent::staticGet(__CLASS__, $k, $v);
|
||||
}
|
||||
|
||||
/**
|
||||
* return table definition for DB_DataObject
|
||||
*
|
||||
* DB_DataObject needs to know something about the table to manipulate
|
||||
* instances. This method provides all the DB_DataObject needs to know.
|
||||
*
|
||||
* @return array array of column definitions
|
||||
*/
|
||||
|
||||
function table()
|
||||
{
|
||||
return array('notice_id' => DB_DATAOBJECT_INT + DB_DATAOBJECT_NOTNULL,
|
||||
'profile_uri' => DB_DATAOBJECT_STR,
|
||||
'method' => DB_DATAOBJECT_STR);
|
||||
}
|
||||
|
||||
static function schemaDef()
|
||||
{
|
||||
return array(new ColumnDef('notice_id', 'integer',
|
||||
null, false, 'PRI'),
|
||||
new ColumnDef('profile_uri', 'varchar',
|
||||
255, false),
|
||||
new ColumnDef('method', "ENUM('push','salmon')",
|
||||
null, false));
|
||||
}
|
||||
|
||||
/**
|
||||
* return key definitions for DB_DataObject
|
||||
*
|
||||
* DB_DataObject needs to know about keys that the table has; this function
|
||||
* defines them.
|
||||
*
|
||||
* @return array key definitions
|
||||
*/
|
||||
|
||||
function keys()
|
||||
{
|
||||
return array_keys($this->keyTypes());
|
||||
}
|
||||
|
||||
/**
|
||||
* return key definitions for Memcached_DataObject
|
||||
*
|
||||
* Our caching system uses the same key definitions, but uses a different
|
||||
* method to get them.
|
||||
*
|
||||
* @return array key definitions
|
||||
*/
|
||||
|
||||
function keyTypes()
|
||||
{
|
||||
return array('notice_id' => 'K');
|
||||
}
|
||||
|
||||
function sequenceKey()
|
||||
{
|
||||
return array(false, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a remote notice source record; this helps indicate how trusted we are.
|
||||
* @param string $method
|
||||
*/
|
||||
public static function saveNew(Notice $notice, Ostatus_profile $oprofile, $method)
|
||||
{
|
||||
$osource = new Ostatus_source();
|
||||
$osource->notice_id = $notice->id;
|
||||
$osource->profile_uri = $oprofile->uri;
|
||||
$osource->method = $method;
|
||||
if ($osource->insert()) {
|
||||
return true;
|
||||
} else {
|
||||
common_log_db_error($osource, 'INSERT', __FILE__);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
524
plugins/OStatus/extlib/Crypt/RSA.php
Normal file
524
plugins/OStatus/extlib/Crypt/RSA.php
Normal file
@@ -0,0 +1,524 @@
|
||||
<?php
|
||||
/**
|
||||
* Crypt_RSA allows to do following operations:
|
||||
* - key pair generation
|
||||
* - encryption and decryption
|
||||
* - signing and sign validation
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005, 2006 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version 1.2.0b
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
*/
|
||||
|
||||
/**
|
||||
* RSA error handling facilities
|
||||
*/
|
||||
require_once 'Crypt/RSA/ErrorHandler.php';
|
||||
|
||||
/**
|
||||
* loader for math wrappers
|
||||
*/
|
||||
require_once 'Crypt/RSA/MathLoader.php';
|
||||
|
||||
/**
|
||||
* helper class for mange single key
|
||||
*/
|
||||
require_once 'Crypt/RSA/Key.php';
|
||||
|
||||
/**
|
||||
* helper class for manage key pair
|
||||
*/
|
||||
require_once 'Crypt/RSA/KeyPair.php';
|
||||
|
||||
/**
|
||||
* Crypt_RSA class, derived from Crypt_RSA_ErrorHandler
|
||||
*
|
||||
* Provides the following functions:
|
||||
* - setParams($params) - sets parameters of current object
|
||||
* - encrypt($plain_data, $key = null) - encrypts data
|
||||
* - decrypt($enc_data, $key = null) - decrypts data
|
||||
* - createSign($doc, $private_key = null) - signs document by private key
|
||||
* - validateSign($doc, $signature, $public_key = null) - validates signature of document
|
||||
*
|
||||
* Example usage:
|
||||
* // creating an error handler
|
||||
* $error_handler = create_function('$obj', 'echo "error: ", $obj->getMessage(), "\n"');
|
||||
*
|
||||
* // 1024-bit key pair generation
|
||||
* $key_pair = new Crypt_RSA_KeyPair(1024);
|
||||
*
|
||||
* // check consistence of Crypt_RSA_KeyPair object
|
||||
* $error_handler($key_pair);
|
||||
*
|
||||
* // creating Crypt_RSA object
|
||||
* $rsa_obj = new Crypt_RSA;
|
||||
*
|
||||
* // check consistence of Crypt_RSA object
|
||||
* $error_handler($rsa_obj);
|
||||
*
|
||||
* // set error handler on Crypt_RSA object ( see Crypt/RSA/ErrorHandler.php for details )
|
||||
* $rsa_obj->setErrorHandler($error_handler);
|
||||
*
|
||||
* // encryption (usually using public key)
|
||||
* $enc_data = $rsa_obj->encrypt($plain_data, $key_pair->getPublicKey());
|
||||
*
|
||||
* // decryption (usually using private key)
|
||||
* $plain_data = $rsa_obj->decrypt($enc_data, $key_pair->getPrivateKey());
|
||||
*
|
||||
* // signing
|
||||
* $signature = $rsa_obj->createSign($document, $key_pair->getPrivateKey());
|
||||
*
|
||||
* // signature checking
|
||||
* $is_valid = $rsa_obj->validateSign($document, $signature, $key_pair->getPublicKey());
|
||||
*
|
||||
* // signing many documents by one private key
|
||||
* $rsa_obj = new Crypt_RSA(array('private_key' => $key_pair->getPrivateKey()));
|
||||
* // check consistence of Crypt_RSA object
|
||||
* $error_handler($rsa_obj);
|
||||
* // set error handler ( see Crypt/RSA/ErrorHandler.php for details )
|
||||
* $rsa_obj->setErrorHandler($error_handler);
|
||||
* // sign many documents
|
||||
* $sign_1 = $rsa_obj->sign($doc_1);
|
||||
* $sign_2 = $rsa_obj->sign($doc_2);
|
||||
* //...
|
||||
* $sign_n = $rsa_obj->sign($doc_n);
|
||||
*
|
||||
* // changing default hash function, which is used for sign
|
||||
* // creating/validation
|
||||
* $rsa_obj->setParams(array('hash_func' => 'md5'));
|
||||
*
|
||||
* // using factory() method instead of constructor (it returns PEAR_Error object on failure)
|
||||
* $rsa_obj = &Crypt_RSA::factory();
|
||||
* if (PEAR::isError($rsa_obj)) {
|
||||
* echo "error: ", $rsa_obj->getMessage(), "\n";
|
||||
* }
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005, 2006 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
* @version @package_version@
|
||||
* @access public
|
||||
*/
|
||||
class Crypt_RSA extends Crypt_RSA_ErrorHandler
|
||||
{
|
||||
/**
|
||||
* Reference to math wrapper, which is used to
|
||||
* manipulate large integers in RSA algorithm.
|
||||
*
|
||||
* @var object of Crypt_RSA_Math_* class
|
||||
* @access private
|
||||
*/
|
||||
var $_math_obj;
|
||||
|
||||
/**
|
||||
* key for encryption, which is used by encrypt() method
|
||||
*
|
||||
* @var object of Crypt_RSA_KEY class
|
||||
* @access private
|
||||
*/
|
||||
var $_enc_key;
|
||||
|
||||
/**
|
||||
* key for decryption, which is used by decrypt() method
|
||||
*
|
||||
* @var object of Crypt_RSA_KEY class
|
||||
* @access private
|
||||
*/
|
||||
var $_dec_key;
|
||||
|
||||
/**
|
||||
* public key, which is used by validateSign() method
|
||||
*
|
||||
* @var object of Crypt_RSA_KEY class
|
||||
* @access private
|
||||
*/
|
||||
var $_public_key;
|
||||
|
||||
/**
|
||||
* private key, which is used by createSign() method
|
||||
*
|
||||
* @var object of Crypt_RSA_KEY class
|
||||
* @access private
|
||||
*/
|
||||
var $_private_key;
|
||||
|
||||
/**
|
||||
* name of hash function, which is used by validateSign()
|
||||
* and createSign() methods. Default hash function is SHA-1
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_hash_func = 'sha1';
|
||||
|
||||
/**
|
||||
* Crypt_RSA constructor.
|
||||
*
|
||||
* @param array $params
|
||||
* Optional associative array of parameters, such as:
|
||||
* enc_key, dec_key, private_key, public_key, hash_func.
|
||||
* See setParams() method for more detailed description of
|
||||
* these parameters.
|
||||
* @param string $wrapper_name
|
||||
* Name of math wrapper, which will be used to
|
||||
* perform different operations with big integers.
|
||||
* See contents of Crypt/RSA/Math folder for examples of wrappers.
|
||||
* Read docs/Crypt_RSA/docs/math_wrappers.txt for details.
|
||||
* @param string $error_handler name of error handler function
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function Crypt_RSA($params = null, $wrapper_name = 'default', $error_handler = '')
|
||||
{
|
||||
// set error handler
|
||||
$this->setErrorHandler($error_handler);
|
||||
// try to load math wrapper
|
||||
$obj = &Crypt_RSA_MathLoader::loadWrapper($wrapper_name);
|
||||
if ($this->isError($obj)) {
|
||||
// error during loading of math wrapper
|
||||
// Crypt_RSA object is partially constructed.
|
||||
$this->pushError($obj);
|
||||
return;
|
||||
}
|
||||
$this->_math_obj = &$obj;
|
||||
|
||||
if (!is_null($params)) {
|
||||
if (!$this->setParams($params)) {
|
||||
// error in Crypt_RSA::setParams() function
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crypt_RSA factory.
|
||||
*
|
||||
* @param array $params
|
||||
* Optional associative array of parameters, such as:
|
||||
* enc_key, dec_key, private_key, public_key, hash_func.
|
||||
* See setParams() method for more detailed description of
|
||||
* these parameters.
|
||||
* @param string $wrapper_name
|
||||
* Name of math wrapper, which will be used to
|
||||
* perform different operations with big integers.
|
||||
* See contents of Crypt/RSA/Math folder for examples of wrappers.
|
||||
* Read docs/Crypt_RSA/docs/math_wrappers.txt for details.
|
||||
* @param string $error_handler name of error handler function
|
||||
*
|
||||
* @return object new Crypt_RSA object on success or PEAR_Error object on failure
|
||||
* @access public
|
||||
*/
|
||||
function &factory($params = null, $wrapper_name = 'default', $error_handler = '')
|
||||
{
|
||||
$obj = &new Crypt_RSA($params, $wrapper_name, $error_handler);
|
||||
if ($obj->isError()) {
|
||||
// error during creating a new object. Retrurn PEAR_Error object
|
||||
return $obj->getLastError();
|
||||
}
|
||||
// object created successfully. Return it
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Accepts any combination of available parameters as associative array:
|
||||
* enc_key - encryption key for encrypt() method
|
||||
* dec_key - decryption key for decrypt() method
|
||||
* public_key - key for validateSign() method
|
||||
* private_key - key for createSign() method
|
||||
* hash_func - name of hash function, which will be used to create and validate sign
|
||||
*
|
||||
* @param array $params
|
||||
* associative array of permitted parameters (see above)
|
||||
*
|
||||
* @return bool true on success or false on error
|
||||
* @access public
|
||||
*/
|
||||
function setParams($params)
|
||||
{
|
||||
if (!is_array($params)) {
|
||||
$this->pushError('parameters must be passed to function as associative array', CRYPT_RSA_ERROR_WRONG_PARAMS);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isset($params['enc_key'])) {
|
||||
if (Crypt_RSA_Key::isValid($params['enc_key'])) {
|
||||
$this->_enc_key = $params['enc_key'];
|
||||
}
|
||||
else {
|
||||
$this->pushError('wrong encryption key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (isset($params['dec_key'])) {
|
||||
if (Crypt_RSA_Key::isValid($params['dec_key'])) {
|
||||
$this->_dec_key = $params['dec_key'];
|
||||
}
|
||||
else {
|
||||
$this->pushError('wrong decryption key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (isset($params['private_key'])) {
|
||||
if (Crypt_RSA_Key::isValid($params['private_key'])) {
|
||||
if ($params['private_key']->getKeyType() != 'private') {
|
||||
$this->pushError('private key must have "private" attribute', CRYPT_RSA_ERROR_WRONG_KEY_TYPE);
|
||||
return false;
|
||||
}
|
||||
$this->_private_key = $params['private_key'];
|
||||
}
|
||||
else {
|
||||
$this->pushError('wrong private key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (isset($params['public_key'])) {
|
||||
if (Crypt_RSA_Key::isValid($params['public_key'])) {
|
||||
if ($params['public_key']->getKeyType() != 'public') {
|
||||
$this->pushError('public key must have "public" attribute', CRYPT_RSA_ERROR_WRONG_KEY_TYPE);
|
||||
return false;
|
||||
}
|
||||
$this->_public_key = $params['public_key'];
|
||||
}
|
||||
else {
|
||||
$this->pushError('wrong public key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (isset($params['hash_func'])) {
|
||||
if (!function_exists($params['hash_func'])) {
|
||||
$this->pushError('cannot find hash function with name [' . $params['hash_func'] . ']', CRYPT_RSA_ERROR_WRONG_HASH_FUNC);
|
||||
return false;
|
||||
}
|
||||
$this->_hash_func = $params['hash_func'];
|
||||
}
|
||||
return true; // all ok
|
||||
}
|
||||
|
||||
/**
|
||||
* Ecnrypts $plain_data by the key $this->_enc_key or $key.
|
||||
*
|
||||
* @param string $plain_data data, which must be encrypted
|
||||
* @param object $key encryption key (object of Crypt_RSA_Key class)
|
||||
* @return mixed
|
||||
* encrypted data as string on success or false on error
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function encrypt($plain_data, $key = null)
|
||||
{
|
||||
$enc_data = $this->encryptBinary($plain_data, $key);
|
||||
if ($enc_data !== false) {
|
||||
return base64_encode($enc_data);
|
||||
}
|
||||
// error during encripting data
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ecnrypts $plain_data by the key $this->_enc_key or $key.
|
||||
*
|
||||
* @param string $plain_data data, which must be encrypted
|
||||
* @param object $key encryption key (object of Crypt_RSA_Key class)
|
||||
* @return mixed
|
||||
* encrypted data as binary string on success or false on error
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function encryptBinary($plain_data, $key = null)
|
||||
{
|
||||
if (is_null($key)) {
|
||||
// use current encryption key
|
||||
$key = $this->_enc_key;
|
||||
}
|
||||
else if (!Crypt_RSA_Key::isValid($key)) {
|
||||
$this->pushError('invalid encryption key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY);
|
||||
return false;
|
||||
}
|
||||
|
||||
// append tail \x01 to plain data. It needs for correctly decrypting of data
|
||||
$plain_data .= "\x01";
|
||||
|
||||
$plain_data = $this->_math_obj->bin2int($plain_data);
|
||||
$exp = $this->_math_obj->bin2int($key->getExponent());
|
||||
$modulus = $this->_math_obj->bin2int($key->getModulus());
|
||||
|
||||
// divide plain data into chunks
|
||||
$data_len = $this->_math_obj->bitLen($plain_data);
|
||||
$chunk_len = $key->getKeyLength() - 1;
|
||||
$block_len = (int) ceil($chunk_len / 8);
|
||||
$curr_pos = 0;
|
||||
$enc_data = '';
|
||||
while ($curr_pos < $data_len) {
|
||||
$tmp = $this->_math_obj->subint($plain_data, $curr_pos, $chunk_len);
|
||||
$enc_data .= str_pad(
|
||||
$this->_math_obj->int2bin($this->_math_obj->powmod($tmp, $exp, $modulus)),
|
||||
$block_len,
|
||||
"\0"
|
||||
);
|
||||
$curr_pos += $chunk_len;
|
||||
}
|
||||
return $enc_data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts $enc_data by the key $this->_dec_key or $key.
|
||||
*
|
||||
* @param string $enc_data encrypted data as string
|
||||
* @param object $key decryption key (object of RSA_Crypt_Key class)
|
||||
* @return mixed
|
||||
* decrypted data as string on success or false on error
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function decrypt($enc_data, $key = null)
|
||||
{
|
||||
$enc_data = base64_decode($enc_data);
|
||||
return $this->decryptBinary($enc_data, $key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypts $enc_data by the key $this->_dec_key or $key.
|
||||
*
|
||||
* @param string $enc_data encrypted data as binary string
|
||||
* @param object $key decryption key (object of RSA_Crypt_Key class)
|
||||
* @return mixed
|
||||
* decrypted data as string on success or false on error
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function decryptBinary($enc_data, $key = null)
|
||||
{
|
||||
if (is_null($key)) {
|
||||
// use current decryption key
|
||||
$key = $this->_dec_key;
|
||||
}
|
||||
else if (!Crypt_RSA_Key::isValid($key)) {
|
||||
$this->pushError('invalid decryption key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY);
|
||||
return false;
|
||||
}
|
||||
|
||||
$exp = $this->_math_obj->bin2int($key->getExponent());
|
||||
$modulus = $this->_math_obj->bin2int($key->getModulus());
|
||||
|
||||
$data_len = strlen($enc_data);
|
||||
$chunk_len = $key->getKeyLength() - 1;
|
||||
$block_len = (int) ceil($chunk_len / 8);
|
||||
$curr_pos = 0;
|
||||
$bit_pos = 0;
|
||||
$plain_data = $this->_math_obj->bin2int("\0");
|
||||
while ($curr_pos < $data_len) {
|
||||
$tmp = $this->_math_obj->bin2int(substr($enc_data, $curr_pos, $block_len));
|
||||
$tmp = $this->_math_obj->powmod($tmp, $exp, $modulus);
|
||||
$plain_data = $this->_math_obj->bitOr($plain_data, $tmp, $bit_pos);
|
||||
$bit_pos += $chunk_len;
|
||||
$curr_pos += $block_len;
|
||||
}
|
||||
$result = $this->_math_obj->int2bin($plain_data);
|
||||
|
||||
// delete tail, containing of \x01
|
||||
$tail = ord($result{strlen($result) - 1});
|
||||
if ($tail != 1) {
|
||||
$this->pushError("Error tail of decrypted text = {$tail}. Expected 1", CRYPT_RSA_ERROR_WRONG_TAIL);
|
||||
return false;
|
||||
}
|
||||
return substr($result, 0, -1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates sign for document $document, using $this->_private_key or $private_key
|
||||
* as private key and $this->_hash_func or $hash_func as hash function.
|
||||
*
|
||||
* @param string $document document, which must be signed
|
||||
* @param object $private_key private key (object of Crypt_RSA_Key type)
|
||||
* @param string $hash_func name of hash function, which will be used during signing
|
||||
* @return mixed
|
||||
* signature of $document as string on success or false on error
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function createSign($document, $private_key = null, $hash_func = null)
|
||||
{
|
||||
// check private key
|
||||
if (is_null($private_key)) {
|
||||
$private_key = $this->_private_key;
|
||||
}
|
||||
else if (!Crypt_RSA_Key::isValid($private_key)) {
|
||||
$this->pushError('invalid private key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY);
|
||||
return false;
|
||||
}
|
||||
if ($private_key->getKeyType() != 'private') {
|
||||
$this->pushError('signing key must be private', CRYPT_RSA_ERROR_NEED_PRV_KEY);
|
||||
return false;
|
||||
}
|
||||
|
||||
// check hash_func
|
||||
if (is_null($hash_func)) {
|
||||
$hash_func = $this->_hash_func;
|
||||
}
|
||||
if (!function_exists($hash_func)) {
|
||||
$this->pushError("cannot find hash function with name [$hash_func]", CRYPT_RSA_ERROR_WRONG_HASH_FUNC);
|
||||
return false;
|
||||
}
|
||||
|
||||
return $this->encrypt($hash_func($document), $private_key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates $signature for document $document with public key $this->_public_key
|
||||
* or $public_key and hash function $this->_hash_func or $hash_func.
|
||||
*
|
||||
* @param string $document document, signature of which must be validated
|
||||
* @param string $signature signature, which must be validated
|
||||
* @param object $public_key public key (object of Crypt_RSA_Key class)
|
||||
* @param string $hash_func hash function, which will be used during validating signature
|
||||
* @return mixed
|
||||
* true, if signature of document is valid
|
||||
* false, if signature of document is invalid
|
||||
* null on error
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function validateSign($document, $signature, $public_key = null, $hash_func = null)
|
||||
{
|
||||
// check public key
|
||||
if (is_null($public_key)) {
|
||||
$public_key = $this->_public_key;
|
||||
}
|
||||
else if (!Crypt_RSA_Key::isValid($public_key)) {
|
||||
$this->pushError('invalid public key. It must be an object of Crypt_RSA_Key class', CRYPT_RSA_ERROR_WRONG_KEY);
|
||||
return null;
|
||||
}
|
||||
if ($public_key->getKeyType() != 'public') {
|
||||
$this->pushError('validating key must be public', CRYPT_RSA_ERROR_NEED_PUB_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
// check hash_func
|
||||
if (is_null($hash_func)) {
|
||||
$hash_func = $this->_hash_func;
|
||||
}
|
||||
if (!function_exists($hash_func)) {
|
||||
$this->pushError("cannot find hash function with name [$hash_func]", CRYPT_RSA_ERROR_WRONG_HASH_FUNC);
|
||||
return null;
|
||||
}
|
||||
|
||||
return $hash_func($document) == $this->decrypt($signature, $public_key);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
234
plugins/OStatus/extlib/Crypt/RSA/ErrorHandler.php
Normal file
234
plugins/OStatus/extlib/Crypt/RSA/ErrorHandler.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
/**
|
||||
* Crypt_RSA allows to do following operations:
|
||||
* - key pair generation
|
||||
* - encryption and decryption
|
||||
* - signing and sign validation
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id: ErrorHandler.php,v 1.4 2009/01/05 08:30:29 clockwerx Exp $
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
*/
|
||||
|
||||
/**
|
||||
* uses PEAR's error handling
|
||||
*/
|
||||
require_once 'PEAR.php';
|
||||
|
||||
/**
|
||||
* cannot load required extension for math wrapper
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_NO_EXT', 1);
|
||||
|
||||
/**
|
||||
* cannot load any math wrappers.
|
||||
* Possible reasons:
|
||||
* - there is no any wrappers (they must exist in Crypt/RSA/Math folder )
|
||||
* - all available wrappers are incorrect (read docs/Crypt_RSA/docs/math_wrappers.txt )
|
||||
* - cannot load any extension, required by available wrappers
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_NO_WRAPPERS', 2);
|
||||
|
||||
/**
|
||||
* cannot find file, containing requested math wrapper
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_NO_FILE', 3);
|
||||
|
||||
/**
|
||||
* cannot find math wrapper class in the math wrapper file
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_NO_CLASS', 4);
|
||||
|
||||
/**
|
||||
* invalid key type passed to function (it must be 'public' or 'private')
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_WRONG_KEY_TYPE', 5);
|
||||
|
||||
/**
|
||||
* key modulus must be greater than key exponent
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_EXP_GE_MOD', 6);
|
||||
|
||||
/**
|
||||
* missing $key_len parameter in Crypt_RSA_KeyPair::generate($key_len) function
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_MISSING_KEY_LEN', 7);
|
||||
|
||||
/**
|
||||
* wrong key object passed to function (it must be an object of Crypt_RSA_Key class)
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_WRONG_KEY', 8);
|
||||
|
||||
/**
|
||||
* wrong name of hash function passed to Crypt_RSA::setParams() function
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_WRONG_HASH_FUNC', 9);
|
||||
|
||||
/**
|
||||
* key, used for signing, must be private
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_NEED_PRV_KEY', 10);
|
||||
|
||||
/**
|
||||
* key, used for sign validating, must be public
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_NEED_PUB_KEY', 11);
|
||||
|
||||
/**
|
||||
* parameters must be passed to function as associative array
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_WRONG_PARAMS', 12);
|
||||
|
||||
/**
|
||||
* error tail of decrypted text. Maybe, wrong decryption key?
|
||||
*/
|
||||
define('CRYPT_RSA_ERROR_WRONG_TAIL', 13);
|
||||
|
||||
/**
|
||||
* Crypt_RSA_ErrorHandler class.
|
||||
*
|
||||
* This class is used as base for Crypt_RSA, Crypt_RSA_Key
|
||||
* and Crypt_RSA_KeyPair classes.
|
||||
*
|
||||
* It provides following functions:
|
||||
* - isError() - returns true, if list contains errors, else returns false
|
||||
* - getErrorList() - returns error list
|
||||
* - getLastError() - returns last error from error list or false, if list is empty
|
||||
* - pushError($errstr) - pushes $errstr into the error list
|
||||
* - setErrorHandler($new_error_handler) - sets error handler function
|
||||
* - getErrorHandler() - returns name of error handler function
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version Release: @package_version@
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
* @access public
|
||||
*/
|
||||
class Crypt_RSA_ErrorHandler
|
||||
{
|
||||
/**
|
||||
* array of error objects, pushed by $this->pushError()
|
||||
*
|
||||
* @var array
|
||||
* @access private
|
||||
*/
|
||||
var $_errors = array();
|
||||
|
||||
/**
|
||||
* name of error handler - function, which calls on $this->pushError() call
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_error_handler = '';
|
||||
|
||||
/**
|
||||
* Returns true if list of errors is not empty, else returns false
|
||||
*
|
||||
* @param mixed $err Check if the object is an error
|
||||
*
|
||||
* @return bool true, if list of errors is not empty or $err is PEAR_Error object, else false
|
||||
* @access public
|
||||
*/
|
||||
function isError($err = null)
|
||||
{
|
||||
return is_null($err) ? (sizeof($this->_errors) > 0) : PEAR::isError($err);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of all errors, pushed to error list by $this->pushError()
|
||||
*
|
||||
* @return array list of errors (usually it contains objects of PEAR_Error class)
|
||||
* @access public
|
||||
*/
|
||||
function getErrorList()
|
||||
{
|
||||
return $this->_errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns last error from errors list or false, if list is empty
|
||||
*
|
||||
* @return mixed
|
||||
* last error from errors list (usually it is PEAR_Error object)
|
||||
* or false, if list is empty.
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function getLastError()
|
||||
{
|
||||
$len = sizeof($this->_errors);
|
||||
return $len ? $this->_errors[$len - 1] : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* pushes error object $error to the error list
|
||||
*
|
||||
* @param string $errstr error string
|
||||
* @param int $errno error number
|
||||
*
|
||||
* @return bool true on success, false on error
|
||||
* @access public
|
||||
*/
|
||||
function pushError($errstr, $errno = 0)
|
||||
{
|
||||
$this->_errors[] = PEAR::raiseError($errstr, $errno);
|
||||
|
||||
if ($this->_error_handler != '') {
|
||||
// call user defined error handler
|
||||
$func = $this->_error_handler;
|
||||
$func($this);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* sets error handler to function with name $func_name.
|
||||
* Function $func_name must accept one parameter - current
|
||||
* object, which triggered error.
|
||||
*
|
||||
* @param string $func_name name of error handler function
|
||||
*
|
||||
* @return bool true on success, false on error
|
||||
* @access public
|
||||
*/
|
||||
function setErrorHandler($func_name = '')
|
||||
{
|
||||
if ($func_name == '') {
|
||||
$this->_error_handler = '';
|
||||
}
|
||||
if (!function_exists($func_name)) {
|
||||
return false;
|
||||
}
|
||||
$this->_error_handler = $func_name;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* returns name of current error handler, or null if there is no error handler
|
||||
*
|
||||
* @return mixed error handler name as string or null, if there is no error handler
|
||||
* @access public
|
||||
*/
|
||||
function getErrorHandler()
|
||||
{
|
||||
return $this->_error_handler;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
315
plugins/OStatus/extlib/Crypt/RSA/Key.php
Normal file
315
plugins/OStatus/extlib/Crypt/RSA/Key.php
Normal file
@@ -0,0 +1,315 @@
|
||||
<?php
|
||||
/**
|
||||
* Crypt_RSA allows to do following operations:
|
||||
* - key pair generation
|
||||
* - encryption and decryption
|
||||
* - signing and sign validation
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id: Key.php,v 1.6 2009/01/05 08:30:29 clockwerx Exp $
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
*/
|
||||
|
||||
/**
|
||||
* RSA error handling facilities
|
||||
*/
|
||||
require_once 'Crypt/RSA/ErrorHandler.php';
|
||||
|
||||
/**
|
||||
* loader for RSA math wrappers
|
||||
*/
|
||||
require_once 'Crypt/RSA/MathLoader.php';
|
||||
|
||||
/**
|
||||
* Crypt_RSA_Key class, derived from Crypt_RSA_ErrorHandler
|
||||
*
|
||||
* Provides the following functions:
|
||||
* - getKeyLength() - returns bit key length
|
||||
* - getExponent() - returns key exponent as binary string
|
||||
* - getModulus() - returns key modulus as binary string
|
||||
* - getKeyType() - returns type of the key (public or private)
|
||||
* - toString() - returns serialized key as string
|
||||
* - fromString($key_str) - static function; returns key, unserialized from string
|
||||
* - isValid($key) - static function for validating of $key
|
||||
*
|
||||
* Example usage:
|
||||
* // create new 1024-bit key pair
|
||||
* $key_pair = new Crypt_RSA_KeyPair(1024);
|
||||
*
|
||||
* // get public key (its class is Crypt_RSA_Key)
|
||||
* $key = $key_pair->getPublicKey();
|
||||
*
|
||||
* // get key length
|
||||
* $len = $key->getKeyLength();
|
||||
*
|
||||
* // get modulus as string
|
||||
* $modulus = $key->getModulus();
|
||||
*
|
||||
* // get exponent as string
|
||||
* $exponent = $key->getExponent();
|
||||
*
|
||||
* // get string represenation of key (use it instead of serialization of Crypt_RSA_Key object)
|
||||
* $key_in_str = $key->toString();
|
||||
*
|
||||
* // restore key object from string using 'BigInt' math wrapper
|
||||
* $key = Crypt_RSA_Key::fromString($key_in_str, 'BigInt');
|
||||
*
|
||||
* // error check
|
||||
* if ($key->isError()) {
|
||||
* echo "error while unserializing key object:\n";
|
||||
* $erorr = $key->getLastError();
|
||||
* echo $error->getMessage(), "\n";
|
||||
* }
|
||||
*
|
||||
* // validate key
|
||||
* if (Crypt_RSA_Key::isValid($key)) echo 'valid key';
|
||||
* else echo 'invalid key';
|
||||
*
|
||||
* // using factory() method instead of constructor (it returns PEAR_Error object on failure)
|
||||
* $rsa_obj = &Crypt_RSA_Key::factory($modulus, $exp, $key_type);
|
||||
* if (PEAR::isError($rsa_obj)) {
|
||||
* echo "error: ", $rsa_obj->getMessage(), "\n";
|
||||
* }
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version Release: @package_version@
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
* @access public
|
||||
*/
|
||||
class Crypt_RSA_Key extends Crypt_RSA_ErrorHandler
|
||||
{
|
||||
/**
|
||||
* Reference to math wrapper object, which is used to
|
||||
* manipulate large integers in RSA algorithm.
|
||||
*
|
||||
* @var object of Crypt_RSA_Math_* class
|
||||
* @access private
|
||||
*/
|
||||
var $_math_obj;
|
||||
|
||||
/**
|
||||
* shared modulus
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_modulus;
|
||||
|
||||
/**
|
||||
* exponent
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_exp;
|
||||
|
||||
/**
|
||||
* key type (private or public)
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_key_type;
|
||||
|
||||
/**
|
||||
* key length in bits
|
||||
*
|
||||
* @var int
|
||||
* @access private
|
||||
*/
|
||||
var $_key_len;
|
||||
|
||||
/**
|
||||
* Crypt_RSA_Key constructor.
|
||||
*
|
||||
* You should pass in the name of math wrapper, which will be used to
|
||||
* perform different operations with big integers.
|
||||
* See contents of Crypt/RSA/Math folder for examples of wrappers.
|
||||
* Read docs/Crypt_RSA/docs/math_wrappers.txt for details.
|
||||
*
|
||||
* @param string $modulus key modulus
|
||||
* @param string $exp key exponent
|
||||
* @param string $key_type type of the key (public or private)
|
||||
* @param string $wrapper_name wrapper to use
|
||||
* @param string $error_handler name of error handler function
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function Crypt_RSA_Key($modulus, $exp, $key_type, $wrapper_name = 'default', $error_handler = '')
|
||||
{
|
||||
// set error handler
|
||||
$this->setErrorHandler($error_handler);
|
||||
// try to load math wrapper $wrapper_name
|
||||
$obj = &Crypt_RSA_MathLoader::loadWrapper($wrapper_name);
|
||||
if ($this->isError($obj)) {
|
||||
// error during loading of math wrapper
|
||||
$this->pushError($obj); // push error object into error list
|
||||
return;
|
||||
}
|
||||
$this->_math_obj = &$obj;
|
||||
|
||||
$this->_modulus = $modulus;
|
||||
$this->_exp = $exp;
|
||||
|
||||
if (!in_array($key_type, array('private', 'public'))) {
|
||||
$this->pushError('invalid key type. It must be private or public', CRYPT_RSA_ERROR_WRONG_KEY_TYPE);
|
||||
return;
|
||||
}
|
||||
$this->_key_type = $key_type;
|
||||
|
||||
/* check length of modulus & exponent ( abs(modulus) > abs(exp) ) */
|
||||
$mod_num = $this->_math_obj->bin2int($this->_modulus);
|
||||
$exp_num = $this->_math_obj->bin2int($this->_exp);
|
||||
|
||||
if ($this->_math_obj->cmpAbs($mod_num, $exp_num) <= 0) {
|
||||
$this->pushError('modulus must be greater than exponent', CRYPT_RSA_ERROR_EXP_GE_MOD);
|
||||
return;
|
||||
}
|
||||
|
||||
// determine key length
|
||||
$this->_key_len = $this->_math_obj->bitLen($mod_num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crypt_RSA_Key factory.
|
||||
*
|
||||
* @param string $modulus key modulus
|
||||
* @param string $exp key exponent
|
||||
* @param string $key_type type of the key (public or private)
|
||||
* @param string $wrapper_name wrapper to use
|
||||
* @param string $error_handler name of error handler function
|
||||
*
|
||||
* @return object new Crypt_RSA_Key object on success or PEAR_Error object on failure
|
||||
* @access public
|
||||
*/
|
||||
function factory($modulus, $exp, $key_type, $wrapper_name = 'default', $error_handler = '')
|
||||
{
|
||||
$obj = new Crypt_RSA_Key($modulus, $exp, $key_type, $wrapper_name, $error_handler);
|
||||
if ($obj->isError()) {
|
||||
// error during creating a new object. Retrurn PEAR_Error object
|
||||
return $obj->getLastError();
|
||||
}
|
||||
// object created successfully. Return it
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bit length of the key
|
||||
*
|
||||
* @return int bit length of key
|
||||
* @access public
|
||||
*/
|
||||
function getKeyLength()
|
||||
{
|
||||
return $this->_key_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns modulus part of the key as binary string,
|
||||
* which can be used to construct new Crypt_RSA_Key object.
|
||||
*
|
||||
* @return string modulus as binary string
|
||||
* @access public
|
||||
*/
|
||||
function getModulus()
|
||||
{
|
||||
return $this->_modulus;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns exponent part of the key as binary string,
|
||||
* which can be used to construct new Crypt_RSA_Key object.
|
||||
*
|
||||
* @return string exponent as binary string
|
||||
* @access public
|
||||
*/
|
||||
function getExponent()
|
||||
{
|
||||
return $this->_exp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns key type (public, private)
|
||||
*
|
||||
* @return string key type (public, private)
|
||||
* @access public
|
||||
*/
|
||||
function getKeyType()
|
||||
{
|
||||
return $this->_key_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns string representation of key
|
||||
*
|
||||
* @return string key, serialized to string
|
||||
* @access public
|
||||
*/
|
||||
function toString()
|
||||
{
|
||||
return base64_encode(
|
||||
serialize(
|
||||
array($this->_modulus, $this->_exp, $this->_key_type)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns Crypt_RSA_Key object, unserialized from
|
||||
* string representation of key.
|
||||
*
|
||||
* optional parameter $wrapper_name - is the name of math wrapper,
|
||||
* which will be used during unserialization of this object.
|
||||
*
|
||||
* This function can be called statically:
|
||||
* $key = Crypt_RSA_Key::fromString($key_in_string, 'BigInt');
|
||||
*
|
||||
* @param string $key_str RSA key, serialized into string
|
||||
* @param string $wrapper_name optional math wrapper name
|
||||
*
|
||||
* @return object key as Crypt_RSA_Key object
|
||||
* @access public
|
||||
* @static
|
||||
*/
|
||||
function fromString($key_str, $wrapper_name = 'default')
|
||||
{
|
||||
list($modulus, $exponent, $key_type) = unserialize(base64_decode($key_str));
|
||||
$obj = new Crypt_RSA_Key($modulus, $exponent, $key_type, $wrapper_name);
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates key
|
||||
* This function can be called statically:
|
||||
* $is_valid = Crypt_RSA_Key::isValid($key)
|
||||
*
|
||||
* Returns true, if $key is valid Crypt_RSA key, else returns false
|
||||
*
|
||||
* @param object $key Crypt_RSA_Key object for validating
|
||||
*
|
||||
* @return bool true if $key is valid, else false
|
||||
* @access public
|
||||
*/
|
||||
function isValid($key)
|
||||
{
|
||||
return (is_object($key) && strtolower(get_class($key)) === strtolower(__CLASS__));
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
804
plugins/OStatus/extlib/Crypt/RSA/KeyPair.php
Normal file
804
plugins/OStatus/extlib/Crypt/RSA/KeyPair.php
Normal file
@@ -0,0 +1,804 @@
|
||||
<?php
|
||||
/**
|
||||
* Crypt_RSA allows to do following operations:
|
||||
* - key pair generation
|
||||
* - encryption and decryption
|
||||
* - signing and sign validation
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id: KeyPair.php,v 1.7 2009/01/05 08:30:29 clockwerx Exp $
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
*/
|
||||
|
||||
/**
|
||||
* RSA error handling facilities
|
||||
*/
|
||||
require_once 'Crypt/RSA/ErrorHandler.php';
|
||||
|
||||
/**
|
||||
* loader for RSA math wrappers
|
||||
*/
|
||||
require_once 'Crypt/RSA/MathLoader.php';
|
||||
|
||||
/**
|
||||
* helper class for single key managing
|
||||
*/
|
||||
require_once 'Crypt/RSA/Key.php';
|
||||
|
||||
/**
|
||||
* Crypt_RSA_KeyPair class, derived from Crypt_RSA_ErrorHandler
|
||||
*
|
||||
* Provides the following functions:
|
||||
* - generate($key) - generates new key pair
|
||||
* - getPublicKey() - returns public key
|
||||
* - getPrivateKey() - returns private key
|
||||
* - getKeyLength() - returns bit key length
|
||||
* - setRandomGenerator($func_name) - sets random generator to $func_name
|
||||
* - fromPEMString($str) - retrieves keypair from PEM-encoded string
|
||||
* - toPEMString() - stores keypair to PEM-encoded string
|
||||
* - isEqual($keypair2) - compares current keypair to $keypair2
|
||||
*
|
||||
* Example usage:
|
||||
* // create new 1024-bit key pair
|
||||
* $key_pair = new Crypt_RSA_KeyPair(1024);
|
||||
*
|
||||
* // error check
|
||||
* if ($key_pair->isError()) {
|
||||
* echo "error while initializing Crypt_RSA_KeyPair object:\n";
|
||||
* $erorr = $key_pair->getLastError();
|
||||
* echo $error->getMessage(), "\n";
|
||||
* }
|
||||
*
|
||||
* // get public key
|
||||
* $public_key = $key_pair->getPublicKey();
|
||||
*
|
||||
* // get private key
|
||||
* $private_key = $key_pair->getPrivateKey();
|
||||
*
|
||||
* // generate new 512-bit key pair
|
||||
* $key_pair->generate(512);
|
||||
*
|
||||
* // error check
|
||||
* if ($key_pair->isError()) {
|
||||
* echo "error while generating key pair:\n";
|
||||
* $erorr = $key_pair->getLastError();
|
||||
* echo $error->getMessage(), "\n";
|
||||
* }
|
||||
*
|
||||
* // get key pair length
|
||||
* $length = $key_pair->getKeyLength();
|
||||
*
|
||||
* // set random generator to $func_name, where $func_name
|
||||
* // consists name of random generator function. See comments
|
||||
* // before setRandomGenerator() method for details
|
||||
* $key_pair->setRandomGenerator($func_name);
|
||||
*
|
||||
* // error check
|
||||
* if ($key_pair->isError()) {
|
||||
* echo "error while changing random generator:\n";
|
||||
* $erorr = $key_pair->getLastError();
|
||||
* echo $error->getMessage(), "\n";
|
||||
* }
|
||||
*
|
||||
* // using factory() method instead of constructor (it returns PEAR_Error object on failure)
|
||||
* $rsa_obj = &Crypt_RSA_KeyPair::factory($key_len);
|
||||
* if (PEAR::isError($rsa_obj)) {
|
||||
* echo "error: ", $rsa_obj->getMessage(), "\n";
|
||||
* }
|
||||
*
|
||||
* // read key pair from PEM-encoded string:
|
||||
* $str = "-----BEGIN RSA PRIVATE KEY-----"
|
||||
* . "MCsCAQACBHr5LDkCAwEAAQIEBc6jbQIDAOCfAgMAjCcCAk3pAgJMawIDAL41"
|
||||
* . "-----END RSA PRIVATE KEY-----";
|
||||
* $keypair = Crypt_RSA_KeyPair::fromPEMString($str);
|
||||
*
|
||||
* // read key pair from .pem file 'private.pem':
|
||||
* $str = file_get_contents('private.pem');
|
||||
* $keypair = Crypt_RSA_KeyPair::fromPEMString($str);
|
||||
*
|
||||
* // generate and write 1024-bit key pair to .pem file 'private_new.pem'
|
||||
* $keypair = new Crypt_RSA_KeyPair(1024);
|
||||
* $str = $keypair->toPEMString();
|
||||
* file_put_contents('private_new.pem', $str);
|
||||
*
|
||||
* // compare $keypair1 to $keypair2
|
||||
* if ($keypair1->isEqual($keypair2)) {
|
||||
* echo "keypair1 = keypair2\n";
|
||||
* }
|
||||
* else {
|
||||
* echo "keypair1 != keypair2\n";
|
||||
* }
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version Release: @package_version@
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
* @access public
|
||||
*/
|
||||
class Crypt_RSA_KeyPair extends Crypt_RSA_ErrorHandler
|
||||
{
|
||||
/**
|
||||
* Reference to math wrapper object, which is used to
|
||||
* manipulate large integers in RSA algorithm.
|
||||
*
|
||||
* @var object of Crypt_RSA_Math_* class
|
||||
* @access private
|
||||
*/
|
||||
var $_math_obj;
|
||||
|
||||
/**
|
||||
* length of each key in the key pair
|
||||
*
|
||||
* @var int
|
||||
* @access private
|
||||
*/
|
||||
var $_key_len;
|
||||
|
||||
/**
|
||||
* public key
|
||||
*
|
||||
* @var object of Crypt_RSA_KEY class
|
||||
* @access private
|
||||
*/
|
||||
var $_public_key;
|
||||
|
||||
/**
|
||||
* private key
|
||||
*
|
||||
* @var object of Crypt_RSA_KEY class
|
||||
* @access private
|
||||
*/
|
||||
var $_private_key;
|
||||
|
||||
/**
|
||||
* name of function, which is used as random generator
|
||||
*
|
||||
* @var string
|
||||
* @access private
|
||||
*/
|
||||
var $_random_generator;
|
||||
|
||||
/**
|
||||
* RSA keypair attributes [version, n, e, d, p, q, dmp1, dmq1, iqmp] as associative array
|
||||
*
|
||||
* @var array
|
||||
* @access private
|
||||
*/
|
||||
var $_attrs;
|
||||
|
||||
/**
|
||||
* Returns names of keypair attributes from $this->_attrs array
|
||||
*
|
||||
* @return array Array of keypair attributes names
|
||||
* @access private
|
||||
*/
|
||||
function _get_attr_names()
|
||||
{
|
||||
return array('version', 'n', 'e', 'd', 'p', 'q', 'dmp1', 'dmq1', 'iqmp');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses ASN.1 string [$str] starting form position [$pos].
|
||||
* Returns tag and string value of parsed object.
|
||||
*
|
||||
* @param string $str
|
||||
* @param int &$pos
|
||||
* @param Crypt_RSA_ErrorHandler &$err_handler
|
||||
*
|
||||
* @return mixed Array('tag' => ..., 'str' => ...) on success, false on error
|
||||
* @access private
|
||||
*/
|
||||
function _ASN1Parse($str, &$pos, &$err_handler)
|
||||
{
|
||||
$max_pos = strlen($str);
|
||||
if ($max_pos < 2) {
|
||||
$err_handler->pushError("ASN.1 string too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
// get ASN.1 tag value
|
||||
$tag = ord($str[$pos++]) & 0x1f;
|
||||
if ($tag == 0x1f) {
|
||||
$tag = 0;
|
||||
do {
|
||||
$n = ord($str[$pos++]);
|
||||
$tag <<= 7;
|
||||
$tag |= $n & 0x7f;
|
||||
} while (($n & 0x80) && $pos < $max_pos);
|
||||
}
|
||||
if ($pos >= $max_pos) {
|
||||
$err_handler->pushError("ASN.1 string too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
// get ASN.1 object length
|
||||
$len = ord($str[$pos++]);
|
||||
if ($len & 0x80) {
|
||||
$n = $len & 0x1f;
|
||||
$len = 0;
|
||||
while ($n-- && $pos < $max_pos) {
|
||||
$len <<= 8;
|
||||
$len |= ord($str[$pos++]);
|
||||
}
|
||||
}
|
||||
if ($pos >= $max_pos || $len > $max_pos - $pos) {
|
||||
$err_handler->pushError("ASN.1 string too short");
|
||||
return false;
|
||||
}
|
||||
|
||||
// get string value of ASN.1 object
|
||||
$str = substr($str, $pos, $len);
|
||||
|
||||
return array(
|
||||
'tag' => $tag,
|
||||
'str' => $str,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses ASN.1 sting [$str] starting from position [$pos].
|
||||
* Returns string representation of number, which can be passed
|
||||
* in bin2int() function of math wrapper.
|
||||
*
|
||||
* @param string $str
|
||||
* @param int &$pos
|
||||
* @param Crypt_RSA_ErrorHandler &$err_handler
|
||||
*
|
||||
* @return mixed string representation of parsed number on success, false on error
|
||||
* @access private
|
||||
*/
|
||||
function _ASN1ParseInt($str, &$pos, &$err_handler)
|
||||
{
|
||||
$tmp = Crypt_RSA_KeyPair::_ASN1Parse($str, $pos, $err_handler);
|
||||
if ($err_handler->isError()) {
|
||||
return false;
|
||||
}
|
||||
if ($tmp['tag'] != 0x02) {
|
||||
$errstr = sprintf("wrong ASN tag value: 0x%02x. Expected 0x02 (INTEGER)", $tmp['tag']);
|
||||
$err_handler->pushError($errstr);
|
||||
return false;
|
||||
}
|
||||
$pos += strlen($tmp['str']);
|
||||
|
||||
return strrev($tmp['str']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ASN.1 string from tag $tag and object $str
|
||||
*
|
||||
* @param string $str ASN.1 object string
|
||||
* @param int $tag ASN.1 tag value
|
||||
* @param bool $is_constructed
|
||||
* @param bool $is_private
|
||||
*
|
||||
* @return ASN.1-encoded string
|
||||
* @access private
|
||||
*/
|
||||
function _ASN1Store($str, $tag, $is_constructed = false, $is_private = false)
|
||||
{
|
||||
$out = '';
|
||||
|
||||
// encode ASN.1 tag value
|
||||
$tag_ext = ($is_constructed ? 0x20 : 0) | ($is_private ? 0xc0 : 0);
|
||||
if ($tag < 0x1f) {
|
||||
$out .= chr($tag | $tag_ext);
|
||||
} else {
|
||||
$out .= chr($tag_ext | 0x1f);
|
||||
$tmp = chr($tag & 0x7f);
|
||||
$tag >>= 7;
|
||||
while ($tag) {
|
||||
$tmp .= chr(($tag & 0x7f) | 0x80);
|
||||
$tag >>= 7;
|
||||
}
|
||||
$out .= strrev($tmp);
|
||||
}
|
||||
|
||||
// encode ASN.1 object length
|
||||
$len = strlen($str);
|
||||
if ($len < 0x7f) {
|
||||
$out .= chr($len);
|
||||
} else {
|
||||
$tmp = '';
|
||||
$n = 0;
|
||||
while ($len) {
|
||||
$tmp .= chr($len & 0xff);
|
||||
$len >>= 8;
|
||||
$n++;
|
||||
}
|
||||
$out .= chr($n | 0x80);
|
||||
$out .= strrev($tmp);
|
||||
}
|
||||
|
||||
return $out . $str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs ASN.1 string from binary representation of big integer
|
||||
*
|
||||
* @param string $str binary representation of big integer
|
||||
*
|
||||
* @return ASN.1-encoded string
|
||||
* @access private
|
||||
*/
|
||||
function _ASN1StoreInt($str)
|
||||
{
|
||||
$str = strrev($str);
|
||||
return Crypt_RSA_KeyPair::_ASN1Store($str, 0x02);
|
||||
}
|
||||
|
||||
/**
|
||||
* Crypt_RSA_KeyPair constructor.
|
||||
*
|
||||
* Wrapper: name of math wrapper, which will be used to
|
||||
* perform different operations with big integers.
|
||||
* See contents of Crypt/RSA/Math folder for examples of wrappers.
|
||||
* Read docs/Crypt_RSA/docs/math_wrappers.txt for details.
|
||||
*
|
||||
* @param int $key_len bit length of key pair, which will be generated in constructor
|
||||
* @param string $wrapper_name wrapper name
|
||||
* @param string $error_handler name of error handler function
|
||||
* @param callback $random_generator function which will be used as random generator
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function Crypt_RSA_KeyPair($key_len, $wrapper_name = 'default', $error_handler = '', $random_generator = null)
|
||||
{
|
||||
// set error handler
|
||||
$this->setErrorHandler($error_handler);
|
||||
// try to load math wrapper
|
||||
$obj = &Crypt_RSA_MathLoader::loadWrapper($wrapper_name);
|
||||
if ($this->isError($obj)) {
|
||||
// error during loading of math wrapper
|
||||
$this->pushError($obj);
|
||||
return;
|
||||
}
|
||||
$this->_math_obj = &$obj;
|
||||
|
||||
// set random generator
|
||||
if (!$this->setRandomGenerator($random_generator)) {
|
||||
// error in setRandomGenerator() function
|
||||
return;
|
||||
}
|
||||
|
||||
if (is_array($key_len)) {
|
||||
// ugly BC hack - it is possible to pass RSA private key attributes [version, n, e, d, p, q, dmp1, dmq1, iqmp]
|
||||
// as associative array instead of key length to Crypt_RSA_KeyPair constructor
|
||||
$rsa_attrs = $key_len;
|
||||
|
||||
// convert attributes to big integers
|
||||
$attr_names = $this->_get_attr_names();
|
||||
foreach ($attr_names as $attr) {
|
||||
if (!isset($rsa_attrs[$attr])) {
|
||||
$this->pushError("missing required RSA attribute [$attr]");
|
||||
return;
|
||||
}
|
||||
${$attr} = $this->_math_obj->bin2int($rsa_attrs[$attr]);
|
||||
}
|
||||
|
||||
// check primality of p and q
|
||||
if (!$this->_math_obj->isPrime($p)) {
|
||||
$this->pushError("[p] must be prime");
|
||||
return;
|
||||
}
|
||||
if (!$this->_math_obj->isPrime($q)) {
|
||||
$this->pushError("[q] must be prime");
|
||||
return;
|
||||
}
|
||||
|
||||
// check n = p * q
|
||||
$n1 = $this->_math_obj->mul($p, $q);
|
||||
if ($this->_math_obj->cmpAbs($n, $n1)) {
|
||||
$this->pushError("n != p * q");
|
||||
return;
|
||||
}
|
||||
|
||||
// check e * d = 1 mod (p-1) * (q-1)
|
||||
$p1 = $this->_math_obj->dec($p);
|
||||
$q1 = $this->_math_obj->dec($q);
|
||||
$p1q1 = $this->_math_obj->mul($p1, $q1);
|
||||
$ed = $this->_math_obj->mul($e, $d);
|
||||
$one = $this->_math_obj->mod($ed, $p1q1);
|
||||
if (!$this->_math_obj->isOne($one)) {
|
||||
$this->pushError("e * d != 1 mod (p-1)*(q-1)");
|
||||
return;
|
||||
}
|
||||
|
||||
// check dmp1 = d mod (p-1)
|
||||
$dmp = $this->_math_obj->mod($d, $p1);
|
||||
if ($this->_math_obj->cmpAbs($dmp, $dmp1)) {
|
||||
$this->pushError("dmp1 != d mod (p-1)");
|
||||
return;
|
||||
}
|
||||
|
||||
// check dmq1 = d mod (q-1)
|
||||
$dmq = $this->_math_obj->mod($d, $q1);
|
||||
if ($this->_math_obj->cmpAbs($dmq, $dmq1)) {
|
||||
$this->pushError("dmq1 != d mod (q-1)");
|
||||
return;
|
||||
}
|
||||
|
||||
// check iqmp = 1/q mod p
|
||||
$q1 = $this->_math_obj->invmod($iqmp, $p);
|
||||
if ($this->_math_obj->cmpAbs($q, $q1)) {
|
||||
$this->pushError("iqmp != 1/q mod p");
|
||||
return;
|
||||
}
|
||||
|
||||
// try to create public key object
|
||||
$public_key = &new Crypt_RSA_Key($rsa_attrs['n'], $rsa_attrs['e'], 'public', $wrapper_name, $error_handler);
|
||||
if ($public_key->isError()) {
|
||||
// error during creating public object
|
||||
$this->pushError($public_key->getLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
// try to create private key object
|
||||
$private_key = &new Crypt_RSA_Key($rsa_attrs['n'], $rsa_attrs['d'], 'private', $wrapper_name, $error_handler);
|
||||
if ($private_key->isError()) {
|
||||
// error during creating private key object
|
||||
$this->pushError($private_key->getLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->_public_key = $public_key;
|
||||
$this->_private_key = $private_key;
|
||||
$this->_key_len = $public_key->getKeyLength();
|
||||
$this->_attrs = $rsa_attrs;
|
||||
} else {
|
||||
// generate key pair
|
||||
if (!$this->generate($key_len)) {
|
||||
// error during generating key pair
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Crypt_RSA_KeyPair factory.
|
||||
*
|
||||
* Wrapper - Name of math wrapper, which will be used to
|
||||
* perform different operations with big integers.
|
||||
* See contents of Crypt/RSA/Math folder for examples of wrappers.
|
||||
* Read docs/Crypt_RSA/docs/math_wrappers.txt for details.
|
||||
*
|
||||
* @param int $key_len bit length of key pair, which will be generated in constructor
|
||||
* @param string $wrapper_name wrapper name
|
||||
* @param string $error_handler name of error handler function
|
||||
* @param callback $random_generator function which will be used as random generator
|
||||
*
|
||||
* @return object new Crypt_RSA_KeyPair object on success or PEAR_Error object on failure
|
||||
* @access public
|
||||
*/
|
||||
function &factory($key_len, $wrapper_name = 'default', $error_handler = '', $random_generator = null)
|
||||
{
|
||||
$obj = &new Crypt_RSA_KeyPair($key_len, $wrapper_name, $error_handler, $random_generator);
|
||||
if ($obj->isError()) {
|
||||
// error during creating a new object. Return PEAR_Error object
|
||||
return $obj->getLastError();
|
||||
}
|
||||
// object created successfully. Return it
|
||||
return $obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates new Crypt_RSA key pair with length $key_len.
|
||||
* If $key_len is missed, use an old key length from $this->_key_len
|
||||
*
|
||||
* @param int $key_len bit length of key pair, which will be generated
|
||||
*
|
||||
* @return bool true on success or false on error
|
||||
* @access public
|
||||
*/
|
||||
function generate($key_len = null)
|
||||
{
|
||||
if (is_null($key_len)) {
|
||||
// use an old key length
|
||||
$key_len = $this->_key_len;
|
||||
if (is_null($key_len)) {
|
||||
$this->pushError('missing key_len parameter', CRYPT_RSA_ERROR_MISSING_KEY_LEN);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// minimal key length is 8 bit ;)
|
||||
if ($key_len < 8) {
|
||||
$key_len = 8;
|
||||
}
|
||||
// store key length in the _key_len property
|
||||
$this->_key_len = $key_len;
|
||||
|
||||
// set [e] to 0x10001 (65537)
|
||||
$e = $this->_math_obj->bin2int("\x01\x00\x01");
|
||||
|
||||
// generate [p], [q] and [n]
|
||||
$p_len = intval(($key_len + 1) / 2);
|
||||
$q_len = $key_len - $p_len;
|
||||
$p1 = $q1 = 0;
|
||||
do {
|
||||
// generate prime number [$p] with length [$p_len] with the following condition:
|
||||
// GCD($e, $p - 1) = 1
|
||||
do {
|
||||
$p = $this->_math_obj->getPrime($p_len, $this->_random_generator);
|
||||
$p1 = $this->_math_obj->dec($p);
|
||||
$tmp = $this->_math_obj->GCD($e, $p1);
|
||||
} while (!$this->_math_obj->isOne($tmp));
|
||||
// generate prime number [$q] with length [$q_len] with the following conditions:
|
||||
// GCD($e, $q - 1) = 1
|
||||
// $q != $p
|
||||
do {
|
||||
$q = $this->_math_obj->getPrime($q_len, $this->_random_generator);
|
||||
$q1 = $this->_math_obj->dec($q);
|
||||
$tmp = $this->_math_obj->GCD($e, $q1);
|
||||
} while (!$this->_math_obj->isOne($tmp) && !$this->_math_obj->cmpAbs($q, $p));
|
||||
// if (p < q), then exchange them
|
||||
if ($this->_math_obj->cmpAbs($p, $q) < 0) {
|
||||
$tmp = $p;
|
||||
$p = $q;
|
||||
$q = $tmp;
|
||||
$tmp = $p1;
|
||||
$p1 = $q1;
|
||||
$q1 = $tmp;
|
||||
}
|
||||
// calculate n = p * q
|
||||
$n = $this->_math_obj->mul($p, $q);
|
||||
} while ($this->_math_obj->bitLen($n) != $key_len);
|
||||
|
||||
// calculate d = 1/e mod (p - 1) * (q - 1)
|
||||
$pq = $this->_math_obj->mul($p1, $q1);
|
||||
$d = $this->_math_obj->invmod($e, $pq);
|
||||
|
||||
// calculate dmp1 = d mod (p - 1)
|
||||
$dmp1 = $this->_math_obj->mod($d, $p1);
|
||||
|
||||
// calculate dmq1 = d mod (q - 1)
|
||||
$dmq1 = $this->_math_obj->mod($d, $q1);
|
||||
|
||||
// calculate iqmp = 1/q mod p
|
||||
$iqmp = $this->_math_obj->invmod($q, $p);
|
||||
|
||||
// store RSA keypair attributes
|
||||
$this->_attrs = array(
|
||||
'version' => "\x00",
|
||||
'n' => $this->_math_obj->int2bin($n),
|
||||
'e' => $this->_math_obj->int2bin($e),
|
||||
'd' => $this->_math_obj->int2bin($d),
|
||||
'p' => $this->_math_obj->int2bin($p),
|
||||
'q' => $this->_math_obj->int2bin($q),
|
||||
'dmp1' => $this->_math_obj->int2bin($dmp1),
|
||||
'dmq1' => $this->_math_obj->int2bin($dmq1),
|
||||
'iqmp' => $this->_math_obj->int2bin($iqmp),
|
||||
);
|
||||
|
||||
$n = $this->_attrs['n'];
|
||||
$e = $this->_attrs['e'];
|
||||
$d = $this->_attrs['d'];
|
||||
|
||||
// try to create public key object
|
||||
$obj = &new Crypt_RSA_Key($n, $e, 'public', $this->_math_obj->getWrapperName(), $this->_error_handler);
|
||||
if ($obj->isError()) {
|
||||
// error during creating public object
|
||||
$this->pushError($obj->getLastError());
|
||||
return false;
|
||||
}
|
||||
$this->_public_key = &$obj;
|
||||
|
||||
// try to create private key object
|
||||
$obj = &new Crypt_RSA_Key($n, $d, 'private', $this->_math_obj->getWrapperName(), $this->_error_handler);
|
||||
if ($obj->isError()) {
|
||||
// error during creating private key object
|
||||
$this->pushError($obj->getLastError());
|
||||
return false;
|
||||
}
|
||||
$this->_private_key = &$obj;
|
||||
|
||||
return true; // key pair successfully generated
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns public key from the pair
|
||||
*
|
||||
* @return object public key object of class Crypt_RSA_Key
|
||||
* @access public
|
||||
*/
|
||||
function getPublicKey()
|
||||
{
|
||||
return $this->_public_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns private key from the pair
|
||||
*
|
||||
* @return object private key object of class Crypt_RSA_Key
|
||||
* @access public
|
||||
*/
|
||||
function getPrivateKey()
|
||||
{
|
||||
return $this->_private_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets name of random generator function for key generation.
|
||||
* If parameter is skipped, then sets to default random generator.
|
||||
*
|
||||
* Random generator function must return integer with at least 8 lower
|
||||
* significant bits, which will be used as random values.
|
||||
*
|
||||
* @param string $random_generator name of random generator function
|
||||
*
|
||||
* @return bool true on success or false on error
|
||||
* @access public
|
||||
*/
|
||||
function setRandomGenerator($random_generator = null)
|
||||
{
|
||||
static $default_random_generator = null;
|
||||
|
||||
if (is_string($random_generator)) {
|
||||
// set user's random generator
|
||||
if (!function_exists($random_generator)) {
|
||||
$this->pushError("can't find random generator function with name [{$random_generator}]");
|
||||
return false;
|
||||
}
|
||||
$this->_random_generator = $random_generator;
|
||||
} else {
|
||||
// set default random generator
|
||||
$this->_random_generator = is_null($default_random_generator) ?
|
||||
($default_random_generator = create_function('', '$a=explode(" ",microtime());return(int)($a[0]*1000000);')) :
|
||||
$default_random_generator;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns length of each key in the key pair
|
||||
*
|
||||
* @return int bit length of each key in key pair
|
||||
* @access public
|
||||
*/
|
||||
function getKeyLength()
|
||||
{
|
||||
return $this->_key_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves RSA keypair from PEM-encoded string, containing RSA private key.
|
||||
* Example of such string:
|
||||
* -----BEGIN RSA PRIVATE KEY-----
|
||||
* MCsCAQACBHtvbSECAwEAAQIEeYrk3QIDAOF3AgMAjCcCAmdnAgJMawIDALEk
|
||||
* -----END RSA PRIVATE KEY-----
|
||||
*
|
||||
* Wrapper: Name of math wrapper, which will be used to
|
||||
* perform different operations with big integers.
|
||||
* See contents of Crypt/RSA/Math folder for examples of wrappers.
|
||||
* Read docs/Crypt_RSA/docs/math_wrappers.txt for details.
|
||||
*
|
||||
* @param string $str PEM-encoded string
|
||||
* @param string $wrapper_name Wrapper name
|
||||
* @param string $error_handler name of error handler function
|
||||
*
|
||||
* @return Crypt_RSA_KeyPair object on success, PEAR_Error object on error
|
||||
* @access public
|
||||
* @static
|
||||
*/
|
||||
function &fromPEMString($str, $wrapper_name = 'default', $error_handler = '')
|
||||
{
|
||||
if (isset($this)) {
|
||||
if ($wrapper_name == 'default') {
|
||||
$wrapper_name = $this->_math_obj->getWrapperName();
|
||||
}
|
||||
if ($error_handler == '') {
|
||||
$error_handler = $this->_error_handler;
|
||||
}
|
||||
}
|
||||
$err_handler = &new Crypt_RSA_ErrorHandler;
|
||||
$err_handler->setErrorHandler($error_handler);
|
||||
|
||||
// search for base64-encoded private key
|
||||
if (!preg_match('/-----BEGIN RSA PRIVATE KEY-----([^-]+)-----END RSA PRIVATE KEY-----/', $str, $matches)) {
|
||||
$err_handler->pushError("can't find RSA private key in the string [{$str}]");
|
||||
return $err_handler->getLastError();
|
||||
}
|
||||
|
||||
// parse private key. It is ASN.1-encoded
|
||||
$str = base64_decode($matches[1]);
|
||||
$pos = 0;
|
||||
$tmp = Crypt_RSA_KeyPair::_ASN1Parse($str, $pos, $err_handler);
|
||||
if ($err_handler->isError()) {
|
||||
return $err_handler->getLastError();
|
||||
}
|
||||
if ($tmp['tag'] != 0x10) {
|
||||
$errstr = sprintf("wrong ASN tag value: 0x%02x. Expected 0x10 (SEQUENCE)", $tmp['tag']);
|
||||
$err_handler->pushError($errstr);
|
||||
return $err_handler->getLastError();
|
||||
}
|
||||
|
||||
// parse ASN.1 SEQUENCE for RSA private key
|
||||
$attr_names = Crypt_RSA_KeyPair::_get_attr_names();
|
||||
$n = sizeof($attr_names);
|
||||
$rsa_attrs = array();
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
$tmp = Crypt_RSA_KeyPair::_ASN1ParseInt($str, $pos, $err_handler);
|
||||
if ($err_handler->isError()) {
|
||||
return $err_handler->getLastError();
|
||||
}
|
||||
$attr = $attr_names[$i];
|
||||
$rsa_attrs[$attr] = $tmp;
|
||||
}
|
||||
|
||||
// create Crypt_RSA_KeyPair object.
|
||||
$keypair = &new Crypt_RSA_KeyPair($rsa_attrs, $wrapper_name, $error_handler);
|
||||
if ($keypair->isError()) {
|
||||
return $keypair->getLastError();
|
||||
}
|
||||
|
||||
return $keypair;
|
||||
}
|
||||
|
||||
/**
|
||||
* converts keypair to PEM-encoded string, which can be stroed in
|
||||
* .pem compatible files, contianing RSA private key.
|
||||
*
|
||||
* @return string PEM-encoded keypair on success, false on error
|
||||
* @access public
|
||||
*/
|
||||
function toPEMString()
|
||||
{
|
||||
// store RSA private key attributes into ASN.1 string
|
||||
$str = '';
|
||||
$attr_names = $this->_get_attr_names();
|
||||
$n = sizeof($attr_names);
|
||||
$rsa_attrs = $this->_attrs;
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
$attr = $attr_names[$i];
|
||||
if (!isset($rsa_attrs[$attr])) {
|
||||
$this->pushError("Cannot find value for ASN.1 attribute [$attr]");
|
||||
return false;
|
||||
}
|
||||
$tmp = $rsa_attrs[$attr];
|
||||
$str .= Crypt_RSA_KeyPair::_ASN1StoreInt($tmp);
|
||||
}
|
||||
|
||||
// prepend $str by ASN.1 SEQUENCE (0x10) header
|
||||
$str = Crypt_RSA_KeyPair::_ASN1Store($str, 0x10, true);
|
||||
|
||||
// encode and format PEM string
|
||||
$str = base64_encode($str);
|
||||
$str = chunk_split($str, 64, "\n");
|
||||
return "-----BEGIN RSA PRIVATE KEY-----\n$str-----END RSA PRIVATE KEY-----\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares keypairs in Crypt_RSA_KeyPair objects $this and $key_pair
|
||||
*
|
||||
* @param Crypt_RSA_KeyPair $key_pair keypair to compare
|
||||
*
|
||||
* @return bool true, if keypair stored in $this equal to keypair stored in $key_pair
|
||||
* @access public
|
||||
*/
|
||||
function isEqual($key_pair)
|
||||
{
|
||||
$attr_names = $this->_get_attr_names();
|
||||
foreach ($attr_names as $attr) {
|
||||
if ($this->_attrs[$attr] != $key_pair->_attrs[$attr]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
482
plugins/OStatus/extlib/Crypt/RSA/Math/BCMath.php
Normal file
482
plugins/OStatus/extlib/Crypt/RSA/Math/BCMath.php
Normal file
@@ -0,0 +1,482 @@
|
||||
<?php
|
||||
/**
|
||||
* Crypt_RSA allows to do following operations:
|
||||
* - key pair generation
|
||||
* - encryption and decryption
|
||||
* - signing and sign validation
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2006 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version 1.2.0b
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
*/
|
||||
|
||||
/**
|
||||
* Crypt_RSA_Math_BCMath class.
|
||||
*
|
||||
* Provides set of math functions, which are used by Crypt_RSA package
|
||||
* This class is a wrapper for PHP BCMath extension.
|
||||
* See http://php.net/manual/en/ref.bc.php for details.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005, 2006 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
* @version @package_version@
|
||||
* @access public
|
||||
*/
|
||||
class Crypt_RSA_Math_BCMath
|
||||
{
|
||||
/**
|
||||
* error description
|
||||
*
|
||||
* @var string
|
||||
* @access public
|
||||
*/
|
||||
var $errstr = '';
|
||||
|
||||
/**
|
||||
* Performs Miller-Rabin primality test for number $num
|
||||
* with base $base. Returns true, if $num is strong pseudoprime
|
||||
* by base $base. Else returns false.
|
||||
*
|
||||
* @param string $num
|
||||
* @param string $base
|
||||
* @return bool
|
||||
* @access private
|
||||
*/
|
||||
function _millerTest($num, $base)
|
||||
{
|
||||
if (!bccomp($num, '1')) {
|
||||
// 1 is not prime ;)
|
||||
return false;
|
||||
}
|
||||
$tmp = bcsub($num, '1');
|
||||
|
||||
$zero_bits = 0;
|
||||
while (!bccomp(bcmod($tmp, '2'), '0')) {
|
||||
$zero_bits++;
|
||||
$tmp = bcdiv($tmp, '2');
|
||||
}
|
||||
|
||||
$tmp = $this->powmod($base, $tmp, $num);
|
||||
if (!bccomp($tmp, '1')) {
|
||||
// $num is probably prime
|
||||
return true;
|
||||
}
|
||||
|
||||
while ($zero_bits--) {
|
||||
if (!bccomp(bcadd($tmp, '1'), $num)) {
|
||||
// $num is probably prime
|
||||
return true;
|
||||
}
|
||||
$tmp = $this->powmod($tmp, '2', $num);
|
||||
}
|
||||
// $num is composite
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Crypt_RSA_Math_BCMath constructor.
|
||||
* Checks an existance of PHP BCMath extension.
|
||||
* On failure saves error description in $this->errstr
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function Crypt_RSA_Math_BCMath()
|
||||
{
|
||||
if (!extension_loaded('bcmath')) {
|
||||
if (!@dl('bcmath.' . PHP_SHLIB_SUFFIX) && !@dl('php_bcmath.' . PHP_SHLIB_SUFFIX)) {
|
||||
// cannot load BCMath extension. Set error string
|
||||
$this->errstr = 'Crypt_RSA package requires the BCMath extension. See http://php.net/manual/en/ref.bc.php for details';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms binary representation of large integer into its native form.
|
||||
*
|
||||
* Example of transformation:
|
||||
* $str = "\x12\x34\x56\x78\x90";
|
||||
* $num = 0x9078563412;
|
||||
*
|
||||
* @param string $str
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function bin2int($str)
|
||||
{
|
||||
$result = '0';
|
||||
$n = strlen($str);
|
||||
do {
|
||||
$result = bcadd(bcmul($result, '256'), ord($str{--$n}));
|
||||
} while ($n > 0);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms large integer into binary representation.
|
||||
*
|
||||
* Example of transformation:
|
||||
* $num = 0x9078563412;
|
||||
* $str = "\x12\x34\x56\x78\x90";
|
||||
*
|
||||
* @param string $num
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function int2bin($num)
|
||||
{
|
||||
$result = '';
|
||||
do {
|
||||
$result .= chr(bcmod($num, '256'));
|
||||
$num = bcdiv($num, '256');
|
||||
} while (bccomp($num, '0'));
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates pow($num, $pow) (mod $mod)
|
||||
*
|
||||
* @param string $num
|
||||
* @param string $pow
|
||||
* @param string $mod
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function powmod($num, $pow, $mod)
|
||||
{
|
||||
if (function_exists('bcpowmod')) {
|
||||
// bcpowmod is only available under PHP5
|
||||
return bcpowmod($num, $pow, $mod);
|
||||
}
|
||||
|
||||
// emulate bcpowmod
|
||||
$result = '1';
|
||||
do {
|
||||
if (!bccomp(bcmod($pow, '2'), '1')) {
|
||||
$result = bcmod(bcmul($result, $num), $mod);
|
||||
}
|
||||
$num = bcmod(bcpow($num, '2'), $mod);
|
||||
$pow = bcdiv($pow, '2');
|
||||
} while (bccomp($pow, '0'));
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates $num1 * $num2
|
||||
*
|
||||
* @param string $num1
|
||||
* @param string $num2
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function mul($num1, $num2)
|
||||
{
|
||||
return bcmul($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates $num1 % $num2
|
||||
*
|
||||
* @param string $num1
|
||||
* @param string $num2
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function mod($num1, $num2)
|
||||
{
|
||||
return bcmod($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares abs($num1) to abs($num2).
|
||||
* Returns:
|
||||
* -1, if abs($num1) < abs($num2)
|
||||
* 0, if abs($num1) == abs($num2)
|
||||
* 1, if abs($num1) > abs($num2)
|
||||
*
|
||||
* @param string $num1
|
||||
* @param string $num2
|
||||
* @return int
|
||||
* @access public
|
||||
*/
|
||||
function cmpAbs($num1, $num2)
|
||||
{
|
||||
return bccomp($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests $num on primality. Returns true, if $num is strong pseudoprime.
|
||||
* Else returns false.
|
||||
*
|
||||
* @param string $num
|
||||
* @return bool
|
||||
* @access private
|
||||
*/
|
||||
function isPrime($num)
|
||||
{
|
||||
static $primes = null;
|
||||
static $primes_cnt = 0;
|
||||
if (is_null($primes)) {
|
||||
// generate all primes up to 10000
|
||||
$primes = array();
|
||||
for ($i = 0; $i < 10000; $i++) {
|
||||
$primes[] = $i;
|
||||
}
|
||||
$primes[0] = $primes[1] = 0;
|
||||
for ($i = 2; $i < 100; $i++) {
|
||||
while (!$primes[$i]) {
|
||||
$i++;
|
||||
}
|
||||
$j = $i;
|
||||
for ($j += $i; $j < 10000; $j += $i) {
|
||||
$primes[$j] = 0;
|
||||
}
|
||||
}
|
||||
$j = 0;
|
||||
for ($i = 0; $i < 10000; $i++) {
|
||||
if ($primes[$i]) {
|
||||
$primes[$j++] = $primes[$i];
|
||||
}
|
||||
}
|
||||
$primes_cnt = $j;
|
||||
}
|
||||
|
||||
// try to divide number by small primes
|
||||
for ($i = 0; $i < $primes_cnt; $i++) {
|
||||
if (bccomp($num, $primes[$i]) <= 0) {
|
||||
// number is prime
|
||||
return true;
|
||||
}
|
||||
if (!bccomp(bcmod($num, $primes[$i]), '0')) {
|
||||
// number divides by $primes[$i]
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
try Miller-Rabin's probable-primality test for first
|
||||
7 primes as bases
|
||||
*/
|
||||
for ($i = 0; $i < 7; $i++) {
|
||||
if (!$this->_millerTest($num, $primes[$i])) {
|
||||
// $num is composite
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// $num is strong pseudoprime
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates prime number with length $bits_cnt
|
||||
* using $random_generator as random generator function.
|
||||
*
|
||||
* @param int $bits_cnt
|
||||
* @param string $rnd_generator
|
||||
* @access public
|
||||
*/
|
||||
function getPrime($bits_cnt, $random_generator)
|
||||
{
|
||||
$bytes_n = intval($bits_cnt / 8);
|
||||
$bits_n = $bits_cnt % 8;
|
||||
do {
|
||||
$str = '';
|
||||
for ($i = 0; $i < $bytes_n; $i++) {
|
||||
$str .= chr(call_user_func($random_generator) & 0xff);
|
||||
}
|
||||
$n = call_user_func($random_generator) & 0xff;
|
||||
$n |= 0x80;
|
||||
$n >>= 8 - $bits_n;
|
||||
$str .= chr($n);
|
||||
$num = $this->bin2int($str);
|
||||
|
||||
// search for the next closest prime number after [$num]
|
||||
if (!bccomp(bcmod($num, '2'), '0')) {
|
||||
$num = bcadd($num, '1');
|
||||
}
|
||||
while (!$this->isPrime($num)) {
|
||||
$num = bcadd($num, '2');
|
||||
}
|
||||
} while ($this->bitLen($num) != $bits_cnt);
|
||||
return $num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates $num - 1
|
||||
*
|
||||
* @param string $num
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function dec($num)
|
||||
{
|
||||
return bcsub($num, '1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if $num is equal to one. Else returns false
|
||||
*
|
||||
* @param string $num
|
||||
* @return bool
|
||||
* @access public
|
||||
*/
|
||||
function isOne($num)
|
||||
{
|
||||
return !bccomp($num, '1');
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds greatest common divider (GCD) of $num1 and $num2
|
||||
*
|
||||
* @param string $num1
|
||||
* @param string $num2
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function GCD($num1, $num2)
|
||||
{
|
||||
do {
|
||||
$tmp = bcmod($num1, $num2);
|
||||
$num1 = $num2;
|
||||
$num2 = $tmp;
|
||||
} while (bccomp($num2, '0'));
|
||||
return $num1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds inverse number $inv for $num by modulus $mod, such as:
|
||||
* $inv * $num = 1 (mod $mod)
|
||||
*
|
||||
* @param string $num
|
||||
* @param string $mod
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function invmod($num, $mod)
|
||||
{
|
||||
$x = '1';
|
||||
$y = '0';
|
||||
$num1 = $mod;
|
||||
do {
|
||||
$tmp = bcmod($num, $num1);
|
||||
$q = bcdiv($num, $num1);
|
||||
$num = $num1;
|
||||
$num1 = $tmp;
|
||||
|
||||
$tmp = bcsub($x, bcmul($y, $q));
|
||||
$x = $y;
|
||||
$y = $tmp;
|
||||
} while (bccomp($num1, '0'));
|
||||
if (bccomp($x, '0') < 0) {
|
||||
$x = bcadd($x, $mod);
|
||||
}
|
||||
return $x;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bit length of number $num
|
||||
*
|
||||
* @param string $num
|
||||
* @return int
|
||||
* @access public
|
||||
*/
|
||||
function bitLen($num)
|
||||
{
|
||||
$tmp = $this->int2bin($num);
|
||||
$bit_len = strlen($tmp) * 8;
|
||||
$tmp = ord($tmp{strlen($tmp) - 1});
|
||||
if (!$tmp) {
|
||||
$bit_len -= 8;
|
||||
}
|
||||
else {
|
||||
while (!($tmp & 0x80)) {
|
||||
$bit_len--;
|
||||
$tmp <<= 1;
|
||||
}
|
||||
}
|
||||
return $bit_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise or of $num1 and $num2,
|
||||
* starting from bit $start_pos for number $num1
|
||||
*
|
||||
* @param string $num1
|
||||
* @param string $num2
|
||||
* @param int $start_pos
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function bitOr($num1, $num2, $start_pos)
|
||||
{
|
||||
$start_byte = intval($start_pos / 8);
|
||||
$start_bit = $start_pos % 8;
|
||||
$tmp1 = $this->int2bin($num1);
|
||||
|
||||
$num2 = bcmul($num2, 1 << $start_bit);
|
||||
$tmp2 = $this->int2bin($num2);
|
||||
if ($start_byte < strlen($tmp1)) {
|
||||
$tmp2 |= substr($tmp1, $start_byte);
|
||||
$tmp1 = substr($tmp1, 0, $start_byte) . $tmp2;
|
||||
}
|
||||
else {
|
||||
$tmp1 = str_pad($tmp1, $start_byte, "\0") . $tmp2;
|
||||
}
|
||||
return $this->bin2int($tmp1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns part of number $num, starting at bit
|
||||
* position $start with length $length
|
||||
*
|
||||
* @param string $num
|
||||
* @param int start
|
||||
* @param int length
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function subint($num, $start, $length)
|
||||
{
|
||||
$start_byte = intval($start / 8);
|
||||
$start_bit = $start % 8;
|
||||
$byte_length = intval($length / 8);
|
||||
$bit_length = $length % 8;
|
||||
if ($bit_length) {
|
||||
$byte_length++;
|
||||
}
|
||||
$num = bcdiv($num, 1 << $start_bit);
|
||||
$tmp = substr($this->int2bin($num), $start_byte, $byte_length);
|
||||
$tmp = str_pad($tmp, $byte_length, "\0");
|
||||
$tmp = substr_replace($tmp, $tmp{$byte_length - 1} & chr(0xff >> (8 - $bit_length)), $byte_length - 1, 1);
|
||||
return $this->bin2int($tmp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns name of current wrapper
|
||||
*
|
||||
* @return string name of current wrapper
|
||||
* @access public
|
||||
*/
|
||||
function getWrapperName()
|
||||
{
|
||||
return 'BCMath';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
313
plugins/OStatus/extlib/Crypt/RSA/Math/BigInt.php
Normal file
313
plugins/OStatus/extlib/Crypt/RSA/Math/BigInt.php
Normal file
@@ -0,0 +1,313 @@
|
||||
<?php
|
||||
/**
|
||||
* Crypt_RSA allows to do following operations:
|
||||
* - key pair generation
|
||||
* - encryption and decryption
|
||||
* - signing and sign validation
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005, 2006 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version 1.2.0b
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
*/
|
||||
|
||||
/**
|
||||
* Crypt_RSA_Math_BigInt class.
|
||||
*
|
||||
* Provides set of math functions, which are used by Crypt_RSA package
|
||||
* This class is a wrapper for big_int PECL extension,
|
||||
* which could be loaded from http://pecl.php.net/packages/big_int
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005, 2006 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
* @version @package_version@
|
||||
* @access public
|
||||
*/
|
||||
class Crypt_RSA_Math_BigInt
|
||||
{
|
||||
/**
|
||||
* error description
|
||||
*
|
||||
* @var string
|
||||
* @access public
|
||||
*/
|
||||
var $errstr = '';
|
||||
|
||||
/**
|
||||
* Crypt_RSA_Math_BigInt constructor.
|
||||
* Checks an existance of big_int PECL math package.
|
||||
* This package is available at http://pecl.php.net/packages/big_int
|
||||
* On failure saves error description in $this->errstr
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function Crypt_RSA_Math_BigInt()
|
||||
{
|
||||
if (!extension_loaded('big_int')) {
|
||||
if (!@dl('big_int.' . PHP_SHLIB_SUFFIX) && !@dl('php_big_int.' . PHP_SHLIB_SUFFIX)) {
|
||||
// cannot load big_int extension
|
||||
$this->errstr = 'Crypt_RSA package requires big_int PECL package. ' .
|
||||
'It is available at http://pecl.php.net/packages/big_int';
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// check version of big_int extension ( Crypt_RSA requires version 1.0.2 and higher )
|
||||
if (!in_array('bi_info', get_extension_funcs('big_int'))) {
|
||||
// there is no bi_info() function in versions, older than 1.0.2
|
||||
$this->errstr = 'Crypt_RSA package requires big_int package version 1.0.2 and higher';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms binary representation of large integer into its native form.
|
||||
*
|
||||
* Example of transformation:
|
||||
* $str = "\x12\x34\x56\x78\x90";
|
||||
* $num = 0x9078563412;
|
||||
*
|
||||
* @param string $str
|
||||
* @return big_int resource
|
||||
* @access public
|
||||
*/
|
||||
function bin2int($str)
|
||||
{
|
||||
return bi_unserialize($str);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms large integer into binary representation.
|
||||
*
|
||||
* Example of transformation:
|
||||
* $num = 0x9078563412;
|
||||
* $str = "\x12\x34\x56\x78\x90";
|
||||
*
|
||||
* @param big_int resource $num
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function int2bin($num)
|
||||
{
|
||||
return bi_serialize($num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates pow($num, $pow) (mod $mod)
|
||||
*
|
||||
* @param big_int resource $num
|
||||
* @param big_int resource $pow
|
||||
* @param big_int resource $mod
|
||||
* @return big_int resource
|
||||
* @access public
|
||||
*/
|
||||
function powmod($num, $pow, $mod)
|
||||
{
|
||||
return bi_powmod($num, $pow, $mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates $num1 * $num2
|
||||
*
|
||||
* @param big_int resource $num1
|
||||
* @param big_int resource $num2
|
||||
* @return big_int resource
|
||||
* @access public
|
||||
*/
|
||||
function mul($num1, $num2)
|
||||
{
|
||||
return bi_mul($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates $num1 % $num2
|
||||
*
|
||||
* @param string $num1
|
||||
* @param string $num2
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function mod($num1, $num2)
|
||||
{
|
||||
return bi_mod($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares abs($num1) to abs($num2).
|
||||
* Returns:
|
||||
* -1, if abs($num1) < abs($num2)
|
||||
* 0, if abs($num1) == abs($num2)
|
||||
* 1, if abs($num1) > abs($num2)
|
||||
*
|
||||
* @param big_int resource $num1
|
||||
* @param big_int resource $num2
|
||||
* @return int
|
||||
* @access public
|
||||
*/
|
||||
function cmpAbs($num1, $num2)
|
||||
{
|
||||
return bi_cmp_abs($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests $num on primality. Returns true, if $num is strong pseudoprime.
|
||||
* Else returns false.
|
||||
*
|
||||
* @param string $num
|
||||
* @return bool
|
||||
* @access private
|
||||
*/
|
||||
function isPrime($num)
|
||||
{
|
||||
return bi_is_prime($num) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates prime number with length $bits_cnt
|
||||
* using $random_generator as random generator function.
|
||||
*
|
||||
* @param int $bits_cnt
|
||||
* @param string $rnd_generator
|
||||
* @access public
|
||||
*/
|
||||
function getPrime($bits_cnt, $random_generator)
|
||||
{
|
||||
$bytes_n = intval($bits_cnt / 8);
|
||||
$bits_n = $bits_cnt % 8;
|
||||
do {
|
||||
$str = '';
|
||||
for ($i = 0; $i < $bytes_n; $i++) {
|
||||
$str .= chr(call_user_func($random_generator) & 0xff);
|
||||
}
|
||||
$n = call_user_func($random_generator) & 0xff;
|
||||
$n |= 0x80;
|
||||
$n >>= 8 - $bits_n;
|
||||
$str .= chr($n);
|
||||
$num = $this->bin2int($str);
|
||||
|
||||
// search for the next closest prime number after [$num]
|
||||
$num = bi_next_prime($num);
|
||||
} while ($this->bitLen($num) != $bits_cnt);
|
||||
return $num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates $num - 1
|
||||
*
|
||||
* @param big_int resource $num
|
||||
* @return big_int resource
|
||||
* @access public
|
||||
*/
|
||||
function dec($num)
|
||||
{
|
||||
return bi_dec($num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if $num is equal to 1. Else returns false
|
||||
*
|
||||
* @param big_int resource $num
|
||||
* @return bool
|
||||
* @access public
|
||||
*/
|
||||
function isOne($num)
|
||||
{
|
||||
return bi_is_one($num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds greatest common divider (GCD) of $num1 and $num2
|
||||
*
|
||||
* @param big_int resource $num1
|
||||
* @param big_int resource $num2
|
||||
* @return big_int resource
|
||||
* @access public
|
||||
*/
|
||||
function GCD($num1, $num2)
|
||||
{
|
||||
return bi_gcd($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds inverse number $inv for $num by modulus $mod, such as:
|
||||
* $inv * $num = 1 (mod $mod)
|
||||
*
|
||||
* @param big_int resource $num
|
||||
* @param big_int resource $mod
|
||||
* @return big_int resource
|
||||
* @access public
|
||||
*/
|
||||
function invmod($num, $mod)
|
||||
{
|
||||
return bi_invmod($num, $mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bit length of number $num
|
||||
*
|
||||
* @param big_int resource $num
|
||||
* @return int
|
||||
* @access public
|
||||
*/
|
||||
function bitLen($num)
|
||||
{
|
||||
return bi_bit_len($num);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise or of $num1 and $num2,
|
||||
* starting from bit $start_pos for number $num1
|
||||
*
|
||||
* @param big_int resource $num1
|
||||
* @param big_int resource $num2
|
||||
* @param int $start_pos
|
||||
* @return big_int resource
|
||||
* @access public
|
||||
*/
|
||||
function bitOr($num1, $num2, $start_pos)
|
||||
{
|
||||
return bi_or($num1, $num2, $start_pos);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns part of number $num, starting at bit
|
||||
* position $start with length $length
|
||||
*
|
||||
* @param big_int resource $num
|
||||
* @param int start
|
||||
* @param int length
|
||||
* @return big_int resource
|
||||
* @access public
|
||||
*/
|
||||
function subint($num, $start, $length)
|
||||
{
|
||||
return bi_subint($num, $start, $length);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns name of current wrapper
|
||||
*
|
||||
* @return string name of current wrapper
|
||||
* @access public
|
||||
*/
|
||||
function getWrapperName()
|
||||
{
|
||||
return 'BigInt';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
361
plugins/OStatus/extlib/Crypt/RSA/Math/GMP.php
Normal file
361
plugins/OStatus/extlib/Crypt/RSA/Math/GMP.php
Normal file
@@ -0,0 +1,361 @@
|
||||
<?php
|
||||
/**
|
||||
* Crypt_RSA allows to do following operations:
|
||||
* - key pair generation
|
||||
* - encryption and decryption
|
||||
* - signing and sign validation
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005, 2006 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version 1.2.0b
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
*/
|
||||
|
||||
/**
|
||||
* Crypt_RSA_Math_GMP class.
|
||||
*
|
||||
* Provides set of math functions, which are used by Crypt_RSA package
|
||||
* This class is a wrapper for PHP GMP extension.
|
||||
* See http://php.net/gmp for details.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright 2005, 2006 Alexander Valyalkin
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
* @version @package_version@
|
||||
* @access public
|
||||
*/
|
||||
class Crypt_RSA_Math_GMP
|
||||
{
|
||||
/**
|
||||
* error description
|
||||
*
|
||||
* @var string
|
||||
* @access public
|
||||
*/
|
||||
var $errstr = '';
|
||||
|
||||
/**
|
||||
* Crypt_RSA_Math_GMP constructor.
|
||||
* Checks an existance of PHP GMP package.
|
||||
* See http://php.net/gmp for details.
|
||||
*
|
||||
* On failure saves error description in $this->errstr
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function Crypt_RSA_Math_GMP()
|
||||
{
|
||||
if (!extension_loaded('gmp')) {
|
||||
if (!@dl('gmp.' . PHP_SHLIB_SUFFIX) && !@dl('php_gmp.' . PHP_SHLIB_SUFFIX)) {
|
||||
// cannot load GMP extension
|
||||
$this->errstr = 'Crypt_RSA package requires PHP GMP package. ' .
|
||||
'See http://php.net/gmp for details';
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms binary representation of large integer into its native form.
|
||||
*
|
||||
* Example of transformation:
|
||||
* $str = "\x12\x34\x56\x78\x90";
|
||||
* $num = 0x9078563412;
|
||||
*
|
||||
* @param string $str
|
||||
* @return gmp resource
|
||||
* @access public
|
||||
*/
|
||||
function bin2int($str)
|
||||
{
|
||||
$result = 0;
|
||||
$n = strlen($str);
|
||||
do {
|
||||
// dirty hack: GMP returns FALSE, when second argument equals to int(0).
|
||||
// so, it must be converted to string '0'
|
||||
$result = gmp_add(gmp_mul($result, 256), strval(ord($str{--$n})));
|
||||
} while ($n > 0);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms large integer into binary representation.
|
||||
*
|
||||
* Example of transformation:
|
||||
* $num = 0x9078563412;
|
||||
* $str = "\x12\x34\x56\x78\x90";
|
||||
*
|
||||
* @param gmp resource $num
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function int2bin($num)
|
||||
{
|
||||
$result = '';
|
||||
do {
|
||||
$result .= chr(gmp_intval(gmp_mod($num, 256)));
|
||||
$num = gmp_div($num, 256);
|
||||
} while (gmp_cmp($num, 0));
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates pow($num, $pow) (mod $mod)
|
||||
*
|
||||
* @param gmp resource $num
|
||||
* @param gmp resource $pow
|
||||
* @param gmp resource $mod
|
||||
* @return gmp resource
|
||||
* @access public
|
||||
*/
|
||||
function powmod($num, $pow, $mod)
|
||||
{
|
||||
return gmp_powm($num, $pow, $mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates $num1 * $num2
|
||||
*
|
||||
* @param gmp resource $num1
|
||||
* @param gmp resource $num2
|
||||
* @return gmp resource
|
||||
* @access public
|
||||
*/
|
||||
function mul($num1, $num2)
|
||||
{
|
||||
return gmp_mul($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates $num1 % $num2
|
||||
*
|
||||
* @param string $num1
|
||||
* @param string $num2
|
||||
* @return string
|
||||
* @access public
|
||||
*/
|
||||
function mod($num1, $num2)
|
||||
{
|
||||
return gmp_mod($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares abs($num1) to abs($num2).
|
||||
* Returns:
|
||||
* -1, if abs($num1) < abs($num2)
|
||||
* 0, if abs($num1) == abs($num2)
|
||||
* 1, if abs($num1) > abs($num2)
|
||||
*
|
||||
* @param gmp resource $num1
|
||||
* @param gmp resource $num2
|
||||
* @return int
|
||||
* @access public
|
||||
*/
|
||||
function cmpAbs($num1, $num2)
|
||||
{
|
||||
return gmp_cmp($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests $num on primality. Returns true, if $num is strong pseudoprime.
|
||||
* Else returns false.
|
||||
*
|
||||
* @param string $num
|
||||
* @return bool
|
||||
* @access private
|
||||
*/
|
||||
function isPrime($num)
|
||||
{
|
||||
return gmp_prob_prime($num) ? true : false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates prime number with length $bits_cnt
|
||||
* using $random_generator as random generator function.
|
||||
*
|
||||
* @param int $bits_cnt
|
||||
* @param string $rnd_generator
|
||||
* @access public
|
||||
*/
|
||||
function getPrime($bits_cnt, $random_generator)
|
||||
{
|
||||
$bytes_n = intval($bits_cnt / 8);
|
||||
$bits_n = $bits_cnt % 8;
|
||||
do {
|
||||
$str = '';
|
||||
for ($i = 0; $i < $bytes_n; $i++) {
|
||||
$str .= chr(call_user_func($random_generator) & 0xff);
|
||||
}
|
||||
$n = call_user_func($random_generator) & 0xff;
|
||||
$n |= 0x80;
|
||||
$n >>= 8 - $bits_n;
|
||||
$str .= chr($n);
|
||||
$num = $this->bin2int($str);
|
||||
|
||||
// search for the next closest prime number after [$num]
|
||||
if (!gmp_cmp(gmp_mod($num, '2'), '0')) {
|
||||
$num = gmp_add($num, '1');
|
||||
}
|
||||
while (!gmp_prob_prime($num)) {
|
||||
$num = gmp_add($num, '2');
|
||||
}
|
||||
} while ($this->bitLen($num) != $bits_cnt);
|
||||
return $num;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates $num - 1
|
||||
*
|
||||
* @param gmp resource $num
|
||||
* @return gmp resource
|
||||
* @access public
|
||||
*/
|
||||
function dec($num)
|
||||
{
|
||||
return gmp_sub($num, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true, if $num is equal to one. Else returns false
|
||||
*
|
||||
* @param gmp resource $num
|
||||
* @return bool
|
||||
* @access public
|
||||
*/
|
||||
function isOne($num)
|
||||
{
|
||||
return !gmp_cmp($num, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds greatest common divider (GCD) of $num1 and $num2
|
||||
*
|
||||
* @param gmp resource $num1
|
||||
* @param gmp resource $num2
|
||||
* @return gmp resource
|
||||
* @access public
|
||||
*/
|
||||
function GCD($num1, $num2)
|
||||
{
|
||||
return gmp_gcd($num1, $num2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds inverse number $inv for $num by modulus $mod, such as:
|
||||
* $inv * $num = 1 (mod $mod)
|
||||
*
|
||||
* @param gmp resource $num
|
||||
* @param gmp resource $mod
|
||||
* @return gmp resource
|
||||
* @access public
|
||||
*/
|
||||
function invmod($num, $mod)
|
||||
{
|
||||
return gmp_invert($num, $mod);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns bit length of number $num
|
||||
*
|
||||
* @param gmp resource $num
|
||||
* @return int
|
||||
* @access public
|
||||
*/
|
||||
function bitLen($num)
|
||||
{
|
||||
$tmp = $this->int2bin($num);
|
||||
$bit_len = strlen($tmp) * 8;
|
||||
$tmp = ord($tmp{strlen($tmp) - 1});
|
||||
if (!$tmp) {
|
||||
$bit_len -= 8;
|
||||
}
|
||||
else {
|
||||
while (!($tmp & 0x80)) {
|
||||
$bit_len--;
|
||||
$tmp <<= 1;
|
||||
}
|
||||
}
|
||||
return $bit_len;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates bitwise or of $num1 and $num2,
|
||||
* starting from bit $start_pos for number $num1
|
||||
*
|
||||
* @param gmp resource $num1
|
||||
* @param gmp resource $num2
|
||||
* @param int $start_pos
|
||||
* @return gmp resource
|
||||
* @access public
|
||||
*/
|
||||
function bitOr($num1, $num2, $start_pos)
|
||||
{
|
||||
$start_byte = intval($start_pos / 8);
|
||||
$start_bit = $start_pos % 8;
|
||||
$tmp1 = $this->int2bin($num1);
|
||||
|
||||
$num2 = gmp_mul($num2, 1 << $start_bit);
|
||||
$tmp2 = $this->int2bin($num2);
|
||||
if ($start_byte < strlen($tmp1)) {
|
||||
$tmp2 |= substr($tmp1, $start_byte);
|
||||
$tmp1 = substr($tmp1, 0, $start_byte) . $tmp2;
|
||||
}
|
||||
else {
|
||||
$tmp1 = str_pad($tmp1, $start_byte, "\0") . $tmp2;
|
||||
}
|
||||
return $this->bin2int($tmp1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns part of number $num, starting at bit
|
||||
* position $start with length $length
|
||||
*
|
||||
* @param gmp resource $num
|
||||
* @param int start
|
||||
* @param int length
|
||||
* @return gmp resource
|
||||
* @access public
|
||||
*/
|
||||
function subint($num, $start, $length)
|
||||
{
|
||||
$start_byte = intval($start / 8);
|
||||
$start_bit = $start % 8;
|
||||
$byte_length = intval($length / 8);
|
||||
$bit_length = $length % 8;
|
||||
if ($bit_length) {
|
||||
$byte_length++;
|
||||
}
|
||||
$num = gmp_div($num, 1 << $start_bit);
|
||||
$tmp = substr($this->int2bin($num), $start_byte, $byte_length);
|
||||
$tmp = str_pad($tmp, $byte_length, "\0");
|
||||
$tmp = substr_replace($tmp, $tmp{$byte_length - 1} & chr(0xff >> (8 - $bit_length)), $byte_length - 1, 1);
|
||||
return $this->bin2int($tmp);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns name of current wrapper
|
||||
*
|
||||
* @return string name of current wrapper
|
||||
* @access public
|
||||
*/
|
||||
function getWrapperName()
|
||||
{
|
||||
return 'GMP';
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
135
plugins/OStatus/extlib/Crypt/RSA/MathLoader.php
Normal file
135
plugins/OStatus/extlib/Crypt/RSA/MathLoader.php
Normal file
@@ -0,0 +1,135 @@
|
||||
<?php
|
||||
/**
|
||||
* Crypt_RSA allows to do following operations:
|
||||
* - key pair generation
|
||||
* - encryption and decryption
|
||||
* - signing and sign validation
|
||||
*
|
||||
* PHP versions 4 and 5
|
||||
*
|
||||
* LICENSE: This source file is subject to version 3.0 of the PHP license
|
||||
* that is available through the world-wide-web at the following URI:
|
||||
* http://www.php.net/license/3_0.txt. If you did not receive a copy of
|
||||
* the PHP License and are unable to obtain it through the web, please
|
||||
* send a note to license@php.net so we can mail you a copy immediately.
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright Alexander Valyalkin 2005
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version CVS: $Id: MathLoader.php,v 1.5 2009/01/05 08:30:29 clockwerx Exp $
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
*/
|
||||
|
||||
/**
|
||||
* RSA error handling facilities
|
||||
*/
|
||||
require_once 'Crypt/RSA/ErrorHandler.php';
|
||||
|
||||
/**
|
||||
* Crypt_RSA_MathLoader class.
|
||||
*
|
||||
* Provides static function:
|
||||
* - loadWrapper($wrapper_name) - loads RSA math wrapper with name $wrapper_name
|
||||
* or most suitable wrapper if $wrapper_name == 'default'
|
||||
*
|
||||
* Example usage:
|
||||
* // load BigInt wrapper
|
||||
* $big_int_wrapper = Crypt_RSA_MathLoader::loadWrapper('BigInt');
|
||||
*
|
||||
* // load BCMath wrapper
|
||||
* $bcmath_wrapper = Crypt_RSA_MathLoader::loadWrapper('BCMath');
|
||||
*
|
||||
* // load the most suitable wrapper
|
||||
* $bcmath_wrapper = Crypt_RSA_MathLoader::loadWrapper();
|
||||
*
|
||||
* @category Encryption
|
||||
* @package Crypt_RSA
|
||||
* @author Alexander Valyalkin <valyala@gmail.com>
|
||||
* @copyright Alexander Valyalkin 2005
|
||||
* @license http://www.php.net/license/3_0.txt PHP License 3.0
|
||||
* @version Release: @package_version@
|
||||
* @link http://pear.php.net/package/Crypt_RSA
|
||||
* @access public
|
||||
*/
|
||||
class Crypt_RSA_MathLoader
|
||||
{
|
||||
/**
|
||||
* Loads RSA math wrapper with name $wrapper_name.
|
||||
* Implemented wrappers can be found at Crypt/RSA/Math folder.
|
||||
* Read docs/Crypt_RSA/docs/math_wrappers.txt for details
|
||||
*
|
||||
* This is a static function:
|
||||
* // load BigInt wrapper
|
||||
* $big_int_wrapper = &Crypt_RSA_MathLoader::loadWrapper('BigInt');
|
||||
*
|
||||
* // load BCMath wrapper
|
||||
* $bcmath_wrapper = &Crypt_RSA_MathLoader::loadWrapper('BCMath');
|
||||
*
|
||||
* @param string $wrapper_name Name of wrapper
|
||||
*
|
||||
* @return object
|
||||
* Reference to object of wrapper with name $wrapper_name on success
|
||||
* or PEAR_Error object on error
|
||||
*
|
||||
* @access public
|
||||
*/
|
||||
function loadWrapper($wrapper_name = 'default')
|
||||
{
|
||||
static $math_objects = array();
|
||||
// ordered by performance. GMP is the fastest math library, BCMath - the slowest.
|
||||
static $math_wrappers = array('GMP', 'BigInt', 'BCMath',);
|
||||
|
||||
if (isset($math_objects[$wrapper_name])) {
|
||||
/*
|
||||
wrapper with name $wrapper_name is already loaded and created.
|
||||
Return reference to existing copy of wrapper
|
||||
*/
|
||||
return $math_objects[$wrapper_name];
|
||||
}
|
||||
|
||||
$err_handler = new Crypt_RSA_ErrorHandler();
|
||||
|
||||
if ($wrapper_name === 'default') {
|
||||
// try to load the most suitable wrapper
|
||||
$n = sizeof($math_wrappers);
|
||||
for ($i = 0; $i < $n; $i++) {
|
||||
$obj = Crypt_RSA_MathLoader::loadWrapper($math_wrappers[$i]);
|
||||
if (!$err_handler->isError($obj)) {
|
||||
// wrapper for $math_wrappers[$i] successfully loaded
|
||||
// register it as default wrapper and return reference to it
|
||||
return $math_objects['default'] = $obj;
|
||||
}
|
||||
}
|
||||
// can't load any wrapper
|
||||
$err_handler->pushError("can't load any wrapper for existing math libraries", CRYPT_RSA_ERROR_NO_WRAPPERS);
|
||||
return $err_handler->getLastError();
|
||||
}
|
||||
|
||||
$class_name = 'Crypt_RSA_Math_' . $wrapper_name;
|
||||
$class_filename = dirname(__FILE__) . '/Math/' . $wrapper_name . '.php';
|
||||
|
||||
if (!is_file($class_filename)) {
|
||||
$err_handler->pushError("can't find file [{$class_filename}] for RSA math wrapper [{$wrapper_name}]", CRYPT_RSA_ERROR_NO_FILE);
|
||||
return $err_handler->getLastError();
|
||||
}
|
||||
|
||||
include_once $class_filename;
|
||||
if (!class_exists($class_name)) {
|
||||
$err_handler->pushError("can't find class [{$class_name}] in file [{$class_filename}]", CRYPT_RSA_ERROR_NO_CLASS);
|
||||
return $err_handler->getLastError();
|
||||
}
|
||||
|
||||
// create and return wrapper object on success or PEAR_Error object on error
|
||||
$obj = new $class_name;
|
||||
if ($obj->errstr) {
|
||||
// cannot load required extension for math wrapper
|
||||
$err_handler->pushError($obj->errstr, CRYPT_RSA_ERROR_NO_EXT);
|
||||
return $err_handler->getLastError();
|
||||
}
|
||||
return $math_objects[$wrapper_name] = $obj;
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
@@ -1,6 +1,38 @@
|
||||
/*
|
||||
* StatusNet - a distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category OStatus UI interaction
|
||||
* @package StatusNet
|
||||
* @author Sarven Capadisli <csarven@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
|
||||
* @link http://status.net/
|
||||
* @note Everything in here should eventually migrate over to /js/util.js's SN.
|
||||
*/
|
||||
|
||||
SN.Init.OStatusCookie = function() {
|
||||
if (SN.U.StatusNetInstance.Get() === null) {
|
||||
SN.U.StatusNetInstance.Set({RemoteProfile: null});
|
||||
}
|
||||
};
|
||||
|
||||
SN.U.DialogBox = {
|
||||
Subscribe: function(a) {
|
||||
var f = a.parent().find('#form_ostatus_connect');
|
||||
var f = a.parent().find('.form_settings');
|
||||
if (f.length > 0) {
|
||||
f.show();
|
||||
}
|
||||
@@ -8,7 +40,7 @@ SN.U.DialogBox = {
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
dataType: 'xml',
|
||||
url: a[0].href+'&ajax=1',
|
||||
url: a[0].href + ((a[0].href.match(/[\\?]/) === null)?'?':'&') + 'ajax=1',
|
||||
beforeSend: function(formData) {
|
||||
a.addClass('processing');
|
||||
},
|
||||
@@ -19,7 +51,7 @@ SN.U.DialogBox = {
|
||||
if (typeof($('form', data)[0]) != 'undefined') {
|
||||
a.after(document._importNode($('form', data)[0], true));
|
||||
|
||||
var form = a.parent().find('#form_ostatus_connect');
|
||||
var form = a.parent().find('.form_settings');
|
||||
|
||||
form
|
||||
.addClass('dialogbox')
|
||||
@@ -39,7 +71,17 @@ SN.U.DialogBox = {
|
||||
return false;
|
||||
});
|
||||
|
||||
form.find('#acct').focus();
|
||||
form.find('#profile').focus();
|
||||
|
||||
if (form.attr('id') == 'form_ostatus_connect') {
|
||||
SN.Init.OStatusCookie();
|
||||
form.find('#profile').val(SN.U.StatusNetInstance.Get().RemoteProfile);
|
||||
|
||||
form.find("[type=submit]").bind('click', function() {
|
||||
SN.U.StatusNetInstance.Set({RemoteProfile: form.find('#profile').val()});
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
a.removeClass('processing');
|
||||
@@ -50,11 +92,11 @@ SN.U.DialogBox = {
|
||||
};
|
||||
|
||||
SN.Init.Subscribe = function() {
|
||||
$('.entity_subscribe a').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; });
|
||||
$('.entity_subscribe .entity_remote_subscribe').live('click', function() { SN.U.DialogBox.Subscribe($(this)); return false; });
|
||||
};
|
||||
|
||||
$(document).ready(function() {
|
||||
if ($('.entity_subscribe .entity_remote_subscribe').length > 0) {
|
||||
SN.Init.Subscribe();
|
||||
}
|
||||
SN.Init.Subscribe();
|
||||
|
||||
$('.form_remote_authorize').bind('submit', function() { $(this).addClass(SN.C.S.Processing); return true; });
|
||||
});
|
||||
|
@@ -1,716 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet, the distributed open-source microblogging tool
|
||||
*
|
||||
* An activity
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* LICENCE: This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @category OStatus
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
if (!defined('STATUSNET')) {
|
||||
exit(1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Utilities for turning DOMish things into Activityish things
|
||||
*
|
||||
* Some common functions that I didn't have the bandwidth to try to factor
|
||||
* into some kind of reasonable superclass, so just dumped here. Might
|
||||
* be useful to have an ActivityObject parent class or something.
|
||||
*
|
||||
* @category OStatus
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
class ActivityUtils
|
||||
{
|
||||
const ATOM = 'http://www.w3.org/2005/Atom';
|
||||
|
||||
const LINK = 'link';
|
||||
const REL = 'rel';
|
||||
const TYPE = 'type';
|
||||
const HREF = 'href';
|
||||
|
||||
const CONTENT = 'content';
|
||||
const SRC = 'src';
|
||||
|
||||
/**
|
||||
* Get the permalink for an Activity object
|
||||
*
|
||||
* @param DOMElement $element A DOM element
|
||||
*
|
||||
* @return string related link, if any
|
||||
*/
|
||||
|
||||
static function getPermalink($element)
|
||||
{
|
||||
return self::getLink($element, 'alternate', 'text/html');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the permalink for an Activity object
|
||||
*
|
||||
* @param DOMElement $element A DOM element
|
||||
*
|
||||
* @return string related link, if any
|
||||
*/
|
||||
|
||||
static function getLink($element, $rel, $type=null)
|
||||
{
|
||||
$links = $element->getElementsByTagnameNS(self::ATOM, self::LINK);
|
||||
|
||||
foreach ($links as $link) {
|
||||
|
||||
$linkRel = $link->getAttribute(self::REL);
|
||||
$linkType = $link->getAttribute(self::TYPE);
|
||||
|
||||
if ($linkRel == $rel &&
|
||||
(is_null($type) || $linkType == $type)) {
|
||||
return $link->getAttribute(self::HREF);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the first child element with the given tag
|
||||
*
|
||||
* @param DOMElement $element element to pick at
|
||||
* @param string $tag tag to look for
|
||||
* @param string $namespace Namespace to look under
|
||||
*
|
||||
* @return DOMElement found element or null
|
||||
*/
|
||||
|
||||
static function child($element, $tag, $namespace=self::ATOM)
|
||||
{
|
||||
$els = $element->childNodes;
|
||||
if (empty($els) || $els->length == 0) {
|
||||
return null;
|
||||
} else {
|
||||
for ($i = 0; $i < $els->length; $i++) {
|
||||
$el = $els->item($i);
|
||||
if ($el->localName == $tag && $el->namespaceURI == $namespace) {
|
||||
return $el;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Grab the text content of a DOM element child of the current element
|
||||
*
|
||||
* @param DOMElement $element Element whose children we examine
|
||||
* @param string $tag Tag to look up
|
||||
* @param string $namespace Namespace to use, defaults to Atom
|
||||
*
|
||||
* @return string content of the child
|
||||
*/
|
||||
|
||||
static function childContent($element, $tag, $namespace=self::ATOM)
|
||||
{
|
||||
$el = self::child($element, $tag, $namespace);
|
||||
|
||||
if (empty($el)) {
|
||||
return null;
|
||||
} else {
|
||||
return $el->textContent;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the content of an atom:entry-like object
|
||||
*
|
||||
* @param DOMElement $element The element to examine.
|
||||
*
|
||||
* @return string unencoded HTML content of the element, like "This -< is <b>HTML</b>."
|
||||
*
|
||||
* @todo handle remote content
|
||||
* @todo handle embedded XML mime types
|
||||
* @todo handle base64-encoded non-XML and non-text mime types
|
||||
*/
|
||||
|
||||
static function getContent($element)
|
||||
{
|
||||
$contentEl = ActivityUtils::child($element, self::CONTENT);
|
||||
|
||||
if (!empty($contentEl)) {
|
||||
|
||||
$src = $contentEl->getAttribute(self::SRC);
|
||||
|
||||
if (!empty($src)) {
|
||||
throw new ClientException(_("Can't handle remote content yet."));
|
||||
}
|
||||
|
||||
$type = $contentEl->getAttribute(self::TYPE);
|
||||
|
||||
// slavishly following http://atompub.org/rfc4287.html#rfc.section.4.1.3.3
|
||||
|
||||
if ($type == 'text') {
|
||||
return $contentEl->textContent;
|
||||
} else if ($type == 'html') {
|
||||
$text = $contentEl->textContent;
|
||||
return htmlspecialchars_decode($text, ENT_QUOTES);
|
||||
} else if ($type == 'xhtml') {
|
||||
$divEl = ActivityUtils::child($contentEl, 'div');
|
||||
if (empty($divEl)) {
|
||||
return null;
|
||||
}
|
||||
$doc = $divEl->ownerDocument;
|
||||
$text = '';
|
||||
$children = $divEl->childNodes;
|
||||
|
||||
for ($i = 0; $i < $children->length; $i++) {
|
||||
$child = $children->item($i);
|
||||
$text .= $doc->saveXML($child);
|
||||
}
|
||||
return trim($text);
|
||||
} else if (in_array(array('text/xml', 'application/xml'), $type) ||
|
||||
preg_match('#(+|/)xml$#', $type)) {
|
||||
throw new ClientException(_("Can't handle embedded XML content yet."));
|
||||
} else if (strncasecmp($type, 'text/', 5)) {
|
||||
return $contentEl->textContent;
|
||||
} else {
|
||||
throw new ClientException(_("Can't handle embedded Base64 content yet."));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A noun-ish thing in the activity universe
|
||||
*
|
||||
* The activity streams spec talks about activity objects, while also having
|
||||
* a tag activity:object, which is in fact an activity object. Aaaaaah!
|
||||
*
|
||||
* This is just a thing in the activity universe. Can be the subject, object,
|
||||
* or indirect object (target!) of an activity verb. Rotten name, and I'm
|
||||
* propagating it. *sigh*
|
||||
*
|
||||
* @category OStatus
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
class ActivityObject
|
||||
{
|
||||
const ARTICLE = 'http://activitystrea.ms/schema/1.0/article';
|
||||
const BLOGENTRY = 'http://activitystrea.ms/schema/1.0/blog-entry';
|
||||
const NOTE = 'http://activitystrea.ms/schema/1.0/note';
|
||||
const STATUS = 'http://activitystrea.ms/schema/1.0/status';
|
||||
const FILE = 'http://activitystrea.ms/schema/1.0/file';
|
||||
const PHOTO = 'http://activitystrea.ms/schema/1.0/photo';
|
||||
const ALBUM = 'http://activitystrea.ms/schema/1.0/photo-album';
|
||||
const PLAYLIST = 'http://activitystrea.ms/schema/1.0/playlist';
|
||||
const VIDEO = 'http://activitystrea.ms/schema/1.0/video';
|
||||
const AUDIO = 'http://activitystrea.ms/schema/1.0/audio';
|
||||
const BOOKMARK = 'http://activitystrea.ms/schema/1.0/bookmark';
|
||||
const PERSON = 'http://activitystrea.ms/schema/1.0/person';
|
||||
const GROUP = 'http://activitystrea.ms/schema/1.0/group';
|
||||
const PLACE = 'http://activitystrea.ms/schema/1.0/place';
|
||||
const COMMENT = 'http://activitystrea.ms/schema/1.0/comment';
|
||||
// ^^^^^^^^^^ tea!
|
||||
|
||||
// Atom elements we snarf
|
||||
|
||||
const TITLE = 'title';
|
||||
const SUMMARY = 'summary';
|
||||
const ID = 'id';
|
||||
const SOURCE = 'source';
|
||||
|
||||
const NAME = 'name';
|
||||
const URI = 'uri';
|
||||
const EMAIL = 'email';
|
||||
|
||||
public $element;
|
||||
public $type;
|
||||
public $id;
|
||||
public $title;
|
||||
public $summary;
|
||||
public $content;
|
||||
public $link;
|
||||
public $source;
|
||||
public $avatar;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* This probably needs to be refactored
|
||||
* to generate a local class (ActivityPerson, ActivityFile, ...)
|
||||
* based on the object type.
|
||||
*
|
||||
* @param DOMElement $element DOM thing to turn into an Activity thing
|
||||
*/
|
||||
|
||||
function __construct($element = null)
|
||||
{
|
||||
if (empty($element)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->element = $element;
|
||||
|
||||
if ($element->tagName == 'author') {
|
||||
|
||||
$this->type = self::PERSON; // XXX: is this fair?
|
||||
$this->title = $this->_childContent($element, self::NAME);
|
||||
$this->id = $this->_childContent($element, self::URI);
|
||||
|
||||
if (empty($this->id)) {
|
||||
$email = $this->_childContent($element, self::EMAIL);
|
||||
if (!empty($email)) {
|
||||
// XXX: acct: ?
|
||||
$this->id = 'mailto:'.$email;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
$this->type = $this->_childContent($element, Activity::OBJECTTYPE,
|
||||
Activity::SPEC);
|
||||
|
||||
if (empty($this->type)) {
|
||||
$this->type = ActivityObject::NOTE;
|
||||
}
|
||||
|
||||
$this->id = $this->_childContent($element, self::ID);
|
||||
$this->title = $this->_childContent($element, self::TITLE);
|
||||
$this->summary = $this->_childContent($element, self::SUMMARY);
|
||||
|
||||
$this->source = $this->_getSource($element);
|
||||
|
||||
$this->content = ActivityUtils::getContent($element);
|
||||
|
||||
$this->link = ActivityUtils::getPermalink($element);
|
||||
|
||||
// XXX: grab PoCo stuff
|
||||
}
|
||||
|
||||
// Some per-type attributes...
|
||||
if ($this->type == self::PERSON || $this->type == self::GROUP) {
|
||||
$this->displayName = $this->title;
|
||||
|
||||
// @fixme we may have multiple avatars with different resolutions specified
|
||||
$this->avatar = ActivityUtils::getLink($element, 'avatar');
|
||||
}
|
||||
}
|
||||
|
||||
private function _childContent($element, $tag, $namespace=ActivityUtils::ATOM)
|
||||
{
|
||||
return ActivityUtils::childContent($element, $tag, $namespace);
|
||||
}
|
||||
|
||||
// Try to get a unique id for the source feed
|
||||
|
||||
private function _getSource($element)
|
||||
{
|
||||
$sourceEl = ActivityUtils::child($element, 'source');
|
||||
|
||||
if (empty($sourceEl)) {
|
||||
return null;
|
||||
} else {
|
||||
$href = ActivityUtils::getLink($sourceEl, 'self');
|
||||
if (!empty($href)) {
|
||||
return $href;
|
||||
} else {
|
||||
return ActivityUtils::childContent($sourceEl, 'id');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static function fromNotice($notice)
|
||||
{
|
||||
$object = new ActivityObject();
|
||||
|
||||
$object->type = ActivityObject::NOTE;
|
||||
|
||||
$object->id = $notice->uri;
|
||||
$object->title = $notice->content;
|
||||
$object->content = $notice->rendered;
|
||||
$object->link = $notice->bestUrl();
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
static function fromProfile($profile)
|
||||
{
|
||||
$object = new ActivityObject();
|
||||
|
||||
$object->type = ActivityObject::PERSON;
|
||||
$object->id = $profile->getUri();
|
||||
$object->title = $profile->getBestName();
|
||||
$object->link = $profile->profileurl;
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
function asString($tag='activity:object')
|
||||
{
|
||||
$xs = new XMLStringer(true);
|
||||
|
||||
$xs->elementStart($tag);
|
||||
|
||||
$xs->element('activity:object-type', null, $this->type);
|
||||
|
||||
$xs->element(self::ID, null, $this->id);
|
||||
|
||||
if (!empty($this->title)) {
|
||||
$xs->element(self::TITLE, null, $this->title);
|
||||
}
|
||||
|
||||
if (!empty($this->summary)) {
|
||||
$xs->element(self::SUMMARY, null, $this->summary);
|
||||
}
|
||||
|
||||
if (!empty($this->content)) {
|
||||
// XXX: assuming HTML content here
|
||||
$xs->element(self::CONTENT, array('type' => 'html'), $this->content);
|
||||
}
|
||||
|
||||
if (!empty($this->link)) {
|
||||
$xs->element('link', array('rel' => 'alternate', 'type' => 'text/html'),
|
||||
$this->content);
|
||||
}
|
||||
|
||||
$xs->elementEnd($tag);
|
||||
|
||||
return $xs->getString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class to hold a bunch of constant defining default verb types
|
||||
*
|
||||
* @category OStatus
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
class ActivityVerb
|
||||
{
|
||||
const POST = 'http://activitystrea.ms/schema/1.0/post';
|
||||
const SHARE = 'http://activitystrea.ms/schema/1.0/share';
|
||||
const SAVE = 'http://activitystrea.ms/schema/1.0/save';
|
||||
const FAVORITE = 'http://activitystrea.ms/schema/1.0/favorite';
|
||||
const PLAY = 'http://activitystrea.ms/schema/1.0/play';
|
||||
const FOLLOW = 'http://activitystrea.ms/schema/1.0/follow';
|
||||
const FRIEND = 'http://activitystrea.ms/schema/1.0/make-friend';
|
||||
const JOIN = 'http://activitystrea.ms/schema/1.0/join';
|
||||
const TAG = 'http://activitystrea.ms/schema/1.0/tag';
|
||||
|
||||
// Custom OStatus verbs for the flipside until they're standardized
|
||||
const DELETE = 'http://ostatus.org/schema/1.0/unfollow';
|
||||
const UNFAVORITE = 'http://ostatus.org/schema/1.0/unfavorite';
|
||||
const UNFOLLOW = 'http://ostatus.org/schema/1.0/unfollow';
|
||||
const LEAVE = 'http://ostatus.org/schema/1.0/leave';
|
||||
}
|
||||
|
||||
class ActivityContext
|
||||
{
|
||||
public $replyToID;
|
||||
public $replyToUrl;
|
||||
public $location;
|
||||
public $attention = array();
|
||||
public $conversation;
|
||||
|
||||
const THR = 'http://purl.org/syndication/thread/1.0';
|
||||
const GEORSS = 'http://www.georss.org/georss';
|
||||
const OSTATUS = 'http://ostatus.org/schema/1.0';
|
||||
|
||||
const INREPLYTO = 'in-reply-to';
|
||||
const REF = 'ref';
|
||||
const HREF = 'href';
|
||||
|
||||
const POINT = 'point';
|
||||
|
||||
const ATTENTION = 'ostatus:attention';
|
||||
const CONVERSATION = 'ostatus:conversation';
|
||||
|
||||
function __construct($element)
|
||||
{
|
||||
$replyToEl = ActivityUtils::child($element, self::INREPLYTO, self::THR);
|
||||
|
||||
if (!empty($replyToEl)) {
|
||||
$this->replyToID = $replyToEl->getAttribute(self::REF);
|
||||
$this->replyToUrl = $replyToEl->getAttribute(self::HREF);
|
||||
}
|
||||
|
||||
$this->location = $this->getLocation($element);
|
||||
|
||||
$this->conversation = ActivityUtils::getLink($element, self::CONVERSATION);
|
||||
|
||||
// Multiple attention links allowed
|
||||
|
||||
$links = $element->getElementsByTagNameNS(ActivityUtils::ATOM, ActivityUtils::LINK);
|
||||
|
||||
for ($i = 0; $i < $links->length; $i++) {
|
||||
|
||||
$link = $links->item($i);
|
||||
|
||||
$linkRel = $link->getAttribute(ActivityUtils::REL);
|
||||
|
||||
if ($linkRel == self::ATTENTION) {
|
||||
$this->attention[] = $link->getAttribute(self::HREF);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse location given as a GeoRSS-simple point, if provided.
|
||||
* http://www.georss.org/simple
|
||||
*
|
||||
* @param feed item $entry
|
||||
* @return mixed Location or false
|
||||
*/
|
||||
function getLocation($dom)
|
||||
{
|
||||
$points = $dom->getElementsByTagNameNS(self::GEORSS, self::POINT);
|
||||
|
||||
for ($i = 0; $i < $points->length; $i++) {
|
||||
$point = $points->item($i)->textContent;
|
||||
$point = str_replace(',', ' ', $point); // per spec "treat commas as whitespace"
|
||||
$point = preg_replace('/\s+/', ' ', $point);
|
||||
$point = trim($point);
|
||||
$coords = explode(' ', $point);
|
||||
if (count($coords) == 2) {
|
||||
list($lat, $lon) = $coords;
|
||||
if (is_numeric($lat) && is_numeric($lon)) {
|
||||
common_log(LOG_INFO, "Looking up location for $lat $lon from georss");
|
||||
return Location::fromLatLon($lat, $lon);
|
||||
}
|
||||
}
|
||||
common_log(LOG_ERR, "Ignoring bogus georss:point value $point");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An activity in the ActivityStrea.ms world
|
||||
*
|
||||
* An activity is kind of like a sentence: someone did something
|
||||
* to something else.
|
||||
*
|
||||
* 'someone' is the 'actor'; 'did something' is the verb;
|
||||
* 'something else' is the object.
|
||||
*
|
||||
* @category OStatus
|
||||
* @package StatusNet
|
||||
* @author Evan Prodromou <evan@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPLv3
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
class Activity
|
||||
{
|
||||
const SPEC = 'http://activitystrea.ms/spec/1.0/';
|
||||
const SCHEMA = 'http://activitystrea.ms/schema/1.0/';
|
||||
|
||||
const VERB = 'verb';
|
||||
const OBJECT = 'object';
|
||||
const ACTOR = 'actor';
|
||||
const SUBJECT = 'subject';
|
||||
const OBJECTTYPE = 'object-type';
|
||||
const CONTEXT = 'context';
|
||||
const TARGET = 'target';
|
||||
|
||||
const ATOM = 'http://www.w3.org/2005/Atom';
|
||||
|
||||
const AUTHOR = 'author';
|
||||
const PUBLISHED = 'published';
|
||||
const UPDATED = 'updated';
|
||||
|
||||
public $actor; // an ActivityObject
|
||||
public $verb; // a string (the URL)
|
||||
public $object; // an ActivityObject
|
||||
public $target; // an ActivityObject
|
||||
public $context; // an ActivityObject
|
||||
public $time; // Time of the activity
|
||||
public $link; // an ActivityObject
|
||||
public $entry; // the source entry
|
||||
public $feed; // the source feed
|
||||
|
||||
public $summary; // summary of activity
|
||||
public $content; // HTML content of activity
|
||||
public $id; // ID of the activity
|
||||
public $title; // title of the activity
|
||||
|
||||
/**
|
||||
* Turns a regular old Atom <entry> into a magical activity
|
||||
*
|
||||
* @param DOMElement $entry Atom entry to poke at
|
||||
* @param DOMElement $feed Atom feed, for context
|
||||
*/
|
||||
|
||||
function __construct($entry = null, $feed = null)
|
||||
{
|
||||
if (is_null($entry)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->entry = $entry;
|
||||
$this->feed = $feed;
|
||||
|
||||
$pubEl = $this->_child($entry, self::PUBLISHED, self::ATOM);
|
||||
|
||||
if (!empty($pubEl)) {
|
||||
$this->time = strtotime($pubEl->textContent);
|
||||
} else {
|
||||
// XXX technically an error; being liberal. Good idea...?
|
||||
$updateEl = $this->_child($entry, self::UPDATED, self::ATOM);
|
||||
if (!empty($updateEl)) {
|
||||
$this->time = strtotime($updateEl->textContent);
|
||||
} else {
|
||||
$this->time = null;
|
||||
}
|
||||
}
|
||||
|
||||
$this->link = ActivityUtils::getPermalink($entry);
|
||||
|
||||
$verbEl = $this->_child($entry, self::VERB);
|
||||
|
||||
if (!empty($verbEl)) {
|
||||
$this->verb = trim($verbEl->textContent);
|
||||
} else {
|
||||
$this->verb = ActivityVerb::POST;
|
||||
// XXX: do other implied stuff here
|
||||
}
|
||||
|
||||
$objectEl = $this->_child($entry, self::OBJECT);
|
||||
|
||||
if (!empty($objectEl)) {
|
||||
$this->object = new ActivityObject($objectEl);
|
||||
} else {
|
||||
$this->object = new ActivityObject($entry);
|
||||
}
|
||||
|
||||
$actorEl = $this->_child($entry, self::ACTOR);
|
||||
|
||||
if (!empty($actorEl)) {
|
||||
|
||||
$this->actor = new ActivityObject($actorEl);
|
||||
|
||||
} else if (!empty($feed) &&
|
||||
$subjectEl = $this->_child($feed, self::SUBJECT)) {
|
||||
|
||||
$this->actor = new ActivityObject($subjectEl);
|
||||
|
||||
} else if ($authorEl = $this->_child($entry, self::AUTHOR, self::ATOM)) {
|
||||
|
||||
$this->actor = new ActivityObject($authorEl);
|
||||
|
||||
} else if (!empty($feed) && $authorEl = $this->_child($feed, self::AUTHOR,
|
||||
self::ATOM)) {
|
||||
|
||||
$this->actor = new ActivityObject($authorEl);
|
||||
}
|
||||
|
||||
$contextEl = $this->_child($entry, self::CONTEXT);
|
||||
|
||||
if (!empty($contextEl)) {
|
||||
$this->context = new ActivityContext($contextEl);
|
||||
} else {
|
||||
$this->context = new ActivityContext($entry);
|
||||
}
|
||||
|
||||
$targetEl = $this->_child($entry, self::TARGET);
|
||||
|
||||
if (!empty($targetEl)) {
|
||||
$this->target = new ActivityObject($targetEl);
|
||||
}
|
||||
|
||||
$this->summary = ActivityUtils::childContent($entry, 'summary');
|
||||
$this->id = ActivityUtils::childContent($entry, 'id');
|
||||
$this->content = ActivityUtils::getContent($entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an Atom <entry> based on this activity
|
||||
*
|
||||
* @return DOMElement Atom entry
|
||||
*/
|
||||
|
||||
function toAtomEntry()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
function asString($namespace=false)
|
||||
{
|
||||
$xs = new XMLStringer(true);
|
||||
|
||||
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');
|
||||
} else {
|
||||
$attrs = array();
|
||||
}
|
||||
|
||||
$xs->elementStart('entry', $attrs);
|
||||
|
||||
$xs->element('id', null, $this->id);
|
||||
$xs->element('title', null, $this->title);
|
||||
$xs->element('published', null, common_date_iso8601($this->time));
|
||||
$xs->element('content', array('type' => 'html'), $this->content);
|
||||
|
||||
if (!empty($this->summary)) {
|
||||
$xs->element('summary', null, $this->summary);
|
||||
}
|
||||
|
||||
if (!empty($this->link)) {
|
||||
$xs->element('link', array('rel' => 'alternate',
|
||||
'type' => 'text/html'),
|
||||
$this->link);
|
||||
}
|
||||
|
||||
// XXX: add context
|
||||
// XXX: add target
|
||||
|
||||
$xs->raw($this->actor->asString());
|
||||
$xs->element('activity:verb', null, $this->verb);
|
||||
$xs->raw($this->object->asString());
|
||||
|
||||
$xs->elementEnd('entry');
|
||||
|
||||
return $xs->getString();
|
||||
}
|
||||
|
||||
private function _child($element, $tag, $namespace=self::SPEC)
|
||||
{
|
||||
return ActivityUtils::child($element, $tag, $namespace);
|
||||
}
|
||||
}
|
@@ -22,11 +22,11 @@
|
||||
* @package Hub
|
||||
* @author Brion Vibber <brion@status.net>
|
||||
*/
|
||||
class HubVerifyQueueHandler extends QueueHandler
|
||||
class HubConfQueueHandler extends QueueHandler
|
||||
{
|
||||
function transport()
|
||||
{
|
||||
return 'hubverify';
|
||||
return 'hubconf';
|
||||
}
|
||||
|
||||
function handle($data)
|
@@ -33,6 +33,7 @@ class HubOutQueueHandler extends QueueHandler
|
||||
{
|
||||
$sub = $data['sub'];
|
||||
$atom = $data['atom'];
|
||||
$retries = $data['retries'];
|
||||
|
||||
assert($sub instanceof HubSub);
|
||||
assert(is_string($atom));
|
||||
@@ -40,13 +41,20 @@ class HubOutQueueHandler extends QueueHandler
|
||||
try {
|
||||
$sub->push($atom);
|
||||
} catch (Exception $e) {
|
||||
common_log(LOG_ERR, "Failed PuSH to $sub->callback for $sub->topic: " .
|
||||
$e->getMessage());
|
||||
// @fixme Reschedule a later delivery?
|
||||
return true;
|
||||
$retries--;
|
||||
$msg = "Failed PuSH to $sub->callback for $sub->topic: " .
|
||||
$e->getMessage();
|
||||
if ($retries > 0) {
|
||||
common_log(LOG_ERR, "$msg; scheduling for $retries more tries");
|
||||
|
||||
// @fixme when we have infrastructure to schedule a retry
|
||||
// after a delay, use it.
|
||||
$sub->distribute($atom, $retries);
|
||||
} else {
|
||||
common_log(LOG_ERR, "$msg; discarding");
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
172
plugins/OStatus/lib/magicenvelope.php
Normal file
172
plugins/OStatus/lib/magicenvelope.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/**
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* A sample module to show best practices for StatusNet plugins
|
||||
*
|
||||
* PHP version 5
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* @package StatusNet
|
||||
* @author James Walker <james@status.net>
|
||||
* @copyright 2010 StatusNet, Inc.
|
||||
* @license http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
class MagicEnvelope
|
||||
{
|
||||
const ENCODING = 'base64url';
|
||||
|
||||
const NS = 'http://salmon-protocol.org/ns/magic-env';
|
||||
|
||||
private function normalizeUser($user_id)
|
||||
{
|
||||
if (substr($user_id, 0, 5) == 'http:' ||
|
||||
substr($user_id, 0, 6) == 'https:' ||
|
||||
substr($user_id, 0, 5) == 'acct:') {
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
if (strpos($user_id, '@') !== FALSE) {
|
||||
return 'acct:' . $user_id;
|
||||
}
|
||||
|
||||
return 'http://' . $user_id;
|
||||
}
|
||||
|
||||
public function getKeyPair($signer_uri)
|
||||
{
|
||||
return 'RSA.79_L2gq-TD72Nsb5yGS0r9stLLpJZF5AHXyxzWmQmlqKl276LEJEs8CppcerLcR90MbYQUwt-SX9slx40Yq3vA==.AQAB.AR-jo5KMfSISmDAT2iMs2_vNFgWRjl5rbJVvA0SpGIEWyPdCGxlPtCbTexp8-0ZEIe8a4SyjatBECH5hxgMTpw==';
|
||||
}
|
||||
|
||||
|
||||
public function signMessage($text, $mimetype, $signer_uri)
|
||||
{
|
||||
$signer_uri = $this->normalizeUser($signer_uri);
|
||||
|
||||
if (!$this->checkAuthor($text, $signer_uri)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$signature_alg = Magicsig::fromString($this->getKeyPair($signer_uri));
|
||||
$armored_text = base64_encode($text);
|
||||
|
||||
return array(
|
||||
'data' => $armored_text,
|
||||
'encoding' => MagicEnvelope::ENCODING,
|
||||
'data_type' => $mimetype,
|
||||
'sig' => $signature_alg->sign($armored_text),
|
||||
'alg' => $signature_alg->getName()
|
||||
);
|
||||
|
||||
|
||||
}
|
||||
|
||||
public function unfold($env)
|
||||
{
|
||||
$dom = new DOMDocument();
|
||||
$dom->loadXML(base64_decode($env['data']));
|
||||
|
||||
if ($dom->documentElement->tagName != 'entry') {
|
||||
return false;
|
||||
}
|
||||
|
||||
$prov = $dom->createElementNS(MagicEnvelope::NS, 'me:provenance');
|
||||
$prov->setAttribute('xmlns:me', MagicEnvelope::NS);
|
||||
$data = $dom->createElementNS(MagicEnvelope::NS, 'me:data', $env['data']);
|
||||
$data->setAttribute('type', $env['data_type']);
|
||||
$prov->appendChild($data);
|
||||
$enc = $dom->createElementNS(MagicEnvelope::NS, 'me:encoding', $env['encoding']);
|
||||
$prov->appendChild($enc);
|
||||
$alg = $dom->createElementNS(MagicEnvelope::NS, 'me:alg', $env['alg']);
|
||||
$prov->appendChild($alg);
|
||||
$sig = $dom->createElementNS(MagicEnvelope::NS, 'me:sig', $env['sig']);
|
||||
$prov->appendChild($sig);
|
||||
|
||||
$dom->documentElement->appendChild($prov);
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
public function getAuthor($text) {
|
||||
$doc = new DOMDocument();
|
||||
if (!$doc->loadXML($text)) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if ($doc->documentElement->tagName == 'entry') {
|
||||
$authors = $doc->documentElement->getElementsByTagName('author');
|
||||
foreach ($authors as $author) {
|
||||
$uris = $author->getElementsByTagName('uri');
|
||||
foreach ($uris as $uri) {
|
||||
return $this->normalizeUser($uri->nodeValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function checkAuthor($text, $signer_uri)
|
||||
{
|
||||
return ($this->getAuthor($text) == $signer_uri);
|
||||
}
|
||||
|
||||
public function verify($env)
|
||||
{
|
||||
if ($env['alg'] != 'RSA-SHA256') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($env['encoding'] != MagicEnvelope::ENCODING) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$text = base64_decode($env['data']);
|
||||
$signer_uri = $this->getAuthor($text);
|
||||
|
||||
$verifier = Magicsig::fromString($this->getKeyPair($signer_uri));
|
||||
|
||||
return $verifier->verify($env['data'], $env['sig']);
|
||||
}
|
||||
|
||||
public function parse($text)
|
||||
{
|
||||
$dom = DOMDocument::loadXML($text);
|
||||
return $this->fromDom($dom);
|
||||
}
|
||||
|
||||
public function fromDom($dom)
|
||||
{
|
||||
if ($dom->documentElement->tagName == 'entry') {
|
||||
$env_element = $dom->getElementsByTagNameNS(MagicEnvelope::NS, 'provenance')->item(0);
|
||||
} else if ($dom->documentElement->tagName == 'me:env') {
|
||||
$env_element = $dom->documentElement;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
$data_element = $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'data')->item(0);
|
||||
|
||||
return array(
|
||||
'data' => trim($data_element->nodeValue),
|
||||
'data_type' => $data_element->getAttribute('type'),
|
||||
'encoding' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'encoding')->item(0)->nodeValue,
|
||||
'alg' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'alg')->item(0)->nodeValue,
|
||||
'sig' => $env_element->getElementsByTagNameNS(MagicEnvelope::NS, 'sig')->item(0)->nodeValue,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -18,46 +18,77 @@
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send a PuSH subscription verification from our internal hub.
|
||||
* Queue up final distribution for
|
||||
* @package Hub
|
||||
* Prepare PuSH and Salmon distributions for an outgoing message.
|
||||
*
|
||||
* @package OStatusPlugin
|
||||
* @author Brion Vibber <brion@status.net>
|
||||
*/
|
||||
class HubDistribQueueHandler extends QueueHandler
|
||||
class OStatusQueueHandler extends QueueHandler
|
||||
{
|
||||
function transport()
|
||||
{
|
||||
return 'hubdistrib';
|
||||
return 'ostatus';
|
||||
}
|
||||
|
||||
function handle($notice)
|
||||
{
|
||||
assert($notice instanceof Notice);
|
||||
|
||||
$this->pushUser($notice);
|
||||
$this->notice = $notice;
|
||||
$this->user = User::staticGet($notice->profile_id);
|
||||
|
||||
$this->pushUser();
|
||||
|
||||
foreach ($notice->getGroups() as $group) {
|
||||
$this->pushGroup($notice, $group->group_id);
|
||||
$oprofile = Ostatus_profile::staticGet('group_id', $group->id);
|
||||
if ($oprofile) {
|
||||
$this->pingReply($oprofile);
|
||||
} else {
|
||||
$this->pushGroup($group->id);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($notice->getReplies() as $profile_id) {
|
||||
$oprofile = Ostatus_profile::staticGet('profile_id', $profile_id);
|
||||
if ($oprofile) {
|
||||
$this->pingReply($oprofile);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function pushUser($notice)
|
||||
|
||||
function pushUser()
|
||||
{
|
||||
// See if there's any PuSH subscriptions, including OStatus clients.
|
||||
// @fixme handle group subscriptions as well
|
||||
// http://identi.ca/api/statuses/user_timeline/1.atom
|
||||
$feed = common_local_url('ApiTimelineUser',
|
||||
array('id' => $notice->profile_id,
|
||||
'format' => 'atom'));
|
||||
$this->pushFeed($feed, array($this, 'userFeedForNotice'), $notice);
|
||||
if ($this->user) {
|
||||
// For local posts, ping the PuSH hub to update their feed.
|
||||
// http://identi.ca/api/statuses/user_timeline/1.atom
|
||||
$feed = common_local_url('ApiTimelineUser',
|
||||
array('id' => $this->user->id,
|
||||
'format' => 'atom'));
|
||||
$this->pushFeed($feed, array($this, 'userFeedForNotice'));
|
||||
}
|
||||
}
|
||||
|
||||
function pushGroup($notice, $group_id)
|
||||
function pushGroup($group_id)
|
||||
{
|
||||
// For a local group, ping the PuSH hub to update its feed.
|
||||
// Updates may come from either a local or a remote user.
|
||||
$feed = common_local_url('ApiTimelineGroup',
|
||||
array('id' => $group_id,
|
||||
'format' => 'atom'));
|
||||
$this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id, $notice);
|
||||
$this->pushFeed($feed, array($this, 'groupFeedForNotice'), $group_id);
|
||||
}
|
||||
|
||||
function pingReply($oprofile)
|
||||
{
|
||||
if ($this->user) {
|
||||
// For local posts, send a Salmon ping to the mentioned
|
||||
// remote user or group.
|
||||
// @fixme as an optimization we can skip this if the
|
||||
// remote profile is subscribed to the author.
|
||||
$oprofile->notifyDeferred($this->notice);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -122,31 +153,26 @@ class HubDistribQueueHandler extends QueueHandler
|
||||
function pushFeedInternal($atom, $sub)
|
||||
{
|
||||
common_log(LOG_INFO, "Preparing $sub->N PuSH distribution(s) for $sub->topic");
|
||||
$qm = QueueManager::get();
|
||||
while ($sub->fetch()) {
|
||||
common_log(LOG_INFO, "Prepping PuSH distribution to $sub->callback for $sub->topic");
|
||||
$data = array('sub' => clone($sub),
|
||||
'atom' => $atom);
|
||||
$qm->enqueue($data, 'hubout');
|
||||
$sub->distribute($atom);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a single-item version of the sending user's Atom feed.
|
||||
* @param Notice $notice
|
||||
* @return string
|
||||
*/
|
||||
function userFeedForNotice($notice)
|
||||
function userFeedForNotice()
|
||||
{
|
||||
// @fixme this feels VERY hacky...
|
||||
// should probably be a cleaner way to do it
|
||||
|
||||
ob_start();
|
||||
$api = new ApiTimelineUserAction();
|
||||
$api->prepare(array('id' => $notice->profile_id,
|
||||
$api->prepare(array('id' => $this->notice->profile_id,
|
||||
'format' => 'atom',
|
||||
'max_id' => $notice->id,
|
||||
'since_id' => $notice->id - 1));
|
||||
'max_id' => $this->notice->id,
|
||||
'since_id' => $this->notice->id - 1));
|
||||
$api->showTimeline();
|
||||
$feed = ob_get_clean();
|
||||
|
||||
@@ -158,7 +184,7 @@ class HubDistribQueueHandler extends QueueHandler
|
||||
return $feed;
|
||||
}
|
||||
|
||||
function groupFeedForNotice($group_id, $notice)
|
||||
function groupFeedForNotice($group_id)
|
||||
{
|
||||
// @fixme this feels VERY hacky...
|
||||
// should probably be a cleaner way to do it
|
||||
@@ -167,8 +193,8 @@ class HubDistribQueueHandler extends QueueHandler
|
||||
$api = new ApiTimelineGroupAction();
|
||||
$args = array('id' => $group_id,
|
||||
'format' => 'atom',
|
||||
'max_id' => $notice->id,
|
||||
'since_id' => $notice->id - 1);
|
||||
'max_id' => $this->notice->id,
|
||||
'since_id' => $this->notice->id - 1);
|
||||
$api->prepare($args);
|
||||
$api->handle($args);
|
||||
$feed = ob_get_clean();
|
49
plugins/OStatus/lib/pushinqueuehandler.php
Normal file
49
plugins/OStatus/lib/pushinqueuehandler.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/*
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Process a feed distribution POST from a PuSH hub.
|
||||
* @package FeedSub
|
||||
* @author Brion Vibber <brion@status.net>
|
||||
*/
|
||||
|
||||
class PushInQueueHandler extends QueueHandler
|
||||
{
|
||||
function transport()
|
||||
{
|
||||
return 'pushin';
|
||||
}
|
||||
|
||||
function handle($data)
|
||||
{
|
||||
assert(is_array($data));
|
||||
|
||||
$feedsub_id = $data['feedsub_id'];
|
||||
$post = $data['post'];
|
||||
$hmac = $data['hmac'];
|
||||
|
||||
$feedsub = FeedSub::staticGet('id', $feedsub_id);
|
||||
if ($feedsub) {
|
||||
$feedsub->receive($post, $hmac);
|
||||
} else {
|
||||
common_log(LOG_ERR, "Discarding POST to unknown feed subscription id $feedsub_id");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -28,13 +28,26 @@
|
||||
*/
|
||||
class Salmon
|
||||
{
|
||||
/**
|
||||
* Sign and post the given Atom entry as a Salmon message.
|
||||
*
|
||||
* @fixme pass through the actor for signing?
|
||||
*
|
||||
* @param string $endpoint_uri
|
||||
* @param string $xml
|
||||
* @return boolean success
|
||||
*/
|
||||
public function post($endpoint_uri, $xml)
|
||||
{
|
||||
if (empty($endpoint_uri)) {
|
||||
return FALSE;
|
||||
return false;
|
||||
}
|
||||
|
||||
$headers = array('Content-type: application/atom+xml');
|
||||
if (!common_config('ostatus', 'skip_signatures')) {
|
||||
$xml = $this->createMagicEnv($xml);
|
||||
}
|
||||
|
||||
$headers = array('Content-Type: application/atom+xml');
|
||||
|
||||
try {
|
||||
$client = new HTTPClient();
|
||||
@@ -49,19 +62,28 @@ class Salmon
|
||||
$response->getStatus() . ': ' . $response->getBody());
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function createMagicEnv($text, $userid)
|
||||
public function createMagicEnv($text)
|
||||
{
|
||||
$magic_env = new MagicEnvelope();
|
||||
|
||||
// TODO: Should probably be getting the signer uri as an argument?
|
||||
$signer_uri = $magic_env->getAuthor($text);
|
||||
|
||||
$env = $magic_env->signMessage($text, 'application/atom+xml', $signer_uri);
|
||||
|
||||
return $magic_env->unfold($env);
|
||||
}
|
||||
|
||||
|
||||
public function verifyMagicEnv($env)
|
||||
public function verifyMagicEnv($dom)
|
||||
{
|
||||
$magic_env = new MagicEnvelope();
|
||||
|
||||
$env = $magic_env->fromDom($dom);
|
||||
|
||||
|
||||
return $magic_env->verify($env);
|
||||
}
|
||||
}
|
||||
|
@@ -38,11 +38,11 @@ class SalmonAction extends Action
|
||||
parent::prepare($args);
|
||||
|
||||
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
|
||||
$this->clientError(_('This method requires a POST.'));
|
||||
$this->clientError(_m('This method requires a POST.'));
|
||||
}
|
||||
|
||||
if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
|
||||
$this->clientError(_('Salmon requires application/atom+xml'));
|
||||
if (empty($_SERVER['CONTENT_TYPE']) || $_SERVER['CONTENT_TYPE'] != 'application/atom+xml') {
|
||||
$this->clientError(_m('Salmon requires application/atom+xml'));
|
||||
}
|
||||
|
||||
$xml = file_get_contents('php://input');
|
||||
@@ -54,7 +54,15 @@ class SalmonAction extends Action
|
||||
common_log(LOG_DEBUG, "Got invalid Salmon post: $xml");
|
||||
$this->clientError(_m('Salmon post must be an Atom entry.'));
|
||||
}
|
||||
// XXX: check the signature
|
||||
|
||||
// Check the signature
|
||||
$salmon = new Salmon;
|
||||
if (!common_config('ostatus', 'skip_signatures')) {
|
||||
if (!$salmon->verifyMagicEnv($dom)) {
|
||||
common_log(LOG_DEBUG, "Salmon signature verification failed.");
|
||||
$this->clientError(_m('Salmon signature verification failed.'));
|
||||
}
|
||||
}
|
||||
|
||||
$this->act = new Activity($dom->documentElement);
|
||||
return true;
|
||||
@@ -68,8 +76,7 @@ class SalmonAction extends Action
|
||||
{
|
||||
StatusNet::setApi(true); // Send smaller error pages
|
||||
|
||||
// TODO : Insert new $xml -> notice code
|
||||
|
||||
common_log(LOG_DEBUG, "Got a " . $this->act->verb);
|
||||
if (Event::handle('StartHandleSalmon', array($this->activity))) {
|
||||
switch ($this->act->verb)
|
||||
{
|
||||
@@ -95,8 +102,14 @@ class SalmonAction extends Action
|
||||
case ActivityVerb::JOIN:
|
||||
$this->handleJoin();
|
||||
break;
|
||||
case ActivityVerb::LEAVE:
|
||||
$this->handleLeave();
|
||||
break;
|
||||
case ActivityVerb::UPDATE_PROFILE:
|
||||
$this->handleUpdateProfile();
|
||||
break;
|
||||
default:
|
||||
throw new ClientException(_("Unimplemented."));
|
||||
throw new ClientException(_m("Unrecognized activity type."));
|
||||
}
|
||||
Event::handle('EndHandleSalmon', array($this->activity));
|
||||
}
|
||||
@@ -104,48 +117,57 @@ class SalmonAction extends Action
|
||||
|
||||
function handlePost()
|
||||
{
|
||||
throw new ClientException(_("Unimplemented!"));
|
||||
throw new ClientException(_m("This target doesn't understand posts."));
|
||||
}
|
||||
|
||||
function handleFollow()
|
||||
{
|
||||
throw new ClientException(_("Unimplemented!"));
|
||||
throw new ClientException(_m("This target doesn't understand follows."));
|
||||
}
|
||||
|
||||
function handleUnfollow()
|
||||
{
|
||||
throw new ClientException(_("Unimplemented!"));
|
||||
throw new ClientException(_m("This target doesn't understand unfollows."));
|
||||
}
|
||||
|
||||
function handleFavorite()
|
||||
{
|
||||
throw new ClientException(_("Unimplemented!"));
|
||||
throw new ClientException(_m("This target doesn't understand favorites."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Remote user doesn't like one of our posts after all!
|
||||
* Confirm the post is ours, and delete a local favorite event.
|
||||
*/
|
||||
|
||||
function handleUnfavorite()
|
||||
{
|
||||
throw new ClientException(_("Unimplemented!"));
|
||||
throw new ClientException(_m("This target doesn't understand unfavorites."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hmmmm
|
||||
*/
|
||||
function handleShare()
|
||||
{
|
||||
throw new ClientException(_("Unimplemented!"));
|
||||
throw new ClientException(_m("This target doesn't understand share events."));
|
||||
}
|
||||
|
||||
function handleJoin()
|
||||
{
|
||||
throw new ClientException(_m("This target doesn't understand joins."));
|
||||
}
|
||||
|
||||
function handleLeave()
|
||||
{
|
||||
throw new ClientException(_m("This target doesn't understand leave events."));
|
||||
}
|
||||
|
||||
/**
|
||||
* Hmmmm
|
||||
* Remote user sent us an update to their profile.
|
||||
* If we already know them, accept the updates.
|
||||
*/
|
||||
function handleJoin()
|
||||
function handleUpdateProfile()
|
||||
{
|
||||
throw new ClientException(_("Unimplemented!"));
|
||||
$oprofile = Ostatus_profile::getActorProfile($this->act);
|
||||
if ($oprofile) {
|
||||
common_log(LOG_INFO, "Got a profile-update ping from $oprofile->uri");
|
||||
$oprofile->updateFromActivityObject($this->act->actor);
|
||||
} else {
|
||||
common_log(LOG_INFO, "Ignoring profile-update ping from unknown " . $this->act->actor->id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -156,54 +178,16 @@ class SalmonAction extends Action
|
||||
$actor = $this->act->actor;
|
||||
if (empty($actor->id)) {
|
||||
common_log(LOG_ERR, "broken actor: " . var_export($actor, true));
|
||||
common_log(LOG_ERR, "activity with no actor: " . var_export($this->act, true));
|
||||
throw new Exception("Received a salmon slap from unidentified actor.");
|
||||
}
|
||||
|
||||
return Ostatus_profile::ensureActorProfile($this->act);
|
||||
return Ostatus_profile::ensureActivityObjectProfile($actor);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
$rendered = HTMLPurifier::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);
|
||||
|
||||
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_time($this->act->time);
|
||||
}
|
||||
|
||||
return Notice::saveNew($oprofile->profile_id,
|
||||
$content,
|
||||
'ostatus+salmon',
|
||||
$options);
|
||||
return $oprofile->processPost($this->act, 'salmon');
|
||||
}
|
||||
}
|
||||
|
44
plugins/OStatus/lib/salmonqueuehandler.php
Normal file
44
plugins/OStatus/lib/salmonqueuehandler.php
Normal file
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
/*
|
||||
* StatusNet - the distributed open-source microblogging tool
|
||||
* Copyright (C) 2010, StatusNet, Inc.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Send a Salmon notification in the background.
|
||||
* @package OStatusPlugin
|
||||
* @author Brion Vibber <brion@status.net>
|
||||
*/
|
||||
class SalmonQueueHandler extends QueueHandler
|
||||
{
|
||||
function transport()
|
||||
{
|
||||
return 'salmon';
|
||||
}
|
||||
|
||||
function handle($data)
|
||||
{
|
||||
assert(is_array($data));
|
||||
assert(is_string($data['salmonuri']));
|
||||
assert(is_string($data['entry']));
|
||||
|
||||
$salmon = new Salmon();
|
||||
$salmon->post($data['salmonuri'], $data['entry']);
|
||||
|
||||
// @fixme detect failure and attempt to resend
|
||||
return true;
|
||||
}
|
||||
}
|
@@ -32,11 +32,16 @@ define('WEBFINGER_SERVICE_REL_VALUE', 'lrdd');
|
||||
/**
|
||||
* Implement the webfinger protocol.
|
||||
*/
|
||||
|
||||
class Webfinger
|
||||
{
|
||||
const PROFILEPAGE = 'http://webfinger.net/rel/profile-page';
|
||||
const UPDATESFROM = 'http://schemas.google.com/g/2010#updates-from';
|
||||
|
||||
/**
|
||||
* Perform a webfinger lookup given an account.
|
||||
*/
|
||||
*/
|
||||
|
||||
public function lookup($id)
|
||||
{
|
||||
$id = $this->normalize($id);
|
||||
@@ -46,7 +51,7 @@ class Webfinger
|
||||
if (!$links) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$services = array();
|
||||
foreach ($links as $link) {
|
||||
if ($link['template']) {
|
||||
@@ -64,7 +69,7 @@ class Webfinger
|
||||
function normalize($id)
|
||||
{
|
||||
if (substr($id, 0, 7) == 'acct://') {
|
||||
return substr($id, 7);
|
||||
return substr($id, 7);
|
||||
} else if (substr($id, 0, 5) == 'acct:') {
|
||||
return substr($id, 5);
|
||||
}
|
||||
@@ -86,7 +91,7 @@ class Webfinger
|
||||
if ($result->host != $domain) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
$links = array();
|
||||
foreach ($result->links as $link) {
|
||||
if ($link['rel'] == WEBFINGER_SERVICE_REL_VALUE) {
|
||||
@@ -103,6 +108,10 @@ class Webfinger
|
||||
|
||||
$content = $this->fetchURL($url);
|
||||
|
||||
if (!$content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return XRD::parse($content);
|
||||
}
|
||||
|
||||
@@ -140,4 +149,3 @@ class Webfinger
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@@ -1,209 +0,0 @@
|
||||
<?php
|
||||
|
||||
if (isset($_SERVER) && array_key_exists('REQUEST_METHOD', $_SERVER)) {
|
||||
print "This script must be run from the command line\n";
|
||||
exit();
|
||||
}
|
||||
|
||||
// XXX: we should probably have some common source for this stuff
|
||||
|
||||
define('INSTALLDIR', realpath(dirname(__FILE__) . '/../../..'));
|
||||
define('STATUSNET', true);
|
||||
|
||||
require_once INSTALLDIR . '/lib/common.php';
|
||||
require_once INSTALLDIR . '/plugins/OStatus/lib/activity.php';
|
||||
|
||||
class ActivityParseTests extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
public function testExample1()
|
||||
{
|
||||
global $_example1;
|
||||
$dom = DOMDocument::loadXML($_example1);
|
||||
$act = new Activity($dom->documentElement);
|
||||
|
||||
$this->assertFalse(empty($act));
|
||||
|
||||
$this->assertEquals($act->time, 1243860840);
|
||||
$this->assertEquals($act->verb, ActivityVerb::POST);
|
||||
|
||||
$this->assertFalse(empty($act->object));
|
||||
$this->assertEquals($act->object->title, 'Punctuation Changeset');
|
||||
$this->assertEquals($act->object->type, 'http://versioncentral.example.org/activity/changeset');
|
||||
$this->assertEquals($act->object->summary, 'Fixing punctuation because it makes it more readable.');
|
||||
$this->assertEquals($act->object->id, 'tag:versioncentral.example.org,2009:/change/1643245');
|
||||
}
|
||||
|
||||
public function testExample3()
|
||||
{
|
||||
global $_example3;
|
||||
$dom = DOMDocument::loadXML($_example3);
|
||||
|
||||
$feed = $dom->documentElement;
|
||||
|
||||
$entries = $feed->getElementsByTagName('entry');
|
||||
|
||||
$entry = $entries->item(0);
|
||||
|
||||
$act = new Activity($entry, $feed);
|
||||
|
||||
$this->assertFalse(empty($act));
|
||||
$this->assertEquals($act->time, 1071340202);
|
||||
$this->assertEquals($act->link, 'http://example.org/2003/12/13/atom03.html');
|
||||
|
||||
$this->assertEquals($act->verb, ActivityVerb::POST);
|
||||
|
||||
$this->assertFalse(empty($act->actor));
|
||||
$this->assertEquals($act->actor->type, ActivityObject::PERSON);
|
||||
$this->assertEquals($act->actor->title, 'John Doe');
|
||||
$this->assertEquals($act->actor->id, 'mailto:johndoe@example.com');
|
||||
|
||||
$this->assertFalse(empty($act->object));
|
||||
$this->assertEquals($act->object->type, ActivityObject::NOTE);
|
||||
$this->assertEquals($act->object->id, 'urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a');
|
||||
$this->assertEquals($act->object->title, 'Atom-Powered Robots Run Amok');
|
||||
$this->assertEquals($act->object->summary, 'Some text.');
|
||||
$this->assertEquals($act->object->link, 'http://example.org/2003/12/13/atom03.html');
|
||||
|
||||
$this->assertFalse(empty($act->context));
|
||||
|
||||
$this->assertTrue(empty($act->target));
|
||||
|
||||
$this->assertEquals($act->entry, $entry);
|
||||
$this->assertEquals($act->feed, $feed);
|
||||
}
|
||||
|
||||
public function testExample4()
|
||||
{
|
||||
global $_example4;
|
||||
$dom = DOMDocument::loadXML($_example4);
|
||||
|
||||
$entry = $dom->documentElement;
|
||||
|
||||
$act = new Activity($entry);
|
||||
|
||||
$this->assertFalse(empty($act));
|
||||
$this->assertEquals(1266547958, $act->time);
|
||||
$this->assertEquals('http://example.net/notice/14', $act->link);
|
||||
|
||||
$this->assertFalse(empty($act->context));
|
||||
$this->assertEquals('http://example.net/notice/12', $act->context->replyToID);
|
||||
$this->assertEquals('http://example.net/notice/12', $act->context->replyToUrl);
|
||||
$this->assertEquals('http://example.net/conversation/11', $act->context->conversation);
|
||||
$this->assertEquals(array('http://example.net/user/1'), $act->context->attention);
|
||||
|
||||
$this->assertFalse(empty($act->object));
|
||||
$this->assertEquals($act->object->content,
|
||||
'@<span class="vcard"><a href="http://example.net/user/1" class="url"><span class="fn nickname">evan</span></a></span> now is the time for all good men to come to the aid of their country. #<span class="tag"><a href="http://example.net/tag/thetime" rel="tag">thetime</a></span>');
|
||||
|
||||
$this->assertFalse(empty($act->actor));
|
||||
}
|
||||
}
|
||||
|
||||
$_example1 = <<<EXAMPLE1
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
|
||||
<id>tag:versioncentral.example.org,2009:/commit/1643245</id>
|
||||
<published>2009-06-01T12:54:00Z</published>
|
||||
<title>Geraldine committed a change to yate</title>
|
||||
<content type="xhtml">Geraldine just committed a change to yate on VersionCentral</content>
|
||||
<link rel="alternate" type="text/html"
|
||||
href="http://versioncentral.example.org/geraldine/yate/commit/1643245" />
|
||||
<activity:verb>http://activitystrea.ms/schema/1.0/post</activity:verb>
|
||||
<activity:verb>http://versioncentral.example.org/activity/commit</activity:verb>
|
||||
<activity:object>
|
||||
<activity:object-type>http://versioncentral.example.org/activity/changeset</activity:object-type>
|
||||
<id>tag:versioncentral.example.org,2009:/change/1643245</id>
|
||||
<title>Punctuation Changeset</title>
|
||||
<summary>Fixing punctuation because it makes it more readable.</summary>
|
||||
<link rel="alternate" type="text/html" href="..." />
|
||||
</activity:object>
|
||||
</entry>
|
||||
EXAMPLE1;
|
||||
|
||||
$_example2 = <<<EXAMPLE2
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<entry xmlns='http://www.w3.org/2005/Atom' xmlns:activity='http://activitystrea.ms/spec/1.0/'>
|
||||
<id>tag:photopanic.example.com,2008:activity01</id>
|
||||
<title>Geraldine posted a Photo on PhotoPanic</title>
|
||||
<published>2008-11-02T15:29:00Z</published>
|
||||
<link rel="alternate" type="text/html" href="/geraldine/activities/1" />
|
||||
<activity:verb>
|
||||
http://activitystrea.ms/schema/1.0/post
|
||||
</activity:verb>
|
||||
<activity:object>
|
||||
<id>tag:photopanic.example.com,2008:photo01</id>
|
||||
<title>My Cat</title>
|
||||
<published>2008-11-02T15:29:00Z</published>
|
||||
<link rel="alternate" type="text/html" href="/geraldine/photos/1" />
|
||||
<activity:object-type>
|
||||
tag:atomactivity.example.com,2008:photo
|
||||
</activity:object-type>
|
||||
<source>
|
||||
<title>Geraldine's Photos</title>
|
||||
<link rel="self" type="application/atom+xml" href="/geraldine/photofeed.xml" />
|
||||
<link rel="alternate" type="text/html" href="/geraldine/" />
|
||||
</source>
|
||||
</activity:object>
|
||||
<content type="html">
|
||||
<p>Geraldine posted a Photo on PhotoPanic</p>
|
||||
<img src="/geraldine/photo1.jpg">
|
||||
</content>
|
||||
</entry>
|
||||
EXAMPLE2;
|
||||
|
||||
$_example3 = <<<EXAMPLE3
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<feed xmlns="http://www.w3.org/2005/Atom">
|
||||
|
||||
<title>Example Feed</title>
|
||||
<subtitle>A subtitle.</subtitle>
|
||||
<link href="http://example.org/feed/" rel="self" />
|
||||
<link href="http://example.org/" />
|
||||
<id>urn:uuid:60a76c80-d399-11d9-b91C-0003939e0af6</id>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
<author>
|
||||
<name>John Doe</name>
|
||||
<email>johndoe@example.com</email>
|
||||
</author>
|
||||
|
||||
<entry>
|
||||
<title>Atom-Powered Robots Run Amok</title>
|
||||
<link href="http://example.org/2003/12/13/atom03" />
|
||||
<link rel="alternate" type="text/html" href="http://example.org/2003/12/13/atom03.html"/>
|
||||
<link rel="edit" href="http://example.org/2003/12/13/atom03/edit"/>
|
||||
<id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
|
||||
<updated>2003-12-13T18:30:02Z</updated>
|
||||
<summary>Some text.</summary>
|
||||
</entry>
|
||||
|
||||
</feed>
|
||||
EXAMPLE3;
|
||||
|
||||
$_example4 = <<<EXAMPLE4
|
||||
<?xml version='1.0' encoding='UTF-8'?>
|
||||
<entry xmlns="http://www.w3.org/2005/Atom" xmlns:thr="http://purl.org/syndication/thread/1.0" xmlns:georss="http://www.georss.org/georss" xmlns:activity="http://activitystrea.ms/spec/1.0/" xmlns:ostatus="http://ostatus.org/schema/1.0">
|
||||
<title>@evan now is the time for all good men to come to the aid of their country. #thetime</title>
|
||||
<summary>@evan now is the time for all good men to come to the aid of their country. #thetime</summary>
|
||||
<author>
|
||||
<name>spock</name>
|
||||
<uri>http://example.net/user/2</uri>
|
||||
</author>
|
||||
<activity:actor>
|
||||
<activity:object-type>http://activitystrea.ms/schema/1.0/person</activity:object-type>
|
||||
<id>http://example.net/user/2</id>
|
||||
<title>spock</title>
|
||||
<link type="image/png" rel="avatar" href="http://example.net/theme/identica/default-avatar-profile.png"></link>
|
||||
</activity:actor>
|
||||
<link rel="alternate" type="text/html" href="http://example.net/notice/14"/>
|
||||
<id>http://example.net/notice/14</id>
|
||||
<published>2010-02-19T02:52:38+00:00</published>
|
||||
<updated>2010-02-19T02:52:38+00:00</updated>
|
||||
<link rel="related" href="http://example.net/notice/12"/>
|
||||
<thr:in-reply-to ref="http://example.net/notice/12" href="http://example.net/notice/12"></thr:in-reply-to>
|
||||
<link rel="ostatus:conversation" href="http://example.net/conversation/11"/>
|
||||
<link rel="ostatus:attention" href="http://example.net/user/1"/>
|
||||
<content type="html">@<span class="vcard"><a href="http://example.net/user/1" class="url"><span class="fn nickname">evan</span></a></span> now is the time for all good men to come to the aid of their country. #<span class="tag"><a href="http://example.net/tag/thetime" rel="tag">thetime</a></span></content>
|
||||
<category term="thetime"></category>
|
||||
</entry>
|
||||
EXAMPLE4;
|
@@ -7,24 +7,42 @@
|
||||
* @link http://status.net/
|
||||
*/
|
||||
|
||||
#form_ostatus_connect.dialogbox {
|
||||
#form_ostatus_connect.dialogbox,
|
||||
#form_ostatus_sub.dialogbox {
|
||||
width:70%;
|
||||
background-image:none;
|
||||
}
|
||||
#form_ostatus_connect.dialogbox .form_data label {
|
||||
#form_ostatus_sub.dialogbox {
|
||||
width:65%;
|
||||
}
|
||||
#form_ostatus_connect.dialogbox .form_data label,
|
||||
#form_ostatus_sub.dialogbox .form_data label {
|
||||
width:34%;
|
||||
}
|
||||
#form_ostatus_connect.dialogbox .form_data input {
|
||||
#form_ostatus_connect.dialogbox .form_data input,
|
||||
#form_ostatus_sub.dialogbox .form_data input {
|
||||
width:57%;
|
||||
}
|
||||
#form_ostatus_connect.dialogbox .form_data .form_guide {
|
||||
#form_ostatus_connect.dialogbox .form_data .form_guide,
|
||||
#form_ostatus_sub.dialogbox .form_data .form_guide {
|
||||
margin-left:36%;
|
||||
}
|
||||
|
||||
#form_ostatus_connect.dialogbox #ostatus_nickname {
|
||||
#form_ostatus_connect.dialogbox #ostatus_nickname,
|
||||
#form_ostatus_sub.dialogbox #ostatus_nickname {
|
||||
display:none;
|
||||
}
|
||||
|
||||
#form_ostatus_connect.dialogbox .submit_dialogbox {
|
||||
#form_ostatus_connect.dialogbox .submit_dialogbox,
|
||||
#form_ostatus_sub.dialogbox .submit_dialogbox {
|
||||
min-width:96px;
|
||||
}
|
||||
|
||||
#subscriptions #entity_remote_subscribe {
|
||||
padding:0;
|
||||
float:right;
|
||||
}
|
||||
|
||||
#subscriptions .entity_remote_subscribe {
|
||||
float:right;
|
||||
}
|
||||
|
Reference in New Issue
Block a user