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();
34 common_debug('userauthorization.php - fetching request');
35 # We get called after login if we have a stored request
36 $req = $this->get_stored_request();
38 # this must be a new request
39 common_debug('userauthorization.php - getting new request');
40 $req = $this->get_new_request();
42 common_server_error(_t('No request found!'));
44 common_debug('userauthorization.php - validating request');
45 # XXX: only validate new requests, since nonce is one-time use
46 $this->validate_request($req);
48 } catch (OAuthException $e) {
49 $this->clear_request();
50 common_server_error($e->getMessage());
54 if (common_logged_in()) {
55 common_debug('userauthorization.php - showing form');
56 $this->show_form($req);
58 common_debug('userauthorization.php - storing request in session');
59 # Go log in, and then come back
60 $this->store_request($req);
61 common_debug('userauthorization.php - saving URL for returnto');
62 common_set_returnto(common_local_url('userauthorization'));
63 common_debug('userauthorization.php - redirecting to login');
64 common_redirect(common_local_url('login'));
69 function show_form($req) {
71 $nickname = $req->get_parameter('omb_listenee_nickname');
72 $profile = $req->get_parameter('omb_listenee_profile');
73 $license = $req->get_parameter('omb_listenee_license');
74 $fullname = $req->get_parameter('omb_listenee_fullname');
75 $homepage = $req->get_parameter('omb_listenee_homepage');
76 $bio = $req->get_parameter('omb_listenee_bio');
77 $location = $req->get_parameter('omb_listenee_location');
78 $avatar = $req->get_parameter('omb_listenee_avatar');
80 common_show_header(_t('Authorize subscription'));
81 common_element('p', _t('Please check these details to make sure '.
82 'that you want to subscribe to this user\'s notices. '.
83 'If you didn\'t just ask to subscribe to someone\'s notices, '.
85 common_element_start('div', 'profile');
87 common_element('img', array('src' => $avatar,
88 'class' => 'avatar profile',
89 'width' => AVATAR_PROFILE_SIZE,
90 'height' => AVATAR_PROFILE_SIZE,
93 common_element('a', array('href' => $profile,
94 'class' => 'external profile nickname'),
97 common_element_start('div', 'fullname');
99 common_element('a', array('href' => $homepage),
102 common_text($fullname);
104 common_element_end('div');
107 common_element('div', 'location', $location);
110 common_element('div', 'bio', $bio);
112 common_element_start('div', 'license');
113 common_element('a', array('href' => $license,
114 'class' => 'license'),
116 common_element_end('div');
117 common_element_end('div');
118 common_element_start('form', array('method' => 'POST',
119 'id' => 'userauthorization',
120 'name' => 'userauthorization',
121 'action' => common_local_url('userauthorization')));
122 common_submit('accept', _t('Accept'));
123 common_submit('reject', _t('Reject'));
124 common_element_end('form');
125 common_show_footer();
128 function send_authorization() {
129 $req = $this->get_stored_request();
132 common_user_error(_t('No authorization request!'));
136 $callback = $req->get_parameter('oauth_callback');
138 if ($this->arg('accept')) {
139 $this->authorize_token($req);
140 $this->save_remote_profile($req);
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');
191 $rt->consumer_key = $consumer_key;
192 $rt->tok = $token_field;
193 if ($rt->find(TRUE)) {
194 $orig_rt = clone($rt);
195 $rt->state = 1; # Authorized but not used
196 if ($rt->update($orig_rt)) {
203 # XXX: refactor with similar code in finishremotesubscribe.php
205 function save_remote_profile(&$req) {
206 # FIXME: we should really do this when the consumer comes
207 # back for an access token. If they never do, we've got stuff in a
210 $fullname = $req->get_parameter('omb_listenee_fullname');
211 $profile_url = $req->get_parameter('omb_listenee_profile');
212 $homepage = $req->get_parameter('omb_listenee_homepage');
213 $bio = $req->get_parameter('omb_listenee_bio');
214 $location = $req->get_parameter('omb_listenee_location');
215 $avatar_url = $req->get_parameter('omb_listenee_avatar');
217 $listenee = $req->get_parameter('omb_listenee');
218 $remote = Remote_profile::staticGet('uri', $listenee);
222 $profile = Profile::staticGet($remote->id);
223 $orig_remote = clone($remote);
224 $orig_profile = clone($profile);
227 $remote = new Remote_profile();
228 $remote->uri = $omb['listener'];
229 $profile = new Profile();
232 $profile->nickname = $nickname;
233 $profile->profileurl = $profile_url;
236 $profile->fullname = $fullname;
239 $profile->homepage = $homepage;
242 $profile->bio = $bio;
245 $profile->location = $location;
249 $profile->update($orig_profile);
251 $profile->created = DB_DataObject_Cast::dateTime(); # current time
252 $id = $profile->insert();
257 $this->add_avatar($avatar_url);
261 $remote->update($orig_remote);
263 $remote->created = DB_DataObject_Cast::dateTime(); # current time
267 $user = common_current_user();
268 $datastore = omb_oauth_datastore();
269 $consumer = $this->get_consumer($datastore, $req);
270 $token = $this->get_token($datastore, $req, $consumer);
272 $sub = new Subscription();
273 $sub->subscriber = $user->id;
274 $sub->subscribed = $remote->id;
275 $sub->token = $token->key; # NOTE: request token, not valid for use!
276 $sub->created = DB_DataObject_Cast::dateTime(); # current time
278 if (!$sub->insert()) {
279 common_user_error(_t('Couldn\'t insert new subscription.'));
284 function show_accept_message($tok) {
285 common_show_header(_t('Subscription authorized'));
286 common_element('p', NULL,
287 _t('The subscription has been authorized, but no '.
288 'callback URL was passed. Check with the site\'s instructions for '.
289 'details on how to authorize the subscription. Your subscription token is:'));
290 common_element('blockquote', 'token', $tok);
291 common_show_footer();
294 function show_reject_message($tok) {
295 common_show_header(_t('Subscription rejected'));
296 common_element('p', NULL,
297 _t('The subscription has been rejected, but no '.
298 'callback URL was passed. Check with the site\'s instructions for '.
299 'details on how to fully reject the subscription.'));
300 common_show_footer();
303 function store_request($req) {
304 common_ensure_session();
305 $_SESSION['userauthorizationrequest'] = $req;
308 function clear_request($req) {
309 common_ensure_session();
310 unset($_SESSION['userauthorizationrequest']);
313 function get_stored_request() {
314 common_ensure_session();
315 $req = $_SESSION['userauthorizationrequest'];
319 function get_new_request() {
320 $req = OAuthRequest::from_request();
324 # Throws an OAuthException if anything goes wrong
326 function validate_request(&$req) {
327 # OAuth stuff -- have to copy from OAuth.php since they're
328 # all private methods, and there's no user-authentication method
329 common_debug('checking version', __FILE__);
330 $this->check_version($req);
331 common_debug('getting datastore', __FILE__);
332 $datastore = omb_oauth_datastore();
333 common_debug('getting consumer', __FILE__);
334 $consumer = $this->get_consumer($datastore, $req);
335 common_debug('getting token', __FILE__);
336 $token = $this->get_token($datastore, $req, $consumer);
337 common_debug('checking timestamp', __FILE__);
338 $this->check_timestamp($req);
339 common_debug('checking nonce', __FILE__);
340 $this->check_nonce($datastore, $req, $consumer, $token);
341 common_debug('checking signature', __FILE__);
342 $this->check_signature($req, $consumer, $token);
343 common_debug('validating omb stuff', __FILE__);
344 $this->validate_omb($req);
345 common_debug('done validating', __FILE__);
349 function validate_omb(&$req) {
350 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
351 'omb_listenee_profile', 'omb_listenee_nickname',
352 'omb_listenee_license') as $param)
354 if (!$req->get_parameter($param)) {
355 throw new OAuthException("Required parameter '$param' not found");
359 $version = $req->get_parameter('omb_version');
360 if ($version != OMB_VERSION_01) {
361 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
363 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
365 throw new OAuthException("Listener URI '$listener' not found here");
367 $listenee = $req->get_parameter('omb_listenee');
368 if (!Validate::uri($listenee)) {
369 throw new OAuthException("Listenee URI '$listenee' not a valid URI");
370 } else if (strlen($listenee) > 255) {
371 throw new OAuthException("Listenee URI '$listenee' too long");
373 $nickname = $req->get_parameter('omb_listenee_nickname');
374 if (!Validate::string($nickname, array('min_length' => 1,
376 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
377 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
379 $profile = $req->get_parameter('omb_listenee_profile');
380 if (!common_valid_http_url($profile)) {
381 throw new OAuthException("Invalid profile URL '$profile'.");
383 $license = $req->get_parameter('omb_listenee_license');
384 if (!common_valid_http_url($license)) {
385 throw new OAuthException("Invalid license URL '$license'.");
388 $fullname = $req->get_parameter('omb_listenee_fullname');
389 if ($fullname && strlen($fullname) > 255) {
390 throw new OAuthException("Full name '$fullname' too long.");
392 $homepage = $req->get_parameter('omb_listenee_homepage');
393 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
394 throw new OAuthException("Invalid homepage '$homepage'");
396 $bio = $req->get_parameter('omb_listenee_bio');
397 if ($bio && strlen($bio) > 140) {
398 throw new OAuthException("Bio too long '$bio'");
400 $location = $req->get_parameter('omb_listenee_location');
401 if ($location && strlen($location) > 255) {
402 throw new OAuthException("Location too long '$location'");
404 $avatar = $req->get_parameter('omb_listenee_avatar');
405 if ($avatar && (!common_valid_http_url($avatar) || strlen($avatar) > 255)) {
406 throw new OAuthException("Invalid avatar '$avatar'");
408 $callback = $req->get_parameter('oauth_callback');
409 if ($avatar && common_valid_http_url($callback)) {
410 throw new OAuthException("Invalid callback URL '$callback'");
414 # Snagged from OAuthServer
416 function check_version(&$req) {
417 $version = $req->get_parameter("oauth_version");
421 if ($version != 1.0) {
422 throw new OAuthException("OAuth version '$version' not supported");
427 # Snagged from OAuthServer
429 function get_consumer($datastore, $req) {
430 $consumer_key = @$req->get_parameter("oauth_consumer_key");
431 if (!$consumer_key) {
432 throw new OAuthException("Invalid consumer key");
435 $consumer = $datastore->lookup_consumer($consumer_key);
437 throw new OAuthException("Invalid consumer");
442 # Mostly cadged from OAuthServer
444 function get_token($datastore, &$req, $consumer) {/*{{{*/
445 $token_field = @$req->get_parameter('oauth_token');
446 $token = $datastore->lookup_token($consumer, 'request', $token_field);
448 throw new OAuthException("Invalid $token_type token: $token_field");
453 function check_timestamp(&$req) {
454 $timestamp = @$req->get_parameter('oauth_timestamp');
456 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
457 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
461 # NOTE: don't call twice on the same request; will fail!
462 function check_nonce(&$datastore, &$req, $consumer, $token) {
463 $timestamp = @$req->get_parameter('oauth_timestamp');
464 $nonce = @$req->get_parameter('oauth_nonce');
465 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
467 throw new OAuthException("Nonce already used");
472 function check_signature(&$req, $consumer, $token) {
473 $signature_method = $this->get_signature_method($req);
474 $signature = $req->get_parameter('oauth_signature');
475 $valid_sig = $signature_method->check_signature($req,
480 throw new OAuthException("Invalid signature");
484 function get_signature_method(&$req) {
485 $signature_method = @$req->get_parameter("oauth_signature_method");
486 if (!$signature_method) {
487 $signature_method = "PLAINTEXT";
489 if ($signature_method != 'HMAC-SHA1') {
490 throw new OAuthException("Signature method '$signature_method' not supported.");
492 return omb_hmac_sha1();