NodeRevisionsTest.php 19 KB

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