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 $params = $this->getStoredParams();
39 $this->showForm($params, _('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'));
55 $this->validateRequest();
56 $this->storeParams($_GET);
57 $this->showForm($_GET);
58 } catch (OAuthException $e) {
60 $this->clientError($e->getMessage());
67 function showForm($params, $error=null)
69 $this->params = $params;
70 $this->error = $error;
76 return _('Authorize subscription');
79 function showPageNotice()
81 $this->element('p', null, _('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, '.
87 function showContent()
89 $params = $this->params;
91 $nickname = $params['omb_listenee_nickname'];
92 $profile = $params['omb_listenee_profile'];
93 $license = $params['omb_listenee_license'];
94 $fullname = $params['omb_listenee_fullname'];
95 $homepage = $params['omb_listenee_homepage'];
96 $bio = $params['omb_listenee_bio'];
97 $location = $params['omb_listenee_location'];
98 $avatar = $params['omb_listenee_avatar'];
100 $this->elementStart('div', array('class' => 'profile'));
101 $this->elementStart('div', 'entity_profile vcard');
102 $this->elementStart('a', array('href' => $profile,
105 $this->element('img', array('src' => $avatar,
106 'class' => 'photo avatar',
107 'width' => AVATAR_PROFILE_SIZE,
108 'height' => AVATAR_PROFILE_SIZE,
109 'alt' => $nickname));
111 $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname';
112 $this->elementStart('span', $hasFN);
113 $this->raw($nickname);
114 $this->elementEnd('span');
115 $this->elementEnd('a');
117 if (!is_null($fullname)) {
118 $this->elementStart('dl', 'entity_fn');
119 $this->elementStart('dd');
120 $this->elementStart('span', 'fn');
121 $this->raw($fullname);
122 $this->elementEnd('span');
123 $this->elementEnd('dd');
124 $this->elementEnd('dl');
126 if (!is_null($location)) {
127 $this->elementStart('dl', 'entity_location');
128 $this->element('dt', null, _('Location'));
129 $this->elementStart('dd', 'label');
130 $this->raw($location);
131 $this->elementEnd('dd');
132 $this->elementEnd('dl');
135 if (!is_null($homepage)) {
136 $this->elementStart('dl', 'entity_url');
137 $this->element('dt', null, _('URL'));
138 $this->elementStart('dd');
139 $this->elementStart('a', array('href' => $homepage,
141 $this->raw($homepage);
142 $this->elementEnd('a');
143 $this->elementEnd('dd');
144 $this->elementEnd('dl');
147 if (!is_null($bio)) {
148 $this->elementStart('dl', 'entity_note');
149 $this->element('dt', null, _('Note'));
150 $this->elementStart('dd', 'note');
152 $this->elementEnd('dd');
153 $this->elementEnd('dl');
156 if (!is_null($license)) {
157 $this->elementStart('dl', 'entity_license');
158 $this->element('dt', null, _('License'));
159 $this->elementStart('dd', 'license');
160 $this->element('a', array('href' => $license,
161 'class' => 'license'),
163 $this->elementEnd('dd');
164 $this->elementEnd('dl');
166 $this->elementEnd('div');
168 $this->elementStart('div', 'entity_actions');
169 $this->elementStart('ul');
170 $this->elementStart('li', 'entity_subscribe');
171 $this->elementStart('form', array('method' => 'post',
172 'id' => 'userauthorization',
173 'name' => 'userauthorization',
174 'action' => common_local_url('userauthorization')));
175 $this->hidden('token', common_session_token());
177 $this->submit('accept', _('Accept'), 'submit accept', null, _('Subscribe to this user'));
178 $this->submit('reject', _('Reject'), 'submit reject', null, _('Reject this subscription'));
179 $this->elementEnd('form');
180 $this->elementEnd('li');
181 $this->elementEnd('ul');
182 $this->elementEnd('div');
183 $this->elementEnd('div');
186 function sendAuthorization()
188 $params = $this->getStoredParams();
191 $this->clientError(_('No authorization request!'));
195 $callback = $params['oauth_callback'];
197 if ($this->arg('accept')) {
198 if (!$this->authorizeToken($params)) {
199 $this->clientError(_('Error authorizing token'));
201 if (!$this->saveRemoteProfile($params)) {
202 $this->clientError(_('Error saving remote profile'));
205 $this->showAcceptMessage($params['oauth_token']);
207 $newparams = array();
208 $newparams['oauth_token'] = $params['oauth_token'];
209 $newparams['omb_version'] = OMB_VERSION_01;
210 $user = User::staticGet('uri', $params['omb_listener']);
211 $profile = $user->getProfile();
213 common_log_db_error($user, 'SELECT', __FILE__);
214 $this->serverError(_('User without matching profile'));
217 $newparams['omb_listener_nickname'] = $user->nickname;
218 $newparams['omb_listener_profile'] = common_local_url('showstream',
219 array('nickname' => $user->nickname));
220 if (!is_null($profile->fullname)) {
221 $newparams['omb_listener_fullname'] = $profile->fullname;
223 if (!is_null($profile->homepage)) {
224 $newparams['omb_listener_homepage'] = $profile->homepage;
226 if (!is_null($profile->bio)) {
227 $newparams['omb_listener_bio'] = $profile->bio;
229 if (!is_null($profile->location)) {
230 $newparams['omb_listener_location'] = $profile->location;
232 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
234 $newparams['omb_listener_avatar'] = $avatar->url;
237 foreach ($newparams as $k => $v) {
238 $parts[] = $k . '=' . OAuthUtil::urlencode_rfc3986($v);
240 $query_string = implode('&', $parts);
241 $parsed = parse_url($callback);
242 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
243 common_redirect($url, 303);
247 $this->showRejectMessage();
249 # XXX: not 100% sure how to signal failure... just redirect without token?
250 common_redirect($callback, 303);
255 function authorizeToken(&$params)
257 $token_field = $params['oauth_token'];
259 $rt->tok = $token_field;
262 if ($rt->find(true)) {
263 $orig_rt = clone($rt);
264 $rt->state = 1; # Authorized but not used
265 if ($rt->update($orig_rt)) {
272 # XXX: refactor with similar code in finishremotesubscribe.php
274 function saveRemoteProfile(&$params)
276 # FIXME: we should really do this when the consumer comes
277 # back for an access token. If they never do, we've got stuff in a
280 $nickname = $params['omb_listenee_nickname'];
281 $fullname = $params['omb_listenee_fullname'];
282 $profile_url = $params['omb_listenee_profile'];
283 $homepage = $params['omb_listenee_homepage'];
284 $bio = $params['omb_listenee_bio'];
285 $location = $params['omb_listenee_location'];
286 $avatar_url = $params['omb_listenee_avatar'];
288 $listenee = $params['omb_listenee'];
289 $remote = Remote_profile::staticGet('uri', $listenee);
293 $profile = Profile::staticGet($remote->id);
294 $orig_remote = clone($remote);
295 $orig_profile = clone($profile);
298 $remote = new Remote_profile();
299 $remote->uri = $listenee;
300 $profile = new Profile();
303 $profile->nickname = $nickname;
304 $profile->profileurl = $profile_url;
306 if (!is_null($fullname)) {
307 $profile->fullname = $fullname;
309 if (!is_null($homepage)) {
310 $profile->homepage = $homepage;
312 if (!is_null($bio)) {
313 $profile->bio = $bio;
315 if (!is_null($location)) {
316 $profile->location = $location;
320 $profile->update($orig_profile);
322 $profile->created = DB_DataObject_Cast::dateTime(); # current time
323 $id = $profile->insert();
331 if (!$remote->update($orig_remote)) {
335 $remote->created = DB_DataObject_Cast::dateTime(); # current time
336 if (!$remote->insert()) {
342 if (!$this->addAvatar($profile, $avatar_url)) {
347 $user = common_current_user();
349 $sub = new Subscription();
350 $sub->subscriber = $user->id;
351 $sub->subscribed = $remote->id;
352 $sub->token = $params['oauth_token']; # NOTE: request token, not valid for use!
353 $sub->created = DB_DataObject_Cast::dateTime(); # current time
355 if (!$sub->insert()) {
362 function addAvatar($profile, $url)
364 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
365 copy($url, $temp_filename);
366 $imagefile = new ImageFile($profile->id, $temp_filename);
367 $filename = Avatar::filename($profile->id,
368 image_type_to_extension($imagefile->type),
371 rename($temp_filename, Avatar::path($filename));
372 return $profile->setOriginal($filename);
375 function showAcceptMessage($tok)
377 common_show_header(_('Subscription authorized'));
378 $this->element('p', null,
379 _('The subscription has been authorized, but no '.
380 'callback URL was passed. Check with the site\'s instructions for '.
381 'details on how to authorize the subscription. Your subscription token is:'));
382 $this->element('blockquote', 'token', $tok);
383 common_show_footer();
386 function showRejectMessage($tok)
388 common_show_header(_('Subscription rejected'));
389 $this->element('p', null,
390 _('The subscription has been rejected, but no '.
391 'callback URL was passed. Check with the site\'s instructions for '.
392 'details on how to fully reject the subscription.'));
393 common_show_footer();
396 function storeParams($params)
398 common_ensure_session();
399 $_SESSION['userauthorizationparams'] = $params;
402 function clearParams()
404 common_ensure_session();
405 unset($_SESSION['userauthorizationparams']);
408 function getStoredParams()
410 common_ensure_session();
411 $params = $_SESSION['userauthorizationparams'];
415 # Throws an OAuthException if anything goes wrong
417 function validateRequest()
420 TODO: If no token is passed the user should get a prompt to enter it
421 according to OAuth Core 1.0 */
423 $t->tok = $_GET['oauth_token'];
425 if (!$t->find(true)) {
426 throw new OAuthException("Invalid request token: " . $_GET['oauth_token']);
429 $this->validateOmb();
433 function validateOmb()
435 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
436 'omb_listenee_profile', 'omb_listenee_nickname',
437 'omb_listenee_license') as $param)
439 if (!isset($_GET[$param]) || is_null($_GET[$param])) {
440 throw new OAuthException("Required parameter '$param' not found");
444 $version = $_GET['omb_version'];
445 if ($version != OMB_VERSION_01) {
446 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
448 $listener = $_GET['omb_listener'];
449 $user = User::staticGet('uri', $listener);
451 throw new OAuthException("Listener URI '$listener' not found here");
453 $cur = common_current_user();
454 if ($cur->id != $user->id) {
455 throw new OAuthException("Can't add for another user!");
457 $listenee = $_GET['omb_listenee'];
458 if (!Validate::uri($listenee) &&
459 !common_valid_tag($listenee)) {
460 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
462 if (strlen($listenee) > 255) {
463 throw new OAuthException("Listenee URI '$listenee' too long");
466 $other = User::staticGet('uri', $listenee);
468 throw new OAuthException("Listenee URI '$listenee' is local user");
471 $remote = Remote_profile::staticGet('uri', $listenee);
473 $sub = new Subscription();
474 $sub->subscriber = $user->id;
475 $sub->subscribed = $remote->id;
476 if ($sub->find(true)) {
477 throw new OAuthException("Already subscribed to user!");
480 $nickname = $_GET['omb_listenee_nickname'];
481 if (!Validate::string($nickname, array('min_length' => 1,
483 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
484 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
486 $profile = $_GET['omb_listenee_profile'];
487 if (!common_valid_http_url($profile)) {
488 throw new OAuthException("Invalid profile URL '$profile'.");
491 if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
492 throw new OAuthException("Profile URL '$profile' is for a local user.");
495 $license = $_GET['omb_listenee_license'];
496 if (!common_valid_http_url($license)) {
497 throw new OAuthException("Invalid license URL '$license'.");
499 $site_license = common_config('license', 'url');
500 if (!common_compatible_license($license, $site_license)) {
501 throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
504 $fullname = $_GET['omb_listenee_fullname'];
505 if ($fullname && mb_strlen($fullname) > 255) {
506 throw new OAuthException("Full name '$fullname' too long.");
508 $homepage = $_GET['omb_listenee_homepage'];
509 if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
510 throw new OAuthException("Invalid homepage '$homepage'");
512 $bio = $_GET['omb_listenee_bio'];
513 if ($bio && mb_strlen($bio) > 140) {
514 throw new OAuthException("Bio too long '$bio'");
516 $location = $_GET['omb_listenee_location'];
517 if ($location && mb_strlen($location) > 255) {
518 throw new OAuthException("Location too long '$location'");
520 $avatar = $_GET['omb_listenee_avatar'];
522 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
523 throw new OAuthException("Invalid avatar URL '$avatar'");
525 $size = @getimagesize($avatar);
527 throw new OAuthException("Can't read avatar URL '$avatar'");
529 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
530 throw new OAuthException("Wrong size image at '$avatar'");
532 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
534 throw new OAuthException("Wrong image type for '$avatar'");
537 $callback = $_GET['oauth_callback'];
538 if ($callback && !common_valid_http_url($callback)) {
539 throw new OAuthException("Invalid callback URL '$callback'");
541 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
542 throw new OAuthException("Callback URL '$callback' is for local site.");