You are here

expire.module in Cache Expiration 6

Same filename and directory in other branches
  1. 7.2 expire.module
  2. 7 expire.module

Provides logic for page cache expiration

File

expire.module
View source
<?php

/**
 * @file
 * Provides logic for page cache expiration
 */

// Defaults used if variable_get is not set.
define('EXPIRE_FLUSH_NODE_TERMS', TRUE);
define('EXPIRE_FLUSH_MENU_ITEMS', 1);
define('EXPIRE_FLUSH_CCK_REFERENCES', TRUE);
define('EXPIRE_FLUSH_FRONT', TRUE);
define('EXPIRE_INCLUDE_BASE_URL', TRUE);

/**
 * Implementation of hook_menu().
 */
function expire_menu() {
  $items = array();
  $items['admin/settings/performance/default'] = array(
    'title' => 'Performance',
    'type' => MENU_DEFAULT_LOCAL_TASK,
    'file path' => drupal_get_path('module', 'system'),
  );
  $items['admin/settings/performance/expire'] = array(
    'type' => MENU_LOCAL_TASK,
    'title' => 'Cache Expiration',
    'page callback' => 'drupal_get_form',
    'page arguments' => array(
      'expire_admin_settings_form',
    ),
    'access arguments' => array(
      'administer site configuration',
    ),
    'file path' => drupal_get_path('module', 'expire'),
    'file' => 'expire.admin.inc',
    'weight' => 1,
  );
  return $items;
}

/**
 * Implementation of hook_comment().
 *
 * Acts on comment modification.
 */
function expire_comment($comment, $op) {

  // Convert array to object
  if (is_array($comment)) {
    $comment = (object) $comment;
  }

  // Return if no node id is attached to the comment.
  if (empty($comment->nid)) {
    return;
  }

  // Select which comment opperations require a cache flush.
  $cases = array(
    'insert',
    'update',
    'publish',
    'unpublish',
    'delete',
  );

  // Expire the relevant node page from the static page cache to prevent serving stale content.
  if (in_array($op, $cases)) {
    $node = node_load($comment->nid);
    if ($node) {
      expire_node($node);
    }
  }
}

/**
 * Implementation of hook_nodeapi().
 *
 * Acts on nodes defined by other modules.
 */
function expire_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) {

  // Return if no node id is attached to the node object.
  if (empty($node->nid)) {
    return;
  }

  // Select which node opperations require a cache flush.
  $cases = array(
    'insert',
    'update',
    'delete',
    'delete revision',
  );

  // Expire the relevant node page from the static page cache to prevent serving stale content.
  if (in_array($op, $cases)) {
    expire_node($node);
  }
}

/**
 * Implementation of hook_user().
 *
 * Acts on user account actions: expire the relevant user page from the static
 * page cache to prevent serving stale content.
 */
function expire_user($op, &$edit, &$account, $category = NULL) {

  // Return if no user id is attached to the user object.
  if (empty($account->uid)) {
    return;
  }

  // Select which user opperations require a cache flush.
  $cases = array(
    'insert',
    'update',
    'after update',
  );

  // Expire the relevant user page from the static page cache to prevent serving stale content.
  if (in_array($op, $cases)) {
    $paths[] = 'user/' . $account->uid;
    $flushed = expire_cache_derivative($paths, $account);
    watchdog('expire', 'User !op for !uid resulted in !flushed pages being expired from the cache', array(
      '!op' => $op,
      '!uid' => $account->uid,
      '!flushed' => $flushed,
    ));
  }
}

/**
 * Implementation of hook_votingapi_insert().
 *
 * @param $votes
 *  array of votes
 */
function expire_votingapi_insert($votes) {
  _expire_votingapi($votes);
}

/**
 * Implementation of hook_votingapi_delete().
 *
 * @param $votes
 *  array of votes
 */
function expire_votingapi_delete($votes) {
  _expire_votingapi($votes);
}

/**
 * Common expiry logic for votingapi.
 */
function _expire_votingapi($votes) {
  foreach ($votes as $vote) {
    if ($vote['content_type'] == 'comment') {
      $nid = db_result(db_query("SELECT nid FROM {comments} WHERE cid = %d", $vote['content_id']));
      if (is_numeric($nid)) {
        $node = node_load($nid);
        if ($node) {
          expire_node($node);
        }
      }
    }
    if ($vote['content_type'] == 'node') {
      $node = node_load($vote['content_id']);
      if ($node) {
        expire_node($node);
      }
    }
  }
}

/**
 * Expires a node from the cache; including related pages.
 *
 * Expires front page if promoted, taxonomy terms,
 *
 * @param $node
 *  node object
 */
function expire_node(&$node) {
  $paths = array();

  // Check node object
  if (empty($node->nid)) {
    return FALSE;
  }

  // Expire this node
  $paths['node'] = 'node/' . $node->nid;

  // If promoted to front page, expire front page
  if (variable_get('expire_flush_front', EXPIRE_FLUSH_FRONT) && $node->promote == 1) {
    $paths['front'] = '<front>';
  }

  // Get taxonomy terms and flush
  if (module_exists('taxonomy') && variable_get('expire_flush_node_terms', EXPIRE_FLUSH_NODE_TERMS)) {

    // Get old terms from DB
    $tids = expire_taxonomy_node_get_tids($node->nid);

    // Get old terms from static variable
    $terms = taxonomy_node_get_terms($node);
    if (!empty($terms)) {
      foreach ($terms as $term) {
        $tids[$term->tid] = $term->tid;
      }
    }

    // Get new terms from node object
    if (!empty($node->taxonomy)) {
      foreach ($node->taxonomy as $vocab) {
        if (is_array($vocab)) {
          foreach ($vocab as $term) {
            $tids[$term] = $term;
          }
        }
      }
    }
    $filenames = array();
    foreach ($tids as $tid) {
      if (is_numeric($tid)) {
        $term = taxonomy_get_term($tid);
        $paths['term' . $tid] = taxonomy_term_path($term);
      }
    }
  }

  // Get menu and flush related items in the menu.
  if (variable_get('expire_flush_menu_items', EXPIRE_FLUSH_MENU_ITEMS) != 0) {
    if (!isset($node->menu['menu_name'])) {
      menu_nodeapi($node, 'prepare');
    }
    $menu = menu_tree_all_data($node->menu['menu_name']);
    $tempa = NULL;
    $tempb = NULL;
    if (variable_get('expire_flush_menu_items', EXPIRE_FLUSH_MENU_ITEMS) == 1) {
      $links = expire_get_menu_structure($menu, FALSE, 'node/' . $node->nid, NULL, $tempa, $tempb);
    }
    elseif (variable_get('expire_flush_menu_items', EXPIRE_FLUSH_MENU_ITEMS) == 2) {
      $links = expire_get_menu_structure($menu, NULL, NULL, NULL, $tempa, $tempb);
    }
    unset($tempa);
    unset($tempb);
    $paths = array_merge($links, $paths);
  }

  // Get CCK References and flush.
  if (variable_get('expire_flush_cck_references', EXPIRE_FLUSH_CCK_REFERENCES) && module_exists('nodereference')) {
    $nids = array();
    $type = content_types($node->type);
    if ($type) {
      foreach ($type['fields'] as $field) {

        // Add referenced nodes to nids. This will clean up nodereferrer fields
        // when the referencing node is updated.
        if ($field['type'] == 'nodereference') {
          $node_field = isset($node->{$field}['field_name']) ? $node->{$field}['field_name'] : array();
          foreach ($node_field as $delta => $item) {
            if (is_numeric($item['nid'])) {
              $paths['reference' . $item['nid']] = 'node/' . $item['nid'];
            }
          }

          // Look for node referers without using nodereferrer
          $info = content_database_info($field);
          $table = $info['table'];
          $column = $info['columns']['nid']['column'];
          $results = db_query("SELECT n.nid\n            FROM {%s} nr\n            INNER JOIN {node} n USING (vid)\n            WHERE nr.%s = %d", $table, $column, $node->nid);
          while ($nid = db_result($results)) {
            if (is_numeric($nid)) {
              $paths['referenceparent' . $nid] = 'node/' . $nid;
            }
          }
        }
      }
    }

    // Get CCK references pointing to this node and flush.
    if (module_exists('nodereferrer')) {
      $nids = nodereferrer_referrers($node->nid);
      foreach ($nids as $nid) {
        if (is_numeric($nid['nid'])) {
          $paths['referrer' . $nid['nid']] = 'node/' . $nid['nid'];
        }
      }
    }
  }

  // Flush array of paths
  if (!empty($paths)) {
    $flushed = expire_cache_derivative($paths, $node);
    watchdog('expire', 'Node !nid was flushed resulting in !flushed pages being expired from the cache', array(
      '!nid' => $node->nid,
      '!flushed' => $flushed,
    ));
  }
}

/**
 * Finds parent, siblings and children of the menu item. UGLY CODE...
 *
 * @param array $menu
 *  Output from menu_tree_all_data()
 * @param bool $found
 *  Signal for when the needle was found in the menu array.
 *  Set TRUE to get entire menu
 * @param string $needle
 *  Name of menu link. Example 'node/21'
 * @param bool $first
 *  Keep track of the first call; this is a recursive function.
 * @param bool &$found_global
 *  Used to signal the parent item was found in one of it's children
 * @param bool &$menu_out
 *  Output array of parent, siblings and children menu links
 */
function expire_get_menu_structure($menu, $found, $needle, $first, &$found_global, &$menu_out) {

  // Set Defaults
  $found = !is_null($found) ? $found : TRUE;
  $needle = !is_null($needle) ? $needle : '';
  $first = !is_null($first) ? $first : TRUE;
  $found_global = FALSE;
  $menu_out = !is_null($menu_out) ? $menu_out : array();

  // Get Siblings
  foreach ($menu as $item) {
    if ($item['link']['hidden'] == 0 && $item['link']['page_callback'] != '' && ($item['link']['link_path'] == $needle || $found)) {
      $menu_out[] = $item['link']['link_path'];
      $found = TRUE;
    }
  }

  // Get Children
  foreach ($menu as $item) {
    if ($item['link']['hidden'] != 0) {
      continue;
    }
    if ($item['link']['page_callback'] != '' && ($item['link']['link_path'] == $needle || $found)) {
      $menu_out[] = $item['link']['link_path'];
      $found = TRUE;
    }

    // Get Grandkids
    if (!empty($item['below'])) {
      $sub_menu = array();
      foreach ($item['below'] as $below) {
        if ($below['link']['hidden'] == 0) {
          $sub_menu[] = $below;
        }
      }
      expire_get_menu_structure($sub_menu, $needle, $found, FALSE, $found_global, $menu_out);
      $structure[$item['link']['link_path']][] = $sub;
      if ($item['link']['page_callback'] != '' && $found_global) {

        // Get Parent of kid
        $menu_out[] = $item['link']['link_path'];
      }
    }
    else {
      $structure[$item['link']['link_path']] = '';
    }
  }

  // Clean up
  if (isset($structure) && is_array($structure)) {
    $structure = array_unique($structure);
  }
  $found_global = $found;
  if ($first) {
    if (isset($menu_out) && is_array($menu_out)) {
      $menu_out = array_unique($menu_out);
      sort($menu_out);
      return $menu_out;
    }
    else {
      return array();
    }
  }
  else {
    return $structure;
  }
}

/**
 * Return taxonomy terms given a nid.
 *
 * Needed because of a weird bug with CCK & node_load()
 *  http://drupal.org/node/545922
 */
function expire_taxonomy_node_get_tids($nid) {
  $vid = db_result(db_query("SELECT vid FROM {node} WHERE nid = %d", $nid));
  $result = db_query(db_rewrite_sql("SELECT t.tid FROM {term_node} r INNER JOIN {term_data} t ON r.tid = t.tid INNER JOIN {vocabulary} v ON t.vid = v.vid WHERE r.vid = %d ORDER BY v.weight, t.weight, t.name", 't', 'tid'), $vid);
  $tids = array();
  while ($term = db_result($result)) {
    $tids[] = $term;
  }
  return $tids;
}

/**
 * Finds all possible paths/redirects/aliases given the root path.
 *
 * @param $paths
 *   Array of current URLs
 * @param $node
 *   node object
 */
function expire_cache_derivative($paths, &$node = NULL) {
  global $base_path;
  $expire = array();
  if (empty($paths)) {
    return FALSE;
  }
  $site_frontpage = variable_get('site_frontpage', 'node');
  foreach ($paths as $path) {

    // Special front page handling
    if ($path == $site_frontpage || $path == '' || $path == '<front>') {
      $expire[] = '';
      $expire[] = 'rss.xml';
      $expire[] = $site_frontpage;
    }

    // Add given path
    if ($path != '<front>') {
      $expire[] = $path;
    }

    // Path alias
    $path_alias = url($path, array(
      'absolute' => FALSE,
    ));

    // Remove the base path
    $expire[] = substr($path_alias, strlen($base_path));

    // Path redirects
    if (module_exists('path_redirect')) {
      $path_redirects = expire_path_redirect_load(array(
        'redirect' => $path,
      ));
      if (isset($path_redirects)) {
        foreach ($path_redirects as $path_redirect) {
          if (!empty($path_redirect['redirect'])) {
            $expire[] = $path_redirect['redirect'];
          }
        }
      }
    }
  }

  // Allow other modules to modify the list prior to expiring
  drupal_alter('expire_cache', $expire, $node, $paths);

  // Expire cached files
  if (empty($expire)) {
    return FALSE;
  }
  $expire = array_unique($expire);

  // Add on the url to these paths
  $urls = array();
  global $base_url;
  if (variable_get('expire_include_base_url', EXPIRE_INCLUDE_BASE_URL)) {
    foreach (expire_get_base_urls($node) as $domain_id) {
      foreach ($domain_id as $base) {
        foreach ($expire as $path) {
          $urls[] = $base . $path;
        }
      }
    }
  }
  else {
    $urls = $expire;
  }

  // hook_expire_cache
  $modules = module_implements('expire_cache');
  foreach ($modules as $module) {
    module_invoke($module, 'expire_cache', $urls);
  }
  watchdog('expire', 'Input: !paths <br /> Output: !urls <br /> Modules Using hook_expire_cache(): !modules', array(
    '!paths' => expire_print_r($paths),
    '!urls' => expire_print_r($urls),
    '!modules' => expire_print_r($modules),
  ));
  return count($urls);
}

/**
 * Retrieve a specific URL redirect from the database.
 * http://drupal.org/node/451790
 *
 * @param $where
 *   Array containing 'redirect' => $path
 */
function expire_path_redirect_load($where = array(), $args = array(), $sort = array()) {
  $redirects = array();
  if (is_numeric($where)) {
    $where = array(
      'rid' => $where,
    );
  }
  foreach ($where as $key => $value) {
    if (is_string($key)) {
      $args[] = $value;
      $where[$key] = $key . ' = ' . (is_numeric($value) ? '%d' : "'%s'");
    }
  }
  if ($where && $args) {
    $sql = "SELECT * FROM {path_redirect} WHERE " . implode(' AND ', $where);
    if ($sort) {
      $sql .= ' ORDER BY ' . implode(' ,', $sort);
    }
    $result = db_query($sql, $args);
    while ($redirect = db_fetch_array($result)) {
      $redirects[] = $redirect;
    }
    return $redirects;
  }
}

/**
 * Simple print_r to html function
 *
 * @param $data
 *
 * @return string
 *   print_r contents in nicely formatted html
 */
function expire_print_r($data) {
  return str_replace('    ', '&nbsp;&nbsp;&nbsp;&nbsp;', nl2br(htmlentities(print_r($data, TRUE))));
}

/**
 * Get all base url's where this node can appear. Domain access support.
 *
 * @param $node
 *   node object
 * @return array
 *   array(0 => array($base_url . '/'))
 */
function expire_get_base_urls(&$node) {
  global $base_url, $base_path;

  // Get list of URL's if using domain access
  $base_urls = array();
  $domains = array();
  if (module_exists('domain') && isset($node->domains)) {

    // Get domains from node/user object
    foreach ($node->domains as $key => $domain_id) {
      if ($key != $domain_id) {
        continue;
      }
      $domains[$domain_id] = $domain_id;
    }

    // Get domains from database
    foreach (expire_get_domains($node) as $domain_id) {
      $domains[$domain_id] = $domain_id;
    }

    // Get aliases and set base url
    foreach ($domains as $domain_id) {
      $domain = domain_lookup($domain_id);
      if ($domain['valid'] == 1) {
        if (isset($domain['path'])) {
          $base_urls[$domain_id][] = $domain['path'];
        }
        if (is_array($domain['aliases'])) {
          foreach ($domain['aliases'] as $alias) {
            if ($alias['redirect'] != 1) {
              $temp_domain = array(
                'scheme' => $domain['scheme'],
                'subdomain' => $alias['pattern'],
              );
              $base_urls[$domain_id][] = domain_get_path($temp_domain);
            }
          }
        }
      }
    }
  }
  else {
    $base_urls[0][] = $base_url . '/';
  }
  return $base_urls;
}

/**
 * Get domains the node is currently published to
 *
 * @param $node
 *   node object
 * @return array
 *   array('$gid' => $gid)
 */
function expire_get_domains(&$node) {
  $domains = array();
  if ($node->nid) {
    $result = db_query("SELECT gid FROM {domain_access} WHERE nid = %d", $node->nid);
    while ($row = db_fetch_array($result)) {
      $gid = $row['gid'];
      $domains[$gid] = $gid;
    }
  }
  elseif ($node->mail && $node->name) {
    $result = db_query("SELECT domain_id FROM {domain_editor} WHERE uid = %d", $node->uid);
    while ($row = db_fetch_array($result)) {
      $gid = $row['domain_id'];
      $domains[$gid] = $gid;
    }
  }
  return $domains;
}
function expire_normal_path_check($path) {
  $original_map = arg(NULL, $path);
  $parts = array_slice($original_map, 0, MENU_MAX_PARTS);
  list($ancestors, $placeholders) = menu_get_ancestors($parts);
  $router_item = db_fetch_array(db_query_range('SELECT path FROM {menu_router} WHERE path IN (' . implode(',', $placeholders) . ') ORDER BY fit DESC', $ancestors, 0, 1));
  return $router_item;
}

Functions

Namesort descending Description
expire_cache_derivative Finds all possible paths/redirects/aliases given the root path.
expire_comment Implementation of hook_comment().
expire_get_base_urls Get all base url's where this node can appear. Domain access support.
expire_get_domains Get domains the node is currently published to
expire_get_menu_structure Finds parent, siblings and children of the menu item. UGLY CODE...
expire_menu Implementation of hook_menu().
expire_node Expires a node from the cache; including related pages.
expire_nodeapi Implementation of hook_nodeapi().
expire_normal_path_check
expire_path_redirect_load Retrieve a specific URL redirect from the database. http://drupal.org/node/451790
expire_print_r Simple print_r to html function
expire_taxonomy_node_get_tids Return taxonomy terms given a nid.
expire_user Implementation of hook_user().
expire_votingapi_delete Implementation of hook_votingapi_delete().
expire_votingapi_insert Implementation of hook_votingapi_insert().
_expire_votingapi Common expiry logic for votingapi.

Constants