5 * Authenticated User Page Caching (and anonymous users, too!)
7 * @see authcache.admin.inc for admin page functionality
10 // Default caching rules (Never cache these pages)
11 define('AUTHCACHE_NOCACHE_DEFAULT', '
24 // Default non-HTML cachable content-types
25 define('AUTHCACHE_MIMETYPE_DEFAULT', '
33 // Flags for authcache_fix_cookies:
34 define('AUTHCACHE_FLAGS_NONE', 0x0);
35 define('AUTHCACHE_FLAGS_ACCOUNT_ENABLED', 0x1);
36 define('AUTHCACHE_FLAGS_LOGIN_ACTION', 0x2);
37 define('AUTHCACHE_FLAGS_LOGOUT_ACTION', 0x4);
40 * Implements hook_menu().
42 function authcache_menu() {
44 $items['admin/config/system/authcache'] = array(
45 'title' => 'Authcache',
46 'description' => 'Configure authenticated user page caching.',
47 'page callback' => 'drupal_get_form',
48 'page arguments' => array('authcache_admin_config'),
49 'access arguments' => array('administer site configuration'),
50 'file' => 'authcache.admin.inc',
54 $items['admin/config/system/authcache/config'] = array(
55 'title' => 'Configuration',
56 'type' => MENU_DEFAULT_LOCAL_TASK
,
60 $items['admin/config/system/authcache/pagecaching'] = array(
61 'title' => 'Page caching settings',
62 'description' => "Configure page cache settings.",
63 'page callback' => 'drupal_get_form',
64 'page arguments' => array('authcache_admin_pagecaching'),
65 'access arguments' => array('administer site configuration'),
66 'file' => 'authcache.admin.inc',
67 'type' => MENU_LOCAL_TASK
,
70 $items['authcache/ajax'] = array(
71 'title' => 'Javascript ajax Callback',
72 'page callback' => 'authcache_ajax',
73 'access arguments' => array('administer site configuration'),
74 'file' => 'authcache.admin.inc',
75 'type' => MENU_CALLBACK
,
83 * Implements hook_module_implements_alter().
85 * Make sure that hook_init of this module is called before all other modules
86 * and vice versa for hook_exit.
88 function authcache_module_implements_alter(&$implementations, $hook) {
89 if ($hook == 'init') {
90 $me = $implementations['authcache'];
91 unset($implementations['authcache']);
92 $implementations = array_merge(array('authcache' => $me), $implementations);
95 if ($hook == 'exit') {
96 $me = $implementations['authcache'];
97 unset($implementations['authcache']);
98 $implementations['authcache'] = $me;
103 * Implements hook_init().
106 function authcache_init() {
109 $reasons = module_invoke_all('authcache_request_exclude');
110 if (!empty($reasons)) {
111 _authcache_exclude(reset($reasons));
114 $reasons = module_invoke_all('authcache_account_exclude', $user);
115 if (!empty($reasons)) {
116 _authcache_exclude(reset($reasons));
119 if (!authcache_excluded()) {
120 // Don't allow format_date() to use the user's local timezone
121 $conf['configurable_timezones'] = FALSE
;
123 // Start output buffering
127 // Attach required JavaScript
128 drupal_add_library('system', 'jquery.cookie');
129 drupal_add_js(drupal_get_path('module', 'authcache') .
'/authcache.js');
131 // Inject authcache cookie settings.
132 $lifetime = ini_get('session.cookie_lifetime');
133 $lifetime = (!empty($lifetime) && is_numeric($lifetime) ?
(int)$lifetime : 0);
134 drupal_add_js(array('authcache' => array(
137 'path' => ini_get('session.cookie_path'),
138 'domain' => ini_get('session.cookie_domain'),
139 'secure' => ini_get('session.cookie_secure') == '1',
141 'cl' => $lifetime/86400,
144 // Fix cookies if necessary.
145 $flags = (authcache_account_allows_caching()) ?
146 AUTHCACHE_FLAGS_ACCOUNT_ENABLED
: AUTHCACHE_FLAGS_NONE
;
147 authcache_fix_cookies($flags);
152 * Implements hook_user_login().
154 function authcache_user_login(&$edit, $account) {
155 $flags = AUTHCACHE_FLAGS_LOGIN_ACTION
;
157 if (authcache_account_allows_caching($account)) {
158 $flags |= AUTHCACHE_FLAGS_ACCOUNT_ENABLED
;
161 authcache_fix_cookies($flags, $account);
166 * Implements hook_user_logout().
168 function authcache_user_logout($account) {
169 // Note: include same cookie deletion in ajax/authcache.module
170 authcache_fix_cookies(AUTHCACHE_FLAGS_LOGOUT_ACTION
, $account);
175 * Implements hook_form_alter(),
177 function authcache_form_alter(&$form, &$form_state, $form_id) {
178 if (authcache_page_is_cacheable()) {
179 // Need to postpone the decision whether the form and the page is cacheable
180 // to an after-build callback.
181 $form['#after_build'][] = '_authcache_form_after_build';
186 // Alter Drupal's "Performance" admin form
187 case
'system_performance_settings':
188 $form['caching']['cache']['#description'] = ' <strong>' .
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.') .
'</strong>';
189 if (authcache_account_allows_caching(drupal_anonymous_user())) {
190 $form['caching']['cache']['#disabled'] = TRUE
; //array(0 => t('Disabled') . ' ' . t('by') . ' Authcache');
191 $form['caching']['cache']['#value'] = TRUE
;
195 case
'user_profile_form':
196 // Don't allow user local timezone
197 if (authcache_account_allows_caching()) {
198 unset($form['timezone']);
205 * Form after_build callback for all forms on cacheable pages
207 * Disable storing the form to form-cache if possible. However some forms (especially
208 * Ajax-enabled ones) require the form cache. In this case page-caching must be
211 * @see drupal_build_form().
213 function _authcache_form_after_build($form, $form_state) {
214 $form_id = $form['#form_id'];
216 if (isset($form['form_token']) && !authcache_get_request_property('ajax')) {
217 authcache_cancel(t('Form with CSRF protected on page but cannot use AJAX to defer token-retrieval.'));
220 if (empty($form_state['rebuild']) && empty($form_state['cache'])) {
221 // Disable form cache and remove build_id if caching is not explicitely requested
222 $form_state['no_cache'] = TRUE
;
223 unset($form['form_build_id']);
224 unset($form['#build_id']);
227 if (isset($form['form_token']) && authcache_get_request_property('ajax')) {
228 // Remove CSRF-token from built form and make sure it can be retrieved
230 unset($form['form_token']);
232 drupal_add_js(drupal_get_path('module', 'authcache') .
'/authcache.formtokenids.js');
233 drupal_add_js(array('aceformtokenids' => array(
234 $form_id => (isset($form['#token'])) ?
$form['#token'] : $form_id,
243 * Process page template variables.
245 function authcache_preprocess_page(&$variables) {
246 if (user_is_logged_in() && authcache_page_is_cacheable()) {
247 if (authcache_get_request_property('ajax')) {
248 drupal_add_js(drupal_get_path('module', 'authcache') .
'/authcache.tabs.js');
250 $variables['tabs']['#post_render'][] = 'authcache_wrap_tabs';
251 $variables['action_links']['#post_render'][] = 'authcache_wrap_local_actions';
256 * Post-render callback for page-tabs. Wrap them into an authcache span, so we
257 * can find it again in JavaScript.
259 function authcache_wrap_tabs($markup) {
260 if (!empty($markup)) {
261 if (authcache_get_request_property('ajax')) {
262 $markup = '<span id="authcache-tabs">' .
$markup .
'</span>';
265 authcache_cancel(t('Tabs on page but Authcache AJAX not enabled.'));
273 * Post-render callback for local actions. Wrap them into an authcache span, so
274 * we can find it again in JavaScript.
276 function authcache_wrap_local_actions($markup) {
277 if (!empty($markup)) {
278 if (authcache_get_request_property('ajax')) {
279 $markup = '<span id="authcache-local-actions">' .
$markup .
'</span>';
282 authcache_cancel(t('Local actions on page but Authcache AJAX not enabled.'));
290 * Implements hook_exit().
292 * Called on drupal_goto() redirect.
293 * Make sure status messages show up, if applicable.
295 function authcache_exit($destination = NULL
) {
296 // Cancel caching when hook_exit was called from drupal_goto.
297 if ($destination !== NULL
) {
298 authcache_cancel(t('Redirecting to @destination', array('@destination' => $destination)));
301 // Disable authcache on next page request if there are messages pending which
302 // did not manage it onto the current page.
303 if (drupal_set_message()) {
304 authcache_fix_cookies(AUTHCACHE_FLAGS_NONE
);
307 // If this page was excluded in hook_init, we're done here.
308 if (authcache_excluded()) {
312 // Forcibly disable drupal built-in page caching for anonymous users.
313 // Prevent drupal_page_set_cache() called from drupal_page_footer() to
314 // store the page a second time after we did.
315 drupal_page_is_cacheable(FALSE
);
318 if ($cache = authcache_page_set_cache()) {
319 drupal_serve_page_from_cache($cache);
327 // Preprocess functions
331 * Implements hook_preprocess().
333 * Inject authcache variables into every template.
335 function authcache_preprocess(&$variables, $hook) {
336 // Define variables for templates files
337 $variables['authcache_is_cacheable'] = authcache_page_is_cacheable();
341 * Implements hook_process_HOOK().
343 * Prevent caching pages with status messages on them. Note that due to the
344 * fact the messages are only added in template_process_page, we also need to
345 * use the process-hook.
347 function authcache_process_page(&$variables) {
348 if (!empty($variables['messages']) && authcache_page_is_cacheable()) {
349 authcache_cancel(t('Status message on page'));
354 // API for other modules.
358 * Private function called from authcache_init. Authcache should not alter any
359 * aspect of this page.
361 function _authcache_exclude($reason = NULL
) {
362 // No need for drupal_static here, flag may not be reset anyway.
363 static
$excluded = FALSE
;
365 if (!$excluded && !empty($reason)) {
367 module_invoke_all('authcache_excluded', $reason);
374 * Return true if this page is excluded from page caching.
376 function authcache_excluded() {
377 return _authcache_exclude();
381 * Prevent this page of beeing stored in the cache after it is built up.
383 function authcache_cancel($reason = NULL
) {
384 // No need for drupal_static here, flag may not be reset anyway.
385 static
$cancelled = FALSE
;
387 if (!$cancelled && !empty($reason)) {
389 module_invoke_all('authcache_cancelled', $reason);
396 * Return true if the caching of the page request was cancelled during
399 function authcache_cancelled() {
400 return authcache_cancel();
404 * Return true if this page possibly will be cached later.
406 function authcache_page_is_cacheable() {
407 return !(authcache_excluded() || authcache_cancelled());
411 * Return true if the given account is cacheable.
413 function authcache_account_allows_caching($account = NULL
) {
415 $cacheable = &drupal_static(__FUNCTION__
);
417 if (!isset($account)) {
421 if (!isset($cacheable[$account->uid
])) {
422 $reasons = module_invoke_all('authcache_account_exclude', $account);
423 $cacheable[$account->uid
] = empty($reasons);
426 return $cacheable[$account->uid
];
430 * Return characterizing key-value pairs of a browsers capabilities and the
433 function authcache_request_properties() {
436 if (!isset($properties)) {
437 $properties = module_invoke_all('authcache_request_properties');
438 drupal_alter('authcache_request_properties', $properties);
445 * Return characterizing properties of groups the given account is a member of.
447 function authcache_account_properties($account = NULL
) {
451 if (!isset($account)) {
455 if (!isset($properties)) {
456 $properties = module_invoke_all('authcache_account_properties', $account);
457 drupal_alter('authcache_account_properties', $properties, $account);
464 * Return the property value of the given request property or null.
466 * @see hook_authcache_request_properties().
468 function authcache_get_request_property($name) {
469 $properties = authcache_request_properties();
470 return isset($properties[$name]) ?
$properties[$name] : NULL
;
474 * Return the property value of the given account property or null.
476 * @see hook_authcache_account_properties().
478 function authcache_get_account_property($name, $account = NULL
) {
479 $properties = authcache_account_properties($account);
480 return isset($properties[$name]) ?
$properties[$name] : NULL
;
484 * Return the properties used as a base for calculation of the authcache key.
486 * @see authcache_key().
487 * @see hook_authcache_key_properties_alter().
489 function authcache_key_properties($account = NULL
) {
492 if (!isset($account)) {
497 'request' => authcache_request_properties(),
498 'account' => authcache_account_properties($account),
500 drupal_alter('authcache_key_properties', $properties, $account);
506 * Generate and return the authcache key for the given account.
508 * @see hook_authcache_key_properties().
509 * @see hook_authcache_key_properties_alter().
511 function authcache_key($account = NULL
) {
512 global $base_root, $user;
514 if (!isset($account)) {
519 // Calculate the key for logged in users from key-properties.
520 $data = serialize(authcache_key_properties($account));
521 $hmac = hash_hmac('sha1', $data, drupal_get_private_key(), FALSE
);
523 $abbrev = variable_get('authcache_hmac_abbrev', 7);
524 $key = $abbrev ?
substr($hmac, 0, $abbrev) : $hmac;
527 // Generate base-key for anonymous users.
528 $generator = variable_get('authcache_key_generator');
529 if (is_callable($generator)) {
530 $key = call_user_func($generator);
541 * Return the authcache cache-id for the given path.
543 * @see authcache_key().
545 function authcache_cid($request_uri = NULL
, $account = NULL
) {
546 if (!isset($request_uri)) {
547 $request_uri = request_uri();
550 $key = authcache_key($account);
551 return $key .
$request_uri;
555 * Add and remove cookies to the browser session as required.
557 * @see hook_authcache_cookie().
558 * @see hook_authcache_cookie_alter().
560 function authcache_fix_cookies($flags, $account = NULL
) {
563 if (!isset($account)) {
567 $cookies = module_invoke_all('authcache_cookie', $flags, $account);
568 drupal_alter('authcache_cookie', $cookies, $flags, $account);
570 $default_params = array(
573 'lifetime' => ini_get('session.cookie_lifetime'),
574 'path' => ini_get('session.cookie_path'),
575 'domain' => ini_get('session.cookie_domain'),
576 'secure' => ini_get('session.cookie_secure') == '1',
580 foreach ($cookies as
$name => $params) {
581 $params += $default_params;
583 if ($params['present']) {
584 // Fix cookie if it is not present in the users browser or the value does
585 // not match our expectations.
586 if (!isset($_COOKIE[$name]) || $_COOKIE[$name] != $params['value']) {
587 $expires = $params['lifetime'] ? REQUEST_TIME
+ $params['lifetime'] : 0;
588 setcookie($name, $params['value'], $expires, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
591 elseif (!$params['present'] && isset($_COOKIE[$name])) {
592 // Remove spare cookie
593 setcookie($name, "", REQUEST_TIME
- 86400, $params['path'], $params['domain'], $params['secure'], $params['httponly']);
599 * Returns an array containing all the roles from account_roles that are not
600 * present in allowed_roles.
602 function authcache_diff_roles($account_roles, $allowed_roles) {
603 // Remove "authenticated user"-role from the account roles except when it is
604 // the only role on the account.
605 if (array_keys($account_roles) != array(DRUPAL_AUTHENTICATED_RID
)) {
606 unset($account_roles[DRUPAL_AUTHENTICATED_RID
]);
609 return array_diff_key($account_roles, $allowed_roles);
613 * Determines the MIME content type of the current page response based on
614 * the currently set Content-Type HTTP header.
616 * This should normally return the string 'text/html' unless another module
617 * has overridden the content type.
619 function _authcache_get_content_type($default = NULL
) {
620 $params = explode(';', drupal_get_http_header('content-type'));
621 $params = array_map('trim', $params);
622 $mime = array_shift($params);
631 * Determines the HTTP response code that the current page request will be
632 * returning by examining the HTTP headers that have been output so far.
634 function _authcache_get_http_status($status = 200) {
635 $value = drupal_get_http_header('status');
636 return isset($value) ?
(int) $value : $status;
640 * Stores the current page in the cache.
642 * @see hook_authcache_presave().
643 * @see hook_authcache_cache_alter().
644 * @see drupal_page_set_cache().
646 function authcache_page_set_cache() {
647 // Give other modules a last chance to cancel page saving
648 module_invoke_all('authcache_presave');
650 if (authcache_page_is_cacheable()) {
651 $cache = (object) array(
652 'cid' => authcache_cid(),
654 'path' => $_GET['q'],
655 'body' => ob_get_clean(),
656 'title' => drupal_get_title(),
657 'headers' => array(),
659 'expire' => CACHE_TEMPORARY
,
660 'created' => REQUEST_TIME
,
663 // Restore preferred header names based on the lower-case names returned
664 // by drupal_get_http_header().
665 $header_names = _drupal_set_preferred_header_name();
666 foreach (drupal_get_http_header() as
$name_lower => $value) {
667 $cache->data
['headers'][$header_names[$name_lower]] = $value;
668 if ($name_lower == 'expires') {
669 // Use the actual timestamp from an Expires header if available.
670 $cache->expire
= strtotime($value);
674 if ($cache->data
['body']) {
675 if (variable_get('page_compression', TRUE
) && extension_loaded('zlib')) {
676 $cache->data
['body'] = gzencode($cache->data
['body'], 9, FORCE_GZIP
);
679 // Let other modules act on the cacheable data.
680 drupal_alter('authcache_cache', $cache);
682 cache_set($cache->cid
, $cache->data
, 'cache_page', $cache->expire
);
693 * Implements hook_authcache_request_properties().
695 function authcache_authcache_request_properties() {
699 'js' => !empty($_COOKIE['has_js']),
700 'base_url' => $base_url,
705 * Implements hook_authcache_account_properties().
707 function authcache_authcache_account_properties($account) {
708 $roles = array_keys($account->roles
);
717 * Implements hook_authcache_request_exclude().
719 function authcache_authcache_request_exclude() {
722 // The following three basic exclusion rules are mirrored in
723 // authcacheinc_retrieve_cache_page() in authcache.inc
724 // BEGIN: basic exclusion rules
725 if (drupal_is_cli()) {
726 return t('Running as CLI script');
729 if (!($_SERVER['REQUEST_METHOD'] == 'GET' || $_SERVER['REQUEST_METHOD'] == 'HEAD')) {
730 return t('Only GET and HEAD requests allowed. Method for this request is: @method.',
731 array('@method' => $_SERVER['REQUEST_METHOD']));
734 if (($ar = explode('?', basename(request_uri()))) && substr(array_shift($ar), -4) == '.php') {
735 return t('PHP files (cron.php, update.php, etc)');
737 // END: basic exclusion rules
739 module_load_install('authcache');
740 $requirements = module_invoke('authcache', 'requirements', 'runtime');
741 if (isset($requirements['authcache']['severity']) && $requirements['authcache']['severity'] == REQUIREMENT_ERROR
) {
742 return $requirements['authcache']['description'];
745 if (variable_get('authcache_noajax', FALSE
)
746 && isset($_SERVER['HTTP_X_REQUESTED_WITH'])
747 && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) == 'xmlhttprequest'
749 return t('Ajax request');
752 $alias = drupal_get_path_alias($_GET['q']);
754 // Now check page caching settings, defined by the site admin
755 $pagecaching = variable_get('authcache_pagecaching', array(array(
757 'pages' => AUTHCACHE_NOCACHE_DEFAULT
,
758 'roles' => array(DRUPAL_ANONYMOUS_RID
),
761 foreach ($pagecaching as
$i => $page_rules) {
762 // Do caching page roles apply to current user?
763 $extra_roles = authcache_diff_roles($user->roles
, $page_rules['roles']);
764 if (empty($extra_roles)) {
765 switch ($page_rules['option']) {
766 case
'0': // Cache every page except the listed pages.
767 case
'1': // Cache only the listed pages.
768 $page_listed = drupal_match_path($alias, $page_rules['pages']);
769 if (!(!($page_rules['option'] xor
$page_listed))) {
770 return t('Caching disabled by path list of page ruleset #@number', array('@number' => $i));
774 case
'2': // Cache pages for which the following PHP code returns TRUE
776 if (module_exists('php')) {
777 $result = php_eval($page_rules['pages']);
779 if (empty($result)) {
780 return t('Caching disabled by PHP rule of page ruleset #@number', array('@number' => $i));
788 if (!empty($page_rules['noadmin']) && path_is_admin(current_path())) {
789 return t('Not caching admin pages (by page ruleset #@number)', array('@number' => $i));
797 * Implements hook_authcache_account_exclude().
799 function authcache_authcache_account_exclude($account) {
800 // Bail out from requests by superuser (uid=1)
801 if ($account->uid
== 1 && !variable_get('authcache_su', 0)) {
802 return t('Caching disabled for superuser');
805 // Check for non-cacheable roles of the account.
806 $cache_roles = variable_get('authcache_roles', array());
807 $extra_roles = authcache_diff_roles($account->roles
, $cache_roles);
808 if (!empty($extra_roles)) {
809 return format_plural(count($extra_roles),
810 'Account has non-cachable role @roles',
811 'Account has non-cachable roles @roles',
812 array('@roles' => implode(', ', $extra_roles)));
815 // If JavaScript is disabled on the users browser, check if chaching is still
817 if (!authcache_get_request_property('js')) {
818 $nojs_roles = variable_get('authcache_nojsroles', drupal_map_assoc(array(DRUPAL_ANONYMOUS_RID
)));
819 $extra_roles = authcache_diff_roles($account->roles
, $nojs_roles);
820 if (!empty($extra_roles)) {
821 return format_plural(count($extra_roles),
822 'Role @roles is not cacheable if JavaScript is disabled.',
823 'Roles @roles are not cacheable if JavaScript is disabled.',
824 array('@roles' => implode(', ', $extra_roles)));
831 * Implements hook_authcache_presave().
833 function authcache_authcache_presave() {
834 // Check content-type
835 $content_type = _authcache_get_content_type();
836 $allowed_mimetypes = preg_split('/(\r\n?|\n)/', variable_get('authcache_mimetype', AUTHCACHE_MIMETYPE_DEFAULT
), -1, PREG_SPLIT_NO_EMPTY
);
837 if (!in_array($content_type['mimetype'], $allowed_mimetypes)) {
838 authcache_cancel(t('Only cache allowed HTTP content types (HTML, JS, etc)'));
842 if (variable_get('authcache_http200', FALSE
) && _authcache_get_http_status() != 200) {
843 authcache_cancel(t('Don`t cache 404/403s/etc'));
846 // Check headers already were sent
847 if (headers_sent()) {
848 authcache_cancel(t('Don`t cache private file transfers or if headers were unexpectly sent.'));
851 // Make sure "Location" redirect isn't used
852 foreach (headers_list() as
$header) {
853 if (strpos($header, 'Location:') === 0) {
854 authcache_cancel(t('Location header detected'));
858 // Don't cache pages with PHP errors (Drupal can't catch fatal errors)
859 if (function_exists('error_get_last') && $error = error_get_last()) {
860 switch ($error['type']) {
861 // Ignore these errors:
862 case E_NOTICE
: // run-time notices
863 case E_USER_NOTICE
: // user-generated notice message
864 case E_DEPRECATED
: // run-time notices
865 case E_USER_DEPRECATED
: // user-generated notice message
868 // Let user know there is PHP error and return
869 authcache_cancel(t('PHP Error: @error', array('@error' => error_get_last())));
877 * Implements hook_authcache_cookie().
879 function authcache_authcache_cookie($flags, $account) {
880 $authenticated = $account->uid
;
881 $enabled = $flags & AUTHCACHE_FLAGS_ACCOUNT_ENABLED
;
882 $present = $authenticated && $enabled;
884 $cookies['authcache']['present'] = $present;
885 $cookies['authcache']['httponly'] = TRUE
;
886 $cookies['drupal_user']['present'] = $present;
887 $cookies['drupal_uid']['present'] = $present;
890 $cookies['authcache']['value'] = authcache_key($account);
891 $cookies['drupal_user']['value'] = $account->name
;
892 $cookies['drupal_uid']['value'] = $account->uid
;
899 * Implements hook_aceajax_request().
901 function authcache_aceajax_request() {
902 $request['tab'] = array(
910 * Implements hook_aceajax_command().
912 function authcache_aceajax_command() {
914 'form_token_id' => array(
915 'class' => 'AuthcacheFormTokenIdCommand',
917 'menu_local_tasks' => array(
918 'class' => 'AuthcacheMenuLocalTasksCommand',
919 'bootstrap' => DRUPAL_BOOTSTRAP_FULL
,