You are here

fb_user.module in Drupal for Facebook 5.2

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_CREATE_NEVER', 1);
define('FB_USER_OPTION_CREATE_LOGIN', 2);
define('FB_USER_OPTION_MAP_NEVER', 1);
define('FB_USER_OPTION_MAP_ALWAYS', 2);
function fb_user_perm() {
  return array(
    'edit own extended permissions',
    'delete own fb_user authmap',
  );
}

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

  // TODO: hopefully this can be simplified.
  return arg(0) == 'user' && arg(1) == 'login' || arg(0) == 'user' && arg(1) == 'register' || arg(0) == 'fb_user' || arg(0) == 'fb' && arg(1) == 'form_cache' || arg(0) == 'fb_app' && arg(1) == 'event' || arg(0) == 'fb_connect' && arg(2) == 'receiver';
}

/**
 * Keep track of when the user has visited the app, and whether they've
 * authorized the app or not.
 */
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.
  // 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']) {

    // Should we even keep data for users who have not added???
    $result = db_query("UPDATE {fb_user_app} SET time_access=%d, session_key='%s', session_key_expires=%d, uid=NULL WHERE apikey='%s' AND fbu=%d", time(), $fb->api_client->session_key, $fb->session_expires, $fb_app->apikey, fb_facebook_user($fb));
  }
  else {

    // Uid is accurate.
    $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(), $fb->api_client->session_key, $fb->session_expires, $user->uid, $fb_app->apikey, fb_facebook_user($fb));
  }
  if ($result === FALSE) {
    watchdog('fb_user', t("Failed to update fb_user_app table."), WATCHDOG_ERROR);
  }
}

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

  // our configuration
  if ($op == FB_OP_INITIALIZE) {

    // Here we ask facebook to prompt the user to authorize the app.
    if (fb_user_authentication_is_required($fb_app)) {
      $fb
        ->require_login();
    }
  }
  else {
    if ($op == FB_OP_POST_INIT) {

      // Observe special rules for canvas page users without local accounts
      if (!$user->uid && !_fb_user_special_page() && !$_REQUEST['form_id']) {
        if ($fbu = fb_facebook_user($fb) && fb_api_check_session($fb) && !$fb->api_client
          ->users_isAppUser()) {
          $uid = $fb_app_data['fb_user']['logged_in_uid'];
        }
        else {
          $uid = $fb_app_data['fb_user']['not_logged_in_uid'];
        }
        if ($uid) {
          dpm("XXX setting global user in fb_user_fb!");
          $user = user_load(array(
            'uid' => $uid,
          ));
        }
      }

      // TODO: is it now necessary to redirect the user back to current page?
    }
    else {
      if ($op == FB_OP_APP_IS_AUTHORIZED) {

        // We reach this point if the user has authorized the app.
        // Could be they visited a canvas page,
        // or could be they clicked a fbConnect link.
        $fbu = $data['fbu'];

        // The user id on facebook.
        // Remember the original uid
        $original_uid = $user->uid;
        if ($user->fbu != $fbu && fb_api_check_session($fb)) {

          // 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;
            if ($user->uid == 0) {
              $user = $account;
            }
            else {
              watchdog('fb_user', t('fb_user encountered something it cannot handle.  The current user, !user1 is logged in.  But the facebook user id %fbu is associated with another user, !user2', array(
                '!user1' => theme('username', $user),
                '!user2' => theme('username', $account),
                '%fbu' => $fbu,
              )), WATCHDOG_ERROR);
            }
          }
          else {

            // There is no account associated with the facebook id.
            // If the user is not anonymous, we can make an association now.
            if ($user->uid != 0 && $user->uid != $fb_app_data['fb_user']['logged_in_uid']) {
              if ($fb_user_data['map_account'] == FB_USER_OPTION_MAP_ALWAYS) {
                list($module, $authname) = _fb_user_get_authmap($fb_app, $fbu);
                user_set_authmaps($user, array(
                  $module => $authname,
                ));

                // is this right?
                $user->fbu = $fbu;

                //dpm("setting up authmap to associate " . theme('username', $user) . " with fbu $fbu");
                watchdog('fb_user', t('Using authmap to associate user !user with facebook user id %fbu.', array(
                  '!user' => theme('username', $user),
                  '%fbu' => $fbu,
                )));
              }
            }
          }
        }

        // Check if the local account needs to be made.
        if ($user->fbu != fb_facebook_user($fb) && !_fb_user_special_page() && fb_api_check_session($fb)) {
          if ($fb_user_data['create_account'] == FB_USER_OPTION_CREATE_ADD && $fb->api_client
            ->users_isAppUser() || $fb_user_data['create_account'] == FB_USER_OPTION_CREATE_LOGIN) {

            // We need to make a local account for this facebook user.
            $user = fb_user_create_local_user($fb, $fb_app, fb_facebook_user($fb), 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 && !(arg(0) == 'fb_app' && arg(1) == 'event')) {

          // We've changed the user.  In order to ensure that drupal handles
          // permissions properly, the user must make the request all over
          // again.  Skip this for the profile tab, as facebook does not allow
          // redirects (or persistent session) there.
          if (function_exists('fb_canvas_is_fbml') && (fb_canvas_is_fbml() || fb_canvas_is_iframe()) && !$_REQUEST['fb_sig_in_profile_tab']) {

            // 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);
          }
          else {
            if (!fb_settings(FB_SETTINGS_APP_NID)) {

              // Facebook connect pages (not callback or receiver)
              $path = $_REQUEST['q'];
              if (fb_verbose()) {
                watchdog('fb_debug', "User uid is now {$user->uid} (was {$original_uid}), redirecting to {$path} to ensure permissions are correct.");
              }

              // debug
              drupal_goto($path);
            }
          }
        }

        // Keep a record of when user accesses app.
        _fb_user_track($fb, $fb_app, $user);

        // 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.

        /** I _think_ the new api no longer requires this.  TODO: confirm.
            if (strpos($_GET['q'],'user/login') === 0) {
              // Have to check idAppUser in case of iframe.
              if (!$fb->api_client->users_isAppUser())
                $fb->require_login();
            }
            else if (strpos($_GET['q'],'user/register') === 0) {
              if (!$fb->api_client->users_isAppUser())
                $fb->require_login();
            }
            */

        // 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 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,
              );
            }
          }
          else {
            if ($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.
                db_query("REPLACE INTO {fb_user_app} (apikey, fbu, added, session_key, session_key_expires) VALUES ('%s', %d, 1, '%s', %d)", $fb_app->apikey, fb_facebook_user($fb), $fb->api_client->session_key, $fb->session_expires);
              }
              else {
                if ($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));
                }
              }
            }
          }
        }
      }
    }
  }
}

/**
 * Determines whether authentication is required to serve the current
 * page.  This will return true if the application has been configured
 * to require login for all canvas pages.  Except in special cases,
 * such as profile tabs.
 *
 * @return TRUE only for canvas pages if authentication is required to
 * use the application and the page is not a special exception to the
 * rule.
 */
function fb_user_authentication_is_required($fb_app) {
  $fb_app_data = fb_app_get_data($fb_app);
  $fb_user_data = $fb_app_data['fb_user'];

  // our configuration
  if ($fb_user_data['require_login'] == FB_USER_OPTION_REQUIRE_LOGIN) {

    // The application is configured to require login on all canvas pages.
    // However, there are exceptions.
    if (isset($_REQUEST['fb_sig_in_profile_tab']) && $_REQUEST['fb_sig_in_profile_tab']) {

      // Redirects are not allowed for the profile tab.
      return FALSE;
    }

    // There may be other exceptions, for example some ajax callbacks.  Potential todo item.
    // No exceptions apply, authentication is required.
    return TRUE;
  }
}
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('If some of your canvas pages are visible to the public at large, allow anyone.  You will have to call Facebook\'s require_login() on pages that require more information about the user.<br />Allow only logged-in users if you want Drupal for Facebook to call require_login() on every page.'),
      '#options' => array(
        FB_USER_OPTION_ALLOW_ANON => t('Allow anyone'),
        FB_USER_OPTION_REQUIRE_LOGIN => t('Allow logged-in users'),
      ),
      '#default_value' => isset($fb_user_data['require_login']) ? $fb_user_data['require_login'] : FB_USER_OPTION_ALLOW_ANON,
      '#required' => TRUE,
    );
    $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 create an entry in the authmap table.  This happens whenever the user visits a canvas page, except pages whose path starts with "user/" and the landing page for anonymous users.  Choose never to use Drupal\'s built in user registration.'),
      '#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'),
      ),
      '#default_value' => isset($fb_user_data['create_account']) ? $fb_user_data['create_account'] : FB_USER_OPTION_CREATE_LOGIN,
      '#required' => TRUE,
    );
    $form['fb_app_data']['fb_user']['map_account'] = array(
      '#type' => 'radios',
      '#title' => t('Map Accounts'),
      '#description' => t('Mapping an account means creating an entry in the authmap table.  This entry allows Drupal to know which Facebook id corresponds to which local uid.'),
      '#options' => array(
        FB_USER_OPTION_MAP_NEVER => t('Never map accounts'),
        FB_USER_OPTION_MAP_ALWAYS => t('Map account when both local uid and Facebook id are known'),
      ),
      '#default_value' => isset($fb_user_data['map_account']) ? $fb_user_data['map_account'] : FB_USER_OPTION_MAP_ALWAYS,
      '#required' => TRUE,
    );
    $form['fb_app_data']['fb_user']['unique_account'] = array(
      '#type' => 'checkbox',
      '#title' => t('Make Account Mapping Unique (experimental, not recommended)'),
      '#description' => t('If checked, the relationship between the local uid and the Facebook id applies only to this Application.  This matters only when you host more than one application on this instance of Drupal.'),
      '#default_value' => $fb_user_data['unique_account'],
    );

    // 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 the local uid is known and the user has logged in (in the Facebook sense) to the app, the user will be granted this role.'),
      '#default_value' => $fb_user_data['new_user_rid'],
    );

    // Experimental.  May be removed or drastically changed anytime
    // 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 (recommended - this feature is experimental and likely to disappear), or create a dedicated account for this purpose.'),
      '#default_value' => $fb_user_data['logged_in_uid'],
    );
  }
  else {
    if ($form_id == 'user_edit' && ($app = $form['#fb_app'])) {

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

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

  // 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 ($GLOBALS['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') {
    if (user_access('administer users') || user_access('delete own fb_user authmap') && $user->uid == $account->uid) {

      // A tab for administrators
      $items[] = array(
        'name' => 'fb_user',
        'title' => t('Facebook Applications'),
        'weight' => 1,
      );
    }
    if (user_access('edit own extended permissions') && $user->uid == $account->uid) {

      // A tab for each application the user has authorized
      $result = _fb_app_query_all();
      $apps = array();
      while ($fb_app = db_fetch_object($result)) {
        $apps[$fb_app->label] = $fb_app;
        $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',
          ));
          if ($info[$fbu][0]['is_app_user']) {
            $items[] = array(
              'name' => $fb_app->label,
              'title' => $fb_app->title,
              'weight' => 2,
            );
          }
        }
      }
    }
    return $items;
  }
  else {
    if ($op == 'form' && $category == 'fb_user') {
      if (!user_access('administer users') && !(user_access('delete own fb_user authmap') && $user->uid == $account->uid)) {
        return;
      }

      // hide from this 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 or active fbconnect session.  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) {

            // TODO: give a user a way to map their facebook account to their local account.
          }
          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 == 'form' && ($fb_app = $apps[$category])) {
        if (user_access('edit own extended permissions') && $GLOBALS['user']->uid == $account->uid) {

          // Application-specific settings
          $form['#fb_app'] = $fb_app;

          // used in hook_form_alter.
          if (function_exists('fb_canvas_is_fbml') && fb_canvas_is_fbml()) {
            $sections = array(
              'profile',
              'info',
            );
            foreach ($sections as $section) {
              $form[$section] = array(
                '#type' => 'markup',
                '#value' => '<fb:add-section-button section="' . $section . '" />',
              );
            }
          }

          // http://wiki.developers.facebook.com/index.php/Extended_permissions
          $permissions = array(
            'email' => 'Allow %application to send you email',
            'offline_access' => 'Grant %application access to your Facebook profile.',
            'status_update' => 'Allow %application to set your status.',
            'photo_upload' => 'Allow %application to upload photos.',
            'create_listing' => 'Allow %application to create marketplace listings on your behalf.',
            'create_event' => 'Allow %application to create events on your behalf.',
            'rsvp_event' => 'Allow %application to RSVP to events on your behalf',
            'sms' => 'Allow %application to send you SMS text messages.',
          );
          foreach ($permissions as $key => $t) {
            if (function_exists('fb_canvas_is_fbml') && fb_canvas_is_fbml()) {
              $form[$key] = array(
                '#type' => 'markup',
                '#value' => '<fb:prompt-permission perms="' . $key . '">' . t($t, array(
                  '%application' => $fb_app->title,
                )) . '<br /></fb:prompt-permission>',
              );
            }
            else {

              // Non-fbml page
              // TODO: use API to hide permissions we already have
              $url = url($_GET['q'], NULL, NULL, TRUE);
              $form[$key] = array(
                '#type' => 'markup',
                '#value' => l(t($t, array(
                  '%application' => $fb_app->title,
                )), "http://www.facebook.com/authorize.php", array(), "api_key={$fb_app->api_key}&v=1.0&ext_perm={$key}&next={$url}&next_cancel={$url}", NULL, TRUE, TRUE),
                '#prefix' => '<p>',
                '#suffix' => '</p>',
              );
            }
          }

          // Add buttons for boxes and info
          $sections = array(
            'profile',
            'info',
          );
          foreach ($sections as $section) {
            if (function_exists('fb_canvas_is_fbml') && fb_canvas_is_fbml()) {
              $form[$section] = array(
                '#type' => 'markup',
                '#value' => '<fb:add-section-button section="' . $section . '" />',
              );
            }

            // No way to add these to a non-canvas page at the moment
          }
          $form['description'] = array(
            '#type' => 'markup',
            '#value' => l(t('All settings for %application (and other Facebook Applications).', array(
              '%application' => $fb_app->title,
            )), 'http://www.facebook.com/editapps.php', array(), NULL, NULL, FALSE, TRUE),
            '#prefix' => '<p>',
            '#suffix' => "</p>\n",
          );
          return $form;
        }
      }
      else {
        if ($op == 'update' && $category == 'fb_user') {

          //dpm($edit, "fb_user_user($op)");
          if (is_array($edit['map'])) {
            foreach ($edit['map'] as $module => $authname) {
              user_set_authmaps($account, array(
                $module => $authname,
              ));
            }
          }
        }
        else {
          if ($op == 'delete') {
            db_query("DELETE FROM {fb_user_app} WHERE uid=%d", $account->uid);
          }
        }
      }
    }
  }
}

/**
 * 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)");
  list($module, $authname) = _fb_user_get_authmap($fb_app, $fbu);
  $account = user_external_load($authname);
  if (!$account) {

    // Create a new user in our system
    // Default username is authname, usernames must be unique.
    $config['username'] = $authname;

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

    // 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' => $authname,
    );

    // 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' => theme('username', $account),
      '%email' => '<' . $account->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));
    fb_invoke(FB_OP_POST_USER, array(
      'account' => $account,
      'fb_app' => $fb_app,
      'fb' => $fb,
    ));

    // 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 function should probably use user_external_load, rather than query the database directly.  See deprecated fb_user_load for example.
  // TODO: this query probably needs to search for one authname or the other, not both.
  // Alternately, use the fb_user_app table rather than authmap to look up this information.
  $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)) {
      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().
 */

// 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_authentication_is_required Determines whether authentication is required to serve the current page. This will return true if the application has been configured to require login for all canvas pages. Except in special cases, such as profile tabs.
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_perm
fb_user_token_list
fb_user_token_values
fb_user_user Implementation of hook_user.
_fb_user_get_authmap Helper function to create an authname for the authmap table.
_fb_user_get_fbu
_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.
_fb_user_track Keep track of when the user has visited the app, and whether they've authorized the app or not.

Constants