'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, ), ); }