Skip to content
fb_canvas.module 14.7 KiB
Newer Older
<?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
 * module 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();
}

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
	  $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.'/';
	}
	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;
}
?>