diff options
Diffstat (limited to 'plugins/OStatus/actions')
-rw-r--r-- | plugins/OStatus/actions/feedsubsettings.php | 269 | ||||
-rw-r--r-- | plugins/OStatus/actions/groupsalmon.php | 188 | ||||
-rw-r--r-- | plugins/OStatus/actions/hostmeta.php | 16 | ||||
-rw-r--r-- | plugins/OStatus/actions/ostatusgroup.php | 181 | ||||
-rw-r--r-- | plugins/OStatus/actions/ostatusinit.php | 121 | ||||
-rw-r--r-- | plugins/OStatus/actions/ostatussub.php | 484 | ||||
-rw-r--r-- | plugins/OStatus/actions/ownerxrd.php | 56 | ||||
-rw-r--r-- | plugins/OStatus/actions/pushcallback.php | 60 | ||||
-rw-r--r-- | plugins/OStatus/actions/pushhub.php | 138 | ||||
-rw-r--r-- | plugins/OStatus/actions/salmon.php | 81 | ||||
-rw-r--r-- | plugins/OStatus/actions/usersalmon.php | 212 | ||||
-rw-r--r-- | plugins/OStatus/actions/userxrd.php (renamed from plugins/OStatus/actions/webfinger.php) | 39 |
12 files changed, 1216 insertions, 629 deletions
diff --git a/plugins/OStatus/actions/feedsubsettings.php b/plugins/OStatus/actions/feedsubsettings.php deleted file mode 100644 index 6933c9bf2..000000000 --- a/plugins/OStatus/actions/feedsubsettings.php +++ /dev/null @@ -1,269 +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 $feedurl; - 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(); - - $fuser = null; - - $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE); - - if (!empty($flink)) { - $fuser = $flink->getForeignUser(); - } - - $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('feedurl', _('Feed URL'), $this->feedurl, _('Enter the 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() - { - $feedurl = trim($this->arg('feedurl')); - - if ($feedurl == '') { - $this->showForm(_m('Empty feed URL!')); - return; - } - $this->feedurl = $feedurl; - - // Get the canonical feed URI and check it - try { - $discover = new FeedDiscovery(); - $uri = $discover->discoverFromURL($feedurl); - } catch (FeedSubBadURLException $e) { - $this->showForm(_m('Invalid URL or could not reach server.')); - return false; - } catch (FeedSubBadResponseException $e) { - $this->showForm(_m('Cannot read feed; server returned error.')); - return false; - } catch (FeedSubEmptyException $e) { - $this->showForm(_m('Cannot read feed; server returned an empty page.')); - return false; - } catch (FeedSubBadHTMLException $e) { - $this->showForm(_m('Bad HTML, could not find feed link.')); - return false; - } catch (FeedSubNoFeedException $e) { - $this->showForm(_m('Could not find a feed linked from this URL.')); - return false; - } catch (FeedSubUnrecognizedTypeException $e) { - $this->showForm(_m('Not a recognized feed type.')); - return false; - } catch (FeedSubException $e) { - // Any new ones we forgot about - $this->showForm(_m('Bad feed URL.')); - return false; - } - - $this->munger = $discover->feedMunger(); - $this->profile = $this->munger->ostatusProfile(); - - if ($this->profile->huburi == '' && !common_config('feedsub', 'nohub')) { - $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); - return false; - } - - return true; - } - - function saveFeed() - { - if ($this->validateFeed()) { - $this->preview = true; - $this->profile = Ostatus_profile::ensureProfile($this->munger); - if (!$this->profile) { - throw new ServerException("Feed profile was not saved properly."); - } - - // If not already in use, subscribe to updates via the hub - if ($this->profile->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); - } else { - $ok = $this->profile->subscribe(); - common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); - if (!$ok) { - $this->showForm(_m('Feed subscription failed! Bad response from hub.')); - return; - } - } - - // And subscribe the current user to the local profile - $user = common_current_user(); - - if ($this->profile->isGroup()) { - $group = $this->profile->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->profile->localProfile(); - if ($user->isSubscribed($local)) { - $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($local)) { - $this->showForm(_m('Feed subscribed!')); - } else { - $this->showForm(_m('Feed subscription failed!')); - } - } - } - } - - function validateAndPreview() - { - if ($this->validateFeed()) { - $this->preview = true; - $this->showForm(_m('Previewing feed:')); - } - } - - function previewFeed() - { - $profile = $this->munger->ostatusProfile(); - $notice = $this->munger->notice(0, true); // preview - - if ($notice) { - $this->element('b', null, 'Preview of latest post from this feed:'); - - $item = new NoticeList($notice, $this); - $item->show(); - } else { - $this->element('b', null, 'No posts in this feed yet.'); - } - } - - function showScripts() - { - parent::showScripts(); - $this->autofocus('feedurl'); - } -} diff --git a/plugins/OStatus/actions/groupsalmon.php b/plugins/OStatus/actions/groupsalmon.php new file mode 100644 index 000000000..29377b5fa --- /dev/null +++ b/plugins/OStatus/actions/groupsalmon.php @@ -0,0 +1,188 @@ +<?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 + * @author James Walker <james@status.net> + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class GroupsalmonAction extends SalmonAction +{ + var $group = null; + + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->group = User_group::staticGet('id', $id); + + if (empty($this->group)) { + $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; + } + + /** + * We've gotten a post event on the Salmon backchannel, probably a reply. + */ + + function handlePost() + { + switch ($this->act->object->type) { + case ActivityObject::ARTICLE: + case ActivityObject::BLOGENTRY: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of post."); + } + + // Notice must be to the attention of this group + + $context = $this->act->context; + + if (empty($context->attention)) { + throw new ClientException("Not to the attention of anyone."); + } else { + $uri = common_local_url('groupbyid', array('id' => $this->group->id)); + if (!in_array($uri, $context->attention)) { + throw new ClientException("Not to the attention of this group."); + } + } + + $profile = $this->ensureProfile(); + $this->saveNotice(); + } + + /** + * We've gotten a follow/subscribe notification from a remote user. + * Save a subscription relationship for them. + */ + + /** + * Postel's law: consider a "follow" notification as a "join". + */ + function handleFollow() + { + $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; + } + } + +} diff --git a/plugins/OStatus/actions/hostmeta.php b/plugins/OStatus/actions/hostmeta.php index 850b8a0fe..6d35ada6c 100644 --- a/plugins/OStatus/actions/hostmeta.php +++ b/plugins/OStatus/actions/hostmeta.php @@ -31,12 +31,18 @@ class HostMetaAction extends Action { parent::handle(); - $w = new Webfinger(); - - $domain = common_config('site', 'server'); - $url = common_local_url('webfinger'); + $url = common_local_url('userxrd'); $url.= '?uri={uri}'; - print $w->getHostMeta($domain, $url); + + $xrd = new XRD(); + + $xrd = new XRD(); + $xrd->host = $domain; + $xrd->links[] = array('rel' => Discovery::LRDD_REL, + 'template' => $url, + 'title' => array('Resource Descriptor')); + + print $xrd->toXML(); } } diff --git a/plugins/OStatus/actions/ostatusgroup.php b/plugins/OStatus/actions/ostatusgroup.php new file mode 100644 index 000000000..f325ba053 --- /dev/null +++ b/plugins/OStatus/actions/ostatusgroup.php @@ -0,0 +1,181 @@ +<?php +/* + * StatusNet - the distributed open-source microblogging tool + * Copyright (C) 2009-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> + */ + +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 + * preview() - display profile for a remote group + * + * success() - redirects to groups page on join + */ +class OStatusGroupAction extends OStatusSubAction +{ + protected $profile_uri; // provided acct: or URI of remote entity + protected $oprofile; // Ostatus_profile of remote entity, if valid + + + function validateRemoteProfile() + { + if (!$this->oprofile->isGroup()) { + // Send us to the user subscription form for conf + $target = common_local_url('ostatussub', array(), array('profile' => $this->profile_uri)); + common_redirect($target, 303); + } + } + + /** + * 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' => $this->selfLink())); + + $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('Join group'), + $this->profile_uri, + _m("OStatus group's address, like http://example.net/group/nickname")); + $this->elementEnd('li'); + $this->elementEnd('ul'); + + $this->submit('validate', _m('Continue')); + + $this->elementEnd('fieldset'); + + $this->elementEnd('form'); + } + + /** + * Show a preview for a remote group's profile + * @return boolean true if we're ok to try joining + */ + function preview() + { + $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; + } + + /** + * Redirect on successful remote group join + */ + function success() + { + $cur = common_current_user(); + $url = common_local_url('usergroups', array('nickname' => $cur->nickname)); + common_redirect($url, 303); + } + + /** + * Attempt to finalize subscription. + * validateFeed must have been run first. + * + * Calls showForm on failure or success on success. + */ + function saveFeed() + { + $user = common_current_user(); + $group = $this->oprofile->localGroup(); + if ($user->isMember($group)) { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Already a member!')); + return; + } + + if (Event::handle('StartJoinGroup', array($group, $user))) { + $ok = Group_member::join($this->oprofile->group_id, $user->id); + if ($ok) { + Event::handle('EndJoinGroup', array($group, $user)); + $this->success(); + } else { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Remote group join failed!')); + } + } else { + // TRANS: OStatus remote group subscription dialog error. + $this->showForm(_m('Remote group join aborted!')); + } + } + + /** + * Title of the page + * + * @return string Title of the page + */ + + function title() + { + // TRANS: Page title for OStatus remote group join form + return _m('Confirm joining remote group'); + } + + /** + * Instructions for use + * + * @return instructions for use + */ + + function getInstructions() + { + return _m('You can subscribe to groups from other supported sites. Paste the group\'s profile URI below:'); + } + + function selfLink() + { + return common_local_url('ostatusgroup'); + } +} diff --git a/plugins/OStatus/actions/ostatusinit.php b/plugins/OStatus/actions/ostatusinit.php index d21774420..22aea9f70 100644 --- a/plugins/OStatus/actions/ostatusinit.php +++ b/plugins/OStatus/actions/ostatusinit.php @@ -29,7 +29,8 @@ class OStatusInitAction extends Action { var $nickname; - var $acct; + var $group; + var $profile; var $err; function prepare($args) @@ -37,12 +38,16 @@ class OStatusInitAction extends Action parent::prepare($args); if (common_logged_in()) { - $this->clientError(_('You can use the local subscription!')); + $this->clientError(_m('You can use the local subscription!')); return false; } - $this->nickname = $this->trimmed('nickname'); - $this->acct = $this->trimmed('acct'); + // Local user or group the remote wants to subscribe to + $this->nickname = $this->trimmed('nickname'); + $this->group = $this->trimmed('group'); + + // Webfinger or profile URL of the remote user + $this->profile = $this->trimmed('profile'); return true; } @@ -55,7 +60,7 @@ class OStatusInitAction extends Action /* Use a session token for CSRF protection. */ $token = $this->trimmed('token'); if (!$token || $token != common_session_token()) { - $this->showForm(_('There was a problem with your session token. '. + $this->showForm(_m('There was a problem with your session token. '. 'Try again, please.')); return; } @@ -73,7 +78,7 @@ class OStatusInitAction extends Action $this->xw->startDocument('1.0', 'UTF-8'); $this->elementStart('html'); $this->elementStart('head'); - $this->element('title', null, _('Subscribe to user')); + $this->element('title', null, _m('Subscribe to user')); $this->elementEnd('head'); $this->elementStart('body'); $this->showContent(); @@ -86,55 +91,115 @@ class OStatusInitAction extends Action function showContent() { + if ($this->group) { + $header = sprintf(_m('Join group %s'), $this->group); + $submit = _m('Join'); + } else { + $header = sprintf(_m('Subscribe to %s'), $this->nickname); + $submit = _m('Subscribe'); + } $this->elementStart('form', array('id' => 'form_ostatus_connect', 'method' => 'post', 'class' => 'form_settings', 'action' => common_local_url('ostatusinit'))); $this->elementStart('fieldset'); - $this->element('legend', null, sprintf(_('Subscribe to %s'), $this->nickname)); + $this->element('legend', null, $header); $this->hidden('token', common_session_token()); $this->elementStart('ul', 'form_data'); $this->elementStart('li', array('id' => 'ostatus_nickname')); - $this->input('nickname', _('User nickname'), $this->nickname, - _('Nickname of the user you want to follow')); + $this->input('nickname', _m('User nickname'), $this->nickname, + _m('Nickname of the user you want to follow')); + $this->hidden('group', $this->group); // pass-through for magic links $this->elementEnd('li'); $this->elementStart('li', array('id' => 'ostatus_profile')); - $this->input('acct', _('Profile Account'), $this->acct, - _('Your account id (i.e. user@identi.ca)')); + $this->input('profile', _m('Profile Account'), $this->profile, + _m('Your account id (i.e. user@identi.ca)')); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('submit', _('Subscribe')); + $this->submit('submit', $submit); $this->elementEnd('fieldset'); $this->elementEnd('form'); } function ostatusConnect() { - $w = new Webfinger; + $opts = array('allowed_schemes' => array('http', 'https', 'acct')); + if (Validate::uri($this->profile, $opts)) { + $bits = parse_url($this->profile); + if ($bits['scheme'] == 'acct') { + $this->connectWebfinger($bits['path']); + } else { + $this->connectProfile($this->profile); + } + } elseif (strpos($this->profile, '@') !== false) { + $this->connectWebfinger($this->profile); + } else { + $this->clientError(_m("Must provide a remote profile.")); + } + } - $result = $w->lookup($this->acct); - foreach ($result->links as $link) { - if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') { - // We found a URL - let's redirect! + function connectWebfinger($acct) + { + $target_profile = $this->targetProfile(); - $user = User::staticGet('nickname', $this->nickname); + $disco = new Discovery; + $result = $disco->lookup($acct); + if (!$result) { + $this->clientError(_m("Couldn't look up OStatus account profile.")); + } - $feed_url = common_local_url('ApiTimelineUser', - array('id' => $user->id, - 'format' => 'atom')); - $url = $w->applyTemplate($link['template'], $feed_url); + foreach ($result->links as $link) { + if ($link['rel'] == 'http://ostatus.org/schema/1.0/subscribe') { + // We found a URL - let's redirect! + $url = Discovery::applyTemplate($link['template'], $target_profile); + common_log(LOG_INFO, "Sending remote subscriber $acct to $url"); + common_redirect($url, 303); + } - common_redirect($url, 303); - } + } + $this->clientError(_m("Couldn't confirm remote profile address.")); + } - } - + function connectProfile($subscriber_profile) + { + $target_profile = $this->targetProfile(); + + // @fixme hack hack! We should look up the remote sub URL from XRDS + $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); } - + + /** + * Build the canonical profile URI+URL of the requested user or group + */ + function targetProfile() + { + if ($this->nickname) { + $user = User::staticGet('nickname', $this->nickname); + if ($user) { + return common_local_url('userbyid', array('id' => $user->id)); + } else { + $this->clientError("No such user."); + } + } else if ($this->group) { + $group = Local_group::staticGet('nickname', $this->group); + if ($group) { + return common_local_url('groupbyid', array('id' => $group->group_id)); + } else { + $this->clientError("No such group."); + } + } else { + $this->clientError("No local user or group nickname provided."); + } + } + function title() { - return _('OStatus Connect'); + return _m('OStatus Connect'); } } diff --git a/plugins/OStatus/actions/ostatussub.php b/plugins/OStatus/actions/ostatussub.php index 239122501..65dee2392 100644 --- a/plugins/OStatus/actions/ostatussub.php +++ b/plugins/OStatus/actions/ostatussub.php @@ -1,7 +1,7 @@ <?php /* * StatusNet - the distributed open-source microblogging tool - * Copyright (C) 2010, StatusNet, Inc. + * Copyright (C) 2009-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 @@ -19,57 +19,41 @@ /** * @package OStatusPlugin - * @maintainer James Walker <james@status.net> + * @maintainer Brion Vibber <brion@status.net> */ 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 + * preview() - display profile for a remote user + * + * success() - redirects to subscriptions page on subscribe + */ class OStatusSubAction extends Action { + protected $profile_uri; // provided acct: or URI of remote entity + protected $oprofile; // Ostatus_profile of remote entity, if valid - protected $feedurl; - - function title() - { - return _m("OStatus Subscribe"); - } - - function handle($args) - { - if ($this->validateFeed()) { - $this->showForm(); - } - - return true; - - } - - function showForm($err = null) - { - $this->err = $err; - $this->showPage(); - } - - - function showContent() + /** + * Show the initial form, when we haven't yet been given a valid + * remote profile. + */ + function showInputForm() { $user = common_current_user(); $profile = $user->getProfile(); - $fuser = null; - - $flink = Foreign_link::getByUserID($user->id, FEEDSUB_SERVICE); - - if (!empty($flink)) { - $fuser = $flink->getForeignUser(); - } - $this->elementStart('form', array('method' => 'post', - 'id' => 'form_settings_feedsub', + 'id' => 'form_ostatus_sub', 'class' => 'form_settings', - 'action' => - common_local_url('feedsubsettings'))); + 'action' => $this->selfLink())); $this->hidden('token', common_session_token()); @@ -77,150 +61,390 @@ class OStatusSubAction extends Action $this->elementStart('ul', 'form_data'); $this->elementStart('li'); - $this->input('feedurl', _('Feed URL'), $this->feedurl, _('Enter the URL of a PubSubHubbub-enabled feed')); + $this->input('profile', + _m('Subscribe to'), + $this->profile_uri, + _m("OStatus user's address, like nickname@example.com or http://example.net/nickname")); $this->elementEnd('li'); $this->elementEnd('ul'); - $this->submit('subscribe', _m('Subscribe')); + $this->submit('validate', _m('Continue')); $this->elementEnd('fieldset'); $this->elementEnd('form'); - - $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. + * Show the preview-and-confirm form. We've got a valid remote + * profile and are ready to poke it! * - * All sub-functions reload the form with a message -- success or failure. - * - * @return void + * This controls the wrapper form; actual profile display will + * be in previewUser() or previewGroup() depending on the type. */ - - function handlePost() + function showPreviewForm() { - // 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.')); + $ok = $this->preview(); + if (!$ok) { + // @fixme maybe provide a cancel button or link back? return; } - if ($this->arg('subscribe')) { - $this->saveFeed(); + $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' => + $this->selfLink())); + $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->showForm(_('Unexpected form submission.')); + $this->submit('submit', _m('Confirm'), 'submit', null, + _m('Subscribe to this user')); } + $this->elementEnd('fieldset'); + $this->elementEnd('form'); + $this->elementEnd('li'); + $this->elementEnd('ul'); + $this->elementEnd('div'); } - /** - * Set up and add a feed - * - * @return boolean true if feed successfully read - * Sends you back to input form if not. + * Show a preview for a remote user's profile + * @return boolean true if we're ok to try subscribing */ - function validateFeed() + function preview() { - $feedurl = $this->trimmed('feed'); - - if ($feedurl == '') { - $this->showForm(_m('Empty feed URL!')); - return; + $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; } - $this->feedurl = $feedurl; + + $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE); + $avatarUrl = $avatar ? $avatar->displayUrl() : false; + + $this->showEntity($profile, + $profile->profileurl, + $avatarUrl, + $profile->bio); + return $ok; + } + + function showEntity($entity, $profile, $avatar, $note) + { + $nickname = $entity->nickname; + $fullname = $entity->fullname; + $homepage = $entity->homepage; + $location = $entity->location; - // Get the canonical feed URI and check it + 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 success() + { + $cur = common_current_user(); + $url = common_local_url('subscriptions', 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 pullRemoteProfile() + { + $this->profile_uri = $this->trimmed('profile'); try { - $discover = new FeedDiscovery(); - $uri = $discover->discoverFromURL($feedurl); + 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("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + common_debug('Invalid address format.', __FILE__); + return false; + } + return true; } catch (FeedSubBadURLException $e) { - $this->showForm(_m('Invalid URL or could not reach server.')); - return false; + $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + common_debug('Invalid URL or could not reach server.', __FILE__); } catch (FeedSubBadResponseException $e) { - $this->showForm(_m('Cannot read feed; server returned error.')); - return false; + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Cannot read feed; server returned error.', __FILE__); } catch (FeedSubEmptyException $e) { - $this->showForm(_m('Cannot read feed; server returned an empty page.')); - return false; + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Cannot read feed; server returned an empty page.', __FILE__); } catch (FeedSubBadHTMLException $e) { - $this->showForm(_m('Bad HTML, could not find feed link.')); - return false; + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Bad HTML, could not find feed link.', __FILE__); } catch (FeedSubNoFeedException $e) { - $this->showForm(_m('Could not find a feed linked from this URL.')); - return false; + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Could not find a feed linked from this URL.', __FILE__); } catch (FeedSubUnrecognizedTypeException $e) { - $this->showForm(_m('Not a recognized feed type.')); - return false; - } catch (FeedSubException $e) { + $this->error = _m("Sorry, we could not reach that feed. Please try that OStatus address again later."); + common_debug('Not a recognized feed type.', __FILE__); + } catch (Exception $e) { // Any new ones we forgot about - $this->showForm(_m('Bad feed URL.')); - return false; + $this->error = _m("Sorry, we could not reach that address. Please make sure that the OStatus address is like nickname@example.com or http://example.net/nickname"); + common_debug(sprintf('Bad feed URL: %s %s', get_class($e), $e->getMessage()), __FILE__); } - - $this->munger = $discover->feedMunger(); - $this->profile = $this->munger->ostatusProfile(); - if ($this->profile->huburi == '') { - $this->showForm(_m('Feed is not PuSH-enabled; cannot subscribe.')); - return false; + return false; + } + + function validateRemoteProfile() + { + if ($this->oprofile->isGroup()) { + // Send us to the group subscription form for conf + $target = common_local_url('ostatusgroup', array(), array('profile' => $this->profile_uri)); + common_redirect($target, 303); } - - return true; } + /** + * Attempt to finalize subscription. + * validateFeed must have been run first. + * + * Calls showForm on failure or success on success. + */ function saveFeed() { - if ($this->validateFeed()) { - $this->preview = true; - $this->profile = Ostatus_profile::ensureProfile($this->munger); + // And subscribe the current user to the local profile + $user = common_current_user(); + $local = $this->oprofile->localProfile(); + if ($user->isSubscribed($local)) { + // TRANS: OStatus remote subscription dialog error. + $this->showForm(_m('Already subscribed!')); + } elseif ($this->oprofile->subscribeLocalToRemote($user)) { + $this->success(); + } else { + // TRANS: OStatus remote subscription dialog error. + $this->showForm(_m('Remote subscription failed!')); + } + } + + function prepare($args) + { + parent::prepare($args); - // If not already in use, subscribe to updates via the hub - if ($this->profile->sub_start) { - common_log(LOG_INFO, __METHOD__ . ": double the fun! new sub for {$this->profile->feeduri} last subbed {$this->profile->sub_start}"); - } else { - $ok = $this->profile->subscribe(); - common_log(LOG_INFO, __METHOD__ . ": sub was $ok"); - if (!$ok) { - $this->showForm(_m('Feed subscription failed! Bad response from hub.')); - return; - } + 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); } - - // And subscribe the current user to the local profile - $user = common_current_user(); - $profile = $this->profile->getProfile(); - - if ($user->isSubscribed($profile)) { - $this->showForm(_m('Already subscribed!')); - } elseif ($user->subscribeTo($profile)) { - $this->showForm(_m('Feed subscribed!')); - } else { - $this->showForm(_m('Feed subscription failed!')); + return false; + } + + if ($this->pullRemoteProfile()) { + $this->validateRemoteProfile(); + } + return true; + } + + /** + * Handle the submission. + */ + function handle($args) + { + parent::handle($args); + if ($_SERVER['REQUEST_METHOD'] == 'POST') { + $this->handlePost(); + } else { + $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->oprofile) { + if ($this->arg('submit')) { + $this->saveFeed(); + return; } } + $this->showForm(); } - - function previewFeed() + /** + * Show the appropriate form based on our input state. + */ + function showForm($err=null) { - $profile = $this->munger->ostatusProfile(); - $notice = $this->munger->notice(0, true); // preview + 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(); + } + } - if ($notice) { - $this->element('b', null, 'Preview of latest post from this feed:'); + /** + * Title of the page + * + * @return string Title of the page + */ - $item = new NoticeList($notice, $this); - $item->show(); + function title() + { + // TRANS: Page title for OStatus remote subscription form + return _m('Confirm'); + } + + /** + * Instructions for use + * + * @return instructions for use + */ + + function getInstructions() + { + return _m('You can subscribe to users from other supported sites. Paste their address or profile URI below:'); + } + + function showPageNotice() + { + if (!empty($this->error)) { + $this->element('p', 'error', $this->error); + } + } + + /** + * Content area of the page + * + * Shows a form for associating a remote OStatus account with this + * StatusNet account. + * + * @return void + */ + + function showContent() + { + if ($this->oprofile) { + $this->showPreviewForm(); } else { - $this->element('b', null, 'No posts in this feed yet.'); + $this->showInputForm(); } } + function showScripts() + { + parent::showScripts(); + $this->autofocus('feedurl'); + } + function selfLink() + { + return common_local_url('ostatussub'); + } } diff --git a/plugins/OStatus/actions/ownerxrd.php b/plugins/OStatus/actions/ownerxrd.php new file mode 100644 index 000000000..9c141d8c7 --- /dev/null +++ b/plugins/OStatus/actions/ownerxrd.php @@ -0,0 +1,56 @@ +<?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 James Walker <james@status.net> + */ + +if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } + +class OwnerxrdAction extends XrdAction +{ + + public $uri; + + function prepare($args) + { + $this->user = User::siteOwner(); + + if (!$this->user) { + $this->clientError(_('No such user.'), 404); + return false; + } + + $nick = common_canonical_nickname($this->user->nickname); + $acct = 'acct:' . $nick . '@' . common_config('site', 'server'); + + $this->xrd = new XRD(); + + // Check to see if a $config['webfinger']['owner'] has been set + if ($owner = common_config('webfinger', 'owner')) { + $this->xrd->subject = Discovery::normalize($owner); + $this->xrd->alias[] = $acct; + } else { + $this->xrd->subject = $acct; + } + + return true; + } +} diff --git a/plugins/OStatus/actions/pushcallback.php b/plugins/OStatus/actions/pushcallback.php index 388c8f9c3..9a2067b8c 100644 --- a/plugins/OStatus/actions/pushcallback.php +++ b/plugins/OStatus/actions/pushcallback.php @@ -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(); @@ -48,9 +49,9 @@ class PushCallbackAction extends Action throw new ServerException('Empty or invalid feed id', 400); } - $profile = Ostatus_profile::staticGet('id', $feedid); - if (!$profile) { - throw new ServerException('Unknown OStatus/PuSH feed id ' . $feedid, 400); + $feedsub = FeedSub::staticGet('id', $feedid); + if (!$feedsub) { + throw new ServerException('Unknown PuSH feed id ' . $feedid, 400); } $hmac = ''; @@ -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. - $profile->postUpdates($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,35 +81,41 @@ 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); } - - $profile = Ostatus_profile::staticGet('feeduri', $topic); - if (!$profile) { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback for unknown feed $topic"); - throw new ServerException("Bogus hub callback: unknown feed", 404); + + $feedsub = FeedSub::staticGet('uri', $topic); + if (!$feedsub) { + throw new ClientException("Bad hub.topic feed $topic", 404); } - if ($profile->verify_token !== $verify_token) { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad token \"$verify_token\" for feed $topic"); - throw new ServerError("Bogus hub callback: bad token", 404); + if ($feedsub->verify_token !== $verify_token) { + throw new ClientException("Bad hub.verify_token $token for $topic", 404); } - if ($mode != $profile->sub_state) { - common_log(LOG_WARNING, __METHOD__ . ": bogus hub callback with bad mode \"$mode\" for feed $topic in state \"{$profile->sub_state}\""); - throw new ServerException("Bogus hub callback: mode doesn't match subscription state.", 404); + if ($mode == 'subscribe') { + // 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); + } } - // OK! if ($mode == 'subscribe') { - common_log(LOG_INFO, __METHOD__ . ': sub confirmed'); - $profile->confirmSubscribe($lease_seconds); + 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"); - $profile->confirmUnsubscribe(); + $feedsub->confirmUnsubscribe(); } print $challenge; } diff --git a/plugins/OStatus/actions/pushhub.php b/plugins/OStatus/actions/pushhub.php index 13ec09d52..842d65e7d 100644 --- a/plugins/OStatus/actions/pushhub.php +++ b/plugins/OStatus/actions/pushhub.php @@ -59,99 +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'); - common_log(LOG_DEBUG, __METHOD__ . ": checking sub'd to $feed $callback"); - if ($this->getSub($feed, $callback)) { - // Already subscribed; return 204 per spec. - header('HTTP/1.1 204 No Content'); - common_log(LOG_DEBUG, __METHOD__ . ': already subscribed'); - return; + $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."); } - common_log(LOG_DEBUG, __METHOD__ . ': setting up'); - $sub = new HubSub(); - $sub->topic = $feed; - $sub->callback = $callback; - $sub->verify_token = $this->arg('hub.verify_token', null); - $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); + $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."); } - $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? + $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."); + } - common_log(LOG_DEBUG, __METHOD__ . ': inserting'); - $ok = $sub->insert(); - - if (!$ok) { - throw new ServerException("Failed to save subscription record", 500); + $token = $this->arg('hub.verify_token', null); + + $secret = $this->arg('hub.secret', null); + if ($secret != '' && strlen($secret) >= 200) { + throw new ClientException("Invalid hub.secret $secret; must be under 200 bytes."); } - // @fixme check errors ;) + $sub = HubSub::staticGet($topic, $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)); + } + } - $data = array('sub' => $sub, 'mode' => 'subscribe'); - $qm = QueueManager::get(); - $qm->enqueue($data, 'hubverify'); - - header('HTTP/1.1 202 Accepted'); - common_log(LOG_DEBUG, __METHOD__ . ': done'); + 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'); + } } /** - * 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 + * @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) { - if ($sub->verify('unsubscribe')) { - $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) { @@ -161,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'"); } } diff --git a/plugins/OStatus/actions/salmon.php b/plugins/OStatus/actions/salmon.php deleted file mode 100644 index c79d09c95..000000000 --- a/plugins/OStatus/actions/salmon.php +++ /dev/null @@ -1,81 +0,0 @@ -<?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 - * @author James Walker <james@status.net> - */ - -if (!defined('STATUSNET')) { - exit(1); -} - -class SalmonAction extends Action -{ - var $user = null; - var $xml = null; - var $activity = null; - - function prepare($args) - { - if ($_SERVER['REQUEST_METHOD'] != 'POST') { - $this->clientError(_('This method requires a POST.')); - } - - if ($_SERVER['CONTENT_TYPE'] != 'application/atom+xml') { - $this->clientError(_('Salmon requires application/atom+xml')); - } - - $id = $this->trimmed('id'); - - if (!$id) { - $this->clientError(_('No ID.')); - } - - $this->user = User::staticGet($id); - - if (empty($this->user)) { - $this->clientError(_('No such user.')); - } - - $xml = file_get_contents('php://input'); - - $dom = DOMDocument::loadXML($xml); - - // XXX: check that document element is Atom entry - // XXX: check the signature - - $this->act = new Activity($dom->documentElement); - } - - function handle($args) - { - common_log(LOG_DEBUG, 'Salmon: incoming post for user: '. $user_id); - - // TODO : Insert new $xml -> notice code - - switch ($this->act->verb) - { - case Activity::POST: - case Activity::SHARE: - case Activity::FAVORITE: - case Activity::FOLLOW: - } - } -} diff --git a/plugins/OStatus/actions/usersalmon.php b/plugins/OStatus/actions/usersalmon.php new file mode 100644 index 000000000..c8a16e06f --- /dev/null +++ b/plugins/OStatus/actions/usersalmon.php @@ -0,0 +1,212 @@ +<?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 + * @author James Walker <james@status.net> + */ + +if (!defined('STATUSNET')) { + exit(1); +} + +class UsersalmonAction extends SalmonAction +{ + function prepare($args) + { + parent::prepare($args); + + $id = $this->trimmed('id'); + + if (!$id) { + $this->clientError(_('No ID.')); + } + + $this->user = User::staticGet('id', $id); + + if (empty($this->user)) { + $this->clientError(_('No such user.')); + } + + return true; + } + + /** + * We've gotten a post event on the Salmon backchannel, probably a reply. + * + * @todo validate if we need to handle this post, then call into + * ostatus_profile's general incoming-post handling. + */ + 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: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of post."); + } + + // Notice must either be a) in reply to a notice by this user + // or b) to the attention of this user + + $context = $this->act->context; + + if (!empty($context->replyToID)) { + $notice = Notice::staticGet('uri', $context->replyToID); + if (empty($notice)) { + throw new ClientException("In reply to unknown notice"); + } + if ($notice->profile_id != $this->user->id) { + throw new ClientException("In reply to a notice not by this user"); + } + } else if (!empty($context->attention)) { + 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(); + } + + /** + * We've gotten a follow/subscribe notification from a remote user. + * Save a subscription relationship for them. + */ + + function handleFollow() + { + $oprofile = $this->ensureProfile(); + if ($oprofile) { + common_log(LOG_INFO, "Setting up subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); + Subscription::start($oprofile->localProfile(), + $this->user->getProfile()); + } else { + common_log(LOG_INFO, "Can't set up subscription from remote; missing profile."); + } + } + + /** + * We've gotten an unfollow/unsubscribe notification from a remote user. + * Check if we have a subscription relationship for them and kill it. + * + * @fixme probably catch exceptions on fail? + */ + function handleUnfollow() + { + $oprofile = $this->ensureProfile(); + if ($oprofile) { + common_log(LOG_INFO, "Canceling subscription from remote {$oprofile->uri} to local {$this->user->nickname}"); + Subscription::cancel($oprofile->localProfile(), $this->user->getProfile()); + } else { + common_log(LOG_ERR, "Can't cancel subscription from remote, didn't find the profile"); + } + } + + /** + * Remote user likes one of our posts. + * Confirm the post is ours, and save a local favorite event. + */ + + function handleFavorite() + { + $notice = $this->getNotice($this->act->object); + $profile = $this->ensureProfile()->localProfile(); + + $old = Fave::pkeyGet(array('user_id' => $profile->id, + 'notice_id' => $notice->id)); + + if (!empty($old)) { + throw new ClientException("We already know that's a fave!"); + } + + if (!Fave::addNew($profile, $notice)) { + throw new ClientException("Could not save new favorite."); + } + } + + /** + * Remote user doesn't like one of our posts after all! + * Confirm the post is ours, and save a local favorite event. + */ + function handleUnfavorite() + { + $notice = $this->getNotice($this->act->object); + $profile = $this->ensureProfile()->localProfile(); + + $fave = Fave::pkeyGet(array('user_id' => $profile->id, + 'notice_id' => $notice->id)); + if (empty($fave)) { + throw new ClientException("Notice wasn't favorited!"); + } + + $fave->delete(); + } + + /** + * @param ActivityObject $object + * @return Notice + * @throws ClientException on invalid input + */ + function getNotice($object) + { + if (!$object) { + throw new ClientException("Can't favorite/unfavorite without an object."); + } + + switch ($object->type) { + case ActivityObject::ARTICLE: + case ActivityObject::BLOGENTRY: + case ActivityObject::NOTE: + case ActivityObject::STATUS: + case ActivityObject::COMMENT: + break; + default: + throw new ClientException("Can't handle that kind of object for liking/faving."); + } + + $notice = Notice::staticGet('uri', $object->id); + + if (empty($notice)) { + throw new ClientException("Notice with ID $object->id unknown."); + } + + if ($notice->profile_id != $this->user->id) { + throw new ClientException("Notice with ID $object->id not posted by $this->user->id."); + } + + return $notice; + } + +} diff --git a/plugins/OStatus/actions/webfinger.php b/plugins/OStatus/actions/userxrd.php index 75ba16638..414de9364 100644 --- a/plugins/OStatus/actions/webfinger.php +++ b/plugins/OStatus/actions/userxrd.php @@ -22,29 +22,19 @@ * @maintainer James Walker <james@status.net> */ -if (!defined('STATUSNET') && !defined('LACONICA')) { exit(1); } +if (!defined('STATUSNET')) { exit(1); } -class WebfingerAction extends Action +class UserxrdAction extends XrdAction { - public $uri; - function prepare($args) { parent::prepare($args); $this->uri = $this->trimmed('uri'); + $acct = Discovery::normalize($this->uri); - return true; - } - - function handle() - { - $acct = Webfinger::normalize($this->uri); - - $xrd = new XRD(); - - list($nick, $domain) = explode('@', urldecode($acct)); + list($nick, $domain) = explode('@', substr(urldecode($acct), 5)); $nick = common_canonical_nickname($nick); $this->user = User::staticGet('nickname', $nick); @@ -53,25 +43,6 @@ class WebfingerAction extends Action return false; } - $xrd->subject = $this->uri; - $xrd->alias[] = common_profile_url($nick); - $xrd->links[] = array('rel' => 'http://webfinger.net/rel/profile-page', - 'type' => 'text/html', - 'href' => common_profile_url($nick)); - - $salmon_url = common_local_url('salmon', - array('id' => $this->user->id)); - - $xrd->links[] = array('rel' => 'salmon', - 'href' => $salmon_url); - - // TODO - finalize where the redirect should go on the publisher - $url = common_local_url('ostatussub') . '?feed={uri}'; - $xrd->links[] = array('rel' => 'http://ostatus.org/schema/1.0/subscribe', - 'template' => $url ); - - header('Content-type: text/xml'); - print $xrd->toXML(); + return true; } - } |