<?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;

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

    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,
              // It's unclear exactly which parts of the sr are needed.
              //'signedRequest' => $_SESSION['_fb_' . $_fb_app->id],
              //'expiresIn' => 6988, // ???
            );
            $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), // http://drupal.org/node/1000452
                                     )));
  }

  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',
    //'fb' => 'http://ogp.me/ns/fb#',
  );
}

/**
 * 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, // debug
    );
    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',
        'status',
        '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, // POSTed via ajax.
      );

      $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();
}
