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