You are here

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

Form token retrieval for Authcache.

File

modules/authcache_form/authcache_form.module
View source
<?php

/**
 * @file
 * Form token retrieval for Authcache.
 */

/**
 * Implements hook_menu().
 */
function authcache_form_menu() {
  $items['admin/config/system/authcache/forms'] = array(
    'title' => 'Forms',
    'description' => "Configure form settings.",
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'authcache_form_admin',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file' => 'authcache_form.admin.inc',
    'type' => MENU_LOCAL_TASK,
  );
  return $items;
}

/**
 * Implements hook_authcache_p13n_fragment().
 */
function authcache_form_authcache_p13n_fragment() {
  return array(
    'form-token' => array(
      'admin name' => t('Token'),
      'admin group' => t('Form'),
      'admin description' => t('Retrieve CSRF form tokens for authenticated users.'),
      'admin path' => 'admin/config/system/authcache/forms',
      'fragment' => array(
        '#class' => 'AuthcacheFormTokenFragment',
      ),
      'cache maxage' => authcache_form_cache_lifespan(),
    ),
  );
}

/**
 * Implements hook_cacheobject_load().
 */
function authcache_form_cacheobject_load($objects, $cids, $bin) {
  if ($bin === 'cache_form') {
    foreach ($objects as $object) {

      // Restore the token if this entry has been cloned from an immutable form
      // cache entry (see authcache_form_form_alter).
      if (!empty($object->data['#authcache_immutable'])) {
        unset($object->data['#authcache_immutable']);
        $object->data['#cache_token'] = drupal_get_token();
      }
      elseif (isset($object->data['#cache_token_authcache_key'])) {

        // Compatibility layer for cache entries created with previous authcache
        // versions.
        if ($object->data['#cache_token_authcache_key'] === authcache_key()) {
          $object->data['#cache_token'] = drupal_get_token();
        }
      }
    }
  }
}

/**
 * Implements hook_cacheobject_presave().
 */
function authcache_form_cacheobject_presave($object, $cid, $bin) {
  if ($bin === 'cache_form' && authcache_page_is_cacheable()) {

    // Extend the expiry period for cached forms.
    $object->expire = REQUEST_TIME + authcache_form_cache_lifespan();
  }
}

/**
 * Implements hook_form_alter().
 */
function authcache_form_form_alter(&$form, &$form_state, $form_id) {
  $page_is_cacheable = authcache_page_is_cacheable();
  if ($page_is_cacheable) {

    // When a form is rendered on a cached page, set the 'immutable' flag in the
    // form state. Drupal makes sure that the form build_id is regenerated and
    // the 'immutable' flag cleared when a form and its associated form state
    // are subsequently loaded and reused by (different) anonymous users. As a
    // result, forms and their assaciated form state having that flag set are
    // never modified. Instead a mutable copy is used whenever users begin to
    // interact with a form on a cached page. This mechanism has been introduced
    // in Drupal 7.27 in order to prevent form state leaking between users
    // interacting with the same form at the same time on a cached page.
    //
    // For authenticated users, there is an additional measure in place. A
    // per-user token is stored along with the form structure in the form cache.
    // When the form is loaded from the cache, this token is validated in order
    // to prevent reuse of the form structure by multiple users. Regrettably
    // this validation is also enforced for forms with the 'immutable' flag set
    // even though this wouldn't be necessary at all.
    //
    // Therefore we also mark the form structure (not only the form state) with
    // an authcache-specific immutable flag such that we can regenarete the
    // #cache_token cacheobject_load hook in order to sidestep the token
    // validation for immutable forms.
    //
    // @see
    // https://www.drupal.org/SA-CORE-2014-002
    $form_state['build_info']['immutable'] = TRUE;
    $form['#authcache_immutable'] = TRUE;
  }
  if (_authcache_form_allow_notoken($form_id)) {

    // Removal of form token is allowed on this form. When removing form tokens
    // we need to do that on both cacheable as well as uncacheable versions of
    // the page. Otherwise form-processing will not work as soon as the form is
    // submitted.
    unset($form['#token']);
    unset($form['form_token']);
  }
  elseif ($page_is_cacheable && _authcache_form_allow_p13n($form_id)) {
    $form['#after_build'][] = '_authcache_form_after_build';
  }

  // Use the base_form_id as the CSRF token value. This helps with reducing
  // the number of tokens which need to be retrieved if one form is repeated
  // over and over accross a site, e.g., the commerce add-to-cart form on a
  // product listing. Note we need to do that on both cacheable as well as
  // uncacheable versions of the page. Otherwise form-processing will not work
  // as soon as the form is submitted.
  if (isset($form['#token']) && isset($form_state['build_info']['base_form_id'])) {
    $base_form_id = $form_state['build_info']['base_form_id'];
    if (_authcache_form_allow_base_id_token($base_form_id)) {
      $form['#token'] = $base_form_id;
      if (!empty($form['form_token']['#default_value'])) {

        // Just in case caching is canceled later on, ensure that the hidden
        // token field has the correct token.
        $form['form_token']['#default_value'] = drupal_get_token($form['#token']);
      }
    }
  }
}

/**
 * Returns the ttl for form-cache entries.
 *
 * @returns int
 *   The number of seconds a form should be retained in the cache.
 */
function authcache_form_cache_lifespan() {

  // The default ttl for form cache entries is hard-coded to 6 hours in
  // form_set_cache(). Let's extend that to one week if Cache Object API is
  // enabled.
  $default = module_exists('cacheobject') ? 604800 : 21600;
  return (int) variable_get('authcache_form_cache_lifespan', $default);
}

/**
 * Form after_build callback for forms on cacheable pages.
 *
 * Setup form such that the per-session form token (used for CSRF protection)
 * can be retrieved separately.
 *
 * @see drupal_build_form()
 */
function _authcache_form_after_build($form, $form_state) {
  global $user;
  if (authcache_page_is_cacheable()) {
    if (!empty($form['form_build_id']) && $user->uid && !authcache_element_is_cacheable($form['form_build_id'])) {

      // Cached forms break for authenticated users unless Cache Object API is
      // configured properly.
      if (module_exists('cacheobject')) {
        authcache_element_set_cacheable($form['form_build_id']);
      }
      else {
        authcache_cancel(t('Cached form on the page (likely Ajax enabled). Download and configure the Cache Object API module.'));
      }
    }
    if (!empty($form['form_token']) && !authcache_element_is_cacheable($form['form_token'])) {

      // Replace hidden form_token input with personalization request fragment.
      $form_token_id = isset($form['#token']) ? $form['#token'] : $form['#form_id'];
      authcache_p13n_attach($form['form_token'], array(
        '#theme' => 'authcache_p13n_fragment',
        '#fragment' => 'form-token',
        '#param' => $form_token_id,
        '#fallback' => 'cancel',
      ));
      authcache_element_set_cacheable($form['form_token']);
    }
  }
  return $form;
}

/**
 * Test whether defered retrieval of form token / build-id is allowed.
 *
 * @param string $form_id
 *   The form id to test (not used currently)
 * @param object $account
 *   The account to test.
 *
 * @return bool
 *   TRUE if config allows retrieval of the form token, FALSE otherwise.
 */
function _authcache_form_allow_p13n($form_id, $account = NULL) {
  return authcache_role_restrict_access(variable_get('authcache_form_roles'), $account) && module_exists('authcache_p13n');
}

/**
 * Test whether stripping of CSRF token is allowed for the given form.
 *
 * @param string $form_id
 *   The form id to test.
 * @param object $account
 *   The account to test.
 *
 * @return bool
 *   TRUE if config allows removal of the form token, FALSE otherwise.
 */
function _authcache_form_allow_notoken($form_id, $account = NULL) {
  return authcache_role_restrict_members_access(variable_get('authcache_form_notoken_roles'), $account) && _authcache_form_match_form_id($form_id, variable_get('authcache_form_notoken', ''));
}

/**
 * Test whether CSRF token based on base form id is allowed.
 *
 * @param string $base_form_id
 *   The form id to test.
 * @param object $account
 *   The account to test.
 *
 * @return bool
 *   TRUE if config allows tokens based on base form id, FALSE otherwise.
 */
function _authcache_form_allow_base_id_token($base_form_id, $account = NULL) {
  return authcache_account_allows_caching($account) && _authcache_form_match_form_id($base_form_id, variable_get('authcache_form_base_id_token', '*'));
}

/**
 * Check if a form_id matches any pattern in a set of patterns.
 *
 * @param string $form_id
 *   The form id to match.
 * @param string $patterns
 *   String containing a set of patterns separated by \n, \r or \r\n.
 *
 * @return bool
 *   TRUE if the form id matches a pattern, FALSE otherwise.
 *
 * @see drupal_match_path()
 */
function _authcache_form_match_form_id($form_id, $patterns) {
  $regexps =& drupal_static(__FUNCTION__);
  if (!isset($regexps[$patterns])) {

    // Convert path settings to a regular expression.
    // Therefore replace newlines with a logical or and /* with asterisks.
    $to_replace = array(
      '/(\\r\\n?|\\n)/',
      '/\\\\\\*/',
    );
    $replacements = array(
      '|',
      '.*',
    );
    $patterns_quoted = preg_quote($patterns, '/');
    $regexps[$patterns] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/';
  }
  return (bool) preg_match($regexps[$patterns], $form_id);
}

Functions

Namesort descending Description
authcache_form_authcache_p13n_fragment Implements hook_authcache_p13n_fragment().
authcache_form_cacheobject_load Implements hook_cacheobject_load().
authcache_form_cacheobject_presave Implements hook_cacheobject_presave().
authcache_form_cache_lifespan Returns the ttl for form-cache entries.
authcache_form_form_alter Implements hook_form_alter().
authcache_form_menu Implements hook_menu().
_authcache_form_after_build Form after_build callback for forms on cacheable pages.
_authcache_form_allow_base_id_token Test whether CSRF token based on base form id is allowed.
_authcache_form_allow_notoken Test whether stripping of CSRF token is allowed for the given form.
_authcache_form_allow_p13n Test whether defered retrieval of form token / build-id is allowed.
_authcache_form_match_form_id Check if a form_id matches any pattern in a set of patterns.