Commit e3d86383 authored by Dries's avatar Dries

Issue #1269742 by katbailey, beejeebus, pwolanin, g.oechsler, ksenzee: Make...

Issue #1269742 by katbailey, beejeebus, pwolanin, g.oechsler, ksenzee: Make path lookup code into an pluggable class.
parent daf490d3
......@@ -2490,6 +2490,10 @@ function drupal_container(Container $new_container = NULL, $rebuild = FALSE) {
->register('keyvalue.database', 'Drupal\Core\KeyValueStore\KeyValueDatabaseFactory')
->addArgument(new Reference('database'));
$container->register('path.alias_manager', 'Drupal\Core\Path\AliasManager')
->addArgument(new Reference('database'))
->addArgument(new Reference('keyvalue.database'));
// Register the EntityManager.
$container->register('plugin.manager.entity', 'Drupal\Core\Entity\EntityManager');
}
......
......@@ -2208,7 +2208,7 @@ function url($path = NULL, array $options = array()) {
}
elseif (!empty($path) && !$options['alias']) {
$langcode = isset($options['language']) && isset($options['language']->langcode) ? $options['language']->langcode : '';
$alias = drupal_get_path_alias($original_path, $langcode);
$alias = drupal_container()->get('path.alias_manager')->getPathAlias($original_path, $langcode);
if ($alias != $original_path) {
$path = $alias;
}
......@@ -4922,9 +4922,6 @@ function _drupal_bootstrap_full($skip = FALSE) {
// current_path().
drupal_language_initialize();
// Initialize current_path() prior to invoking hook_init().
drupal_path_initialize();
// Let all modules take action before the menu system handles the request.
// We do not want this while running update.php.
if (!defined('MAINTENANCE_MODE') || MAINTENANCE_MODE != 'update') {
......
......@@ -2994,7 +2994,7 @@ function _menu_delete_item($item, $force = FALSE) {
* @param $item
* An associative array representing a menu link item, with elements:
* - link_path: (required) The path of the menu item, which should be
* normalized first by calling drupal_get_normal_path() on it.
* normalized first by calling drupal_container()->get('path.alias_manager')->getSystemPath() on it.
* - link_title: (required) Title to appear in menu for the link.
* - menu_name: (optional) The machine name of the menu for the link.
* Defaults to 'tools'.
......
......@@ -2,296 +2,13 @@
/**
* @file
* Functions to handle paths in Drupal, including path aliasing.
* Functions to handle paths in Drupal.
*
* These functions are not loaded for cached pages, but modules that need
* to use them in hook_boot() or hook exit() can make them available, by
* executing "drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);".
*/
/**
* Initializes the current path to the proper normal path.
*/
function drupal_path_initialize() {
// At this point, the current path is either the request path (due to
// drupal_environment_initialize()) or some modified version of it due to
// other bootstrap code (e.g., language negotiation), but it has not yet been
// normalized by drupal_get_normal_path().
$path = _current_path();
// If on the front page, resolve to the front page path, including for calls
// to current_path() while drupal_get_normal_path() is in progress.
if (empty($path)) {
$path = config('system.site')->get('page.front');
_current_path($path);
}
// Normalize the path.
_current_path(drupal_get_normal_path($path));
}
/**
* Given an alias, return its Drupal system URL if one exists. Given a Drupal
* system URL return one of its aliases if such a one exists. Otherwise,
* return FALSE.
*
* @param $action
* One of the following values:
* - wipe: delete the alias cache.
* - alias: return an alias for a given Drupal system path (if one exists).
* - source: return the Drupal system URL for a path alias (if one exists).
* @param $path
* The path to investigate for corresponding aliases or system URLs.
* @param $langcode
* Optional language code to search the path with. Defaults to the page language.
* If there's no path defined for that language it will search paths without
* language.
*
* @return
* Either a Drupal system path, an aliased path, or FALSE if no path was
* found.
*/
function drupal_lookup_path($action, $path = '', $langcode = NULL) {
// Use the advanced drupal_static() pattern, since this is called very often.
static $drupal_static_fast;
if (!isset($drupal_static_fast)) {
$drupal_static_fast['cache'] = &drupal_static(__FUNCTION__);
}
$cache = &$drupal_static_fast['cache'];
if (!isset($cache)) {
$cache = array(
'map' => array(),
'no_source' => array(),
'whitelist' => NULL,
'system_paths' => array(),
'no_aliases' => array(),
'first_call' => TRUE,
);
}
// Retrieve the path alias whitelist.
if (!isset($cache['whitelist'])) {
$cache['whitelist'] = state()->get('system.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
// alias matching the URL path.
$langcode = $langcode ? $langcode : language(LANGUAGE_TYPE_URL)->langcode;
if ($action == 'wipe') {
$cache = array();
$cache['whitelist'] = drupal_path_alias_whitelist_rebuild();
}
elseif ($cache['whitelist'] && $path != '') {
if ($action == 'alias') {
// 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;
$cache['map'][$langcode] = array();
// Load system paths from cache.
$cid = current_path();
if ($cached = cache('path')->get($cid)) {
$cache['system_paths'] = $cached->data;
// Now fetch the aliases corresponding to these system paths.
$args = array(
':system' => $cache['system_paths'],
':langcode' => $langcode,
':langcode_undetermined' => LANGUAGE_NOT_SPECIFIED,
);
// Always get the language-specific alias before the language-neutral
// one. For example 'de' is less than 'und' so the order needs to be
// ASC, while 'xx-lolspeak' is more than 'und' so the order needs to
// be DESC. We also order by pid ASC so that fetchAllKeyed() returns
// the most recently created alias for each source. Subsequent queries
// using fetchField() must use pid DESC to have the same effect.
// For performance reasons, the query builder is not used here.
if ($langcode == LANGUAGE_NOT_SPECIFIED) {
// Prevent PDO from complaining about a token the query doesn't use.
unset($args[':langcode']);
$result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND langcode = :langcode_undetermined ORDER BY pid ASC', $args);
}
elseif ($langcode < LANGUAGE_NOT_SPECIFIED) {
$result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid ASC', $args);
}
else {
$result = db_query('SELECT source, alias FROM {url_alias} WHERE source IN (:system) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid ASC', $args);
}
$cache['map'][$langcode] = $result->fetchAllKeyed();
// Keep a record of paths with no alias to avoid querying twice.
$cache['no_aliases'][$langcode] = array_flip(array_diff_key($cache['system_paths'], array_keys($cache['map'][$langcode])));
}
}
// If the alias has already been loaded, return it.
if (isset($cache['map'][$langcode][$path])) {
return $cache['map'][$langcode][$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.
elseif (!isset($cache['no_aliases'][$langcode][$path])) {
$args = array(
':source' => $path,
':langcode' => $langcode,
':langcode_undetermined' => LANGUAGE_NOT_SPECIFIED,
);
// See the queries above.
if ($langcode == LANGUAGE_NOT_SPECIFIED) {
unset($args[':langcode']);
$alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode = :langcode_undetermined ORDER BY pid DESC", $args)->fetchField();
}
elseif ($langcode > LANGUAGE_NOT_SPECIFIED) {
$alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args)->fetchField();
}
else {
$alias = db_query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args)->fetchField();
}
$cache['map'][$langcode][$path] = $alias;
return $alias;
}
}
// Check $no_source for this $path in case we've already determined that there
// isn't a path that has this alias
elseif ($action == 'source' && !isset($cache['no_source'][$langcode][$path])) {
// Look for the value $path within the cached $map
$source = FALSE;
if (!isset($cache['map'][$langcode]) || !($source = array_search($path, $cache['map'][$langcode]))) {
$args = array(
':alias' => $path,
':langcode' => $langcode,
':langcode_undetermined' => LANGUAGE_NOT_SPECIFIED,
);
// See the queries above.
if ($langcode == LANGUAGE_NOT_SPECIFIED) {
unset($args[':langcode']);
$result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode = :langcode_undetermined ORDER BY pid DESC", $args);
}
elseif ($langcode > LANGUAGE_NOT_SPECIFIED) {
$result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args);
}
else {
$result = db_query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args);
}
if ($source = $result->fetchField()) {
$cache['map'][$langcode][$source] = $path;
}
else {
// 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_source.
$cache['no_source'][$langcode][$path] = TRUE;
}
}
return $source;
}
}
return FALSE;
}
/**
* Cache system paths for a page.
*
* Cache an array of the system paths available on each page. We assume
* that aliases 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 (empty($cache['system_paths']) && !empty($cache['map'])) {
// @todo Because we are not within the request scope at this time, we cannot
// use current_path(), which would give us the system path to use as the
// key. Instead we call _current_path(), which may give us the alias
// instead. However, at lookup time the system path will be used as the
// key, because it uses current_path(), and so it will be a cache miss.
// There is a critical issue for fixing the path alias logic here:
// http://drupal.org/node/1269742
// Generate a cache ID (cid) specifically for this page.
$cid = _current_path();
// 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 = REQUEST_TIME + (60 * 60 * 24);
cache('path')->set($cid, $data, $expire);
}
}
}
/**
* Given an internal Drupal path, return the alias set by the administrator.
*
* If no path is provided, the function will return the alias of the current
* page.
*
* @param $path
* An internal Drupal path.
* @param $langcode
* An optional language code to look up the path in.
*
* @return
* An aliased path if one was found, or the original path if no alias was
* found.
*/
function drupal_get_path_alias($path = NULL, $langcode = NULL) {
// If no path is specified, use the current page's path.
if ($path == NULL) {
$path = current_path();
}
$result = $path;
if ($alias = drupal_lookup_path('alias', $path, $langcode)) {
$result = $alias;
}
return $result;
}
/**
* Given a path alias, return the internal path it represents.
*
* @param $path
* A Drupal path alias.
* @param $langcode
* An optional language code to look up the path in.
*
* @return
* The internal path represented by the alias, or the original alias if no
* internal path was found.
*/
function drupal_get_normal_path($path, $langcode = NULL) {
$original_path = $path;
// Lookup the path alias first.
if ($source = drupal_lookup_path('source', $path, $langcode)) {
$path = $source;
}
// Allow other modules to alter the inbound URL. We cannot use drupal_alter()
// here because we need to run hook_url_inbound_alter() in the reverse order
// of hook_url_outbound_alter().
foreach (array_reverse(module_implements('url_inbound_alter')) as $module) {
$function = $module . '_url_inbound_alter';
$function($path, $original_path, $langcode);
}
return $path;
}
/**
* Check if the current page is the front page.
*
......@@ -378,36 +95,6 @@ function current_path() {
return _current_path();
}
/**
* Rebuild the path alias white list.
*
* @param $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.
if (!empty($source)) {
$whitelist = state()->get('system.path_alias_whitelist', NULL);
if (isset($whitelist[strtok($source, '/')])) {
return $whitelist;
}
}
// 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;
}
state()->set('system.path_alias_whitelist', $whitelist);
return $whitelist;
}
/**
* Fetch a specific URL alias from the database.
*
......@@ -415,13 +102,7 @@ function drupal_path_alias_whitelist_rebuild($source = NULL) {
* A string representing the source, a number representing the pid, or an
* array of query conditions.
*
* @return
* FALSE if no alias was found or an associative array containing the
* following keys:
* - source: The internal system path.
* - alias: The URL alias.
* - pid: Unique path alias identifier.
* - langcode: The language code of the alias.
* @see \Drupal\Core\Path\Path::load()
*/
function path_load($conditions) {
if (is_numeric($conditions)) {
......@@ -433,68 +114,7 @@ function path_load($conditions) {
elseif (!is_array($conditions)) {
return FALSE;
}
$select = db_select('url_alias');
foreach ($conditions as $field => $value) {
$select->condition($field, $value);
}
return $select
->fields('url_alias')
->execute()
->fetchAssoc();
}
/**
* Save a path alias to the database.
*
* @param $path
* An associative array containing the following keys:
* - source: The internal system path.
* - alias: The URL alias.
* - pid: (optional) Unique path alias identifier.
* - langcode: (optional) The language code of the alias.
*/
function path_save(&$path) {
$path += array('langcode' => LANGUAGE_NOT_SPECIFIED);
// Load the stored alias, if any.
if (!empty($path['pid']) && !isset($path['original'])) {
$path['original'] = path_load($path['pid']);
}
if (empty($path['pid'])) {
drupal_write_record('url_alias', $path);
module_invoke_all('path_insert', $path);
}
else {
drupal_write_record('url_alias', $path, array('pid'));
module_invoke_all('path_update', $path);
}
// Clear internal properties.
unset($path['original']);
// Clear the static alias cache.
drupal_clear_path_cache($path['source']);
}
/**
* Delete a URL alias.
*
* @param $criteria
* A number representing the pid or an array of criteria.
*/
function path_delete($criteria) {
if (!is_array($criteria)) {
$criteria = array('pid' => $criteria);
}
$path = path_load($criteria);
$query = db_delete('url_alias');
foreach ($criteria as $field => $value) {
$query->condition($field, $value);
}
$query->execute();
module_invoke_all('path_delete', $path);
drupal_clear_path_cache($path['source']);
return drupal_container()->get('path.crud')->load($conditions);
}
/**
......@@ -597,14 +217,3 @@ function drupal_valid_path($path, $dynamic_allowed = FALSE) {
return $item && $item['access'];
}
/**
* Clear the path cache.
*
* @param $source
* An optional system path for which an alias is being changed.
*/
function drupal_clear_path_cache($source = NULL) {
// Clear the drupal_lookup_path() static cache.
drupal_static_reset('drupal_lookup_path');
drupal_path_alias_whitelist_rebuild($source);
}
<?php
/**
* @file
* Contains Drupal\Core\CacheDecorator\AliasManagerCacheDecorator.
*/
namespace Drupal\Core\CacheDecorator;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Path\AliasManagerInterface;
/**
* Class used by the PathSubscriber to get the system path and cache path lookups.
*/
class AliasManagerCacheDecorator implements CacheDecoratorInterface, AliasManagerInterface {
/**
* @var \Drupal\Core\Path\AliasManagerInterface
*/
protected $aliasManager;
/**
* @var \Drupal\Core\Cache\CacheBackendInterface;
*/
protected $cache;
/**
* Stack of request paths for use as cids when caching system paths.
*
* @var array
*/
protected $cacheKeys = array();
/**
* Holds an array of previously cached paths based on a request path.
*
* @var array
*/
protected $preloadedPathLookups = array();
/**
* Whether the cache needs to be written.
*
* @var boolean
*/
protected $cacheNeedsWriting = TRUE;
/**
* Constructs a \Drupal\Core\CacheDecorator\AliasManagerCacheDecorator.
*/
public function __construct(AliasManagerInterface $alias_manager, CacheBackendInterface $cache) {
$this->aliasManager = $alias_manager;
$this->cache = $cache;
}
/**
* Implements \Drupal\Core\CacheDecorator\CacheDecoratorInterface::setCacheKey().
*/
public function setCacheKey($key) {
$this->cacheKeys[] = $key;
}
/**
* Implements \Drupal\Core\CacheDecorator\CacheDecoratorInterface::writeCache().
*
* Cache an array of the system paths available on each page. We assume
* that aliases will be needed for the majority of these paths during
* subsequent requests, and load them in a single query during path alias
* lookup.
*/
public function writeCache() {
$path_lookups = $this->getPathLookups();
// Check if the system paths for this page were loaded from cache in this
// request to avoid writing to cache on every request.
if ($this->cacheNeedsWriting && !empty($path_lookups) && !empty($this->cacheKeys)) {
// Use the system path of the current request for the cache ID (cid).
$cid = end($this->cacheKeys);
// Set the path cache to expire in 24 hours.
$expire = REQUEST_TIME + (60 * 60 * 24);
$this->cache->set($cid, $path_lookups, $expire);
}
// We are at the end of the request, so pop off the last request path.
array_pop($this->cacheKeys);
}
/**
* Implements \Drupal\Core\Path\AliasManagerInterface::getSystemPath().
*/
public function getSystemPath($path, $path_language = NULL) {
$system_path = $this->aliasManager->getSystemPath($path, $path_language);
// We need to pass on the list of previously cached system paths for this
// key to the alias manager for use in subsequent lookups.
$cached = $this->cache->get($system_path);
$cached_paths = array();
if ($cached) {
$cached_paths = $cached->data;
$this->cacheNeedsWriting = FALSE;
}
$this->preloadPathLookups($cached_paths);
return $system_path;
}
/**
* Implements \Drupal\Core\Path\AliasManagerInterface::getPathAlias().
*/
public function getPathAlias($path, $path_language = NULL) {
return $this->aliasManager->getPathAlias($path, $path_language);
}
/**
* Implements \Drupal\Core\Path\AliasManagerInterface::getPathLookups().
*/
public function getPathLookups() {
return $this->aliasManager->getPathLookups();
}
/**
* Implements \Drupal\Core\Path\AliasManagerInterface::preloadPathLookups().
*/
public function preloadPathLookups(array $path_list) {
$this->aliasManager->preloadPathLookups($path_list);
}
}
<?php
/**
* @file
* Contains Drupal\Core\CacheDecorator\CacheDecoratorInterface.
*/