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