You are here

fb_canvas.module in Drupal for Facebook 6.2

This module provides support for Canvas page applications. Use Drupal to power traditional Facebook Apps.

See also fb_connect.module for Facebook Connect.

File

fb_canvas.module
View source
<?php

/**
 * @file
 * 
 * This module provides support for Canvas page applications.  Use
 * Drupal to power traditional Facebook Apps.  
 *
 * See also fb_connect.module for Facebook Connect.
 * 
 */

// Option to require_login() on all canvas pages.
define('FB_CANVAS_OPTION_ALLOW_ANON', 1);
define('FB_CANVAS_OPTION_REQUIRE_LOGIN', 2);
define('FB_CANVAS_VAR_PROCESS_FBML', 'fb_canvas_process_fbml');
define('FB_CANVAS_VAR_PROCESS_FBML_FORM', 'fb_canvas_process_fbml_form');
define('FB_CANVAS_VAR_PROCESS_IFRAME', 'fb_canvas_process_iframe');
define('FB_CANVAS_FORM_HANDLER_WRAP', '_fb_canvas_form_handler');
define('FB_CANVAS_FORM_HANDLER_ENABLED', '_fb_handling_form');

/**
 * Implementation of hook_menu().
 */
function fb_canvas_menu() {
  $items = array();

  // Admin pages
  $items[FB_PATH_ADMIN . '/fb_canvas'] = array(
    'title' => 'Canvas Pages',
    'description' => 'Configure Canvas Pages',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'fb_canvas_admin_settings',
    ),
    'access arguments' => array(
      FB_PERM_ADMINISTER,
    ),
    'file' => 'fb_canvas.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

/**
 * Implementation of hook_fb().
 */
function fb_canvas_fb($op, $data, &$return) {
  static $original_uid;
  global $user;
  $fb = isset($data['fb']) ? $data['fb'] : NULL;
  $fb_app = isset($data['fb_app']) ? $data['fb_app'] : NULL;
  if ($op == FB_OP_CURRENT_APP) {
    if (function_exists('fb_settings')) {
      if (fb_settings(FB_SETTINGS_TYPE) == FB_SETTINGS_TYPE_CANVAS && ($apikey = fb_settings(FB_SETTINGS_APIKEY))) {

        // This is the proper way to discover the app on canvas pages.
        $fb_app = fb_get_app(array(
          'apikey' => $apikey,
        ));

        // If were on an iframe excursion, the fb_sig will be in session.
        if (!isset($_REQUEST['fb_sig']) && isset($_SESSION['fb_canvas_fb_sig'])) {
          foreach ($_SESSION['fb_canvas_fb_params'] as $k => $value) {
            $key = 'fb_sig_' . $k;
            if (!isset($_REQUEST[$key])) {

              // We use $_REQUEST, facebook uses $_GET.
              $_REQUEST[$key] = $value;
              $_GET[$key] = $value;
            }
          }
          $_REQUEST['fb_sig'] = $_SESSION['fb_canvas_fb_sig'];
          $_GET['fb_sig'] = $_SESSION['fb_canvas_fb_sig'];
        }
      }
      elseif ($id = fb_settings(FB_SETTINGS_CB)) {

        // This is the old way dependent on url rewriting.
        $fb_app = fb_get_app(array(
          'id' => $id,
        ));
      }
    }
    else {

      // Fallback in case fb_settings not used.
      if (isset($_REQUEST[FB_APP_REQ_API_KEY]) && ($apikey = $_REQUEST[FB_APP_REQ_API_KEY])) {

        // If facebook has passed the app key, let's use that.
        $fb_app = fb_get_app(array(
          'apikey' => $apikey,
        ));
      }
    }
    if ($fb_app) {
      $return = $fb_app;
    }
  }
  elseif ($op == FB_OP_INITIALIZE) {

    // Get our configuration settings.
    $fb_app_data = fb_get_app_data($fb_app);
    $fb_canvas_data = $fb_app_data['fb_canvas'];
    $is_canvas = FALSE;

    // Set an app-specific theme.
    global $custom_theme;

    // Set by this function.
    if (fb_canvas_is_fbml()) {
      $custom_theme = $fb_canvas_data['theme_fbml'];
      $is_canvas = TRUE;
      $use_ob = variable_get(FB_CANVAS_VAR_PROCESS_FBML, TRUE);
    }
    elseif (fb_canvas_is_iframe()) {
      $custom_theme = $fb_canvas_data['theme_iframe'];
      $is_canvas = TRUE;
      $use_ob = variable_get(FB_CANVAS_VAR_PROCESS_IFRAME, TRUE);
    }
    if ($is_canvas) {

      // We are serving a canvas page.
      $conf['admin_theme'] = $custom_theme;
      if ($fb_canvas_data['require_login'] == FB_CANVAS_OPTION_REQUIRE_LOGIN) {

        // The application is configured to require login on all canvas pages.
        // However, there are exceptions.
        if (fb_is_profile_tab()) {

          // Redirects are not allowed for the profile tab.
        }
        else {

          // There may be other exceptions, for example some ajax callbacks.  Potential todo item.
          $fb
            ->require_login();
        }
      }

      // Remember the user id.  If fb_user.module changes it, we'll need to refresh the page.  See FB_OP_POST_INIT.
      $original_uid = $user->uid;

      // Hack to init the theme before _drupal_maintenance_theme initializes the wrong one.
      if (variable_get('site_offline', FALSE)) {
        $dummy = theme('dummy');
      }
    }

    // Use buffer for form posts.
    if (!$use_ob && fb_canvas_handling_form()) {
      $use_ob = variable_get(FB_CANVAS_VAR_PROCESS_FBML_FORM, TRUE);
    }

    // Store entire page in output buffer.  Will post-process on exit.
    if ($use_ob) {
      ob_start();
      $GLOBALS['fb_canvas_post_process'] = TRUE;
    }
    if ($is_canvas && $_GET['q'] == drupal_get_normal_path(variable_get('site_frontpage', 'node'))) {
      if ($fb
        ->get_loggedin_user()) {
        if ($fb->api_client
          ->users_isAppUser()) {
          $front = $fb_canvas_data['front_added'];
        }
        else {
          $front = $fb_canvas_data['front_loggedin'];
        }
      }
      else {
        $front = $fb_canvas_data['front_anonymous'];
      }
      if ($front) {
        menu_set_active_item(drupal_get_normal_path($front));
      }
    }
  }
  elseif ($op == FB_OP_POST_INIT) {

    //dpm($_REQUEST, "request post init");
    if (isset($original_uid) && $original_uid != $user->uid) {

      // The user has changed, presumably fb_user.module recognized the facebookuser.  We need to refresh canvas pages.
      if (!(arg(0) == 'fb_app' && arg(1) == 'event')) {

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

    // The ?destination=... url param means something to drupal but something
    // else to facebook.  If ?fb_canvas_destination=... is set, we honor that.
    if (isset($_REQUEST['fb_canvas_destination'])) {
      $_REQUEST['destination'] = $_REQUEST['fb_canvas_destination'];
    }

    // Give FBML themes a chance to render tabs.
    if (fb_canvas_is_fbml()) {
      _fb_canvas_menu_hack();
    }

    // Include our admin hooks.
    if (fb_is_fb_admin_page()) {
      require drupal_get_path('module', 'fb_canvas') . '/fb_canvas.admin.inc';
    }
  }
  elseif ($op == FB_OP_EXIT) {

    /* We do some unpleasant stuff in this hook... on FBML canvas
           pages we might use $fb->redirect(), in which case other
           modules' hook_exit() might not be called.

           In other cases we call drupal_goto(), in which case other
           modules' hook_exit() might be called twice.  I hate to do this
           but so far have not figured another way.  And so far no
           problems... if problems arise, please post to issue queue.
        */
    $destination = $return;
    if ($GLOBALS['fb_canvas_post_process']) {
      $output = ob_get_contents();
      ob_end_clean();
      if (fb_canvas_is_fbml()) {
        $output = fb_canvas_process($output, array(
          'force_absolute_canvas' => TRUE,
        ));
      }
      elseif (fb_canvas_is_iframe()) {
        $output = fb_canvas_process($output, array(
          'add_target' => TRUE,
        ));
      }
    }
    if (fb_canvas_handling_form() && fb_canvas_handling_form() !== 'avoid_loop' && $output) {

      // Avoid infinite loop if we get called from drupal_goto.
      fb_canvas_handling_form('avoid_loop');

      // Special handling for forms submitted to Drupal from FBML canvas pages.
      // Cache the results to show the user later.
      $token = uniqid('fb_');
      $cid = session_id() . "_{$token}";
      cache_set($cid, $output, 'cache_page', time() + 60 * 5, drupal_get_headers());

      // (60 * 5) == 5 minutes
      $dest = 'http://apps.facebook.com/' . $fb_app->canvas . "/fb/form_cache/{$cid}";

      // $fb->redirect($dest); // Does not work!
      // Preserve some URL parameters
      $query = array();
      foreach (array(
        'fb_force_mode',
      ) as $key) {
        if ($_REQUEST[$key]) {
          $query[] = $key . '=' . $_REQUEST[$key];
        }
      }

      // drupal_goto honors $_REQUEST['destination'], but we only want
      // that when no errors occurred.
      if (form_get_errors()) {
        unset($_REQUEST['destination']);
        if ($_REQUEST['edit']) {
          unset($_REQUEST['edit']['destination']);
        }
      }
      if (fb_verbose() == 'extreme') {
        watchdog('fb', "Storing cached form page {$cid}, then redirecting to {$dest}, query is " . implode('&', $query));
      }
      drupal_goto($dest, implode('&', $query), NULL, 303);

      // appears to work
    }
    if ((fb_canvas_is_fbml() || fb_canvas_is_iframe()) && !isset($GLOBALS['_fb_canvas_goto'])) {
      if ($destination) {

        // Fully qualified URLs need to be modified to point to facebook app.
        // URLs are fully qualified when a form submit handler returns a path,
        // or any call to drupal_goto.
        $app_destination = fb_canvas_fix_url($destination, $fb_app);

        // If here, drupal_goto has been called, but it may not work within a
        // canvas page, so we'll use Facebook's method.
        // Unfortunately, other modules' hook_exit() may not be called.
        if (fb_verbose()) {
          watchdog('fb_debug', "FB_OP_EXIT on canvas page redirecting to {$app_destination} (original destination was {$destination}).");
          $fb
            ->redirect($app_destination);
        }
      }
    }
    if (isset($output)) {
      print $output;
    }
  }
}

/**
 * Helper returns configuration for this module, on a per-app basis.
 */
function _fb_canvas_get_config($fb_app) {
  $fb_app_data = fb_get_app_data($fb_app);
  $fb_canvas_data = $fb_app_data['fb_canvas'] ? $fb_app_data['fb_canvas'] : array();

  // Merge in defaults
  $fb_canvas_data += array(
    'require_login' => FB_CANVAS_OPTION_ALLOW_ANON,
    'theme_fbml' => 'fb_fbml',
    'theme_iframe' => 'fb_fbml',
  );
  return $fb_canvas_data;
}

/**
 * Implementation of hook_form_alter.
 */
function fb_canvas_form_alter(&$form, &$form_state, $form_id) {

  // Add our settings to the fb_app edit form.
  if (isset($form['fb_app_data']) && is_array($form['fb_app_data'])) {
    $fb_app = $form['#fb_app'];
    $fb_canvas_data = _fb_canvas_get_config($fb_app);
    $form['fb_app_data']['fb_canvas'] = array(
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#title' => t('Facebook canvas pages'),
      '#description' => t('Settings which apply to <a href=!url target=_blank>canvas pages</a>.', array(
        '!url' => 'http://developers.facebook.com/get_started.php?tab=anatomy#canvas',
      )),
    );
    $form['fb_app_data']['fb_canvas']['require_login'] = array(
      '#type' => 'radios',
      '#title' => t('Require authorization'),
      '#description' => t('Require authorization if you want Drupal for Facebook to call require_login() on <strong>every</strong> canvas page.'),
      '#options' => array(
        FB_CANVAS_OPTION_ALLOW_ANON => t('Allow anonymous visitors'),
        FB_CANVAS_OPTION_REQUIRE_LOGIN => t('Require all users to authorize the application'),
      ),
      '#default_value' => $fb_canvas_data['require_login'],
      '#required' => TRUE,
    );
    $form['fb_app_data']['fb_canvas']['front_anonymous'] = array(
      '#type' => 'textfield',
      '#title' => t('Front page when user is not logged in to facebook'),
      '#description' => t('Leave blank to use the site-wide front page.  See <a href="!link" target=_blank>Public Canvas Pages</a>.', array(
        '!link' => 'http://wiki.developers.facebook.com/index.php/Public_Canvas_Pages',
      )),
      '#default_value' => $fb_canvas_data['front_anonymous'],
    );
    $form['fb_app_data']['fb_canvas']['front_loggedin'] = array(
      '#type' => 'textfield',
      '#title' => t('Front page when user is logged in to facebook, but is not a user of the app'),
      '#description' => t('Leave blank to use the site-wide front page.'),
      '#default_value' => $fb_canvas_data['front_loggedin'],
    );
    $form['fb_app_data']['fb_canvas']['front_added'] = array(
      '#type' => 'textfield',
      '#title' => t('Front page for authorized users of this application'),
      '#description' => t('Leave blank to use the site-wide front page.'),
      '#default_value' => $fb_canvas_data['front_added'],
    );

    // Override themes
    $themes = system_theme_data();
    ksort($themes);
    $theme_options[0] = t('System default');
    foreach ($themes as $theme) {
      $theme_options[$theme->name] = $theme->name;
    }
    $form['fb_app_data']['fb_canvas']['theme_fbml'] = array(
      '#type' => 'select',
      '#title' => t('Theme for FBML pages'),
      '#description' => t('Choose only a theme that is FBML-aware.'),
      '#options' => $theme_options,
      '#required' => TRUE,
      '#default_value' => $fb_canvas_data['theme_fbml'],
    );
    $form['fb_app_data']['fb_canvas']['theme_iframe'] = array(
      '#type' => 'select',
      '#title' => t('Theme for iframe pages'),
      '#description' => t('Choose only a facebook-aware theme'),
      '#options' => $theme_options,
      '#required' => TRUE,
      '#default_value' => $fb_canvas_data['theme_iframe'],
    );
  }

  // We will send all form submission directly to us, not via
  // apps.facebook.com/whatever.
  if (fb_canvas_is_fbml()) {

    //dpm($form, "fb_canvas_form_alter($form_id)"); // debug

    // We're in a facebook callback
    if (!isset($form[FB_CANVAS_FORM_HANDLER_WRAP])) {

      // Use the form data structure to keep track of whether data is
      // posted directly to us, via absolute URL, or instead via
      // relative URL on apps.facebook.com/...
      $form[FB_CANVAS_FORM_HANDLER_WRAP] = array(
        '#action_fb' => $form['#action'],
        '#action_local' => _fb_canvas_make_form_action_local($form['#action']),
      );

      // This helper function allows third-party modules to control
      // where the post goes.  By default use local server.
      fb_canvas_form_action_via_facebook($form, FALSE);
    }

    // Drupal includes wacky markup for javascript junk in node forms.  It
    // makes things look terrible in FBML.  It only works when javascript is
    // enabled and it should have been implemented to degrade gracefully, but
    // it wasn't.
    if (isset($form['body_field'])) {
      unset($form['body_field']['teaser_js']);
      unset($form['body_field']['teaser_include']);
    }
  }
  elseif (fb_canvas_is_iframe()) {

    // Include the fb_sig so that when post is received, we know we are still in a canvas.
    foreach ($_REQUEST as $key => $value) {
      if (strpos($key, 'fb_sig') === 0) {
        $form['fb_canvas_iframe'][$key] = array(
          '#type' => 'hidden',
          '#value' => $value,
        );
      }
    }
  }
}

/**
 * Call this from your form_alter hook to prevent changes to the
 * form's default action.
 */
function fb_canvas_form_action_via_facebook(&$form, $use_facebook = TRUE) {
  if (isset($form[FB_CANVAS_FORM_HANDLER_WRAP])) {
    if ($use_facebook) {
      $form['#action'] = $form[FB_CANVAS_FORM_HANDLER_WRAP]['#action_fb'];
      $form[FB_CANVAS_FORM_HANDLER_WRAP][FB_CANVAS_FORM_HANDLER_ENABLED] = array(
        '#value' => FALSE,
        '#type' => 'hidden',
      );
    }
    else {
      $form['#action'] = $form[FB_CANVAS_FORM_HANDLER_WRAP]['#action_local'];
      $form[FB_CANVAS_FORM_HANDLER_WRAP][FB_CANVAS_FORM_HANDLER_ENABLED] = array(
        '#value' => TRUE,
        '#type' => 'hidden',
      );
    }
  }
}

/**
 * Implementation of hook_footer().
 *
 * Invoke FB_OP_CANVAS_FBJS_INIT, allowing other modules to insert
 * FBJS to the current page.
 */
function fb_canvas_footer($main = 0) {
  if (fb_canvas_is_fbml()) {

    // Add FBJS only to FBML pages.
    global $_fb, $_fb_app;
    $data = array(
      'fb' => $_fb,
      'fb_app' => $_fb_app,
    );
    $extra = fb_invoke(FB_OP_CANVAS_FBJS_INIT, $data, array());
    if (count($extra)) {
      $extra_js = implode("\n", $extra);
      fb_add_js('', '');

      // prime javascript
      drupal_add_js($extra_js, 'inline', 'fbml');
    }
  }
}
function fb_canvas_is_fbml() {
  global $_fb, $_fb_app;
  if ($_fb && $_fb_app) {

    // Facebook events are not canvas pages
    if (arg(0) == 'fb_app' && arg(1) == 'event') {
      return FALSE;
    }
    else {
      return $_fb
        ->in_fb_canvas() || fb_canvas_handling_form();
    }
  }
}
function fb_canvas_is_iframe() {
  global $_fb, $_fb_app;
  if ($_fb && $_fb_app) {
    return $_fb
      ->in_frame() && !fb_canvas_is_fbml();
  }
}
function fb_canvas_handling_form($value = NULL) {
  global $_fb;

  // Test whether a form has been submitted via facebook canvas page.
  if ($_fb && isset($_REQUEST[FB_CANVAS_FORM_HANDLER_ENABLED])) {
    $result = $_REQUEST[FB_CANVAS_FORM_HANDLER_ENABLED];
  }
  if (isset($value)) {
    $_REQUEST[FB_CANVAS_FORM_HANDLER_ENABLED] = $value;
  }
  return $result;
}

/**
 * This is intended to help sites which have some FBML and some iframe pages.
 * Makes the current FBML canvas page an iframe. And attempts to preserve
 * session while visiting multiple iframe pages.
 *
 * Set $conf['fb_session_cookieless_iframe'] = TRUE for best results (but
 * perhaps less session security).
 */
function fb_canvas_iframe_excursion() {
  if (fb_canvas_is_fbml()) {
    $query = $_GET;
    unset($query['q']);
    $path = $_GET['q'];

    //$query['XDEBUG_SESSION_START']=1;
    if (function_exists('fb_settings')) {
      fb_settings(FB_SETTINGS_CB, $GLOBALS['_fb_app']->label);
      fb_settings(FB_SETTINGS_CB_SESSION, fb_settings(FB_SETTINGS_SESSION_KEY));
    }

    // Make the FBML page a canvas page.
    print '<fb:iframe resizable=true name=resizeme frameborder=0 scrolling="auto" src="' . url($path, array(
      'absolute' => TRUE,
      'query' => $query,
    )) . '"></fb:iframe>';
    unset($_SESSION['fb_canvas_fb_sig']);
    unset($_SESSION['fb_canvas_fb_params']);
    exit;
  }
  else {
    if (fb_canvas_is_iframe()) {

      // Add to session paramaters that iframes will need.
      if (!isset($_SESSION['fb_canvas_fb_sig'])) {
        $_SESSION['fb_canvas_fb_params'] = $GLOBALS['_fb']
          ->get_valid_fb_params($_REQUEST);
        $_SESSION['fb_canvas_fb_sig'] = $_REQUEST['fb_sig'];
      }
      if (module_exists('fb_connect')) {

        // Resize the iframe.
        fb_connect_require_feature('CanvasUtil');
        fb_connect_init_js("FB.CanvasClient.startTimerToSizeToContent();");
      }
    }
  }
}
function fb_canvas_iframe_excursion_end() {

  //fb_settings(FB_SETTINGS_CB, NULL);

  //fb_settings(FB_SETTINGS_CB_SESSION, NULL);
}

// Recent changes may have broken the "old way".
function _fb_canvas_make_form_action_local($action = NULL) {
  global $base_path;
  if (!isset($action)) {

    // Is this ever reached?
    if (function_exists('dpm')) {
      dpm("_fb_canvas_make_form_action_local({$action})");
    }

    // XXX
    $action = $_GET['q'];
  }

  // If action is fully qualified, do not change it
  if (strpos($action, ':')) {
    if (function_exists('dpm')) {
      dpm($action, "leaving form action untouched");
    }

    // XXX
    return $action;
  }

  // I'm not sure where the problem is, but sometimes actions have two question marks.  I.e.
  // /htdocs/?app=foo&q=user/login?destination=comment/reply/1%2523comment-form
  // Here we replace 3rd (or more) '?' with '&'.
  $parts = explode('?', $action);
  if (count($parts) > 2) {
    if (fb_verbose()) {
      watchdog('fb_canvas', "fixing badly formed action: " . $action);
    }
    $action = array_shift($parts) . '?' . array_shift($parts);
    $action .= '&' . implode('&', $parts);
  }
  $relative = url('');
  $absolute = url('', array(
    'absolute' => TRUE,
  ));
  $base_path = base_path();

  // By default, '#action' will be request_uri(), which starts with base_path();
  global $_fb_app;
  if (strpos($action, FB_SETTINGS_CB)) {

    // XXX old way.  Will go away.
    $action = $absolute . substr($action, strlen($relative));
  }
  elseif (strpos($action, $relative) === 0 && function_exists('fb_url_inbound_alter')) {

    // XXX another old way.
    // Replace relative action with absolute.
    // Include fb settings
    // TODO: FB_SETTINGS_CB_TYPE
    $action = $absolute . FB_SETTINGS_CB . '/' . $_fb_app->label . '/' . substr($action, strlen($relative));
  }
  else {

    // New way...
    if (strpos($action, $relative) === 0) {

      // If here, the action was made by call to url() and we prepended
      // canvas_name.  Now remove the canvas name for a proper absolute url.
      // (Comment forms reach this clause but node forms do not.)
      $action = substr($action, strlen($relative));
    }
    elseif ($base_path && strpos($action, $base_path) === 0) {
      $action = substr($action, strlen($base_path));
    }
    $action = url(ltrim($action, '/'), array(
      'absolute' => TRUE,
    ));
  }

  // Changed code to work when $base_path set.  Need to test on server where base_path is ''.

  //dpm($action, "_fb_canvas_make_form_action_local returning<br/>");
  return $action;
}

/**
 * Uses $_fb->redirect on canvas pages, otherwise drupal_goto.
 */
function fb_canvas_goto($path) {
  global $_fb, $_fb_app;
  if ($_fb && (fb_canvas_is_fbml() || fb_canvas_is_iframe())) {
    $url = fb_canvas_fix_url(url($path, array(
      'absolute' => TRUE,
    )), $_fb_app);

    // Allow modules to react to the end of the page request before redirecting.
    // We do not want this while running update.php.
    if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
      $GLOBALS['_fb_canvas_goto'] = TRUE;

      // prevents fb_canvas_exit from calling redirect.
      module_invoke_all('exit', $url);
    }
    $_fb
      ->redirect($url);
  }
  else {
    drupal_goto($path);
  }
  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_canvas_fix_url($url, $fb_app) {

  //dpm(debug_backtrace(), "fb_canvas_fix_url($url)");
  global $base_url;

  // Url rewrites still used for iframe excursions.
  $patterns[] = "|{$base_url}/" . FB_SETTINGS_CB . "/{$fb_app->label}/|";

  // Here we hard-wire apps.facebook.com.  Is there an API to get that?
  $replacements[] = "http://apps.facebook.com/{$fb_app->canvas}/";

  // Page type helps themes support multiple iframes.
  $patterns[] = "|" . FB_SETTINGS_CB_TYPE . "/[^/]*/|";
  $replacements[] = "";
  $patterns[] = "|" . FB_SETTINGS_CB_SESSION . "/[^/]*/|";
  $replacements[] = "";

  // Fully qualified paths.
  $patterns[] = "|" . url('', array(
    'absolute' => TRUE,
  )) . "|";
  $replacements[] = "http://apps.facebook.com/{$fb_app->canvas}/";

  // Facebook will prepend "appNNN_" all our ids
  $patterns[] = "|#([^\\?]*)|";
  $replacements[] = "#app{$fb_app->id}_\$1";
  $url = preg_replace($patterns, $replacements, $url);
  return $url;
}

/**
 * Define custom_url_rewrite_outbound() if not defined already.
 */
if (!function_exists('custom_url_rewrite_outbound')) {
  function custom_url_rewrite_outbound(&$path, &$options, $original_path) {
    fb_canvas_url_outbound_alter($path, $options, $original_path);
  }
}
function fb_canvas_url_outbound_alter(&$path, &$options, $original_path) {
  global $_fb, $_fb_app;
  if ($_fb_app && $_fb_app->canvas) {
    if (fb_canvas_is_fbml()) {
      if (!$options['absolute']) {
        if (FALSE && file_exists($path)) {

          // Disabled on purpose
          // This is disabled because Drupal is unlikely to honor the changes
          // we make here.  Sadly, routines like theme_image() do not honor url
          // alters.
          // Make absolute link to local file.
          $options['absolute'] = TRUE;
        }
        else {

          // Make a relative link to the application.
          // base_path is not in options, so we kludge a faux absolute url.
          $options['base_url'] = '/' . $_fb_app->canvas;
          $options['absolute'] = TRUE;

          // Note that facebook will prepend canvas_name to our relative url.
        }
      }
    }
    else {
      if (fb_canvas_is_iframe()) {
        if (!$options['absolute']) {

          // Could append all 'fb_sig' params to internal links.  But for now we rely on fb_canvas_process.
        }
      }
    }

    // Drupal has a habit of adding ?destination=... to some URLs.
    // And Facebook for no good reason screws up when you do that.
    if ($options['query']) {
      $options['query'] = str_replace('destination=', 'fb_canvas_destination=', $options['query']);
    }
  }
}

/**
 * This function uses regular expressions to convert links on canvas pages 
 * to URLs that begin http://apps.facebook.com/...  
 * 
 * Call this method from themes when producing either FBML or iframe canvas
 * pages.  This is a relatively expensive operation.  Its unfortunate that we
 * must do it on every page request.  However to the best of my knowledge,
 * Drupal provides no better way.
 *
 * In Drupal 7.x, there should be a way to alter URLs before they are
 * rendered.  That could provide a more efficient solution.  Until
 * then we are stuck with this.
 * 
 * @param $output is the page (or iframe block) about to be returned.
 * 
 * @param $options - 'add_target' will cause target=_top to be added
 * when producing an iframe. 'force_absolute_canvas' will turn
 * relative urls for canvas pages into absolute, for use on profile
 * tabs.
 * 
 */
function fb_canvas_process($output, $options = array()) {
  global $base_url;
  global $_fb, $_fb_app;
  $patterns = array();
  $replacements = array();
  $base_path = base_path();
  if ($_fb) {
    if (function_exists('fb_url_outbound_alter')) {
      $base_before_rewrite = '';
      $rewrite_options = array();
      $base = $base_path . fb_url_outbound_alter($base_before_rewrite, $rewrite_options, '');

      // short URL with rewrite applied.
    }
    else {

      // Paving the way to make url alters optional.  (may not work properly)
      $base = $base_path;

      //print("in fb_canvas_process, base is '$base'.");
    }
    if (fb_canvas_is_fbml()) {

      //dpm($output, "before fb_canvas_process");

      // We're producing FBML for a canvas page
      // Fix for relative images
      $absolute_base = url('<front>', array(
        'absolute' => TRUE,
      ));
      $patterns[] = "|src=\"{$base}|";
      $replacements[] = "src=\"{$absolute_base}";

      // This fixes places in drupal that use base_url() . something, instead of calling url().
      // including links to stylesheets and such.
      $pattern = "|href=\"{$base}(?!" . preg_quote($_fb_app->canvas) . ")|";

      //print ("pattern is $pattern \n\n"); // debug
      $patterns[] = $pattern;
      $replacements[] = "href=\"{$absolute_base}";

      // This is needed under the old url rewriting scheme.
      // Hope to do away with this for good eventually!  XXX
      if (isset($options['force_absolute_canvas']) && $options['force_absolute_canvas'] && $base != '/') {

        // Change links to use canvas on Facebook
        // Links ending in #something:
        $patterns[] = "|=\"{$base}([^\"]*#)|";
        $replacements[] = "=\"http://apps.facebook.com/{$_fb_app->canvas}/\$1app{$_fb_app->id}_";

        // Other links
        $patterns[] = "|=\"{$base}|";
        $replacements[] = "=\"http://apps.facebook.com/{$_fb_app->canvas}/";
      }

      /*
      else {
        // Change links to use canvas on Facebook
        // Links ending in #something:
        $patterns[] = "|=\"{$base}([^\"]*#)|";
        $replacements[] = "=\"/{$_fb_app->canvas}/$1app{$_fb_app->id}_";
        // Other links
        $patterns[] = "|=\"{$base}|";
        $replacements[] = "=\"/{$_fb_app->canvas}/";
      }
      // Change paths to files to fully qualified URLs. This matches relative
      // URLs that do not include the canvas (that is, not matched by previous
      // patterns).
      if ($base_path != "/{$_fb_app->canvas}/") {
        $patterns[] = '|="'. $base_path . "(?!{$_fb_app->canvas})|";
        $replacements[] = '="'. $base_url .'/';
      }
      */

      // Experimental!  Change 1234@facebook to an <fb:name> tag.  This is our
      // default user name convention when creating new users.  Ideally, this
      // would be accomplished with something like:
      // http://drupal.org/node/102679.  In the meantime, this may help for
      // canvas pages only.
      // Regexp avoids "1234@facebook" (surrounded by quotes) because
      // that can appear in forms and FBML tags.  Also avoids
      // 1234@facebook.com, which can also appear in forms because it
      // is used in authmaps.  TODO: investigate the efficiency of
      // this regexp (and/or make it optional)
      $patterns[] = '|(?<!["\\d])([\\d]*)@facebook(?!\\.com)|';
      $replacements[] = '<fb:name uid=$1 linked=false ifcantsee="$1@facebook" useyou=false />';
    }
    elseif (fb_canvas_is_iframe()) {

      // Add target=_top so that entire pages do not appear within an iframe.
      // TODO: make these pattern replacements more sophisticated, detect whether target is already set.
      if (isset($options['add_target']) && $options['add_target']) {

        // Add target=_top to all links
        $patterns[] = "|<a ([^>]*)href=\"|";
        $replacements[] = "<a \$1 target=\"_top\" href=\"";

        // Do not change local forms, but do change external ones
        $patterns[] = "|<form([^>]*)action=\"([^:\"]*):|";
        $replacements[] = "<form target=\"_top\" \$1 action=\"\$2:";

        // Make internal links point to canvas pages
        $patterns[] = "|<a([^>]*)href=\"{$base}|";
        $replacements[] = "<a \$1 href=\"http://apps.facebook.com/{$_fb_app->canvas}/";
      }
      else {

        // Add target=_top to only external links
        $patterns[] = "|<a([^>]*)href=\"([^:\"]*):|";
        $replacements[] = "<a target=\"_top\" \$1 href=\"\$2:";
        $patterns[] = "|<form([^>]*)action=\"([^:\"]*):|";
        $replacements[] = "<form target=\"_top\" \$1 action=\"\$2:";
      }
    }
  }
  if (count($patterns)) {
    $count = 0;
    $return = preg_replace($patterns, $replacements, $output, -1, $count);

    //print ("fb_canvas_process replaced $count.\n\n"); // debug
    return $return;
  }
  else {
    return $output;
  }
}

//// Theme definitions, overrides and helpers.

/**
 * Implements hook_theme().
 *
 * We use theme function for FBML username and picture so that the
 * markup can be relatively easily customized.
 */
function fb_canvas_theme() {
  return array(
    'fb_canvas_fbml_username' => array(
      'arguments' => array(
        'fbu' => NULL,
        'object' => NULL,
        'orig_username' => NULL,
      ),
    ),
    'fb_canvas_fbml_user_picture' => array(
      'arguments' => array(
        'fbu' => NULL,
        'account' => NULL,
        'orig' => NULL,
      ),
    ),
  );
}

/**
 * Implementation of hook_theme_registry_alter().
 *
 * Wrap original theme functions in our overrides.
 */
function fb_canvas_theme_registry_alter(&$theme_registry) {

  // Ideally, we'd do this only on themes which will certainly be used for FBML canvas pages.
  if ($theme_registry['username']['type'] == 'module') {

    // Override theme_username
    $theme_registry['fb_canvas_username_orig'] = $theme_registry['username'];
    $theme_registry['username'] = array(
      'arguments' => array(
        'object' => NULL,
      ),
      'function' => 'fb_canvas_theme_username_override',
      'type' => 'module',
    );
  }
  if ($theme_registry['user_picture']['type'] == 'module') {

    // Override theme_user_picture
    $theme_registry['fb_canvas_user_picture_orig'] = $theme_registry['user_picture'];
    $theme_registry['user_picture'] = array(
      'arguments' => array(
        'account' => NULL,
      ),
      'function' => 'fb_canvas_theme_user_picture_override',
      'type' => 'module',
    );
  }
}

/**
 * Build tabs in facebook-aware theme.
 */
function _fb_canvas_menu_hack() {
  if (fb_canvas_is_fbml()) {

    // We have to go out of our way here to theme the tabs.
    // The code in menu.inc that themes them is complex,
    // incomprehensible, and tangles the theme layer with the logic
    // layer.  It doesn't help that the same theme functions are called
    // for tabs as are called for all other menus.  So we use a global
    // to keep track of what we're doing.
    global $_fb_canvas_state;
    $_fb_canvas_state = 'tabs';

    // Why does a call to menu_tab_root_path theme the tabs?  I have no
    // idea, but it does and caches the result.
    menu_tab_root_path();
    $_fb_canvas_state = NULL;
  }
}

/**
 * Our replacement for theme('username', ...)
 */
function fb_canvas_theme_username_override($object) {
  $orig = theme('fb_canvas_username_orig', $object);

  // Make no changes to non-FBML pages.
  if (fb_canvas_is_fbml() && ($fbu = fb_get_object_fbu($object))) {

    // Theme the username with FBML, using original username as backup.
    return theme('fb_canvas_fbml_username', $fbu, $object, $orig);
  }
  else {
    return $orig;
  }
}

/**
 * Theme FBML markup for username.
 */
function theme_fb_canvas_fbml_username($fbu, $object, $orig) {
  if (!$fbu) {
    return $orig;
  }
  $wrap_pre = '';
  $wrap_post = '';
  if ($object->uid && user_access('access user profiles')) {

    // Provide link if local account.
    $wrap_pre = $wrap_pre . '<a href="' . url('user/' . $object->uid) . '">';
    $wrap_post = '</a>' . $wrap_post;
    $ifcantsee = 'ifcantsee="' . addslashes(check_plain($object->name)) . '"';
  }
  $fbml = "<fb:name linked=false useyou=false uid=\"{$fbu}\" {$ifcantsee}></fb:name>";
  return $wrap_pre . $fbml . $wrap_post;
}

/**
 * Our replacement for theme('user_picture', ...)
 */
function fb_canvas_theme_user_picture_override($object) {
  $orig = theme('fb_canvas_user_picture_orig', $object);

  // Respect Drupal's profile pic, if uploaded.
  if (isset($object->picture) && $object->picture) {
    return $orig;
  }

  // Make no changes to non-FBML pages.
  if (fb_canvas_is_fbml() && ($fbu = fb_get_object_fbu($object))) {

    // Theme the username with FBML, using original username as backup.
    return theme('fb_canvas_fbml_user_picture', $fbu, $object, $orig);
  }
  else {
    return $orig;
  }
}

/**
 * Theme FBML markup for user_picture.
 */
function theme_fb_canvas_fbml_user_picture($fbu, $object, $orig) {
  if (!$fbu) {
    return $orig;
  }
  $fbml = "<fb:profile-pic linked=false uid=\"{$fbu}\"></fb:profile-pic>";
  $wrap_pre = '';
  $wrap_post = '';
  if ($object->uid && user_access('access user profiles')) {

    // Provide link if local account.
    $wrap_pre = $wrap_pre . '<a href="' . url('user/' . $object->uid) . '">';
    $wrap_post = '</a>' . $wrap_post;
  }
  return $wrap_pre . $fbml . $wrap_post;
}

Functions

Namesort descending Description
fb_canvas_fb Implementation of hook_fb().
fb_canvas_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_canvas_footer Implementation of hook_footer().
fb_canvas_form_action_via_facebook Call this from your form_alter hook to prevent changes to the form's default action.
fb_canvas_form_alter Implementation of hook_form_alter.
fb_canvas_goto Uses $_fb->redirect on canvas pages, otherwise drupal_goto.
fb_canvas_handling_form
fb_canvas_iframe_excursion This is intended to help sites which have some FBML and some iframe pages. Makes the current FBML canvas page an iframe. And attempts to preserve session while visiting multiple iframe pages.
fb_canvas_iframe_excursion_end
fb_canvas_is_fbml
fb_canvas_is_iframe
fb_canvas_menu Implementation of hook_menu().
fb_canvas_process This function uses regular expressions to convert links on canvas pages to URLs that begin http://apps.facebook.com/...
fb_canvas_theme Implements hook_theme().
fb_canvas_theme_registry_alter Implementation of hook_theme_registry_alter().
fb_canvas_theme_username_override Our replacement for theme('username', ...)
fb_canvas_theme_user_picture_override Our replacement for theme('user_picture', ...)
fb_canvas_url_outbound_alter
theme_fb_canvas_fbml_username Theme FBML markup for username.
theme_fb_canvas_fbml_user_picture Theme FBML markup for user_picture.
_fb_canvas_get_config Helper returns configuration for this module, on a per-app basis.
_fb_canvas_make_form_action_local
_fb_canvas_menu_hack Build tabs in facebook-aware theme.

Constants