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