Skip to content
boost.module 140 KiB
Newer Older
 * Provides static file caching for Drupal text output. Pages, Feeds, ect...
 */

//////////////////////////////////////////////////////////////////////////////
define('BOOST_TIME',                 time());
Mike Carper's avatar
Mike Carper committed
define('BOOST_MAX_TIMESTAMP',        variable_get('boost_max_timestamp', BOOST_TIME));
define('BOOST_ENABLED',              variable_get('boost_enabled', CACHE_NORMAL));
Mike Carper's avatar
Mike Carper committed
define('BOOST_GZIP',                 variable_get('page_compression', TRUE));

// This cookie is set for all authenticated 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 @ %cached_at, expires @ %expires_at"));

// Caching Options
define('BOOST_CACHE_LIFETIME',       variable_get('boost_cache_lifetime', 3600));
define('BOOST_CACHE_XML_LIFETIME',   variable_get('boost_cache_xml_lifetime', 3600));
define('BOOST_CACHE_JSON_LIFETIME',  variable_get('boost_cache_json_lifetime', 3600));
define('BOOST_CACHE_QUERY',          variable_get('boost_cache_query', TRUE));
define('BOOST_CACHE_HTML',           variable_get('boost_cache_html', TRUE));
define('BOOST_CACHE_XML',            variable_get('boost_cache_xml', FALSE));
define('BOOST_CACHE_JSON',           variable_get('boost_cache_json', FALSE));
define('BOOST_CACHE_CSS',            variable_get('boost_cache_css', TRUE));
define('BOOST_CACHE_JS',             variable_get('boost_cache_js', TRUE));
define('BOOST_CACHEABILITY_OPTION',  variable_get('boost_cacheability_option', 0));
define('BOOST_CACHEABILITY_PAGES',   variable_get('boost_cacheability_pages', ''));

// Dir & File Structure
define('BOOST_ROOT_CACHE_DIR',       variable_get('boost_root_cache_dir', 'cache'));
Mike Carper's avatar
Mike Carper committed
define('BOOST_MULTISITE_SINGLE_DB',  variable_get('boost_multisite_single_db', FALSE));
define('BOOST_NORMAL_DIR',           variable_get('boost_normal_dir', 'normal'));
define('BOOST_GZIP_DIR',             variable_get('boost_gzip_dir', 'normal'));
define('BOOST_PERM_NORMAL_DIR',      variable_get('boost_perm_normal_dir', 'perm'));
define('BOOST_PERM_GZ_DIR',          variable_get('boost_perm_gz_dir', 'perm'));
define('BOOST_CHAR',                 variable_get('boost_char', '_'));
define('BOOST_PERM_CHAR',            variable_get('boost_perm_char', '_'));
define('BOOST_HOST',                 variable_get('boost_host', ''));
define('BOOST_FILE_EXTENSION',       variable_get('boost_file_extension', '.html'));
define('BOOST_XML_EXTENSION',        variable_get('boost_xml_extension', '.xml'));
Mike Carper's avatar
Mike Carper committed
define('BOOST_JSON_EXTENSION',       variable_get('boost_json_extension', '.json'));
define('BOOST_CSS_EXTENSION',        variable_get('boost_css_extension', '.css'));
define('BOOST_JS_EXTENSION',         variable_get('boost_js_extension', '.js'));
define('BOOST_GZIP_EXTENSION',       variable_get('boost_gzip_extension', '.gz'));
define('BOOST_ROOT_FILE',            variable_get('boost_root_file', '.boost'));
define('BOOST_MAX_PATH_DEPTH',       10);

// Advanced Settings
Mike Carper's avatar
Mike Carper committed
define('BOOST_CHECK_BEFORE_CRON_EXPIRE', variable_get('boost_check_before_cron_expire', FALSE));
define('BOOST_PRE_PROCESS_FUNCTION', variable_get('boost_pre_process_function', ''));
define('BOOST_FLUSH_ALL_MULTISITE',  variable_get('boost_flush_all_multisite', FALSE));
define('BOOST_ONLY_ASCII_PATH',      variable_get('boost_only_ascii_path', TRUE));
Mike Carper's avatar
Mike Carper committed
define('BOOST_ASYNCHRONOUS_OUTPUT',  variable_get('boost_asynchronous_output', TRUE));
define('BOOST_FLUSH_DIR',            variable_get('boost_flush_dir', FALSE));
Mike Carper's avatar
Mike Carper committed
define('BOOST_FLUSH_CCK_REFERENCES', variable_get('boost_flush_cck_references', TRUE));
define('BOOST_FLUSH_NODE_TERMS',     variable_get('boost_flush_node_terms', TRUE));
define('BOOST_FLUSH_MENU_ITEMS',     variable_get('boost_flush_menu_items', 0));
define('BOOST_FLUSH_VIEWS',          variable_get('boost_flush_views', TRUE));
define('BOOST_FLUSH_VIEWS_INSERT',   variable_get('boost_flush_views_insert', TRUE));
define('BOOST_CLEAR_CACHE_OFFLINE',  variable_get('boost_clear_cache_offline', FALSE));
Mike Carper's avatar
Mike Carper committed
define('BOOST_OVERWRITE_FILE',       variable_get('boost_overwrite_file', FALSE));
define('BOOST_HALT_ON_ERRORS',       variable_get('boost_halt_on_errors', FALSE));
define('BOOST_HALT_ON_MESSAGES',     variable_get('boost_halt_on_messages', TRUE));
Mike Carper's avatar
Mike Carper committed
define('BOOST_DISABLE_CLEAN_URL',    variable_get('boost_disable_clean_url', FALSE));
define('BOOST_AGGRESSIVE_GZIP',      BOOST_GZIP ? variable_get('boost_aggressive_gzip', TRUE) : FALSE);
define('BOOST_PERMISSIONS_FILE',     variable_get('boost_permissions_file', ''));
define('BOOST_PERMISSIONS_DIR',      variable_get('boost_permissions_dir', ''));
define('BOOST_EXPIRE_NO_FLUSH',      variable_get('boost_expire_no_flush', FALSE));
Mike Carper's avatar
Mike Carper committed
define('BOOST_VERBOSE',              variable_get('boost_verbose', 5));
define('BOOST_IGNORE_SAFE_WARNING',  variable_get('boost_ignore_safe_warning', FALSE));
define('BOOST_IGNORE_SUBDIR_LIMIT',  variable_get('boost_ignore_subdir_limit', TRUE));
define('BOOST_NO_DATABASE',          variable_get('boost_no_database', FALSE));

// Crawler Settings
define('BOOST_CRAWL_ON_CRON',        variable_get('boost_crawl_on_cron', FALSE));
define('BOOST_LOOPBACK_BYPASS',      BOOST_OVERWRITE_FILE && BOOST_CRAWL_ON_CRON ? variable_get('boost_loopback_bypass', FALSE) : FALSE);
define('BOOST_PUSH_HTML',            variable_get('boost_push_html', TRUE));
define('BOOST_PUSH_XML',             variable_get('boost_push_xml', TRUE));
define('BOOST_PUSH_JSON',            variable_get('boost_push_json', TRUE));
define('BOOST_CRAWLER_THROTTLE',     variable_get('boost_crawler_throttle', 0));
define('BOOST_CRAWLER_THREADS',      variable_get('boost_crawler_threads', 2));
define('BOOST_CRAWL_URL_ALIAS',      variable_get('boost_crawl_url_alias', FALSE));
define('BOOST_MAX_THREADS',          8);
// Requires Boost Functions or global scope, Define These Last
global $base_url;
define('BOOST_CRAWLER_SELF',         $base_url . '/' . 'boost-crawler?nocache=1&key=' . variable_get('boost_crawler_key', FALSE));
define('BOOST_FILE_PATH',            BOOST_MULTISITE_SINGLE_DB ? boost_cache_directory(NULL, FALSE) : variable_get('boost_file_path', boost_cache_directory(BOOST_HOST, FALSE)));
define('BOOST_GZIP_FILE_PATH',       implode('/', array_filter(explode('/', str_replace(BOOST_ROOT_CACHE_DIR . '/' . BOOST_NORMAL_DIR, BOOST_ROOT_CACHE_DIR . '/' . BOOST_GZIP_DIR . '/', BOOST_FILE_PATH)))));
define('BOOST_PERM_GZIP_FILE_PATH',  implode('/', array_filter(explode('/', str_replace(BOOST_ROOT_CACHE_DIR . '/' . BOOST_NORMAL_DIR, BOOST_ROOT_CACHE_DIR . '/' . BOOST_PERM_GZ_DIR . '/', BOOST_FILE_PATH)))));
define('BOOST_PERM_FILE_PATH',       implode('/', array_filter(explode('/', str_replace(BOOST_ROOT_CACHE_DIR . '/' . BOOST_NORMAL_DIR, BOOST_ROOT_CACHE_DIR . '/' . BOOST_PERM_NORMAL_DIR . '/', BOOST_FILE_PATH)))));
define('BOOST_CRAWLER_BATCH_SIZE',   variable_get('boost_crawler_batch_size', min(15, ini_get('max_execution_time')/(2 * boost_average_time()))));
define('BOOST_MAX_THREAD_TIME',      max(300, 2 * boost_average_time() * BOOST_CRAWLER_THREADS * BOOST_CRAWLER_BATCH_SIZE));

//////////////////////////////////////////////////////////////////////////////
// Global variables

//$GLOBALS['_boost_path'] = '';
//$GLOBALS['_boost_query'] = '';
//$GLOBALS['_boost_message_count'] = '';
//$GLOBALS['_boost_cache_this'] = '';
//$GLOBALS['_boost_max_execution_time'] = '';
//$GLOBALS['_boost_output_buffering'] = '';
//$GLOBALS['_boost_default_socket_timeout'] = '';
//$GLOBALS['_boost_router_item'] = '';
//$GLOBALS['_boost_relationships'] = '';
//$GLOBALS['_boost_nid'] = '';
//////////////////////////////////////////////////////////////////////////////

/**
 * Implementation of hook_help(). Provides online user help.
 */
function boost_help($path, $arg) {
  switch ($path) {
      if (file_exists($file = drupal_get_path('module', 'boost') . '/README.txt')) {
        return '<pre>' . implode("\n", array_slice(explode("\n", @file_get_contents($file)), 2)) . '</pre>';
    case 'admin/settings/performance/boost':
      return '<p>' . t('') . '</p>'; // TODO: add help text.
  //hack to get drupal_get_messages before they are destroyed.
  $GLOBALS['_boost_message_count'] = count(drupal_get_messages(NULL, FALSE));
/**
 * Implementation of hook_views_pre_render().
 *
 * This is called right before the render process. Used to grab the NID's listed
 * in this view, and set the view node relationship in the database.
 *
 * @param &$view
 *  reference to the view being worked on
 */
function boost_views_pre_render(&$view) {
  if (!is_null($view) && $GLOBALS['_boost_cache_this'] && !BOOST_NO_DATABASE) {
    foreach ($view->result as $item) {
      $node = node_load($item->nid);
      $GLOBALS['_boost_relationships'][] = array('child_page_callback' => 'node', 'child_page_type' => $node->type, 'child_page_id' => $item->nid);
    }
  }
}

 * Implementation of hook_init(). Performs page setup tasks if page not cached.
  // Disable all caches when nocache is set
  if (isset($_GET['nocache'])) {
    $GLOBALS['conf']['cache'] = CACHE_DISABLED;
    $GLOBALS['_boost_cache_this'] = FALSE;
    return;
  }

  // Make sure this is not a 404 redirect from the htaccesss file
  $path = explode($base_path, request_uri());
  array_shift($path);
  $path = implode($base_path, $path);
  $path = explode('?', $path);
  $path = array_shift($path);
  if ($path != '' && $_REQUEST['q'] == '' && !stristr($path, '.php') && isset($_SERVER['REDIRECT_STATUS']) && $_SERVER['REDIRECT_STATUS'] == 404) {
    $GLOBALS['conf']['cache'] = CACHE_DISABLED;
    $GLOBALS['_boost_cache_this'] = FALSE;
    if (BOOST_VERBOSE >= 7) {
      watchdog('boost', '404 recieved from server via redirect, going to send a 404. Info: !output', array('!output' => boost_print_r($_SERVER, TRUE, TRUE)));
    }
  $GLOBALS['_boost_path'] = $_REQUEST['q'];
  // Make the proper filename for our query
  $GLOBALS['_boost_query'] = BOOST_CHAR;
    if ($key != 'q' && $key != 'destination') {
      $GLOBALS['_boost_query'] .= (($GLOBALS['_boost_query'] == BOOST_CHAR) ? '' : '&') . $key . '=' . $val;
  if (!empty($user->uid)) {
    boost_set_cookie($user);
    if (BOOST_DISABLE_CLEAN_URL) {
      $GLOBALS['conf']['clean_url'] = 0;
      db_query('TRUNCATE {cache_filter}');
      db_query('TRUNCATE {cache_menu}');
      cache_clear_all('*', 'cache_menu');
      cache_clear_all('*', 'cache_filter');
    }
  }

  // Make sure the page is/should be cached according to our current configuration
  if (   strpos($_SERVER['SCRIPT_FILENAME'], 'index.php') === FALSE
      || variable_get('site_offline', 0)
      || $_SERVER['REQUEST_METHOD'] != 'GET'
      || $_SERVER['SERVER_SOFTWARE'] === 'PHP CLI'
      || !BOOST_ENABLED
      || !boost_is_cacheable($GLOBALS['_boost_path'])
  // We only generate cached pages for anonymous visitors.
    if (BOOST_ENABLED != CACHE_AGGRESSIVE) {
      $GLOBALS['conf']['cache'] = CACHE_DISABLED;
    }
    register_shutdown_function('_boost_ob_handler');
    ob_start();
/**
 * 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())) {
      // FIXME: call any remaining exit hooks since we're about to terminate?

      $query_parts = parse_url($destination);
      // Add a nocache parameter to query. Such pages will never be cached
      $query_parts['query'] .= (empty($query_parts['query']) ? '' : '&') . 'nocache=1';

      // Rebuild the URL with the new query string.  Do not use url() since
      // destination has presumably already been run through url().
      $destination = boost_glue_url($query_parts);

      // Do what drupal_goto() would do if we were to return to it:
      exit(header('Location: ' . $destination));
/**
 * Implementation of hook_menu().
 */
function boost_menu() {
  $items['admin/settings/performance/default'] = array(
    'title' => 'Performance',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'file path' => drupal_get_path('module', 'system'),
  );
  $items['admin/settings/performance/boost'] = array(
    'title' => 'Boost Settings',
    'description' => 'Advanced boost configuration.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('boost_admin_boost_performance_page'),
    'access arguments' => array('administer site configuration'),
    'weight' => 10,
    'type' => MENU_LOCAL_TASK,
    'file' => 'boost.admin.inc',
  );
  $items['admin/settings/performance/boost-rules'] = array(
    'title' => 'Boost htaccess rules generation',
    'description' => 'htaccess boost rules.',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('boost_admin_htaccess_page'),
    'access arguments' => array('administer site configuration'),
    'weight' => 12,
    'type' => MENU_LOCAL_TASK,
    'file' => 'boost.admin.inc',
  );
  $items['boost_stats.php'] = array(
Mike Carper's avatar
Mike Carper committed
    'page callback' => 'boost_stats_ajax_callback',
    'type' => MENU_CALLBACK,
    'access arguments' => array('access content'),
    'file path' => drupal_get_path('module', 'boost'),
Mike Carper's avatar
Mike Carper committed
    'file' => 'stats/boost_stats.ajax.inc',
  );
  $items['boost-crawler'] = array(
    'type' => MENU_CALLBACK,
    'access callback' => 1,
    'access arguments' => array('access content'),
    'file path' => drupal_get_path('module', 'boost'),
  );
/**
 * Implementation of hook_form_alter(). Performs alterations before a form
 * is rendered.
 */
function boost_form_alter(&$form, $form_state, $form_id) {
  switch ($form_id) {

    // Alter Drupal's system performance settings form by hiding the default
    // cache enabled/disabled control (which will now always default to
    // CACHE_DISABLED), and inject our own settings in its stead.
    case 'system_performance_settings':
      module_load_include('inc', 'boost', 'boost.admin');
      $form['page_cache'] = boost_admin_performance_page($form['page_cache']);
      $form['#submit'][] = 'boost_admin_performance_page_submit';
      $form['clear_cache']['clear']['#submit'][0] = 'boost_admin_clear_cache_submit';
      break;

    // Alter Drupal's site maintenance settings form in order to ensure that
    // the static page cache gets wiped if the administrator decides to take
    // the site offline.
    case 'system_site_maintenance_settings':
      module_load_include('inc', 'boost', 'boost.admin');
      $form['#submit'][] = 'boost_admin_site_offline_submit';
      break;

    // Alter Drupal's modules build form in order to ensure that
    // the static page cache gets wiped if the administrator decides to
    // change enabled modules
    case 'system_modules':
      module_load_include('inc', 'boost', 'boost.admin');
      $form['#submit'][] = 'boost_admin_modules_submit';
      break;

    // Alter Drupal's theme build form in order to ensure that
    // the static page cache gets wiped if the administrator decides to
    // change theme
    case 'system_themes_form':
      module_load_include('inc', 'boost', 'boost.admin');
      $form['#submit'][] = 'boost_admin_themes_submit';

      // Added below due to this bug: http://drupal.org/node/276615
      if (   variable_get('preprocess_css', FALSE)==TRUE
          && floatval(VERSION) <= 6.13
          && boost_cache_clear_all()
          ) {
        drupal_set_message(t('Boost: Static page cache cleared. See <a href="http://drupal.org/node/276615">http://drupal.org/node/276615</a> for reason why (core bug that is fixed in 6.14+).'), 'warning');
  }
}

/**
 * Implementation of hook_cron(). Performs periodic actions.
 */
function boost_cron() {
  $expire = TRUE;
  if (BOOST_CHECK_BEFORE_CRON_EXPIRE) {
    $expire = boost_has_site_changed(TRUE);
  if (!BOOST_LOOPBACK_BYPASS && variable_get('boost_expire_cron', TRUE) && $expire && boost_cache_expire_all()) {
    if (BOOST_VERBOSE >= 5) {
      watchdog('boost', 'Expired stale files from static page cache.', array(), WATCHDOG_NOTICE);
    }

  // Update Stats
  if (module_exists('statistics') && variable_get('boost_block_show_stats', FALSE)) {
    $block = module_invoke('statistics', 'block', 'view', 0);
    variable_set('boost_statistics_html', $block['content']);
  }
  if (BOOST_CRAWL_ON_CRON && !variable_get('site_offline', 0)) {
    boost_crawler_run((int)$expire);
/*
 * Implementation of hook_flush_caches(). Deletes all static files.
 */
function boost_flush_caches() {
  if (variable_get('cron_semaphore', FALSE)==FALSE && (variable_get('preprocess_css', FALSE)==TRUE || variable_get('preprocess_js', FALSE)==TRUE)) {
/**
 * 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_expire_node($node, $comment['nid']);
    case 'publish':
    case 'unpublish':
    case 'delete':
      if (!empty($comment->nid)) {
        boost_expire_node($node, $comment->nid);
  }
}

/**
 * Implementation of hook_nodeapi(). Acts on nodes defined by other modules.
 */
function boost_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {
  if (!BOOST_ENABLED) return;
  $data[] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => 'node', 'page_id' => $node->nid);
      if (BOOST_FLUSH_VIEWS_INSERT && module_exists('views')) {
        $GLOBALS['_boost_nid'] = $node->nid;
        register_shutdown_function('_boost_view_insert');
      }
      boost_expire_node($node);
      if (!$node->status) {
        boost_cache_expire_router($data, TRUE, TRUE);
      }
      break;
      boost_cache_expire_router($data, TRUE, TRUE);
/**
 * Shutdown function, gets called at the very end of node creation.
 *
 * Node is now created, thus views has access to the new node. Searches all
Mike Carper's avatar
Mike Carper committed
 * cached views for newly created node. Expires the outdated views from the cache.
 */
function _boost_view_insert() {
  $result = db_query("SELECT * FROM {boost_cache} WHERE base_dir = '%s' AND page_callback = 'view' AND expire > 0 AND expire <> 434966400", BOOST_FILE_PATH);
  $data = array();
  $num_views = 0;
  $num_hits = 0;
  while ($boost = db_fetch_array($result)) {
    $view = views_get_view($boost['page_type']);
    $view->set_display($boost['page_id']);
    $view->pre_execute();
    $view->set_items_per_page(0);
    $view->execute();
    $number_views++;
    foreach ($view->result as $item) {
      if ($item->nid == $GLOBALS['_boost_nid']) {
        $hash = BOOST_FILE_PATH . 'view' . $boost['page_type'] . $boost['page_id'];
        $data[$hash] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => 'view', 'page_type' => $boost['page_type'], 'page_id' => $boost['page_id']);
        $number_hits++;
  if ($data) {
    $flushed = boost_cache_expire_router($data);
  }
  if (BOOST_VERBOSE >= 7) {
    watchdog('boost', 'Debug: _boost_view_insert() <br />!views Views Searched; !hits of them contain the new node and where thus flushed. As a result of this !flushed pages where expired from the boost cache.', array('!views' => $num_views, '!hits' => $num_hits, '!flushed' => $flushed));
  }
/**
 * Implementation of hook_votingapi_insert().
 *
 * @param $votes
 *  array of votes
 */
function boost_votingapi_insert($votes) {
  if (!BOOST_ENABLED) return;

    boost_expire_node($node, $vote['content_id']);
  }
}

/**
 * Implementation of hook_votingapi_delete().
 *
 * @param $votes
 *  array of votes
 */
function boost_votingapi_delete($votes) {
  if (!BOOST_ENABLED) return;

    boost_expire_node($node, $vote['content_id']);
 * Expires a node from the cache; including related pages.
 *
 * Expires front page if promoted, taxonomy terms,
function boost_expire_node($node, $nid = 0) {
  $data = array();
  $paths = array();
  // Check node object
  if (empty($node->nid)) {
    if ($nid) {
      $node->nid = $nid;
    }
    else {
      return FALSE;
    }
  }

  // Expire this node
  if (BOOST_NO_DATABASE) {
    $paths[] = 'node/' . $node->nid;
  }
  else {
    $data[] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => 'node', 'page_id' => $node->nid);
  // If promoted to front page, expire front page
  if ($node->promote == 1) {
    $paths[] = '<front>';
  }

  // Get taxonomy terms and flush
  if (module_exists('taxonomy') && BOOST_FLUSH_NODE_TERMS) {
    $tids = boost_taxonomy_node_get_tids($node->nid);
    $filenames = array();
      if (BOOST_NO_DATABASE) {
        $paths[] = 'taxonomy/term/' . $tid;
      }
      else {
        $data[] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => 'taxonomy', 'page_id' => $tid);
      }
  // Get menu and flush related items in the menu.
  if (BOOST_FLUSH_MENU_ITEMS !=0) {
    if (!isset($node->menu['menu_name'])) {
      menu_nodeapi($node, 'prepare');
    }
    $menu = menu_tree_all_data($node->menu['menu_name']);
    if (BOOST_FLUSH_MENU_ITEMS == 1) {
      $links = boost_get_menu_structure($menu, FALSE, 'node/' . $node->nid);
    }
    elseif (BOOST_FLUSH_MENU_ITEMS == 2) {
      $links = boost_get_menu_structure($menu);
    }
    $paths = array_merge($links, $paths);
  }

  // Get CCK References and flush.
  if (BOOST_FLUSH_CCK_REFERENCES && module_exists('nodereference')) {
    $nids = array();
    $type = content_types($node->type);
    if ($type) {
      foreach ($type['fields'] as $field) {
        // Add referenced nodes to nids. This will clean up nodereferrer fields
        // when the referencing node is updated.
        if ($field['type'] == 'nodereference') {
          $node_field = isset($node->$field['field_name']) ? $node->$field['field_name'] : array();
          foreach ($node_field as $delta => $item) {
            $nids[$item['nid']] = $item['nid'];
          }
      foreach ($nids as $nid) {
        if (BOOST_NO_DATABASE) {
          $paths[] = 'node/' . $nid;
        }
        else {
          $data[] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => 'node', 'page_id' => $nid);
        }
    }

    // Get CCK references pointing to this node and flush.
    if (module_exists('nodereferrer')) {
      $nids = nodereferrer_referrers($node->nid);
      foreach ($nids as $nid) {
        if (BOOST_NO_DATABASE) {
          $paths[] = 'node/' . $nid['nid'];
        }
        else {
          $data[] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => 'node', 'page_id' => $nid['nid']);
        }
  // Get views containing this node and flush.
  if (BOOST_FLUSH_VIEWS && module_exists('views')) {
    $GLOBALS['_boost_router_item'] = isset($GLOBALS['_boost_router_item']) ? $GLOBALS['_boost_router_item'] : _boost_get_menu_router();
    $router_item = $GLOBALS['_boost_router_item'];
    $relationship = array();
    $relationship[] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => $router_item['page_callback'], 'page_type' => $router_item['page_type'], 'page_id' => $router_item['page_id']);
    $data[] = boost_cache_get_node_relationships($relationship);
  // Flush the cache
  $flushed = 0;
  if (!empty($data)) {
    $flushed += boost_cache_expire_router($data);
  }
  if (!empty($paths)) {
    $flushed += boost_cache_expire_derivative($paths, TRUE);
  }
  if (BOOST_VERBOSE >= 7) {
    watchdog('boost', 'Debug: boost_expire_node() <br />Node !nid was flushed resulting in !flushed pages being expired from the cache',  array('!nid' => $node->nid, '!flushed' => $flushed));
  }
/**
 * Finds parent, siblings and children of the menu item. UGLY CODE...
 *
 * @param array $menu
 *  Output from menu_tree_all_data()
 * @param bool $found
 *  Signal for when the needle was found in the menu array.
 *  Set TRUE to get entire menu
 * @param string $needle
 *  Name of menu link. Example 'node/21'
 * @param bool $first
 *  Keep track of the first call; this is a recursive function.
 * @param bool &$found_global
 *  Used to signal the parent item was found in one of it's children
 * @param bool &$menu_out
 *  Output array of parent, siblings and children menu links
 *
 * TODO: Use page_callback and page_arguments instead of link_path.
 *  Can use boost_cache_expire_router() then.
 */
function boost_get_menu_structure($menu, $found = TRUE, $needle = '', $first = TRUE, &$found_global = FALSE, &$menu_out = array()) {
  $found_global = FALSE;
  // Get Siblings
  foreach ($menu as $item) {
    if ($item['link']['hidden'] == 0 && $item['link']['page_callback'] != '' && ($item['link']['link_path'] == $needle || $found)) {
      $menu_out[] = $item['link']['link_path'];
      $found = TRUE;
    }
  }
  // Get Children
  foreach ($menu as $item) {
    if ($item['link']['hidden'] != 0) {
      continue;
    }
    if ($item['link']['page_callback'] != '' && ($item['link']['link_path'] == $needle || $found)) {
      $menu_out[] = $item['link']['link_path'];
      $found = TRUE;
    }
    // Get Grandkids
    if (!empty($item['below'])) {
      $sub_menu = array();
      foreach ($item['below'] as $below) {
        if ($below['link']['hidden'] == 0) {
          $sub_menu[] = $below;
        }
      }
      boost_get_menu_structure($sub_menu, $needle, $found, FALSE, $found_global, $menu_out);
      $structure[$item['link']['link_path']][] = $sub;
      if ($item['link']['page_callback'] != '' && $found_global) {
        // Get Parent of kid
        $menu_out[] = $item['link']['link_path'];
      }
    }
    else {
      $structure[$item['link']['link_path']] = '';
    }
  }

  // Clean up
  $structure = array_unique($structure);
  $found_global = $found;
  if ($first) {
    $menu_out = array_unique($menu_out);
    sort($menu_out);
    return $menu_out;
  }
  else {
    return $structure;
  }
}

/**
 * Return taxonomy terms given a nid.
 *
 * Needed because of a weird bug with CCK & node_load()
 *  http://drupal.org/node/545922
 */
function boost_taxonomy_node_get_tids($nid) {
  $vid = db_result(db_query('SELECT vid FROM {node} WHERE nid = %d', $nid));
  $result = db_query(db_rewrite_sql('SELECT t.tid FROM {term_node} r INNER JOIN {term_data} t ON r.tid = t.tid INNER JOIN {vocabulary} v ON t.vid = v.vid WHERE r.vid = %d ORDER BY v.weight, t.weight, t.name', 't', 'tid'), $vid);
  $tids = array();
  while ($term = db_result($result)) {
    $tids[] = $term;
  }
  return $tids;
}

/**
 * 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 a special cookie to prevent authenticated users getting served
      // pages from the static page cache.
      boost_set_cookie($user, BOOST_TIME - 86400);
      break;
    case 'delete':
      // Expire the relevant user page from the static page cache to prevent serving stale content:
      if (!empty($account->uid)) {
        if (BOOST_NO_DATABASE) {
          $paths[] = 'user/' . $account->uid;
          $flushed = boost_cache_expire_derivative($paths, TRUE);
        }
        else {
          $data[] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => 'user', 'page_id' => $account->uid);
          $flushed = boost_cache_expire_router($data, TRUE, TRUE);
        }
        if (BOOST_VERBOSE >= 7) {
          watchdog('boost', 'Debug: boost_user() <br />User !uid was deleted resulting in !flushed pages being expired from the cache',  array('!uid' => $account->uid, '!flushed' => $flushed));
/**
 * Implementation of hook_block().
 */
function boost_block($op = 'list', $delta = 0, $edit = array()) {
  global $user;

  switch ($op) {
    case 'list':
      return array(
        'status' => array(
          'info'   => t('Boost: Pages cache status'),
          'region' => 'right',
          'weight' => 10,
          'cache'  => BLOCK_NO_CACHE,
        ),
        'config' => array(
          'info'   => t('Boost: Pages cache configuration'),
          'region' => 'right',
          'weight' => 10,
          'cache'  => BLOCK_NO_CACHE,
        ),
        'stats' => array(
          'info'   => t('Boost: AJAX core statistics'),
          'region' => 'right',
          'weight' => 10,
          'cache'  => BLOCK_NO_CACHE,
        ),
      );
        $form['boost_block_show_stats'] = array(
Mike Carper's avatar
Mike Carper committed
        '#title' => t('Display Statistics.'),
        '#default_value' => variable_get('boost_block_show_stats', FALSE),
Mike Carper's avatar
Mike Carper committed
        '#description' => t('If false, uses Javascript to hide the block via "parent().parent().hide()".'),
        $form['boost_block_cache_stats_block'] = array(
        '#type' => 'checkbox',
        '#title' => t('Cache Statistics Block'),
        '#default_value' => variable_get('boost_block_cache_stats_block', FALSE),
        );
        variable_set('boost_block_show_stats', $edit['boost_block_show_stats']);
        variable_set('boost_block_cache_stats_block', $edit['boost_block_cache_stats_block']);
    case 'view':
      $block = array();
      switch ($delta) {
        case 'status':
          // Don't show the block to anonymous users, nor on any pages that
          // aren't even cacheable to begin with (e.g. admin/*).
          if (!empty($user->uid) && boost_is_cacheable($GLOBALS['_boost_path'])) {
            $output = t('This page is being served <strong>live</strong> to anonymous visitors, as it is not currently in the static page cache.');

            if (boost_is_cached($GLOBALS['_boost_path'])) {
              $filename = boost_file_path($GLOBALS['_boost_path']);
              $ttl = boost_db_get_ttl($filename);
              $generate = boost_get_generation_time($filename);
              $output = '';
              if (BOOST_CHECK_BEFORE_CRON_EXPIRE) {
                $output .= t('Site Has Changed: %old<br />', array('%old' => boost_has_site_changed() ? 'True' : 'False'));
              }
              if ($ttl < 0) {
                $output .= t('<strong>Expired: %interval ago</strong><br />', array('%interval' => format_interval(abs($ttl))));
                $output .= t('Expire In: %interval<br />', array('%interval' => format_interval(abs($ttl))));
              $output .= t('Cache Generated: %time seconds<br />', array('%time' => round($generate, 2))) . ' ';
              $output .=  drupal_get_form('boost_block_flush_form');
            $error = _boost_page_have_error();
            $drupal_msg = max(count(drupal_get_messages(NULL, FALSE)), $GLOBALS['_boost_message_count']);
            if ($error || (BOOST_HALT_ON_MESSAGES && $drupal_msg != 0)) {
              $output = t('There are <strong>php errors</strong> or <strong>drupal messages</strong> on this page, preventing boost from caching.') . ' ';
              if ($error) {
                $output .= t('ERROR: <pre>%error</pre> !link <br /> !performance', array('%error' => boost_print_r($error, TRUE), '!link' => l(t('Lookup Error Type'), 'http://php.net/errorfunc.constants'), '!performance' => l(t('Turn Off Error Checking'), 'admin/settings/performance')));
              if (BOOST_HALT_ON_MESSAGES && $drupal_msg != 0) {
                $output .= t('MESSAGES: %msg <br /> !performance', array('%msg' => $drupal_msg, '!performance' => l(t('Turn Off Error Checking'), 'admin/settings/performance')));

            $block['subject'] = '';
            $block['content'] = theme('boost_cache_status', isset($ttl) ? $ttl : -1, $output);
          }
          break;
        case 'config':
          // Don't show the block to anonymous users, nor on any pages that
          // aren't even cacheable to begin with (e.g. admin/*).
          if (!empty($user->uid) && boost_is_cacheable($GLOBALS['_boost_path']) && !BOOST_NO_DATABASE) {
            $block['subject'] = '';
            $block['content'] = theme('boost_cache_status', -1, drupal_get_form('boost_block_db_settings_form'));
          }
          break;
        case 'stats':
          $filename = 'boost_stats.php';
          $block = module_invoke('statistics', 'block', 'view', 0);
          variable_set('boost_statistics_html', $block['content']);

          if (!( strpos($_SERVER['SCRIPT_FILENAME'], 'index.php') === FALSE
              || variable_get('site_offline', 0)
              || $_SERVER['REQUEST_METHOD'] != 'GET'
              || $_SERVER['SERVER_SOFTWARE'] === 'PHP CLI'
              || !BOOST_ENABLED
              || isset($_GET['nocache'])
              || !boost_is_cacheable($GLOBALS['_boost_path'])
              || !empty($user->uid)
              || !module_exists('statistics')
              )) {
Mike Carper's avatar
Mike Carper committed
            $block = array();
            $block['subject'] = 'Popular content';
            $block['content'] = '<div id="boost-stats"></div>' . boost_stats_generate($filename);
          }
          elseif (!variable_get('boost_block_show_stats', FALSE)) {
            $block['content'] .= '<div id="boost-stats"></div>';
            drupal_add_js('$("#boost-stats").parent().parent().hide();', 'inline', 'footer');
function boost_block_flush_form() {
  $GLOBALS['_boost_router_item'] = isset($GLOBALS['_boost_router_item']) ? $GLOBALS['_boost_router_item'] : _boost_get_menu_router();
  $router_item = $GLOBALS['_boost_router_item'];
  $form['boost_clear']['page_callback'] = array(
    '#type' => 'hidden',
    '#value' => $router_item['page_callback'],
  );
  $form['boost_clear']['page_type'] = array(
    '#value' => $router_item['page_type'],
  );
  $form['boost_clear']['page_id'] = array(
    '#value' => $router_item['page_id'],
  $form['boost_clear']['path'] = array(
    '#type' => 'hidden',
    '#value' => $GLOBALS['_boost_path'],
  );
  $form['boost_cache']['clear'] = array(
    '#type' => 'submit',
    '#value' => t('Flush Page'),
    '#submit' => array('boost_block_form_flush_submit'),
function boost_block_form_flush_submit(&$form_state, $form) {
  // Special front page handling
  if ($form['values']['page_callback'] == 'node_page_default' && BOOST_CACHE_XML) {
    $data[] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => 'node_feed');
  }
  $data[] = array('base_dir' => BOOST_FILE_PATH, 'page_callback' => $form['values']['page_callback'], 'page_type' => $form['values']['page_type'], 'page_id' => $form['values']['page_id']);
  $flushed = 0;
  if ($data) {
    $flushed += boost_cache_expire_router($data, TRUE);
  }
  if (isset($form['values']['path'])) {
    $flushed += boost_cache_expire_derivative(array($form['values']['path']));
  }
  if (BOOST_VERBOSE >= 7) {
    watchdog('boost', 'Debug: boost_block_form_flush_submit() <br />Page !path was deleted resulting in !flushed pages being expired from the cache',  array('!path' => $form['values']['path'], '!flushed' => $flushed));
  }
function boost_block_db_settings_form() {
  // set info
  $period = drupal_map_assoc(array(-1, 0, 60, 180, 300, 600, 900, 1800, 2700, 3600, 10800, 21600, 32400, 43200, 64800, 86400, 2*86400, 3*86400, 4*86400, 5*86400, 6*86400, 604800, 2*604800, 3*604800, 4*604800, 8*604800, 16*604800, 52*604800), 'format_interval');
  $period[0] = '<' . t('none') . '>';
  $period[-1] = t('default');
  //$info = boost_get_db(boost_file_path($GLOBALS['_boost_path']));
  $GLOBALS['_boost_router_item'] = isset($GLOBALS['_boost_router_item']) ? $GLOBALS['_boost_router_item'] : _boost_get_menu_router();
  $router_item = $GLOBALS['_boost_router_item'];
  $settings = boost_get_settings_db($router_item);
  $default = 0;
  foreach ($settings as $key => $value) {
    if ($value != NULL) {
      $info = $value;
      $default = $key;
      break;
    }
  }
  if (!isset($info)) {
    $info['lifetime'] = -1;
    $info['push'] = -1;
  }
  // create form
  $form['boost_db_settings']['lifetime'] = array(
    '#type' => 'select',
    '#title' => t('Maximum cache lifetime'),
    '#default_value' => $info['lifetime'],
    '#options' => $period,
    '#description' => t('Default: %default', array('%default' => format_interval(BOOST_CACHE_LIFETIME))),
  );
  $form['boost_db_settings']['push'] = array(
    '#title' => t('Preemptive Cache'),
    '#default_value' => $info['push'],
    '#options' => array(
      -1 => 'default',
      0 => 'No',
      1 => 'Yes',
    ),