]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/userauthorization.php
Merge branch 'master' into devel
[quix0rs-gnu-social.git] / actions / userauthorization.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 define('TIMESTAMP_THRESHOLD', 300);
24
25 class UserauthorizationAction extends Action
26 {
27
28     function handle($args)
29     {
30         parent::handle($args);
31
32         if ($_SERVER['REQUEST_METHOD'] == 'POST') {
33             # CSRF protection
34             $token = $this->trimmed('token');
35             if (!$token || $token != common_session_token()) {
36                 $req = $this->get_stored_request();
37                 $this->show_form(_('There was a problem with your session token. Try again, please.'), $req);
38                 return;
39             }
40             # We've shown the form, now post user's choice
41             $this->send_authorization();
42         } else {
43             if (!common_logged_in()) {
44                 # Go log in, and then come back
45                 common_debug('saving URL for returnto', __FILE__);
46                 common_set_returnto($_SERVER['REQUEST_URI']);
47
48                 common_debug('redirecting to login', __FILE__);
49                 common_redirect(common_local_url('login'));
50                 return;
51             }
52             try {
53                 # this must be a new request
54                 common_debug('getting new request', __FILE__);
55                 $req = $this->get_new_request();
56                 if (!$req) {
57                     $this->client_error(_('No request found!'));
58                 }
59                 common_debug('validating request', __FILE__);
60                 # XXX: only validate new requests, since nonce is one-time use
61                 $this->validate_request($req);
62                 common_debug('showing form', __FILE__);
63                 $this->store_request($req);
64                 $this->show_form($req);
65             } catch (OAuthException $e) {
66                 $this->clear_request();
67                 $this->client_error($e->getMessage());
68                 return;
69             }
70
71         }
72     }
73
74     function show_form($req)
75     {
76
77         $nickname = $req->get_parameter('omb_listenee_nickname');
78         $profile = $req->get_parameter('omb_listenee_profile');
79         $license = $req->get_parameter('omb_listenee_license');
80         $fullname = $req->get_parameter('omb_listenee_fullname');
81         $homepage = $req->get_parameter('omb_listenee_homepage');
82         $bio = $req->get_parameter('omb_listenee_bio');
83         $location = $req->get_parameter('omb_listenee_location');
84         $avatar = $req->get_parameter('omb_listenee_avatar');
85
86         common_show_header(_('Authorize subscription'));
87         common_element('p', null, _('Please check these details to make sure '.
88                                      'that you want to subscribe to this user\'s notices. '.
89                                      'If you didn\'t just ask to subscribe to someone\'s notices, '.
90                                      'click "Cancel".'));
91         common_element_start('div', 'profile');
92         if ($avatar) {
93             common_element('img', array('src' => $avatar,
94                                         'class' => 'avatar profile',
95                                         'width' => AVATAR_PROFILE_SIZE,
96                                         'height' => AVATAR_PROFILE_SIZE,
97                                         'alt' => $nickname));
98         }
99         common_element('a', array('href' => $profile,
100                                   'class' => 'external profile nickname'),
101                        $nickname);
102         if ($fullname) {
103             common_element_start('div', 'fullname');
104             if ($homepage) {
105                 common_element('a', array('href' => $homepage),
106                                $fullname);
107             } else {
108                 common_text($fullname);
109             }
110             common_element_end('div');
111         }
112         if ($location) {
113             common_element('div', 'location', $location);
114         }
115         if ($bio) {
116             common_element('div', 'bio', $bio);
117         }
118         common_element_start('div', 'license');
119         common_element('a', array('href' => $license,
120                                   'class' => 'license'),
121                        $license);
122         common_element_end('div');
123         common_element_end('div');
124         common_element_start('form', array('method' => 'post',
125                                            'id' => 'userauthorization',
126                                            'name' => 'userauthorization',
127                                            'action' => common_local_url('userauthorization')));
128         common_hidden('token', common_session_token());
129         common_submit('accept', _('Accept'));
130         common_submit('reject', _('Reject'));
131         common_element_end('form');
132         common_show_footer();
133     }
134
135     function send_authorization()
136     {
137         $req = $this->get_stored_request();
138
139         if (!$req) {
140             common_user_error(_('No authorization request!'));
141             return;
142         }
143
144         $callback = $req->get_parameter('oauth_callback');
145
146         if ($this->arg('accept')) {
147             if (!$this->authorize_token($req)) {
148                 $this->client_error(_('Error authorizing token'));
149             }
150             if (!$this->save_remote_profile($req)) {
151                 $this->client_error(_('Error saving remote profile'));
152             }
153             if (!$callback) {
154                 $this->show_accept_message($req->get_parameter('oauth_token'));
155             } else {
156                 $params = array();
157                 $params['oauth_token'] = $req->get_parameter('oauth_token');
158                 $params['omb_version'] = OMB_VERSION_01;
159                 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
160                 $profile = $user->getProfile();
161                 if (!$profile) {
162                     common_log_db_error($user, 'SELECT', __FILE__);
163                     $this->server_error(_('User without matching profile'));
164                     return;
165                 }
166                 $params['omb_listener_nickname'] = $user->nickname;
167                 $params['omb_listener_profile'] = common_local_url('showstream',
168                                                                    array('nickname' => $user->nickname));
169                 if ($profile->fullname) {
170                     $params['omb_listener_fullname'] = $profile->fullname;
171                 }
172                 if ($profile->homepage) {
173                     $params['omb_listener_homepage'] = $profile->homepage;
174                 }
175                 if ($profile->bio) {
176                     $params['omb_listener_bio'] = $profile->bio;
177                 }
178                 if ($profile->location) {
179                     $params['omb_listener_location'] = $profile->location;
180                 }
181                 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
182                 if ($avatar) {
183                     $params['omb_listener_avatar'] = $avatar->url;
184                 }
185                 $parts = array();
186                 foreach ($params as $k => $v) {
187                     $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
188                 }
189                 $query_string = implode('&', $parts);
190                 $parsed = parse_url($callback);
191                 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
192                 common_redirect($url, 303);
193             }
194         } else {
195             if (!$callback) {
196                 $this->show_reject_message();
197             } else {
198                 # XXX: not 100% sure how to signal failure... just redirect without token?
199                 common_redirect($callback, 303);
200             }
201         }
202     }
203
204     function authorize_token(&$req)
205     {
206         $consumer_key = $req->get_parameter('oauth_consumer_key');
207         $token_field = $req->get_parameter('oauth_token');
208         common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
209         common_debug('token field = "'.$token_field.'"', __FILE__);
210         $rt = new Token();
211         $rt->consumer_key = $consumer_key;
212         $rt->tok = $token_field;
213         $rt->type = 0;
214         $rt->state = 0;
215         common_debug('request token to look up: "'.print_r($rt,true).'"');
216         if ($rt->find(true)) {
217             common_debug('found request token to authorize', __FILE__);
218             $orig_rt = clone($rt);
219             $rt->state = 1; # Authorized but not used
220             if ($rt->update($orig_rt)) {
221                 common_debug('updated request token so it is authorized', __FILE__);
222                 return true;
223             }
224         }
225         return false;
226     }
227
228     # XXX: refactor with similar code in finishremotesubscribe.php
229
230     function save_remote_profile(&$req)
231     {
232         # FIXME: we should really do this when the consumer comes
233         # back for an access token. If they never do, we've got stuff in a
234         # weird state.
235
236         $nickname = $req->get_parameter('omb_listenee_nickname');
237         $fullname = $req->get_parameter('omb_listenee_fullname');
238         $profile_url = $req->get_parameter('omb_listenee_profile');
239         $homepage = $req->get_parameter('omb_listenee_homepage');
240         $bio = $req->get_parameter('omb_listenee_bio');
241         $location = $req->get_parameter('omb_listenee_location');
242         $avatar_url = $req->get_parameter('omb_listenee_avatar');
243
244         $listenee = $req->get_parameter('omb_listenee');
245         $remote = Remote_profile::staticGet('uri', $listenee);
246
247         if ($remote) {
248             $exists = true;
249             $profile = Profile::staticGet($remote->id);
250             $orig_remote = clone($remote);
251             $orig_profile = clone($profile);
252         } else {
253             $exists = false;
254             $remote = new Remote_profile();
255             $remote->uri = $listenee;
256             $profile = new Profile();
257         }
258
259         $profile->nickname = $nickname;
260         $profile->profileurl = $profile_url;
261
262         if ($fullname) {
263             $profile->fullname = $fullname;
264         }
265         if ($homepage) {
266             $profile->homepage = $homepage;
267         }
268         if ($bio) {
269             $profile->bio = $bio;
270         }
271         if ($location) {
272             $profile->location = $location;
273         }
274
275         if ($exists) {
276             $profile->update($orig_profile);
277         } else {
278             $profile->created = DB_DataObject_Cast::dateTime(); # current time
279             $id = $profile->insert();
280             if (!$id) {
281                 return false;
282             }
283             $remote->id = $id;
284         }
285
286         if ($exists) {
287             if (!$remote->update($orig_remote)) {
288                 return false;
289             }
290         } else {
291             $remote->created = DB_DataObject_Cast::dateTime(); # current time
292             if (!$remote->insert()) {
293                 return false;
294             }
295         }
296
297         if ($avatar_url) {
298             if (!$this->add_avatar($profile, $avatar_url)) {
299                 return false;
300             }
301         }
302
303         $user = common_current_user();
304         $datastore = omb_oauth_datastore();
305         $consumer = $this->get_consumer($datastore, $req);
306         $token = $this->get_token($datastore, $req, $consumer);
307
308         $sub = new Subscription();
309         $sub->subscriber = $user->id;
310         $sub->subscribed = $remote->id;
311         $sub->token = $token->key; # NOTE: request token, not valid for use!
312         $sub->created = DB_DataObject_Cast::dateTime(); # current time
313
314         if (!$sub->insert()) {
315             return false;
316         }
317
318         return true;
319     }
320
321     function add_avatar($profile, $url)
322     {
323         $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
324         copy($url, $temp_filename);
325         return $profile->setOriginal($temp_filename);
326     }
327
328     function show_accept_message($tok)
329     {
330         common_show_header(_('Subscription authorized'));
331         common_element('p', null,
332                        _('The subscription has been authorized, but no '.
333                           'callback URL was passed. Check with the site\'s instructions for '.
334                           'details on how to authorize the subscription. Your subscription token is:'));
335         common_element('blockquote', 'token', $tok);
336         common_show_footer();
337     }
338
339     function show_reject_message($tok)
340     {
341         common_show_header(_('Subscription rejected'));
342         common_element('p', null,
343                        _('The subscription has been rejected, but no '.
344                           'callback URL was passed. Check with the site\'s instructions for '.
345                           'details on how to fully reject the subscription.'));
346         common_show_footer();
347     }
348
349     function store_request($req)
350     {
351         common_ensure_session();
352         $_SESSION['userauthorizationrequest'] = $req;
353     }
354
355     function clear_request()
356     {
357         common_ensure_session();
358         unset($_SESSION['userauthorizationrequest']);
359     }
360
361     function get_stored_request()
362     {
363         common_ensure_session();
364         $req = $_SESSION['userauthorizationrequest'];
365         return $req;
366     }
367
368     function get_new_request()
369     {
370         common_remove_magic_from_request();
371         $req = OAuthRequest::from_request();
372         return $req;
373     }
374
375     # Throws an OAuthException if anything goes wrong
376
377     function validate_request(&$req)
378     {
379         # OAuth stuff -- have to copy from OAuth.php since they're
380         # all private methods, and there's no user-authentication method
381         common_debug('checking version', __FILE__);
382         $this->check_version($req);
383         common_debug('getting datastore', __FILE__);
384         $datastore = omb_oauth_datastore();
385         common_debug('getting consumer', __FILE__);
386         $consumer = $this->get_consumer($datastore, $req);
387         common_debug('getting token', __FILE__);
388         $token = $this->get_token($datastore, $req, $consumer);
389         common_debug('checking timestamp', __FILE__);
390         $this->check_timestamp($req);
391         common_debug('checking nonce', __FILE__);
392         $this->check_nonce($datastore, $req, $consumer, $token);
393         common_debug('checking signature', __FILE__);
394         $this->check_signature($req, $consumer, $token);
395         common_debug('validating omb stuff', __FILE__);
396         $this->validate_omb($req);
397         common_debug('done validating', __FILE__);
398         return true;
399     }
400
401     function validate_omb(&$req)
402     {
403         foreach (array('omb_version', 'omb_listener', 'omb_listenee',
404                        'omb_listenee_profile', 'omb_listenee_nickname',
405                        'omb_listenee_license') as $param)
406         {
407             if (!$req->get_parameter($param)) {
408                 throw new OAuthException("Required parameter '$param' not found");
409             }
410         }
411         # Now, OMB stuff
412         $version = $req->get_parameter('omb_version');
413         if ($version != OMB_VERSION_01) {
414             throw new OAuthException("OpenMicroBlogging version '$version' not supported");
415         }
416         $listener =    $req->get_parameter('omb_listener');
417         $user = User::staticGet('uri', $listener);
418         if (!$user) {
419             throw new OAuthException("Listener URI '$listener' not found here");
420         }
421         $cur = common_current_user();
422         if ($cur->id != $user->id) {
423             throw new OAuthException("Can't add for another user!");
424         }
425         $listenee = $req->get_parameter('omb_listenee');
426         if (!Validate::uri($listenee) &&
427             !common_valid_tag($listenee)) {
428             throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
429         }
430         if (strlen($listenee) > 255) {
431             throw new OAuthException("Listenee URI '$listenee' too long");
432         }
433
434         $other = User::staticGet('uri', $listenee);
435         if ($other) {
436             throw new OAuthException("Listenee URI '$listenee' is local user");
437         }
438
439         $remote = Remote_profile::staticGet('uri', $listenee);
440         if ($remote) {
441             $sub = new Subscription();
442             $sub->subscriber = $user->id;
443             $sub->subscribed = $remote->id;
444             if ($sub->find(true)) {
445                 throw new OAuthException("Already subscribed to user!");
446             }
447         }
448         $nickname = $req->get_parameter('omb_listenee_nickname');
449         if (!Validate::string($nickname, array('min_length' => 1,
450                                                'max_length' => 64,
451                                                'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
452             throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
453         }
454         $profile = $req->get_parameter('omb_listenee_profile');
455         if (!common_valid_http_url($profile)) {
456             throw new OAuthException("Invalid profile URL '$profile'.");
457         }
458
459         if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
460             throw new OAuthException("Profile URL '$profile' is for a local user.");
461         }
462
463         $license = $req->get_parameter('omb_listenee_license');
464         if (!common_valid_http_url($license)) {
465             throw new OAuthException("Invalid license URL '$license'.");
466         }
467         $site_license = common_config('license', 'url');
468         if (!common_compatible_license($license, $site_license)) {
469             throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
470         }
471         # optional stuff
472         $fullname = $req->get_parameter('omb_listenee_fullname');
473         if ($fullname && strlen($fullname) > 255) {
474             throw new OAuthException("Full name '$fullname' too long.");
475         }
476         $homepage = $req->get_parameter('omb_listenee_homepage');
477         if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
478             throw new OAuthException("Invalid homepage '$homepage'");
479         }
480         $bio = $req->get_parameter('omb_listenee_bio');
481         if ($bio && strlen($bio) > 140) {
482             throw new OAuthException("Bio too long '$bio'");
483         }
484         $location = $req->get_parameter('omb_listenee_location');
485         if ($location && strlen($location) > 255) {
486             throw new OAuthException("Location too long '$location'");
487         }
488         $avatar = $req->get_parameter('omb_listenee_avatar');
489         if ($avatar) {
490             if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
491                 throw new OAuthException("Invalid avatar URL '$avatar'");
492             }
493             $size = @getimagesize($avatar);
494             if (!$size) {
495                 throw new OAuthException("Can't read avatar URL '$avatar'");
496             }
497             if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
498                 throw new OAuthException("Wrong size image at '$avatar'");
499             }
500             if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
501                                           IMAGETYPE_PNG))) {
502                 throw new OAuthException("Wrong image type for '$avatar'");
503             }
504         }
505         $callback = $req->get_parameter('oauth_callback');
506         if ($callback && !common_valid_http_url($callback)) {
507             throw new OAuthException("Invalid callback URL '$callback'");
508         }
509         if ($callback && $callback == common_local_url('finishremotesubscribe')) {
510             throw new OAuthException("Callback URL '$callback' is for local site.");
511         }
512     }
513
514     # Snagged from OAuthServer
515
516     function check_version(&$req)
517     {
518         $version = $req->get_parameter("oauth_version");
519         if (!$version) {
520             $version = 1.0;
521         }
522         if ($version != 1.0) {
523             throw new OAuthException("OAuth version '$version' not supported");
524         }
525         return $version;
526     }
527
528     # Snagged from OAuthServer
529
530     function get_consumer($datastore, $req)
531     {
532         $consumer_key = @$req->get_parameter("oauth_consumer_key");
533         if (!$consumer_key) {
534             throw new OAuthException("Invalid consumer key");
535         }
536
537         $consumer = $datastore->lookup_consumer($consumer_key);
538         if (!$consumer) {
539             throw new OAuthException("Invalid consumer");
540         }
541         return $consumer;
542     }
543
544     # Mostly cadged from OAuthServer
545
546     function get_token($datastore, &$req, $consumer)
547     {/*{{{*/
548         $token_field = @$req->get_parameter('oauth_token');
549         $token = $datastore->lookup_token($consumer, 'request', $token_field);
550         if (!$token) {
551             throw new OAuthException("Invalid $token_type token: $token_field");
552         }
553         return $token;
554     }
555
556     function check_timestamp(&$req)
557     {
558         $timestamp = @$req->get_parameter('oauth_timestamp');
559         $now = time();
560         if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
561             throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
562         }
563     }
564
565     # NOTE: don't call twice on the same request; will fail!
566     function check_nonce(&$datastore, &$req, $consumer, $token)
567     {
568         $timestamp = @$req->get_parameter('oauth_timestamp');
569         $nonce = @$req->get_parameter('oauth_nonce');
570         $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
571         if ($found) {
572             throw new OAuthException("Nonce already used");
573         }
574         return true;
575     }
576
577     function check_signature(&$req, $consumer, $token)
578     {
579         $signature_method = $this->get_signature_method($req);
580         $signature = $req->get_parameter('oauth_signature');
581         $valid_sig = $signature_method->check_signature($req,
582                                                         $consumer,
583                                                         $token,
584                                                         $signature);
585         if (!$valid_sig) {
586             throw new OAuthException("Invalid signature");
587         }
588     }
589
590     function get_signature_method(&$req)
591     {
592         $signature_method = @$req->get_parameter("oauth_signature_method");
593         if (!$signature_method) {
594             $signature_method = "PLAINTEXT";
595         }
596         if ($signature_method != 'HMAC-SHA1') {
597             throw new OAuthException("Signature method '$signature_method' not supported.");
598         }
599         return omb_hmac_sha1();
600     }
601 }