Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / node / tests / src / Functional / NodeTranslationUITest.php
1 <?php
2
3 namespace Drupal\Tests\node\Functional;
4
5 use Drupal\Core\Entity\EntityInterface;
6 use Drupal\Tests\content_translation\Functional\ContentTranslationUITestBase;
7 use Drupal\Core\Language\LanguageInterface;
8 use Drupal\Core\Url;
9 use Drupal\node\Entity\Node;
10 use Drupal\language\Entity\ConfigurableLanguage;
11
12 /**
13  * Tests the Node Translation UI.
14  *
15  * @group node
16  */
17 class NodeTranslationUITest extends ContentTranslationUITestBase {
18
19   /**
20    * {inheritdoc}
21    */
22   protected $defaultCacheContexts = [
23     'languages:language_interface',
24     'session',
25     'theme',
26     'route',
27     'timezone',
28     'url.path.parent',
29     'url.query_args:_wrapper_format',
30     'user'
31   ];
32
33   /**
34    * Modules to enable.
35    *
36    * @var array
37    */
38   public static $modules = ['block', 'language', 'content_translation', 'node', 'datetime', 'field_ui', 'help'];
39
40   /**
41    * The profile to install as a basis for testing.
42    *
43    * @var string
44    */
45   protected $profile = 'standard';
46
47   protected function setUp() {
48     $this->entityTypeId = 'node';
49     $this->bundle = 'article';
50     parent::setUp();
51
52     // Ensure the help message is shown even with prefixed paths.
53     $this->drupalPlaceBlock('help_block', ['region' => 'content']);
54
55     // Display the language selector.
56     $this->drupalLogin($this->administrator);
57     $edit = ['language_configuration[language_alterable]' => TRUE];
58     $this->drupalPostForm('admin/structure/types/manage/article', $edit, t('Save content type'));
59     $this->drupalLogin($this->translator);
60   }
61
62   /**
63    * Tests the basic translation UI.
64    */
65   public function testTranslationUI() {
66     parent::testTranslationUI();
67     $this->doUninstallTest();
68   }
69
70   /**
71    * Tests changing the published status on a node without fields.
72    */
73   public function testPublishedStatusNoFields() {
74     // Test changing the published status of an article without fields.
75     $this->drupalLogin($this->administrator);
76     // Delete all fields.
77     $this->drupalGet('admin/structure/types/manage/article/fields');
78     $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.' . $this->fieldName . '/delete', [], t('Delete'));
79     $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_tags/delete', [], t('Delete'));
80     $this->drupalPostForm('admin/structure/types/manage/article/fields/node.article.field_image/delete', [], t('Delete'));
81
82     // Add a node.
83     $default_langcode = $this->langcodes[0];
84     $values[$default_langcode] = ['title' => [['value' => $this->randomMachineName()]]];
85     $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
86     $storage = $this->container->get('entity_type.manager')
87       ->getStorage($this->entityTypeId);
88     $storage->resetCache([$this->entityId]);
89     $entity = $storage->load($this->entityId);
90
91     // Add a content translation.
92     $langcode = 'fr';
93     $language = ConfigurableLanguage::load($langcode);
94     $values[$langcode] = ['title' => [['value' => $this->randomMachineName()]]];
95
96     $entity_type_id = $entity->getEntityTypeId();
97     $add_url = Url::fromRoute("entity.$entity_type_id.content_translation_add", [
98       $entity->getEntityTypeId() => $entity->id(),
99       'source' => $default_langcode,
100       'target' => $langcode
101     ], ['language' => $language]);
102     $edit = $this->getEditValues($values, $langcode);
103     $edit['status[value]'] = FALSE;
104     $this->drupalPostForm($add_url, $edit, t('Save (this translation)'));
105
106     $storage->resetCache([$this->entityId]);
107     $entity = $storage->load($this->entityId);
108     $translation = $entity->getTranslation($langcode);
109     // Make sure we unpublished the node correctly.
110     $this->assertFalse($this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
111   }
112
113   /**
114    * {@inheritdoc}
115    */
116   protected function getTranslatorPermissions() {
117     return array_merge(parent::getTranslatorPermissions(), ['administer nodes', "edit any $this->bundle content"]);
118   }
119
120   /**
121    * {@inheritdoc}
122    */
123   protected function getEditorPermissions() {
124     return ['administer nodes', 'create article content'];
125   }
126
127   /**
128    * {@inheritdoc}
129    */
130   protected function getAdministratorPermissions() {
131     return array_merge(parent::getAdministratorPermissions(), ['access administration pages', 'administer content types', 'administer node fields', 'access content overview', 'bypass node access', 'administer languages', 'administer themes', 'view the administration theme']);
132   }
133
134   /**
135    * {@inheritdoc}
136    */
137   protected function getNewEntityValues($langcode) {
138     return ['title' => [['value' => $this->randomMachineName()]]] + parent::getNewEntityValues($langcode);
139   }
140
141   /**
142    * {@inheritdoc}
143    */
144   protected function doTestPublishedStatus() {
145     $storage = $this->container->get('entity_type.manager')
146       ->getStorage($this->entityTypeId);
147     $storage->resetCache([$this->entityId]);
148     $entity = $storage->load($this->entityId);
149     $languages = $this->container->get('language_manager')->getLanguages();
150
151     $statuses = [
152       TRUE,
153       FALSE,
154     ];
155
156     foreach ($statuses as $index => $value) {
157       // (Un)publish the node translations and check that the translation
158       // statuses are (un)published accordingly.
159       foreach ($this->langcodes as $langcode) {
160         $options = ['language' => $languages[$langcode]];
161         $url = $entity->urlInfo('edit-form', $options);
162         $this->drupalPostForm($url, ['status[value]' => $value], t('Save') . $this->getFormSubmitSuffix($entity, $langcode), $options);
163       }
164       $storage->resetCache([$this->entityId]);
165       $entity = $storage->load($this->entityId);
166       foreach ($this->langcodes as $langcode) {
167         // The node is created as unpublished thus we switch to the published
168         // status first.
169         $status = !$index;
170         $translation = $entity->getTranslation($langcode);
171         $this->assertEqual($status, $this->manager->getTranslationMetadata($translation)->isPublished(), 'The translation has been correctly unpublished.');
172       }
173     }
174   }
175
176   /**
177    * {@inheritdoc}
178    */
179   protected function doTestAuthoringInfo() {
180     $storage = $this->container->get('entity_type.manager')
181       ->getStorage($this->entityTypeId);
182     $storage->resetCache([$this->entityId]);
183     $entity = $storage->load($this->entityId);
184     $languages = $this->container->get('language_manager')->getLanguages();
185     $values = [];
186
187     // Post different base field information for each translation.
188     foreach ($this->langcodes as $langcode) {
189       $user = $this->drupalCreateUser();
190       $values[$langcode] = [
191         'uid' => $user->id(),
192         'created' => REQUEST_TIME - mt_rand(0, 1000),
193         'sticky' => (bool) mt_rand(0, 1),
194         'promote' => (bool) mt_rand(0, 1),
195       ];
196       $edit = [
197         'uid[0][target_id]' => $user->getUsername(),
198         'created[0][value][date]' => format_date($values[$langcode]['created'], 'custom', 'Y-m-d'),
199         'created[0][value][time]' => format_date($values[$langcode]['created'], 'custom', 'H:i:s'),
200         'sticky[value]' => $values[$langcode]['sticky'],
201         'promote[value]' => $values[$langcode]['promote'],
202       ];
203       $options = ['language' => $languages[$langcode]];
204       $url = $entity->urlInfo('edit-form', $options);
205       $this->drupalPostForm($url, $edit, $this->getFormSubmitAction($entity, $langcode), $options);
206     }
207
208     $storage->resetCache([$this->entityId]);
209     $entity = $storage->load($this->entityId);
210     foreach ($this->langcodes as $langcode) {
211       $translation = $entity->getTranslation($langcode);
212       $metadata = $this->manager->getTranslationMetadata($translation);
213       $this->assertEqual($metadata->getAuthor()->id(), $values[$langcode]['uid'], 'Translation author correctly stored.');
214       $this->assertEqual($metadata->getCreatedTime(), $values[$langcode]['created'], 'Translation date correctly stored.');
215       $this->assertEqual($translation->isSticky(), $values[$langcode]['sticky'], 'Sticky of Translation correctly stored.');
216       $this->assertEqual($translation->isPromoted(), $values[$langcode]['promote'], 'Promoted of Translation correctly stored.');
217     }
218   }
219
220   /**
221    * Tests that translation page inherits admin status of edit page.
222    */
223   public function testTranslationLinkTheme() {
224     $this->drupalLogin($this->administrator);
225     $article = $this->drupalCreateNode(['type' => 'article', 'langcode' => $this->langcodes[0]]);
226
227     // Set up Seven as the admin theme and use it for node editing.
228     $this->container->get('theme_handler')->install(['seven']);
229     $edit = [];
230     $edit['admin_theme'] = 'seven';
231     $edit['use_admin_theme'] = TRUE;
232     $this->drupalPostForm('admin/appearance', $edit, t('Save configuration'));
233     $this->drupalGet('node/' . $article->id() . '/translations');
234     $this->assertRaw('core/themes/seven/css/base/elements.css', 'Translation uses admin theme if edit is admin.');
235
236     // Turn off admin theme for editing, assert inheritance to translations.
237     $edit['use_admin_theme'] = FALSE;
238     $this->drupalPostForm('admin/appearance', $edit, t('Save configuration'));
239     $this->drupalGet('node/' . $article->id() . '/translations');
240     $this->assertNoRaw('core/themes/seven/css/base/elements.css', 'Translation uses frontend theme if edit is frontend.');
241
242     // Assert presence of translation page itself (vs. DisabledBundle below).
243     $this->assertResponse(200);
244   }
245
246   /**
247    * Tests that no metadata is stored for a disabled bundle.
248    */
249   public function testDisabledBundle() {
250     // Create a bundle that does not have translation enabled.
251     $disabledBundle = $this->randomMachineName();
252     $this->drupalCreateContentType(['type' => $disabledBundle, 'name' => $disabledBundle]);
253
254     // Create a node for each bundle.
255     $node = $this->drupalCreateNode([
256       'type' => $this->bundle,
257       'langcode' => LanguageInterface::LANGCODE_NOT_SPECIFIED,
258     ]);
259
260     // Make sure that nothing was inserted into the {content_translation} table.
261     $rows = db_query('SELECT nid, count(nid) AS count FROM {node_field_data} WHERE type <> :type GROUP BY nid HAVING count(nid) >= 2', [':type' => $this->bundle])->fetchAll();
262     $this->assertEqual(0, count($rows));
263
264     // Ensure the translation tab is not accessible.
265     $this->drupalGet('node/' . $node->id() . '/translations');
266     $this->assertResponse(403);
267   }
268
269   /**
270    * Tests that translations are rendered properly.
271    */
272   public function testTranslationRendering() {
273     $default_langcode = $this->langcodes[0];
274     $values[$default_langcode] = $this->getNewEntityValues($default_langcode);
275     $this->entityId = $this->createEntity($values[$default_langcode], $default_langcode);
276     $node = \Drupal::entityManager()->getStorage($this->entityTypeId)->load($this->entityId);
277     $node->setPromoted(TRUE);
278
279     // Create translations.
280     foreach (array_diff($this->langcodes, [$default_langcode]) as $langcode) {
281       $values[$langcode] = $this->getNewEntityValues($langcode);
282       $translation = $node->addTranslation($langcode, $values[$langcode]);
283       // Publish and promote the translation to frontpage.
284       $translation->setPromoted(TRUE);
285       $translation->setPublished(TRUE);
286     }
287     $node->save();
288
289     // Test that the frontpage view displays the correct translations.
290     \Drupal::service('module_installer')->install(['views'], TRUE);
291     $this->rebuildContainer();
292     $this->doTestTranslations('node', $values);
293
294     // Enable the translation language renderer.
295     $view = \Drupal::entityManager()->getStorage('view')->load('frontpage');
296     $display = &$view->getDisplay('default');
297     $display['display_options']['rendering_language'] = '***LANGUAGE_entity_translation***';
298     $view->save();
299
300     // Need to check from the beginning, including the base_path, in the url
301     // since the pattern for the default language might be a substring of
302     // the strings for other languages.
303     $base_path = base_path();
304
305     // Check the frontpage for 'Read more' links to each translation.
306     // See also assertTaxonomyPage() in NodeAccessBaseTableTest.
307     $node_href = 'node/' . $node->id();
308     foreach ($this->langcodes as $langcode) {
309       $this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
310       $num_match_found = 0;
311       if ($langcode == 'en') {
312         // Site default language does not have langcode prefix in the URL.
313         $expected_href = $base_path . $node_href;
314       }
315       else {
316         $expected_href = $base_path . $langcode . '/' . $node_href;
317       }
318       $pattern = '|^' . $expected_href . '$|';
319       foreach ($this->xpath("//a[text()='Read more']") as $link) {
320         if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
321           $num_match_found++;
322         }
323       }
324       $this->assertTrue($num_match_found == 1, 'There is 1 Read more link, ' . $expected_href . ', for the ' . $langcode . ' translation of a node on the frontpage. (Found ' . $num_match_found . '.)');
325     }
326
327     // Check the frontpage for 'Add new comment' links that include the
328     // language.
329     $comment_form_href = 'node/' . $node->id() . '#comment-form';
330     foreach ($this->langcodes as $langcode) {
331       $this->drupalGet('node', ['language' => \Drupal::languageManager()->getLanguage($langcode)]);
332       $num_match_found = 0;
333       if ($langcode == 'en') {
334         // Site default language does not have langcode prefix in the URL.
335         $expected_href = $base_path . $comment_form_href;
336       }
337       else {
338         $expected_href = $base_path . $langcode . '/' . $comment_form_href;
339       }
340       $pattern = '|^' . $expected_href . '$|';
341       foreach ($this->xpath("//a[text()='Add new comment']") as $link) {
342         if (preg_match($pattern, $link->getAttribute('href'), $matches) == TRUE) {
343           $num_match_found++;
344         }
345       }
346       $this->assertTrue($num_match_found == 1, 'There is 1 Add new comment link, ' . $expected_href . ', for the ' . $langcode . ' translation of a node on the frontpage. (Found ' . $num_match_found . '.)');
347     }
348
349     // Test that the node page displays the correct translations.
350     $this->doTestTranslations('node/' . $node->id(), $values);
351
352     // Test that the node page has the correct alternate hreflang links.
353     $this->doTestAlternateHreflangLinks($node->urlInfo());
354   }
355
356   /**
357    * Tests that the given path displays the correct translation values.
358    *
359    * @param string $path
360    *   The path to be tested.
361    * @param array $values
362    *   The translation values to be found.
363    */
364   protected function doTestTranslations($path, array $values) {
365     $languages = $this->container->get('language_manager')->getLanguages();
366     foreach ($this->langcodes as $langcode) {
367       $this->drupalGet($path, ['language' => $languages[$langcode]]);
368       $this->assertText($values[$langcode]['title'][0]['value'], format_string('The %langcode node translation is correctly displayed.', ['%langcode' => $langcode]));
369     }
370   }
371
372   /**
373    * Tests that the given path provides the correct alternate hreflang links.
374    *
375    * @param \Drupal\Core\Url $url
376    *   The path to be tested.
377    */
378   protected function doTestAlternateHreflangLinks(Url $url) {
379     $languages = $this->container->get('language_manager')->getLanguages();
380     $url->setAbsolute();
381     $urls = [];
382     foreach ($this->langcodes as $langcode) {
383       $language_url = clone $url;
384       $urls[$langcode] = $language_url->setOption('language', $languages[$langcode]);
385     }
386     foreach ($this->langcodes as $langcode) {
387       $this->drupalGet($urls[$langcode]);
388       foreach ($urls as $alternate_langcode => $language_url) {
389         // Retrieve desired link elements from the HTML head.
390         $links = $this->xpath('head/link[@rel = "alternate" and @href = :href and @hreflang = :hreflang]',
391           [':href' => $language_url->toString(), ':hreflang' => $alternate_langcode]);
392         $this->assert(isset($links[0]), format_string('The %langcode node translation has the correct alternate hreflang link for %alternate_langcode: %link.', ['%langcode' => $langcode, '%alternate_langcode' => $alternate_langcode, '%link' => $url->toString()]));
393       }
394     }
395   }
396
397   /**
398    * {@inheritdoc}
399    */
400   protected function getFormSubmitSuffix(EntityInterface $entity, $langcode) {
401     if (!$entity->isNew() && $entity->isTranslatable()) {
402       $translations = $entity->getTranslationLanguages();
403       if ((count($translations) > 1 || !isset($translations[$langcode])) && ($field = $entity->getFieldDefinition('status'))) {
404         return ' ' . ($field->isTranslatable() ? t('(this translation)') : t('(all translations)'));
405       }
406     }
407     return '';
408   }
409
410   /**
411    * Tests uninstalling content_translation.
412    */
413   protected function doUninstallTest() {
414     // Delete all the nodes so there is no data.
415     $nodes = Node::loadMultiple();
416     foreach ($nodes as $node) {
417       $node->delete();
418     }
419     $language_count = count(\Drupal::configFactory()->listAll('language.content_settings.'));
420     \Drupal::service('module_installer')->uninstall(['content_translation']);
421     $this->rebuildContainer();
422     $this->assertEqual($language_count, count(\Drupal::configFactory()->listAll('language.content_settings.')), 'Languages have been fixed rather than deleted during content_translation uninstall.');
423   }
424
425   /**
426    * {@inheritdoc}
427    */
428   protected function doTestTranslationEdit() {
429     $storage = $this->container->get('entity_type.manager')
430       ->getStorage($this->entityTypeId);
431     $storage->resetCache([$this->entityId]);
432     $entity = $storage->load($this->entityId);
433     $languages = $this->container->get('language_manager')->getLanguages();
434     $type_name = node_get_type_label($entity);
435
436     foreach ($this->langcodes as $langcode) {
437       // We only want to test the title for non-english translations.
438       if ($langcode != 'en') {
439         $options = ['language' => $languages[$langcode]];
440         $url = $entity->urlInfo('edit-form', $options);
441         $this->drupalGet($url);
442
443         $title = t('<em>Edit @type</em> @title [%language translation]', [
444           '@type' => $type_name,
445           '@title' => $entity->getTranslation($langcode)->label(),
446           '%language' => $languages[$langcode]->getName(),
447         ]);
448         $this->assertRaw($title);
449       }
450     }
451   }
452
453   /**
454    * Tests that revision translations are rendered properly.
455    */
456   public function testRevisionTranslationRendering() {
457     $storage = \Drupal::entityTypeManager()->getStorage('node');
458
459     // Create a node.
460     $nid = $this->createEntity(['title' => 'First rev en title'], 'en');
461     $node = $storage->load($nid);
462     $original_revision_id = $node->getRevisionId();
463
464     // Add a French translation.
465     $translation = $node->addTranslation('fr');
466     $translation->title = 'First rev fr title';
467     $translation->setNewRevision(FALSE);
468     $translation->save();
469
470     // Create a new revision.
471     $node->title = 'Second rev en title';
472     $node->setNewRevision(TRUE);
473     $node->save();
474
475     // Get an English view of this revision.
476     $original_revision = $storage->loadRevision($original_revision_id);
477     $original_revision_url = $original_revision->toUrl('revision')->toString();
478
479     // Should be different from regular node URL.
480     $this->assertNotIdentical($original_revision_url, $original_revision->toUrl()->toString());
481     $this->drupalGet($original_revision_url);
482     $this->assertResponse(200);
483
484     // Contents should be in English, of correct revision.
485     $this->assertText('First rev en title');
486     $this->assertNoText('First rev fr title');
487
488     // Get a French view.
489     $url_fr = $original_revision->getTranslation('fr')->toUrl('revision')->toString();
490
491     // Should have different URL from English.
492     $this->assertNotIdentical($url_fr, $original_revision->toUrl()->toString());
493     $this->assertNotIdentical($url_fr, $original_revision_url);
494     $this->drupalGet($url_fr);
495     $this->assertResponse(200);
496
497     // Contents should be in French, of correct revision.
498     $this->assertText('First rev fr title');
499     $this->assertNoText('First rev en title');
500   }
501
502 }