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(_t('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(_t('Authorize subscription'));
77 common_element('p', _t('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', _t('Accept'));
119 common_submit('reject', _t('Reject'));
120 common_element_end('form');
121 common_show_footer();
124 function send_authorization() {
125 $req = $this->get_stored_request();
128 common_user_error(_t('No authorization request!'));
132 $callback = $req->get_parameter('oauth_callback');
134 if ($this->arg('accept')) {
135 $this->authorize_token($req);
136 $this->save_remote_profile($req);
138 $this->show_accept_message($req->get_parameter('oauth_token'));
141 $params['oauth_token'] = $req->get_parameter('oauth_token');
142 $params['omb_version'] = OMB_VERSION_01;
143 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
144 $profile = $user->getProfile();
145 $params['omb_listener_nickname'] = $user->nickname;
146 $params['omb_listener_profile'] = common_local_url('showstream',
147 array('nickname' => $user->nickname));
148 if ($profile->fullname) {
149 $params['omb_listener_fullname'] = $profile->fullname;
151 if ($profile->homepage) {
152 $params['omb_listener_homepage'] = $profile->homepage;
155 $params['omb_listener_bio'] = $profile->bio;
157 if ($profile->location) {
158 $params['omb_listener_location'] = $profile->location;
160 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
162 $params['omb_listener_avatar'] = $avatar->url;
165 foreach ($params as $k => $v) {
166 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
168 $query_string = implode('&', $parts);
169 $parsed = parse_url($callback);
170 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
171 common_redirect($url, 303);
175 $this->show_reject_message();
177 # XXX: not 100% sure how to signal failure... just redirect without token?
178 common_redirect($callback, 303);
183 function authorize_token(&$req) {
184 $consumer_key = @$req->get_parameter('oauth_consumer_key');
185 $token_field = @$req->get_parameter('oauth_token');
187 $rt->consumer_key = $consumer_key;
188 $rt->tok = $token_field;
189 if ($rt->find(TRUE)) {
190 $orig_rt = clone($rt);
191 $rt->state = 1; # Authorized but not used
192 if ($rt->update($orig_rt)) {
199 # XXX: refactor with similar code in finishremotesubscribe.php
201 function save_remote_profile(&$req) {
202 # FIXME: we should really do this when the consumer comes
203 # back for an access token. If they never do, we've got stuff in a
206 $nickname = $req->get_parameter('omb_listenee_nickname');
207 $fullname = $req->get_parameter('omb_listenee_fullname');
208 $profile_url = $req->get_parameter('omb_listenee_profile');
209 $homepage = $req->get_parameter('omb_listenee_homepage');
210 $bio = $req->get_parameter('omb_listenee_bio');
211 $location = $req->get_parameter('omb_listenee_location');
212 $avatar_url = $req->get_parameter('omb_listenee_avatar');
214 $listenee = $req->get_parameter('omb_listenee');
215 $remote = Remote_profile::staticGet('uri', $listenee);
219 $profile = Profile::staticGet($remote->id);
220 $orig_remote = clone($remote);
221 $orig_profile = clone($profile);
224 $remote = new Remote_profile();
225 $remote->uri = $listenee;
226 $profile = new Profile();
229 $profile->nickname = $nickname;
230 $profile->profileurl = $profile_url;
233 $profile->fullname = $fullname;
236 $profile->homepage = $homepage;
239 $profile->bio = $bio;
242 $profile->location = $location;
246 $profile->update($orig_profile);
248 $profile->created = DB_DataObject_Cast::dateTime(); # current time
249 $id = $profile->insert();
254 $remote->update($orig_remote);
256 $remote->created = DB_DataObject_Cast::dateTime(); # current time
261 $this->add_avatar($profile, $avatar_url);
264 $user = common_current_user();
265 $datastore = omb_oauth_datastore();
266 $consumer = $this->get_consumer($datastore, $req);
267 $token = $this->get_token($datastore, $req, $consumer);
269 $sub = new Subscription();
270 $sub->subscriber = $user->id;
271 $sub->subscribed = $remote->id;
272 $sub->token = $token->key; # NOTE: request token, not valid for use!
273 $sub->created = DB_DataObject_Cast::dateTime(); # current time
275 if (!$sub->insert()) {
276 common_user_error(_t('Couldn\'t insert new subscription.'));
281 function add_avatar($profile, $url) {
282 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
283 copy($url, $temp_filename);
284 return $profile->setOriginal($temp_filename);
287 function show_accept_message($tok) {
288 common_show_header(_t('Subscription authorized'));
289 common_element('p', NULL,
290 _t('The subscription has been authorized, but no '.
291 'callback URL was passed. Check with the site\'s instructions for '.
292 'details on how to authorize the subscription. Your subscription token is:'));
293 common_element('blockquote', 'token', $tok);
294 common_show_footer();
297 function show_reject_message($tok) {
298 common_show_header(_t('Subscription rejected'));
299 common_element('p', NULL,
300 _t('The subscription has been rejected, but no '.
301 'callback URL was passed. Check with the site\'s instructions for '.
302 'details on how to fully reject the subscription.'));
303 common_show_footer();
306 function store_request($req) {
307 common_ensure_session();
308 $_SESSION['userauthorizationrequest'] = $req;
311 function clear_request() {
312 common_ensure_session();
313 unset($_SESSION['userauthorizationrequest']);
316 function get_stored_request() {
317 common_ensure_session();
318 $req = $_SESSION['userauthorizationrequest'];
322 function get_new_request() {
323 $req = OAuthRequest::from_request();
327 # Throws an OAuthException if anything goes wrong
329 function validate_request(&$req) {
330 # OAuth stuff -- have to copy from OAuth.php since they're
331 # all private methods, and there's no user-authentication method
332 common_debug('checking version', __FILE__);
333 $this->check_version($req);
334 common_debug('getting datastore', __FILE__);
335 $datastore = omb_oauth_datastore();
336 common_debug('getting consumer', __FILE__);
337 $consumer = $this->get_consumer($datastore, $req);
338 common_debug('getting token', __FILE__);
339 $token = $this->get_token($datastore, $req, $consumer);
340 common_debug('checking timestamp', __FILE__);
341 $this->check_timestamp($req);
342 common_debug('checking nonce', __FILE__);
343 $this->check_nonce($datastore, $req, $consumer, $token);
344 common_debug('checking signature', __FILE__);
345 $this->check_signature($req, $consumer, $token);
346 common_debug('validating omb stuff', __FILE__);
347 $this->validate_omb($req);
348 common_debug('done validating', __FILE__);
352 function validate_omb(&$req) {
353 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
354 'omb_listenee_profile', 'omb_listenee_nickname',
355 'omb_listenee_license') as $param)
357 if (!$req->get_parameter($param)) {
358 throw new OAuthException("Required parameter '$param' not found");
362 $version = $req->get_parameter('omb_version');
363 if ($version != OMB_VERSION_01) {
364 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
366 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
368 throw new OAuthException("Listener URI '$listener' not found here");
370 $listenee = $req->get_parameter('omb_listenee');
371 if (!Validate::uri($listenee) &&
372 !common_valid_tag($listenee)) {
373 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
375 if (strlen($listenee) > 255) {
376 throw new OAuthException("Listenee URI '$listenee' too long");
378 $nickname = $req->get_parameter('omb_listenee_nickname');
379 if (!Validate::string($nickname, array('min_length' => 1,
381 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
382 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
384 $profile = $req->get_parameter('omb_listenee_profile');
385 if (!common_valid_http_url($profile)) {
386 throw new OAuthException("Invalid profile URL '$profile'.");
388 $license = $req->get_parameter('omb_listenee_license');
389 if (!common_valid_http_url($license)) {
390 throw new OAuthException("Invalid license URL '$license'.");
393 $fullname = $req->get_parameter('omb_listenee_fullname');
394 if ($fullname && strlen($fullname) > 255) {
395 throw new OAuthException("Full name '$fullname' too long.");
397 $homepage = $req->get_parameter('omb_listenee_homepage');
398 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
399 throw new OAuthException("Invalid homepage '$homepage'");
401 $bio = $req->get_parameter('omb_listenee_bio');
402 if ($bio && strlen($bio) > 140) {
403 throw new OAuthException("Bio too long '$bio'");
405 $location = $req->get_parameter('omb_listenee_location');
406 if ($location && strlen($location) > 255) {
407 throw new OAuthException("Location too long '$location'");
409 $avatar = $req->get_parameter('omb_listenee_avatar');
411 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
412 throw new OAuthException("Invalid avatar URL '$avatar'");
414 $size = @getimagesize($avatar);
416 throw new OAuthException("Can't read avatar URL '$avatar'");
418 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
419 throw new OAuthException("Wrong size image at '$avatar'");
421 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
423 throw new OAuthException("Wrong image type for '$avatar'");
426 $callback = $req->get_parameter('oauth_callback');
427 if ($callback && !common_valid_http_url($callback)) {
428 throw new OAuthException("Invalid callback URL '$callback'");
432 # Snagged from OAuthServer
434 function check_version(&$req) {
435 $version = $req->get_parameter("oauth_version");
439 if ($version != 1.0) {
440 throw new OAuthException("OAuth version '$version' not supported");
445 # Snagged from OAuthServer
447 function get_consumer($datastore, $req) {
448 $consumer_key = @$req->get_parameter("oauth_consumer_key");
449 if (!$consumer_key) {
450 throw new OAuthException("Invalid consumer key");
453 $consumer = $datastore->lookup_consumer($consumer_key);
455 throw new OAuthException("Invalid consumer");
460 # Mostly cadged from OAuthServer
462 function get_token($datastore, &$req, $consumer) {/*{{{*/
463 $token_field = @$req->get_parameter('oauth_token');
464 $token = $datastore->lookup_token($consumer, 'request', $token_field);
466 throw new OAuthException("Invalid $token_type token: $token_field");
471 function check_timestamp(&$req) {
472 $timestamp = @$req->get_parameter('oauth_timestamp');
474 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
475 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
479 # NOTE: don't call twice on the same request; will fail!
480 function check_nonce(&$datastore, &$req, $consumer, $token) {
481 $timestamp = @$req->get_parameter('oauth_timestamp');
482 $nonce = @$req->get_parameter('oauth_nonce');
483 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
485 throw new OAuthException("Nonce already used");
490 function check_signature(&$req, $consumer, $token) {
491 $signature_method = $this->get_signature_method($req);
492 $signature = $req->get_parameter('oauth_signature');
493 $valid_sig = $signature_method->check_signature($req,
498 throw new OAuthException("Invalid signature");
502 function get_signature_method(&$req) {
503 $signature_method = @$req->get_parameter("oauth_signature_method");
504 if (!$signature_method) {
505 $signature_method = "PLAINTEXT";
507 if ($signature_method != 'HMAC-SHA1') {
508 throw new OAuthException("Signature method '$signature_method' not supported.");
510 return omb_hmac_sha1();