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
28 function handle($args)
30 parent::handle($args);
32 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
34 $token = $this->trimmed('token');
35 if (!$token || $token != common_session_token()) {
36 $req = $this->get_stored_request();
37 $this->show_form(_('There was a problem with your session token. Try again, please.'), $req);
40 # We've shown the form, now post user's choice
41 $this->send_authorization();
43 if (!common_logged_in()) {
44 # Go log in, and then come back
45 common_debug('saving URL for returnto', __FILE__);
46 common_set_returnto($_SERVER['REQUEST_URI']);
48 common_debug('redirecting to login', __FILE__);
49 common_redirect(common_local_url('login'));
53 # this must be a new request
54 common_debug('getting new request', __FILE__);
55 $req = $this->get_new_request();
57 $this->client_error(_('No request found!'));
59 common_debug('validating request', __FILE__);
60 # XXX: only validate new requests, since nonce is one-time use
61 $this->validate_request($req);
62 common_debug('showing form', __FILE__);
63 $this->store_request($req);
64 $this->show_form($req);
65 } catch (OAuthException $e) {
66 $this->clear_request();
67 $this->client_error($e->getMessage());
74 function show_form($req)
77 $nickname = $req->get_parameter('omb_listenee_nickname');
78 $profile = $req->get_parameter('omb_listenee_profile');
79 $license = $req->get_parameter('omb_listenee_license');
80 $fullname = $req->get_parameter('omb_listenee_fullname');
81 $homepage = $req->get_parameter('omb_listenee_homepage');
82 $bio = $req->get_parameter('omb_listenee_bio');
83 $location = $req->get_parameter('omb_listenee_location');
84 $avatar = $req->get_parameter('omb_listenee_avatar');
86 common_show_header(_('Authorize subscription'));
87 common_element('p', null, _('Please check these details to make sure '.
88 'that you want to subscribe to this user\'s notices. '.
89 'If you didn\'t just ask to subscribe to someone\'s notices, '.
91 common_element_start('div', 'profile');
93 common_element('img', array('src' => $avatar,
94 'class' => 'avatar profile',
95 'width' => AVATAR_PROFILE_SIZE,
96 'height' => AVATAR_PROFILE_SIZE,
99 common_element('a', array('href' => $profile,
100 'class' => 'external profile nickname'),
103 common_element_start('div', 'fullname');
105 common_element('a', array('href' => $homepage),
108 common_text($fullname);
110 common_element_end('div');
113 common_element('div', 'location', $location);
116 common_element('div', 'bio', $bio);
118 common_element_start('div', 'license');
119 common_element('a', array('href' => $license,
120 'class' => 'license'),
122 common_element_end('div');
123 common_element_end('div');
124 common_element_start('form', array('method' => 'post',
125 'id' => 'userauthorization',
126 'name' => 'userauthorization',
127 'action' => common_local_url('userauthorization')));
128 common_hidden('token', common_session_token());
129 common_submit('accept', _('Accept'));
130 common_submit('reject', _('Reject'));
131 common_element_end('form');
132 common_show_footer();
135 function send_authorization()
137 $req = $this->get_stored_request();
140 common_user_error(_('No authorization request!'));
144 $callback = $req->get_parameter('oauth_callback');
146 if ($this->arg('accept')) {
147 if (!$this->authorize_token($req)) {
148 $this->client_error(_('Error authorizing token'));
150 if (!$this->save_remote_profile($req)) {
151 $this->client_error(_('Error saving remote profile'));
154 $this->show_accept_message($req->get_parameter('oauth_token'));
157 $params['oauth_token'] = $req->get_parameter('oauth_token');
158 $params['omb_version'] = OMB_VERSION_01;
159 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
160 $profile = $user->getProfile();
162 common_log_db_error($user, 'SELECT', __FILE__);
163 $this->server_error(_('User without matching profile'));
166 $params['omb_listener_nickname'] = $user->nickname;
167 $params['omb_listener_profile'] = common_local_url('showstream',
168 array('nickname' => $user->nickname));
169 if ($profile->fullname) {
170 $params['omb_listener_fullname'] = $profile->fullname;
172 if ($profile->homepage) {
173 $params['omb_listener_homepage'] = $profile->homepage;
176 $params['omb_listener_bio'] = $profile->bio;
178 if ($profile->location) {
179 $params['omb_listener_location'] = $profile->location;
181 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
183 $params['omb_listener_avatar'] = $avatar->url;
186 foreach ($params as $k => $v) {
187 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
189 $query_string = implode('&', $parts);
190 $parsed = parse_url($callback);
191 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
192 common_redirect($url, 303);
196 $this->show_reject_message();
198 # XXX: not 100% sure how to signal failure... just redirect without token?
199 common_redirect($callback, 303);
204 function authorize_token(&$req)
206 $consumer_key = $req->get_parameter('oauth_consumer_key');
207 $token_field = $req->get_parameter('oauth_token');
208 common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
209 common_debug('token field = "'.$token_field.'"', __FILE__);
211 $rt->consumer_key = $consumer_key;
212 $rt->tok = $token_field;
215 common_debug('request token to look up: "'.print_r($rt,true).'"');
216 if ($rt->find(true)) {
217 common_debug('found request token to authorize', __FILE__);
218 $orig_rt = clone($rt);
219 $rt->state = 1; # Authorized but not used
220 if ($rt->update($orig_rt)) {
221 common_debug('updated request token so it is authorized', __FILE__);
228 # XXX: refactor with similar code in finishremotesubscribe.php
230 function save_remote_profile(&$req)
232 # FIXME: we should really do this when the consumer comes
233 # back for an access token. If they never do, we've got stuff in a
236 $nickname = $req->get_parameter('omb_listenee_nickname');
237 $fullname = $req->get_parameter('omb_listenee_fullname');
238 $profile_url = $req->get_parameter('omb_listenee_profile');
239 $homepage = $req->get_parameter('omb_listenee_homepage');
240 $bio = $req->get_parameter('omb_listenee_bio');
241 $location = $req->get_parameter('omb_listenee_location');
242 $avatar_url = $req->get_parameter('omb_listenee_avatar');
244 $listenee = $req->get_parameter('omb_listenee');
245 $remote = Remote_profile::staticGet('uri', $listenee);
249 $profile = Profile::staticGet($remote->id);
250 $orig_remote = clone($remote);
251 $orig_profile = clone($profile);
254 $remote = new Remote_profile();
255 $remote->uri = $listenee;
256 $profile = new Profile();
259 $profile->nickname = $nickname;
260 $profile->profileurl = $profile_url;
263 $profile->fullname = $fullname;
266 $profile->homepage = $homepage;
269 $profile->bio = $bio;
272 $profile->location = $location;
276 $profile->update($orig_profile);
278 $profile->created = DB_DataObject_Cast::dateTime(); # current time
279 $id = $profile->insert();
287 if (!$remote->update($orig_remote)) {
291 $remote->created = DB_DataObject_Cast::dateTime(); # current time
292 if (!$remote->insert()) {
298 if (!$this->add_avatar($profile, $avatar_url)) {
303 $user = common_current_user();
304 $datastore = omb_oauth_datastore();
305 $consumer = $this->get_consumer($datastore, $req);
306 $token = $this->get_token($datastore, $req, $consumer);
308 $sub = new Subscription();
309 $sub->subscriber = $user->id;
310 $sub->subscribed = $remote->id;
311 $sub->token = $token->key; # NOTE: request token, not valid for use!
312 $sub->created = DB_DataObject_Cast::dateTime(); # current time
314 if (!$sub->insert()) {
321 function add_avatar($profile, $url)
323 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
324 copy($url, $temp_filename);
325 return $profile->setOriginal($temp_filename);
328 function show_accept_message($tok)
330 common_show_header(_('Subscription authorized'));
331 common_element('p', null,
332 _('The subscription has been authorized, but no '.
333 'callback URL was passed. Check with the site\'s instructions for '.
334 'details on how to authorize the subscription. Your subscription token is:'));
335 common_element('blockquote', 'token', $tok);
336 common_show_footer();
339 function show_reject_message($tok)
341 common_show_header(_('Subscription rejected'));
342 common_element('p', null,
343 _('The subscription has been rejected, but no '.
344 'callback URL was passed. Check with the site\'s instructions for '.
345 'details on how to fully reject the subscription.'));
346 common_show_footer();
349 function store_request($req)
351 common_ensure_session();
352 $_SESSION['userauthorizationrequest'] = $req;
355 function clear_request()
357 common_ensure_session();
358 unset($_SESSION['userauthorizationrequest']);
361 function get_stored_request()
363 common_ensure_session();
364 $req = $_SESSION['userauthorizationrequest'];
368 function get_new_request()
370 common_remove_magic_from_request();
371 $req = OAuthRequest::from_request();
375 # Throws an OAuthException if anything goes wrong
377 function validate_request(&$req)
379 # OAuth stuff -- have to copy from OAuth.php since they're
380 # all private methods, and there's no user-authentication method
381 common_debug('checking version', __FILE__);
382 $this->check_version($req);
383 common_debug('getting datastore', __FILE__);
384 $datastore = omb_oauth_datastore();
385 common_debug('getting consumer', __FILE__);
386 $consumer = $this->get_consumer($datastore, $req);
387 common_debug('getting token', __FILE__);
388 $token = $this->get_token($datastore, $req, $consumer);
389 common_debug('checking timestamp', __FILE__);
390 $this->check_timestamp($req);
391 common_debug('checking nonce', __FILE__);
392 $this->check_nonce($datastore, $req, $consumer, $token);
393 common_debug('checking signature', __FILE__);
394 $this->check_signature($req, $consumer, $token);
395 common_debug('validating omb stuff', __FILE__);
396 $this->validate_omb($req);
397 common_debug('done validating', __FILE__);
401 function validate_omb(&$req)
403 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
404 'omb_listenee_profile', 'omb_listenee_nickname',
405 'omb_listenee_license') as $param)
407 if (!$req->get_parameter($param)) {
408 throw new OAuthException("Required parameter '$param' not found");
412 $version = $req->get_parameter('omb_version');
413 if ($version != OMB_VERSION_01) {
414 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
416 $listener = $req->get_parameter('omb_listener');
417 $user = User::staticGet('uri', $listener);
419 throw new OAuthException("Listener URI '$listener' not found here");
421 $cur = common_current_user();
422 if ($cur->id != $user->id) {
423 throw new OAuthException("Can't add for another user!");
425 $listenee = $req->get_parameter('omb_listenee');
426 if (!Validate::uri($listenee) &&
427 !common_valid_tag($listenee)) {
428 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
430 if (strlen($listenee) > 255) {
431 throw new OAuthException("Listenee URI '$listenee' too long");
434 $other = User::staticGet('uri', $listenee);
436 throw new OAuthException("Listenee URI '$listenee' is local user");
439 $remote = Remote_profile::staticGet('uri', $listenee);
441 $sub = new Subscription();
442 $sub->subscriber = $user->id;
443 $sub->subscribed = $remote->id;
444 if ($sub->find(true)) {
445 throw new OAuthException("Already subscribed to user!");
448 $nickname = $req->get_parameter('omb_listenee_nickname');
449 if (!Validate::string($nickname, array('min_length' => 1,
451 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
452 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
454 $profile = $req->get_parameter('omb_listenee_profile');
455 if (!common_valid_http_url($profile)) {
456 throw new OAuthException("Invalid profile URL '$profile'.");
459 if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
460 throw new OAuthException("Profile URL '$profile' is for a local user.");
463 $license = $req->get_parameter('omb_listenee_license');
464 if (!common_valid_http_url($license)) {
465 throw new OAuthException("Invalid license URL '$license'.");
467 $site_license = common_config('license', 'url');
468 if (!common_compatible_license($license, $site_license)) {
469 throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
472 $fullname = $req->get_parameter('omb_listenee_fullname');
473 if ($fullname && strlen($fullname) > 255) {
474 throw new OAuthException("Full name '$fullname' too long.");
476 $homepage = $req->get_parameter('omb_listenee_homepage');
477 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
478 throw new OAuthException("Invalid homepage '$homepage'");
480 $bio = $req->get_parameter('omb_listenee_bio');
481 if ($bio && strlen($bio) > 140) {
482 throw new OAuthException("Bio too long '$bio'");
484 $location = $req->get_parameter('omb_listenee_location');
485 if ($location && strlen($location) > 255) {
486 throw new OAuthException("Location too long '$location'");
488 $avatar = $req->get_parameter('omb_listenee_avatar');
490 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
491 throw new OAuthException("Invalid avatar URL '$avatar'");
493 $size = @getimagesize($avatar);
495 throw new OAuthException("Can't read avatar URL '$avatar'");
497 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
498 throw new OAuthException("Wrong size image at '$avatar'");
500 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
502 throw new OAuthException("Wrong image type for '$avatar'");
505 $callback = $req->get_parameter('oauth_callback');
506 if ($callback && !common_valid_http_url($callback)) {
507 throw new OAuthException("Invalid callback URL '$callback'");
509 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
510 throw new OAuthException("Callback URL '$callback' is for local site.");
514 # Snagged from OAuthServer
516 function check_version(&$req)
518 $version = $req->get_parameter("oauth_version");
522 if ($version != 1.0) {
523 throw new OAuthException("OAuth version '$version' not supported");
528 # Snagged from OAuthServer
530 function get_consumer($datastore, $req)
532 $consumer_key = @$req->get_parameter("oauth_consumer_key");
533 if (!$consumer_key) {
534 throw new OAuthException("Invalid consumer key");
537 $consumer = $datastore->lookup_consumer($consumer_key);
539 throw new OAuthException("Invalid consumer");
544 # Mostly cadged from OAuthServer
546 function get_token($datastore, &$req, $consumer)
548 $token_field = @$req->get_parameter('oauth_token');
549 $token = $datastore->lookup_token($consumer, 'request', $token_field);
551 throw new OAuthException("Invalid $token_type token: $token_field");
556 function check_timestamp(&$req)
558 $timestamp = @$req->get_parameter('oauth_timestamp');
560 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
561 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
565 # NOTE: don't call twice on the same request; will fail!
566 function check_nonce(&$datastore, &$req, $consumer, $token)
568 $timestamp = @$req->get_parameter('oauth_timestamp');
569 $nonce = @$req->get_parameter('oauth_nonce');
570 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
572 throw new OAuthException("Nonce already used");
577 function check_signature(&$req, $consumer, $token)
579 $signature_method = $this->get_signature_method($req);
580 $signature = $req->get_parameter('oauth_signature');
581 $valid_sig = $signature_method->check_signature($req,
586 throw new OAuthException("Invalid signature");
590 function get_signature_method(&$req)
592 $signature_method = @$req->get_parameter("oauth_signature_method");
593 if (!$signature_method) {
594 $signature_method = "PLAINTEXT";
596 if ($signature_method != 'HMAC-SHA1') {
597 throw new OAuthException("Signature method '$signature_method' not supported.");
599 return omb_hmac_sha1();