]> 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 $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 "Reject".'));
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',
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::urlencode_rfc3986($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         $imagefile = new ImageFile($profile->id, $temp_filename);
334         $filename = Avatar::filename($profile->id,
335                                      image_type_to_extension($imagefile->type),
336                                      null,
337                                      common_timestamp());
338         rename($temp_filename, Avatar::path($filename));
339         return $profile->setOriginal($filename);
340     }
341
342     function showAcceptMessage($tok)
343     {
344         common_show_header(_('Subscription authorized'));
345         $this->element('p', null,
346                        _('The subscription has been authorized, but no '.
347                          'callback URL was passed. Check with the site\'s instructions for '.
348                          'details on how to authorize the subscription. Your subscription token is:'));
349         $this->element('blockquote', 'token', $tok);
350         common_show_footer();
351     }
352
353     function showRejectMessage($tok)
354     {
355         common_show_header(_('Subscription rejected'));
356         $this->element('p', null,
357                        _('The subscription has been rejected, but no '.
358                          'callback URL was passed. Check with the site\'s instructions for '.
359                          'details on how to fully reject the subscription.'));
360         common_show_footer();
361     }
362
363     function storeRequest($req)
364     {
365         common_ensure_session();
366         $_SESSION['userauthorizationrequest'] = $req;
367     }
368
369     function clearRequest()
370     {
371         common_ensure_session();
372         unset($_SESSION['userauthorizationrequest']);
373     }
374
375     function getStoredRequest()
376     {
377         common_ensure_session();
378         $req = $_SESSION['userauthorizationrequest'];
379         return $req;
380     }
381
382     function getNewRequest()
383     {
384         common_remove_magic_from_request();
385         $req = OAuthRequest::from_request();
386         return $req;
387     }
388
389     # Throws an OAuthException if anything goes wrong
390
391     function validateRequest(&$req)
392     {
393         # OAuth stuff -- have to copy from OAuth.php since they're
394         # all private methods, and there's no user-authentication method
395         $this->checkVersion($req);
396         $datastore = omb_oauth_datastore();
397         $consumer = $this->getConsumer($datastore, $req);
398         $token = $this->getToken($datastore, $req, $consumer);
399         $this->checkTimestamp($req);
400         $this->checkNonce($datastore, $req, $consumer, $token);
401         $this->checkSignature($req, $consumer, $token);
402         $this->validateOmb($req);
403         return true;
404     }
405
406     function validateOmb(&$req)
407     {
408         foreach (array('omb_version', 'omb_listener', 'omb_listenee',
409                        'omb_listenee_profile', 'omb_listenee_nickname',
410                        'omb_listenee_license') as $param)
411         {
412             if (!$req->get_parameter($param)) {
413                 throw new OAuthException("Required parameter '$param' not found");
414             }
415         }
416         # Now, OMB stuff
417         $version = $req->get_parameter('omb_version');
418         if ($version != OMB_VERSION_01) {
419             throw new OAuthException("OpenMicroBlogging version '$version' not supported");
420         }
421         $listener =    $req->get_parameter('omb_listener');
422         $user = User::staticGet('uri', $listener);
423         if (!$user) {
424             throw new OAuthException("Listener URI '$listener' not found here");
425         }
426         $cur = common_current_user();
427         if ($cur->id != $user->id) {
428             throw new OAuthException("Can't add for another user!");
429         }
430         $listenee = $req->get_parameter('omb_listenee');
431         if (!Validate::uri($listenee) &&
432             !common_valid_tag($listenee)) {
433             throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
434         }
435         if (strlen($listenee) > 255) {
436             throw new OAuthException("Listenee URI '$listenee' too long");
437         }
438
439         $other = User::staticGet('uri', $listenee);
440         if ($other) {
441             throw new OAuthException("Listenee URI '$listenee' is local user");
442         }
443
444         $remote = Remote_profile::staticGet('uri', $listenee);
445         if ($remote) {
446             $sub = new Subscription();
447             $sub->subscriber = $user->id;
448             $sub->subscribed = $remote->id;
449             if ($sub->find(true)) {
450                 throw new OAuthException("Already subscribed to user!");
451             }
452         }
453         $nickname = $req->get_parameter('omb_listenee_nickname');
454         if (!Validate::string($nickname, array('min_length' => 1,
455                                                'max_length' => 64,
456                                                'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
457             throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
458         }
459         $profile = $req->get_parameter('omb_listenee_profile');
460         if (!common_valid_http_url($profile)) {
461             throw new OAuthException("Invalid profile URL '$profile'.");
462         }
463
464         if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
465             throw new OAuthException("Profile URL '$profile' is for a local user.");
466         }
467
468         $license = $req->get_parameter('omb_listenee_license');
469         if (!common_valid_http_url($license)) {
470             throw new OAuthException("Invalid license URL '$license'.");
471         }
472         $site_license = common_config('license', 'url');
473         if (!common_compatible_license($license, $site_license)) {
474             throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
475         }
476         # optional stuff
477         $fullname = $req->get_parameter('omb_listenee_fullname');
478         if ($fullname && mb_strlen($fullname) > 255) {
479             throw new OAuthException("Full name '$fullname' too long.");
480         }
481         $homepage = $req->get_parameter('omb_listenee_homepage');
482         if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
483             throw new OAuthException("Invalid homepage '$homepage'");
484         }
485         $bio = $req->get_parameter('omb_listenee_bio');
486         if ($bio && mb_strlen($bio) > 140) {
487             throw new OAuthException("Bio too long '$bio'");
488         }
489         $location = $req->get_parameter('omb_listenee_location');
490         if ($location && mb_strlen($location) > 255) {
491             throw new OAuthException("Location too long '$location'");
492         }
493         $avatar = $req->get_parameter('omb_listenee_avatar');
494         if ($avatar) {
495             if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
496                 throw new OAuthException("Invalid avatar URL '$avatar'");
497             }
498             $size = @getimagesize($avatar);
499             if (!$size) {
500                 throw new OAuthException("Can't read avatar URL '$avatar'");
501             }
502             if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
503                 throw new OAuthException("Wrong size image at '$avatar'");
504             }
505             if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
506                                           IMAGETYPE_PNG))) {
507                 throw new OAuthException("Wrong image type for '$avatar'");
508             }
509         }
510         $callback = $req->get_parameter('oauth_callback');
511         if ($callback && !common_valid_http_url($callback)) {
512             throw new OAuthException("Invalid callback URL '$callback'");
513         }
514         if ($callback && $callback == common_local_url('finishremotesubscribe')) {
515             throw new OAuthException("Callback URL '$callback' is for local site.");
516         }
517     }
518
519     # Snagged from OAuthServer
520
521     function checkVersion(&$req)
522     {
523         $version = $req->get_parameter("oauth_version");
524         if (!$version) {
525             $version = 1.0;
526         }
527         if ($version != 1.0) {
528             throw new OAuthException("OAuth version '$version' not supported");
529         }
530         return $version;
531     }
532
533     # Snagged from OAuthServer
534
535     function getConsumer($datastore, $req)
536     {
537         $consumer_key = @$req->get_parameter("oauth_consumer_key");
538         if (!$consumer_key) {
539             throw new OAuthException("Invalid consumer key");
540         }
541
542         $consumer = $datastore->lookup_consumer($consumer_key);
543         if (!$consumer) {
544             throw new OAuthException("Invalid consumer");
545         }
546         return $consumer;
547     }
548
549     # Mostly cadged from OAuthServer
550
551     function getToken($datastore, &$req, $consumer)
552     {/*{{{*/
553         $token_field = @$req->get_parameter('oauth_token');
554         $token = $datastore->lookup_token($consumer, 'request', $token_field);
555         if (!$token) {
556             throw new OAuthException("Invalid $token_type token: $token_field");
557         }
558         return $token;
559     }
560
561     function checkTimestamp(&$req)
562     {
563         $timestamp = @$req->get_parameter('oauth_timestamp');
564         $now = time();
565         if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
566             throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
567         }
568     }
569
570     # NOTE: don't call twice on the same request; will fail!
571     function checkNonce(&$datastore, &$req, $consumer, $token)
572     {
573         $timestamp = @$req->get_parameter('oauth_timestamp');
574         $nonce = @$req->get_parameter('oauth_nonce');
575         $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
576         if ($found) {
577             throw new OAuthException("Nonce already used");
578         }
579         return true;
580     }
581
582     function checkSignature(&$req, $consumer, $token)
583     {
584         $signature_method = $this->getSignatureMethod($req);
585         $signature = $req->get_parameter('oauth_signature');
586         $valid_sig = $signature_method->check_signature($req,
587                                                         $consumer,
588                                                         $token,
589                                                         $signature);
590         if (!$valid_sig) {
591             throw new OAuthException("Invalid signature");
592         }
593     }
594
595     function getSignatureMethod(&$req)
596     {
597         $signature_method = @$req->get_parameter("oauth_signature_method");
598         if (!$signature_method) {
599             $signature_method = "PLAINTEXT";
600         }
601         if ($signature_method != 'HMAC-SHA1') {
602             throw new OAuthException("Signature method '$signature_method' not supported.");
603         }
604         return omb_hmac_sha1();
605     }
606 }