]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/OStatus/actions/pushhub.php
OStatus support for people tags
[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 if (!defined('STATUSNET')) {
27     exit(1);
28 }
29
30 /**
31
32
33 Things to consider...
34 * should we purge incomplete subscriptions that never get a verification pingback?
35 * when can we send subscription renewal checks?
36     - at next send time probably ok
37 * when can we handle trimming of subscriptions?
38     - at next send time probably ok
39 * should we keep a fail count?
40
41 */
42
43 class PushHubAction extends Action
44 {
45     function arg($arg, $def=null)
46     {
47         // PHP converts '.'s in incoming var names to '_'s.
48         // It also merges multiple values, which'll break hub.verify and hub.topic for publishing
49         // @fixme handle multiple args
50         $arg = str_replace('hub.', 'hub_', $arg);
51         return parent::arg($arg, $def);
52     }
53
54     function prepare($args)
55     {
56         StatusNet::setApi(true); // reduce exception reports to aid in debugging
57         return parent::prepare($args);
58     }
59
60     function handle()
61     {
62         $mode = $this->trimmed('hub.mode');
63         switch ($mode) {
64         case "subscribe":
65         case "unsubscribe":
66             $this->subunsub($mode);
67             break;
68         case "publish":
69             // TRANS: Client exception.
70             throw new ClientException(_m('Publishing outside feeds not supported.'), 400);
71         default:
72             // TRANS: Client exception. %s is a mode.
73             throw new ClientException(sprintf(_m('Unrecognized mode "%s".'),$mode), 400);
74         }
75     }
76
77     /**
78      * Process a request for a new or modified PuSH feed subscription.
79      * If asynchronous verification is requested, updates won't be saved immediately.
80      *
81      * HTTP return codes:
82      *   202 Accepted - request saved and awaiting verification
83      *   204 No Content - already subscribed
84      *   400 Bad Request - rejecting this (not specifically spec'd)
85      */
86     function subunsub($mode)
87     {
88         $callback = $this->argUrl('hub.callback');
89
90         $topic = $this->argUrl('hub.topic');
91         if (!$this->recognizedFeed($topic)) {
92             // TRANS: Client exception. %s is a topic.
93             throw new ClientException(sprintf(_m('Unsupported hub.topic %s this hub only serves local user and group Atom feeds.'),$topic));
94         }
95
96         $verify = $this->arg('hub.verify'); // @fixme may be multiple
97         if ($verify != 'sync' && $verify != 'async') {
98             // TRANS: Client exception.
99             throw new ClientException(sprintf(_m('Invalid hub.verify "%s". It must be sync or async.'),$verify));
100         }
101
102         $lease = $this->arg('hub.lease_seconds', null);
103         if ($mode == 'subscribe' && $lease != '' && !preg_match('/^\d+$/', $lease)) {
104             // TRANS: Client exception.
105             throw new ClientException(sprintf(_m('Invalid hub.lease "%s". It must be empty or positive integer.'),$lease));
106         }
107
108         $token = $this->arg('hub.verify_token', null);
109
110         $secret = $this->arg('hub.secret', null);
111         if ($secret != '' && strlen($secret) >= 200) {
112             // TRANS: Client exception.
113             throw new ClientException(sprintf(_m('Invalid hub.secret "%s". It must be under 200 bytes.'),$secret));
114         }
115
116         $sub = HubSub::staticGet($topic, $callback);
117         if (!$sub) {
118             // Creating a new one!
119             $sub = new HubSub();
120             $sub->topic = $topic;
121             $sub->callback = $callback;
122         }
123         if ($mode == 'subscribe') {
124             if ($secret) {
125                 $sub->secret = $secret;
126             }
127             if ($lease) {
128                 $sub->setLease(intval($lease));
129             }
130         }
131
132         if (!common_config('queue', 'enabled')) {
133             // Won't be able to background it.
134             $verify = 'sync';
135         }
136         if ($verify == 'async') {
137             $sub->scheduleVerify($mode, $token);
138             header('HTTP/1.1 202 Accepted');
139         } else {
140             $sub->verify($mode, $token);
141             header('HTTP/1.1 204 No Content');
142         }
143     }
144
145     /**
146      * Check whether the given URL represents one of our canonical
147      * user or group Atom feeds.
148      *
149      * @param string $feed URL
150      * @return boolean true if it matches
151      */
152     function recognizedFeed($feed)
153     {
154         $matches = array();
155         if (preg_match('!/(\d+)\.atom$!', $feed, $matches)) {
156             $id = $matches[1];
157             $params = array('id' => $id, 'format' => 'atom');
158             $userFeed = common_local_url('ApiTimelineUser', $params);
159             $groupFeed = common_local_url('ApiTimelineGroup', $params);
160
161             if ($feed == $userFeed) {
162                 $user = User::staticGet('id', $id);
163                 if (!$user) {
164                     // TRANS: Client exception.
165                     throw new ClientException(sprintt(_m('Invalid hub.topic "%s". User doesn\'t exist.'),$feed));
166                 } else {
167                     return true;
168                 }
169             }
170             if ($feed == $groupFeed) {
171                 $user = User_group::staticGet('id', $id);
172                 if (!$user) {
173                     // TRANS: Client exception.
174                     throw new ClientException(sprintf(_m('Invalid hub.topic "%s". Group doesn\'t exist.'),$feed));
175                 } else {
176                     return true;
177                 }
178             }
179         } else if (preg_match('!/(\d+)/lists/(\d+)/statuses\.atom$!', $feed, $matches)) {
180             $user = $matches[1];
181             $id = $matches[2];
182             $params = array('user' => $user, 'id' => $id, 'format' => 'atom');
183             $listFeed = common_local_url('ApiTimelineList', $params);
184
185             if ($feed == $listFeed) {
186                 $list = Profile_list::staticGet('id', $id);
187                 $user = User::staticGet('id', $user);
188                 if (!$list || !$user || $list->tagger != $user->id) {
189                     throw new ClientException("Invalid hub.topic $feed; people tag doesn't exist.");
190                 } else {
191                     return true;
192                 }
193             }
194             common_log(LOG_DEBUG, "Not a user, group or people tag feed? $feed $userFeed $groupFeed $listFeed");
195         }
196         common_log(LOG_DEBUG, "LOST $feed");
197         return false;
198     }
199
200     /**
201      * Grab and validate a URL from POST parameters.
202      * @throws ClientException for malformed or non-http/https URLs
203      */
204     protected function argUrl($arg)
205     {
206         $url = $this->arg($arg);
207         $params = array('domain_check' => false, // otherwise breaks my local tests :P
208                         'allowed_schemes' => array('http', 'https'));
209         if (Validate::uri($url, $params)) {
210             return $url;
211         } else {
212             // TRANS: Client exception.
213             // TRANS: %1$s is this argument to the method this exception occurs in, %2$s is a URL.
214             throw new ClientException(sprintf(_m('Invalid URL passed for %1$s: "%2$s"'),$arg,$url));
215         }
216     }
217
218     /**
219      * Get HubSub subscription record for a given feed & subscriber.
220      *
221      * @param string $feed
222      * @param string $callback
223      * @return mixed HubSub or false
224      */
225     protected function getSub($feed, $callback)
226     {
227         return HubSub::staticGet($feed, $callback);
228     }
229 }