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