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