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 PuSH 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.
     */
    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!
        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') {
            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");
            $feedsub->confirmUnsubscribe();
        }
        print $challenge;
    }
}

repo_name: piratas/tortuga

license: mit


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 PuSH 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 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
     *   400 Bad Request - rejecting this (not specifically spec'd)
     */
    function subunsub($mode)
    {
        $callback = $this->argUrl('hub.callback');

        $topic = $this->argUrl('hub.topic');
        if (!$this->recognizedFeed($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)) {
            // 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) {
            // 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!
            $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 PuSH 0.4
            $sub->scheduleVerify($mode, $token);    // If we were certain it's 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 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)) {
            return $url;
        } else {
            // 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));
        }
    }

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

        $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->user = User::getKV('id', $id);

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

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

        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.'));
        }

        // 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

        $context = $this->activity->context;
        $notice = false;

        if (!empty($context->replyToID)) {
            $notice = Notice::getKV('uri', $context->replyToID);
        }

        if ($notice instanceof Notice &&
            ($notice->profile_id == $this->target->id ||
             array_key_exists($this->target->id, $notice->getReplies())))
        {
            // In reply to a notice either from or mentioning this user.
        } elseif (!empty($context->attention) &&
                   array_key_exists($this->target->getUri(), $context->attention)) {
            // To the attention of this user.
        } else {
            // TRANS: Client exception.
            throw new ClientException(_m('Not to anyone in reply to anything.'));
        }

        $existing = Notice::getKV('uri', $this->activity->objects[0]->id);
        if ($existing instanceof Notice) {
            common_log(LOG_ERR, "Not saving notice with duplicate URI '".$existing->getUri()."' (seems it already exists).");
            return;
        }

        $this->saveNotice();
    }

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


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/>.
 */

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

/**
 * @package OStatusPlugin
 * @maintainer Brion Vibber <brion@status.net>
 */

/*
PuSH subscription flow:

    $profile->subscribe()
        sends a sub request to the hub...

    main/push/callback
        hub sends confirmation back to us via GET
        We verify the request, then echo back the challenge.
        On our end, we save the time we subscribed and the lease expiration

    main/push/callback
        hub sends us updates via POST

*/
class FeedDBException extends FeedSubException
{
    public $obj;

    function __construct($obj)
    {
        parent::__construct('Database insert failure');
        $this->obj = $obj;
    }
}

/**
 * FeedSub handles low-level PubHubSubbub (PuSH) subscriptions.
 * Higher-level behavior building OStatus stuff on top is handled
 * under Ostatus_profile.
 */
class FeedSub extends Managed_DataObject
{
    public $__table = 'feedsub';

    public $id;
    public $uri;    // varchar(191)   not 255 because utf8mb4 takes more space

    // PuSH subscription data
    public $huburi;
    public $secret;
    public $sub_state; // subscribe, active, unsubscribe, inactive, nohub
    public $sub_start;
    public $sub_end;
    public $last_update;

    public $created;
    public $modified;

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'id' => array('type' => 'serial', 'not null' => true, 'description' => 'FeedSub local unique id'),
                'uri' => array('type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'FeedSub uri'),
                'huburi' => array('type' => 'text', 'description' => 'FeedSub hub-uri'),
                'secret' => array('type' => 'text', 'description' => 'FeedSub stored secret'),
                'sub_state' => array('type' => 'enum("subscribe","active","unsubscribe","inactive","nohub")', 'not null' => true, 'description' => 'subscription state'),
                'sub_start' => array('type' => 'datetime', 'description' => 'subscription start'),
                'sub_end' => array('type' => 'datetime', 'description' => 'subscription end'),
                'last_update' => array('type' => 'datetime', 'description' => 'when this record was last updated'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('id'),
            'unique keys' => array(
                'feedsub_uri_key' => array('uri'),
            ),
        );
    }

    /**
     * Get the feed uri (http/https)
     */
    public function getUri()
    {
        if (empty($this->uri)) {
            throw new NoUriException($this);
        }
        return $this->uri;
    }

    /**
     * Do we have a hub? Then we are a PuSH feed.
     * https://en.wikipedia.org/wiki/PubSubHubbub
     *
     * If huburi is empty, then doublecheck that we are not using
     * a fallback hub. If there is a fallback hub, it is only if the
     * sub_state is "nohub" that we assume it's not a PuSH feed.
     */
    public function isPuSH()
    {
        if (empty($this->huburi)
                && (!common_config('feedsub', 'fallback_hub')
                    || $this->sub_state === 'nohub')) {
                // Here we have no huburi set. Also, either there is no
                // fallback hub configured or sub_state is "nohub".
            return false;
        }
        return true;
    }

    /**
     * Fetch the StatusNet-side profile for this feed
     * @return Profile
     */
    public function localProfile()
    {
        if ($this->profile_id) {
            return Profile::getKV('id', $this->profile_id);
        }
        return null;
    }

    /**
     * Fetch the StatusNet-side profile for this feed
     * @return Profile
     */
    public function localGroup()
    {
        if ($this->group_id) {
            return User_group::getKV('id', $this->group_id);
        }
        return null;
    }

    /**
     * @param string $feeduri
     * @return FeedSub
     * @throws FeedSubException if feed is invalid or lacks PuSH setup
     */
    public static function ensureFeed($feeduri)
    {
        $current = self::getKV('uri', $feeduri);
        if ($current instanceof FeedSub) {
            return $current;
        }

        $discover = new FeedDiscovery();
        $discover->discoverFromFeedURL($feeduri);

        $huburi = $discover->getHubLink();
        if (!$huburi && !common_config('feedsub', 'fallback_hub') && !common_config('feedsub', 'nohub')) {
            throw new FeedSubNoHubException();
        }

        $feedsub = new FeedSub();
        $feedsub->uri = $feeduri;
        $feedsub->huburi = $huburi;
        $feedsub->sub_state = 'inactive';

        $feedsub->created = common_sql_now();
        $feedsub->modified = common_sql_now();

        $result = $feedsub->insert();
        if ($result === false) {
            throw new FeedDBException($feedsub);
        }

        return $feedsub;
    }

    /**
     * Send a subscription request to the hub for this feed.
     * The hub will later send us a confirmation POST to /main/push/callback.
     *
     * @return void
     * @throws ServerException if feed state is not valid
     */
    public function subscribe()
    {
        if ($this->sub_state && $this->sub_state != 'inactive') {
            common_log(LOG_WARNING, sprintf('Attempting to (re)start PuSH subscription to %s in unexpected state %s', $this->getUri(), $this->sub_state));
        }

        if (!Event::handle('FeedSubscribe', array($this))) {
            // A plugin handled it
            return;
        }

        if (empty($this->huburi)) {
            if (common_config('feedsub', 'fallback_hub')) {
                // No native hub on this feed?
                // Use our fallback hub, which handles polling on our behalf.
            } else if (common_config('feedsub', 'nohub')) {
                // For this to actually work, we'll need some polling mechanism.
                // The FeedPoller plugin should take care of it.
                return;
            } else {
                // TRANS: Server exception.
                throw new ServerException(_m('Attempting to start PuSH subscription for feed with no hub.'));
            }
        }

        $this->doSubscribe('subscribe');
    }

    /**
     * Send a PuSH unsubscription request to the hub for this feed.
     * The hub will later send us a confirmation POST to /main/push/callback.
     * Warning: this will cancel the subscription even if someone else in
     * the system is using it. Most callers will want garbageCollect() instead,
     * which confirms there's no uses left.
     *
     * @throws ServerException if feed state is not valid
     */
    public function unsubscribe() {
        if ($this->sub_state != 'active') {
            common_log(LOG_WARNING, sprintf('Attempting to (re)end PuSH subscription to %s in unexpected state %s', $this->getUri(), $this->sub_state));
        }

        if (!Event::handle('FeedUnsubscribe', array($this))) {
            // A plugin handled it
            return;
        }

        if (empty($this->huburi)) {
            if (common_config('feedsub', 'fallback_hub')) {
                // No native hub on this feed?
                // Use our fallback hub, which handles polling on our behalf.
            } else if (common_config('feedsub', 'nohub')) {
                // We need a feedpolling plugin (like FeedPoller) active so it will
                // set the 'nohub' state to 'inactive' for us.
                return;
            } else {
                // TRANS: Server exception.
                throw new ServerException(_m('Attempting to end PuSH subscription for feed with no hub.'));
            }
        }

        $this->doSubscribe('unsubscribe');
    }

    /**
     * Check if there are any active local uses of this feed, and if not then
     * make sure it's inactive, unsubscribing if necessary.
     *
     * @return boolean true if the subscription is now inactive, false if still active.
     * @throws NoProfileException in FeedSubSubscriberCount for missing Profile entries
     * @throws Exception if something goes wrong in unsubscribe() method
     */
    public function garbageCollect()
    {
        if ($this->sub_state == '' || $this->sub_state == 'inactive') {
            // No active PuSH subscription, we can just leave it be.
            return true;
        }

        // PuSH subscription is either active or in an indeterminate state.
        // Check if we're out of subscribers, and if so send an unsubscribe.
        $count = 0;
        Event::handle('FeedSubSubscriberCount', array($this, &$count));

        if ($count > 0) {
            common_log(LOG_INFO, __METHOD__ . ': ok, ' . $count . ' user(s) left for ' . $this->getUri());
            return false;
        }

        common_log(LOG_INFO, __METHOD__ . ': unsubscribing, no users left for ' . $this->getUri());
        // Unsubscribe throws various Exceptions on failure
        $this->unsubscribe();

        return true;
    }

    static public function renewalCheck()
    {
        $fs = new FeedSub();
        // the "" empty string check is because we historically haven't saved unsubscribed feeds as NULL
        $fs->whereAdd('sub_end IS NOT NULL AND sub_end!="" AND sub_end < NOW() - INTERVAL 1 day');
        if (!$fs->find()) { // find can be both false and 0, depending on why nothing was found
            throw new NoResultException($fs);
        }
        return $fs;
    }

    public function renew()
    {
        $this->subscribe();
    }

    /**
     * Setting to subscribe means it is _waiting_ to become active. This
     * cannot be done in a transaction because there is a chance that the
     * remote script we're calling (as in the case of PuSHpress) performs
     * the lookup _while_ we're POSTing data, which means the transaction
     * never completes (PushcallbackAction gets an 'inactive' state).
     *
     * @return boolean true when everything is ok (throws Exception on fail)
     * @throws Exception on failure, can be HTTPClient's or our own.
     */
    protected function doSubscribe($mode)
    {
        $orig = clone($this);
        if ($mode == 'subscribe') {
            $this->secret = common_random_hexstr(32);
        }
        $this->sub_state = $mode;
        $this->update($orig);
        unset($orig);

        try {
            $callback = common_local_url('pushcallback', array('feed' => $this->id));
            $headers = array('Content-Type: application/x-www-form-urlencoded');
            $post = array('hub.mode' => $mode,
                          'hub.callback' => $callback,
                          'hub.verify' => 'async',  // TODO: deprecated, remove when noone uses PuSH <0.4 (only 'async' method used there)
                          'hub.verify_token' => 'Deprecated-since-PuSH-0.4', // TODO: rm!

                          'hub.secret' => $this->secret,
                          'hub.topic' => $this->getUri());
            $client = new HTTPClient();
            if ($this->huburi) {
                $hub = $this->huburi;
            } else {
                if (common_config('feedsub', 'fallback_hub')) {
                    $hub = common_config('feedsub', 'fallback_hub');
                    if (common_config('feedsub', 'hub_user')) {
                        $u = common_config('feedsub', 'hub_user');
                        $p = common_config('feedsub', 'hub_pass');
                        $client->setAuth($u, $p);
                    }
                } else {
                    throw new FeedSubException('Server could not find a usable PuSH hub.');
                }
            }
            $response = $client->post($hub, $headers, $post);
            $status = $response->getStatus();
            // PuSH specificed response status code
            if ($status == 202) {
                common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
                return;
            } else if ($status >= 200 && $status < 300) {
                common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
            } else {
                common_log(LOG_ERR, __METHOD__ . ": sub req failed with HTTP $status: " . $response->getBody());
            }
        } catch (Exception $e) {
            common_log(LOG_ERR, __METHOD__ . ": error \"{$e->getMessage()}\" hitting hub {$this->huburi} subscribing to {$this->getUri()}");

            // Reset the subscription state.
            $orig = clone($this);
            $this->sub_state = 'inactive';
            $this->update($orig);

            // Throw the Exception again.
            throw $e;
        }
        throw new ServerException("{$mode} request failed.");
    }

    /**
     * Save PuSH subscription confirmation.
     * Sets approximate lease start and end times and finalizes state.
     *
     * @param int $lease_seconds provided hub.lease_seconds parameter, if given
     */
    public function confirmSubscribe($lease_seconds)
    {
        $original = clone($this);

        $this->sub_state = 'active';
        $this->sub_start = common_sql_date(time());
        if ($lease_seconds > 0) {
            $this->sub_end = common_sql_date(time() + $lease_seconds);
        } else {
            $this->sub_end = null;  // Backwards compatibility to StatusNet (PuSH <0.4 supported permanent subs)
        }
        $this->modified = common_sql_now();

        return $this->update($original);
    }

    /**
     * Save PuSH unsubscription confirmation.
     * Wipes active PuSH sub info and resets state.
     */
    public function confirmUnsubscribe()
    {
        $original = clone($this);

        // @fixme these should all be null, but DB_DataObject doesn't save null values...?????
        $this->secret = '';
        $this->sub_state = '';
        $this->sub_start = '';
        $this->sub_end = '';
        $this->modified = common_sql_now();

        return $this->update($original);
    }

    /**
     * Accept updates from a PuSH feed. If validated, this object and the
     * feed (as a DOMDocument) will be passed to the StartFeedSubHandleFeed
     * and EndFeedSubHandleFeed events for processing.
     *
     * Not guaranteed to be running in an immediate POST context; may be run
     * from a queue handler.
     *
     * Side effects: the feedsub record's lastupdate field will be updated
     * to the current time (not published time) if we got a legit update.
     *
     * @param string $post source of Atom or RSS feed
     * @param string $hmac X-Hub-Signature header, if present
     */
    public function receive($post, $hmac)
    {
        common_log(LOG_INFO, __METHOD__ . ": packet for \"" . $this->getUri() . "\"! $hmac $post");

        if (!in_array($this->sub_state, array('active', 'nohub'))) {
            common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH for inactive feed " . $this->getUri() . " (in state '$this->sub_state')");
            return;
        }

        if ($post === '') {
            common_log(LOG_ERR, __METHOD__ . ": ignoring empty post");
            return;
        }

        if (!$this->validatePushSig($post, $hmac)) {
            // Per spec we silently drop input with a bad sig,
            // while reporting receipt to the server.
            return;
        }

        $feed = new DOMDocument();
        if (!$feed->loadXML($post)) {
            // @fixme might help to include the err message
            common_log(LOG_ERR, __METHOD__ . ": ignoring invalid XML");
            return;
        }

        $orig = clone($this);
        $this->last_update = common_sql_now();
        $this->update($orig);

        Event::handle('StartFeedSubReceive', array($this, $feed));
        Event::handle('EndFeedSubReceive', array($this, $feed));
    }

    /**
     * Validate the given Atom chunk and HMAC signature against our
     * shared secret that was set up at subscription time.
     *
     * If we don't have a shared secret, there should be no signature.
     * If we we do, our the calculated HMAC should match theirs.
     *
     * @param string $post raw XML source as POSTed to us
     * @param string $hmac X-Hub-Signature HTTP header value, or empty
     * @return boolean true for a match
     */
    protected function validatePushSig($post, $hmac)
    {
        if ($this->secret) {
            if (preg_match('/^sha1=([0-9a-fA-F]{40})$/', $hmac, $matches)) {
                $their_hmac = strtolower($matches[1]);
                $our_hmac = hash_hmac('sha1', $post, $this->secret);
                if ($their_hmac === $our_hmac) {
                    return true;
                }
                if (common_config('feedsub', 'debug')) {
                    $tempfile = tempnam(sys_get_temp_dir(), 'feedsub-receive');
                    if ($tempfile) {
                        file_put_contents($tempfile, $post);
                    }
                    common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac for feed " . $this->getUri() . " on $this->huburi; saved to $tempfile");
                } else {
                    common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bad SHA-1 HMAC: got $their_hmac, expected $our_hmac for feed " . $this->getUri() . " on $this->huburi");
                }
            } else {
                common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with bogus HMAC '$hmac'");
            }
        } else {
            if (empty($hmac)) {
                return true;
            } else {
                common_log(LOG_ERR, __METHOD__ . ": ignoring PuSH with unexpected HMAC '$hmac'");
            }
        }
        return false;
    }

    public function delete($useWhere=false)
    {
        try {
            $oprofile = Ostatus_profile::getKV('feeduri', $this->getUri());
            if ($oprofile instanceof Ostatus_profile) {
                // Check if there's a profile. If not, handle the NoProfileException below
                $profile = $oprofile->localProfile();
            }
        } catch (NoProfileException $e) {
            // If the Ostatus_profile has no local Profile bound to it, let's clean it out at the same time
            $oprofile->delete();
        } catch (NoUriException $e) {
            // FeedSub->getUri() can throw a NoUriException, let's just go ahead and delete it
        }
        return parent::delete($useWhere);
    }
}


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

/**
 * PuSH feed subscription record
 * @package Hub
 * @author Brion Vibber <brion@status.net>
 */
class HubSub extends Managed_DataObject
{
    public $__table = 'hubsub';

    public $hashkey; // sha1(topic . '|' . $callback); (topic, callback) key is too long for myisam in utf8
    public $topic;      // varchar(191)   not 255 because utf8mb4 takes more space
    public $callback;   // varchar(191)   not 255 because utf8mb4 takes more space
    public $secret;
    public $lease;
    public $sub_start;
    public $sub_end;
    public $created;
    public $modified;

    protected static function hashkey($topic, $callback)
    {
        return sha1($topic . '|' . $callback);
    }

    public static function getByHashkey($topic, $callback)
    {
        return self::getKV('hashkey', self::hashkey($topic, $callback));
    }

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'hashkey' => array('type' => 'char', 'not null' => true, 'length' => 40, 'description' => 'HubSub hashkey'),
                'topic' => array('type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'HubSub topic'),
                'callback' => array('type' => 'varchar', 'not null' => true, 'length' => 191, 'description' => 'HubSub callback'),
                'secret' => array('type' => 'text', 'description' => 'HubSub stored secret'),
                'lease' => array('type' => 'int', 'description' => 'HubSub leasetime'),
                'sub_start' => array('type' => 'datetime', 'description' => 'subscription start'),
                'sub_end' => array('type' => 'datetime', 'description' => 'subscription end'),
                'created' => array('type' => 'datetime', 'not null' => true, 'description' => 'date this record was created'),
                'modified' => array('type' => 'timestamp', 'not null' => true, 'description' => 'date this record was modified'),
            ),
            'primary key' => array('hashkey'),
            'indexes' => array(
                'hubsub_topic_idx' => array('topic'),
            ),
        );
    }

    /**
     * Validates a requested lease length, sets length plus
     * subscription start & end dates.
     *
     * Does not save to database -- use before insert() or update().
     *
     * @param int $length in seconds
     */
    function setLease($length)
    {
        assert(is_int($length));

        $min = 86400;
        $max = 86400 * 30;

        if ($length == 0) {
            // We want to garbage collect dead subscriptions!
            $length = $max;
        } elseif( $length < $min) {
            $length = $min;
        } else if ($length > $max) {
            $length = $max;
        }

        $this->lease = $length;
        $this->start_sub = common_sql_now();
        $this->end_sub = common_sql_date(time() + $length);
    }

    /**
     * Schedule a future verification ping to the subscriber.
     * If queues are disabled, will be immediate.
     *
     * @param string $mode 'subscribe' or 'unsubscribe'
     * @param string $token hub.verify_token value, if provided by client
     */
    function scheduleVerify($mode, $token=null, $retries=null)
    {
        if ($retries === null) {
            $retries = intval(common_config('ostatus', 'hub_retries'));
        }
        $data = array('sub' => clone($this),
                      'mode' => $mode,
                      'token' => $token,    // let's put it in there if remote uses PuSH <0.4
                      'retries' => $retries);
        $qm = QueueManager::get();
        $qm->enqueue($data, 'hubconf');
    }

    /**
     * Send a verification ping to subscriber, and if confirmed apply the changes.
     * This may create, update, or delete the database record.
     *
     * @param string $mode 'subscribe' or 'unsubscribe'
     * @param string $token hub.verify_token value, if provided by client
     * @throws ClientException on failure
     */
    function verify($mode, $token=null)
    {
        assert($mode == 'subscribe' || $mode == 'unsubscribe');

        $challenge = common_random_hexstr(32);
        $params = array('hub.mode' => $mode,
                        'hub.topic' => $this->topic,
                        'hub.challenge' => $challenge);
        if ($mode == 'subscribe') {
            $params['hub.lease_seconds'] = $this->lease;
        }
        if ($token !== null) {  // TODO: deprecated in PuSH 0.4
            $params['hub.verify_token'] = $token;   // let's put it in there if remote uses PuSH <0.4
        }

        // Any existing query string parameters must be preserved
        $url = $this->callback;
        if (strpos($url, '?') !== false) {
            $url .= '&';
        } else {
            $url .= '?';
        }
        $url .= http_build_query($params, '', '&');

        $request = new HTTPClient();
        $response = $request->get($url);
        $status = $response->getStatus();

        if ($status >= 200 && $status < 300) {
            common_log(LOG_INFO, "Verified {$mode} of {$this->callback}:{$this->topic}");
        } else {
            // TRANS: Client exception. %s is a HTTP status code.
            throw new ClientException(sprintf(_m('Hub subscriber verification returned HTTP %s.'),$status));
        }

        $old = HubSub::getByHashkey($this->topic, $this->callback);
        if ($mode == 'subscribe') {
            if ($old instanceof HubSub) {
                $this->update($old);
            } else {
                $ok = $this->insert();
            }
        } else if ($mode == 'unsubscribe') {
            if ($old instanceof HubSub) {
                $old->delete();
            } else {
                // That's ok, we're already unsubscribed.
            }
        }
    }

    /**
     * Insert wrapper; transparently set the hash key from topic and callback columns.
     * @return mixed success
     */
    function insert()
    {
        $this->hashkey = self::hashkey($this->topic, $this->callback);
        $this->created = common_sql_now();
        $this->modified = common_sql_now();
        return parent::insert();
    }

    /**
     * Schedule delivery of a 'fat ping' to the subscriber's callback
     * endpoint. If queues are disabled, this will run immediately.
     *
     * @param string $atom well-formed Atom feed
     * @param int $retries optional count of retries if POST fails; defaults to hub_retries from config or 0 if unset
     */
    function distribute($atom, $retries=null)
    {
        if ($retries === null) {
            $retries = intval(common_config('ostatus', 'hub_retries'));
        }

        // We dare not clone() as when the clone is discarded it'll
        // destroy the result data for the parent query.
        // @fixme use clone() again when it's safe to copy an
        // individual item from a multi-item query again.
        $sub = HubSub::getByHashkey($this->topic, $this->callback);
        $data = array('sub' => $sub,
                      'atom' => $atom,
                      'retries' => $retries);
        common_log(LOG_INFO, "Queuing PuSH: $this->topic to $this->callback");
        $qm = QueueManager::get();
        $qm->enqueue($data, 'hubout');
    }

    /**
     * Queue up a large batch of pushes to multiple subscribers
     * for this same topic update.
     *
     * If queues are disabled, this will run immediately.
     *
     * @param string $atom well-formed Atom feed
     * @param array $pushCallbacks list of callback URLs
     */
    function bulkDistribute($atom, $pushCallbacks)
    {
        $data = array('atom' => $atom,
                      'topic' => $this->topic,
                      'pushCallbacks' => $pushCallbacks);
        common_log(LOG_INFO, "Queuing PuSH batch: $this->topic to " .
                             count($pushCallbacks) . " sites");
        $qm = QueueManager::get();
        $qm->enqueue($data, 'hubprep');
    }

    /**
     * Send a 'fat ping' to the subscriber's callback endpoint
     * containing the given Atom feed chunk.
     *
     * Determination of which items to send should be done at
     * a higher level; don't just shove in a complete feed!
     *
     * @param string $atom well-formed Atom feed
     * @throws Exception (HTTP or general)
     */
    function push($atom)
    {
        $headers = array('Content-Type: application/atom+xml');
        if ($this->secret) {
            $hmac = hash_hmac('sha1', $atom, $this->secret);
            $headers[] = "X-Hub-Signature: sha1=$hmac";
        } else {
            $hmac = '(none)';
        }
        common_log(LOG_INFO, "About to push feed to $this->callback for $this->topic, HMAC $hmac");

        $request = new HTTPClient();
        $request->setBody($atom);
        $response = $request->post($this->callback, $headers);

        if ($response->isOk()) {
            return true;
        } else {
            // TRANS: Exception. %1$s is a response status code, %2$s is the body of the response.
            throw new Exception(sprintf(_m('Callback returned status: %1$s. Body: %2$s'),
                                $response->getStatus(),trim($response->getBody())));
        }
    }
}


code:
<?php
/**
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * A sample module to show best practices for StatusNet plugins
 *
 * PHP version 5
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * @package   StatusNet
 * @author    James Walker <james@status.net>
 * @copyright 2010 StatusNet, Inc.
 * @license   http://www.fsf.org/licensing/licenses/agpl-3.0.html AGPL 3.0
 * @link      http://status.net/
 */

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

require_once 'Crypt/RSA.php';

class Magicsig extends Managed_DataObject
{
    const PUBLICKEYREL = 'magic-public-key';
    const DIASPORA_PUBLICKEYREL = 'diaspora-public-key';

    const DEFAULT_KEYLEN = 1024;
    const DEFAULT_SIGALG = 'RSA-SHA256';

    public $__table = 'magicsig';

    /**
     * Key to user.id/profile.id for the local user whose key we're storing.
     *
     * @var int
     */
    public $user_id;

    /**
     * Flattened string representation of the key pair; callers should
     * usually use $this->publicKey and $this->privateKey directly,
     * which hold live Crypt_RSA key objects.
     *
     * @var string
     */
    public $keypair;

    /**
     * Crypto algorithm used for this key; currently only RSA-SHA256 is supported.
     *
     * @var string
     */
    public $alg;

    /**
     * Public RSA key; gets serialized in/out via $this->keypair string.
     *
     * @var Crypt_RSA
     */
    public $publicKey;

    /**
     * PrivateRSA key; gets serialized in/out via $this->keypair string.
     *
     * @var Crypt_RSA
     */
    public $privateKey;

    public function __construct($alg=self::DEFAULT_SIGALG)
    {
        $this->alg = $alg;
    }

    /**
     * Fetch a Magicsig object from the cache or database on a field match.
     *
     * @param string $k
     * @param mixed $v
     * @return Magicsig
     */
    static function getKV($k, $v=null)
    {
        $obj =  parent::getKV($k, $v);
        if ($obj instanceof Magicsig) {
            $obj->importKeys(); // Loads Crypt_RSA objects etc.

            // Throw out a big fat warning for keys of less than 1024 bits. (
            // The only case these show up in would be imported or
            // legacy very-old-StatusNet generated keypairs.
            if (strlen($obj->publicKey->modulus->toBits()) < 1024) {
                common_log(LOG_WARNING, sprintf('Salmon key with <1024 bits (%d) belongs to profile with id==%d',
                                            strlen($obj->publicKey->modulus->toBits()),
                                            $obj->user_id));
            }
        }

        return $obj;
    }

    public static function schemaDef()
    {
        return array(
            'fields' => array(
                'user_id' => array('type' => 'int', 'not null' => true, 'description' => 'user id'),
                'keypair' => array('type' => 'text', 'description' => 'keypair text representation'),
                'alg' => array('type' => 'varchar', 'length' => 64, 'description' => 'algorithm'),
            ),
            'primary key' => array('user_id'),
            'foreign keys' => array(
                'magicsig_user_id_fkey' => array('profile', array('user_id' => 'id')),
            ),
        );
    }

    /**
     * Save this keypair into the database.
     *
     * Overloads default insert behavior to encode the live key objects
     * as a flat string for storage.
     *
     * @return mixed
     */
    function insert()
    {
        $this->keypair = $this->toString(true);

        return parent::insert();
    }

    /**
     * Generate a new keypair for a local user and store in the database.
     *
     * Warning: this can be very slow on systems without the GMP module.
     * Runtimes of 20-30 seconds are not unheard-of.
     *
     * FIXME: More than 1024 bits please. But StatusNet _discards_ non-1024 bits,
     *        so we'll have to wait the last mohican out before switching defaults.
     *
     * @param User $user the local user (since we don't have remote private keys)
     */
    public static function generate(User $user, $bits=self::DEFAULT_KEYLEN, $alg=self::DEFAULT_SIGALG)
    {
        $magicsig = new Magicsig($alg);
        $magicsig->user_id = $user->id;

        $rsa = new Crypt_RSA();

        $keypair = $rsa->createKey($bits);

        $magicsig->privateKey = new Crypt_RSA();
        $magicsig->privateKey->loadKey($keypair['privatekey']);

        $magicsig->publicKey = new Crypt_RSA();
        $magicsig->publicKey->loadKey($keypair['publickey']);

        $magicsig->insert();        // will do $this->keypair = $this->toString(true);
        $magicsig->importKeys();    // seems it's necessary to re-read keys from text keypair

        return $magicsig;
    }

    /**
     * Encode the keypair or public key as a string.
     *
     * @param boolean $full_pair set to true to include the private key.
     * @return string
     */
    public function toString($full_pair=false)
    {
        $mod = Magicsig::base64_url_encode($this->publicKey->modulus->toBytes());
        $exp = Magicsig::base64_url_encode($this->publicKey->exponent->toBytes());
        $private_exp = '';
        if ($full_pair && $this->privateKey instanceof Crypt_RSA && $this->privateKey->exponent->toBytes()) {
            $private_exp = '.' . Magicsig::base64_url_encode($this->privateKey->exponent->toBytes());
        }

        return 'RSA.' . $mod . '.' . $exp . $private_exp;
    }

    public function exportPublicKey($format=CRYPT_RSA_PUBLIC_FORMAT_PKCS1)
    {
        $this->publicKey->setPublicKey();
        return $this->publicKey->getPublicKey($format);
    }

    /**
     * importKeys will load the object's keypair string, which initiates
     * loadKey() and configures Crypt_RSA objects.
     *
     * @param string $keypair optional, otherwise the object's "keypair" property will be used
     */
    public function importKeys($keypair=null)
    {
        $this->keypair = $keypair===null ? $this->keypair : preg_replace('/\s+/', '', $keypair);

        // parse components
        if (!preg_match('/RSA\.([^\.]+)\.([^\.]+)(\.([^\.]+))?/', $this->keypair, $matches)) {
            common_debug('Magicsig error: RSA key not found in provided string.');
            throw new ServerException('RSA key not found in keypair string.');
        }

        $mod = $matches[1];
        $exp = $matches[2];
        if (!empty($matches[4])) {
            $private_exp = $matches[4];
        } else {
            $private_exp = false;
        }

        $this->loadKey($mod, $exp, 'public');
        if ($private_exp) {
            $this->loadKey($mod, $private_exp, 'private');
        }
    }

    /**
     * Fill out $this->privateKey or $this->publicKey with a Crypt_RSA object
     * representing the give key (as mod/exponent pair).
     *
     * @param string $mod base64-encoded
     * @param string $exp base64-encoded exponent
     * @param string $type one of 'public' or 'private'
     */
    public function loadKey($mod, $exp, $type = 'public')
    {
        $rsa = new Crypt_RSA();
        $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1);
        $rsa->setHash($this->getHash());
        $rsa->modulus = new Math_BigInteger(Magicsig::base64_url_decode($mod), 256);
        $rsa->k = strlen($rsa->modulus->toBytes());
        $rsa->exponent = new Math_BigInteger(Magicsig::base64_url_decode($exp), 256);

        if ($type == 'private') {
            $this->privateKey = $rsa;
        } else {
            $this->publicKey = $rsa;
        }
    }

    /**
     * Returns the name of the crypto algorithm used for this key.
     *
     * @return string
     */
    public function getName()
    {
        return $this->alg;
    }

    /**
     * Returns the name of a hash function to use for signing with this key.
     *
     * @return string
     */
    public function getHash()
    {
        switch ($this->alg) {
        case 'RSA-SHA256':
            return 'sha256';
        }
        throw new ServerException('Unknown or unsupported hash algorithm for Salmon');
    }

    /**
     * Generate base64-encoded signature for the given byte string
     * using our private key.
     *
     * @param string $bytes as raw byte string
     * @return string base64url-encoded signature
     */
    public function sign($bytes)
    {
        $sig = $this->privateKey->sign($bytes);
        if ($sig === false) {
            throw new ServerException('Could not sign data');
        }
        return Magicsig::base64_url_encode($sig);
    }

    /**
     *
     * @param string $signed_bytes as raw byte string
     * @param string $signature as base64url encoded
     * @return boolean
     */
    public function verify($signed_bytes, $signature)
    {
        $signature = self::base64_url_decode($signature);
        return $this->publicKey->verify($signed_bytes, $signature);
    }

    /**
     * URL-encoding-friendly base64 variant encoding.
     *
     * @param string $input
     * @return string
     */
    public static function base64_url_encode($input)
    {
        return strtr(base64_encode($input), '+/', '-_');
    }

    /**
     * URL-encoding-friendly base64 variant decoding.
     *
     * @param string $input
     * @return string
     */
    public static function base64_url_decode($input)
    {
        return base64_decode(strtr($input, '-_', '+/'));
    }
}