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