]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/remotesubscribe.php
b9e29d645ddec9d82064601d620c4729e7b96c91
[quix0rs-gnu-social.git] / actions / remotesubscribe.php
1 <?php
2 /*
3  * Laconica - a distributed open-source microblogging tool
4  * Copyright (C) 2008, Controlez-Vous, 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 if (!defined('LACONICA')) { exit(1); }
21
22 require_once(INSTALLDIR.'/lib/omb.php');
23
24 class RemotesubscribeAction extends Action
25 {
26
27     function handle($args)
28     {
29
30         parent::handle($args);
31
32         if (common_logged_in()) {
33             common_user_error(_('You can use the local subscription!'));
34             return;
35         }
36
37         if ($_SERVER['REQUEST_METHOD'] == 'POST') {
38
39             # CSRF protection
40             $token = $this->trimmed('token');
41             if (!$token || $token != common_session_token()) {
42                 $this->show_form(_('There was a problem with your session token. Try again, please.'));
43                 return;
44             }
45
46             $this->remote_subscription();
47         } else {
48             $this->show_form();
49         }
50     }
51
52     function get_instructions()
53     {
54         return _('To subscribe, you can [login](%%action.login%%),' .
55                   ' or [register](%%action.register%%) a new ' .
56                   ' account. If you already have an account ' .
57                   ' on a [compatible microblogging site](%%doc.openmublog%%), ' .
58                   ' enter your profile URL below.');
59     }
60
61     function show_top($err=null)
62     {
63         if ($err) {
64             $this->element('div', 'error', $err);
65         } else {
66             $instructions = $this->get_instructions();
67             $output = common_markup_to_html($instructions);
68             $this->elementStart('div', 'instructions');
69             $this->raw($output);
70             $this->elementEnd('p');
71         }
72     }
73
74     function show_form($err=null)
75     {
76         $nickname = $this->trimmed('nickname');
77         $profile = $this->trimmed('profile_url');
78         common_show_header(_('Remote subscribe'), null, $err,
79                            array($this, 'show_top'));
80         # id = remotesubscribe conflicts with the
81         # button on profile page
82         $this->elementStart('form', array('id' => 'remsub', 'method' => 'post',
83                                            'action' => common_local_url('remotesubscribe')));
84         $this->hidden('token', common_session_token());
85         $this->input('nickname', _('User nickname'), $nickname,
86                      _('Nickname of the user you want to follow'));
87         $this->input('profile_url', _('Profile URL'), $profile,
88                      _('URL of your profile on another compatible microblogging service'));
89         $this->submit('submit', _('Subscribe'));
90         $this->elementEnd('form');
91         common_show_footer();
92     }
93
94     function remote_subscription()
95     {
96         $user = $this->get_user();
97
98         if (!$user) {
99             $this->show_form(_('No such user.'));
100             return;
101         }
102
103         $profile = $this->trimmed('profile_url');
104
105         if (!$profile) {
106             $this->show_form(_('No such user.'));
107             return;
108         }
109
110         if (!Validate::uri($profile, array('allowed_schemes' => array('http', 'https')))) {
111             $this->show_form(_('Invalid profile URL (bad format)'));
112             return;
113         }
114
115         $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
116         $yadis = Auth_Yadis_Yadis::discover($profile, $fetcher);
117
118         if (!$yadis || $yadis->failed) {
119             $this->show_form(_('Not a valid profile URL (no YADIS document).'));
120             return;
121         }
122
123         # XXX: a little liberal for sites that accidentally put whitespace before the xml declaration
124
125         $xrds =& Auth_Yadis_XRDS::parseXRDS(trim($yadis->response_text));
126
127         if (!$xrds) {
128             $this->show_form(_('Not a valid profile URL (no XRDS defined).'));
129             return;
130         }
131
132         $omb = $this->getOmb($xrds);
133
134         if (!$omb) {
135             $this->show_form(_('Not a valid profile URL (incorrect services).'));
136             return;
137         }
138
139         if (omb_service_uri($omb[OAUTH_ENDPOINT_REQUEST]) ==
140             common_local_url('requesttoken'))
141         {
142             $this->show_form(_('That\'s a local profile! Login to subscribe.'));
143             return;
144         }
145
146         if (User::staticGet('uri', omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]))) {
147             $this->show_form(_('That\'s a local profile! Login to subscribe.'));
148             return;
149         }
150
151         list($token, $secret) = $this->request_token($omb);
152
153         if (!$token || !$secret) {
154             $this->show_form(_('Couldn\'t get a request token.'));
155             return;
156         }
157
158         $this->request_authorization($user, $omb, $token, $secret);
159     }
160
161     function get_user()
162     {
163         $user = null;
164         $nickname = $this->trimmed('nickname');
165         if ($nickname) {
166             $user = User::staticGet('nickname', $nickname);
167         }
168         return $user;
169     }
170
171     function getOmb($xrds)
172     {
173
174         static $omb_endpoints = array(OMB_ENDPOINT_UPDATEPROFILE, OMB_ENDPOINT_POSTNOTICE);
175         static $oauth_endpoints = array(OAUTH_ENDPOINT_REQUEST, OAUTH_ENDPOINT_AUTHORIZE,
176                                         OAUTH_ENDPOINT_ACCESS);
177         $omb = array();
178
179         # XXX: the following code could probably be refactored to eliminate dupes
180
181         $oauth_services = omb_get_services($xrds, OAUTH_DISCOVERY);
182
183         if (!$oauth_services) {
184             return null;
185         }
186
187         $oauth_service = $oauth_services[0];
188
189         $oauth_xrd = $this->getXRD($oauth_service, $xrds);
190
191         if (!$oauth_xrd) {
192             return null;
193         }
194
195         if (!$this->addServices($oauth_xrd, $oauth_endpoints, $omb)) {
196             return null;
197         }
198
199         $omb_services = omb_get_services($xrds, OMB_NAMESPACE);
200
201         if (!$omb_services) {
202             return null;
203         }
204
205         $omb_service = $omb_services[0];
206
207         $omb_xrd = $this->getXRD($omb_service, $xrds);
208
209         if (!$omb_xrd) {
210             return null;
211         }
212
213         if (!$this->addServices($omb_xrd, $omb_endpoints, $omb)) {
214             return null;
215         }
216
217         # XXX: check that we got all the services we needed
218
219         foreach (array_merge($omb_endpoints, $oauth_endpoints) as $type) {
220             if (!array_key_exists($type, $omb) || !$omb[$type]) {
221                 return null;
222             }
223         }
224
225         if (!omb_local_id($omb[OAUTH_ENDPOINT_REQUEST])) {
226             return null;
227         }
228
229         return $omb;
230     }
231
232     function getXRD($main_service, $main_xrds)
233     {
234         $uri = omb_service_uri($main_service);
235         if (strpos($uri, "#") !== 0) {
236             # FIXME: more rigorous handling of external service definitions
237             return null;
238         }
239         $id = substr($uri, 1);
240         $nodes = $main_xrds->allXrdNodes;
241         $parser = $main_xrds->parser;
242         foreach ($nodes as $node) {
243             $attrs = $parser->attributes($node);
244             if (array_key_exists('xml:id', $attrs) &&
245                 $attrs['xml:id'] == $id) {
246                 # XXX: trick the constructor into thinking this is the only node
247                 $bogus_nodes = array($node);
248                 return new Auth_Yadis_XRDS($parser, $bogus_nodes);
249             }
250         }
251         return null;
252     }
253
254     function addServices($xrd, $types, &$omb)
255     {
256         foreach ($types as $type) {
257             $matches = omb_get_services($xrd, $type);
258             if ($matches) {
259                 $omb[$type] = $matches[0];
260             } else {
261                 # no match for type
262                 return false;
263             }
264         }
265         return true;
266     }
267
268     function request_token($omb)
269     {
270         $con = omb_oauth_consumer();
271
272         $url = omb_service_uri($omb[OAUTH_ENDPOINT_REQUEST]);
273
274         # XXX: Is this the right thing to do? Strip off GET params and make them
275         # POST params? Seems wrong to me.
276
277         $parsed = parse_url($url);
278         $params = array();
279         parse_str($parsed['query'], $params);
280
281         $req = OAuthRequest::from_consumer_and_token($con, null, "POST", $url, $params);
282
283         $listener = omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]);
284
285         if (!$listener) {
286             return null;
287         }
288
289         $req->set_parameter('omb_listener', $listener);
290         $req->set_parameter('omb_version', OMB_VERSION_01);
291
292         # XXX: test to see if endpoint accepts this signature method
293
294         $req->sign_request(omb_hmac_sha1(), $con, null);
295
296         # We re-use this tool's fetcher, since it's pretty good
297
298         $fetcher = Auth_Yadis_Yadis::getHTTPFetcher();
299
300         $result = $fetcher->post($req->get_normalized_http_url(),
301                                  $req->to_postdata(),
302                                  array('User-Agent' => 'Laconica/' . LACONICA_VERSION));
303
304         if ($result->status != 200) {
305             return null;
306         }
307
308         parse_str($result->body, $return);
309
310         return array($return['oauth_token'], $return['oauth_token_secret']);
311     }
312
313     function request_authorization($user, $omb, $token, $secret)
314     {
315         global $config; # for license URL
316
317         $con = omb_oauth_consumer();
318         $tok = new OAuthToken($token, $secret);
319
320         $url = omb_service_uri($omb[OAUTH_ENDPOINT_AUTHORIZE]);
321
322         # XXX: Is this the right thing to do? Strip off GET params and make them
323         # POST params? Seems wrong to me.
324
325         $parsed = parse_url($url);
326         $params = array();
327         parse_str($parsed['query'], $params);
328
329         $req = OAuthRequest::from_consumer_and_token($con, $tok, 'GET', $url, $params);
330
331         # We send over a ton of information. This lets the other
332         # server store info about our user, and it lets the current
333         # user decide if they really want to authorize the subscription.
334
335         $req->set_parameter('omb_version', OMB_VERSION_01);
336         $req->set_parameter('omb_listener', omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]));
337         $req->set_parameter('omb_listenee', $user->uri);
338         $req->set_parameter('omb_listenee_profile', common_profile_url($user->nickname));
339         $req->set_parameter('omb_listenee_nickname', $user->nickname);
340         $req->set_parameter('omb_listenee_license', $config['license']['url']);
341
342         $profile = $user->getProfile();
343         if (!$profile) {
344             common_log_db_error($user, 'SELECT', __FILE__);
345             $this->server_error(_('User without matching profile'));
346             return;
347         }
348
349         if ($profile->fullname) {
350             $req->set_parameter('omb_listenee_fullname', $profile->fullname);
351         }
352         if ($profile->homepage) {
353             $req->set_parameter('omb_listenee_homepage', $profile->homepage);
354         }
355         if ($profile->bio) {
356             $req->set_parameter('omb_listenee_bio', $profile->bio);
357         }
358         if ($profile->location) {
359             $req->set_parameter('omb_listenee_location', $profile->location);
360         }
361         $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
362         if ($avatar) {
363             $req->set_parameter('omb_listenee_avatar', $avatar->url);
364         }
365
366         # XXX: add a nonce to prevent replay attacks
367
368         $req->set_parameter('oauth_callback', common_local_url('finishremotesubscribe'));
369
370         # XXX: test to see if endpoint accepts this signature method
371
372         $req->sign_request(omb_hmac_sha1(), $con, $tok);
373
374         # store all our info here
375
376         $omb['listenee'] = $user->nickname;
377         $omb['listener'] = omb_local_id($omb[OAUTH_ENDPOINT_REQUEST]);
378         $omb['token'] = $token;
379         $omb['secret'] = $secret;
380         # call doesn't work after bounce back so we cache; maybe serialization issue...?
381         $omb['access_token_url'] = omb_service_uri($omb[OAUTH_ENDPOINT_ACCESS]);
382         $omb['post_notice_url'] = omb_service_uri($omb[OMB_ENDPOINT_POSTNOTICE]);
383         $omb['update_profile_url'] = omb_service_uri($omb[OMB_ENDPOINT_UPDATEPROFILE]);
384
385         common_ensure_session();
386
387         $_SESSION['oauth_authorization_request'] = $omb;
388
389         # Redirect to authorization service
390
391         common_redirect($req->to_url());
392         return;
393     }
394
395     function make_nonce()
396     {
397         return common_good_rand(16);
398     }
399 }