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

repo_name: spookie/gnu

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

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


code:
<?php
// This file is part of GNU social - https://www.gnu.org/software/social
//
// GNU social 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.
//
// GNU social 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 GNU social.  If not, see <http://www.gnu.org/licenses/>.

/**
 * FeedSub handles low-level WebSub (PubSubHubbub/PuSH) subscriptions.
 * Higher-level behavior building OStatus stuff on top is handled
 * under Ostatus_profile.
 *
 * @package   OStatusPlugin
 * @author    Brion Vibber <brion@status.net>
 * @copyright 2009-2010 StatusNet, Inc.
 * @license   https://www.gnu.org/licenses/agpl.html GNU AGPL v3 or later
 */

defined('GNUSOCIAL') || die();

/*
WebSub (previously PubSubHubbub/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 FeedSub extends Managed_DataObject
{
    public $__table = 'feedsub';

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

    // WebSub 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', 'enum' => array('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;
    }

    public function getLeaseRemaining()
    {
        if (empty($this->sub_end)) {
            return null;
        }
        return strtotime($this->sub_end) - time();
    }

    /**
     * Do we have a hub? Then we are a WebSub feed.
     * WebSub standard: https://www.w3.org/TR/websub/
     * old: 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 WebSub feed.
     */
    public function isWebSub()
    {
        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 WebSub setup
     */
    public static function ensureFeed($feeduri)
    {
        $feedsub = self::getKV('uri', $feeduri);
        if ($feedsub instanceof FeedSub) {
            if (!empty($feedsub->huburi)) {
                // If there is already a huburi we don't
                // rediscover it on ensureFeed, call
                // ensureHub to do that (compare ->modified
                // to see if it might be time to do it).
                return $feedsub;
            }
            if ($feedsub->sub_state !== 'inactive') {
                throw new ServerException('Can only ensure WebSub hub for inactive (unsubscribed) feeds.');
            }
            // If huburi is empty we continue with ensureHub
        } else {
            // If we don't have that local feed URI
            // stored then we create a new DB object.
            $feedsub = new FeedSub();
            $feedsub->uri = $feeduri;
            $feedsub->sub_state = 'inactive';
        }

        try {
            // discover the hub uri
            $feedsub->ensureHub();
        } catch (FeedSubNoHubException $e) {
            // Only throw this exception if we can't handle huburi-less feeds
            // (i.e. we have a fallback hub or we can do feed polling (nohub)
            if (!common_config('feedsub', 'fallback_hub') && !common_config('feedsub', 'nohub')) {
                throw $e;
            }
        }

        if (empty($feedsub->id)) {
            // if $feedsub doesn't have an id we'll insert it into the db here
            $feedsub->created = common_sql_now();
            $feedsub->modified = common_sql_now();
            $result = $feedsub->insert();
            if ($result === false) {
                throw new FeedDBException($feedsub);
            }
        }

        return $feedsub;
    }

    /**
     * ensureHub will only do $this->update if !empty($this->id)
     * because otherwise the object has not been created yet.
     *
     * @param   bool    $rediscovered   Whether the hub info is rediscovered (to avoid endless loop nesting)
     *
     * @return  null    if actively avoiding the database
     *          int     number of rows updated in the database (0 means untouched)
     *
     * @throws  ServerException if something went wrong when updating the database
     *          FeedSubNoHubException   if no hub URL was discovered
     */
    public function ensureHub($rediscovered=false)
    {
        common_debug('Now inside ensureHub again, $rediscovered=='._ve($rediscovered));
        if ($this->sub_state !== 'inactive') {
            common_log(LOG_INFO, sprintf(__METHOD__ . ': Running hub discovery a possibly active feed in %s state for URI %s', _ve($this->sub_state), _ve($this->uri)));
        }

        $discover = new FeedDiscovery();
        $discover->discoverFromFeedURL($this->uri);

        $huburi = $discover->getHubLink();
        if (empty($huburi)) {
            // Will be caught and treated with if statements in regards to
            // fallback hub and feed polling (nohub) configuration.
            throw new FeedSubNoHubException();
        }

        // if we've already got a DB object stored, we want to UPDATE, not INSERT
        $orig = !empty($this->id) ? clone($this) : null;

        $old_huburi = $this->huburi;    // most likely null if we're INSERTing
        $this->huburi = $huburi;

        if (!empty($this->id)) {
            common_debug(sprintf(__METHOD__ . ': Feed uri==%s huburi before=%s after=%s (identical==%s)', _ve($this->uri), _ve($old_huburi), _ve($this->huburi), _ve($old_huburi===$this->huburi)));
            $result = $this->update($orig);
            if ($result === false) {
                // TODO: Get a DB exception class going...
                common_debug('Database update failed for FeedSub id=='._ve($this->id).' with new huburi: '._ve($this->huburi));
                throw new ServerException('Database update failed for FeedSub.');
            }
            if (!$rediscovered) {
                $this->renew();
            }
            return $result;
        }

        return null;    // we haven't done anything with the database
    }

    /**
     * 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($rediscovered=false)
    {
        if ($this->sub_state !== 'inactive') {
            common_log(LOG_WARNING, sprintf('Attempting to (re)start WebSub 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.
            } elseif (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 WebSub subscription for feed with no hub.'));
            }
        }

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

    /**
     * Send a WebSub 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 WebSub 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) && !common_config('feedsub', 'fallback_hub')) {
            /**
             * If the huburi is empty and we don't have a fallback hub,
             * there is nowhere we can send an unsubscribe to.
             *
             * A plugin should handle the FeedSub above and set the proper state
             * if there is no hub. (instead of 'nohub' it should be 'inactive' if
             * the instance has enabled feed polling for feeds that don't publish
             * WebSub/PuSH hubs. FeedPoller is a plugin which enables polling.
             *
             * Secondly, if we don't have the setting "nohub" enabled (i.e.)
             * we're ready to poll ourselves, there is something odd with the
             * database, such as a polling plugin that has been disabled.
             */

            if (!common_config('feedsub', 'nohub')) {
                // TRANS: Server exception.
                throw new ServerException(_m('Attempting to end WebSub subscription for feed with no hub.'));
            }

            return;
        }

        $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 === 'inactive') {
            // No active WebSub subscription, we can just leave it be.
            return true;
        }

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

    public static function renewalCheck()
    {
        $fs = new FeedSub();
        $fs->whereAdd("sub_end IS NOT NULL AND sub_end < (CURRENT_TIMESTAMP + INTERVAL '1' DAY)");
        // find can be both false and 0, depending on why nothing was found
        if (!$fs->find()) {
            throw new NoResultException($fs);
        }
        return $fs;
    }

    public function renew($rediscovered=false)
    {
        common_debug('FeedSub is being renewed for uri=='._ve($this->uri).' on huburi=='._ve($this->huburi));
        $this->subscribe($rediscovered);
    }

    /**
     * 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, $rediscovered=false)
    {
        $msg = null;    // carries descriptive error message to enduser (no remote data strings!)

        $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.lease_seconds' => 2592000,   // 3600*24*30, request approximately month long lease (may be changed by hub)
                          '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 WebSub hub.');
                }
            }
            $response = $client->post($hub, $headers, $post);
            $status = $response->getStatus();
            // WebSub specificed response status code
            if ($status == 202  || $status == 204) {
                common_log(LOG_INFO, __METHOD__ . ': sub req ok, awaiting verification callback');
                return;
            } elseif ($status >= 200 && $status < 300) {
                common_log(LOG_ERR, __METHOD__ . ": sub req returned unexpected HTTP $status: " . $response->getBody());
                $msg = sprintf(_m("Unexpected HTTP status: %d"), $status);
            } elseif ($status == 422 && !$rediscovered) {
                // Error code regarding something wrong in the data (it seems
                // that we're talking to a WebSub hub at least, so let's check
                // our own data to be sure we're not mistaken somehow, which
                // means rediscovering hub data (the boolean parameter means
                // we avoid running this part over and over and over and over):

                common_debug('Running ensureHub again due to 422 status, $rediscovered=='._ve($rediscovered));
                $discoveryResult = $this->ensureHub(true);
                common_debug('ensureHub is now done and its result was: '._ve($discoveryResult));
            } 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" . (!is_null($msg) ? " ($msg)" : '.'));
    }

    /**
     * Save WebSub 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 {
            // Backwards compatibility to StatusNet (PuSH <0.4 supported permanent subs)
            $this->sub_end = $this->sqlValue('NULL');
        }
        $this->modified = common_sql_now();

        common_debug(__METHOD__ . ': Updating sub state and metadata for '.$this->getUri());
        return $this->update($original);
    }

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

        $this->secret = $this->sqlValue('NULL');
        $this->sub_state = 'inactive';
        $this->sub_start = $this->sqlValue('NULL');
        $this->sub_end = $this->sqlValue('NULL');
        $this->modified = common_sql_now();

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

    /**
     * Accept updates from a WebSub 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, sprintf(__METHOD__.': packet for %s with HMAC %s', _ve($this->getUri()), _ve($hmac)));

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

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

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

            $this->receiveFeed($post);
        } catch (FeedSubBadPushSignatureException $e) {
            // We got a signature, so something could be wrong. Let's check to see if
            // maybe upstream has switched to another hub. Let's fetch feed and then
            // compare rel="hub" with $this->huburi, which is done in $this->ensureHub()

            $this->ensureHub(true);
        }
    }

    /**
     * All our feed URIs should be URLs.
     */
    public function importFeed()
    {
        $feed_url = $this->getUri();

        // Fetch the URL
        try {
            common_log(LOG_INFO, sprintf('Importing feed backlog from %s', $feed_url));
            $feed_xml = HTTPClient::quickGet($feed_url, 'application/atom+xml');
        } catch (Exception $e) {
            throw new FeedSubException("Could not fetch feed from URL '%s': %s (%d).\n", $feed_url, $e->getMessage(), $e->getCode());
        }

        return $this->receiveFeed($feed_xml);
    }

    protected function receiveFeed($feed_xml)
    {
        // We're passed the XML for the Atom feed as $feed_xml,
        // so read it into a DOMDocument and process.
        $feed = new DOMDocument();
        if (!$feed->loadXML($feed_xml)) {
            // @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 do, our 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) {
            // {3,16} because shortest hash algorithm name is 3 characters (md2,md4,md5) and longest
            // is currently 11 characters, but we'll leave some margin in the end...
            if (preg_match('/^([0-9a-zA-Z\-\,]{3,16})=([0-9a-fA-F]+)$/', $hmac, $matches)) {
                $hash_algo  = strtolower($matches[1]);
                $their_hmac = strtolower($matches[2]);
                common_debug(sprintf(__METHOD__ . ': WebSub push from feed %s uses HMAC algorithm %s with value: %s', _ve($this->getUri()), _ve($hash_algo), _ve($their_hmac)));

                if (!in_array($hash_algo, hash_algos())) {
                    // We can't handle this at all, PHP doesn't recognize the algorithm name ('md5', 'sha1', 'sha256' etc: https://secure.php.net/manual/en/function.hash-algos.php)
                    common_log(LOG_ERR, sprintf(__METHOD__.': HMAC algorithm %s unsupported, not found in PHP hash_algos()', _ve($hash_algo)));
                    return false;
                } elseif (!is_null(common_config('security', 'hash_algos')) && !in_array($hash_algo, common_config('security', 'hash_algos'))) {
                    // We _won't_ handle this because there is a list of accepted hash algorithms and this one is not in it.
                    common_log(LOG_ERR, sprintf(__METHOD__.': Whitelist for HMAC algorithms exist, but %s is not included.', _ve($hash_algo)));
                    return false;
                }

                $our_hmac = hash_hmac($hash_algo, $post, $this->secret);
                if ($their_hmac !== $our_hmac) {
                    common_log(LOG_ERR, sprintf(__METHOD__.': ignoring WebSub push with bad HMAC hash: got %s, expected %s for feed %s from hub %s', _ve($their_hmac), _ve($our_hmac), _ve($this->getUri()), _ve($this->huburi)));
                    throw new FeedSubBadPushSignatureException('Incoming WebSub push signature did not match expected HMAC hash.');
                }
                return true;
            } else {
                common_log(LOG_ERR, sprintf(__METHOD__.': ignoring WebSub push with bogus HMAC==', _ve($hmac)));
            }
        } else {
            if (empty($hmac)) {
                return true;
            } else {
                common_log(LOG_ERR, sprintf(__METHOD__.': ignoring WebSub push with unexpected HMAC==%s', _ve($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('GNUSOCIAL')) { exit(1); }

/**
 * WebSub (previously 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 $sub_start;
    public $sub_end;
    public $created;
    public $modified;

    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'),
                'sub_start' => array('type' => 'datetime', 'description' => 'subscription start'),
                'sub_end' => array('type' => 'datetime', 'description' => 'subscription end'),
                'errors' => array('type' => 'int', 'not null' => true, 'default' => 0, 'description' => 'Queue handling error count, is reset on success.'),
                'error_start' => array('type' => 'datetime', 'default' => null, 'description' => 'time of first error since latest success, should be null if no errors have been counted'),
                'last_error' => array('type' => 'datetime', 'default' => null, 'description' => 'time of last failure, if ever'),
                'last_error_msg' => array('type' => 'text', 'default' => null, 'description' => 'Last error _message_'),
                '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_callback_idx' => array('callback'),
                'hubsub_topic_idx' => array('topic'),
            ),
        );
    }

    function getErrors()
    {
        return intval($this->errors);
    }

    // $msg is only set if $error_count is 0
    function setErrors($error_count, $msg=null)
    {
        assert(is_int($error_count));
        if (!is_int($error_count) || $error_count < 0) {
            common_log(LOG_ERR, 'HubSub->setErrors was given a bad value: '._ve($error_count));
            throw new ServerException('HubSub error count must be an integer higher or equal to 0.');
        }

        $orig = clone($this);
        $now = common_sql_now();

        if ($error_count === 1) {
            // Record when the errors started
            $this->error_start = $now;
        }
        if ($error_count > 0) {
            // Record this error's occurrence in time
            $this->last_error = $now;
            $this->last_error_msg = $msg;
        } else {
            $this->error_start = null;
            $this->last_error = null;
            $this->last_error_msg = null;
        }

        $this->errors = $error_count;
        $this->update($orig);
    }

    function resetErrors()
    {
        return $this->setErrors(0);
    }

    function incrementErrors($msg=null)
    {
        return $this->setErrors($this->getErrors()+1, $msg);
    }

    /**
     * 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)
    {
        common_debug('WebSub hub got requested lease_seconds=='._ve($length));
        assert(is_int($length));

        $min = 86400;   // 3600*24 (one day)
        $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;
        }

        common_debug('WebSub hub after sanitation: lease_seconds=='._ve($length));
        $this->sub_start = common_sql_now();
        $this->sub_end = common_sql_date(time() + $length);
    }

    function getLeaseTime()
    {
        if (empty($this->sub_start) || empty($this->sub_end)) {
            return null;
        }
        $length = strtotime($this->sub_end) - strtotime($this->sub_start);
        assert($length > 0);
        return $length;
    }

    function getLeaseRemaining()
    {
        if (empty($this->sub_end)) {
            return null;
        }
        return strtotime($this->sub_end) - time();
    }

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

    public function getTopic()
    {
        return $this->topic;
    }

    /**
     * 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->getTopic(),
                        'hub.challenge' => $challenge);
        if ($mode == 'subscribe') {
            $params['hub.lease_seconds'] = $this->getLeaseTime();
        }
        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->getTopic()}");
        } 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->getTopic(), $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.
            }
        }
    }

    // set the hashkey automagically on insert
    protected function onInsert()
    {
        $this->setHashkey();
        $this->created = common_sql_now();
        $this->modified = common_sql_now();
    }

    // update the hashkey automagically if needed
    protected function onUpdateKeys(Managed_DataObject $orig)
    {
        if ($this->topic !== $orig->topic || $this->callback !== $orig->callback) {
            $this->setHashkey();
        }
    }

    protected function setHashkey()
    {
        $this->hashkey = self::hashkey($this->topic, $this->callback);
    }

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

        $data = array('topic' => $this->getTopic(),
                      'callback' => $this->callback,
                      'atom' => $atom,
                      'retries' => $retries);
        common_log(LOG_INFO, sprintf('Queuing WebSub: %s to %s', _ve($data['topic']), _ve($data['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, array $pushCallbacks)
    {
        if (empty($pushCallbacks)) {
            common_log(LOG_ERR, 'Callback list empty for bulkDistribute.');
            return false;
        }
        $data = array('atom' => $atom,
                      'topic' => $this->getTopic(),
                      'pushCallbacks' => $pushCallbacks);
        common_log(LOG_INFO, "Queuing WebSub batch: {$this->getTopic()} to ".count($pushCallbacks)." sites");
        $qm = QueueManager::get();
        $qm->enqueue($data, 'hubprep');
        return true;
    }

    /**
     * @return  boolean     true/false for HTTP response
     * @throws  Exception   for lower-than-HTTP errors (such as NS lookup failure, connection refused...)
     */
    public static function pushAtom($topic, $callback, $atom, $secret=null, $hashalg='sha1')
    {
        $headers = array('Content-Type: application/atom+xml');
        if ($secret) {
            $hmac = hash_hmac($hashalg, $atom, $secret);
            $headers[] = "X-Hub-Signature: {$hashalg}={$hmac}";
        } else {
            $hmac = '(none)';
        }
        common_log(LOG_INFO, sprintf('About to WebSub-push feed to %s for %s, HMAC %s', _ve($callback), _ve($topic), _ve($hmac)));

        $request = new HTTPClient();
        $request->setConfig(array('follow_redirects' => false));
        $request->setBody($atom);

        // This will throw exception on non-HTTP failures
        try {
            $response = $request->post($callback, $headers);
        } catch (Exception $e) {
            common_debug(sprintf('WebSub callback to %s for %s failed with exception %s: %s', _ve($callback), _ve($topic), _ve(get_class($e)), _ve($e->getMessage())));
            throw $e;
        }

        return $response->isOk();
    }

    /**
     * Send a 'fat ping' to the subscriber's callback endpoint
     * containing the given Atom feed chunk.
     *
     * Determination of which feed items to send should be done at
     * a higher level; don't just shove in a complete feed!
     *
     * FIXME: Add 'failed' incremental count.
     *
     * @param string $atom well-formed Atom feed
     * @return  boolean     Whether the PuSH was accepted or not.
     * @throws Exception (HTTP or general)
     */
    function push($atom)
    {
        try {
            $success = self::pushAtom($this->getTopic(), $this->callback, $atom, $this->secret);
            if ($success) {
                return true;
            } elseif ('https' === parse_url($this->callback, PHP_URL_SCHEME)) {
                // Already HTTPS, no need to check remote http/https migration issues
                return false;
            }
            // if pushAtom returned false and we didn't try an HTTPS endpoint,
            // let's try HTTPS too (assuming only http:// and https:// are used ;))

        } catch (Exception $e) {
            if ('https' === parse_url($this->callback, PHP_URL_SCHEME)) {
                // Already HTTPS, no need to check remote http/https migration issues
                throw $e;
            }
        }

        // We failed the WebSub push, but it might be that the remote site has changed their configuration to HTTPS
        common_debug('WebSub HTTPSFIX: push failed, so we need to see if it can be the remote http->https migration issue.');

        // XXX: DO NOT trust a Location header here, _especially_ from 'http' protocols,
        // but not 'https' either at least if we don't do proper CA verification. Trust that
        // the most common change here is simply switching 'http' to 'https' and we will
        // solve 99% of all of these issues for now. There should be a proper mechanism
        // if we want to change the callback URLs, preferrably just manual resubscriptions
        // from the remote side, combined with implemented WebSub subscription timeouts.

        // Test if the feed callback for this node has already been migrated to HTTPS in our database
        // (otherwise we'd get collisions when inserting it further down)
        $httpscallback = preg_replace('/^http/', 'https', $this->callback, 1);
        $alreadyreplaced = self::getByHashKey($this->getTopic(), $httpscallback);
        if ($alreadyreplaced instanceof HubSub) {
            // Let's remove the old HTTP callback object.
            $this->delete();

            // XXX: I think this means we might lose a message or two when
            //      remote side migrates to HTTPS because we only try _once_
            //      for _one_ WebSub push. The rest of the posts already
            //      stored in our queue (if any) will not find a HubSub
            //      object. This could maybe be fixed by handling migration
            //      in HubOutQueueHandler while handling the item there.
            common_debug('WebSub HTTPSFIX: Pushing Atom to HTTPS callback instead of HTTP, because of switch to HTTPS since enrolled in queue.');
            return self::pushAtom($this->getTopic(), $httpscallback, $atom, $this->secret);
        }

        common_debug('WebSub HTTPSFIX: callback to '._ve($this->callback).' for '._ve($this->getTopic()).' trying HTTPS callback: '._ve($httpscallback));
        $success = self::pushAtom($this->getTopic(), $httpscallback, $atom, $this->secret);
        if ($success) {
            // Yay, we made a successful push to https://, let's remember this in the future!
            $orig = clone($this);
            $this->callback = $httpscallback;
            // NOTE: hashkey will be set in $this->onUpdateKeys($orig) through updateWithKeys
            $this->updateWithKeys($orig);
            return true;
        }

        // If there have been any exceptions thrown before, they're handled
        // higher up. This function's return value is just whether the WebSub
        // push was accepted or not.
        return $success;
    }
}