Security update for Core, with self-updated composer
[yaffs-website] / web / core / modules / node / src / Tests / NodeRevisionsTest.php
1 <?php
2
3 namespace Drupal\node\Tests;
4
5 use Drupal\Core\Url;
6 use Drupal\field\Entity\FieldConfig;
7 use Drupal\field\Entity\FieldStorageConfig;
8 use Drupal\language\Entity\ConfigurableLanguage;
9 use Drupal\node\Entity\Node;
10 use Drupal\node\NodeInterface;
11 use Drupal\Component\Serialization\Json;
12
13 /**
14  * Create a node with revisions and test viewing, saving, reverting, and
15  * deleting revisions for users with access for this content type.
16  *
17  * @group node
18  */
19 class NodeRevisionsTest extends NodeTestBase {
20
21   /**
22    * An array of node revisions.
23    *
24    * @var \Drupal\node\NodeInterface[]
25    */
26   protected $nodes;
27
28   /**
29    * Revision log messages.
30    *
31    * @var array
32    */
33   protected $revisionLogs;
34
35   /**
36    * {@inheritdoc}
37    */
38   public static $modules = ['node', 'contextual', 'datetime', 'language', 'content_translation'];
39
40   /**
41    * {@inheritdoc}
42    */
43   protected function setUp() {
44     parent::setUp();
45
46     // Enable additional languages.
47     ConfigurableLanguage::createFromLangcode('de')->save();
48     ConfigurableLanguage::createFromLangcode('it')->save();
49
50     $field_storage_definition = [
51       'field_name' => 'untranslatable_string_field',
52       'entity_type' => 'node',
53       'type' => 'string',
54       'cardinality' => 1,
55       'translatable' => FALSE,
56     ];
57     $field_storage = FieldStorageConfig::create($field_storage_definition);
58     $field_storage->save();
59
60     $field_definition = [
61       'field_storage' => $field_storage,
62       'bundle' => 'page',
63     ];
64     $field = FieldConfig::create($field_definition);
65     $field->save();
66
67     // Create and log in user.
68     $web_user = $this->drupalCreateUser(
69       [
70         'view page revisions',
71         'revert page revisions',
72         'delete page revisions',
73         'edit any page content',
74         'delete any page content',
75         'access contextual links',
76         'translate any entity',
77         'administer content types',
78       ]
79     );
80
81     $this->drupalLogin($web_user);
82
83     // Create initial node.
84     $node = $this->drupalCreateNode();
85     $settings = get_object_vars($node);
86     $settings['revision'] = 1;
87     $settings['isDefaultRevision'] = TRUE;
88
89     $nodes = [];
90     $logs = [];
91
92     // Get original node.
93     $nodes[] = clone $node;
94
95     // Create three revisions.
96     $revision_count = 3;
97     for ($i = 0; $i < $revision_count; $i++) {
98       $logs[] = $node->revision_log = $this->randomMachineName(32);
99
100       // Create revision with a random title and body and update variables.
101       $node->title = $this->randomMachineName();
102       $node->body = [
103         'value' => $this->randomMachineName(32),
104         'format' => filter_default_format(),
105       ];
106       $node->untranslatable_string_field->value = $this->randomString();
107       $node->setNewRevision();
108
109       // Edit the 2nd revision with a different user.
110       if ($i == 1) {
111         $editor = $this->drupalCreateUser();
112         $node->setRevisionUserId($editor->id());
113       }
114       else {
115         $node->setRevisionUserId($web_user->id());
116       }
117
118       $node->save();
119
120       // Make sure we get revision information.
121       $node = Node::load($node->id());
122       $nodes[] = clone $node;
123     }
124
125     $this->nodes = $nodes;
126     $this->revisionLogs = $logs;
127   }
128
129   /**
130    * Checks node revision related operations.
131    */
132   public function testRevisions() {
133     $node_storage = $this->container->get('entity.manager')->getStorage('node');
134     $nodes = $this->nodes;
135     $logs = $this->revisionLogs;
136
137     // Get last node for simple checks.
138     $node = $nodes[3];
139
140     // Confirm the correct revision text appears on "view revisions" page.
141     $this->drupalGet("node/" . $node->id() . "/revisions/" . $node->getRevisionId() . "/view");
142     $this->assertText($node->body->value, 'Correct text displays for version.');
143
144     // Confirm the correct log message appears on "revisions overview" page.
145     $this->drupalGet("node/" . $node->id() . "/revisions");
146     foreach ($logs as $revision_log) {
147       $this->assertText($revision_log, 'Revision log message found.');
148     }
149     // Original author, and editor names should appear on revisions overview.
150     $web_user = $nodes[0]->revision_uid->entity;
151     $this->assertText(t('by @name', ['@name' => $web_user->getAccountName()]));
152     $editor = $nodes[2]->revision_uid->entity;
153     $this->assertText(t('by @name', ['@name' => $editor->getAccountName()]));
154
155     // Confirm that this is the default revision.
156     $this->assertTrue($node->isDefaultRevision(), 'Third node revision is the default one.');
157
158     // Confirm that the "Edit" and "Delete" contextual links appear for the
159     // default revision.
160     $ids = ['node:node=' . $node->id() . ':changed=' . $node->getChangedTime()];
161     $json = $this->renderContextualLinks($ids, 'node/' . $node->id());
162     $this->verbose($json[$ids[0]]);
163
164     $expected = '<li class="entitynodeedit-form"><a href="' . base_path() . 'node/' . $node->id() . '/edit">Edit</a></li>';
165     $this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Edit" contextual link is shown for the default revision.');
166     $expected = '<li class="entitynodedelete-form"><a href="' . base_path() . 'node/' . $node->id() . '/delete">Delete</a></li>';
167     $this->assertTrue(strstr($json[$ids[0]], $expected), 'The "Delete" contextual link is shown for the default revision.');
168
169     // Confirm that revisions revert properly.
170     $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionid() . "/revert", [], t('Revert'));
171     $this->assertRaw(t('@type %title has been reverted to the revision from %revision-date.', [
172       '@type' => 'Basic page',
173       '%title' => $nodes[1]->label(),
174       '%revision-date' => format_date($nodes[1]->getRevisionCreationTime())
175     ]), 'Revision reverted.');
176     $node_storage->resetCache([$node->id()]);
177     $reverted_node = $node_storage->load($node->id());
178     $this->assertTrue(($nodes[1]->body->value == $reverted_node->body->value), 'Node reverted correctly.');
179
180     // Confirm that this is not the default version.
181     $node = node_revision_load($node->getRevisionId());
182     $this->assertFalse($node->isDefaultRevision(), 'Third node revision is not the default one.');
183
184     // Confirm that "Edit" and "Delete" contextual links don't appear for
185     // non-default revision.
186     $ids = ['node_revision::node=' . $node->id() . '&node_revision=' . $node->getRevisionId() . ':'];
187     $json = $this->renderContextualLinks($ids, 'node/' . $node->id() . '/revisions/' . $node->getRevisionId() . '/view');
188     $this->verbose($json[$ids[0]]);
189
190     $this->assertFalse(strstr($json[$ids[0]], '<li class="entitynodeedit-form">'), 'The "Edit" contextual link is not shown for a non-default revision.');
191     $this->assertFalse(strstr($json[$ids[0]], '<li class="entitynodedelete-form">'), 'The "Delete" contextual link is not shown for a non-default revision.');
192
193     // Confirm revisions delete properly.
194     $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[1]->getRevisionId() . "/delete", [], t('Delete'));
195     $this->assertRaw(t('Revision from %revision-date of @type %title has been deleted.', [
196       '%revision-date' => format_date($nodes[1]->getRevisionCreationTime()),
197       '@type' => 'Basic page',
198       '%title' => $nodes[1]->label(),
199     ]), 'Revision deleted.');
200     $this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_revision} WHERE nid = :nid and vid = :vid', [':nid' => $node->id(), ':vid' => $nodes[1]->getRevisionId()])->fetchField() == 0, 'Revision not found.');
201     $this->assertTrue(db_query('SELECT COUNT(vid) FROM {node_field_revision} WHERE nid = :nid and vid = :vid', [':nid' => $node->id(), ':vid' => $nodes[1]->getRevisionId()])->fetchField() == 0, 'Field revision not found.');
202
203     // Set the revision timestamp to an older date to make sure that the
204     // confirmation message correctly displays the stored revision date.
205     $old_revision_date = REQUEST_TIME - 86400;
206     db_update('node_revision')
207       ->condition('vid', $nodes[2]->getRevisionId())
208       ->fields([
209         'revision_timestamp' => $old_revision_date,
210       ])
211       ->execute();
212     $this->drupalPostForm("node/" . $node->id() . "/revisions/" . $nodes[2]->getRevisionId() . "/revert", [], t('Revert'));
213     $this->assertRaw(t('@type %title has been reverted to the revision from %revision-date.', [
214       '@type' => 'Basic page',
215       '%title' => $nodes[2]->label(),
216       '%revision-date' => format_date($old_revision_date),
217     ]));
218
219     // Make a new revision and set it to not be default.
220     // This will create a new revision that is not "front facing".
221     $new_node_revision = clone $node;
222     $new_body = $this->randomMachineName();
223     $new_node_revision->body->value = $new_body;
224     // Save this as a non-default revision.
225     $new_node_revision->setNewRevision();
226     $new_node_revision->isDefaultRevision = FALSE;
227     $new_node_revision->save();
228
229     $this->drupalGet('node/' . $node->id());
230     $this->assertNoText($new_body, 'Revision body text is not present on default version of node.');
231
232     // Verify that the new body text is present on the revision.
233     $this->drupalGet("node/" . $node->id() . "/revisions/" . $new_node_revision->getRevisionId() . "/view");
234     $this->assertText($new_body, 'Revision body text is present when loading specific revision.');
235
236     // Verify that the non-default revision vid is greater than the default
237     // revision vid.
238     $default_revision = db_select('node', 'n')
239       ->fields('n', ['vid'])
240       ->condition('nid', $node->id())
241       ->execute()
242       ->fetchCol();
243     $default_revision_vid = $default_revision[0];
244     $this->assertTrue($new_node_revision->getRevisionId() > $default_revision_vid, 'Revision vid is greater than default revision vid.');
245
246     // Create an 'EN' node with a revision log message.
247     $node = $this->drupalCreateNode();
248     $node->title = 'Node title in EN';
249     $node->revision_log = 'Simple revision message (EN)';
250     $node->save();
251
252     $this->drupalGet("node/" . $node->id() . "/revisions");
253     $this->assertResponse(403);
254
255     // Create a new revision and new log message.
256     $node = Node::load($node->id());
257     $node->body->value = 'New text (EN)';
258     $node->revision_log = 'New revision message (EN)';
259     $node->setNewRevision();
260     $node->save();
261
262     // Check both revisions are shown on the node revisions overview page.
263     $this->drupalGet("node/" . $node->id() . "/revisions");
264     $this->assertText('Simple revision message (EN)');
265     $this->assertText('New revision message (EN)');
266
267     // Create an 'EN' node with a revision log message.
268     $node = $this->drupalCreateNode();
269     $node->langcode = 'en';
270     $node->title = 'Node title in EN';
271     $node->revision_log = 'Simple revision message (EN)';
272     $node->save();
273
274     $this->drupalGet("node/" . $node->id() . "/revisions");
275     $this->assertResponse(403);
276
277     // Add a translation in 'DE' and create a new revision and new log message.
278     $translation = $node->addTranslation('de');
279     $translation->title->value = 'Node title in DE';
280     $translation->body->value = 'New text (DE)';
281     $translation->revision_log = 'New revision message (DE)';
282     $translation->setNewRevision();
283     $translation->save();
284
285     // View the revision UI in 'IT', only the original node revision is shown.
286     $this->drupalGet("it/node/" . $node->id() . "/revisions");
287     $this->assertText('Simple revision message (EN)');
288     $this->assertNoText('New revision message (DE)');
289
290     // View the revision UI in 'DE', only the translated node revision is shown.
291     $this->drupalGet("de/node/" . $node->id() . "/revisions");
292     $this->assertNoText('Simple revision message (EN)');
293     $this->assertText('New revision message (DE)');
294
295     // View the revision UI in 'EN', only the original node revision is shown.
296     $this->drupalGet("node/" . $node->id() . "/revisions");
297     $this->assertText('Simple revision message (EN)');
298     $this->assertNoText('New revision message (DE)');
299   }
300
301   /**
302    * Checks that revisions are correctly saved without log messages.
303    */
304   public function testNodeRevisionWithoutLogMessage() {
305     $node_storage = $this->container->get('entity.manager')->getStorage('node');
306     // Create a node with an initial log message.
307     $revision_log = $this->randomMachineName(10);
308     $node = $this->drupalCreateNode(['revision_log' => $revision_log]);
309
310     // Save over the same revision and explicitly provide an empty log message
311     // (for example, to mimic the case of a node form submitted with no text in
312     // the "log message" field), and check that the original log message is
313     // preserved.
314     $new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage1';
315
316     $node = clone $node;
317     $node->title = $new_title;
318     $node->revision_log = '';
319     $node->setNewRevision(FALSE);
320
321     $node->save();
322     $this->drupalGet('node/' . $node->id());
323     $this->assertText($new_title, 'New node title appears on the page.');
324     $node_storage->resetCache([$node->id()]);
325     $node_revision = $node_storage->load($node->id());
326     $this->assertEqual($node_revision->revision_log->value, $revision_log, 'After an existing node revision is re-saved without a log message, the original log message is preserved.');
327
328     // Create another node with an initial revision log message.
329     $node = $this->drupalCreateNode(['revision_log' => $revision_log]);
330
331     // Save a new node revision without providing a log message, and check that
332     // this revision has an empty log message.
333     $new_title = $this->randomMachineName(10) . 'testNodeRevisionWithoutLogMessage2';
334
335     $node = clone $node;
336     $node->title = $new_title;
337     $node->setNewRevision();
338     $node->revision_log = NULL;
339
340     $node->save();
341     $this->drupalGet('node/' . $node->id());
342     $this->assertText($new_title, 'New node title appears on the page.');
343     $node_storage->resetCache([$node->id()]);
344     $node_revision = $node_storage->load($node->id());
345     $this->assertTrue(empty($node_revision->revision_log->value), 'After a new node revision is saved with an empty log message, the log message for the node is empty.');
346   }
347
348   /**
349    * Gets server-rendered contextual links for the given contextual links IDs.
350    *
351    * @param string[] $ids
352    *   An array of contextual link IDs.
353    * @param string $current_path
354    *   The Drupal path for the page for which the contextual links are rendered.
355    *
356    * @return string
357    *   The decoded JSON response body.
358    */
359   protected function renderContextualLinks(array $ids, $current_path) {
360     $post = [];
361     for ($i = 0; $i < count($ids); $i++) {
362       $post['ids[' . $i . ']'] = $ids[$i];
363     }
364     $response = $this->drupalPost('contextual/render', 'application/json', $post, ['query' => ['destination' => $current_path]]);
365
366     return Json::decode($response);
367   }
368
369   /**
370    * Tests the revision translations are correctly reverted.
371    */
372   public function testRevisionTranslationRevert() {
373     // Create a node and a few revisions.
374     $node = $this->drupalCreateNode(['langcode' => 'en']);
375
376     $initial_revision_id = $node->getRevisionId();
377     $initial_title = $node->label();
378     $this->createRevisions($node, 2);
379
380     // Translate the node and create a few translation revisions.
381     $translation = $node->addTranslation('it');
382     $this->createRevisions($translation, 3);
383     $revert_id = $node->getRevisionId();
384     $translated_title = $translation->label();
385     $untranslatable_string = $node->untranslatable_string_field->value;
386
387     // Create a new revision for the default translation in-between a series of
388     // translation revisions.
389     $this->createRevisions($node, 1);
390     $default_translation_title = $node->label();
391
392     // And create a few more translation revisions.
393     $this->createRevisions($translation, 2);
394     $translation_revision_id = $translation->getRevisionId();
395
396     // Now revert the a translation revision preceding the last default
397     // translation revision, and check that the desired value was reverted but
398     // the default translation value was preserved.
399     $revert_translation_url = Url::fromRoute('node.revision_revert_translation_confirm', [
400       'node' => $node->id(),
401       'node_revision' => $revert_id,
402       'langcode' => 'it',
403     ]);
404     $this->drupalPostForm($revert_translation_url, [], t('Revert'));
405     /** @var \Drupal\node\NodeStorage $node_storage */
406     $node_storage = $this->container->get('entity.manager')->getStorage('node');
407     $node_storage->resetCache();
408     /** @var \Drupal\node\NodeInterface $node */
409     $node = $node_storage->load($node->id());
410     $this->assertTrue($node->getRevisionId() > $translation_revision_id);
411     $this->assertEqual($node->label(), $default_translation_title);
412     $this->assertEqual($node->getTranslation('it')->label(), $translated_title);
413     $this->assertNotEqual($node->untranslatable_string_field->value, $untranslatable_string);
414
415     $latest_revision_id = $translation->getRevisionId();
416
417     // Now revert the a translation revision preceding the last default
418     // translation revision again, and check that the desired value was reverted
419     // but the default translation value was preserved. But in addition the
420     // untranslated field will be reverted as well.
421     $this->drupalPostForm($revert_translation_url, ['revert_untranslated_fields' => TRUE], t('Revert'));
422     $node_storage->resetCache();
423     /** @var \Drupal\node\NodeInterface $node */
424     $node = $node_storage->load($node->id());
425     $this->assertTrue($node->getRevisionId() > $latest_revision_id);
426     $this->assertEqual($node->label(), $default_translation_title);
427     $this->assertEqual($node->getTranslation('it')->label(), $translated_title);
428     $this->assertEqual($node->untranslatable_string_field->value, $untranslatable_string);
429
430     $latest_revision_id = $translation->getRevisionId();
431
432     // Now revert the entity revision to the initial one where the translation
433     // didn't exist.
434     $revert_url = Url::fromRoute('node.revision_revert_confirm', [
435       'node' => $node->id(),
436       'node_revision' => $initial_revision_id,
437     ]);
438     $this->drupalPostForm($revert_url, [], t('Revert'));
439     $node_storage->resetCache();
440     /** @var \Drupal\node\NodeInterface $node */
441     $node = $node_storage->load($node->id());
442     $this->assertTrue($node->getRevisionId() > $latest_revision_id);
443     $this->assertEqual($node->label(), $initial_title);
444     $this->assertFalse($node->hasTranslation('it'));
445   }
446
447   /**
448    * Creates a series of revisions for the specified node.
449    *
450    * @param \Drupal\node\NodeInterface $node
451    *   The node object.
452    * @param $count
453    *   The number of revisions to be created.
454    */
455   protected function createRevisions(NodeInterface $node, $count) {
456     for ($i = 0; $i < $count; $i++) {
457       $node->title = $this->randomString();
458       $node->untranslatable_string_field->value = $this->randomString();
459       $node->setNewRevision(TRUE);
460       $node->save();
461     }
462   }
463
464 }