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