boost.module 10.8 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87
<?php
// $Id$

/**
 * @file
 * Provides static page caching for Drupal.
 */

//////////////////////////////////////////////////////////////////////////////
// BOOST SETTINGS

define('BOOST_PATH',                 dirname(__FILE__));
define('BOOST_FRONTPAGE',            drupal_get_normal_path(variable_get('site_frontpage', 'node')));

define('BOOST_ENABLED',              variable_get('boost', CACHE_DISABLED));
define('BOOST_FILE_PATH',            variable_get('boost_file_path', 'cache'));
define('BOOST_FILE_EXTENSION',       variable_get('boost_file_extension', '.html'));
define('BOOST_CACHEABILITY_OPTION',  variable_get('boost_cacheability_option', 0));
define('BOOST_CACHEABILITY_PAGES',   variable_get('boost_cacheability_pages', ''));
define('BOOST_FETCH_METHOD',         variable_get('boost_fetch_method', 'php'));
define('BOOST_PRE_PROCESS_FUNCTION', variable_get('boost_pre_process_function', ''));
define('BOOST_POST_UPDATE_COMMAND',  variable_get('boost_post_update_command', ''));
define('BOOST_CRON_LIMIT',           variable_get('boost_cron_limit', 100));

// This cookie is set for all logged-in users, so that they can be excluded
// from caching (or, in the future, get a user-specific cached page):
define('BOOST_COOKIE',               variable_get('boost_cookie', 'DRUPAL_UID'));

// This line is appended to the generated static files; it is very useful
// for troubleshooting (e.g. determining whether one got the dynamic or
// static version):
define('BOOST_BANNER',               variable_get('boost_banner', "<!-- Page cached by Boost at %date -->\n"));

// This is needed since the $user object is already destructed in _boost_ob_handler():
define('BOOST_USER_ID',              $GLOBALS['user']->uid);

//////////////////////////////////////////////////////////////////////////////
// BOOST INCLUDES

require_once BOOST_PATH . '/boost.helpers.inc';
require_once BOOST_PATH . '/boost.api.inc';

//////////////////////////////////////////////////////////////////////////////
// DRUPAL API HOOKS

/**
 * Implementation of hook_help(). Provides online user help.
 */
function boost_help($section) {
  switch ($section) {
    case 'admin/modules#name':
      return t('boost');
    case 'admin/modules#description':
      return t('Provides a performance and scalability boost through caching Drupal pages as static HTML files.');
    case 'admin/help#boost':
      $file = drupal_get_path('module', 'boost') . '/README.txt';
      if (file_exists($file))
        return '<pre>' . implode("\n", array_slice(explode("\n", @file_get_contents($file)), 2)) . '</pre>';
      break;
    case 'admin/settings/boost':
      return '<p>' . '</p>'; // TODO: add help text.
  }
}

/**
 * Implementation of hook_perm(). Defines user permissions.
 */
function boost_perm() {
  return array('administer cache');
}

/**
 * Implementation of hook_menu(). Defines menu items and page callbacks.
 */
function boost_menu($may_cache) {
  $access = user_access('administer cache');
  $items = array();
  if ($may_cache) {
    // TODO: define menu actions for cache administration.
  }
  return $items;
}

/**
 * Implementation of hook_init(). Performs page setup tasks.
 */
function boost_init() {
88 89 90 91
  // Stop right here unless we're being called for an ordinary page request
  if (strpos($_SERVER['PHP_SELF'], 'index.php') === FALSE)
    return;

92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
  // TODO: check interaction with other modules that use ob_start(); this
  // may have to be moved to an earlier stage of the page request.
  if (!variable_get('cache', CACHE_DISABLED) && BOOST_ENABLED) {
    global $user;
    if (empty($user->uid) && $_SERVER['REQUEST_METHOD'] == 'GET') {
      if (boost_is_cacheable($_GET['q']))
        ob_start('_boost_ob_handler');
    }
  }

  // Executed when saving Drupal's settings:
  if (!empty($_POST['edit']) && $_GET['q'] == 'admin/settings') {
    // Forcibly disable Drupal's built-in SQL caching to prevent any conflicts of interest:
    variable_set('cache', CACHE_DISABLED);

    // TODO: handle 'offline' site maintenance settings.

    $old = variable_get('boost', '');
    if (!empty($_POST['edit']['boost'])) {
      // Ensure the cache directory exists or can be created
      file_check_directory($_POST['edit']['boost_file_path'], FILE_CREATE_DIRECTORY, 'boost_file_path');
    }
    else if (!empty($old)) { // the cache was previously enabled
      if (boost_cache_expire_all())
        drupal_set_message('Static cache files deleted.');
    }
  }
}

121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
/**
 * Implementation of hook_exit(). Performs cleanup tasks.
 *
 * For POST requests by anonymous visitors, this adds a dummy query string
 * to any URL being redirected to using drupal_goto().
 *
 * This is pretty much a hack that assumes a bit too much familiarity with
 * what happens under the hood of the Drupal core function drupal_goto().
 *
 * It's necessary, though, in order for any session messages set on form
 * submission to actually show up on the next page if that page has been
 * cached by Boost.
 */
function boost_exit($destination = NULL) {
  // Check that hook_exit() was invoked by drupal_goto() for a POST request:
  if (!empty($destination) && $_SERVER['REQUEST_METHOD'] == 'POST') {

    // Check that we're dealing with an anonymous visitor. and that some
    // session messages have actually been set during this page request:
    global $user;
    if (empty($user->uid) && ($messages = drupal_set_message())) {

      // Check that the page we're redirecting to has been cached by Boost
      // and really necessitates special handling:
      extract(parse_url($destination));
      $path = ($path == base_path() ? '' : substr($path, strlen(base_path())));
      if (boost_is_cached($path) && empty($query)) {
        // FIXME: call any remaining exit hooks since we're about to terminate.

        // Add a query string to ensure we don't serve a static copy of
        // the page we're redirecting to, which would prevent the session
        // messages from showing up:
        $destination = url($path, 't=' . time(), $fragment, TRUE);

        // Do what drupal_goto() would do if we were to return to it:
        exit(header('Location: ' . $destination));
      }
    }
  }
}

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285
/**
 * Implementation of hook_form_alter(). Performs alterations before a form
 * is rendered.
 */
function boost_form_alter($form_id, &$form) {
  // Alter Drupal's settings form by hiding the default cache enabled/disabled control (which will now always default to CACHE_DISABLED), and add our own control instead.
  if ($form_id == 'system_settings_form') {
    require_once BOOST_PATH . '/boost.admin.inc';
    $form['cache'] = boost_system_settings_form($form['cache']);
  }
}

/**
 * Implementation of hook_cron(). Performs periodic actions.
 */
function boost_cron() {
  if (!BOOST_ENABLED) return;

  if (boost_cache_expire_all()) {
    watchdog('boost', t('Expired stale files from static page cache.'), WATCHDOG_NOTICE);
  }
}

/**
 * Implementation of hook_comment(). Acts on comment modification.
 */
function boost_comment($comment, $op) {
  if (!BOOST_ENABLED) return;

  switch ($op) {
    case 'insert':
    case 'update':
      // Expire the relevant node page from the static page cache to prevent serving stale content:
      if (!empty($comment['nid']))
        boost_cache_expire('node/' . $comment['nid'], TRUE);
      break;
  }
}

/**
 * Implementation of hook_nodeapi(). Acts on nodes defined by other modules.
 */
function boost_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  if (!BOOST_ENABLED) return;

  switch ($op) {
    case 'insert':
    case 'update':
    case 'delete':
      // Expire all relevant node pages from the static page cache to prevent serving stale content:
      if (!empty($node->nid))
        boost_cache_expire('node/' . $node->nid, TRUE);
      break;
  }
}

/**
 * Implementation of hook_taxonomy(). Acts on taxonomy changes.
 */
function boost_taxonomy($op, $type, $term = NULL) {
  if (!BOOST_ENABLED) return;

  switch ($op) {
    case 'insert':
    case 'update':
    case 'delete':
      // TODO: Expire all relevant taxonomy pages from the static page cache to prevent serving stale content.
      break;
  }
}

/**
 * Implementation of hook_user(). Acts on user account actions.
 */
function boost_user($op, &$edit, &$account, $category = NULL) {
  if (!BOOST_ENABLED) return;

  global $user;
  switch ($op) {
    case 'login':
      // Set special cookie to prevent logged-in users getting served pages from the static page cache.
      $expires = ini_get('session.cookie_lifetime');
      $expires = (!empty($expires) && is_numeric($expires) ? time() + (int)$expires : 0);
      setcookie(BOOST_COOKIE, $user->uid, $expires, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      break;
    case 'logout':
      setcookie(BOOST_COOKIE, FALSE, time() - 86400, ini_get('session.cookie_path'), ini_get('session.cookie_domain'), ini_get('session.cookie_secure') == '1');
      break;
    case 'insert':
      // TODO: create user-specific cache directory.
      break;
    case 'delete':
      // Expire the relevant user page from the static page cache to prevent serving stale content:
      if (!empty($account->uid))
        boost_cache_expire('user/' . $account->uid);
      // TODO: recursively delete user-specific cache directory.
      break;
  }
}

/**
 * Implementation of hook_settings(). Declares administrative settings for a module.
 *
 * @deprecated in Drupal 5.0.
 */
function boost_settings() {
  require_once BOOST_PATH . '/boost.admin.inc';
  return boost_settings_form();
}

//////////////////////////////////////////////////////////////////////////////
// OUTPUT BUFFERING CALLBACK

/**
 * PHP output buffering callback.
 *
 * NOTE: objects have already been destructed so $user is not available.
 */
function _boost_ob_handler($buffer) {
  // Ensure we're in the correct working directory, since some web servers (e.g. Apache) mess this up here.
  chdir(dirname($_SERVER['SCRIPT_FILENAME']));

  // Check the currently set content type; at present we can't deal with anything else than HTML.
  if (_boost_get_content_type() == 'text/html') {
286 287 288
    if (strlen($buffer) > 0) { // Sanity check
      boost_cache_set($_GET['q'], $buffer);
    }
289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
  }

  // Allow the page request to finish up normally
  return $buffer;
}

/**
 * Determines the MIME content type of the current page response based on
 * the currently set Content-Type HTTP header.
 *
 * This should normally return the string 'text/html' unless another module
 * has overridden the content type.
 */
function _boost_get_content_type($default = NULL) {
  static $regex = '/^Content-Type:\s*([\w\d\/\-]+)/i';

  // The last Content-Type header is the one that counts:
  $headers = preg_grep($regex, explode("\n", drupal_set_header()));
  if (!empty($headers) && preg_match($regex, array_pop($headers), $matches))
    return $matches[1]; // found it

  return $default;
}

//////////////////////////////////////////////////////////////////////////////