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