diff --git a/includes/menu.inc b/includes/menu.inc index 2be0903..b6c93fb 100644 --- a/includes/menu.inc +++ b/includes/menu.inc @@ -2710,8 +2710,7 @@ function menu_rebuild() { $transaction = db_transaction(); try { - list($menu, $masks) = menu_router_build(); - _menu_router_save($menu, $masks); + list($menu, $masks) = menu_router_build(TRUE); _menu_navigation_links_rebuild($menu); // Clear the menu, page and block caches. menu_cache_clear_all(); @@ -2736,7 +2735,7 @@ function menu_rebuild() { /** * Collects and alters the menu definitions. */ -function menu_router_build() { +function menu_router_build($save = FALSE) { // We need to manually call each module so that we can know which module // a given item came from. $callbacks = array(); @@ -2751,7 +2750,7 @@ function menu_router_build() { } // Alter the menu as defined in modules, keys are like user/%user. drupal_alter('menu', $callbacks); - list($menu, $masks) = _menu_router_build($callbacks); + list($menu, $masks) = _menu_router_build($callbacks, $save); _menu_router_cache($menu); return array($menu, $masks); @@ -3534,11 +3533,12 @@ function _menu_link_parents_set(&$item, $parent) { /** * Builds the router table based on the data from hook_menu(). */ -function _menu_router_build($callbacks) { +function _menu_router_build($callbacks, $save = FALSE) { // First pass: separate callbacks from paths, making paths ready for // matching. Calculate fitness, and fill some default values. $menu = array(); $masks = array(); + $path_roots = array(); foreach ($callbacks as $path => $item) { $load_functions = array(); $to_arg_functions = array(); @@ -3546,6 +3546,7 @@ function _menu_router_build($callbacks) { $move = FALSE; $parts = explode('/', $path, MENU_MAX_PARTS); + $path_roots[$parts[0]] = $parts[0]; $number_parts = count($parts); // We store the highest index of parts here to save some work in the fit // calculation loop. @@ -3755,6 +3756,17 @@ function _menu_router_build($callbacks) { $masks = array_keys($masks); rsort($masks); + if ($save) { + $path_roots = array_values($path_roots); + // Update the path roots variable and reset the path alias whitelist cache + // if the list has changed. + if ($path_roots != variable_get('menu_path_roots', array())) { + variable_set('menu_path_roots', array_values($path_roots)); + drupal_clear_path_cache(); + } + _menu_router_save($menu, $masks); + } + return array($menu, $masks); } diff --git a/includes/path.inc b/includes/path.inc index 234430e..8fb820f 100644 --- a/includes/path.inc +++ b/includes/path.inc @@ -55,21 +55,13 @@ function drupal_lookup_path($action, $path = '', $path_language = NULL) { $cache = array( 'map' => array(), 'no_source' => array(), - 'whitelist' => NULL, + 'whitelist' => new PathAliasWhitelist('path_alias_whitelist', 'cache'), 'system_paths' => array(), 'no_aliases' => array(), 'first_call' => TRUE, ); } - // 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(); - } - } - // If no language is explicitly specified we default to the current URL // language. If we used a language different from the one conveyed by the // requested URL, we might end up being unable to check if there is a path @@ -78,9 +70,9 @@ function drupal_lookup_path($action, $path = '', $path_language = NULL) { if ($action == 'wipe') { $cache = array(); - $cache['whitelist'] = drupal_path_alias_whitelist_rebuild(); + drupal_path_alias_whitelist_rebuild(); } - elseif ($cache['whitelist'] && $path != '') { + elseif ($path != '') { if ($action == 'alias') { // During the first call to drupal_lookup_path() per language, load the // expected system paths for the page from cache. @@ -355,34 +347,131 @@ function current_path() { return $_GET['q']; } +/* + * Extends DrupalCacheArray to build the path alias whitelist over time. + */ +class PathAliasWhitelist extends DrupalCacheArray { + + /** + * Constructs an PathAliasWhitelist object. + * + * @param string $cid + * The cache id to use. + * @param string $bin + * The cache bin that should be used. + */ + public function __construct($cid, $bin) { + parent::__construct($cid, $bin); + + // On a cold start $this->storage will be empty and the whitelist will + // need to be rebuilt from scratch. The whitelist is initialized from the + // list of all valid path roots stored in the 'menu_path_roots' variable, + // with values initialized to NULL. During the request, each path requested + // that matches one of these keys will be looked up and the array value set + // to either TRUE or FALSE. This ensures that paths which do not exist in + // the router are not looked up, and that paths that do exist in the router + // are only looked up once. + if (empty($this->storage)) { + $this->loadMenuPathRoots(); + } + } + + /** + * Load menu path roots to prepopulate cache. + */ + protected function loadMenuPathRoots() { + if ($roots = variable_get('menu_path_roots', array())) { + foreach ($roots as $root) { + $this->storage[$root] = NULL; + $this->persist($root); + } + } + } + + /** + * Overrides DrupalCacheArray::offsetGet(). + */ + public function offsetGet($offset) { + // url() may be called with paths that are not represented by menu router + // items such as paths that will be rewritten by hook_url_outbound_alter(). + // Therefore internally TRUE is used to indicate whitelisted paths. FALSE is + // used to indicate paths that have already been checked but are not + // whitelisted, and NULL indicates paths that have not been checked yet. + if (isset($this->storage[$offset])) { + if ($this->storage[$offset]) { + return TRUE; + } + } + elseif (array_key_exists($offset, $this->storage)) { + return $this->resolveCacheMiss($offset); + } + } + + /** + * Overrides DrupalCacheArray::resolveCacheMiss(). + */ + public function resolveCacheMiss($root) { + $query = db_select('url_alias', 'u'); + $query->addExpression(1); + $exists = (bool) $query + ->condition('u.source', db_like($root) . '%', 'LIKE') + ->range(0, 1) + ->execute() + ->fetchField(); + $this->storage[$root] = $exists; + $this->persist($root); + if ($exists) { + return TRUE; + } + } + + /** + * Overrides DrupalCacheArray::set(). + */ + public function set($data, $lock = TRUE) { + $lock_name = $this->cid . ':' . $this->bin; + if (!$lock || lock_acquire($lock_name)) { + if ($cached = cache_get($this->cid, $this->bin)) { + // Use array merge instead of union so that filled in values in $data + // overwrite empty values in the current cache. + $data = array_merge($cached->data, $data); + } + cache_set($this->cid, $data, $this->bin); + if ($lock) { + lock_release($lock_name); + } + } + } + + /** + * Clear the cache. + */ + public function clear() { + cache_clear_all($this->cid, $this->bin); + $this->loadMenuPathRoots(); + } +} + /** * Rebuild the path alias white list. * - * @param $source + * @param string $source * An optional system path for which an alias is being inserted. - * - * @return - * An array containing a white list of path aliases. */ function drupal_path_alias_whitelist_rebuild($source = NULL) { // When paths are inserted, only rebuild the whitelist if the system path // has a top level component which is not already in the whitelist. + $static = &drupal_static('drupal_lookup_path'); if (!empty($source)) { - $whitelist = variable_get('path_alias_whitelist', NULL); - if (isset($whitelist[strtok($source, '/')])) { - return $whitelist; + if (isset($static['whitelist'][strtok($source, '/')])) { + return; } } - // 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 DISTINCT SUBSTRING_INDEX(source, '/', 1) AS path FROM {url_alias}"); - foreach ($result as $row) { - $whitelist[$row->path] = TRUE; + // There might be an uninitialized case. + if (!isset($static['whitelist'])) { + $static['whitelist'] = new PathAliasWhitelist('path_alias_whitelist', 'cache'); } - variable_set('path_alias_whitelist', $whitelist); - return $whitelist; + $static['whitelist']->clear(); } /**