From 50b5b4d6f85d27a86d0333ec6e138259537c32a9 Mon Sep 17 00:00:00 2001
From: catch <catch@35733.no-reply.drupal.org>
Date: Mon, 16 Dec 2019 15:18:16 +0000
Subject: [PATCH] Issue #3092714 by alexpott, hchonov, Berdir, gease: Config
 entity updater misbehaves when updating multiple entity types

---
 .../Config/Entity/ConfigEntityUpdater.php     | 24 ++++++++++----
 core/modules/system/system.post_update.php    | 31 +++++++++++++++++--
 core/modules/text/text.post_update.php        | 28 +++++++++++------
 .../Config/Entity/ConfigEntityUpdaterTest.php | 25 +++++++++++----
 4 files changed, 84 insertions(+), 24 deletions(-)

diff --git a/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php b/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php
index 571b9914250a..7ba77000aca1 100644
--- a/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php
+++ b/core/lib/Drupal/Core/Config/Entity/ConfigEntityUpdater.php
@@ -25,6 +25,11 @@
  */
 class ConfigEntityUpdater implements ContainerInjectionInterface {
 
+  /**
+   * The key used to store information in the update sandbox.
+   */
+  const SANDBOX_KEY = 'config_entity_updater';
+
   /**
    * The entity type manager.
    *
@@ -88,17 +93,24 @@ public static function create(ContainerInterface $container) {
    * @throws \InvalidArgumentException
    *   Thrown when the provided entity type ID is not a configuration entity
    *   type.
+   * @throws \RuntimeException
+   *   Thrown when used twice in the same update function for different entity
+   *   types. This method should only be called once per update function.
    */
   public function update(array &$sandbox, $entity_type_id, callable $callback = NULL) {
     $storage = $this->entityTypeManager->getStorage($entity_type_id);
-    $sandbox_key = 'config_entity_updater:' . $entity_type_id;
-    if (!isset($sandbox[$sandbox_key])) {
+
+    if (isset($sandbox[self::SANDBOX_KEY]) && $sandbox[self::SANDBOX_KEY]['entity_type'] !== $entity_type_id) {
+      throw new \RuntimeException('Updating multiple entity types in the same update function is not supported');
+    }
+    if (!isset($sandbox[self::SANDBOX_KEY])) {
       $entity_type = $this->entityTypeManager->getDefinition($entity_type_id);
       if (!($entity_type instanceof ConfigEntityTypeInterface)) {
         throw new \InvalidArgumentException("The provided entity type ID '$entity_type_id' is not a configuration entity type");
       }
-      $sandbox[$sandbox_key]['entities'] = $storage->getQuery()->accessCheck(FALSE)->execute();
-      $sandbox[$sandbox_key]['count'] = count($sandbox[$sandbox_key]['entities']);
+      $sandbox[self::SANDBOX_KEY]['entity_type'] = $entity_type_id;
+      $sandbox[self::SANDBOX_KEY]['entities'] = $storage->getQuery()->accessCheck(FALSE)->execute();
+      $sandbox[self::SANDBOX_KEY]['count'] = count($sandbox[self::SANDBOX_KEY]['entities']);
     }
 
     // The default behaviour is to fix dependencies.
@@ -111,7 +123,7 @@ public function update(array &$sandbox, $entity_type_id, callable $callback = NU
     }
 
     /** @var \Drupal\Core\Config\Entity\ConfigEntityInterface $entity */
-    $entities = $storage->loadMultiple(array_splice($sandbox[$sandbox_key]['entities'], 0, $this->batchSize));
+    $entities = $storage->loadMultiple(array_splice($sandbox[self::SANDBOX_KEY]['entities'], 0, $this->batchSize));
     foreach ($entities as $entity) {
       if (call_user_func($callback, $entity)) {
         $entity->trustData();
@@ -119,7 +131,7 @@ public function update(array &$sandbox, $entity_type_id, callable $callback = NU
       }
     }
 
-    $sandbox['#finished'] = empty($sandbox[$sandbox_key]['entities']) ? 1 : ($sandbox[$sandbox_key]['count'] - count($sandbox[$sandbox_key]['entities'])) / $sandbox[$sandbox_key]['count'];
+    $sandbox['#finished'] = empty($sandbox[self::SANDBOX_KEY]['entities']) ? 1 : ($sandbox[self::SANDBOX_KEY]['count'] - count($sandbox[self::SANDBOX_KEY]['entities'])) / $sandbox[self::SANDBOX_KEY]['count'];
   }
 
 }
diff --git a/core/modules/system/system.post_update.php b/core/modules/system/system.post_update.php
index 114d28912022..242ad9d1b551 100644
--- a/core/modules/system/system.post_update.php
+++ b/core/modules/system/system.post_update.php
@@ -150,7 +150,7 @@ function system_post_update_language_item_callback() {
 }
 
 /**
- * Update all entity displays that contain extra fields.
+ * Update all entity view displays that contain extra fields.
  */
 function system_post_update_extra_fields(&$sandbox = NULL) {
   $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class);
@@ -174,10 +174,37 @@ function system_post_update_extra_fields(&$sandbox = NULL) {
     return $needs_save;
   };
 
-  $config_entity_updater->update($sandbox, 'entity_form_display', $callback);
   $config_entity_updater->update($sandbox, 'entity_view_display', $callback);
 }
 
+/**
+ * Update all entity form displays that contain extra fields.
+ */
+function system_post_update_extra_fields_form_display(&$sandbox = NULL) {
+  $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class);
+  $entity_field_manager = \Drupal::service('entity_field.manager');
+
+  $callback = function (EntityDisplayInterface $display) use ($entity_field_manager) {
+    $display_context = $display instanceof EntityViewDisplayInterface ? 'display' : 'form';
+    $extra_fields = $entity_field_manager->getExtraFields($display->getTargetEntityTypeId(), $display->getTargetBundle());
+
+    // If any extra fields are used as a component, resave the display with the
+    // updated component information.
+    $needs_save = FALSE;
+    if (!empty($extra_fields[$display_context])) {
+      foreach ($extra_fields[$display_context] as $name => $extra_field) {
+        if ($component = $display->getComponent($name)) {
+          $display->setComponent($name, $component);
+          $needs_save = TRUE;
+        }
+      }
+    }
+    return $needs_save;
+  };
+
+  $config_entity_updater->update($sandbox, 'entity_form_display', $callback);
+}
+
 /**
  * Force cache clear to ensure aggregated JavaScript files are regenerated.
  *
diff --git a/core/modules/text/text.post_update.php b/core/modules/text/text.post_update.php
index 5c98e26b0062..1ea9f7eab77b 100644
--- a/core/modules/text/text.post_update.php
+++ b/core/modules/text/text.post_update.php
@@ -11,10 +11,27 @@
 use Drupal\text\Plugin\Field\FieldWidget\TextareaWithSummaryWidget;
 
 /**
- * Update text_with_summary fields and widgets to add summary required flags.
+ * Update text_with_summary fields to add summary required flags.
  */
 function text_post_update_add_required_summary_flag(&$sandbox = NULL) {
   $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class);
+
+  $field_callback = function (FieldConfigInterface $field) {
+    if ($field->getType() !== 'text_with_summary') {
+      return FALSE;
+    }
+    $field->setSetting('required_summary', FALSE);
+    return TRUE;
+  };
+
+  $config_entity_updater->update($sandbox, 'field_config', $field_callback);
+}
+
+/**
+ * Update text_with_summary widgets to add summary required flags.
+ */
+function text_post_update_add_required_summary_flag_form_display(&$sandbox = NULL) {
+  $config_entity_updater = \Drupal::classResolver(ConfigEntityUpdater::class);
   /** @var \Drupal\Core\Field\WidgetPluginManager $field_widget_manager */
   $field_widget_manager = \Drupal::service('plugin.manager.field.widget');
 
@@ -36,14 +53,5 @@ function text_post_update_add_required_summary_flag(&$sandbox = NULL) {
     return $needs_save;
   };
 
-  $field_callback = function (FieldConfigInterface $field) {
-    if ($field->getType() !== 'text_with_summary') {
-      return FALSE;
-    }
-    $field->setSetting('required_summary', FALSE);
-    return TRUE;
-  };
-
   $config_entity_updater->update($sandbox, 'entity_form_display', $widget_callback);
-  $config_entity_updater->update($sandbox, 'field_config', $field_callback);
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Config/Entity/ConfigEntityUpdaterTest.php b/core/tests/Drupal/KernelTests/Core/Config/Entity/ConfigEntityUpdaterTest.php
index f76578f99cf5..313e58b45762 100644
--- a/core/tests/Drupal/KernelTests/Core/Config/Entity/ConfigEntityUpdaterTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Config/Entity/ConfigEntityUpdaterTest.php
@@ -58,8 +58,9 @@ public function testUpdate() {
     $this->assertEquals('config_test_9', $entities['config_test_9']->label());
     $this->assertEquals('config_test_10', $entities['config_test_10']->label());
     $this->assertEquals('config_test_14', $entities['config_test_14']->label());
-    $this->assertEquals(15, $sandbox['config_entity_updater:config_test']['count']);
-    $this->assertCount(5, $sandbox['config_entity_updater:config_test']['entities']);
+    $this->assertEquals(15, $sandbox['config_entity_updater']['count']);
+    $this->assertEquals('config_test', $sandbox['config_entity_updater']['entity_type']);
+    $this->assertCount(5, $sandbox['config_entity_updater']['entities']);
     $this->assertEquals(10 / 15, $sandbox['#finished']);
 
     // Update the rest.
@@ -70,7 +71,7 @@ public function testUpdate() {
     $this->assertEquals('config_test_10 (updated)', $entities['config_test_10']->label());
     $this->assertEquals('config_test_14 (updated)', $entities['config_test_14']->label());
     $this->assertEquals(1, $sandbox['#finished']);
-    $this->assertCount(0, $sandbox['config_entity_updater:config_test']['entities']);
+    $this->assertCount(0, $sandbox['config_entity_updater']['entities']);
   }
 
   /**
@@ -100,8 +101,8 @@ public function testUpdateDefaultCallback() {
     $this->assertEquals(['added_dependency'], $entities['config_test_8']->getDependencies()['module']);
     $this->assertEquals([], $entities['config_test_9']->getDependencies());
     $this->assertEquals([], $entities['config_test_14']->getDependencies());
-    $this->assertEquals(15, $sandbox['config_entity_updater:config_test']['count']);
-    $this->assertCount(6, $sandbox['config_entity_updater:config_test']['entities']);
+    $this->assertEquals(15, $sandbox['config_entity_updater']['count']);
+    $this->assertCount(6, $sandbox['config_entity_updater']['entities']);
     $this->assertEquals(9 / 15, $sandbox['#finished']);
 
     // Update the rest.
@@ -110,7 +111,7 @@ public function testUpdateDefaultCallback() {
     $this->assertEquals(['added_dependency'], $entities['config_test_9']->getDependencies()['module']);
     $this->assertEquals(['added_dependency'], $entities['config_test_14']->getDependencies()['module']);
     $this->assertEquals(1, $sandbox['#finished']);
-    $this->assertCount(0, $sandbox['config_entity_updater:config_test']['entities']);
+    $this->assertCount(0, $sandbox['config_entity_updater']['entities']);
   }
 
   /**
@@ -125,4 +126,16 @@ public function testUpdateException() {
     $updater->update($sandbox, 'entity_test_mul_changed');
   }
 
+  /**
+   * @covers ::update
+   */
+  public function testUpdateOncePerUpdateException() {
+    $this->expectException(\RuntimeException::class);
+    $this->expectExceptionMessage('Updating multiple entity types in the same update function is not supported');
+    $updater = $this->container->get('class_resolver')->getInstanceFromDefinition(ConfigEntityUpdater::class);
+    $sandbox = [];
+    $updater->update($sandbox, 'config_test');
+    $updater->update($sandbox, 'config_query_test');
+  }
+
 }
-- 
GitLab