summaryrefslogtreecommitdiffstats
path: root/core/lib/Drupal/Core/Path/AliasManager.php
blob: 47bf77bfddb28302f21c45088eacc6cb33914764 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
<?php

/**
 * @file
 * Contains Drupal\Core\Path\AliasManager.
 */

namespace Drupal\Core\Path;

use Drupal\Core\Database\Connection;
use Drupal\Core\KeyValueStore\KeyValueFactory;

class AliasManager implements AliasManagerInterface {

  /**
   * The database connectino to use for path lookups.
   *
   * @var \Drupal\Core\Database\Connection
   */
  protected $connection;

  /**
   * The Key/Value Store to use for state
   *
   * @var \Drupal\Core\KeyValueStore\DatabaseStorage
   */
  protected $state;

  /**
   * The default langcode to use when none is specified for path lookups.
   *
   * @var string
   */
  protected $langcode;

  /**
   * Holds the map of path lookups per language.
   *
   * @var array
   */
  protected $lookupMap = array();

  /**
   * Holds an array of path alias for which no source was found.
   *
   * @var array
   */
  protected $noSource = array();

  /**
   * Holds the array of whitelisted path aliases.
   *
   * @var array
   */
  protected $whitelist;

  /**
   * Holds an array of system paths that have no aliases.
   *
   * @var array
   */
  protected $noAliases = array();

  /**
   * Whether lookupPath() has not yet been called.
   *
   * @var boolean
   */
  protected $firstLookup = TRUE;

  /**
   * Holds an array of previously looked up paths for the current request path.
   *
   * This will only ever get populated if the alias manager is being used in
   * the context of a request.
   *
   * @var array
   */
  protected $preloadedPathLookups = array();

  public function __construct(Connection $connection, KeyValueFactory $keyvalue) {
    $this->connection = $connection;
    $this->state = $keyvalue->get('state');
    $this->langcode = language(LANGUAGE_TYPE_URL)->langcode;
    $this->whitelist = $this->state->get('system.path_alias_whitelist', NULL);
    if (!isset($this->whitelist)) {
      $this->whitelist = $this->pathAliasWhitelistRebuild();
    }
  }

  /**
   * Implements \Drupal\Core\Path\AliasManagerInterface::getSystemPath().
   */
  public function getSystemPath($path, $path_language = 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.
    $path_language = $path_language ?: $this->langcode;
    $original_path = $path;
    // Lookup the path alias first.
    if (!empty($path) && $source = $this->lookupPathSource($path, $path_language)) {
      $path = $source;
    }

    return $path;
  }

  /**
   * Implements \Drupal\Core\Path\AliasManagerInterface::getPathAlias().
   */
  public function getPathAlias($path, $path_language = 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.
    $path_language = $path_language ?: $this->langcode;
    $result = $path;
    if (!empty($path) && $alias = $this->lookupPathAlias($path, $path_language)) {
      $result = $alias;
    }
    return $result;
  }

  /**
   * Implements \Drupal\Core\Path\AliasManagerInterface::cacheClear().
   */
  public function cacheClear($source = NULL) {
    $this->lookupMap = array();
    $this->noSource = array();
    $this->no_aliases = array();
    $this->firstCall = TRUE;
    $this->preloadedPathLookups = array();
    $this->whitelist = $this->pathAliasWhitelistRebuild($source);
  }

  /**
   * Implements \Drupal\Core\Path\AliasManagerInterface::getPathLookups().
   */
  public function getPathLookups() {
    $current = current($this->lookupMap);
    if ($current) {
      return array_keys($current);
    }
    return array();
  }

  /**
   * Implements \Drupal\Core\Path\AliasManagerInterface::preloadPathLookups().
   */
  public function preloadPathLookups(array $path_list) {
    $this->preloadedPathLookups = $path_list;
  }

  /**
   * Given a Drupal system URL return one of its aliases if such a one exists.
   * Otherwise, return FALSE.

   * @param $path
   *   The path to investigate for corresponding aliases.
   * @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
   *   An aliased path, or FALSE if no path was found.
   */
  protected function lookupPathAlias($path, $langcode) {
    // During the first call to this method per language, load the expected
    // system paths for the page from cache.
    if (!empty($this->firstLookup)) {
      $this->firstLookup = FALSE;
      $this->lookupMap[$langcode] = array();
      // Load system paths from cache.
      if (!empty($this->preloadedPathLookups)) {
        // Now fetch the aliases corresponding to these system paths.
        $args = array(
          ':system' => $this->preloadedPathLookups,
          ':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 = $this->connection->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 = $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);
        }
        $this->lookupMap[$langcode] = $result->fetchAllKeyed();
        // Keep a record of paths with no alias to avoid querying twice.
        $this->noAliases[$langcode] = array_flip(array_diff_key($this->preloadedPathLookups, array_keys($this->lookupMap[$langcode])));
      }
    }
    // If the alias has already been loaded, return it.
    if (isset($this->lookupMap[$langcode][$path])) {
      return $this->lookupMap[$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($this->whitelist[strtok($path, '/')])) {
      return FALSE;
    }
    // For system paths which were not cached, query aliases individually.
    elseif (!isset($this->noAliases[$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 = $this->connection->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 = $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();
      }
      $this->lookupMap[$langcode][$path] = $alias;
      return $alias;
    }
    return FALSE;
  }

  /**
   * Given an alias, return its Drupal system URL if one exists. Otherwise,
   * return FALSE.
   *
   * @param $path
   *   The path to investigate for corresponding 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
   *   A Drupal system path, or FALSE if no path was found.
   */
  protected function lookupPathSource($path, $langcode) {
    if ($this->whitelist && !isset($this->noSource[$langcode][$path])) {
      // Look for the value $path within the cached $map
      $source = FALSE;
      if (!isset($this->lookupMap[$langcode]) || !($source = array_search($path, $this->lookupMap[$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 = $this->connection->query("SELECT source FROM {url_alias} WHERE alias = :alias AND langcode = :langcode_undetermined ORDER BY pid DESC", $args);
        }
        elseif ($langcode > LANGUAGE_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);
        }
        if ($source = $result->fetchField()) {
          $this->lookupMap[$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.
          $this->noSource[$langcode][$path] = TRUE;
        }
      }
      return $source;
    }
    return FALSE;
  }

  /**
   * 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.
   */
  protected function pathAliasWhitelistRebuild($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)) {
      // @todo Inject state so we don't have this function call.
      $whitelist = $this->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 = $this->connection->query("SELECT DISTINCT SUBSTRING_INDEX(source, '/', 1) AS path FROM {url_alias}");
    foreach ($result as $row) {
      $whitelist[$row->path] = TRUE;
    }
    $this->state->set('system.path_alias_whitelist', $whitelist);
    return $whitelist;
  }
}