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