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