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