]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/userauthorization.php
add some extra checks to avoid remote subscriptions to local users
[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                 # optional stuff
453                 $fullname = $req->get_parameter('omb_listenee_fullname');
454                 if ($fullname && strlen($fullname) > 255) {
455                         throw new OAuthException("Full name '$fullname' too long.");
456                 }
457                 $homepage = $req->get_parameter('omb_listenee_homepage');
458                 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
459                         throw new OAuthException("Invalid homepage '$homepage'");
460                 }
461                 $bio = $req->get_parameter('omb_listenee_bio');
462                 if ($bio && strlen($bio) > 140) {
463                         throw new OAuthException("Bio too long '$bio'");
464                 }
465                 $location = $req->get_parameter('omb_listenee_location');
466                 if ($location && strlen($location) > 255) {
467                         throw new OAuthException("Location too long '$location'");
468                 }
469                 $avatar = $req->get_parameter('omb_listenee_avatar');
470                 if ($avatar) {
471                         if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
472                                 throw new OAuthException("Invalid avatar URL '$avatar'");
473                         }
474                         $size = @getimagesize($avatar);
475                         if (!$size) {
476                                 throw new OAuthException("Can't read avatar URL '$avatar'");
477                         }
478                         if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
479                                 throw new OAuthException("Wrong size image at '$avatar'");
480                         }
481                         if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
482                                                                                   IMAGETYPE_PNG))) {
483                                 throw new OAuthException("Wrong image type for '$avatar'");
484                         }
485                 }
486                 $callback = $req->get_parameter('oauth_callback');
487                 if ($callback && !common_valid_http_url($callback)) {
488                         throw new OAuthException("Invalid callback URL '$callback'");
489                 }
490                 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
491                         throw new OAuthException("Callback URL '$callback' is for local site.");
492                 }
493         }
494
495         # Snagged from OAuthServer
496
497         function check_version(&$req) {
498                 $version = $req->get_parameter("oauth_version");
499                 if (!$version) {
500                         $version = 1.0;
501                 }
502                 if ($version != 1.0) {
503                         throw new OAuthException("OAuth version '$version' not supported");
504                 }
505                 return $version;
506         }
507
508         # Snagged from OAuthServer
509
510         function get_consumer($datastore, $req) {
511                 $consumer_key = @$req->get_parameter("oauth_consumer_key");
512                 if (!$consumer_key) {
513                         throw new OAuthException("Invalid consumer key");
514                 }
515
516                 $consumer = $datastore->lookup_consumer($consumer_key);
517                 if (!$consumer) {
518                         throw new OAuthException("Invalid consumer");
519                 }
520                 return $consumer;
521         }
522
523         # Mostly cadged from OAuthServer
524
525         function get_token($datastore, &$req, $consumer) {/*{{{*/
526                 $token_field = @$req->get_parameter('oauth_token');
527                 $token = $datastore->lookup_token($consumer, 'request', $token_field);
528                 if (!$token) {
529                         throw new OAuthException("Invalid $token_type token: $token_field");
530                 }
531                 return $token;
532         }
533
534         function check_timestamp(&$req) {
535                 $timestamp = @$req->get_parameter('oauth_timestamp');
536                 $now = time();
537                 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
538                         throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
539                 }
540         }
541
542         # NOTE: don't call twice on the same request; will fail!
543         function check_nonce(&$datastore, &$req, $consumer, $token) {
544                 $timestamp = @$req->get_parameter('oauth_timestamp');
545                 $nonce = @$req->get_parameter('oauth_nonce');
546                 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
547                 if ($found) {
548                         throw new OAuthException("Nonce already used");
549                 }
550                 return true;
551         }
552
553         function check_signature(&$req, $consumer, $token) {
554                 $signature_method = $this->get_signature_method($req);
555                 $signature = $req->get_parameter('oauth_signature');
556                 $valid_sig = $signature_method->check_signature($req,
557                                                                                                                 $consumer,
558                                                                                                                 $token,
559                                                                                                                 $signature);
560                 if (!$valid_sig) {
561                         throw new OAuthException("Invalid signature");
562                 }
563         }
564
565         function get_signature_method(&$req) {
566                 $signature_method = @$req->get_parameter("oauth_signature_method");
567                 if (!$signature_method) {
568                         $signature_method = "PLAINTEXT";
569                 }
570                 if ($signature_method != 'HMAC-SHA1') {
571                         throw new OAuthException("Signature method '$signature_method' not supported.");
572                 }
573                 return omb_hmac_sha1();
574         }
575 }