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 is_readonly() {
31 function handle($args) {
32 parent::handle($args);
34 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
35 # We've shown the form, now post user's choice
36 $this->send_authorization();
38 if (!common_logged_in()) {
39 # Go log in, and then come back
40 common_debug('userauthorization.php - saving URL for returnto');
42 unset($argsclone['action']);
43 common_set_returnto(common_local_url('userauthorization', $argsclone));
44 common_debug('userauthorization.php - redirecting to login');
45 common_redirect(common_local_url('login'));
49 # this must be a new request
50 common_debug('userauthorization.php - getting new request');
51 $req = $this->get_new_request();
53 common_server_error(_('No request found!'));
55 common_debug('userauthorization.php - validating request');
56 # XXX: only validate new requests, since nonce is one-time use
57 $this->validate_request($req);
58 common_debug('userauthorization.php - showing form');
59 $this->store_request($req);
60 $this->show_form($req);
61 } catch (OAuthException $e) {
62 $this->clear_request();
63 common_server_error($e->getMessage());
70 function show_form($req) {
72 $nickname = $req->get_parameter('omb_listenee_nickname');
73 $profile = $req->get_parameter('omb_listenee_profile');
74 $license = $req->get_parameter('omb_listenee_license');
75 $fullname = $req->get_parameter('omb_listenee_fullname');
76 $homepage = $req->get_parameter('omb_listenee_homepage');
77 $bio = $req->get_parameter('omb_listenee_bio');
78 $location = $req->get_parameter('omb_listenee_location');
79 $avatar = $req->get_parameter('omb_listenee_avatar');
81 common_show_header(_('Authorize subscription'));
82 common_element('p', NULL, _('Please check these details to make sure '.
83 'that you want to subscribe to this user\'s notices. '.
84 'If you didn\'t just ask to subscribe to someone\'s notices, '.
86 common_element_start('div', 'profile');
88 common_element('img', array('src' => $avatar,
89 'class' => 'avatar profile',
90 'width' => AVATAR_PROFILE_SIZE,
91 'height' => AVATAR_PROFILE_SIZE,
94 common_element('a', array('href' => $profile,
95 'class' => 'external profile nickname'),
98 common_element_start('div', 'fullname');
100 common_element('a', array('href' => $homepage),
103 common_text($fullname);
105 common_element_end('div');
108 common_element('div', 'location', $location);
111 common_element('div', 'bio', $bio);
113 common_element_start('div', 'license');
114 common_element('a', array('href' => $license,
115 'class' => 'license'),
117 common_element_end('div');
118 common_element_end('div');
119 common_element_start('form', array('method' => 'post',
120 'id' => 'userauthorization',
121 'name' => 'userauthorization',
122 'action' => common_local_url('userauthorization')));
123 common_submit('accept', _('Accept'));
124 common_submit('reject', _('Reject'));
125 common_element_end('form');
126 common_show_footer();
129 function send_authorization() {
130 $req = $this->get_stored_request();
133 common_user_error(_('No authorization request!'));
137 $callback = $req->get_parameter('oauth_callback');
139 if ($this->arg('accept')) {
140 if (!$this->authorize_token($req)) {
141 common_server_error(_('Error authorizing token'));
143 if (!$this->save_remote_profile($req)) {
144 common_server_error(_('Error saving remote profile'));
147 $this->show_accept_message($req->get_parameter('oauth_token'));
150 $params['oauth_token'] = $req->get_parameter('oauth_token');
151 $params['omb_version'] = OMB_VERSION_01;
152 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
153 $profile = $user->getProfile();
154 $params['omb_listener_nickname'] = $user->nickname;
155 $params['omb_listener_profile'] = common_local_url('showstream',
156 array('nickname' => $user->nickname));
157 if ($profile->fullname) {
158 $params['omb_listener_fullname'] = $profile->fullname;
160 if ($profile->homepage) {
161 $params['omb_listener_homepage'] = $profile->homepage;
164 $params['omb_listener_bio'] = $profile->bio;
166 if ($profile->location) {
167 $params['omb_listener_location'] = $profile->location;
169 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
171 $params['omb_listener_avatar'] = $avatar->url;
174 foreach ($params as $k => $v) {
175 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
177 $query_string = implode('&', $parts);
178 $parsed = parse_url($callback);
179 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
180 common_redirect($url, 303);
184 $this->show_reject_message();
186 # XXX: not 100% sure how to signal failure... just redirect without token?
187 common_redirect($callback, 303);
192 function authorize_token(&$req) {
193 $consumer_key = $req->get_parameter('oauth_consumer_key');
194 $token_field = $req->get_parameter('oauth_token');
195 common_debug('consumer key = "'.$consumer_key.'"', __FILE__);
196 common_debug('token field = "'.$token_field.'"', __FILE__);
198 $rt->consumer_key = $consumer_key;
199 $rt->tok = $token_field;
202 common_debug('request token to look up: "'.print_r($rt,TRUE).'"');
203 if ($rt->find(true)) {
204 common_debug('found request token to authorize', __FILE__);
205 $orig_rt = clone($rt);
206 $rt->state = 1; # Authorized but not used
207 if ($rt->update($orig_rt)) {
208 common_debug('updated request token so it is authorized', __FILE__);
215 # XXX: refactor with similar code in finishremotesubscribe.php
217 function save_remote_profile(&$req) {
218 # FIXME: we should really do this when the consumer comes
219 # back for an access token. If they never do, we've got stuff in a
222 $nickname = $req->get_parameter('omb_listenee_nickname');
223 $fullname = $req->get_parameter('omb_listenee_fullname');
224 $profile_url = $req->get_parameter('omb_listenee_profile');
225 $homepage = $req->get_parameter('omb_listenee_homepage');
226 $bio = $req->get_parameter('omb_listenee_bio');
227 $location = $req->get_parameter('omb_listenee_location');
228 $avatar_url = $req->get_parameter('omb_listenee_avatar');
230 $listenee = $req->get_parameter('omb_listenee');
231 $remote = Remote_profile::staticGet('uri', $listenee);
235 $profile = Profile::staticGet($remote->id);
236 $orig_remote = clone($remote);
237 $orig_profile = clone($profile);
240 $remote = new Remote_profile();
241 $remote->uri = $listenee;
242 $profile = new Profile();
245 $profile->nickname = $nickname;
246 $profile->profileurl = $profile_url;
249 $profile->fullname = $fullname;
252 $profile->homepage = $homepage;
255 $profile->bio = $bio;
258 $profile->location = $location;
262 $profile->update($orig_profile);
264 $profile->created = DB_DataObject_Cast::dateTime(); # current time
265 $id = $profile->insert();
273 if (!$remote->update($orig_remote)) {
277 $remote->created = DB_DataObject_Cast::dateTime(); # current time
278 if (!$remote->insert()) {
284 if (!$this->add_avatar($profile, $avatar_url)) {
289 $user = common_current_user();
290 $datastore = omb_oauth_datastore();
291 $consumer = $this->get_consumer($datastore, $req);
292 $token = $this->get_token($datastore, $req, $consumer);
294 $sub = new Subscription();
295 $sub->subscriber = $user->id;
296 $sub->subscribed = $remote->id;
297 $sub->token = $token->key; # NOTE: request token, not valid for use!
298 $sub->created = DB_DataObject_Cast::dateTime(); # current time
300 if (!$sub->insert()) {
307 function add_avatar($profile, $url) {
308 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
309 copy($url, $temp_filename);
310 return $profile->setOriginal($temp_filename);
313 function show_accept_message($tok) {
314 common_show_header(_('Subscription authorized'));
315 common_element('p', NULL,
316 _('The subscription has been authorized, but no '.
317 'callback URL was passed. Check with the site\'s instructions for '.
318 'details on how to authorize the subscription. Your subscription token is:'));
319 common_element('blockquote', 'token', $tok);
320 common_show_footer();
323 function show_reject_message($tok) {
324 common_show_header(_('Subscription rejected'));
325 common_element('p', NULL,
326 _('The subscription has been rejected, but no '.
327 'callback URL was passed. Check with the site\'s instructions for '.
328 'details on how to fully reject the subscription.'));
329 common_show_footer();
332 function store_request($req) {
333 common_ensure_session();
334 $_SESSION['userauthorizationrequest'] = $req;
337 function clear_request() {
338 common_ensure_session();
339 unset($_SESSION['userauthorizationrequest']);
342 function get_stored_request() {
343 common_ensure_session();
344 $req = $_SESSION['userauthorizationrequest'];
348 function get_new_request() {
349 $req = OAuthRequest::from_request();
353 # Throws an OAuthException if anything goes wrong
355 function validate_request(&$req) {
356 # OAuth stuff -- have to copy from OAuth.php since they're
357 # all private methods, and there's no user-authentication method
358 common_debug('checking version', __FILE__);
359 $this->check_version($req);
360 common_debug('getting datastore', __FILE__);
361 $datastore = omb_oauth_datastore();
362 common_debug('getting consumer', __FILE__);
363 $consumer = $this->get_consumer($datastore, $req);
364 common_debug('getting token', __FILE__);
365 $token = $this->get_token($datastore, $req, $consumer);
366 common_debug('checking timestamp', __FILE__);
367 $this->check_timestamp($req);
368 common_debug('checking nonce', __FILE__);
369 $this->check_nonce($datastore, $req, $consumer, $token);
370 common_debug('checking signature', __FILE__);
371 $this->check_signature($req, $consumer, $token);
372 common_debug('validating omb stuff', __FILE__);
373 $this->validate_omb($req);
374 common_debug('done validating', __FILE__);
378 function validate_omb(&$req) {
379 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
380 'omb_listenee_profile', 'omb_listenee_nickname',
381 'omb_listenee_license') as $param)
383 if (!$req->get_parameter($param)) {
384 throw new OAuthException("Required parameter '$param' not found");
388 $version = $req->get_parameter('omb_version');
389 if ($version != OMB_VERSION_01) {
390 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
392 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
394 throw new OAuthException("Listener URI '$listener' not found here");
396 $cur = common_current_user();
397 if ($cur->id != $user->id) {
398 throw new OAuthException("Can't add for another user!");
400 $listenee = $req->get_parameter('omb_listenee');
401 if (!Validate::uri($listenee) &&
402 !common_valid_tag($listenee)) {
403 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
405 if (strlen($listenee) > 255) {
406 throw new OAuthException("Listenee URI '$listenee' too long");
408 $remote = Remote_profile::staticGet('uri', $listenee);
410 $sub = new Subscription();
411 $sub->subscriber = $user->id;
412 $sub->subscribed = $remote->id;
413 if ($sub->find(TRUE)) {
414 throw new OAuthException("Already subscribed to user!");
417 $nickname = $req->get_parameter('omb_listenee_nickname');
418 if (!Validate::string($nickname, array('min_length' => 1,
420 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
421 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
423 $profile = $req->get_parameter('omb_listenee_profile');
424 if (!common_valid_http_url($profile)) {
425 throw new OAuthException("Invalid profile URL '$profile'.");
427 $license = $req->get_parameter('omb_listenee_license');
428 if (!common_valid_http_url($license)) {
429 throw new OAuthException("Invalid license URL '$license'.");
432 $fullname = $req->get_parameter('omb_listenee_fullname');
433 if ($fullname && strlen($fullname) > 255) {
434 throw new OAuthException("Full name '$fullname' too long.");
436 $homepage = $req->get_parameter('omb_listenee_homepage');
437 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
438 throw new OAuthException("Invalid homepage '$homepage'");
440 $bio = $req->get_parameter('omb_listenee_bio');
441 if ($bio && strlen($bio) > 140) {
442 throw new OAuthException("Bio too long '$bio'");
444 $location = $req->get_parameter('omb_listenee_location');
445 if ($location && strlen($location) > 255) {
446 throw new OAuthException("Location too long '$location'");
448 $avatar = $req->get_parameter('omb_listenee_avatar');
450 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
451 throw new OAuthException("Invalid avatar URL '$avatar'");
453 $size = @getimagesize($avatar);
455 throw new OAuthException("Can't read avatar URL '$avatar'");
457 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
458 throw new OAuthException("Wrong size image at '$avatar'");
460 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
462 throw new OAuthException("Wrong image type for '$avatar'");
465 $callback = $req->get_parameter('oauth_callback');
466 if ($callback && !common_valid_http_url($callback)) {
467 throw new OAuthException("Invalid callback URL '$callback'");
471 # Snagged from OAuthServer
473 function check_version(&$req) {
474 $version = $req->get_parameter("oauth_version");
478 if ($version != 1.0) {
479 throw new OAuthException("OAuth version '$version' not supported");
484 # Snagged from OAuthServer
486 function get_consumer($datastore, $req) {
487 $consumer_key = @$req->get_parameter("oauth_consumer_key");
488 if (!$consumer_key) {
489 throw new OAuthException("Invalid consumer key");
492 $consumer = $datastore->lookup_consumer($consumer_key);
494 throw new OAuthException("Invalid consumer");
499 # Mostly cadged from OAuthServer
501 function get_token($datastore, &$req, $consumer) {/*{{{*/
502 $token_field = @$req->get_parameter('oauth_token');
503 $token = $datastore->lookup_token($consumer, 'request', $token_field);
505 throw new OAuthException("Invalid $token_type token: $token_field");
510 function check_timestamp(&$req) {
511 $timestamp = @$req->get_parameter('oauth_timestamp');
513 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
514 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
518 # NOTE: don't call twice on the same request; will fail!
519 function check_nonce(&$datastore, &$req, $consumer, $token) {
520 $timestamp = @$req->get_parameter('oauth_timestamp');
521 $nonce = @$req->get_parameter('oauth_nonce');
522 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
524 throw new OAuthException("Nonce already used");
529 function check_signature(&$req, $consumer, $token) {
530 $signature_method = $this->get_signature_method($req);
531 $signature = $req->get_parameter('oauth_signature');
532 $valid_sig = $signature_method->check_signature($req,
537 throw new OAuthException("Invalid signature");
541 function get_signature_method(&$req) {
542 $signature_method = @$req->get_parameter("oauth_signature_method");
543 if (!$signature_method) {
544 $signature_method = "PLAINTEXT";
546 if ($signature_method != 'HMAC-SHA1') {
547 throw new OAuthException("Signature method '$signature_method' not supported.");
549 return omb_hmac_sha1();