From e152fefcdff53e466946cd7ec14a2e03a31466ef Mon Sep 17 00:00:00 2001
From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org>
Date: Wed, 4 Feb 2015 11:59:36 +0000
Subject: [PATCH] Issue #2414991 by alexpott: Prevent
 hook_config_schema_info_alter from adding or removing definitions

---
 .../Schema/ConfigSchemaAlterException.php     | 14 +++++
 .../Drupal/Core/Config/TypedConfigManager.php | 25 +++++++++
 .../Core/Plugin/DefaultPluginManager.php      | 16 ++++--
 .../config/src/Tests/ConfigSchemaTest.php     | 51 +++++++++++++++++++
 .../schema/config_schema_test.schema.yml      |  2 +
 .../config_schema_test.module                 | 23 +++++++++
 core/modules/system/system.api.php            |  7 ++-
 7 files changed, 133 insertions(+), 5 deletions(-)
 create mode 100644 core/lib/Drupal/Core/Config/Schema/ConfigSchemaAlterException.php
 create mode 100644 core/modules/config/tests/config_schema_test/config_schema_test.module

diff --git a/core/lib/Drupal/Core/Config/Schema/ConfigSchemaAlterException.php b/core/lib/Drupal/Core/Config/Schema/ConfigSchemaAlterException.php
new file mode 100644
index 000000000000..fa87e06d9643
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/Schema/ConfigSchemaAlterException.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Config\Schema\ConfigSchemaAlterException.
+ */
+
+namespace Drupal\Core\Config\Schema;
+
+/**
+ * Exception for when hook_config_schema_info_alter() adds or removes schema.
+ */
+class ConfigSchemaAlterException extends \RuntimeException {
+}
diff --git a/core/lib/Drupal/Core/Config/TypedConfigManager.php b/core/lib/Drupal/Core/Config/TypedConfigManager.php
index a78436404a06..22f43c1db873 100644
--- a/core/lib/Drupal/Core/Config/TypedConfigManager.php
+++ b/core/lib/Drupal/Core/Config/TypedConfigManager.php
@@ -8,7 +8,9 @@
 namespace Drupal\Core\Config;
 
 use Drupal\Component\Utility\NestedArray;
+use Drupal\Component\Utility\String;
 use Drupal\Core\Cache\CacheBackendInterface;
+use Drupal\Core\Config\Schema\ConfigSchemaAlterException;
 use Drupal\Core\Config\Schema\ConfigSchemaDiscovery;
 use Drupal\Core\Config\Schema\Element;
 use Drupal\Core\Extension\ModuleHandlerInterface;
@@ -296,6 +298,29 @@ public function hasConfigSchema($name) {
     return is_array($definition) && ($definition['class'] != '\Drupal\Core\Config\Schema\Undefined');
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  protected function alterDefinitions(&$definitions) {
+    $discovered_schema = array_keys($definitions);
+    parent::alterDefinitions($definitions);
+    $altered_schema = array_keys($definitions);
+    if ($discovered_schema != $altered_schema) {
+      $added_keys = array_diff($altered_schema, $discovered_schema);
+      $removed_keys = array_diff($discovered_schema, $altered_schema);
+      if (!empty($added_keys) && !empty($removed_keys)) {
+        $message = 'Invoking hook_config_schema_info_alter() has added (@added) and removed (@removed) schema definitions';
+      }
+      elseif (!empty($added_keys)) {
+        $message = 'Invoking hook_config_schema_info_alter() has added (@added) schema definitions';
+      }
+      else {
+        $message = 'Invoking hook_config_schema_info_alter() has removed (@removed) schema definitions';
+      }
+      throw new ConfigSchemaAlterException(String::format($message, ['@added' => implode(',', $added_keys), '@removed' => implode(',', $removed_keys)]));
+    }
+  }
+
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php
index 95928e4e24ed..5c37aa99d7bd 100644
--- a/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php
+++ b/core/lib/Drupal/Core/Plugin/DefaultPluginManager.php
@@ -224,9 +224,7 @@ protected function findDefinitions() {
     foreach ($definitions as $plugin_id => &$definition) {
       $this->processDefinition($definition, $plugin_id);
     }
-    if ($this->alterHook) {
-      $this->moduleHandler->alter($this->alterHook, $definitions);
-    }
+    $this->alterDefinitions($definitions);
     // If this plugin was provided by a module that does not exist, remove the
     // plugin definition.
     foreach ($definitions as $plugin_id => $plugin_definition) {
@@ -242,4 +240,16 @@ protected function findDefinitions() {
     return $definitions;
   }
 
+  /**
+   * Invokes the hook to alter the definitions if the alter hook is set.
+   *
+   * @param $definitions
+   *   The discovered plugin defintions.
+   */
+  protected function alterDefinitions(&$definitions) {
+    if ($this->alterHook) {
+      $this->moduleHandler->alter($this->alterHook, $definitions);
+    }
+  }
+
 }
diff --git a/core/modules/config/src/Tests/ConfigSchemaTest.php b/core/modules/config/src/Tests/ConfigSchemaTest.php
index c7a6f55513ae..a2a3510e771b 100644
--- a/core/modules/config/src/Tests/ConfigSchemaTest.php
+++ b/core/modules/config/src/Tests/ConfigSchemaTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Core\Config\FileStorage;
 use Drupal\Core\Config\InstallStorage;
+use Drupal\Core\Config\Schema\ConfigSchemaAlterException;
 use Drupal\Core\TypedData\Type\IntegerInterface;
 use Drupal\Core\TypedData\Type\StringInterface;
 use Drupal\simpletest\KernelTestBase;
@@ -436,4 +437,54 @@ function testColonsInSchemaTypeDetermination() {
     $this->assertEqual($definition['type'], 'test_with_parents.plugin_types.*');
   }
 
+  /**
+   * Tests hook_config_schema_info_alter().
+   */
+  public function testConfigSchemaInfoAlter() {
+    /** @var \Drupal\Core\Config\TypedConfigManagerInterface $typed_config */
+    $typed_config = \Drupal::service('config.typed');
+    $typed_config->clearCachedDefinitions();
+
+    // Ensure that keys can not be added or removed by
+    // hook_config_schema_info_alter().
+    \Drupal::state()->set('config_schema_test_exception_remove', TRUE);
+    $message = 'Expected ConfigSchemaAlterException thrown.';
+    try {
+      $typed_config->getDefinitions();
+      $this->fail($message);
+    }
+    catch (ConfigSchemaAlterException $e) {
+      $this->pass($message);
+      $this->assertEqual($e->getMessage(), 'Invoking hook_config_schema_info_alter() has removed (config_schema_test.hook) schema definitions');
+    }
+
+    \Drupal::state()->set('config_schema_test_exception_add', TRUE);
+    $message = 'Expected ConfigSchemaAlterException thrown.';
+    try {
+      $typed_config->getDefinitions();
+      $this->fail($message);
+    }
+    catch (ConfigSchemaAlterException $e) {
+      $this->pass($message);
+      $this->assertEqual($e->getMessage(), 'Invoking hook_config_schema_info_alter() has added (config_schema_test.hook_added_defintion) and removed (config_schema_test.hook) schema definitions');
+    }
+
+    \Drupal::state()->set('config_schema_test_exception_remove', FALSE);
+    $message = 'Expected ConfigSchemaAlterException thrown.';
+    try {
+      $typed_config->getDefinitions();
+      $this->fail($message);
+    }
+    catch (ConfigSchemaAlterException $e) {
+      $this->pass($message);
+      $this->assertEqual($e->getMessage(), 'Invoking hook_config_schema_info_alter() has added (config_schema_test.hook_added_defintion) schema definitions');
+    }
+
+    // Tests that hook_config_schema_info_alter() can add additional metadata to
+    // existing configuration schema.
+    \Drupal::state()->set('config_schema_test_exception_add', FALSE);
+    $definitions = $typed_config->getDefinitions();
+    $this->assertEqual($definitions['config_schema_test.hook']['additional_metadata'], 'new schema info');
+  }
+
 }
diff --git a/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml b/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml
index cbc3c6190023..719865a47d24 100644
--- a/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml
+++ b/core/modules/config/tests/config_schema_test/config/schema/config_schema_test.schema.yml
@@ -207,3 +207,5 @@ test_with_parents.plugin_types.*:
     value:
       type: string
 
+config_schema_test.hook:
+  type: string
diff --git a/core/modules/config/tests/config_schema_test/config_schema_test.module b/core/modules/config/tests/config_schema_test/config_schema_test.module
new file mode 100644
index 000000000000..3ce1170222e1
--- /dev/null
+++ b/core/modules/config/tests/config_schema_test/config_schema_test.module
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * @file
+ * Tests configuration schema functionality.
+ */
+
+/**
+ * Implements hook_config_schema_info_alter().
+ */
+function config_schema_test_config_schema_info_alter(&$definitions) {
+  if (\Drupal::state()->get('config_schema_test_exception_add')) {
+    $definitions['config_schema_test.hook_added_defintion'] = $definitions['config_schema_test.hook'];
+  }
+  if (\Drupal::state()->get('config_schema_test_exception_remove')) {
+    unset($definitions['config_schema_test.hook']);
+  }
+
+  // Since code can not be unloaded only alter the definition if it exists.
+  if (isset($definitions['config_schema_test.hook'])) {
+    $definitions['config_schema_test.hook']['additional_metadata'] = 'new schema info';
+  }
+}
diff --git a/core/modules/system/system.api.php b/core/modules/system/system.api.php
index cfdf5e2b2106..eb4eb9bebebf 100644
--- a/core/modules/system/system.api.php
+++ b/core/modules/system/system.api.php
@@ -580,8 +580,8 @@ function hook_config_import_steps_alter(&$sync_steps, \Drupal\Core\Config\Config
  * configuration schema type to change default labels or form element renderers
  * used for configuration translation.
  *
- * It is strongly advised not to use this hook to add new data types or to
- * change the structure of existing ones. Keep in mind that there are tools
+ * If implementations of this hook add or remove configuration schema a
+ * ConfigSchemaAlterException will be thrown. Keep in mind that there are tools
  * that may use the configuration schema for static analysis of configuration
  * files, like the string extractor for the localization system. Such systems
  * won't work with dynamically defined configuration schemas.
@@ -591,6 +591,9 @@ function hook_config_import_steps_alter(&$sync_steps, \Drupal\Core\Config\Config
  * @param $definitions
  *   Associative array of configuration type definitions keyed by schema type
  *   names. The elements are themselves array with information about the type.
+ *
+ * @see \Drupal\Core\Config\TypedConfigManager
+ * @see \Drupal\Core\Config\Schema\ConfigSchemaAlterException
  */
 function hook_config_schema_info_alter(&$definitions) {
   // Enhance the text and date type definitions with classes to generate proper
-- 
GitLab