Skip to content
fb_user.module 34.1 KiB
Newer Older
 * 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
 * 
 * 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_STATS', 'fb_user_stats'); // Whether to use fb_user_app table.
define('FB_USER_VAR_ALTER_REGISTER', 'fb_user_alter_register');
define('FB_USER_VAR_ALTER_LOGIN', 'fb_user_alter_login');

// 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');

  return array('delete own fb_user authmap');
 */
function fb_user_menu() {
  $items = array();
  
  // Admin pages
  $items[FB_PATH_ADMIN . '/fb_user'] = array(
    'title' => 'User Management',
    '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,
  );
/**
 * Returns configuration for this module, on a per-app basis.
 */
function _fb_user_get_config($fb_app) {
Dave Cohen's avatar
Dave Cohen committed
  $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,
    ),
Dave Cohen's avatar
Dave Cohen committed
    'new_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'));
/**
 * Keep track of when the user has visited the app, and whether they've
 * authorized the app or not.
 *
 * Historically this supported infinite sessions.  I believe if this data is
 * no longer necessary for the offline access extended permission.
function _fb_user_track($fb, $fb_app, $account) {
  if (variable_get(FB_USER_VAR_STATS, TRUE)) {
    // In special cases, do not modify the uid column.
    $fb_user_data = _fb_user_get_config($fb_app);
    
      // Keep data for fbu, even if we do not know uid.
      $uid = 0;
    }
    else {
      // Uid is accurate.
      $uid = $account->uid;
    }
    $session = $fb->getSession();
    $session_key = isset($session['session_key']) ? $session['session_key'] : '';
Dave Cohen's avatar
Dave Cohen committed
    $result = db_query("UPDATE {fb_user_app} SET time_access=%d, session_key='%s', session_key_expires=%d, uid=%d WHERE apikey='%s' AND fbu=%d",
                       time(),
                       $session_key, $session['expires'],
                       $uid,
                       $fb_app->apikey, fb_facebook_user($fb));
    
    if ($result && !db_affected_rows()) {
      // The row for this user was never inserted, or deleted.  Insert now.
      $fbu = fb_facebook_user($fb);
      if ($fbu) {
        $info = fb_users_getInfo(array($fbu), $fb);
        $data = $info[0];
Dave Cohen's avatar
Dave Cohen committed
        $result = db_query("INSERT INTO {fb_user_app} (apikey, fbu, added, session_key, session_key_expires, time_access, uid, proxied_email, time_cron) VALUES ('%s', %d, %d, '%s', %d, %d, %d, '%s', %d)",
                           $fb_app->apikey, $fbu,
                           $data['is_app_user'],
                           $session['session_key'],
Dave Cohen's avatar
Dave Cohen committed
                           $session['expires'],
                           time(),
                           $uid,
                           $data['proxied_email'],
                           0 // time_cron
        );
      }
    }
    if ($result === FALSE) {
      watchdog('fb_user', "Failed to update fb_user_app table.", array(), WATCHDOG_ERROR);
/**
 * Helper to ensure local user is logged out.
 */
function _fb_user_logout() {
  session_destroy();
  $user = drupal_anonymous_user();
}

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_POST_INIT) {
    if (isset($_SESSION['fb_user_fbu']) &&
        $_SESSION['fb_user_fbu'] != fb_facebook_user()) {
      // User has logged out of facebook, and drupal is only now learning about it.
      _fb_user_logout();
      drupal_goto($_GET['q']); // @TODO - need request params here?

    if (_fb_user_special_page() ||
        (variable_get('site_offline', 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);
    }
  }
  elseif ($op == FB_OP_APP_IS_AUTHORIZED) {
    // This hook is called on every page request, if the user has authorized
    // the app.  We used to create accounts and maps here.  That code is now
    // in FB_OP_AJAX_EVENT, because it turns out this hook is invoked even on
    // page not found and access denied pages.
    // Keep a record of user visiting this app.
    _fb_user_track($fb, $fb_app, $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_USER_SESSION) { // Still necessary???
    // The fb module is asking for session login information.  For example, to
    // log in as the user when not on a canvas page.  This module may be able
    // to provide it, depending on whether the user has logged in, and whether
    // the session has expired.
    $fbu = $data['fbu'];
    $result = db_query("SELECT * FROM {fb_user_app} WHERE apikey = '%s' and fbu = %d AND session_key_expires > %d", $fb_app->apikey, $fbu, time());
    $data = db_fetch_object($result);
    if ($data && $data->session_key)
      // Return array with FB id and apikey.
      $return = array($data->fbu, $data->session_key);
  }
  elseif ($op == FB_APP_OP_EVENT) {
    // Facebook has notified us of some event.
    // We handle some of the events here.
    $event_type = $data['event_type'];
    // Ensure fb_user_app table accurately reflects whether user has authorized.
    
    if ($event_type == FB_APP_EVENT_POST_AUTHORIZE) {
      // User has authorized us to know some details about her.
      $fbu = fb_facebook_user($fb);
      $proxied_email = fb_user_get_proxied_email($fbu, $fb_app);
      
      // In special cases, do not store the uid column.
      $fb_user_data = _fb_user_get_config($fb_app);
      
      if (variable_get(FB_USER_VAR_STATS, TRUE)) {
        // If user has authorized then later removed, there will be a row we can replace
        db_query("DELETE FROM {fb_user_app} WHERE apikey = '%s' AND fbu = %d", $fb_app->apikey, $fbu);
Dave Cohen's avatar
Dave Cohen committed
        $session = $fb->getSession();
          db_query("INSERT INTO {fb_user_app} (apikey, fbu, uid, added, session_key, session_key_expires, time_cron, time_access, proxied_email) VALUES ('%s', %d, %d, 1, '%s', %d, %d, %d, '%s')",
                   $fb_app->apikey, $fbu,
                   $user->uid,
Dave Cohen's avatar
Dave Cohen committed
                   $session['session_key'], $session['expires'],
                   0, // time_cron
                   time(), // time_access
                   $proxied_email
          );
        }
        else {
          db_query("INSERT INTO {fb_user_app} (apikey, fbu, uid, added, session_key, session_key_expires, time_cron, time_access, proxied_email) VALUES ('%s', %d, %d, 1, '%s', %d, %d, %d, '%s')",
                   $fb_app->apikey, $fbu,
                   0, // uid not known
Dave Cohen's avatar
Dave Cohen committed
                   $session['session_key'], $session['expires'],
                   0, // time_cron
                   time(), // time_access
                   $proxied_email
          );
        }
    elseif ($event_type == FB_APP_EVENT_POST_REMOVE) {
      // User has removed the app from their account.
      // Should we delete the row here???
      db_query("UPDATE {fb_user_app} SET added=0, session_key=NULL, session_key_expires=NULL WHERE apikey='%s' AND fbu=%d",
               $fb_app->apikey, fb_facebook_user($fb));
  elseif ($op == FB_OP_AJAX_EVENT) {
    // fb.js has notified us of an event via AJAX.  Not the same as facebook event callback above.
    if ($data['event_type'] == 'session_change' && isset($data['event_data']['fbu'])) {
      // A user has logged in.
      // Don't trust fbu from $data['event_data'], too easy to spoof.
      // Don't set fb_user if SESSION[fb_user_fbu], could be an old session not properly cleaned up.
      if (($fbu = fb_facebook_user($data['fb'])) &&
        // There's no need to honor maps in ajax callback.  If honored,
        // could lead to new session start which would not actually have any
        // effect in ajax callback.
        fb_controls(FB_USER_CONTROL_NO_HONOR_MAP, TRUE);
        _fb_user_process_authorized_user();
  if (($fbu = fb_facebook_user()) &&
      $fbu != fb_get_fbu($GLOBALS['user']) &&
      $_GET['q'] !== variable_get('site_403', FALSE) &&
      $_GET['q'] !== variable_get('site_404', FALSE)) {
    $uid = $GLOBALS['user']->uid; // Remember original uid.
    _fb_user_process_authorized_user();
    if ($uid != $GLOBALS['user']->uid) {
      // during user processing, we started a new session.
      drupal_goto($_GET['q']); // @TODO - need request params here?
/**
 * 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) {
  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.
    if (fb_verbose() === 'extreme') { // debug
      watchdog("fb_user", "fb_user_fb changing user to $account->uid");
      $session_id = session_id();
    }
    // user_external_login() fails if already logged in, so log out first.
    if ($GLOBALS['user']->uid) {
      _fb_user_logout();
    }
    
    $valid_user = user_external_login($account);
    if (fb_verbose() === 'extreme') { // debug
      watchdog("fb_user", "fb_user_fb changed session from $session_id to " . session_id());
    }
    
    if ($valid_user) {
      // Session changed after external login.  Invoking hook here allows modules to drupal_set_message().
      fb_invoke(FB_OP_POST_EXTERNAL_LOGIN, array('account' => $account));
      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);
      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);
    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/build/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 && _fb_user_check_session($fbu)) {
    // First check if map already exists.
    $account = fb_user_get_local_user($fbu, $GLOBALS['_fb_app']);
    $config = _fb_user_get_config($GLOBALS['_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, $GLOBALS['_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...
        db_query('INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)', 
        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);
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']);
  }
  elseif ($form_id == 'user_profile_form') {
    // On user/edit, hide proxied email
    if (isset($form['account']) && isset($form['account']['mail'])) {
      $account = $form['_account']['#value'];
      if (isset($account->fb_user_proxied_mail) &&
          ($form['account']['mail']['#default_value'] == $account->fb_user_proxied_mail)) {
        unset($form['account']['mail']['#default_value']);
      } 
    }
  }
  elseif (($form_id == 'user_register' && variable_get(FB_USER_VAR_ALTER_REGISTER, TRUE)) ||
          ($form_id == 'user_login' && variable_get(FB_USER_VAR_ALTER_LOGIN, TRUE))) {
    if ($fb = $GLOBALS['_fb']) {
      if ($fbu = fb_facebook_user()) {
        // Facebook user has authorized app.
        
        // Show user name and picture.
        $form['fb_user'] = array(
          'name' => array(
            '#value' => '<fb:name uid="' . $fbu . '" useyou="false" linked="false"></fb:name>',
            '#type' => 'markup',
            '#prefix' => '<div class="fb_user_name">',
            '#suffix' => '</div>',
          ),
          'picture' => array(
            '#value' => '<fb:profile-pic uid="' . $fbu .'" linked="false"></fb:profile-pic>',
            '#type' => 'markup',
            '#prefix' => '<div class="fb_user_picture">',
            '#suffix' => '</div>',
          ),
          '#weight' => -1,
        );
        if ($form_id == 'user_register') {
          // Provide defaults for name and email.
          try {
            $data = $fb->api('/' . $fbu); // Facebook Graph lookup.
            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'];
            }
          }
          catch (FacebookApiException $e) {
            fb_log_exception($e, t('Failed lookup of %fbu.', array('%fbu' => $fbu)));
          }
      else {
        // facebook user has not authorized app.
        $fb_button = theme('fb_login_button', t('Connect with Facebook'), array('form_id' => $form_id));
        $form['fb_user'] = array(
          '#value' => $fb_button,
          '#type' => 'markup',
          '#weight' => -1,
        );
}

/**
 * Learn the user's proxied email address.
 * http://wiki.developers.facebook.com/index.php/Proxied_Email
 */
function fb_user_get_proxied_email($fbu, $fb_app) {
  // Try to learn from local database
  $result = db_query("SELECT * FROM {fb_user_app} WHERE apikey='%s' AND fbu=%d",
                     $fb_app->apikey,
                     $fbu);
  if ($data = db_fetch_object($result)) {
    $mail = $data->proxied_email;
  }
  
  if (!$mail) {
    // Ask facebook for info.
    $fb = fb_api_init($fb_app);
    $info = fb_users_getInfo(array($fbu), $fb);
    $data = $info[0];
    $mail = $data['proxied_email'];
    if ($mail && variable_get(FB_USER_VAR_STATS, TRUE)) {
      // Store locally.
      $result = db_query("UPDATE {fb_user_app} SET proxied_email='%s' WHERE apikey='%s' AND fbu=%d",
                         $mail, $fb_app->apikey, $fbu);
    }
  }
  return $mail;

/**
 * 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');
  }
}

/**
 * Implementation of hook_user.
 */
function fb_user_user($op, &$edit, &$account, $category = NULL) {
  if ($op == 'load' && $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.
    }
  }

  if (!$_fb_app && $op == 'insert' || $op == 'login') {
    // 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)
  // Add tabs on user edit pages to manage maps between local accounts and facebook accounts.
  if ($op == 'categories') {
    // A tab allowing authmaps to be changed.
    $items[] = array(
      'name' => 'fb_user',
      'title' => t('Facebook'),
      'access callback' => 'fb_user_access_own',
      'access arguments' => array(1, 'delete own fb_user authmap', TRUE),
      'weight' => 1,
  elseif ($op == 'form' && $category == 'fb_user') {
        !(user_access('delete own fb_user authmap') && 
          $user->uid == $account->uid))
    $fb_user_data = _fb_user_get_config($_fb_app);
    
    $fbu = _fb_user_get_fbu($account->uid);
    
    if ($fbu) {
      // The drupal user is a facebook user.  Now, learn more from facebook.
      $info = fb_users_getInfo(array($fbu));
      $form['map'] = array(
        '#type' => 'checkbox',
        '#title' => t('Connect account to facebook.com'),
        '#default_value' => $fbu,
        '#return_value' => $fbu,
        '#description' => '',
      );
      if (is_array($info[0])) {
        $data = $info[0];
        $fb_link = l($data['name'] ? $data['name'] : $fbu, 
                     'http://www.facebook.com/profile.php', array(
                       'query' => array('id' => $data['uid'])));
        $form['map']['#description'] .= 
          t('Local account !username corresponds to !profile_page on Facebook.com. <br/>Uncheck then click save to delete this connection.',
            array(
              '!username' => l($account->name, 'user/' . $account->uid),
              '!profile_page' => $fb_link));
    }
    
    if (!$fbu) {
      if ($user->uid == $account->uid) {
        // @TODO - give a user a way to map their facebook account to their local account.
      else {
        $form[$_fb_app->label] = array(
          '#type' => 'markup',
          '#value' => t('Local account !username is not connected to facebook.com.',
                        array('!username' => theme('username', $account),
                        )),
          '#prefix' => "\n<p>",
          '#suffix' => "</p>\n",
        );
    if (isset($form)) {
      $form['map']['#tree'] = TRUE;
    }
    else {
      // Could add a facebook connect button or canvas page authorization link.
      $form['description'] = array(
        '#type' => 'markup',
        '#value' => t('This account is not associated with a Facebook Application.'),
        '#prefix' => '<p>',
        '#suffix' => '</p>',
      );
    }
  elseif ($op == 'update' && $category == 'fb_user') {
    if ($edit['map']) {
      _fb_user_set_map($account, $edit['map']);
    }
    else {
      // Delete account mapping, because administrator has unchecked the connect option.
      db_query('DELETE FROM {fb_user} WHERE uid=%d', $account->uid);
  elseif ($op == 'delete') {
    db_query('DELETE FROM {fb_user_app} WHERE uid=%d', $account->uid);
    db_query('DELETE FROM {fb_user} WHERE uid=%d', $account->uid);
 * 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) {
    $an_fb_user = array(
      'uid' => $account->uid,
      'fbu' => $fbu,
    );
    
    // test if record exists
    // @TODO - should this be DELETE FROM {fb_user} WHERE uid=%d OR fbu=%d?
    $sql = "SELECT count(*) AS c FROM {fb_user} WHERE uid = %d";
    $result = db_query($sql, $account->uid);
    
    $rec = db_fetch_object($result);
    if ($rec->c > 0) {
      // exists so send along keys for drupal_write_record to update record
      $result = drupal_write_record('fb_user', $an_fb_user, "uid");
    } 
    else {
      $result = drupal_write_record('fb_user', $an_fb_user);
    }
    
    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,
  // 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
    $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 = 1;
    while (db_result(db_query("SELECT name FROM {users} WHERE name='%s'", $edit['name']))) {
      $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
    $edit = fb_invoke(FB_OP_PRE_USER, array('fbu' => $fbu,

    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' => db_escape_string($fbu . '@facebook'), // Supposed to be email, but we may not know it.
        'status' => 1,
      );
      // Mail available only if user has granted extended permission.
      if (isset($info['email']) && ($info['email'] != $info['proxied_email'])) {
        $defaults['mail'] = $info['email'];
      }
      // Merge defaults
      $edit = array_merge($defaults, $edit);
      // Confirm username is not taken.  FB_OP_PRE_USER may have changed it.    
      if ($uid = db_result(db_query("SELECT uid FROM {users} WHERE name='%s'", $edit['name']))) {
        // The desired name is taken.
        watchdog('fb_user', 'Failed to create new user %name.  That name is already in the users table.', 
                 WATCHDOG_ERROR, l(t('view user'), 'user/' . $uid));
      }
      else {
        $account = user_save('', $edit);
        
        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_OP_POST_USER, array('account' => $account,
                                         'fb_app' => $fb_app,
                                         'fb' => $fb));
      }
/**
 * 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) {
  $result = db_result(db_query("SELECT uid FROM {fb_user} WHERE fbu=%d", array(
                                 $fbu,
                               )));
  if ($result) {
    return user_load($result);
/**
 * 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) {
    $info = $_fb->api($fbu);
    if (isset($info['email']) &&
        ($email = $info['email'])) {
      return user_load(array('mail' => $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)) {
    $query = "SELECT uid FROM {fb_user} WHERE fbu IN ('%s')"; 
    $args[] = implode(',', $fbus);
    $result = db_query($query, $args);
    
    while ($data = db_fetch_object($result)) {
      if ($data->uid) {
        $uids[] = $data->uid;
      }

/**
 * 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) {
  static $cache = array(); // cache to avoid excess queries.
  
  if (!isset($cache[$uid])) {
    $result = db_result(db_query("SELECT fbu FROM {fb_user} WHERE uid=%d", array(
                                   $uid,
                                 )));
    if ($result) {
      $cache[$uid] = $result;

  if (isset($cache[$uid])) {
    return $cache[$uid];
  }
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) {
Dave Cohen's avatar
Dave Cohen committed
  $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 XXX'; // XXX
      $values['fb-app-user-name-fbml'] = '<fb:name uid="'. $fbu .'" />';
      $values['fb-app-profile-url'] = 
        'http://www.facebook.com/profile.php?id='. $fbu;