You are here

fb_user.module in Drupal for Facebook 7.3

This module manages relations between local Drupal user accounts and their accounts on facebook.com.

This module can create a new local user account, when a facebook user authorizes an application hosted on this server.

Links existing local accounts to remote accounts on facebook via fb_user table.

Drupal refers to a local user id as 'uid'. Facebook's documentation and code also uses 'uid'. In these modules we use 'fbu' for facebook's id and 'uid' for Drupal's id.

File

fb_user.module
View source
<?php

/**
 * @file
 * This module manages relations between local Drupal user accounts
 * and their accounts on facebook.com.
 *
 * This module can create a new local user account, when a facebook
 * user authorizes an application hosted on this server.
 *
 * Links existing local accounts to remote accounts on facebook via
 * fb_user table.
 *
 * Drupal refers to a local user id as 'uid'. Facebook's documentation
 * and code also uses 'uid'. In these modules we use 'fbu' for facebook's
 * id and 'uid' for Drupal's id.
 */
define('FB_USER_OPTION_CREATE_NEVER', 1);
define('FB_USER_OPTION_CREATE_LOGIN', 2);
define('FB_USER_OPTION_MAP_NEVER', 1);
define('FB_USER_OPTION_MAP_ALWAYS', 2);

// Map when user is registered and authorized.
define('FB_USER_OPTION_MAP_EMAIL', 3);

// Map when email is exact match.
define('FB_USER_VAR_USERNAME_STYLE', 'fb_user_username_style');

// Key used in variables table for this option.
define('FB_USER_OPTION_USERNAME_FULL', 1);

// Get full name from FB
define('FB_USER_OPTION_USERNAME_FBU', 2);

// Use unique name
define('FB_USER_VAR_ALTER_REGISTER', 'fb_user_alter_register');
define('FB_USER_VAR_ALTER_LOGIN', 'fb_user_alter_login');
define('FB_USER_VAR_ALTER_LOGIN_BLOCK', 'fb_user_alter_login_block');
define('FB_USER_VAR_ALTER_CONTACT', 'fb_user_alter_contact');
define('FB_USER_VAR_TEXT_REGISTER', 'fb_button_text_register');
define('FB_USER_VAR_TEXT_LOGIN', 'fb_button_text_login');
define('FB_USER_VAR_TEXT_LOGIN_BLOCK', 'fb_button_text_login_block');
define('FB_USER_VAR_CHECK_SESSION', 'fb_user_check_session');

// Controls - see fb_controls().
define('FB_USER_CONTROL_NO_CREATE_ACCOUNT', 'fb_user_no_account');
define('FB_USER_CONTROL_NO_CREATE_MAP', 'fb_user_no_map_create');
define('FB_USER_CONTROL_NO_HONOR_MAP', 'fb_user_no_map');
define('FB_USER_CONTROL_NO_REDIRECT', 'fb_user_no_redirect');

// hook_fb_user().
define('FB_USER_OP_PRE_USER', 'pre_user');

// Before account creation, fb_user.module
define('FB_USER_OP_POST_USER', 'post_user');

// After account creation, fb_user.module
define('FB_USER_OP_POST_EXTERNAL_LOGIN', 'post_external_login');

// user map has changed global user.
define('FB_USER_OP_POST_USER_CONNECT', 'post_user_connect');

// Connected local account to FB account, fb_user.module
define('FB_USER_OP_POST_USER_DISCONNECT', 'post_user_disconnect');

// Disconnected local account from FB account, fb_user.module

/**
 * Implements hook_permission().
 */
function fb_user_permission() {
  return array(
    'delete own fb_user authmap' => array(
      'title' => t('Delete own fb_user authmap'),
      'description' => t('User can remove their connection to Facebook.'),
    ),
  );
}

/**
 * Implements hook_menu().
 */
function fb_user_menu() {
  $items = array();

  // Admin pages
  $items[FB_PATH_ADMIN . '/fb_user'] = array(
    'title' => 'User Settings',
    'description' => 'Local account to facebook account mapping',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'fb_user_admin_settings',
    ),
    'access arguments' => array(
      FB_PERM_ADMINISTER,
    ),
    'file' => 'fb_user.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );

  // Callback to visit a user page when their fbu is known but not their uid.  (i.e. from javascript)
  $items['fb_user/%fb_graph'] = array(
    'title' => 'My account',
    'title callback' => 'fb_user_page_title',
    'title arguments' => array(
      1,
    ),
    'page callback' => 'fb_user_view_page',
    'page arguments' => array(
      1,
    ),
    'access callback' => TRUE,
    'type' => MENU_CALLBACK,
  );
  return $items;
}

/**
 * Menu callback to set title.
 */
function fb_user_page_title($fb_user) {
  return $fb_user['name'];
}

/**
 * Menu page callback.
 * Send user to local profile page of a facebook user.
 */
function fb_user_view_page($fb_user) {
  if ($uid = fb_get_uid($fb_user['id'])) {

    // Send user to Drupal profile page. The primary purpose of this callback.
    drupal_goto("user/{$uid}");
  }

  // A very simple page.
  $output['picture'] = array(
    '#type' => 'markup',
    '#markup' => '<img src="//graph.facebook.com/' . $fb_user['id'] . '/picture" alt="' . $fb_user['name'] . '" />',
  );
  return $output;
}

/**
 * Returns configuration for this module, on a per-app basis.
 */
function _fb_user_get_config($fb_app) {
  $fb_app_data = fb_get_app_data($fb_app);
  $fb_user_data = isset($fb_app_data['fb_user']) ? $fb_app_data['fb_user'] : array();

  // Merge in defaults
  $fb_user_data += array(
    'create_account' => FB_USER_OPTION_CREATE_NEVER,
    'map_account' => array(
      FB_USER_OPTION_MAP_ALWAYS => FB_USER_OPTION_MAP_ALWAYS,
      FB_USER_OPTION_MAP_EMAIL => FB_USER_OPTION_MAP_EMAIL,
    ),
    'new_user_rid' => NULL,
    'connected_user_rid' => NULL,
  );
  return $fb_user_data;
}

/**
 * There are several pages where we don't want to automatically create a new
 * account or use an account configured for this app.
 */
function _fb_user_special_page() {

  // fb_app/event is called by facebook. Don't create accounts on that page.
  return arg(0) == 'fb_app' && arg(1) == 'event';
}

/**
 * Implements hook_fb.
 */
function fb_user_fb($op, $data, &$return) {
  $fb_app = isset($data['fb_app']) ? $data['fb_app'] : NULL;
  $fb = isset($data['fb']) ? $data['fb'] : NULL;
  global $user;
  if ($fb_app) {
    $fb_user_data = _fb_user_get_config($fb_app);
  }
  if ($op == FB_OP_APP_IS_AUTHORIZED) {

    // There is an app, and the current user has authorized (is connected).
    if ($rid = $fb_user_data['connected_user_rid']) {

      // User is connected to facebook.
      if (!isset($user->roles[$rid])) {
        $user->roles[$rid] = $rid;

        // Should be role name, but that requires db query.
        // Reload user permissions.
        drupal_static_reset('user_access');
        drupal_static_reset('menu_get_item');
      }
    }
  }
  if ($op == FB_OP_POST_INIT && $fb) {

    // Drupal gives access denied on /user/login page, so we want to avoid redirecting there.
    if (arg(0) == 'user' && !isset($_REQUEST['destination'])) {
      fb_js_settings('reload_url', url('user', array(
        'absolute' => TRUE,
        'fb_canvas' => fb_is_canvas(),
      )));
    }
    $fbu = fb_facebook_user();
    if (isset($_SESSION['fb_user_fbu']) && $_SESSION['fb_user_fbu'] != $fbu && !(fb_settings(FB_SETTINGS_CB_SESSION) && !$fbu)) {

      // User has logged out of facebook, and drupal is only now learning
      // about it. Check disabled when using FB_SETTINGS_CB_SESSION, because
      // we aren't always passed a signed_request in that case, which would
      // otherwise trigger this.
      _fb_logout();
      if (!fb_controls(FB_USER_CONTROL_NO_REDIRECT)) {
        drupal_goto(current_path());

        // @TODO - need request params here?
      }
    }
    if (_fb_user_special_page() || variable_get('maintenance_mode', FALSE) && !user_access('administer site configuration')) {

      // Prevent some behavior.
      fb_controls(FB_USER_CONTROL_NO_HONOR_MAP, TRUE);
      fb_controls(FB_USER_CONTROL_NO_CREATE_MAP, TRUE);
      fb_controls(FB_USER_CONTROL_NO_CREATE_ACCOUNT, TRUE);
    }
    if (isset($_REQUEST['_fb_user_fbu']) && $_REQUEST['_fb_user_fbu'] == $fbu) {

      // We've triggered a reload. Don't redirect again, as that will
      // cause infinite loop if browser not accepting third-party cookies.
      fb_controls(FB_USER_CONTROL_NO_REDIRECT, TRUE);
    }
    if ($rid = $fb_user_data['connected_user_rid']) {
      if (!$fbu) {

        // User is not connected to facebook.
        if ($rid != DRUPAL_AUTHENTICATED_RID && isset($user->roles[$rid])) {

          // Out of paranoia, unset role.  This will be reached only if the

          //user was somehow saved while connected to facebook.
          unset($user->roles[$rid]);

          // Reload user permissions.
          drupal_static_reset('user_access');
          drupal_static_reset('menu_get_item');
        }
      }
    }

    // During ajax, we need to check for a change in user.
  }
  elseif ($op == FB_OP_GET_FBU) {

    // This is a request to learn the user's FB id.
    $return = _fb_user_get_fbu($data['uid']);
  }
  elseif ($op == FB_OP_GET_UID) {

    // This is a request to learn the facebook user's local id.
    $return = _fb_user_get_uid($data['fbu'], $data['fb_app']);
  }
  elseif ($op == FB_OP_AJAX_EVENT) {

    // fb.js has notified us of an event via AJAX. Not the same as facebook event callback above.
    extract($data);

    // $event_type, $event_data.
    if ($event_type == 'session_change' && isset($event_data['fbu'])) {

      // A user has logged in.
      // Don't trust fbu from $data['event_data'], too easy to spoof.
      // Instead call fb_facebook_user().  It will only work if the facebook php sdk is properly initialized.  Since this is an ajax event, probably the signed_request was passed to us.
      if (($fbu = fb_facebook_user($data['fb'])) && $fbu != fb_get_fbu($GLOBALS['user'])) {

        // In ajax callback, there's no reason to redirect even if user
        // changes. But we should honor session, as even ajax can set a new
        // cookie.
        fb_controls(FB_USER_CONTROL_NO_REDIRECT, TRUE);
        _fb_user_process_authorized_user();
      }
    }

    // fb.js no longer reloads after all session changes.  We need to explicitly reload when we know the user changed.
    if ($event_type == 'session_change') {
      if (isset($event_data['fbu']) && $event_data['fbu']) {
        $uid = _fb_user_get_uid($event_data['fbu'], $data['fb_app']);
      }
      else {
        $uid = 0;
        _fb_logout();
      }
      if ($uid || $event_data['is_anonymous'] != 'true') {

        // The Drupal user has changed, we should reload after ajax returns to fb.js.
        $return['fb_user'] = 'FB_JS.reload()';
      }
    }
  }
}

/**
 * Implements hook_page_alter().
 *
 * Reload page if user has changed.  This would not make sense during an ajax
 * callback (or anything else) where a redirect would not refresh the browsers
 * page.  That's why we do it here in page_alter().
 */
function fb_user_page_alter(&$page) {
  _fb_user_check_and_goto();
}

/**
 * Detect whether facebook indicates the user has changed.  If so, redirect.
 */
function _fb_user_check_and_goto() {
  if (($fbu = fb_facebook_user()) && !fb_is_tab() && $fbu != fb_get_fbu($GLOBALS['user'])) {
    $uid = $GLOBALS['user']->uid;

    // Remember original uid.
    _fb_user_process_authorized_user();
    if ($uid != $GLOBALS['user']->uid) {

      // We've detected a change of user and started a new session.  Refresh page.
      if (!fb_controls(FB_USER_CONTROL_NO_REDIRECT)) {
        if (empty($REQUEST['_fb_user_fbu']) || $REQUEST['_fb_user_fbu'] != $fbu) {
          drupal_goto(request_path(), array(
            // Parameter avoids refresh loop when third-party cookies disabled on Safari.
            'query' => array(
              '_fb_user_fbu' => $fbu,
            ),
          ));
        }
      }
    }
  }
}

/**
 * Test facebook session by calling into facebook. This is expensive, so
 * limit check to once per session. Use session variable to flag that we have
 * completed the test.
 */
function _fb_user_check_session($fbu) {

  // Make sure facebook session is valid and fb_user table is correct.
  // Relatively expensive operations, so we perform them only once per session.
  if (!isset($_SESSION['fb_user_fbu']) || $_SESSION['fb_user_fbu'] != $fbu) {
    if ($valid_session = fb_api_check_session($GLOBALS['_fb'])) {

      // Expensive check.
      $_SESSION['fb_user_fbu'] = $fbu;
    }
    else {
      unset($_SESSION['fb_user_fbu']);
    }
  }
  return isset($_SESSION['fb_user_fbu']) && $_SESSION['fb_user_fbu'] == $fbu;
}

/**
 * If facebook user has authorized app, and account map exists, login as the local user.
 *
 * @return - TRUE, if user_external_login succeeds.
 */
function _fb_user_external_login($account = NULL) {
  $fbu = fb_facebook_user();
  if (!$account) {
    $account = fb_user_get_local_user($fbu, $GLOBALS['_fb_app']);
  }
  if ($account && $account->uid == $GLOBALS['user']->uid) {

    // Already logged in.
    return $account;
  }
  elseif ($fbu && $account && $account->uid != $GLOBALS['user']->uid && !fb_controls(FB_USER_CONTROL_NO_HONOR_MAP)) {

    // Map exists. Log in as local user.
    $session_id = session_id();
    if (fb_verbose() === 'extreme') {

      // debug
      watchdog("fb_user", "fb_user_fb changing user to {$account->uid}");
    }

    // user_external_login() fails if already logged in, so log out first.
    if ($GLOBALS['user']->uid) {
      _fb_logout();
    }

    // user_external_login() removed in D7, no replacement.  Let's hope the following works.
    $GLOBALS['user'] = $account;
    $account_array = (array) $account;
    user_login_finalize($account_array);

    // Special effort to support browsers without third-party cookies.
    if (function_exists('fb_sess_regenerate_hack')) {
      fb_sess_regenerate_hack();
    }
    if (fb_verbose() === 'extreme') {

      // debug
      watchdog("fb_user", "fb_user_fb changed session from {$session_id} to " . session_id());
    }

    // Session changed after external login. Invoking hook here allows modules to drupal_set_message().
    fb_invoke(FB_USER_OP_POST_EXTERNAL_LOGIN, array(
      'account' => $account,
    ), NULL, 'fb_user');
    return $account;
  }
  return FALSE;
}

/**
 * Create a map linking the facebook account to the currently logged in local user account.
 *
 * @return - TRUE, if map created.
 */
function _fb_user_create_map() {
  if ($GLOBALS['user']->uid) {
    $fbu = fb_facebook_user();
    $account = fb_user_get_local_user($fbu);
    if ($fbu && !$account && !fb_controls(FB_USER_CONTROL_NO_CREATE_MAP)) {
      _fb_user_set_map($GLOBALS['user'], $fbu);
      fb_invoke(FB_USER_OP_POST_USER_CONNECT, array(
        'account' => $GLOBALS['user'],
        'fbu' => $fbu,
      ), NULL, 'fb_user');
      return TRUE;
    }
  }
  return FALSE;
}
function _fb_user_create_map_by_email() {
  $fbu = fb_facebook_user();
  $account = fb_user_get_local_user($fbu, $GLOBALS['_fb_app']);
  if ($fbu && !$account && ($email_account = fb_user_get_local_user_by_email($fbu)) && !fb_controls(FB_USER_CONTROL_NO_CREATE_MAP)) {
    _fb_user_set_map($email_account, $fbu);
    fb_invoke(FB_USER_OP_POST_USER_CONNECT, array(
      'account' => $GLOBALS['user'],
      'fbu' => $fbu,
    ), NULL, 'fb_user');
    return TRUE;
  }
  return FALSE;
}

/**
 * Helper function to create local account for the currently authorized user.
 */
function _fb_user_create_local_account() {
  $fbu = fb_facebook_user();
  $account = fb_user_get_local_user($fbu);
  if ($fbu && !$account && !fb_controls(FB_USER_CONTROL_NO_CREATE_ACCOUNT)) {
    $config = _fb_user_get_config($GLOBALS['_fb_app']);

    // Establish user name.
    // Case 1: use name from FB
    // Case 2: create a unique user name ourselves
    // Which we use is determined by the setting at
    // admin/structure/fb/fb_user
    if (variable_get(FB_USER_VAR_USERNAME_STYLE, FB_USER_OPTION_USERNAME_FBU) == FB_USER_OPTION_USERNAME_FULL) {
      try {

        // Use fb->api() rather than fb_users_getInfo(). Later fails to learn name on test accounts.
        $info = $GLOBALS['_fb']
          ->api($fbu);
        $username = $info['name'];
      } catch (Exception $e) {
        fb_log_exception($e, t('Failed to learn full name of new user'), $GLOBALS['_fb']);
      }
    }
    else {

      // Create a name that is likely to be unique.
      $username = "{$fbu}@facebook";
    }
    if ($config['new_user_rid']) {
      $roles = array(
        $config['new_user_rid'] => TRUE,
      );
    }
    else {
      $roles = array();
    }
    $account = fb_user_create_local_user($GLOBALS['_fb'], $GLOBALS['_fb_app'], $fbu, array(
      'name' => $username,
      'roles' => $roles,
    ));
    watchdog('fb_user', t("Created new user !username for application %app", array(
      '!username' => l($account->name, 'user/' . $account->uid),
      '%app' => $GLOBALS['_fb_app']->label,
    )));
    return $account;
  }
  return FALSE;
}

/**
 * Create local account or account map for a facebook user who has authorized the application.
 */
function _fb_user_process_authorized_user() {
  $fbu = fb_facebook_user();
  $mapped = FALSE;
  if ($fbu && (!variable_get(FB_USER_VAR_CHECK_SESSION, FALSE) || _fb_user_check_session($fbu))) {
    $fb_app = $GLOBALS['_fb_app'];

    // First check if map already exists.
    $account = fb_user_get_local_user($fbu, $fb_app);
    $config = _fb_user_get_config($fb_app);
    if (!$account) {
      if ($GLOBALS['user']->uid > 0 && $config['map_account'][FB_USER_OPTION_MAP_ALWAYS]) {

        // Create map for logged in user.
        $mapped = _fb_user_create_map();
      }
      if (!$mapped && $config['map_account'][FB_USER_OPTION_MAP_EMAIL]) {

        // Create map if email matches.
        $mapped = _fb_user_create_map_by_email();
      }
      if (!$mapped && $config['create_account'] == FB_USER_OPTION_CREATE_LOGIN) {

        // Create new local account with map.
        $mapped = _fb_user_create_local_account();
      }
      if ($mapped) {
        $account = fb_user_get_local_user($fbu, $fb_app);
      }
    }
    if ($account) {

      // Ensure the user has any roles associated with this app.
      $rid = $config['new_user_rid'];
      if ($account && $rid && (!isset($account->roles[$rid]) || !$account->roles[$rid])) {

        // there should be an API for this...
        $query = db_insert('users_roles')
          ->fields(array(
          'uid' => $account->uid,
          'rid' => $rid,
        ))
          ->execute();
        watchdog('fb_user', "Added role %role to existing user !username for application %app", array(
          '!username' => theme('username', $account),
          '%app' => $fb_app->label,
          '%role' => $rid,
        ));
      }

      // Login as facebook user, if not already.
      _fb_user_external_login($account);
    }
  }
}

// TODO make a public function that caches this data.
function _fb_user_facebook_data($fb) {
  if ($fbu = fb_facebook_user($fb)) {
    try {
      $data = fb_api($fbu);
      return $data;
    } catch (FacebookApiException $e) {
      fb_log_exception($e, t('Failed lookup of %fbu.', array(
        '%fbu' => $fbu,
      )));
    }
  }
}

/**
 * Helper function to retrieve button text.
 */
function _fb_user_button_text($form_id) {
  $button_text =& drupal_static(__FUNCTION__);
  if (!isset($button_text)) {
    $button_text = array(
      'user_register_form' => variable_get(FB_USER_VAR_TEXT_REGISTER, NULL),
      'user_login' => variable_get(FB_USER_VAR_TEXT_LOGIN, NULL),
      'user_login_block' => variable_get(FB_USER_VAR_TEXT_LOGIN_BLOCK, NULL),
    );
  }
  return isset($button_text[$form_id]) ? $button_text[$form_id] : '';
}

/**
 * Implements hook_form_alter().
 */
function fb_user_form_alter(&$form, &$form_state, $form_id) {
  if (isset($form['fb_app_data'])) {

    // Add our settings to the fb_app edit form.
    module_load_include('inc', 'fb_user', 'fb_user.admin');
    fb_user_admin_form_alter($form, $form_state, $form_id);
  }
  elseif ($form_id == 'user_edit' && ($app = $form['#fb_app'])) {

    // Disable buttons on user/edit/app pages, nothing to submit
    unset($form['submit']);
    unset($form['delete']);
  }

  // Add name and email to some forms.
  if (isset($GLOBALS['_fb'])) {
    $fb = $GLOBALS['_fb'];
    if (!$GLOBALS['user']->uid && ($form_id == 'user_register_form' && variable_get(FB_USER_VAR_ALTER_REGISTER, TRUE) || $form_id == 'user_login' && variable_get(FB_USER_VAR_ALTER_LOGIN, TRUE) || $form_id == 'user_login_block' && variable_get(FB_USER_VAR_ALTER_LOGIN_BLOCK, TRUE))) {

      // Add a facebook connect button or profile pic to the form.
      $fb_button = theme('fb_login_button', array(
        'text' => t(_fb_user_button_text($form_id)),
      ), array(
        'form_id' => $form_id,
      ));
      $form['fb_user'] = array(
        '#fb_user' => NULL,
        'connected' => array(
          '#prefix' => '<div class="fb_connected">',
          '#suffix' => '</div>',
          'picture' => array(
            '#markup' => '<fb:profile-pic uid="loggedinuser" linked="false" facebook-logo="true"></fb:profile-pic>',
            '#type' => 'markup',
            '#prefix' => '<div class="fb_user_picture">',
            '#suffix' => '</div>',
          ),
          'text' => array(
            '#type' => 'markup',
            '#markup' => t('!name, after <a href="!login_url">login</a> or <a href="!register_url">registration</a> your account will be connected.', array(
              '!name' => '<fb:name uid="loggedinuser" useyou="false" linked="false"></fb:name>',
              '!login_url' => url('user/login'),
              '!register_url' => url('user/register'),
            )),
          ),
        ),
        'loginbutton' => array(
          '#markup' => $fb_button,
          '#type' => 'markup',
          '#prefix' => '<div class="fb_user-login-button-wrapper fb_not_connected">',
          '#suffix' => '</div>',
        ),
        '#weight' => -1,
      );

      // @TODO confirm fb session before sharing sensitive data!
      if ($fbu = fb_facebook_user($fb)) {

        // Include $fb_user in form, so other modules can custom form_alter.
        $data = _fb_user_facebook_data($fb);
        $form['fb_user']['#fb_user'] = $data;
        if ($form_id == 'user_register_form') {
          if ($data) {

            // Provide defaults for name and email.
            if (isset($form['name']) && !$form['name']['#default_value']) {

              // @TODO - ensure name is unique to Drupal.
              $form['name']['#default_value'] = $data['name'];
            }
            elseif (isset($form['account']) && isset($form['account']['name']) && !$form['account']['name']['#default_value']) {

              // @TODO - ensure name is unique to Drupal.
              $form['account']['name']['#default_value'] = $data['name'];
            }
            if (isset($form['mail']) && !$form['mail']['#default_value']) {
              $form['mail']['#default_value'] = $data['email'];
            }
            elseif (isset($form['account']['mail']) && isset($form['account']['mail']) && !$form['account']['mail']['#default_value']) {
              $form['account']['mail']['#default_value'] = $data['email'];
            }
          }
        }
      }
    }
    elseif ($form_id == 'contact_site_form' && variable_get(FB_USER_VAR_ALTER_CONTACT, TRUE)) {
      if ($data = _fb_user_facebook_data($fb)) {
        if (!$form['name']['#default_value'] || strpos($form['name']['#default_value'], '@facebook')) {
          $form['name']['#default_value'] = $data['name'];
        }
        if (!$form['mail']['#default_value']) {
          $form['mail']['#default_value'] = $data['email'];
        }
      }
    }
  }
}

/**
 * Helper function for menu item access check.
 */
function fb_user_access_own($account, $perm, $allow_admin) {
  if ($GLOBALS['user']->uid == $account->uid && user_access($perm)) {
    return TRUE;
  }
  elseif ($allow_admin) {
    return user_access('administer users');
  }
}

/**
 * Implements hook_user_load.
 *
 * Use no standard email, use proxy email if available
 */
function fb_user_user_load($users) {
  global $_fb_app;
  foreach ($users as $account) {
    if ($account->uid && $_fb_app) {
      if (!$account->mail && ($fbu = _fb_user_get_fbu($account->uid))) {

        // Use proxied email, if facebook app is active and user uses it.
        // TODO: confirm drupal never saves proxied address to users.mail.
        $account->mail = fb_user_get_proxied_email($fbu, $_fb_app);
        $account->fb_user_proxied_mail = $account->mail;

        // Remember where we got address.
      }
    }
  }
}

/**
 * Implements hook_user_login.
 *
 * Map local Drupal user to FB user under certain circumstances.
 */
function fb_user_user_login(&$edit, $account) {
  global $user, $_fb_app;

  // A facebook user has logged in. We can map the two accounts together.
  $fb_user_data = _fb_user_get_config($_fb_app);
  if (($fbu = fb_facebook_user()) && $fb_user_data['map_account'][FB_USER_OPTION_MAP_ALWAYS] && !fb_controls(FB_USER_CONTROL_NO_CREATE_MAP)) {

    // Create fb_user record if it doesn't exist or update existing one
    _fb_user_set_map($account, $fbu);

    // @TODO - if the app has a role, make sure the user gets that role. (presently,
    // that will not happen until their next request)
  }
}

/**
 * Implements hook_user_insert.
 *
 * When user is created create record
 * in fb_user to map local Drupal user to FB user.
 */
function fb_user_user_insert(&$edit, $account, $category) {
  global $user, $_fb_app;

  // Map the two accounts together.
  $fb_user_data = _fb_user_get_config($_fb_app);
  if (($fbu = fb_facebook_user()) && $fb_user_data['map_account'][FB_USER_OPTION_MAP_ALWAYS] && !fb_controls(FB_USER_CONTROL_NO_CREATE_MAP)) {

    // Create fb_user record if it doesn't exist or update existing one.
    if ($account->uid == $user->uid) {
      _fb_user_set_map($account, $fbu);
    }

    // @TODO - if the app has a role, make sure the user gets that role. (presently, that will not happen until their next request)
  }
}

/**
 * Implements hook_user_view.
 *
 * Show extra info when user being viewed.
 */
function fb_user_user_view($account, $view_mode, $langcode) {
}

/**
 * Implements hook_user_update().
 *
 * User is about to be updated.
 */
function fb_user_user_update(&$edit, $account, $category) {
  if (isset($edit['fb_user_map'])) {

    // Only set on account form submit.
    if ($edit['fb_user_map']) {
      _fb_user_set_map($account, $edit['fb_user_map']);
    }
    else {

      // Delete account mapping, because administrator has unchecked the connect option.
      $num_deleted = db_delete('fb_user')
        ->condition('uid', $account->uid)
        ->execute();
      fb_invoke(FB_USER_OP_POST_USER_DISCONNECT, array(
        'account' => $account,
      ), NULL, 'fb_user');
    }
  }
}

/**
 * Implements hook_user_delete.
 *
 * User is about to be deleted.
 */
function fb_user_user_delete($account) {
  $num_deleted = db_delete('fb_user')
    ->condition('uid', $account->uid)
    ->execute();
}

/**
 * Implements hook_user_logout.
 *
 * User has logged out.
 */
function fb_user_user_logoutXXX($account) {

  // Disabled.  Still needed?  Seems to cause problems in some cases.
  global $user, $_fb_app;
  if (fb_facebook_user() && fb_api_check_session($GLOBALS['_fb'])) {

    // Log out of facebook, as well as Drupal. Note that code in
    // fb_connect.js and fb_canvas.js attempts to call FB.logout. However,
    // that code is not reached if the user types "/logout" directly into
    // the browser URL. Also, a sometimes-occuring bug in firefox prevents
    // FB.logout from always succeeding.
    // Figure out where to send the user.
    if (isset($_REQUEST['destination'])) {
      $next_url = url($_REQUEST['destination'], array(
        'absolute' => TRUE,
        'fb_canvas' => fb_is_canvas(),
      ));

      // Unset desination so drupal_goto() below does what we need it to do.
      unset($_REQUEST['destination']);
    }
    else {
      $next_url = url('<front>', array(
        'absolute' => TRUE,
        'fb_canvas' => fb_is_canvas(),
      ));
    }
    $logout_url = $GLOBALS['_fb']
      ->getLogoutUrl(array(
      'next' => $next_url,
      'cancel_url' => $next_url,
    ));

    // Drupal 7 calls us before destroying the session, so destroy it here
    // manually, as we're about to redirect.
    session_destroy();
    drupal_goto($logout_url);
  }
}

/**
 * Implements hook_form_user_profile_form_alter.
 */
function fb_user_form_user_profile_form_alter(&$form, &$form_state, $form_id) {
  global $user, $_fb_app;
  if (empty($_fb_app)) {

    // No application active.
    // @todo: should this alter loop through all known apps?
    return;
  }
  if (!user_access('administer users') && !(user_access('delete own fb_user authmap') && $user->uid == $form['#user']->uid)) {
    return;
  }

  // hide from this user
  if (empty($form['#user_category']) || $form['#user_category'] != 'account') {
    return;

    // Alter only the primary user account form.
  }
  $fb_user_data = _fb_user_get_config($_fb_app);
  $account = $form['#user'];
  $fbu = _fb_user_get_fbu($account->uid);
  if ($fbu) {

    // The drupal user is a facebook user.
    $form['fb_user'] = array(
      '#type' => 'fieldset',
      '#title' => t('Facebook Application ' . $_fb_app->title),
      '#collapsed' => false,
      '#collapsible' => false,
    );
    $form['fb_user']['fb_user_map'] = array(
      '#type' => 'checkbox',
      '#title' => t('Connect to facebook.com'),
      '#default_value' => $fbu,
      '#return_value' => $fbu,
      '#description' => '',
    );

    // Now, learn more from facebook.
    try {
      $data = fb_api($fbu, array(
        'access_token' => fb_get_token(),
      ));
      if (count($data)) {
        if (empty($data['link']) && !empty($data['id'])) {
          $data['link'] = 'https://www.facebook.com/profile.php?id=' . $data['id'];
        }
        $form['fb_user']['fb_user_map']['#description'] .= t('Local account !username corresponds to !profile_page on Facebook.com.', array(
          '!username' => l($account->name, 'user/' . $account->uid),
          '!profile_page' => l($data['name'], $data['link']),
        ));
      }
    } catch (Exception $e) {
      fb_log_exception($e, t('Failed to get user data from facebook.'));
    }
    if (fb_facebook_user() == $fbu) {

      // The user is currently connected to facebook.  Depending on
      // config, they may not be able to break the connection.
      $form['fb_user']['fb_user_map']['#disabled'] = TRUE;
      $form['fb_user']['fb_user_map']['#description'] .= '<br/>' . t('(Checkbox disabled because you are currently connected to facebook.)');
    }
    else {
      $form['fb_user']['fb_user_map']['#description'] .= '<br/>' . t('Uncheck then click save to delete this connection.');
    }
  }
  if (!$fbu) {

    // this tells us that a mapping hasn't been created
    if ($user->uid == $account->uid) {

      // Could not obtain the $fbu from an existing map.
      $fbu = fb_facebook_user();
      if ($fbu) {

        // they are connected to facebook; give option to map
        $form['fb_user'] = array(
          '#type' => 'fieldset',
          '#title' => t('Facebook Application ' . $_fb_app->title),
          '#collapsed' => false,
          '#collapsible' => false,
        );
        $form['fb_user']['fb_user_map'] = array(
          '#type' => 'checkbox',
          '#title' => t('Connect account to facebook.com'),
          '#default_value' => 0,
          '#return_value' => $fbu,
          '#description' => '',
        );
        $form['fb_user']['message'] = array(
          '#markup' => t('If checked, link local account (!username) to facebook.com account (!fb_name).', array(
            '!username' => theme('username', array(
              'account' => $form['#user'],
            )),
            '!fb_name' => "<fb:name uid={$fbu} useyou=false></fb:name>",
          )),
          '#prefix' => "\n<p>",
          '#suffix' => "</p>\n",
        );
      }
      elseif (!$fbu && $_fb_app) {

        // they are not connected to facebook; give option to connect here
        $form['fb_user'] = array(
          '#type' => 'fieldset',
          '#title' => t('Facebook Application ' . $_fb_app->title),
          '#collapsed' => false,
          '#collapsible' => false,
        );
        $fb_button = theme('fb_login_button', array(
          'text' => t('Connect with Facebook'),
        ));
        $form['fb_user']['button'] = array(
          '#markup' => $fb_button,
          '#weight' => -1,
          '#prefix' => "\n<p>",
          '#suffix' => "</p>\n",
        );
      }
    }
    else {
      $form['fb_user'] = array(
        '#type' => 'fieldset',
        '#title' => t('Facebook Application ' . $_fb_app->title),
        '#collapsed' => false,
        '#collapsible' => false,
      );
      $form['fb_user']['message'] = array(
        '#markup' => t('Local account !username is not connected to facebook.com.', array(
          '!username' => theme('username', array(
            'account' => $form['#user'],
          )),
        )),
        '#prefix' => "\n<p>",
        '#suffix' => "</p>\n",
      );
    }
  }

  // On user/edit, hide proxied email
  if (isset($form['account']) && isset($form['account']['mail'])) {
    $account = $form['#user'];

    // Proxied email obsolete.  Deprecated.
    if (isset($account->fb_user_proxied_mail) && $form['account']['mail']['#default_value'] == $account->fb_user_proxied_mail) {
      unset($form['account']['mail']['#default_value']);
    }
  }
  return $form;
}

/**
 * Helper function to add or update a row in the fb_user table, which maps local uid to facebook ids.
 */
function _fb_user_set_map($account, $fbu) {
  if ($fbu && $account->uid != 0) {

    // Delete any pre-existing mapping that might exist for this local uid or fbu.
    db_query("DELETE FROM {fb_user} WHERE uid=:uid OR fbu=:fbu", array(
      ':uid' => $account->uid,
      ':fbu' => $fbu,
    ));

    // Create the new mapping.
    db_query("INSERT INTO {fb_user} (uid, fbu) VALUES (:uid, :fbu)", array(
      ':uid' => $account->uid,
      ':fbu' => $fbu,
    ));
    if (fb_verbose()) {
      watchdog('fb_user', 'Using fb_user to associate user !user with facebook user id %fbu.', array(
        '!user' => l($account->name, 'user/' . $account->uid),
        '%fbu' => $fbu,
      ));
    }
  }
}

/**
 * Creates a local Drupal account for the specified facebook user id.
 *
 * @param fbu
 * The facebook user id corresponding to this account.
 *
 * @param edit
 * An associative array with user configuration. As would be passed to user_save().
 */
function fb_user_create_local_user($fb, $fb_app, $fbu, $edit = array()) {

  // Ensure $fbu is a real facebook user id.
  if (!$fbu || !is_numeric($fbu)) {
    return;
  }
  $account = fb_user_get_local_user($fbu);
  if (!$account) {

    // Create a new user in our system
    // Learn some details from facebook.
    $infos = fb_users_getInfo(array(
      $fbu,
    ), $fb);
    $info = $infos[0];

    // All Drupal users get authenticated user role.
    $edit['roles'][DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
    if (isset($edit['name']) && $edit['name']) {
      $username = $edit['name'];
    }
    else {

      // Fallback, should never be reached.
      $username = "{$fbu}@facebook";
      $edit['name'] = $username;
    }
    $i = 0;

    // Keep looking until we find a username_n that isn't being used.
    while (db_query("SELECT 1 FROM {users} WHERE name = :name", array(
      ':name' => $edit['name'],
    ))
      ->fetchField(0)) {
      $i++;
      $edit['name'] = $username . '_' . $i;
    }

    // Give modules a way to suppress new account creation.
    $edit['fb_user_do_create'] = TRUE;

    // Allow third-party module to adjust any of our data before we create
    // the user.
    $edit = fb_invoke(FB_USER_OP_PRE_USER, array(
      'fbu' => $fbu,
      'fb' => $GLOBALS['_fb'],
      'fb_app' => $fb_app,
      'info' => $info,
    ), $edit, 'fb_user');
    if ($edit['fb_user_do_create']) {
      unset($edit['fb_user_do_create']);

      // Don't confuse user_save.
      // Fill in any default that are missing.
      $defaults = array(
        'pass' => user_password(),
        'init' => $fbu . '@facebook',
        // Supposed to be email, but we may not know it.
        'status' => 1,
      );

      // Mail available only if user has granted email extended permission.
      if (isset($info['email'])) {
        $defaults['mail'] = $info['email'];
      }

      // Merge defaults
      $edit = array_merge($defaults, $edit);

      // Confirm username is not taken. FB_USER_OP_PRE_USER may have changed it.
      if ($uid = db_query("SELECT uid FROM {users} WHERE name = :name", array(
        ':name' => $edit['name'],
      ))
        ->fetchField(0)) {

        // The desired name is taken.
        watchdog('fb_user', 'Failed to create new user %name. That name is already in the users table.', array(
          '%name' => $edit['name'],
        ), WATCHDOG_ERROR, l(t('view user'), 'user/' . $uid));
      }
      else {
        $account = user_save('', $edit);
        _fb_user_set_map($account, $fbu);
        watchdog('fb_user', 'New user: %name %email.', array(
          '%name' => $account->name,
          '%email' => '<' . $account->mail . '>',
        ), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit'));

        // Allow third-party modules to act after account creation.
        fb_invoke(FB_USER_OP_POST_USER, array(
          'account' => $account,
          'fb_app' => $fb_app,
          'fb' => $fb,
        ), NULL, 'fb_user');
      }
    }
  }
  return $account;
}

/**
 * Given an app and facebook user id, return the corresponding local user.
 *
 * @param $fbu
 * User's id on facebook.com
 *
 * @param $fb_app
 * Historically, this method took the app details into account when mapping user ids. Presently, this parameter is not used.
 */
function fb_user_get_local_user($fbu, $fb_app = NULL) {
  if ($uid = _fb_user_get_uid($fbu, $fb_app)) {
    return user_load($uid);
  }
}

// TODO $fb_app is holdover and can be removed in the future
function _fb_user_get_uid($fbu, $fb_app = NULL) {
  $result = db_query("SELECT uid FROM {fb_user} WHERE fbu = :fbu", array(
    ':fbu' => $fbu,
  ))
    ->fetchObject();
  if (is_object($result)) {
    return $result->uid;
  }
  else {
    return 0;
  }
}

/**
 * Try to determine the local user account by the email address.
 */
function fb_user_get_local_user_by_email($fbu) {
  global $_fb;
  if (isset($_fb) && $fbu) {
    try {
      $info = $_fb
        ->api($fbu);
      if (isset($info['email']) && ($email = $info['email'])) {
        return user_load_by_mail($email);
      }
    } catch (Exception $e) {

      // This can occur when user logs out of facebook in another window, then returns to our site.
      if (fb_verbose()) {
        fb_log_exception($e, t('Failed to get facebook user email.'));
      }
    }
  }
}

/**
 * Returns local uids of friends of a given user.
 *
 * Query is relatively efficient for the current user of a canvas page. For
 * all other users, and non-canvas pages it requires expensive call to
 * facebook. That said, our local database query may be inefficient for users
 * with large numbers of friends, so use with caution.
 *
 * TODO: should this function cache results?
 *
 * Note: the api takes fbu as a parameter, but this usually causes problems
 * because facebook restricts users to query only about their own friends.
 * For the time being, expect this function to work only on canvas pages to
 * find friends of the current user.
 *
 * Only works if the "map accounts" feature is enabled.
 */
function fb_user_get_local_friends($fbu = NULL, $fb_app = NULL) {
  if (!isset($fbu)) {
    $fbu = fb_facebook_user();
  }
  $uids = array();
  if ($fbus = fb_get_friends($fbu, $fb_app)) {
    $result = db_select('fb_user', 'fb')
      ->fields('fb', array(
      'uid',
    ))
      ->condition('fb.fbu', $fbus, 'IN')
      ->execute();
    foreach ($result as $data) {
      if ($data->uid) {
        $uids[] = $data->uid;
      }
    }
  }
  return $uids;
}

/**
 * Given a local user id, find the facebook id. This is for internal use.
 * Outside modules use fb_get_fbu().
 *
 * Only works if the "map accounts" feature is enabled, or the account was created by this module.
 */
function _fb_user_get_fbu($uid) {
  $cache =& drupal_static(__FUNCTION__);

  // cache to avoid excess queries.
  if (!isset($cache[$uid])) {

    // Look up this user in the authmap
    $result = db_query("SELECT fbu FROM {fb_user} WHERE uid = :uid", array(
      ':uid' => $uid,
    ))
      ->fetchObject();
    if ($result) {
      $cache[$uid] = $result->fbu;
    }
    else {
      $cache[$uid] = NULL;
    }
  }
  return $cache[$uid];
}

//// token module hooks.
function fb_user_token_list($type = 'all') {
  if ($type == 'all' || $type == 'fb' || $type == 'fb_app') {
    $tokens['fb_app']['fb-app-user-fbu'] = t('Current user\'s Facebook ID');
    $tokens['fb_app']['fb-app-user-name'] = t('Current user\'s name on Facebook (TODO)');
    $tokens['fb_app']['fb-app-user-name-fbml'] = t('Current user\'s name for display on Facebook profile and canvas pages.');
    $tokens['fb_app']['fb-app-profile-url'] = t('Current user\'s Facebook profile URL');
    return $tokens;
  }
}
function fb_user_token_values($type = 'all', $object = NULL) {
  $values = array();
  if ($type == 'fb_app' && $object) {
    $fb_app = $object;
    global $user;
    $fbu = _fb_user_get_fbu($user->uid);
    if ($fbu) {
      $values['fb-app-user-fbu'] = $fbu;

      //$values['fb-app-user-name'] = ''; // @TODO
      $values['fb-app-user-name-fbml'] = '<fb:name uid="' . $fbu . '" />';
      $values['fb-app-profile-url'] = 'http://www.facebook.com/profile.php?id=' . $fbu;
    }
  }
  return $values;
}

/**
 * Learn the user's proxied email address.  If fb_user_app.module is enabled,
 * it will defer to that module, which queries a local database.  If not, ask
 * facebook for the data.
 *
 * @TODO: Facebook may no longer provide proxied_email. Does this work?
 */
function fb_user_get_proxied_email($fbu, $fb_app) {
  $mail = "";
  if (function_exists("fb_user_app_get_proxied_email")) {

    // Function at fb_user_app module queries fb_use_app table first
    $mail = fb_user_app_get_proxied_email($fbu, $fb_app);
  }
  if (!$mail) {

    // Ask facebook for info.
    $fb = fb_api_init($fb_app);
    $info = fb_users_getInfo(array(
      $fbu,
    ), $fb);

    // TODO deprecated
    $data = $info[0];
    if (isset($data['email'])) {
      $mail = $data['email'];
    }
    elseif (isset($data['proxied_email'])) {
      $mail = $data['proxied_email'];
    }
  }
  return $mail;
}

Functions

Namesort descending Description
fb_user_access_own Helper function for menu item access check.
fb_user_create_local_user Creates a local Drupal account for the specified facebook user id.
fb_user_fb Implements hook_fb.
fb_user_form_alter Implements hook_form_alter().
fb_user_form_user_profile_form_alter Implements hook_form_user_profile_form_alter.
fb_user_get_local_friends Returns local uids of friends of a given user.
fb_user_get_local_user Given an app and facebook user id, return the corresponding local user.
fb_user_get_local_user_by_email Try to determine the local user account by the email address.
fb_user_get_proxied_email Learn the user's proxied email address. If fb_user_app.module is enabled, it will defer to that module, which queries a local database. If not, ask facebook for the data.
fb_user_menu Implements hook_menu().
fb_user_page_alter Implements hook_page_alter().
fb_user_page_title Menu callback to set title.
fb_user_permission Implements hook_permission().
fb_user_token_list
fb_user_token_values
fb_user_user_delete Implements hook_user_delete.
fb_user_user_insert Implements hook_user_insert.
fb_user_user_load Implements hook_user_load.
fb_user_user_login Implements hook_user_login.
fb_user_user_logoutXXX Implements hook_user_logout.
fb_user_user_update Implements hook_user_update().
fb_user_user_view Implements hook_user_view.
fb_user_view_page Menu page callback. Send user to local profile page of a facebook user.
_fb_user_button_text Helper function to retrieve button text.
_fb_user_check_and_goto Detect whether facebook indicates the user has changed. If so, redirect.
_fb_user_check_session Test facebook session by calling into facebook. This is expensive, so limit check to once per session. Use session variable to flag that we have completed the test.
_fb_user_create_local_account Helper function to create local account for the currently authorized user.
_fb_user_create_map Create a map linking the facebook account to the currently logged in local user account.
_fb_user_create_map_by_email
_fb_user_external_login If facebook user has authorized app, and account map exists, login as the local user.
_fb_user_facebook_data
_fb_user_get_config Returns configuration for this module, on a per-app basis.
_fb_user_get_fbu Given a local user id, find the facebook id. This is for internal use. Outside modules use fb_get_fbu().
_fb_user_get_uid
_fb_user_process_authorized_user Create local account or account map for a facebook user who has authorized the application.
_fb_user_set_map Helper function to add or update a row in the fb_user table, which maps local uid to facebook ids.
_fb_user_special_page There are several pages where we don't want to automatically create a new account or use an account configured for this app.

Constants