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', 'profile');
102 $this->element('img', array('src' => $avatar,
104 'width' => AVATAR_PROFILE_SIZE,
105 'height' => AVATAR_PROFILE_SIZE,
106 'alt' => $nickname));
108 $this->element('a', array('href' => $profile,
109 'class' => 'external profile nickname'),
111 if (!is_null($fullname)) {
112 $this->elementStart('div', 'fullname');
113 if (!is_null($homepage)) {
114 $this->element('a', array('href' => $homepage),
117 $this->text($fullname);
119 $this->elementEnd('div');
121 if (!is_null($location)) {
122 $this->element('div', 'location', $location);
124 if (!is_null($bio)) {
125 $this->element('div', 'bio', $bio);
127 $this->elementStart('div', 'license');
128 $this->element('a', array('href' => $license,
129 'class' => 'license'),
131 $this->elementEnd('div');
132 $this->elementEnd('div');
133 $this->elementStart('form', array('method' => 'post',
134 'id' => 'userauthorization',
135 'name' => 'userauthorization',
136 'action' => common_local_url('userauthorization')));
137 $this->hidden('token', common_session_token());
138 $this->submit('accept', _('Accept'));
139 $this->submit('reject', _('Reject'));
140 $this->elementEnd('form');
143 function sendAuthorization()
145 $params = $this->getStoredParams();
148 $this->clientError(_('No authorization request!'));
152 $callback = $params['oauth_callback'];
154 if ($this->arg('accept')) {
155 if (!$this->authorizeToken($params)) {
156 $this->clientError(_('Error authorizing token'));
158 if (!$this->saveRemoteProfile($params)) {
159 $this->clientError(_('Error saving remote profile'));
162 $this->showAcceptMessage($params['oauth_token']);
164 $newparams = array();
165 $newparams['oauth_token'] = $params['oauth_token'];
166 $newparams['omb_version'] = OMB_VERSION_01;
167 $user = User::staticGet('uri', $params['omb_listener']);
168 $profile = $user->getProfile();
170 common_log_db_error($user, 'SELECT', __FILE__);
171 $this->serverError(_('User without matching profile'));
174 $newparams['omb_listener_nickname'] = $user->nickname;
175 $newparams['omb_listener_profile'] = common_local_url('showstream',
176 array('nickname' => $user->nickname));
177 if (!is_null($profile->fullname)) {
178 $newparams['omb_listener_fullname'] = $profile->fullname;
180 if (!is_null($profile->homepage)) {
181 $newparams['omb_listener_homepage'] = $profile->homepage;
183 if (!is_null($profile->bio)) {
184 $newparams['omb_listener_bio'] = $profile->bio;
186 if (!is_null($profile->location)) {
187 $newparams['omb_listener_location'] = $profile->location;
189 $avatar = $profile->getAvatar(AVATAR_PROFILE_SIZE);
191 $newparams['omb_listener_avatar'] = $avatar->url;
194 foreach ($newparams as $k => $v) {
195 $parts[] = $k . '=' . OAuthUtil::urlencode_rfc3986($v);
197 $query_string = implode('&', $parts);
198 $parsed = parse_url($callback);
199 $url = $callback . (($parsed['query']) ? '&' : '?') . $query_string;
200 common_redirect($url, 303);
204 $this->showRejectMessage();
206 # XXX: not 100% sure how to signal failure... just redirect without token?
207 common_redirect($callback, 303);
212 function authorizeToken(&$params)
214 $token_field = $params['oauth_token'];
216 $rt->tok = $token_field;
219 if ($rt->find(true)) {
220 $orig_rt = clone($rt);
221 $rt->state = 1; # Authorized but not used
222 if ($rt->update($orig_rt)) {
229 # XXX: refactor with similar code in finishremotesubscribe.php
231 function saveRemoteProfile(&$params)
233 # FIXME: we should really do this when the consumer comes
234 # back for an access token. If they never do, we've got stuff in a
237 $nickname = $params['omb_listenee_nickname'];
238 $fullname = $params['omb_listenee_fullname'];
239 $profile_url = $params['omb_listenee_profile'];
240 $homepage = $params['omb_listenee_homepage'];
241 $bio = $params['omb_listenee_bio'];
242 $location = $params['omb_listenee_location'];
243 $avatar_url = $params['omb_listenee_avatar'];
245 $listenee = $params['omb_listenee'];
246 $remote = Remote_profile::staticGet('uri', $listenee);
250 $profile = Profile::staticGet($remote->id);
251 $orig_remote = clone($remote);
252 $orig_profile = clone($profile);
255 $remote = new Remote_profile();
256 $remote->uri = $listenee;
257 $profile = new Profile();
260 $profile->nickname = $nickname;
261 $profile->profileurl = $profile_url;
263 if (!is_null($fullname)) {
264 $profile->fullname = $fullname;
266 if (!is_null($homepage)) {
267 $profile->homepage = $homepage;
269 if (!is_null($bio)) {
270 $profile->bio = $bio;
272 if (!is_null($location)) {
273 $profile->location = $location;
277 $profile->update($orig_profile);
279 $profile->created = DB_DataObject_Cast::dateTime(); # current time
280 $id = $profile->insert();
288 if (!$remote->update($orig_remote)) {
292 $remote->created = DB_DataObject_Cast::dateTime(); # current time
293 if (!$remote->insert()) {
299 if (!$this->addAvatar($profile, $avatar_url)) {
304 $user = common_current_user();
306 $sub = new Subscription();
307 $sub->subscriber = $user->id;
308 $sub->subscribed = $remote->id;
309 $sub->token = $params['oauth_token']; # NOTE: request token, not valid for use!
310 $sub->created = DB_DataObject_Cast::dateTime(); # current time
312 if (!$sub->insert()) {
319 function addAvatar($profile, $url)
321 $temp_filename = tempnam(sys_get_temp_dir(), 'listenee_avatar');
322 copy($url, $temp_filename);
323 $imagefile = new ImageFile($profile->id, $temp_filename);
324 $filename = Avatar::filename($profile->id,
325 image_type_to_extension($imagefile->type),
328 rename($temp_filename, Avatar::path($filename));
329 return $profile->setOriginal($filename);
332 function showAcceptMessage($tok)
334 common_show_header(_('Subscription authorized'));
335 $this->element('p', null,
336 _('The subscription has been authorized, but no '.
337 'callback URL was passed. Check with the site\'s instructions for '.
338 'details on how to authorize the subscription. Your subscription token is:'));
339 $this->element('blockquote', 'token', $tok);
340 common_show_footer();
343 function showRejectMessage($tok)
345 common_show_header(_('Subscription rejected'));
346 $this->element('p', null,
347 _('The subscription has been rejected, but no '.
348 'callback URL was passed. Check with the site\'s instructions for '.
349 'details on how to fully reject the subscription.'));
350 common_show_footer();
353 function storeParams($params)
355 common_ensure_session();
356 $_SESSION['userauthorizationparams'] = $params;
359 function clearParams()
361 common_ensure_session();
362 unset($_SESSION['userauthorizationparams']);
365 function getStoredParams()
367 common_ensure_session();
368 $params = $_SESSION['userauthorizationparams'];
372 # Throws an OAuthException if anything goes wrong
374 function validateRequest()
377 TODO: If no token is passed the user should get a prompt to enter it
378 according to OAuth Core 1.0 */
380 $t->tok = $_GET['oauth_token'];
382 if (!$t->find(true)) {
383 throw new OAuthException("Invalid request token: " . $_GET['oauth_token']);
386 $this->validateOmb();
390 function validateOmb()
392 foreach (array('omb_version', 'omb_listener', 'omb_listenee',
393 'omb_listenee_profile', 'omb_listenee_nickname',
394 'omb_listenee_license') as $param)
396 if (!isset($_GET[$param]) || is_null($_GET[$param])) {
397 throw new OAuthException("Required parameter '$param' not found");
401 $version = $_GET['omb_version'];
402 if ($version != OMB_VERSION_01) {
403 throw new OAuthException("OpenMicroBlogging version '$version' not supported");
405 $listener = $_GET['omb_listener'];
406 $user = User::staticGet('uri', $listener);
408 throw new OAuthException("Listener URI '$listener' not found here");
410 $cur = common_current_user();
411 if ($cur->id != $user->id) {
412 throw new OAuthException("Can't add for another user!");
414 $listenee = $_GET['omb_listenee'];
415 if (!Validate::uri($listenee) &&
416 !common_valid_tag($listenee)) {
417 throw new OAuthException("Listenee URI '$listenee' not a recognizable URI");
419 if (strlen($listenee) > 255) {
420 throw new OAuthException("Listenee URI '$listenee' too long");
423 $other = User::staticGet('uri', $listenee);
425 throw new OAuthException("Listenee URI '$listenee' is local user");
428 $remote = Remote_profile::staticGet('uri', $listenee);
430 $sub = new Subscription();
431 $sub->subscriber = $user->id;
432 $sub->subscribed = $remote->id;
433 if ($sub->find(true)) {
434 throw new OAuthException("Already subscribed to user!");
437 $nickname = $_GET['omb_listenee_nickname'];
438 if (!Validate::string($nickname, array('min_length' => 1,
440 'format' => VALIDATE_NUM . VALIDATE_ALPHA_LOWER))) {
441 throw new OAuthException('Nickname must have only letters and numbers and no spaces.');
443 $profile = $_GET['omb_listenee_profile'];
444 if (!common_valid_http_url($profile)) {
445 throw new OAuthException("Invalid profile URL '$profile'.");
448 if ($profile == common_local_url('showstream', array('nickname' => $nickname))) {
449 throw new OAuthException("Profile URL '$profile' is for a local user.");
452 $license = $_GET['omb_listenee_license'];
453 if (!common_valid_http_url($license)) {
454 throw new OAuthException("Invalid license URL '$license'.");
456 $site_license = common_config('license', 'url');
457 if (!common_compatible_license($license, $site_license)) {
458 throw new OAuthException("Listenee stream license '$license' not compatible with site license '$site_license'.");
461 $fullname = $_GET['omb_listenee_fullname'];
462 if ($fullname && mb_strlen($fullname) > 255) {
463 throw new OAuthException("Full name '$fullname' too long.");
465 $homepage = $_GET['omb_listenee_homepage'];
466 if ($homepage && (!common_valid_http_url($homepage) || mb_strlen($homepage) > 255)) {
467 throw new OAuthException("Invalid homepage '$homepage'");
469 $bio = $_GET['omb_listenee_bio'];
470 if ($bio && mb_strlen($bio) > 140) {
471 throw new OAuthException("Bio too long '$bio'");
473 $location = $_GET['omb_listenee_location'];
474 if ($location && mb_strlen($location) > 255) {
475 throw new OAuthException("Location too long '$location'");
477 $avatar = $_GET['omb_listenee_avatar'];
479 if (!common_valid_http_url($avatar) || strlen($avatar) > 255) {
480 throw new OAuthException("Invalid avatar URL '$avatar'");
482 $size = @getimagesize($avatar);
484 throw new OAuthException("Can't read avatar URL '$avatar'");
486 if ($size[0] != AVATAR_PROFILE_SIZE || $size[1] != AVATAR_PROFILE_SIZE) {
487 throw new OAuthException("Wrong size image at '$avatar'");
489 if (!in_array($size[2], array(IMAGETYPE_GIF, IMAGETYPE_JPEG,
491 throw new OAuthException("Wrong image type for '$avatar'");
494 $callback = $_GET['oauth_callback'];
495 if ($callback && !common_valid_http_url($callback)) {
496 throw new OAuthException("Invalid callback URL '$callback'");
498 if ($callback && $callback == common_local_url('finishremotesubscribe')) {
499 throw new OAuthException("Callback URL '$callback' is for local site.");