]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/userauthorization.php
3d4661b25d2a375b1dab19bee04622e20c27c131
[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_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                 $req = OAuthRequest::from_request();
353                 return $req;
354         }
355
356         # Throws an OAuthException if anything goes wrong
357
358         function validate_request(&$req) {
359                 # OAuth stuff -- have to copy from OAuth.php since they're
360                 # all private methods, and there's no user-authentication method
361                 common_debug('checking version', __FILE__);
362                 $this->check_version($req);
363                 common_debug('getting datastore', __FILE__);
364                 $datastore = omb_oauth_datastore();
365                 common_debug('getting consumer', __FILE__);
366                 $consumer = $this->get_consumer($datastore, $req);
367                 common_debug('getting token', __FILE__);
368                 $token = $this->get_token($datastore, $req, $consumer);
369                 common_debug('checking timestamp', __FILE__);
370                 $this->check_timestamp($req);
371                 common_debug('checking nonce', __FILE__);
372                 $this->check_nonce($datastore, $req, $consumer, $token);
373                 common_debug('checking signature', __FILE__);
374                 $this->check_signature($req, $consumer, $token);
375                 common_debug('validating omb stuff', __FILE__);
376                 $this->validate_omb($req);
377                 common_debug('done validating', __FILE__);
378                 return true;
379         }
380
381         function validate_omb(&$req) {
382                 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
383                                            'omb_listenee_profile', 'omb_listenee_nickname',
384                                            'omb_listenee_license') as $param)
385                 {
386                         if (!$req->get_parameter($param)) {
387                                 throw new OAuthException("Required parameter '$param' not found");
388                         }
389                 }
390                 # Now, OMB stuff
391                 $version = $req->get_parameter('omb_version');
392                 if ($version != OMB_VERSION_01) {
393                         throw new OAuthException("OpenMicroBlogging version '$version' not supported");
394                 }
395                 $listener =     $req->get_parameter('omb_listener');
396                 $user = User::staticGet('uri', $listener);
397                 if (!$user) {
398                         throw new OAuthException("Listener URI '$listener' not found here");
399                 }
400                 $cur = common_current_user();
401                 if ($cur->id != $user->id) {
402                         throw new OAuthException("Can't add for another user!");
403                 }
404                 $listenee = $req->get_parameter('omb_listenee');
405                 if (!Validate::uri($listenee) &&
406                         !common_valid_tag($listenee)) {
407                         throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
408                 }
409                 if (strlen($listenee) > 255) {
410                         throw new OAuthException("Listenee URI '$listenee' too long");
411                 }
412                 $remote = Remote_profile::staticGet('uri', $listenee);
413                 if ($remote) {
414                         $sub = new Subscription();
415                         $sub->subscriber = $user->id;
416                         $sub->subscribed = $remote->id;
417                         if ($sub->find(TRUE)) {
418                                 throw new OAuthException("Already subscribed to user!");
419                         }
420                 }
421                 $nickname = $req->get_parameter('omb_listenee_nickname');
422                 if (!Validate::string($nickname, array('min_length' => 1,
423                                                                                            'max_length' => 64,
424                                                                                            'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
425                         throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
426                 }
427                 $profile = $req->get_parameter('omb_listenee_profile');
428                 if (!common_valid_http_url($profile)) {
429                         throw new OAuthException("Invalid profile URL '$profile'.");
430                 }
431                 $license = $req->get_parameter('omb_listenee_license');
432                 if (!common_valid_http_url($license)) {
433                         throw new OAuthException("Invalid license URL '$license'.");
434                 }
435                 # optional stuff
436                 $fullname = $req->get_parameter('omb_listenee_fullname');
437                 if ($fullname && strlen($fullname) > 255) {
438                         throw new OAuthException("Full name '$fullname' too long.");
439                 }
440                 $homepage = $req->get_parameter('omb_listenee_homepage');
441                 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
442                         throw new OAuthException("Invalid homepage '$homepage'");
443                 }
444                 $bio = $req->get_parameter('omb_listenee_bio');
445                 if ($bio && strlen($bio) > 140) {
446                         throw new OAuthException("Bio too long '$bio'");
447                 }
448                 $location = $req->get_parameter('omb_listenee_location');
449                 if ($location && strlen($location) > 255) {
450                         throw new OAuthException("Location too long '$location'");
451                 }
452                 $avatar = $req->get_parameter('omb_listenee_avatar');
453                 if ($avatar) {
454                         if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
455                                 throw new OAuthException("Invalid avatar URL '$avatar'");
456                         }
457                         $size = @getimagesize($avatar);
458                         if (!$size) {
459                                 throw new OAuthException("Can't read avatar URL '$avatar'");
460                         }
461                         if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
462                                 throw new OAuthException("Wrong size image at '$avatar'");
463                         }
464                         if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
465                                                                                   IMAGETYPE_PNG))) {
466                                 throw new OAuthException("Wrong image type for '$avatar'");
467                         }
468                 }
469                 $callback = $req->get_parameter('oauth_callback');
470                 if ($callback && !common_valid_http_url($callback)) {
471                         throw new OAuthException("Invalid callback URL '$callback'");
472                 }
473         }
474
475         # Snagged from OAuthServer
476
477         function check_version(&$req) {
478                 $version = $req->get_parameter("oauth_version");
479                 if (!$version) {
480                         $version = 1.0;
481                 }
482                 if ($version != 1.0) {
483                         throw new OAuthException("OAuth version '$version' not supported");
484                 }
485                 return $version;
486         }
487
488         # Snagged from OAuthServer
489
490         function get_consumer($datastore, $req) {
491                 $consumer_key = @$req->get_parameter("oauth_consumer_key");
492                 if (!$consumer_key) {
493                         throw new OAuthException("Invalid consumer key");
494                 }
495
496                 $consumer = $datastore->lookup_consumer($consumer_key);
497                 if (!$consumer) {
498                         throw new OAuthException("Invalid consumer");
499                 }
500                 return $consumer;
501         }
502
503         # Mostly cadged from OAuthServer
504
505         function get_token($datastore, &$req, $consumer) {/*{{{*/
506                 $token_field = @$req->get_parameter('oauth_token');
507                 $token = $datastore->lookup_token($consumer, 'request', $token_field);
508                 if (!$token) {
509                         throw new OAuthException("Invalid $token_type token: $token_field");
510                 }
511                 return $token;
512         }
513
514         function check_timestamp(&$req) {
515                 $timestamp = @$req->get_parameter('oauth_timestamp');
516                 $now = time();
517                 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
518                         throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
519                 }
520         }
521
522         # NOTE: don't call twice on the same request; will fail!
523         function check_nonce(&$datastore, &$req, $consumer, $token) {
524                 $timestamp = @$req->get_parameter('oauth_timestamp');
525                 $nonce = @$req->get_parameter('oauth_nonce');
526                 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
527                 if ($found) {
528                         throw new OAuthException("Nonce already used");
529                 }
530                 return true;
531         }
532
533         function check_signature(&$req, $consumer, $token) {
534                 $signature_method = $this->get_signature_method($req);
535                 $signature = $req->get_parameter('oauth_signature');
536                 $valid_sig = $signature_method->check_signature($req,
537                                                                                                                 $consumer,
538                                                                                                                 $token,
539                                                                                                                 $signature);
540                 if (!$valid_sig) {
541                         throw new OAuthException("Invalid signature");
542                 }
543         }
544
545         function get_signature_method(&$req) {
546                 $signature_method = @$req->get_parameter("oauth_signature_method");
547                 if (!$signature_method) {
548                         $signature_method = "PLAINTEXT";
549                 }
550                 if ($signature_method != 'HMAC-SHA1') {
551                         throw new OAuthException("Signature method '$signature_method' not supported.");
552                 }
553                 return omb_hmac_sha1();
554         }
555 }