AliasManager.php 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. <?php
  2. namespace Drupal\Core\Path;
  3. use Drupal\Core\Cache\CacheBackendInterface;
  4. use Drupal\Core\CacheDecorator\CacheDecoratorInterface;
  5. use Drupal\Core\Language\LanguageInterface;
  6. use Drupal\Core\Language\LanguageManagerInterface;
  7. /**
  8. * The default alias manager implementation.
  9. */
  10. class AliasManager implements AliasManagerInterface, CacheDecoratorInterface {
  11. /**
  12. * The alias storage service.
  13. *
  14. * @var \Drupal\Core\Path\AliasStorageInterface
  15. */
  16. protected $storage;
  17. /**
  18. * Cache backend service.
  19. *
  20. * @var \Drupal\Core\Cache\CacheBackendInterface;
  21. */
  22. protected $cache;
  23. /**
  24. * The cache key to use when caching paths.
  25. *
  26. * @var string
  27. */
  28. protected $cacheKey;
  29. /**
  30. * Whether the cache needs to be written.
  31. *
  32. * @var bool
  33. */
  34. protected $cacheNeedsWriting = FALSE;
  35. /**
  36. * Language manager for retrieving the default langcode when none is specified.
  37. *
  38. * @var \Drupal\Core\Language\LanguageManagerInterface
  39. */
  40. protected $languageManager;
  41. /**
  42. * Holds the map of path lookups per language.
  43. *
  44. * @var array
  45. */
  46. protected $lookupMap = [];
  47. /**
  48. * Holds an array of aliases for which no path was found.
  49. *
  50. * @var array
  51. */
  52. protected $noPath = [];
  53. /**
  54. * Holds the array of whitelisted path aliases.
  55. *
  56. * @var \Drupal\Core\Path\AliasWhitelistInterface
  57. */
  58. protected $whitelist;
  59. /**
  60. * Holds an array of paths that have no alias.
  61. *
  62. * @var array
  63. */
  64. protected $noAlias = [];
  65. /**
  66. * Whether preloaded path lookups has already been loaded.
  67. *
  68. * @var array
  69. */
  70. protected $langcodePreloaded = [];
  71. /**
  72. * Holds an array of previously looked up paths for the current request path.
  73. *
  74. * This will only get populated if a cache key has been set, which for example
  75. * happens if the alias manager is used in the context of a request.
  76. *
  77. * @var array
  78. */
  79. protected $preloadedPathLookups = FALSE;
  80. /**
  81. * Constructs an AliasManager.
  82. *
  83. * @param \Drupal\Core\Path\AliasStorageInterface $storage
  84. * The alias storage service.
  85. * @param \Drupal\Core\Path\AliasWhitelistInterface $whitelist
  86. * The whitelist implementation to use.
  87. * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
  88. * The language manager.
  89. * @param \Drupal\Core\Cache\CacheBackendInterface $cache
  90. * Cache backend.
  91. */
  92. public function __construct(AliasStorageInterface $storage, AliasWhitelistInterface $whitelist, LanguageManagerInterface $language_manager, CacheBackendInterface $cache) {
  93. $this->storage = $storage;
  94. $this->languageManager = $language_manager;
  95. $this->whitelist = $whitelist;
  96. $this->cache = $cache;
  97. }
  98. /**
  99. * {@inheritdoc}
  100. */
  101. public function setCacheKey($key) {
  102. // Prefix the cache key to avoid clashes with other caches.
  103. $this->cacheKey = 'preload-paths:' . $key;
  104. }
  105. /**
  106. * {@inheritdoc}
  107. *
  108. * Cache an array of the paths available on each page. We assume that aliases
  109. * will be needed for the majority of these paths during subsequent requests,
  110. * and load them in a single query during path alias lookup.
  111. */
  112. public function writeCache() {
  113. // Check if the paths for this page were loaded from cache in this request
  114. // to avoid writing to cache on every request.
  115. if ($this->cacheNeedsWriting && !empty($this->cacheKey)) {
  116. // Start with the preloaded path lookups, so that cached entries for other
  117. // languages will not be lost.
  118. $path_lookups = $this->preloadedPathLookups ?: [];
  119. foreach ($this->lookupMap as $langcode => $lookups) {
  120. $path_lookups[$langcode] = array_keys($lookups);
  121. if (!empty($this->noAlias[$langcode])) {
  122. $path_lookups[$langcode] = array_merge($path_lookups[$langcode], array_keys($this->noAlias[$langcode]));
  123. }
  124. }
  125. $twenty_four_hours = 60 * 60 * 24;
  126. $this->cache->set($this->cacheKey, $path_lookups, $this->getRequestTime() + $twenty_four_hours);
  127. }
  128. }
  129. /**
  130. * {@inheritdoc}
  131. */
  132. public function getPathByAlias($alias, $langcode = NULL) {
  133. // If no language is explicitly specified we default to the current URL
  134. // language. If we used a language different from the one conveyed by the
  135. // requested URL, we might end up being unable to check if there is a path
  136. // alias matching the URL path.
  137. $langcode = $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
  138. // If we already know that there are no paths for this alias simply return.
  139. if (empty($alias) || !empty($this->noPath[$langcode][$alias])) {
  140. return $alias;
  141. }
  142. // Look for the alias within the cached map.
  143. if (isset($this->lookupMap[$langcode]) && ($path = array_search($alias, $this->lookupMap[$langcode]))) {
  144. return $path;
  145. }
  146. // Look for path in storage.
  147. if ($path = $this->storage->lookupPathSource($alias, $langcode)) {
  148. $this->lookupMap[$langcode][$path] = $alias;
  149. return $path;
  150. }
  151. // We can't record anything into $this->lookupMap because we didn't find any
  152. // paths for this alias. Thus cache to $this->noPath.
  153. $this->noPath[$langcode][$alias] = TRUE;
  154. return $alias;
  155. }
  156. /**
  157. * {@inheritdoc}
  158. */
  159. public function getAliasByPath($path, $langcode = NULL) {
  160. if ($path[0] !== '/') {
  161. throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $path));
  162. }
  163. // If no language is explicitly specified we default to the current URL
  164. // language. If we used a language different from the one conveyed by the
  165. // requested URL, we might end up being unable to check if there is a path
  166. // alias matching the URL path.
  167. $langcode = $langcode ?: $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_URL)->getId();
  168. // Check the path whitelist, if the top-level part before the first /
  169. // is not in the list, then there is no need to do anything further,
  170. // it is not in the database.
  171. if ($path === '/' || !$this->whitelist->get(strtok(trim($path, '/'), '/'))) {
  172. return $path;
  173. }
  174. // During the first call to this method per language, load the expected
  175. // paths for the page from cache.
  176. if (empty($this->langcodePreloaded[$langcode])) {
  177. $this->langcodePreloaded[$langcode] = TRUE;
  178. $this->lookupMap[$langcode] = [];
  179. // Load the cached paths that should be used for preloading. This only
  180. // happens if a cache key has been set.
  181. if ($this->preloadedPathLookups === FALSE) {
  182. $this->preloadedPathLookups = [];
  183. if ($this->cacheKey) {
  184. if ($cached = $this->cache->get($this->cacheKey)) {
  185. $this->preloadedPathLookups = $cached->data;
  186. }
  187. else {
  188. $this->cacheNeedsWriting = TRUE;
  189. }
  190. }
  191. }
  192. // Load paths from cache.
  193. if (!empty($this->preloadedPathLookups[$langcode])) {
  194. $this->lookupMap[$langcode] = $this->storage->preloadPathAlias($this->preloadedPathLookups[$langcode], $langcode);
  195. // Keep a record of paths with no alias to avoid querying twice.
  196. $this->noAlias[$langcode] = array_flip(array_diff_key($this->preloadedPathLookups[$langcode], array_keys($this->lookupMap[$langcode])));
  197. }
  198. }
  199. // If we already know that there are no aliases for this path simply return.
  200. if (!empty($this->noAlias[$langcode][$path])) {
  201. return $path;
  202. }
  203. // If the alias has already been loaded, return it from static cache.
  204. if (isset($this->lookupMap[$langcode][$path])) {
  205. return $this->lookupMap[$langcode][$path];
  206. }
  207. // Try to load alias from storage.
  208. if ($alias = $this->storage->lookupPathAlias($path, $langcode)) {
  209. $this->lookupMap[$langcode][$path] = $alias;
  210. return $alias;
  211. }
  212. // We can't record anything into $this->lookupMap because we didn't find any
  213. // aliases for this path. Thus cache to $this->noAlias.
  214. $this->noAlias[$langcode][$path] = TRUE;
  215. return $path;
  216. }
  217. /**
  218. * {@inheritdoc}
  219. */
  220. public function cacheClear($source = NULL) {
  221. if ($source) {
  222. foreach (array_keys($this->lookupMap) as $lang) {
  223. unset($this->lookupMap[$lang][$source]);
  224. }
  225. }
  226. else {
  227. $this->lookupMap = [];
  228. }
  229. $this->noPath = [];
  230. $this->noAlias = [];
  231. $this->langcodePreloaded = [];
  232. $this->preloadedPathLookups = [];
  233. $this->cache->delete($this->cacheKey);
  234. $this->pathAliasWhitelistRebuild($source);
  235. }
  236. /**
  237. * Rebuild the path alias white list.
  238. *
  239. * @param string $path
  240. * An optional path for which an alias is being inserted.
  241. *
  242. * @return
  243. * An array containing a white list of path aliases.
  244. */
  245. protected function pathAliasWhitelistRebuild($path = NULL) {
  246. // When paths are inserted, only rebuild the whitelist if the path has a top
  247. // level component which is not already in the whitelist.
  248. if (!empty($path)) {
  249. if ($this->whitelist->get(strtok($path, '/'))) {
  250. return;
  251. }
  252. }
  253. $this->whitelist->clear();
  254. }
  255. /**
  256. * Wrapper method for REQUEST_TIME constant.
  257. *
  258. * @return int
  259. */
  260. protected function getRequestTime() {
  261. return defined('REQUEST_TIME') ? REQUEST_TIME : (int) $_SERVER['REQUEST_TIME'];
  262. }
  263. }