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) {
28 parent::handle($args);
30 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
32 $token = $this->trimmed('token');
33 if (!$token || $token != common_session_token()) {
34 $req = $this->get_stored_request();
35 $this->show_form(_('There was a problem with your session token. Try again, please.'), $req);
38 # We've shown the form, now post user's choice
39 $this->send_authorization();
41 if (!common_logged_in()) {
42 # Go log in, and then come back
43 common_debug('saving URL for returnto', __FILE__);
45 unset($argsclone['action']);
46 common_set_returnto(common_local_url('userauthorization', $argsclone));
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) {
75 $nickname = $req->get_parameter('omb_listenee_nickname');
76 $profile = $req->get_parameter('omb_listenee_profile');
77 $license = $req->get_parameter('omb_listenee_license');
78 $fullname = $req->get_parameter('omb_listenee_fullname');
79 $homepage = $req->get_parameter('omb_listenee_homepage');
80 $bio = $req->get_parameter('omb_listenee_bio');
81 $location = $req->get_parameter('omb_listenee_location');
82 $avatar = $req->get_parameter('omb_listenee_avatar');
84 common_show_header(_('Authorize subscription'));
85 common_element('p', NULL, _('Please check these details to make sure '.
86 'that you want to subscribe to this user\'s notices. '.
87 'If you didn\'t just ask to subscribe to someone\'s notices, '.
89 common_element_start('div', 'profile');
91 common_element('img', array('src' => $avatar,
92 'class' => 'avatar profile',
93 'width' => AVATAR_PROFILE_SIZE,
94 'height' => AVATAR_PROFILE_SIZE,
97 common_element('a', array('href' => $profile,
98 'class' => 'external profile nickname'),
101 common_element_start('div', 'fullname');
103 common_element('a', array('href' => $homepage),
106 common_text($fullname);
108 common_element_end('div');
111 common_element('div', 'location', $location);
114 common_element('div', 'bio', $bio);
116 common_element_start('div', 'license');
117 common_element('a', array('href' => $license,
118 'class' => 'license'),
120 common_element_end('div');
121 common_element_end('div');
122 common_element_start('form', array('method' => 'post',
123 'id' => 'userauthorization',
124 'name' => 'userauthorization',
125 'action' => common_local_url('userauthorization')));
126 common_hidden('token', common_session_token());
127 common_submit('accept', _('Accept'));
128 common_submit('reject', _('Reject'));
129 common_element_end('form');
130 common_show_footer();
133 function send_authorization() {
134 $req = $this->get_stored_request();
137 common_user_error(_('No authorization request!'));
141 $callback = $req->get_parameter('oauth_callback');
143 if ($this->arg('accept')) {
144 if (!$this->authorize_token($req)) {
145 $this->client_error(_('Error authorizing token'));
147 if (!$this->save_remote_profile($req)) {
148 $this->client_error(_('Error saving remote profile'));
151 $this->show_accept_message($req->get_parameter('oauth_token'));
154 $params['oauth_token'] = $req->get_parameter('oauth_token');
155 $params['omb_version'] = OMB_VERSION_01;
156 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
157 $profile = $user->getProfile();
158 $params['omb_listener_nickname'] = $user->nickname;
159 $params['omb_listener_profile'] = common_local_url('showstream',
160 array('nickname' => $user->nickname));
161 if ($profile->fullname) {
162 $params['omb_listener_fullname'] = $profile->fullname;
164 if ($profile->homepage) {
165 $params['omb_listener_homepage'] = $profile->homepage;
168 $params['omb_listener_bio'] = $profile->bio;
170 if ($profile->location) {
171 $params['omb_listener_location'] = $profile->location;
173 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
175 $params['omb_listener_avatar'] = $avatar->url;
178 foreach ($params as $k => $v) {
179 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
181 $query_string = implode('&', $parts);
182 $parsed = parse_url($callback);
183 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
184 common_redirect($url, 303);
188 $this->show_reject_message();
190 # XXX: not 100% sure how to signal failure... just redirect without token?
191 common_redirect($callback, 303);
196 function authorize_token(&$req) {
197 $consumer_key = $req->get_parameter('oauth_consumer_key');
198 $token_field = $req->get_parameter('oauth_token');
199 common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
200 common_debug('token field = "'.$token_field.'"', __FILE__);
202 $rt->consumer_key = $consumer_key;
203 $rt->tok = $token_field;
206 common_debug('request token to look up: "'.print_r($rt,TRUE).'"');
207 if ($rt->find(true)) {
208 common_debug('found request token to authorize', __FILE__);
209 $orig_rt = clone($rt);
210 $rt->state = 1; # Authorized but not used
211 if ($rt->update($orig_rt)) {
212 common_debug('updated request token so it is authorized', __FILE__);
219 # XXX: refactor with similar code in finishremotesubscribe.php
221 function save_remote_profile(&$req) {
222 # FIXME: we should really do this when the consumer comes
223 # back for an access token. If they never do, we've got stuff in a
226 $nickname = $req->get_parameter('omb_listenee_nickname');
227 $fullname = $req->get_parameter('omb_listenee_fullname');
228 $profile_url = $req->get_parameter('omb_listenee_profile');
229 $homepage = $req->get_parameter('omb_listenee_homepage');
230 $bio = $req->get_parameter('omb_listenee_bio');
231 $location = $req->get_parameter('omb_listenee_location');
232 $avatar_url = $req->get_parameter('omb_listenee_avatar');
234 $listenee = $req->get_parameter('omb_listenee');
235 $remote = Remote_profile::staticGet('uri', $listenee);
239 $profile = Profile::staticGet($remote->id);
240 $orig_remote = clone($remote);
241 $orig_profile = clone($profile);
244 $remote = new Remote_profile();
245 $remote->uri = $listenee;
246 $profile = new Profile();
249 $profile->nickname = $nickname;
250 $profile->profileurl = $profile_url;
253 $profile->fullname = $fullname;
256 $profile->homepage = $homepage;
259 $profile->bio = $bio;
262 $profile->location = $location;
266 $profile->update($orig_profile);
268 $profile->created = DB_DataObject_Cast::dateTime(); # current time
269 $id = $profile->insert();
277 if (!$remote->update($orig_remote)) {
281 $remote->created = DB_DataObject_Cast::dateTime(); # current time
282 if (!$remote->insert()) {
288 if (!$this->add_avatar($profile, $avatar_url)) {
293 $user = common_current_user();
294 $datastore = omb_oauth_datastore();
295 $consumer = $this->get_consumer($datastore, $req);
296 $token = $this->get_token($datastore, $req, $consumer);
298 $sub = new Subscription();
299 $sub->subscriber = $user->id;
300 $sub->subscribed = $remote->id;
301 $sub->token = $token->key; # NOTE: request token, not valid for use!
302 $sub->created = DB_DataObject_Cast::dateTime(); # current time
304 if (!$sub->insert()) {
311 function add_avatar($profile, $url) {
312 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
313 copy($url, $temp_filename);
314 return $profile->setOriginal($temp_filename);
317 function show_accept_message($tok) {
318 common_show_header(_('Subscription authorized'));
319 common_element('p', NULL,
320 _('The subscription has been authorized, but no '.
321 'callback URL was passed. Check with the site\'s instructions for '.
322 'details on how to authorize the subscription. Your subscription token is:'));
323 common_element('blockquote', 'token', $tok);
324 common_show_footer();
327 function show_reject_message($tok) {
328 common_show_header(_('Subscription rejected'));
329 common_element('p', NULL,
330 _('The subscription has been rejected, but no '.
331 'callback URL was passed. Check with the site\'s instructions for '.
332 'details on how to fully reject the subscription.'));
333 common_show_footer();
336 function store_request($req) {
337 common_ensure_session();
338 $_SESSION['userauthorizationrequest'] = $req;
341 function clear_request() {
342 common_ensure_session();
343 unset($_SESSION['userauthorizationrequest']);
346 function get_stored_request() {
347 common_ensure_session();
348 $req = $_SESSION['userauthorizationrequest'];
352 function get_new_request() {
353 common_remove_magic_from_request();
354 $req = OAuthRequest::from_request();
358 # Throws an OAuthException if anything goes wrong
360 function validate_request(&$req) {
361 # OAuth stuff -- have to copy from OAuth.php since they're
362 # all private methods, and there's no user-authentication method
363 common_debug('checking version', __FILE__);
364 $this->check_version($req);
365 common_debug('getting datastore', __FILE__);
366 $datastore = omb_oauth_datastore();
367 common_debug('getting consumer', __FILE__);
368 $consumer = $this->get_consumer($datastore, $req);
369 common_debug('getting token', __FILE__);
370 $token = $this->get_token($datastore, $req, $consumer);
371 common_debug('checking timestamp', __FILE__);
372 $this->check_timestamp($req);
373 common_debug('checking nonce', __FILE__);
374 $this->check_nonce($datastore, $req, $consumer, $token);
375 common_debug('checking signature', __FILE__);
376 $this->check_signature($req, $consumer, $token);
377 common_debug('validating omb stuff', __FILE__);
378 $this->validate_omb($req);
379 common_debug('done validating', __FILE__);
383 function validate_omb(&$req) {
384 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
385 'omb_listenee_profile', 'omb_listenee_nickname',
386 'omb_listenee_license') as $param)
388 if (!$req->get_parameter($param)) {
389 throw new OAuthException("Required parameter '$param' not found");
393 $version = $req->get_parameter('omb_version');
394 if ($version != OMB_VERSION_01) {
395 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
397 $listener = $req->get_parameter('omb_listener');
398 $user = User::staticGet('uri', $listener);
400 throw new OAuthException("Listener URI '$listener' not found here");
402 $cur = common_current_user();
403 if ($cur->id != $user->id) {
404 throw new OAuthException("Can't add for another user!");
406 $listenee = $req->get_parameter('omb_listenee');
407 if (!Validate::uri($listenee) &&
408 !common_valid_tag($listenee)) {
409 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
411 if (strlen($listenee) > 255) {
412 throw new OAuthException("Listenee URI '$listenee' too long");
414 $remote = Remote_profile::staticGet('uri', $listenee);
416 $sub = new Subscription();
417 $sub->subscriber = $user->id;
418 $sub->subscribed = $remote->id;
419 if ($sub->find(TRUE)) {
420 throw new OAuthException("Already subscribed to user!");
423 $nickname = $req->get_parameter('omb_listenee_nickname');
424 if (!Validate::string($nickname, array('min_length' => 1,
426 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
427 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
429 $profile = $req->get_parameter('omb_listenee_profile');
430 if (!common_valid_http_url($profile)) {
431 throw new OAuthException("Invalid profile URL '$profile'.");
433 $license = $req->get_parameter('omb_listenee_license');
434 if (!common_valid_http_url($license)) {
435 throw new OAuthException("Invalid license URL '$license'.");
438 $fullname = $req->get_parameter('omb_listenee_fullname');
439 if ($fullname && strlen($fullname) > 255) {
440 throw new OAuthException("Full name '$fullname' too long.");
442 $homepage = $req->get_parameter('omb_listenee_homepage');
443 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
444 throw new OAuthException("Invalid homepage '$homepage'");
446 $bio = $req->get_parameter('omb_listenee_bio');
447 if ($bio && strlen($bio) > 140) {
448 throw new OAuthException("Bio too long '$bio'");
450 $location = $req->get_parameter('omb_listenee_location');
451 if ($location && strlen($location) > 255) {
452 throw new OAuthException("Location too long '$location'");
454 $avatar = $req->get_parameter('omb_listenee_avatar');
456 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
457 throw new OAuthException("Invalid avatar URL '$avatar'");
459 $size = @getimagesize($avatar);
461 throw new OAuthException("Can't read avatar URL '$avatar'");
463 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
464 throw new OAuthException("Wrong size image at '$avatar'");
466 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
468 throw new OAuthException("Wrong image type for '$avatar'");
471 $callback = $req->get_parameter('oauth_callback');
472 if ($callback && !common_valid_http_url($callback)) {
473 throw new OAuthException("Invalid callback URL '$callback'");
477 # Snagged from OAuthServer
479 function check_version(&$req) {
480 $version = $req->get_parameter("oauth_version");
484 if ($version != 1.0) {
485 throw new OAuthException("OAuth version '$version' not supported");
490 # Snagged from OAuthServer
492 function get_consumer($datastore, $req) {
493 $consumer_key = @$req->get_parameter("oauth_consumer_key");
494 if (!$consumer_key) {
495 throw new OAuthException("Invalid consumer key");
498 $consumer = $datastore->lookup_consumer($consumer_key);
500 throw new OAuthException("Invalid consumer");
505 # Mostly cadged from OAuthServer
507 function get_token($datastore, &$req, $consumer) {/*{{{*/
508 $token_field = @$req->get_parameter('oauth_token');
509 $token = $datastore->lookup_token($consumer, 'request', $token_field);
511 throw new OAuthException("Invalid $token_type token: $token_field");
516 function check_timestamp(&$req) {
517 $timestamp = @$req->get_parameter('oauth_timestamp');
519 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
520 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
524 # NOTE: don't call twice on the same request; will fail!
525 function check_nonce(&$datastore, &$req, $consumer, $token) {
526 $timestamp = @$req->get_parameter('oauth_timestamp');
527 $nonce = @$req->get_parameter('oauth_nonce');
528 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
530 throw new OAuthException("Nonce already used");
535 function check_signature(&$req, $consumer, $token) {
536 $signature_method = $this->get_signature_method($req);
537 $signature = $req->get_parameter('oauth_signature');
538 $valid_sig = $signature_method->check_signature($req,
543 throw new OAuthException("Invalid signature");
547 function get_signature_method(&$req) {
548 $signature_method = @$req->get_parameter("oauth_signature_method");
549 if (!$signature_method) {
550 $signature_method = "PLAINTEXT";
552 if ($signature_method != 'HMAC-SHA1') {
553 throw new OAuthException("Signature method '$signature_method' not supported.");
555 return omb_hmac_sha1();