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