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 {
26 function handle($args) {
27 parent::handle($args);
29 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
30 # We've shown the form, now post user's choice
31 $this->send_authorization();
33 if (!common_logged_in()) {
34 # Go log in, and then come back
35 common_debug('userauthorization.php - saving URL for returnto');
37 unset($argsclone['action']);
38 common_set_returnto(common_local_url('userauthorization', $argsclone));
39 common_debug('userauthorization.php - redirecting to login');
40 common_redirect(common_local_url('login'));
44 # this must be a new request
45 common_debug('userauthorization.php - getting new request');
46 $req = $this->get_new_request();
48 common_server_error(_('No request found!'));
50 common_debug('userauthorization.php - validating request');
51 # XXX: only validate new requests, since nonce is one-time use
52 $this->validate_request($req);
53 common_debug('userauthorization.php - showing form');
54 $this->store_request($req);
55 $this->show_form($req);
56 } catch (OAuthException $e) {
57 $this->clear_request();
58 common_server_error($e->getMessage());
65 function show_form($req) {
67 $nickname = $req->get_parameter('omb_listenee_nickname');
68 $profile = $req->get_parameter('omb_listenee_profile');
69 $license = $req->get_parameter('omb_listenee_license');
70 $fullname = $req->get_parameter('omb_listenee_fullname');
71 $homepage = $req->get_parameter('omb_listenee_homepage');
72 $bio = $req->get_parameter('omb_listenee_bio');
73 $location = $req->get_parameter('omb_listenee_location');
74 $avatar = $req->get_parameter('omb_listenee_avatar');
76 common_show_header(_('Authorize subscription'));
77 common_element('p', NULL, _('Please check these details to make sure '.
78 'that you want to subscribe to this user\'s notices. '.
79 'If you didn\'t just ask to subscribe to someone\'s notices, '.
81 common_element_start('div', 'profile');
83 common_element('img', array('src' => $avatar,
84 'class' => 'avatar profile',
85 'width' => AVATAR_PROFILE_SIZE,
86 'height' => AVATAR_PROFILE_SIZE,
89 common_element('a', array('href' => $profile,
90 'class' => 'external profile nickname'),
93 common_element_start('div', 'fullname');
95 common_element('a', array('href' => $homepage),
98 common_text($fullname);
100 common_element_end('div');
103 common_element('div', 'location', $location);
106 common_element('div', 'bio', $bio);
108 common_element_start('div', 'license');
109 common_element('a', array('href' => $license,
110 'class' => 'license'),
112 common_element_end('div');
113 common_element_end('div');
114 common_element_start('form', array('method' => 'post',
115 'id' => 'userauthorization',
116 'name' => 'userauthorization',
117 'action' => common_local_url('userauthorization')));
118 common_submit('accept', _('Accept'));
119 common_submit('reject', _('Reject'));
120 common_element_end('form');
121 common_show_footer();
124 function send_authorization() {
125 $req = $this->get_stored_request();
128 common_user_error(_('No authorization request!'));
132 $callback = $req->get_parameter('oauth_callback');
134 if ($this->arg('accept')) {
135 if (!$this->authorize_token($req)) {
136 common_server_error(_('Error authorizing token'));
138 if (!$this->save_remote_profile($req)) {
139 common_server_error(_('Error saving remote profile'));
142 $this->show_accept_message($req->get_parameter('oauth_token'));
145 $params['oauth_token'] = $req->get_parameter('oauth_token');
146 $params['omb_version'] = OMB_VERSION_01;
147 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
148 $profile = $user->getProfile();
149 $params['omb_listener_nickname'] = $user->nickname;
150 $params['omb_listener_profile'] = common_local_url('showstream',
151 array('nickname' => $user->nickname));
152 if ($profile->fullname) {
153 $params['omb_listener_fullname'] = $profile->fullname;
155 if ($profile->homepage) {
156 $params['omb_listener_homepage'] = $profile->homepage;
159 $params['omb_listener_bio'] = $profile->bio;
161 if ($profile->location) {
162 $params['omb_listener_location'] = $profile->location;
164 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
166 $params['omb_listener_avatar'] = $avatar->url;
169 foreach ($params as $k => $v) {
170 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
172 $query_string = implode('&', $parts);
173 $parsed = parse_url($callback);
174 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
175 common_redirect($url, 303);
179 $this->show_reject_message();
181 # XXX: not 100% sure how to signal failure... just redirect without token?
182 common_redirect($callback, 303);
187 function authorize_token(&$req) {
188 $consumer_key = $req->get_parameter('oauth_consumer_key');
189 $token_field = $req->get_parameter('oauth_token');
190 common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
191 common_debug('token field = "'.$token_field.'"', __FILE__);
193 $rt->consumer_key = $consumer_key;
194 $rt->tok = $token_field;
197 common_debug('request token to look up: "'.print_r($rt,TRUE).'"');
198 if ($rt->find(true)) {
199 common_debug('found request token to authorize', __FILE__);
200 $orig_rt = clone($rt);
201 $rt->state = 1; # Authorized but not used
202 if ($rt->update($orig_rt)) {
203 common_debug('updated request token so it is authorized', __FILE__);
210 # XXX: refactor with similar code in finishremotesubscribe.php
212 function save_remote_profile(&$req) {
213 # FIXME: we should really do this when the consumer comes
214 # back for an access token. If they never do, we've got stuff in a
217 $nickname = $req->get_parameter('omb_listenee_nickname');
218 $fullname = $req->get_parameter('omb_listenee_fullname');
219 $profile_url = $req->get_parameter('omb_listenee_profile');
220 $homepage = $req->get_parameter('omb_listenee_homepage');
221 $bio = $req->get_parameter('omb_listenee_bio');
222 $location = $req->get_parameter('omb_listenee_location');
223 $avatar_url = $req->get_parameter('omb_listenee_avatar');
225 $listenee = $req->get_parameter('omb_listenee');
226 $remote = Remote_profile::staticGet('uri', $listenee);
230 $profile = Profile::staticGet($remote->id);
231 $orig_remote = clone($remote);
232 $orig_profile = clone($profile);
235 $remote = new Remote_profile();
236 $remote->uri = $listenee;
237 $profile = new Profile();
240 $profile->nickname = $nickname;
241 $profile->profileurl = $profile_url;
244 $profile->fullname = $fullname;
247 $profile->homepage = $homepage;
250 $profile->bio = $bio;
253 $profile->location = $location;
257 $profile->update($orig_profile);
259 $profile->created = DB_DataObject_Cast::dateTime(); # current time
260 $id = $profile->insert();
268 if (!$remote->update($orig_remote)) {
272 $remote->created = DB_DataObject_Cast::dateTime(); # current time
273 if (!$remote->insert()) {
279 if (!$this->add_avatar($profile, $avatar_url)) {
284 $user = common_current_user();
285 $datastore = omb_oauth_datastore();
286 $consumer = $this->get_consumer($datastore, $req);
287 $token = $this->get_token($datastore, $req, $consumer);
289 $sub = new Subscription();
290 $sub->subscriber = $user->id;
291 $sub->subscribed = $remote->id;
292 $sub->token = $token->key; # NOTE: request token, not valid for use!
293 $sub->created = DB_DataObject_Cast::dateTime(); # current time
295 if (!$sub->insert()) {
302 function add_avatar($profile, $url) {
303 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
304 copy($url, $temp_filename);
305 return $profile->setOriginal($temp_filename);
308 function show_accept_message($tok) {
309 common_show_header(_('Subscription authorized'));
310 common_element('p', NULL,
311 _('The subscription has been authorized, but no '.
312 'callback URL was passed. Check with the site\'s instructions for '.
313 'details on how to authorize the subscription. Your subscription token is:'));
314 common_element('blockquote', 'token', $tok);
315 common_show_footer();
318 function show_reject_message($tok) {
319 common_show_header(_('Subscription rejected'));
320 common_element('p', NULL,
321 _('The subscription has been rejected, but no '.
322 'callback URL was passed. Check with the site\'s instructions for '.
323 'details on how to fully reject the subscription.'));
324 common_show_footer();
327 function store_request($req) {
328 common_ensure_session();
329 $_SESSION['userauthorizationrequest'] = $req;
332 function clear_request() {
333 common_ensure_session();
334 unset($_SESSION['userauthorizationrequest']);
337 function get_stored_request() {
338 common_ensure_session();
339 $req = $_SESSION['userauthorizationrequest'];
343 function get_new_request() {
344 $req = OAuthRequest::from_request();
348 # Throws an OAuthException if anything goes wrong
350 function validate_request(&$req) {
351 # OAuth stuff -- have to copy from OAuth.php since they're
352 # all private methods, and there's no user-authentication method
353 common_debug('checking version', __FILE__);
354 $this->check_version($req);
355 common_debug('getting datastore', __FILE__);
356 $datastore = omb_oauth_datastore();
357 common_debug('getting consumer', __FILE__);
358 $consumer = $this->get_consumer($datastore, $req);
359 common_debug('getting token', __FILE__);
360 $token = $this->get_token($datastore, $req, $consumer);
361 common_debug('checking timestamp', __FILE__);
362 $this->check_timestamp($req);
363 common_debug('checking nonce', __FILE__);
364 $this->check_nonce($datastore, $req, $consumer, $token);
365 common_debug('checking signature', __FILE__);
366 $this->check_signature($req, $consumer, $token);
367 common_debug('validating omb stuff', __FILE__);
368 $this->validate_omb($req);
369 common_debug('done validating', __FILE__);
373 function validate_omb(&$req) {
374 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
375 'omb_listenee_profile', 'omb_listenee_nickname',
376 'omb_listenee_license') as $param)
378 if (!$req->get_parameter($param)) {
379 throw new OAuthException("Required parameter '$param' not found");
383 $version = $req->get_parameter('omb_version');
384 if ($version != OMB_VERSION_01) {
385 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
387 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
389 throw new OAuthException("Listener URI '$listener' not found here");
391 $cur = common_current_user();
392 if ($cur->id != $user->id) {
393 throw new OAuthException("Can't add for another user!");
395 $listenee = $req->get_parameter('omb_listenee');
396 if (!Validate::uri($listenee) &&
397 !common_valid_tag($listenee)) {
398 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
400 if (strlen($listenee) > 255) {
401 throw new OAuthException("Listenee URI '$listenee' too long");
403 $remote = Remote_profile::staticGet('uri', $listenee);
405 $sub = new Subscription();
406 $sub->subscriber = $user->id;
407 $sub->subscribed = $remote->id;
408 if ($sub->find(TRUE)) {
409 throw new OAuthException("Already subscribed to user!");
412 $nickname = $req->get_parameter('omb_listenee_nickname');
413 if (!Validate::string($nickname, array('min_length' => 1,
415 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
416 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
418 $profile = $req->get_parameter('omb_listenee_profile');
419 if (!common_valid_http_url($profile)) {
420 throw new OAuthException("Invalid profile URL '$profile'.");
422 $license = $req->get_parameter('omb_listenee_license');
423 if (!common_valid_http_url($license)) {
424 throw new OAuthException("Invalid license URL '$license'.");
427 $fullname = $req->get_parameter('omb_listenee_fullname');
428 if ($fullname && strlen($fullname) > 255) {
429 throw new OAuthException("Full name '$fullname' too long.");
431 $homepage = $req->get_parameter('omb_listenee_homepage');
432 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
433 throw new OAuthException("Invalid homepage '$homepage'");
435 $bio = $req->get_parameter('omb_listenee_bio');
436 if ($bio && strlen($bio) > 140) {
437 throw new OAuthException("Bio too long '$bio'");
439 $location = $req->get_parameter('omb_listenee_location');
440 if ($location && strlen($location) > 255) {
441 throw new OAuthException("Location too long '$location'");
443 $avatar = $req->get_parameter('omb_listenee_avatar');
445 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
446 throw new OAuthException("Invalid avatar URL '$avatar'");
448 $size = @getimagesize($avatar);
450 throw new OAuthException("Can't read avatar URL '$avatar'");
452 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
453 throw new OAuthException("Wrong size image at '$avatar'");
455 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
457 throw new OAuthException("Wrong image type for '$avatar'");
460 $callback = $req->get_parameter('oauth_callback');
461 if ($callback && !common_valid_http_url($callback)) {
462 throw new OAuthException("Invalid callback URL '$callback'");
466 # Snagged from OAuthServer
468 function check_version(&$req) {
469 $version = $req->get_parameter("oauth_version");
473 if ($version != 1.0) {
474 throw new OAuthException("OAuth version '$version' not supported");
479 # Snagged from OAuthServer
481 function get_consumer($datastore, $req) {
482 $consumer_key = @$req->get_parameter("oauth_consumer_key");
483 if (!$consumer_key) {
484 throw new OAuthException("Invalid consumer key");
487 $consumer = $datastore->lookup_consumer($consumer_key);
489 throw new OAuthException("Invalid consumer");
494 # Mostly cadged from OAuthServer
496 function get_token($datastore, &$req, $consumer) {/*{{{*/
497 $token_field = @$req->get_parameter('oauth_token');
498 $token = $datastore->lookup_token($consumer, 'request', $token_field);
500 throw new OAuthException("Invalid $token_type token: $token_field");
505 function check_timestamp(&$req) {
506 $timestamp = @$req->get_parameter('oauth_timestamp');
508 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
509 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
513 # NOTE: don't call twice on the same request; will fail!
514 function check_nonce(&$datastore, &$req, $consumer, $token) {
515 $timestamp = @$req->get_parameter('oauth_timestamp');
516 $nonce = @$req->get_parameter('oauth_nonce');
517 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
519 throw new OAuthException("Nonce already used");
524 function check_signature(&$req, $consumer, $token) {
525 $signature_method = $this->get_signature_method($req);
526 $signature = $req->get_parameter('oauth_signature');
527 $valid_sig = $signature_method->check_signature($req,
532 throw new OAuthException("Invalid signature");
536 function get_signature_method(&$req) {
537 $signature_method = @$req->get_parameter("oauth_signature_method");
538 if (!$signature_method) {
539 $signature_method = "PLAINTEXT";
541 if ($signature_method != 'HMAC-SHA1') {
542 throw new OAuthException("Signature method '$signature_method' not supported.");
544 return omb_hmac_sha1();