'Authcache',
'description' => 'Configure authenticated user page caching.',
'page callback' => 'drupal_get_form',
'page arguments' => array('authcache_admin_config'),
'access arguments' => array('administer site configuration'),
'file' => 'authcache.admin.inc',
'weight' => 10,
);
$items['admin/config/system/authcache/config'] = array(
'title' => 'Configuration',
'type' => MENU_DEFAULT_LOCAL_TASK,
'weight' => -10,
);
$items['admin/config/system/authcache/pagecaching'] = array(
'title' => 'Page caching settings',
'description' => "Configure page cache settings.",
'page callback' => 'drupal_get_form',
'page arguments' => array('authcache_admin_pagecaching'),
'access arguments' => array('administer site configuration'),
'file' => 'authcache.admin.inc',
'type' => MENU_LOCAL_TASK,
'weight' => 20,
);
$items['authcache/ajax'] = array(
'title' => 'Javascript ajax Callback',
'page callback' => 'authcache_ajax',
'access arguments' => array('administer site configuration'),
'file' => 'authcache.admin.inc',
'type' => MENU_CALLBACK,
);
return $items;
}
/**
* Implements hook_module_implements_alter().
*
* Make sure that hook_init of this module is called before all other modules
* and vice versa for hook_exit.
*/
function authcache_module_implements_alter(&$implementations, $hook) {
if ($hook == 'init') {
$me = $implementations['authcache'];
unset($implementations['authcache']);
$implementations = array_merge(array('authcache' => $me), $implementations);
}
if ($hook == 'exit') {
$me = $implementations['authcache'];
unset($implementations['authcache']);
$implementations['authcache'] = $me;
}
}
/**
* Implements hook_init().
*/
function authcache_init() {
global $conf, $user;
$reasons = module_invoke_all('authcache_request_exclude');
if (!empty($reasons)) {
_authcache_exclude(reset($reasons));
}
$reasons = module_invoke_all('authcache_account_exclude', $user);
if (!empty($reasons)) {
_authcache_exclude(reset($reasons));
}
if (!authcache_excluded()) {
// Don't allow format_date() to use the user's local timezone
$conf['configurable_timezones'] = FALSE;
// Start output buffering
ob_start();
}
// Attach required JavaScript
drupal_add_library('system', 'jquery.cookie');
drupal_add_js(drupal_get_path('module', 'authcache') . '/authcache.js');
// Inject authcache cookie settings.
$lifetime = ini_get('session.cookie_lifetime');
$lifetime = (!empty($lifetime) && is_numeric($lifetime) ? (int)$lifetime : 0);
drupal_add_js(array('authcache' => array(
'q' => $_GET['q'],
'cp' => array(
'path' => ini_get('session.cookie_path'),
'domain' => ini_get('session.cookie_domain'),
'secure' => ini_get('session.cookie_secure') == '1',
),
'cl' => $lifetime/86400,
)), 'setting');
// Fix cookies if necessary.
$flags = (authcache_account_allows_caching()) ?
AUTHCACHE_FLAGS_ACCOUNT_ENABLED : AUTHCACHE_FLAGS_NONE;
authcache_fix_cookies($flags);
}
/**
* Implements hook_user_login().
*/
function authcache_user_login(&$edit, $account) {
$flags = AUTHCACHE_FLAGS_LOGIN_ACTION;
if (authcache_account_allows_caching($account)) {
$flags |= AUTHCACHE_FLAGS_ACCOUNT_ENABLED;
}
authcache_fix_cookies($flags, $account);
}
/**
* Implements hook_user_logout().
*/
function authcache_user_logout($account) {
// Note: include same cookie deletion in ajax/authcache.module
authcache_fix_cookies(AUTHCACHE_FLAGS_LOGOUT_ACTION, $account);
}
/**
* Implements hook_form_alter(),
*/
function authcache_form_alter(&$form, &$form_state, $form_id) {
if (authcache_page_is_cacheable()) {
// Need to postpone the decision whether the form and the page is cacheable
// to an after-build callback.
$form['#after_build'][] = '_authcache_form_after_build';
}
// Alter all forms
switch ($form_id) {
// Alter Drupal's "Performance" admin form
case 'system_performance_settings':
$form['caching']['cache']['#description'] = ' ' . t('If Authcache is enabled for the "anonymous user" role, Drupal\'s built-in page caching will be automatically disabled since all page caching is done through Authcache API instead of Drupal core.') . '';
if (authcache_account_allows_caching(drupal_anonymous_user())) {
$form['caching']['cache']['#disabled'] = TRUE; //array(0 => t('Disabled') . ' ' . t('by') . ' Authcache');
$form['caching']['cache']['#value'] = TRUE;
}
break;
case 'user_profile_form':
// Don't allow user local timezone
if (authcache_account_allows_caching()) {
unset($form['timezone']);
}
break;
}
}
/**
* Form after_build callback for all forms on cacheable pages
*
* Disable storing the form to form-cache if possible. However some forms (especially
* Ajax-enabled ones) require the form cache. In this case page-caching must be
* cancelled.
*
* @see drupal_build_form().
*/
function _authcache_form_after_build($form, $form_state) {
$form_id = $form['#form_id'];
if (isset($form['form_token']) && !authcache_get_request_property('ajax')) {
authcache_cancel(t('Form with CSRF protected on page but cannot use AJAX to defer token-retrieval.'));
}
else {
if (empty($form_state['rebuild']) && empty($form_state['cache'])) {
// Disable form cache and remove build_id if caching is not explicitely requested
$form_state['no_cache'] = TRUE;
unset($form['form_build_id']);
unset($form['#build_id']);
}
if (isset($form['form_token']) && authcache_get_request_property('ajax')) {
// Remove CSRF-token from built form and make sure it can be retrieved
// later using AJAX.
unset($form['form_token']);
drupal_add_js(drupal_get_path('module', 'authcache') . '/authcache.formtokenids.js');
drupal_add_js(array('aceformtokenids' => array(
$form_id => (isset($form['#token'])) ? $form['#token'] : $form_id,
)), 'setting');
}
}
return $form;
}
/**
* Process page template variables.
*/
function authcache_preprocess_page(&$variables) {
if (user_is_logged_in() && authcache_page_is_cacheable()) {
if (authcache_get_request_property('ajax')) {
drupal_add_js(drupal_get_path('module', 'authcache') . '/authcache.tabs.js');
}
$variables['tabs']['#post_render'][] = 'authcache_wrap_tabs';
$variables['action_links']['#post_render'][] = 'authcache_wrap_local_actions';
}
}
/**
* Post-render callback for page-tabs. Wrap them into an authcache span, so we
* can find it again in JavaScript.
*/
function authcache_wrap_tabs($markup) {
if (!empty($markup)) {
if (authcache_get_request_property('ajax')) {
$markup = '' . $markup . '';
}
else {
authcache_cancel(t('Tabs on page but Authcache AJAX not enabled.'));
}
}
return $markup;
}
/**
* Post-render callback for local actions. Wrap them into an authcache span, so
* we can find it again in JavaScript.
*/
function authcache_wrap_local_actions($markup) {
if (!empty($markup)) {
if (authcache_get_request_property('ajax')) {
$markup = '' . $markup . '';
}
else {
authcache_cancel(t('Local actions on page but Authcache AJAX not enabled.'));
}
}
return $markup;
}
/**
* Implements hook_exit().
*
* Called on drupal_goto() redirect.
* Make sure status messages show up, if applicable.
*/
function authcache_exit($destination = NULL) {
// Cancel caching when hook_exit was called from drupal_goto.
if ($destination !== NULL) {
authcache_cancel(t('Redirecting to @destination', array('@destination' => $destination)));
}
// Disable authcache on next page request if there are messages pending which
// did not manage it onto the current page.
if (drupal_set_message()) {
authcache_fix_cookies(AUTHCACHE_FLAGS_NONE);
}
// If this page was excluded in hook_init, we're done here.
if (authcache_excluded()) {
return;
}
// Forcibly disable drupal built-in page caching for anonymous users.
// Prevent drupal_page_set_cache() called from drupal_page_footer() to
// store the page a second time after we did.
drupal_page_is_cacheable(FALSE);
// Cache and output
if ($cache = authcache_page_set_cache()) {
drupal_serve_page_from_cache($cache);
}
else {
ob_end_flush();
}
}
//
// Preprocess functions
//
/**
* Implements hook_preprocess().
*
* Inject authcache variables into every template.
*/
function authcache_preprocess(&$variables, $hook) {
// Define variables for templates files
$variables['authcache_is_cacheable'] = authcache_page_is_cacheable();
}
/**
* Implements hook_process_HOOK().
*
* Prevent caching pages with status messages on them. Note that due to the
* fact the messages are only added in template_process_page, we also need to
* use the process-hook.
*/
function authcache_process_page(&$variables) {
if (!empty($variables['messages']) && authcache_page_is_cacheable()) {
authcache_cancel(t('Status message on page'));
}
}
//
// API for other modules.
//
/**
* Private function called from authcache_init. Authcache should not alter any
* aspect of this page.
*/
function _authcache_exclude($reason = NULL) {
// No need for drupal_static here, flag may not be reset anyway.
static $excluded = FALSE;
if (!$excluded && !empty($reason)) {
$excluded = TRUE;
module_invoke_all('authcache_excluded', $reason);
}
return $excluded;
}
/**
* Return true if this page is excluded from page caching.
*/
function authcache_excluded() {
return _authcache_exclude();
}
/**
* Prevent this page of beeing stored in the cache after it is built up.
*/
function authcache_cancel($reason = NULL) {
// No need for drupal_static here, flag may not be reset anyway.
static $cancelled = FALSE;
if (!$cancelled && !empty($reason)) {
$cancelled = TRUE;
module_invoke_all('authcache_cancelled', $reason);
}
return $cancelled;
}
/**
* Return true if the caching of the page request was cancelled during
* page-build.
*/
function authcache_cancelled() {
return authcache_cancel();
}
/**
* Return true if this page possibly will be cached later.
*/
function authcache_page_is_cacheable() {
return !(authcache_excluded() || authcache_cancelled());
}
/**
* Return true if the given account is cacheable.
*/
function authcache_account_allows_caching($account = NULL) {
global $user;
$cacheable = &drupal_static(__FUNCTION__);
if (!isset($account)) {
$account = $user;
}
if (!isset($cacheable[$account->uid])) {
$reasons = module_invoke_all('authcache_account_exclude', $account);
$cacheable[$account->uid] = empty($reasons);
}
return $cacheable[$account->uid];
}
/**
* Return characterizing key-value pairs of a browsers capabilities and the
* HTTP request.
*/
function authcache_request_properties() {
static $properties;
if (!isset($properties)) {
$properties = module_invoke_all('authcache_request_properties');
drupal_alter('authcache_request_properties', $properties);
}
return $properties;
}
/**
* Return characterizing properties of groups the given account is a member of.
*/
function authcache_account_properties($account = NULL) {
global $user;
static $properties;
if (!isset($account)) {
$account = $user;
}
if (!isset($properties)) {
$properties = module_invoke_all('authcache_account_properties', $account);
drupal_alter('authcache_account_properties', $properties, $account);
}
return $properties;
}
/**
* Return the property value of the given request property or null.
*
* @see hook_authcache_request_properties().
*/
function authcache_get_request_property($name) {
$properties = authcache_request_properties();
return isset($properties[$name]) ? $properties[$name] : NULL;
}
/**
* Return the property value of the given account property or null.
*
* @see hook_authcache_account_properties().
*/
function authcache_get_account_property($name, $account = NULL) {
$properties = authcache_account_properties($account);
return isset($properties[$name]) ? $properties[$name] : NULL;
}
/**
* Return the properties used as a base for calculation of the authcache key.
*
* @see authcache_key().
* @see hook_authcache_key_properties_alter().
*/
function authcache_key_properties($account = NULL) {
global $user;
if (!isset($account)) {
$account = $user;
}
$properties = array(
'request' => authcache_request_properties(),
'account' => authcache_account_properties($account),
);
drupal_alter('authcache_key_properties', $properties, $account);
return $properties;
}
/**
* Generate and return the authcache key for the given account.
*
* @see hook_authcache_key_properties().
* @see hook_authcache_key_properties_alter().
*/
function authcache_key($account = NULL) {
global $base_root, $user;
if (!isset($account)) {
$account = $user;
}
if ($account->uid) {
// Calculate the key for logged in users from key-properties.
$data = serialize(authcache_key_properties($account));
$hmac = hash_hmac('sha1', $data, drupal_get_private_key(), FALSE);
$abbrev = variable_get('authcache_hmac_abbrev', 7);
$key = $abbrev ? substr($hmac, 0, $abbrev) : $hmac;
}
else {
// Generate base-key for anonymous users.
$generator = variable_get('authcache_key_generator');
if (is_callable($generator)) {
$key = call_user_func($generator);
}
else {
$key = $base_root;
}
}
return $key;
}
/**
* Return the authcache cache-id for the given path.
*
* @see authcache_key().
*/
function authcache_cid($request_uri = NULL, $account = NULL) {
if (!isset($request_uri)) {
$request_uri = request_uri();
}
$key = authcache_key($account);
return $key . $request_uri;
}
/**
* Add and remove cookies to the browser session as required.
*
* @see hook_authcache_cookie().
* @see hook_authcache_cookie_alter().
*/
function authcache_fix_cookies($flags, $account = NULL) {
global $user;
if (!isset($account)) {
$account = $user;
}
$cookies = module_invoke_all('authcache_cookie', $flags, $account);
drupal_alter('authcache_cookie', $cookies, $flags, $account);
$default_params = array(
'present' => FALSE,
'value' => NULL,
'lifetime' => ini_get('session.cookie_lifetime'),
'path' => ini_get('session.cookie_path'),
'domain' => ini_get('session.cookie_domain'),
'secure' => ini_get('session.cookie_secure') == '1',
'httponly' => FALSE,
);
foreach ($cookies as $name => $params) {
$params += $default_params;
if ($params['present']) {
// Fix cookie if it is not present in the users browser or the value does
// not match our expectations.
if (!isset($_COOKIE[$name]) || $_COOKIE[$name] != $params['value']) {
$expires = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
setcookie($name, $params['value'], $expires, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
}
elseif (!$params['present'] && isset($_COOKIE[$name])) {
// Remove spare cookie
setcookie($name, "", REQUEST_TIME - 86400, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
}
}
}
/**
* Returns an array containing all the roles from account_roles that are not
* present in allowed_roles.
*/
function authcache_diff_roles($account_roles, $allowed_roles) {
// Remove "authenticated user"-role from the account roles except when it is
// the only role on the account.
if (array_keys($account_roles) != array(DRUPAL_AUTHENTICATED_RID)) {
unset($account_roles[DRUPAL_AUTHENTICATED_RID]);
}
return array_diff_key($account_roles, $allowed_roles);
}
/**
* 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 _authcache_get_content_type($default = NULL) {
$params = explode(';', drupal_get_http_header('content-type'));
$params = array_map('trim', $params);
$mime = array_shift($params);
return array(
'mimetype' => $mime,
'params' => $params,
);
}
/**
* 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 _authcache_get_http_status($status = 200) {
$value = drupal_get_http_header('status');
return isset($value) ? (int) $value : $status;
}
/**
* Stores the current page in the cache.
*
* @see hook_authcache_presave().
* @see hook_authcache_cache_alter().
* @see drupal_page_set_cache().
*/
function authcache_page_set_cache() {
// Give other modules a last chance to cancel page saving
module_invoke_all('authcache_presave');
if (authcache_page_is_cacheable()) {
$cache = (object) array(
'cid' => authcache_cid(),
'data' => array(
'path' => $_GET['q'],
'body' => ob_get_clean(),
'title' => drupal_get_title(),
'headers' => array(),
),
'expire' => CACHE_TEMPORARY,
'created' => REQUEST_TIME,
);
// Restore preferred header names based on the lower-case names returned
// by drupal_get_http_header().
$header_names = _drupal_set_preferred_header_name();
foreach (drupal_get_http_header() as $name_lower => $value) {
$cache->data['headers'][$header_names[$name_lower]] = $value;
if ($name_lower == 'expires') {
// Use the actual timestamp from an Expires header if available.
$cache->expire = strtotime($value);
}
}
if ($cache->data['body']) {
if (variable_get('page_compression', TRUE) && extension_loaded('zlib')) {
$cache->data['body'] = gzencode($cache->data['body'], 9, FORCE_GZIP);
}
// Let other modules act on the cacheable data.
drupal_alter('authcache_cache', $cache);
cache_set($cache->cid, $cache->data, 'cache_page', $cache->expire);
}
return $cache;
}
}
//
// Authcache hooks
//
/**
* Implements hook_authcache_request_properties().
*/
function authcache_authcache_request_properties() {
global $base_url;
return array(
'js' => !empty($_COOKIE['has_js']),
'base_url' => $base_url,
);
}
/**
* Implements hook_authcache_account_properties().
*/
function authcache_authcache_account_properties($account) {
$roles = array_keys($account->roles);
sort($roles);
return array(
'roles' => $roles,
);
}
/**
* Implements hook_authcache_request_exclude().
*/
function authcache_authcache_request_exclude() {
global $user;
// The following three basic exclusion rules are mirrored in
// authcacheinc_retrieve_cache_page() in authcache.inc
// BEGIN: basic exclusion rules
if (drupal_is_cli()) {
return t('Running as CLI script');
}
if (!($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')) {
return t('Only GET and HEAD requests allowed. Method for this request is: @method.',
array('@method' => $_SERVER['REQUEST_METHOD']));
}
if (($ar = explode('?', basename(request_uri()))) && substr(array_shift($ar), -4) == '.php') {
return t('PHP files (cron.php, update.php, etc)');
}
// END: basic exclusion rules
module_load_install('authcache');
$requirements = module_invoke('authcache', 'requirements', 'runtime');
if (isset($requirements['authcache']['severity']) && $requirements['authcache']['severity'] == REQUIREMENT_ERROR) {
return $requirements['authcache']['description'];
}
if (variable_get('authcache_noajax', FALSE)
&& isset($_SERVER['HTTP_X_REQUESTED_WITH'])
&& strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'
) {
return t('Ajax request');
}
$alias = drupal_get_path_alias($_GET['q']);
// Now check page caching settings, defined by the site admin
$pagecaching = variable_get('authcache_pagecaching', array(array(
'option' => 0,
'pages' => AUTHCACHE_NOCACHE_DEFAULT,
'roles' => array(DRUPAL_ANONYMOUS_RID),
)));
foreach ($pagecaching as $i => $page_rules) {
// Do caching page roles apply to current user?
$extra_roles = authcache_diff_roles($user->roles, $page_rules['roles']);
if (empty($extra_roles)) {
switch ($page_rules['option']) {
case '0': // Cache every page except the listed pages.
case '1': // Cache only the listed pages.
$page_listed = drupal_match_path($alias, $page_rules['pages']);
if (!(!($page_rules['option'] xor $page_listed))) {
return t('Caching disabled by path list of page ruleset #@number', array('@number' => $i));
}
break;
case '2': // Cache pages for which the following PHP code returns TRUE
$result = 0;
if (module_exists('php')) {
$result = php_eval($page_rules['pages']);
}
if (empty($result)) {
return t('Caching disabled by PHP rule of page ruleset #@number', array('@number' => $i));
}
break;
default:
break;
}
if (!empty($page_rules['noadmin']) && path_is_admin(current_path())) {
return t('Not caching admin pages (by page ruleset #@number)', array('@number' => $i));
}
}
}
}
/**
* Implements hook_authcache_account_exclude().
*/
function authcache_authcache_account_exclude($account) {
// Bail out from requests by superuser (uid=1)
if ($account->uid == 1 && !variable_get('authcache_su', 0)) {
return t('Caching disabled for superuser');
}
// Check for non-cacheable roles of the account.
$cache_roles = variable_get('authcache_roles', array());
$extra_roles = authcache_diff_roles($account->roles, $cache_roles);
if (!empty($extra_roles)) {
return format_plural(count($extra_roles),
'Account has non-cachable role @roles',
'Account has non-cachable roles @roles',
array('@roles' => implode(', ', $extra_roles)));
}
// If JavaScript is disabled on the users browser, check if chaching is still
// allowed.
if (!authcache_get_request_property('js')) {
$nojs_roles = variable_get('authcache_nojsroles', drupal_map_assoc(array(DRUPAL_ANONYMOUS_RID)));
$extra_roles = authcache_diff_roles($account->roles, $nojs_roles);
if (!empty($extra_roles)) {
return format_plural(count($extra_roles),
'Role @roles is not cacheable if JavaScript is disabled.',
'Roles @roles are not cacheable if JavaScript is disabled.',
array('@roles' => implode(', ', $extra_roles)));
}
}
}
/**
* Implements hook_authcache_presave().
*/
function authcache_authcache_presave() {
// Check content-type
$content_type = _authcache_get_content_type();
$allowed_mimetypes = preg_split('/(\r\n?|\n)/', variable_get('authcache_mimetype', AUTHCACHE_MIMETYPE_DEFAULT), -1, PREG_SPLIT_NO_EMPTY);
if (!in_array($content_type['mimetype'], $allowed_mimetypes)) {
authcache_cancel(t('Only cache allowed HTTP content types (HTML, JS, etc)'));
}
// Check http status
if (variable_get('authcache_http200', FALSE) && _authcache_get_http_status() != 200) {
authcache_cancel(t('Don`t cache 404/403s/etc'));
}
// Check headers already were sent
if (headers_sent()) {
authcache_cancel(t('Don`t cache private file transfers or if headers were unexpectly sent.'));
}
// Make sure "Location" redirect isn't used
foreach (headers_list() as $header) {
if (strpos($header, 'Location:') === 0) {
authcache_cancel(t('Location header detected'));
}
}
// Don't cache pages with PHP errors (Drupal can't catch fatal errors)
if (function_exists('error_get_last') && $error = error_get_last()) {
switch ($error['type']) {
// Ignore these errors:
case E_NOTICE: // run-time notices
case E_USER_NOTICE: // user-generated notice message
case E_DEPRECATED: // run-time notices
case E_USER_DEPRECATED: // user-generated notice message
break;
default:
// Let user know there is PHP error and return
authcache_cancel(t('PHP Error: @error', array('@error' => error_get_last())));
break;
}
}
}
/**
* Implements hook_authcache_cookie().
*/
function authcache_authcache_cookie($flags, $account) {
$authenticated = $account->uid;
$enabled = $flags & AUTHCACHE_FLAGS_ACCOUNT_ENABLED;
$present = $authenticated && $enabled;
$cookies['authcache']['present'] = $present;
$cookies['authcache']['httponly'] = TRUE;
$cookies['drupal_user']['present'] = $present;
$cookies['drupal_uid']['present'] = $present;
if ($present) {
$cookies['authcache']['value'] = authcache_key($account);
$cookies['drupal_user']['value'] = $account->name;
$cookies['drupal_uid']['value'] = $account->uid;
}
return $cookies;
}
/**
* Implements hook_aceajax_request().
*/
function authcache_aceajax_request() {
$request['tab'] = array(
'maxage' => 86400,
);
return $request;
}
/**
* Implements hook_aceajax_command().
*/
function authcache_aceajax_command() {
return array(
'form_token_id' => array(
'class' => 'AuthcacheFormTokenIdCommand',
),
'menu_local_tasks' => array(
'class' => 'AuthcacheMenuLocalTasksCommand',
'bootstrap' => DRUPAL_BOOTSTRAP_FULL,
),
);
}