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 'class' => 'form_user_authorization',
174 'name' => 'userauthorization',
175 'action' => common_local_url('userauthorization')));
176 $this->hidden('token', common_session_token());
178 $this->submit('accept', _('Accept'), 'submit accept', null, _('Subscribe to this user'));
179 $this->submit('reject', _('Reject'), 'submit reject', null, _('Reject this subscription'));
180 $this->elementEnd('form');
181 $this->elementEnd('li');
182 $this->elementEnd('ul');
183 $this->elementEnd('div');
184 $this->elementEnd('div');
187 function sendAuthorization()
189 $params = $this->getStoredParams();
192 $this->clientError(_('No authorization request!'));
196 $callback = $params['oauth_callback'];
198 if ($this->arg('accept')) {
199 if (!$this->authorizeToken($params)) {
200 $this->clientError(_('Error authorizing token'));
202 if (!$this->saveRemoteProfile($params)) {
203 $this->clientError(_('Error saving remote profile'));
206 $this->showAcceptMessage($params['oauth_token']);
208 $newparams = array();
209 $newparams['oauth_token'] = $params['oauth_token'];
210 $newparams['omb_version'] = OMB_VERSION_01;
211 $user = User::staticGet('uri', $params['omb_listener']);
212 $profile = $user->getProfile();
214 common_log_db_error($user, 'SELECT', __FILE__);
215 $this->serverError(_('User without matching profile'));
218 $newparams['omb_listener_nickname'] = $user->nickname;
219 $newparams['omb_listener_profile'] = common_local_url('showstream',
220 array('nickname' => $user->nickname));
221 if (!is_null($profile->fullname)) {
222 $newparams['omb_listener_fullname'] = $profile->fullname;
224 if (!is_null($profile->homepage)) {
225 $newparams['omb_listener_homepage'] = $profile->homepage;
227 if (!is_null($profile->bio)) {
228 $newparams['omb_listener_bio'] = $profile->bio;
230 if (!is_null($profile->location)) {
231 $newparams['omb_listener_location'] = $profile->location;
233 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
235 $newparams['omb_listener_avatar'] = $avatar->url;
238 foreach ($newparams as $k => $v) {
239 $parts[] = $k . '=' . OAuthUtil::urlencode_rfc3986($v);
241 $query_string = implode('&', $parts);
242 $parsed = parse_url($callback);
243 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
244 common_redirect($url, 303);
248 $this->showRejectMessage();
250 # XXX: not 100% sure how to signal failure... just redirect without token?
251 common_redirect($callback, 303);
256 function authorizeToken(&$params)
258 $token_field = $params['oauth_token'];
260 $rt->tok = $token_field;
263 if ($rt->find(true)) {
264 $orig_rt = clone($rt);
265 $rt->state = 1; # Authorized but not used
266 if ($rt->update($orig_rt)) {
273 # XXX: refactor with similar code in finishremotesubscribe.php
275 function saveRemoteProfile(&$params)
277 # FIXME: we should really do this when the consumer comes
278 # back for an access token. If they never do, we've got stuff in a
281 $nickname = $params['omb_listenee_nickname'];
282 $fullname = $params['omb_listenee_fullname'];
283 $profile_url = $params['omb_listenee_profile'];
284 $homepage = $params['omb_listenee_homepage'];
285 $bio = $params['omb_listenee_bio'];
286 $location = $params['omb_listenee_location'];
287 $avatar_url = $params['omb_listenee_avatar'];
289 $listenee = $params['omb_listenee'];
290 $remote = Remote_profile::staticGet('uri', $listenee);
294 $profile = Profile::staticGet($remote->id);
295 $orig_remote = clone($remote);
296 $orig_profile = clone($profile);
299 $remote = new Remote_profile();
300 $remote->uri = $listenee;
301 $profile = new Profile();
304 $profile->nickname = $nickname;
305 $profile->profileurl = $profile_url;
307 if (!is_null($fullname)) {
308 $profile->fullname = $fullname;
310 if (!is_null($homepage)) {
311 $profile->homepage = $homepage;
313 if (!is_null($bio)) {
314 $profile->bio = $bio;
316 if (!is_null($location)) {
317 $profile->location = $location;
321 $profile->update($orig_profile);
323 $profile->created = DB_DataObject_Cast::dateTime(); # current time
324 $id = $profile->insert();
332 if (!$remote->update($orig_remote)) {
336 $remote->created = DB_DataObject_Cast::dateTime(); # current time
337 if (!$remote->insert()) {
343 if (!$this->addAvatar($profile, $avatar_url)) {
348 $user = common_current_user();
350 $sub = new Subscription();
351 $sub->subscriber = $user->id;
352 $sub->subscribed = $remote->id;
353 $sub->token = $params['oauth_token']; # NOTE: request token, not valid for use!
354 $sub->created = DB_DataObject_Cast::dateTime(); # current time
356 if (!$sub->insert()) {
363 function addAvatar($profile, $url)
365 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
366 copy($url, $temp_filename);
367 $imagefile = new ImageFile($profile->id, $temp_filename);
368 $filename = Avatar::filename($profile->id,
369 image_type_to_extension($imagefile->type),
372 rename($temp_filename, Avatar::path($filename));
373 return $profile->setOriginal($filename);
376 function showAcceptMessage($tok)
378 common_show_header(_('Subscription authorized'));
379 $this->element('p', null,
380 _('The subscription has been authorized, but no '.
381 'callback URL was passed. Check with the site\'s instructions for '.
382 'details on how to authorize the subscription. Your subscription token is:'));
383 $this->element('blockquote', 'token', $tok);
384 common_show_footer();
387 function showRejectMessage($tok)
389 common_show_header(_('Subscription rejected'));
390 $this->element('p', null,
391 _('The subscription has been rejected, but no '.
392 'callback URL was passed. Check with the site\'s instructions for '.
393 'details on how to fully reject the subscription.'));
394 common_show_footer();
397 function storeParams($params)
399 common_ensure_session();
400 $_SESSION['userauthorizationparams'] = $params;
403 function clearParams()
405 common_ensure_session();
406 unset($_SESSION['userauthorizationparams']);
409 function getStoredParams()
411 common_ensure_session();
412 $params = $_SESSION['userauthorizationparams'];
416 # Throws an OAuthException if anything goes wrong
418 function validateRequest()
421 TODO: If no token is passed the user should get a prompt to enter it
422 according to OAuth Core 1.0 */
424 $t->tok = $_GET['oauth_token'];
426 if (!$t->find(true)) {
427 throw new OAuthException("Invalid request token: " . $_GET['oauth_token']);
430 $this->validateOmb();
434 function validateOmb()
436 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
437 'omb_listenee_profile', 'omb_listenee_nickname',
438 'omb_listenee_license') as $param)
440 if (!isset($_GET[$param]) || is_null($_GET[$param])) {
441 throw new OAuthException("Required parameter '$param' not found");
445 $version = $_GET['omb_version'];
446 if ($version != OMB_VERSION_01) {
447 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
449 $listener = $_GET['omb_listener'];
450 $user = User::staticGet('uri', $listener);
452 throw new OAuthException("Listener URI '$listener' not found here");
454 $cur = common_current_user();
455 if ($cur->id != $user->id) {
456 throw new OAuthException("Can't add for another user!");
458 $listenee = $_GET['omb_listenee'];
459 if (!Validate::uri($listenee) &&
460 !common_valid_tag($listenee)) {
461 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
463 if (strlen($listenee) > 255) {
464 throw new OAuthException("Listenee URI '$listenee' too long");
467 $other = User::staticGet('uri', $listenee);
469 throw new OAuthException("Listenee URI '$listenee' is local user");
472 $remote = Remote_profile::staticGet('uri', $listenee);
474 $sub = new Subscription();
475 $sub->subscriber = $user->id;
476 $sub->subscribed = $remote->id;
477 if ($sub->find(true)) {
478 throw new OAuthException("Already subscribed to user!");
481 $nickname = $_GET['omb_listenee_nickname'];
482 if (!Validate::string($nickname, array('min_length' => 1,
484 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
485 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
487 $profile = $_GET['omb_listenee_profile'];
488 if (!common_valid_http_url($profile)) {
489 throw new OAuthException("Invalid profile URL '$profile'.");
492 if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
493 throw new OAuthException("Profile URL '$profile' is for a local user.");
496 $license = $_GET['omb_listenee_license'];
497 if (!common_valid_http_url($license)) {
498 throw new OAuthException("Invalid license URL '$license'.");
500 $site_license = common_config('license', 'url');
501 if (!common_compatible_license($license, $site_license)) {
502 throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
505 $fullname = $_GET['omb_listenee_fullname'];
506 if ($fullname && mb_strlen($fullname) > 255) {
507 throw new OAuthException("Full name '$fullname' too long.");
509 $homepage = $_GET['omb_listenee_homepage'];
510 if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
511 throw new OAuthException("Invalid homepage '$homepage'");
513 $bio = $_GET['omb_listenee_bio'];
514 if ($bio && mb_strlen($bio) > 140) {
515 throw new OAuthException("Bio too long '$bio'");
517 $location = $_GET['omb_listenee_location'];
518 if ($location && mb_strlen($location) > 255) {
519 throw new OAuthException("Location too long '$location'");
521 $avatar = $_GET['omb_listenee_avatar'];
523 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
524 throw new OAuthException("Invalid avatar URL '$avatar'");
526 $size = @getimagesize($avatar);
528 throw new OAuthException("Can't read avatar URL '$avatar'");
530 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
531 throw new OAuthException("Wrong size image at '$avatar'");
533 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
535 throw new OAuthException("Wrong image type for '$avatar'");
538 $callback = $_GET['oauth_callback'];
539 if ($callback && !common_valid_http_url($callback)) {
540 throw new OAuthException("Invalid callback URL '$callback'");
542 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
543 throw new OAuthException("Callback URL '$callback' is for local site.");