\n")); // This is needed since the $user object is already destructed in _boost_ob_handler(): define('BOOST_USER_ID', @$GLOBALS['user']->uid); ////////////////////////////////////////////////////////////////////////////// // Core API hooks /** * Implementation of hook_help(). Provides online user help. */ function boost_help($path, $arg) { switch ($path) { case 'admin/help#boost': if (file_exists($file = drupal_get_path('module', 'boost') . '/README.txt')) { return '
' . implode("\n", array_slice(explode("\n", @file_get_contents($file)), 2)) . ''; } break; case 'admin/settings/performance/boost': return '
' . t('') . '
'; // TODO: add help text. } } /** * Implementation of hook_init(). Performs page setup tasks if page not cached. */ function boost_init() { // Stop right here unless we're being called for an ordinary page request if (strpos($_SERVER['SCRIPT_FILENAME'], 'index.php') === FALSE || variable_get('site_offline', 0)) return; // 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; $GLOBALS['_boost_path'] = (empty($_REQUEST['q'])) ? 'index' : $_REQUEST['q']; // For authenticated users, set a special cookie to prevent them // inadvertently getting served pages from the static page cache. if (!empty($user->uid)) { boost_set_cookie($user); } // We only serve cached pages for GET requests by anonymous visitors. // Also pages with 'nocache' parameter in request will never be cached else if ($_SERVER['REQUEST_METHOD'] == 'GET' && !isset($_GET['nocache'])) { // Make the proper filename for our query // $_GET here is okay to use - $_GET['q'] is not involved here - see != 'q' below $fname = ''; foreach ($_GET as $key => $val) { if ($key != 'q') { $fname .= (empty($fname) ? '' : '&') . $key . '=' . $val; } } $fname = (empty($fname) ? '' : '_' . $fname); // Make sure the page is cacheable according to our current configuration: if (boost_is_cacheable($GLOBALS['_boost_path'])) { // In the event of errors such as drupal_not_found(), GET['q'] is // changed before _boost_ob_handler() is called. Apache is going to // look in the cache for the original path, however, so we need to // preserve it. $GLOBALS['_boost_query'] = $fname; ob_start('_boost_ob_handler'); } } } } /** * 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); $query_parts['path'] = ($query_parts['path'] == base_path() ? '' : substr($query_parts['path'], strlen(base_path()))); // Add a nocache parameter to query. Such pages will never be cached $query_parts['query'] .= (empty($query_parts['query']) ? '' : '&') . 'nocache=1'; $destination = url($query_parts['path'], $query_parts); // Do what drupal_goto() would do if we were to return to it: exit(header('Location: ' . $destination)); } } } /** * 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_settings($form['page_cache']); $form['#validate'][] = 'boost_admin_settings_validate'; $form['#submit'][] = 'boost_admin_settings_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'; break; } } /** * Implementation of hook_cron(). Performs periodic actions. */ function boost_cron() { if (!BOOST_ENABLED) return; if (variable_get('boost_expire_cron', TRUE) && boost_cache_expire_all()) { watchdog('boost', 'Expired stale files from static page cache.', array(), WATCHDOG_NOTICE); } } /* * Implementation of hook_flush_caches(). Deletes all static files. */ function boost_flush_caches() { if (variable_get('preprocess_css', FALSE)==TRUE || variable_get('preprocess_js', FALSE)==TRUE) { boost_cache_clear_all(); } return; } /** * 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; case 'publish': case 'unpublish': case 'delete': 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 a special cookie to prevent authenticated users getting served // pages from the static page cache. boost_set_cookie($user); break; case 'logout': boost_set_cookie($user, time() - 86400); 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_block(). */ function boost_block($op = 'list', $delta = 0, $edit = array()) { global $user; switch ($op) { case 'list': return array( 'status' => array( 'info' => t('Boost page cache status'), 'region' => 'right', 'weight' => 10, 'cache' => BLOCK_NO_CACHE, ), ); 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 live to anonymous visitors, as it is not currently in the static page cache.'); if (boost_is_cached($GLOBALS['_boost_path'])) { $ttl = boost_file_get_ttl(boost_file_path($GLOBALS['_boost_path'])); $output = t('This page is being served to anonymous visitors from the static page cache.') . ' '; $output .= t($ttl < 0 ? 'The cached copy expired %interval ago.' : 'The cached copy will expire in %interval.', array('%interval' => format_interval(abs($ttl)))); } $block['subject'] = ''; $block['content'] = theme('boost_cache_status', isset($ttl) ? $ttl : -1, $output); } break; } return $block; } } /** * Implementation of hook_theme(). */ function boost_theme() { return array( 'boost_cache_status' => array( 'arguments' => array('ttl' => NULL, 'text' => NULL), ), ); } function theme_boost_cache_status($ttl, $text) { return '' . $text . ''; } ////////////////////////////////////////////////////////////////////////////// // Output buffering callback /** * PHP output buffering callback for static page caching. * * 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) and the HTTP response code. We're going to be // exceedingly conservative here and only cache 'text/html' pages that // were output with a 200 OK status. Anything more is simply asking for // loads of trouble. if (_boost_get_content_type() == 'text/html' && _boost_get_http_status() == 200) { if (strlen($buffer) > 0) { // Sanity check boost_cache_set($GLOBALS['_boost_path'], $buffer); } } // 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'; return _boost_get_http_header($regex, $default); } /** * Determines the HTTP response code that the current page request will be * returning by examining the HTTP headers that have been output so far. */ function _boost_get_http_status($default = 200) { static $regex = '!^HTTP/1.1\s+(\d+)!'; return (int)_boost_get_http_header($regex, $default); } function _boost_get_http_header($regex, $default = NULL) { // The last 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; // no such luck } ////////////////////////////////////////////////////////////////////////////// // Boost API implementation /** * Determines whether a given Drupal page can be cached or not. * * To avoid potentially troublesome situations, the user login page is never * cached, nor are any admin pages. At present, we also refuse to cache any * RSS feeds provided by Drupal, since they would require special handling * in the mod_rewrite ruleset as they shouldn't be sent out using the * text/html content type. * TODO: don't cache pages with unacceptable symbols */ function boost_is_cacheable($path) { $normal_path = drupal_get_normal_path($path); // normalize path // Never cache the basic user login/registration pages or any administration pages if ($normal_path == 'user' || preg_match('!^user/(login|register|password)!', $normal_path) || preg_match('!^admin!', $normal_path)) return FALSE; // At present, RSS feeds are not cacheable due to content type restrictions if ($normal_path == 'rss.xml' || preg_match('!/feed$!', $normal_path)) return FALSE; // Don't cache comment reply pages if (preg_match('!^comment/reply!', $normal_path)) return FALSE; // Match the user's cacheability settings against the path if (BOOST_CACHEABILITY_OPTION == 2) { $result = drupal_eval(BOOST_CACHEABILITY_PAGES); return !empty($result); } $regexp = '/^('. preg_replace(array('/(\r\n?|\n)/', '/\\\\\*/', '/(^|\|)\\\\