1<?php
2
3namespace Drupal\Tests\path\Functional;
4
5/**
6 * Confirm that paths work with translated nodes.
7 *
8 * @group path
9 */
10class PathLanguageTest extends PathTestBase {
11
12  /**
13   * Modules to enable.
14   *
15   * @var array
16   */
17  protected static $modules = [
18    'path',
19    'locale',
20    'locale_test',
21    'content_translation',
22  ];
23
24  /**
25   * {@inheritdoc}
26   */
27  protected $defaultTheme = 'stark';
28
29  /**
30   * A user with permissions to administer content types.
31   *
32   * @var \Drupal\user\UserInterface
33   */
34  protected $webUser;
35
36  protected function setUp(): void {
37    parent::setUp();
38
39    $permissions = [
40      'access administration pages',
41      'administer content translation',
42      'administer content types',
43      'administer languages',
44      'administer url aliases',
45      'create content translations',
46      'create page content',
47      'create url aliases',
48      'edit any page content',
49      'translate any entity',
50    ];
51    // Create and log in user.
52    $this->webUser = $this->drupalCreateUser($permissions);
53    $this->drupalLogin($this->webUser);
54
55    // Enable French language.
56    $edit = [];
57    $edit['predefined_langcode'] = 'fr';
58
59    $this->drupalGet('admin/config/regional/language/add');
60    $this->submitForm($edit, 'Add language');
61
62    // Enable URL language detection and selection.
63    $edit = ['language_interface[enabled][language-url]' => 1];
64    $this->drupalGet('admin/config/regional/language/detection');
65    $this->submitForm($edit, 'Save settings');
66
67    // Enable translation for page node.
68    $edit = [
69      'entity_types[node]' => 1,
70      'settings[node][page][translatable]' => 1,
71      'settings[node][page][fields][path]' => 1,
72      'settings[node][page][fields][body]' => 1,
73      'settings[node][page][settings][language][language_alterable]' => 1,
74    ];
75    $this->drupalGet('admin/config/regional/content-language');
76    $this->submitForm($edit, 'Save configuration');
77
78    $definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions('node', 'page');
79    $this->assertTrue($definitions['path']->isTranslatable(), 'Node path is translatable.');
80    $this->assertTrue($definitions['body']->isTranslatable(), 'Node body is translatable.');
81  }
82
83  /**
84   * Tests alias functionality through the admin interfaces.
85   */
86  public function testAliasTranslation() {
87    $node_storage = $this->container->get('entity_type.manager')->getStorage('node');
88    $english_node = $this->drupalCreateNode(['type' => 'page', 'langcode' => 'en']);
89    $english_alias = $this->randomMachineName();
90
91    // Edit the node to set language and path.
92    $edit = [];
93    $edit['path[0][alias]'] = '/' . $english_alias;
94    $this->drupalGet('node/' . $english_node->id() . '/edit');
95    $this->submitForm($edit, 'Save');
96
97    // Confirm that the alias works.
98    $this->drupalGet($english_alias);
99    $this->assertSession()->pageTextContains($english_node->body->value);
100
101    // Translate the node into French.
102    $this->drupalGet('node/' . $english_node->id() . '/translations');
103    $this->clickLink('Add');
104
105    $edit = [];
106    $edit['title[0][value]'] = $this->randomMachineName();
107    $edit['body[0][value]'] = $this->randomMachineName();
108    $french_alias = $this->randomMachineName();
109    $edit['path[0][alias]'] = '/' . $french_alias;
110    $this->submitForm($edit, 'Save (this translation)');
111
112    // Clear the path lookup cache.
113    $this->container->get('path_alias.manager')->cacheClear();
114
115    // Languages are cached on many levels, and we need to clear those caches.
116    $this->container->get('language_manager')->reset();
117    $this->rebuildContainer();
118    $languages = $this->container->get('language_manager')->getLanguages();
119
120    // Ensure the node was created.
121    $node_storage->resetCache([$english_node->id()]);
122    $english_node = $node_storage->load($english_node->id());
123    $english_node_french_translation = $english_node->getTranslation('fr');
124    $this->assertTrue($english_node->hasTranslation('fr'), 'Node found in database.');
125
126    // Confirm that the alias works.
127    $this->drupalGet('fr' . $edit['path[0][alias]']);
128    $this->assertSession()->pageTextContains($english_node_french_translation->body->value);
129
130    // Confirm that the alias is returned for the URL. Languages are cached on
131    // many levels, and we need to clear those caches.
132    $this->container->get('language_manager')->reset();
133    $languages = $this->container->get('language_manager')->getLanguages();
134    $url = $english_node_french_translation->toUrl('canonical', ['language' => $languages['fr']])->toString();
135
136    $this->assertStringContainsString($edit['path[0][alias]'], $url, 'URL contains the path alias.');
137
138    // Confirm that the alias works even when changing language negotiation
139    // options. Enable User language detection and selection over URL one.
140    $edit = [
141      'language_interface[enabled][language-user]' => 1,
142      'language_interface[weight][language-user]' => -9,
143      'language_interface[enabled][language-url]' => 1,
144      'language_interface[weight][language-url]' => -8,
145    ];
146    $this->drupalGet('admin/config/regional/language/detection');
147    $this->submitForm($edit, 'Save settings');
148
149    // Change user language preference.
150    $edit = ['preferred_langcode' => 'fr'];
151    $this->drupalGet("user/" . $this->webUser->id() . "/edit");
152    $this->submitForm($edit, 'Save');
153
154    // Check that the English alias works. In this situation French is the
155    // current UI and content language, while URL language is English (since we
156    // do not have a path prefix we fall back to the site's default language).
157    // We need to ensure that the user language preference is not taken into
158    // account while determining the path alias language, because if this
159    // happens we have no way to check that the path alias is valid: there is no
160    // path alias for French matching the english alias. So the alias manager
161    // needs to use the URL language to check whether the alias is valid.
162    $this->drupalGet($english_alias);
163    $this->assertSession()->pageTextContains($english_node_french_translation->body->value);
164
165    // Check that the French alias works.
166    $this->drupalGet("fr/$french_alias");
167    $this->assertSession()->pageTextContains($english_node_french_translation->body->value);
168
169    // Disable URL language negotiation.
170    $edit = ['language_interface[enabled][language-url]' => FALSE];
171    $this->drupalGet('admin/config/regional/language/detection');
172    $this->submitForm($edit, 'Save settings');
173
174    // Check that the English alias still works.
175    $this->drupalGet($english_alias);
176    $this->assertSession()->pageTextContains($english_node_french_translation->body->value);
177
178    // Check that the French alias is not available. We check the unprefixed
179    // alias because we disabled URL language negotiation above. In this
180    // situation only aliases in the default language and language neutral ones
181    // should keep working.
182    $this->drupalGet($french_alias);
183    $this->assertSession()->statusCodeEquals(404);
184
185    // The alias manager has an internal path lookup cache. Check to see that
186    // it has the appropriate contents at this point.
187    $this->container->get('path_alias.manager')->cacheClear();
188    $french_node_path = $this->container->get('path_alias.manager')->getPathByAlias('/' . $french_alias, 'fr');
189    $this->assertEquals('/node/' . $english_node_french_translation->id(), $french_node_path, 'Normal path works.');
190    // Second call should return the same path.
191    $french_node_path = $this->container->get('path_alias.manager')->getPathByAlias('/' . $french_alias, 'fr');
192    $this->assertEquals('/node/' . $english_node_french_translation->id(), $french_node_path, 'Normal path is the same.');
193
194    // Confirm that the alias works.
195    $french_node_alias = $this->container->get('path_alias.manager')->getAliasByPath('/node/' . $english_node_french_translation->id(), 'fr');
196    $this->assertEquals('/' . $french_alias, $french_node_alias, 'Alias works.');
197    // Second call should return the same alias.
198    $french_node_alias = $this->container->get('path_alias.manager')->getAliasByPath('/node/' . $english_node_french_translation->id(), 'fr');
199    $this->assertEquals('/' . $french_alias, $french_node_alias, 'Alias is the same.');
200
201    // Confirm that the alias is removed if the translation is deleted.
202    $english_node->removeTranslation('fr');
203    $english_node->save();
204    $this->assertPathAliasNotExists('/' . $french_alias, 'fr', NULL, 'Alias for French translation is removed when translation is deleted.');
205
206    // Check that the English alias still works.
207    $this->drupalGet($english_alias);
208    $this->assertPathAliasExists('/' . $english_alias, 'en', NULL, 'English alias is not deleted when French translation is removed.');
209    $this->assertSession()->pageTextContains($english_node->body->value);
210  }
211
212}
213