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

/**
 * OStatusPlugin implementation for GNU Social
 * Depends on: WebFinger plugin
 *
 * @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();

require_once __DIR__ . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'util.php';

class OStatusPlugin extends Plugin
{
    const PLUGIN_VERSION = '2.1.0';

    /**
     * Hook for RouterInitialized event.
     *
     * @param URLMapper $m path-to-action mapper
     * @return boolean hook return
     */
    public function onRouterInitialized(URLMapper $m)
    {
        // Discovery actions
        $m->connect(
            'main/ostatustag',
            ['action' => 'ostatustag']
        );
        $m->connect(
            'main/ostatustag?nickname=:nickname',
            ['action'   => 'ostatustag'],
            ['nickname' => '[A-Za-z0-9_-]+']
        );
        $m->connect(
            'main/ostatus/group/:group',
            ['action' => 'ostatusinit'],
            ['group'  => '[A-Za-z0-9_-]+']
        );
        $m->connect(
            'main/ostatus/peopletag/:peopletag/tagger/:tagger',
            ['action' => 'ostatusinit'],
            [
                'tagger'    => '[A-Za-z0-9_-]+',
                'peopletag' => '[A-Za-z0-9_-]+',
            ]
        );
        $m->connect(
            'main/ostatus',
            ['action' => 'ostatusinit']
        );

        // Remote subscription actions
        $m->connect(
            'main/ostatusgroup',
            ['action' => 'ostatusgroup']
        );
        $m->connect(
            'main/ostatuspeopletag',
            ['action' => 'ostatuspeopletag']
        );

        // WebSub actions
        $m->connect('main/push/hub', ['action' => 'pushhub']);

        $m->connect(
            'main/push/callback/:feed',
            ['action' => 'pushcallback'],
            ['feed' => '[0-9]+']
        );

        // Salmon endpoint
        $m->connect(
            'main/salmon/user/:id',
            ['action' => 'usersalmon'],
            ['id' => '[0-9]+']
        );
        $m->connect(
            'main/salmon/group/:id',
            ['action' => 'groupsalmon'],
            ['id' => '[0-9]+']
        );
        $m->connect(
            'main/salmon/peopletag/:id',
            ['action' => 'peopletagsalmon'],
            ['id' => '[0-9]+']
        );
        return true;
    }

    /**
     * Set up queue handlers for outgoing hub pushes
     * @param QueueManager $qm
     * @return boolean hook return
     */
    public function onEndInitializeQueueManager(QueueManager $qm)
    {
        // Prepare outgoing distributions after notice save.
        $qm->connect('ostatus', 'OStatusQueueHandler');

        // Outgoing from our internal WebSub hub
        $qm->connect('hubconf', 'HubConfQueueHandler');
        $qm->connect('hubprep', 'HubPrepQueueHandler');

        $qm->connect('hubout', 'HubOutQueueHandler');

        // Outgoing Salmon replies (when we don't need a return value)
        $qm->connect('salmon', 'SalmonQueueHandler');

        // Incoming from a foreign WebSub hub
        $qm->connect('pushin', 'PushInQueueHandler');

        // Re-subscribe feeds that need renewal
        $qm->connect('pushrenew', 'PushRenewQueueHandler');
        return true;
    }

    /**
     * Put saved notices into the queue for pubsub distribution.
     */
    public function onStartEnqueueNotice($notice, &$transports)
    {
        if ($notice->inScope(null) && $notice->getProfile()->hasRight(Right::PUBLICNOTICE)) {
            $transports[] = 'ostatus';
            $this->log(LOG_INFO, "OSTATUS [{$notice->getID()}]: queued for OStatus processing");
        } else {
            // FIXME: we don't do privacy-controlled OStatus updates yet.
            // once that happens, finer grain of control here.
            $this->log(LOG_NOTICE, "OSTATUS [{$notice->getID()}]: Not queueing because of privacy; scope = {$notice->scope}");
        }
        return true;
    }

    /**
     * Set up a WebSub hub link to our internal link for canonical timeline
     * Atom feeds for users and groups.
     */
    public function onStartApiAtom($feed)
    {
        $id = null;

        if ($feed instanceof AtomUserNoticeFeed) {
            $salmonAction = 'usersalmon';
            $user = $feed->getUser();
            $id   = $user->id;
            $profile = $user->getProfile();
        } elseif ($feed instanceof AtomGroupNoticeFeed) {
            $salmonAction = 'groupsalmon';
            $group = $feed->getGroup();
            $id = $group->id;
        } elseif ($feed instanceof AtomListNoticeFeed) {
            $salmonAction = 'peopletagsalmon';
            $peopletag = $feed->getList();
            $id = $peopletag->id;
        } else {
            return true;
        }

        if (!empty($id)) {
            $hub = common_config('ostatus', 'hub');
            if (empty($hub)) {
                // Updates will be handled through our internal WebSub hub.
                $hub = common_local_url('pushhub');
            }
            $feed->addLink($hub, array('rel' => 'hub'));

            // Also, we'll add in the salmon link
            $salmon = common_local_url($salmonAction, array('id' => $id));
            $feed->addLink($salmon, array('rel' => Salmon::REL_SALMON));

            // XXX: these are deprecated, but StatusNet only looks for NS_REPLIES
            $feed->addLink($salmon, array('rel' => Salmon::NS_REPLIES));
            $feed->addLink($salmon, array('rel' => Salmon::NS_MENTIONS));
        }

        return true;
    }

    /**
     * Add in an OStatus list button
     *
     * @param HTMLOutputter $output
     * @param Profile $profile
     * @return bool hook return value
     */
    public function onStartProfileRemoteSubscribe(HTMLOutputter $output, Profile $profile): bool
    {
        $this->onStartProfileListItemActionElements($output);
        return true;
    }

    public function onStartGroupSubscribe($widget, $group)
    {
        $cur = common_current_user();

        if (empty($cur)) {
            $widget->out->elementStart('li', 'entity_subscribe');

            $url = common_local_url(
                'ostatusinit',
                ['group' => $group->nickname]
            );
            $widget->out->element(
                'a',
                [
                    'href' => $url,
                    'class' => 'entity_remote_subscribe',
                ],
                // TRANS: Link to subscribe to a remote entity.
                _m('Subscribe')
            );

            $widget->out->elementEnd('li');
            return false;
        }

        return true;
    }

    public function onStartSubscribePeopletagForm($output, $peopletag)
    {
        $cur = common_current_user();

        if (empty($cur)) {
            $output->elementStart('li', 'entity_subscribe');
            $profile = $peopletag->getTagger();
            $url = common_local_url(
                'ostatusinit',
                ['tagger' => $profile->nickname, 'peopletag' => $peopletag->tag]
            );
            $output->element(
                'a',
                [
                    'href'  => $url,
                    'class' => 'entity_remote_subscribe',
                ],
                // TRANS: Link to subscribe to a remote entity.
                _m('Subscribe')
            );

            $output->elementEnd('li');
            return false;
        }

        return true;
    }

    /*
     * If the field being looked for is URI look for the profile
     */
    public function onStartProfileCompletionSearch($action, $profile, $search_engine)
    {
        if ($action->field == 'uri') {
            $profile->joinAdd(['id', 'user:id']);
            $profile->whereAdd("uri LIKE '%" . $profile->escape($q) . "%'");
            $profile->query();

            $validate = new Validate();

            if ($profile->N == 0) {
                try {
                    if ($validate->email($q)) {
                        $oprofile = Ostatus_profile::ensureWebfinger($q);
                    } elseif ($validate->uri($q)) {
                        $oprofile = Ostatus_profile::ensureProfileURL($q);
                    } else {
                        // TRANS: Exception in OStatus when invalid URI was entered.
                        throw new Exception(_m('Invalid URI.'));
                    }
                    return $this->filter(array($oprofile->localProfile()));
                } catch (Exception $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->msg = _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.");
                    return array();
                }
            }
            return false;
        }
        return true;
    }

    /**
     * Webfinger matches: @user@example.com or even @user--one.george_orwell@1984.biz
     * @param   string  $text       The text from which to extract webfinger IDs
     * @param   string  $preMention Character(s) that signals a mention ('@', '!'...)
     *
     * @return  array   The matching IDs (without $preMention) and each respective position in the given string.
     */
    public static function extractWebfingerIds($text, $preMention = '@')
    {
        $wmatches = array();
        $result = preg_match_all(
            '/(?<!\S)' . preg_quote($preMention, '/') . '(' . Nickname::WEBFINGER_FMT . ')/',
            $text,
            $wmatches,
            PREG_OFFSET_CAPTURE
        );
        if ($result === false) {
            common_log(LOG_ERR, __METHOD__ . ': Error parsing webfinger IDs from text (preg_last_error=='.preg_last_error().').');
            return [];
        } elseif (count($wmatches)) {
            common_debug(sprintf('Found %d matches for WebFinger IDs: %s', count($wmatches), _ve($wmatches)));
        }
        return $wmatches[1];
    }

    /**
     * Profile URL matches: @example.com/mublog/user
     * @param   string  $text       The text from which to extract URL mentions
     * @param   string  $preMention Character(s) that signals a mention ('@', '!'...)
     *
     * @return  array   The matching URLs (without @ or acct:) and each respective position in the given string.
     */
    public static function extractUrlMentions($text, $preMention = '@')
    {
        $wmatches = array();
        // In the regexp below we need to match / _before_ URL_REGEX_VALID_PATH_CHARS because it otherwise gets merged
        // with the TLD before (but / is in URL_REGEX_VALID_PATH_CHARS anyway, it's just its positioning that is important)
        $result = preg_match_all(
            '/(?:^|\s+)' . preg_quote($preMention, '/') . '(' . URL_REGEX_DOMAIN_NAME . '(?:\/[' . URL_REGEX_VALID_PATH_CHARS . ']*)*)/',
            $text,
            $wmatches,
            PREG_OFFSET_CAPTURE
        );
        if ($result === false) {
            common_log(LOG_ERR, __METHOD__ . ': Error parsing profile URL mentions from text (preg_last_error=='.preg_last_error().').');
            return [];
        } elseif (count($wmatches)) {
            common_debug(sprintf('Found %d matches for profile URL mentions: %s', count($wmatches), _ve($wmatches)));
        }
        return $wmatches[1];
    }

    /**
     * Find any explicit remote mentions. Accepted forms:
     *   Webfinger: @user@example.com
     *   Profile link: @example.com/mublog/user
     * @param Profile $sender
     * @param string $text input markup text
     * @param array &$mention in/out param: set of found mentions
     * @return boolean hook return value
     */
    public function onEndFindMentions(Profile $sender, $text, &$mentions)
    {
        $matches = array();

        foreach (self::extractWebfingerIds($text, '@') as $wmatch) {
            list($target, $pos) = $wmatch;
            $this->log(LOG_INFO, "Checking webfinger person '$target'");
            $profile = null;
            try {
                $oprofile = Ostatus_profile::ensureWebfinger($target);
                if (!$oprofile instanceof Ostatus_profile || !$oprofile->isPerson()) {
                    continue;
                }
                $profile = $oprofile->localProfile();
            } catch (OStatusShadowException $e) {
                // This means we got a local user in the webfinger lookup
                $profile = $e->profile;
            } catch (Exception $e) {
                $this->log(LOG_ERR, "Webfinger check failed: " . $e->getMessage());
                continue;
            }

            assert($profile instanceof Profile);

            $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target)
                    ? $profile->getNickname()   // TODO: we could do getBestName() or getFullname() here
                    : $target;
            $url = $profile->getUri();
            if (!common_valid_http_url($url)) {
                $url = $profile->getUrl();
            }
            $matches[$pos] = array('mentioned' => array($profile),
                                   'type' => 'mention',
                                   'text' => $displayName,
                                   'position' => $pos,
                                   'length' => mb_strlen($target),
                                   'url' => $url);
        }

        // Doing groups in a separate routine because webfinger lookups don't work
        // remotely until everyone updates etc. etc.
        foreach (self::extractWebfingerIds($text, '!') as $wmatch) {
            list($target, $pos) = $wmatch;
            list($target_nickname, $target_hostname) = explode('@', parse_url($target, PHP_URL_PATH));
            $this->log(LOG_INFO, sprintf('Checking webfinger group %s as user %s on server %s', $target, $target_nickname, $target_hostname));

            $profile = null;
            if ($target_hostname === mb_strtolower(common_config('site', 'server'))) {
                try {
                    $profile = Local_group::getKV('nickname', $target_nickname)->getProfile();
                } catch (NoSuchGroupException $e) {
                    // referenced a local group which does not exist, so not returning it as a mention
                    $this->log(LOG_ERR, "Local group lookup failed: " . _ve($e->getMessage()));
                    continue;
                }
            } else {
                // XXX: Superhacky. Domain name can be incorrectly matched
                //      here. But since users are only members of groups
                //      they trust (of course they are!), the likelihood of
                //      a mention-hijacking is very very low... for now.
                $possible_groups = new User_group();
                $possible_groups->nickname = $target_nickname;
                if (!$possible_groups->find()) {
                    common_debug('No groups at all found with nickname: '._ve($target_nickname));
                    continue;
                }
                while ($possible_groups->fetch()) {
                    if (!$sender->isMember($possible_groups)) {
                        continue;
                    }
                    $group_hostname = mb_strtolower(parse_url($possible_groups->mainpage, PHP_URL_HOST));
                    if ($target_hostname === $group_hostname) {
                        common_debug(sprintf('Found group with nick@host (%s@%s) matching %s', _ve($possible_groups->nickname), _ve($group_hostname), _ve($target)));
                        $profile = $possible_groups->getProfile();
                        break;
                    }
                }
                $possible_groups->free();
                if (!$profile instanceof Profile) {
                    common_debug('Found groups with correct nickname but not hostname for: '._ve($target));
                    continue;
                }
            }

            assert($profile instanceof Profile);

            $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target)
                    ? $profile->getNickname()   // TODO: we could do getBestName() or getFullname() here
                    : $target;
            $url = $profile->getUri();
            if (!common_valid_http_url($url)) {
                $url = $profile->getUrl();
            }
            $matches[$pos] = array('mentioned' => array($profile),
                                   'type' => 'group',
                                   'text' => $displayName,
                                   'position' => $pos,
                                   'length' => mb_strlen($target),
                                   'url' => $url);
        }

        foreach (self::extractUrlMentions($text) as $wmatch) {
            list($target, $pos) = $wmatch;
            $schemes = array('https', 'http');
            foreach ($schemes as $scheme) {
                $url = "$scheme://$target";
                $this->log(LOG_INFO, "Checking profile address '$url'");
                try {
                    $oprofile = Ostatus_profile::ensureProfileURL($url);
                    if ($oprofile instanceof Ostatus_profile && !$oprofile->isGroup()) {
                        $profile = $oprofile->localProfile();
                        $displayName = !empty($profile->nickname) && mb_strlen($profile->nickname) < mb_strlen($target) ?
                                $profile->nickname : $target;
                        $matches[$pos] = array('mentioned' => array($profile),
                                               'type' => 'mention',
                                               'text' => $displayName,
                                               'position' => $pos,
                                               'length' => mb_strlen($target),
                                               'url' => $profile->getUrl());
                        break;
                    }
                } catch (Exception $e) {
                    $this->log(LOG_ERR, "Profile check failed: " . $e->getMessage());
                }
            }
        }

        foreach ($mentions as $i => $other) {
            // If we share a common prefix with a local user, override it!
            $pos = $other['position'];
            if (isset($matches[$pos])) {
                $mentions[$i] = $matches[$pos];
                unset($matches[$pos]);
            }
        }
        foreach ($matches as $mention) {
            $mentions[] = $mention;
        }

        return true;
    }

    /**
     * Allow remote profile references to be used in commands:
     *   sub update@status.net
     *   whois evan@identi.ca
     *   reply http://identi.ca/evan hey what's up
     *
     * @param Command $command
     * @param string $arg
     * @param Profile &$profile
     * @return hook return code
     */
    public function onStartCommandGetProfile($command, $arg, &$profile)
    {
        $oprofile = $this->pullRemoteProfile($arg);
        if ($oprofile instanceof Ostatus_profile && !$oprofile->isGroup()) {
            try {
                $profile = $oprofile->localProfile();
            } catch (NoProfileException $e) {
                // No locally stored profile found for remote profile
                return true;
            }
            return false;
        } else {
            return true;
        }
    }

    /**
     * Allow remote group references to be used in commands:
     *   join group+statusnet@identi.ca
     *   join http://identi.ca/group/statusnet
     *   drop identi.ca/group/statusnet
     *
     * @param Command $command
     * @param string $arg
     * @param User_group &$group
     * @return hook return code
     */
    public function onStartCommandGetGroup($command, $arg, &$group)
    {
        $oprofile = $this->pullRemoteProfile($arg);
        if ($oprofile instanceof Ostatus_profile && $oprofile->isGroup()) {
            $group = $oprofile->localGroup();
            return false;
        } else {
            return true;
        }
    }

    protected function pullRemoteProfile($arg)
    {
        $oprofile = null;
        if (preg_match('!^((?:\w+\.)*\w+@(?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+)$!', $arg)) {
            // webfinger lookup
            try {
                return Ostatus_profile::ensureWebfinger($arg);
            } catch (Exception $e) {
                common_log(LOG_ERR, 'Webfinger lookup failed for ' .
                                    $arg . ': ' . $e->getMessage());
            }
        }

        // Look for profile URLs, with or without scheme:
        $urls = array();
        if (preg_match('!^https?://((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
            $urls[] = $arg;
        }
        if (preg_match('!^((?:\w+\.)*\w+(?:\w+\-\w+)*\.\w+(?:/\w+)+)$!', $arg)) {
            $schemes = array('http', 'https');
            foreach ($schemes as $scheme) {
                $urls[] = "$scheme://$arg";
            }
        }

        foreach ($urls as $url) {
            try {
                return Ostatus_profile::ensureProfileURL($url);
            } catch (Exception $e) {
                common_log(LOG_ERR, 'Profile lookup failed for ' .
                                    $arg . ': ' . $e->getMessage());
            }
        }
        return null;
    }

    public function onEndProfileSettingsActions($out) {
        $siteName = common_config('site', 'name');
        $js = 'navigator.registerContentHandler("application/vnd.mozilla.maybe.feed", "'.addslashes(common_local_url('RemoteFollowSub', null, array('profile' => '%s'))).'", "'.addslashes($siteName).'")';
        $out->elementStart('li');
        $out->element(
            'a',
            ['href' => 'javascript:' . $js],
            // TRANS: Option in profile settings to add this instance to Firefox as a feedreader
            _('Add to Firefox as feedreader')
        );
        $out->elementEnd('li');
    }

    /**
     * Make sure necessary tables are filled out.
     */
    public function onCheckSchema()
    {
        $schema = Schema::get();
        $schema->ensureTable('ostatus_profile', Ostatus_profile::schemaDef());
        $schema->ensureTable('feedsub', FeedSub::schemaDef());
        $schema->ensureTable('hubsub', HubSub::schemaDef());
        $schema->ensureTable('magicsig', Magicsig::schemaDef());
        return true;
    }

    public function onEndShowStylesheets(Action $action)
    {
        $action->cssLink($this->path('theme/base/css/ostatus.css'));
        return true;
    }

    public function onEndShowStatusNetScripts($action)
    {
        $action->script($this->path('js/ostatus.js'));
        return true;
    }

    /**
     * Override the "from ostatus" bit in notice lists to link to the
     * original post and show the domain it came from.
     *
     * @param Notice in $notice
     * @param string out &$name
     * @param string out &$url
     * @param string out &$title
     * @return mixed hook return code
     */
    public function onStartNoticeSourceLink($notice, &$name, &$url, &$title)
    {
        // If we don't handle this, keep the event handler going
        if (!in_array($notice->source, array('ostatus', 'share'))) {
            return true;
        }

        try {
            $url = $notice->getUrl();
            // If getUrl() throws exception, $url is never set

            $bits = parse_url($url);
            $domain = $bits['host'];
            if (substr($domain, 0, 4) == 'www.') {
                $name = substr($domain, 4);
            } else {
                $name = $domain;
            }

            // TRANS: Title. %s is a domain name.
            $title = sprintf(_m('Sent from %s via OStatus'), $domain);

            // Abort event handler, we have a name and URL!
            return false;
        } catch (InvalidUrlException $e) {
            // This just means we don't have the notice source data
            return true;
        }
    }

    /**
     * Send incoming WebSub feeds for OStatus endpoints in for processing.
     *
     * @param FeedSub $feedsub
     * @param DOMDocument $feed
     * @return mixed hook return code
     */
    public function onStartFeedSubReceive($feedsub, $feed)
    {
        $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
        if ($oprofile instanceof Ostatus_profile) {
            $oprofile->processFeed($feed, 'push');
        } else {
            common_log(LOG_DEBUG, "No ostatus profile for incoming feed $feedsub->uri");
        }
    }

    /**
     * Tell the FeedSub infrastructure whether we have any active OStatus
     * usage for the feed; if not it'll be able to garbage-collect the
     * feed subscription.
     *
     * @param FeedSub $feedsub
     * @param integer $count in/out
     * @return mixed hook return code
     */
    public function onFeedSubSubscriberCount($feedsub, &$count)
    {
        $oprofile = Ostatus_profile::getKV('feeduri', $feedsub->uri);
        if ($oprofile instanceof Ostatus_profile) {
            $count += $oprofile->subscriberCount();
        }
        return true;
    }

    /**
     * When about to subscribe to a remote user, start a server-to-server
     * WebSub subscription if needed. If we can't establish that, abort.
     *
     * @fixme If something else aborts later, we could end up with a stray
     *        WebSub subscription. This is relatively harmless, though.
     *
     * @param Profile $profile  subscriber
     * @param Profile $other    subscribee
     *
     * @return hook return code
     *
     * @throws Exception
     */
    public function onStartSubscribe(Profile $profile, Profile $other)
    {
        if (!$profile->isLocal()) {
            return true;
        }

        $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        $oprofile->subscribe();
    }

    /**
     * Having established a remote subscription, send a notification to the
     * remote OStatus profile's endpoint.
     *
     * @param Profile $profile  subscriber
     * @param Profile $other    subscribee
     *
     * @return hook return code
     *
     * @throws Exception
     */
    public function onEndSubscribe(Profile $profile, Profile $other)
    {
        if (!$profile->isLocal()) {
            return true;
        }

        $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        $sub = Subscription::pkeyGet(array('subscriber' => $profile->id,
                                           'subscribed' => $other->id));

        $act = $sub->asActivity();

        $oprofile->notifyActivity($act, $profile);

        return true;
    }

    /**
     * Notify remote server and garbage collect unused feeds on unsubscribe.
     * @todo FIXME: Send these operations to background queues
     *
     * @param User $user
     * @param Profile $other
     * @return hook return value
     */
    public function onEndUnsubscribe(Profile $profile, Profile $other)
    {
        if (!$profile->isLocal()) {
            return true;
        }

        $oprofile = Ostatus_profile::getKV('profile_id', $other->id);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        // Drop the WebSub subscription if there are no other subscribers.
        $oprofile->garbageCollect();

        $act = new Activity();

        $act->verb = ActivityVerb::UNFOLLOW;

        $act->id   = TagURI::mint(
            'unfollow:%d:%d:%s',
            $profile->id,
            $other->id,
            common_date_iso8601(time())
        );

        $act->time    = time();
        // TRANS: Title for unfollowing a remote profile.
        $act->title   = _m('TITLE', 'Unfollow');
        // TRANS: Success message for unsubscribe from user attempt through OStatus.
        // TRANS: %1$s is the unsubscriber's name, %2$s is the unsubscribed user's name.
        $act->content = sprintf(
            _m('%1$s stopped following %2$s.'),
            $profile->getBestName(),
            $other->getBestName()
        );

        $act->actor   = $profile->asActivityObject();
        $act->objects[] = $other->asActivityObject();

        $oprofile->notifyActivity($act, $profile);

        return true;
    }

    /**
     * When one of our local users tries to join a remote group,
     * notify the remote server. If the notification is rejected,
     * deny the join.
     *
     * @param User_group $group
     * @param Profile    $profile
     *
     * @return mixed hook return value
     * @throws Exception of various kinds, some from $oprofile->subscribe();
     */
    public function onStartJoinGroup($group, $profile)
    {
        $oprofile = Ostatus_profile::getKV('group_id', $group->id);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        $oprofile->subscribe();

        // NOTE: we don't use Group_member::asActivity() since that record
        // has not yet been created.

        $act = new Activity();
        $act->id = TagURI::mint(
            'join:%d:%d:%s',
            $profile->id,
            $group->id,
            common_date_iso8601(time())
        );

        $act->actor = $profile->asActivityObject();
        $act->verb = ActivityVerb::JOIN;
        $act->objects[] = $oprofile->asActivityObject();

        $act->time = time();
        // TRANS: Title for joining a remote groep.
        $act->title = _m('TITLE', 'Join');
        // TRANS: Success message for subscribe to group attempt through OStatus.
        // TRANS: %1$s is the member name, %2$s is the subscribed group's name.
        $act->content = sprintf(
            _m('%1$s has joined group %2$s.'),
            $profile->getBestName(),
            $oprofile->getBestName()
        );

        if ($oprofile->notifyActivity($act, $profile)) {
            return true;
        } else {
            $oprofile->garbageCollect();
            // TRANS: Exception thrown when joining a remote group fails.
            throw new Exception(_m('Failed joining remote group.'));
        }
    }

    /**
     * When one of our local users leaves a remote group, notify the remote
     * server.
     *
     * @fixme Might be good to schedule a resend of the leave notification
     * if it failed due to a transitory error. We've canceled the local
     * membership already anyway, but if the remote server comes back up
     * it'll be left with a stray membership record.
     *
     * @param User_group $group
     * @param Profile $profile
     *
     * @return mixed hook return value
     */
    public function onEndLeaveGroup($group, $profile)
    {
        $oprofile = Ostatus_profile::getKV('group_id', $group->id);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        // Drop the WebSub subscription if there are no other subscribers.
        $oprofile->garbageCollect();

        $member = $profile;

        $act = new Activity();
        $act->id = TagURI::mint(
            'leave:%d:%d:%s',
            $member->id,
            $group->id,
            common_date_iso8601(time())
        );

        $act->actor = $member->asActivityObject();
        $act->verb = ActivityVerb::LEAVE;
        $act->objects[] = $oprofile->asActivityObject();

        $act->time = time();
        // TRANS: Title for leaving a remote group.
        $act->title = _m('TITLE', 'Leave');
        // TRANS: Success message for unsubscribe from group attempt through OStatus.
        // TRANS: %1$s is the member name, %2$s is the unsubscribed group's name.
        $act->content = sprintf(
            _m('%1$s has left group %2$s.'),
            $member->getBestName(),
            $oprofile->getBestName()
        );

        $oprofile->notifyActivity($act, $member);
    }

    /**
     * When one of our local users tries to subscribe to a remote peopletag,
     * notify the remote server. If the notification is rejected,
     * deny the subscription.
     *
     * @param Profile_list $peopletag
     * @param User         $user
     *
     * @return mixed hook return value
     * @throws Exception of various kinds, some from $oprofile->subscribe();
     */

    public function onStartSubscribePeopletag($peopletag, $user)
    {
        $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        $oprofile->subscribe();

        $sub = $user->getProfile();
        $tagger = Profile::getKV($peopletag->tagger);

        $act = new Activity();
        $act->id = TagURI::mint(
            'subscribe_peopletag:%d:%d:%s',
            $sub->id,
            $peopletag->id,
            common_date_iso8601(time())
        );

        $act->actor = $sub->asActivityObject();
        $act->verb = ActivityVerb::FOLLOW;
        $act->objects[] = $oprofile->asActivityObject();

        $act->time = time();
        // TRANS: Title for following a remote list.
        $act->title = _m('TITLE', 'Follow list');
        // TRANS: Success message for remote list follow through OStatus.
        // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
        $act->content = sprintf(
            _m('%1$s is now following people listed in %2$s by %3$s.'),
            $sub->getBestName(),
            $oprofile->getBestName(),
            $tagger->getBestName()
        );

        if ($oprofile->notifyActivity($act, $sub)) {
            return true;
        } else {
            $oprofile->garbageCollect();
            // TRANS: Exception thrown when subscription to remote list fails.
            throw new Exception(_m('Failed subscribing to remote list.'));
        }
    }

    /**
     * When one of our local users unsubscribes to a remote peopletag, notify the remote
     * server.
     *
     * @param Profile_list $peopletag
     * @param User         $user
     *
     * @return mixed hook return value
     */

    public function onEndUnsubscribePeopletag($peopletag, $user)
    {
        $oprofile = Ostatus_profile::getKV('peopletag_id', $peopletag->id);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        // Drop the WebSub subscription if there are no other subscribers.
        $oprofile->garbageCollect();

        $sub = Profile::getKV($user->id);
        $tagger = Profile::getKV($peopletag->tagger);

        $act = new Activity();
        $act->id = TagURI::mint(
            'unsubscribe_peopletag:%d:%d:%s',
            $sub->id,
            $peopletag->id,
            common_date_iso8601(time())
        );

        $act->actor = $member->asActivityObject();
        $act->verb = ActivityVerb::UNFOLLOW;
        $act->objects[] = $oprofile->asActivityObject();

        $act->time = time();
        // TRANS: Title for unfollowing a remote list.
        $act->title = _m('Unfollow list');
        // TRANS: Success message for remote list unfollow through OStatus.
        // TRANS: %1$s is the subscriber name, %2$s is the list, %3$s is the lister's name.
        $act->content = sprintf(
            _m('%1$s stopped following the list %2$s by %3$s.'),
            $sub->getBestName(),
            $oprofile->getBestName(),
            $tagger->getBestName()
        );

        $oprofile->notifyActivity($act, $user);
    }

    /**
     * Notify remote users when their notices get favorited.
     *
     * @param Profile or User $profile of local user doing the faving
     * @param Notice $notice being favored
     * @return hook return value
     */
    public function onEndFavorNotice(Profile $profile, Notice $notice)
    {
        // Only distribute local users' favor actions, remote users
        // will have already distributed theirs.
        if (!$profile->isLocal()) {
            return true;
        }

        $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        $fav = Fave::pkeyGet(array('user_id' => $profile->id,
                                   'notice_id' => $notice->id));

        if (!$fav instanceof Fave) {
            // That's weird.
            // TODO: Make pkeyGet throw exception, since this is a critical failure.
            return true;
        }

        $act = $fav->asActivity();

        $oprofile->notifyActivity($act, $profile);

        return true;
    }

    /**
     * Notify remote user it has got a new people tag
     *   - tag verb is queued
     *   - the subscription is done immediately if not present
     *
     * @param Profile_tag $ptag the people tag that was created
     * @return hook return value
     * @throws Exception of various kinds, some from $oprofile->subscribe();
     */
    public function onEndTagProfile($ptag)
    {
        $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        $plist = $ptag->getMeta();
        if ($plist->private) {
            return true;
        }

        $act = new Activity();

        $tagger = $plist->getTagger();
        $tagged = Profile::getKV('id', $ptag->tagged);

        $act->verb = ActivityVerb::TAG;
        $act->id   = TagURI::mint(
            'tag_profile:%d:%d:%s',
            $plist->tagger,
            $plist->id,
            common_date_iso8601(time())
        );
        $act->time = time();
        // TRANS: Title for listing a remote profile.
        $act->title = _m('TITLE', 'List');
        // TRANS: Success message for remote list addition through OStatus.
        // TRANS: %1$s is the list creator's name, %2$s is the added list member, %3$s is the list name.
        $act->content = sprintf(
            _m('%1$s listed %2$s in the list %3$s.'),
            $tagger->getBestName(),
            $tagged->getBestName(),
            $plist->getBestName()
        );

        $act->actor  = $tagger->asActivityObject();
        $act->objects = array($tagged->asActivityObject());
        $act->target = ActivityObject::fromPeopletag($plist);

        $oprofile->notifyDeferred($act, $tagger);

        // initiate a WebSub subscription for the person being tagged
        $oprofile->subscribe();
        return true;
    }

    /**
     * Notify remote user that a people tag has been removed
     *   - untag verb is queued
     *   - the subscription is undone immediately if not required
     *     i.e garbageCollect()'d
     *
     * @param Profile_tag $ptag the people tag that was deleted
     * @return hook return value
     */
    public function onEndUntagProfile($ptag)
    {
        $oprofile = Ostatus_profile::getKV('profile_id', $ptag->tagged);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        $plist = $ptag->getMeta();
        if ($plist->private) {
            return true;
        }

        $act = new Activity();

        $tagger = $plist->getTagger();
        $tagged = Profile::getKV('id', $ptag->tagged);

        $act->verb = ActivityVerb::UNTAG;
        $act->id   = TagURI::mint(
            'untag_profile:%d:%d:%s',
            $plist->tagger,
            $plist->id,
            common_date_iso8601(time())
        );
        $act->time = time();
        // TRANS: Title for unlisting a remote profile.
        $act->title = _m('TITLE', 'Unlist');
        // TRANS: Success message for remote list removal through OStatus.
        // TRANS: %1$s is the list creator's name, %2$s is the removed list member, %3$s is the list name.
        $act->content = sprintf(
            _m('%1$s removed %2$s from the list %3$s.'),
            $tagger->getBestName(),
            $tagged->getBestName(),
            $plist->getBestName()
        );

        $act->actor  = $tagger->asActivityObject();
        $act->objects = array($tagged->asActivityObject());
        $act->target = ActivityObject::fromPeopletag($plist);

        $oprofile->notifyDeferred($act, $tagger);

        // unsubscribe to WebSub feed if no more required
        $oprofile->garbageCollect();

        return true;
    }

    /**
     * Notify remote users when their notices get de-favorited.
     *
     * @param Profile $profile Profile person doing the de-faving
     * @param Notice  $notice  Notice being favored
     *
     * @return hook return value
     */
    public function onEndDisfavorNotice(Profile $profile, Notice $notice)
    {
        // Only distribute local users' disfavor actions, remote users
        // will have already distributed theirs.
        if (!$profile->isLocal()) {
            return true;
        }

        $oprofile = Ostatus_profile::getKV('profile_id', $notice->profile_id);
        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        $act = new Activity();

        $act->verb = ActivityVerb::UNFAVORITE;
        $act->id   = TagURI::mint(
            'disfavor:%d:%d:%s',
            $profile->id,
            $notice->id,
            common_date_iso8601(time())
        );
        $act->time    = time();
        // TRANS: Title for unliking a remote notice.
        $act->title   = _m('Unlike');
        // TRANS: Success message for remove a favorite notice through OStatus.
        // TRANS: %1$s is the unfavoring user's name, %2$s is URI to the no longer favored notice.
        $act->content = sprintf(
            _m('%1$s no longer likes %2$s.'),
            $profile->getBestName(),
            $notice->getUrl()
        );

        $act->actor   = $profile->asActivityObject();
        $act->objects[]  = $notice->asActivityObject();

        $oprofile->notifyActivity($act, $profile);

        return true;
    }

    public function onStartGetProfileUri($profile, &$uri)
    {
        $oprofile = Ostatus_profile::getKV('profile_id', $profile->id);
        if ($oprofile instanceof Ostatus_profile) {
            $uri = $oprofile->uri;
            return false;
        }
        return true;
    }

    public function onStartUserGroupHomeUrl($group, &$url)
    {
        return $this->onStartUserGroupPermalink($group, $url);
    }

    public function onStartUserGroupPermalink($group, &$url)
    {
        $oprofile = Ostatus_profile::getKV('group_id', $group->id);
        if ($oprofile instanceof Ostatus_profile) {
            // @fixme this should probably be in the user_group table
            // @fixme this uri not guaranteed to be a profile page
            $url = $oprofile->uri;
            return false;
        }
    }

    public function onStartShowUserGroupsContent($action)
    {
        $this->showEntityRemoteSubscribe($action);

        return true;
    }

    public function onEndShowGroupsMiniList($action)
    {
        $this->showEntityRemoteSubscribe($action);

        return true;
    }

    public function showEntityRemoteSubscribe($action)
    {
        if (!$action->getScoped() instanceof Profile) {
            // early return if we're not logged in
            return true;
        }

        if ($action->getScoped()->sameAs($action->getTarget())) {
            $action->elementStart('div', 'entity_actions');
            $action->elementStart('p', array('id' => 'entity_remote_subscribe',
                                             'class' => 'entity_subscribe'));
            $action->element(
                'a',
                [
                    'href' => common_local_url('ostatusgroup'),
                    'class' => 'entity_remote_subscribe',
                ],
                // TRANS: Link text for link to remote subscribe.
                _m('Remote')
            );
            $action->elementEnd('p');
            $action->elementEnd('div');
        }
    }

    /**
     * Ping remote profiles with updates to this profile.
     * Salmon pings are queued for background processing.
     */
    public function onEndBroadcastProfile(Profile $profile)
    {
        $user = User::getKV('id', $profile->id);

        // Find foreign accounts I'm subscribed to that support Salmon pings.
        //
        // @fixme we could run updates through the WebSub feed too,
        // in which case we can skip Salmon pings to folks who
        // are also subscribed to me.
        $sql = "SELECT * FROM ostatus_profile " .
               "WHERE profile_id IN " .
               "(SELECT subscribed FROM subscription WHERE subscriber=%d) " .
               "OR group_id IN " .
               "(SELECT group_id FROM group_member WHERE profile_id=%d)";
        $oprofile = new Ostatus_profile();
        $oprofile->query(sprintf($sql, $profile->id, $profile->id));

        if ($oprofile->N == 0) {
            common_log(LOG_DEBUG, "No OStatus remote subscribees for $profile->nickname");
            return true;
        }

        $act = new Activity();

        $act->verb = ActivityVerb::UPDATE_PROFILE;
        $act->id   = TagURI::mint(
            'update-profile:%d:%s',
            $profile->id,
            common_date_iso8601(time())
        );
        $act->time    = time();
        // TRANS: Title for activity.
        $act->title   = _m('Profile update');
        // TRANS: Ping text for remote profile update through OStatus.
        // TRANS: %s is user that updated their profile.
        $act->content = sprintf(
            _m('%s has updated their profile page.'),
            $profile->getBestName()
        );

        $act->actor   = $profile->asActivityObject();
        $act->objects[]  = $act->actor;

        while ($oprofile->fetch()) {
            $oprofile->notifyDeferred($act, $profile);
        }

        return true;
    }

    public function onEndShowAccountProfileBlock(HTMLOutputter $out, Profile $profile)
    {
        if ($profile->isLocal()) {
            return true;
        }
        try {
            $oprofile = Ostatus_profile::fromProfile($profile);
        } catch (Exception $e) {
            // Not a remote Ostatus_profile! Maybe some other network
            // that has imported a non-local user?
            return true;
        }
        try {
            $feedsub = $oprofile->getFeedSub();
        } catch (NoResultException $e) {
            // No WebSub subscription has been attempted or exists for this profile
            // which is the case, say for remote profiles that are only included
            // via mentions or repeat/share.
            return true;
        }

        $websub_states = [
            'subscribe'   => _m('Pending'),
            'active'      => _m('Active'),
            'unsubscribe' => _m('Unsubscribing'),
            'nohub'       => _m('Polling'),
            'inactive'    => _m('Inactive'),
        ];
        $out->elementStart('dl', 'entity_tags ostatus_profile');
        $out->element('dt', null, _m('WebSub'));
        $out->element('dd', null, $websub_states[$feedsub->sub_state]);
        $out->elementEnd('dl');
    }

    // FIXME: This one can accept both an Action and a Widget. Confusing! Refactor to (HTMLOutputter $out, Profile $target)!
    public function onStartProfileListItemActionElements($item)
    {
        if (common_logged_in()) {
            // only non-logged in users get to see the "remote subscribe" form
            return true;
        }

        $target = $item->getTarget();

        if (!$target->isLocal()) {
            // we can (for now) only provide remote subscribe forms for local users
            return true;
        }

        if ($item instanceof ProfileAction) {
            $output = $item;
        } elseif ($item instanceof Widget) {
            $output = $item->out;
        } else {
            // Bad $item class, don't know how to use this for outputting!
            throw new ServerException('Bad item type for onStartProfileListItemActionElements');
        }

        $output->elementStart('li', 'entity_tag');
        $url = common_local_url('ostatustag', ['nickname' => $target->getNickname()]);
        $output->element('a',
                         [
                             'href'  => $url,
                             'class' => 'entity_remote_tag',
                         ],
                         // TRANS: Link text for a user to list an OStatus user.
                         _m('List'));
        $output->elementEnd('li');

        return true;
    }

    /**
     * Plugin Nodeinfo information
     *
     * @param array $protocols
     * @return bool hook true
     */
    public function onNodeInfoProtocols(array &$protocols)
    {
        $protocols[] = "ostatus";
        return true;
    }

    public function onPluginVersion(array &$versions): bool
    {
        $versions[] = array('name' => 'OStatus',
                            'version' => self::PLUGIN_VERSION,
                            'author' => 'Evan Prodromou, James Walker, Brion Vibber, Zach Copley',
                            'homepage' => GNUSOCIAL_ENGINE_REPO_URL . 'tree/master/plugins/OStatus',
                            // TRANS: Plugin description.
                            'rawdescription' => _m('Follow people across social networks that implement '.
                               '<a href="http://ostatus.org/">OStatus</a>.'));

        return true;
    }

    /**
     * Utility function to check if the given URI is a canonical group profile
     * page, and if so return the ID number.
     *
     * @param string $url
     * @return mixed int or false
     */
    public static function localGroupFromUrl($url)
    {
        $group = User_group::getKV('uri', $url);
        if ($group instanceof User_group) {
            if ($group->isLocal()) {
                return $group->id;
            }
        } else {
            // To find local groups which haven't had their uri fields filled out...
            // If the domain has changed since a subscriber got the URI, it'll
            // be broken.
            $template = common_local_url('groupbyid', array('id' => '31337'));
            $template = preg_quote($template, '/');
            $template = str_replace('31337', '(\d+)', $template);
            if (preg_match("/$template/", $url, $matches)) {
                return intval($matches[1]);
            }
        }
        return false;
    }

    public function onStartProfileGetAtomFeed($profile, &$feed)
    {
        $oprofile = Ostatus_profile::getKV('profile_id', $profile->id);

        if (!$oprofile instanceof Ostatus_profile) {
            return true;
        }

        $feed = $oprofile->feeduri;
        return false;
    }

    public function onStartGetProfileFromURI($uri, &$profile)
    {
        // Don't want to do Web-based discovery on our own server,
        // so we check locally first. This duplicates the functionality
        // in the Profile class, since the plugin always runs before
        // that local lookup, but since we return false it won't run double.

        $user = User::getKV('uri', $uri);
        if ($user instanceof User) {
            $profile = $user->getProfile();
            return false;
        } else {
            $group = User_group::getKV('uri', $uri);
            if ($group instanceof User_group) {
                $profile = $group->getProfile();
                return false;
            }
        }

        // Now, check remotely
        try {
            $oprofile = Ostatus_profile::ensureProfileURI($uri);
            $profile = $oprofile->localProfile();
            return !($profile instanceof Profile);  // localProfile won't throw exception but can return null
        } catch (Exception $e) {
            return true; // It's not an OStatus profile as far as we know, continue event handling
        }
    }

    public function onEndWebFingerNoticeLinks(XML_XRD $xrd, Notice $target)
    {
        $salmon_url = null;
        $actor = $target->getProfile();
        if ($actor->isLocal()) {
            $profiletype = $this->profileTypeString($actor);
            $salmon_url = common_local_url("{$profiletype}salmon", array('id' => $actor->getID()));
        } else {
            try {
                $oprofile = Ostatus_profile::fromProfile($actor);
                $salmon_url = $oprofile->salmonuri;
            } catch (Exception $e) {
                // Even though it's not a local user, we couldn't get an Ostatus_profile?!
            }
        }
        // Ostatus_profile salmon URL may be empty
        if (!empty($salmon_url)) {
            $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url);
        }
        return true;
    }

    public function onEndWebFingerProfileLinks(XML_XRD $xrd, Profile $target)
    {
        if ($target->getObjectType() === ActivityObject::PERSON) {
            $this->addWebFingerPersonLinks($xrd, $target);
        } elseif ($target->getObjectType() === ActivityObject::GROUP) {
            $xrd->links[] = new XML_XRD_Element_Link(
                Discovery::UPDATESFROM,
                common_local_url(
                    'ApiTimelineGroup',
                    ['id' => $target->getGroup()->getID(), 'format' => 'atom']
                ),
                'application/atom+xml'
            );
        }

        // Salmon
        $profiletype = $this->profileTypeString($target);
        $salmon_url = common_local_url("{$profiletype}salmon", array('id' => $target->id));

        $xrd->links[] = new XML_XRD_Element_Link(Salmon::REL_SALMON, $salmon_url);

        // XXX: these are deprecated, but StatusNet only looks for NS_REPLIES
        $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_REPLIES, $salmon_url);
        $xrd->links[] = new XML_XRD_Element_Link(Salmon::NS_MENTIONS, $salmon_url);

        return true;
    }

    protected function profileTypeString(Profile $target)
    {
        // This is just used to have a definitive string response to "USERsalmon" or "GROUPsalmon"
        switch ($target->getObjectType()) {
        case ActivityObject::PERSON:
            return 'user';
        case ActivityObject::GROUP:
            return 'group';
        default:
            throw new ServerException('Unknown profile type for WebFinger profile links');
        }
    }

    protected function addWebFingerPersonLinks(XML_XRD $xrd, Profile $target)
    {
        $xrd->links[] = new XML_XRD_Element_Link(
            Discovery::UPDATESFROM,
            common_local_url(
                'ApiTimelineUser',
                ['id' => $target->id, 'format' => 'atom']
            ),
            'application/atom+xml'
        );

        // Get this profile's keypair
        $magicsig = Magicsig::getKV('user_id', $target->id);
        if (!$magicsig instanceof Magicsig && $target->isLocal()) {
            $magicsig = Magicsig::generate($target->getUser());
        }

        if (!$magicsig instanceof Magicsig) {
            return false;   // value doesn't mean anything, just figured I'd indicate this function didn't do anything
        }
        if (Event::handle('StartAttachPubkeyToUserXRD', array($magicsig, $xrd, $target))) {
            $xrd->links[] = new XML_XRD_Element_Link(
                Magicsig::PUBLICKEYREL,
                'data:application/magic-public-key,' . $magicsig->toString()
            );
            // The following event handles plugins like Diaspora which add their own version of the Magicsig pubkey
            Event::handle('EndAttachPubkeyToUserXRD', array($magicsig, $xrd, $target));
        }
    }

    public function onGetLocalAttentions(Profile $actor, array $attention_uris, array &$mentions, array &$groups)
    {
        list($groups, $mentions) = Ostatus_profile::filterAttention($actor, $attention_uris);
    }

    // FIXME: Maybe this shouldn't be so authoritative that it breaks other remote profile lookups?
    public static function onCheckActivityAuthorship(Activity $activity, Profile &$profile)
    {
        try {
            $oprofile = Ostatus_profile::ensureProfileURL($profile->getUrl());
            $profile = $oprofile->checkAuthorship($activity);
        } catch (Exception $e) {
            common_log(LOG_ERR, 'Could not get a profile or check authorship ('.get_class($e).': "'.$e->getMessage().'") for activity ID: '.$activity->id);
            $profile = null;
            return false;
        }
        return true;
    }

    public function onProfileDeleteRelated($profile, &$related)
    {
        // Ostatus_profile has a 'profile_id' property, which will be used to find the object
        $related[] = 'Ostatus_profile';

        // Magicsig has a "user_id" column instead, so we have to delete it more manually:
        $magicsig = Magicsig::getKV('user_id', $profile->id);
        if ($magicsig instanceof Magicsig) {
            $magicsig->delete();
        }
        return true;
    }

    public function onSalmonSlap($endpoint_uri, MagicEnvelope $magic_env, Profile $target=null)
    {
        try {
            $envxml = $magic_env->toXML($target);
        } catch (Exception $e) {
            common_log(LOG_ERR, sprintf('Could not generate Magic Envelope XML for profile id=='.$target->getID().': '.$e->getMessage()));
            return false;
        }

        $headers = array('Content-Type: application/magic-envelope+xml');

        try {
            $client = new HTTPClient();
            $client->setBody($envxml);
            $response = $client->post($endpoint_uri, $headers);
        } catch (Exception $e) {
            common_log(LOG_ERR, "Salmon post to $endpoint_uri failed: " . $e->getMessage());
            return false;
        }
        if ($response->getStatus() === 422) {
            common_debug(sprintf('Salmon (from profile %d) endpoint %s returned status %s. We assume it is a Diaspora seed; will adapt and try again if that plugin is enabled!', $magic_env->getActor()->getID(), $endpoint_uri, $response->getStatus()));
            return true;
        }

        // The different kinds of accepted responses...
        // 200 OK means it's all ok
        // 201 Created is what Mastodon returns when it's ok
        // 202 Accepted is what we get from Diaspora, also good
        if (!in_array($response->getStatus(), array(200, 201, 202))) {
            common_log(LOG_ERR, sprintf(
                'Salmon (from profile %d) endpoint %s returned status %s: %s',
                $magic_env->getActor()->getID(),
                $endpoint_uri,
                $response->getStatus(),
                $response->getBody()
            ));
            return true;
        }

        // Since we completed the salmon slap, we discontinue the event
        return false;
    }

    public function onCronDaily()
    {
        try {
            $sub = FeedSub::renewalCheck();
        } catch (NoResultException $e) {
            common_log(LOG_INFO, "There were no expiring feeds.");
            return;
        }

        $qm = QueueManager::get();
        while ($sub->fetch()) {
            $item = array('feedsub_id' => $sub->id);
            $qm->enqueue($item, 'pushrenew');
        }
    }

    /**
     * Try to grab and store the remote profile by the given uri
     *
     * @param string $uri
     * @param Profile &$profile
     * @return bool
     */
    public function onRemoteFollowPullProfile(string $uri, ?Profile &$profile): bool
    {
        $oprofile = pullRemoteProfile($uri);
        if ($oprofile instanceof Ostatus_profile) {

            // validation
            if ($oprofile->isGroup()) {
                $target = common_local_url('ostatusgroup', [], ['profile' => $uri]);
                common_redirect($target, 303);
            } else if ($oprofile->isPeopletag()) {
                $target = common_local_url('ostatuspeopletag', [], ['profile' => $uri]);
                common_redirect($target, 303);
            }

            $profile = $oprofile->localProfile();
        }

        return is_null($profile);
    }
}


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

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

Additional functionality:

The RemoteFollow plugin is highly recommended as it is the only way to remote
follow a user in OStatus, it adds a remote follow button to user profiles.

Configuration options available:

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

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

Settings controlling incoming feed subscription:

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

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

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

For testing, shouldn't be used in production:

$config['ostatus']['skip_signatures']
    (default use signatures)
    Disable generation and validation of Salmon magicenv signatures

$config['feedsub']['nohub']
    (default require hub)
    Allow low-level feed subscription setup for feeds without hubs.
    Not actually usable at this stage, OStatus will check for hubs too
    and we have no polling backend. (The fallback hub option can be used
    with a 3rd-party service to provide such polling.)

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

repo_name: spookie/gnu

path: plugins/OStatus/README

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

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

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

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

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

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

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

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

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

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

        return true;
    }

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

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

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

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

    /**
     * We've gotten a follow/subscribe notification from a remote user.
     * Save a subscription relationship for them.
     */

    /**
     * Postel's law: consider a "follow" notification as a "join".
     */
    function handleFollow()
    {
        $this->handleJoin();
    }

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

    /**
     * A remote user joined our group.
     * @fixme move permission checks and event call into common code,
     *        currently we're doing the main logic in joingroup action
     *        and so have to repeat it here.
     */
    function handleJoin()
    {
        if ($this->oprofile->isGroup()) {
            // TRANS: Client error.
            $this->clientError(_m('Groups cannot join groups.'));
        }

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

        if ($this->actor->isMember($this->group)) {
            // Already a member; we'll take it silently to aid in resolving
            // inconsistencies on the other side.
            return true;
        }

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

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

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

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

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


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2009-2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

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

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

/**
 * Key UI methods:
 *
 *  showInputForm() - form asking for a remote profile account or URL
 *                    We end up back here on errors
 *
 *  showPreviewForm() - surrounding form for preview-and-confirm
 *    preview() - display profile for a remote group
 *
 *  success() - redirects to groups page on join
 */
class OStatusGroupAction extends OStatusSubAction
{
    protected $profile_uri; // provided acct: or URI of remote entity
    protected $oprofile; // Ostatus_profile of remote entity, if valid

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

    /**
     * Show the initial form, when we haven't yet been given a valid
     * remote profile.
     */
    function showInputForm()
    {
        $this->elementStart('form', array('method' => 'post',
                                          'id' => 'form_ostatus_sub',
                                          'class' => 'form_settings',
                                          'action' => $this->selfLink()));

        $this->hidden('token', common_session_token());

        $this->elementStart('fieldset', array('id' => 'settings_feeds'));

        $this->elementStart('ul', 'form_data');
        $this->elementStart('li');
        $this->input('profile',
                     // TRANS: Field label.
                     _m('Join group'),
                     $this->profile_uri,
                     // TRANS: Tooltip for field label "Join group". Do not translate the "example.net"
                     // TRANS: domain name in the URL, as it is an official standard domain name for examples.
                     _m("OStatus group's address, like http://example.net/group/nickname."));
        $this->elementEnd('li');
        $this->elementEnd('ul');

        // TRANS: Button text.
        $this->submit('validate', _m('BUTTON','Continue'));

        $this->elementEnd('fieldset');

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

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

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

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

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

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

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

        $this->success();
    }

    /**
     * Title of the page
     *
     * @return string Title of the page
     */
    function title()
    {
        // TRANS: Page title for OStatus remote group join form
        return _m('Confirm joining remote group');
    }

    /**
     * Instructions for use
     *
     * @return instructions for use
     */
    function getInstructions()
    {
        // TRANS: Form instructions.
        return _m('You can subscribe to groups from other supported sites. Paste the group\'s profile URI below:');
    }

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


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/**
 * @package OStatusPlugin
 * @maintainer James Walker <james@status.net>
 */

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

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

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

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

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

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

        return true;
    }

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

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

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

    function showContent()
    {

        if ($this->group) {
            // TRANS: Form legend. %s is a group name.
            $header = sprintf(_m('Join group %s'), $this->group);
            // TRANS: Button text to join a group.
            $submit = _m('BUTTON','Join');
        } else if ($this->peopletag && $this->tagger) {
            // TRANS: Form legend. %1$s is a list, %2$s is a lister's name.
            $header = sprintf(_m('Subscribe to list %1$s by %2$s'), $this->peopletag, $this->tagger);
            // TRANS: Button text to subscribe to a list.
            $submit = _m('BUTTON','Subscribe');
            // TRANS: Button text.
        } else {
            // TRANS: Form legend. %s is a nickname.
            $header = sprintf(_m('Subscribe to %s'), $this->nickname);
            // TRANS: Button text to subscribe to a profile.
            $submit = _m('BUTTON','Subscribe');
        }
        $this->elementStart('form', array('id' => 'form_ostatus_connect',
                                          'method' => 'post',
                                          'class' => 'form_settings',
                                          'action' => common_local_url('ostatusinit')));
        $this->elementStart('fieldset');
        $this->element('legend', null,  $header);
        $this->hidden('token', common_session_token());

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

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

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

    function ostatusConnect()
    {
        $validate = new Validate();
        $opts = array('allowed_schemes' => array('http', 'https', 'acct'));
        if ($validate->uri($this->profile, $opts)) {
            $bits = parse_url($this->profile);
            if ($bits['scheme'] == 'acct') {
                $this->connectWebfinger($bits['path']);
            } else {
                $this->connectProfile($this->profile);
            }
        } elseif (strpos($this->profile, '@') !== false) {
            $this->connectWebfinger($this->profile);
        } else {
            // TRANS: Client error.
            $this->clientError(_m('Must provide a remote profile.'));
        }
    }

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

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

        $link = $xrd->get('http://ostatus.org/schema/1.0/subscribe');
        if (!is_null($link)) {
            // We found a URL - let's redirect!
            if (!empty($link->template)) {
                $url = Discovery::applyTemplate($link->template, $target_profile);
            } else {
                $url = $link->href;
            }
            common_log(LOG_INFO, "Sending remote subscriber $acct to $url");
            common_redirect($url, 303);
        }
        // TRANS: Client error.
        $this->clientError(_m('Could not confirm remote profile address.'));
    }

    function connectProfile($subscriber_profile)
    {
        $target_profile = $this->targetProfile();

        // @fixme hack hack! We should look up the remote sub URL from XRDS
        $suburl = preg_replace('!^(.*)/(.*?)$!', '$1/main/ostatussub', $subscriber_profile);
        $suburl .= '?profile=' . urlencode($target_profile);

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

    /**
     * Build the canonical profile URI+URL of the requested user or group
     */
    function targetProfile()
    {
        if ($this->nickname) {
            $user = User::getKV('nickname', $this->nickname);
            if ($user) {
                return common_local_url('userbyid', array('id' => $user->id));
            } else {
                // TRANS: Client error.
                $this->clientError(_m('No such user.'));
            }
        } else if ($this->group) {
            $group = Local_group::getKV('nickname', $this->group);
            if ($group instanceof Local_group) {
                return common_local_url('groupbyid', array('id' => $group->group_id));
            } else {
                // TRANS: Client error.
                $this->clientError(_m('No such group.'));
            }
        } else if ($this->peopletag && $this->tagger) {
            $user = User::getKV('nickname', $this->tagger);
            if (empty($user)) {
                // TRANS: Client error.
                $this->clientError(_m('No such user.'));
            }

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

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


code:
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2009-2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

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

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

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

/**
 * Key UI methods:
 *
 *  showInputForm() - form asking for a remote profile account or URL
 *                    We end up back here on errors
 *
 *  showPreviewForm() - surrounding form for preview-and-confirm
 *    preview() - display profile for a remote group
 *
 *  success() - redirects to groups page on join
 */
class OStatusPeopletagAction extends OStatusSubAction
{
    protected $profile_uri; // provided acct: or URI of remote entity
    protected $oprofile; // Ostatus_profile of remote entity, if valid

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

    /**
     * Show the initial form, when we haven't yet been given a valid
     * remote profile.
     */
    function showInputForm()
    {
        $this->elementStart('form', array('method' => 'post',
                                          'id' => 'form_ostatus_sub',
                                          'class' => 'form_settings',
                                          'action' => $this->selfLink()));

        $this->hidden('token', common_session_token());

        $this->elementStart('fieldset', array('id' => 'settings_feeds'));

        $this->elementStart('ul', 'form_data');
        $this->elementStart('li');
        $this->input('profile',
                     // TRANS: Field label.
                     _m('Subscribe to list'),
                     $this->profile_uri,
                     // TRANS: Field title.
                     _m("Address of the OStatus list, like http://example.net/user/all/tag."));
        $this->elementEnd('li');
        $this->elementEnd('ul');

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

        $this->elementEnd('fieldset');

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

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

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

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

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

    /**
     * Redirect on successful remote people tag subscription
     */
    function success()
    {
        $cur = common_current_user();
        $url = common_local_url('peopletagsubscriptions', array('nickname' => $cur->nickname));
        common_redirect($url, 303);
    }

    /**
     * Attempt to finalize subscription.
     * validateFeed must have been run first.
     *
     * Calls showForm on failure or success on success.
     */
    function saveFeed()
    {
        $user = common_current_user();
        $ptag = $this->oprofile->localPeopletag();
        if ($ptag->hasSubscriber($user->id)) {
            // TRANS: OStatus remote group subscription dialog error.
            $this->showForm(_m('Already subscribed!'));
            return;
        }

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

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

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

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

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

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