authcache_form.module in Authenticated User Page Caching (Authcache) 7.2
Form token retrieval for Authcache.
File
modules/authcache_form/authcache_form.moduleView 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
Name | 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. |