3 * Laconica - a distributed open-source microblogging tool
4 * Copyright (C) 2008, Controlez-Vous, Inc.
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.
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.
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/>.
20 if (!defined('LACONICA')) { exit(1); }
22 require_once(INSTALLDIR.'/lib/omb.php');
23 define('TIMESTAMP_THRESHOLD', 300);
25 class UserauthorizationAction extends Action
30 function handle($args)
32 parent::handle($args);
34 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
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.'));
43 # We've shown the form, now post user's choice
44 $this->sendAuthorization();
46 if (!common_logged_in()) {
47 # Go log in, and then come back
48 common_set_returnto($_SERVER['REQUEST_URI']);
50 common_redirect(common_local_url('login'));
54 # this must be a new request
55 $req = $this->getNewRequest();
57 $this->clientError(_('No request found!'));
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());
72 function showForm($req, $error=null)
75 $this->error = $error;
81 return _('Authorize subscription');
84 function showPageNotice()
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, '.
92 function showContent()
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');
105 $this->elementStart('div', 'profile');
107 $this->element('img', array('src' => $avatar,
109 'width' => AVATAR_PROFILE_SIZE,
110 'height' => AVATAR_PROFILE_SIZE,
111 'alt' => $nickname));
113 $this->element('a', array('href' => $profile,
114 'class' => 'external profile nickname'),
117 $this->elementStart('div', 'fullname');
119 $this->element('a', array('href' => $homepage),
122 $this->text($fullname);
124 $this->elementEnd('div');
127 $this->element('div', 'location', $location);
130 $this->element('div', 'bio', $bio);
132 $this->elementStart('div', 'license');
133 $this->element('a', array('href' => $license,
134 'class' => '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');
148 function sendAuthorization()
150 $req = $this->getStoredRequest();
153 $this->clientError(_('No authorization request!'));
157 $callback = $req->get_parameter('oauth_callback');
159 if ($this->arg('accept')) {
160 if (!$this->authorizeToken($req)) {
161 $this->clientError(_('Error authorizing token'));
163 if (!$this->saveRemoteProfile($req)) {
164 $this->clientError(_('Error saving remote profile'));
167 $this->showAcceptMessage($req->get_parameter('oauth_token'));
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();
175 common_log_db_error($user, 'SELECT', __FILE__);
176 $this->serverError(_('User without matching profile'));
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;
185 if ($profile->homepage) {
186 $params['omb_listener_homepage'] = $profile->homepage;
189 $params['omb_listener_bio'] = $profile->bio;
191 if ($profile->location) {
192 $params['omb_listener_location'] = $profile->location;
194 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
196 $params['omb_listener_avatar'] = $avatar->url;
199 foreach ($params as $k => $v) {
200 $parts[] = $k . '=' . OAuthUtil::urlencode_rfc3986($v);
202 $query_string = implode('&', $parts);
203 $parsed = parse_url($callback);
204 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
205 common_redirect($url, 303);
209 $this->showRejectMessage();
211 # XXX: not 100% sure how to signal failure... just redirect without token?
212 common_redirect($callback, 303);
217 function authorizeToken(&$req)
219 $consumer_key = $req->get_parameter('oauth_consumer_key');
220 $token_field = $req->get_parameter('oauth_token');
222 $rt->consumer_key = $consumer_key;
223 $rt->tok = $token_field;
226 if ($rt->find(true)) {
227 $orig_rt = clone($rt);
228 $rt->state = 1; # Authorized but not used
229 if ($rt->update($orig_rt)) {
236 # XXX: refactor with similar code in finishremotesubscribe.php
238 function saveRemoteProfile(&$req)
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
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');
252 $listenee = $req->get_parameter('omb_listenee');
253 $remote = Remote_profile::staticGet('uri', $listenee);
257 $profile = Profile::staticGet($remote->id);
258 $orig_remote = clone($remote);
259 $orig_profile = clone($profile);
262 $remote = new Remote_profile();
263 $remote->uri = $listenee;
264 $profile = new Profile();
267 $profile->nickname = $nickname;
268 $profile->profileurl = $profile_url;
271 $profile->fullname = $fullname;
274 $profile->homepage = $homepage;
277 $profile->bio = $bio;
280 $profile->location = $location;
284 $profile->update($orig_profile);
286 $profile->created = DB_DataObject_Cast::dateTime(); # current time
287 $id = $profile->insert();
295 if (!$remote->update($orig_remote)) {
299 $remote->created = DB_DataObject_Cast::dateTime(); # current time
300 if (!$remote->insert()) {
306 if (!$this->addAvatar($profile, $avatar_url)) {
311 $user = common_current_user();
312 $datastore = omb_oauth_datastore();
313 $consumer = $this->getConsumer($datastore, $req);
314 $token = $this->getToken($datastore, $req, $consumer);
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
322 if (!$sub->insert()) {
329 function addAvatar($profile, $url)
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),
338 rename($temp_filename, Avatar::path($filename));
339 return $profile->setOriginal($filename);
342 function showAcceptMessage($tok)
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();
353 function showRejectMessage($tok)
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();
363 function storeRequest($req)
365 common_ensure_session();
366 $_SESSION['userauthorizationrequest'] = $req;
369 function clearRequest()
371 common_ensure_session();
372 unset($_SESSION['userauthorizationrequest']);
375 function getStoredRequest()
377 common_ensure_session();
378 $req = $_SESSION['userauthorizationrequest'];
382 function getNewRequest()
384 common_remove_magic_from_request();
385 $req = OAuthRequest::from_request();
389 # Throws an OAuthException if anything goes wrong
391 function validateRequest(&$req)
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);
406 function validateOmb(&$req)
408 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
409 'omb_listenee_profile', 'omb_listenee_nickname',
410 'omb_listenee_license') as $param)
412 if (!$req->get_parameter($param)) {
413 throw new OAuthException("Required parameter '$param' not found");
417 $version = $req->get_parameter('omb_version');
418 if ($version != OMB_VERSION_01) {
419 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
421 $listener = $req->get_parameter('omb_listener');
422 $user = User::staticGet('uri', $listener);
424 throw new OAuthException("Listener URI '$listener' not found here");
426 $cur = common_current_user();
427 if ($cur->id != $user->id) {
428 throw new OAuthException("Can't add for another user!");
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");
435 if (strlen($listenee) > 255) {
436 throw new OAuthException("Listenee URI '$listenee' too long");
439 $other = User::staticGet('uri', $listenee);
441 throw new OAuthException("Listenee URI '$listenee' is local user");
444 $remote = Remote_profile::staticGet('uri', $listenee);
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!");
453 $nickname = $req->get_parameter('omb_listenee_nickname');
454 if (!Validate::string($nickname, array('min_length' => 1,
456 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
457 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
459 $profile = $req->get_parameter('omb_listenee_profile');
460 if (!common_valid_http_url($profile)) {
461 throw new OAuthException("Invalid profile URL '$profile'.");
464 if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
465 throw new OAuthException("Profile URL '$profile' is for a local user.");
468 $license = $req->get_parameter('omb_listenee_license');
469 if (!common_valid_http_url($license)) {
470 throw new OAuthException("Invalid license URL '$license'.");
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'.");
477 $fullname = $req->get_parameter('omb_listenee_fullname');
478 if ($fullname && mb_strlen($fullname) > 255) {
479 throw new OAuthException("Full name '$fullname' too long.");
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'");
485 $bio = $req->get_parameter('omb_listenee_bio');
486 if ($bio && mb_strlen($bio) > 140) {
487 throw new OAuthException("Bio too long '$bio'");
489 $location = $req->get_parameter('omb_listenee_location');
490 if ($location && mb_strlen($location) > 255) {
491 throw new OAuthException("Location too long '$location'");
493 $avatar = $req->get_parameter('omb_listenee_avatar');
495 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
496 throw new OAuthException("Invalid avatar URL '$avatar'");
498 $size = @getimagesize($avatar);
500 throw new OAuthException("Can't read avatar URL '$avatar'");
502 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
503 throw new OAuthException("Wrong size image at '$avatar'");
505 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
507 throw new OAuthException("Wrong image type for '$avatar'");
510 $callback = $req->get_parameter('oauth_callback');
511 if ($callback && !common_valid_http_url($callback)) {
512 throw new OAuthException("Invalid callback URL '$callback'");
514 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
515 throw new OAuthException("Callback URL '$callback' is for local site.");
519 # Snagged from OAuthServer
521 function checkVersion(&$req)
523 $version = $req->get_parameter("oauth_version");
527 if ($version != 1.0) {
528 throw new OAuthException("OAuth version '$version' not supported");
533 # Snagged from OAuthServer
535 function getConsumer($datastore, $req)
537 $consumer_key = @$req->get_parameter("oauth_consumer_key");
538 if (!$consumer_key) {
539 throw new OAuthException("Invalid consumer key");
542 $consumer = $datastore->lookup_consumer($consumer_key);
544 throw new OAuthException("Invalid consumer");
549 # Mostly cadged from OAuthServer
551 function getToken($datastore, &$req, $consumer)
553 $token_field = @$req->get_parameter('oauth_token');
554 $token = $datastore->lookup_token($consumer, 'request', $token_field);
556 throw new OAuthException("Invalid $token_type token: $token_field");
561 function checkTimestamp(&$req)
563 $timestamp = @$req->get_parameter('oauth_timestamp');
565 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
566 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
570 # NOTE: don't call twice on the same request; will fail!
571 function checkNonce(&$datastore, &$req, $consumer, $token)
573 $timestamp = @$req->get_parameter('oauth_timestamp');
574 $nonce = @$req->get_parameter('oauth_nonce');
575 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
577 throw new OAuthException("Nonce already used");
582 function checkSignature(&$req, $consumer, $token)
584 $signature_method = $this->getSignatureMethod($req);
585 $signature = $req->get_parameter('oauth_signature');
586 $valid_sig = $signature_method->check_signature($req,
591 throw new OAuthException("Invalid signature");
595 function getSignatureMethod(&$req)
597 $signature_method = @$req->get_parameter("oauth_signature_method");
598 if (!$signature_method) {
599 $signature_method = "PLAINTEXT";
601 if ($signature_method != 'HMAC-SHA1') {
602 throw new OAuthException("Signature method '$signature_method' not supported.");
604 return omb_hmac_sha1();