Index: esi.module =================================================================== RCS file: /cvs/drupal-contrib/contributions/modules/esi/esi.module,v retrieving revision 1.2 diff -u -p -r1.2 esi.module --- esi.module 17 Nov 2010 11:37:25 -0000 1.2 +++ esi.module 29 Nov 2010 16:17:43 -0000 @@ -6,6 +6,9 @@ * delivered by ESI, with support for per-block cache times. */ +// Tested against 1.7.2 of the ctools API. +define('ESI_REQUIRED_CTOOLS_API', '1.7.2'); + // Default interval for rotating the seed key: defaults to change-daily. define('ESI__DEFAULT_SEED_KEY_ROTATION_INTERVAL', 86400); @@ -57,6 +60,13 @@ function esi_menu() { 'page arguments' => array(2), 'access callback' => TRUE, 'type' => MENU_CALLBACK + ), + 'esi/panels_pane/%/%' => array( + 'title' => 'ESI handler', + 'page callback' => 'esi__panel_pane_handler', + 'page arguments' => array(2, 3), + 'access callback' => TRUE, + 'type' => MENU_CALLBACK ) ); } @@ -251,3 +261,152 @@ function esi__block_handler($bid, $page echo $output; return NULL; } + +/** + * Implementation of hook_ctools_plugin_directory(). + */ +function esi_ctools_plugin_directory($module, $plugin) { + // Safety: go away if CTools is not at an appropriate version. + if (!module_invoke('ctools', 'api_version', ESI_REQUIRED_CTOOLS_API)) { + return; + } + if ($module == 'page_manager' || $module == 'panels' || $module == 'ctools') { + return 'plugins/' . $plugin; + } +} + +/** + * Implementation of hook_pane_content_alter(). + * + * If the pane isn't being served up by the ESI menu handler, and is set to use + * ESI-caching, replace with ESI tag. + * This needs to be handled in hook_pane_content_alter() - i.e. after the + * content has been processed - because the cache object needs the meta-data + * provided by ctype-render handler, such as module, delta, etc. + * Subsequent requests are pulled from the system-cache. + */ +function esi_panels_pane_content_alter($content, $pane, $args, $context) { + // Bail out if it's not handled by ESI. + if(!is_array($pane->cache) || $pane->cache['method'] != 'esi') { + return; + } + + $display = panels_get_current_page_display(); + // The cache-key variable is set by the panel-context render function. + list($context, $task_name, $handler_name) = explode(':', $display->cache_key); + + // Build the esi tag. + $url = "esi/panels_pane/{$pane->did}/{$pane->pid}"; + if(array_key_exists('context', $pane->configuration)) { + // Add the page-manager task name. + $url .= '/' . $task_name; + + // Add the name of the context which is supplied to this pane. + $url .= '/' . $pane->configuration['context']; + + // Add the URL the panel was requested from. + $url .= '/' . base64_encode($_GET['q']); + } + $url = url($url); + $esi_tag = '' . "\n"; + + $content->content = $esi_tag; +} + + +/** + * Menu handler to serve individual panel-panes via ESI. + * + * If the pane uses context, the task_name, context_string and q variables will + * be set. + */ +function esi__panel_pane_handler($display_id, $pane_id, $task_name = NULL, $context_string = NULL, $q = NULL) { + $q = base64_decode($q); + $_GET['q'] = $q; + + $display = panels_load_display($display_id); + + if(!is_null($task_name)) { + // Get the context for this pane. + list($args, $contexts) = esi__panels_get_task_context($task_name); + + $display->context = $contexts; + $display->args = $args; + } + + // Switch ESI off so the contents of the pane are served. + unset($display->content[$pane_id]->cache); + + // Use the standard renderer to render the pane. + $renderer = panels_get_renderer_handler('standard', $display); + $renderer->prepare_panes($renderer->display->content); + $content = $renderer->render_pane($renderer->prepared['panes'][$pane_id]); + + echo $content; + + return NULL; +} + +/** + * Each of the panel task plugins provides a default context based on the menu + * path. + * This function looks up the menu handler for a URL, and provides the contexts + * for the menu-handler. + */ +function esi__panels_get_task_context($task_name) { + $task = page_manager_get_task($task_name); + + // Invoke the module's hook_esi_get_context_arguments to get the context + // provided by that task. + $context_arguments = module_invoke($task['module'], 'esi_get_context_arguments', $task['name']); + + // Parse the arguments into context objects. + ctools_include('context'); + ctools_include('context-task-handler'); + $contexts = ctools_context_handler_get_task_contexts($task, '', $context_arguments); + + return array($context_arguments, $contexts); +} + +/** + * Implementation of hook_esi_get_context, provided for page_manager. + */ +function page_manager_esi_get_context_arguments($task_name) { + switch($task_menu_callback) { + // The blog, poll, and contact_site tasks don't provide default context. + case 'blog': + case 'poll': + case 'contact_site': + return array(); + + // The blog_user, and contact_user tasks provide a user-object. + case 'blog_user': + case 'contact_user': + $uid = arg(1); + $account = user_load($uid); + return array($account); + + // The comment_reply task provide a node object and a comment CID. + case 'comment_reply': + // Path is comment/reply/%node + $nid = arg(2); + $pid = arg(3); + $node = node_load($nid); + return array($node, $pid); + + // The node_edit and node_view tasks provide a node object. + case 'node_edit': + case 'node_view': + $nid = arg(1); + $node = node_load($nid); + return array($node); + + case 'search': + // @TODO. + // return array($keys); + + case 'term_view': + // @TODO. + // return array($terms, $depth); + } +} Index: plugins/cache/esi.inc =================================================================== RCS file: plugins/cache/esi.inc diff -N plugins/cache/esi.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ plugins/cache/esi.inc 29 Nov 2010 16:17:43 -0000 @@ -0,0 +1,98 @@ + t("ESI"), + 'description' => t('ESI caching is a proxy-based cache. Panes are replaced by tags and requested separately by the proxy.'), + 'cache get' => 'esi_esi_cache_get_cache', + 'cache set' => 'esi_esi_cache_set_cache', +// 'cache clear' => 'esi_esi_cache_clear_cache', + 'settings form' => 'esi_esi_cache_settings_form', + 'settings form submit' => 'esi_esi_cache_settings_form_submit', + 'defaults' => array( + // TODO: review settings. + 'use_esi' => FALSE, // The use_esi flag is set to TRUE in pre-render, so it's only used when viewing a complete panel. + 'lifetime' => 3600, // Lifetime measured in seconds. 3600 = 1 hour. Sent by ESI callback as cache-control header. + ), +); + +/** + * Get cached content. + */ +function esi_esi_cache_get_cache($conf, $display, $args, $contexts, $pane = NULL) { + // If the pane is being rendered directly through panels, 'use_esi' will be + // set to TRUE (by hook_panels_pre_render). If it's being rendered via + // hook_menu as an ESI callback, 'use_esi' will be FALSE and we should return + // FALSE here so that it's rendered normally. + + $cid = esi_esi_cache_get_id($conf, $display, $args, $contexts, $pane); + $cache = cache_get($cid, 'cache'); + if (!$cache) { + return FALSE; + } + + if ((time() - $cache->created) > $conf['lifetime']) { + return FALSE; + } + + return $cache->data; +} + +/** + * Set cached content. + */ +function esi_esi_cache_set_cache($conf, $content, $display, $args, $contexts, $pane = NULL) { + $cid = esi_esi_cache_get_id($conf, $display, $args, $contexts, $pane); + cache_set($cid, $content); +} + +/** + * Clear cached content. + * + * Cache clears are always for an entire display, regardless of arguments. + */ +function esi_esi_cache_clear_cache($display) { + // TODO: wipe the varnish cache. +} + +/** + * Figure out an id for our cache based upon input and settings. + */ +function esi_esi_cache_get_id($conf, $display, $args, $contexts, $pane) { + // Build the cache-key using display ID, pane ID and pane context (if set). + $key = array(); + $key[] = 'esi_esi_cache'; + $key[] = $display->id; + $key[] = $pane->pid; + if(array_key_exists('context', $pane->configuration)) { + $key[] = $pane->configuration['context']; + } + + return implode(':', $key); +} + +function esi_esi_cache_settings_form($conf, $display, $pid) { + // The ESI callback URL provides the display ID, pane ID, and the string + // identifier for the context passed to the pane. + // E.g. /esi/panel_panes/21/2 + // E.g. /esi/panel_panes/21/3/argument_current_page_content_1 + + + + // The contexts-definition for this pane is in $display->configuration['context'] + $options = drupal_map_assoc(array(15, 30, 60, 120, 180, 240, 300, 600, 900, 1200, 1800, 3600, 7200, 14400, 28800, 43200, 86400, 172800, 259200, 345600, 604800), 'format_interval'); + $form['lifetime'] = array( + '#title' => t('Lifetime'), + '#type' => 'select', + '#options' => $options, + '#default_value' => $conf['lifetime'], + ); + + return $form; +}