]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/userauthorization.php
relative info, too
[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('userauthorization.php - saving URL for returnto');
37                                 $argsclone = $_GET;
38                                 unset($argsclone['action']);
39                                 common_set_returnto(common_local_url('userauthorization', $argsclone));
40                                 common_debug('userauthorization.php - redirecting to login');
41                                 common_redirect(common_local_url('login'));
42                                 return;
43                         }
44                         try {
45                                 # this must be a new request
46                                 common_debug('userauthorization.php - getting new request');
47                                 $req = $this->get_new_request();
48                                 if (!$req) {
49                                         common_server_error(_('No request found!'));
50                                 }
51                                 common_debug('userauthorization.php - validating request');
52                                 # XXX: only validate new requests, since nonce is one-time use
53                                 $this->validate_request($req);
54                                 common_debug('userauthorization.php - showing form');
55                                 $this->store_request($req);
56                                 $this->show_form($req);
57                         } catch (OAuthException $e) {
58                                 $this->clear_request();
59                                 common_server_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                                 common_server_error(_('Error authorizing token'));
138                         }
139                         if (!$this->save_remote_profile($req)) {
140                                 common_server_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                 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
389                 if (!$user) {
390                         throw new OAuthException("Listener URI '$listener' not found here");
391                 }
392                 $cur = common_current_user();
393                 if ($cur->id != $user->id) {
394                         throw new OAuthException("Can't add for another user!");
395                 }
396                 $listenee = $req->get_parameter('omb_listenee');
397                 if (!Validate::uri($listenee) &&
398                         !common_valid_tag($listenee)) {
399                         throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
400                 }
401                 if (strlen($listenee) > 255) {
402                         throw new OAuthException("Listenee URI '$listenee' too long");
403                 }
404                 $remote = Remote_profile::staticGet('uri', $listenee);
405                 if ($remote) {
406                         $sub = new Subscription();
407                         $sub->subscriber = $user->id;
408                         $sub->subscribed = $remote->id;
409                         if ($sub->find(TRUE)) {
410                                 throw new OAuthException("Already subscribed to user!");
411                         }
412                 }
413                 $nickname = $req->get_parameter('omb_listenee_nickname');
414                 if (!Validate::string($nickname, array('min_length' => 1,
415                                                                                            'max_length' => 64,
416                                                                                            'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
417                         throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
418                 }
419                 $profile = $req->get_parameter('omb_listenee_profile');
420                 if (!common_valid_http_url($profile)) {
421                         throw new OAuthException("Invalid profile URL '$profile'.");
422                 }
423                 $license = $req->get_parameter('omb_listenee_license');
424                 if (!common_valid_http_url($license)) {
425                         throw new OAuthException("Invalid license URL '$license'.");
426                 }
427                 # optional stuff
428                 $fullname = $req->get_parameter('omb_listenee_fullname');
429                 if ($fullname && strlen($fullname) > 255) {
430                         throw new OAuthException("Full name '$fullname' too long.");
431                 }
432                 $homepage = $req->get_parameter('omb_listenee_homepage');
433                 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
434                         throw new OAuthException("Invalid homepage '$homepage'");
435                 }
436                 $bio = $req->get_parameter('omb_listenee_bio');
437                 if ($bio && strlen($bio) > 140) {
438                         throw new OAuthException("Bio too long '$bio'");
439                 }
440                 $location = $req->get_parameter('omb_listenee_location');
441                 if ($location && strlen($location) > 255) {
442                         throw new OAuthException("Location too long '$location'");
443                 }
444                 $avatar = $req->get_parameter('omb_listenee_avatar');
445                 if ($avatar) {
446                         if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
447                                 throw new OAuthException("Invalid avatar URL '$avatar'");
448                         }
449                         $size = @getimagesize($avatar);
450                         if (!$size) {
451                                 throw new OAuthException("Can't read avatar URL '$avatar'");
452                         }
453                         if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
454                                 throw new OAuthException("Wrong size image at '$avatar'");
455                         }
456                         if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
457                                                                                   IMAGETYPE_PNG))) {
458                                 throw new OAuthException("Wrong image type for '$avatar'");
459                         }
460                 }
461                 $callback = $req->get_parameter('oauth_callback');
462                 if ($callback && !common_valid_http_url($callback)) {
463                         throw new OAuthException("Invalid callback URL '$callback'");
464                 }
465         }
466
467         # Snagged from OAuthServer
468
469         function check_version(&$req) {
470                 $version = $req->get_parameter("oauth_version");
471                 if (!$version) {
472                         $version = 1.0;
473                 }
474                 if ($version != 1.0) {
475                         throw new OAuthException("OAuth version '$version' not supported");
476                 }
477                 return $version;
478         }
479
480         # Snagged from OAuthServer
481
482         function get_consumer($datastore, $req) {
483                 $consumer_key = @$req->get_parameter("oauth_consumer_key");
484                 if (!$consumer_key) {
485                         throw new OAuthException("Invalid consumer key");
486                 }
487
488                 $consumer = $datastore->lookup_consumer($consumer_key);
489                 if (!$consumer) {
490                         throw new OAuthException("Invalid consumer");
491                 }
492                 return $consumer;
493         }
494
495         # Mostly cadged from OAuthServer
496
497         function get_token($datastore, &$req, $consumer) {/*{{{*/
498                 $token_field = @$req->get_parameter('oauth_token');
499                 $token = $datastore->lookup_token($consumer, 'request', $token_field);
500                 if (!$token) {
501                         throw new OAuthException("Invalid $token_type token: $token_field");
502                 }
503                 return $token;
504         }
505
506         function check_timestamp(&$req) {
507                 $timestamp = @$req->get_parameter('oauth_timestamp');
508                 $now = time();
509                 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
510                         throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
511                 }
512         }
513
514         # NOTE: don't call twice on the same request; will fail!
515         function check_nonce(&$datastore, &$req, $consumer, $token) {
516                 $timestamp = @$req->get_parameter('oauth_timestamp');
517                 $nonce = @$req->get_parameter('oauth_nonce');
518                 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
519                 if ($found) {
520                         throw new OAuthException("Nonce already used");
521                 }
522                 return true;
523         }
524
525         function check_signature(&$req, $consumer, $token) {
526                 $signature_method = $this->get_signature_method($req);
527                 $signature = $req->get_parameter('oauth_signature');
528                 $valid_sig = $signature_method->check_signature($req,
529                                                                                                                 $consumer,
530                                                                                                                 $token,
531                                                                                                                 $signature);
532                 if (!$valid_sig) {
533                         throw new OAuthException("Invalid signature");
534                 }
535         }
536
537         function get_signature_method(&$req) {
538                 $signature_method = @$req->get_parameter("oauth_signature_method");
539                 if (!$signature_method) {
540                         $signature_method = "PLAINTEXT";
541                 }
542                 if ($signature_method != 'HMAC-SHA1') {
543                         throw new OAuthException("Signature method '$signature_method' not supported.");
544                 }
545                 return omb_hmac_sha1();
546         }
547 }