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