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
30 function handle($args)
32 parent::handle($args);
34 if ($_SERVER['REQUEST_METHOD'] == 'POST') {
36 $token = $this->trimmed('token');
37 if (!$token || $token != common_session_token()) {
38 $req = $this->getStoredRequest();
39 $this->showForm($req, _('There was a problem with your session token. '.
40 'Try again, please.'));
43 # We've shown the form, now post user's choice
44 $this->sendAuthorization();
46 if (!common_logged_in()) {
47 # Go log in, and then come back
48 common_set_returnto($_SERVER['REQUEST_URI']);
50 common_redirect(common_local_url('login'));
54 # this must be a new request
55 $req = $this->getNewRequest();
57 $this->clientError(_('No request found!'));
59 # XXX: only validate new requests, since nonce is one-time use
60 $this->validateRequest($req);
61 $this->storeRequest($req);
62 $this->showForm($req);
63 } catch (OAuthException $e) {
64 $this->clearRequest();
65 $this->clientError($e->getMessage());
72 function showForm($req, $error=null)
75 $this->error = $error;
81 return _('Authorize subscription');
84 function showPageNotice()
86 $this->element('p', null, _('Please check these details to make sure '.
87 'that you want to subscribe to this user\'s notices. '.
88 'If you didn\'t just ask to subscribe to someone\'s notices, '.
92 function showContent()
96 $nickname = $req->get_parameter('omb_listenee_nickname');
97 $profile = $req->get_parameter('omb_listenee_profile');
98 $license = $req->get_parameter('omb_listenee_license');
99 $fullname = $req->get_parameter('omb_listenee_fullname');
100 $homepage = $req->get_parameter('omb_listenee_homepage');
101 $bio = $req->get_parameter('omb_listenee_bio');
102 $location = $req->get_parameter('omb_listenee_location');
103 $avatar = $req->get_parameter('omb_listenee_avatar');
105 $this->elementStart('div', 'profile');
107 $this->element('img', array('src' => $avatar,
109 'width' => AVATAR_PROFILE_SIZE,
110 'height' => AVATAR_PROFILE_SIZE,
111 'alt' => $nickname));
113 $this->element('a', array('href' => $profile,
114 'class' => 'external profile nickname'),
116 if (!is_null($fullname)) {
117 $this->elementStart('div', 'fullname');
118 if (!is_null($homepage)) {
119 $this->element('a', array('href' => $homepage),
122 $this->text($fullname);
124 $this->elementEnd('div');
126 if (!is_null($location)) {
127 $this->element('div', 'location', $location);
129 if (!is_null($bio)) {
130 $this->element('div', 'bio', $bio);
132 $this->elementStart('div', 'license');
133 $this->element('a', array('href' => $license,
134 'class' => 'license'),
136 $this->elementEnd('div');
137 $this->elementEnd('div');
138 $this->elementStart('form', array('method' => 'post',
139 'id' => 'userauthorization',
140 'name' => 'userauthorization',
141 'action' => common_local_url('userauthorization')));
142 $this->hidden('token', common_session_token());
143 $this->submit('accept', _('Accept'));
144 $this->submit('reject', _('Reject'));
145 $this->elementEnd('form');
148 function sendAuthorization()
150 $req = $this->getStoredRequest();
153 $this->clientError(_('No authorization request!'));
157 $callback = $req->get_parameter('oauth_callback');
159 if ($this->arg('accept')) {
160 if (!$this->authorizeToken($req)) {
161 $this->clientError(_('Error authorizing token'));
163 if (!$this->saveRemoteProfile($req)) {
164 $this->clientError(_('Error saving remote profile'));
167 $this->showAcceptMessage($req->get_parameter('oauth_token'));
170 $params['oauth_token'] = $req->get_parameter('oauth_token');
171 $params['omb_version'] = OMB_VERSION_01;
172 $user = User::staticGet('uri', $req->get_parameter('omb_listener'));
173 $profile = $user->getProfile();
175 common_log_db_error($user, 'SELECT', __FILE__);
176 $this->serverError(_('User without matching profile'));
179 $params['omb_listener_nickname'] = $user->nickname;
180 $params['omb_listener_profile'] = common_local_url('showstream',
181 array('nickname' => $user->nickname));
182 if (!is_null($profile->fullname)) {
183 $params['omb_listener_fullname'] = $profile->fullname;
185 if (!is_null($profile->homepage)) {
186 $params['omb_listener_homepage'] = $profile->homepage;
188 if (!is_null($profile->bio)) {
189 $params['omb_listener_bio'] = $profile->bio;
191 if (!is_null($profile->location)) {
192 $params['omb_listener_location'] = $profile->location;
194 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
196 $params['omb_listener_avatar'] = $avatar->url;
199 foreach ($params as $k => $v) {
200 $parts[] = $k . '=' . OAuthUtil::urlencode_rfc3986($v);
202 $query_string = implode('&', $parts);
203 $parsed = parse_url($callback);
204 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
205 common_redirect($url, 303);
209 $this->showRejectMessage();
211 # XXX: not 100% sure how to signal failure... just redirect without token?
212 common_redirect($callback, 303);
217 function authorizeToken(&$req)
219 $token_field = $req->get_parameter('oauth_token');
221 $rt->tok = $token_field;
224 if ($rt->find(true)) {
225 $orig_rt = clone($rt);
226 $rt->state = 1; # Authorized but not used
227 if ($rt->update($orig_rt)) {
234 # XXX: refactor with similar code in finishremotesubscribe.php
236 function saveRemoteProfile(&$req)
238 # FIXME: we should really do this when the consumer comes
239 # back for an access token. If they never do, we've got stuff in a
242 $nickname = $req->get_parameter('omb_listenee_nickname');
243 $fullname = $req->get_parameter('omb_listenee_fullname');
244 $profile_url = $req->get_parameter('omb_listenee_profile');
245 $homepage = $req->get_parameter('omb_listenee_homepage');
246 $bio = $req->get_parameter('omb_listenee_bio');
247 $location = $req->get_parameter('omb_listenee_location');
248 $avatar_url = $req->get_parameter('omb_listenee_avatar');
250 $listenee = $req->get_parameter('omb_listenee');
251 $remote = Remote_profile::staticGet('uri', $listenee);
255 $profile = Profile::staticGet($remote->id);
256 $orig_remote = clone($remote);
257 $orig_profile = clone($profile);
260 $remote = new Remote_profile();
261 $remote->uri = $listenee;
262 $profile = new Profile();
265 $profile->nickname = $nickname;
266 $profile->profileurl = $profile_url;
268 if (!is_null($fullname)) {
269 $profile->fullname = $fullname;
271 if (!is_null($homepage)) {
272 $profile->homepage = $homepage;
274 if (!is_null($bio)) {
275 $profile->bio = $bio;
277 if (!is_null($location)) {
278 $profile->location = $location;
282 $profile->update($orig_profile);
284 $profile->created = DB_DataObject_Cast::dateTime(); # current time
285 $id = $profile->insert();
293 if (!$remote->update($orig_remote)) {
297 $remote->created = DB_DataObject_Cast::dateTime(); # current time
298 if (!$remote->insert()) {
304 if (!$this->addAvatar($profile, $avatar_url)) {
309 $user = common_current_user();
310 $datastore = omb_oauth_datastore();
311 $consumer = $this->getConsumer($datastore, $req);
312 $token = $this->getToken($datastore, $req, $consumer);
314 $sub = new Subscription();
315 $sub->subscriber = $user->id;
316 $sub->subscribed = $remote->id;
317 $sub->token = $token->key; # NOTE: request token, not valid for use!
318 $sub->created = DB_DataObject_Cast::dateTime(); # current time
320 if (!$sub->insert()) {
327 function addAvatar($profile, $url)
329 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
330 copy($url, $temp_filename);
331 $imagefile = new ImageFile($profile->id, $temp_filename);
332 $filename = Avatar::filename($profile->id,
333 image_type_to_extension($imagefile->type),
336 rename($temp_filename, Avatar::path($filename));
337 return $profile->setOriginal($filename);
340 function showAcceptMessage($tok)
342 common_show_header(_('Subscription authorized'));
343 $this->element('p', null,
344 _('The subscription has been authorized, but no '.
345 'callback URL was passed. Check with the site\'s instructions for '.
346 'details on how to authorize the subscription. Your subscription token is:'));
347 $this->element('blockquote', 'token', $tok);
348 common_show_footer();
351 function showRejectMessage($tok)
353 common_show_header(_('Subscription rejected'));
354 $this->element('p', null,
355 _('The subscription has been rejected, but no '.
356 'callback URL was passed. Check with the site\'s instructions for '.
357 'details on how to fully reject the subscription.'));
358 common_show_footer();
361 function storeRequest($req)
363 common_ensure_session();
364 $_SESSION['userauthorizationrequest'] = $req;
367 function clearRequest()
369 common_ensure_session();
370 unset($_SESSION['userauthorizationrequest']);
373 function getStoredRequest()
375 common_ensure_session();
376 $req = $_SESSION['userauthorizationrequest'];
380 function getNewRequest()
382 common_remove_magic_from_request();
383 $req = OAuthRequest::from_request();
387 # Throws an OAuthException if anything goes wrong
389 function validateRequest(&$req)
393 $t->tok = $req->get_parameter('oauth_token');
395 if (!$t->find(true)) {
396 throw new OAuthException("Invalid request token: " . $req->get_parameter('oauth_token'));
399 $this->validateOmb($req);
403 function validateOmb(&$req)
405 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
406 'omb_listenee_profile', 'omb_listenee_nickname',
407 'omb_listenee_license') as $param)
409 if (is_null($req->get_parameter($param))) {
410 throw new OAuthException("Required parameter '$param' not found");
414 $version = $req->get_parameter('omb_version');
415 if ($version != OMB_VERSION_01) {
416 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
418 $listener = $req->get_parameter('omb_listener');
419 $user = User::staticGet('uri', $listener);
421 throw new OAuthException("Listener URI '$listener' not found here");
423 $cur = common_current_user();
424 if ($cur->id != $user->id) {
425 throw new OAuthException("Can't add for another user!");
427 $listenee = $req->get_parameter('omb_listenee');
428 if (!Validate::uri($listenee) &&
429 !common_valid_tag($listenee)) {
430 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
432 if (strlen($listenee) > 255) {
433 throw new OAuthException("Listenee URI '$listenee' too long");
436 $other = User::staticGet('uri', $listenee);
438 throw new OAuthException("Listenee URI '$listenee' is local user");
441 $remote = Remote_profile::staticGet('uri', $listenee);
443 $sub = new Subscription();
444 $sub->subscriber = $user->id;
445 $sub->subscribed = $remote->id;
446 if ($sub->find(true)) {
447 throw new OAuthException("Already subscribed to user!");
450 $nickname = $req->get_parameter('omb_listenee_nickname');
451 if (!Validate::string($nickname, array('min_length' => 1,
453 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
454 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
456 $profile = $req->get_parameter('omb_listenee_profile');
457 if (!common_valid_http_url($profile)) {
458 throw new OAuthException("Invalid profile URL '$profile'.");
461 if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
462 throw new OAuthException("Profile URL '$profile' is for a local user.");
465 $license = $req->get_parameter('omb_listenee_license');
466 if (!common_valid_http_url($license)) {
467 throw new OAuthException("Invalid license URL '$license'.");
469 $site_license = common_config('license', 'url');
470 if (!common_compatible_license($license, $site_license)) {
471 throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
474 $fullname = $req->get_parameter('omb_listenee_fullname');
475 if ($fullname && mb_strlen($fullname) > 255) {
476 throw new OAuthException("Full name '$fullname' too long.");
478 $homepage = $req->get_parameter('omb_listenee_homepage');
479 if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
480 throw new OAuthException("Invalid homepage '$homepage'");
482 $bio = $req->get_parameter('omb_listenee_bio');
483 if ($bio && mb_strlen($bio) > 140) {
484 throw new OAuthException("Bio too long '$bio'");
486 $location = $req->get_parameter('omb_listenee_location');
487 if ($location && mb_strlen($location) > 255) {
488 throw new OAuthException("Location too long '$location'");
490 $avatar = $req->get_parameter('omb_listenee_avatar');
492 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
493 throw new OAuthException("Invalid avatar URL '$avatar'");
495 $size = @getimagesize($avatar);
497 throw new OAuthException("Can't read avatar URL '$avatar'");
499 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
500 throw new OAuthException("Wrong size image at '$avatar'");
502 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
504 throw new OAuthException("Wrong image type for '$avatar'");
507 $callback = $req->get_parameter('oauth_callback');
508 if ($callback && !common_valid_http_url($callback)) {
509 throw new OAuthException("Invalid callback URL '$callback'");
511 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
512 throw new OAuthException("Callback URL '$callback' is for local site.");