Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / lib / Drupal / Core / Path / AliasStorage.php
1 <?php
2
3 namespace Drupal\Core\Path;
4
5 use Drupal\Core\Cache\Cache;
6 use Drupal\Core\Database\Connection;
7 use Drupal\Core\Database\SchemaObjectExistsException;
8 use Drupal\Core\Extension\ModuleHandlerInterface;
9 use Drupal\Core\Language\LanguageInterface;
10 use Drupal\Core\Database\Query\Condition;
11
12 /**
13  * Provides a class for CRUD operations on path aliases.
14  *
15  * All queries perform case-insensitive matching on the 'source' and 'alias'
16  * fields, so the aliases '/test-alias' and '/test-Alias' are considered to be
17  * the same, and will both refer to the same internal system path.
18  */
19 class AliasStorage implements AliasStorageInterface {
20
21   /**
22    * The table for the url_alias storage.
23    */
24   const TABLE = 'url_alias';
25
26   /**
27    * The database connection.
28    *
29    * @var \Drupal\Core\Database\Connection
30    */
31   protected $connection;
32
33   /**
34    * The module handler.
35    *
36    * @var \Drupal\Core\Extension\ModuleHandlerInterface
37    */
38   protected $moduleHandler;
39
40   /**
41    * Constructs a Path CRUD object.
42    *
43    * @param \Drupal\Core\Database\Connection $connection
44    *   A database connection for reading and writing path aliases.
45    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
46    *   The module handler.
47    */
48   public function __construct(Connection $connection, ModuleHandlerInterface $module_handler) {
49     $this->connection = $connection;
50     $this->moduleHandler = $module_handler;
51   }
52
53   /**
54    * {@inheritdoc}
55    */
56   public function save($source, $alias, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, $pid = NULL) {
57
58     if ($source[0] !== '/') {
59       throw new \InvalidArgumentException(sprintf('Source path %s has to start with a slash.', $source));
60     }
61
62     if ($alias[0] !== '/') {
63       throw new \InvalidArgumentException(sprintf('Alias path %s has to start with a slash.', $alias));
64     }
65
66     $fields = [
67       'source' => $source,
68       'alias' => $alias,
69       'langcode' => $langcode,
70     ];
71
72     // Insert or update the alias.
73     if (empty($pid)) {
74       $try_again = FALSE;
75       try {
76         $query = $this->connection->insert(static::TABLE)
77           ->fields($fields);
78         $pid = $query->execute();
79       }
80       catch (\Exception $e) {
81         // If there was an exception, try to create the table.
82         if (!$try_again = $this->ensureTableExists()) {
83           // If the exception happened for other reason than the missing table,
84           // propagate the exception.
85           throw $e;
86         }
87       }
88       // Now that the table has been created, try again if necessary.
89       if ($try_again) {
90         $query = $this->connection->insert(static::TABLE)
91           ->fields($fields);
92         $pid = $query->execute();
93       }
94
95       $fields['pid'] = $pid;
96       $operation = 'insert';
97     }
98     else {
99       // Fetch the current values so that an update hook can identify what
100       // exactly changed.
101       try {
102         $original = $this->connection->query('SELECT source, alias, langcode FROM {url_alias} WHERE pid = :pid', [':pid' => $pid])
103           ->fetchAssoc();
104       }
105       catch (\Exception $e) {
106         $this->catchException($e);
107         $original = FALSE;
108       }
109       $fields['pid'] = $pid;
110       $query = $this->connection->update(static::TABLE)
111         ->fields($fields)
112         ->condition('pid', $pid);
113       $pid = $query->execute();
114       $fields['original'] = $original;
115       $operation = 'update';
116     }
117     if ($pid) {
118       // @todo Switch to using an event for this instead of a hook.
119       $this->moduleHandler->invokeAll('path_' . $operation, [$fields]);
120       Cache::invalidateTags(['route_match']);
121       return $fields;
122     }
123     return FALSE;
124   }
125
126   /**
127    * {@inheritdoc}
128    */
129   public function load($conditions) {
130     $select = $this->connection->select(static::TABLE);
131     foreach ($conditions as $field => $value) {
132       if ($field == 'source' || $field == 'alias') {
133         // Use LIKE for case-insensitive matching.
134         $select->condition($field, $this->connection->escapeLike($value), 'LIKE');
135       }
136       else {
137         $select->condition($field, $value);
138       }
139     }
140     try {
141       return $select
142         ->fields(static::TABLE)
143         ->orderBy('pid', 'DESC')
144         ->range(0, 1)
145         ->execute()
146         ->fetchAssoc();
147     }
148     catch (\Exception $e) {
149       $this->catchException($e);
150       return FALSE;
151     }
152   }
153
154   /**
155    * {@inheritdoc}
156    */
157   public function delete($conditions) {
158     $path = $this->load($conditions);
159     $query = $this->connection->delete(static::TABLE);
160     foreach ($conditions as $field => $value) {
161       if ($field == 'source' || $field == 'alias') {
162         // Use LIKE for case-insensitive matching.
163         $query->condition($field, $this->connection->escapeLike($value), 'LIKE');
164       }
165       else {
166         $query->condition($field, $value);
167       }
168     }
169     try {
170       $deleted = $query->execute();
171     }
172     catch (\Exception $e) {
173       $this->catchException($e);
174       $deleted = FALSE;
175     }
176     // @todo Switch to using an event for this instead of a hook.
177     $this->moduleHandler->invokeAll('path_delete', [$path]);
178     Cache::invalidateTags(['route_match']);
179     return $deleted;
180   }
181
182   /**
183    * {@inheritdoc}
184    */
185   public function preloadPathAlias($preloaded, $langcode) {
186     $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
187     $select = $this->connection->select(static::TABLE)
188       ->fields(static::TABLE, ['source', 'alias']);
189
190     if (!empty($preloaded)) {
191       $conditions = new Condition('OR');
192       foreach ($preloaded as $preloaded_item) {
193         $conditions->condition('source', $this->connection->escapeLike($preloaded_item), 'LIKE');
194       }
195       $select->condition($conditions);
196     }
197
198     // Always get the language-specific alias before the language-neutral one.
199     // For example 'de' is less than 'und' so the order needs to be ASC, while
200     // 'xx-lolspeak' is more than 'und' so the order needs to be DESC. We also
201     // order by pid ASC so that fetchAllKeyed() returns the most recently
202     // created alias for each source. Subsequent queries using fetchField() must
203     // use pid DESC to have the same effect.
204     if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
205       array_pop($langcode_list);
206     }
207     elseif ($langcode < LanguageInterface::LANGCODE_NOT_SPECIFIED) {
208       $select->orderBy('langcode', 'ASC');
209     }
210     else {
211       $select->orderBy('langcode', 'DESC');
212     }
213
214     $select->orderBy('pid', 'ASC');
215     $select->condition('langcode', $langcode_list, 'IN');
216     try {
217       return $select->execute()->fetchAllKeyed();
218     }
219     catch (\Exception $e) {
220       $this->catchException($e);
221       return FALSE;
222     }
223   }
224
225   /**
226    * {@inheritdoc}
227    */
228   public function lookupPathAlias($path, $langcode) {
229     $source = $this->connection->escapeLike($path);
230     $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
231
232     // See the queries above. Use LIKE for case-insensitive matching.
233     $select = $this->connection->select(static::TABLE)
234       ->fields(static::TABLE, ['alias'])
235       ->condition('source', $source, 'LIKE');
236     if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
237       array_pop($langcode_list);
238     }
239     elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
240       $select->orderBy('langcode', 'DESC');
241     }
242     else {
243       $select->orderBy('langcode', 'ASC');
244     }
245
246     $select->orderBy('pid', 'DESC');
247     $select->condition('langcode', $langcode_list, 'IN');
248     try {
249       return $select->execute()->fetchField();
250     }
251     catch (\Exception $e) {
252       $this->catchException($e);
253       return FALSE;
254     }
255   }
256
257   /**
258    * {@inheritdoc}
259    */
260   public function lookupPathSource($path, $langcode) {
261     $alias = $this->connection->escapeLike($path);
262     $langcode_list = [$langcode, LanguageInterface::LANGCODE_NOT_SPECIFIED];
263
264     // See the queries above. Use LIKE for case-insensitive matching.
265     $select = $this->connection->select(static::TABLE)
266       ->fields(static::TABLE, ['source'])
267       ->condition('alias', $alias, 'LIKE');
268     if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED) {
269       array_pop($langcode_list);
270     }
271     elseif ($langcode > LanguageInterface::LANGCODE_NOT_SPECIFIED) {
272       $select->orderBy('langcode', 'DESC');
273     }
274     else {
275       $select->orderBy('langcode', 'ASC');
276     }
277
278     $select->orderBy('pid', 'DESC');
279     $select->condition('langcode', $langcode_list, 'IN');
280     try {
281       return $select->execute()->fetchField();
282     }
283     catch (\Exception $e) {
284       $this->catchException($e);
285       return FALSE;
286     }
287   }
288
289   /**
290    * {@inheritdoc}
291    */
292   public function aliasExists($alias, $langcode, $source = NULL) {
293     // Use LIKE and NOT LIKE for case-insensitive matching.
294     $query = $this->connection->select(static::TABLE)
295       ->condition('alias', $this->connection->escapeLike($alias), 'LIKE')
296       ->condition('langcode', $langcode);
297     if (!empty($source)) {
298       $query->condition('source', $this->connection->escapeLike($source), 'NOT LIKE');
299     }
300     $query->addExpression('1');
301     $query->range(0, 1);
302     try {
303       return (bool) $query->execute()->fetchField();
304     }
305     catch (\Exception $e) {
306       $this->catchException($e);
307       return FALSE;
308     }
309   }
310
311   /**
312    * {@inheritdoc}
313    */
314   public function languageAliasExists() {
315     try {
316       return (bool) $this->connection->queryRange('SELECT 1 FROM {url_alias} WHERE langcode <> :langcode', 0, 1, [':langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED])->fetchField();
317     }
318     catch (\Exception $e) {
319       $this->catchException($e);
320       return FALSE;
321     }
322   }
323
324   /**
325    * {@inheritdoc}
326    */
327   public function getAliasesForAdminListing($header, $keys = NULL) {
328     $query = $this->connection->select(static::TABLE)
329       ->extend('Drupal\Core\Database\Query\PagerSelectExtender')
330       ->extend('Drupal\Core\Database\Query\TableSortExtender');
331     if ($keys) {
332       // Replace wildcards with PDO wildcards.
333       $query->condition('alias', '%' . preg_replace('!\*+!', '%', $keys) . '%', 'LIKE');
334     }
335     try {
336       return $query
337         ->fields(static::TABLE)
338         ->orderByHeader($header)
339         ->limit(50)
340         ->execute()
341         ->fetchAll();
342     }
343     catch (\Exception $e) {
344       $this->catchException($e);
345       return [];
346     }
347   }
348
349   /**
350    * {@inheritdoc}
351    */
352   public function pathHasMatchingAlias($initial_substring) {
353     $query = $this->connection->select(static::TABLE, 'u');
354     $query->addExpression(1);
355     try {
356       return (bool) $query
357         ->condition('u.source', $this->connection->escapeLike($initial_substring) . '%', 'LIKE')
358         ->range(0, 1)
359         ->execute()
360         ->fetchField();
361     }
362     catch (\Exception $e) {
363       $this->catchException($e);
364       return FALSE;
365     }
366   }
367
368   /**
369    * Check if the table exists and create it if not.
370    */
371   protected function ensureTableExists() {
372     try {
373       $database_schema = $this->connection->schema();
374       if (!$database_schema->tableExists(static::TABLE)) {
375         $schema_definition = $this->schemaDefinition();
376         $database_schema->createTable(static::TABLE, $schema_definition);
377         return TRUE;
378       }
379     }
380     // If another process has already created the table, attempting to recreate
381     // it will throw an exception. In this case just catch the exception and do
382     // nothing.
383     catch (SchemaObjectExistsException $e) {
384       return TRUE;
385     }
386     return FALSE;
387   }
388
389   /**
390    * Act on an exception when url_alias might be stale.
391    *
392    * If the table does not yet exist, that's fine, but if the table exists and
393    * yet the query failed, then the url_alias is stale and the exception needs
394    * to propagate.
395    *
396    * @param $e
397    *   The exception.
398    *
399    * @throws \Exception
400    */
401   protected function catchException(\Exception $e) {
402     if ($this->connection->schema()->tableExists(static::TABLE)) {
403       throw $e;
404     }
405   }
406
407   /**
408    * Defines the schema for the {url_alias} table.
409    *
410    * @internal
411    */
412   public static function schemaDefinition() {
413     return [
414       'description' => 'A list of URL aliases for Drupal paths; a user may visit either the source or destination path.',
415       'fields' => [
416         'pid' => [
417           'description' => 'A unique path alias identifier.',
418           'type' => 'serial',
419           'unsigned' => TRUE,
420           'not null' => TRUE,
421         ],
422         'source' => [
423           'description' => 'The Drupal path this alias is for; e.g. node/12.',
424           'type' => 'varchar',
425           'length' => 255,
426           'not null' => TRUE,
427           'default' => '',
428         ],
429         'alias' => [
430           'description' => 'The alias for this path; e.g. title-of-the-story.',
431           'type' => 'varchar',
432           'length' => 255,
433           'not null' => TRUE,
434           'default' => '',
435         ],
436         'langcode' => [
437           'description' => "The language code this alias is for; if 'und', the alias will be used for unknown languages. Each Drupal path can have an alias for each supported language.",
438           'type' => 'varchar_ascii',
439           'length' => 12,
440           'not null' => TRUE,
441           'default' => '',
442         ],
443       ],
444       'primary key' => ['pid'],
445       'indexes' => [
446         'alias_langcode_pid' => ['alias', 'langcode', 'pid'],
447         'source_langcode_pid' => ['source', 'langcode', 'pid'],
448       ],
449     ];
450   }
451
452 }