]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/FacebookBridge/actions/facebookfinishlogin.php
XSS vulnerability when remote-subscribing
[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-2011 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 $fbuid       = null; // Facebook user ID
37     private $fbuser      = null; // Facebook user object (JSON)
38     private $accessToken = null; // Access token provided by Facebook JS API
39
40     function prepare($args) {
41         parent::prepare($args);
42
43         // Check cookie for a valid access_token
44
45         if (isset($_COOKIE['fb_access_token'])) {
46             $this->accessToken = $_COOKIE['fb_access_token'];
47         }
48
49         if (empty($this->accessToken)) {
50             $this->clientError(_m("Unable to authenticate you with Facebook."));
51         }
52
53         $graphUrl = 'https://graph.facebook.com/me?access_token=' . urlencode($this->accessToken);
54         $this->fbuser = json_decode(file_get_contents($graphUrl));
55
56         if (empty($this->fbuser)) {
57             // log badness
58
59             list($proxy, $ip) = common_client_ip();
60
61             common_log(
62                 LOG_WARNING,
63                     sprintf(
64                         'Failed Facebook authentication attempt, proxy = %s, ip = %s.',
65                          $proxy,
66                          $ip
67                     ),
68                     __FILE__
69             );
70
71             $this->clientError(
72                 // TRANS: Client error displayed when trying to connect to Facebook while not logged in.
73                 _m('You must be logged into Facebook to register a local account using Facebook.')
74             );
75         }
76
77         $this->fbuid  = $this->fbuser->id;
78         // OKAY, all is well... proceed to register
79         return true;
80     }
81
82     function handle($args)
83     {
84         parent::handle($args);
85
86         if (common_is_real_login()) {
87
88             // This will throw a client exception if the user already
89             // has some sort of foreign_link to Facebook.
90
91             $this->checkForExistingLink();
92
93             // Possibly reconnect an existing account
94
95             $this->connectUser();
96
97         } else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
98             $this->handlePost();
99         } else {
100             $this->tryLogin();
101         }
102     }
103
104     function checkForExistingLink() {
105
106         // User is already logged in, are her accounts already linked?
107
108         $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
109
110         if (!empty($flink)) {
111
112             // User already has a linked Facebook account and shouldn't be here!
113
114             $this->clientError(
115                 // TRANS: Client error displayed when trying to connect to a Facebook account that is already linked
116                 // TRANS: in the same StatusNet site.
117                 _m('There is already a local account linked with that Facebook account.')
118             );
119        }
120
121        $cur = common_current_user();
122        $flink = Foreign_link::getByUserID($cur->id, FACEBOOK_SERVICE);
123
124        if (!empty($flink)) {
125
126             // There's already a local user linked to this Facebook account.
127
128             $this->clientError(
129                 // TRANS: Client error displayed when trying to connect to a Facebook account that is already linked
130                 // TRANS: in the same StatusNet site.
131                 _m('There is already a local account linked with that Facebook account.')
132             );
133         }
134     }
135
136     function handlePost()
137     {
138         $token = $this->trimmed('token');
139
140         // CSRF protection
141         if (!$token || $token != common_session_token()) {
142             $this->showForm(
143                 // TRANS: Client error displayed when the session token does not match or is not given.
144                 _m('There was a problem with your session token. Try again, please.')
145             );
146             return;
147         }
148
149         if ($this->arg('create')) {
150
151             if (!$this->boolean('license')) {
152                 $this->showForm(
153                     // TRANS: Form validation error displayed when user has not agreed to the license.
154                     _m('You cannot register if you do not agree to the license.'),
155                     $this->trimmed('newname')
156                 );
157                 return;
158             }
159
160             // We has a valid Facebook session and the Facebook user has
161             // agreed to the SN license, so create a new user
162             $this->createNewUser();
163
164         } else if ($this->arg('connect')) {
165
166             $this->connectNewUser();
167
168         } else {
169
170             $this->showForm(
171                 // TRANS: Form validation error displayed when an unhandled error occurs.
172                 _m('An unknown error has occured.'),
173                 $this->trimmed('newname')
174             );
175         }
176     }
177
178     function showPageNotice()
179     {
180         if ($this->error) {
181
182             $this->element('div', array('class' => 'error'), $this->error);
183
184         } else {
185
186             $this->element(
187                 'div', 'instructions',
188                 sprintf(
189                     // TRANS: Form instructions for connecting to Facebook.
190                     // TRANS: %s is the site name.
191                     _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.'),
192                     common_config('site', 'name')
193                 )
194             );
195         }
196     }
197
198     function title()
199     {
200         // TRANS: Page title.
201         return _m('Facebook Setup');
202     }
203
204     function showForm($error=null, $username=null)
205     {
206         $this->error = $error;
207         $this->username = $username;
208
209         $this->showPage();
210     }
211
212     function showPage()
213     {
214         parent::showPage();
215     }
216
217     /**
218      * @todo FIXME: Much of this duplicates core code, which is very fragile.
219      * Should probably be replaced with an extensible mini version of
220      * the core registration form.
221      */
222     function showContent()
223     {
224         if (!empty($this->message_text)) {
225             $this->element('p', null, $this->message);
226             return;
227         }
228
229         $this->elementStart('form', array('method' => 'post',
230                                           'id' => 'form_settings_facebook_connect',
231                                           'class' => 'form_settings',
232                                           'action' => common_local_url('facebookfinishlogin')));
233         $this->elementStart('fieldset', array('id' => 'settings_facebook_connect_options'));
234         // TRANS: Fieldset legend.
235         $this->element('legend', null, _m('Connection options'));
236         $this->elementStart('ul', 'form_data');
237         $this->elementStart('li');
238         $this->element('input', array('type' => 'checkbox',
239                                       'id' => 'license',
240                                       'class' => 'checkbox',
241                                       'name' => 'license',
242                                       'value' => 'true'));
243         $this->elementStart('label', array('class' => 'checkbox', 'for' => 'license'));
244         // TRANS: %s is the name of the license used by the user for their status updates.
245         $message = _m('My text and files are available under %s ' .
246                      'except this private data: password, ' .
247                      'email address, IM address, and phone number.');
248         $link = '<a href="' .
249                 htmlspecialchars(common_config('license', 'url')) .
250                 '">' .
251                 htmlspecialchars(common_config('license', 'title')) .
252                 '</a>';
253         $this->raw(sprintf(htmlspecialchars($message), $link));
254         $this->elementEnd('label');
255         $this->elementEnd('li');
256         $this->elementEnd('ul');
257
258         $this->elementStart('fieldset');
259         $this->hidden('token', common_session_token());
260         $this->element('legend', null,
261                        // TRANS: Fieldset legend.
262                        _m('Create new account'));
263         $this->element('p', null,
264                        // TRANS: Form instructions.
265                        _m('Create a new user with this nickname.'));
266         $this->elementStart('ul', 'form_data');
267
268         // Hook point for captcha etc
269         Event::handle('StartRegistrationFormData', array($this));
270
271         $this->elementStart('li');
272         // TRANS: Field label.
273         $this->input('newname', _m('New nickname'),
274                      ($this->username) ? $this->username : '',
275                      // TRANS: Field title.
276                      _m('1-64 lowercase letters or numbers, no punctuation or spaces.'));
277         $this->elementEnd('li');
278
279         // Hook point for captcha etc
280         Event::handle('EndRegistrationFormData', array($this));
281
282         $this->elementEnd('ul');
283         // TRANS: Submit button to create a new account.
284         $this->submit('create', _m('BUTTON','Create'));
285         $this->elementEnd('fieldset');
286
287         $this->elementStart('fieldset');
288         $this->element('legend', null,
289                        // TRANS: Fieldset legend.
290                        _m('Connect existing account'));
291         $this->element('p', null,
292                        // TRANS: Form instructions.
293                        _m('If you already have an account, login with your username and password to connect it to your Facebook.'));
294         $this->elementStart('ul', 'form_data');
295         $this->elementStart('li');
296         // TRANS: Field label.
297         $this->input('nickname', _m('Existing nickname'));
298         $this->elementEnd('li');
299         $this->elementStart('li');
300         // TRANS: Field label.
301         $this->password('password', _m('Password'));
302         $this->elementEnd('li');
303         $this->elementEnd('ul');
304         // TRANS: Submit button to connect a Facebook account to an existing StatusNet account.
305         $this->submit('connect', _m('BUTTON','Connect'));
306         $this->elementEnd('fieldset');
307
308         $this->elementEnd('fieldset');
309         $this->elementEnd('form');
310     }
311
312     function message($msg)
313     {
314         $this->message_text = $msg;
315         $this->showPage();
316     }
317
318     function createNewUser()
319     {
320         if (!Event::handle('StartRegistrationTry', array($this))) {
321             return;
322         }
323
324         if (common_config('site', 'closed')) {
325             // TRANS: Client error trying to register with registrations not allowed.
326             $this->clientError(_m('Registration not allowed.'));
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             }
337
338             $invite = Invitation::getKV($code);
339
340             if (empty($invite)) {
341                 // TRANS: Client error trying to register with an invalid invitation code.
342                 $this->clientError(_m('Not a valid invitation code.'));
343             }
344         }
345
346         try {
347             $nickname = Nickname::normalize($this->trimmed('newname'), true);
348         } catch (NicknameException $e) {
349             $this->showForm($e->getMessage());
350             return;
351         }
352
353         $args = array(
354             'nickname' => $nickname,
355             'fullname' => $this->fbuser->name,
356             'homepage' => $this->fbuser->website,
357             'location' => $this->fbuser->location->name
358         );
359
360         // It's possible that the email address is already in our
361         // DB. It's a unique key, so we need to check
362         if ($this->isNewEmail($this->fbuser->email)) {
363             $args['email']           = $this->fbuser->email;
364             if (isset($this->fuser->verified) && $this->fuser->verified == true) {
365                 $args['email_confirmed'] = true;
366             }
367         }
368
369         if (!empty($invite)) {
370             $args['code'] = $invite->code;
371         }
372
373         $user   = User::register($args);
374         $result = $this->flinkUser($user->id, $this->fbuid);
375
376         if (!$result) {
377             // TRANS: Server error displayed when connecting to Facebook fails.
378             $this->serverError(_m('Error connecting user to Facebook.'));
379         }
380
381         // Add a Foreign_user record
382         Facebookclient::addFacebookUser($this->fbuser);
383
384         $this->setAvatar($user);
385
386         common_set_user($user);
387         common_real_login(true);
388
389         common_log(
390             LOG_INFO,
391             sprintf(
392                 'Registered new user %s (%d) from Facebook user %s, (fbuid %d)',
393                 $user->nickname,
394                 $user->id,
395                 $this->fbuser->name,
396                 $this->fbuid
397             ),
398             __FILE__
399         );
400
401         Event::handle('EndRegistrationTry', array($this));
402
403         $this->goHome($user->nickname);
404     }
405
406     /*
407      * Attempt to download the user's Facebook picture and create a
408      * StatusNet avatar for the new user.
409      */
410     function setAvatar($user)
411     {
412          try {
413             $picUrl = sprintf(
414                 'http://graph.facebook.com/%d/picture?type=large',
415                 $this->fbuser->id
416             );
417
418             // fetch the picture from Facebook
419             $client = new HTTPClient();
420
421             // fetch the actual picture
422             $response = $client->get($picUrl);
423
424             if ($response->isOk()) {
425
426                 // seems to always be jpeg, but not sure
427                 $tmpname = "facebook-avatar-tmp-" . common_random_hexstr(4);
428
429                 $ok = file_put_contents(
430                     Avatar::path($tmpname),
431                     $response->getBody()
432                 );
433
434                 if (!$ok) {
435                     common_log(LOG_WARNING, 'Couldn\'t save tmp Facebook avatar: ' . $tmpname, __FILE__);
436                 } else {
437                     // save it as an avatar
438
439                     $imagefile = new ImageFile(null, Avatar::path($tmpname));
440                     $filename = Avatar::filename($user->id, image_type_to_extension($imagefile->preferredType()),
441                                                  180, common_timestamp());
442                     // Previous docs said 180 is the "biggest img we get from Facebook"
443                     $imagefile->resizeTo(Avatar::path($filename, array('width'=>180, 'height'=>180)));
444
445                     // No need to keep the temporary file around...
446                     @unlink(Avatar::path($tmpname));
447
448                     $profile   = $user->getProfile();
449
450                     if ($profile->setOriginal($filename)) {
451                         common_log(
452                             LOG_INFO,
453                             sprintf(
454                                 'Saved avatar for %s (%d) from Facebook picture for '
455                                     . '%s (fbuid %d), filename = %s',
456                                  $user->nickname,
457                                  $user->id,
458                                  $this->fbuser->name,
459                                  $this->fbuid,
460                                  $filename
461                              ),
462                              __FILE__
463                         );
464
465                         // clean up tmp file
466                     }
467
468                 }
469             }
470         } catch (Exception $e) {
471             common_log(LOG_WARNING, 'Couldn\'t save Facebook avatar: ' . $e->getMessage(), __FILE__);
472             // error isn't fatal, continue
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             // TRANS: Form validation error displayed when username/password combination is incorrect.
483             $this->showForm(_m('Invalid username or password.'));
484             return;
485         }
486
487         $user = User::getKV('nickname', $nickname);
488
489         $this->tryLinkUser($user);
490
491         common_set_user($user);
492         common_real_login(true);
493
494         // clear out the stupid cookie
495         setcookie('fb_access_token', '', time() - 3600); // one hour ago
496
497         $this->goHome($user->nickname);
498     }
499
500     function connectUser()
501     {
502         $user = common_current_user();
503         $this->tryLinkUser($user);
504
505         // clear out the stupid cookie
506         setcookie('fb_access_token', '', time() - 3600); // one hour ago
507         common_redirect(common_local_url('facebookfinishlogin'), 303);
508     }
509
510     function tryLinkUser($user)
511     {
512         $result = $this->flinkUser($user->id, $this->fbuid);
513
514         if (empty($result)) {
515             // TRANS: Server error displayed when connecting to Facebook fails.
516             $this->serverError(_m('Error connecting user to Facebook.'));
517         }
518     }
519
520     function tryLogin()
521     {
522         try {
523             $flink = Foreign_link::getByForeignID($this->fbuid, FACEBOOK_SERVICE);
524             $user = $flink->getUser();
525
526             common_log(
527                 LOG_INFO,
528                 sprintf(
529                     'Logged in Facebook user %s as user %d (%s)',
530                     $this->fbuid,
531                     $user->nickname,
532                     $user->id
533                 ),
534                 __FILE__
535             );
536
537             common_set_user($user);
538             common_real_login(true);
539
540             // clear out the stupid cookie
541             setcookie('fb_access_token', '', time() - 3600); // one hour ago
542
543             $this->goHome($user->nickname);
544
545         } catch (NoResultException $e) {
546             $this->showForm(null, $this->bestNewNickname());
547         }
548     }
549
550     function goHome($nickname)
551     {
552         $url = common_get_returnto();
553         if ($url) {
554             // We don't have to return to it again
555             common_set_returnto(null);
556         } else {
557             $url = common_local_url('all',
558                                     array('nickname' =>
559                                           $nickname));
560         }
561
562         common_redirect($url, 303);
563     }
564
565     function flinkUser($user_id, $fbuid)
566     {
567         $flink = new Foreign_link();
568
569         $flink->user_id     = $user_id;
570         $flink->foreign_id  = $fbuid;
571         $flink->service     = FACEBOOK_SERVICE;
572         $flink->credentials = $this->accessToken;
573         $flink->created     = common_sql_now();
574
575         $flink_id = $flink->insert();
576
577         return $flink_id;
578     }
579
580     function bestNewNickname()
581     {
582         try {
583             $nickname = Nickname::normalize($this->fbuser->username, true);
584             return $nickname;
585         } catch (NicknameException $e) {
586             // Failed to normalize nickname, but let's try the full name
587         }
588
589         try {
590             $nickname = Nickname::normalize($this->fbuser->name, true);
591             return $nickname;
592         } catch (NicknameException $e) {
593             // Any more ideas? Nope.
594         }
595
596         return null;
597     }
598
599     /*
600      * Do we already have a user record with this email?
601      * (emails have to be unique but they can change)
602      *
603      * @param string $email the email address to check
604      *
605      * @return boolean result
606      */
607      function isNewEmail($email)
608      {
609          // we shouldn't have to validate the format
610          $result = User::getKV('email', $email);
611
612          if (empty($result)) {
613              return true;
614          }
615
616          return false;
617      }
618 }