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