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