From a6c2e2b1c56b67e41b8f87779bd08ca695686039 Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Mon, 28 Sep 2015 22:08:45 +0100
Subject: [PATCH] Issue #2506213 by hchonov, mkalkbrenner: Update content
 entity changed timestamp on UI save

---
 .../Drupal/Core/Entity/ContentEntityForm.php  | 23 ++++++
 .../Core/Entity/EntityChangedInterface.php    | 10 +++
 .../Drupal/Core/Entity/EntityChangedTrait.php | 22 ++++++
 .../EntityChangedConstraintValidator.php      |  2 +-
 .../block_content/src/Entity/BlockContent.php |  7 --
 core/modules/comment/src/Entity/Comment.php   |  7 --
 .../src/ContentTranslationHandler.php         | 32 +++++++-
 .../Tests/ContentTranslationUITestBase.php    | 27 +++++++
 core/modules/file/src/Entity/File.php         |  7 --
 .../src/Entity/MenuLinkContent.php            |  7 --
 core/modules/node/src/Entity/Node.php         |  8 +-
 .../src/Tests/NodeFormSaveChangedTimeTest.php | 76 +++++++++++++++++++
 core/modules/rest/src/Tests/RESTTestBase.php  |  1 +
 .../src/Entity/EntityTestConstraints.php      |  8 --
 .../src/Entity/EntityTestMulChanged.php       |  8 --
 core/modules/taxonomy/src/Entity/Term.php     |  7 --
 core/modules/user/src/Entity/User.php         |  7 --
 17 files changed, 192 insertions(+), 67 deletions(-)
 create mode 100644 core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php

diff --git a/core/lib/Drupal/Core/Entity/ContentEntityForm.php b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
index 1fd99dcbc21d..eed3cac3f9a3 100644
--- a/core/lib/Drupal/Core/Entity/ContentEntityForm.php
+++ b/core/lib/Drupal/Core/Entity/ContentEntityForm.php
@@ -62,6 +62,15 @@ public function form(array $form, FormStateInterface $form_state) {
     return $form;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function submitForm(array &$form, FormStateInterface $form_state) {
+    parent::submitForm($form, $form_state);
+    // Update the changed timestamp of the entity.
+    $this->updateChangedTime($this->entity);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -269,4 +278,18 @@ public function updateFormLangcode($entity_type_id, EntityInterface $entity, arr
     }
   }
 
+  /**
+   * Updates the changed time of the entity.
+   *
+   * Applies only if the entity implements the EntityChangedInterface.
+   *
+   * @param \Drupal\Core\Entity\EntityInterface $entity
+   *   The entity updated with the submitted values.
+   */
+  public function updateChangedTime(EntityInterface $entity) {
+    if ($entity->getEntityType()->isSubclassOf(EntityChangedInterface::class)) {
+      $entity->setChangedTime(REQUEST_TIME);
+    }
+  }
+
 }
diff --git a/core/lib/Drupal/Core/Entity/EntityChangedInterface.php b/core/lib/Drupal/Core/Entity/EntityChangedInterface.php
index 5f031a38ae62..2bd29ed65ec2 100644
--- a/core/lib/Drupal/Core/Entity/EntityChangedInterface.php
+++ b/core/lib/Drupal/Core/Entity/EntityChangedInterface.php
@@ -29,6 +29,16 @@ interface EntityChangedInterface {
    */
   public function getChangedTime();
 
+  /**
+   * Sets the timestamp of the last entity change for the current translation.
+   *
+   * @param int $timestamp
+   *   The timestamp of the last entity save operation.
+   *
+   * @return $this
+   */
+  public function setChangedTime($timestamp);
+
   /**
    * Gets the timestamp of the last entity change across all translations.
    *
diff --git a/core/lib/Drupal/Core/Entity/EntityChangedTrait.php b/core/lib/Drupal/Core/Entity/EntityChangedTrait.php
index 5b34d1abaf23..416d4aaf2e04 100644
--- a/core/lib/Drupal/Core/Entity/EntityChangedTrait.php
+++ b/core/lib/Drupal/Core/Entity/EntityChangedTrait.php
@@ -28,4 +28,26 @@ public function getChangedTimeAcrossTranslations() {
     return $changed;
   }
 
+  /**
+   * Gets the timestamp of the last entity change for the current translation.
+   *
+   * @return int
+   *   The timestamp of the last entity save operation.
+   */
+  public function getChangedTime() {
+    return $this->get('changed')->value;
+  }
+
+  /**
+   * Sets the timestamp of the last entity change for the current translation.
+   *
+   * @param int $timestamp
+   *   The timestamp of the last entity save operation.
+   *
+   * @return $this
+   */
+  public function setChangedTime($timestamp) {
+    $this->set('changed', $timestamp);
+    return $this;
+  }
 }
diff --git a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
index 8fd44dacc070..1d26d0b868c2 100644
--- a/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
+++ b/core/lib/Drupal/Core/Entity/Plugin/Validation/Constraint/EntityChangedConstraintValidator.php
@@ -25,7 +25,7 @@ public function validate($entity, Constraint $constraint) {
         $saved_entity = \Drupal::entityManager()->getStorage($entity->getEntityTypeId())->loadUnchanged($entity->id());
         // A change to any other translation must add a violation to the current
         // translation because there might be untranslatable shared fields.
-        if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTime()) {
+        if ($saved_entity && $saved_entity->getChangedTimeAcrossTranslations() > $entity->getChangedTimeAcrossTranslations()) {
           $this->context->addViolation($constraint->message);
         }
       }
diff --git a/core/modules/block_content/src/Entity/BlockContent.php b/core/modules/block_content/src/Entity/BlockContent.php
index 7f572bd495b8..2a052027107b 100644
--- a/core/modules/block_content/src/Entity/BlockContent.php
+++ b/core/modules/block_content/src/Entity/BlockContent.php
@@ -219,13 +219,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
     return $fields;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTime() {
-    return $this->get('changed')->value;
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/comment/src/Entity/Comment.php b/core/modules/comment/src/Entity/Comment.php
index 56930493a046..b20d9bb0d77e 100644
--- a/core/modules/comment/src/Entity/Comment.php
+++ b/core/modules/comment/src/Entity/Comment.php
@@ -527,13 +527,6 @@ public function setThread($thread) {
     return $this;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTime() {
-    return $this->get('changed')->value;
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/content_translation/src/ContentTranslationHandler.php b/core/modules/content_translation/src/ContentTranslationHandler.php
index 3979fff51823..d7eb42ab3e6c 100644
--- a/core/modules/content_translation/src/ContentTranslationHandler.php
+++ b/core/modules/content_translation/src/ContentTranslationHandler.php
@@ -479,6 +479,13 @@ public function entityFormAlter(array &$form, FormStateInterface $form_state, En
     if (isset($form['actions']['delete'])) {
       $form['actions']['delete']['#submit'][] = array($this, 'entityFormDelete');
     }
+
+    // Handle entity form submission before the entity has been saved.
+    foreach (Element::children($form['actions']) as $action) {
+      if (isset($form['actions'][$action]['#type']) && $form['actions'][$action]['#type'] == 'submit') {
+        array_unshift($form['actions'][$action]['#submit'], [$this, 'entityFormSubmit']);
+      }
+    }
   }
 
   /**
@@ -579,7 +586,6 @@ public function entityFormEntityBuild($entity_type, EntityInterface $entity, arr
     $metadata->setAuthor(!empty($values['uid']) ? User::load($values['uid']) : User::load(0));
     $metadata->setPublished(!empty($values['status']));
     $metadata->setCreatedTime(!empty($values['created']) ? strtotime($values['created']) : REQUEST_TIME);
-    $metadata->setChangedTime(REQUEST_TIME);
 
     $source_langcode = $this->getSourceLangcode($form_state);
     if ($source_langcode) {
@@ -611,6 +617,30 @@ function entityFormValidate($form, FormStateInterface $form_state) {
     }
   }
 
+  /**
+   * Form submission handler for ContentTranslationHandler::entityFormAlter().
+   *
+   * Updates metadata fields, which should be updated only after the validation
+   * has run and before the entity is saved.
+   */
+  function entityFormSubmit($form, FormStateInterface $form_state) {
+    /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
+    $form_object = $form_state->getFormObject();
+    /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
+    $entity = $form_object->getEntity();
+
+    // ContentEntityForm::submit will update the changed timestamp on submit
+    // after the entity has been validated, so that it does not break the
+    // EntityChanged constraint validator. The content translation metadata
+    // field for the changed timestamp  does not have such a constraint defined
+    // at the moment, but it is correct to update it's value in a submission
+    // handler as well and have the same logic like in the Form API.
+    if ($entity->hasField('content_translation_changed')) {
+      $metadata = $this->manager->getTranslationMetadata($entity);
+      $metadata->setChangedTime(REQUEST_TIME);
+    }
+  }
+
   /**
    * Form submission handler for ContentTranslationHandler::entityFormAlter().
    *
diff --git a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
index a5d6d09c1555..de8c349ece50 100644
--- a/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
+++ b/core/modules/content_translation/src/Tests/ContentTranslationUITestBase.php
@@ -65,6 +65,7 @@ function testTranslationUI() {
     $this->doTestAuthoringInfo();
     $this->doTestTranslationEdit();
     $this->doTestTranslationChanged();
+    $this->doTestChangedTimeAfterSaveWithoutChanges();
     $this->doTestTranslationDeletion();
   }
 
@@ -546,4 +547,30 @@ protected function doTestTranslationChanged() {
     }
   }
 
+  /**
+   * Test the changed time after API and FORM save without changes.
+   */
+  public function doTestChangedTimeAfterSaveWithoutChanges() {
+    $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
+    // Test only entities, which implement the EntityChangedInterface.
+    if ($entity->getEntityType()->isSubclassOf('Drupal\Core\Entity\EntityChangedInterface')) {
+      $changed_timestamp = $entity->getChangedTime();
+
+      $entity->save();
+      $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
+      $this->assertEqual($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time wasn\'t updated after API save without changes.');
+
+      // Ensure different save timestamps.
+      sleep(1);
+
+      // Save the entity on the regular edit form.
+      $language = $entity->language();
+      $edit_path = $entity->urlInfo('edit-form', array('language' => $language));
+      $this->drupalPostForm($edit_path, [], $this->getFormSubmitAction($entity, $language->getId()));
+
+      $entity = entity_load($this->entityTypeId, $this->entityId, TRUE);
+      $this->assertNotEqual($changed_timestamp, $entity->getChangedTime(), 'The entity\'s changed time was updated after form save without changes.');
+    }
+  }
+
 }
diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php
index 1a38c16132a1..ae5ca764b218 100644
--- a/core/modules/file/src/Entity/File.php
+++ b/core/modules/file/src/Entity/File.php
@@ -110,13 +110,6 @@ public function getCreatedTime() {
     return $this->get('created')->value;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTime() {
-    return $this->get('changed')->value;
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
index 7d51ae292147..d83f87115214 100644
--- a/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
+++ b/core/modules/menu_link_content/src/Entity/MenuLinkContent.php
@@ -132,13 +132,6 @@ public function getWeight() {
     return (int) $this->get('weight')->value;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTime() {
-    return $this->get('changed')->value;
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php
index 1e5e5ef8210e..34d57b46ecaf 100644
--- a/core/modules/node/src/Entity/Node.php
+++ b/core/modules/node/src/Entity/Node.php
@@ -231,13 +231,6 @@ public function setCreatedTime($timestamp) {
     return $this;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTime() {
-    return $this->get('changed')->value;
-  }
-
   /**
    * {@inheritdoc}
    */
@@ -498,6 +491,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ->setDescription(t('Briefly describe the changes you have made.'))
       ->setRevisionable(TRUE)
       ->setTranslatable(TRUE)
+      ->setDefaultValue('')
       ->setDisplayOptions('form', array(
         'type' => 'string_textarea',
         'weight' => 25,
diff --git a/core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php b/core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php
new file mode 100644
index 000000000000..285a743746f0
--- /dev/null
+++ b/core/modules/node/src/Tests/NodeFormSaveChangedTimeTest.php
@@ -0,0 +1,76 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\node\Tests\NodeFormSaveChangedTimeTest.
+ */
+
+namespace Drupal\node\Tests;
+
+use Drupal\simpletest\WebTestBase;
+
+/**
+ * Tests updating the changed time after API and FORM entity save.
+ *
+ * @group node
+ */
+class NodeFormSaveChangedTimeTest extends WebTestBase {
+
+  /**
+   * Modules to enable.
+   *
+   * @var array
+   */
+  public static $modules = array(
+    'node',
+  );
+
+  /**
+   * An user with permissions to create and edit articles.
+   *
+   * @var \Drupal\user\UserInterface
+   */
+  protected $authorUser;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUp() {
+    parent::setUp();
+
+    // Create a node type.
+    $this->drupalCreateContentType(array(
+      'type' => 'article',
+      'name' => 'Article',
+    ));
+
+    $this->authorUser = $this->drupalCreateUser(['access content', 'create article content', 'edit any article content'], 'author');
+    $this->drupalLogin($this->authorUser);
+
+    // Create one node of the above node type .
+    $this->drupalCreateNode(array(
+      'type' => 'article',
+    ));
+  }
+
+  /**
+   * Test the changed time after API and FORM save without changes.
+   */
+  public function testChangedTimeAfterSaveWithoutChanges() {
+    $node = entity_load('node', 1);
+    $changed_timestamp = $node->getChangedTime();
+
+    $node->save();
+    $node = entity_load('node', 1, TRUE);
+    $this->assertEqual($changed_timestamp, $node->getChangedTime(), "The entity's changed time wasn't updated after API save without changes.");
+
+    // Ensure different save timestamps.
+    sleep(1);
+
+    // Save the node on the regular node edit form.
+    $this->drupalPostForm('node/1/edit', array(), t('Save'));
+
+    $node = entity_load('node', 1, TRUE);
+    $this->assertNotEqual($changed_timestamp, $node->getChangedTime(), "The entity's changed time was updated after form save without changes.");
+  }
+}
diff --git a/core/modules/rest/src/Tests/RESTTestBase.php b/core/modules/rest/src/Tests/RESTTestBase.php
index 5e84827a9c18..b6eb46a7716f 100644
--- a/core/modules/rest/src/Tests/RESTTestBase.php
+++ b/core/modules/rest/src/Tests/RESTTestBase.php
@@ -389,6 +389,7 @@ protected function removeNodeFieldsForNonAdminUsers(NodeInterface $node) {
     $node->set('promote', NULL);
     $node->set('sticky', NULL);
     $node->set('revision_timestamp', NULL);
+    $node->set('revision_log', NULL);
     $node->set('uid', NULL);
 
     return $node;
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php
index 61200f7cb42e..e9e9bbf00ab0 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestConstraints.php
@@ -46,12 +46,4 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
 
     return $fields;
   }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTime() {
-    return $this->get('changed')->value;
-  }
-
 }
diff --git a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php
index 48c3ddca9f05..67811ac92546 100644
--- a/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php
+++ b/core/modules/system/tests/modules/entity_test/src/Entity/EntityTestMulChanged.php
@@ -72,12 +72,4 @@ public function save() {
     sleep(1);
     parent::save();
   }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTime() {
-    return $this->get('changed')->value;
-  }
-
 }
diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php
index 2105a03b684c..7020f0fdcf7c 100644
--- a/core/modules/taxonomy/src/Entity/Term.php
+++ b/core/modules/taxonomy/src/Entity/Term.php
@@ -187,13 +187,6 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
     return $fields;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTime() {
-    return $this->get('changed')->value;
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/modules/user/src/Entity/User.php b/core/modules/user/src/Entity/User.php
index 781061a7f287..1dcdefbef799 100644
--- a/core/modules/user/src/Entity/User.php
+++ b/core/modules/user/src/Entity/User.php
@@ -389,13 +389,6 @@ public function setUsername($username) {
     return $this;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getChangedTime() {
-    return $this->get('changed')->value;
-  }
-
   /**
    * {@inheritdoc}
    */
-- 
GitLab