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') {
31 # We've shown the form, now post user's choice
32 $this->send_authorization();
34 if (!common_logged_in()) {
35 # Go log in, and then come back
36 common_debug('saving URL for returnto', __FILE__);
38 unset($argsclone['action']);
39 common_set_returnto(common_local_url('userauthorization', $argsclone));
40 common_debug('redirecting to login', __FILE__);
41 common_redirect(common_local_url('login'));
45 # this must be a new request
46 common_debug('getting new request', __FILE__);
47 $req = $this->get_new_request();
49 $this->client_error(_('No request found!'));
51 common_debug('validating request', __FILE__);
52 # XXX: only validate new requests, since nonce is one-time use
53 $this->validate_request($req);
54 common_debug('showing form', __FILE__);
55 $this->store_request($req);
56 $this->show_form($req);
57 } catch (OAuthException $e) {
58 $this->clear_request();
59 $this->client_error($e->getMessage());
66 function show_form($req) {
68 $nickname = $req->get_parameter('omb_listenee_nickname');
69 $profile = $req->get_parameter('omb_listenee_profile');
70 $license = $req->get_parameter('omb_listenee_license');
71 $fullname = $req->get_parameter('omb_listenee_fullname');
72 $homepage = $req->get_parameter('omb_listenee_homepage');
73 $bio = $req->get_parameter('omb_listenee_bio');
74 $location = $req->get_parameter('omb_listenee_location');
75 $avatar = $req->get_parameter('omb_listenee_avatar');
77 common_show_header(_('Authorize subscription'));
78 common_element('p', NULL, _('Please check these details to make sure '.
79 'that you want to subscribe to this user\'s notices. '.
80 'If you didn\'t just ask to subscribe to someone\'s notices, '.
82 common_element_start('div', 'profile');
84 common_element('img', array('src' => $avatar,
85 'class' => 'avatar profile',
86 'width' => AVATAR_PROFILE_SIZE,
87 'height' => AVATAR_PROFILE_SIZE,
90 common_element('a', array('href' => $profile,
91 'class' => 'external profile nickname'),
94 common_element_start('div', 'fullname');
96 common_element('a', array('href' => $homepage),
99 common_text($fullname);
101 common_element_end('div');
104 common_element('div', 'location', $location);
107 common_element('div', 'bio', $bio);
109 common_element_start('div', 'license');
110 common_element('a', array('href' => $license,
111 'class' => 'license'),
113 common_element_end('div');
114 common_element_end('div');
115 common_element_start('form', array('method' => 'post',
116 'id' => 'userauthorization',
117 'name' => 'userauthorization',
118 'action' => common_local_url('userauthorization')));
119 common_submit('accept', _('Accept'));
120 common_submit('reject', _('Reject'));
121 common_element_end('form');
122 common_show_footer();
125 function send_authorization() {
126 $req = $this->get_stored_request();
129 common_user_error(_('No authorization request!'));
133 $callback = $req->get_parameter('oauth_callback');
135 if ($this->arg('accept')) {
136 if (!$this->authorize_token($req)) {
137 $this->client_error(_('Error authorizing token'));
139 if (!$this->save_remote_profile($req)) {
140 $this->client_error(_('Error saving remote profile'));
143 $this->show_accept_message($req->get_parameter('oauth_token'));
146 $params['oauth_token'] = $req->get_parameter('oauth_token');
147 $params['omb_version'] = OMB_VERSION_01;
148 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
149 $profile = $user->getProfile();
150 $params['omb_listener_nickname'] = $user->nickname;
151 $params['omb_listener_profile'] = common_local_url('showstream',
152 array('nickname' => $user->nickname));
153 if ($profile->fullname) {
154 $params['omb_listener_fullname'] = $profile->fullname;
156 if ($profile->homepage) {
157 $params['omb_listener_homepage'] = $profile->homepage;
160 $params['omb_listener_bio'] = $profile->bio;
162 if ($profile->location) {
163 $params['omb_listener_location'] = $profile->location;
165 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
167 $params['omb_listener_avatar'] = $avatar->url;
170 foreach ($params as $k => $v) {
171 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
173 $query_string = implode('&', $parts);
174 $parsed = parse_url($callback);
175 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
176 common_redirect($url, 303);
180 $this->show_reject_message();
182 # XXX: not 100% sure how to signal failure... just redirect without token?
183 common_redirect($callback, 303);
188 function authorize_token(&$req) {
189 $consumer_key = $req->get_parameter('oauth_consumer_key');
190 $token_field = $req->get_parameter('oauth_token');
191 common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
192 common_debug('token field = "'.$token_field.'"', __FILE__);
194 $rt->consumer_key = $consumer_key;
195 $rt->tok = $token_field;
198 common_debug('request token to look up: "'.print_r($rt,TRUE).'"');
199 if ($rt->find(true)) {
200 common_debug('found request token to authorize', __FILE__);
201 $orig_rt = clone($rt);
202 $rt->state = 1; # Authorized but not used
203 if ($rt->update($orig_rt)) {
204 common_debug('updated request token so it is authorized', __FILE__);
211 # XXX: refactor with similar code in finishremotesubscribe.php
213 function save_remote_profile(&$req) {
214 # FIXME: we should really do this when the consumer comes
215 # back for an access token. If they never do, we've got stuff in a
218 $nickname = $req->get_parameter('omb_listenee_nickname');
219 $fullname = $req->get_parameter('omb_listenee_fullname');
220 $profile_url = $req->get_parameter('omb_listenee_profile');
221 $homepage = $req->get_parameter('omb_listenee_homepage');
222 $bio = $req->get_parameter('omb_listenee_bio');
223 $location = $req->get_parameter('omb_listenee_location');
224 $avatar_url = $req->get_parameter('omb_listenee_avatar');
226 $listenee = $req->get_parameter('omb_listenee');
227 $remote = Remote_profile::staticGet('uri', $listenee);
231 $profile = Profile::staticGet($remote->id);
232 $orig_remote = clone($remote);
233 $orig_profile = clone($profile);
236 $remote = new Remote_profile();
237 $remote->uri = $listenee;
238 $profile = new Profile();
241 $profile->nickname = $nickname;
242 $profile->profileurl = $profile_url;
245 $profile->fullname = $fullname;
248 $profile->homepage = $homepage;
251 $profile->bio = $bio;
254 $profile->location = $location;
258 $profile->update($orig_profile);
260 $profile->created = DB_DataObject_Cast::dateTime(); # current time
261 $id = $profile->insert();
269 if (!$remote->update($orig_remote)) {
273 $remote->created = DB_DataObject_Cast::dateTime(); # current time
274 if (!$remote->insert()) {
280 if (!$this->add_avatar($profile, $avatar_url)) {
285 $user = common_current_user();
286 $datastore = omb_oauth_datastore();
287 $consumer = $this->get_consumer($datastore, $req);
288 $token = $this->get_token($datastore, $req, $consumer);
290 $sub = new Subscription();
291 $sub->subscriber = $user->id;
292 $sub->subscribed = $remote->id;
293 $sub->token = $token->key; # NOTE: request token, not valid for use!
294 $sub->created = DB_DataObject_Cast::dateTime(); # current time
296 if (!$sub->insert()) {
303 function add_avatar($profile, $url) {
304 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
305 copy($url, $temp_filename);
306 return $profile->setOriginal($temp_filename);
309 function show_accept_message($tok) {
310 common_show_header(_('Subscription authorized'));
311 common_element('p', NULL,
312 _('The subscription has been authorized, but no '.
313 'callback URL was passed. Check with the site\'s instructions for '.
314 'details on how to authorize the subscription. Your subscription token is:'));
315 common_element('blockquote', 'token', $tok);
316 common_show_footer();
319 function show_reject_message($tok) {
320 common_show_header(_('Subscription rejected'));
321 common_element('p', NULL,
322 _('The subscription has been rejected, but no '.
323 'callback URL was passed. Check with the site\'s instructions for '.
324 'details on how to fully reject the subscription.'));
325 common_show_footer();
328 function store_request($req) {
329 common_ensure_session();
330 $_SESSION['userauthorizationrequest'] = $req;
333 function clear_request() {
334 common_ensure_session();
335 unset($_SESSION['userauthorizationrequest']);
338 function get_stored_request() {
339 common_ensure_session();
340 $req = $_SESSION['userauthorizationrequest'];
344 function get_new_request() {
345 $req = OAuthRequest::from_request();
349 # Throws an OAuthException if anything goes wrong
351 function validate_request(&$req) {
352 # OAuth stuff -- have to copy from OAuth.php since they're
353 # all private methods, and there's no user-authentication method
354 common_debug('checking version', __FILE__);
355 $this->check_version($req);
356 common_debug('getting datastore', __FILE__);
357 $datastore = omb_oauth_datastore();
358 common_debug('getting consumer', __FILE__);
359 $consumer = $this->get_consumer($datastore, $req);
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);
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);
368 common_debug('validating omb stuff', __FILE__);
369 $this->validate_omb($req);
370 common_debug('done validating', __FILE__);
374 function validate_omb(&$req) {
375 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
376 'omb_listenee_profile', 'omb_listenee_nickname',
377 'omb_listenee_license') as $param)
379 if (!$req->get_parameter($param)) {
380 throw new OAuthException("Required parameter '$param' not found");
384 $version = $req->get_parameter('omb_version');
385 if ($version != OMB_VERSION_01) {
386 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
388 $listener = $req->get_parameter('omb_listener');
389 $user = User::staticGet('uri', $listener);
391 throw new OAuthException("Listener URI '$listener' not found here");
393 $cur = common_current_user();
394 if ($cur->id != $user->id) {
395 throw new OAuthException("Can't add for another user!");
397 $listenee = $req->get_parameter('omb_listenee');
398 if (!Validate::uri($listenee) &&
399 !common_valid_tag($listenee)) {
400 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
402 if (strlen($listenee) > 255) {
403 throw new OAuthException("Listenee URI '$listenee' too long");
405 $remote = Remote_profile::staticGet('uri', $listenee);
407 $sub = new Subscription();
408 $sub->subscriber = $user->id;
409 $sub->subscribed = $remote->id;
410 if ($sub->find(TRUE)) {
411 throw new OAuthException("Already subscribed to user!");
414 $nickname = $req->get_parameter('omb_listenee_nickname');
415 if (!Validate::string($nickname, array('min_length' => 1,
417 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
418 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
420 $profile = $req->get_parameter('omb_listenee_profile');
421 if (!common_valid_http_url($profile)) {
422 throw new OAuthException("Invalid profile URL '$profile'.");
424 $license = $req->get_parameter('omb_listenee_license');
425 if (!common_valid_http_url($license)) {
426 throw new OAuthException("Invalid license URL '$license'.");
429 $fullname = $req->get_parameter('omb_listenee_fullname');
430 if ($fullname && strlen($fullname) > 255) {
431 throw new OAuthException("Full name '$fullname' too long.");
433 $homepage = $req->get_parameter('omb_listenee_homepage');
434 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
435 throw new OAuthException("Invalid homepage '$homepage'");
437 $bio = $req->get_parameter('omb_listenee_bio');
438 if ($bio && strlen($bio) > 140) {
439 throw new OAuthException("Bio too long '$bio'");
441 $location = $req->get_parameter('omb_listenee_location');
442 if ($location && strlen($location) > 255) {
443 throw new OAuthException("Location too long '$location'");
445 $avatar = $req->get_parameter('omb_listenee_avatar');
447 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
448 throw new OAuthException("Invalid avatar URL '$avatar'");
450 $size = @getimagesize($avatar);
452 throw new OAuthException("Can't read avatar URL '$avatar'");
454 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
455 throw new OAuthException("Wrong size image at '$avatar'");
457 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
459 throw new OAuthException("Wrong image type for '$avatar'");
462 $callback = $req->get_parameter('oauth_callback');
463 if ($callback && !common_valid_http_url($callback)) {
464 throw new OAuthException("Invalid callback URL '$callback'");
468 # Snagged from OAuthServer
470 function check_version(&$req) {
471 $version = $req->get_parameter("oauth_version");
475 if ($version != 1.0) {
476 throw new OAuthException("OAuth version '$version' not supported");
481 # Snagged from OAuthServer
483 function get_consumer($datastore, $req) {
484 $consumer_key = @$req->get_parameter("oauth_consumer_key");
485 if (!$consumer_key) {
486 throw new OAuthException("Invalid consumer key");
489 $consumer = $datastore->lookup_consumer($consumer_key);
491 throw new OAuthException("Invalid consumer");
496 # Mostly cadged from OAuthServer
498 function get_token($datastore, &$req, $consumer) {/*{{{*/
499 $token_field = @$req->get_parameter('oauth_token');
500 $token = $datastore->lookup_token($consumer, 'request', $token_field);
502 throw new OAuthException("Invalid $token_type token: $token_field");
507 function check_timestamp(&$req) {
508 $timestamp = @$req->get_parameter('oauth_timestamp');
510 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
511 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
515 # NOTE: don't call twice on the same request; will fail!
516 function check_nonce(&$datastore, &$req, $consumer, $token) {
517 $timestamp = @$req->get_parameter('oauth_timestamp');
518 $nonce = @$req->get_parameter('oauth_nonce');
519 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
521 throw new OAuthException("Nonce already used");
526 function check_signature(&$req, $consumer, $token) {
527 $signature_method = $this->get_signature_method($req);
528 $signature = $req->get_parameter('oauth_signature');
529 $valid_sig = $signature_method->check_signature($req,
534 throw new OAuthException("Invalid signature");
538 function get_signature_method(&$req) {
539 $signature_method = @$req->get_parameter("oauth_signature_method");
540 if (!$signature_method) {
541 $signature_method = "PLAINTEXT";
543 if ($signature_method != 'HMAC-SHA1') {
544 throw new OAuthException("Signature method '$signature_method' not supported.");
546 return omb_hmac_sha1();