You are here

fb_user.module in Drupal for Facebook 5

This module allows Drupal user records to be associated with Facebook user ids. It can create local user accounts when Facebook users visit an application's canvas pages.

File

fb_user.module
View source
<?php

/**
 * @file
 * 
 * This module allows Drupal user records to be associated with Facebook user
 * ids.  It can create local user accounts when Facebook users visit an
 * application's canvas pages.
 */
define('FB_USER_OPTION_ALLOW_ANON', 1);
define('FB_USER_OPTION_REQUIRE_LOGIN', 2);
define('FB_USER_OPTION_REQUIRE_ADD', 3);
define('FB_USER_OPTION_CREATE_NEVER', 1);
define('FB_USER_OPTION_CREATE_LOGIN', 2);
define('FB_USER_OPTION_CREATE_ADD', 3);
define('FB_USER_OPTION_MAP_NEVER', 1);
define('FB_USER_OPTION_MAP_ALWAYS', 2);
define('FB_USER_SYNC_PATH', 'fb_user/sync');
define('FB_USER_POST_ADD_PATH', 'fb_user/post_add');
define('FB_USER_POST_REMOVE_PATH', 'fb_user/post_remove');
function fb_user_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    global $user;
    $items[] = array(
      'path' => FB_USER_SYNC_PATH,
      'access' => TRUE,
      'callback' => 'fb_user_sync_cb',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => FB_USER_POST_ADD_PATH,
      'access' => TRUE,
      'callback' => 'fb_user_post_add_cb',
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => FB_USER_POST_REMOVE_PATH,
      'access' => TRUE,
      'callback' => 'fb_user_post_remove_cb',
      'type' => MENU_CALLBACK,
    );
  }
  return $items;
}

/**
 * This method will redirect a user if the post-add page need not be displayed.
 * 
 * If replacing the normal post-add page with a custom implementation, do call this.
 */
function fb_user_post_add_check() {
  global $fb, $fb_app, $user;

  // This page is for canvas pages only
  if (!$fb) {
    drupal_access_denied();
    exit;
  }

  // redirect will be set if we've called $fb->require_add.  In this case
  // we forward the user on.
  if ($_REQUEST['redirect']) {
    if (function_exists('fb_canvas_fix_url')) {

      // Redirect to a canvas page
      $url = fb_canvas_fix_url($_REQUEST['redirect'], $fb_app);
      if (fb_verbose()) {
        watchdog('fb_debug', "post_add_check redirecting to {$url}");
      }
      $fb
        ->redirect($url);
      exit;
    }
    else {

      // XXX is this still necessary?
      _fb_user_sync_redirect($_REQUEST['redirect']);
    }
  }

  // This function returns only if post add page needs to be rendered.  Otherwise user is redirected.
}

/**
 * The post-add page is where the user is sent after adding the application.
 * 
 * Note that the Facebook App settings must be set up.  See "post-add"
 * callback.  To customize this behavior, use form_alter to modify the default
 * form, or create your own callback.
 * 
 * Special case when $_REQUEST['redirect'] is set.  In this case we've come
 * after a request from our sync callback (or any call to $fb->require_add)
 * and we're going to cache some data so it can be used on the other end.
 */
function fb_user_post_add_cb() {
  global $fb, $fb_app, $user;
  if (fb_verbose()) {
    watchdog('fb_debug', "New user has added an app, post-add callback called.");
    if (function_exists('dprint_r')) {
      watchdog('fb_debug', "New user has added an app." . dprint_r($_REQUEST, 1));
    }
  }

  // Check that actually need to render this page.
  fb_user_post_add_check();

  // Will exit() if user has been redirected.
  // redirect was not passed in, present form(s) prompting the user to
  // decide what to do next.
  $weight = 0;

  // Although user has just added the app, they may have an authmap entry
  // already.  This happens if they have installed then removed the app, or
  // they've added another app on this same server.  In this case, we don't
  // want to display the login form.
  if (!$user->uid) {

    //watchdog('debug', 'fb_user_post_add_cb' . dprint_r($_REQUEST, 1));
    $output['login'] = array(
      '#type' => 'fieldset',
      '#title' => t('Login to !site account', array(
        '!site' => variable_get('site_name', 'Drupal'),
      )),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#weight' => $weight++,
    );
    $output['login']['form'] = array(
      '#value' => drupal_get_form('user_login'),
    );
    $output['register'] = array(
      '#type' => 'fieldset',
      '#title' => t('Register new account', array(
        '!site' => variable_get('site_name', 'Drupal'),
      )),
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#weight' => $weight++,
    );
    $output['register']['form'] = array(
      '#value' => drupal_get_form('user_register'),
    );
  }
  $output['skip'] = array(
    '#value' => l(t('Skip registration, use !app', array(
      '!app' => t($fb_app->title),
    )), '<front>', array(
      'class' => 'fb_button',
    )),
    '#weight' => $weight++,
    '#prefix' => '<p>',
    '#suffix' => '</p>',
  );

  // TODO: make output customizable by third-party modules.
  return drupal_render($output);
}

/**
 * A form which helps a facebook user either register a new local drupal
 * account or synchronize facebook account with an existing account.  Really,
 * all we do is redirect the user to the login or registration forms.
 */
function fb_user_post_add_form() {
  global $fb, $fb_app, $user;

  // This form only works on canvas pages.
  if (!$fb_app) {
    drupal_access_denied();
    exit;
  }

  // And if the user's account is recognized, we can skip this.
  if ($user->uid && FALSE) {
    drupal_goto("<front>");
    exit;
  }

  // Substitutions for translation
  $t = array(
    '%sitename' => variable_get('site_name', t('this website')),
    '%appname' => $fb_app->title,
  );
  drupal_set_title(t('Welcome to %appname', $t));
  $parents = array(
    'redirect',
  );

  // Could use 'radios' type, but using 'radio' is more flexible.
  $form['redirect']['user/login'] = array(
    '#type' => 'radio',
    '#title' => t('I already have an account on %sitename', $t),
    '#description' => t('You will be prompted for your password.', $t),
    '#return_value' => 'user/login',
    '#parents' => $parents,
  );
  $form['redirect']['user/register'] = array(
    '#type' => 'radio',
    '#title' => t('Complete my registration now', $t),
    '#description' => t('You will be asked for additional information and given a new password.', $t),
    '#return_value' => 'user/register',
    '#parents' => $parents,
    '#default_value' => 'user/register',
  );
  $form['redirect']['frontpage'] = array(
    '#type' => 'radio',
    '#title' => t('I will register later', $t),
    '#description' => t('Use this application without registering on %sitename.', $t),
    '#return_value' => '<front>',
    '#parents' => $parents,
  );
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('Continue'),
  );
  return $form;
}
function fb_user_post_add_form_submit($form_id, $values) {

  //dpm(func_get_args(), 'fb_user_post_add_form_submit');

  // Here we simply redirect the user to the appropriate page
  return $values['redirect'];
}

/**
 * The post-remove page is visited by Facebook after a user removes the
 * application.  The user never visits the page, it is simply called by
 * Facebook to notify us of the change.
 */
function fb_user_post_remove_cb() {
  global $fb, $fb_app, $user;
  if (fb_verbose()) {
    watchdog('debug', 'fb_user_post_remove_cb, session_id is ' . session_id() . ' session_name is ' . session_name() . dprint_r($_REQUEST, 1) . dprint_r($user, 1) . dprint_r($fb_app, 1));
  }

  // Update our database to reflect application is NOT added
  _fb_user_track($fb, $fb_app, $user);

  // TODO: invoke hook_fb to let other modules know about removal
  // Ideally, we would destroy the user's session here.  However the session
  // currently active is not the same as the user's.  We have no robust way to
  // delete their session.
  // Nothing to return
  exit;
}
function _fb_user_sync_redirect($redirect) {
  global $user;
  global $fb, $fb_app;
  $cache_data = array(
    'request' => $_REQUEST,
    'fbu' => fb_facebook_user(),
    'fb_app' => $fb_app,
  );

  // Generate a one-time key for cached data.  We used to use Facebook's
  // auth_token here, but it's not always available
  // so better to generate our own.
  $key = uniqid('fb_user_');
  cache_set($key, 'cache', serialize($cache_data), CACHE_TEMPORARY);

  // Note that drupal_goto will fail here, but $fb->redirect succeeds.
  $url = fb_scrub_urls(url($redirect, 'sync_token=' . $key, NULL, TRUE));
  if (fb_verbose()) {
    watchdog('fb_debug', "Redirecting to {$url}");
  }
  $fb
    ->redirect($url);
  exit;
}

/**
 * The sync callback is invoked first on a canvas page, in which case we
 * require the user to add the application.  Later the user will be redirected
 * to this callback on the locale server, with an token that allows us to
 * write the necessary row to the authmap table.
 */
function fb_user_sync_cb() {
  global $user;
  global $fb, $fb_app;

  // TODO: ensure that user does not already have a mapping to some other facebook id.
  // On canvas pages, require user to add the app...
  if ($fb) {
    if (fb_verbose() && function_exists('dprint_r')) {
      watchdog('debug', 'fb_user_sync_cb request ' . dprint_r($_REQUEST, 1));
    }
    $fb
      ->require_add();

    // The user has already added the app.
    // Redirect to the local page where the authmap will be generated.
    _fb_user_sync_redirect(FB_USER_SYNC_PATH);
  }
  else {

    // Double check user is logged in.  No point syncing the anonymous account.
    if (!$user->uid) {
      drupal_access_denied();
      exit;
    }
    $output = '';

    // On non canvas pages, we have returned to the local server.  Hopefully
    // the user has added the app and we can now sync the two accounts.
    if ($_REQUEST['sync_token']) {
      $key = $_REQUEST['sync_token'];
      $cache = cache_get($key);
      cache_clear_all($key, 'cache');

      // So noone else uses this key
      $data = unserialize($cache->data);
      if (!$data) {
        drupal_set_message(t('Unable to locate Facebook account information.'), 'error');
        drupal_access_denied();
        exit;
      }
      watchdog('debug', 'got the data ' . dprint_r($data, 1));
      $fbu = $data['fbu'];
      $fb_app = $data['fb_app'];
      drupal_set_title(t('Added %appname Application', array(
        '%appname' => $fb_app->title,
      )));
      if ($fbu && $fb_app) {
        list($module, $authname) = _fb_user_get_authmap($fb_app, $fbu);

        // Check if the authname is already in use.
        $account = user_external_load($authname);
        if ($account && $account->uid != $user->uid) {
          watchdog('fb_user', t('Re-syncing facebook account.  The authname %authname currently refers to !old_user_link.  Updating to point to !new_user_link.', array(
            '%authname' => $authname,
            '%old_username' => $account->name,
            '%old_uid' => $account->uid,
            '!old_user_link' => theme('username', $account),
            '!new_user_link' => theme('username', $user),
          )));

          // This will delete the old authmap entry
          user_set_authmaps($account, array(
            $module => NULL,
          ));
        }
        if (fb_verbose()) {
          watchdog('fb_user', t('Syncing local user %uid with facebook user %fbu via authmap entry %authname', array(
            '%fbu' => $fbu,
            '%uid' => $user->uid,
            '%authname' => $authname,
          )));
        }

        // Write the authmap
        user_set_authmaps($user, array(
          $module => $authname,
        ));
        drupal_set_message(t('Your <a href="!localurl">local account</a> is linked to your <a href="!facebookurl">Facebook profile</a>.', array(
          '!localurl' => url('user/' . $user->uid),
          '!facebookurl' => url('http://www.facebook.com/profile.php', 'id=' . $fbu),
        )));

        // Give the user feedback that the sync has been successful.
        // Query facebook to learn more about their facebook account.
        $fb = fb_api_init($fb_app, FB_FBU_ANY);
        if ($fb) {
          $info = $fb->api_client
            ->users_getInfo(array(
            $fbu,
          ), array(
            'about_me',
            'affiliations',
            'name',
            'is_app_user',
            'pic_big',
            'profile_update_time',
            'status',
          ));
          if (count($info)) {
            $output .= theme('fb_app_user_info', $fb_app, $info[0]);
          }
        }
      }
    }
  }

  // TODO: allow modules a way to customize the output of this function.
  return $output;
}

// 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() {
  return arg(0) == 'user' || arg(0) == 'fb_user' || arg(0) == 'fb' && arg(1) == 'form_cache';
}
function _fb_user_track($fb, $fb_app, $user) {

  // Keep track of all our app users.  We need this info when updating
  // profiles during cron.  We keep session keys in case user has an
  // infinite session, and we can actually log in as them during cron.
  // TODO: is this a violation of facebook terms?
  // In special cases, do not modify the uid column.
  $fb_app_data = fb_app_get_data($fb_app);
  $fb_user_data = $fb_app_data['fb_user'];

  // our configuration
  if (!$user->uid || $user->uid == $fb_user_data['not_logged_in_uid'] || $user->uid == $fb_user_data['logged_in_uid']) {
    $result = db_query("REPLACE INTO {fb_user_app} (apikey, fbu, added, time_access, session_key, session_key_expires) VALUES ('%s', %d, %d, %d, '%s', %d)", $fb_app->apikey, fb_facebook_user(), $fb->api_client
      ->users_isAppAdded(), time(), $fb->api_client->session_key, $_REQUEST['fb_sig_expires']);
  }
  else {

    // Uid is accurate.
    $result = db_query("REPLACE INTO {fb_user_app} (apikey, fbu, uid, added, time_access, session_key, session_key_expires) VALUES ('%s', %d, %d, %d, %d, '%s', %d)", $fb_app->apikey, fb_facebook_user(), $user->uid, $fb->api_client
      ->users_isAppAdded(), time(), $fb->api_client->session_key, $_REQUEST['fb_sig_expires']);
  }
  if ($result === FALSE) {
    watchdog('fb_user', t("Failed to update fb_user_app table."), WATCHDOG_ERROR);
  }
}

/**
 * Implementation of hook_fb.
 */
function fb_user_fb($fb, $fb_app, $op, &$return, $data) {
  if ($op == FB_OP_INITIALIZE) {
    global $user;
    $fb_app_data = fb_app_get_data($fb_app);
    $fb_user_data = $fb_app_data['fb_user'];

    // our configuration
    // Here we ask facebook to prompt the user to login or add the app.
    if ($fb_user_data['require_login'] == FB_USER_OPTION_REQUIRE_LOGIN) {
      $fb
        ->require_login();
    }
    else {
      if ($fb_user_data['require_login'] == FB_USER_OPTION_REQUIRE_ADD) {
        $fb
          ->require_add();
      }
    }

    // If we know the user's fbu, try to load the corresponding local account.
    $fbu = fb_facebook_user();
    if ($fbu) {

      // Remember the original uid (probably 0 for anonymous)
      $original_uid = $user->uid;
      if ($user->fbu != $fbu) {

        // Try the application-specific account.
        $account = user_external_load("{$fbu}-{$fb_app->apikey}@facebook.com");
        if (!$account) {

          // Try the cross-application account.
          $account = user_external_load("{$fbu}@facebook.com");
        }
        if ($account) {
          $account->fbu = $fbu;
          $user = $account;

          // change the global user
        }
      }

      // Later, if we do not create an account, we'll load the default user
      // specified in the app config.
      // Check if we need to create a local account for this user.
      if ($fb_user_data['create_account'] == FB_USER_OPTION_CREATE_ADD && $fb->api_client
        ->users_isAppAdded() || $fb_user_data['create_account'] == FB_USER_OPTION_CREATE_LOGIN) {

        // Check if the local account is already made.
        if ($user->fbu != fb_facebook_user() && !_fb_user_special_page()) {

          // We need to make a local account for this facebook user.
          $user = fb_user_create_local_user($fb, $fb_app, fb_facebook_user(), array(
            'app_specific' => $fb_user_data['unique_account'],
            'roles' => array(
              $fb_user_data['new_user_rid'] => TRUE,
            ),
          ));
          watchdog('fb_user', t("Created new user !username for application %app", array(
            '!username' => theme('username', $user),
            '%app' => $fb_app->label,
          )));
        }
      }

      // It's possible the user was already created by another app.
      // In this case we need to add our role.
      if ($user->fbu == fb_facebook_user() && $fb_user_data['new_user_rid'] && !$user->roles[$fb_user_data['new_user_rid']]) {

        // there should be an API for this...
        db_query('INSERT INTO {users_roles} (uid, rid) VALUES (%d, %d)', $user->uid, $fb_user_data['new_user_rid']);
        watchdog('fb_user', t("Added role %role to existing user !username for application %app", array(
          '!username' => theme('username', $user),
          '%app' => $fb_app->label,
          '%role' => $fb_user_data['new_user_rid'],
        )));
      }
      if ($user->uid != $original_uid) {

        // We've changed the user.  In order to ensure that drupal handles permissions properly, the user must make the request all over again.
        if (function_exists('fb_canvas_fix_url')) {

          // Redirect to a canvas page
          $url = fb_canvas_fix_url(url(fb_scrub_urls($_REQUEST['q']), NULL, NULL, TRUE), $fb_app);
          if (fb_verbose()) {
            watchdog('fb_debug', "User uid is now {$user->uid} (was {$original_uid}), redirecting to {$url} to ensure permissions are correct.");
          }

          // debug
          $fb
            ->redirect($url);
        }
      }

      // Keep a record of when user accesses app, and whether they have added it.
      _fb_user_track($fb, $fb_app, $user);
    }

    // Don't mess with the user info if the user is visiting the login pages or submitting a form (i.e. the login form).
    if (!$user->uid && !_fb_user_special_page() && !$_REQUEST['form_id']) {
      if ($fbu = fb_facebook_user()) {
        $uid = $fb_app_data['fb_user']['logged_in_uid'];
      }
      else {
        $uid = $fb_app_data['fb_user']['not_logged_in_uid'];
      }
      if ($uid) {
        $user = user_load(array(
          'uid' => $uid,
        ));

        /* too verbose
           watchdog('fb_user', t('Treating a facebook user as local user !user',
                                 array('!user' => $user->name,
                                 )));
           */
      }
    }

    // We don't want user's who are not logged in (in the facebook sense) to
    // login locally.  So let's make sure they've added the app before doing
    // anything related to Drupal accounts.
    if (strpos($_GET['q'], 'user/login') === 0) {

      // Have to check idAppAdded in case of iframe.
      if (!$fb->api_client
        ->users_isAppAdded()) {
        $fb
          ->require_add();
      }
    }
    else {
      if (strpos($_GET['q'], 'user/register') === 0) {
        if (!$fb->api_client
          ->users_isAppAdded()) {
          $fb
            ->require_add();
        }
      }
    }

    // Now do I need a goto or some such???
    // debug

    /*
    drupal_set_message("To Drupal, you are " . theme('username', $user));
    drupal_set_message("Facebook user id is " . fb_facebook_user());
    drupal_set_message("Facebook logged in is " . $fb->get_loggedin_user());
    */
  }
  else {
    if ($op == FB_OP_GET_FBU) {

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

        // 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", $fb_app->apikey, $fbu);
        $data = db_fetch_object($result);
        if ($data && $data->session_key) {

          // Return array with FB id and apikey.
          $return = array(
            $data->fbu,
            $data->session_key,
          );
        }
      }
    }
  }
}

/**
 * To help map local accounts to facebook accounts, we can bounce a logged in
 * user to a canvas page.  That page will ensure they have added the
 * application, then bounce them back to a local HTML page.  When they are
 * bounced back, a cached key allows us to know which facebook account
 * corresponds to the user.
 * 
 * @return a fully qualified URL of the canvas page
 */
function fb_user_get_map_url($fb_app) {
  if ($fb_app->canvas) {
    return "http://apps.facebook.com/{$fb_app->canvas}/" . FB_USER_SYNC_PATH;
  }
}
function fb_user_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if ($op == 'view' && $node->type == 'fb_app') {

    //dpm(func_get_args(), 'fb_user_nodeapi');
    if (user_access('administer fb apps')) {
      $fb_app = $node->fb_app;
      $add_url = fb_user_get_map_url($fb_app);
      $output = theme('dl', array(
        t('Post-add URL') => "http://apps.facebook.com/{$fb_app->canvas}/" . FB_USER_POST_ADD_PATH . "?redirect=",
        t('Post-remove URL') => url(FB_SETTINGS_APP_NID . '/' . $node->nid . '/' . FB_USER_POST_REMOVE_PATH, NULL, NULL, TRUE),
        t('Add URL') => $add_url ? $add_url . '<br/>' . t('(Send an authenticated user to this URL so that local account will be authmapped to facebook account.)') : t('none'),
      ));
      $node->content['fb_user'] = array(
        '#value' => $output,
        '#weight' => 2,
      );
    }
  }
}
function fb_user_form_alter($form_id, &$form) {

  //drupal_set_message("fb_user_form_alter($form_id) " . dpr($form, 1));

  // Add our settings to the fb_app edit form.
  if (is_array($form['fb_app_data'])) {
    $node = $form['#node'];
    $fb_app_data = fb_app_get_data($node->fb_app);
    $fb_user_data = $fb_app_data['fb_user'];
    $form['fb_app_data']['fb_user'] = array(
      '#type' => 'fieldset',
      '#title' => t('Facebook user settings'),
      '#tree' => TRUE,
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
    );
    $form['fb_app_data']['fb_user']['require_login'] = array(
      '#type' => 'radios',
      '#title' => t('Require Login'),
      '#description' => t('When viewing canvas pages, how strict do you want to be?  Most FBML applications will not want \'Allow anyone\' because facebook allows no persistent session; features like drupal_set_message() will not work properly.  Also facebook does not allow form submissions unless the user logs in.'),
      '#options' => array(
        FB_USER_OPTION_ALLOW_ANON => t('Allow anyone'),
        FB_USER_OPTION_REQUIRE_LOGIN => t('Allow logged-in users'),
        FB_USER_OPTION_REQUIRE_ADD => t('Allow users who have added this app to their profile'),
      ),
      '#default_value' => $fb_user_data['require_login'],
      '#required' => TRUE,
    );

    // TODO: fix this so that it prompts for username with autocomplete, not a uid.
    $form['fb_app_data']['fb_user']['not_logged_in_uid'] = array(
      '#type' => 'textfield',
      '#title' => t('Not logged in user (uid)'),
      '#description' => t('If allowing non-logged in users, when such a user visits the site, which Drupal user should they be treated as?  Use 0 for the anonymous user (recommended - this feature is experimental and likely to disappear).'),
      '#default_value' => $fb_user_data['not_logged_in_uid'],
    );
    $form['fb_app_data']['fb_user']['logged_in_uid'] = array(
      '#type' => 'textfield',
      '#title' => t('Logged in user (uid)'),
      '#description' => t('If allowing logged in users, when such a user visits the site, and they do not have a local Drupal account, which Drupal user should they be treated as?  Use 0 for the Anonymous user, or create a dedicated account for this purpose.'),
      '#default_value' => $fb_user_data['logged_in_uid'],
    );
    $form['fb_app_data']['fb_user']['create_account'] = array(
      '#type' => 'radios',
      '#title' => t('Create Local Account'),
      '#description' => t('This option will create a local account automatically and map the local account to the Facebook account.  This happens whenever the user visits a canvas page, except user/ pages and the landing page for anonymous users.'),
      '#options' => array(
        FB_USER_OPTION_CREATE_NEVER => t('Never (user/register page will still work)'),
        FB_USER_OPTION_CREATE_LOGIN => t('If user has logged in'),
        FB_USER_OPTION_CREATE_ADD => t('If user has added this app'),
      ),
      '#default_value' => $fb_user_data['create_account'],
      '#required' => TRUE,
    );
    $form['fb_app_data']['fb_user']['map_account'] = array(
      '#type' => 'radios',
      '#title' => t('Map Accounts'),
      '#description' => t('This option maps a Facebook account to a previously created local account, when a user logs in or registers via a canvas page.'),
      '#options' => array(
        FB_USER_OPTION_MAP_NEVER => t('Never map accounts'),
        FB_USER_OPTION_MAP_ALWAYS => t('Map account when user logs in or registers'),
      ),
      '#default_value' => $fb_user_data['map_account'],
      '#required' => TRUE,
    );

    // TODO: prompt for role with a select.  Don't make user figure out id
    $form['fb_app_data']['fb_user']['new_user_rid'] = array(
      '#type' => 'select',
      '#title' => t('New user role'),
      '#options' => user_roles(1),
      '#description' => t('When a user has added this application, they will receive this role.'),
      '#default_value' => $fb_user_data['new_user_rid'],
    );
    $form['fb_app_data']['fb_user']['unique_account'] = array(
      '#type' => 'checkbox',
      '#title' => t('Make Local Account Unique'),
      '#description' => t('When creating a local account, a unique account will apply only to this app.  A non-unique account will be shared by all apps which do not have this checked.'),
      '#default_value' => $fb_user_data['unique_account'],
    );
  }
}

/**
 * Implementation of hook_user.
 */
function fb_user_user($op, &$edit, &$account, $category = NULL) {
  global $fb, $fb_app;

  // Set only in canvas pages.
  global $user;

  // If form posted from an FBML canvas page, we learn the app and fbu from the post.
  // TODO: do we need additional validation here? (i.e. an fb_api_init to confirm the facebook params?)
  if ($_REQUEST['fb_sig']) {

    //watchdog('debug', dprint_r($_REQUEST, 'fb_user_user request'));
    $fb_app = fb_get_app(array(
      'apikey' => $_REQUEST['fb_sig_api_key'],
    ));
    $fbu = $_REQUEST['fb_sig_user'];
  }
  else {
    if ($fb) {

      // Post from iframe
      $fbu = fb_facebook_user();
    }
  }
  if ($fb_app && $op == 'insert' || $op == 'login') {

    // A facebook user has logged in.  We can map the two acounts together.
    $fb_app_data = fb_app_get_data($fb_app);
    $fb_user_data = $fb_app_data['fb_user'];

    // our configuration
    if ($fbu && $fb_user_data['map_account'] == FB_USER_OPTION_MAP_ALWAYS) {
      list($module, $authname) = _fb_user_get_authmap($fb_app, $fbu);
      if ($op == 'insert') {

        // User has registered, we set up the authmap this way...
        $edit['authname_fb_user'] = $authname;
      }
      else {
        if ($op == 'login') {

          // On login, we set up the map this way...
          user_set_authmaps($account, array(
            $module => $authname,
          ));
        }
      }

      // 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') {
    $items[] = array(
      'name' => 'fb_user',
      'title' => t('Facebook Applications'),
      'weight' => 1,
    );
    return $items;
  }
  else {
    if ($op == 'form' && $category == 'fb_user') {
      $form['map'] = array(
        '#tree' => TRUE,
      );

      // Iterate through all facebook apps, because they do not all use the same
      // map scheme.
      $result = _fb_app_query_all();
      while ($fb_app = db_fetch_object($result)) {
        $fb_app_data = fb_app_get_data($fb_app);
        $fb_user_data = $fb_app_data['fb_user'];

        // our configuration
        $fbu = _fb_user_get_fbu($account->uid, $fb_app);
        if ($fbu && !$info[$fbu]) {

          // The drupal user is a facebook user.  Now, learn more from facebook.
          $fb = fb_api_init($fb_app, FB_FBU_ANY);

          // Note: this requires infinite session with facebook.  TODO: fallback to fb_user_app table.
          $info[$fbu] = $fb->api_client
            ->users_getInfo(array(
            $fbu,
          ), array(
            'name',
            'is_app_user',
          ));

          //dpm($info[$fbu], "Info from facebook for $fbu");
        }
        if ($fbu) {
          list($module, $authname) = _fb_user_get_authmap($fb_app, $fbu);
          if ($fb_user_data['unique_account']) {
            $form['map'][$module] = array(
              '#type' => 'checkbox',
              '#title' => $fb_app->title,
              '#default_value' => $authname,
              '#return_value' => $authname,
            );
          }
          else {
            $shared_maps[] = $fb_app->title;
            $shared_fbu = $fbu;

            // Same for all shared apps.
            $shared_module = $module;
            $shared_authname = $authname;
          }
        }
        if ($shared_maps) {
          $form['map'][$shared_module] = array(
            '#type' => 'checkbox',
            '#title' => implode('<br/>', $shared_maps),
            '#default_value' => $shared_authname,
            '#return_value' => $shared_authname,
          );
          if ($info[$shared_fbu]) {
            $data = $info[$shared_fbu][0];
            $fb_link = l($data['name'], 'http://www.facebook.com/profile.php', NULL, 'id=' . $data['uid']);
            $form['map'][$shared_module]['#description'] .= t('Local account (!username) corresponds to !profile_page on Facebook.com.', array(
              '!username' => theme('username', $account),
              '!profile_page' => $fb_link,
            ));
          }
        }
        if (!$fbu) {
          if ($user->uid == $account->uid && ($map_url = fb_user_get_map_url($fb_app))) {
            $about_url = fb_app_get_about_url($fb_app);
            $form[$fb_app->nid] = array(
              '#type' => 'markup',
              '#value' => t('You may add !application_about_page to your <a href="!facebook_url" target=_blank>Facebook</a> account.  <ol><li>First, log into <a href="!facebook_url" target=_blank>Facebook</a>.</li><li>Then, <a href="!map_url">click here to add the %application application</a>.</li></ol>', array(
                '!map_url' => $map_url,
                '!facebook_url' => 'http://www.facebook.com',
                '%application' => $fb_app->title,
                '!application_about_page' => $about_url ? l($fb_app->title, $about_url) : '<em>' . $fb_app->title . '</em>',
              )),
              '#prefix' => "\n<p>",
              '#suffix' => "</p>\n",
            );
          }
          else {
            $form[$fb_app->nid] = array(
              '#type' => 'markup',
              '#value' => t('!username does not use !application.', array(
                '!username' => theme('username', $account),
                '!application' => l($fb_app->title, 'node/' . $fb_app->nid),
              )),
              '#prefix' => "\n<p>",
              '#suffix' => "</p>\n",
            );
          }
        }
      }
      return $form;
    }
    else {
      if ($op == 'update' && $category == 'fb_user') {

        //dpm($edit, "fb_user_user($op)");
        foreach ($edit['map'] as $module => $authname) {
          user_set_authmaps($account, array(
            $module => $authname,
          ));
        }
      }
    }
  }
}
function theme_fb_app_name_with_links($fb_app, $is_added = NULL) {
  $output = $fb_app->title;

  // TODO add link to about page
  if ($is_added === FALSE) {
    $links[] = 'add link';

    //XXX
  }
  return $output;
}

/**
 * Helper function to create an authname for the authmap table.
 * 
 * When a single Drupal instance hosts multiple Facebook apps, the apps can
 * share the same mapping, or each have their own.
 * 
 * @return an array with both a 'module' and an authname.  A 
 * data structure necessary for Drupal's authmap api.
 */
function _fb_user_get_authmap($fb_app, $fbu) {
  $fb_app_data = fb_app_get_data($fb_app);
  $fb_user_data = $fb_app_data['fb_user'];

  // our configuration
  $app_specific = $fb_user_data['unique_account'];

  // map fbu to uid, include apikey if user is app_specific
  if ($app_specific) {

    // would rather use the shorter app id (not apikey), but no way to query it
    $authname = "{$fbu}-{$fb_app->apikey}@facebook.com";
    $module = "fb_user-{$fb_app->nid}";
  }
  else {
    $authname = "{$fbu}@facebook.com";
    $module = "fb_user";
  }

  //return array('module' => $module, 'authname' => $authname);
  return array(
    $module,
    $authname,
  );
}

/**
 * Creates a local Drupal account for the specified facebook user id.
 * 
 * @param fbu
 * The facebook user id corresponding to this account.
 * 
 * @param config
 * An associative array with user configuration.  Possible values include:
 * 'app_specific' - Set to true if the same facebook id might correspond to different local accounts, depending on which apps the user has used.  Set to false if the user shares one local account across facebook apps.
 * 'roles' - an array with keys corresponding to role ids the new user should receive.
 */
function fb_user_create_local_user($fb, $fb_app, $fbu, $config = array()) {

  // TODO: ensure $fbu is a real user, not FB_FB_ANY or FB_FBU_CURRENT
  // debugging.

  //drupal_set_message("Facebook knows you as $username ($fbu)");
  $authmap = _fb_user_get_authmap($fb_app, $fbu);
  $account = user_external_load($authmap);
  if (!$account) {

    // Create a new user in our system
    // We need a username that will not collide with any already in our
    // system.  Could use $authmap, but this will be just slightly more
    // user-friendly.
    if ($config['app_specific'] && !$config['username']) {
      $config['username'] = "{$fbu}-{$fb_app->label}@facebook";
    }
    else {
      $config['username'] = "{$fbu}@facebook";
    }

    // Allow third-party module to adjust any of our settings before we create
    // the user.
    $config = _fb_invoke($fb_app, FB_OP_PRE_USER, $config, array(
      'fbu' => $fbu,
    ));

    // TODO: double-check that username is not taken.
    $user_default = array(
      'name' => $config['username'],
      'pass' => user_password(),
      'init' => db_escape_string($config['username']),
      'status' => 1,
      'authname_fb_user' => $authmap,
    );

    // Allow $config to set other values, including mail
    $user_default = array_merge($user_default, $config);
    $user_default['roles'][DRUPAL_AUTHENTICATED_RID] = 'authenticated user';
    if (count($config['roles'])) {
      foreach ($config['roles'] as $rid => $value) {
        if ($rid) {
          $user_default['roles'][$rid] = $value;
        }
      }
    }
    $user_default['fbu'] = $fbu;

    // Will get saved as user data.
    $account = user_save('', $user_default);
    watchdog('fb_user', t('New user: %name %email.', array(
      '%name' => $name,
      '%email' => '<' . $mail . '>',
    )), WATCHDOG_NOTICE, l(t('edit'), 'user/' . $account->uid . '/edit'));

    // Allow third-party modules to act after account creation.
    $config = _fb_invoke($fb_app, FB_OP_POST_USER, NULL, array(
      'account' => $account,
    ));

    // TODO: move this to fb_action.  Temporarily disabled.
    if (FALSE) {

      // Prepare to send an email.
      $base = url('<front>', NULL, NULL, TRUE);
      $variables = array(
        '!username' => $account->name,
        '!site' => variable_get('site_name', 'Drupal'),
        '!password' => $user_default['pass'],
        '!uri' => $base,
        '!uri_brief' => substr($base, strlen('http://')),
        '!mailto' => $mail,
        '!date' => format_date(time()),
        '!login_uri' => url('user', NULL, NULL, TRUE),
        '!edit_uri' => url('user/' . $account->uid . '/edit', NULL, NULL, TRUE),
        '!login_url' => user_pass_reset_url($account),
      );
      $subject = _user_mail_text('welcome_subject', $variables);
      $body = _user_mail_text('welcome_body', $variables);

      // Fix links
      $subject = fb_scrub_urls($subject);
      $body = fb_scrub_urls($body);

      // TODO: make it configurable whether an email is sent, and what it contains.
      $fb->api_client
        ->notifications_sendEmail($fbu, $subject, $body, $body);
    }
  }
  if (!$account->fbu) {

    // This should only happen on older, automatically created accounts.
    $account->fbu = $fbu;
    user_save($account, array(
      'fbu' => $fbu,
    ));
  }
  return $account;
}

/**
 * Given an app and facebook user id, return the corresponding local user.
 */
function fb_user_get_local_user($fbu, $fb_app) {

  // TODO: this query probably needs to search for one authname or the other, not both.
  $result = db_query("SELECT am.* FROM authmap am WHERE am.authname='%s' OR am.authname='%s' ORDER BY am.authname", "{$fbu}-{$fb_app->apikey}@facebook.com", "{$fbu}@facebook.com");
  if ($data = db_fetch_object($result)) {
    $account = user_load(array(
      'uid' => $data->uid,
    ));
    return $account;
  }
}

/**
 * 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.
 */
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)) {

    // Should this query be limited to users of the app?
    $query = "SELECT uid FROM {fb_user_app} WHERE fbu in (%s)";
    $args[] = implode(',', $fbus);
    $result = db_query($query, $args);
    while ($data = db_fetch_object($result)) {
      $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().
 */

// TODO: change this to use the newly added uid column in fb_user_app table.
function _fb_user_get_fbu($uid, $fb_app) {
  static $cache = array();
  if (!$cache[$uid]) {
    $cache[$uid] = array();

    // Look up this user in the authmap
    $result = db_query("SELECT * FROM {authmap} WHERE uid=%d AND authname LIKE '%@facebook.com'", $uid);
    while (!$fbu && ($data = db_fetch_object($result))) {

      // get the part before the '@'
      $substr = substr($data->authname, 0, strpos($data->authname, '@'));

      // then split at the '-'
      $parts = explode('-', $substr);
      if ($parts[1]) {

        // $parts[1] is app id
        $cache[$uid][$parts[1]] = $parts[0];
      }
      else {
        $cache[$uid]['global'] = $parts[0];
      }
    }
  }

  // Return either the global or the app-specific mapping, depending on the app configuration.
  $fb_app_data = fb_app_get_data($fb_app);
  $fb_user_data = $fb_app_data['fb_user'];

  // our configuration
  if ($fb_user_data['unique_account']) {

    // Return the app-specific mapping
    return $cache[$uid][$fb_app->apikey];
  }
  else {

    // Return the global mapping
    return $cache[$uid]['global'];
  }
}
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) {
  if ($type == 'fb_app' && $object) {
    $fb_app = $object;
    global $user;
    $fbu = _fb_user_get_fbu($user->uid, $fb_app);
    if ($fbu) {
      $values['fb-app-user-fbu'] = $fbu;
      $values['fb-app-user-name'] = 'TODO XXX';
      $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;
}

Functions

Namesort descending Description
fb_user_create_local_user Creates a local Drupal account for the specified facebook user id.
fb_user_fb Implementation of hook_fb.
fb_user_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_map_url To help map local accounts to facebook accounts, we can bounce a logged in user to a canvas page. That page will ensure they have added the application, then bounce them back to a local HTML page. When they are bounced back, a cached key allows us…
fb_user_menu
fb_user_nodeapi
fb_user_post_add_cb The post-add page is where the user is sent after adding the application.
fb_user_post_add_check This method will redirect a user if the post-add page need not be displayed.
fb_user_post_add_form A form which helps a facebook user either register a new local drupal account or synchronize facebook account with an existing account. Really, all we do is redirect the user to the login or registration forms.
fb_user_post_add_form_submit
fb_user_post_remove_cb The post-remove page is visited by Facebook after a user removes the application. The user never visits the page, it is simply called by Facebook to notify us of the change.
fb_user_sync_cb The sync callback is invoked first on a canvas page, in which case we require the user to add the application. Later the user will be redirected to this callback on the locale server, with an token that allows us to write the necessary row to the…
fb_user_token_list
fb_user_token_values
fb_user_user Implementation of hook_user.
theme_fb_app_name_with_links
_fb_user_get_authmap Helper function to create an authname for the authmap table.
_fb_user_get_fbu
_fb_user_special_page
_fb_user_sync_redirect
_fb_user_track

Constants