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