Index: includes/common.inc =================================================================== --- includes/common.inc +++ includes/common.inc @@ -1611,7 +1611,7 @@ function drupal_page_footer() { if (variable_get('cache', CACHE_DISABLED) != CACHE_DISABLED) { page_set_cache(); } - + drupal_cache_system_paths(); module_invoke_all('exit'); } Index: includes/path.inc =================================================================== --- includes/path.inc +++ includes/path.inc @@ -45,46 +45,90 @@ function drupal_init_path() { */ function drupal_lookup_path($action, $path = '', $path_language = '') { global $language; - // $map is an array with language keys, holding arrays of Drupal paths to alias relations - static $map = array(), $no_src = array(), $count; + $cache = &drupal_static(__FUNCTION__, array( + 'map' => array(), + 'no_src' => array(), + 'whitelist' => NULL, + 'system_paths' => array(), + 'no_aliases' => array(), + 'first_call' => TRUE, + )); - $path_language = $path_language ? $path_language : $language->language; - - // Use $count to avoid looking up paths in subsequent calls if there simply are no aliases - if (!isset($count)) { - $count = db_result(db_query('SELECT COUNT(pid) FROM {url_alias}')); + // Retrieve the path alias whitelist. + if (!isset($cache['whitelist'])) { + $cache['whitelist'] = variable_get('path_alias_whitelist', NULL); + if (!isset($cache['whitelist'])) { + $cache['whitelist'] = drupal_path_alias_whitelist_rebuild(); + } } + $path_language = $path_language ? $path_language : $language->language; + if ($action == 'wipe') { - $map = array(); - $no_src = array(); - $count = NULL; + $cache = array(); + $cache['whitelist'] = drupal_path_alias_whitelist_rebuild(); } - elseif ($count > 0 && $path != '') { + elseif ($cache['whitelist'] && $path != '') { if ($action == 'alias') { - if (isset($map[$path_language][$path])) { - return $map[$path_language][$path]; + // During the first call to drupal_lookup_path() per language, load the + // expected system paths for the page from cache. + if (!empty($cache['first_call'])) { + $cache['first_call'] = FALSE; + + if (!isset($cache['map'][$path_language]) || !is_array($cache['map'][$path_language])) { + $cache['map'][$path_language] = array(); + } + // Load system paths from cache. + $cid = $_GET['q']; + if ($cached = cache_get($cid, 'cache_path')) { + $cache['system_paths'] = $cached->data; + // Now fetch the aliases corresponding to these system paths. + // We order by ASC and overwrite array keys to ensure the correct + // alias is used when there are multiple aliases per path. + $placeholders = db_placeholders($cache['system_paths'], 'varchar'); + $result = db_query("SELECT src, dst FROM {url_alias} WHERE src IN($placeholders) AND language IN('%s', '') ORDER BY language ASC", $cache['system_paths'], $path_language); + while ($record = db_fetch_object($result)) { + if (!isset($cache['map'][$path_language][$record->src])) { + $cache['map'][$path_language][$record->src] = $record->dst; + } + } + // Keep a record of paths with no alias to avoid querying twice. + $cache['no_aliases'][$path_language] = array_flip(array_diff_key($cache['system_paths'], array_keys($cache['map'][$path_language]))); + } + } + // If the alias has already been loaded, return it. + if (isset($cache['map'][$path_language][$path])) { + return $cache['map'][$path_language][$path]; + } + // Check the path whitelist, if the top_level part before the first / + // is not in the list, then there is no need to do anything further, + // it is not in the database. + elseif (!isset($cache['whitelist'][strtok($path, '/')])) { + return FALSE; + } + // For system paths which were not cached, query aliases individually. + else if (!isset($cache['no_aliases'][$path_language][$path])) { + // Get the most fitting result falling back with alias without language + $alias = db_result(db_query("SELECT dst FROM {url_alias} WHERE src = '%s' AND language IN('%s', '') ORDER BY language DESC", $path, $path_language)); + $cache['map'][$path_language][$path] = $alias; + return $alias; } - // Get the most fitting result falling back with alias without language - $alias = db_result(db_query("SELECT dst FROM {url_alias} WHERE src = '%s' AND language IN('%s', '') ORDER BY language DESC", $path, $path_language)); - $map[$path_language][$path] = $alias; - return $alias; } - // Check $no_src for this $path in case we've already determined that there + // Check no_src for this $path in case we've already determined that there // isn't a path that has this alias - elseif ($action == 'source' && !isset($no_src[$path_language][$path])) { - // Look for the value $path within the cached $map + elseif ($action == 'source' && !isset($cache['no_src'][$path_language][$path])) { + // Look for the value $path within the cached map $src = ''; - if (!isset($map[$path_language]) || !($src = array_search($path, $map[$path_language]))) { + if (!isset($cache['map'][$path_language]) || !($src = array_search($path, $cache['map'][$path_language]))) { // Get the most fitting result falling back with alias without language if ($src = db_result(db_query("SELECT src FROM {url_alias} WHERE dst = '%s' AND language IN('%s', '') ORDER BY language DESC", $path, $path_language))) { - $map[$path_language][$src] = $path; + $cache['map'][$path_language][$src] = $path; } else { - // We can't record anything into $map because we do not have a valid + // We can't record anything into map because we do not have a valid // index and there is no need because we have not learned anything - // about any Drupal path. Thus cache to $no_src. - $no_src[$path_language][$path] = TRUE; + // about any Drupal path. Thus cache to no_src. + $cache['no_src'][$path_language][$path] = TRUE; } } return $src; @@ -95,6 +139,31 @@ function drupal_lookup_path($action, $path = '', $path_language = '') { } /** + * Cache system paths for a page. + * + * Cache an array of the system paths available on each page. We assume + * that aiases will be needed for the majority of these paths during + * subsequent requests, and load them in a single query during + * drupal_lookup_path(). + */ +function drupal_cache_system_paths() { + // Check if the system paths for this page were loaded from cache in this + // request to avoid writing to cache on every request. + $cache = &drupal_static('drupal_lookup_path', array()); + if (!$cache['system_paths']) { + // Generate a cache ID (cid) specifically for this page. + $cid = $_GET['q']; + // The static $map array used by drupal_lookup_path() includes all + // system paths for the page request. + if ($paths = current($cache['map'])) { + $data = array_keys($paths); + $expire = $_SERVER['REQUEST_TIME'] + (60 * 60 * 24); + cache_set($cid, $data, 'cache_path', $expire); + } + } +} + +/** * Given an internal Drupal path, return the alias set by the administrator. * * @param $path @@ -241,3 +310,64 @@ function drupal_match_path($path, $patterns) { } return preg_match($regexps[$patterns], $path); } + +/** + * Central static variable storage. + * + * @param $name + * Globally unique name for the variable. For a function with only one static, + * variable, the function name (e.g. via the PHP magic __FUNCTION__ constant) + * is recommended. For a function with multiple static variables add a + * distinguishing suffix to the function name for each one. + * @param $default_value + * Optional default value. + * @param $reset + * TRUE to reset a specific named variable, or all variables if $name is NULL. + * Resetting every variable should only be used, for example, for running + * unit tests with a clean environment. Should be used only though via + * function drupal_static_reset(). + * + * @return + * Returns a variable by reference if $reset is FALSE. + */ +function &drupal_static($name, $default_value = NULL, $reset = FALSE) { + static $data = array(); + + // Reset a single value, or all values. + if ($reset) { + if (isset($name)) { + unset($data[$name]); + } + else { + $data = array(); + } + // We must return a reference to a variable. + $dummy = NULL; + return $dummy; + } + + if (!isset($data[$name])) { + $data[$name] = $default_value; + } + + return $data[$name]; +} + +/** + * Rebuild the path alias white list. + * + * @return + * An array containing a white list of path aliases. + */ +function drupal_path_alias_whitelist_rebuild() { + // For each alias in the database, get the top level component of the system + // path it corresponds to. This is the portion of the path before the first / + // if present, otherwise the whole path itself. + $whitelist = array(); + $result = db_query("SELECT SUBSTRING_INDEX(src, '/', 1) AS path FROM {url_alias} GROUP BY path"); + while ($row = db_fetch_object($result)) { + $whitelist[$row->path] = TRUE; + } + variable_set('path_alias_whitelist', $whitelist); + return $whitelist; +}