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_submit('accept', _('Accept'));
127 common_submit('reject', _('Reject'));
128 common_element_end('form');
129 common_show_footer();
132 function send_authorization() {
133 $req = $this->get_stored_request();
136 common_user_error(_('No authorization request!'));
140 $callback = $req->get_parameter('oauth_callback');
142 if ($this->arg('accept')) {
143 if (!$this->authorize_token($req)) {
144 $this->client_error(_('Error authorizing token'));
146 if (!$this->save_remote_profile($req)) {
147 $this->client_error(_('Error saving remote profile'));
150 $this->show_accept_message($req->get_parameter('oauth_token'));
153 $params['oauth_token'] = $req->get_parameter('oauth_token');
154 $params['omb_version'] = OMB_VERSION_01;
155 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
156 $profile = $user->getProfile();
157 $params['omb_listener_nickname'] = $user->nickname;
158 $params['omb_listener_profile'] = common_local_url('showstream',
159 array('nickname' => $user->nickname));
160 if ($profile->fullname) {
161 $params['omb_listener_fullname'] = $profile->fullname;
163 if ($profile->homepage) {
164 $params['omb_listener_homepage'] = $profile->homepage;
167 $params['omb_listener_bio'] = $profile->bio;
169 if ($profile->location) {
170 $params['omb_listener_location'] = $profile->location;
172 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
174 $params['omb_listener_avatar'] = $avatar->url;
177 foreach ($params as $k => $v) {
178 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
180 $query_string = implode('&', $parts);
181 $parsed = parse_url($callback);
182 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
183 common_redirect($url, 303);
187 $this->show_reject_message();
189 # XXX: not 100% sure how to signal failure... just redirect without token?
190 common_redirect($callback, 303);
195 function authorize_token(&$req) {
196 $consumer_key = $req->get_parameter('oauth_consumer_key');
197 $token_field = $req->get_parameter('oauth_token');
198 common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
199 common_debug('token field = "'.$token_field.'"', __FILE__);
201 $rt->consumer_key = $consumer_key;
202 $rt->tok = $token_field;
205 common_debug('request token to look up: "'.print_r($rt,TRUE).'"');
206 if ($rt->find(true)) {
207 common_debug('found request token to authorize', __FILE__);
208 $orig_rt = clone($rt);
209 $rt->state = 1; # Authorized but not used
210 if ($rt->update($orig_rt)) {
211 common_debug('updated request token so it is authorized', __FILE__);
218 # XXX: refactor with similar code in finishremotesubscribe.php
220 function save_remote_profile(&$req) {
221 # FIXME: we should really do this when the consumer comes
222 # back for an access token. If they never do, we've got stuff in a
225 $nickname = $req->get_parameter('omb_listenee_nickname');
226 $fullname = $req->get_parameter('omb_listenee_fullname');
227 $profile_url = $req->get_parameter('omb_listenee_profile');
228 $homepage = $req->get_parameter('omb_listenee_homepage');
229 $bio = $req->get_parameter('omb_listenee_bio');
230 $location = $req->get_parameter('omb_listenee_location');
231 $avatar_url = $req->get_parameter('omb_listenee_avatar');
233 $listenee = $req->get_parameter('omb_listenee');
234 $remote = Remote_profile::staticGet('uri', $listenee);
238 $profile = Profile::staticGet($remote->id);
239 $orig_remote = clone($remote);
240 $orig_profile = clone($profile);
243 $remote = new Remote_profile();
244 $remote->uri = $listenee;
245 $profile = new Profile();
248 $profile->nickname = $nickname;
249 $profile->profileurl = $profile_url;
252 $profile->fullname = $fullname;
255 $profile->homepage = $homepage;
258 $profile->bio = $bio;
261 $profile->location = $location;
265 $profile->update($orig_profile);
267 $profile->created = DB_DataObject_Cast::dateTime(); # current time
268 $id = $profile->insert();
276 if (!$remote->update($orig_remote)) {
280 $remote->created = DB_DataObject_Cast::dateTime(); # current time
281 if (!$remote->insert()) {
287 if (!$this->add_avatar($profile, $avatar_url)) {
292 $user = common_current_user();
293 $datastore = omb_oauth_datastore();
294 $consumer = $this->get_consumer($datastore, $req);
295 $token = $this->get_token($datastore, $req, $consumer);
297 $sub = new Subscription();
298 $sub->subscriber = $user->id;
299 $sub->subscribed = $remote->id;
300 $sub->token = $token->key; # NOTE: request token, not valid for use!
301 $sub->created = DB_DataObject_Cast::dateTime(); # current time
303 if (!$sub->insert()) {
310 function add_avatar($profile, $url) {
311 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
312 copy($url, $temp_filename);
313 return $profile->setOriginal($temp_filename);
316 function show_accept_message($tok) {
317 common_show_header(_('Subscription authorized'));
318 common_element('p', NULL,
319 _('The subscription has been authorized, but no '.
320 'callback URL was passed. Check with the site\'s instructions for '.
321 'details on how to authorize the subscription. Your subscription token is:'));
322 common_element('blockquote', 'token', $tok);
323 common_show_footer();
326 function show_reject_message($tok) {
327 common_show_header(_('Subscription rejected'));
328 common_element('p', NULL,
329 _('The subscription has been rejected, but no '.
330 'callback URL was passed. Check with the site\'s instructions for '.
331 'details on how to fully reject the subscription.'));
332 common_show_footer();
335 function store_request($req) {
336 common_ensure_session();
337 $_SESSION['userauthorizationrequest'] = $req;
340 function clear_request() {
341 common_ensure_session();
342 unset($_SESSION['userauthorizationrequest']);
345 function get_stored_request() {
346 common_ensure_session();
347 $req = $_SESSION['userauthorizationrequest'];
351 function get_new_request() {
352 $req = OAuthRequest::from_request();
356 # Throws an OAuthException if anything goes wrong
358 function validate_request(&$req) {
359 # OAuth stuff -- have to copy from OAuth.php since they're
360 # all private methods, and there's no user-authentication method
361 common_debug('checking version', __FILE__);
362 $this->check_version($req);
363 common_debug('getting datastore', __FILE__);
364 $datastore = omb_oauth_datastore();
365 common_debug('getting consumer', __FILE__);
366 $consumer = $this->get_consumer($datastore, $req);
367 common_debug('getting token', __FILE__);
368 $token = $this->get_token($datastore, $req, $consumer);
369 common_debug('checking timestamp', __FILE__);
370 $this->check_timestamp($req);
371 common_debug('checking nonce', __FILE__);
372 $this->check_nonce($datastore, $req, $consumer, $token);
373 common_debug('checking signature', __FILE__);
374 $this->check_signature($req, $consumer, $token);
375 common_debug('validating omb stuff', __FILE__);
376 $this->validate_omb($req);
377 common_debug('done validating', __FILE__);
381 function validate_omb(&$req) {
382 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
383 'omb_listenee_profile', 'omb_listenee_nickname',
384 'omb_listenee_license') as $param)
386 if (!$req->get_parameter($param)) {
387 throw new OAuthException("Required parameter '$param' not found");
391 $version = $req->get_parameter('omb_version');
392 if ($version != OMB_VERSION_01) {
393 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
395 $listener = $req->get_parameter('omb_listener');
396 $user = User::staticGet('uri', $listener);
398 throw new OAuthException("Listener URI '$listener' not found here");
400 $cur = common_current_user();
401 if ($cur->id != $user->id) {
402 throw new OAuthException("Can't add for another user!");
404 $listenee = $req->get_parameter('omb_listenee');
405 if (!Validate::uri($listenee) &&
406 !common_valid_tag($listenee)) {
407 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
409 if (strlen($listenee) > 255) {
410 throw new OAuthException("Listenee URI '$listenee' too long");
412 $remote = Remote_profile::staticGet('uri', $listenee);
414 $sub = new Subscription();
415 $sub->subscriber = $user->id;
416 $sub->subscribed = $remote->id;
417 if ($sub->find(TRUE)) {
418 throw new OAuthException("Already subscribed to user!");
421 $nickname = $req->get_parameter('omb_listenee_nickname');
422 if (!Validate::string($nickname, array('min_length' => 1,
424 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
425 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
427 $profile = $req->get_parameter('omb_listenee_profile');
428 if (!common_valid_http_url($profile)) {
429 throw new OAuthException("Invalid profile URL '$profile'.");
431 $license = $req->get_parameter('omb_listenee_license');
432 if (!common_valid_http_url($license)) {
433 throw new OAuthException("Invalid license URL '$license'.");
436 $fullname = $req->get_parameter('omb_listenee_fullname');
437 if ($fullname && strlen($fullname) > 255) {
438 throw new OAuthException("Full name '$fullname' too long.");
440 $homepage = $req->get_parameter('omb_listenee_homepage');
441 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
442 throw new OAuthException("Invalid homepage '$homepage'");
444 $bio = $req->get_parameter('omb_listenee_bio');
445 if ($bio && strlen($bio) > 140) {
446 throw new OAuthException("Bio too long '$bio'");
448 $location = $req->get_parameter('omb_listenee_location');
449 if ($location && strlen($location) > 255) {
450 throw new OAuthException("Location too long '$location'");
452 $avatar = $req->get_parameter('omb_listenee_avatar');
454 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
455 throw new OAuthException("Invalid avatar URL '$avatar'");
457 $size = @getimagesize($avatar);
459 throw new OAuthException("Can't read avatar URL '$avatar'");
461 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
462 throw new OAuthException("Wrong size image at '$avatar'");
464 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
466 throw new OAuthException("Wrong image type for '$avatar'");
469 $callback = $req->get_parameter('oauth_callback');
470 if ($callback && !common_valid_http_url($callback)) {
471 throw new OAuthException("Invalid callback URL '$callback'");
475 # Snagged from OAuthServer
477 function check_version(&$req) {
478 $version = $req->get_parameter("oauth_version");
482 if ($version != 1.0) {
483 throw new OAuthException("OAuth version '$version' not supported");
488 # Snagged from OAuthServer
490 function get_consumer($datastore, $req) {
491 $consumer_key = @$req->get_parameter("oauth_consumer_key");
492 if (!$consumer_key) {
493 throw new OAuthException("Invalid consumer key");
496 $consumer = $datastore->lookup_consumer($consumer_key);
498 throw new OAuthException("Invalid consumer");
503 # Mostly cadged from OAuthServer
505 function get_token($datastore, &$req, $consumer) {/*{{{*/
506 $token_field = @$req->get_parameter('oauth_token');
507 $token = $datastore->lookup_token($consumer, 'request', $token_field);
509 throw new OAuthException("Invalid $token_type token: $token_field");
514 function check_timestamp(&$req) {
515 $timestamp = @$req->get_parameter('oauth_timestamp');
517 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
518 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
522 # NOTE: don't call twice on the same request; will fail!
523 function check_nonce(&$datastore, &$req, $consumer, $token) {
524 $timestamp = @$req->get_parameter('oauth_timestamp');
525 $nonce = @$req->get_parameter('oauth_nonce');
526 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
528 throw new OAuthException("Nonce already used");
533 function check_signature(&$req, $consumer, $token) {
534 $signature_method = $this->get_signature_method($req);
535 $signature = $req->get_parameter('oauth_signature');
536 $valid_sig = $signature_method->check_signature($req,
541 throw new OAuthException("Invalid signature");
545 function get_signature_method(&$req) {
546 $signature_method = @$req->get_parameter("oauth_signature_method");
547 if (!$signature_method) {
548 $signature_method = "PLAINTEXT";
550 if ($signature_method != 'HMAC-SHA1') {
551 throw new OAuthException("Signature method '$signature_method' not supported.");
553 return omb_hmac_sha1();