]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/userauthorization.php
Fix remote sub when redirected via login (Ticket #618)
[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                                 $params['omb_listener_nickname'] = $user->nickname;
158                                 $params['omb_listener_profile'] = common_local_url('showstream',
159                                                                                                                                    array('nickname' => $user->nickname));
160                                 if ($profile->fullname) {
161                                         $params['omb_listener_fullname'] = $profile->fullname;
162                                 }
163                                 if ($profile->homepage) {
164                                         $params['omb_listener_homepage'] = $profile->homepage;
165                                 }
166                                 if ($profile->bio) {
167                                         $params['omb_listener_bio'] = $profile->bio;
168                                 }
169                                 if ($profile->location) {
170                                         $params['omb_listener_location'] = $profile->location;
171                                 }
172                                 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
173                                 if ($avatar) {
174                                         $params['omb_listener_avatar'] = $avatar->url;
175                                 }
176                                 $parts = array();
177                                 foreach ($params as $k => $v) {
178                                         $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
179                                 }
180                                 $query_string = implode('&', $parts);
181                                 $parsed = parse_url($callback);
182                                 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
183                                 common_redirect($url, 303);
184                         }
185                 } else {
186                         if (!$callback) {
187                                 $this->show_reject_message();
188                         } else {
189                                 # XXX: not 100% sure how to signal failure... just redirect without token?
190                                 common_redirect($callback, 303);
191                         }
192                 }
193         }
194
195         function authorize_token(&$req) {
196                 $consumer_key = $req->get_parameter('oauth_consumer_key');
197                 $token_field = $req->get_parameter('oauth_token');
198                 common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
199                 common_debug('token field = "'.$token_field.'"', __FILE__);
200                 $rt = new Token();
201                 $rt->consumer_key = $consumer_key;
202                 $rt->tok = $token_field;
203                 $rt->type = 0;
204                 $rt->state = 0;
205                 common_debug('request token to look up: "'.print_r($rt,TRUE).'"');
206                 if ($rt->find(true)) {
207                         common_debug('found request token to authorize', __FILE__);
208                         $orig_rt = clone($rt);
209                         $rt->state = 1; # Authorized but not used
210                         if ($rt->update($orig_rt)) {
211                                 common_debug('updated request token so it is authorized', __FILE__);
212                                 return true;
213                         }
214                 }
215                 return FALSE;
216         }
217
218         # XXX: refactor with similar code in finishremotesubscribe.php
219
220         function save_remote_profile(&$req) {
221                 # FIXME: we should really do this when the consumer comes
222                 # back for an access token. If they never do, we've got stuff in a
223                 # weird state.
224
225                 $nickname = $req->get_parameter('omb_listenee_nickname');
226                 $fullname = $req->get_parameter('omb_listenee_fullname');
227                 $profile_url = $req->get_parameter('omb_listenee_profile');
228                 $homepage = $req->get_parameter('omb_listenee_homepage');
229                 $bio = $req->get_parameter('omb_listenee_bio');
230                 $location = $req->get_parameter('omb_listenee_location');
231                 $avatar_url = $req->get_parameter('omb_listenee_avatar');
232
233                 $listenee = $req->get_parameter('omb_listenee');
234                 $remote = Remote_profile::staticGet('uri', $listenee);
235
236                 if ($remote) {
237                         $exists = true;
238                         $profile = Profile::staticGet($remote->id);
239                         $orig_remote = clone($remote);
240                         $orig_profile = clone($profile);
241                 } else {
242                         $exists = false;
243                         $remote = new Remote_profile();
244                         $remote->uri = $listenee;
245                         $profile = new Profile();
246                 }
247
248                 $profile->nickname = $nickname;
249                 $profile->profileurl = $profile_url;
250
251                 if ($fullname) {
252                         $profile->fullname = $fullname;
253                 }
254                 if ($homepage) {
255                         $profile->homepage = $homepage;
256                 }
257                 if ($bio) {
258                         $profile->bio = $bio;
259                 }
260                 if ($location) {
261                         $profile->location = $location;
262                 }
263
264                 if ($exists) {
265                         $profile->update($orig_profile);
266                 } else {
267                         $profile->created = DB_DataObject_Cast::dateTime(); # current time
268                         $id = $profile->insert();
269                         if (!$id) {
270                                 return FALSE;
271                         }
272                         $remote->id = $id;
273                 }
274
275                 if ($exists) {
276                         if (!$remote->update($orig_remote)) {
277                                 return FALSE;
278                         }
279                 } else {
280                         $remote->created = DB_DataObject_Cast::dateTime(); # current time
281                         if (!$remote->insert()) {
282                                 return FALSE;
283                         }
284                 }
285
286                 if ($avatar_url) {
287                         if (!$this->add_avatar($profile, $avatar_url)) {
288                                 return FALSE;
289                         }
290                 }
291
292                 $user = common_current_user();
293                 $datastore = omb_oauth_datastore();
294                 $consumer = $this->get_consumer($datastore, $req);
295                 $token = $this->get_token($datastore, $req, $consumer);
296
297                 $sub = new Subscription();
298                 $sub->subscriber = $user->id;
299                 $sub->subscribed = $remote->id;
300                 $sub->token = $token->key; # NOTE: request token, not valid for use!
301                 $sub->created = DB_DataObject_Cast::dateTime(); # current time
302
303                 if (!$sub->insert()) {
304                         return FALSE;
305                 }
306
307                 return TRUE;
308         }
309
310         function add_avatar($profile, $url) {
311                 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
312                 copy($url, $temp_filename);
313                 return $profile->setOriginal($temp_filename);
314         }
315
316         function show_accept_message($tok) {
317                 common_show_header(_('Subscription authorized'));
318                 common_element('p', NULL,
319                                            _('The subscription has been authorized, but no '.
320                                                   'callback URL was passed. Check with the site\'s instructions for '.
321                                                   'details on how to authorize the subscription. Your subscription token is:'));
322                 common_element('blockquote', 'token', $tok);
323                 common_show_footer();
324         }
325
326         function show_reject_message($tok) {
327                 common_show_header(_('Subscription rejected'));
328                 common_element('p', NULL,
329                                            _('The subscription has been rejected, but no '.
330                                                   'callback URL was passed. Check with the site\'s instructions for '.
331                                                   'details on how to fully reject the subscription.'));
332                 common_show_footer();
333         }
334
335         function store_request($req) {
336                 common_ensure_session();
337                 $_SESSION['userauthorizationrequest'] = $req;
338         }
339
340         function clear_request() {
341                 common_ensure_session();
342                 unset($_SESSION['userauthorizationrequest']);
343         }
344
345         function get_stored_request() {
346                 common_ensure_session();
347                 $req = $_SESSION['userauthorizationrequest'];
348                 return $req;
349         }
350
351         function get_new_request() {
352                 common_remove_magic_from_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 }