]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - actions/userauthorization.php
Merge branch 'master' of evan@dev.controlyourself.ca:/var/www/trunk
[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 "Cancel".'));
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 profile',
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 ($fullname) {
117             $this->elementStart('div', 'fullname');
118             if ($homepage) {
119                 $this->element('a', array('href' => $homepage),
120                                $fullname);
121             } else {
122                 $this->text($fullname);
123             }
124             $this->elementEnd('div');
125         }
126         if ($location) {
127             $this->element('div', 'location', $location);
128         }
129         if ($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 ($profile->fullname) {
183                     $params['omb_listener_fullname'] = $profile->fullname;
184                 }
185                 if ($profile->homepage) {
186                     $params['omb_listener_homepage'] = $profile->homepage;
187                 }
188                 if ($profile->bio) {
189                     $params['omb_listener_bio'] = $profile->bio;
190                 }
191                 if ($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::urlencodeRFC3986($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         $consumer_key = $req->get_parameter('oauth_consumer_key');
220         $token_field = $req->get_parameter('oauth_token');
221         $rt = new Token();
222         $rt->consumer_key = $consumer_key;
223         $rt->tok = $token_field;
224         $rt->type = 0;
225         $rt->state = 0;
226         if ($rt->find(true)) {
227             $orig_rt = clone($rt);
228             $rt->state = 1; # Authorized but not used
229             if ($rt->update($orig_rt)) {
230                 return true;
231             }
232         }
233         return false;
234     }
235
236     # XXX: refactor with similar code in finishremotesubscribe.php
237
238     function saveRemoteProfile(&$req)
239     {
240         # FIXME: we should really do this when the consumer comes
241         # back for an access token. If they never do, we've got stuff in a
242         # weird state.
243
244         $nickname = $req->get_parameter('omb_listenee_nickname');
245         $fullname = $req->get_parameter('omb_listenee_fullname');
246         $profile_url = $req->get_parameter('omb_listenee_profile');
247         $homepage = $req->get_parameter('omb_listenee_homepage');
248         $bio = $req->get_parameter('omb_listenee_bio');
249         $location = $req->get_parameter('omb_listenee_location');
250         $avatar_url = $req->get_parameter('omb_listenee_avatar');
251
252         $listenee = $req->get_parameter('omb_listenee');
253         $remote = Remote_profile::staticGet('uri', $listenee);
254
255         if ($remote) {
256             $exists = true;
257             $profile = Profile::staticGet($remote->id);
258             $orig_remote = clone($remote);
259             $orig_profile = clone($profile);
260         } else {
261             $exists = false;
262             $remote = new Remote_profile();
263             $remote->uri = $listenee;
264             $profile = new Profile();
265         }
266
267         $profile->nickname = $nickname;
268         $profile->profileurl = $profile_url;
269
270         if ($fullname) {
271             $profile->fullname = $fullname;
272         }
273         if ($homepage) {
274             $profile->homepage = $homepage;
275         }
276         if ($bio) {
277             $profile->bio = $bio;
278         }
279         if ($location) {
280             $profile->location = $location;
281         }
282
283         if ($exists) {
284             $profile->update($orig_profile);
285         } else {
286             $profile->created = DB_DataObject_Cast::dateTime(); # current time
287             $id = $profile->insert();
288             if (!$id) {
289                 return false;
290             }
291             $remote->id = $id;
292         }
293
294         if ($exists) {
295             if (!$remote->update($orig_remote)) {
296                 return false;
297             }
298         } else {
299             $remote->created = DB_DataObject_Cast::dateTime(); # current time
300             if (!$remote->insert()) {
301                 return false;
302             }
303         }
304
305         if ($avatar_url) {
306             if (!$this->addAvatar($profile, $avatar_url)) {
307                 return false;
308             }
309         }
310
311         $user = common_current_user();
312         $datastore = omb_oauth_datastore();
313         $consumer = $this->getConsumer($datastore, $req);
314         $token = $this->getToken($datastore, $req, $consumer);
315
316         $sub = new Subscription();
317         $sub->subscriber = $user->id;
318         $sub->subscribed = $remote->id;
319         $sub->token = $token->key; # NOTE: request token, not valid for use!
320         $sub->created = DB_DataObject_Cast::dateTime(); # current time
321
322         if (!$sub->insert()) {
323             return false;
324         }
325
326         return true;
327     }
328
329     function addAvatar($profile, $url)
330     {
331         $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
332         copy($url, $temp_filename);
333         return $profile->setOriginal($temp_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         # OAuth stuff -- have to copy from OAuth.php since they're
388         # all private methods, and there's no user-authentication method
389         $this->checkVersion($req);
390         $datastore = omb_oauth_datastore();
391         $consumer = $this->getConsumer($datastore, $req);
392         $token = $this->getToken($datastore, $req, $consumer);
393         $this->checkTimestamp($req);
394         $this->checkNonce($datastore, $req, $consumer, $token);
395         $this->checkSignature($req, $consumer, $token);
396         $this->validateOmb($req);
397         return true;
398     }
399
400     function validateOmb(&$req)
401     {
402         foreach (array('omb_version', 'omb_listener', 'omb_listenee',
403                        'omb_listenee_profile', 'omb_listenee_nickname',
404                        'omb_listenee_license') as $param)
405         {
406             if (!$req->get_parameter($param)) {
407                 throw new OAuthException("Required parameter '$param' not found");
408             }
409         }
410         # Now, OMB stuff
411         $version = $req->get_parameter('omb_version');
412         if ($version != OMB_VERSION_01) {
413             throw new OAuthException("OpenMicroBlogging version '$version' not supported");
414         }
415         $listener =    $req->get_parameter('omb_listener');
416         $user = User::staticGet('uri', $listener);
417         if (!$user) {
418             throw new OAuthException("Listener URI '$listener' not found here");
419         }
420         $cur = common_current_user();
421         if ($cur->id != $user->id) {
422             throw new OAuthException("Can't add for another user!");
423         }
424         $listenee = $req->get_parameter('omb_listenee');
425         if (!Validate::uri($listenee) &&
426             !common_valid_tag($listenee)) {
427             throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
428         }
429         if (strlen($listenee) > 255) {
430             throw new OAuthException("Listenee URI '$listenee' too long");
431         }
432
433         $other = User::staticGet('uri', $listenee);
434         if ($other) {
435             throw new OAuthException("Listenee URI '$listenee' is local user");
436         }
437
438         $remote = Remote_profile::staticGet('uri', $listenee);
439         if ($remote) {
440             $sub = new Subscription();
441             $sub->subscriber = $user->id;
442             $sub->subscribed = $remote->id;
443             if ($sub->find(true)) {
444                 throw new OAuthException("Already subscribed to user!");
445             }
446         }
447         $nickname = $req->get_parameter('omb_listenee_nickname');
448         if (!Validate::string($nickname, array('min_length' => 1,
449                                                'max_length' => 64,
450                                                'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
451             throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
452         }
453         $profile = $req->get_parameter('omb_listenee_profile');
454         if (!common_valid_http_url($profile)) {
455             throw new OAuthException("Invalid profile URL '$profile'.");
456         }
457
458         if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
459             throw new OAuthException("Profile URL '$profile' is for a local user.");
460         }
461
462         $license = $req->get_parameter('omb_listenee_license');
463         if (!common_valid_http_url($license)) {
464             throw new OAuthException("Invalid license URL '$license'.");
465         }
466         $site_license = common_config('license', 'url');
467         if (!common_compatible_license($license, $site_license)) {
468             throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
469         }
470         # optional stuff
471         $fullname = $req->get_parameter('omb_listenee_fullname');
472         if ($fullname && strlen($fullname) > 255) {
473             throw new OAuthException("Full name '$fullname' too long.");
474         }
475         $homepage = $req->get_parameter('omb_listenee_homepage');
476         if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
477             throw new OAuthException("Invalid homepage '$homepage'");
478         }
479         $bio = $req->get_parameter('omb_listenee_bio');
480         if ($bio && strlen($bio) > 140) {
481             throw new OAuthException("Bio too long '$bio'");
482         }
483         $location = $req->get_parameter('omb_listenee_location');
484         if ($location && strlen($location) > 255) {
485             throw new OAuthException("Location too long '$location'");
486         }
487         $avatar = $req->get_parameter('omb_listenee_avatar');
488         if ($avatar) {
489             if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
490                 throw new OAuthException("Invalid avatar URL '$avatar'");
491             }
492             $size = @getimagesize($avatar);
493             if (!$size) {
494                 throw new OAuthException("Can't read avatar URL '$avatar'");
495             }
496             if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
497                 throw new OAuthException("Wrong size image at '$avatar'");
498             }
499             if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
500                                           IMAGETYPE_PNG))) {
501                 throw new OAuthException("Wrong image type for '$avatar'");
502             }
503         }
504         $callback = $req->get_parameter('oauth_callback');
505         if ($callback && !common_valid_http_url($callback)) {
506             throw new OAuthException("Invalid callback URL '$callback'");
507         }
508         if ($callback && $callback == common_local_url('finishremotesubscribe')) {
509             throw new OAuthException("Callback URL '$callback' is for local site.");
510         }
511     }
512
513     # Snagged from OAuthServer
514
515     function checkVersion(&$req)
516     {
517         $version = $req->get_parameter("oauth_version");
518         if (!$version) {
519             $version = 1.0;
520         }
521         if ($version != 1.0) {
522             throw new OAuthException("OAuth version '$version' not supported");
523         }
524         return $version;
525     }
526
527     # Snagged from OAuthServer
528
529     function getConsumer($datastore, $req)
530     {
531         $consumer_key = @$req->get_parameter("oauth_consumer_key");
532         if (!$consumer_key) {
533             throw new OAuthException("Invalid consumer key");
534         }
535
536         $consumer = $datastore->lookup_consumer($consumer_key);
537         if (!$consumer) {
538             throw new OAuthException("Invalid consumer");
539         }
540         return $consumer;
541     }
542
543     # Mostly cadged from OAuthServer
544
545     function getToken($datastore, &$req, $consumer)
546     {/*{{{*/
547         $token_field = @$req->get_parameter('oauth_token');
548         $token = $datastore->lookup_token($consumer, 'request', $token_field);
549         if (!$token) {
550             throw new OAuthException("Invalid $token_type token: $token_field");
551         }
552         return $token;
553     }
554
555     function checkTimestamp(&$req)
556     {
557         $timestamp = @$req->get_parameter('oauth_timestamp');
558         $now = time();
559         if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
560             throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
561         }
562     }
563
564     # NOTE: don't call twice on the same request; will fail!
565     function checkNonce(&$datastore, &$req, $consumer, $token)
566     {
567         $timestamp = @$req->get_parameter('oauth_timestamp');
568         $nonce = @$req->get_parameter('oauth_nonce');
569         $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
570         if ($found) {
571             throw new OAuthException("Nonce already used");
572         }
573         return true;
574     }
575
576     function checkSignature(&$req, $consumer, $token)
577     {
578         $signature_method = $this->getSignatureMethod($req);
579         $signature = $req->get_parameter('oauth_signature');
580         $valid_sig = $signature_method->checkSignature($req,
581                                                        $consumer,
582                                                        $token,
583                                                        $signature);
584         if (!$valid_sig) {
585             throw new OAuthException("Invalid signature");
586         }
587     }
588
589     function getSignatureMethod(&$req)
590     {
591         $signature_method = @$req->get_parameter("oauth_signature_method");
592         if (!$signature_method) {
593             $signature_method = "PLAINTEXT";
594         }
595         if ($signature_method != 'HMAC-SHA1') {
596             throw new OAuthException("Signature method '$signature_method' not supported.");
597         }
598         return omb_hmac_sha1();
599     }
600 }