code:
Plugin to support importing and exporting notices through Atom and RSS feeds.
The OStatus plugin concentrates on user-to-user cases for federating StatusNet
and similar social networking / microblogging / blogging sites, but includes
low-level feed subscription systems which are used by some other plugins.

Uses WebSub (previously named PubSubHubbub or PuSH) for push feed updates;
currently non-WebSub feeds cannot be subscribed unless an external
WebSub hub proxy is used.

Configuration options available:

$config['ostatus']['hub']
    (default internal hub)
    Set to URL of an external WebSub hub to use it instead of our internal hub
    for sending outgoing updates in user and group feeds.

$config['ostatus']['hub_retries']
    (default 0)
    Number of times to retry a WebSub send to consumers if using internal hub

Settings controlling incoming feed subscription:

$config['feedsub']['fallback_hub']
    To subscribe to feeds that don't have a hub, an external WebSub proxy hub
    such as Superfeedr may be used. Any feed without a hub of its own will
    be subscribed through the specified hub URL instead. If the external hub
    has usage charges, be aware that there is no restriction placed to how
    many feeds may be subscribed!

    $config['feedsub']['fallback_hub'] = 'https://superfeedr.com/hubbub';

$config['feedsub']['hub_user']
$config['feedsub']['hub_password']
    If using the fallback hub mode, these settings may be used to provide
    HTTP authentication credentials for contacting the hub. Default hubs
    specified from feeds are assumed to not require

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. (The fallback hub option can be used
    with a 3rd-party service to provide such polling.)

Todo:
* better support for feeds that aren't natively oriented at social networking
* make use of tags/categories from feeds
* better repeat handling

path: plugins/OStatus/README


code:
<?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/>.
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * @package OStatusPlugin
 * @author James Walker <james@status.net>
 */
class GroupsalmonAction extends SalmonAction
{
    var $group = null;

    protected function prepare(array $args=array())
    {
        parent::prepare($args);

        $id = $this->trimmed('id');

        if (!$id) {
            // TRANS: Client error.
            $this->clientError(_m('No ID.'));
        }

        $this->group = User_group::getKV('id', $id);

        if (!$this->group instanceof User_group) {
            // TRANS: Client error.
            $this->clientError(_m('No such group.'));
        }

        $this->target = $this->group;

        $remote_group = Ostatus_profile::getKV('group_id', $id);
        if ($remote_group instanceof Ostatus_profile) {
            // TRANS: Client error.
            $this->clientError(_m('Cannot accept remote posts for a remote group.'));
        }

        return true;
    }

    /**
     * We've gotten a post event on the Salmon backchannel, probably a reply.
     */
    function handlePost()
    {
        // @fixme process all objects?
        switch ($this->activity->objects[0]->type) {
        case ActivityObject::ARTICLE:
        case ActivityObject::BLOGENTRY:
        case ActivityObject::NOTE:
        case ActivityObject::STATUS:
        case ActivityObject::COMMENT:
            break;
        default:
            // TRANS: Client exception.
            throw new ClientException('Cannot handle that kind of post.');
        }

        // Notice must be to the attention of this group
        if (empty($this->activity->context->attention)) {
            // TRANS: Client exception.
            throw new ClientException("Not to the attention of anyone.");
        } else {
            $uri = common_local_url('groupbyid', array('id' => $this->group->id));

            if (!array_key_exists($uri, $this->activity->context->attention)) {
                // TRANS: Client exception.
                throw new ClientException("Not to the attention of this group.");
            }
        }

        try {
            $this->saveNotice();
        } catch (AlreadyFulfilledException $e) {
            return;
        }
    }

    /**
     * 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()
    {
        if ($this->oprofile->isGroup()) {
            // TRANS: Client error.
            $this->clientError(_m('Groups cannot join groups.'));
        }

        common_log(LOG_INFO, sprintf('Remote profile %s joining local group %s', $this->oprofile->getUri(), $this->group->getNickname()));

        if ($this->actor->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, $this->actor)) {
            // TRANS: Client error displayed when trying to join a group the user is blocked from by a group admin.
            $this->clientError(_m('You have been blocked from that group by the admin.'), 403);
        }

        try {
            $this->actor->joinGroup($this->group);
        } catch (Exception $e) {
            // TRANS: Server error. %1$s is a profile URI, %2$s is a group nickname.
            $this->serverError(sprintf(_m('Could not join remote user %1$s to group %2$s.'),
                                       $this->oprofile->getUri(), $this->group->getNickname()));
        }
    }

    /**
     * A remote user left our group.
     */
    function handleLeave()
    {
        if ($this->oprofile->isGroup()) {
            // TRANS: Client error displayed when trying to have a group join another group.
            throw new AlreadyFulfilledException(_m('Groups cannot be members of groups'));
        }

        common_log(LOG_INFO, sprintf('Remote profile %s leaving local group %s', $this->oprofile->getUri(), $this->group->getNickname()));

        try {
            $this->actor->leaveGroup($this->group);
        } catch (Exception $e) {
            // TRANS: Server error. %1$s is a profile URI, %2$s is a group nickname.
            $this->serverError(sprintf(_m('Could not remove remote user %1$s from group %2$s.'),
                                       $this->oprofile->getUri(), $this->group->getNickname()));
        }
    }
}

repo_name: andremichelle/gnu

license: mit


code:
<?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')) {
    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()
    {
        $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',
                     // TRANS: Field label.
                     _m('Join group'),
                     $this->profile_uri,
                     // TRANS: Tooltip for field label "Join group". Do not translate the "example.net"
                     // TRANS: domain name in the URL, as it is an official standard domain name for examples.
                     _m("OStatus group's address, like http://example.net/group/nickname."));
        $this->elementEnd('li');
        $this->elementEnd('ul');

        // TRANS: Button text.
        $this->submit('validate', _m('BUTTON','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()
    {
        $group = $this->oprofile->localGroup();

        if ($this->scoped->isMember($group)) {
            $this->element('div', array('class' => 'error'),
                           // TRANS: Error text displayed when trying to join a remote group the user is already a member of.
                           _m('You are already a member of this group.'));
            $ok = false;
        } else {
            $ok = true;
        }

        $this->showEntity($group,
                          $group->homeUrl(),
                          $group->homepage_logo,
                          $group->description);
        return $ok;
    }

    /**
     * Redirect on successful remote group join
     */
    function success()
    {
        $url = common_local_url('usergroups', array('nickname' => $this->scoped->getNickname()));
        common_redirect($url, 303);
    }

    /**
     * Attempt to finalize subscription.
     * validateFeed must have been run first.
     *
     * Calls showForm on failure or success on success.
     */
    function saveFeed()
    {
        $group = $this->oprofile->localGroup();
        if ($this->scoped->isMember($group)) {
            // TRANS: OStatus remote group subscription dialog error.
            $this->showForm(_m('Already a member!'));
            return;
        }

        try {
            $this->scoped->joinGroup($group);
        } catch (Exception $e) {
            common_log(LOG_ERR, "Exception on remote group join: " . $e->getMessage());
            common_log(LOG_ERR, $e->getTraceAsString());
            // TRANS: OStatus remote group subscription dialog error.
            $this->showForm(_m('Remote group join failed!'));
            return;
        }

        $this->success();
    }

    /**
     * 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()
    {
        // TRANS: Form instructions.
        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');
    }
}


code:
<?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')) {
    exit(1);
}

class OStatusInitAction extends Action
{
    var $nickname;
    var $tagger;
    var $peopletag;
    var $group;
    var $profile;
    var $err;

    protected function prepare(array $args=array())
    {
        parent::prepare($args);

        if (common_logged_in()) {
            // TRANS: Client error.
            $this->clientError(_m('You can use the local subscription!'));
        }

        // Local user or group the remote wants to subscribe to
        $this->nickname = $this->trimmed('nickname');
        $this->tagger = $this->trimmed('tagger');
        $this->peopletag = $this->trimmed('peopletag');
        $this->group = $this->trimmed('group');

        // Webfinger or profile URL of the remote user
        $this->profile = $this->trimmed('profile');

        return true;
    }

    protected function handle()
    {
        parent::handle();

        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
            /* Use a session token for CSRF protection. */
            $token = $this->trimmed('token');
            if (!$token || $token != common_session_token()) {
                // TRANS: Client error displayed when the session token does not match or is not given.
                $this->showForm(_m('There was a problem with your session token. '.
                                  'Try again, please.'));
                return;
            }
            $this->ostatusConnect();
        } else {
            $this->showForm();
        }
    }

    function showForm($err = null)
    {
        $this->err = $err;
        if ($this->boolean('ajax')) {
            $this->startHTML('text/xml;charset=utf-8');
            $this->elementStart('head');
            // TRANS: Form title.
            $this->element('title', null, _m('TITLE','Subscribe to user'));
            $this->elementEnd('head');
            $this->elementStart('body');
            $this->showContent();
            $this->elementEnd('body');
            $this->endHTML();
        } else {
            $this->showPage();
        }
    }

    function showContent()
    {

        if ($this->group) {
            // TRANS: Form legend. %s is a group name.
            $header = sprintf(_m('Join group %s'), $this->group);
            // TRANS: Button text to join a group.
            $submit = _m('BUTTON','Join');
        } else if ($this->peopletag && $this->tagger) {
            // TRANS: Form legend. %1$s is a list, %2$s is a lister's name.
            $header = sprintf(_m('Subscribe to list %1$s by %2$s'), $this->peopletag, $this->tagger);
            // TRANS: Button text to subscribe to a list.
            $submit = _m('BUTTON','Subscribe');
            // TRANS: Button text.
        } else {
            // TRANS: Form legend. %s is a nickname.
            $header = sprintf(_m('Subscribe to %s'), $this->nickname);
            // TRANS: Button text to subscribe to a profile.
            $submit = _m('BUTTON','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,  $header);
        $this->hidden('token', common_session_token());

        $this->elementStart('ul', 'form_data');
        $this->elementStart('li', array('id' => 'ostatus_nickname'));

        if ($this->group) {
            // TRANS: Field label.
            $this->input('group', _m('Group nickname'), $this->group,
                         // TRANS: Field title.
                         _m('Nickname of the group you want to join.'));
        } else {
            // TRANS: Field label.
            $this->input('nickname', _m('User nickname'), $this->nickname,
                         // TRANS: Field title.
                         _m('Nickname of the user you want to follow.'));
            $this->hidden('tagger', $this->tagger);
            $this->hidden('peopletag', $this->peopletag);
        }

        $this->elementEnd('li');
        $this->elementStart('li', array('id' => 'ostatus_profile'));
        // TRANS: Field label.
        $this->input('profile', _m('Profile Account'), $this->profile,
                      // TRANS: Tooltip for field label "Profile Account".
                     _m('Your account ID (e.g. user@example.com).'));
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->submit('submit', $submit);
        $this->elementEnd('fieldset');
        $this->elementEnd('form');
    }

    function ostatusConnect()
    {
        $validate = new Validate();
        $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 {
            // TRANS: Client error.
            $this->clientError(_m('Must provide a remote profile.'));
        }
    }

    function connectWebfinger($acct)
    {
        $target_profile = $this->targetProfile();

        $disco = new Discovery;
        $xrd = $disco->lookup($acct);

        $link = $xrd->get('http://ostatus.org/schema/1.0/subscribe');
        if (!is_null($link)) {
            // We found a URL - let's redirect!
            if (!empty($link->template)) {
                $url = Discovery::applyTemplate($link->template, $target_profile);
            } else {
                $url = $link->href;
            }
            common_log(LOG_INFO, "Sending remote subscriber $acct to $url");
            common_redirect($url, 303);
        }
        // TRANS: Client error.
        $this->clientError(_m('Could not 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::getKV('nickname', $this->nickname);
            if ($user) {
                return common_local_url('userbyid', array('id' => $user->id));
            } else {
                // TRANS: Client error.
                $this->clientError(_m('No such user.'));
            }
        } else if ($this->group) {
            $group = Local_group::getKV('nickname', $this->group);
            if ($group instanceof Local_group) {
                return common_local_url('groupbyid', array('id' => $group->group_id));
            } else {
                // TRANS: Client error.
                $this->clientError(_m('No such group.'));
            }
        } else if ($this->peopletag && $this->tagger) {
            $user = User::getKV('nickname', $this->tagger);
            if (empty($user)) {
                // TRANS: Client error.
                $this->clientError(_m('No such user.'));
            }

            $peopletag = Profile_list::getByTaggerAndTag($user->id, $this->peopletag);
            if ($peopletag) {
                return common_local_url('profiletagbyid',
                    array('tagger_id' => $user->id, 'id' => $peopletag->id));
            }
            // TRANS: Client error.
            $this->clientError(_m('No such list.'));
        } else {
            // TRANS: Client error.
            $this->clientError(_m('No local user or group nickname provided.'));
        }
    }

    function title()
    {
      // TRANS: Page title.
      return _m('OStatus Connect');
    }
}


code:
<?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); }

require_once INSTALLDIR . '/lib/peopletaglist.php';

/**
 * 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 OStatusPeopletagAction 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->isPeopletag()) {
            // 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()
    {
        $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',
                     // TRANS: Field label.
                     _m('Subscribe to list'),
                     $this->profile_uri,
                     // TRANS: Field title.
                     _m("Address of the OStatus list, like http://example.net/user/all/tag."));
        $this->elementEnd('li');
        $this->elementEnd('ul');

        // TRANS: Button text to continue joining a remote list.
        $this->submit('validate', _m('BUTTON','Continue'));

        $this->elementEnd('fieldset');

        $this->elementEnd('form');
    }

    /**
     * Show a preview for a remote peopletag's profile
     * @return boolean true if we're ok to try joining
     */
    function preview()
    {
        $oprofile = $this->oprofile;
        $ptag = $oprofile->localPeopletag();

        $cur = common_current_user();
        if ($ptag->hasSubscriber($cur->id)) {
            $this->element('div', array('class' => 'error'),
                           // TRANS: Error text displayed when trying to subscribe to a list already a subscriber to.
                           _m('You are already subscribed to this list.'));
            $ok = false;
        } else {
            $ok = true;
        }

        $this->showEntity($ptag);
        return $ok;
    }

    function showEntity($ptag)
    {
        $this->elementStart('div', 'peopletag');
        $widget = new PeopletagListItem($ptag, common_current_user(), $this);
        $widget->showCreator();
        $widget->showTag();
        $widget->showDescription();
        $this->elementEnd('div');
    }

    /**
     * Redirect on successful remote people tag subscription
     */
    function success()
    {
        $cur = common_current_user();
        $url = common_local_url('peopletagsubscriptions', 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();
        $ptag = $this->oprofile->localPeopletag();
        if ($ptag->hasSubscriber($user->id)) {
            // TRANS: OStatus remote group subscription dialog error.
            $this->showForm(_m('Already subscribed!'));
            return;
        }

        try {
            Profile_tag_subscription::add($ptag, $user);
            $this->success();
        } catch (Exception $e) {
            $this->showForm($e->getMessage());
        }
    }

    /**
     * Title of the page
     *
     * @return string Title of the page
     */

    function title()
    {
        // TRANS: Page title for OStatus remote list subscription form
        return _m('Confirm subscription to remote list');
    }

    /**
     * Instructions for use
     *
     * @return instructions for use
     */

    function getInstructions()
    {
        // TRANS: Instructions for OStatus list subscription form.
        return _m('You can subscribe to lists from other supported sites. Paste the list\'s URI below:');
    }

    function selfLink()
    {
        return common_local_url('ostatuspeopletag');
    }
}


code:
<?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('GNUSOCIAL') && !defined('STATUSNET')) { 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 function prepare(array $args=array())
    {
        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;
        }

        if ($this->pullRemoteProfile()) {
            $this->validateRemoteProfile();
        }
        return true;
    }

    /**
     * Handle the submission.
     */
    protected function handle()
    {
        parent::handle();
        if ($_SERVER['REQUEST_METHOD'] == 'POST') {
            $this->handlePost();
        } else {
            $this->showForm();
        }
    }

    /**
     * Show the initial form, when we haven't yet been given a valid
     * remote profile.
     */
    function showInputForm()
    {
        $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',
                     // TRANS: Field label for a field that takes an OStatus user address.
                     _m('Subscribe to'),
                     $this->profile_uri,
                     // TRANS: Tooltip for field label "Subscribe to".
                     _m('OStatus user\'s address, like nickname@example.com or http://example.net/nickname.'));
        $this->elementEnd('li');
        $this->elementEnd('ul');
        // TRANS: Button text.
        $this->submit('validate', _m('BUTTON','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()
    {
        $ok = $this->preview();
        if (!$ok) {
            // @todo 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' =>
                                          $this->selfLink()));
        $this->elementStart('fieldset');
        $this->hidden('token', common_session_token());
        $this->hidden('profile', $this->profile_uri);
        if ($this->oprofile->isGroup()) {
            // TRANS: Button text.
            $this->submit('submit', _m('Join'), 'submit', null,
                         // TRANS: Tooltip for button "Join".
                         _m('BUTTON','Join this group'));
        } else {
            // TRANS: Button text.
            $this->submit('submit', _m('BUTTON','Confirm'), 'submit', null,
                         // TRANS: Tooltip for button "Confirm".
                         _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 preview()
    {
        // Throws NoProfileException on localProfile when remote user's Profile not found
        $profile = $this->oprofile->localProfile();

        if ($this->scoped->isSubscribed($profile)) {
            $this->element('div', array('class' => 'error'),
                           // TRANS: Extra paragraph in remote profile view when already subscribed.
                           _m('You are already subscribed to this user.'));
            $ok = false;
        } else {
            $ok = true;
        }

        $avatarUrl = $profile->avatarUrl(AVATAR_PROFILE_SIZE);

        $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;

        $this->elementStart('div', 'entity_profile vcard');
        $this->element('img', array('src' => $avatar,
                                    'class' => 'photo avatar entity_depiction',
                                    'width' => AVATAR_PROFILE_SIZE,
                                    'height' => AVATAR_PROFILE_SIZE,
                                    'alt' => $nickname));

        $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname entity_nickname';
        $this->elementStart('a', array('href' => $profile,
                                       'class' => 'url '.$hasFN));
        $this->text($nickname);
        $this->elementEnd('a');

        if (!is_null($fullname)) {
            $this->elementStart('div', 'fn entity_fn');
            $this->text($fullname);
            $this->elementEnd('div');
        }

        if (!is_null($location)) {
            $this->elementStart('div', 'label entity_location');
            $this->text($location);
            $this->elementEnd('div');
        }

        if (!is_null($homepage)) {
            $this->elementStart('a', array('href' => $homepage,
                                           'class' => 'url entity_url'));
            $this->text($homepage);
            $this->elementEnd('a');
        }

        if (!is_null($note)) {
            $this->elementStart('div', 'note entity_note');
            $this->text($note);
            $this->elementEnd('div');
        }
        $this->elementEnd('div');
    }

    /**
     * Redirect on successful remote user subscription
     */
    function success()
    {
        $url = common_local_url('subscriptions', array('nickname' => $this->scoped->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()
    {
        $validate = new Validate();
        try {
            $this->profile_uri = Discovery::normalize($this->trimmed('profile'));
        } catch (Exception $e) {
            return false;
        }

        try {
            if (Discovery::isAcct($this->profile_uri) && $validate->email(mb_substr($this->profile_uri, 5))) {
                $this->oprofile = Ostatus_profile::ensureWebfinger($this->profile_uri);
            } else if ($validate->uri($this->profile_uri)) {
                $this->oprofile = Ostatus_profile::ensureProfileURL($this->profile_uri);
            } else {
                // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
                // TRANS: and example.net, as these are official standard domain names for use in examples.
                $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) {
                // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
                // TRANS: and example.net, as these are official standard domain names for use in examples.
            $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) {
            // TRANS: Error text.
            $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) {
            // TRANS: Error text.
            $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) {
            // TRANS: Error text.
            $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) {
            // TRANS: Error text.
            $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) {
            // TRANS: Error text.
            $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 (FeedSubNoHubException $e) {
            // TRANS: Error text.
            $this->error = _m("Sorry, that feed is not Pubsubhubub enabled.");
            common_debug('No hub found.', __FILE__);
        } catch (Exception $e) {
            // Any new ones we forgot about
                // TRANS: Error message in OStatus plugin. Do not translate the domain names example.com
                // TRANS: and example.net, as these are official standard domain names for use in examples.
            $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__);
        }

        return false;
    }

    function validateRemoteProfile()
    {
        // Send us to the respective subscription form for conf
        if ($this->oprofile->isGroup()) {
            $target = common_local_url('ostatusgroup', array(), array('profile' => $this->profile_uri));
            common_redirect($target, 303);
        } else if ($this->oprofile->isPeopletag()) {
            $target = common_local_url('ostatuspeopletag', array(), array('profile' => $this->profile_uri));
            common_redirect($target, 303);
        }
    }

    /**
     * Attempt to finalize subscription.
     * validateFeed must have been run first.
     *
     * Calls showForm on failure or success on success.
     */
    function saveFeed()
    {
        // And subscribe the current user to the local profile
        $local = $this->oprofile->localProfile();
        if ($this->scoped->isSubscribed($local)) {
            // TRANS: OStatus remote subscription dialog error.
            $this->showForm(_m('Already subscribed!'));
        } elseif (Subscription::start($this->scoped, $local)) {
            $this->success();
        } else {
            // TRANS: OStatus remote subscription dialog error.
            $this->showForm(_m('Remote subscription failed!'));
        }
    }

    /**
     * Handle posts to this form
     *
     * @return void
     */

    function handlePost()
    {
        // CSRF protection
        $token = $this->trimmed('token');
        if (!$token || $token != common_session_token()) {
            // TRANS: Client error displayed when the session token does not match or is not given.
            $this->showForm(_m('There was a problem with your session token. '.
                              'Try again, please.'));
            return;
        }

        if ($this->oprofile) {
            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')) {
            $this->startHTML('text/xml;charset=utf-8');
            $this->elementStart('head');
            // TRANS: Form title.
            $this->element('title', null, _m('Subscribe to user'));
            $this->elementEnd('head');
            $this->elementStart('body');
            $this->showContent();
            $this->elementEnd('body');
            $this->endHTML();
        } else {
            $this->showPage();
        }
    }

    /**
     * Title of the page
     *
     * @return string Title of the page
     */

    function title()
    {
        // TRANS: Page title for OStatus remote subscription form.
        return !empty($this->profile_uri) ? _m('Confirm') : _m('Remote subscription');
    }

    /**
     * Instructions for use
     *
     * @return instructions for use
     */

    function getInstructions()
    {
        // TRANS: Instructions.
        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->showInputForm();
        }
    }

    function showScripts()
    {
        parent::showScripts();
        $this->autofocus('feedurl');
    }

    function selfLink()
    {
        return common_local_url('ostatussub');
    }

    /**
     * Disable the send-notice form at the top of the page.
     * This is really just a hack for the broken CSS in the Cloudy theme,
     * I think; copying from other non-notice-navigation pages that do this
     * as well. There will be plenty of others also broken.
     *
     * @fixme fix the cloudy theme
     * @fixme do this in a more general way
     */
    function showNoticeForm() {
        // nop
    }
}


code:
<?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 OStatusTagAction extends OStatusInitAction
{
    var $nickname;
    var $profile;
    var $err;

    function prepare(array $args = array())
    {
        parent::prepare($args);

        if (common_logged_in()) {
            // TRANS: Client error displayed when trying to list a local object as if it is remote.
            $this->clientError(_m('You can use the local list functionality!'));
        }

        $this->nickname = $this->trimmed('nickname');

        // Webfinger or profile URL of the remote user
        $this->profile = $this->trimmed('profile');

        return true;
    }

    function showContent()
    {
        // TRANS: Header for listing a remote object. %s is a remote object's name.
        $header = sprintf(_m('List %s'), $this->nickname);
        // TRANS: Button text to list a remote object.
        $submit = _m('BUTTON','Go');
        $this->elementStart('form', array('id' => 'form_ostatus_connect',
                                          'method' => 'post',
                                          'class' => 'form_settings',
                                          'action' => common_local_url('ostatustag')));
        $this->elementStart('fieldset');
        $this->element('legend', null,  $header);
        $this->hidden('token', common_session_token());

        $this->elementStart('ul', 'form_data');
        $this->elementStart('li', array('id' => 'ostatus_nickname'));
        // TRANS: Field label.
        $this->input('nickname', _m('User nickname'), $this->nickname,
                     // TRANS: Field title.
                     _m('Nickname of the user you want to list.'));
        $this->elementEnd('li');
        $this->elementStart('li', array('id' => 'ostatus_profile'));
        // TRANS: Field label.
        $this->input('profile', _m('Profile Account'), $this->profile,
                     // TRANS: Field title.
                     _m('Your account id (for example user@example.com).'));
        $this->elementEnd('li');
        $this->elementEnd('ul');
        $this->submit('submit', $submit);
        $this->elementEnd('fieldset');
        $this->elementEnd('form');
    }

    function connectWebfinger($acct)
    {
        $target_profile = $this->targetProfile();

        $disco = new Discovery;
        $xrd = $disco->lookup($acct);

        $link = $xrd->get('http://ostatus.org/schema/1.0/tag');
        if (!is_null($link)) {
            // We found a URL - let's redirect!
            if (!empty($link->template)) {
                $url = Discovery::applyTemplate($link->template, $target_profile);
            } else {
                $url = $link->href;
            }
            common_log(LOG_INFO, "Sending remote subscriber $acct to $url");
            common_redirect($url, 303);
        }
        // TRANS: Client error displayed when remote profile address could not be confirmed.
        $this->clientError(_m('Could not 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/tagprofile', $subscriber_profile);
        $suburl .= '?uri=' . urlencode($target_profile);

        common_log(LOG_INFO, "Sending remote subscriber $subscriber_profile to $suburl");
        common_redirect($suburl, 303);
    }

    function title()
    {
      // TRANS: Title for an OStatus list.
      return _m('OStatus list');
    }
}


code:
<?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
 */

if (!defined('GNUSOCIAL')) { exit(1); }

class PeopletagsalmonAction extends SalmonAction
{
    var $peopletag = null;

    protected function prepare(array $args=array())
    {
        parent::prepare($args);

        $id = $this->trimmed('id');

        if (!$id) {
            // TRANS: Client error displayed trying to perform an action without providing an ID.
            $this->clientError(_m('No ID.'));
        }

        $this->peopletag = Profile_list::getKV('id', $id);

        if (!$this->peopletag instanceof Profile_list) {
            // TRANS: Client error displayed when referring to a non-existing list.
            $this->clientError(_m('No such list.'));
        }

        $this->target = $this->peopletag;

        $remote_list = Ostatus_profile::getKV('peopletag_id', $id);

        if ($remote_list instanceof Ostatus_profile) {
            // TRANS: Client error displayed when trying to send a message to a remote list.
            $this->clientError(_m('Cannot accept remote posts for a remote list.'));
        }

        return true;
    }

    /**
     * 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->handleSubscribe();
    }

    /**
     * Postel's law: consider an "unfollow" notification as a "unsubscribe".
     */
    function handleUnfollow()
    {
        $this->handleUnsubscribe();
    }

    /**
     * A remote user subscribed.
     * @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 handleSubscribe()
    {
        if ($this->oprofile->isGroup()) {
            // TRANS: Client error displayed when trying to subscribe a group to a list.
            $this->clientError(_m('Groups cannot subscribe to lists.'));
        }

        common_log(LOG_INFO, sprintf('Remote profile %s subscribing to local peopletag %s', $this->oprofile->getUri(), $this->peopletag->getBestName()));
        if ($this->peopletag->hasSubscriber($this->actor)) {
            // Already a member; we'll take it silently to aid in resolving
            // inconsistencies on the other side.
            return true;
        }

        // should we block those whom the tagger has blocked from listening to
        // his own updates?

        try {
            Profile_tag_subscription::add($this->peopletag, $this->actor);
        } catch (Exception $e) {
            // TRANS: Server error displayed when subscribing a remote user to a list fails.
            // TRANS: %1$s is a profile URI, %2$s is a list name.
            $this->serverError(sprintf(_m('Could not subscribe remote user %1$s to list %2$s.'),
                                       $this->oprofile->getUri(), $this->peopletag->getBestName()));
        }
    }

    /**
     * A remote user unsubscribed from our list.
     *
     * @return void
     * @throws Exception through clientError and serverError
     */
    function handleUnsubscribe()
    {
        if ($this->oprofile->isGroup()) {
            // TRANS: Client error displayed when trying to unsubscribe a group from a list.
            $this->clientError(_m('Groups cannot subscribe to lists.'));
        }

        common_log(LOG_INFO, sprintf('Remote profile %s unsubscribing from local peopletag %s', $this->oprofile->getUri(), $this->peopletag->getBestName()));
        try {
            Profile_tag_subscription::remove($this->peopletag->tagger, $this->peopletag->tag, $this->actor->id);
        } catch (Exception $e) {
            // TRANS: Client error displayed when trying to unsubscribe a remote user from a list fails.
            // TRANS: %1$s is a profile URL, %2$s is a list name.
            $this->serverError(sprintf(_m('Could not unsubscribe remote user %1$s from list %2$s.'),
                                       $this->oprofile->getUri(), $this->peopletag->getBestName()));
        }
    }
}


code:
<?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')) {
    exit(1);
}

class PushCallbackAction extends Action
{
    protected function handle()
    {
        GNUsocial::setApi(true); // Minimize error messages to aid in debugging
        parent::handle();
        if ($this->isPost()) {
            return $this->handlePost();
        }

        return $this->handleGet();
    }

    /**
     * Handler for POST content updates from the hub
     */
    function handlePost()
    {
        $feedid = $this->arg('feed');
        common_log(LOG_INFO, "POST for feed id $feedid");
        if (!$feedid) {
            // TRANS: Server exception thrown when referring to a non-existing or empty feed.
            throw new ServerException(_m('Empty or invalid feed id.'), 400);
        }

        $feedsub = FeedSub::getKV('id', $feedid);
        if (!$feedsub instanceof FeedSub) {
            // TRANS: Server exception. %s is a feed ID.
            throw new ServerException(sprintf(_m('Unknown WebSub subscription feed id %s'),$feedid), 400);
        }

        $hmac = '';
        if (isset($_SERVER['HTTP_X_HUB_SIGNATURE'])) {
            $hmac = $_SERVER['HTTP_X_HUB_SIGNATURE'];
        }

        $post = file_get_contents('php://input');

        // Queue this to a background process; we should return
        // as quickly as possible from a distribution POST.
        // 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.
     */
    public function handleGet()
    {
        $mode = $this->arg('hub_mode');
        $topic = $this->arg('hub_topic');
        $challenge = $this->arg('hub_challenge');
        $lease_seconds = $this->arg('hub_lease_seconds');   // Must be >0 for PuSH 0.4! And only checked on mode='subscribe' of course
        common_log(LOG_INFO, __METHOD__ . ": sub verification mode: $mode topic: $topic challenge: $challenge lease_seconds: $lease_seconds");

        if ($mode != 'subscribe' && $mode != 'unsubscribe') {
            // TRANS: Client exception. %s is an invalid value for hub.mode.
            throw new ClientException(sprintf(_m('Bad hub.mode "$s".',$mode)), 404);
        }

        $feedsub = FeedSub::getKV('uri', $topic);
        if (!$feedsub instanceof FeedSub) {
            // TRANS: Client exception. %s is an invalid feed name.
            throw new ClientException(sprintf(_m('Bad hub.topic feed "%s".'),$topic), 404);
        }

        if ($mode == 'subscribe') {
            // We may get re-sub requests legitimately.
            if ($feedsub->sub_state != 'subscribe' && $feedsub->sub_state != 'active') {
                // TRANS: Client exception. %s is an invalid topic.
                throw new ClientException(sprintf(_m('Unexpected subscribe request for %s.'),$topic), 404);
            }
        } else {
            if ($feedsub->sub_state != 'unsubscribe') {
                // TRANS: Client exception. %s is an invalid topic.
                throw new ClientException(sprintf(_m('Unexpected unsubscribe request for %s.'),$topic), 404);
            }
        }

        if ($mode == 'subscribe') {
            $renewal = ($feedsub->sub_state == 'active');
            if ($renewal) {
                common_log(LOG_INFO, __METHOD__ . ': sub update confirmed');
            } else {
                common_log(LOG_INFO, __METHOD__ . ': sub confirmed');
            }

            $feedsub->confirmSubscribe($lease_seconds);

            if (!$renewal) {
                // Kickstart the feed by importing its most recent backlog
                // FIXME: Disabled until we can either limit the amount and/or send to background queue handling
                //common_log(LOG_INFO, __METHOD__ . ': Confirmed a new subscription, importing backlog...');
                //$feedsub->importFeed();
            }
        } else {
            common_log(LOG_INFO, __METHOD__ . ": unsub confirmed; deleting sub record for $topic");
            $feedsub->confirmUnsubscribe();
        }
        print $challenge;
    }
}


code:
<?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/>.
 */

/**
 * Integrated WebSub hub; lets us only ping them what need it.
 * @package Hub
 * @maintainer Brion Vibber <brion@status.net>
 */

if (!defined('STATUSNET')) {
    exit(1);
}

/**
 * Things to consider...
 * should we purge incomplete subscriptions that never get a verification pingback?
 * when can we send subscription renewal checks?
 *    - at next send time probably ok
 * when can we handle trimming of subscriptions?
 *    - at next send time probably ok
 * should we keep a fail count?
 */
class PushHubAction extends Action
{
    function arg($arg, $def=null)
    {
        // PHP converts '.'s in incoming var names to '_'s.
        // It also merges multiple values, which'll break hub.verify and hub.topic for publishing
        // @fixme handle multiple args
        $arg = str_replace('hub.', 'hub_', $arg);
        return parent::arg($arg, $def);
    }

    protected function prepare(array $args=array())
    {
        GNUsocial::setApi(true); // reduce exception reports to aid in debugging
        return parent::prepare($args);
    }

    protected function handle()
    {
        $mode = $this->trimmed('hub.mode');
        switch ($mode) {
        case "subscribe":
        case "unsubscribe":
            $this->subunsub($mode);
            break;
        case "publish":
            // TRANS: Client exception.
            throw new ClientException(_m('Publishing outside feeds not supported.'), 400);
        default:
            // TRANS: Client exception. %s is a mode.
            throw new ClientException(sprintf(_m('Unrecognized mode "%s".'),$mode), 400);
        }
    }

    /**
     * Process a request for a new or modified WebSub 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
     *   400 Bad Request - rejecting this (not specifically spec'd)
     */
    function subunsub($mode)
    {
        $callback = $this->argUrl('hub.callback');

        common_debug('New WebSub hub request ('._ve($mode).') for callback '._ve($callback));
        $topic = $this->argUrl('hub.topic');
        if (!$this->recognizedFeed($topic)) {
            common_debug('WebSub hub request had unrecognized feed topic=='._ve($topic));
            // TRANS: Client exception. %s is a topic.
            throw new ClientException(sprintf(_m('Unsupported hub.topic %s this hub only serves local user and group Atom feeds.'),$topic));
        }

        $lease = $this->arg('hub.lease_seconds', null);
        if ($mode == 'subscribe' && $lease != '' && !preg_match('/^\d+$/', $lease)) {
            common_debug('WebSub hub request had invalid lease_seconds=='._ve($lease));
            // TRANS: Client exception. %s is the invalid lease value.
            throw new ClientException(sprintf(_m('Invalid hub.lease "%s". It must be empty or positive integer.'),$lease));
        }

        $secret = $this->arg('hub.secret', null);
        if ($secret != '' && strlen($secret) >= 200) {
            common_debug('WebSub hub request had invalid secret=='._ve($secret));
            // TRANS: Client exception. %s is the invalid hub secret.
            throw new ClientException(sprintf(_m('Invalid hub.secret "%s". It must be under 200 bytes.'),$secret));
        }

        $sub = HubSub::getByHashkey($topic, $callback);
        if (!$sub instanceof HubSub) {
            // Creating a new one!
            common_debug('WebSub creating new HubSub entry for topic=='._ve($topic).' to remote callback '._ve($callback));
            $sub = new HubSub();
            $sub->topic = $topic;
            $sub->callback = $callback;
        }
        if ($mode == 'subscribe') {
            if ($secret) {
                $sub->secret = $secret;
            }
            if ($lease) {
                $sub->setLease(intval($lease));
            }
        }

        $verify = $this->arg('hub.verify'); // TODO: deprecated
        $token = $this->arg('hub.verify_token', null);  // TODO: deprecated
        if ($verify == 'sync') {    // pre-0.4 PuSH
            $sub->verify($mode, $token);
            header('HTTP/1.1 204 No Content');
        } else {    // If $verify is not "sync", we might be using WebSub or PuSH 0.4
            $sub->scheduleVerify($mode, $token);    // If we were certain it's WebSub or PuSH 0.4, token could be removed
            header('HTTP/1.1 202 Accepted');
        }
    }

    /**
     * Check whether the given URL represents one of our canonical
     * user or group Atom feeds.
     *
     * @param string $feed URL
     * @return boolean true if it matches, false if not a recognized local feed
     * @throws exception if local entity does not exist
     */
    protected function recognizedFeed($feed)
    {
        $matches = array();
        // Simple mapping to local ID for user or group
        if (preg_match('!/(\d+)\.atom$!', $feed, $matches)) {
            $id = $matches[1];
            $params = array('id' => $id, 'format' => 'atom');

            // Double-check against locally generated URLs
            switch ($feed) {
            case common_local_url('ApiTimelineUser', $params):
                $user = User::getKV('id', $id);
                if (!$user instanceof User) {
                    // TRANS: Client exception. %s is a feed URL.
                    throw new ClientException(sprintf(_m('Invalid hub.topic "%s". User does not exist.'),$feed));
                }
                return true;

            case common_local_url('ApiTimelineGroup', $params):
                $group = Local_group::getKV('group_id', $id);
                if (!$group instanceof Local_group) {
                    // TRANS: Client exception. %s is a feed URL.
                    throw new ClientException(sprintf(_m('Invalid hub.topic "%s". Local_group does not exist.'),$feed));
                }
                return true;
            }
            common_debug("Feed was not recognized by any local User or Group Atom feed URLs: {$feed}");
            return false;
        }

        // Profile lists are unique per user, so we need both IDs
        if (preg_match('!/(\d+)/lists/(\d+)/statuses\.atom$!', $feed, $matches)) {
            $user = $matches[1];
            $id = $matches[2];
            $params = array('user' => $user, 'id' => $id, 'format' => 'atom');

            // Double-check against locally generated URLs
            switch ($feed) {
            case common_local_url('ApiTimelineList', $params):
                $list = Profile_list::getKV('id', $id);
                $user = User::getKV('id', $user);
                if (!$list instanceof Profile_list || !$user instanceof User || $list->tagger != $user->id) {
                    // TRANS: Client exception. %s is a feed URL.
                    throw new ClientException(sprintf(_m('Invalid hub.topic %s; list does not exist.'),$feed));
                }
                return true;
            }
            common_debug("Feed was not recognized by any local Profile_list Atom feed URL: {$feed}");
            return false;
        }

        common_debug("Unknown feed URL structure, can't match against local user, group or profile_list: {$feed}");
        return false;
    }

    /**
     * Grab and validate a URL from POST parameters.
     * @throws ClientException for malformed or non-http/https or blacklisted URLs
     */
    protected function argUrl($arg)
    {
        $url = $this->arg($arg);
        $params = array('domain_check' => false, // otherwise breaks my local tests :P
                        'allowed_schemes' => array('http', 'https'));
        $validate = new Validate();
        if (!$validate->uri($url, $params)) {
            // TRANS: Client exception.
            // TRANS: %1$s is this argument to the method this exception occurs in, %2$s is a URL.
            throw new ClientException(sprintf(_m('Invalid URL passed for %1$s: "%2$s"'),$arg,$url));
        }

        Event::handle('UrlBlacklistTest', array($url));
        return $url;
    }

    /**
     * Get HubSub subscription record for a given feed & subscriber.
     *
     * @param string $feed
     * @param string $callback
     * @return mixed HubSub or false
     */
    protected function getSub($feed, $callback)
    {
        return HubSub::getByHashkey($feed, $callback);
    }
}


code:
<?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/>.
 */

if (!defined('GNUSOCIAL')) { exit(1); }

/**
 * @package OStatusPlugin
 * @author James Walker <james@status.net>
 */
class UsersalmonAction extends SalmonAction
{
    protected function prepare(array $args=array())
    {
        parent::prepare($args);

        $this->user = User::getByID($this->trimmed('id'));

        $this->target = $this->user->getProfile();

        // Notice must either be a) in reply to a notice by this user
        // or b) in reply to a notice to the attention of this user
        // or c) to the attention of this user
        // or d) reference the user as an activity:object

        $notice = null;

        if (!empty($this->activity->context->replyToID)) {
            try {
                $notice = Notice::getByUri($this->activity->context->replyToID);
                common_debug('Referenced Notice object found with URI: '.$notice->getUri());
            } catch (NoResultException $e) {
                common_debug('Referenced Notice object NOT found with URI: '.$this->activity->context->replyToID);
                $notice = false;
            }
        }

        if ($notice instanceof Notice &&
                ($this->target->sameAs($notice->getProfile())
                    || in_array($this->target->getID(), $notice->getAttentionProfileIDs())
                )) {
            // In reply to a notice either from or mentioning this user.
            common_debug('User is the owner or was in the attention list of thr:in-reply-to activity.');
        } elseif (!empty($this->activity->context->attention) &&
                   array_key_exists($this->target->getUri(), $this->activity->context->attention)) {
            // To the attention of this user.
            common_debug('User was in attention list of salmon slap.');
        } elseif (!empty($this->activity->objects) && $this->activity->objects[0]->id === $this->target->getUri()) {
            // The user is the object of this slap (unfollow for example)
            common_debug('User URI was the id of the salmon slap object.');
        } else {
            common_debug('User was NOT found in salmon slap context.');
            // TRANS: Client exception.
            throw new ClientException(_m('The owner of this salmon endpoint was not in the context of the carried slap.'));
        }

        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->activity->objects[0]->id}' from '{$this->activity->actor->id}'");

        // @fixme: process all activity objects?
        switch ($this->activity->objects[0]->type) {
        case ActivityObject::ARTICLE:
        case ActivityObject::BLOGENTRY:
        case ActivityObject::NOTE:
        case ActivityObject::STATUS:
        case ActivityObject::COMMENT:
            break;
        default:
            // TRANS: Client exception thrown when an undefied activity is performed.
            throw new ClientException(_m('Cannot handle that kind of post.'));
        }

        try {
            $this->saveNotice();
        } catch (AlreadyFulfilledException $e) {
            return;
        }
    }

    /**
     * We've gotten a follow/subscribe notification from a remote user.
     * Save a subscription relationship for them.
     */
    function handleFollow()
    {
        common_log(LOG_INFO, sprintf('Setting up subscription from remote %s to local %s', $this->oprofile->getUri(), $this->target->getNickname()));
        Subscription::start($this->actor, $this->target);
    }

    /**
     * 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()
    {
        common_log(LOG_INFO, sprintf('Canceling subscription from remote %s to local %s', $this->oprofile->getUri(), $this->target->getNickname()));
        try {
            Subscription::cancel($this->actor, $this->target);
        } catch (NoProfileException $e) {
            common_debug('Could not find profile for Subscription: '.$e->getMessage());
        }
    }

    function handleTag()
    {
        if ($this->activity->target->type == ActivityObject::_LIST) {
            if ($this->activity->objects[0]->type != ActivityObject::PERSON) {
                // TRANS: Client exception.
                throw new ClientException(_m('Not a person object.'));
            }
            // this is a peopletag
            $tagged = User::getKV('uri', $this->activity->objects[0]->id);

            if (!$tagged instanceof User) {
                // TRANS: Client exception.
                throw new ClientException(_m('Unidentified profile being listed.'));
            }

            if ($tagged->id !== $this->target->id) {
                // TRANS: Client exception.
                throw new ClientException(_m('This user is not the one being listed.'));
            }

            // save the list
            $list   = Ostatus_profile::ensureActivityObjectProfile($this->activity->target);

            $ptag = $list->localPeopletag();
            $result = Profile_tag::setTag($ptag->tagger, $tagged->id, $ptag->tag);
            if (!$result) {
                // TRANS: Client exception.
                throw new ClientException(_m('The listing could not be saved.'));
            }
        }
    }

    function handleUntag()
    {
        if ($this->activity->target->type == ActivityObject::_LIST) {
            if ($this->activity->objects[0]->type != ActivityObject::PERSON) {
                // TRANS: Client exception.
                throw new ClientException(_m('Not a person object.'));
            }
            // this is a peopletag
            $tagged = User::getKV('uri', $this->activity->objects[0]->id);

            if (!$tagged instanceof User) {
                // TRANS: Client exception.
                throw new ClientException(_m('Unidentified profile being unlisted.'));
            }

            if ($tagged->id !== $this->target->id) {
                // TRANS: Client exception.
                throw new ClientException(_m('This user is not the one being unlisted.'));
            }

            // save the list
            $list   = Ostatus_profile::ensureActivityObjectProfile($this->activity->target);

            $ptag = $list->localPeopletag();
            $result = Profile_tag::unTag($ptag->tagger, $tagged->id, $ptag->tag);

            if (!$result) {
                // TRANS: Client exception.
                throw new ClientException(_m('The listing could not be deleted.'));
            }
        }
    }

    /**
     * @param ActivityObject $object
     * @return Notice
     * @throws ClientException on invalid input
     */
    function getNotice(ActivityObject $object)
    {
        switch ($object->type) {
        case ActivityObject::ARTICLE:
        case ActivityObject::BLOGENTRY:
        case ActivityObject::NOTE:
        case ActivityObject::STATUS:
        case ActivityObject::COMMENT:
            break;
        default:
            // TRANS: Client exception.
            throw new ClientException(_m('Cannot handle that kind of object for liking/faving.'));
        }

        $notice = Notice::getKV('uri', $object->id);

        if (!$notice instanceof Notice) {
            // TRANS: Client exception. %s is an object ID.
            throw new ClientException(sprintf(_m('Notice with ID %s unknown.'),$object->id));
        }

        if ($notice->profile_id != $this->target->id) {
            // TRANS: Client exception. %1$s is a notice ID, %2$s is a user ID.
            throw new ClientException(sprintf(_m('Notice with ID %1$s not posted by %2$s.'), $object->id, $this->target->id));
        }

        return $notice;
    }
}