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 $req = $this->get_new_request();
41 common_server_error(_t('No request found!'));
43 # XXX: only validate new requests, since nonce is one-time use
44 $this->validate_request($req);
46 } catch (OAuthException $e) {
47 $this->clear_request();
48 common_server_error($e->getMessage());
52 if (common_logged_in()) {
53 $this->show_form($req);
55 # Go log in, and then come back
56 $this->store_request($req);
57 common_set_returnto(common_local_url('userauthorization'));
58 common_redirect(common_local_url('login'));
63 function show_form($req) {
65 $nickname = $req->get_parameter('omb_listenee_nickname');
66 $profile = $req->get_parameter('omb_listenee_profile');
67 $license = $req->get_parameter('omb_listenee_license');
68 $fullname = $req->get_parameter('omb_listenee_fullname');
69 $homepage = $req->get_parameter('omb_listenee_homepage');
70 $bio = $req->get_parameter('omb_listenee_bio');
71 $location = $req->get_parameter('omb_listenee_location');
72 $avatar = $req->get_parameter('omb_listenee_avatar');
74 common_show_header(_t('Authorize subscription'));
75 common_element('p', _t('Please check these details to make sure '.
76 'that you want to subscribe to this user\'s notices. '.
77 'If you didn\'t just ask to subscribe to someone\'s notices, '.
79 common_element_start('div', 'profile');
81 common_element('img', array('src' => $avatar,
82 'class' => 'avatar profile',
83 'width' => AVATAR_PROFILE_SIZE,
84 'height' => AVATAR_PROFILE_SIZE,
87 common_element('a', array('href' => $profile,
88 'class' => 'external profile nickname'),
91 common_element_start('div', 'fullname');
93 common_element('a', array('href' => $homepage),
96 common_text($fullname);
98 common_element_end('div');
101 common_element('div', 'location', $location);
104 common_element('div', 'bio', $bio);
106 common_element_start('div', 'license');
107 common_element('a', array('href' => $license,
108 'class' => 'license'),
110 common_element_end('div');
111 common_element_end('div');
112 common_element_start('form', array('method' => 'POST',
113 'id' => 'userauthorization',
114 'name' => 'userauthorization',
115 'action' => common_local_url('userauthorization')));
116 common_submit('accept', _t('Accept'));
117 common_submit('reject', _t('Reject'));
118 common_element_end('form');
119 common_show_footer();
122 function send_authorization() {
123 $req = $this->get_stored_request();
126 common_user_error(_t('No authorization request!'));
130 $callback = $req->get_parameter('oauth_callback');
132 if ($this->arg('accept')) {
133 $this->authorize_token($req);
134 $this->save_remote_profile($req);
136 $this->show_accept_message($req->get_parameter('oauth_token'));
139 $params['oauth_token'] = $req->get_parameter('oauth_token');
140 $params['omb_version'] = OMB_VERSION_01;
141 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
142 $profile = $user->getProfile();
143 $params['omb_listener_nickname'] = $user->nickname;
144 $params['omb_listener_profile'] = common_local_url('showstream',
145 array('nickname' => $user->nickname));
146 if ($profile->fullname) {
147 $params['omb_listener_fullname'] = $profile->fullname;
149 if ($profile->homepage) {
150 $params['omb_listener_homepage'] = $profile->homepage;
153 $params['omb_listener_bio'] = $profile->bio;
155 if ($profile->location) {
156 $params['omb_listener_location'] = $profile->location;
158 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
160 $params['omb_listener_avatar'] = $avatar->url;
163 foreach ($params as $k => $v) {
164 $parts[] = $k . '=' . OAuthUtil::urlencodeRFC3986($v);
166 $query_string = implode('&', $parts);
167 $parsed = parse_url($callback);
168 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
169 common_redirect($url, 303);
173 $this->show_reject_message();
175 # XXX: not 100% sure how to signal failure... just redirect without token?
176 common_redirect($callback, 303);
181 function authorize_token(&$req) {
182 $consumer_key = @$req->get_parameter('oauth_consumer_key');
183 $token_field = @$req->get_parameter('oauth_token');
185 $rt->consumer_key = $consumer_key;
186 $rt->tok = $token_field;
187 if ($rt->find(TRUE)) {
188 $orig_rt = clone($rt);
189 $rt->state = 1; # Authorized but not used
190 if ($rt->update($orig_rt)) {
197 # XXX: refactor with similar code in finishremotesubscribe.php
199 function save_remote_profile(&$req) {
200 # FIXME: we should really do this when the consumer comes
201 # back for an access token. If they never do, we've got stuff in a
204 $fullname = $req->get_parameter('omb_listenee_fullname');
205 $profile_url = $req->get_parameter('omb_listenee_profile');
206 $homepage = $req->get_parameter('omb_listenee_homepage');
207 $bio = $req->get_parameter('omb_listenee_bio');
208 $location = $req->get_parameter('omb_listenee_location');
209 $avatar_url = $req->get_parameter('omb_listenee_avatar');
211 $listenee = $req->get_parameter('omb_listenee');
212 $remote = Remote_profile::staticGet('uri', $listenee);
216 $profile = Profile::staticGet($remote->id);
217 $orig_remote = clone($remote);
218 $orig_profile = clone($profile);
221 $remote = new Remote_profile();
222 $remote->uri = $omb['listener'];
223 $profile = new Profile();
226 $profile->nickname = $nickname;
227 $profile->profileurl = $profile_url;
230 $profile->fullname = $fullname;
233 $profile->homepage = $homepage;
236 $profile->bio = $bio;
239 $profile->location = $location;
243 $profile->update($orig_profile);
245 $profile->created = DB_DataObject_Cast::dateTime(); # current time
246 $id = $profile->insert();
251 $this->add_avatar($avatar_url);
255 $remote->update($orig_remote);
257 $remote->created = DB_DataObject_Cast::dateTime(); # current time
261 $user = common_current_user();
262 $datastore = omb_oauth_datastore();
263 $consumer = $this->get_consumer($datastore, $req);
264 $token = $this->get_token($datastore, $req, $consumer);
266 $sub = new Subscription();
267 $sub->subscriber = $user->id;
268 $sub->subscribed = $remote->id;
269 $sub->token = $token->key; # NOTE: request token, not valid for use!
270 $sub->created = DB_DataObject_Cast::dateTime(); # current time
272 if (!$sub->insert()) {
273 common_user_error(_t('Couldn\'t insert new subscription.'));
278 function show_accept_message($tok) {
279 common_show_header(_t('Subscription authorized'));
280 common_element('p', NULL,
281 _t('The subscription has been authorized, but no '.
282 'callback URL was passed. Check with the site\'s instructions for '.
283 'details on how to authorize the subscription. Your subscription token is:'));
284 common_element('blockquote', 'token', $tok);
285 common_show_footer();
288 function show_reject_message($tok) {
289 common_show_header(_t('Subscription rejected'));
290 common_element('p', NULL,
291 _t('The subscription has been rejected, but no '.
292 'callback URL was passed. Check with the site\'s instructions for '.
293 'details on how to fully reject the subscription.'));
294 common_show_footer();
297 function store_request($req) {
298 common_ensure_session();
299 $_SESSION['userauthorizationrequest'] = $req;
302 function clear_request($req) {
303 common_ensure_session();
304 unset($_SESSION['userauthorizationrequest']);
307 function get_stored_request() {
308 common_ensure_session();
309 $req = $_SESSION['userauthorizationrequest'];
313 function get_new_request() {
314 $req = OAuthRequest::from_request();
318 # Throws an OAuthException if anything goes wrong
320 function validate_request(&$req) {
321 # OAuth stuff -- have to copy from OAuth.php since they're
322 # all private methods, and there's no user-authentication method
323 $this->check_version($req);
324 $datastore = omb_oauth_datastore();
325 $consumer = $this->get_consumer($datastore, $req);
326 $token = $this->get_token($datastore, $req, $consumer);
327 $this->check_timestamp($req);
328 $this->check_nonce($datastore, $req, $consumer, $token);
329 $this->check_signature($req, $consumer, $token);
330 $this->validate_omb($req);
334 function validate_omb(&$req) {
335 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
336 'omb_listenee_profile', 'omb_listenee_nickname',
337 'omb_listenee_license') as $param)
339 if (!$req->get_parameter($param)) {
340 throw new OAuthException("Required parameter '$param' not found");
344 $version = $req->get_parameter('omb_version');
345 if ($version != OMB_VERSION_01) {
346 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
348 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
350 throw new OAuthException("Listener URI '$listener' not found here");
352 $listenee = $req->get_parameter('omb_listenee');
353 if (!Validate::uri($listenee)) {
354 throw new OAuthException("Listenee URI '$listenee' not a valid URI");
355 } else if (strlen($listenee) > 255) {
356 throw new OAuthException("Listenee URI '$listenee' too long");
358 $nickname = $req->get_parameter('omb_listenee_nickname');
359 if (!Validate::string($nickname, array('min_length' => 1,
361 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
362 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
364 $profile = $req->get_parameter('omb_listenee_profile');
365 if (!common_valid_http_url($profile)) {
366 throw new OAuthException("Invalid profile URL '$profile'.");
368 $license = $req->get_parameter('omb_listenee_license');
369 if (!common_valid_http_url($license)) {
370 throw new OAuthException("Invalid license URL '$license'.");
373 $fullname = $req->get_parameter('omb_listenee_fullname');
374 if ($fullname && strlen($fullname) > 255) {
375 throw new OAuthException("Full name '$fullname' too long.");
377 $homepage = $req->get_parameter('omb_listenee_homepage');
378 if ($homepage && (!common_valid_http_url($homepage) || strlen($homepage) > 255)) {
379 throw new OAuthException("Invalid homepage '$homepage'");
381 $bio = $req->get_parameter('omb_listenee_bio');
382 if ($bio && strlen($bio) > 140) {
383 throw new OAuthException("Bio too long '$bio'");
385 $location = $req->get_parameter('omb_listenee_location');
386 if ($location && strlen($location) > 255) {
387 throw new OAuthException("Location too long '$location'");
389 $avatar = $req->get_parameter('omb_listenee_avatar');
390 if ($avatar && (!common_valid_http_url($avatar) || strlen($avatar) > 255)) {
391 throw new OAuthException("Invalid avatar '$avatar'");
393 $callback = $req->get_parameter('oauth_callback');
394 if ($avatar && common_valid_http_url($callback)) {
395 throw new OAuthException("Invalid callback URL '$callback'");
399 # Snagged from OAuthServer
401 function check_version(&$req) {
402 $version = $req->get_parameter("oauth_version");
406 if ($version != 1.0) {
407 throw new OAuthException("OAuth version '$version' not supported");
412 # Snagged from OAuthServer
414 function get_consumer($datastore, $req) {
415 $consumer_key = @$req->get_parameter("oauth_consumer_key");
416 if (!$consumer_key) {
417 throw new OAuthException("Invalid consumer key");
420 $consumer = $datastore->lookup_consumer($consumer_key);
422 throw new OAuthException("Invalid consumer");
427 # Mostly cadged from OAuthServer
429 function get_token(&$req, $consumer, $datastore) {/*{{{*/
430 $token_field = @$req->get_parameter('oauth_token');
431 $token = $datastore->lookup_token($consumer, 'request', $token_field);
433 throw new OAuthException("Invalid $token_type token: $token_field");
438 function check_timestamp(&$req) {
439 $timestamp = @$req->get_parameter('oauth_timestamp');
441 if ($now - $timestamp > TIMESTAMP_THRESHOLD) {
442 throw new OAuthException("Expired timestamp, yours $timestamp, ours $now");
446 # NOTE: don't call twice on the same request; will fail!
447 function check_nonce(&$datastore, &$req, $consumer, $token) {
448 $timestamp = @$req->get_parameter('oauth_timestamp');
449 $nonce = @$req->get_parameter('oauth_nonce');
450 $found = $datastore->lookup_nonce($consumer, $token, $nonce, $timestamp);
452 throw new OAuthException("Nonce already used");
457 function check_signature(&$req, $consumer, $token) {
458 $signature_method = $this->get_signature_method($req);
459 $signature = $req->get_parameter('oauth_signature');
460 $valid_sig = $signature_method->check_signature($req,
465 throw new OAuthException("Invalid signature");
469 function get_signature_method(&$req) {
470 $signature_method = @$req->get_parameter("oauth_signature_method");
471 if (!$signature_method) {
472 $signature_method = "PLAINTEXT";
474 if ($signature_method != 'HMAC-SHA1') {
475 throw new OAuthException("Signature method '$signature_method' not supported.");
477 return omb_hmac_sha1();