]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/FacebookBridge/actions/facebookfinishlogin.php
FacebookSSO -> FacebookBridge
[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         $nickname = $this->trimmed('newname');
328
329         if (!Validate::string($nickname, array('min_length' => 1,
330                                                'max_length' => 64,
331                                                'format' => NICKNAME_FMT))) {
332             $this->showForm(_m('Nickname must have only lowercase letters and numbers and no spaces.'));
333             return;
334         }
335
336         if (!User::allowed_nickname($nickname)) {
337             $this->showForm(_m('Nickname not allowed.'));
338             return;
339         }
340
341         if (User::staticGet('nickname', $nickname)) {
342             $this->showForm(_m('Nickname already in use. Try another one.'));
343             return;
344         }
345
346         $args = array(
347             'nickname'        => $nickname,
348             'fullname'        => $this->fbuser['first_name']
349                 . ' ' . $this->fbuser['last_name'],
350             'homepage'        => $this->fbuser['website'],
351             'bio'             => $this->fbuser['about'],
352             'location'        => $this->fbuser['location']['name']
353         );
354
355         // It's possible that the email address is already in our
356         // DB. It's a unique key, so we need to check
357         if ($this->isNewEmail($this->fbuser['email'])) {
358             $args['email']           = $this->fbuser['email'];
359             $args['email_confirmed'] = true;
360         }
361
362         if (!empty($invite)) {
363             $args['code'] = $invite->code;
364         }
365
366         $user   = User::register($args);
367         $result = $this->flinkUser($user->id, $this->fbuid);
368
369         if (!$result) {
370             $this->serverError(_m('Error connecting user to Facebook.'));
371             return;
372         }
373
374         // Add a Foreign_user record
375         Facebookclient::addFacebookUser($this->fbuser);
376
377         $this->setAvatar($user);
378
379         common_set_user($user);
380         common_real_login(true);
381
382         common_log(
383             LOG_INFO,
384             sprintf(
385                 'Registered new user %s (%d) from Facebook user %s, (fbuid %d)',
386                 $user->nickname,
387                 $user->id,
388                 $this->fbuser['name'],
389                 $this->fbuid
390             ),
391             __FILE__
392         );
393
394         $this->goHome($user->nickname);
395     }
396
397     /*
398      * Attempt to download the user's Facebook picture and create a
399      * StatusNet avatar for the new user.
400      */
401     function setAvatar($user)
402     {
403         $picUrl = sprintf(
404             'http://graph.facebook.com/%s/picture?type=large',
405             $this->fbuid
406         );
407
408         // fetch the picture from Facebook
409         $client = new HTTPClient();
410
411         // fetch the actual picture
412         $response = $client->get($picUrl);
413
414         if ($response->isOk()) {
415
416             $finalUrl = $client->getUrl();
417
418             // Make sure the filename is unique becuase it's possible for a user
419             // to deauthorize our app, and then come back in as a new user but
420             // have the same Facebook picture (avatar URLs have a unique index
421             // and their URLs are based on the filenames).
422             $filename = 'facebook-' . common_good_rand(4) . '-'
423                 . substr(strrchr($finalUrl, '/'), 1);
424
425             $ok = file_put_contents(
426                 Avatar::path($filename),
427                 $response->getBody()
428             );
429
430             if (!$ok) {
431                 common_log(
432                     LOG_WARNING,
433                     sprintf(
434                         'Couldn\'t save Facebook avatar %s',
435                         $tmp
436                     ),
437                     __FILE__
438                 );
439
440             } else {
441
442                 // save it as an avatar
443                 $profile = $user->getProfile();
444
445                 if ($profile->setOriginal($filename)) {
446                     common_log(
447                         LOG_INFO,
448                         sprintf(
449                             'Saved avatar for %s (%d) from Facebook picture for '
450                                 . '%s (fbuid %d), filename = %s',
451                              $user->nickname,
452                              $user->id,
453                              $this->fbuser['name'],
454                              $this->fbuid,
455                              $filename
456                         ),
457                         __FILE__
458                     );
459                 }
460             }
461         }
462     }
463
464     function connectNewUser()
465     {
466         $nickname = $this->trimmed('nickname');
467         $password = $this->trimmed('password');
468
469         if (!common_check_user($nickname, $password)) {
470             $this->showForm(_m('Invalid username or password.'));
471             return;
472         }
473
474         $user = User::staticGet('nickname', $nickname);
475
476         if (!empty($user)) {
477             common_debug(
478                 sprintf(
479                     'Found a legit user to connect to Facebook: %s (%d)',
480                     $user->nickname,
481                     $user->id
482                 ),
483                 __FILE__
484             );
485         }
486
487         $this->tryLinkUser($user);
488
489         common_set_user($user);
490         common_real_login(true);
491
492         $this->goHome($user->nickname);
493     }
494
495     function connectUser()
496     {
497         $user = common_current_user();
498         $this->tryLinkUser($user);
499         common_redirect(common_local_url('facebookfinishlogin'), 303);
500     }
501
502     function tryLinkUser($user)
503     {
504         $result = $this->flinkUser($user->id, $this->fbuid);
505
506         if (empty($result)) {
507             $this->serverError(_m('Error connecting user to Facebook.'));
508             return;
509         }
510
511         common_debug(
512             sprintf(
513                 'Connected Facebook user %s (fbuid %d) to local user %s (%d)',
514                 $this->fbuser['name'],
515                 $this->fbuid,
516                 $user->nickname,
517                 $user->id
518             ),
519             __FILE__
520         );
521     }
522
523     function tryLogin()
524     {
525         common_debug(
526             sprintf(
527                 'Trying login for Facebook user %s',
528                 $this->fbuid
529             ),
530             __FILE__
531         );
532
533         $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
534
535         if (!empty($flink)) {
536             $user = $flink->getUser();
537
538             if (!empty($user)) {
539
540                 common_log(
541                     LOG_INFO,
542                     sprintf(
543                         'Logged in Facebook user %s as user %d (%s)',
544                         $this->fbuid,
545                         $user->nickname,
546                         $user->id
547                     ),
548                     __FILE__
549                 );
550
551                 common_set_user($user);
552                 common_real_login(true);
553                 $this->goHome($user->nickname);
554             }
555
556         } else {
557
558             common_debug(
559                 sprintf(
560                     'No flink found for fbuid: %s - new user',
561                     $this->fbuid
562                 ),
563                 __FILE__
564             );
565
566             $this->showForm(null, $this->bestNewNickname());
567         }
568     }
569
570     function goHome($nickname)
571     {
572         $url = common_get_returnto();
573         if ($url) {
574             // We don't have to return to it again
575             common_set_returnto(null);
576         } else {
577             $url = common_local_url('all',
578                                     array('nickname' =>
579                                           $nickname));
580         }
581
582         common_redirect($url, 303);
583     }
584
585     function flinkUser($user_id, $fbuid)
586     {
587         $flink = new Foreign_link();
588         $flink->user_id = $user_id;
589         $flink->foreign_id = $fbuid;
590         $flink->service = FACEBOOK_SERVICE;
591
592         // Pull the access token from the Facebook cookies
593         $flink->credentials = $this->facebook->getAccessToken();
594
595         $flink->created = common_sql_now();
596
597         $flink_id = $flink->insert();
598
599         return $flink_id;
600     }
601
602     function bestNewNickname()
603     {
604         if (!empty($this->fbuser['name'])) {
605             $nickname = $this->nicknamize($this->fbuser['name']);
606             if ($this->isNewNickname($nickname)) {
607                 return $nickname;
608             }
609         }
610
611         // Try the full name
612
613         $fullname = trim($this->fbuser['first_name'] .
614             ' ' . $this->fbuser['last_name']);
615
616         if (!empty($fullname)) {
617             $fullname = $this->nicknamize($fullname);
618             if ($this->isNewNickname($fullname)) {
619                 return $fullname;
620             }
621         }
622
623         return null;
624     }
625
626      /**
627       * Given a string, try to make it work as a nickname
628       */
629      function nicknamize($str)
630      {
631          $str = preg_replace('/\W/', '', $str);
632          return strtolower($str);
633      }
634
635      /*
636       * Is the desired nickname already taken?
637       *
638       * @return boolean result
639       */
640      function isNewNickname($str)
641      {
642         if (
643             !Validate::string(
644                 $str,
645                 array(
646                     'min_length' => 1,
647                     'max_length' => 64,
648                     'format' => NICKNAME_FMT
649                 )
650             )
651         ) {
652             return false;
653         }
654
655         if (!User::allowed_nickname($str)) {
656             return false;
657         }
658
659         if (User::staticGet('nickname', $str)) {
660             return false;
661         }
662
663         return true;
664     }
665
666     /*
667      * Do we already have a user record with this email?
668      * (emails have to be unique but they can change)
669      *
670      * @param string $email the email address to check
671      *
672      * @return boolean result
673      */
674      function isNewEmail($email)
675      {
676          // we shouldn't have to validate the format
677          $result = User::staticGet('email', $email);
678
679          if (empty($result)) {
680              common_debug("XXXXXXXXXXXXXXXXXX We've never seen this email before!!!");
681              return true;
682          }
683          common_debug("XXXXXXXXXXXXXXXXXX dupe email address!!!!");
684
685          return false;
686      }
687
688 }