userauthorization.php 17.3 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
<?php
/*
 * Laconica - a distributed open-source microblogging tool
 * Copyright (C) 2008, Controlez-Vous, Inc.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

if (!defined('LACONICA')) { exit(1); }

22
require_once(INSTALLDIR.'/lib/omb.php');
23
define('TIMESTAMP_THRESHOLD', 300);
24

25
class UserauthorizationAction extends Action {
26
	
27 28
	function handle($args) {
		parent::handle($args);
Evan Prodromou's avatar
method  
Evan Prodromou committed
29

30
		if ($_SERVER['REQUEST_METHOD'] == 'POST') {
31
			# We've shown the form, now post user's choice
32 33
			$this->send_authorization();
		} else {
Evan Prodromou's avatar
a  
Evan Prodromou committed
34 35 36
			if (!common_logged_in()) {
				# Go log in, and then come back
				common_debug('userauthorization.php - saving URL for returnto');
Evan Prodromou's avatar
Evan Prodromou committed
37
				$argsclone = $_GET;
Evan Prodromou's avatar
Evan Prodromou committed
38 39
				unset($argsclone['action']);
				common_set_returnto(common_local_url('userauthorization', $argsclone));
Evan Prodromou's avatar
method  
Evan Prodromou committed
40
				common_debug('userauthorization.php - redirecting to login');
Evan Prodromou's avatar
a  
Evan Prodromou committed
41 42 43
				common_redirect(common_local_url('login'));
				return;
			}
44
			try {
Evan Prodromou's avatar
a  
Evan Prodromou committed
45 46 47
				# this must be a new request
				common_debug('userauthorization.php - getting new request');
				$req = $this->get_new_request();
48
				if (!$req) {
49
					common_server_error(_('No request found!'));
50
				}
Evan Prodromou's avatar
a  
Evan Prodromou committed
51 52 53 54 55 56
				common_debug('userauthorization.php - validating request');
				# XXX: only validate new requests, since nonce is one-time use
				$this->validate_request($req);
				common_debug('userauthorization.php - showing form');
				$this->store_request($req);
				$this->show_form($req);
57 58 59 60 61
			} catch (OAuthException $e) {
				$this->clear_request();
				common_server_error($e->getMessage());
				return;
			}
Evan Prodromou's avatar
method  
Evan Prodromou committed
62

63 64
		}
	}
65 66 67 68 69 70 71 72 73 74 75

	function show_form($req) {

		$nickname = $req->get_parameter('omb_listenee_nickname');
		$profile = $req->get_parameter('omb_listenee_profile');
		$license = $req->get_parameter('omb_listenee_license');
		$fullname = $req->get_parameter('omb_listenee_fullname');
		$homepage = $req->get_parameter('omb_listenee_homepage');
		$bio = $req->get_parameter('omb_listenee_bio');
		$location = $req->get_parameter('omb_listenee_location');
		$avatar = $req->get_parameter('omb_listenee_avatar');
Evan Prodromou's avatar
method  
Evan Prodromou committed
76

77 78
		common_show_header(_('Authorize subscription'));
		common_element('p', NULL, _('Please check these details to make sure '.
79 80 81
									 'that you want to subscribe to this user\'s notices. '.
									 'If you didn\'t just ask to subscribe to someone\'s notices, '.
									 'click "Cancel".'));
82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
		common_element_start('div', 'profile');
		if ($avatar) {
			common_element('img', array('src' => $avatar,
										'class' => 'avatar profile',
										'width' => AVATAR_PROFILE_SIZE,
										'height' => AVATAR_PROFILE_SIZE,
										'alt' => $nickname));
		}
		common_element('a', array('href' => $profile,
								  'class' => 'external profile nickname'),
					   $nickname);
		if ($fullname) {
			common_element_start('div', 'fullname');
			if ($homepage) {
				common_element('a', array('href' => $homepage),
							   $fullname);
			} else {
				common_text($fullname);
			}
			common_element_end('div');
		}
		if ($location) {
			common_element('div', 'location', $location);
		}
		if ($bio) {
			common_element('div', 'bio', $bio);
		}
		common_element_start('div', 'license');
		common_element('a', array('href' => $license,
								  'class' => 'license'),
					   $license);
		common_element_end('div');
		common_element_end('div');
Evan Prodromou's avatar
method  
Evan Prodromou committed
115
		common_element_start('form', array('method' => 'post',
116 117 118
										   'id' => 'userauthorization',
										   'name' => 'userauthorization',
										   'action' => common_local_url('userauthorization')));
119 120
		common_submit('accept', _('Accept'));
		common_submit('reject', _('Reject'));
121 122 123
		common_element_end('form');
		common_show_footer();
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
124

125 126
	function send_authorization() {
		$req = $this->get_stored_request();
Evan Prodromou's avatar
method  
Evan Prodromou committed
127

128
		if (!$req) {
129
			common_user_error(_('No authorization request!'));
130 131 132 133 134 135
			return;
		}

		$callback = $req->get_parameter('oauth_callback');

		if ($this->arg('accept')) {
136
			if (!$this->authorize_token($req)) {
137
				common_server_error(_('Error authorizing token'));
138 139
			}
			if (!$this->save_remote_profile($req)) {
140
				common_server_error(_('Error saving remote profile'));
141
			}
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
			if (!$callback) {
				$this->show_accept_message($req->get_parameter('oauth_token'));
			} else {
				$params = array();
				$params['oauth_token'] = $req->get_parameter('oauth_token');
				$params['omb_version'] = OMB_VERSION_01;
				$user = User::staticGet('uri', $req->get_parameter('omb_listener'));
				$profile = $user->getProfile();
				$params['omb_listener_nickname'] = $user->nickname;
				$params['omb_listener_profile'] = common_local_url('showstream',
																   array('nickname' => $user->nickname));
				if ($profile->fullname) {
					$params['omb_listener_fullname'] = $profile->fullname;
				}
				if ($profile->homepage) {
					$params['omb_listener_homepage'] = $profile->homepage;
				}
				if ($profile->bio) {
					$params['omb_listener_bio'] = $profile->bio;
				}
				if ($profile->location) {
					$params['omb_listener_location'] = $profile->location;
				}
				$avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
				if ($avatar) {
					$params['omb_listener_avatar'] = $avatar->url;
				}
				$parts = array();
				foreach ($params as $k => $v) {
					$parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
				}
				$query_string = implode('&', $parts);
				$parsed = parse_url($callback);
				$url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
				common_redirect($url, 303);
			}
		} else {
			if (!$callback) {
				$this->show_reject_message();
			} else {
				# XXX: not 100% sure how to signal failure... just redirect without token?
				common_redirect($callback, 303);
			}
		}
	}

	function authorize_token(&$req) {
189 190 191
		$consumer_key = $req->get_parameter('oauth_consumer_key');
		$token_field = $req->get_parameter('oauth_token');
		common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
Evan Prodromou's avatar
method  
Evan Prodromou committed
192
		common_debug('token field = "'.$token_field.'"', __FILE__);
193 194 195
		$rt = new Token();
		$rt->consumer_key = $consumer_key;
		$rt->tok = $token_field;
196 197 198 199 200
		$rt->type = 0;
		$rt->state = 0;
		common_debug('request token to look up: "'.print_r($rt,TRUE).'"');
		if ($rt->find(true)) {
			common_debug('found request token to authorize', __FILE__);
201 202 203
			$orig_rt = clone($rt);
			$rt->state = 1; # Authorized but not used
			if ($rt->update($orig_rt)) {
204
				common_debug('updated request token so it is authorized', __FILE__);
205 206 207 208 209 210 211
				return true;
			}
		}
		return FALSE;
	}

	# XXX: refactor with similar code in finishremotesubscribe.php
Evan Prodromou's avatar
method  
Evan Prodromou committed
212

213 214
	function save_remote_profile(&$req) {
		# FIXME: we should really do this when the consumer comes
Evan Prodromou's avatar
method  
Evan Prodromou committed
215
		# back for an access token. If they never do, we've got stuff in a
216
		# weird state.
Evan Prodromou's avatar
method  
Evan Prodromou committed
217

Evan Prodromou's avatar
a  
Evan Prodromou committed
218
		$nickname = $req->get_parameter('omb_listenee_nickname');
219
		$fullname = $req->get_parameter('omb_listenee_fullname');
Evan Prodromou's avatar
method  
Evan Prodromou committed
220
		$profile_url = $req->get_parameter('omb_listenee_profile');
221 222 223 224
		$homepage = $req->get_parameter('omb_listenee_homepage');
		$bio = $req->get_parameter('omb_listenee_bio');
		$location = $req->get_parameter('omb_listenee_location');
		$avatar_url = $req->get_parameter('omb_listenee_avatar');
Evan Prodromou's avatar
method  
Evan Prodromou committed
225

226 227
		$listenee = $req->get_parameter('omb_listenee');
		$remote = Remote_profile::staticGet('uri', $listenee);
Evan Prodromou's avatar
method  
Evan Prodromou committed
228

229 230 231 232 233 234 235 236
		if ($remote) {
			$exists = true;
			$profile = Profile::staticGet($remote->id);
			$orig_remote = clone($remote);
			$orig_profile = clone($profile);
		} else {
			$exists = false;
			$remote = new Remote_profile();
Evan Prodromou's avatar
a  
Evan Prodromou committed
237
			$remote->uri = $listenee;
238 239 240 241 242
			$profile = new Profile();
		}

		$profile->nickname = $nickname;
		$profile->profileurl = $profile_url;
Evan Prodromou's avatar
method  
Evan Prodromou committed
243

244 245 246 247 248 249 250 251 252 253 254 255
		if ($fullname) {
			$profile->fullname = $fullname;
		}
		if ($homepage) {
			$profile->homepage = $homepage;
		}
		if ($bio) {
			$profile->bio = $bio;
		}
		if ($location) {
			$profile->location = $location;
		}
Evan Prodromou's avatar
method  
Evan Prodromou committed
256

257 258 259 260 261
		if ($exists) {
			$profile->update($orig_profile);
		} else {
			$profile->created = DB_DataObject_Cast::dateTime(); # current time
			$id = $profile->insert();
262 263 264
			if (!$id) {
				return FALSE;
			}
265 266 267 268
			$remote->id = $id;
		}

		if ($exists) {
269 270 271
			if (!$remote->update($orig_remote)) {
				return FALSE;
			}
272 273
		} else {
			$remote->created = DB_DataObject_Cast::dateTime(); # current time
274 275 276
			if (!$remote->insert()) {
				return FALSE;
			}
277 278
		}

Evan Prodromou's avatar
a  
Evan Prodromou committed
279
		if ($avatar_url) {
280 281 282
			if (!$this->add_avatar($profile, $avatar_url)) {
				return FALSE;
			}
Evan Prodromou's avatar
a  
Evan Prodromou committed
283 284
		}

285 286 287 288 289 290 291 292 293 294
		$user = common_current_user();
		$datastore = omb_oauth_datastore();
		$consumer = $this->get_consumer($datastore, $req);
		$token = $this->get_token($datastore, $req, $consumer);

		$sub = new Subscription();
		$sub->subscriber = $user->id;
		$sub->subscribed = $remote->id;
		$sub->token = $token->key; # NOTE: request token, not valid for use!
		$sub->created = DB_DataObject_Cast::dateTime(); # current time
Evan Prodromou's avatar
method  
Evan Prodromou committed
295

296
		if (!$sub->insert()) {
297
			return FALSE;
298
		}
Evan Prodromou's avatar
method  
Evan Prodromou committed
299

300
		return TRUE;
301
	}
Evan Prodromou's avatar
a  
Evan Prodromou committed
302 303

	function add_avatar($profile, $url) {
304
		$temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
Evan Prodromou's avatar
a  
Evan Prodromou committed
305
		copy($url, $temp_filename);
306
		return $profile->setOriginal($temp_filename);
Evan Prodromou's avatar
a  
Evan Prodromou committed
307
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
308

309
	function show_accept_message($tok) {
310
		common_show_header(_('Subscription authorized'));
Evan Prodromou's avatar
method  
Evan Prodromou committed
311
		common_element('p', NULL,
312
					   _('The subscription has been authorized, but no '.
313 314 315 316 317 318 319
						  'callback URL was passed. Check with the site\'s instructions for '.
						  'details on how to authorize the subscription. Your subscription token is:'));
		common_element('blockquote', 'token', $tok);
		common_show_footer();
	}

	function show_reject_message($tok) {
320
		common_show_header(_('Subscription rejected'));
Evan Prodromou's avatar
method  
Evan Prodromou committed
321
		common_element('p', NULL,
322
					   _('The subscription has been rejected, but no '.
323 324 325 326
						  'callback URL was passed. Check with the site\'s instructions for '.
						  'details on how to fully reject the subscription.'));
		common_show_footer();
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
327

328 329 330 331
	function store_request($req) {
		common_ensure_session();
		$_SESSION['userauthorizationrequest'] = $req;
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
332

333
	function clear_request() {
334 335 336
		common_ensure_session();
		unset($_SESSION['userauthorizationrequest']);
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
337

338
	function get_stored_request() {
Evan Prodromou's avatar
method  
Evan Prodromou committed
339
		common_ensure_session();
340 341 342
		$req = $_SESSION['userauthorizationrequest'];
		return $req;
	}
343 344 345

	function get_new_request() {
		$req = OAuthRequest::from_request();
Evan Prodromou's avatar
Evan Prodromou committed
346
		return $req;
347
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
348

349
	# Throws an OAuthException if anything goes wrong
Evan Prodromou's avatar
method  
Evan Prodromou committed
350

Evan Prodromou's avatar
Evan Prodromou committed
351
	function validate_request(&$req) {
352 353
		# OAuth stuff -- have to copy from OAuth.php since they're
		# all private methods, and there's no user-authentication method
354
		common_debug('checking version', __FILE__);
355
		$this->check_version($req);
Evan Prodromou's avatar
method  
Evan Prodromou committed
356
		common_debug('getting datastore', __FILE__);
357
		$datastore = omb_oauth_datastore();
358
		common_debug('getting consumer', __FILE__);
359
		$consumer = $this->get_consumer($datastore, $req);
Evan Prodromou's avatar
method  
Evan Prodromou committed
360
		common_debug('getting token', __FILE__);
361
		$token = $this->get_token($datastore, $req, $consumer);
362
		common_debug('checking timestamp', __FILE__);
363
		$this->check_timestamp($req);
Evan Prodromou's avatar
method  
Evan Prodromou committed
364
		common_debug('checking nonce', __FILE__);
365
		$this->check_nonce($datastore, $req, $consumer, $token);
366
		common_debug('checking signature', __FILE__);
367
		$this->check_signature($req, $consumer, $token);
Evan Prodromou's avatar
method  
Evan Prodromou committed
368
		common_debug('validating omb stuff', __FILE__);
369
		$this->validate_omb($req);
Evan Prodromou's avatar
method  
Evan Prodromou committed
370
		common_debug('done validating', __FILE__);
371 372
		return true;
	}
373

374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
	function validate_omb(&$req) {
		foreach (array('omb_version', 'omb_listener', 'omb_listenee',
					   'omb_listenee_profile', 'omb_listenee_nickname',
					   'omb_listenee_license') as $param)
		{
			if (!$req->get_parameter($param)) {
				throw new OAuthException("Required parameter '$param' not found");
			}
		}
		# Now, OMB stuff
		$version = $req->get_parameter('omb_version');
		if ($version != OMB_VERSION_01) {
			throw new OAuthException("OpenMicroBlogging version '$version' not supported");
		}
		$user = User::staticGet('uri', $req->get_parameter('omb_listener'));
		if (!$user) {
			throw new OAuthException("Listener URI '$listener' not found here");
		}
392 393 394 395
		$cur = common_current_user();
		if ($cur->id != $user->id) {
			throw new OAuthException("Can't add for another user!");
		}
396
		$listenee = $req->get_parameter('omb_listenee');
397 398 399 400
		if (!Validate::uri($listenee) &&
			!common_valid_tag($listenee)) {
			throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
		}
Evan Prodromou's avatar
Evan Prodromou committed
401
		if (strlen($listenee) > 255) {
402 403
			throw new OAuthException("Listenee URI '$listenee' too long");
		}
404 405 406 407 408 409 410 411 412
		$remote = Remote_profile::staticGet('uri', $listenee);
		if ($remote) {
			$sub = new Subscription();
			$sub->subscriber = $user->id;
			$sub->subscribed = $remote->id;
			if ($sub->find(TRUE)) {
				throw new OAuthException("Already subscribed to user!");
			}
		}
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444
		$nickname = $req->get_parameter('omb_listenee_nickname');
		if (!Validate::string($nickname, array('min_length' => 1,
											   'max_length' => 64,
											   'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
			throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
		}
		$profile = $req->get_parameter('omb_listenee_profile');
		if (!common_valid_http_url($profile)) {
			throw new OAuthException("Invalid profile URL '$profile'.");
		}
		$license = $req->get_parameter('omb_listenee_license');
		if (!common_valid_http_url($license)) {
			throw new OAuthException("Invalid license URL '$license'.");
		}
		# optional stuff
		$fullname = $req->get_parameter('omb_listenee_fullname');
		if ($fullname && strlen($fullname) > 255) {
			throw new OAuthException("Full name '$fullname' too long.");
		}
		$homepage = $req->get_parameter('omb_listenee_homepage');
		if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
			throw new OAuthException("Invalid homepage '$homepage'");
		}
		$bio = $req->get_parameter('omb_listenee_bio');
		if ($bio && strlen($bio) > 140) {
			throw new OAuthException("Bio too long '$bio'");
		}
		$location = $req->get_parameter('omb_listenee_location');
		if ($location && strlen($location) > 255) {
			throw new OAuthException("Location too long '$location'");
		}
		$avatar = $req->get_parameter('omb_listenee_avatar');
Evan Prodromou's avatar
a  
Evan Prodromou committed
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459
		if ($avatar) {
			if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
				throw new OAuthException("Invalid avatar URL '$avatar'");
			}
			$size = @getimagesize($avatar);
			if (!$size) {
				throw new OAuthException("Can't read avatar URL '$avatar'");
			}
			if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
				throw new OAuthException("Wrong size image at '$avatar'");
			}
			if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
										  IMAGETYPE_PNG))) {
				throw new OAuthException("Wrong image type for '$avatar'");
			}
460 461
		}
		$callback = $req->get_parameter('oauth_callback');
462
		if ($callback && !common_valid_http_url($callback)) {
463 464
			throw new OAuthException("Invalid callback URL '$callback'");
		}
465
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
466

467
	# Snagged from OAuthServer
Evan Prodromou's avatar
method  
Evan Prodromou committed
468

Evan Prodromou's avatar
Evan Prodromou committed
469
	function check_version(&$req) {
470 471 472 473 474 475 476 477 478 479 480
		$version = $req->get_parameter("oauth_version");
		if (!$version) {
			$version = 1.0;
		}
		if ($version != 1.0) {
			throw new OAuthException("OAuth version '$version' not supported");
		}
		return $version;
	}

	# Snagged from OAuthServer
Evan Prodromou's avatar
method  
Evan Prodromou committed
481

482 483 484 485
	function get_consumer($datastore, $req) {
		$consumer_key = @$req->get_parameter("oauth_consumer_key");
		if (!$consumer_key) {
			throw new OAuthException("Invalid consumer key");
486
		}
Evan Prodromou's avatar
method  
Evan Prodromou committed
487

488 489 490 491 492 493 494 495
		$consumer = $datastore->lookup_consumer($consumer_key);
		if (!$consumer) {
			throw new OAuthException("Invalid consumer");
		}
		return $consumer;
	}

	# Mostly cadged from OAuthServer
Evan Prodromou's avatar
method  
Evan Prodromou committed
496

497
	function get_token($datastore, &$req, $consumer) {/*{{{*/
498 499 500 501 502 503 504
		$token_field = @$req->get_parameter('oauth_token');
		$token = $datastore->lookup_token($consumer, 'request', $token_field);
		if (!$token) {
			throw new OAuthException("Invalid $token_type token: $token_field");
		}
		return $token;
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
505

506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523
	function check_timestamp(&$req) {
		$timestamp = @$req->get_parameter('oauth_timestamp');
		$now = time();
		if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
			throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
		}
	}

	# NOTE: don't call twice on the same request; will fail!
	function check_nonce(&$datastore, &$req, $consumer, $token) {
		$timestamp = @$req->get_parameter('oauth_timestamp');
		$nonce = @$req->get_parameter('oauth_nonce');
		$found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
		if ($found) {
			throw new OAuthException("Nonce already used");
		}
		return true;
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
524

525 526
	function check_signature(&$req, $consumer, $token) {
		$signature_method = $this->get_signature_method($req);
Evan Prodromou's avatar
method  
Evan Prodromou committed
527 528 529 530
		$signature = $req->get_parameter('oauth_signature');
		$valid_sig = $signature_method->check_signature($req,
														$consumer,
														$token,
531 532 533 534 535
														$signature);
		if (!$valid_sig) {
			throw new OAuthException("Invalid signature");
		}
	}
Evan Prodromou's avatar
method  
Evan Prodromou committed
536

537 538 539 540 541 542 543
	function get_signature_method(&$req) {
		$signature_method = @$req->get_parameter("oauth_signature_method");
		if (!$signature_method) {
			$signature_method = "PLAINTEXT";
		}
		if ($signature_method != 'HMAC-SHA1') {
			throw new OAuthException("Signature method '$signature_method' not supported.");
544
		}
545
		return omb_hmac_sha1();
546 547
	}
}