]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/actions/pushhub.php
Merge branch 'testing' of git@gitorious.org:statusnet/mainline into testing
[quix0rs-gnu-social.git] / plugins / OStatus / actions / pushhub.php
1 <?php
2 /*
3  * StatusNet - the distributed open-source microblogging tool
4  * Copyright (C) 2010, StatusNet, Inc.
5  *
6  * This program is free software: you can redistribute it and/or modify
7  * it under the terms of the GNU Affero General Public License as published by
8  * the Free Software Foundation, either version 3 of the License, or
9  * (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU Affero General Public License for more details.
15  *
16  * You should have received a copy of the GNU Affero General Public License
17  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
18  */
19
20 /**
21  * Integrated PuSH hub; lets us only ping them what need it.
22  * @package Hub
23  * @maintainer Brion Vibber <brion@status.net>
24  */
25
26 /**
27
28
29 Things to consider...
30 * should we purge incomplete subscriptions that never get a verification pingback?
31 * when can we send subscription renewal checks?
32     - at next send time probably ok
33 * when can we handle trimming of subscriptions?
34     - at next send time probably ok
35 * should we keep a fail count?
36
37 */
38
39
40 class PushHubAction extends Action
41 {
42     function arg($arg, $def=null)
43     {
44         // PHP converts '.'s in incoming var names to '_'s.
45         // It also merges multiple values, which'll break hub.verify and hub.topic for publishing
46         // @fixme handle multiple args
47         $arg = str_replace('hub.', 'hub_', $arg);
48         return parent::arg($arg, $def);
49     }
50
51     function prepare($args)
52     {
53         StatusNet::setApi(true); // reduce exception reports to aid in debugging
54         return parent::prepare($args);
55     }
56
57     function handle()
58     {
59         $mode = $this->trimmed('hub.mode');
60         switch ($mode) {
61         case "subscribe":
62         case "unsubscribe":
63             $this->subunsub($mode);
64             break;
65         case "publish":
66             throw new ClientException("Publishing outside feeds not supported.", 400);
67         default:
68             throw new ClientException("Unrecognized mode '$mode'.", 400);
69         }
70     }
71
72     /**
73      * Process a request for a new or modified PuSH feed subscription.
74      * If asynchronous verification is requested, updates won't be saved immediately.
75      *
76      * HTTP return codes:
77      *   202 Accepted - request saved and awaiting verification
78      *   204 No Content - already subscribed
79      *   400 Bad Request - rejecting this (not specifically spec'd)
80      */
81     function subunsub($mode)
82     {
83         $callback = $this->argUrl('hub.callback');
84
85         $topic = $this->argUrl('hub.topic');
86         if (!$this->recognizedFeed($topic)) {
87             throw new ClientException("Unsupported hub.topic $topic; this hub only serves local user and group Atom feeds.");
88         }
89
90         $verify = $this->arg('hub.verify'); // @fixme may be multiple
91         if ($verify != 'sync' && $verify != 'async') {
92             throw new ClientException("Invalid hub.verify $verify; must be sync or async.");
93         }
94
95         $lease = $this->arg('hub.lease_seconds', null);
96         if ($mode == 'subscribe' && $lease != '' && !preg_match('/^\d+$/', $lease)) {
97             throw new ClientException("Invalid hub.lease $lease; must be empty or positive integer.");
98         }
99
100         $token = $this->arg('hub.verify_token', null);
101
102         $secret = $this->arg('hub.secret', null);
103         if ($secret != '' && strlen($secret) >= 200) {
104             throw new ClientException("Invalid hub.secret $secret; must be under 200 bytes.");
105         }
106
107         $sub = HubSub::staticGet($topic, $callback);
108         if (!$sub) {
109             // Creating a new one!
110             $sub = new HubSub();
111             $sub->topic = $topic;
112             $sub->callback = $callback;
113         }
114         if ($mode == 'subscribe') {
115             if ($secret) {
116                 $sub->secret = $secret;
117             }
118             if ($lease) {
119                 $sub->setLease(intval($lease));
120             }
121         }
122
123         if (!common_config('queue', 'enabled')) {
124             // Won't be able to background it.
125             $verify = 'sync';
126         }
127         if ($verify == 'async') {
128             $sub->scheduleVerify($mode, $token);
129             header('HTTP/1.1 202 Accepted');
130         } else {
131             $sub->verify($mode, $token);
132             header('HTTP/1.1 204 No Content');
133         }
134     }
135
136     /**
137      * Check whether the given URL represents one of our canonical
138      * user or group Atom feeds.
139      *
140      * @param string $feed URL
141      * @return boolean true if it matches
142      */
143     function recognizedFeed($feed)
144     {
145         $matches = array();
146         if (preg_match('!/(\d+)\.atom$!', $feed, $matches)) {
147             $id = $matches[1];
148             $params = array('id' => $id, 'format' => 'atom');
149             $userFeed = common_local_url('ApiTimelineUser', $params);
150             $groupFeed = common_local_url('ApiTimelineGroup', $params);
151
152             if ($feed == $userFeed) {
153                 $user = User::staticGet('id', $id);
154                 if (!$user) {
155                     throw new ClientException("Invalid hub.topic $feed; user doesn't exist.");
156                 } else {
157                     return true;
158                 }
159             }
160             if ($feed == $groupFeed) {
161                 $user = User_group::staticGet('id', $id);
162                 if (!$user) {
163                     throw new ClientException("Invalid hub.topic $feed; group doesn't exist.");
164                 } else {
165                     return true;
166                 }
167             }
168             common_log(LOG_DEBUG, "Not a user or group feed? $feed $userFeed $groupFeed");
169         }
170         common_log(LOG_DEBUG, "LOST $feed");
171         return false;
172     }
173
174     /**
175      * Grab and validate a URL from POST parameters.
176      * @throws ClientException for malformed or non-http/https URLs
177      */
178     protected function argUrl($arg)
179     {
180         $url = $this->arg($arg);
181         $params = array('domain_check' => false, // otherwise breaks my local tests :P
182                         'allowed_schemes' => array('http', 'https'));
183         if (Validate::uri($url, $params)) {
184             return $url;
185         } else {
186             throw new ClientException("Invalid URL passed for $arg: '$url'");
187         }
188     }
189
190     /**
191      * Get HubSub subscription record for a given feed & subscriber.
192      *
193      * @param string $feed
194      * @param string $callback
195      * @return mixed HubSub or false
196      */
197     protected function getSub($feed, $callback)
198     {
199         return HubSub::staticGet($feed, $callback);
200     }
201 }
202