]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/userauthorization.php
add inboxed and regenerate data objects
[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                 $remote = Remote_profile::staticGet('uri', $listenee);
419                 if ($remote) {
420                         $sub = new Subscription();
421                         $sub->subscriber = $user->id;
422                         $sub->subscribed = $remote->id;
423                         if ($sub->find(TRUE)) {
424                                 throw new OAuthException("Already subscribed to user!");
425                         }
426                 }
427                 $nickname = $req->get_parameter('omb_listenee_nickname');
428                 if (!Validate::string($nickname, array('min_length' => 1,
429                                                                                            'max_length' => 64,
430                                                                                            'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
431                         throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
432                 }
433                 $profile = $req->get_parameter('omb_listenee_profile');
434                 if (!common_valid_http_url($profile)) {
435                         throw new OAuthException("Invalid profile URL '$profile'.");
436                 }
437                 $license = $req->get_parameter('omb_listenee_license');
438                 if (!common_valid_http_url($license)) {
439                         throw new OAuthException("Invalid license URL '$license'.");
440                 }
441                 # optional stuff
442                 $fullname = $req->get_parameter('omb_listenee_fullname');
443                 if ($fullname && strlen($fullname) > 255) {
444                         throw new OAuthException("Full name '$fullname' too long.");
445                 }
446                 $homepage = $req->get_parameter('omb_listenee_homepage');
447                 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
448                         throw new OAuthException("Invalid homepage '$homepage'");
449                 }
450                 $bio = $req->get_parameter('omb_listenee_bio');
451                 if ($bio && strlen($bio) > 140) {
452                         throw new OAuthException("Bio too long '$bio'");
453                 }
454                 $location = $req->get_parameter('omb_listenee_location');
455                 if ($location && strlen($location) > 255) {
456                         throw new OAuthException("Location too long '$location'");
457                 }
458                 $avatar = $req->get_parameter('omb_listenee_avatar');
459                 if ($avatar) {
460                         if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
461                                 throw new OAuthException("Invalid avatar URL '$avatar'");
462                         }
463                         $size = @getimagesize($avatar);
464                         if (!$size) {
465                                 throw new OAuthException("Can't read avatar URL '$avatar'");
466                         }
467                         if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
468                                 throw new OAuthException("Wrong size image at '$avatar'");
469                         }
470                         if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
471                                                                                   IMAGETYPE_PNG))) {
472                                 throw new OAuthException("Wrong image type for '$avatar'");
473                         }
474                 }
475                 $callback = $req->get_parameter('oauth_callback');
476                 if ($callback && !common_valid_http_url($callback)) {
477                         throw new OAuthException("Invalid callback URL '$callback'");
478                 }
479         }
480
481         # Snagged from OAuthServer
482
483         function check_version(&$req) {
484                 $version = $req->get_parameter("oauth_version");
485                 if (!$version) {
486                         $version = 1.0;
487                 }
488                 if ($version != 1.0) {
489                         throw new OAuthException("OAuth version '$version' not supported");
490                 }
491                 return $version;
492         }
493
494         # Snagged from OAuthServer
495
496         function get_consumer($datastore, $req) {
497                 $consumer_key = @$req->get_parameter("oauth_consumer_key");
498                 if (!$consumer_key) {
499                         throw new OAuthException("Invalid consumer key");
500                 }
501
502                 $consumer = $datastore->lookup_consumer($consumer_key);
503                 if (!$consumer) {
504                         throw new OAuthException("Invalid consumer");
505                 }
506                 return $consumer;
507         }
508
509         # Mostly cadged from OAuthServer
510
511         function get_token($datastore, &$req, $consumer) {/*{{{*/
512                 $token_field = @$req->get_parameter('oauth_token');
513                 $token = $datastore->lookup_token($consumer, 'request', $token_field);
514                 if (!$token) {
515                         throw new OAuthException("Invalid $token_type token: $token_field");
516                 }
517                 return $token;
518         }
519
520         function check_timestamp(&$req) {
521                 $timestamp = @$req->get_parameter('oauth_timestamp');
522                 $now = time();
523                 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
524                         throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
525                 }
526         }
527
528         # NOTE: don't call twice on the same request; will fail!
529         function check_nonce(&$datastore, &$req, $consumer, $token) {
530                 $timestamp = @$req->get_parameter('oauth_timestamp');
531                 $nonce = @$req->get_parameter('oauth_nonce');
532                 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
533                 if ($found) {
534                         throw new OAuthException("Nonce already used");
535                 }
536                 return true;
537         }
538
539         function check_signature(&$req, $consumer, $token) {
540                 $signature_method = $this->get_signature_method($req);
541                 $signature = $req->get_parameter('oauth_signature');
542                 $valid_sig = $signature_method->check_signature($req,
543                                                                                                                 $consumer,
544                                                                                                                 $token,
545                                                                                                                 $signature);
546                 if (!$valid_sig) {
547                         throw new OAuthException("Invalid signature");
548                 }
549         }
550
551         function get_signature_method(&$req) {
552                 $signature_method = @$req->get_parameter("oauth_signature_method");
553                 if (!$signature_method) {
554                         $signature_method = "PLAINTEXT";
555                 }
556                 if ($signature_method != 'HMAC-SHA1') {
557                         throw new OAuthException("Signature method '$signature_method' not supported.");
558                 }
559                 return omb_hmac_sha1();
560         }
561 }