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 {
27 function handle($args)
29 parent::handle($args);
31 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
33 $token = $this->trimmed('token');
34 if (!$token || $token != common_session_token()) {
35 $req = $this->get_stored_request();
36 $this->show_form(_('There was a problem with your session token. Try again, please.'), $req);
39 # We've shown the form, now post user's choice
40 $this->send_authorization();
42 if (!common_logged_in()) {
43 # Go log in, and then come back
44 common_debug('saving URL for returnto', __FILE__);
45 common_set_returnto($_SERVER['REQUEST_URI']);
47 common_debug('redirecting to login', __FILE__);
48 common_redirect(common_local_url('login'));
52 # this must be a new request
53 common_debug('getting new request', __FILE__);
54 $req = $this->get_new_request();
56 $this->client_error(_('No request found!'));
58 common_debug('validating request', __FILE__);
59 # XXX: only validate new requests, since nonce is one-time use
60 $this->validate_request($req);
61 common_debug('showing form', __FILE__);
62 $this->store_request($req);
63 $this->show_form($req);
64 } catch (OAuthException $e) {
65 $this->clear_request();
66 $this->client_error($e->getMessage());
73 function show_form($req)
76 $nickname = $req->get_parameter('omb_listenee_nickname');
77 $profile = $req->get_parameter('omb_listenee_profile');
78 $license = $req->get_parameter('omb_listenee_license');
79 $fullname = $req->get_parameter('omb_listenee_fullname');
80 $homepage = $req->get_parameter('omb_listenee_homepage');
81 $bio = $req->get_parameter('omb_listenee_bio');
82 $location = $req->get_parameter('omb_listenee_location');
83 $avatar = $req->get_parameter('omb_listenee_avatar');
85 common_show_header(_('Authorize subscription'));
86 common_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, '.
90 common_element_start('div', 'profile');
92 common_element('img', array('src' => $avatar,
93 'class' => 'avatar profile',
94 'width' => AVATAR_PROFILE_SIZE,
95 'height' => AVATAR_PROFILE_SIZE,
98 common_element('a', array('href' => $profile,
99 'class' => 'external profile nickname'),
102 common_element_start('div', 'fullname');
104 common_element('a', array('href' => $homepage),
107 common_text($fullname);
109 common_element_end('div');
112 common_element('div', 'location', $location);
115 common_element('div', 'bio', $bio);
117 common_element_start('div', 'license');
118 common_element('a', array('href' => $license,
119 'class' => 'license'),
121 common_element_end('div');
122 common_element_end('div');
123 common_element_start('form', array('method' => 'post',
124 'id' => 'userauthorization',
125 'name' => 'userauthorization',
126 'action' => common_local_url('userauthorization')));
127 common_hidden('token', common_session_token());
128 common_submit('accept', _('Accept'));
129 common_submit('reject', _('Reject'));
130 common_element_end('form');
131 common_show_footer();
134 function send_authorization()
136 $req = $this->get_stored_request();
139 common_user_error(_('No authorization request!'));
143 $callback = $req->get_parameter('oauth_callback');
145 if ($this->arg('accept')) {
146 if (!$this->authorize_token($req)) {
147 $this->client_error(_('Error authorizing token'));
149 if (!$this->save_remote_profile($req)) {
150 $this->client_error(_('Error saving remote profile'));
153 $this->show_accept_message($req->get_parameter('oauth_token'));
156 $params['oauth_token'] = $req->get_parameter('oauth_token');
157 $params['omb_version'] = OMB_VERSION_01;
158 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
159 $profile = $user->getProfile();
161 common_log_db_error($user, 'SELECT', __FILE__);
162 $this->server_error(_('User without matching profile'));
165 $params['omb_listener_nickname'] = $user->nickname;
166 $params['omb_listener_profile'] = common_local_url('showstream',
167 array('nickname' => $user->nickname));
168 if ($profile->fullname) {
169 $params['omb_listener_fullname'] = $profile->fullname;
171 if ($profile->homepage) {
172 $params['omb_listener_homepage'] = $profile->homepage;
175 $params['omb_listener_bio'] = $profile->bio;
177 if ($profile->location) {
178 $params['omb_listener_location'] = $profile->location;
180 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
182 $params['omb_listener_avatar'] = $avatar->url;
185 foreach ($params as $k => $v) {
186 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
188 $query_string = implode('&', $parts);
189 $parsed = parse_url($callback);
190 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
191 common_redirect($url, 303);
195 $this->show_reject_message();
197 # XXX: not 100% sure how to signal failure... just redirect without token?
198 common_redirect($callback, 303);
203 function authorize_token(&$req)
205 $consumer_key = $req->get_parameter('oauth_consumer_key');
206 $token_field = $req->get_parameter('oauth_token');
207 common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
208 common_debug('token field = "'.$token_field.'"', __FILE__);
210 $rt->consumer_key = $consumer_key;
211 $rt->tok = $token_field;
214 common_debug('request token to look up: "'.print_r($rt,true).'"');
215 if ($rt->find(true)) {
216 common_debug('found request token to authorize', __FILE__);
217 $orig_rt = clone($rt);
218 $rt->state = 1; # Authorized but not used
219 if ($rt->update($orig_rt)) {
220 common_debug('updated request token so it is authorized', __FILE__);
227 # XXX: refactor with similar code in finishremotesubscribe.php
229 function save_remote_profile(&$req)
231 # FIXME: we should really do this when the consumer comes
232 # back for an access token. If they never do, we've got stuff in a
235 $nickname = $req->get_parameter('omb_listenee_nickname');
236 $fullname = $req->get_parameter('omb_listenee_fullname');
237 $profile_url = $req->get_parameter('omb_listenee_profile');
238 $homepage = $req->get_parameter('omb_listenee_homepage');
239 $bio = $req->get_parameter('omb_listenee_bio');
240 $location = $req->get_parameter('omb_listenee_location');
241 $avatar_url = $req->get_parameter('omb_listenee_avatar');
243 $listenee = $req->get_parameter('omb_listenee');
244 $remote = Remote_profile::staticGet('uri', $listenee);
248 $profile = Profile::staticGet($remote->id);
249 $orig_remote = clone($remote);
250 $orig_profile = clone($profile);
253 $remote = new Remote_profile();
254 $remote->uri = $listenee;
255 $profile = new Profile();
258 $profile->nickname = $nickname;
259 $profile->profileurl = $profile_url;
262 $profile->fullname = $fullname;
265 $profile->homepage = $homepage;
268 $profile->bio = $bio;
271 $profile->location = $location;
275 $profile->update($orig_profile);
277 $profile->created = DB_DataObject_Cast::dateTime(); # current time
278 $id = $profile->insert();
286 if (!$remote->update($orig_remote)) {
290 $remote->created = DB_DataObject_Cast::dateTime(); # current time
291 if (!$remote->insert()) {
297 if (!$this->add_avatar($profile, $avatar_url)) {
302 $user = common_current_user();
303 $datastore = omb_oauth_datastore();
304 $consumer = $this->get_consumer($datastore, $req);
305 $token = $this->get_token($datastore, $req, $consumer);
307 $sub = new Subscription();
308 $sub->subscriber = $user->id;
309 $sub->subscribed = $remote->id;
310 $sub->token = $token->key; # NOTE: request token, not valid for use!
311 $sub->created = DB_DataObject_Cast::dateTime(); # current time
313 if (!$sub->insert()) {
320 function add_avatar($profile, $url)
322 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
323 copy($url, $temp_filename);
324 return $profile->setOriginal($temp_filename);
327 function show_accept_message($tok)
329 common_show_header(_('Subscription authorized'));
330 common_element('p', null,
331 _('The subscription has been authorized, but no '.
332 'callback URL was passed. Check with the site\'s instructions for '.
333 'details on how to authorize the subscription. Your subscription token is:'));
334 common_element('blockquote', 'token', $tok);
335 common_show_footer();
338 function show_reject_message($tok)
340 common_show_header(_('Subscription rejected'));
341 common_element('p', null,
342 _('The subscription has been rejected, but no '.
343 'callback URL was passed. Check with the site\'s instructions for '.
344 'details on how to fully reject the subscription.'));
345 common_show_footer();
348 function store_request($req)
350 common_ensure_session();
351 $_SESSION['userauthorizationrequest'] = $req;
354 function clear_request()
356 common_ensure_session();
357 unset($_SESSION['userauthorizationrequest']);
360 function get_stored_request()
362 common_ensure_session();
363 $req = $_SESSION['userauthorizationrequest'];
367 function get_new_request()
369 common_remove_magic_from_request();
370 $req = OAuthRequest::from_request();
374 # Throws an OAuthException if anything goes wrong
376 function validate_request(&$req)
378 # OAuth stuff -- have to copy from OAuth.php since they're
379 # all private methods, and there's no user-authentication method
380 common_debug('checking version', __FILE__);
381 $this->check_version($req);
382 common_debug('getting datastore', __FILE__);
383 $datastore = omb_oauth_datastore();
384 common_debug('getting consumer', __FILE__);
385 $consumer = $this->get_consumer($datastore, $req);
386 common_debug('getting token', __FILE__);
387 $token = $this->get_token($datastore, $req, $consumer);
388 common_debug('checking timestamp', __FILE__);
389 $this->check_timestamp($req);
390 common_debug('checking nonce', __FILE__);
391 $this->check_nonce($datastore, $req, $consumer, $token);
392 common_debug('checking signature', __FILE__);
393 $this->check_signature($req, $consumer, $token);
394 common_debug('validating omb stuff', __FILE__);
395 $this->validate_omb($req);
396 common_debug('done validating', __FILE__);
400 function validate_omb(&$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 && 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) || strlen($homepage) > 255)) {
477 throw new OAuthException("Invalid homepage '$homepage'");
479 $bio = $req->get_parameter('omb_listenee_bio');
480 if ($bio && strlen($bio) > 140) {
481 throw new OAuthException("Bio too long '$bio'");
483 $location = $req->get_parameter('omb_listenee_location');
484 if ($location && 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 check_version(&$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 get_consumer($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 get_token($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 check_timestamp(&$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 check_nonce(&$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 check_signature(&$req, $consumer, $token)
578 $signature_method = $this->get_signature_method($req);
579 $signature = $req->get_parameter('oauth_signature');
580 $valid_sig = $signature_method->check_signature($req,
585 throw new OAuthException("Invalid signature");
589 function get_signature_method(&$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();