You are here

fb_canvas.module in Drupal for Facebook 5.2

This module provides some app-specific navigation to facebook apps.

File

fb_canvas.module
View source
<?php

/**
 * @file
 * 
 * This module provides some app-specific navigation to facebook apps.
 * 
 */

/**
 * Implementation of hook_fb.
 */
function fb_canvas_fb($op, $data, &$return) {
  $fb = $data['fb'];
  $fb_app = $data['fb_app'];

  //watchdog('XXX', "fb_canvas_fb($op) data is " . dprint_r($data, 1));
  if ($op == FB_OP_CURRENT_APP) {
    if ($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,
      ));
    }
    else {
      if (function_exists('fb_settings')) {

        // See fb_settings.inc
        if ($nid = fb_settings(FB_SETTINGS_APP_NID)) {

          // Here if we're in iframe, using our /fb_canvas/nid/ path convention.
          $fb_app = fb_get_app(array(
            'nid' => $nid,
          ));
        }
      }
    }
    if ($fb_app) {
      $return = $fb_app;
    }
  }
  else {
    if ($op == FB_OP_INITIALIZE) {

      // Get our configuration settings.
      $fb_app_data = fb_app_get_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;
      }
      else {
        if (fb_canvas_is_iframe()) {
          $custom_theme = $fb_canvas_data['theme_iframe'];
          $is_canvas = TRUE;
        }
      }

      // Special handling for forms, as they are submitted directly to us, not
      // to apps.facebook.com/canvas
      // we will buffer, and later cache, the results.
      if (fb_canvas_handling_form()) {
        ob_start();
      }
      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));
        }
      }
    }
    else {
      if ($op == FB_OP_EXIT) {
        $destination = $return;
        if (fb_canvas_handling_form() && $fb_app) {
          $output = ob_get_contents();
          ob_end_clean();
          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.
            $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.
            // Will this preempt other hook_exits?
            if ($fb) {
              $fb
                ->redirect($destination);
            }
          }
          else {

            // Save the results to show the user later
            $token = uniqid('fb_');
            $cid = session_id() . "_{$token}";
            watchdog('fb', "Storing cached form page {$cid}, then redirecting");
            cache_set($cid, 'cache_page', $output, time() + 60 * 5, drupal_get_headers());

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

            // $fb->redirect($url); // 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']);
              }
            }
            drupal_goto($dest, implode('&', $query), NULL, 303);

            // appears to work
          }
        }
      }
      else {
        if ($op == FB_OP_SET_PROPERTIES) {

          // Compute properties which we can set automatically.
          $callback_url = url('', NULL, NULL, TRUE) . FB_SETTINGS_APP_NID . '/' . $fb_app->nid . '/';
          $return['callback_url'] = $callback_url;
        }
        else {
          if ($op == FB_OP_LIST_PROPERTIES) {
            $return[t('Callback URL')] = 'callback_url';
            $return[t('Canvas Page Suffix')] = 'canvas_name';
          }
        }
      }
    }
  }
}

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

  // Add our settings to the fb_app edit form.
  if (is_array($form['fb_app_data'])) {
    $node = $form['#node'];
    $fb_app_data = fb_app_get_data($node->fb_app);
    $fb_canvas_data = $fb_app_data['fb_canvas'];

    // defaults
    if (!$fb_canvas_data) {
      $fb_canvas_data = array();
    }
    $fb_canvas_data = array_merge(array(
      'theme_fbml' => 'fb_fbml',
      'theme_iframe' => 0,
    ), $fb_canvas_data);
    $form['fb_app_data']['fb_canvas'] = array(
      '#type' => 'fieldset',
      '#collapsible' => TRUE,
      '#collapsed' => TRUE,
      '#title' => t('Facebook canvas page settings'),
      '#description' => t('Allows application-specific front page and navigation links.'),
    );
    $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 users of this application'),
      '#description' => t('Leave blank to use the site-wide front page.'),
      '#default_value' => $fb_canvas_data['front_added'],
    );

    // Allow primary links to be different on facebook versus the rest of the
    // site.  Code from menu_configure() in menu.module.
    $root_menus = menu_get_root_menus();
    $primary_options = $root_menus;
    $primary_options[0] = t('<use sitewide setting>');
    $secondary_options = $root_menus;
    $secondary_options[0] = t('<use sitewide setting>');
    $form['fb_app_data']['fb_canvas']['primary_links'] = array(
      '#type' => 'select',
      '#title' => t('Menu containing primary links'),
      '#description' => t('Your application can have primary links different from those used elsewhere on your site.'),
      '#default_value' => $fb_canvas_data['primary_links'],
      '#options' => $primary_options,
    );
    $form['fb_app_data']['fb_canvas']['secondary_links'] = array(
      '#type' => 'select',
      '#title' => t('Menu containing secondary links'),
      '#default_value' => $fb_canvas_data['secondary_links'],
      '#options' => $secondary_options,
      '#description' => t('If you select the same menu as primary links then secondary links will display the appropriate second level of your navigation hierarchy.'),
    );

    // 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'],
    );
  }
  global $fb, $fb_app;

  // 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'])) {
      $form['fb_canvas_form_handler'] = array();

      // This variable tells us to handle the form on submit.
      // Can't use 'fb_handling_form' because facebook strips it.
      $form['fb_canvas_form_handler']['_fb_handling_form'] = array(
        '#value' => TRUE,
        '#type' => 'hidden',
      );

      // We need to make sure the action goes to our domain and not apps.facebook.com, so here we tweak the form action.
      $form['fb_canvas_form_handler']['#action_old'] = $form['action'];
      if ($form['#action'] == '') {
        $form['#action'] = $_GET['q'];
      }
      $form['#action'] = _fb_canvas_make_form_action_local($form['#action']);
      $form['fb_canvas_form_handler']['#action_new'] = $form['#action'];

      // We've stored #action_old and #action_new so custom modules have the option to change it back.
    }
  }
  else {
    if (fb_canvas_is_iframe()) {

      //dpm($form, 'fb_canvas_form_alter');
    }
  }
}

/**
 * Call this from your form_alter hook to prevent changes to the
 * form's default action.
 */
function fb_canvas_form_action_via_facebook(&$form) {
  if ($form['fb_canvas_form_handler']) {
    $form['#action'] = $form['fb_canvas_form_handler']['#action_old'];
  }
  $form['fb_canvas_form_handler'] = array();
}
function fb_canvas_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if ($op == 'view' && $node->type == 'fb_app') {
    if (user_access('administer fb apps')) {
      $fb_app = $node->fb_app;
      $output = theme('dl', array(
        t('Canvas URL') => "http://apps.facebook.com/{$fb_app->canvas}",
      ));
      $node->content['fb_canvas'] = array(
        '#value' => $output,
        '#weight' => 2,
      );
    }
  }
}
function fb_canvas_footer($is_front) {
  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());

    //dpm($extra, "FB_OP_CANVAS_FBJS_INIT returning"); // XXX
    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() {
  global $fb;

  // Test whether a form has been submitted via facebook canvas page.
  if ($fb && $_REQUEST['form_id'] && $_REQUEST['_fb_handling_form']) {
    return TRUE;
  }
}

// This may need work
function _fb_canvas_make_form_action_local($action) {

  // If action is fully qualified, do not change it
  if (strpos($action, ':')) {
    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) {
    $action = array_shift($parts) . '?' . array_shift($parts);
    $action .= '&' . implode('&', $parts);
  }

  //drupal_set_message("form action now " . "http://".$_SERVER['HTTP_HOST']. $action); // debug
  return "http://" . $_SERVER['HTTP_HOST'] . $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, NULL, NULL, TRUE), $fb_app);
    $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, I believe.
 */
function fb_canvas_fix_url($url, $fb_app) {
  global $base_url;
  $patterns[] = "|{$base_url}/" . FB_SETTINGS_APP_NID . "/{$fb_app->nid}/|";

  // Here we assume apps.facebook.com.  Is this safe?
  $replacements[] = "http://apps.facebook.com/{$fb_app->canvas}/";
  $patterns[] = "|fb_cb_type/[^/]*/|";
  $replacements[] = "";

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

/**
 * Returns the 'type' of the page.  This helps themes determine whether they
 * are to provide an iframe or an iframe within FBML.
 */
function fb_canvas_page_type() {
  return fb_settings(FB_SETTINGS_PAGE_TYPE);
}
function fb_canvas_primary_links() {
  global $fb_app;
  $mid = 0;
  if ($fb_app) {
    $fb_app_data = fb_app_get_data($fb_app);
    $fb_canvas_data = $fb_app_data['fb_canvas'];
    $mid = $fb_canvas_data['primary_links'];
  }
  if ($mid) {
    return menu_primary_links(1, $mid);
  }
  else {
    return menu_primary_links();
  }
}
function fb_canvas_secondary_links() {
  global $fb_app;
  if ($fb_app) {
    $fb_app_data = fb_app_get_data($fb_app);
    $fb_canvas_data = $fb_app_data['fb_canvas'];
    $mid1 = $fb_canvas_data['primary_links'];
    $mid2 = $fb_canvas_data['secondary_links'];
  }
  if ($mid2) {
    if ($mid2 == $mid1) {
      return menu_primary_links(2, $mid2);
    }
    else {
      return menu_primary_links(1, $mid2);
    }
  }
  else {
    return menu_secondary_links();
  }
}

/**
 * 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.
 * 
 * @param $output is the page (or iframe block) about to be returned.
 * 
 * @param $add_target will cause target=_top to be added when producing an
 * iframe.
 * 
 */
function fb_canvas_process($output, $add_target = TRUE) {
  global $base_path, $base_url;
  global $fb, $fb_app;
  $patterns = array();
  $replacements = array();
  if ($fb) {
    $page_type = fb_settings(FB_SETTINGS_PAGE_TYPE);
    $nid = $fb_app->nid;
    $base = url();

    // short URL with rewrite applied.
    if (fb_canvas_is_fbml()) {

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

      // We're producing FBML for a canvas page
      // 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}/";

      // Workaround Drupal does not let us rewrite the frontpage url

      /* Not needed thanks to: http://drupal.org/node/241878
      	 $patterns[] = "|=\"{$base_path}\"|";
      	 $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 some forms.  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 />';
    }
    else {

      // In 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 ($add_target) {

        // Add target=_top to all links
        $patterns[] = "|<a |";
        $replacements[] = "<a target=\"_top\" ";

        // 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:";
      }

      // Workaround Drupal does not let us rewrite the frontpage url

      //$patterns[] = "|=\"{$base_path}\"|";

      //$replacements[] = "=\"{$base_path}fb_canvas/{$page_type}/{$nid}/\"";	 // XXX
    }
  }
  if (count($patterns)) {
    $return = preg_replace($patterns, $replacements, $output);
    return $return;
  }
  else {
    return $output;
  }
}

//This API needs testing and may need to be improved...

/**
 * Similar to fb_canvas_process, this also uses regular expressions to alter
 * link destinations.  Use this function when producing FBML for a profile box
 * or news feed, and the pages need to link to canvas pages rather than the
 * default URL.
 * 
 */
function fb_canvas_process_fbml($output, $fb_app) {
  $patterns = array();
  $replacements = array();
  $base = url();

  // short URL with rewrite applied.
  if ($fb_app->canvas) {

    // Change links to use canvas on Facebook
    $patterns[] = "|href=\"{$base}|";
    $replacements[] = "href=\"http://apps.facebook.com/{$fb_app->canvas}/";
  }
  if (count($patterns)) {
    $return = preg_replace($patterns, $replacements, $output);
    return $return;
  }
  else {
    return $output;
  }
}

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, I believe.
fb_canvas_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_is_fbml
fb_canvas_is_iframe
fb_canvas_nodeapi
fb_canvas_page_type Returns the 'type' of the page. This helps themes determine whether they are to provide an iframe or an iframe within FBML.
fb_canvas_primary_links
fb_canvas_process This function uses regular expressions to convert links on canvas pages to URLs that begin http://apps.facebook.com/...
fb_canvas_process_fbml Similar to fb_canvas_process, this also uses regular expressions to alter link destinations. Use this function when producing FBML for a profile box or news feed, and the pages need to link to canvas pages rather than the default URL.
fb_canvas_secondary_links
_fb_canvas_make_form_action_local