We are no longer offering accounts on this server. Consider https://gitlab.freedesktop.org/ as a place to host projects.

pushhub.php 8.17 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
<?php
/*
 * StatusNet - the distributed open-source microblogging tool
 * Copyright (C) 2010, StatusNet, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

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

26 27 28 29
if (!defined('STATUSNET')) {
    exit(1);
}

30
/**
31 32 33 34 35 36 37 38
 * 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?
 */
Brion Vibber's avatar
Brion Vibber committed
39
class PushHubAction extends Action
40 41 42 43 44 45
{
    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
46
        $arg = str_replace('hub.', 'hub_', $arg);
47 48 49
        return parent::arg($arg, $def);
    }

50
    protected function prepare($args)
51 52 53 54 55
    {
        StatusNet::setApi(true); // reduce exception reports to aid in debugging
        return parent::prepare($args);
    }

56
    protected function handle()
57 58 59 60 61
    {
        $mode = $this->trimmed('hub.mode');
        switch ($mode) {
        case "subscribe":
        case "unsubscribe":
Brion Vibber's avatar
Brion Vibber committed
62
            $this->subunsub($mode);
63 64
            break;
        case "publish":
Siebrand Mazeland's avatar
Siebrand Mazeland committed
65 66
            // TRANS: Client exception.
            throw new ClientException(_m('Publishing outside feeds not supported.'), 400);
67
        default:
Siebrand Mazeland's avatar
Siebrand Mazeland committed
68 69
            // TRANS: Client exception. %s is a mode.
            throw new ClientException(sprintf(_m('Unrecognized mode "%s".'),$mode), 400);
70 71 72 73
        }
    }

    /**
Brion Vibber's avatar
Brion Vibber committed
74 75
     * Process a request for a new or modified PuSH feed subscription.
     * If asynchronous verification is requested, updates won't be saved immediately.
76 77 78 79
     *
     * HTTP return codes:
     *   202 Accepted - request saved and awaiting verification
     *   204 No Content - already subscribed
Brion Vibber's avatar
Brion Vibber committed
80
     *   400 Bad Request - rejecting this (not specifically spec'd)
81
     */
Brion Vibber's avatar
Brion Vibber committed
82
    function subunsub($mode)
83 84 85
    {
        $callback = $this->argUrl('hub.callback');

Brion Vibber's avatar
Brion Vibber committed
86 87
        $topic = $this->argUrl('hub.topic');
        if (!$this->recognizedFeed($topic)) {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
88 89
            // 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));
90 91
        }

Brion Vibber's avatar
Brion Vibber committed
92 93
        $verify = $this->arg('hub.verify'); // @fixme may be multiple
        if ($verify != 'sync' && $verify != 'async') {
94
            // TRANS: Client exception. %s is sync or async.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
95
            throw new ClientException(sprintf(_m('Invalid hub.verify "%s". It must be sync or async.'),$verify));
96
        }
97

Brion Vibber's avatar
Brion Vibber committed
98 99
        $lease = $this->arg('hub.lease_seconds', null);
        if ($mode == 'subscribe' && $lease != '' && !preg_match('/^\d+$/', $lease)) {
100
            // TRANS: Client exception. %s is the invalid lease value.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
101
            throw new ClientException(sprintf(_m('Invalid hub.lease "%s". It must be empty or positive integer.'),$lease));
Brion Vibber's avatar
Brion Vibber committed
102 103 104
        }

        $token = $this->arg('hub.verify_token', null);
105

Brion Vibber's avatar
Brion Vibber committed
106 107
        $secret = $this->arg('hub.secret', null);
        if ($secret != '' && strlen($secret) >= 200) {
108
            // TRANS: Client exception. %s is the invalid hub secret.
Siebrand Mazeland's avatar
Siebrand Mazeland committed
109
            throw new ClientException(sprintf(_m('Invalid hub.secret "%s". It must be under 200 bytes.'),$secret));
110 111
        }

112
        $sub = HubSub::getByHashkey($topic, $callback);
Brion Vibber's avatar
Brion Vibber committed
113 114 115 116 117 118 119 120 121 122 123 124 125 126
        if (!$sub) {
            // Creating a new one!
            $sub = new HubSub();
            $sub->topic = $topic;
            $sub->callback = $callback;
        }
        if ($mode == 'subscribe') {
            if ($secret) {
                $sub->secret = $secret;
            }
            if ($lease) {
                $sub->setLease(intval($lease));
            }
        }
127

Brion Vibber's avatar
Brion Vibber committed
128 129 130 131 132 133 134 135 136 137 138
        if (!common_config('queue', 'enabled')) {
            // Won't be able to background it.
            $verify = 'sync';
        }
        if ($verify == 'async') {
            $sub->scheduleVerify($mode, $token);
            header('HTTP/1.1 202 Accepted');
        } else {
            $sub->verify($mode, $token);
            header('HTTP/1.1 204 No Content');
        }
139 140 141
    }

    /**
Brion Vibber's avatar
Brion Vibber committed
142 143
     * Check whether the given URL represents one of our canonical
     * user or group Atom feeds.
Brion Vibber's avatar
Brion Vibber committed
144
     *
Brion Vibber's avatar
Brion Vibber committed
145 146
     * @param string $feed URL
     * @return boolean true if it matches
147
     */
Brion Vibber's avatar
Brion Vibber committed
148
    function recognizedFeed($feed)
149
    {
Brion Vibber's avatar
Brion Vibber committed
150 151 152 153 154 155 156 157
        $matches = array();
        if (preg_match('!/(\d+)\.atom$!', $feed, $matches)) {
            $id = $matches[1];
            $params = array('id' => $id, 'format' => 'atom');
            $userFeed = common_local_url('ApiTimelineUser', $params);
            $groupFeed = common_local_url('ApiTimelineGroup', $params);

            if ($feed == $userFeed) {
158
                $user = User::getKV('id', $id);
Brion Vibber's avatar
Brion Vibber committed
159
                if (!$user) {
160 161
                    // TRANS: Client exception. %s is a feed URL.
                    throw new ClientException(sprintt(_m('Invalid hub.topic "%s". User does not exist.'),$feed));
Brion Vibber's avatar
Brion Vibber committed
162 163 164
                } else {
                    return true;
                }
165
            }
Brion Vibber's avatar
Brion Vibber committed
166
            if ($feed == $groupFeed) {
167
                $user = User_group::getKV('id', $id);
Brion Vibber's avatar
Brion Vibber committed
168
                if (!$user) {
169 170
                    // TRANS: Client exception. %s is a feed URL.
                    throw new ClientException(sprintf(_m('Invalid hub.topic "%s". Group does not exist.'),$feed));
Brion Vibber's avatar
Brion Vibber committed
171 172 173 174
                } else {
                    return true;
                }
            }
Shashi Gowda's avatar
Shashi Gowda committed
175 176 177 178 179 180 181
        } else if (preg_match('!/(\d+)/lists/(\d+)/statuses\.atom$!', $feed, $matches)) {
            $user = $matches[1];
            $id = $matches[2];
            $params = array('user' => $user, 'id' => $id, 'format' => 'atom');
            $listFeed = common_local_url('ApiTimelineList', $params);

            if ($feed == $listFeed) {
182 183
                $list = Profile_list::getKV('id', $id);
                $user = User::getKV('id', $user);
Shashi Gowda's avatar
Shashi Gowda committed
184
                if (!$list || !$user || $list->tagger != $user->id) {
185
                    // TRANS: Client exception. %s is a feed URL.
186
                    throw new ClientException(sprintf(_m('Invalid hub.topic %s; list does not exist.'),$feed));
Shashi Gowda's avatar
Shashi Gowda committed
187 188 189 190 191
                } else {
                    return true;
                }
            }
            common_log(LOG_DEBUG, "Not a user, group or people tag feed? $feed $userFeed $groupFeed $listFeed");
192
        }
Brion Vibber's avatar
Brion Vibber committed
193 194
        common_log(LOG_DEBUG, "LOST $feed");
        return false;
195 196 197 198
    }

    /**
     * Grab and validate a URL from POST parameters.
Brion Vibber's avatar
Brion Vibber committed
199
     * @throws ClientException for malformed or non-http/https URLs
200 201 202 203 204 205
     */
    protected function argUrl($arg)
    {
        $url = $this->arg($arg);
        $params = array('domain_check' => false, // otherwise breaks my local tests :P
                        'allowed_schemes' => array('http', 'https'));
206 207
        $validate = new Validate;
        if ($validate->uri($url, $params)) {
208 209
            return $url;
        } else {
Siebrand Mazeland's avatar
Siebrand Mazeland committed
210 211 212
            // 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));
213 214 215 216 217 218 219 220 221 222 223 224
        }
    }

    /**
     * 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)
    {
225
        return HubSub::getByHashkey($feed, $callback);
226 227
    }
}