You are here

fb.module in Drupal for Facebook 7.3

Same filename and directory in other branches
  1. 5.2 fb.module
  2. 5 fb.module
  3. 6.3 fb.module
  4. 6.2 fb.module
  5. 7.4 fb.module

This is the core required module of Drupal for Facebook.

File

fb.module
View source
<?php

/**
 * @file
 * This is the core required module of Drupal for Facebook.
 *
 * @see http://drupal.org/project/fb
 *
 */

// hook_fb
define('FB_HOOK', 'fb');

// Paths.
define('FB_PATH_ADMIN', 'admin/structure/fb');
define('FB_PATH_ADMIN_ARGS', 3);

// how many args in path.
define('FB_PATH_ADMIN_APPS', 'admin/structure/fb/app');
define('FB_PATH_ADMIN_APPS_ARGS', 4);
define('FB_PATH_AJAX_EVENT', 'fb/ajax');
define('FB_PATH_AJAX_EVENT_ARGS', 2);
define('FB_FACEBOOK_BASE_URL', '//www.facebook.com');

// permissions
define('FB_PERM_ADMINISTER', 'administer fb apps');

// Ops for hook_fb.
define('FB_OP_GET_APP', 'get_app');

// Load data from a known app
define('FB_OP_GET_ALL_APPS', 'get_all_apps');

// Load data about all apps
define('FB_OP_CURRENT_APP', 'current_app');

// determine active app in canvas page or facebook connect
define('FB_OP_INITIALIZE', 'init');

//
define('FB_OP_POST_INIT', 'post init');

//
define('FB_OP_EXIT', 'exit');

// End an FB callback. DEPRECATED
define('FB_OP_GET_FBU', 'get_fbu');

// Query the local user's FB account
define('FB_OP_GET_UID', 'get_uid');

// Query the facebook user's local account
define('FB_OP_GET_USER_SESSION', 'get_user_sess');
define('FB_OP_APP_IS_AUTHORIZED', 'app_authorized');

// Invoked if user has authorized an app. Triggers creation of user accounts or fb_user entries
define('FB_OP_JS', 'fb_op_js');

// A chance to inject javascript onto the page.
define('FB_OP_AJAX_EVENT', 'fb_op_ajax');

// Notification of an event via ajax.
// Variables and $conf[] keys.
define('FB_VAR_LANGUAGE_OVERRIDE', 'fb_language_override');
define('FB_VAR_JS_SDK', 'fb_js_sdk');
define('FB_VAR_API_FILE', 'fb_api_file');
define('FB_VAR_JS_CHANNEL', 'fb_js_channel');
define('FB_VAR_VERBOSE', 'fb_verbose');
define('FB_VAR_APIKEY', 'fb_apikey');

// Deprecated.  Use FB_VAR_ID
define('FB_VAR_ID', 'fb_id');
define('FB_VAR_USE_COOKIE', 'fb_use_cookie');
define('FB_VAR_USE_SESSION', 'fb_use_session');
define('FB_VAR_JS_USE_SESSION', 'fb_js_session_token');

// Initialize JS with token from session.
define('FB_VAR_JS_GET_LOGIN_STATUS', 'fb_js_get_login_status');

// call FB.getLoginStatus during js init?
define('FB_VAR_JS_TEST_LOGIN_STATUS', 'fb_js_test_login_status');
define('FB_VAR_JS_OAUTH', 'fb_js_oauth');

// Whether to pass oauth: true to FB.init().
define('FB_VAR_CURL_NOVERIFY', 'fb_curl_noverify');
define('FB_VAR_SECURE_URLS', 'fb_secure_urls');
define('FB_VAR_ALTER_USERNAME', 'fb_format_username');
define('FB_VAR_ALTER_USERNAME_AND_CACHE', 'fb_cache_username');

// Possible choices for secure urls.
define('FB_SECURE_URLS_ALWAYS', 1);
define('FB_SECURE_URLS_SOMETIMES', 0);
define('FB_SECURE_URLS_NEVER', -1);

// Possible choices for formatting username.
define('FB_ALTER_USERNAME_ALWAYS', 'always');
define('FB_ALTER_USERNAME_NEVER', 'never');
define('FB_ALTER_USERNAME_NOT_THEMING', 'when_not_theming');

// Possible choices for caching the formated username
define('FB_ALTER_USERNAME_AND_CACHE', 'on');
define('FB_ALTER_USERNAME_DONT_CACHE', 'off');

// node_access realms (belongs here?)
define('FB_GRANT_REALM_FRIEND', 'fb_friend');
define('FB_GRANT_REALM_GROUP', 'fb_group');

// NOTE: on Connect Pages, using anything other than FB_FBU_CURRENT will cause cookies to be set which cause problems on subsequent pages. So only use something other than FB_FBU_CURRENT if you absolutely must!
// @TODO - new libs, are these FBU values still needed???
define('FB_FBU_CURRENT', 'fbu_current');

// Canvas pages and Connect pages
define('FB_FBU_ANY', 'fbu_any');

// Use current user on canvas page, fall back to infinite session otherwise.

//// Constants for internal use
define('FB_APP_CURRENT', '000_app_current');

// Canvas pages only. 000 makes it appear first in options list

/**
 * Controls are one way to customize the behavior of Drupal for Facebook modules.
 *
 * Controls are stored as an array of flags. Each flag overrides a
 * configurable or built-in behavior. Third-party modules can use this to
 * provide exceptions to otherwise useful behavior. For example see
 * fb_user.module, where this is used to suppress some behavior in off-line
 * mode.
 *
 * Controls take effect not just for the current page request, but also for
 * ajax callbacks generated by the subsequent page.
 *
 * Because ajax controls could be spoofed by a malicious client, flags should
 * not enable any "risky" features. For example, fb_user.module provides a
 * control to suppress the creation of account, but not a control to enable
 * new accounts, as that would be a security risk.
 *
 */
function fb_controls($control = NULL, $value = NULL) {
  $controls =& drupal_static(__FUNCTION__);
  if (!isset($controls)) {

    // Initialize.
    if (isset($_REQUEST['fb_controls'])) {

      // Comma separated list passed to ajax calls.
      foreach (explode(',', $_REQUEST['fb_controls']) as $key) {
        $controls[$key] = TRUE;
      }
    }
    else {
      $controls = array();
    }

    // @TODO - would a drupal_alter() be useful here?
  }
  if (isset($control)) {
    if ($value === FALSE) {
      unset($controls[$control]);
      return;
    }
    elseif ($value === TRUE) {
      $controls[$control] = TRUE;
    }
    return isset($controls[$control]) ? $controls[$control] : FALSE;

    // Return requested control.
  }
  return array_keys($controls);

  // Return all controls.
}

/**
 * Implements hook_custom_theme().
 *
 * hook_custom_theme() is the new hook_init().  It is called before
 * hook_init(), and therefore the first opportunity that a module has to
 * act.  Although this module is not interested in setting a custom theme,
 * fb_canvas and fb_tab might, so we must initialize some data here.
 *
 * This function is also called before node_access hooks.  So the more we can
 * initialize here, the better.  That's why we initialize both $_fb_app and $_fb.
 *
 * @see fb_canvas_custom_theme().
 * @see fb_init().
 */
function fb_custom_theme() {

  // The code here is conceptually part of fb_init().
  global $_fb_app;
  global $_fb;

  // fb_settings.inc may have been included in settings.php. If not, include it now.
  if (!function_exists('fb_settings')) {
    module_load_include('inc', 'fb', 'fb_settings');

    // trigger test in fb_devel.module
    $GLOBALS['fb_init_no_settings'] = TRUE;
  }

  // Figure out which app the current request is for.
  $_fb_app = fb_invoke(FB_OP_CURRENT_APP);
  if ($_fb_app) {

    // Initialize the PHP API.
    $_fb = fb_api_init($_fb_app);
  }
}

/**
 * Implements hook_init().
 *
 * Initializes facebook's javascript.
 * Determines whether we are servicing a Facebook App request.
 *
 * We invoke our hook, first to determine which application is being invoked
 * (because we support more than one in the same Drupal instance). We invoke
 * the hook again to let interested modules know the sdk is initialized.
 *
 */
function fb_init() {

  // Globals provided for internal use and convenience to third-parties.
  global $_fb;
  global $_fb_app;

  // When the site is in maintenance mode, hook_custom_theme won't be invoked so
  // we call our implementation manually.
  if (!function_exists('fb_settings')) {
    fb_custom_theme();
  }

  // Javascript settings needed by fb.js.
  fb_js_settings('base_url', trim(url('', array(
    'absolute' => TRUE,
  )), '/'));
  fb_js_settings('ajax_event_url', url(FB_PATH_AJAX_EVENT, array(
    'absolute' => TRUE,
  )));
  fb_js_settings('is_anonymous', !user_is_logged_in());

  // Data structure to pass to FB.init();
  $fb_init_settings = array(
    'xfbml' => FALSE,
    'status' => FALSE,
    // We will call getLoginStatus() instead.
    'oauth' => variable_get(FB_VAR_JS_OAUTH, TRUE),
  );
  if (variable_get(FB_VAR_USE_COOKIE, TRUE)) {
    $fb_init_settings['cookie'] = TRUE;
  }
  if ($_fb_app) {

    // An App is configured.
    // Javascript settings needed by fb.js and/or other modules.
    fb_js_settings('label', $_fb_app->label);
    fb_js_settings('namespace', $_fb_app->canvas);
    fb_js_settings('page_type', fb_settings(FB_SETTINGS_TYPE));

    // canvas or connect.
    // Add perms to settings, for calling FB.login().
    $perms = array();
    drupal_alter('fb_required_perms', $perms);
    fb_js_settings('perms', implode(',', $perms));

    //$fb_init_settings['apiKey'] = $_fb_app->apikey;
    $fb_init_settings['appId'] = $_fb_app->id;
    if ($_fb) {

      // @TODO: test if this is still true: Sometimes when canvas page is open in one tab, and user logs out of
      // facebook in another, the canvas page has bogus session info when
      // refreshed.  Here we attempt to detect and cleanup.
      // Give other modules a chance to initialize.
      fb_invoke(FB_OP_INITIALIZE, array(
        'fb_app' => $_fb_app,
        'fb' => $_fb,
      ));

      // See if the facebook user id is known
      if ($fbu = $_fb
        ->getUser()) {
        fb_invoke(FB_OP_APP_IS_AUTHORIZED, array(
          'fb_app' => $_fb_app,
          'fb' => $_fb,
          'fbu' => $fbu,
        ));
        fb_js_settings('fbu', $fbu);
      }
      if (!empty($_REQUEST['fb_reload'])) {

        // Tell javascript not to reload indefinately.
        fb_js_settings('fb_reloading', $_REQUEST['fb_reload']);
      }

      // When not using cookies, or third-party cookies disabled, we can pass all the auth details in javascript settings.
      if (variable_get(FB_VAR_JS_USE_SESSION, TRUE)) {
        if (!empty($_REQUEST['fb_login_status'])) {

          // Remember for duration of session whether test failed.
          $_SESSION['fb_get_login_status_test'] = $_REQUEST['fb_login_status'];
        }
        if ($fb_uid = $_fb
          ->getUser()) {
          if (!empty($_SESSION['fb_get_login_status_test']) && $_SESSION['fb_get_login_status_test'] == 'false') {
            $fb_token = $_fb
              ->getAccessToken();

            // This uses a data structure not documented by facebook.  May not continue to work.
            $js_sr = array(
              'accessToken' => $fb_token,
              'userID' => $fb_uid,
            );
            $fb_init_settings['authResponse'] = (object) $js_sr;
          }
        }
        else {
          fb_js_settings('get_login_status_test', TRUE);
        }
      }
    }
    else {
      unset($_fb_app);
      watchdog('fb', "Could not initialize Facebook API.", array(), WATCHDOG_ERROR);
    }
    if (isset($_REQUEST['destination'])) {
      $destination = $_REQUEST['destination'];
    }
    elseif (current_path()) {
      $destination = current_path();
    }
    else {
      $destination = '<front>';
    }
    if (fb_is_canvas()) {
      $destination = fb_scrub_urls($destination);

      // Needed?
    }

    //Stripping the fragment out to be tacked on during the javascript redirect
    if (strpos($destination, '#') !== FALSE) {
      list($destination, $fragment) = explode('#', $destination, 2);
      fb_js_settings('reload_url_fragment', $fragment);
    }
    fb_js_settings('reload_url', url($destination, array(
      'absolute' => TRUE,
      'fb_canvas' => fb_is_canvas(),
      'language' => (object) array(
        'prefix' => NULL,
        'language' => NULL,
      ),
    )));
  }
  if ($channel = variable_get(FB_VAR_JS_CHANNEL, TRUE)) {
    if (!is_string($channel)) {
      $channel = url('fb/channel', array(
        'absolute' => TRUE,
        'fb_url_alter' => FALSE,
      ));
    }
    $fb_init_settings['channelUrl'] = $channel;
  }
  fb_js_settings('fb_init_settings', $fb_init_settings);

  // Allow third-parties to act, even if we did not initialize $_fb.
  fb_invoke(FB_OP_POST_INIT, array(
    'fb_app' => $_fb_app,
    'fb' => $_fb,
  ));
  fb_js_settings('test_login_status', variable_get(FB_VAR_JS_TEST_LOGIN_STATUS, TRUE));
  fb_js_settings('get_login_status', variable_get(FB_VAR_JS_GET_LOGIN_STATUS, TRUE));
  fb_js_settings('controls', implode(',', fb_controls()));
  if (!fb_js_settings('js_sdk_url')) {
    if (isset($_SESSION['fb_locale']) && variable_get(FB_VAR_LANGUAGE_OVERRIDE, 'override')) {

      // @TODO - get locale from signed request.  It appears to contain it now.
      $fb_lang = $_SESSION['fb_locale'];
    }
    else {
      global $language;
      $fb_lang = variable_get('fb_language_' . $language->language, 'en_US');
    }
    $js_sdk = fb_protocol() . "://connect.facebook.net/{$fb_lang}/all.js";
    fb_js_settings('js_sdk_url', variable_get(FB_VAR_JS_SDK, $js_sdk));
  }

  // Add our module's javascript.
  drupal_add_js(drupal_get_path('module', 'fb') . '/fb.js', array(
    'type' => 'file',
    'scope' => 'header',
    'group' => JS_LIBRARY,
  ));

  // See also fb_page_alter(), where we initialize facebook's SDK.
}

/**
 * Implements hook_tokens().
 *
 * Nothing to do with facebook access tokens.  This drupal hook supports
 * token replacement via token_replace().
 */
function fb_tokens($type, $tokens, array $data = array(), array $options = array()) {
  if (strpos($type, 'fb') === 0) {
    $items = array();

    // Add defaults to $data array.
    $data = $data + fb_vars();
    if (!isset($data['fb_signed_request']) && !empty($data['fb'])) {
      $data['fb_signed_request'] = $data['fb']
        ->getSignedRequest();
    }
    if (!isset($data['fb_settings']) && function_exists('fb_settings')) {
      $data['fb_settings'] = fb_settings();
    }
    if (is_array($data[$type])) {
      foreach ($data[$type] as $key => $value) {
        if (!is_array($value)) {
          $items["[{$type}:{$key}]"] = $value;
        }

        // TODO: support nested values (i.e. recurse into arrays)
      }
    }

    // TODO support additional fb token types.  I.e. fb_app, fb_settings.
    return $items;
  }
}

/**
 * Implements hook_rdf_namespaces().
 * Adds the xmlns:fb attribute to html tag.
 */
function fb_rdf_namespaces() {
  return array(
    // It's unclear from facebook doc which of the URLs below is correct.
    'fb' => 'http://www.facebook.com/2008/fbml',
  );
}

/**
 * Helper to get the configured variables.
 *
 * Adds the javascript setting with the supplied key/value.  This function
 * merely keeps track of the settings and writes them as late as possible.
 * Currently, in the fb_footer() function.  There has been a lot of
 * experimentation as to the best place to initialize the facebook javascript
 * SDK.  The footer appears to be the best place because we may not know all
 * settings until well after hook_init().
 *
 * @param $key
 *   The javascript setting name. If the key is null then nothing is modified and the settings are returned.
 * @param $value
 *   The value of the javascript setting. If the key is not NULL by the value is the setting is removed
 * @return
 *    The associative array containing the current fb javascript settings
 */
function fb_js_settings($key = NULL, $value = NULL) {
  $fb_js_settings =& drupal_static(__FUNCTION__);
  if (isset($key) && isset($value)) {
    $fb_js_settings[$key] = $value;
    return $value;
  }
  elseif (isset($key)) {
    return isset($fb_js_settings[$key]) ? $fb_js_settings[$key] : NULL;
  }
  else {
    return $fb_js_settings;
  }
}

/**
 * Include and initialize Facebook's PHP SDK.
 */
function fb_api_init($fb_app) {
  $cache =& drupal_static(__FUNCTION__);
  if (isset($cache[$fb_app->id])) {
    return $cache[$fb_app->id];
  }

  // Find Facebook's PHP SDK.  Use libraries API if enabled.
  $fb_lib_path = function_exists('libraries_get_path') ? libraries_get_path('facebook-php-sdk') : 'sites/all/libraries/facebook-php-sdk';
  $fb_platform = variable_get(FB_VAR_API_FILE, $fb_lib_path . '/src/facebook.php');
  try {
    if (!class_exists('Facebook') && !@(include $fb_platform)) {
      $message = t('Failed to find the Facebook client libraries at %filename.  Read the !readme and follow the instructions carefully.', array(
        '!drupal_for_facebook' => l(t('Drupal for Facebook'), 'http://drupal.org/project/fb'),
        // This link should work with clean URLs disabled.
        '!readme' => '<a href=' . base_path() . '/' . drupal_get_path('module', 'fb') . '/README.txt>README.txt</a>',
        '%filename' => $fb_platform,
      ));
      drupal_set_message($message, 'error');
      watchdog('fb', $message);
      return NULL;
    }
    if (Facebook::VERSION < "3") {
      $message = 'This version of modules/fb is compatible with Facebook PHP SDK version 3.x.y, but %version was found (%fb_platform).';
      $args = array(
        '%fb_platform' => $fb_platform,
        '%version' => Facebook::VERSION,
      );
      if (user_access('access administration pages')) {
        drupal_set_message(t($message, $args));
      }
      watchdog('fb', $message, $args, WATCHDOG_ERROR);
      return NULL;
    }

    // Hack.  In case third-party cookies disabled, put signed request where facebook's SDK will find it.
    if (variable_get(FB_VAR_USE_SESSION, TRUE) && !isset($_REQUEST['signed_request']) && !isset($_COOKIE['fbsr_' . $fb_app->id]) && isset($_SESSION['_fb_' . $fb_app->id])) {
      $_REQUEST['signed_request'] = $_SESSION['_fb_' . $fb_app->id];
    }

    // We don't have a cached resource for this app, so we're going to create one.
    $fb = new Facebook(array(
      'appId' => $fb_app->id,
      'secret' => isset($fb_app->secret) ? $fb_app->secret : NULL,
      'cookie' => variable_get(FB_VAR_USE_COOKIE, TRUE),
    ));

    // Hack in case third-party cookies disabled, find access token in session.
    // This comes into play when oauth is not used in JS.
    if (variable_get(FB_VAR_USE_SESSION, TRUE) && !isset($_REQUEST['signed_request']) && isset($_SESSION['_fb_token_' . $fb_app->id])) {
      $fb
        ->setAccessToken($_SESSION['_fb_token_' . $fb_app->id]);
    }

    // Some servers need these settings.
    if (variable_get(FB_VAR_CURL_NOVERIFY, TRUE)) {
      Facebook::$CURL_OPTS[CURLOPT_SSL_VERIFYPEER] = FALSE;
      Facebook::$CURL_OPTS[CURLOPT_SSL_VERIFYHOST] = FALSE;

      //Facebook::$CURL_OPTS[CURLOPT_VERBOSE] = 1; // debug
    }

    // Cache the result, in case we're called again.
    $cache[$fb_app->id] = $fb;

    // Tell Drupal not to store the current page in the cache when user is logged into facebook.
    if (!user_is_logged_in() && fb_facebook_user($fb)) {

      //dpm("Disabling cache for connected user.", __FUNCTION__);
      $GLOBALS['conf']['cache'] = 0;

      // CACHE_DISABLED == 0
    }
    return $fb;
  } catch (Exception $e) {
    fb_log_exception($e, t('Failed to construct Facebook client API.'));
  }
}

/**
 * Wrapper function for fb_api_init. This helps for functions that should
 * work whether or not we are on a canvas page. For canvas pages, the active
 * fb object is used. For non-canvas pages, it will initialize the API using
 * an infinite session, if configured.
 *
 * @param $fb_app Note this is ignored on canvas pages.
 *
 * This is for internal use. Third party modules use fb_api_init().
 */
function _fb_api_init($fb_app = NULL) {
  $fb = $GLOBALS['_fb'];

  // Default to active app on canvas pages
  if (!$fb && $fb_app) {

    // Otherwise, log into facebook api.
    $fb = fb_api_init($fb_app, FB_FBU_ANY);
  }
  if (!$fb) {
    watchdog('fb', '%function unable to initialize Facebook API.', array(
      '%function' => '_fb_api_init()',
    ), WATCHDOG_ERROR);
    return;
  }
  else {
    return $fb;
  }
}

/**
 * Helper function to get the most commonly used values.  In your custom
 * module, call extract(fb_vars()); to set $fb_app, $fb, and $fbu.
 */
function fb_vars() {

  // Access callback are called before hook_init, so make sure FB api initialized.
  if ($GLOBALS['_fb_app'] && !$GLOBALS['_fb']) {
    $GLOBALS['_fb'] = fb_api_init($GLOBALS['_fb_app']);
  }
  return array(
    'fb' => $GLOBALS['_fb'],
    'fb_app' => $GLOBALS['_fb_app'],
    'fbu' => fb_facebook_user(),
  );
}

/**
 * Helper function to work with facebook "open" graph.
 */
function fb_graph($path, $params = array(), $method = 'GET', $fb = NULL) {
  if (!$fb) {
    $fb = $GLOBALS['_fb'];
  }
  if ($method == 'GET') {
    $url = url("https://graph.facebook.com/{$path}", array(
      'query' => $params,
    ));
    $http = drupal_http_request($url);
  }
  else {
    $url = "https://graph.facebook.com/{$path}";
    $headers = array();

    //$headers = array('Content-Type' => 'application/x-www-form-urlencoded'); // Needed??
    $query = http_build_query($params, '', '&');
    $http = drupal_http_request($url, array(
      'headers' => $headers,
      'method' => $method,
      'data' => $query,
    ));
  }
  if (isset($http->data)) {
    $data = json_decode($http->data, TRUE);

    // Most times graph returns JSON, but other times query string.  Thanks Facebook!
    if (!$data) {
      parse_str($http->data, $data);
    }
  }
  else {
    $data = array();

    // avoid php warnings.
  }
  if (!isset($http->error) && !empty($data)) {
    if (is_array($data)) {
      if (isset($data['error_code'])) {
        throw new FacebookApiException($data);
      }
    }
    elseif ($http->data == 'true' || $http->code == 200) {

      // No problems.
    }
    else {

      // Never reach this???
      if (function_exists('dpm')) {
        dpm($http, __FUNCTION__ . " unexpected result from {$url}");
      }

      // XXX
    }
    return $data;
  }
  elseif (!empty($data)) {

    // Error has a message.
    // TODO: parse error code from message.
    $message = t('fb_graph failed querying !path.  !type: !detail', array(
      '!path' => $path,
      '!type' => $data['error']['type'],
      '!detail' => $data['error']['message'],
    ));
    throw new Exception($message);

    // Do we need our own code???
  }
  else {
    $data = json_decode($http->data, TRUE);
    $message = t('fb_graph failed querying !path.  !detail', array(
      '!path' => $path,
      '!detail' => $http->error,
    ));
    throw new Exception($message);

    // Do we need our own code???
  }
}

/**
 * Helper to get the tokens needed to accss facebook's API.
 *
 * You would think that facebook's SDK would provide basic functions like this.
 *
 * @param $fb
 * Get the token for this API instance.  If NULL, use the global $_fb.
 *
 * @param $fbu
 * Get the user-specific token.  If NULL, get the application token.
 */
function fb_get_token($fb = NULL, $fbu = NULL) {
  $cache =& drupal_static(__FUNCTION__);
  if (!isset($cache)) {
    $cache = array();
  }
  if (!$fb) {
    $fb = $GLOBALS['_fb'];
  }
  if (!$fb) {
    return;
  }
  $app_id = $fb
    ->getAppId();
  $cache_key = "fb_token_{$app_id}";

  // @TODO if both fb and fbu are NULL, it might be better performance to use the current user's token, if available.
  if (!$fbu) {

    // Get the application token.
    if (empty($cache[$cache_key])) {

      // try Drupal cache
      $cache_obj = cache_get($cache_key);
      if ($cache_obj && $cache_obj->data) {
        $cache[$cache_key] = $cache_obj->data;
      }
    }
    if (empty($cache[$cache_key])) {

      // Query facebook for token.
      // http://developers.facebook.com/docs/authentication/applications/
      $path = "https://graph.facebook.com/oauth/access_token?client_id=" . $app_id . "&client_secret=" . $fb
        ->getApiSecret() . "&grant_type=client_credentials";
      $http = drupal_http_request($path);
      if ($http->code == 200 && isset($http->data)) {
        $data = explode('=', $http->data);
        $token = $data[1];
        if ($token) {
          $cache[$cache_key] = $token;
          $result = cache_set($cache_key, $token, 'cache', CACHE_TEMPORARY);
        }
      }
      if (empty($token)) {
        watchdog('fb', 'Failed to get application (%app_id) access token.', array(
          '%app_id' => $app_id,
        ), WATCHDOG_ERROR);
      }
    }
  }
  else {
    $cache_key .= '_' . $fbu;

    // Get the user access token.
    if ($fbu == 'me' || $fbu == fb_facebook_user($fb)) {
      $cache[$cache_key] = $fb
        ->getAccessToken();
    }
    else {
      $session_data = fb_invoke(FB_OP_GET_USER_SESSION, array(
        'fb' => $fb,
        'fb_app' => fb_get_app(array(
          'id' => $app_id,
        )),
        'fbu' => $fbu,
      ), array());
      if (count($session_data)) {
        $cache[$cache_key] = $session_data['access_token'];
      }
    }
  }
  return isset($cache[$cache_key]) ? $cache[$cache_key] : NULL;
}

/**
 * This helper original written because facebook's $fb->api() function was
 * very buggy.  I'm not sure this is still needed.  On the other hand, a
 * future version of modules/fb might use this instead of faceobok's PHP SDK,
 * eliminating the need for it entirely.
 */
function fb_call_method($fb, $method, $params = array()) {
  if (!isset($params['access_token'])) {
    $params['access_token'] = fb_get_token($fb);
  }
  $params['format'] = 'json-strings';

  // Here's how to create a url that conforms to standards:
  $url = url("https://api.facebook.com/method/{$method}", array(
    'query' => $params,
  ));

  // If facebook gives errors like "Invalid OAuth 2.0 Access Token 190/Unable to get application prop" it might be necessary to uncomment the urldecode below.
  // http://forum.developers.facebook.net/viewtopic.php?id=76228
  // $url = rawurldecode($url);
  $http = drupal_http_request($url);
  if (!isset($http->error) && isset($http->data)) {
    $data = json_decode($http->data, TRUE);

    // Yes, it's double encoded. At least sometimes.
    if (is_string($data)) {
      $data = json_decode($data, TRUE);
    }
    if (is_array($data)) {
      if (isset($data['error_code'])) {
        throw new FacebookApiException($data);
      }
    }
    elseif ($http->data == 'true' || $http->code == 200) {

      // No problems.
    }
    else {

      // Never reach this???
      if (function_exists('dpm')) {
        dpm($http, __FUNCTION__ . " unexpected result from {$url}");
      }

      // XXX
    }
    return $data;
  }
  else {

    // Should we throw FacebookApiException, or plain old exception?
    throw new FacebookApiException(array(
      'error_msg' => t('fb_call_method failed calling !method.  !detail', array(
        '!method' => $method,
        '!detail' => $http->error,
      )),
      'error_code' => $http->code,
    ));
  }
}

/**
 * Helper function for fql queries.
 *
 * Use $params to pass a session_key, when needed.
 */
function fb_fql_query($fb, $query, $params = array()) {
  try {
    $params['query'] = $query;

    //$result = fb_call_method($fb, 'fql.query', $params);
    $params['method'] = 'fql.query';
    $result = $fb
      ->api($params);
    return $result;
  } catch (Exception $e) {
    fb_log_exception($e, t("FQL query failed.  The query was \"%query\".", array(
      '%query' => $query,
    )));
  }
}

/**
 * Helper function for facebook's batch graph api.
 *
 * This function accepts a simpler interface than facebook's.  The queries are
 * passed in as a simple array, and the data is parsed into a PHP data
 * structure.
 *
 * @TODO: when $method=='GET', share caching with fb_api().
 */
function fb_api_batch($fb, $queries, $params, $method = 'GET') {
  $data = array();

  // Build facebook's data structure.  Our's supports only GET or POST at a time.
  $fb_queries = array();
  foreach ($queries as $query) {
    $fb_queries[] = array(
      'method' => $method,
      'relative_url' => $query,
    );
  }
  $wrapped_data = $fb
    ->api('/?batch=' . json_encode($fb_queries), 'POST', $params);

  // Use POST, not $method.
  foreach ($wrapped_data as $w_d) {
    if ($w_d['code'] == 200 && isset($w_d['body'])) {
      $data[] = json_decode($w_d['body'], TRUE);
    }
    else {

      // Unexpected code
      $data[] = $w_d;
    }
  }
  return $data;
}

/**
 * Helper function determines a name from data returned from fb_graph().
 * Almost always, graph data includes a 'name'.  But in rare cases that is
 * not present and we use an id instead.
 */
function _fb_get_name($graph_data) {
  if (!empty($graph_data['name'])) {
    return $graph_data['name'];
  }
  elseif (!empty($graph_data['id'])) {
    return $graph_data['id'];
  }
  else {
    return t('Unknown');
  }
}

/**
 * Implements hook_page_alter().
 * Can alter the $page['page_bottom'] hidden region here.
 */
function fb_page_alter(&$page) {
  global $_fb, $_fb_app;

  // This element recommended by facebook. http://developers.facebook.com/docs/reference/javascript/
  $output = "<div id=\"fb-root\" class=\"fb_module\"></div>\n";
  $settings = fb_js_settings();
  $output .= "<script type=\"text/javascript\">\n";
  $output .= "<!--//--><![CDATA[//><!--\n";

  // Pending javascript that needs to execute after FB is initialized.
  $js_array = fb_invoke(FB_OP_JS, array(
    'fb' => $GLOBALS['_fb'],
    'fb_app' => $GLOBALS['_fb_app'],
  ), array());
  if (count($js_array)) {

    // The function we define in the footer will be called after FB is initialized.
    $output .= "FB_JS.initHandler = function() {\n";

    //$output .= "debugger;\n";
    $output .= implode("\n  ", $js_array);
    $output .= "};\n";
    $output .= "jQuery(document).bind('fb_init', FB_JS.initHandler);\n\n";
  }

  // Our settings.  We add them here, as late during request as possible.
  $output .= "  jQuery.extend(Drupal.settings, " . json_encode(array(
    'fb' => fb_js_settings(),
  )) . ");\n\n";

  // Initialize FB.
  $output .= "if (typeof(FB) == 'undefined') {\n";

  // Load the JS SDK asynchronously.
  // http://developers.facebook.com/docs/reference/javascript/
  $output .= "  var e = document.createElement('script');\n";
  $output .= "  e.async = true;\n";
  $output .= "  e.src = Drupal.settings.fb.js_sdk_url;\n";
  $output .= "  document.getElementById('fb-root').appendChild(e);\n";
  $output .= "}\n\n";
  $output .= "\n//--><!]]>\n";
  $output .= "\n</script>\n";
  $page['page_bottom']['fb'] = array(
    '#type' => 'markup',
    '#markup' => $output,
  );
}

/**
 * Is the current request a canvas page?
 */
function fb_is_canvas() {
  if (fb_is_tab()) {
    return FALSE;
  }
  elseif (fb_settings(FB_SETTINGS_CB)) {

    // Using fb_url_rewrite.
    return TRUE;
  }
  elseif (fb_settings(FB_SETTINGS_TYPE) == FB_SETTINGS_TYPE_CANVAS) {

    // No rewrite, but fb_settings.inc has detected type.
    return TRUE;
  }
  return FALSE;
}

/**
 * Is the current page a profile tab?
 */
function fb_is_tab() {
  if (fb_settings(FB_SETTINGS_TYPE) == FB_SETTINGS_TYPE_PAGE_TAB) {
    return TRUE;
  }
  elseif (fb_settings(FB_SETTINGS_TYPE) == FB_SETTINGS_TYPE_PROFILE) {

    // deprecated FBML tab
    return TRUE;
  }
  elseif (isset($_REQUEST['fb_sig_in_profile_tab']) && $_REQUEST['fb_sig_in_profile_tab']) {

    // deprecated ancient history
    // Old way, no migrations enabled.
    return TRUE;
  }
  return FALSE;
}

/**
 * Does the current user like the current page?
 *
 * Expect this to work only when fb_is_tab() returns TRUE.
 */
function fb_is_page_liked() {
  global $_fb;
  if (!empty($_fb)) {
    $sr = $_fb
      ->getSignedRequest();
    return isset($sr['page']) && $sr['page']['liked'];
  }
}

/**
 * Does the current user administer the current page?
 *
 * Expect this to work only when fb_is_tab() returns TRUE.
 */
function fb_is_page_admin() {
  global $_fb;
  if (!empty($_fb)) {
    $sr = $_fb
      ->getSignedRequest();
    return isset($sr['page']) && $sr['page']['admin'];
  }
}

/**
 * Sometimes calls to fb_api_init succeed, but calls to the client api
 * will fail because cookies are obsolete or what have you. This
 * function makes a call to facebook to test the session. Expensive,
 * so use only when necessary.
 *
 */
function fb_api_check_session($fb) {
  $success = FALSE;
  try {
    $me = $fb
      ->api('me');

    // Store the locale if set.
    if (isset($me['locale'])) {
      $_SESSION['fb_locale'] = $me['locale'];
    }

    // Does not matter what is returned, as long as exception is not thrown.
    $success = TRUE;
  } catch (Exception $e) {
    if (fb_verbose()) {
      watchdog('fb', 'fb_api_check_session failed. Possible attempt to spoof a facebook session!');

      //watchdog('fb', print_r($fb->getSession(), 1));
    }
    $success = FALSE;
    if (fb_verbose()) {
      fb_log_exception($e, t("fb_api_check_session failed."));
    }
    $app_id = $fb
      ->getAppId();
    unset($_SESSION['fb'][$app_id]);
    unset($_SESSION['_fb_' . $app_id]);
    unset($_SESSION['_fb_token_' . $app_id]);

    // Unsetting the javasript fbu can be helpful when third-party cookies disabled.

    //fb_js_settings('fbu', 0); @TODO still needed? helpful?
  }
  return $success;
}

/**
 * Helper to ensure local user is logged out, or an anonymous session is refreshed.
 */
function _fb_logout() {
  if (session_name()) {

    // Avoid PHP warning.
    session_start();

    // Make sure there is a session before destroy.
    session_destroy();
    $GLOBALS['user'] = drupal_anonymous_user();
    drupal_session_initialize();
  }
}

/**
 * Returns the facebook user id currently visiting a canvas page, or if
 * set_user has been called. Unlike fb_get_fbu(), works only on canvas and
 * connect pages, or when infinite session has been initialized.
 */
function fb_facebook_user($fb = NULL) {
  if (!isset($fb)) {
    $fb = $GLOBALS['_fb'];
  }
  if (!$fb) {
    return;
  }
  try {
    $fbu = $fb
      ->getUser();
    return $fbu;
  } catch (FacebookApiException $e) {
    fb_log_exception($e, t('Failed to get Facebook user id. detail: !detail', array(
      '!detail' => print_r($e, 1),
    )));
  }
}

/**
 * Helper function to ensure user has authorized an application.
 *
 * Similar to the old require_login() provided by the old facebook API.
 * Works by redirecting the user as described in http://developers.facebook.com/docs/authentication/.
 *
 * @TODO handle users who skip.
 */
function fb_require_authorization($fb = NULL, $destination = NULL) {
  if (!$fb) {
    $fb = $GLOBALS['_fb'];
  }
  if (!$fb) {
    throw new Exception(t('Failed to authorize facebook application.  Could not determine application id.'));
  }
  $fbu = fb_facebook_user($fb);
  if (!$fbu) {
    $client_id = $fb
      ->getAppId();
    $redirect_uri = $destination ? $destination : url(current_path(), array(
      'absolute' => TRUE,
      'fb_canvas' => fb_is_canvas(),
    ));
    $url = "https://graph.facebook.com/oauth/authorize?client_id={$client_id}&redirect_uri={$redirect_uri}";

    // Which permissions to prompt for?
    $perms = array();
    drupal_alter('fb_required_perms', $perms);
    if (count($perms)) {
      $url .= '&scope=' . implode(',', $perms);
    }
    if (fb_is_canvas() || fb_is_tab()) {
      fb_iframe_redirect($url);
    }
    else {
      header('Location: ' . $url);

      // drupal_goto is for internal redirects only.
    }
  }
  else {
    return $fbu;
  }
}

/**
 * Helper tells other modules when to load admin hooks.
 */
function fb_is_fb_admin_page() {
  if (arg(0) == 'admin' && (arg(1) == 'fb' || arg(2) == 'fb')) {

    // Keep consistant titles across tabs served by multiple modules.
    if ($label = arg(FB_PATH_ADMIN_APPS_ARGS)) {
      drupal_set_title($label);
    }
    else {
      drupal_set_title(t('Drupal for Facebook'));
    }
    return TRUE;
  }
}

/**
 * Given a facebook user id, learn the local uid, if any.
 *
 */
function fb_get_uid($fbu, $fb_app = NULL) {
  $uid = NULL;
  if ($fbu) {
    $uid = fb_invoke(FB_OP_GET_UID, array(
      'fbu' => $fbu,
      'fb_app' => $fb_app,
    ));
  }
  return $uid;
}

/**
 * Given a local user id, find the facebook id.
 *
 * Invokes hook_fb(FB_OP_GET_FBU) in order to ask other modules what the fbu
 * is. Typically, fb_user.module will answer the question.
 */
function fb_get_fbu($uid, $fb_app = NULL) {

  // Accept either a user object or uid passed in.
  if (is_object($uid) && isset($uid->uid) && !empty($uid->fbu)) {
    return $uid->fbu;
  }
  elseif (is_object($uid)) {
    $uid = isset($uid->uid) ? $uid->uid : 0;
  }
  if ($uid) {

    // User management is handled by another module. Use our hook to ask for mapping.
    $fbu = fb_invoke(FB_OP_GET_FBU, array(
      'uid' => $uid,
      'fb' => $GLOBALS['_fb'],
    ));
  }
  else {
    $fbu = NULL;
  }
  return $fbu;
}

/**
 * Convenience function to learn the fbu associated with a user, node or comment.
 * Used in theming (X)FBML tags.
 */
function fb_get_object_fbu($object) {
  $cache =& drupal_static(__FUNCTION__);
  if (!isset($cache)) {
    $cache = array();
  }
  if (isset($object->uid) && isset($cache[$object->uid])) {
    $fbu = $cache[$object->uid];
    return $fbu;
  }
  elseif (isset($object->fbu)) {

    // Explicitly set.
    $fbu = $object->fbu;
  }
  elseif (isset($object->init) && ($pos = strpos($object->init, '@facebook'))) {

    // Naming convention used by fb_user when creating accounts.
    // $object->init may be present when object is a user.
    $fbu = substr($object->init, 0, $pos);
  }
  elseif (!empty($object->name) && ($pos = strpos($object->name, '@facebook'))) {
    $fbu = substr($object->name, 0, $pos);
  }
  elseif (!empty($object->uid)) {

    // This can be expensive on pages with many comments or nodes!
    $fbu = fb_get_fbu($object->uid);
  }
  if (isset($fbu) && is_numeric($fbu)) {
    if (isset($object->uid) && $object->uid > 0) {
      $cache[$object->uid] = $fbu;
    }
    return $fbu;
  }
}

/**
 * Convenience method to get app info based on id or nid.
 */
function fb_get_app($search_data) {

  // $search_data can be an apikey, or an array of other search params.
  if (!is_array($search_data)) {
    $search_data = array(
      'id' => $search_data,
    );
  }
  $fb_app = fb_invoke(FB_OP_GET_APP, $search_data);
  return $fb_app;
}

/**
 * Convenience method for other modules to attach data to the fb_app
 * object.
 *
 * It is assumed the fb_app implementation will fill in the data
 * field. We really should clean up the separation between modules,
 * or merge fb_app.module into this one.
 */
function fb_get_app_data(&$fb_app) {
  if (!$fb_app) {

    // Avoid PHP strict error.
    return array();
  }
  if (!isset($fb_app->fb_app_data)) {
    $fb_app->fb_app_data = !empty($fb_app->data) ? unserialize($fb_app->data) : array();
  }
  return $fb_app->fb_app_data;
}

/**
 * Will return a human-readable name if the fb_app module supports it, or
 * fb_admin_get_app_info($fb_app) has been called.  However we don't
 * take the relatively expensive step of calling that ourselves.
 */
function fb_get_app_title($fb_app) {
  if (isset($fb_app->title)) {
    return $fb_app->title;
  }
  elseif (isset($fb_app->name)) {
    return $fb_app->name;
  }
  else {
    return $fb_app->label;
  }
}

/**
 * Convenience method to return array of all know fb_apps.
 */
function fb_get_all_apps() {
  $apps = fb_invoke(FB_OP_GET_ALL_APPS, NULL, array());
  return $apps;
}

/**
 * A convenience method for returning a list of facebook friends.
 *
 * This should work efficiently in canvas pages for finding friends of
 * the current user.
 *
 * @TODO - also support users who have permitted offline access.
 *
 * @return: an array of facebook ids
 */
function fb_get_friends($fbu, $fb_app = NULL) {
  $cache =& drupal_static(__FUNCTION__);
  if (!$fb_app) {
    $fb_app = $GLOBALS['_fb_app'];
  }

  // Facebook only allows us to query the current user's friends, so let's try
  // to log in as that user. It will only actually work if they are the
  // current user of a canvas page, or they've signed up for an infinite
  // session.
  $fb = fb_api_init($fb_app, $fbu);
  if (!$fb || !$fbu) {
    return;
  }
  $items = array();
  if (!isset($cache[$fbu])) {
    if ($fb === $GLOBALS['_fb'] && $fbu == fb_facebook_user($fb)) {
      try {
        $items = fb_call_method($fb, 'friends.get', array(
          'uid' => $fbu,
        ));
        $cache[$fbu] = $items;
      } catch (Exception $e) {
        fb_log_exception($e, t('Failed call to friends.get'), $fb);
      }
    }

    // friends_get does not work in cron call, so we double check. @TODO - still needed?
    if (!$items || !count($items)) {
      $logged_in = fb_facebook_user($fb);
      $query = "SELECT uid2 FROM friend WHERE uid1={$fbu}";

      // FQL, no {curly_brackets}!
      try {
        $result = fb_call_method($fb, 'fql.query', array(
          'query' => $query,
        ));

        //dpm($result, "FQL " . $query); // debug
        if (is_array($result)) {
          foreach ($result as $data) {
            $items[] = $data['uid2'];
          }
        }

        // Facebook's API has the annoying habit of returning an item even if user
        // has no friends.  We need to clean that up.
        if (!$items[0]) {
          unset($items[0]);
        }
        $cache[$fbu] = $items;
      } catch (Exception $e) {
        fb_log_exception($e, t('Failed call to fql.query: !query', array(
          '!query' => $query,
        )), $fb);
      }
    }
  }
  if (isset($cache[$fbu])) {
    return $cache[$fbu];
  }
}

// Return array of facebook gids
function fb_get_groups($fbu, $fb_app = NULL) {
  $items = array();
  $groups = fb_get_groups_data($fbu);
  if ($groups && count($groups)) {
    foreach ($groups as $data) {
      $items[] = $data['gid'];
    }
  }
  return $items;
}
function fb_get_groups_data($fbu, $fb_app = NULL) {
  $cache =& drupal_static(__FUNCTION__);
  $fb = _fb_api_init($fb_app);
  if (!$fb || !$fbu) {
    return;
  }
  if (!isset($cache[$fbu])) {
    $cache[$fbu] = fb_call_method($fb, 'groups.get', array(
      'uid' => $fbu,
    ));
  }
  return $cache[$fbu];
}

//// Menu structure.

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

  // Admin pages overview.
  $items[FB_PATH_ADMIN] = array(
    'title' => 'Facebook Apps',
    'description' => 'Facebook Applications',
    'page callback' => 'fb_admin_page',
    'access arguments' => array(
      FB_PERM_ADMINISTER,
    ),
    'file' => 'fb.admin.inc',
    'type' => MENU_NORMAL_ITEM,
  );
  $items[FB_PATH_ADMIN . '/list'] = array(
    'title' => 'List Apps',
    'weight' => -2,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items[FB_PATH_ADMIN . '/settings'] = array(
    'title' => 'Settings',
    'access arguments' => array(
      FB_PERM_ADMINISTER,
    ),
    'weight' => -1,
    'type' => MENU_LOCAL_TASK,
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'fb_admin_settings',
    ),
    'file' => 'fb.admin.inc',
  );

  // Admin pages for each app.
  $items[FB_PATH_ADMIN_APPS . '/%fb'] = array(
    'title' => 'Application Detail',
    'description' => 'Facebook Applications',
    'page callback' => 'fb_admin_app_page',
    'page arguments' => array(
      FB_PATH_ADMIN_APPS_ARGS,
    ),
    'access arguments' => array(
      FB_PERM_ADMINISTER,
    ),
    'file' => 'fb.admin.inc',
    'type' => MENU_CALLBACK,
  );
  $items[FB_PATH_ADMIN_APPS . '/%fb/fb'] = array(
    'title' => 'View',
    'weight' => -2,
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items[FB_PATH_ADMIN_APPS . '/%fb/fb/set_props'] = array(
    'title' => 'Set Properties',
    'description' => 'Set Facebook Application Properties',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'fb_admin_set_properties_form',
      FB_PATH_ADMIN_APPS_ARGS,
    ),
    'access arguments' => array(
      FB_PERM_ADMINISTER,
    ),
    'file' => 'fb.admin.inc',
    'type' => MENU_CALLBACK,
  );

  // Javascript helper
  $items['fb/js'] = array(
    'page callback' => 'fb_js_cb',
    'type' => MENU_CALLBACK,
    'access callback' => TRUE,
  );

  // Ajax event handler.
  $items[FB_PATH_AJAX_EVENT . '/%'] = array(
    'page callback' => 'fb_ajax_event',
    'type' => MENU_CALLBACK,
    'access callback' => TRUE,
    'page arguments' => array(
      FB_PATH_AJAX_EVENT_ARGS,
    ),
  );

  // "Channel" http://developers.facebook.com/docs/reference/javascript/FB.init
  $items['fb/channel'] = array(
    'page callback' => 'fb_channel_page',
    'type' => MENU_CALLBACK,
    'access callback' => TRUE,
  );
  return $items;
}

/**
 * Implementation of a %wildcard_load(). http://drupal.org/node/224170
 *
 * Handles menu items with %fb in the path.  Seems to get called a lot(!) so we cache.
 */
function fb_load($id) {
  $cache =& drupal_static(__FUNCTION__);
  if (!isset($cache)) {
    $cache = array();
  }
  if (!isset($cache[$id])) {
    $query = array(
      'label' => $id,
    );
    if (fb_is_fb_admin_page()) {

      // Show disabled apps to admins.
      $query['status'] = 0;

      // status >= 0
    }
    $cache[$id] = fb_get_app($query);
  }
  return $cache[$id];
}

/**
 * Implementation of a %wildcard_load(). http://drupal.org/node/224170
 *
 * Handles menu items with %fbu in the path.  Simply returns the numerical id.
 */
function fbu_load($id) {
  if (is_numeric($id)) {
    return $id;
  }
  elseif ($id == 'me') {

    // Drupal stupidly calls this before fb_init()! So _fb may not be initialized.
    if ($GLOBALS['_fb_app'] && !$GLOBALS['_fb']) {
      $fb = fb_api_init($GLOBALS['_fb_app']);
      $fbu = fb_facebook_user($fb);
    }
    else {
      $fbu = fb_facebook_user();
    }
    return $fbu;
  }
  return NULL;
}

/**
 * Implementation of a %wildcard_load(). http://drupal.org/node/224170
 *
 * Handles menu items with %fb_graph in the path.  Seems to get called a lot(!) so we cache.
 */
function fb_graph_load($id) {
  extract(fb_vars());

  // Drupal stupidly calls this before fb_init()! So _fb may not be initialized.
  if ($GLOBALS['_fb_app'] && !$GLOBALS['_fb']) {
    $fb = fb_api_init($GLOBALS['_fb_app']);
    $fbu = fb_facebook_user($fb);
  }
  $cache =& drupal_static(__FUNCTION__);
  if (!isset($cache)) {
    $cache = array();
  }
  if (!isset($cache[$id])) {
    $params = array(
      'access_token' => fb_get_token($fb, $fbu),
      'metadata' => 1,
    );
    try {
      $cache[$id] = fb_graph($id, $params, 'GET', $fb);
    } catch (Exception $e) {
      fb_log_exception($e, t('Failed to load facebook graph object %id.', array(
        '%id' => $id,
      )));
    }
  }
  return $cache[$id];
}

/**
 * Implements hook_permission().
 */
function fb_permission() {
  return array(
    FB_PERM_ADMINISTER => array(
      'title' => t('Administer Facebook settings in fb.module'),
    ),
  );
}

/**
 * Implements hook_exit().
 *
 * When completing a canvas page we need special processing for the session. See fb_session.inc.
 *
 * Also invoke hook_fb(FB_OP_EXIT), so that other modules can handle special
 * cases (in particular form support in fb_canvas.module.
 *
 * TODO: remove this hook, for compatibility with Drupal's aggressive caching.  Modules that currently look for FB_OP_EXIT will need to implement hook_exit on their own.
 */
function fb_exit($destination = NULL) {
  global $_fb_app, $_fb;
  $on_exit =& drupal_static('fb_invoke_async');
  if (!empty($on_exit)) {
    while ($args = array_shift($on_exit)) {
      $func = array_shift($args);
      if (fb_verbose()) {
        watchdog('fb', t("Processing delayed call to %function."), array(
          '%function' => $func,
        ));
      }
      try {
        $result = call_user_func_array($func, $args);
      } catch (Exception $e) {
        fb_log_exception($e, t('Failed calling %function.', array(
          '%function' => $func,
        )));
      }
    }
  }
  if ($_fb_app && $_fb) {

    // Invoke other modules.
    fb_invoke(FB_OP_EXIT, array(
      'fb_app' => $_fb_app,
      'fb' => $GLOBALS['_fb'],
    ), $destination);
  }
}

/**
 * Implements hook_module_implements_alter().
 *
 * Unfortunately fb_canvas has to process hook_exit last, so we tweak the order of hooks to enforce that order.
 */
function fb_module_implements_alter(&$implementations, $hook) {
  if ($hook == 'exit') {

    // Move our implementation to end of list.
    $group = $implementations['fb'];
    unset($implementations['fb']);
    $implementations['fb'] = $group;
  }
  if ($hook == 'fb' && isset($implementations['fb_canvas'])) {
    $group = $implementations['fb_canvas'];
    unset($implementations['fb_canvas']);
    $implementations['fb_canvas'] = $group;
  }
}
function fb_iframe_redirect($url) {

  // Unset drupal's header
  if (function_exists('header_remove')) {

    // php 5.3
    header_remove('Location');
  }
  echo "<script type=\"text/javascript\">\ntop.location.href = \"{$url}\";\n</script>";
  exit;
}

/**
 * Convert a local fully qualified path to a facebook app path.  This needs to
 * be used internally, to fix drupal_gotos upon form submission.  Third party
 * modules should not need to call this.
 */
function fb_iframe_fix_url($url, $iframe_base) {
  global $base_url;
  if ($app_id = fb_settings(FB_SETTINGS_ID)) {

    // Fully qualified paths.
    $patterns[] = "|" . url('', array(
      'absolute' => TRUE,
    )) . "|";
    $replacements[] = $iframe_base;

    // Url rewrites still used for iframe pages. Still needed ???
    $patterns[] = "|{$base_url}/" . FB_SETTINGS_CB . '/' . $app_id . '/|';
    $replacements[] = $iframe_base;
    $url = preg_replace($patterns, $replacements, $url);
    if (strpos($url, $iframe_base) !== FALSE) {

      // Facebook expects "appNNN_" prepended to hash
      $patterns = "|#([^\\?]*)|";
      $replacements = "#app{$app_id}_\$1";
      $url = preg_replace($patterns, $replacements, $url);
    }
  }
  return $url;
}

/**
 * Call a function not now but later.  As late as possible during the current request.
 *
 * In practice the function will be invoked during hook_exit().  In an
 * ideal world it might be nice to be invoked even later, similar to the
 * way drupal processes cron in drupal_page_footer() without delaying the
 * page returned to the user.  However I know of no drupal hook called
 * later than hook_exit.
 *
 * Provided as a convenience for third-party modules that want to invoke
 * some facebook API, but cannot do so right away.  For example say your
 * module implements hook_node_insert and wants to publish a message about
 * the new node to facebook.  You could try invoking
 * fb_graph_publish_action() from your hook_node_insert(). Unfortunately
 * facebook would immediately look up the new node on your server (to
 * populate graph details), but drupal would return page not found instead
 * of the inspected node because the insert has not yet finished!  To work
 * around this call fb_invoke_async('fb_graph_publish_action', ...)
 * instead.
 *
 * @see fb_exit().
 */
function fb_invoke_async() {
  $cache =& drupal_static(__FUNCTION__);
  $cache[] = func_get_args();
}

/**
 * Invoke hook_fb().
 * Only modules/fb modules should invoke this helper function which calls third-party hooks.
 */
function fb_invoke($op, $data = NULL, $return = NULL, $hook = FB_HOOK) {
  foreach (module_implements($hook) as $name) {
    $function = $name . '_' . $hook;
    try {
      $function($op, $data, $return);
    } catch (Exception $e) {
      if (isset($data['fb_app'])) {
        fb_log_exception($e, t('Exception calling %function(%op) (!app)', array(
          '%function' => $function,
          '%op' => $op,
          '%label' => $data['fb_app']->label,
          '%id' => $data['fb_app']->id,
          '!app' => l($data['fb_app']->label, FB_PATH_ADMIN_APPS . '/' . $data['fb_app']->label),
        )));
      }
      else {
        fb_log_exception($e, t('Exception calling %function(%op)', array(
          '%function' => $function,
          '%op' => $op,
        )));
      }
    }
  }
  return $return;
}

/**
 * This method will clean up URLs. When serving canvas pages, extra
 * information is included in URLs. This will remove the extra
 * information. Useful when linking back to the website from a canvas page or
 * wall post.
 *
 * For example in the following code, $url2 will link out to the server's domain:
 *
 * $url = url('node/42', array('absolute' => TRUE)); // i.e. http://apps.facebook.com/example/node/42
 * $url2 = fb_scrub_urls($url); // i.e. http://example.com/node/42
 *
 *
 * @see fb_url_rewrite.inc
 */
function fb_scrub_urls($content) {
  if (function_exists('_fb_settings_url_rewrite_prefixes')) {
    foreach (_fb_settings_url_rewrite_prefixes() as $key) {
      $patterns[] = "|{$key}/[^/]*/|";
      $replacements[] = "";
    }
    $content = preg_replace($patterns, $replacements, $content);
  }
  return $content;
}

/**
 * Convenience function to log and report exceptions.
 */
function fb_log_exception($e, $text = '', $fb = NULL) {
  if ($text) {
    $message = $text . ': ' . $e
      ->getMessage();
  }
  else {
    $message = $e
      ->getMessage();
  }
  if ($code = $e
    ->getCode()) {
    $message = "(#{$code}) {$message}";
  }
  if ($fb) {
    $message .= '. (' . t('logged into facebook as %fbu', array(
      '%fbu' => $fb
        ->getUser(),
    )) . ')';
  }
  if (fb_verbose()) {
    $message .= '<pre>' . $e . '</pre>';
  }
  watchdog('fb', $message, array(), WATCHDOG_ERROR);
  if (user_access(FB_PERM_ADMINISTER)) {
    drupal_set_message($message, 'error');
  }
}

/**
 * Simple wrapper around $fb->api() which caches data.  Does not support the
 * polymorphic arguments of $fb->api(). This is not a replacement for that
 * function.
 *
 * This is intended to avoid performace problems when, for example,
 * $fb->api('me') is called several times in a single request.
 *
 * @param $graph_path
 * Something facebook's graph API will understand.  Could be an ID or a path like 'me/accounts', for example.
 *
 * @param $params
 * Extras to pass to the graph API.  When making a request that requires a
 * token, try array('access_token' => fb_get_token()).
 */
function fb_api($graph_path, $params = array()) {
  static $cache;
  $fb = $GLOBALS['_fb'];
  if (!$fb) {
    return;
  }
  if (!isset($cache)) {
    $cache = array();
  }
  if (!isset($cache[$graph_path])) {
    $cache[$graph_path] = $fb
      ->api($graph_path, $params);
  }
  return $cache[$graph_path];
}

/**
 * DEPRECATED.  Use fb_api() instead.
 * Returns information about one or more facebook users.
 *
 * Historically, this helper function used facebook's users_getInfo API, hence
 * the name. Now it uses fql.query, but accomplishes the same thing.
 *
 * @param $oids
 * Array of facebook object IDs. In this case they should each be a user id.
 *
 * @param $fb
 * Rarely needed. For cases when global $_fb is not set, or more than one
 * facebook api has been initialized.
 *
 * @param $refresh_cache
 * If true, force a call to facebook instead of relying on temporarily stored
 * data.
 */
function fb_users_getInfo($oids, $fb = NULL, $refresh_cache = FALSE) {
  if (!$fb) {
    $fb = $GLOBALS['_fb'];
  }
  $infos = array();
  if (!is_array($oids)) {
    $oids = array();
  }
  if ($fb) {
    $app_id = $fb
      ->getAppId();

    // First try cache
    if (!$refresh_cache && isset($_SESSION['fb'])) {
      foreach ($oids as $oid) {
        if (isset($_SESSION['fb'][$app_id]['userinfo'][$oid])) {
          $info = $_SESSION['fb'][$app_id]['userinfo'][$oid];
          $infos[] = $info;
        }
      }
    }
    if (count($infos) != count($oids)) {

      // Session cache did not include all users, update the cache.
      $fields = array(
        'about_me',
        'affiliations',
        'birthday',
        'name',
        'first_name',
        'last_name',
        'is_app_user',
        'pic',
        'pic_big',
        'pic_square',
        'profile_update_time',
        'proxied_email',
        'email_hashes',
        'email',
        'uid',
      );
      try {
        $infos = fb_fql_query($fb, 'SELECT ' . implode(', ', $fields) . ' FROM user WHERE uid in(' . implode(', ', $oids) . ')', array(
          fb_get_token($fb),
        ));

        // Update cache with recent results.
        if (is_array($infos)) {
          foreach ($infos as $info) {
            $_SESSION['fb'][$app_id]['userinfo'][$info['uid']] = $info;
          }
        }
      } catch (FacebookApiException $e) {
        fb_log_exception($e, t('Failed to query facebook user info'), $fb);
      }
    }
    return $infos;
  }
}

/**
 * For debugging, add $conf['fb_verbose'] = TRUE; to settings.php.
 */
function fb_verbose() {
  return variable_get(FB_VAR_VERBOSE, NULL);
}

/**
 * hook_username_alter().
 *
 * Return a user's facebook name, instead of local username.
 */
function fb_username_alter(&$name, $account) {

  // This function can be called very early in the bootstrap process, before
  // the modules are initialized, in which case we will fail to alter the
  // name.
  $is_theming_username =& drupal_static('fb_theming_username');
  $enabled = variable_get(FB_VAR_ALTER_USERNAME, FB_ALTER_USERNAME_NOT_THEMING);
  if ($enabled == FB_ALTER_USERNAME_NEVER || $enabled == FB_ALTER_USERNAME_NOT_THEMING && $is_theming_username) {

    // Altering disabled.
    return;
  }

  // Skip on admin pages.
  if (arg(0) == 'admin') {
    return;
  }
  if (!strpos($name, '@facebook')) {

    // Only alter unique names created by fb_user.module.
    return;
  }
  if ($fbu = fb_get_fbu($account)) {

    // Querying names from facebook is expensive, so some trickery here to optimize things.
    // First we try the static cache.
    $names =& drupal_static(__FUNCTION__);
    if (!isset($names[$fbu])) {

      // Next try database cache.
      $use_cache = variable_get(FB_VAR_ALTER_USERNAME_AND_CACHE, FB_ALTER_USERNAME_DONT_CACHE);
      if ($use_cache == FB_ALTER_USERNAME_AND_CACHE) {
        if ($cache = cache_get('fb_username_' . $fbu)) {
          $names[$fbu] = $cache->data;
        }
      }
    }
    if (!isset($names[$fbu]) && !empty($GLOBALS['_fb'])) {

      // Nothing from the previous attempts worked so we have to query facebook.com.
      try {

        // Use fql query instead of graph api, because it will succeed more often.
        $data = fb_fql_query($GLOBALS['_fb'], "SELECT name FROM user WHERE uid={$fbu}", array(
          'access_token' => fb_get_token($GLOBALS['_fb']),
        ));
        if (count($data) && isset($data[0]['name'])) {
          $names[$fbu] = $data[0]['name'];
          if ($use_cache == FB_ALTER_USERNAME_AND_CACHE) {
            cache_set('fb_username_' . $fbu, $names[$fbu]);
          }
        }
      } catch (Exception $e) {
        fb_log_exception($e, t('Failed to alter username for facebook user %fbu', array(
          '%fbu' => $fbu,
        )));
      }
    }
    if (!empty($names[$fbu])) {
      $name = $names[$fbu];
    }
  }
}

/**
 * Implements hook_theme().
 *
 * Returns description of theme functions.
 *
 * @see fb.theme.inc
 */
function fb_theme() {
  return array(
    'fb_username' => array(
      'arguments' => array(
        'fbu' => NULL,
        'object' => NULL,
        'orig' => NULL,
      ),
      'file' => 'fb.theme.inc',
    ),
    'fb_user_picture' => array(
      'arguments' => array(
        'fbu' => NULL,
        'account' => NULL,
        'orig' => NULL,
      ),
      'file' => 'fb.theme.inc',
    ),
    'fb_fbml_popup' => array(
      'arguments' => array(
        'elements' => NULL,
      ),
      'file' => 'fb.theme.inc',
    ),
    'fb_login_button' => array(
      'arguments' => array(
        'text' => 'Connect with Facebook',
        'options' => NULL,
      ),
      'file' => 'fb.theme.inc',
    ),
    'fb_markup' => array(
      'arguments' => array(
        'not_connected_markup' => NULL,
        'connected_markup' => '<fb:profile-pic linked=false uid=loggedinuser></fb:profile-pic>',
        'options' => NULL,
      ),
      'file' => 'fb.theme.inc',
    ),
  );
}

//// Javascript and Ajax helpers

/**
 * Ajax javascript callback.
 *
 * For sites which use ajax, various events may create javascript which is
 * normally embedded in a page. For example, posting to a user's wall. When
 * ajax is used instead of a page reload, this callback will provide any
 * javascript which should be run.
 */
function fb_js_cb() {
  $js_array = fb_invoke(FB_OP_JS, array(
    'fb' => $GLOBALS['_fb'],
    'fb_app' => $GLOBALS['_fb_app'],
  ), array());
  $extra_js = implode("\n", $extra);
  print $extra_js;
  exit;
}

/**
 * Ajax callback handles an event from facebook's javascript sdk.
 *
 * @see
 *   fb.js and
 *   http://developers.facebook.com/docs/reference/javascript/FB.Event.subscribe
 *
 * @return
 *   Array of javascript to be evaluated by the page which called this
 *   callback.
 */
function fb_ajax_event($event_type) {
  global $_fb, $_fb_app;
  $js_array = array();
  if (isset($_REQUEST['appId'])) {
    $_fb_app = fb_get_app(array(
      'id' => $_REQUEST['appId'],
    ));

    // Remember signed_request in session, in case third party cookies are disabled.
    if (isset($_REQUEST['signed_request']) && $_REQUEST['signed_request'] && variable_get(FB_VAR_USE_SESSION, TRUE)) {
      $_SESSION['_fb_' . $_fb_app->id] = $_REQUEST['signed_request'];
    }
    elseif (isset($_REQUEST['access_token']) && $_REQUEST['access_token'] && variable_get(FB_VAR_USE_SESSION, TRUE)) {
      $_SESSION['_fb_token_' . $_fb_app->id] = $_REQUEST['access_token'];
    }
    else {
      unset($_SESSION['_fb_' . $_fb_app->id]);
      unset($_SESSION['_fb_token_' . $_fb_app->id]);
    }
    if ($_fb_app) {
      $_fb = fb_api_init($_fb_app);

      // Data to pass to hook_fb.
      $data = array(
        'fb_app' => $_fb_app,
        'fb' => $_fb,
        'event_type' => $event_type,
        'event_data' => $_POST,
      );
      $js_array = fb_invoke(FB_OP_AJAX_EVENT, $data, $js_array);
    }
    else {
      watchdog('fb', 'fb_ajax_event did not find application %id', array(
        '%id' => $_REQUEST['appId'],
      ), WATCHDOG_ERROR);
    }
    if ($event_type == 'session_change') {

      // Session change is a special case.  If user has logged out of
      // facebook, we want a new drupal session.  We do this here, even if
      // fb_user.module is not enabled.
      if (!isset($_POST['fbu']) || !$_POST['fbu']) {

        // Logout, not login.
        _fb_logout();
      }
    }
  }
  else {
    watchdog('fb', 'fb_ajax_event called badly.  Not passed appId.', array(), WATCHDOG_ERROR);

    // Trying to track down what makes this happen.
    if (fb_verbose() == 'extreme') {
      watchdog('fb', 'fb_ajax_event called badly.  Not passed appId. trace: !trace', array(
        '!trace' => '<pre>' . print_r(debug_backtrace(), 1) . '</pre>',
      ), WATCHDOG_ERROR);
    }
  }
  drupal_json_output(array_values($js_array));

  //exit();
}

/**
 * Menu callback for custom channel.
 *
 * @see http://developers.facebook.com/docs/reference/javascript/FB.init
 */
function fb_channel_page() {

  // Headers instruct browser to cache this page.
  drupal_add_http_header("Cache-Control", "public");
  drupal_add_http_header("Expires", "Sun, 17-Jan-2038 19:14:07 GMT");
  $date = format_date(time());
  $output = "<!-- modules/fb fb_channel_page() {$date} -->\n";
  $url = fb_js_settings('js_sdk_url');
  $output .= "<script src=\"{$url}\"></script>\n";
  print $output;
  exit;
}

//// Miscellaneous helpers and convenience functions.

/**
 * Protocol (http or https) of the current request.
 */
function fb_protocol() {
  return isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on' ? 'https' : 'http';
}

/**
 * Convenience wrapper around drupal_access_denied().  Call on pages where the
 * access is denied because the user is not logged into facebook.
 */
function fb_access_denied() {
  if (!fb_facebook_user()) {
    drupal_set_message(t('You must <a href="#" onclick="FB.login(function(response) {}, {perms:Drupal.settings.fb.perms}); return false;">log into facebook to view this page</a>.'));
  }
  drupal_access_denied();
  exit;
}

Functions

Namesort descending Description
fbu_load Implementation of a %wildcard_load(). http://drupal.org/node/224170
fb_access_denied Convenience wrapper around drupal_access_denied(). Call on pages where the access is denied because the user is not logged into facebook.
fb_ajax_event Ajax callback handles an event from facebook's javascript sdk.
fb_api Simple wrapper around $fb->api() which caches data. Does not support the polymorphic arguments of $fb->api(). This is not a replacement for that function.
fb_api_batch Helper function for facebook's batch graph api.
fb_api_check_session Sometimes calls to fb_api_init succeed, but calls to the client api will fail because cookies are obsolete or what have you. This function makes a call to facebook to test the session. Expensive, so use only when necessary.
fb_api_init Include and initialize Facebook's PHP SDK.
fb_call_method This helper original written because facebook's $fb->api() function was very buggy. I'm not sure this is still needed. On the other hand, a future version of modules/fb might use this instead of faceobok's PHP SDK, eliminating the…
fb_channel_page Menu callback for custom channel.
fb_controls Controls are one way to customize the behavior of Drupal for Facebook modules.
fb_custom_theme Implements hook_custom_theme().
fb_exit Implements hook_exit().
fb_facebook_user Returns the facebook user id currently visiting a canvas page, or if set_user has been called. Unlike fb_get_fbu(), works only on canvas and connect pages, or when infinite session has been initialized.
fb_fql_query Helper function for fql queries.
fb_get_all_apps Convenience method to return array of all know fb_apps.
fb_get_app Convenience method to get app info based on id or nid.
fb_get_app_data Convenience method for other modules to attach data to the fb_app object.
fb_get_app_title Will return a human-readable name if the fb_app module supports it, or fb_admin_get_app_info($fb_app) has been called. However we don't take the relatively expensive step of calling that ourselves.
fb_get_fbu Given a local user id, find the facebook id.
fb_get_friends A convenience method for returning a list of facebook friends.
fb_get_groups
fb_get_groups_data
fb_get_object_fbu Convenience function to learn the fbu associated with a user, node or comment. Used in theming (X)FBML tags.
fb_get_token Helper to get the tokens needed to accss facebook's API.
fb_get_uid Given a facebook user id, learn the local uid, if any.
fb_graph Helper function to work with facebook "open" graph.
fb_graph_load Implementation of a %wildcard_load(). http://drupal.org/node/224170
fb_iframe_fix_url Convert a local fully qualified path to a facebook app path. This needs to be used internally, to fix drupal_gotos upon form submission. Third party modules should not need to call this.
fb_iframe_redirect
fb_init Implements hook_init().
fb_invoke Invoke hook_fb(). Only modules/fb modules should invoke this helper function which calls third-party hooks.
fb_invoke_async Call a function not now but later. As late as possible during the current request.
fb_is_canvas Is the current request a canvas page?
fb_is_fb_admin_page Helper tells other modules when to load admin hooks.
fb_is_page_admin Does the current user administer the current page?
fb_is_page_liked Does the current user like the current page?
fb_is_tab Is the current page a profile tab?
fb_js_cb Ajax javascript callback.
fb_js_settings Helper to get the configured variables.
fb_load Implementation of a %wildcard_load(). http://drupal.org/node/224170
fb_log_exception Convenience function to log and report exceptions.
fb_menu Implements hook_menu().
fb_module_implements_alter Implements hook_module_implements_alter().
fb_page_alter Implements hook_page_alter(). Can alter the $page['page_bottom'] hidden region here.
fb_permission Implements hook_permission().
fb_protocol Protocol (http or https) of the current request.
fb_rdf_namespaces Implements hook_rdf_namespaces(). Adds the xmlns:fb attribute to html tag.
fb_require_authorization Helper function to ensure user has authorized an application.
fb_scrub_urls This method will clean up URLs. When serving canvas pages, extra information is included in URLs. This will remove the extra information. Useful when linking back to the website from a canvas page or wall post.
fb_theme Implements hook_theme().
fb_tokens Implements hook_tokens().
fb_username_alter hook_username_alter().
fb_users_getInfo DEPRECATED. Use fb_api() instead. Returns information about one or more facebook users.
fb_vars Helper function to get the most commonly used values. In your custom module, call extract(fb_vars()); to set $fb_app, $fb, and $fbu.
fb_verbose For debugging, add $conf['fb_verbose'] = TRUE; to settings.php.
_fb_api_init Wrapper function for fb_api_init. This helps for functions that should work whether or not we are on a canvas page. For canvas pages, the active fb object is used. For non-canvas pages, it will initialize the API using an infinite session, if configured.
_fb_get_name Helper function determines a name from data returned from fb_graph(). Almost always, graph data includes a 'name'. But in rare cases that is not present and we use an id instead.
_fb_logout Helper to ensure local user is logged out, or an anonymous session is refreshed.

Constants