]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/FacebookBridge/actions/facebookfinishlogin.php
OAuth: Fix rare problem in which request tokens were sometimes being
[quix0rs-gnu-social.git] / plugins / FacebookBridge / actions / facebookfinishlogin.php
1 <?php
2 /**
3  * StatusNet, the distributed open-source microblogging tool
4  *
5  * Login or register a local user based on a Facebook user
6  *
7  * PHP version 5
8  *
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.
13  *
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.
18  *
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/>.
21  *
22  * @category  Plugin
23  * @package   StatusNet
24  * @author    Zach Copley <zach@status.net>
25  * @copyright 2010 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/
28  */
29
30 if (!defined('STATUSNET')) {
31     exit(1);
32 }
33
34 class FacebookfinishloginAction extends Action
35 {
36     private $facebook = null; // Facebook client
37     private $fbuid    = null; // Facebook user ID
38     private $fbuser   = null; // Facebook user object (JSON)
39
40     function prepare($args) {
41
42         parent::prepare($args);
43
44         $this->facebook = new Facebook(
45             array(
46                 'appId'  => common_config('facebook', 'appid'),
47                 'secret' => common_config('facebook', 'secret'),
48                 'cookie' => true,
49             )
50         );
51
52         // Check for a Facebook user session
53
54         $session = $this->facebook->getSession();
55         $me      = null;
56
57         if ($session) {
58             try {
59                 $this->fbuid  = $this->facebook->getUser();
60                 $this->fbuser = $this->facebook->api('/me');
61             } catch (FacebookApiException $e) {
62                 common_log(LOG_ERROR, $e, __FILE__);
63             }
64         }
65
66         if (!empty($this->fbuser)) {
67
68             // OKAY, all is well... proceed to register
69
70             common_debug("Found a valid Facebook user.", __FILE__);
71         } else {
72
73             // This shouldn't happen in the regular course of things
74
75             list($proxy, $ip) = common_client_ip();
76
77             common_log(
78                 LOG_WARNING,
79                     sprintf(
80                         'Failed Facebook authentication attempt, proxy = %s, ip = %s.',
81                          $proxy,
82                          $ip
83                     ),
84                     __FILE__
85             );
86
87             $this->clientError(
88                 _m('You must be logged into Facebook to register a local account using Facebook.')
89             );
90         }
91
92         return true;
93     }
94
95     function handle($args)
96     {
97         parent::handle($args);
98
99         if (common_is_real_login()) {
100
101             // User is already logged in, are her accounts already linked?
102
103             $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
104
105             if (!empty($flink)) {
106
107                 // User already has a linked Facebook account and shouldn't be here!
108
109                 common_debug(
110                     sprintf(
111                         'There\'s already a local user %d linked with Facebook user %s.',
112                         $flink->user_id,
113                         $this->fbuid
114                     )
115                 );
116
117                 $this->clientError(
118                     _m('There is already a local account linked with that Facebook account.')
119                 );
120
121             } else {
122
123                 // Possibly reconnect an existing account
124
125                 $this->connectUser();
126             }
127
128         } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
129             $this->handlePost();
130         } else {
131             $this->tryLogin();
132         }
133     }
134
135     function handlePost()
136     {
137         $token = $this->trimmed('token');
138
139         if (!$token || $token != common_session_token()) {
140             $this->showForm(
141                 _m('There was a problem with your session token. Try again, please.')
142             );
143             return;
144         }
145
146         if ($this->arg('create')) {
147
148             if (!$this->boolean('license')) {
149                 $this->showForm(
150                     _m('You can\'t register if you don\'t agree to the license.'),
151                     $this->trimmed('newname')
152                 );
153                 return;
154             }
155
156             // We has a valid Facebook session and the Facebook user has
157             // agreed to the SN license, so create a new user
158             $this->createNewUser();
159
160         } else if ($this->arg('connect')) {
161
162             $this->connectNewUser();
163
164         } else {
165
166             $this->showForm(
167                 _m('An unknown error has occured.'),
168                 $this->trimmed('newname')
169             );
170         }
171     }
172
173     function showPageNotice()
174     {
175         if ($this->error) {
176
177             $this->element('div', array('class' => 'error'), $this->error);
178
179         } else {
180
181             $this->element(
182                 'div', 'instructions',
183                 // TRANS: %s is the site name.
184                 sprintf(
185                     _m('This is the first time you\'ve 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.'),
186                     common_config('site', 'name')
187                 )
188             );
189         }
190     }
191
192     function title()
193     {
194         // TRANS: Page title.
195         return _m('Facebook Setup');
196     }
197
198     function showForm($error=null, $username=null)
199     {
200         $this->error = $error;
201         $this->username = $username;
202
203         $this->showPage();
204     }
205
206     function showPage()
207     {
208         parent::showPage();
209     }
210
211     /**
212      * @fixme much of this duplicates core code, which is very fragile.
213      * Should probably be replaced with an extensible mini version of
214      * the core registration form.
215      */
216     function showContent()
217     {
218         if (!empty($this->message_text)) {
219             $this->element('p', null, $this->message);
220             return;
221         }
222
223         $this->elementStart('form', array('method' => 'post',
224                                           'id' => 'form_settings_facebook_connect',
225                                           'class' => 'form_settings',
226                                           'action' => common_local_url('facebookfinishlogin')));
227         $this->elementStart('fieldset', array('id' => 'settings_facebook_connect_options'));
228         // TRANS: Legend.
229         $this->element('legend', null, _m('Connection options'));
230         $this->elementStart('ul', 'form_data');
231         $this->elementStart('li');
232         $this->element('input', array('type' => 'checkbox',
233                                       'id' => 'license',
234                                       'class' => 'checkbox',
235                                       'name' => 'license',
236                                       'value' => 'true'));
237         $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
238         // TRANS: %s is the name of the license used by the user for their status updates.
239         $message = _m('My text and files are available under %s ' .
240                      'except this private data: password, ' .
241                      'email address, IM address, and phone number.');
242         $link = '<a href="' .
243                 htmlspecialchars(common_config('license', 'url')) .
244                 '">' .
245                 htmlspecialchars(common_config('license', 'title')) .
246                 '</a>';
247         $this->raw(sprintf(htmlspecialchars($message), $link));
248         $this->elementEnd('label');
249         $this->elementEnd('li');
250         $this->elementEnd('ul');
251
252         $this->elementStart('fieldset');
253         $this->hidden('token', common_session_token());
254         $this->element('legend', null,
255                        // TRANS: Legend.
256                        _m('Create new account'));
257         $this->element('p', null,
258                        _m('Create a new user with this nickname.'));
259         $this->elementStart('ul', 'form_data');
260         $this->elementStart('li');
261         // TRANS: Field label.
262         $this->input('newname', _m('New nickname'),
263                      ($this->username) ? $this->username : '',
264                      _m('1-64 lowercase letters or numbers, no punctuation or spaces'));
265         $this->elementEnd('li');
266         $this->elementEnd('ul');
267         // TRANS: Submit button.
268         $this->submit('create', _m('BUTTON','Create'));
269         $this->elementEnd('fieldset');
270
271         $this->elementStart('fieldset');
272         // TRANS: Legend.
273         $this->element('legend', null,
274                        _m('Connect existing account'));
275         $this->element('p', null,
276                        _m('If you already have an account, login with your username and password to connect it to your Facebook.'));
277         $this->elementStart('ul', 'form_data');
278         $this->elementStart('li');
279         // TRANS: Field label.
280         $this->input('nickname', _m('Existing nickname'));
281         $this->elementEnd('li');
282         $this->elementStart('li');
283         $this->password('password', _m('Password'));
284         $this->elementEnd('li');
285         $this->elementEnd('ul');
286         // TRANS: Submit button.
287         $this->submit('connect', _m('BUTTON','Connect'));
288         $this->elementEnd('fieldset');
289
290         $this->elementEnd('fieldset');
291         $this->elementEnd('form');
292     }
293
294     function message($msg)
295     {
296         $this->message_text = $msg;
297         $this->showPage();
298     }
299
300     function createNewUser()
301     {
302         if (common_config('site', 'closed')) {
303             // TRANS: Client error trying to register with registrations not allowed.
304             $this->clientError(_m('Registration not allowed.'));
305             return;
306         }
307
308         $invite = null;
309
310         if (common_config('site', 'inviteonly')) {
311             $code = $_SESSION['invitecode'];
312             if (empty($code)) {
313                 // TRANS: Client error trying to register with registrations 'invite only'.
314                 $this->clientError(_m('Registration not allowed.'));
315                 return;
316             }
317
318             $invite = Invitation::staticGet($code);
319
320             if (empty($invite)) {
321                 // TRANS: Client error trying to register with an invalid invitation code.
322                 $this->clientError(_m('Not a valid invitation code.'));
323                 return;
324             }
325         }
326
327         try {
328             $nickname = Nickname::normalize($this->trimmed('newname'));
329         } catch (NicknameException $e) {
330             $this->showForm($e->getMessage());
331             return;
332         }
333
334         if (!User::allowed_nickname($nickname)) {
335             $this->showForm(_m('Nickname not allowed.'));
336             return;
337         }
338
339         if (User::staticGet('nickname', $nickname)) {
340             $this->showForm(_m('Nickname already in use. Try another one.'));
341             return;
342         }
343
344         $args = array(
345             'nickname'        => $nickname,
346             'fullname'        => $this->fbuser['first_name']
347                 . ' ' . $this->fbuser['last_name'],
348             'homepage'        => $this->fbuser['website'],
349             'bio'             => $this->fbuser['about'],
350             'location'        => $this->fbuser['location']['name']
351         );
352
353         // It's possible that the email address is already in our
354         // DB. It's a unique key, so we need to check
355         if ($this->isNewEmail($this->fbuser['email'])) {
356             $args['email']           = $this->fbuser['email'];
357             $args['email_confirmed'] = true;
358         }
359
360         if (!empty($invite)) {
361             $args['code'] = $invite->code;
362         }
363
364         $user   = User::register($args);
365         $result = $this->flinkUser($user->id, $this->fbuid);
366
367         if (!$result) {
368             $this->serverError(_m('Error connecting user to Facebook.'));
369             return;
370         }
371
372         // Add a Foreign_user record
373         Facebookclient::addFacebookUser($this->fbuser);
374
375         $this->setAvatar($user);
376
377         common_set_user($user);
378         common_real_login(true);
379
380         common_log(
381             LOG_INFO,
382             sprintf(
383                 'Registered new user %s (%d) from Facebook user %s, (fbuid %d)',
384                 $user->nickname,
385                 $user->id,
386                 $this->fbuser['name'],
387                 $this->fbuid
388             ),
389             __FILE__
390         );
391
392         $this->goHome($user->nickname);
393     }
394
395     /*
396      * Attempt to download the user's Facebook picture and create a
397      * StatusNet avatar for the new user.
398      */
399     function setAvatar($user)
400     {
401         $picUrl = sprintf(
402             'http://graph.facebook.com/%s/picture?type=large',
403             $this->fbuid
404         );
405
406         // fetch the picture from Facebook
407         $client = new HTTPClient();
408
409         // fetch the actual picture
410         $response = $client->get($picUrl);
411
412         if ($response->isOk()) {
413
414             $finalUrl = $client->getUrl();
415
416             // Make sure the filename is unique becuase it's possible for a user
417             // to deauthorize our app, and then come back in as a new user but
418             // have the same Facebook picture (avatar URLs have a unique index
419             // and their URLs are based on the filenames).
420             $filename = 'facebook-' . common_good_rand(4) . '-'
421                 . substr(strrchr($finalUrl, '/'), 1);
422
423             $ok = file_put_contents(
424                 Avatar::path($filename),
425                 $response->getBody()
426             );
427
428             if (!$ok) {
429                 common_log(
430                     LOG_WARNING,
431                     sprintf(
432                         'Couldn\'t save Facebook avatar %s',
433                         $tmp
434                     ),
435                     __FILE__
436                 );
437
438             } else {
439
440                 // save it as an avatar
441                 $profile = $user->getProfile();
442
443                 if ($profile->setOriginal($filename)) {
444                     common_log(
445                         LOG_INFO,
446                         sprintf(
447                             'Saved avatar for %s (%d) from Facebook picture for '
448                                 . '%s (fbuid %d), filename = %s',
449                              $user->nickname,
450                              $user->id,
451                              $this->fbuser['name'],
452                              $this->fbuid,
453                              $filename
454                         ),
455                         __FILE__
456                     );
457                 }
458             }
459         }
460     }
461
462     function connectNewUser()
463     {
464         $nickname = $this->trimmed('nickname');
465         $password = $this->trimmed('password');
466
467         if (!common_check_user($nickname, $password)) {
468             $this->showForm(_m('Invalid username or password.'));
469             return;
470         }
471
472         $user = User::staticGet('nickname', $nickname);
473
474         if (!empty($user)) {
475             common_debug(
476                 sprintf(
477                     'Found a legit user to connect to Facebook: %s (%d)',
478                     $user->nickname,
479                     $user->id
480                 ),
481                 __FILE__
482             );
483         }
484
485         $this->tryLinkUser($user);
486
487         common_set_user($user);
488         common_real_login(true);
489
490         $this->goHome($user->nickname);
491     }
492
493     function connectUser()
494     {
495         $user = common_current_user();
496         $this->tryLinkUser($user);
497         common_redirect(common_local_url('facebookfinishlogin'), 303);
498     }
499
500     function tryLinkUser($user)
501     {
502         $result = $this->flinkUser($user->id, $this->fbuid);
503
504         if (empty($result)) {
505             $this->serverError(_m('Error connecting user to Facebook.'));
506             return;
507         }
508
509         common_debug(
510             sprintf(
511                 'Connected Facebook user %s (fbuid %d) to local user %s (%d)',
512                 $this->fbuser['name'],
513                 $this->fbuid,
514                 $user->nickname,
515                 $user->id
516             ),
517             __FILE__
518         );
519     }
520
521     function tryLogin()
522     {
523         common_debug(
524             sprintf(
525                 'Trying login for Facebook user %s',
526                 $this->fbuid
527             ),
528             __FILE__
529         );
530
531         $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
532
533         if (!empty($flink)) {
534             $user = $flink->getUser();
535
536             if (!empty($user)) {
537
538                 common_log(
539                     LOG_INFO,
540                     sprintf(
541                         'Logged in Facebook user %s as user %d (%s)',
542                         $this->fbuid,
543                         $user->nickname,
544                         $user->id
545                     ),
546                     __FILE__
547                 );
548
549                 common_set_user($user);
550                 common_real_login(true);
551                 $this->goHome($user->nickname);
552             }
553
554         } else {
555
556             common_debug(
557                 sprintf(
558                     'No flink found for fbuid: %s - new user',
559                     $this->fbuid
560                 ),
561                 __FILE__
562             );
563
564             $this->showForm(null, $this->bestNewNickname());
565         }
566     }
567
568     function goHome($nickname)
569     {
570         $url = common_get_returnto();
571         if ($url) {
572             // We don't have to return to it again
573             common_set_returnto(null);
574         } else {
575             $url = common_local_url('all',
576                                     array('nickname' =>
577                                           $nickname));
578         }
579
580         common_redirect($url, 303);
581     }
582
583     function flinkUser($user_id, $fbuid)
584     {
585         $flink = new Foreign_link();
586         $flink->user_id = $user_id;
587         $flink->foreign_id = $fbuid;
588         $flink->service = FACEBOOK_SERVICE;
589
590         // Pull the access token from the Facebook cookies
591         $flink->credentials = $this->facebook->getAccessToken();
592
593         $flink->created = common_sql_now();
594
595         $flink_id = $flink->insert();
596
597         return $flink_id;
598     }
599
600     function bestNewNickname()
601     {
602         if (!empty($this->fbuser['name'])) {
603             $nickname = $this->nicknamize($this->fbuser['name']);
604             if ($this->isNewNickname($nickname)) {
605                 return $nickname;
606             }
607         }
608
609         // Try the full name
610
611         $fullname = trim($this->fbuser['first_name'] .
612             ' ' . $this->fbuser['last_name']);
613
614         if (!empty($fullname)) {
615             $fullname = $this->nicknamize($fullname);
616             if ($this->isNewNickname($fullname)) {
617                 return $fullname;
618             }
619         }
620
621         return null;
622     }
623
624      /**
625       * Given a string, try to make it work as a nickname
626       */
627      function nicknamize($str)
628      {
629          $str = preg_replace('/\W/', '', $str);
630          return strtolower($str);
631      }
632
633      /*
634       * Is the desired nickname already taken?
635       *
636       * @return boolean result
637       */
638      function isNewNickname($str)
639      {
640         if (!Nickname::isValid($str)) {
641             return false;
642         }
643
644         if (!User::allowed_nickname($str)) {
645             return false;
646         }
647
648         if (User::staticGet('nickname', $str)) {
649             return false;
650         }
651
652         return true;
653     }
654
655     /*
656      * Do we already have a user record with this email?
657      * (emails have to be unique but they can change)
658      *
659      * @param string $email the email address to check
660      *
661      * @return boolean result
662      */
663      function isNewEmail($email)
664      {
665          // we shouldn't have to validate the format
666          $result = User::staticGet('email', $email);
667
668          if (empty($result)) {
669              common_debug("XXXXXXXXXXXXXXXXXX We've never seen this email before!!!");
670              return true;
671          }
672          common_debug("XXXXXXXXXXXXXXXXXX dupe email address!!!!");
673
674          return false;
675      }
676
677 }