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,
108 'class' => 'avatar profile',
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::urlencodeRFC3986($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 return $profile->setOriginal($temp_filename);
336 function showAcceptMessage($tok)
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();
347 function showRejectMessage($tok)
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();
357 function storeRequest($req)
359 common_ensure_session();
360 $_SESSION['userauthorizationrequest'] = $req;
363 function clearRequest()
365 common_ensure_session();
366 unset($_SESSION['userauthorizationrequest']);
369 function getStoredRequest()
371 common_ensure_session();
372 $req = $_SESSION['userauthorizationrequest'];
376 function getNewRequest()
378 common_remove_magic_from_request();
379 $req = OAuthRequest::from_request();
383 # Throws an OAuthException if anything goes wrong
385 function validateRequest(&$req)
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);
400 function validateOmb(&$req)
402 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
403 'omb_listenee_profile', 'omb_listenee_nickname',
404 'omb_listenee_license') as $param)
406 if (!$req->get_parameter($param)) {
407 throw new OAuthException("Required parameter '$param' not found");
411 $version = $req->get_parameter('omb_version');
412 if ($version != OMB_VERSION_01) {
413 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
415 $listener = $req->get_parameter('omb_listener');
416 $user = User::staticGet('uri', $listener);
418 throw new OAuthException("Listener URI '$listener' not found here");
420 $cur = common_current_user();
421 if ($cur->id != $user->id) {
422 throw new OAuthException("Can't add for another user!");
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");
429 if (strlen($listenee) > 255) {
430 throw new OAuthException("Listenee URI '$listenee' too long");
433 $other = User::staticGet('uri', $listenee);
435 throw new OAuthException("Listenee URI '$listenee' is local user");
438 $remote = Remote_profile::staticGet('uri', $listenee);
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!");
447 $nickname = $req->get_parameter('omb_listenee_nickname');
448 if (!Validate::string($nickname, array('min_length' => 1,
450 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
451 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
453 $profile = $req->get_parameter('omb_listenee_profile');
454 if (!common_valid_http_url($profile)) {
455 throw new OAuthException("Invalid profile URL '$profile'.");
458 if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
459 throw new OAuthException("Profile URL '$profile' is for a local user.");
462 $license = $req->get_parameter('omb_listenee_license');
463 if (!common_valid_http_url($license)) {
464 throw new OAuthException("Invalid license URL '$license'.");
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'.");
471 $fullname = $req->get_parameter('omb_listenee_fullname');
472 if ($fullname && mb_strlen($fullname) > 255) {
473 throw new OAuthException("Full name '$fullname' too long.");
475 $homepage = $req->get_parameter('omb_listenee_homepage');
476 if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
477 throw new OAuthException("Invalid homepage '$homepage'");
479 $bio = $req->get_parameter('omb_listenee_bio');
480 if ($bio && mb_strlen($bio) > 140) {
481 throw new OAuthException("Bio too long '$bio'");
483 $location = $req->get_parameter('omb_listenee_location');
484 if ($location && mb_strlen($location) > 255) {
485 throw new OAuthException("Location too long '$location'");
487 $avatar = $req->get_parameter('omb_listenee_avatar');
489 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
490 throw new OAuthException("Invalid avatar URL '$avatar'");
492 $size = @getimagesize($avatar);
494 throw new OAuthException("Can't read avatar URL '$avatar'");
496 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
497 throw new OAuthException("Wrong size image at '$avatar'");
499 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
501 throw new OAuthException("Wrong image type for '$avatar'");
504 $callback = $req->get_parameter('oauth_callback');
505 if ($callback && !common_valid_http_url($callback)) {
506 throw new OAuthException("Invalid callback URL '$callback'");
508 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
509 throw new OAuthException("Callback URL '$callback' is for local site.");
513 # Snagged from OAuthServer
515 function checkVersion(&$req)
517 $version = $req->get_parameter("oauth_version");
521 if ($version != 1.0) {
522 throw new OAuthException("OAuth version '$version' not supported");
527 # Snagged from OAuthServer
529 function getConsumer($datastore, $req)
531 $consumer_key = @$req->get_parameter("oauth_consumer_key");
532 if (!$consumer_key) {
533 throw new OAuthException("Invalid consumer key");
536 $consumer = $datastore->lookup_consumer($consumer_key);
538 throw new OAuthException("Invalid consumer");
543 # Mostly cadged from OAuthServer
545 function getToken($datastore, &$req, $consumer)
547 $token_field = @$req->get_parameter('oauth_token');
548 $token = $datastore->lookup_token($consumer, 'request', $token_field);
550 throw new OAuthException("Invalid $token_type token: $token_field");
555 function checkTimestamp(&$req)
557 $timestamp = @$req->get_parameter('oauth_timestamp');
559 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
560 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
564 # NOTE: don't call twice on the same request; will fail!
565 function checkNonce(&$datastore, &$req, $consumer, $token)
567 $timestamp = @$req->get_parameter('oauth_timestamp');
568 $nonce = @$req->get_parameter('oauth_nonce');
569 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
571 throw new OAuthException("Nonce already used");
576 function checkSignature(&$req, $consumer, $token)
578 $signature_method = $this->getSignatureMethod($req);
579 $signature = $req->get_parameter('oauth_signature');
580 $valid_sig = $signature_method->check_signature($req,
585 throw new OAuthException("Invalid signature");
589 function getSignatureMethod(&$req)
591 $signature_method = @$req->get_parameter("oauth_signature_method");
592 if (!$signature_method) {
593 $signature_method = "PLAINTEXT";
595 if ($signature_method != 'HMAC-SHA1') {
596 throw new OAuthException("Signature method '$signature_method' not supported.");
598 return omb_hmac_sha1();