]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/userauthorization.php
eb93b6fd301d06a77e3beb8b9bac16eb328a5949
[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(_t('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(_t('Authorize subscription'));
77                 common_element('p', _t('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', _t('Accept'));
119                 common_submit('reject', _t('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(_t('No authorization request!'));
129                         return;
130                 }
131
132                 $callback = $req->get_parameter('oauth_callback');
133
134                 if ($this->arg('accept')) {
135                         $this->authorize_token($req);
136                         $this->save_remote_profile($req);
137                         if (!$callback) {
138                                 $this->show_accept_message($req->get_parameter('oauth_token'));
139                         } else {
140                                 $params = array();
141                                 $params['oauth_token'] = $req->get_parameter('oauth_token');
142                                 $params['omb_version'] = OMB_VERSION_01;
143                                 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
144                                 $profile = $user->getProfile();
145                                 $params['omb_listener_nickname'] = $user->nickname;
146                                 $params['omb_listener_profile'] = common_local_url('showstream',
147                                                                                                                                    array('nickname' => $user->nickname));
148                                 if ($profile->fullname) {
149                                         $params['omb_listener_fullname'] = $profile->fullname;
150                                 }
151                                 if ($profile->homepage) {
152                                         $params['omb_listener_homepage'] = $profile->homepage;
153                                 }
154                                 if ($profile->bio) {
155                                         $params['omb_listener_bio'] = $profile->bio;
156                                 }
157                                 if ($profile->location) {
158                                         $params['omb_listener_location'] = $profile->location;
159                                 }
160                                 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
161                                 if ($avatar) {
162                                         $params['omb_listener_avatar'] = $avatar->url;
163                                 }
164                                 $parts = array();
165                                 foreach ($params as $k => $v) {
166                                         $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
167                                 }
168                                 $query_string = implode('&', $parts);
169                                 $parsed = parse_url($callback);
170                                 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
171                                 common_redirect($url, 303);
172                         }
173                 } else {
174                         if (!$callback) {
175                                 $this->show_reject_message();
176                         } else {
177                                 # XXX: not 100% sure how to signal failure... just redirect without token?
178                                 common_redirect($callback, 303);
179                         }
180                 }
181         }
182
183         function authorize_token(&$req) {
184                 $consumer_key = @$req->get_parameter('oauth_consumer_key');
185                 $token_field = @$req->get_parameter('oauth_token');
186                 $rt = new Token();
187                 $rt->consumer_key = $consumer_key;
188                 $rt->tok = $token_field;
189                 if ($rt->find(TRUE)) {
190                         $orig_rt = clone($rt);
191                         $rt->state = 1; # Authorized but not used
192                         if ($rt->update($orig_rt)) {
193                                 return true;
194                         }
195                 }
196                 return FALSE;
197         }
198
199         # XXX: refactor with similar code in finishremotesubscribe.php
200         
201         function save_remote_profile(&$req) {
202                 # FIXME: we should really do this when the consumer comes
203                 # back for an access token. If they never do, we've got stuff in a 
204                 # weird state.
205         
206                 $nickname = $req->get_parameter('omb_listenee_nickname');
207                 $fullname = $req->get_parameter('omb_listenee_fullname');
208                 $profile_url = $req->get_parameter('omb_listenee_profile');             
209                 $homepage = $req->get_parameter('omb_listenee_homepage');
210                 $bio = $req->get_parameter('omb_listenee_bio');
211                 $location = $req->get_parameter('omb_listenee_location');
212                 $avatar_url = $req->get_parameter('omb_listenee_avatar');
213                 
214                 $listenee = $req->get_parameter('omb_listenee');
215                 $remote = Remote_profile::staticGet('uri', $listenee);
216                 
217                 if ($remote) {
218                         $exists = true;
219                         $profile = Profile::staticGet($remote->id);
220                         $orig_remote = clone($remote);
221                         $orig_profile = clone($profile);
222                 } else {
223                         $exists = false;
224                         $remote = new Remote_profile();
225                         $remote->uri = $listenee;
226                         $profile = new Profile();
227                 }
228
229                 $profile->nickname = $nickname;
230                 $profile->profileurl = $profile_url;
231                 
232                 if ($fullname) {
233                         $profile->fullname = $fullname;
234                 }
235                 if ($homepage) {
236                         $profile->homepage = $homepage;
237                 }
238                 if ($bio) {
239                         $profile->bio = $bio;
240                 }
241                 if ($location) {
242                         $profile->location = $location;
243                 }
244                 
245                 if ($exists) {
246                         $profile->update($orig_profile);
247                 } else {
248                         $profile->created = DB_DataObject_Cast::dateTime(); # current time
249                         $id = $profile->insert();
250                         $remote->id = $id;
251                 }
252
253                 if ($exists) {
254                         $remote->update($orig_remote);
255                 } else {
256                         $remote->created = DB_DataObject_Cast::dateTime(); # current time
257                         $remote->insert();
258                 }
259
260                 if ($avatar_url) {
261                         $this->add_avatar($profile, $avatar_url);
262                 }
263
264                 $user = common_current_user();
265                 $datastore = omb_oauth_datastore();
266                 $consumer = $this->get_consumer($datastore, $req);
267                 $token = $this->get_token($datastore, $req, $consumer);
268
269                 $sub = new Subscription();
270                 $sub->subscriber = $user->id;
271                 $sub->subscribed = $remote->id;
272                 $sub->token = $token->key; # NOTE: request token, not valid for use!
273                 $sub->created = DB_DataObject_Cast::dateTime(); # current time
274                 
275                 if (!$sub->insert()) {
276                         common_user_error(_t('Couldn\'t insert new subscription.'));
277                         return;
278                 }
279         }
280
281         function add_avatar($profile, $url) {
282                 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
283                 copy($url, $temp_filename);
284                 return $profile->setOriginal($temp_filename);
285         }
286         
287         function show_accept_message($tok) {
288                 common_show_header(_t('Subscription authorized'));
289                 common_element('p', NULL, 
290                                            _t('The subscription has been authorized, but no '.
291                                                   'callback URL was passed. Check with the site\'s instructions for '.
292                                                   'details on how to authorize the subscription. Your subscription token is:'));
293                 common_element('blockquote', 'token', $tok);
294                 common_show_footer();
295         }
296
297         function show_reject_message($tok) {
298                 common_show_header(_t('Subscription rejected'));
299                 common_element('p', NULL, 
300                                            _t('The subscription has been rejected, but no '.
301                                                   'callback URL was passed. Check with the site\'s instructions for '.
302                                                   'details on how to fully reject the subscription.'));
303                 common_show_footer();
304         }
305         
306         function store_request($req) {
307                 common_ensure_session();
308                 $_SESSION['userauthorizationrequest'] = $req;
309         }
310         
311         function clear_request() {
312                 common_ensure_session();
313                 unset($_SESSION['userauthorizationrequest']);
314         }
315         
316         function get_stored_request() {
317                 common_ensure_session();                
318                 $req = $_SESSION['userauthorizationrequest'];
319                 return $req;
320         }
321
322         function get_new_request() {
323                 $req = OAuthRequest::from_request();
324                 return $req;
325         }
326         
327         # Throws an OAuthException if anything goes wrong
328         
329         function validate_request(&$req) {
330                 # OAuth stuff -- have to copy from OAuth.php since they're
331                 # all private methods, and there's no user-authentication method
332                 common_debug('checking version', __FILE__);
333                 $this->check_version($req);
334                 common_debug('getting datastore', __FILE__);            
335                 $datastore = omb_oauth_datastore();
336                 common_debug('getting consumer', __FILE__);
337                 $consumer = $this->get_consumer($datastore, $req);
338                 common_debug('getting token', __FILE__);                
339                 $token = $this->get_token($datastore, $req, $consumer);
340                 common_debug('checking timestamp', __FILE__);
341                 $this->check_timestamp($req);
342                 common_debug('checking nonce', __FILE__);               
343                 $this->check_nonce($datastore, $req, $consumer, $token);
344                 common_debug('checking signature', __FILE__);
345                 $this->check_signature($req, $consumer, $token);
346                 common_debug('validating omb stuff', __FILE__);         
347                 $this->validate_omb($req);
348                 common_debug('done validating', __FILE__);                              
349                 return true;
350         }
351
352         function validate_omb(&$req) {
353                 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
354                                            'omb_listenee_profile', 'omb_listenee_nickname',
355                                            'omb_listenee_license') as $param)
356                 {
357                         if (!$req->get_parameter($param)) {
358                                 throw new OAuthException("Required parameter '$param' not found");
359                         }
360                 }
361                 # Now, OMB stuff
362                 $version = $req->get_parameter('omb_version');
363                 if ($version != OMB_VERSION_01) {
364                         throw new OAuthException("OpenMicroBlogging version '$version' not supported");
365                 }
366                 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
367                 if (!$user) {
368                         throw new OAuthException("Listener URI '$listener' not found here");
369                 }
370                 $listenee = $req->get_parameter('omb_listenee');
371                 if (!Validate::uri($listenee) &&
372                         !common_valid_tag($listenee)) {
373                         throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
374                 }
375                 if (strlen($listenee) > 255) {
376                         throw new OAuthException("Listenee URI '$listenee' too long");
377                 }
378                 $nickname = $req->get_parameter('omb_listenee_nickname');
379                 if (!Validate::string($nickname, array('min_length' => 1,
380                                                                                            'max_length' => 64,
381                                                                                            'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
382                         throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
383                 }
384                 $profile = $req->get_parameter('omb_listenee_profile');
385                 if (!common_valid_http_url($profile)) {
386                         throw new OAuthException("Invalid profile URL '$profile'.");
387                 }
388                 $license = $req->get_parameter('omb_listenee_license');
389                 if (!common_valid_http_url($license)) {
390                         throw new OAuthException("Invalid license URL '$license'.");
391                 }
392                 # optional stuff
393                 $fullname = $req->get_parameter('omb_listenee_fullname');
394                 if ($fullname && strlen($fullname) > 255) {
395                         throw new OAuthException("Full name '$fullname' too long.");
396                 }
397                 $homepage = $req->get_parameter('omb_listenee_homepage');
398                 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
399                         throw new OAuthException("Invalid homepage '$homepage'");
400                 }
401                 $bio = $req->get_parameter('omb_listenee_bio');
402                 if ($bio && strlen($bio) > 140) {
403                         throw new OAuthException("Bio too long '$bio'");
404                 }
405                 $location = $req->get_parameter('omb_listenee_location');
406                 if ($location && strlen($location) > 255) {
407                         throw new OAuthException("Location too long '$location'");
408                 }
409                 $avatar = $req->get_parameter('omb_listenee_avatar');
410                 if ($avatar) {
411                         if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
412                                 throw new OAuthException("Invalid avatar URL '$avatar'");
413                         }
414                         $size = @getimagesize($avatar);
415                         if (!$size) {
416                                 throw new OAuthException("Can't read avatar URL '$avatar'");
417                         }
418                         if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
419                                 throw new OAuthException("Wrong size image at '$avatar'");
420                         }
421                         if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
422                                                                                   IMAGETYPE_PNG))) {
423                                 throw new OAuthException("Wrong image type for '$avatar'");
424                         }
425                 }
426                 $callback = $req->get_parameter('oauth_callback');
427                 if ($callback && !common_valid_http_url($callback)) {
428                         throw new OAuthException("Invalid callback URL '$callback'");
429                 }
430         }
431         
432         # Snagged from OAuthServer
433         
434         function check_version(&$req) {
435                 $version = $req->get_parameter("oauth_version");
436                 if (!$version) {
437                         $version = 1.0;
438                 }
439                 if ($version != 1.0) {
440                         throw new OAuthException("OAuth version '$version' not supported");
441                 }
442                 return $version;
443         }
444
445         # Snagged from OAuthServer
446         
447         function get_consumer($datastore, $req) {
448                 $consumer_key = @$req->get_parameter("oauth_consumer_key");
449                 if (!$consumer_key) {
450                         throw new OAuthException("Invalid consumer key");
451                 }
452                 
453                 $consumer = $datastore->lookup_consumer($consumer_key);
454                 if (!$consumer) {
455                         throw new OAuthException("Invalid consumer");
456                 }
457                 return $consumer;
458         }
459
460         # Mostly cadged from OAuthServer
461         
462         function get_token($datastore, &$req, $consumer) {/*{{{*/
463                 $token_field = @$req->get_parameter('oauth_token');
464                 $token = $datastore->lookup_token($consumer, 'request', $token_field);
465                 if (!$token) {
466                         throw new OAuthException("Invalid $token_type token: $token_field");
467                 }
468                 return $token;
469         }
470         
471         function check_timestamp(&$req) {
472                 $timestamp = @$req->get_parameter('oauth_timestamp');
473                 $now = time();
474                 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
475                         throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
476                 }
477         }
478
479         # NOTE: don't call twice on the same request; will fail!
480         function check_nonce(&$datastore, &$req, $consumer, $token) {
481                 $timestamp = @$req->get_parameter('oauth_timestamp');
482                 $nonce = @$req->get_parameter('oauth_nonce');
483                 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
484                 if ($found) {
485                         throw new OAuthException("Nonce already used");
486                 }
487                 return true;
488         }
489         
490         function check_signature(&$req, $consumer, $token) {
491                 $signature_method = $this->get_signature_method($req);
492                 $signature = $req->get_parameter('oauth_signature');    
493                 $valid_sig = $signature_method->check_signature($req, 
494                                                                                                                 $consumer, 
495                                                                                                                 $token, 
496                                                                                                                 $signature);
497                 if (!$valid_sig) {
498                         throw new OAuthException("Invalid signature");
499                 }
500         }
501         
502         function get_signature_method(&$req) {
503                 $signature_method = @$req->get_parameter("oauth_signature_method");
504                 if (!$signature_method) {
505                         $signature_method = "PLAINTEXT";
506                 }
507                 if ($signature_method != 'HMAC-SHA1') {
508                         throw new OAuthException("Signature method '$signature_method' not supported.");
509                 }
510                 return omb_hmac_sha1();
511         }
512 }