You are here

fb_canvas.module in Drupal for Facebook 5

This module provides some app-specific navigation to facebook apps. This is fairly experimental material. May change a lot in near future.

File

fb_canvas.module
View source
<?php

/**
 * @file
 * 
 * This module provides some app-specific navigation to facebook apps.  This
 * is fairly experimental material.  May change a lot in near future.
 * 
 */

/**
 * Implementation of hook_fb.
 */
function fb_canvas_fb($fb, $fb_app, $op, &$return) {
  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'];

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

    // Set by this function.
    if (fb_canvas_is_fbml()) {
      $custom_theme = $fb_canvas_data['theme_fbml'];
    }
    else {
      if (fb_canvas_is_iframe()) {
        $custom_theme = $fb_canvas_data['theme_iframe'];
      }
    }

    // This is for backward compatability (delete eventually)
    if (!$custom_theme) {
      $custom_theme = variable_get('fb_theme', 'fb_fbml');
    }

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

    //drupal_set_message("fb_canvas_fb" . dpr($_GET, 1) . 'arg(0) = ' . dpr(arg(0), 1) . 'arg(1) = ' . dpr(arg(1), 1) . 'frontpage ' . dpr(variable_get('site_frontpage', 'node'), 1));
    if ($_GET['q'] == drupal_get_normal_path(variable_get('site_frontpage', 'node'))) {
      if ($fb->api_client
        ->users_isAppAdded()) {
        $front = $fb_canvas_data['front_added'];
      }
      else {
        if ($fb
          ->get_loggedin_user()) {
          $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
        }
      }
    }
  }
}

/**
 * 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'),
      '#description' => t('Leave blank to use the site-wide front page.'),
      '#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, but has not added 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 when user has added 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()) {

    // We're in a facebook callback
    // Can't use 'fb_handling_form' because facebook strips it.
    $form['_fb_handling_form'] = array(
      '#value' => TRUE,
      '#type' => 'hidden',
    );

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

    // Let's hope no subsequent hook_form_alters mess with #action.
  }
  else {
    if (fb_canvas_is_iframe()) {

      //dpm($form, 'fb_canvas_form_alter');
    }
  }
}
function fb_canvas_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) {
  if ($op == 'view' && $node->type == 'fb_app') {

    //dpm(func_get_args(), 'fb_canvas_nodeapi');
    if (user_access('administer fb apps')) {
      $fb_app = $node->fb_app;
      $output = theme('dl', array(
        t('Canvas page') => "http://apps.facebook.com/{$fb_app->canvas}",
        t('Callback URL') => t("%clean_url<br /> (or %advanced_url only if your theme is designed to support PAGE_TYPE.)<br/>Make sure you have enabled clean URLs, and include the trailing '/'.", array(
          "%clean_url" => url('', NULL, NULL, TRUE) . FB_SETTINGS_APP_NID . '/' . $node->nid . '/',
          "%advanced_url" => url('', NULL, NULL, TRUE) . FB_SETTINGS_APP_NID . '/' . $node->nid . '/' . FB_SETTINGS_PAGE_TYPE . '/PAGE_TYPE/',
        )),
      ));
      $node->content['fb_canvas'] = array(
        '#value' => $output,
        '#weight' => 2,
      );
    }
  }
}
function fb_canvas_is_fbml() {
  global $fb, $fb_app;
  if ($fb && $fb_app) {
    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;
}

/**
 * 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).
      $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.
      $patterns[] = '|([\\d]*)@facebook|';
      $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_form_alter Implementation of hook_form_alter.
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