Drupal 8.0.0 beta 12. More info: https://www.drupal.org/node/2514176

This commit is contained in:
Pantheon Automation 2015-08-17 17:00:26 -07:00 committed by Greg Anderson
commit 9921556621
13277 changed files with 1459781 additions and 0 deletions

View file

@ -0,0 +1,301 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\AliasManager.
*/
namespace Drupal\Core\Path;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\CacheDecorator\CacheDecoratorInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Language\LanguageManagerInterface;
class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
/**
* The alias storage service.
*
* @var \Drupal\Core\Path\AliasStorageInterface
*/
protected $storage;
/**
* Cache backend service.
*
* @var \Drupal\Core\Cache\CacheBackendInterface;
*/
protected $cache;
/**
* The cache key to use when caching paths.
*
* @var string
*/
protected $cacheKey;
/**
* Whether the cache needs to be written.
*
* @var bool
*/
protected $cacheNeedsWriting = FALSE;
/**
* Language manager for retrieving the default langcode when none is specified.
*
* @var \Drupal\Core\Language\LanguageManagerInterface
*/
protected $languageManager;
/**
* Holds the map of path lookups per language.
*
* @var array
*/
protected $lookupMap = array();
/**
* Holds an array of aliases for which no path was found.
*
* @var array
*/
protected $noPath = array();
/**
* Holds the array of whitelisted path aliases.
*
* @var \Drupal\Core\Path\AliasWhitelistInterface
*/
protected $whitelist;
/**
* Holds an array of paths that have no alias.
*
* @var array
*/
protected $noAlias = array();
/**
* Whether preloaded path lookups has already been loaded.
*
* @var array
*/
protected $langcodePreloaded = array();
/**
* Holds an array of previously looked up paths for the current request path.
*
* This will only get populated if a cache key has been set, which for example
* happens if the alias manager is used in the context of a request.
*
* @var array
*/
protected $preloadedPathLookups = FALSE;
/**
* Constructs an AliasManager.
*
* @param \Drupal\Core\Path\AliasStorageInterface $storage
* The alias storage service.
* @param \Drupal\Core\Path\AliasWhitelistInterface $whitelist
* The whitelist implementation to use.
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
* The language manager.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* Cache backend.
*/
public function __construct(AliasStorageInterface $storage, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
$this->storage = $storage;
$this->languageManager = $language_manager;
$this->whitelist = $whitelist;
$this->cache = $cache;
}
/**
* {@inheritdoc}
*/
public function setCacheKey($key) {
// Prefix the cache key to avoid clashes with other caches.
$this->cacheKey = 'preload-paths:' . $key;
}
/**
* {@inheritdoc}
*
* Cache an array of the 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() {
// Check if the paths for this page were loaded from cache in this request
// to avoid writing to cache on every request.
if ($this->cacheNeedsWriting && !empty($this->cacheKey)) {
// Start with the preloaded path lookups, so that cached entries for other
// languages will not be lost.
$path_lookups = $this->preloadedPathLookups ?: array();
foreach ($this->lookupMap as $langcode => $lookups) {
$path_lookups[$langcode] = array_keys($lookups);
if (!empty($this->noAlias[$langcode])) {
$path_lookups[$langcode] = array_merge($path_lookups[$langcode], array_keys($this->noAlias[$langcode]));
}
}
if (!empty($path_lookups)) {
$twenty_four_hours = 60 * 60 * 24;
$this->cache->set($this->cacheKey, $path_lookups, $this->getRequestTime() + $twenty_four_hours);
}
}
}
/**
* {@inheritdoc}
*/
public function getPathByAlias($alias, $langcode = NULL) {
// 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 ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
// If we already know that there are no paths for this alias simply return.
if (empty($alias) || !empty($this->noPath[$langcode][$alias])) {
return $alias;
}
// Look for the alias within the cached map.
if (isset($this->lookupMap[$langcode]) && ($path = array_search($alias, $this->lookupMap[$langcode]))) {
return $path;
}
// Look for path in storage.
if ($path = $this->storage->lookupPathSource($alias, $langcode)) {
$this->lookupMap[$langcode][$path] = $alias;
$this->cacheNeedsWriting = TRUE;
return $path;
}
// We can't record anything into $this->lookupMap because we didn't find any
// paths for this alias. Thus cache to $this->noPath.
$this->noPath[$langcode][$alias] = TRUE;
return $alias;
}
/**
* {@inheritdoc}
*/
public function getAliasByPath($path, $langcode = NULL) {
if ($path[0] !== '/') {
throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $path));
}
// 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 ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
// 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.
if ($path === '/' || !$this->whitelist->get(strtok(trim($path, '/'), '/'))) {
return $path;
}
// During the first call to this method per language, load the expected
// paths for the page from cache.
if (empty($this->langcodePreloaded[$langcode])) {
$this->langcodePreloaded[$langcode] = TRUE;
$this->lookupMap[$langcode] = array();
// Load the cached paths that should be used for preloading. This only
// happens if a cache key has been set.
if ($this->preloadedPathLookups === FALSE) {
$this->preloadedPathLookups = array();
if ($this->cacheKey && $cached = $this->cache->get($this->cacheKey)) {
$this->preloadedPathLookups = $cached->data;
}
}
// Load paths from cache.
if (!empty($this->preloadedPathLookups[$langcode])) {
$this->lookupMap[$langcode] = $this->storage->preloadPathAlias($this->preloadedPathLookups[$langcode], $langcode);
// Keep a record of paths with no alias to avoid querying twice.
$this->noAlias[$langcode] = array_flip(array_diff_key($this->preloadedPathLookups[$langcode], array_keys($this->lookupMap[$langcode])));
}
}
// If we already know that there are no aliases for this path simply return.
if (!empty($this->noAlias[$langcode][$path])) {
return $path;
}
// If the alias has already been loaded, return it from static cache.
if (isset($this->lookupMap[$langcode][$path])) {
return $this->lookupMap[$langcode][$path];
}
// Try to load alias from storage.
if ($alias = $this->storage->lookupPathAlias($path, $langcode)) {
$this->lookupMap[$langcode][$path] = $alias;
$this->cacheNeedsWriting = TRUE;
return $alias;
}
// We can't record anything into $this->lookupMap because we didn't find any
// aliases for this path. Thus cache to $this->noAlias.
$this->noAlias[$langcode][$path] = TRUE;
$this->cacheNeedsWriting = TRUE;
return $path;
}
/**
* Implements \Drupal\Core\Path\AliasManagerInterface::cacheClear().
*/
public function cacheClear($source = NULL) {
if ($source) {
foreach (array_keys($this->lookupMap) as $lang) {
unset($this->lookupMap[$lang][$source]);
}
}
else {
$this->lookupMap = array();
}
$this->noPath = array();
$this->noAlias = array();
$this->langcodePreloaded = array();
$this->preloadedPathLookups = array();
$this->cache->delete($this->cacheKey);
$this->pathAliasWhitelistRebuild($source);
}
/**
* Rebuild the path alias white list.
*
* @param string $path
* An optional path for which an alias is being inserted.
*
* @return
* An array containing a white list of path aliases.
*/
protected function pathAliasWhitelistRebuild($path = NULL) {
// When paths are inserted, only rebuild the whitelist if the path has a top
// level component which is not already in the whitelist.
if (!empty($path)) {
if ($this->whitelist->get(strtok($path, '/'))) {
return;
}
}
$this->whitelist->clear();
}
/**
* Wrapper method for REQUEST_TIME constant.
*
* @return int
*/
protected function getRequestTime() {
return defined('REQUEST_TIME') ? REQUEST_TIME : (int) $_SERVER['REQUEST_TIME'];
}
}

View file

@ -0,0 +1,52 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\AliasManagerInterface.
*/
namespace Drupal\Core\Path;
interface AliasManagerInterface {
/**
* Given the alias, return the path it represents.
*
* @param string $alias
* An alias.
* @param string $langcode
* An optional language code to look up the path in.
*
* @return string
* The path represented by alias, or the alias if no path was found.
*
* @throws \InvalidArgumentException
* Thrown when the path does not start with a slash.
*/
public function getPathByAlias($alias, $langcode = NULL);
/**
* Given a path, return the alias.
*
* @param string $path
* A path.
* @param string $langcode
* An optional language code to look up the path in.
*
* @return string
* An alias that represents the path, or path if no alias was found.
*
* @throws \InvalidArgumentException
* Thrown when the path does not start with a slash.
*/
public function getAliasByPath($path, $langcode = NULL);
/**
* Clear internal caches in alias manager.
*
* @param $source
* Source path of the alias that is being inserted/updated. Can be omitted
* if entire cache needs to be flushed.
*/
public function cacheClear($source = NULL);
}

View file

@ -0,0 +1,259 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\AliasStorage.
*/
namespace Drupal\Core\Path;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides a class for CRUD operations on path aliases.
*/
class AliasStorage implements AliasStorageInterface {
/**
* The database connection.
*
* @var \Drupal\Core\Database\Connection
*/
protected $connection;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* Constructs a Path CRUD object.
*
* @param \Drupal\Core\Database\Connection $connection
* A database connection for reading and writing path aliases.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(Connection $connection, ModuleHandlerInterface $module_handler) {
$this->connection = $connection;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, $pid = NULL) {
if ($source[0] !== '/') {
throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $source));
}
if ($alias[0] !== '/') {
throw new \InvalidArgumentException(sprintf('Alias path %s has to start with a slash.', $alias));
}
$fields = array(
'source' => $source,
'alias' => $alias,
'langcode' => $langcode,
);
// Insert or update the alias.
if (empty($pid)) {
$query = $this->connection->insert('url_alias')
->fields($fields);
$pid = $query->execute();
$fields['pid'] = $pid;
$operation = 'insert';
}
else {
// Fetch the current values so that an update hook can identify what
// exactly changed.
$original = $this->connection->query('SELECT source, alias, langcode FROM {url_alias} WHERE pid = :pid', array(':pid' => $pid))->fetchAssoc();
$fields['pid'] = $pid;
$query = $this->connection->update('url_alias')
->fields($fields)
->condition('pid', $pid);
$pid = $query->execute();
$fields['original'] = $original;
$operation = 'update';
}
if ($pid) {
// @todo Switch to using an event for this instead of a hook.
$this->moduleHandler->invokeAll('path_' . $operation, array($fields));
Cache::invalidateTags(['route_match']);
return $fields;
}
return FALSE;
}
/**
* {@inheritdoc}
*/
public function load($conditions) {
$select = $this->connection->select('url_alias');
foreach ($conditions as $field => $value) {
$select->condition($field, $value);
}
return $select
->fields('url_alias')
->orderBy('pid', 'DESC')
->range(0, 1)
->execute()
->fetchAssoc();
}
/**
* {@inheritdoc}
*/
public function delete($conditions) {
$path = $this->load($conditions);
$query = $this->connection->delete('url_alias');
foreach ($conditions as $field => $value) {
$query->condition($field, $value);
}
$deleted = $query->execute();
// @todo Switch to using an event for this instead of a hook.
$this->moduleHandler->invokeAll('path_delete', array($path));
Cache::invalidateTags(['route_match']);
return $deleted;
}
/**
* {@inheritdoc}
*/
public function preloadPathAlias($preloaded, $langcode) {
$args = array(
':system[]' => $preloaded,
':langcode' => $langcode,
':langcode_undetermined' => LanguageInterface::LANGCODE_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 == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
// Prevent PDO from complaining about a token the query doesn't use.
unset($args[':langcode']);
$result = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode = :langcode_undetermined ORDER BY pid ASC', $args);
}
elseif ($langcode < LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$result = $this->connection->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 = $this->connection->query('SELECT source, alias FROM {url_alias} WHERE source IN ( :system[] ) AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid ASC', $args);
}
return $result->fetchAllKeyed();
}
/**
* {@inheritdoc}
*/
public function lookupPathAlias($path, $langcode) {
$args = array(
':source' => $path,
':langcode' => $langcode,
':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
);
// See the queries above.
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
unset($args[':langcode']);
$alias = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode = :langcode_undetermined ORDER BY pid DESC", $args)->fetchField();
}
elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$alias = $this->connection->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 = $this->connection->query("SELECT alias FROM {url_alias} WHERE source = :source AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args)->fetchField();
}
return $alias;
}
/**
* {@inheritdoc}
*/
public function lookupPathSource($path, $langcode) {
$args = array(
':alias' => $path,
':langcode' => $langcode,
':langcode_undetermined' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
);
// See the queries above.
if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
unset($args[':langcode']);
$result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode = :langcode_undetermined ORDER BY pid DESC", $args);
}
elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
$result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode DESC, pid DESC", $args);
}
else {
$result = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode IN (:langcode, :langcode_undetermined) ORDER BY langcode ASC, pid DESC", $args);
}
return $result->fetchField();
}
/**
* {@inheritdoc}
*/
public function aliasExists($alias, $langcode, $source = NULL) {
$query = $this->connection->select('url_alias')
->condition('alias', $alias)
->condition('langcode', $langcode);
if (!empty($source)) {
$query->condition('source', $source, '<>');
}
$query->addExpression('1');
$query->range(0, 1);
return (bool) $query->execute()->fetchField();
}
/**
* {@inheritdoc}
*/
public function languageAliasExists() {
return (bool) $this->connection->queryRange('SELECT 1 FROM {url_alias} WHERE langcode <> :langcode', 0, 1, array(':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED))->fetchField();
}
/**
* {@inheritdoc}
*/
public function getAliasesForAdminListing($header, $keys = NULL) {
$query = $this->connection->select('url_alias')
->extend('Drupal\Core\Database\Query\PagerSelectExtender')
->extend('Drupal\Core\Database\Query\TableSortExtender');
if ($keys) {
// Replace wildcards with PDO wildcards.
$query->condition('alias', '%' . preg_replace('!\*+!', '%', $keys) . '%', 'LIKE');
}
return $query
->fields('url_alias')
->orderByHeader($header)
->limit(50)
->execute()
->fetchAll();
}
/**
* {@inheritdoc}
*/
public function pathHasMatchingAlias($initial_substring) {
$query = $this->connection->select('url_alias', 'u');
$query->addExpression(1);
return (bool) $query
->condition('u.source', $this->connection->escapeLike($initial_substring) . '%', 'LIKE')
->range(0, 1)
->execute()
->fetchField();
}
}

View file

@ -0,0 +1,156 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\AliasStorageInterface.
*/
namespace Drupal\Core\Path;
use Drupal\Core\Language\LanguageInterface;
/**
* Provides a class for CRUD operations on path aliases.
*/
interface AliasStorageInterface {
/**
* Saves a path alias to the database.
*
* @param string $source
* The internal system path.
* @param string $alias
* The URL alias.
* @param string $langcode
* (optional) The language code of the alias.
* @param int|null $pid
* (optional) Unique path alias identifier.
*
* @return array|false
* FALSE if the path could not be saved or an associative array containing
* the following keys:
* - source (string): The internal system path with a starting slash.
* - alias (string): The URL alias with a starting slash.
* - pid (int): Unique path alias identifier.
* - langcode (string): The language code of the alias.
* - original: For updates, an array with source, alias and langcode with
* the previous values.
*
* @thrown \InvalidArgumentException
* Thrown when either the source or alias has not a starting slash.
*/
public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, $pid = NULL);
/**
* Fetches a specific URL alias from the database.
*
* @param array $conditions
* An array of query conditions.
*
* @return array|false
* FALSE if no alias was found or an associative array containing the
* following keys:
* - source (string): The internal system path with a starting slash.
* - alias (string): The URL alias with a starting slash.
* - pid (int): Unique path alias identifier.
* - langcode (string): The language code of the alias.
*/
public function load($conditions);
/**
* Deletes a URL alias.
*
* @param array $conditions
* An array of criteria.
*/
public function delete($conditions);
/**
* Pre-loads path alias information for a given list of source paths.
*
* @param array $preloaded
* Paths that need preloading of aliases.
* @param string $langcode
* Language code to search the path with. If there's no path defined for
* that language it will search paths without language.
*
* @return string[]
* Source (keys) to alias (values) mapping.
*/
public function preloadPathAlias($preloaded, $langcode);
/**
* Returns an alias of Drupal system URL.
*
* @param string $path
* The path to investigate for corresponding path aliases.
* @param string $langcode
* Language code to search the path with. If there's no path defined for
* that language it will search paths without language.
*
* @return string|false
* A path alias, or FALSE if no path was found.
*/
public function lookupPathAlias($path, $langcode);
/**
* Returns Drupal system URL of an alias.
*
* @param string $path
* The path to investigate for corresponding system URLs.
* @param string $langcode
* Language code to search the path with. If there's no path defined for
* that language it will search paths without language.
*
* @return string|false
* A Drupal system path, or FALSE if no path was found.
*/
public function lookupPathSource($path, $langcode);
/**
* Checks if alias already exists.
*
* @param string $alias
* Alias to check against.
* @param string $langcode
* Language of the alias.
* @param string|null $source
* (optional) Path that alias is to be assigned to.
*
* @return bool
* TRUE if alias already exists and FALSE otherwise.
*/
public function aliasExists($alias, $langcode, $source = NULL);
/**
* Checks if there are any aliases with language defined.
*
* @return bool
* TRUE if aliases with language exist.
*/
public function languageAliasExists();
/**
* Loads aliases for admin listing.
*
* @param array $header
* Table header.
* @param string[]|null $keys
* (optional) Search keys.
*
* @return array
* Array of items to be displayed on the current page.
*/
public function getAliasesForAdminListing($header, $keys = NULL);
/**
* Check if any alias exists starting with $initial_substring.
*
* @param string $initial_substring
* Initial path substring to test against.
*
* @return bool
* TRUE if any alias exists, FALSE otherwise.
*/
public function pathHasMatchingAlias($initial_substring);
}

View file

@ -0,0 +1,126 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\AliasWhitelist.
*/
namespace Drupal\Core\Path;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Database\Connection;
use Drupal\Core\State\StateInterface;
use Drupal\Core\Lock\LockBackendInterface;
/**
* Extends CacheCollector to build the path alias whitelist over time.
*/
class AliasWhitelist extends CacheCollector implements AliasWhitelistInterface {
/**
* The Key/Value Store to use for state.
*
* @var \Drupal\Core\State\StateInterface
*/
protected $state;
/**
* The Path CRUD service.
*
* @var \Drupal\Core\Path\AliasStorageInterface
*/
protected $aliasStorage;
/**
* Constructs an AliasWhitelist object.
*
* @param string $cid
* The cache id to use.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\State\StateInterface $state
* The state keyvalue store.
* @param \Drupal\Core\Path\AliasStorageInterface $alias_storage
* The alias storage service.
*/
public function __construct($cid, CacheBackendInterface $cache, LockBackendInterface $lock, StateInterface $state, AliasStorageInterface $alias_storage) {
parent::__construct($cid, $cache, $lock);
$this->state = $state;
$this->aliasStorage = $alias_storage;
}
/**
* {@inheritdoc}
*/
protected function lazyLoadCache() {
parent::lazyLoadCache();
// 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 'router.path_roots' state,
// 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();
}
}
/**
* Loads menu path roots to prepopulate cache.
*/
protected function loadMenuPathRoots() {
if ($roots = $this->state->get('router.path_roots')) {
foreach ($roots as $root) {
$this->storage[$root] = NULL;
$this->persist($root);
}
}
}
/**
* {@inheritdoc}
*/
public function get($offset) {
$this->lazyLoadCache();
// this 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);
}
}
/**
* {@inheritdoc}
*/
public function resolveCacheMiss($root) {
$exists = $this->aliasStorage->pathHasMatchingAlias('/' . $root);
$this->storage[$root] = $exists;
$this->persist($root);
if ($exists) {
return TRUE;
}
}
/**
* {@inheritdoc}
*/
public function clear() {
parent::clear();
$this->loadMenuPathRoots();
}
}

View file

@ -0,0 +1,12 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\AliasWhitelistInterface.
*/
namespace Drupal\Core\Path;
use Drupal\Core\Cache\CacheCollectorInterface;
interface AliasWhitelistInterface extends CacheCollectorInterface {}

View file

@ -0,0 +1,84 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\CurrentPathStack.
*/
namespace Drupal\Core\Path;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Represents the current path for the current request.
*
* Note: You should not rely on paths but rather on route names / parameters or
* other indicators like context. For some fundamental parts, like routing or
* path processing, there is unfortunately no way around dealing with paths.
*/
class CurrentPathStack {
/**
* Static cache of paths.
*
* @var \SplObjectStorage
*/
protected $paths;
/**
* The request stack.
*
* @var \Symfony\Component\HttpFoundation\RequestStack
*/
protected $requestStack;
/**
* Constructs a new CurrentPathStack instance.
*
* @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
* The request stack.
*/
public function __construct(RequestStack $request_stack) {
$this->requestStack = $request_stack;
$this->paths = new \SplObjectStorage();
}
/**
* Returns the path of the current request.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* (optional) The request.
*
* @return string
* Returns the path, without leading slashes.
*/
public function getPath($request = NULL) {
if (!isset($request)) {
$request = $this->requestStack->getCurrentRequest();
}
if (!isset($this->paths[$request])) {
$this->paths[$request] = $request->getPathInfo();
}
return $this->paths[$request];
}
/**
* Sets the current path.
*
* @param string $path
* The path.
* @param \Symfony\Component\HttpFoundation\Request $request
* (optional) The request.
*
* @return $this
*/
public function setPath($path, $request = NULL) {
if (!isset($request)) {
$request = $this->requestStack->getCurrentRequest();
}
$this->paths[$request] = $path;
return $this;
}
}

View file

@ -0,0 +1,124 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\PathMatcher.
*/
namespace Drupal\Core\Path;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Url;
/**
* Provides a path matcher.
*/
class PathMatcher implements PathMatcherInterface {
/**
* Whether the current page is the front page.
*
* @var bool
*/
protected $isCurrentFrontPage;
/**
* The default front page.
*
* @var string
*/
protected $frontPage;
/**
* The cache of regular expressions.
*
* @var array
*/
protected $regexes;
/**
* The config factory service.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The current route match.
*
* @var \Drupal\Core\Routing\RouteMatchInterface
*/
protected $routeMatch;
/**
* Creates a new PathMatcher.
*
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\Core\Routing\RouteMatchInterface $route_match
* The current route match.
*/
public function __construct(ConfigFactoryInterface $config_factory, RouteMatchInterface $route_match) {
$this->configFactory = $config_factory;
$this->routeMatch = $route_match;
}
/**
* {@inheritdoc}
*/
public function matchPath($path, $patterns) {
if (!isset($this->regexes[$patterns])) {
// Convert path settings to a regular expression.
$to_replace = array(
// Replace newlines with a logical 'or'.
'/(\r\n?|\n)/',
// Quote asterisks.
'/\\\\\*/',
// Quote <front> keyword.
'/(^|\|)\\\\<front\\\\>($|\|)/',
);
$replacements = array(
'|',
'.*',
'\1' . preg_quote($this->getFrontPagePath(), '/') . '\2',
);
$patterns_quoted = preg_quote($patterns, '/');
$this->regexes[$patterns] = '/^(' . preg_replace($to_replace, $replacements, $patterns_quoted) . ')$/';
}
return (bool) preg_match($this->regexes[$patterns], $path);
}
/**
* {@inheritdoc}
*/
public function isFrontPage() {
// Cache the result as this is called often.
if (!isset($this->isCurrentFrontPage)) {
$this->isCurrentFrontPage = FALSE;
// Ensure that the code can also be executed when there is no active
// route match, like on exception responses.
if ($this->routeMatch->getRouteName()) {
$url = Url::fromRouteMatch($this->routeMatch);
$this->isCurrentFrontPage = ($url->getRouteName() && '/' . $url->getInternalPath() === $this->getFrontPagePath());
}
}
return $this->isCurrentFrontPage;
}
/**
* Gets the current front page path.
*
* @return string
* The front page path.
*/
protected function getFrontPagePath() {
// Lazy-load front page config.
if (!isset($this->frontPage)) {
$this->frontPage = $this->configFactory->get('system.site')
->get('page.front');
}
return $this->frontPage;
}
}

View file

@ -0,0 +1,36 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\PathMatcherInterface.
*/
namespace Drupal\Core\Path;
/**
* Provides an interface for URL path matchers.
*/
interface PathMatcherInterface {
/**
* Checks if a path matches any pattern in a set of patterns.
*
* @param string $path
* The path to match.
* @param string $patterns
* A set of patterns separated by a newline.
*
* @return bool
* TRUE if the path matches a pattern, FALSE otherwise.
*/
public function matchPath($path, $patterns);
/**
* Checks if the current page is the front page.
*
* @return bool
* TRUE if the current page is the front page.
*/
public function isFrontPage();
}

View file

@ -0,0 +1,175 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\PathValidator.
*/
namespace Drupal\Core\Path;
use Drupal\Component\Utility\UrlHelper;
use Drupal\Core\ParamConverter\ParamNotConvertedException;
use Drupal\Core\PathProcessor\InboundPathProcessorInterface;
use Drupal\Core\Routing\AccessAwareRouterInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Symfony\Cmf\Component\Routing\RouteObjectInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Routing\Exception\ResourceNotFoundException;
use Symfony\Component\Routing\Matcher\UrlMatcherInterface;
/**
* Provides a default path validator and access checker.
*/
class PathValidator implements PathValidatorInterface {
/**
* The access aware router.
*
* @var \Drupal\Core\Routing\AccessAwareRouterInterface
*/
protected $accessAwareRouter;
/**
* A router implementation which does not check access.
*
* @var \Symfony\Component\Routing\Matcher\UrlMatcherInterface
*/
protected $accessUnawareRouter;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The path processor.
*
* @var \Drupal\Core\PathProcessor\InboundPathProcessorInterface
*/
protected $pathProcessor;
/**
* Creates a new PathValidator.
*
* @param \Drupal\Core\Routing\AccessAwareRouterInterface $access_aware_router
* The access aware router.
* @param \Symfony\Component\Routing\Matcher\UrlMatcherInterface $access_unaware_router
* A router implementation which does not check access.
* @param \Drupal\Core\Session\AccountInterface $account
* The current user.
* @param \Drupal\Core\PathProcessor\InboundPathProcessorInterface $path_processor
* The path processor;
*/
public function __construct(AccessAwareRouterInterface $access_aware_router, UrlMatcherInterface $access_unaware_router, AccountInterface $account, InboundPathProcessorInterface $path_processor) {
$this->accessAwareRouter = $access_aware_router;
$this->accessUnawareRouter = $access_unaware_router;
$this->account = $account;
$this->pathProcessor = $path_processor;
}
/**
* {@inheritdoc}
*/
public function isValid($path) {
return (bool) $this->getUrlIfValid($path);
}
/**
* {@inheritdoc}
*/
public function getUrlIfValid($path) {
return $this->getUrl($path, TRUE);
}
/**
* {@inheritdoc}
*/
public function getUrlIfValidWithoutAccessCheck($path) {
return $this->getUrl($path, FALSE);
}
/**
* Helper for getUrlIfValid() and getUrlIfValidWithoutAccessCheck().
*/
protected function getUrl($path, $access_check) {
$path = ltrim($path, '/');
$parsed_url = UrlHelper::parse($path);
$options = [];
if (!empty($parsed_url['query'])) {
$options['query'] = $parsed_url['query'];
}
if (!empty($parsed_url['fragment'])) {
$options['fragment'] = $parsed_url['fragment'];
}
if ($parsed_url['path'] == '<front>') {
return new Url('<front>', [], $options);
}
elseif ($parsed_url['path'] == '<none>') {
return new Url('<none>', [], $options);
}
elseif (UrlHelper::isExternal($path) && UrlHelper::isValid($path)) {
if (empty($parsed_url['path'])) {
return FALSE;
}
return Url::fromUri($path);
}
$request = Request::create('/' . $path);
$attributes = $this->getPathAttributes($path, $request, $access_check);
if (!$attributes) {
return FALSE;
}
$route_name = $attributes[RouteObjectInterface::ROUTE_NAME];
$route_parameters = $attributes['_raw_variables']->all();
return new Url($route_name, $route_parameters, $options + ['query' => $request->query->all()]);
}
/**
* Gets the matched attributes for a given path.
*
* @param string $path
* The path to check.
* @param \Symfony\Component\HttpFoundation\Request $request
* A request object with the given path.
* @param bool $access_check
* If FALSE then skip access check and check only whether the path is
* valid.
*
* @return array|bool
* An array of request attributes of FALSE if an exception was thrown.
*/
protected function getPathAttributes($path, Request $request, $access_check) {
if (!$access_check || $this->account->hasPermission('link to any page')) {
$router = $this->accessUnawareRouter;
}
else {
$router = $this->accessAwareRouter;
}
$path = $this->pathProcessor->processInbound('/' . $path, $request);
try {
return $router->match($path);
}
catch (ResourceNotFoundException $e) {
return FALSE;
}
catch (ParamNotConvertedException $e) {
return FALSE;
}
catch (AccessDeniedHttpException $e) {
return FALSE;
}
}
}

View file

@ -0,0 +1,51 @@
<?php
/**
* @file
* Contains \Drupal\Core\Path\PathValidatorInterface.
*/
namespace Drupal\Core\Path;
/**
* Provides an interface for url path validators.
*/
interface PathValidatorInterface {
/**
* Returns an URL object, if the path is valid and accessible.
*
* @param string $path
* The path to check.
*
* @return \Drupal\Core\Url|false
* The url object, or FALSE if the path is not valid.
*/
public function getUrlIfValid($path);
/**
* Returns an URL object, if the path is valid.
*
* Unlike getUrlIfValid(), access check is not performed. Do not use this
* method if the $path is about to be presented to a user.
*
* @param string $path
* The path to check.
*
* @return \Drupal\Core\Url|false
* The url object, or FALSE if the path is not valid.
*/
public function getUrlIfValidWithoutAccessCheck($path);
/**
* Checks if the URL path is valid and accessible by the current user.
*
* @param string $path
* The path to check.
*
* @return bool
* TRUE if the path is valid.
*/
public function isValid($path);
}