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__);
44 common_set_returnto($_SERVER['REQUEST_URI']);
46 common_debug('redirecting to login', __FILE__);
47 common_redirect(common_local_url('login'));
51 # this must be a new request
52 common_debug('getting new request', __FILE__);
53 $req = $this->get_new_request();
55 $this->client_error(_('No request found!'));
57 common_debug('validating request', __FILE__);
58 # XXX: only validate new requests, since nonce is one-time use
59 $this->validate_request($req);
60 common_debug('showing form', __FILE__);
61 $this->store_request($req);
62 $this->show_form($req);
63 } catch (OAuthException $e) {
64 $this->clear_request();
65 $this->client_error($e->getMessage());
72 function show_form($req) {
74 $nickname = $req->get_parameter('omb_listenee_nickname');
75 $profile = $req->get_parameter('omb_listenee_profile');
76 $license = $req->get_parameter('omb_listenee_license');
77 $fullname = $req->get_parameter('omb_listenee_fullname');
78 $homepage = $req->get_parameter('omb_listenee_homepage');
79 $bio = $req->get_parameter('omb_listenee_bio');
80 $location = $req->get_parameter('omb_listenee_location');
81 $avatar = $req->get_parameter('omb_listenee_avatar');
83 common_show_header(_('Authorize subscription'));
84 common_element('p', NULL, _('Please check these details to make sure '.
85 'that you want to subscribe to this user\'s notices. '.
86 'If you didn\'t just ask to subscribe to someone\'s notices, '.
88 common_element_start('div', 'profile');
90 common_element('img', array('src' => $avatar,
91 'class' => 'avatar profile',
92 'width' => AVATAR_PROFILE_SIZE,
93 'height' => AVATAR_PROFILE_SIZE,
96 common_element('a', array('href' => $profile,
97 'class' => 'external profile nickname'),
100 common_element_start('div', 'fullname');
102 common_element('a', array('href' => $homepage),
105 common_text($fullname);
107 common_element_end('div');
110 common_element('div', 'location', $location);
113 common_element('div', 'bio', $bio);
115 common_element_start('div', 'license');
116 common_element('a', array('href' => $license,
117 'class' => 'license'),
119 common_element_end('div');
120 common_element_end('div');
121 common_element_start('form', array('method' => 'post',
122 'id' => 'userauthorization',
123 'name' => 'userauthorization',
124 'action' => common_local_url('userauthorization')));
125 common_hidden('token', common_session_token());
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();
158 common_log_db_error($user, 'SELECT', __FILE__);
159 $this->server_error(_('User without matching profile'));
162 $params['omb_listener_nickname'] = $user->nickname;
163 $params['omb_listener_profile'] = common_local_url('showstream',
164 array('nickname' => $user->nickname));
165 if ($profile->fullname) {
166 $params['omb_listener_fullname'] = $profile->fullname;
168 if ($profile->homepage) {
169 $params['omb_listener_homepage'] = $profile->homepage;
172 $params['omb_listener_bio'] = $profile->bio;
174 if ($profile->location) {
175 $params['omb_listener_location'] = $profile->location;
177 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
179 $params['omb_listener_avatar'] = $avatar->url;
182 foreach ($params as $k => $v) {
183 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
185 $query_string = implode('&', $parts);
186 $parsed = parse_url($callback);
187 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
188 common_redirect($url, 303);
192 $this->show_reject_message();
194 # XXX: not 100% sure how to signal failure... just redirect without token?
195 common_redirect($callback, 303);
200 function authorize_token(&$req) {
201 $consumer_key = $req->get_parameter('oauth_consumer_key');
202 $token_field = $req->get_parameter('oauth_token');
203 common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
204 common_debug('token field = "'.$token_field.'"', __FILE__);
206 $rt->consumer_key = $consumer_key;
207 $rt->tok = $token_field;
210 common_debug('request token to look up: "'.print_r($rt,TRUE).'"');
211 if ($rt->find(true)) {
212 common_debug('found request token to authorize', __FILE__);
213 $orig_rt = clone($rt);
214 $rt->state = 1; # Authorized but not used
215 if ($rt->update($orig_rt)) {
216 common_debug('updated request token so it is authorized', __FILE__);
223 # XXX: refactor with similar code in finishremotesubscribe.php
225 function save_remote_profile(&$req) {
226 # FIXME: we should really do this when the consumer comes
227 # back for an access token. If they never do, we've got stuff in a
230 $nickname = $req->get_parameter('omb_listenee_nickname');
231 $fullname = $req->get_parameter('omb_listenee_fullname');
232 $profile_url = $req->get_parameter('omb_listenee_profile');
233 $homepage = $req->get_parameter('omb_listenee_homepage');
234 $bio = $req->get_parameter('omb_listenee_bio');
235 $location = $req->get_parameter('omb_listenee_location');
236 $avatar_url = $req->get_parameter('omb_listenee_avatar');
238 $listenee = $req->get_parameter('omb_listenee');
239 $remote = Remote_profile::staticGet('uri', $listenee);
243 $profile = Profile::staticGet($remote->id);
244 $orig_remote = clone($remote);
245 $orig_profile = clone($profile);
248 $remote = new Remote_profile();
249 $remote->uri = $listenee;
250 $profile = new Profile();
253 $profile->nickname = $nickname;
254 $profile->profileurl = $profile_url;
257 $profile->fullname = $fullname;
260 $profile->homepage = $homepage;
263 $profile->bio = $bio;
266 $profile->location = $location;
270 $profile->update($orig_profile);
272 $profile->created = DB_DataObject_Cast::dateTime(); # current time
273 $id = $profile->insert();
281 if (!$remote->update($orig_remote)) {
285 $remote->created = DB_DataObject_Cast::dateTime(); # current time
286 if (!$remote->insert()) {
292 if (!$this->add_avatar($profile, $avatar_url)) {
297 $user = common_current_user();
298 $datastore = omb_oauth_datastore();
299 $consumer = $this->get_consumer($datastore, $req);
300 $token = $this->get_token($datastore, $req, $consumer);
302 $sub = new Subscription();
303 $sub->subscriber = $user->id;
304 $sub->subscribed = $remote->id;
305 $sub->token = $token->key; # NOTE: request token, not valid for use!
306 $sub->created = DB_DataObject_Cast::dateTime(); # current time
308 if (!$sub->insert()) {
315 function add_avatar($profile, $url) {
316 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
317 copy($url, $temp_filename);
318 return $profile->setOriginal($temp_filename);
321 function show_accept_message($tok) {
322 common_show_header(_('Subscription authorized'));
323 common_element('p', NULL,
324 _('The subscription has been authorized, but no '.
325 'callback URL was passed. Check with the site\'s instructions for '.
326 'details on how to authorize the subscription. Your subscription token is:'));
327 common_element('blockquote', 'token', $tok);
328 common_show_footer();
331 function show_reject_message($tok) {
332 common_show_header(_('Subscription rejected'));
333 common_element('p', NULL,
334 _('The subscription has been rejected, but no '.
335 'callback URL was passed. Check with the site\'s instructions for '.
336 'details on how to fully reject the subscription.'));
337 common_show_footer();
340 function store_request($req) {
341 common_ensure_session();
342 $_SESSION['userauthorizationrequest'] = $req;
345 function clear_request() {
346 common_ensure_session();
347 unset($_SESSION['userauthorizationrequest']);
350 function get_stored_request() {
351 common_ensure_session();
352 $req = $_SESSION['userauthorizationrequest'];
356 function get_new_request() {
357 common_remove_magic_from_request();
358 $req = OAuthRequest::from_request();
362 # Throws an OAuthException if anything goes wrong
364 function validate_request(&$req) {
365 # OAuth stuff -- have to copy from OAuth.php since they're
366 # all private methods, and there's no user-authentication method
367 common_debug('checking version', __FILE__);
368 $this->check_version($req);
369 common_debug('getting datastore', __FILE__);
370 $datastore = omb_oauth_datastore();
371 common_debug('getting consumer', __FILE__);
372 $consumer = $this->get_consumer($datastore, $req);
373 common_debug('getting token', __FILE__);
374 $token = $this->get_token($datastore, $req, $consumer);
375 common_debug('checking timestamp', __FILE__);
376 $this->check_timestamp($req);
377 common_debug('checking nonce', __FILE__);
378 $this->check_nonce($datastore, $req, $consumer, $token);
379 common_debug('checking signature', __FILE__);
380 $this->check_signature($req, $consumer, $token);
381 common_debug('validating omb stuff', __FILE__);
382 $this->validate_omb($req);
383 common_debug('done validating', __FILE__);
387 function validate_omb(&$req) {
388 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
389 'omb_listenee_profile', 'omb_listenee_nickname',
390 'omb_listenee_license') as $param)
392 if (!$req->get_parameter($param)) {
393 throw new OAuthException("Required parameter '$param' not found");
397 $version = $req->get_parameter('omb_version');
398 if ($version != OMB_VERSION_01) {
399 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
401 $listener = $req->get_parameter('omb_listener');
402 $user = User::staticGet('uri', $listener);
404 throw new OAuthException("Listener URI '$listener' not found here");
406 $cur = common_current_user();
407 if ($cur->id != $user->id) {
408 throw new OAuthException("Can't add for another user!");
410 $listenee = $req->get_parameter('omb_listenee');
411 if (!Validate::uri($listenee) &&
412 !common_valid_tag($listenee)) {
413 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
415 if (strlen($listenee) > 255) {
416 throw new OAuthException("Listenee URI '$listenee' too long");
419 $other = User::staticGet('uri', $listenee);
421 throw new OAuthException("Listenee URI '$listenee' is local user");
424 $remote = Remote_profile::staticGet('uri', $listenee);
426 $sub = new Subscription();
427 $sub->subscriber = $user->id;
428 $sub->subscribed = $remote->id;
429 if ($sub->find(TRUE)) {
430 throw new OAuthException("Already subscribed to user!");
433 $nickname = $req->get_parameter('omb_listenee_nickname');
434 if (!Validate::string($nickname, array('min_length' => 1,
436 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
437 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
439 $profile = $req->get_parameter('omb_listenee_profile');
440 if (!common_valid_http_url($profile)) {
441 throw new OAuthException("Invalid profile URL '$profile'.");
444 if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
445 throw new OAuthException("Profile URL '$profile' is for a local user.");
448 $license = $req->get_parameter('omb_listenee_license');
449 if (!common_valid_http_url($license)) {
450 throw new OAuthException("Invalid license URL '$license'.");
453 $fullname = $req->get_parameter('omb_listenee_fullname');
454 if ($fullname && strlen($fullname) > 255) {
455 throw new OAuthException("Full name '$fullname' too long.");
457 $homepage = $req->get_parameter('omb_listenee_homepage');
458 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
459 throw new OAuthException("Invalid homepage '$homepage'");
461 $bio = $req->get_parameter('omb_listenee_bio');
462 if ($bio && strlen($bio) > 140) {
463 throw new OAuthException("Bio too long '$bio'");
465 $location = $req->get_parameter('omb_listenee_location');
466 if ($location && strlen($location) > 255) {
467 throw new OAuthException("Location too long '$location'");
469 $avatar = $req->get_parameter('omb_listenee_avatar');
471 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
472 throw new OAuthException("Invalid avatar URL '$avatar'");
474 $size = @getimagesize($avatar);
476 throw new OAuthException("Can't read avatar URL '$avatar'");
478 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
479 throw new OAuthException("Wrong size image at '$avatar'");
481 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
483 throw new OAuthException("Wrong image type for '$avatar'");
486 $callback = $req->get_parameter('oauth_callback');
487 if ($callback && !common_valid_http_url($callback)) {
488 throw new OAuthException("Invalid callback URL '$callback'");
490 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
491 throw new OAuthException("Callback URL '$callback' is for local site.");
495 # Snagged from OAuthServer
497 function check_version(&$req) {
498 $version = $req->get_parameter("oauth_version");
502 if ($version != 1.0) {
503 throw new OAuthException("OAuth version '$version' not supported");
508 # Snagged from OAuthServer
510 function get_consumer($datastore, $req) {
511 $consumer_key = @$req->get_parameter("oauth_consumer_key");
512 if (!$consumer_key) {
513 throw new OAuthException("Invalid consumer key");
516 $consumer = $datastore->lookup_consumer($consumer_key);
518 throw new OAuthException("Invalid consumer");
523 # Mostly cadged from OAuthServer
525 function get_token($datastore, &$req, $consumer) {/*{{{*/
526 $token_field = @$req->get_parameter('oauth_token');
527 $token = $datastore->lookup_token($consumer, 'request', $token_field);
529 throw new OAuthException("Invalid $token_type token: $token_field");
534 function check_timestamp(&$req) {
535 $timestamp = @$req->get_parameter('oauth_timestamp');
537 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
538 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
542 # NOTE: don't call twice on the same request; will fail!
543 function check_nonce(&$datastore, &$req, $consumer, $token) {
544 $timestamp = @$req->get_parameter('oauth_timestamp');
545 $nonce = @$req->get_parameter('oauth_nonce');
546 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
548 throw new OAuthException("Nonce already used");
553 function check_signature(&$req, $consumer, $token) {
554 $signature_method = $this->get_signature_method($req);
555 $signature = $req->get_parameter('oauth_signature');
556 $valid_sig = $signature_method->check_signature($req,
561 throw new OAuthException("Invalid signature");
565 function get_signature_method(&$req) {
566 $signature_method = @$req->get_parameter("oauth_signature_method");
567 if (!$signature_method) {
568 $signature_method = "PLAINTEXT";
570 if ($signature_method != 'HMAC-SHA1') {
571 throw new OAuthException("Signature method '$signature_method' not supported.");
573 return omb_hmac_sha1();