Now supporting page tabs similar to canvas pages.
[project/fb.git] / fb_user.module
1 <?php
2 /**
3 * @file
4 * This module manages relations between local Drupal user accounts
5 * and their accounts on facebook.com.
6 *
7 * This module can create a new local user account, when a facebook
8 * user authorizes an application hosted on this server.
9 *
10 * Links existing local accounts to remote accounts on facebook via
11 * fb_user table.
12 *
13 * Drupal refers to a local user id as 'uid'. Facebook's documentation
14 * and code also uses 'uid'. In these modules we use 'fbu' for facebook's
15 * id and 'uid' for Drupal's id.
16 */
17
18 define('FB_USER_OPTION_CREATE_NEVER', 1);
19 define('FB_USER_OPTION_CREATE_LOGIN', 2);
20
21 define('FB_USER_OPTION_MAP_NEVER', 1);
22 define('FB_USER_OPTION_MAP_ALWAYS', 2); // Map when user is registered and authorized.
23 define('FB_USER_OPTION_MAP_EMAIL', 3); // Map when email is exact match.
24
25 define('FB_USER_VAR_USERNAME_STYLE', 'fb_user_username_style'); // Key used in variables table for this option.
26 define('FB_USER_OPTION_USERNAME_FULL', 1); // Get full name from FB
27 define('FB_USER_OPTION_USERNAME_FBU', 2); // Use unique name
28
29 define('FB_USER_VAR_ALTER_REGISTER', 'fb_user_alter_register');
30 define('FB_USER_VAR_ALTER_LOGIN', 'fb_user_alter_login');
31 define('FB_USER_VAR_ALTER_LOGIN_BLOCK', 'fb_user_alter_login_block');
32 define('FB_USER_VAR_ALTER_CONTACT', 'fb_user_alter_contact');
33
34 define('FB_USER_VAR_TEXT_REGISTER', 'fb_button_text_register');
35 define('FB_USER_VAR_TEXT_LOGIN', 'fb_button_text_login');
36 define('FB_USER_VAR_TEXT_LOGIN_BLOCK', 'fb_button_text_login_block');
37
38 define('FB_USER_VAR_CHECK_SESSION', 'fb_user_check_session');
39
40 // Controls - see fb_controls().
41 define('FB_USER_CONTROL_NO_CREATE_ACCOUNT', 'fb_user_no_account');
42 define('FB_USER_CONTROL_NO_CREATE_MAP', 'fb_user_no_map_create');
43 define('FB_USER_CONTROL_NO_HONOR_MAP', 'fb_user_no_map');
44 define('FB_USER_CONTROL_NO_REDIRECT', 'fb_user_no_redirect');
45
46 // hook_fb_user().
47 define('FB_USER_OP_PRE_USER', 'pre_user'); // Before account creation, fb_user.module
48 define('FB_USER_OP_POST_USER', 'post_user'); // After account creation, fb_user.module
49 define('FB_USER_OP_POST_EXTERNAL_LOGIN', 'post_external_login'); // user map has changed global user.
50 define('FB_USER_OP_POST_USER_CONNECT', 'post_user_connect'); // Connected local account to FB account, fb_user.module
51 define('FB_USER_OP_POST_USER_DISCONNECT', 'post_user_disconnect'); // Disconnected local account from FB account, fb_user.module
52
53
54 /**
55 * Implements hook_permission().
56 */
57 function fb_user_permission() {
58 return array(
59 'delete own fb_user authmap' => array(
60 'title' => t('Delete own fb_user authmap'),
61 'description' => t('User can remove their connection to Facebook.'),
62 ),
63 );
64 }
65
66 /**
67 * Implements hook_menu().
68 */
69 function fb_user_menu() {
70 $items = array();
71
72 // Admin pages
73 $items[FB_PATH_ADMIN . '/fb_user'] = array(
74 'title' => 'User Settings',
75 'description' => 'Local account to facebook account mapping',
76 'page callback' => 'drupal_get_form',
77 'page arguments' => array('fb_user_admin_settings'),
78 'access arguments' => array(FB_PERM_ADMINISTER),
79 'file' => 'fb_user.admin.inc',
80 'type' => MENU_LOCAL_TASK,
81 );
82
83 return $items;
84 }
85
86 /**
87 * Returns configuration for this module, on a per-app basis.
88 */
89 function _fb_user_get_config($fb_app) {
90 $fb_app_data = fb_get_app_data($fb_app);
91 $fb_user_data = isset($fb_app_data['fb_user']) ? $fb_app_data['fb_user'] : array();
92
93 // Merge in defaults
94 $fb_user_data += array(
95 'create_account' => FB_USER_OPTION_CREATE_NEVER,
96 'map_account' => array(
97 FB_USER_OPTION_MAP_ALWAYS => FB_USER_OPTION_MAP_ALWAYS,
98 FB_USER_OPTION_MAP_EMAIL => FB_USER_OPTION_MAP_EMAIL,
99 ),
100 'new_user_rid' => NULL,
101 'connected_user_rid' => NULL,
102 );
103 return $fb_user_data;
104 }
105
106 /**
107 * There are several pages where we don't want to automatically create a new
108 * account or use an account configured for this app.
109 */
110 function _fb_user_special_page() {
111 // fb_app/event is called by facebook. Don't create accounts on that page.
112 return ((arg(0) == 'fb_app' && arg(1) == 'event'));
113 }
114
115 /**
116 * Implements hook_fb.
117 */
118 function fb_user_fb($op, $data, &$return) {
119 $fb_app = isset($data['fb_app']) ? $data['fb_app'] : NULL;
120 $fb = isset($data['fb']) ? $data['fb'] : NULL;
121
122 global $user;
123
124 if ($fb_app) {
125 $fb_user_data = _fb_user_get_config($fb_app);
126 }
127
128 if ($op == FB_OP_POST_INIT && $fb) {
129 $fbu = fb_facebook_user();
130 if (isset($_SESSION['fb_user_fbu']) &&
131 $_SESSION['fb_user_fbu'] != $fbu &&
132 !(fb_settings(FB_SETTINGS_CB_SESSION) && !$fbu)) {
133 // User has logged out of facebook, and drupal is only now learning
134 // about it. Check disabled when using FB_SETTINGS_CB_SESSION, because
135 // we aren't always passed a signed_request in that case, which would
136 // otherwise trigger this.
137 _fb_logout();
138 if (!fb_controls(FB_USER_CONTROL_NO_REDIRECT)) {
139 drupal_goto(current_path()); // @TODO - need request params here?
140 }
141 }
142
143 if (_fb_user_special_page() ||
144 (variable_get('site_offline', FALSE) && !user_access('administer site configuration'))) {
145 // Prevent some behavior.
146 fb_controls(FB_USER_CONTROL_NO_HONOR_MAP, TRUE);
147 fb_controls(FB_USER_CONTROL_NO_CREATE_MAP, TRUE);
148 fb_controls(FB_USER_CONTROL_NO_CREATE_ACCOUNT, TRUE);
149 }
150 if (isset($_REQUEST['_fb_user_fbu'])) {
151 // We've triggered a reload. Don't redirect again, as that will
152 // cause infinite loop if browser not accepting third-party cookies.
153 fb_controls(FB_USER_CONTROL_NO_REDIRECT, TRUE);
154 }
155
156 if ($rid = $fb_user_data['connected_user_rid']) {
157 if ($fbu) {
158 // User is connected to facebook.
159 if (!isset($user->roles[$rid])) {
160 $user->roles[$rid] = $rid; // Should be role name, but that requires db query.
161 // Reload user permissions.
162 drupal_static_reset('user_access');
163 drupal_static_reset('menu_get_item');
164 }
165 }
166 else {
167 // User is not connected to facebook.
168 if ($rid != DRUPAL_AUTHENTICATED_RID && isset($user->roles[$rid])) {
169 // Out of paranoia, unset role. This will be reached only if the
170 //user was somehow saved while connected to facebook.
171 unset($user->roles[$rid]);
172 // Reload user permissions.
173 drupal_static_reset('user_access');
174 drupal_static_reset('menu_get_item');
175 }
176 }
177 }
178 }
179 elseif ($op == FB_OP_GET_FBU) {
180 // This is a request to learn the user's FB id.
181 $return = _fb_user_get_fbu($data['uid']);
182 }
183 elseif ($op == FB_OP_GET_UID) {
184 // This is a request to learn the facebook user's local id.
185 $return = _fb_user_get_uid($data['fbu'], $data['fb_app']);
186 }
187 elseif ($op == FB_OP_AJAX_EVENT) {
188 // fb.js has notified us of an event via AJAX. Not the same as facebook event callback above.
189 if ($data['event_type'] == 'session_change' && isset($data['event_data']['fbu'])) {
190 // A user has logged in.
191 // Don't trust fbu from $data['event_data'], too easy to spoof.
192 // Don't set fb_user if SESSION[fb_user_fbu], could be an old session not properly cleaned up.
193 if (($fbu = fb_facebook_user($data['fb'])) &&
194 $fbu != fb_get_fbu($GLOBALS['user'])) {
195
196 // In ajax callback, there's no reason to redirect even if user
197 // changes. But we should honor session, as even ajax can set a new
198 // cookie.
199 fb_controls(FB_USER_CONTROL_NO_REDIRECT, TRUE);
200
201 _fb_user_process_authorized_user();
202 }
203 }
204 }
205 }
206
207 /**
208 * Implements hook_page_alter().
209 *
210 * Reload page if user has changed. This would not make sense during an ajax
211 * callback (or anything else) where a redirect would not refresh the browsers
212 * page. That's why we do it here in page_alter().
213 */
214 function fb_user_page_alter(&$page) {
215 _fb_user_check_and_goto();
216 }
217
218 /**
219 * Detect whether facebook indicates the user has changed. If so, redirect.
220 */
221 function _fb_user_check_and_goto() {
222 if (($fbu = fb_facebook_user()) &&
223 !fb_is_tab() && // $fbu is page id, not visitor id, on tabs.
224 $fbu != fb_get_fbu($GLOBALS['user']) &&
225 current_path() !== variable_get('site_403', FALSE) &&
226 current_path() !== variable_get('site_404', FALSE)) {
227 $uid = $GLOBALS['user']->uid; // Remember original uid.
228 _fb_user_process_authorized_user();
229 if ($uid != $GLOBALS['user']->uid) {
230 // during user processing, we started a new session.
231 if (!fb_controls(FB_USER_CONTROL_NO_REDIRECT)) {
232 // No longer passing _fb_user_fbu, because it causes problem when logging out of one frame then refreshing a frame with that parameter in the URL.
233 drupal_goto(request_path(), array(
234 //'_fb_user_fbu' => $fbu, // Avoid refresh loop, disabled for now. Still needed?
235 ));
236 }
237 }
238 }
239 }
240
241 /**
242 * Test facebook session by calling into facebook. This is expensive, so
243 * limit check to once per session. Use session variable to flag that we have
244 * completed the test.
245 */
246 function _fb_user_check_session($fbu) {
247 // Make sure facebook session is valid and fb_user table is correct.
248 // Relatively expensive operations, so we perform them only once per session.
249 if (!isset($_SESSION['fb_user_fbu']) || $_SESSION['fb_user_fbu'] != $fbu) {
250 if ($valid_session = fb_api_check_session($GLOBALS['_fb'])) { // Expensive check.
251 $_SESSION['fb_user_fbu'] = $fbu;
252 }
253 else {
254 unset($_SESSION['fb_user_fbu']);
255 }
256 }
257 return (isset($_SESSION['fb_user_fbu']) && $_SESSION['fb_user_fbu'] == $fbu);
258 }
259
260 /**
261 * If facebook user has authorized app, and account map exists, login as the local user.
262 *
263 * @return - TRUE, if user_external_login succeeds.
264 */
265 function _fb_user_external_login($account = NULL) {
266 $fbu = fb_facebook_user();
267 if (!$account) {
268 $account = fb_user_get_local_user($fbu, $GLOBALS['_fb_app']);
269 }
270 if ($account &&
271 $account->uid == $GLOBALS['user']->uid) {
272 // Already logged in.
273 return $account;
274 }
275 elseif ($fbu &&
276 $account &&
277 $account->uid != $GLOBALS['user']->uid &&
278 !fb_controls(FB_USER_CONTROL_NO_HONOR_MAP)) {
279
280 // Map exists. Log in as local user.
281 $session_id = session_id();
282 if (fb_verbose() === 'extreme') { // debug
283 watchdog("fb_user", "fb_user_fb changing user to $account->uid");
284 }
285
286 // user_external_login() fails if already logged in, so log out first.
287 if ($GLOBALS['user']->uid) {
288 _fb_logout();
289 }
290
291 // user_external_login() removed in D7, no replacement. Let's hope the following works.
292 $GLOBALS['user'] = $account;
293 $drupal_sux = (array) $account;
294 user_login_finalize($drupal_sux);
295
296 // Special effort to support browsers without third-party cookies.
297 if (function_exists('fb_sess_regenerate_hack')) {
298 fb_sess_regenerate_hack();
299 }
300
301 if (fb_verbose() === 'extreme') { // debug
302 watchdog("fb_user", "fb_user_fb changed session from $session_id to " . session_id());
303 }
304
305 // Session changed after external login. Invoking hook here allows modules to drupal_set_message().
306 fb_invoke(FB_USER_OP_POST_EXTERNAL_LOGIN, array('account' => $account), NULL, 'fb_user');
307 return $account;
308 }
309 return FALSE;
310 }
311
312 /**
313 * Create a map linking the facebook account to the currently logged in local user account.
314 *
315 * @return - TRUE, if map created.
316 */
317 function _fb_user_create_map() {
318 if ($GLOBALS['user']->uid) {
319 $fbu = fb_facebook_user();
320 $account = fb_user_get_local_user($fbu);
321 if ($fbu &&
322 !$account &&
323 !fb_controls(FB_USER_CONTROL_NO_CREATE_MAP)) {
324 _fb_user_set_map($GLOBALS['user'], $fbu);
325 fb_invoke(FB_USER_OP_POST_USER_CONNECT, array(
326 'account' => $GLOBALS['user'],
327 'fbu' => $fbu,
328 ), NULL, 'fb_user');
329 return TRUE;
330 }
331 }
332 return FALSE;
333 }
334
335 function _fb_user_create_map_by_email() {
336 $fbu = fb_facebook_user();
337 $account = fb_user_get_local_user($fbu, $GLOBALS['_fb_app']);
338 if ($fbu &&
339 !$account &&
340 ($email_account = fb_user_get_local_user_by_email($fbu)) &&
341 !fb_controls(FB_USER_CONTROL_NO_CREATE_MAP)) {
342 _fb_user_set_map($email_account, $fbu);
343 fb_invoke(FB_USER_OP_POST_USER_CONNECT, array(
344 'account' => $GLOBALS['user'],
345 'fbu' => $fbu,
346 ), NULL, 'fb_user');
347 return TRUE;
348 }
349 return FALSE;
350 }
351
352 /**
353 * Helper function to create local account for the currently authorized user.
354 */
355 function _fb_user_create_local_account() {
356 $fbu = fb_facebook_user();
357 $account = fb_user_get_local_user($fbu);
358 if ($fbu &&
359 !$account &&
360 !fb_controls(FB_USER_CONTROL_NO_CREATE_ACCOUNT)) {
361 $config = _fb_user_get_config($GLOBALS['_fb_app']);
362
363 // Establish user name.
364 // Case 1: use name from FB
365 // Case 2: create a unique user name ourselves
366 // Which we use is determined by the setting at
367 // admin/structure/fb/fb_user
368 if (variable_get(FB_USER_VAR_USERNAME_STYLE, FB_USER_OPTION_USERNAME_FBU) == FB_USER_OPTION_USERNAME_FULL) {
369 try {
370 // Use fb->api() rather than fb_users_getInfo(). Later fails to learn name on test accounts.
371 $info = $GLOBALS['_fb']->api($fbu);
372 $username = $info['name'];
373 } catch (Exception $e) {
374 fb_log_exception($e, t('Failed to learn full name of new user'), $GLOBALS['_fb']);
375 }
376 }
377 else {
378 // Create a name that is likely to be unique.
379 $username = "$fbu@facebook";
380 }
381
382 if ($config['new_user_rid']) {
383 $roles = array($config['new_user_rid'] => TRUE);
384 }
385 else {
386 $roles = array();
387 }
388
389 $account = fb_user_create_local_user($GLOBALS['_fb'], $GLOBALS['_fb_app'],
390 $fbu, array(
391 'name' => $username,
392 'roles' => $roles,
393 ));
394 watchdog('fb_user',
395 t("Created new user !username for application %app", array(
396 '!username' => l($account->name, 'user/' . $account->uid),
397 '%app' => $GLOBALS['_fb_app']->label)));
398
399 return $account;
400 }
401 return FALSE;
402 }
403
404 /**
405 * Create local account or account map for a facebook user who has authorized the application.
406 */
407 function _fb_user_process_authorized_user() {
408 $fbu = fb_facebook_user();
409 $mapped = FALSE;
410
411 if ($fbu &&
412 (!variable_get(FB_USER_VAR_CHECK_SESSION, FALSE) || _fb_user_check_session($fbu))) {
413 $fb_app = $GLOBALS['_fb_app'];
414 // First check if map already exists.
415 $account = fb_user_get_local_user($fbu, $fb_app);
416 $config = _fb_user_get_config($fb_app);
417
418 if (!$account) {
419 if ($GLOBALS['user']->uid > 0 &&
420 $config['map_account'][FB_USER_OPTION_MAP_ALWAYS]) {
421 // Create map for logged in user.
422 $mapped = _fb_user_create_map();
423 }
424 if (!$mapped &&
425 $config['map_account'][FB_USER_OPTION_MAP_EMAIL]) {
426 // Create map if email matches.
427 $mapped = _fb_user_create_map_by_email();
428 }
429
430 if (!$mapped &&
431 $config['create_account'] == FB_USER_OPTION_CREATE_LOGIN) {
432 // Create new local account with map.
433 $mapped = _fb_user_create_local_account();
434 }
435
436 if ($mapped) {
437 $account = fb_user_get_local_user($fbu, $fb_app);
438 }
439 }
440
441 if ($account) {
442 // Ensure the user has any roles associated with this app.
443 $rid = $config['new_user_rid'];
444 if ($account && $rid &&
445 (!isset($account->roles[$rid]) || !$account->roles[$rid])) {
446 // there should be an API for this...
447 $query = db_insert('users_roles')
448 ->fields(array(
449 'uid' => $account->uid,
450 'rid' => $rid))
451 ->execute();
452 watchdog('fb_user', "Added role %role to existing user !username for application %app", array(
453 '!username' => theme('username', $account),
454 '%app' => $fb_app->label,
455 '%role' => $rid));
456 }
457
458 // Login as facebook user, if not already.
459 _fb_user_external_login($account);
460 }
461 }
462 }
463
464
465 function _fb_user_facebook_data($fb) {
466 if ($fbu = fb_facebook_user($fb)) {
467 try {
468 $data = fb_api($fbu);
469 return $data;
470 }
471 catch (FacebookApiException $e) {
472 fb_log_exception($e, t('Failed lookup of %fbu.', array('%fbu' => $fbu)));
473 }
474 }
475 }
476
477 /**
478 * Helper function to retrieve button text.
479 */
480 function _fb_user_button_text($form_id) {
481 $button_text = &drupal_static(__FUNCTION__);
482
483 if (!isset($button_text)) {
484 $button_text = array(
485 'user_register_form' => variable_get(FB_USER_VAR_TEXT_REGISTER, NULL),
486 'user_login' => variable_get(FB_USER_VAR_TEXT_LOGIN, NULL),
487 'user_login_block' => variable_get(FB_USER_VAR_TEXT_LOGIN_BLOCK, NULL),
488 );
489 }
490
491 return isset($button_text[$form_id]) ? $button_text[$form_id] : '';
492 }
493
494 /**
495 * Implements hook_form_alter().
496 */
497 function fb_user_form_alter(&$form, &$form_state, $form_id) {
498 if (isset($form['fb_app_data'])) {
499 // Add our settings to the fb_app edit form.
500 module_load_include('inc', 'fb_user', 'fb_user.admin');
501 fb_user_admin_form_alter($form, $form_state, $form_id);
502 }
503 elseif ($form_id == 'user_edit' && ($app = $form['#fb_app'])) {
504 // Disable buttons on user/edit/app pages, nothing to submit
505 unset($form['submit']);
506 unset($form['delete']);
507 }
508
509 // Add name and email to some forms.
510 if (isset($GLOBALS['_fb'])) {
511 $fb = $GLOBALS['_fb'];
512 if (!$GLOBALS['user']->uid && // No alters to user add form.
513 (($form_id == 'user_register_form' && variable_get(FB_USER_VAR_ALTER_REGISTER, TRUE)) ||
514 ($form_id == 'user_login' && variable_get(FB_USER_VAR_ALTER_LOGIN, TRUE)) ||
515 ($form_id == 'user_login_block' && variable_get(FB_USER_VAR_ALTER_LOGIN_BLOCK, TRUE)))) {
516
517 if ($fbu = fb_facebook_user()) {
518 // Facebook user has authorized app.
519
520 // Show user name and picture.
521 $form['fb_user'] = array(
522 'name' => array(
523 '#markup' => '<fb:name uid="' . $fbu . '" useyou="false" linked="false"></fb:name>',
524 '#prefix' => '<div class="fb_user_name">',
525 '#suffix' => '</div>',
526 ),
527 'picture' => array(
528 '#markup' => '<fb:profile-pic uid="' . $fbu . '" linked="false"></fb:profile-pic>',
529 '#prefix' => '<div class="fb_user_picture">',
530 '#suffix' => '</div>',
531 ),
532 '#weight' => -1,
533 );
534
535 if ($form_id == 'user_register_form') {
536 // Provide defaults for name and email.
537 if ($data = _fb_user_facebook_data($fb)) {
538 $form['fb_user']['#fb_user'] = $data;
539
540 if (isset($form['name']) && !$form['name']['#default_value']) {
541 // @TODO - ensure name is unique to Drupal.
542 $form['name']['#default_value'] = $data['name'];
543 }
544 elseif (isset($form['account']) && isset($form['account']['name']) &&
545 !$form['account']['name']['#default_value']) {
546 // @TODO - ensure name is unique to Drupal.
547 $form['account']['name']['#default_value'] = $data['name'];
548 }
549 if (isset($form['mail']) && !$form['mail']['#default_value']) {
550 $form['mail']['#default_value'] = $data['email'];
551 }
552 elseif (isset($form['account']['mail']) && isset($form['account']['mail']) &&
553 !$form['account']['mail']['#default_value']) {
554 $form['account']['mail']['#default_value'] = $data['email'];
555 }
556 }
557 }
558 }
559
560 else {
561 // facebook user has not authorized app.
562 $fb_button = theme('fb_login_button', array('text' => t(_fb_user_button_text($form_id))));
563 $form['fb_user'] = array(
564 '#type' => 'markup',
565 '#markup' => $fb_button,
566 '#weight' => -1, // Ideally, we'd put ourself next to openid login, but doesn't look right when next to form buttons.
567 '#prefix' => '<div class="fb_user-login-button-wrapper">',
568 '#suffix' => '</div>',
569 );
570 }
571 }
572 elseif ($form_id == 'contact_site_form' && variable_get(FB_USER_VAR_ALTER_CONTACT, TRUE)) {
573 if ($data = _fb_user_facebook_data($fb)) {
574 if (!$form['name']['#default_value'] || strpos($form['name']['#default_value'], '@facebook')) {
575 $form['name']['#default_value'] = $data['name'];
576 }
577 if (!$form['mail']['#default_value']) {
578 $form['mail']['#default_value'] = $data['email'];
579 }
580 }
581 }
582 }
583
584 }
585
586 /**
587 * Helper function for menu item access check.
588 */
589 function fb_user_access_own($account, $perm, $allow_admin) {
590 if ($GLOBALS['user']->uid == $account->uid && user_access($perm)) {
591 return TRUE;
592 }
593 elseif ($allow_admin) {
594 return user_access('administer users');
595 }
596 }
597
598
599 /**
600 * Implements hook_user_load.
601 *
602 * Use no standard email, use proxy email if available
603 */
604 function fb_user_user_load($users) {
605 global $_fb_app;
606
607 foreach ($users as $account) {
608 if ($account->uid && $_fb_app) {
609 if (!$account->mail && ($fbu = _fb_user_get_fbu($account->uid))) {
610 // Use proxied email, if facebook app is active and user uses it.
611 // TODO: confirm drupal never saves proxied address to users.mail.
612 $account->mail = fb_user_get_proxied_email($fbu, $_fb_app);
613 $account->fb_user_proxied_mail = $account->mail; // Remember where we got address.
614 }
615 }
616 }
617 }
618
619 /**
620 * Implements hook_user_login.
621 *
622 * Map local Drupal user to FB user under certain circumstances.
623 */
624 function fb_user_user_login(&$edit, $account) {
625 global $user, $_fb_app;
626
627 // A facebook user has logged in. We can map the two accounts together.
628 $fb_user_data = _fb_user_get_config($_fb_app);
629 if (($fbu = fb_facebook_user()) &&
630 $fb_user_data['map_account'][FB_USER_OPTION_MAP_ALWAYS] &&
631 !fb_controls(FB_USER_CONTROL_NO_CREATE_MAP)) {
632
633 // Create fb_user record if it doesn't exist or update existing one
634 _fb_user_set_map($account, $fbu);
635
636 // @TODO - if the app has a role, make sure the user gets that role. (presently,
637 // that will not happen until their next request)
638 }
639 }
640
641 /**
642 * Implements hook_user_insert.
643 *
644 * When user is created create record
645 * in fb_user to map local Drupal user to FB user.
646 */
647 function fb_user_user_insert(&$edit, $account, $category) {
648 global $user, $_fb_app;
649
650 // Map the two accounts together.
651 $fb_user_data = _fb_user_get_config($_fb_app);
652 if (($fbu = fb_facebook_user()) &&
653 $fb_user_data['map_account'][FB_USER_OPTION_MAP_ALWAYS] &&
654 !fb_controls(FB_USER_CONTROL_NO_CREATE_MAP)) {
655
656 // Create fb_user record if it doesn't exist or update existing one.
657 if ($account->uid == $user->uid) {
658 _fb_user_set_map($account, $fbu);
659 }
660
661 // @TODO - if the app has a role, make sure the user gets that role. (presently, that will not happen until their next request)
662 }
663 }
664
665 /**
666 * Implements hook_user_view.
667 *
668 * Show extra info when user being viewed.
669 */
670 function fb_user_user_view($account, $view_mode, $langcode) {
671
672 }
673
674 /**
675 * Implements hook_user_update.
676 *
677 * User is about to be updated.
678 */
679 function fb_user_user_update(&$edit, $account, $category) {
680 if ($edit['map']) {
681 _fb_user_set_map($account, $edit['map']);
682 }
683 else {
684 // Delete account mapping, because administrator has unchecked the connect option.
685 $num_deleted = db_delete('fb_user')
686 ->condition('uid', $account->uid)
687 ->execute();
688
689 fb_invoke(FB_USER_OP_POST_USER_DISCONNECT, array('account' => $account), NULL, 'fb_user');
690 }
691 }
692
693 /**
694 * Implements hook_user_delete.
695 *
696 * User is about to be deleted.
697 */
698 function fb_user_user_delete($account) {
699 $num_deleted = db_delete('fb_user')
700 ->condition('uid', $account->uid)
701 ->execute();
702 }
703
704 /**
705 * Implements hook_user_logout.
706 *
707 * User has logged out.
708 */
709 function fb_user_user_logout($account) {
710 global $user, $_fb_app;
711
712 if (fb_facebook_user() &&
713 fb_api_check_session($GLOBALS['_fb'])) {
714 // Log out of facebook, as well as Drupal. Note that code in
715 // fb_connect.js and fb_canvas.js attempts to call FB.logout. However,
716 // that code is not reached if the user types "/logout" directly into
717 // the browser URL. Also, a sometimes-occuring bug in firefox prevents
718 // FB.logout from always succeeding.
719
720 // Figure out where to send the user.
721 if (isset($_REQUEST['destination'])) {
722 $next_url = url($_REQUEST['destination'], array('absolute' => TRUE, 'fb_canvas' => fb_is_canvas()));
723 // Unset desination so drupal_goto() below does what we need it to do.
724 unset($_REQUEST['destination']);
725 }
726 else {
727 $next_url = url('<front>', array('absolute' => TRUE, 'fb_canvas' => fb_is_canvas()));
728 }
729 $logout_url = $GLOBALS['_fb']->getLogoutUrl(array(
730 'next' => $next_url,
731 'cancel_url' => $next_url,
732 ));
733 drupal_goto($logout_url);
734 }
735 }
736
737 /**
738 * Implements hook_form_user_profile_form_alter.
739 */
740 function fb_user_form_user_profile_form_alter(&$form, &$form_state, $form_id) {
741 global $user, $_fb_app;
742
743 if (!user_access('administer users') &&
744 !(user_access('delete own fb_user authmap') &&
745 $user->uid == $form['#user']->uid))
746 return; // hide from this user
747
748 $fb_user_data = _fb_user_get_config($_fb_app);
749 $account = $form['#user'];
750 $fbu = _fb_user_get_fbu($account->uid);
751
752 if ($fbu) {
753 // The drupal user is a facebook user.
754 $form['fb_user'] = array(
755 '#type' => 'fieldset',
756 '#title' => t('Facebook Application ' . $_fb_app->title),
757 '#collapsed' => false,
758 '#collapsible' => false,
759 );
760 $form['fb_user']['map'] = array(
761 '#type' => 'checkbox',
762 '#title' => t('Connect to facebook.com'),
763 '#default_value' => $fbu,
764 '#return_value' => $fbu,
765 '#description' => '',
766 );
767 // Now, learn more from facebook.
768 try {
769 $data = fb_api($fbu, array('access_token' => fb_get_token()));
770 if (count($data)) {
771 $form['fb_user']['map']['#description'] .=
772 t('Local account !username corresponds to !profile_page on Facebook.com.',
773 array(
774 '!username' => l($account->name, 'user/' . $account->uid),
775 '!profile_page' => l($data['name'], $data['link'])));
776 }
777 }
778 catch (Exception $e) {
779 fb_log_exception($e, t('Failed to get user data from facebook.'));
780 }
781
782 if (fb_facebook_user() == $fbu) {
783 // The user is currently connected to facebook. Depending on
784 // config, they may not be able to break the connection.
785 $form['fb_user']['map']['#disabled'] = TRUE;
786 $form['fb_user']['map']['#description'] .= '<br/>' . t('(Checkbox disabled because you are currently connected to facebook.)');
787 }
788 else {
789 $form['fb_user']['map']['#description'] .= '<br/>' . t('Uncheck then click save to delete this connection.');
790 }
791 }
792
793 if (!$fbu) { // this tells us that a mapping hasn't been created
794 if ($user->uid == $account->uid) {
795 // Could not obtain the $fbu from an existing map.
796 $fbu = fb_facebook_user();
797
798 if ($fbu) { // they are connected to facebook; give option to map
799
800 $form['fb_user'] = array(
801 '#type' => 'fieldset',
802 '#title' => t('Facebook Application ' . $_fb_app->title),
803 '#collapsed' => false,
804 '#collapsible' => false,
805 );
806
807 $form['fb_user']['map'] = array(
808 '#type' => 'checkbox',
809 '#title' => t('Connect account to facebook.com'),
810 '#default_value' => 0,
811 '#return_value' => $fbu,
812 '#description' => '',
813 );
814 $form['fb_user']['message'] = array(
815 '#markup' => t('If checked, link local account (!username) to facebook.com account (!fb_name).', array(
816 '!username' => theme('username', array('account' => $form['#user'])),
817 '!fb_name' => "<fb:name uid=$fbu useyou=false></fb:name>",
818 )),
819 '#prefix' => "\n<p>",
820 '#suffix' => "</p>\n",
821 );
822
823 }
824 elseif (!$fbu && $_fb_app) {
825 // they are not connected to facebook; give option to connect here
826 $form['fb_user'] = array(
827 '#type' => 'fieldset',
828 '#title' => t('Facebook Application ' . $_fb_app->title),
829 '#collapsed' => false,
830 '#collapsible' => false,
831 );
832
833 $fb_button = theme('fb_login_button', array('text' => t('Connect with Facebook')));
834 $form['fb_user']['button'] = array(
835 '#markup' => $fb_button,
836 '#weight' => -1,
837 '#prefix' => "\n<p>",
838 '#suffix' => "</p>\n",
839 );
840 }
841 }
842 else {
843 $form['fb_user'] = array(
844 '#type' => 'fieldset',
845 '#title' => t('Facebook Application ' . $_fb_app->title),
846 '#collapsed' => false,
847 '#collapsible' => false,
848 );
849
850 $form['fb_user']['message'] = array(
851 '#markup' => t('Local account !username is not connected to facebook.com.',
852 array('!username' => theme('username', array('account' => $form['#user'])),
853 )),
854 '#prefix' => "\n<p>",
855 '#suffix' => "</p>\n",
856 );
857 }
858 }
859
860 if (isset($form)) {
861 $form['fb_user']['map']['#tree'] = TRUE;
862 }
863 else {
864 // Could add a facebook connect button or canvas page authorization link.
865 $form['description'] = array(
866 '#markup' => t('This account is not associated with a Facebook Application.'),
867 '#prefix' => '<p>',
868 '#suffix' => '</p>',
869 );
870 }
871
872 // On user/edit, hide proxied email
873 if (isset($form['account']) && isset($form['account']['mail'])) {
874 $account = $form['#user'];
875 if (isset($account->fb_user_proxied_mail) &&
876 ($form['account']['mail']['#default_value'] == $account->fb_user_proxied_mail)) {
877 unset($form['account']['mail']['#default_value']);
878 }
879 }
880
881 return $form;
882 }
883
884
885 /**
886 * Helper function to add or update a row in the fb_user table, which maps local uid to facebook ids.
887 */
888 function _fb_user_set_map($account, $fbu) {
889 if ($fbu && $account->uid != 0) {
890
891 // Delete any pre-existing mapping that might exist for this local uid or fbu.
892 db_query("DELETE FROM {fb_user} WHERE uid=:uid OR fbu=:fbu", array(
893 ':uid' => $account->uid,
894 ':fbu' => $fbu,
895 ));
896
897 // Create the new mapping.
898 db_query("INSERT INTO {fb_user} (uid, fbu) VALUES (:uid, :fbu)", array(
899 ':uid' => $account->uid,
900 ':fbu' => $fbu,
901 ));
902
903 if (fb_verbose()) {
904 watchdog('fb_user', 'Using fb_user to associate user !user with facebook user id %fbu.',
905 array('!user' => l($account->name, 'user/' . $account->uid),
906 '%fbu' => $fbu,
907 ));
908 }
909 }
910 }
911
912
913 /**
914 * Creates a local Drupal account for the specified facebook user id.
915 *
916 * @param fbu
917 * The facebook user id corresponding to this account.
918 *
919 * @param edit
920 * An associative array with user configuration. As would be passed to user_save().
921 */
922 function fb_user_create_local_user($fb, $fb_app, $fbu,
923 $edit = array()) {
924
925 // Ensure $fbu is a real facebook user id.
926 if (!$fbu || !is_numeric($fbu)) {
927 return;
928 }
929
930 $account = fb_user_get_local_user($fbu);
931
932 if (!$account) {
933 // Create a new user in our system
934
935 // Learn some details from facebook.
936 $infos = fb_users_getInfo(array($fbu), $fb);
937 $info = $infos[0];
938
939 // All Drupal users get authenticated user role.
940 $edit['roles'][DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
941
942 if (isset($edit['name']) && $edit['name']) {
943 $username = $edit['name'];
944 }
945 else {
946 // Fallback, should never be reached.
947 $username = "$fbu@facebook";
948 $edit['name'] = $username;
949 }
950 $i = 0;
951
952 // Keep looking until we find a username_n that isn't being used.
953 while (db_query("SELECT 1 FROM {users} WHERE name = :name", array(':name' => $edit['name']))->fetchField(0)) {
954 $i++;
955 $edit['name'] = $username . '_' . $i;
956 }
957
958 // Give modules a way to suppress new account creation.
959 $edit['fb_user_do_create'] = TRUE;
960
961 // Allow third-party module to adjust any of our data before we create
962 // the user.
963 $edit = fb_invoke(FB_USER_OP_PRE_USER, array(
964 'fbu' => $fbu,
965 'fb' => $GLOBALS['_fb'],
966 'fb_app' => $fb_app,
967 'info' => $info,
968 ), $edit, 'fb_user');
969
970 if ($edit['fb_user_do_create']) {
971 unset($edit['fb_user_do_create']); // Don't confuse user_save.
972
973 // Fill in any default that are missing.
974 $defaults = array(
975 'pass' => user_password(),
976 'init' => $fbu . '@facebook', // Supposed to be email, but we may not know it.
977 'status' => 1,
978 );
979
980 // Mail available only if user has granted email extended permission.
981 if (isset($info['email'])) {
982 $defaults['mail'] = $info['email'];
983 }
984
985 // Merge defaults
986 $edit = array_merge($defaults, $edit);
987
988 // Confirm username is not taken. FB_USER_OP_PRE_USER may have changed it.
989 if ($uid = db_query("SELECT uid FROM {users} WHERE name = :name", array(':name' => $edit['name']))->fetchField(0)) {
990 // The desired name is taken.
991 watchdog('fb_user', 'Failed to create new user %name. That name is already in the users table.',
992 array('%name' => $edit['name']),
993 WATCHDOG_ERROR, l(t('view user'), 'user/' . $uid));
994 }
995 else {
996 $account = user_save('', $edit);
997
998 _fb_user_set_map($account, $fbu);
999
1000 watchdog('fb_user', 'New user: %name %email.',
1001 array('%name' => $account->name, '%email' => '<' . $account->mail . '>'),
1002 WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit'));
1003
1004 // Allow third-party modules to act after account creation.
1005 fb_invoke(FB_USER_OP_POST_USER, array(
1006 'account' => $account,
1007 'fb_app' => $fb_app,
1008 'fb' => $fb,
1009 ), NULL, 'fb_user');
1010 }
1011 }
1012 }
1013 return $account;
1014 }
1015
1016 /**
1017 * Given an app and facebook user id, return the corresponding local user.
1018 *
1019 * @param $fbu
1020 * User's id on facebook.com
1021 *
1022 * @param $fb_app
1023 * Historically, this method took the app details into account when mapping user ids. Presently, this parameter is not used.
1024 */
1025 function fb_user_get_local_user($fbu, $fb_app = NULL) {
1026 if ($uid = _fb_user_get_uid($fbu, $fb_app)) {
1027 return user_load($uid);
1028 }
1029 }
1030
1031 // TODO $fb_app is holdover and can be removed in the future
1032 function _fb_user_get_uid($fbu, $fb_app = NULL) {
1033 $result = db_query("SELECT uid FROM {fb_user} WHERE fbu = :fbu", array(
1034 ':fbu' => $fbu,
1035 ))->fetchObject();
1036
1037 if (is_object($result)) {
1038 return $result->uid;
1039 }
1040 else {
1041 return 0;
1042 }
1043 }
1044
1045 /**
1046 * Try to determine the local user account by the email address.
1047 */
1048 function fb_user_get_local_user_by_email($fbu) {
1049 global $_fb;
1050 if (isset($_fb) && $fbu) {
1051 try {
1052 $info = $_fb->api($fbu);
1053 if (isset($info['email']) &&
1054 ($email = $info['email'])) {
1055 return user_load_by_mail($email);
1056 }
1057 }
1058 catch (Exception $e) {
1059 // This can occur when user logs out of facebook in another window, then returns to our site.
1060 if (fb_verbose()) {
1061 fb_log_exception($e, t('Failed to get facebook user email.'));
1062 }
1063 }
1064 }
1065 }
1066
1067 /**
1068 * Returns local uids of friends of a given user.
1069 *
1070 * Query is relatively efficient for the current user of a canvas page. For
1071 * all other users, and non-canvas pages it requires expensive call to
1072 * facebook. That said, our local database query may be inefficient for users
1073 * with large numbers of friends, so use with caution.
1074 *
1075 * TODO: should this function cache results?
1076 *
1077 * Note: the api takes fbu as a parameter, but this usually causes problems
1078 * because facebook restricts users to query only about their own friends.
1079 * For the time being, expect this function to work only on canvas pages to
1080 * find friends of the current user.
1081 *
1082 * Only works if the "map accounts" feature is enabled.
1083 */
1084 function fb_user_get_local_friends($fbu = NULL, $fb_app = NULL) {
1085 if (!isset($fbu)) {
1086 $fbu = fb_facebook_user();
1087 }
1088 $uids = array();
1089 if ($fbus = fb_get_friends($fbu, $fb_app)) {
1090 $result = db_select('fb_user', 'fb')
1091 ->fields('fb', array('uid'))
1092 ->condition('fb.fbu', $fbus, 'IN')
1093 ->execute();
1094
1095 foreach ($result as $data) {
1096 if ($data->uid) {
1097 $uids[] = $data->uid;
1098 }
1099 }
1100 }
1101 return $uids;
1102 }
1103
1104
1105 /**
1106 * Given a local user id, find the facebook id. This is for internal use.
1107 * Outside modules use fb_get_fbu().
1108 *
1109 * Only works if the "map accounts" feature is enabled, or the account was created by this module.
1110 */
1111 function _fb_user_get_fbu($uid) {
1112 $cache = &drupal_static(__FUNCTION__); // cache to avoid excess queries.
1113
1114 if (!isset($cache[$uid])) {
1115 // Look up this user in the authmap
1116 $result = db_query("SELECT fbu FROM {fb_user} WHERE uid = :uid", array(
1117 ':uid' => $uid,
1118 ))->fetchObject();
1119 if ($result) {
1120 $cache[$uid] = $result->fbu;
1121 }
1122 else {
1123 $cache[$uid] = NULL;
1124 }
1125 }
1126
1127 return $cache[$uid];
1128 }
1129
1130 //// token module hooks.
1131
1132 function fb_user_token_list($type = 'all') {
1133 if ($type == 'all' || $type == 'fb' || $type == 'fb_app') {
1134 $tokens['fb_app']['fb-app-user-fbu'] = t('Current user\'s Facebook ID');
1135 $tokens['fb_app']['fb-app-user-name'] = t('Current user\'s name on Facebook (TODO)');
1136 $tokens['fb_app']['fb-app-user-name-fbml'] = t('Current user\'s name for display on Facebook profile and canvas pages.');
1137 $tokens['fb_app']['fb-app-profile-url'] = t('Current user\'s Facebook profile URL');
1138 return $tokens;
1139 }
1140 }
1141
1142 function fb_user_token_values($type = 'all', $object = NULL) {
1143 $values = array();
1144 if ($type == 'fb_app' && $object) {
1145 $fb_app = $object;
1146 global $user;
1147 $fbu = _fb_user_get_fbu($user->uid);
1148 if ($fbu) {
1149 $values['fb-app-user-fbu'] = $fbu;
1150 //$values['fb-app-user-name'] = ''; // @TODO
1151 $values['fb-app-user-name-fbml'] = '<fb:name uid="' . $fbu . '" />';
1152 $values['fb-app-profile-url'] =
1153 'http://www.facebook.com/profile.php?id=' . $fbu;
1154 }
1155 }
1156 return $values;
1157 }
1158
1159 /**
1160 * Learn the user's proxied email address. If fb_user_app.module is enabled,
1161 * it will defer to that module, which queries a local database. If not, ask
1162 * facebook for the data.
1163 *
1164 * @TODO: Facebook may no longer provide proxied_email. Does this work?
1165 */
1166 function fb_user_get_proxied_email($fbu, $fb_app) {
1167 $mail = "";
1168
1169 if (function_exists("fb_user_app_get_proxied_email")) {
1170 // Function at fb_user_app module queries fb_use_app table first
1171 $mail = fb_user_app_get_proxied_email($fbu, $fb_app);
1172 }
1173
1174 if (!$mail) {
1175 // Ask facebook for info.
1176 $fb = fb_api_init($fb_app);
1177 $info = fb_users_getInfo(array($fbu), $fb); // TODO deprecated
1178 $data = $info[0];
1179 if (isset($data['email'])) {
1180 $mail = $data['email'];
1181 }
1182 elseif (isset($data['proxied_email'])) {
1183 $mail = $data['proxied_email'];
1184 }
1185 }
1186
1187 return $mail;
1188 }