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