Skip to content
fb_canvas.module 14.8 KiB
Newer Older
Dave Cohen's avatar
Dave Cohen committed
/**
 * @file
 * This module provides support for Canvas page applications.  Use
Dave Cohen's avatar
Dave Cohen committed
 * Drupal to power traditional Facebook Apps.
 *
 * See also fb_connect.module for Facebook Connect.
Dave Cohen's avatar
Dave Cohen committed
 */
// 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');
Dave Cohen's avatar
Dave Cohen committed
define('FB_CANVAS_VAR_PROCESS_ABSOLUTE', 'fb_canvas_process_absolute_links');
Dave Cohen's avatar
Dave Cohen committed
/**
 * Implementation of hook_menu().
 */
function fb_canvas_menu() {
  $items = array();
Dave Cohen's avatar
Dave Cohen committed

  // 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,
  );
Dave Cohen's avatar
Dave Cohen committed

  return $items;
}


/**
 * Implementation of hook_fb().
Dave Cohen's avatar
Dave Cohen committed
 */
function fb_canvas_fb($op, $data, &$return) {
  $fb = isset($data['fb']) ? $data['fb'] : NULL;
  $fb_app = isset($data['fb_app']) ? $data['fb_app'] : NULL;
Dave Cohen's avatar
Dave Cohen committed

    if (function_exists('fb_settings')) {
Dave Cohen's avatar
Dave Cohen committed
      if ((fb_settings(FB_SETTINGS_TYPE) == FB_SETTINGS_TYPE_CANVAS)) {
        // fb_settings.inc has determined this is a canvas page.
        if ($app_key = fb_settings(FB_SETTINGS_CB)) {
Dave Cohen's avatar
Dave Cohen committed
          // Using fb_url_rewrite.
          $fb_app = fb_get_app(array('apikey' => $app_key));
          if (!$fb_app) {
            // DEPRECATED.  For backward compatibility, accept label in FB_SETTINGS_CB
            $fb_app = fb_get_app(array('label' => $app_key));
          }
Dave Cohen's avatar
Dave Cohen committed
        }
        elseif ($id = fb_settings(FB_SETTINGS_ID)) {
          // New SDK includes ID when session is present.
          $fb_app = fb_get_app(array('id' => $id));
        }
        elseif ($apikey = fb_settings(FB_SETTINGS_APIKEY)) {
          // Old SDK tells us APIKEY.  Deprecated.
          $fb_app = fb_get_app(array('apikey' => $apikey));
  elseif ($op == FB_OP_INITIALIZE) {
    // Get our configuration settings.
    $fb_canvas_data = _fb_canvas_get_config($fb_app);
Dave Cohen's avatar
Dave Cohen committed

Dave Cohen's avatar
Dave Cohen committed
    $use_ob = FALSE;
Dave Cohen's avatar
Dave Cohen committed

    // Set an app-specific theme.
    global $custom_theme; // Set by this function.
    if (fb_canvas_is_iframe()) {
      $custom_theme = $fb_canvas_data['theme_iframe'];
Dave Cohen's avatar
Dave Cohen committed
      $use_ob = variable_get(FB_CANVAS_VAR_PROCESS_IFRAME, TRUE);
Dave Cohen's avatar
Dave Cohen committed

    if ($is_canvas) {
      // We are serving a canvas page.
      $conf['admin_theme'] = $custom_theme;
      // Hack to init the theme before _drupal_maintenance_theme initializes the wrong one.
      if (variable_get('site_offline', FALSE)) {
        $dummy = theme('dummy');
      }
Dave Cohen's avatar
Dave Cohen committed
    }
    // Store entire page in output buffer.  Will post-process on exit.
    if ($use_ob) {
      ob_start();
      $GLOBALS['fb_canvas_post_process'] = TRUE;
    }
Dave Cohen's avatar
Dave Cohen committed

    if ($is_canvas &&
        $_GET['q'] == drupal_get_normal_path(
          variable_get('site_frontpage', 'node'))) {
        $front = $fb_canvas_data['front_added'];
        $front = $fb_canvas_data['front_anonymous'];
      if ($front)
        menu_set_active_item(drupal_get_normal_path($front));
    }
  }
  elseif ($op == FB_OP_POST_INIT) {
    if (fb_canvas_is_iframe()) {
      // 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'];
      }
      // Include our javascript.
      drupal_add_js(array(
                      'fb_canvas' => array(
                        'fbu' => fb_facebook_user(),
                        'uid' => $GLOBALS['user']->uid,
                        'canvas' => $fb_app->canvas,
                      ),
                    ), 'setting');
      drupal_add_js(drupal_get_path('module', 'fb_canvas') . '/fb_canvas.js');
    // 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.
    */
Dave Cohen's avatar
Dave Cohen committed
    if (isset($GLOBALS['fb_canvas_post_process']) &&
        $GLOBALS['fb_canvas_post_process']) {
      $output = ob_get_contents();
      ob_end_clean();
      if (fb_canvas_is_iframe()) {
Dave Cohen's avatar
Dave Cohen committed
        $output = fb_canvas_process($output, array(
                                      'add_target' => TRUE,
                                      'absolute_links' => variable_get(FB_CANVAS_VAR_PROCESS_ABSOLUTE, TRUE),
                                    ));
    if (fb_canvas_is_iframe() &&
        (!isset($GLOBALS['_fb_canvas_goto']))) {
        // 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_canvas_redirect($app_destination);
    if (isset($output)) {
      print($output);
    }
function fb_canvas_redirect($url) {
  echo "<script type=\"text/javascript\">\ntop.location.href = \"$url\";\n</script>";
  exit;
}

/**
 * Is the current request being displayed in an iframe canvas page?
 */
function fb_canvas_is_iframe() {
  // Use either parameters passed from facebook, or url rewriting.
  return (fb_settings(FB_SETTINGS_TYPE) == FB_SETTINGS_TYPE_CANVAS);
/**
 * Helper returns configuration for this module, on a per-app basis.
 */
function _fb_canvas_get_config($fb_app) {
Dave Cohen's avatar
Dave Cohen committed
  $fb_canvas_data = isset($fb_app_data['fb_canvas']) ? $fb_app_data['fb_canvas'] : array();
    'require_login' => FB_CANVAS_OPTION_ALLOW_ANON, // @TODO - can this still be supported?
    'theme_fbml' => 'fb_fbml',
    'theme_iframe' => 'fb_fbml',
Dave Cohen's avatar
Dave Cohen committed
    'front_anonymous' => NULL,
    'front_loggedin' => NULL, // Facebook API no longer supports this.
Dave Cohen's avatar
Dave Cohen committed
    'front_added' => NULL,
function fb_canvas_form_alter(&$form, &$form_state, $form_id) {
  if (isset($form['fb_app_data']) && is_array($form['fb_app_data'])) {
    // Add our settings to the fb_app edit form.
    //require 'fb_canvas.admin.inc';
    fb_canvas_admin_form_alter($form, $form_state, $form_id);
Dave Cohen's avatar
Dave Cohen committed
  }
 * Uses javascript on iframe canvas pages change top frame, otherwise drupal_goto().
 *
 * @see drupal_goto()
 */
function fb_canvas_goto($path) {
  if ($_fb && 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);
    }
/**
 * 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 canvas pages.
  $patterns[] = "|{$base_url}/" . FB_SETTINGS_CB . "/{$fb_app->apikey}/|";
  // Here we hard-wire apps.facebook.com.  Is there an API to get that?
  $replacements[] = "http://apps.facebook.com/{$fb_app->canvas}/";
  // 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);

/**
 * Define custom_url_rewrite_outbound() if not defined already.
 *
 * The bulk of URL rewriting is performed in fb_url_rewrite.inc.  That file
 * should be included in settings.php.  The url rewriting below was originally
 * an attempt to define those function here, only for canvas pages.  That
 * turned out to not be possible, so now the function just changes the
 * destination parameter to not confict with facebook's parameter of the same
 * name.
 *
 * For best results, admins should include fb_url_rewrite in their settings.php.
 *
 * @see fb_url_rewrite.inc
 */
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);
/**
 * Implements hook_url_outbound_alter().
 *
 * @param $options
 *   If $options['fb_canvas'] == TRUE, create an absolute URL to a canvas
 *   page.  The URL will begin http://apps.facebook.com/...  Also if
 *   $options['fb_canvas'] is an application label the url will link to that
 *   particular application.
function fb_canvas_url_outbound_alter(&$path, &$options, $original_path) {
  if (isset($options['fb_canvas']) && is_string($options['fb_canvas'])) {
    $fb_app = fb_get_app(array('label' => $options['fb_canvas']));
  }
  else {
    $fb_app = isset($GLOBALS['_fb_app']) ? $GLOBALS['_fb_app'] : NULL;
  }

  if ($fb_app && isset($fb_app->canvas)) {
    if ((!isset($options['fb_url_alter']) || $options['fb_url_alter']) &&
        (isset($options['fb_canvas']) && $options['fb_canvas'])) {
      // Make a url starting with apps.facebook.com/...
      $options['external'] = TRUE;
      $options[FB_SETTINGS_CB] = FALSE; // prevent fb_url_rewrite.inc from altering.
      $options['base_url'] = 'http://apps.facebook.com/' . $fb_app->canvas;
    if (fb_canvas_is_iframe()) {
        // Could append session param to internal links.  But for now we rely on fb_canvas_process.
Dave Cohen's avatar
Dave Cohen committed
      else {
        //dpm($options, "fb_canvas_url_outbound_alter($path)");
      }
      // 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']);
      }
Dave Cohen's avatar
Dave Cohen committed
/**
Dave Cohen's avatar
Dave Cohen committed
 * 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 iframe canvas
Dave Cohen's avatar
Dave Cohen committed
 * pages, or from a page_preprocess hook.
 *
 * This is a relatively expensive operation.  In the past, this had to be run
 * on all FBML canvas pages.  Now with iframe canvas pages, this is not
 * strictly needed, but remains useful.  If processed, links on the page will
 * change the parent frame (the one with the URL shown in the browser).  If
 * not processed, the links will change only the iframe.
 * In Drupal 7.x, there may be a way to alter URLs before they are
 * rendered.  That could provide a more efficient solution.  Until
 * then we are stuck with this.
Dave Cohen's avatar
Dave Cohen committed
 * @param $output is the page (or iframe block) about to be returned.
Dave Cohen's avatar
Dave Cohen committed
 *
Dave Cohen's avatar
Dave Cohen committed
 * @param $options - 'add_target' will cause target=_top to be added
Dave Cohen's avatar
Dave Cohen committed
 * when producing an iframe. 'absolute_links' will change hrefs with absolute
 * URLs to refer to canvas pages.
Dave Cohen's avatar
Dave Cohen committed
 *
Dave Cohen's avatar
Dave Cohen committed
 */
Dave Cohen's avatar
Dave Cohen committed
function fb_canvas_process($output, $options = array()) {
  $patterns = array();
  $replacements = array();
    if (function_exists('fb_url_outbound_alter')) {
      $base_before_rewrite = '';
      $rewrite_options = array(FB_SETTINGS_CB_SESSION => FALSE, FB_SETTINGS_CB_TYPE => FALSE);
      $base = $base_path . fb_url_outbound_alter($base_before_rewrite, $rewrite_options, '');  // short URL with rewrite applied.
    }
    else {
Dave Cohen's avatar
Dave Cohen committed
      // If no url_alter, use normal base_path.
    // Make relative links point to canvas pages.
    $patterns[] = "|<a([^>]*)href=\"{$base}|";
    $replacements[] = "<a $1 href=\"http://apps.facebook.com/{$_fb_app->canvas}/";
    // 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:";
    }
    elseif (!isset($options['add_target'])) {
      // 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 (isset($options['absolute_links']) && $options['absolute_links']) {
      // Make absolute links point to canvas pages.
      $absolute_base = url('<front>', array('absolute' => TRUE));
      $patterns[] = "|<a([^>]*)href=\"{$absolute_base}|";
      $replacements[] = "<a $1 href=\"http://apps.facebook.com/{$_fb_app->canvas}/";
    $count = 0;
    $return = preg_replace($patterns, $replacements, $output, -1, $count);
    //print ("fb_canvas_process replaced $count.\n\n"); // debug