3 * StatusNet - a distributed open-source microblogging tool
4 * Copyright (C) 2008, 2009, StatusNet, 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 if (!common_config('site', 'openidonly')) {
51 common_redirect(common_local_url('login'));
53 common_redirect(common_local_url('openidlogin'));
59 $this->validateRequest();
60 $this->storeParams($_GET);
61 $this->showForm($_GET);
62 } catch (OAuthException $e) {
64 $this->clientError($e->getMessage());
71 function showForm($params, $error=null)
73 $this->params = $params;
74 $this->error = $error;
80 return _('Authorize subscription');
83 function showPageNotice()
85 $this->element('p', null, _('Please check these details to make sure '.
86 'that you want to subscribe to this user\'s notices. '.
87 'If you didn\'t just ask to subscribe to someone\'s notices, '.
91 function showContent()
93 $params = $this->params;
95 $nickname = $params['omb_listenee_nickname'];
96 $profile = $params['omb_listenee_profile'];
97 $license = $params['omb_listenee_license'];
98 $fullname = $params['omb_listenee_fullname'];
99 $homepage = $params['omb_listenee_homepage'];
100 $bio = $params['omb_listenee_bio'];
101 $location = $params['omb_listenee_location'];
102 $avatar = $params['omb_listenee_avatar'];
104 $this->elementStart('div', array('class' => 'profile'));
105 $this->elementStart('div', 'entity_profile vcard');
106 $this->elementStart('a', array('href' => $profile,
109 $this->element('img', array('src' => $avatar,
110 'class' => 'photo avatar',
111 'width' => AVATAR_PROFILE_SIZE,
112 'height' => AVATAR_PROFILE_SIZE,
113 'alt' => $nickname));
115 $hasFN = ($fullname !== '') ? 'nickname' : 'fn nickname';
116 $this->elementStart('span', $hasFN);
117 $this->raw($nickname);
118 $this->elementEnd('span');
119 $this->elementEnd('a');
121 if (!is_null($fullname)) {
122 $this->elementStart('dl', 'entity_fn');
123 $this->elementStart('dd');
124 $this->elementStart('span', 'fn');
125 $this->raw($fullname);
126 $this->elementEnd('span');
127 $this->elementEnd('dd');
128 $this->elementEnd('dl');
130 if (!is_null($location)) {
131 $this->elementStart('dl', 'entity_location');
132 $this->element('dt', null, _('Location'));
133 $this->elementStart('dd', 'label');
134 $this->raw($location);
135 $this->elementEnd('dd');
136 $this->elementEnd('dl');
139 if (!is_null($homepage)) {
140 $this->elementStart('dl', 'entity_url');
141 $this->element('dt', null, _('URL'));
142 $this->elementStart('dd');
143 $this->elementStart('a', array('href' => $homepage,
145 $this->raw($homepage);
146 $this->elementEnd('a');
147 $this->elementEnd('dd');
148 $this->elementEnd('dl');
151 if (!is_null($bio)) {
152 $this->elementStart('dl', 'entity_note');
153 $this->element('dt', null, _('Note'));
154 $this->elementStart('dd', 'note');
156 $this->elementEnd('dd');
157 $this->elementEnd('dl');
160 if (!is_null($license)) {
161 $this->elementStart('dl', 'entity_license');
162 $this->element('dt', null, _('License'));
163 $this->elementStart('dd', 'license');
164 $this->element('a', array('href' => $license,
165 'class' => 'license'),
167 $this->elementEnd('dd');
168 $this->elementEnd('dl');
170 $this->elementEnd('div');
172 $this->elementStart('div', 'entity_actions');
173 $this->elementStart('ul');
174 $this->elementStart('li', 'entity_subscribe');
175 $this->elementStart('form', array('method' => 'post',
176 'id' => 'userauthorization',
177 'class' => 'form_user_authorization',
178 'name' => 'userauthorization',
179 'action' => common_local_url('userauthorization')));
180 $this->hidden('token', common_session_token());
182 $this->submit('accept', _('Accept'), 'submit accept', null, _('Subscribe to this user'));
183 $this->submit('reject', _('Reject'), 'submit reject', null, _('Reject this subscription'));
184 $this->elementEnd('form');
185 $this->elementEnd('li');
186 $this->elementEnd('ul');
187 $this->elementEnd('div');
188 $this->elementEnd('div');
191 function sendAuthorization()
193 $params = $this->getStoredParams();
196 $this->clientError(_('No authorization request!'));
200 $callback = $params['oauth_callback'];
202 if ($this->arg('accept')) {
203 if (!$this->authorizeToken($params)) {
204 $this->clientError(_('Error authorizing token'));
206 if (!$this->saveRemoteProfile($params)) {
207 $this->clientError(_('Error saving remote profile'));
210 $this->showAcceptMessage($params['oauth_token']);
212 $newparams = array();
213 $newparams['oauth_token'] = $params['oauth_token'];
214 $newparams['omb_version'] = OMB_VERSION_01;
215 $user = User::staticGet('uri', $params['omb_listener']);
216 $profile = $user->getProfile();
218 common_log_db_error($user, 'SELECT', __FILE__);
219 $this->serverError(_('User without matching profile'));
222 $newparams['omb_listener_nickname'] = $user->nickname;
223 $newparams['omb_listener_profile'] = common_local_url('showstream',
224 array('nickname' => $user->nickname));
225 if (!is_null($profile->fullname)) {
226 $newparams['omb_listener_fullname'] = $profile->fullname;
228 if (!is_null($profile->homepage)) {
229 $newparams['omb_listener_homepage'] = $profile->homepage;
231 if (!is_null($profile->bio)) {
232 $newparams['omb_listener_bio'] = $profile->bio;
234 if (!is_null($profile->location)) {
235 $newparams['omb_listener_location'] = $profile->location;
237 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
239 $newparams['omb_listener_avatar'] = $avatar->url;
242 foreach ($newparams as $k => $v) {
243 $parts[] = $k . '=' . OAuthUtil::urlencode_rfc3986($v);
245 $query_string = implode('&', $parts);
246 $parsed = parse_url($callback);
247 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
248 common_redirect($url, 303);
252 $this->showRejectMessage();
254 # XXX: not 100% sure how to signal failure... just redirect without token?
255 common_redirect($callback, 303);
260 function authorizeToken(&$params)
262 $token_field = $params['oauth_token'];
264 $rt->tok = $token_field;
267 if ($rt->find(true)) {
268 $orig_rt = clone($rt);
269 $rt->state = 1; # Authorized but not used
270 if ($rt->update($orig_rt)) {
277 # XXX: refactor with similar code in finishremotesubscribe.php
279 function saveRemoteProfile(&$params)
281 # FIXME: we should really do this when the consumer comes
282 # back for an access token. If they never do, we've got stuff in a
285 $nickname = $params['omb_listenee_nickname'];
286 $fullname = $params['omb_listenee_fullname'];
287 $profile_url = $params['omb_listenee_profile'];
288 $homepage = $params['omb_listenee_homepage'];
289 $bio = $params['omb_listenee_bio'];
290 $location = $params['omb_listenee_location'];
291 $avatar_url = $params['omb_listenee_avatar'];
293 $listenee = $params['omb_listenee'];
294 $remote = Remote_profile::staticGet('uri', $listenee);
298 $profile = Profile::staticGet($remote->id);
299 $orig_remote = clone($remote);
300 $orig_profile = clone($profile);
303 $remote = new Remote_profile();
304 $remote->uri = $listenee;
305 $profile = new Profile();
308 $profile->nickname = $nickname;
309 $profile->profileurl = $profile_url;
311 if (!is_null($fullname)) {
312 $profile->fullname = $fullname;
314 if (!is_null($homepage)) {
315 $profile->homepage = $homepage;
317 if (!is_null($bio)) {
318 $profile->bio = $bio;
320 if (!is_null($location)) {
321 $profile->location = $location;
325 $profile->update($orig_profile);
327 $profile->created = DB_DataObject_Cast::dateTime(); # current time
328 $id = $profile->insert();
336 if (!$remote->update($orig_remote)) {
340 $remote->created = DB_DataObject_Cast::dateTime(); # current time
341 if (!$remote->insert()) {
347 if (!$this->addAvatar($profile, $avatar_url)) {
352 $user = common_current_user();
354 $sub = new Subscription();
355 $sub->subscriber = $user->id;
356 $sub->subscribed = $remote->id;
357 $sub->token = $params['oauth_token']; # NOTE: request token, not valid for use!
358 $sub->created = DB_DataObject_Cast::dateTime(); # current time
360 if (!$sub->insert()) {
367 function addAvatar($profile, $url)
369 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
370 copy($url, $temp_filename);
371 $imagefile = new ImageFile($profile->id, $temp_filename);
372 $filename = Avatar::filename($profile->id,
373 image_type_to_extension($imagefile->type),
376 rename($temp_filename, Avatar::path($filename));
377 return $profile->setOriginal($filename);
380 function showAcceptMessage($tok)
382 common_show_header(_('Subscription authorized'));
383 $this->element('p', null,
384 _('The subscription has been authorized, but no '.
385 'callback URL was passed. Check with the site\'s instructions for '.
386 'details on how to authorize the subscription. Your subscription token is:'));
387 $this->element('blockquote', 'token', $tok);
388 common_show_footer();
391 function showRejectMessage($tok)
393 common_show_header(_('Subscription rejected'));
394 $this->element('p', null,
395 _('The subscription has been rejected, but no '.
396 'callback URL was passed. Check with the site\'s instructions for '.
397 'details on how to fully reject the subscription.'));
398 common_show_footer();
401 function storeParams($params)
403 common_ensure_session();
404 $_SESSION['userauthorizationparams'] = $params;
407 function clearParams()
409 common_ensure_session();
410 unset($_SESSION['userauthorizationparams']);
413 function getStoredParams()
415 common_ensure_session();
416 $params = $_SESSION['userauthorizationparams'];
420 # Throws an OAuthException if anything goes wrong
422 function validateRequest()
425 TODO: If no token is passed the user should get a prompt to enter it
426 according to OAuth Core 1.0 */
428 $t->tok = $_GET['oauth_token'];
430 if (!$t->find(true)) {
431 throw new OAuthException("Invalid request token: " . $_GET['oauth_token']);
434 $this->validateOmb();
438 function validateOmb()
440 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
441 'omb_listenee_profile', 'omb_listenee_nickname',
442 'omb_listenee_license') as $param)
444 if (!isset($_GET[$param]) || is_null($_GET[$param])) {
445 throw new OAuthException("Required parameter '$param' not found");
449 $version = $_GET['omb_version'];
450 if ($version != OMB_VERSION_01) {
451 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
453 $listener = $_GET['omb_listener'];
454 $user = User::staticGet('uri', $listener);
456 throw new OAuthException("Listener URI '$listener' not found here");
458 $cur = common_current_user();
459 if ($cur->id != $user->id) {
460 throw new OAuthException("Can't add for another user!");
462 $listenee = $_GET['omb_listenee'];
463 if (!Validate::uri($listenee) &&
464 !common_valid_tag($listenee)) {
465 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
467 if (strlen($listenee) > 255) {
468 throw new OAuthException("Listenee URI '$listenee' too long");
471 $other = User::staticGet('uri', $listenee);
473 throw new OAuthException("Listenee URI '$listenee' is local user");
476 $remote = Remote_profile::staticGet('uri', $listenee);
478 $sub = new Subscription();
479 $sub->subscriber = $user->id;
480 $sub->subscribed = $remote->id;
481 if ($sub->find(true)) {
482 throw new OAuthException("Already subscribed to user!");
485 $nickname = $_GET['omb_listenee_nickname'];
486 if (!Validate::string($nickname, array('min_length' => 1,
488 'format' => NICKNAME_FMT))) {
489 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
491 $profile = $_GET['omb_listenee_profile'];
492 if (!common_valid_http_url($profile)) {
493 throw new OAuthException("Invalid profile URL '$profile'.");
496 if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
497 throw new OAuthException("Profile URL '$profile' is for a local user.");
500 $license = $_GET['omb_listenee_license'];
501 if (!common_valid_http_url($license)) {
502 throw new OAuthException("Invalid license URL '$license'.");
504 $site_license = common_config('license', 'url');
505 if (!common_compatible_license($license, $site_license)) {
506 throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
509 $fullname = $_GET['omb_listenee_fullname'];
510 if ($fullname && mb_strlen($fullname) > 255) {
511 throw new OAuthException("Full name '$fullname' too long.");
513 $homepage = $_GET['omb_listenee_homepage'];
514 if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
515 throw new OAuthException("Invalid homepage '$homepage'");
517 $bio = $_GET['omb_listenee_bio'];
518 if ($bio && mb_strlen($bio) > 140) {
519 throw new OAuthException("Bio too long '$bio'");
521 $location = $_GET['omb_listenee_location'];
522 if ($location && mb_strlen($location) > 255) {
523 throw new OAuthException("Location too long '$location'");
525 $avatar = $_GET['omb_listenee_avatar'];
527 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
528 throw new OAuthException("Invalid avatar URL '$avatar'");
530 $size = @getimagesize($avatar);
532 throw new OAuthException("Can't read avatar URL '$avatar'");
534 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
535 throw new OAuthException("Wrong size image at '$avatar'");
537 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
539 throw new OAuthException("Wrong image type for '$avatar'");
542 $callback = $_GET['oauth_callback'];
543 if ($callback && !common_valid_http_url($callback)) {
544 throw new OAuthException("Invalid callback URL '$callback'");
546 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
547 throw new OAuthException("Callback URL '$callback' is for local site.");