You are here

authcache.module in Authenticated User Page Caching (Authcache) 7.2

Same filename and directory in other branches
  1. 6 authcache.module
  2. 7 authcache.module

Authenticated User Page Caching (and anonymous users, too!)

File

authcache.module
View source
<?php

/**
 * @file
 * Authenticated User Page Caching (and anonymous users, too!)
 */

// Default caching rules (Never cache these pages).
define('AUTHCACHE_NOCACHE_DEFAULT', '
user*
node/add/*
node/*/edit
node/*/track
tracker*
civicrm*
cart*
checkout*
system/files/*
system/temporary*
file/ajax/*
js/admin_menu/cache/*
');

// Default non-HTML cachable content-types.
define('AUTHCACHE_MIMETYPE_DEFAULT', '
text/html
text/javascript
text/plain
application/xml
application/atom+xml
');
require_once __DIR__ . '/authcache.cache.inc';

/**
 * Implements hook_menu().
 */
function authcache_menu() {
  $items['admin/config/system/authcache'] = array(
    'title' => '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,
  );
  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;
  }

  // Attach required JavaScript.
  drupal_add_library('system', 'jquery.cookie');
  drupal_add_js(drupal_get_path('module', 'authcache') . '/authcache.js');

  // Inject authcache cookie settings.
  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' => max(1, (int) authcache_key_lifetime() / 86400),
    ),
  ), 'setting');
  if (!authcache_excluded()) {

    // Remember original output buffering level.
    _authcache_original_ob_level(TRUE);
  }
}

/**
 * Implements hook_exit().
 *
 * Called on drupal_goto() redirect.
 * Make sure status messages show up, if applicable.
 */
function authcache_exit($destination = NULL) {
  global $user;

  // Do not continue if not fully bootstrapped.
  if (drupal_get_bootstrap_phase() < DRUPAL_BOOTSTRAP_FULL) {
    return;
  }

  // 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 e.g., if there are messages pending
  // which did not manage it onto the current page.
  $reasons = module_invoke_all('authcache_preclude');
  if (!empty($reasons)) {
    _authcache_preclude(reset($reasons));
  }

  // Set/unset authcache key for this user.
  $authcache_key = authcache_key();
  if ($authcache_key !== authcache_backend_initial_key()) {
    $has_session = !empty($user->uid) || !empty($_SESSION);
    $lifetime = authcache_key_lifetime();
    module_invoke_all('authcache_backend_key_set', $authcache_key, $lifetime, $has_session);
  }

  // Fix cookies if necessary.
  if (!drupal_is_cli() && !headers_sent()) {
    authcache_fix_cookies();
  }

  // Forcibly disable drupal built-in page caching for anonymous users.
  // Prevent drupal_page_set_cache() called from drupal_page_footer() to
  // store the page.
  drupal_page_is_cacheable(FALSE);

  // If this page was excluded in hook_init, we're done here.
  if (authcache_excluded()) {
    return;
  }

  // Give other modules a last chance to cancel page saving.
  $reasons = module_invoke_all('authcache_cancel');
  if (!empty($reasons)) {
    authcache_cancel(reset($reasons));
  }

  // Invoke cache backends and serve page.
  if (authcache_page_is_cacheable()) {
    $cache = authcache_backend_cache_save();
    authcache_serve_page_from_cache($cache, authcache_key());
  }
}

/**
 * Implements hook_flush_caches().
 */
function authcache_flush_caches() {
  return array(
    'cache_authcache_key',
  );
}

/**
 * @defgroup authcache_markup Remove personalization
 * @{
 * Detect personalized content and remove it or cancel caching.
 */

/**
 * Implements hook_form_alter().
 */
function authcache_form_alter(&$form, &$form_state, $form_id) {
  global $user;
  if (authcache_page_is_cacheable()) {

    // Ensure that the default form_after build callback is called first.
    if (empty($form['#after_build'])) {
      $form['#after_build'] = array(
        '_authcache_default_form_after_build',
      );
    }
    else {
      array_unshift($form['#after_build'], '_authcache_default_form_after_build');
    }

    // Add post-render callback ensuring that caching is canceled when a form
    // requiring the form cache is rendered on a page for authenticated users.
    if ($user->uid && !empty($form['form_build_id'])) {
      authcache_element_suspect($form['form_build_id'], t('Form requiring the form cache on the page. Enable authcache_form or implement custom code capable of handling cached forms.'));
    }

    // Add post-render callback ensuring that caching is canceled when a form
    // was not handled by any module.
    if (!empty($form['form_token'])) {
      authcache_element_suspect($form['form_token'], t('Form with CSRF protection on page. Enable authcache_form or implement custom code capable of handling CSRF protected forms.'));
    }

    // Recursively find elements of type value and flag them as suspicous.
    authcache_form_value_suspect($form, $form_id);
  }

  // Alter all forms.
  switch ($form_id) {
    case 'user_profile_form':

      // Don't allow user local timezone.
      if (authcache_account_allows_caching()) {
        unset($form['timezone']);
      }
      break;
    case 'contact_site_form':
    case 'contact_personal_form':
      if ($user->uid && authcache_page_is_cacheable()) {
        unset($form['name']['#default_value']);
        unset($form['mail']['#default_value']);
      }
      break;
  }
}

/**
 * Implements hook_form_BASE_FORM_ID_alter().
 */
function authcache_form_comment_form_alter(&$form, &$form_state) {
  global $user;

  // Author name is tainted when displaying the comment form for new comments.
  // This is not the case when comments are edited though.
  if ($user->uid && authcache_page_is_cacheable() && !$form_state['comment']->cid) {
    $message = t('User name on comment form. Enable Authcache Comment module or implement custom code capable of retrieving/removing the user name.');
    if (isset($form['author']['name']['#type']) && $form['author']['name']['#type'] === 'value') {
      authcache_element_suspect($form['author']['name'], $message);
    }
    if (isset($form['author']['_author'])) {
      authcache_element_suspect($form['author']['_author'], $message);
    }
  }
}

/**
 * Implements hook_form_FORM_ID_alter().
 */
function authcache_form_system_performance_settings_alter(&$form, &$form_state) {
  $roles = authcache_get_roles();
  if (count($roles)) {
    $message = '<p>' . t('Drupal core page caching for anonymous users does not work properly when Authcache is enabled. Please either deactivate Drupal core page caching or disable Authcache.') . '</p>';
    if (!isset($roles[DRUPAL_ANONYMOUS_RID])) {
      $message .= '<p>' . t('You may enable the page cache for anonymous users by <a href="!link">configuring</a> Authcache for anonymous users', array(
        '!link' => url('admin/config/system/authcache'),
      )) . '</p>';
    }
    if (variable_get('page_cache_without_database')) {
      $message .= '<p>' . t('Furthermore it seems that !setting is enabled. Please remove that line from !file, otherwise Authcache will not work properly.', array(
        '!setting' => '<code>$conf[\'page_cache_without_database\']</code>',
        '!file' => '<code>' . conf_path() . '/settings.php</code>',
      )) . '</p>';
    }
    $form['caching']['authcache-warning'] = array(
      '#type' => 'container',
      '#attributes' => array(
        'class' => array(
          'messages',
          'warning',
        ),
      ),
      'message' => array(
        '#markup' => $message,
      ),
      '#weight' => -3,
    );

    // If $conf['page_cache_without_database'] is set in settings.php, the
    // warning will be shown regardless of the value of $conf['cache'].
    // Otherwise states are used to show/hide the warning if appropriate.
    if (!variable_get('page_cache_without_database')) {
      $form['caching']['authcache-warning']['#states'] = array(
        'visible' => array(
          ':input[name=cache]' => array(
            'checked' => TRUE,
          ),
        ),
      );
    }

    // Disable hiding of compression check-box
    unset($form['bandwidth_optimization']['page_compression']['#prefix']);
    unset($form['bandwidth_optimization']['page_compression']['#suffix']);
  }
}

/**
 * Recursively find elements of type value and flag them as suspicous.
 */
function authcache_form_value_suspect(&$element, $form_id, $parents = array()) {
  if (isset($element['#type']) && $element['#type'] === 'value') {
    $name = '';
    $num_parents = count($parents);
    if (isset($element['#name'])) {
      $name = $element['#name'];
    }
    elseif ($num_parents > 0) {
      $name = $parents[0];
      if ($num_parents > 1) {
        $name .= '[' . implode('][', array_slice($parents, 1)) . ']';
      }
    }
    authcache_element_suspect($element, t('Value element %name contained in the cacheable form %form_id. Please enable a suitable Authcache integration module for that form or file a support request.', array(
      '%name' => $name,
      '%form_id' => $form_id,
    )));
  }
  foreach (element_children($element, FALSE) as $key) {
    authcache_form_value_suspect($element[$key], $form_id, array_merge($parents, array(
      $key,
    )));
  }
}

/**
 * Recursively find elements of type value and set them cacheable.
 */
function authcache_form_value_set_cacheable(&$element) {
  if (isset($element['#type']) && $element['#type'] === 'value') {
    authcache_element_set_cacheable($element);
  }
  foreach (element_children($element, FALSE) as $key) {
    authcache_form_value_set_cacheable($element[$key]);
  }
}

/**
 * Form API default after-build callback for forms on cacheable pages.
 */
function _authcache_default_form_after_build($form, $form_state) {
  global $user;
  if (authcache_page_is_cacheable()) {

    // Disable form cache and remove build_id if caching is not explicitely
    // requested.
    if (empty($form_state['rebuild']) && empty($form_state['cache'])) {
      $form_state['no_cache'] = TRUE;
      unset($form['form_build_id']);
      unset($form['#build_id']);

      // Enable caching of form value elements without further processing when
      // the form cache is not used.
      authcache_form_value_set_cacheable($form);
    }
  }
  return $form;
}

/**
 * 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_preprocess_page().
 *
 * Add post-render callback to tabs and action link elements. Caching will be
 * canceled during post-render when no other modules mark the elements as being
 * cacheable.
 */
function authcache_preprocess_page(&$variables) {
  global $user;
  if (authcache_page_is_cacheable()) {
    authcache_element_suspect($variables['tabs'], t('Tabs on page. Enable authcache_menu or implement custom code capable of detecting when caching tabs is acceptable.'));
    authcache_element_suspect($variables['action_links'], t('Action links on page. Enable authcache_menu or implement custom code capable of detecting when caching local actions is acceptable.'));
    if (!$user->uid) {
      authcache_element_set_cacheable($variables['tabs']);
      authcache_element_set_cacheable($variables['action_links']);
    }
  }
}

/**
 * Implements hook_process_page().
 *
 * 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'));
  }
}

/**
 * Implements hook_preprocess_toolbar().
 */
function authcache_preprocess_toolbar(&$variables) {
  if (isset($variables['toolbar']['toolbar_user']['#links']['account'])) {
    $message = t('User name in toolbar. Enable Authcache User module or implement custom code capable of retrieving the user name.');
    authcache_element_suspect($variables['toolbar']['toolbar_user'], $message);
  }
  foreach (element_children($variables['toolbar']['toolbar_drawer']) as $key) {
    if (isset($variables['toolbar']['toolbar_drawer'][$key]['shortcuts']) && function_exists('shortcut_set_switch_access') && shortcut_set_switch_access()) {
      $message = t('Switchable shortcut set in toolbar. Enable Authcache Menu or implement custom code capable of detecting when caching shortcuts is acceptable.');
      authcache_element_suspect($variables['toolbar']['toolbar_drawer'][$key]['shortcuts'], $message);
    }
  }
}

/**
 * Specify that the given element cannot be cached without further processing.
 */
function authcache_element_suspect(&$element, $message = NULL, $strict = FALSE) {
  $element['#post_render'][] = 'authcache_ensure_element_cacheable';
  if ($message) {
    $element['#authcache_cancel_message'] = $message;
  }
  if ($strict || isset($element['#type']) && $element['#type'] === 'value') {
    $element['#authcache_cancel_strict'] = TRUE;
  }
}

/**
 * Mark a render-element as cacheable.
 */
function authcache_element_set_cacheable(&$element, $cacheable = TRUE) {
  $element['#authcache_is_cacheable'] = $cacheable;
}

/**
 * Return TRUE when the given render element is cacheable.
 */
function authcache_element_is_cacheable($element) {
  return !empty($element['#authcache_is_cacheable']);
}

/**
 * Post-render callback for render elements.
 *
 * Ensure that an element was marked as cacheable by one of the supporting
 * modules.
 */
function authcache_ensure_element_cacheable($markup, $element) {
  $strict = !empty($element['#authcache_cancel_strict']);
  if (($strict || !empty($markup)) && !authcache_element_is_cacheable($element)) {
    $message = !empty($element['#authcache_cancel_message']) ? $element['#authcache_cancel_message'] : t('Non cacheable render element on page');
    authcache_cancel($message);
  }
  return $markup;
}

/**
 * @} End of "defgroup authcache_markup"
 */

/**
 * @defgroup authcache_policy Caching policy
 * @{
 * Collect and apply policy rules.
 */

/**
 * Private function called from authcache_init.
 *
 * Turn off authcache, do not alter any aspect of this page.
 */
function _authcache_exclude($reason = NULL) {
  $excluded =& drupal_static(__FUNCTION__, 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) {
  $canceled =& drupal_static(__FUNCTION__, FALSE);
  if (!$canceled && !empty($reason)) {
    $canceled = TRUE;
    module_invoke_all('authcache_canceled', $reason);
  }
  return $canceled;
}

/**
 * Return true if the caching was canceled during page-build.
 */
function authcache_canceled() {
  return authcache_cancel();
}

/**
 * Prevent next page request from beeing served from cache.
 *
 * Private function called from authcache_exit. Authcache should not return a
 * cached result on next page request from the same client.
 */
function _authcache_preclude($reason = NULL) {
  $precluded =& drupal_static(__FUNCTION__, FALSE);
  if (!$precluded && !empty($reason)) {
    $precluded = TRUE;
    module_invoke_all('authcache_precluded', $reason);
  }
  return $precluded;
}

/**
 * Whether next page request will not be served from cache.
 *
 * Return true if the next page request for this client should not be served
 * from the cache.
 */
function authcache_precluded() {
  return _authcache_preclude();
}

/**
 * Return true if this page possibly will be cached later.
 */
function authcache_page_is_cacheable() {
  return !(authcache_excluded() || authcache_canceled());
}

/**
 * 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];
}

/**
 * @} End of "defgroup authcache_policy"
 */

/**
 * @defgroup authcache_key Key computation and management
 * @{
 */

/**
 * Return key-properties.
 *
 * Return characterizing key-value pairs of a browsers capabilities and the
 * HTTP request.
 */
function authcache_key_properties() {
  $properties =& drupal_static(__FUNCTION__);
  if (!isset($properties)) {
    $properties = module_invoke_all('authcache_key_properties');
    drupal_alter('authcache_key_properties', $properties);
  }
  return $properties;
}

/**
 * Calculate key for logged in user from key-properties record.
 *
 * @param array $properties
 *   Structure with key-value pairs as returned by authcache_key_properties()
 * @param bool $superuser
 *   Whether the key is calculated for the superuser. If set to TRUE the
 *   properties data is hashed twice in order to make the key unique.
 */
function authcache_user_key($properties, $superuser = FALSE) {
  ksort($properties);
  $data = serialize($properties);
  $hmac = hash_hmac('sha1', $data, drupal_get_private_key(), FALSE);

  // Make sure superuser has its own key not shared with anyone else (though
  // it would be better if superuser would'nt be allowed to use the cache in
  // the first place).
  if ($superuser) {
    $hmac = hash_hmac('sha1', $hmac . $data, drupal_get_private_key(), FALSE);
  }
  $abbrev = variable_get('authcache_key_abbrev', 7);
  return $abbrev ? substr($hmac, 0, $abbrev) : $hmac;
}

/**
 * Generate and return the authcache key for the given account.
 *
 * @see hook_authcache_key_properties()
 * @see hook_authcache_key_properties_alter()
 */
function authcache_key() {
  global $user;
  if ($user->uid) {

    // Calculate the key for logged in users from key-properties.
    $key = authcache_user_key(authcache_key_properties(), $user->uid == 1);
  }
  else {
    $key = authcache_backend_anonymous_key();
  }
  return $key;
}

/**
 * Return the lifetime of authcache keys in seconds.
 *
 * Defaults to the session.cookie_lifetime INI value. When this function
 * returns zero, client code needs to choose an appropriate method such that
 * the authcache key is stored temporary. For example only as long as a browser
 * is open, or by specifying CACHE_TEMPORARY when the key is stored in a cache
 * bin.
 */
function authcache_key_lifetime() {
  $lifetime = (int) variable_get('authcache_key_lifetime', ini_get('session.cookie_lifetime'));
  return $lifetime > 0 ? $lifetime : 0;
}

/**
 * @} End of "defgroup authcache_key"
 */

/**
 * @defgroup authcache_cookie Cookie management
 * @{
 */

/**
 * Add and remove cookies to the browser session as required.
 *
 * @see hook_authcache_cookie()
 * @see hook_authcache_cookie_alter()
 */
function authcache_fix_cookies($account = NULL) {
  global $user;
  if (!isset($account)) {
    $account = $user;
  }
  $cookies = module_invoke_all('authcache_cookie', $account) + authcache_add_cookie();
  drupal_alter('authcache_cookie', $cookies, $account);
  $default_params = array(
    'present' => FALSE,
    'value' => NULL,
    'lifetime' => authcache_key_lifetime(),
    'path' => ini_get('session.cookie_path'),
    'domain' => ini_get('session.cookie_domain'),
    'secure' => ini_get('session.cookie_secure') == '1',
    'httponly' => FALSE,
    'samesite' => NULL,
  );
  foreach ($cookies as $name => $params) {
    $params += $default_params;

    // PHP converts dots to underscores
    $cookiename = str_replace('.', '_', $name);
    $expires = NULL;
    $value = NULL;
    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[$cookiename]) || $_COOKIE[$cookiename] != $params['value']) {
        $expires = $params['lifetime'] ? REQUEST_TIME + $params['lifetime'] : 0;
        $value = $params['value'];
      }
    }
    elseif (!$params['present'] && isset($_COOKIE[$cookiename])) {

      // Remove spare cookie.
      $expires = REQUEST_TIME - 86400;
      $value = "";
    }
    if (isset($expires) && isset($value)) {
      if (function_exists('drupal_setcookie')) {
        $options = $params;
        $options['expires'] = $expires;
        unset($options['present']);
        unset($options['value']);
        unset($options['lifetime']);
        drupal_setcookie($name, $value, $options);
      }
      else {
        $options = $params;
        setcookie($name, $value, $expires, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
      }
    }
  }
}

/**
 * Add given cookie records to the current page request.
 */
function authcache_add_cookie($new_cookies = array()) {
  $cookies =& drupal_static(__FUNCTION__, array());
  $cookies = $new_cookies + $cookies;
  return $cookies;
}

/**
 * @} End of "defgroup authcache_cookie"
 */

/**
 * @defgroup authcache_helper Helper functions, mostly private
 * @{
 */

/**
 * Diff user roles.
 *
 * 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);
}

/**
 * Helper function, get authcache user roles.
 *
 * @param bool $all_roles
 *   TRUE when all user roles should be returned, FALSE for authcache-enabled
 *   only.
 *
 * @return array
 *   Associative array of role names keyed by their role-id.
 */
function authcache_get_roles($all_roles = FALSE) {
  $roles = user_roles();

  // Clarify that the authenticated user-rid will only be considired if the
  // user does not have any other role.
  $roles[DRUPAL_AUTHENTICATED_RID] = t('@authenticated_user (without additional roles)', array(
    '@authenticated_user' => $roles[DRUPAL_AUTHENTICATED_RID],
  ));
  if (!$all_roles) {
    $authcache_roles = variable_get('authcache_roles', array());
    $roles = array_intersect_key($roles, $authcache_roles);
  }
  return $roles;
}

/**
 * Return current MIME content type.
 *
 * 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($header = NULL) {
  if (!isset($header)) {
    $header = drupal_get_http_header('content-type');
  }
  $params = explode(';', $header);
  $params = array_map('trim', $params);
  $mime = array_shift($params);
  return array(
    'mimetype' => $mime,
    'params' => $params,
  );
}

/**
 * Return HTTP status code.
 *
 * 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;
}

/**
 * Return default pagecaching rule.
 */
function _authcache_default_pagecaching() {

  // By default restrict default cache roles to anonymous and authenticated
  // users.
  return array(
    array(
      'option' => 0,
      'pages' => AUTHCACHE_NOCACHE_DEFAULT,
      'noadmin' => 1,
      'roles' => array(
        'custom' => TRUE,
        'roles' => drupal_map_assoc(array(
          DRUPAL_ANONYMOUS_RID,
          DRUPAL_AUTHENTICATED_RID,
        )),
      ),
    ),
  );
}

/**
 * Utility function used to memoize the ob_level in use during hook_init.
 */
function _authcache_original_ob_level($set = FALSE) {
  $original_ob_level =& drupal_static(__FUNCTION__);
  if ($set) {
    $original_ob_level = ob_get_level();
  }
  return $original_ob_level;
}

/**
 * @} End of "defgroup authcache_helper"
 */

/**
 * @defgroup authcache_hooks Implementations of authcache hooks
 * @{
 */

/**
 * Implements hook_authcache_key_properties().
 */
function authcache_authcache_key_properties() {
  global $user, $base_root;

  // Remove "authenticated user"-role from the account roles except when it is
  // the only role on the account.
  $account_roles = $user->roles;
  if (array_keys($account_roles) !== array(
    DRUPAL_AUTHENTICATED_RID,
  )) {
    unset($account_roles[DRUPAL_AUTHENTICATED_RID]);
  }
  $roles = array_keys($account_roles);
  sort($roles);
  return array(
    'base_root' => $base_root,
    'roles' => $roles,
  );
}

/**
 * Implements hook_authcache_request_exclude().
 */
function authcache_authcache_request_exclude() {

  // The following four basic exclusion rules are mirrored in
  // authcacheinc_retrieve_cache_page() in authcache_builtin.cache.inc
  // BEGIN: basic exclusion rules.
  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 (variable_get('cache') || variable_get('page_cache_without_database')) {
    return t('Drupal core page caching for anonymous users active.');
  }
  $frontscripts = variable_get('authcache_frontcontroller_whitelist', array(
    DRUPAL_ROOT . '/index.php',
  ));
  $frontscripts = array_map('realpath', $frontscripts);
  if (!in_array(realpath($_SERVER['SCRIPT_FILENAME']), $frontscripts)) {
    return t('Not invoked using an allowed front controller.');
  }

  // END: basic exclusion rules.
  //
  if (!authcache_backend()) {
    return t('No active cache backend.');
  }
  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', _authcache_default_pagecaching());
  foreach ($pagecaching as $i => $page_rules) {
    $rule_num = $i + 1;

    // Do caching page roles apply to current user?
    if (authcache_role_restrict_access($page_rules['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' => $rule_num,
            ));
          }
          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' => $rule_num,
            ));
          }
          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' => $rule_num,
        ));
      }
    }
  }
}

/**
 * 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.
  $extra_roles = authcache_diff_roles($account->roles, authcache_get_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),
    ));
  }

  // Ensure that at least one pagecaching rule matches for this account.
  $no_rule_matches = TRUE;
  $pagecaching = variable_get('authcache_pagecaching', _authcache_default_pagecaching());
  foreach ($pagecaching as $page_rules) {

    // Do caching page roles apply to the given account?
    if (authcache_role_restrict_access($page_rules['roles'], $account)) {
      $no_rule_matches = FALSE;
      break;
    }
  }
  if ($no_rule_matches) {
    return t('Account does not match any page caching rule.');
  }
}

/**
 * Implements hook_authcache_preclude().
 */
function authcache_authcache_preclude() {

  // Skip the cache on the next page request when messages are pending.
  if (drupal_set_message()) {
    return t('Pending messages');
  }
}

/**
 * Implements hook_authcache_cancel().
 */
function authcache_authcache_cancel() {

  // 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)) {
    return t('Only cache allowed HTTP content types (HTML, JS, etc)');
  }

  // Check http status.
  if (variable_get('authcache_http200', FALSE) && _authcache_get_http_status() != 200) {
    return t('HTTP status 404/403s/etc');
  }

  // Check headers already were sent.
  if (headers_sent() || ob_get_level() < _authcache_original_ob_level()) {
    return t('Private file transfers or headers were unexpectedly sent');
  }

  // Make sure "Location" redirect isn't used.
  foreach (headers_list() as $header) {
    if (strpos($header, 'Location:') === 0) {
      return 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']) {
      case E_NOTICE:
      case E_USER_NOTICE:
      case E_DEPRECATED:
      case E_USER_DEPRECATED:

        // Ignore these errors.
        break;
      default:

        // Let user know there is PHP error and return.
        return t('PHP Error: @error', array(
          '@error' => $error['message'],
        ));
    }
  }

  // Don't cache if the vary header is not the same as the one required by the
  // active backend.
  if (!authcache_backend_check_vary()) {
    return t('The Vary header was modified during the request');
  }
}

/**
 * Implements hook_authcache_cookie().
 */
function authcache_authcache_cookie($account) {

  // Add/Remove cookie for temporary exclusion.
  $cookies['nocache']['present'] = authcache_precluded();
  $cookies['nocache']['value'] = 1;
  return $cookies;
}

/**
 * @} End of "defgroup authcache_hooks"
 */

/**
 * @defgroup authcache_role_restrict Role configuration widget
 * @{
 * Reusable form API element for role restriction settings.
 */

/**
 * Implements hook_element_info().
 */
function authcache_element_info() {
  $types['authcache_role_restrict'] = array(
    '#input' => TRUE,
    '#tree' => TRUE,
    '#process' => array(
      'authcache_process_role_restrict',
      'form_process_container',
    ),
    '#element_validate' => array(
      'authcache_validate_role_restrict',
    ),
    '#theme_wrappers' => array(
      'container',
    ),
    '#members_only' => FALSE,
  );
  $types['authcache_duration_select'] = array(
    '#input' => TRUE,
    '#process' => array(
      'authcache_process_duration_select',
    ),
    '#element_validate' => array(
      'authcache_validate_duration_select',
    ),
    '#theme_wrappers' => array(
      'form_element',
    ),
  );
  return $types;
}

/**
 * Form API process callback for authcache_role_restrict element.
 */
function authcache_process_role_restrict($element, &$form_state) {
  $authcache_roles = authcache_get_roles();
  $allowed_roles = $authcache_roles;
  if (!empty($element['#members_only'])) {
    unset($allowed_roles[DRUPAL_ANONYMOUS_RID]);
  }
  $defaults = array(
    'custom' => FALSE,
    'roles' => $allowed_roles,
  );
  if (empty($element['#default_value'])) {
    $element['#default_value'] = $defaults;
  }
  else {
    $element['#default_value'] = $element['#default_value'] + $defaults;
  }
  if (empty($allowed_roles)) {
    $description = t('Currently there are no roles enabled for authcache. Fix this in <a href="!admin_link">authcache settings</a>.', array(
      '!admin_link' => url('admin/config/system/authcache'),
    ));
  }
  else {
    $description = format_plural(count($allowed_roles), 'The following role is currently enabled for authcache: %roles.', 'The following roles are currently enabled for authcache: %roles.', array(
      '%roles' => implode(', ', $allowed_roles),
    ));
  }
  $custom_id = drupal_html_id($element['#id'] . '-custom');
  $element['custom'] = array(
    '#type' => 'checkbox',
    '#title' => t('Restrict allowed roles'),
    '#default_value' => $element['#default_value']['custom'],
    '#description' => $description,
    '#disabled' => empty($allowed_roles),
    '#id' => $custom_id,
  );
  $element['roles'] = array(
    '#type' => 'checkboxes',
    '#title' => t('Allowed roles'),
    '#options' => $allowed_roles,
    '#default_value' => array_keys($element['#default_value']['custom'] ? authcache_get_role_restrict_roles($element['#default_value']) : $allowed_roles),
    '#description' => t('Restrict to the selected roles.'),
    '#states' => array(
      'visible' => array(
        '#' . $custom_id => array(
          'checked' => TRUE,
        ),
      ),
    ),
  );
  return $element;
}

/**
 * Validation callback for authcache_role_restrict element.
 */
function authcache_validate_role_restrict($element, &$form_state) {
  $value = $element['#value'];

  // Set value to NULL when custom role restrictions is not selected.
  if (empty($element['custom']['#value'])) {
    $value = NULL;
  }
  elseif (empty($value['roles'])) {
    $value['roles'] = array();
  }
  else {
    $value['roles'] = array_filter($value['roles']);
  }
  form_set_value($element, $value, $form_state);
}

/**
 * Expand duration element to popup menu and text-field for custom values.
 */
function authcache_process_duration_select($element, &$form_state) {
  $durations = isset($element['#durations']) ? $element['#durations'] : array();
  $empty_option = isset($element['#empty_option']) ? $element['#empty_option'] : NULL;
  $empty_value = isset($element['#empty_value']) ? $element['#empty_value'] : NULL;
  $zero_duration = isset($element['#zero_duration']) ? $element['#zero_duration'] : t('Temporary');

  // Determine default values for select and custom textfield.
  $select_default_value = NULL;
  $custom_default_value = NULL;
  if (isset($element['#default_value'])) {
    $select_default_value = $element['#default_value'];
    $custom_default_value = $element['#default_value'];
    if (!in_array($select_default_value, $durations)) {
      $select_default_value = 'custom';
    }
    elseif ($custom_default_value == 0) {

      // We require a positive integer, therefore clear out default value for
      // custom ttl when it is set to 0.
      $custom_default_value = NULL;
    }
  }

  // Generate options.
  // Necessary until #1272900 lands
  // @ignore style_function_spacing
  $options = drupal_map_assoc($durations, function ($duration) use ($zero_duration) {
    return (int) $duration > 0 ? format_interval($duration) : $zero_duration;
  });
  $options['custom'] = t('Custom');

  // Add select for standard durations.
  $element['select'] = array(
    '#type' => 'select',
    '#id' => drupal_html_id($element['#id'] . '-select'),
    '#options' => $options,
    '#default_value' => $select_default_value,
    '#empty_option' => $empty_option,
    '#empty_value' => $empty_value,
  );

  // Add textfield for custom duration.
  $element['custom'] = array(
    '#type' => 'textfield',
    '#id' => drupal_html_id($element['#id'] . '-custom'),
    '#size' => '25',
    '#maxlength' => '30',
    '#default_value' => $custom_default_value,
    '#element_validate' => array(
      'element_validate_integer_positive',
    ),
    '#states' => array(
      'visible' => array(
        '#' . $element['select']['#id'] => array(
          'value' => 'custom',
        ),
      ),
    ),
  );
  $element['#tree'] = TRUE;
  return $element;
}

/**
 * Validation callback for authcache_duration_select element.
 */
function authcache_validate_duration_select($element, &$form_state) {
  $value = $element['select']['#value'] === 'custom' ? $element['custom']['#value'] : $element['select']['#value'];
  form_set_value($element, $value, $form_state);
}

/**
 * Return a list of allowed roles.
 *
 * Depending on the passed in configuration, either the authcache default roles
 * will be returned or the configured roles intersected with the default roles.
 *
 * @param array $config
 *   A role restrict configuration from an authcache_role_restrict widget.
 *
 * @return array
 *   A list of allowed roles (rid => role name)
 */
function authcache_get_role_restrict_roles($config) {
  $authcache_roles = authcache_get_roles();
  if (empty($config['custom'])) {
    $result = $authcache_roles;
  }
  elseif (empty($config['roles'])) {
    $result = array();
  }
  else {
    $result = array_intersect_key($authcache_roles, $config['roles']);
  }
  return $result;
}

/**
 * Check whether given account is allowed by configuration.
 */
function authcache_role_restrict_access($config, $account = NULL) {
  global $user;
  if (!$account) {
    $account = $user;
  }
  $allowed_roles = authcache_get_role_restrict_roles($config);
  $extra_roles = authcache_diff_roles($account->roles, $allowed_roles);
  return empty($extra_roles);
}

/**
 * Check whether given account is allowed by configuration (members only).
 */
function authcache_role_restrict_members_access($config, $account = NULL) {
  global $user;
  if (!$account) {
    $account = $user;
  }
  $allowed_roles = authcache_get_role_restrict_roles($config);
  unset($allowed_roles[DRUPAL_ANONYMOUS_RID]);
  $extra_roles = authcache_diff_roles($account->roles, $allowed_roles);
  return empty($extra_roles);
}

/**
 * @} End of "defgroup authcache_role_restrict"
 */

Functions

Namesort descending Description
authcache_account_allows_caching Return true if the given account is cacheable.
authcache_add_cookie Add given cookie records to the current page request.
authcache_authcache_account_exclude Implements hook_authcache_account_exclude().
authcache_authcache_cancel Implements hook_authcache_cancel().
authcache_authcache_cookie Implements hook_authcache_cookie().
authcache_authcache_key_properties Implements hook_authcache_key_properties().
authcache_authcache_preclude Implements hook_authcache_preclude().
authcache_authcache_request_exclude Implements hook_authcache_request_exclude().
authcache_cancel Prevent this page of beeing stored in the cache after it is built up.
authcache_canceled Return true if the caching was canceled during page-build.
authcache_diff_roles Diff user roles.
authcache_element_info Implements hook_element_info().
authcache_element_is_cacheable Return TRUE when the given render element is cacheable.
authcache_element_set_cacheable Mark a render-element as cacheable.
authcache_element_suspect Specify that the given element cannot be cached without further processing.
authcache_ensure_element_cacheable Post-render callback for render elements.
authcache_excluded Return true if this page is excluded from page caching.
authcache_exit Implements hook_exit().
authcache_fix_cookies Add and remove cookies to the browser session as required.
authcache_flush_caches Implements hook_flush_caches().
authcache_form_alter Implements hook_form_alter().
authcache_form_comment_form_alter Implements hook_form_BASE_FORM_ID_alter().
authcache_form_system_performance_settings_alter Implements hook_form_FORM_ID_alter().
authcache_form_value_set_cacheable Recursively find elements of type value and set them cacheable.
authcache_form_value_suspect Recursively find elements of type value and flag them as suspicous.
authcache_get_content_type Return current MIME content type.
authcache_get_roles Helper function, get authcache user roles.
authcache_get_role_restrict_roles Return a list of allowed roles.
authcache_init Implements hook_init().
authcache_key Generate and return the authcache key for the given account.
authcache_key_lifetime Return the lifetime of authcache keys in seconds.
authcache_key_properties Return key-properties.
authcache_menu Implements hook_menu().
authcache_module_implements_alter Implements hook_module_implements_alter().
authcache_page_is_cacheable Return true if this page possibly will be cached later.
authcache_precluded Whether next page request will not be served from cache.
authcache_preprocess Implements hook_preprocess().
authcache_preprocess_page Implements hook_preprocess_page().
authcache_preprocess_toolbar Implements hook_preprocess_toolbar().
authcache_process_duration_select Expand duration element to popup menu and text-field for custom values.
authcache_process_page Implements hook_process_page().
authcache_process_role_restrict Form API process callback for authcache_role_restrict element.
authcache_role_restrict_access Check whether given account is allowed by configuration.
authcache_role_restrict_members_access Check whether given account is allowed by configuration (members only).
authcache_user_key Calculate key for logged in user from key-properties record.
authcache_validate_duration_select Validation callback for authcache_duration_select element.
authcache_validate_role_restrict Validation callback for authcache_role_restrict element.
_authcache_default_form_after_build Form API default after-build callback for forms on cacheable pages.
_authcache_default_pagecaching Return default pagecaching rule.
_authcache_exclude Private function called from authcache_init.
_authcache_get_http_status Return HTTP status code.
_authcache_original_ob_level Utility function used to memoize the ob_level in use during hook_init.
_authcache_preclude Prevent next page request from beeing served from cache.

Constants