3 * StatusNet, the distributed open-source microblogging tool
5 * Login or register a local user based on a Facebook user
9 * LICENCE: This program is free software: you can redistribute it and/or modify
10 * it under the terms of the GNU Affero General Public License as published by
11 * the Free Software Foundation, either version 3 of the License, or
12 * (at your option) any later version.
14 * This program is distributed in the hope that it will be useful,
15 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17 * GNU Affero General Public License for more details.
19 * You should have received a copy of the GNU Affero General Public License
20 * along with this program. If not, see <http://www.gnu.org/licenses/>.
24 * @author Zach Copley <zach@status.net>
25 * @copyright 2010-2011 StatusNet, Inc.
26 * @license http://www.fsf.org/licensing/licenses/agpl-3.0.html GNU Affero General Public License version 3.0
27 * @link http://status.net/
30 if (!defined('STATUSNET')) {
34 class FacebookfinishloginAction extends Action
36 private $fbuid = null; // Facebook user ID
37 private $fbuser = null; // Facebook user object (JSON)
38 private $accessToken = null; // Access token provided by Facebook JS API
40 function prepare(array $args=array()) {
41 parent::prepare($args);
43 // Check cookie for a valid access_token
45 if (isset($_COOKIE['fb_access_token'])) {
46 $this->accessToken = $_COOKIE['fb_access_token'];
49 if (empty($this->accessToken)) {
50 $this->clientError(_m("Unable to authenticate you with Facebook."));
53 $graphUrl = 'https://graph.facebook.com/me?access_token=' . urlencode($this->accessToken);
54 $this->fbuser = json_decode(file_get_contents($graphUrl));
56 if (empty($this->fbuser)) {
59 list($proxy, $ip) = common_client_ip();
64 'Failed Facebook authentication attempt, proxy = %s, ip = %s.',
72 // TRANS: Client error displayed when trying to connect to Facebook while not logged in.
73 _m('You must be logged into Facebook to register a local account using Facebook.')
77 $this->fbuid = $this->fbuser->id;
78 // OKAY, all is well... proceed to register
82 function handle(array $args=array())
84 parent::handle($args);
86 if (common_is_real_login()) {
88 // This will throw a client exception if the user already
89 // has some sort of foreign_link to Facebook.
91 $this->checkForExistingLink();
93 // Possibly reconnect an existing account
97 } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
104 function checkForExistingLink() {
106 // User is already logged in, are her accounts already linked?
108 $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
110 if (!empty($flink)) {
112 // User already has a linked Facebook account and shouldn't be here!
115 // TRANS: Client error displayed when trying to connect to a Facebook account that is already linked
116 // TRANS: in the same StatusNet site.
117 _m('There is already a local account linked with that Facebook account.')
121 $cur = common_current_user();
122 $flink = Foreign_link::getByUserID($cur->id, FACEBOOK_SERVICE);
124 if (!empty($flink)) {
126 // There's already a local user linked to this Facebook account.
129 // TRANS: Client error displayed when trying to connect to a Facebook account that is already linked
130 // TRANS: in the same StatusNet site.
131 _m('There is already a local account linked with that Facebook account.')
136 function handlePost()
138 $token = $this->trimmed('token');
141 if (!$token || $token != common_session_token()) {
143 // TRANS: Client error displayed when the session token does not match or is not given.
144 _m('There was a problem with your session token. Try again, please.')
149 if ($this->arg('create')) {
151 if (!$this->boolean('license')) {
153 // TRANS: Form validation error displayed when user has not agreed to the license.
154 _m('You cannot register if you do not agree to the license.'),
155 $this->trimmed('newname')
160 // We has a valid Facebook session and the Facebook user has
161 // agreed to the SN license, so create a new user
162 $this->createNewUser();
164 } else if ($this->arg('connect')) {
166 $this->connectNewUser();
171 // TRANS: Form validation error displayed when an unhandled error occurs.
172 _m('An unknown error has occured.'),
173 $this->trimmed('newname')
178 function showPageNotice()
182 $this->element('div', array('class' => 'error'), $this->error);
187 'div', 'instructions',
189 // TRANS: Form instructions for connecting to Facebook.
190 // TRANS: %s is the site name.
191 _m('This is the first time you have logged into %s so we must connect your Facebook to a local account. You can either create a new local account, or connect with an existing local account.'),
192 common_config('site', 'name')
200 // TRANS: Page title.
201 return _m('Facebook Setup');
204 function showForm($error=null, $username=null)
206 $this->error = $error;
207 $this->username = $username;
218 * @todo FIXME: Much of this duplicates core code, which is very fragile.
219 * Should probably be replaced with an extensible mini version of
220 * the core registration form.
222 function showContent()
224 if (!empty($this->message_text)) {
225 $this->element('p', null, $this->message);
229 $this->elementStart('form', array('method' => 'post',
230 'id' => 'form_settings_facebook_connect',
231 'class' => 'form_settings',
232 'action' => common_local_url('facebookfinishlogin')));
233 $this->elementStart('fieldset', array('id' => 'settings_facebook_connect_options'));
234 // TRANS: Fieldset legend.
235 $this->element('legend', null, _m('Connection options'));
236 $this->elementStart('ul', 'form_data');
237 $this->elementStart('li');
238 $this->element('input', array('type' => 'checkbox',
240 'class' => 'checkbox',
243 $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
244 // TRANS: %s is the name of the license used by the user for their status updates.
245 $message = _m('My text and files are available under %s ' .
246 'except this private data: password, ' .
247 'email address, IM address, and phone number.');
248 $link = '<a href="' .
249 htmlspecialchars(common_config('license', 'url')) .
251 htmlspecialchars(common_config('license', 'title')) .
253 $this->raw(sprintf(htmlspecialchars($message), $link));
254 $this->elementEnd('label');
255 $this->elementEnd('li');
256 $this->elementEnd('ul');
258 $this->elementStart('fieldset');
259 $this->hidden('token', common_session_token());
260 $this->element('legend', null,
261 // TRANS: Fieldset legend.
262 _m('Create new account'));
263 $this->element('p', null,
264 // TRANS: Form instructions.
265 _m('Create a new user with this nickname.'));
266 $this->elementStart('ul', 'form_data');
268 // Hook point for captcha etc
269 Event::handle('StartRegistrationFormData', array($this));
271 $this->elementStart('li');
272 // TRANS: Field label.
273 $this->input('newname', _m('New nickname'),
274 ($this->username) ? $this->username : '',
275 // TRANS: Field title.
276 _m('1-64 lowercase letters or numbers, no punctuation or spaces.'));
277 $this->elementEnd('li');
279 // Hook point for captcha etc
280 Event::handle('EndRegistrationFormData', array($this));
282 $this->elementEnd('ul');
283 // TRANS: Submit button to create a new account.
284 $this->submit('create', _m('BUTTON','Create'));
285 $this->elementEnd('fieldset');
287 $this->elementStart('fieldset');
288 $this->element('legend', null,
289 // TRANS: Fieldset legend.
290 _m('Connect existing account'));
291 $this->element('p', null,
292 // TRANS: Form instructions.
293 _m('If you already have an account, login with your username and password to connect it to your Facebook.'));
294 $this->elementStart('ul', 'form_data');
295 $this->elementStart('li');
296 // TRANS: Field label.
297 $this->input('nickname', _m('Existing nickname'));
298 $this->elementEnd('li');
299 $this->elementStart('li');
300 // TRANS: Field label.
301 $this->password('password', _m('Password'));
302 $this->elementEnd('li');
303 $this->elementEnd('ul');
304 // TRANS: Submit button to connect a Facebook account to an existing StatusNet account.
305 $this->submit('connect', _m('BUTTON','Connect'));
306 $this->elementEnd('fieldset');
308 $this->elementEnd('fieldset');
309 $this->elementEnd('form');
312 function message($msg)
314 $this->message_text = $msg;
318 function createNewUser()
320 if (!Event::handle('StartRegistrationTry', array($this))) {
324 if (common_config('site', 'closed')) {
325 // TRANS: Client error trying to register with registrations not allowed.
326 $this->clientError(_m('Registration not allowed.'));
331 if (common_config('site', 'inviteonly')) {
332 $code = $_SESSION['invitecode'];
334 // TRANS: Client error trying to register with registrations 'invite only'.
335 $this->clientError(_m('Registration not allowed.'));
338 $invite = Invitation::getKV($code);
340 if (empty($invite)) {
341 // TRANS: Client error trying to register with an invalid invitation code.
342 $this->clientError(_m('Not a valid invitation code.'));
347 $nickname = Nickname::normalize($this->trimmed('newname'), true);
348 } catch (NicknameException $e) {
349 $this->showForm($e->getMessage());
354 'nickname' => $nickname,
355 'fullname' => $this->fbuser->name,
356 'homepage' => $this->fbuser->website,
357 'location' => $this->fbuser->location->name
360 // It's possible that the email address is already in our
361 // DB. It's a unique key, so we need to check
362 if ($this->isNewEmail($this->fbuser->email)) {
363 $args['email'] = $this->fbuser->email;
364 if (isset($this->fuser->verified) && $this->fuser->verified == true) {
365 $args['email_confirmed'] = true;
369 if (!empty($invite)) {
370 $args['code'] = $invite->code;
373 $user = User::register($args);
374 $result = $this->flinkUser($user->id, $this->fbuid);
377 // TRANS: Server error displayed when connecting to Facebook fails.
378 $this->serverError(_m('Error connecting user to Facebook.'));
381 // Add a Foreign_user record
382 Facebookclient::addFacebookUser($this->fbuser);
384 $this->setAvatar($user);
386 common_set_user($user);
387 common_real_login(true);
392 'Registered new user %s (%d) from Facebook user %s, (fbuid %d)',
401 Event::handle('EndRegistrationTry', array($this));
403 $this->goHome($user->nickname);
407 * Attempt to download the user's Facebook picture and create a
408 * StatusNet avatar for the new user.
410 function setAvatar($user)
414 'http://graph.facebook.com/%d/picture?type=large',
418 // fetch the picture from Facebook
419 $client = new HTTPClient();
421 // fetch the actual picture
422 $response = $client->get($picUrl);
424 if ($response->isOk()) {
426 // seems to always be jpeg, but not sure
427 $tmpname = "facebook-avatar-tmp-" . common_random_hexstr(4);
429 $ok = file_put_contents(
430 Avatar::path($tmpname),
435 common_log(LOG_WARNING, 'Couldn\'t save tmp Facebook avatar: ' . $tmpname, __FILE__);
437 // save it as an avatar
439 $imagefile = new ImageFile(null, Avatar::path($tmpname));
440 $filename = Avatar::filename($user->id, image_type_to_extension($imagefile->preferredType()),
441 180, common_timestamp());
442 // Previous docs said 180 is the "biggest img we get from Facebook"
443 $imagefile->resizeTo(Avatar::path($filename, array('width'=>180, 'height'=>180)));
445 // No need to keep the temporary file around...
446 @unlink(Avatar::path($tmpname));
448 $profile = $user->getProfile();
450 if ($profile->setOriginal($filename)) {
454 'Saved avatar for %s (%d) from Facebook picture for '
455 . '%s (fbuid %d), filename = %s',
470 } catch (Exception $e) {
471 common_log(LOG_WARNING, 'Couldn\'t save Facebook avatar: ' . $e->getMessage(), __FILE__);
472 // error isn't fatal, continue
476 function connectNewUser()
478 $nickname = $this->trimmed('nickname');
479 $password = $this->trimmed('password');
481 if (!common_check_user($nickname, $password)) {
482 // TRANS: Form validation error displayed when username/password combination is incorrect.
483 $this->showForm(_m('Invalid username or password.'));
487 $user = User::getKV('nickname', $nickname);
489 $this->tryLinkUser($user);
491 common_set_user($user);
492 common_real_login(true);
494 // clear out the stupid cookie
495 setcookie('fb_access_token', '', time() - 3600); // one hour ago
497 $this->goHome($user->nickname);
500 function connectUser()
502 $user = common_current_user();
503 $this->tryLinkUser($user);
505 // clear out the stupid cookie
506 setcookie('fb_access_token', '', time() - 3600); // one hour ago
507 common_redirect(common_local_url('facebookfinishlogin'), 303);
510 function tryLinkUser($user)
512 $result = $this->flinkUser($user->id, $this->fbuid);
514 if (empty($result)) {
515 // TRANS: Server error displayed when connecting to Facebook fails.
516 $this->serverError(_m('Error connecting user to Facebook.'));
523 $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
524 $user = $flink->getUser();
529 'Logged in Facebook user %s as user %d (%s)',
537 common_set_user($user);
538 common_real_login(true);
540 // clear out the stupid cookie
541 setcookie('fb_access_token', '', time() - 3600); // one hour ago
543 $this->goHome($user->nickname);
545 } catch (NoResultException $e) {
546 $this->showForm(null, $this->bestNewNickname());
550 function goHome($nickname)
552 $url = common_get_returnto();
554 // We don't have to return to it again
555 common_set_returnto(null);
557 $url = common_local_url('all',
562 common_redirect($url, 303);
565 function flinkUser($user_id, $fbuid)
567 $flink = new Foreign_link();
569 $flink->user_id = $user_id;
570 $flink->foreign_id = $fbuid;
571 $flink->service = FACEBOOK_SERVICE;
572 $flink->credentials = $this->accessToken;
573 $flink->created = common_sql_now();
575 $flink_id = $flink->insert();
580 function bestNewNickname()
583 $nickname = Nickname::normalize($this->fbuser->username, true);
585 } catch (NicknameException $e) {
586 // Failed to normalize nickname, but let's try the full name
590 $nickname = Nickname::normalize($this->fbuser->name, true);
592 } catch (NicknameException $e) {
593 // Any more ideas? Nope.
600 * Do we already have a user record with this email?
601 * (emails have to be unique but they can change)
603 * @param string $email the email address to check
605 * @return boolean result
607 function isNewEmail($email)
609 // we shouldn't have to validate the format
610 $result = User::getKV('email', $email);
612 if (empty($result)) {