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