Version 1
[yaffs-website] / web / core / modules / path / tests / src / Functional / PathAliasTest.php
1 <?php
2
3 namespace Drupal\Tests\path\Functional;
4
5 use Drupal\Component\Utility\Unicode;
6 use Drupal\Core\Cache\Cache;
7 use Drupal\Core\Database\Database;
8
9 /**
10  * Add, edit, delete, and change alias and verify its consistency in the
11  * database.
12  *
13  * @group path
14  */
15 class PathAliasTest extends PathTestBase {
16
17   /**
18    * Modules to enable.
19    *
20    * @var array
21    */
22   public static $modules = ['path'];
23
24   protected function setUp() {
25     parent::setUp();
26
27     // Create test user and log in.
28     $web_user = $this->drupalCreateUser(['create page content', 'edit own page content', 'administer url aliases', 'create url aliases']);
29     $this->drupalLogin($web_user);
30   }
31
32   /**
33    * Tests the path cache.
34    */
35   public function testPathCache() {
36     // Create test node.
37     $node1 = $this->drupalCreateNode();
38
39     // Create alias.
40     $edit = [];
41     $edit['source'] = '/node/' . $node1->id();
42     $edit['alias'] = '/' . $this->randomMachineName(8);
43     $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
44
45     // Check the path alias whitelist cache.
46     $whitelist = \Drupal::cache('bootstrap')->get('path_alias_whitelist');
47     $this->assertTrue($whitelist->data['node']);
48     $this->assertFalse($whitelist->data['admin']);
49
50     // Visit the system path for the node and confirm a cache entry is
51     // created.
52     \Drupal::cache('data')->deleteAll();
53     // Make sure the path is not converted to the alias.
54     $this->drupalGet(trim($edit['source'], '/'), ['alias' => TRUE]);
55     $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), 'Cache entry was created.');
56
57     // Visit the alias for the node and confirm a cache entry is created.
58     \Drupal::cache('data')->deleteAll();
59     // @todo Remove this once https://www.drupal.org/node/2480077 lands.
60     Cache::invalidateTags(['rendered']);
61     $this->drupalGet(trim($edit['alias'], '/'));
62     $this->assertTrue(\Drupal::cache('data')->get('preload-paths:' . $edit['source']), 'Cache entry was created.');
63   }
64
65   /**
66    * Tests alias functionality through the admin interfaces.
67    */
68   public function testAdminAlias() {
69     // Create test node.
70     $node1 = $this->drupalCreateNode();
71
72     // Create alias.
73     $edit = [];
74     $edit['source'] = '/node/' . $node1->id();
75     $edit['alias'] = '/' . $this->getRandomGenerator()->word(8);
76     $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
77
78     // Confirm that the alias works.
79     $this->drupalGet($edit['alias']);
80     $this->assertText($node1->label(), 'Alias works.');
81     $this->assertResponse(200);
82     // Confirm that the alias works in a case-insensitive way.
83     $this->assertTrue(ctype_lower(ltrim($edit['alias'], '/')));
84     $this->drupalGet($edit['alias']);
85     $this->assertText($node1->label(), 'Alias works lower case.');
86     $this->assertResponse(200);
87     $this->drupalGet(Unicode::strtoupper($edit['alias']));
88     $this->assertText($node1->label(), 'Alias works upper case.');
89     $this->assertResponse(200);
90
91     // Change alias to one containing "exotic" characters.
92     $pid = $this->getPID($edit['alias']);
93
94     $previous = $edit['alias'];
95     $edit['alias'] = '/alias' . // Lower-case letters.
96       // "Special" ASCII characters.
97       "- ._~!$'\"()*@[]?&+%#,;=:" .
98       // Characters that look like a percent-escaped string.
99       "%23%25%26%2B%2F%3F" .
100       // Characters from various non-ASCII alphabets.
101       "中國書۞";
102     $connection = Database::getConnection();
103     if ($connection->databaseType() != 'sqlite') {
104       // When using LIKE for case-insensitivity, the SQLite driver is
105       // currently unable to find the upper-case versions of non-ASCII
106       // characters.
107       // @todo fix this in https://www.drupal.org/node/2607432
108       $edit['alias'] .= "ïвβéø";
109     }
110     $this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save'));
111
112     // Confirm that the alias works.
113     $this->drupalGet(Unicode::strtoupper($edit['alias']));
114     $this->assertText($node1->label(), 'Changed alias works.');
115     $this->assertResponse(200);
116
117     $this->container->get('path.alias_manager')->cacheClear();
118     // Confirm that previous alias no longer works.
119     $this->drupalGet($previous);
120     $this->assertNoText($node1->label(), 'Previous alias no longer works.');
121     $this->assertResponse(404);
122
123     // Create second test node.
124     $node2 = $this->drupalCreateNode();
125
126     // Set alias to second test node.
127     $edit['source'] = '/node/' . $node2->id();
128     // leave $edit['alias'] the same
129     $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
130
131     // Confirm no duplicate was created.
132     $this->assertRaw(t('The alias %alias is already in use in this language.', ['%alias' => $edit['alias']]), 'Attempt to move alias was rejected.');
133
134     $edit_upper = $edit;
135     $edit_upper['alias'] = Unicode::strtoupper($edit['alias']);
136     $this->drupalPostForm('admin/config/search/path/add', $edit_upper, t('Save'));
137     $this->assertRaw(t('The alias %alias could not be added because it is already in use in this language with different capitalization: %stored_alias.', [
138       '%alias' => $edit_upper['alias'],
139       '%stored_alias' => $edit['alias'],
140     ]), 'Attempt to move upper-case alias was rejected.');
141
142     // Delete alias.
143     $this->drupalGet('admin/config/search/path/edit/' . $pid);
144     $this->clickLink(t('Delete'));
145     $this->assertRaw(t('Are you sure you want to delete path alias %name?', ['%name' => $edit['alias']]));
146     $this->drupalPostForm(NULL, [], t('Confirm'));
147
148     // Confirm that the alias no longer works.
149     $this->drupalGet($edit['alias']);
150     $this->assertNoText($node1->label(), 'Alias was successfully deleted.');
151     $this->assertResponse(404);
152
153     // Create a really long alias.
154     $edit = [];
155     $edit['source'] = '/node/' . $node1->id();
156     $alias = '/' . $this->randomMachineName(128);
157     $edit['alias'] = $alias;
158     // The alias is shortened to 50 characters counting the ellipsis.
159     $truncated_alias = substr($alias, 0, 47);
160     $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
161     $this->assertNoText($alias, 'The untruncated alias was not found.');
162     // The 'truncated' alias will always be found.
163     $this->assertText($truncated_alias, 'The truncated alias was found.');
164
165     // Create third test node.
166     $node3 = $this->drupalCreateNode();
167
168     // Create absolute path alias.
169     $edit = [];
170     $edit['source'] = '/node/' . $node3->id();
171     $node3_alias = '/' . $this->randomMachineName(8);
172     $edit['alias'] = $node3_alias;
173     $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
174
175     // Create fourth test node.
176     $node4 = $this->drupalCreateNode();
177
178     // Create alias with trailing slash.
179     $edit = [];
180     $edit['source'] = '/node/' . $node4->id();
181     $node4_alias = '/' . $this->randomMachineName(8);
182     $edit['alias'] = $node4_alias . '/';
183     $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
184
185     // Confirm that the alias with trailing slash is not found.
186     $this->assertNoText($edit['alias'], 'The absolute alias was not found.');
187     // The alias without trailing flash is found.
188     $this->assertText(trim($edit['alias'], '/'), 'The alias without trailing slash was found.');
189
190     // Update an existing alias to point to a different source.
191     $pid = $this->getPID($node4_alias);
192     $edit = [];
193     $edit['alias'] = $node4_alias;
194     $edit['source'] = '/node/' . $node2->id();
195     $this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save'));
196     $this->assertText('The alias has been saved.');
197     $this->drupalGet($edit['alias']);
198     $this->assertNoText($node4->label(), 'Previous alias no longer works.');
199     $this->assertText($node2->label(), 'Alias works.');
200     $this->assertResponse(200);
201
202     // Update an existing alias to use a duplicate alias.
203     $pid = $this->getPID($node3_alias);
204     $edit = [];
205     $edit['alias'] = $node4_alias;
206     $edit['source'] = '/node/' . $node3->id();
207     $this->drupalPostForm('admin/config/search/path/edit/' . $pid, $edit, t('Save'));
208     $this->assertRaw(t('The alias %alias is already in use in this language.', ['%alias' => $edit['alias']]));
209
210     // Create an alias without a starting slash.
211     $node5 = $this->drupalCreateNode();
212
213     $edit = [];
214     $edit['source'] = 'node/' . $node5->id();
215     $node5_alias = $this->randomMachineName(8);
216     $edit['alias'] = $node5_alias . '/';
217     $this->drupalPostForm('admin/config/search/path/add', $edit, t('Save'));
218
219     $this->assertUrl('admin/config/search/path/add');
220     $this->assertText('The source path has to start with a slash.');
221     $this->assertText('The alias path has to start with a slash.');
222   }
223
224   /**
225    * Tests alias functionality through the node interfaces.
226    */
227   public function testNodeAlias() {
228     // Create test node.
229     $node1 = $this->drupalCreateNode();
230
231     // Create alias.
232     $edit = [];
233     $edit['path[0][alias]'] = '/' . $this->randomMachineName(8);
234     $this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save'));
235
236     // Confirm that the alias works.
237     $this->drupalGet($edit['path[0][alias]']);
238     $this->assertText($node1->label(), 'Alias works.');
239     $this->assertResponse(200);
240
241     // Confirm the 'canonical' and 'shortlink' URLs.
242     $elements = $this->xpath("//link[contains(@rel, 'canonical') and contains(@href, '" . $edit['path[0][alias]'] . "')]");
243     $this->assertTrue(!empty($elements), 'Page contains canonical link URL.');
244     $elements = $this->xpath("//link[contains(@rel, 'shortlink') and contains(@href, 'node/" . $node1->id() . "')]");
245     $this->assertTrue(!empty($elements), 'Page contains shortlink URL.');
246
247     $previous = $edit['path[0][alias]'];
248     // Change alias to one containing "exotic" characters.
249     $edit['path[0][alias]'] = '/alias' . // Lower-case letters.
250       // "Special" ASCII characters.
251       "- ._~!$'\"()*@[]?&+%#,;=:" .
252       // Characters that look like a percent-escaped string.
253       "%23%25%26%2B%2F%3F" .
254       // Characters from various non-ASCII alphabets.
255       "中國書۞";
256     $connection = Database::getConnection();
257     if ($connection->databaseType() != 'sqlite') {
258       // When using LIKE for case-insensitivity, the SQLite driver is
259       // currently unable to find the upper-case versions of non-ASCII
260       // characters.
261       // @todo fix this in https://www.drupal.org/node/2607432
262       $edit['path[0][alias]'] .= "ïвβéø";
263     }
264     $this->drupalPostForm('node/' . $node1->id() . '/edit', $edit, t('Save'));
265
266     // Confirm that the alias works.
267     $this->drupalGet(Unicode::strtoupper($edit['path[0][alias]']));
268     $this->assertText($node1->label(), 'Changed alias works.');
269     $this->assertResponse(200);
270
271     // Make sure that previous alias no longer works.
272     $this->drupalGet($previous);
273     $this->assertNoText($node1->label(), 'Previous alias no longer works.');
274     $this->assertResponse(404);
275
276     // Create second test node.
277     $node2 = $this->drupalCreateNode();
278
279     // Set alias to second test node.
280     // Leave $edit['path[0][alias]'] the same.
281     $this->drupalPostForm('node/' . $node2->id() . '/edit', $edit, t('Save'));
282
283     // Confirm that the alias didn't make a duplicate.
284     $this->assertText(t('The alias is already in use.'), 'Attempt to moved alias was rejected.');
285
286     // Delete alias.
287     $this->drupalPostForm('node/' . $node1->id() . '/edit', ['path[0][alias]' => ''], t('Save'));
288
289     // Confirm that the alias no longer works.
290     $this->drupalGet($edit['path[0][alias]']);
291     $this->assertNoText($node1->label(), 'Alias was successfully deleted.');
292     $this->assertResponse(404);
293
294     // Create third test node.
295     $node3 = $this->drupalCreateNode();
296
297     // Set its path alias to an absolute path.
298     $edit = ['path[0][alias]' => '/' . $this->randomMachineName(8)];
299     $this->drupalPostForm('node/' . $node3->id() . '/edit', $edit, t('Save'));
300
301     // Confirm that the alias was converted to a relative path.
302     $this->drupalGet(trim($edit['path[0][alias]'], '/'));
303     $this->assertText($node3->label(), 'Alias became relative.');
304     $this->assertResponse(200);
305
306     // Create fourth test node.
307     $node4 = $this->drupalCreateNode();
308
309     // Set its path alias to have a trailing slash.
310     $edit = ['path[0][alias]' => '/' . $this->randomMachineName(8) . '/'];
311     $this->drupalPostForm('node/' . $node4->id() . '/edit', $edit, t('Save'));
312
313     // Confirm that the alias was converted to a relative path.
314     $this->drupalGet(trim($edit['path[0][alias]'], '/'));
315     $this->assertText($node4->label(), 'Alias trimmed trailing slash.');
316     $this->assertResponse(200);
317
318     // Create fifth test node.
319     $node5 = $this->drupalCreateNode();
320
321     // Set a path alias.
322     $edit = ['path[0][alias]' => '/' . $this->randomMachineName(8)];
323     $this->drupalPostForm('node/' . $node5->id() . '/edit', $edit, t('Save'));
324
325     // Delete the node and check that the path alias is also deleted.
326     $node5->delete();
327     $path_alias = \Drupal::service('path.alias_storage')->lookupPathAlias('/node/' . $node5->id(), $node5->language()->getId());
328     $this->assertFalse($path_alias, 'Alias was successfully deleted when the referenced node was deleted.');
329   }
330
331   /**
332    * Returns the path ID.
333    *
334    * @param string $alias
335    *   A string containing an aliased path.
336    *
337    * @return int
338    *   Integer representing the path ID.
339    */
340   public function getPID($alias) {
341     return db_query("SELECT pid FROM {url_alias} WHERE alias = :alias", [':alias' => $alias])->fetchField();
342   }
343
344   /**
345    * Tests that duplicate aliases fail validation.
346    */
347   public function testDuplicateNodeAlias() {
348     // Create one node with a random alias.
349     $node_one = $this->drupalCreateNode();
350     $edit = [];
351     $edit['path[0][alias]'] = '/' . $this->randomMachineName();
352     $this->drupalPostForm('node/' . $node_one->id() . '/edit', $edit, t('Save'));
353
354     // Now create another node and try to set the same alias.
355     $node_two = $this->drupalCreateNode();
356     $this->drupalPostForm('node/' . $node_two->id() . '/edit', $edit, t('Save'));
357     $this->assertText(t('The alias is already in use.'));
358     $this->assertFieldByXPath("//input[@name='path[0][alias]' and contains(@class, 'error')]", $edit['path[0][alias]'], 'Textfield exists and has the error class.');
359
360     // Behavior here differs with the inline_form_errors module enabled.
361     // Enable the inline_form_errors module and try this again. This module
362     // improves validation with a link in the error message(s) to the fields
363     // which have invalid input.
364     $this->assertTrue($this->container->get('module_installer')->install(['inline_form_errors'], TRUE), 'Installed inline_form_errors.');
365     // Attempt to edit the second node again, as before.
366     $this->drupalPostForm('node/' . $node_two->id() . '/edit', $edit, t('Preview'));
367     // This error should still be present next to the field.
368     $this->assertSession()->pageTextContains(t('The alias is already in use.'), 'Field error found with expected text.');
369     // The validation error set for the page should include this text.
370     $this->assertSession()->pageTextContains(t('1 error has been found: URL alias'), 'Form error found with expected text.');
371     // The text 'URL alias' should be a link.
372     $this->assertSession()->linkExists('URL alias');
373     // The link should be to the ID of the URL alias field.
374     $this->assertSession()->linkByHrefExists('#edit-path-0-alias');
375   }
376
377 }