diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml index fd68d501ae8f89d420d193d441272037da003495..51b7a36f9cd83065400ab8fbd463e95654b7393f 100644 --- a/core/config/schema/core.data_types.schema.yml +++ b/core/config/schema/core.data_types.schema.yml @@ -198,6 +198,10 @@ filter: id: type: string label: 'ID' + constraints: + PluginExists: + manager: plugin.manager.filter + interface: 'Drupal\filter\Plugin\FilterInterface' provider: type: string label: 'Provider' @@ -425,6 +429,10 @@ condition.plugin: id: type: string label: 'ID' + constraints: + PluginExists: + manager: plugin.manager.condition + interface: 'Drupal\Core\Condition\ConditionInterface' negate: type: boolean label: 'Negate' @@ -527,6 +535,10 @@ field_config_base: field_type: type: string label: 'Field type' + constraints: + PluginExists: + manager: plugin.manager.field.field_type + interface: '\Drupal\Core\Field\FieldItemInterface' core.base_field_override.*.*.*: type: field_config_base @@ -711,6 +723,13 @@ field.field_settings.entity_reference: handler: type: string label: 'Reference method' + constraints: + PluginExists: + manager: plugin.manager.entity_reference_selection + interface: 'Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface' + # @todo Remove this line and explicitly require valid entity reference + # selection plugin IDs in https://drupal.org/i/3420198. + allowFallback: true handler_settings: type: entity_reference_selection.[%parent.handler] label: 'Entity reference selection plugin settings' diff --git a/core/config/schema/core.entity.schema.yml b/core/config/schema/core.entity.schema.yml index fa7114d3049941d9ba5fa954642515ce429f7263..4b1cfb701d6a7bcbd518f000021715160653bc0d 100644 --- a/core/config/schema/core.entity.schema.yml +++ b/core/config/schema/core.entity.schema.yml @@ -78,6 +78,10 @@ field_formatter: type: type: string label: 'Format type machine name' + constraints: + PluginExists: + manager: plugin.manager.field.formatter + interface: 'Drupal\Core\Field\FormatterInterface' label: type: string label: 'Label setting machine name' @@ -135,6 +139,10 @@ core.entity_form_display.*.*.*: type: type: string label: 'Widget type machine name' + constraints: + PluginExists: + manager: plugin.manager.field.widget + interface: '\Drupal\Core\Field\WidgetInterface' weight: type: integer label: 'Weight' diff --git a/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraint.php b/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraint.php index b2baceedb7cadedb36b8d765fb9f7b523dbc152a..f0ccf4f8735b856bd15f753c8b870b4f4ef5d794 100644 --- a/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraint.php +++ b/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraint.php @@ -48,6 +48,13 @@ class PluginExistsConstraint extends Constraint implements ContainerFactoryPlugi */ public ?string $interface = NULL; + /** + * Whether or not to consider fallback plugin IDs as valid. + * + * @var bool + */ + public bool $allowFallback = FALSE; + /** * Constructs a PluginExistsConstraint. * diff --git a/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php b/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php index b862bc339dfee8323b1609382907ae20f7a87be0..ac11a43a78deb65d81f6fa47e4e0eb43a63bf048 100644 --- a/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php +++ b/core/lib/Drupal/Core/Plugin/Plugin/Validation/Constraint/PluginExistsConstraintValidator.php @@ -25,8 +25,11 @@ public function validate(mixed $plugin_id, Constraint $constraint) { } $definition = $constraint->pluginManager->getDefinition($plugin_id, FALSE); - // Some plugin managers provide fallbacks. - if ($constraint->pluginManager instanceof FallbackPluginManagerInterface) { + // Some plugin managers provide fallbacks. In most cases, the use of a + // fallback plugin ID suggests that the given plugin ID is invalid in some + // way, so by default, we don't consider fallback plugin IDs as valid, + // although that can be overridden by the `allowFallback` option if needed. + if ($constraint->pluginManager instanceof FallbackPluginManagerInterface && $constraint->allowFallback) { $fallback_plugin_id = $constraint->pluginManager->getFallbackPluginId($plugin_id); $definition = $constraint->pluginManager->getDefinition($fallback_plugin_id, FALSE); } diff --git a/core/modules/block/config/schema/block.schema.yml b/core/modules/block/config/schema/block.schema.yml index 8989c1d79efe0f6bb3fbe90b04c1fba8db4e327f..f82c157ebc863c17f4de31b34c14109d511605f3 100644 --- a/core/modules/block/config/schema/block.schema.yml +++ b/core/modules/block/config/schema/block.schema.yml @@ -33,6 +33,10 @@ block.block.*: PluginExists: manager: plugin.manager.block interface: Drupal\Core\Block\BlockPluginInterface + # Block plugin IDs may not be valid in blocks that are backed by + # block_content entities that don't exist yet. Therefore, it's okay + # to consider the fallback plugin ID as valid. + allowFallback: true settings: type: block.settings.[%parent.plugin] visibility: diff --git a/core/modules/comment/config/optional/views.view.comments_recent.yml b/core/modules/comment/config/optional/views.view.comments_recent.yml index 5e15280193612352835d062a9cb199047afe7eba..7c4d8c631f53cac17f3947084b1cb35f364ae4c6 100644 --- a/core/modules/comment/config/optional/views.view.comments_recent.yml +++ b/core/modules/comment/config/optional/views.view.comments_recent.yml @@ -182,7 +182,7 @@ display: admin_label: '' entity_type: comment entity_field: cid - plugin_id: field + plugin_id: standard order: DESC expose: label: '' diff --git a/core/modules/field/config/schema/field.schema.yml b/core/modules/field/config/schema/field.schema.yml index ca182c42abf929852b67feaaedb24ec7ca70eb37..a7ec988b1af90de164c5dfec8007edf8542d56e7 100644 --- a/core/modules/field/config/schema/field.schema.yml +++ b/core/modules/field/config/schema/field.schema.yml @@ -24,6 +24,10 @@ field.storage.*.*: type: type: string label: 'Type' + constraints: + PluginExists: + manager: plugin.manager.field.field_type + interface: '\Drupal\Core\Field\FieldItemInterface' settings: type: field.storage_settings.[%parent.type] module: diff --git a/core/modules/field/tests/modules/field_test/field_test.module b/core/modules/field/tests/modules/field_test/field_test.module index f6b2dbf8144879178ba1ecfca676522dea9bc0e1..34d590bbd2ca2ffb4c9a7ec1016621bc3a645ec2 100644 --- a/core/modules/field/tests/modules/field_test/field_test.module +++ b/core/modules/field/tests/modules/field_test/field_test.module @@ -225,3 +225,9 @@ function field_test_field_info_entity_type_ui_definitions_alter(array &$ui_defin $ui_definitions['boolean']['label'] = new TranslatableMarkup('Boolean (overridden by alter)'); } } + +function field_test_entity_reference_selection_alter(array &$definitions): void { + if (\Drupal::state()->get('field_test_disable_broken_entity_reference_handler')) { + unset($definitions['broken']); + } +} diff --git a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidget.php b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidget.php index ef4efd2078cbd88e052d1ec4e290c3f0317460f3..7447623cbdc82d47dc28e80eafb0b2429023cc46 100644 --- a/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidget.php +++ b/core/modules/field/tests/modules/field_test/src/Plugin/Field/FieldWidget/TestFieldWidget.php @@ -14,6 +14,7 @@ * id = "test_field_widget", * label = @Translation("Test widget"), * field_types = { + * "field_test", * "test_field", * "hidden_test_field", * "test_field_with_preconfigured_options" diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php index 2378e1308567ed2c11f09ece46d8f9295f61b801..45a7b787412f869bb3b2634af6344621cfb6cd7a 100644 --- a/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php +++ b/core/modules/field/tests/src/Kernel/Entity/FieldConfigValidationTest.php @@ -120,6 +120,7 @@ public function testImmutableProperties(array $valid_values = []): void { parent::testImmutableProperties([ 'entity_type' => 'entity_test_with_bundle', 'bundle' => 'another', + 'field_type' => 'string', ]); } @@ -149,4 +150,40 @@ public function testRequiredPropertyValuesMissing(?array $additional_expected_va ]); } + /** + * Tests that the field type plugin's existence is validated. + */ + public function testFieldTypePluginIsValidated(): void { + // The `field_type` property is immutable, so we need to clone the entity in + // order to cleanly change its immutable properties. + $this->entity = $this->entity->createDuplicate() + // We need to clear the current settings, or we will get validation errors + // because the old settings are not supported by the new field type. + ->set('settings', []) + ->set('field_type', 'invalid'); + + $this->assertValidationErrors([ + 'field_type' => "The 'invalid' plugin does not exist.", + ]); + } + + /** + * Tests that entity reference selection handler plugin IDs are validated. + */ + public function testEntityReferenceSelectionHandlerIsValidated(): void { + $this->container->get('state') + ->set('field_test_disable_broken_entity_reference_handler', TRUE); + $this->enableModules(['field_test']); + + // The `field_type` property is immutable, so we need to clone the entity in + // order to cleanly change its immutable properties. + $this->entity = $this->entity->createDuplicate() + ->set('field_type', 'entity_reference') + ->set('settings', ['handler' => 'non_existent']); + + $this->assertValidationErrors([ + 'settings.handler' => "The 'non_existent' plugin does not exist.", + ]); + } + } diff --git a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php index 83e0e72d50306693a28828d46fc937d404690ff3..5813bc7a5a39b0052c4cf1769e8e324e6b287dd1 100644 --- a/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php +++ b/core/modules/field/tests/src/Kernel/Entity/FieldStorageConfigValidationTest.php @@ -33,4 +33,26 @@ protected function setUp(): void { $this->entity->save(); } + /** + * {@inheritdoc} + */ + public function testImmutableProperties(array $valid_values = []): void { + $valid_values['type'] = 'string'; + parent::testImmutableProperties($valid_values); + } + + /** + * Tests that the field type plugin's existence is validated. + */ + public function testFieldTypePluginIsValidated(): void { + // The `type` property is immutable, so we need to clone the entity in + // order to cleanly change its immutable properties. + $this->entity = $this->entity->createDuplicate() + ->set('type', 'invalid'); + + $this->assertValidationErrors([ + 'type' => "The 'invalid' plugin does not exist.", + ]); + } + } diff --git a/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php b/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php index 2d0b9b6e507535890f2fdf0fc4cc7b911aa8cca8..c5c473b4ad1b80107f988fb6776883fa552321e1 100644 --- a/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php +++ b/core/modules/field_layout/tests/src/Kernel/FieldLayoutEntityDisplayTest.php @@ -19,6 +19,7 @@ class FieldLayoutEntityDisplayTest extends KernelTestBase { 'field_layout', 'entity_test', 'field_layout_test', + 'field_test', 'system', ]; @@ -34,7 +35,7 @@ public function testPreSave() { 'mode' => 'default', 'status' => TRUE, 'content' => [ - 'foo' => ['type' => 'visible'], + 'foo' => ['type' => 'field_no_settings'], 'name' => ['type' => 'hidden', 'region' => 'content'], ], 'hidden' => [ @@ -60,7 +61,7 @@ public function testPreSave() { 'mode' => 'default', 'content' => [ 'foo' => [ - 'type' => 'visible', + 'type' => 'field_no_settings', ], ], 'hidden' => [ @@ -99,7 +100,7 @@ public function testPreSave() { ]; // The field was moved to the default region. $expected['content']['foo'] = [ - 'type' => 'visible', + 'type' => 'field_no_settings', 'region' => 'main', 'weight' => -4, 'settings' => [], diff --git a/core/modules/field_ui/tests/src/Functional/EntityDisplayFormBaseTest.php b/core/modules/field_ui/tests/src/Functional/EntityDisplayFormBaseTest.php index a2fb83175b324aff13f2f419918c528057ea9410..e5e1a40fb057c2cbecea502a3407addd9e588867 100644 --- a/core/modules/field_ui/tests/src/Functional/EntityDisplayFormBaseTest.php +++ b/core/modules/field_ui/tests/src/Functional/EntityDisplayFormBaseTest.php @@ -47,7 +47,9 @@ protected function setUp(): void { \Drupal::service('entity_display.repository') ->getFormDisplay($entity_type, $entity_type) - ->setComponent('field_test_no_plugin', []) + ->setComponent('field_test_no_plugin', [ + 'type' => 'test_field_widget', + ]) ->save(); } diff --git a/core/modules/image/config/schema/image.schema.yml b/core/modules/image/config/schema/image.schema.yml index d3c9e980f0cf0fa95f11d7c723082c4f78078d86..7855f536702250153fd6fa7ef60621b402b77320 100644 --- a/core/modules/image/config/schema/image.schema.yml +++ b/core/modules/image/config/schema/image.schema.yml @@ -18,6 +18,10 @@ image.style.*: type: uuid id: type: string + constraints: + PluginExists: + manager: plugin.manager.image.effect + interface: 'Drupal\image\ImageEffectInterface' weight: type: integer data: diff --git a/core/modules/layout_builder/config/schema/layout_builder.schema.yml b/core/modules/layout_builder/config/schema/layout_builder.schema.yml index 7bc4461891b5889f1ce25d63b1ebd91c035bd546..0786286f1f9cae4276a94d1194552754789f02dd 100644 --- a/core/modules/layout_builder/config/schema/layout_builder.schema.yml +++ b/core/modules/layout_builder/config/schema/layout_builder.schema.yml @@ -20,6 +20,10 @@ layout_builder.section: layout_id: type: string label: 'Layout ID' + constraints: + PluginExists: + manager: plugin.manager.core.layout + interface: '\Drupal\Core\Layout\LayoutInterface' layout_settings: type: layout_plugin.settings.[%parent.layout_id] label: 'Layout settings' diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml index 1e1bfb5ad3fd442665d05980514ae0949a6a7263..4311b11049c688520e4a1efd086f1ba3e398498c 100644 --- a/core/modules/media/config/schema/media.schema.yml +++ b/core/modules/media/config/schema/media.schema.yml @@ -37,6 +37,10 @@ media.type.*: source: type: string label: 'Source' + constraints: + PluginExists: + manager: plugin.manager.media.source + interface: 'Drupal\media\MediaSourceInterface' queue_thumbnail_downloads: type: boolean label: 'Whether the thumbnail downloads should be queued' diff --git a/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php index 4f8ba575a529788fac72d3d85b7c5649e0ed566a..cd4f46964358685353220b4f4470e4918db718f9 100644 --- a/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php +++ b/core/modules/media/tests/src/Kernel/MediaTypeValidationTest.php @@ -36,7 +36,28 @@ public function testImmutableProperties(array $valid_values = []): void { // settings from the *old* source won't match the config schema for the // settings of the *new* source. $this->entity->set('source_configuration', []); + $valid_values['source'] = 'image'; parent::testImmutableProperties($valid_values); } + /** + * Tests that the media source plugin's existence is validated. + */ + public function testMediaSourceIsValidated(): void { + // The `source` property is immutable, so we need to clone the entity in + // order to cleanly change its immutable properties. + $this->entity = $this->entity->createDuplicate() + // The `id` property is thrown out by createDuplicate(). + ->set('id', 'test') + // We need to clear the current source configuration, or we will get + // validation errors because the old configuration is not supported by the + // new source. + ->set('source_configuration', []) + ->set('source', 'invalid'); + + $this->assertValidationErrors([ + 'source' => "The 'invalid' plugin does not exist.", + ]); + } + } diff --git a/core/modules/node/config/optional/views.view.frontpage.yml b/core/modules/node/config/optional/views.view.frontpage.yml index e5e5bd6d9f7dd1342bde887c5cbd083105f1ffe6..8a46368b1bfc8266df450b4df93b04906278ded8 100644 --- a/core/modules/node/config/optional/views.view.frontpage.yml +++ b/core/modules/node/config/optional/views.view.frontpage.yml @@ -107,7 +107,7 @@ display: admin_label: '' entity_type: node entity_field: sticky - plugin_id: boolean + plugin_id: standard order: DESC expose: label: '' diff --git a/core/modules/rest/config/schema/rest.schema.yml b/core/modules/rest/config/schema/rest.schema.yml index 467ae5b6dc2ce397e152cbf9ada39b80d7ab7002..f7b3b8a8e7e0eeb70d53ca8253c2fbc8b6909e44 100644 --- a/core/modules/rest/config/schema/rest.schema.yml +++ b/core/modules/rest/config/schema/rest.schema.yml @@ -79,6 +79,10 @@ rest.resource.*: plugin_id: type: string label: 'REST resource plugin id' + constraints: + PluginExists: + manager: plugin.manager.rest + interface: 'Drupal\rest\Plugin\ResourceInterface' granularity: type: string label: 'REST resource configuration granularity' diff --git a/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php b/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php index 1859afa5dd5660388f52aace228ce875a8787005..4660ef802ed4003f1199acad93db28261bf31c81 100644 --- a/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php +++ b/core/modules/rest/tests/src/Kernel/Entity/RestResourceConfigValidationTest.php @@ -38,4 +38,14 @@ protected function setUp(): void { $this->entity->save(); } + /** + * Tests that the resource plugin ID is validated. + */ + public function testInvalidPluginId(): void { + $this->entity->set('plugin_id', 'non_existent'); + $this->assertValidationErrors([ + 'plugin_id' => "The 'non_existent' plugin does not exist.", + ]); + } + } diff --git a/core/modules/search/config/schema/search.schema.yml b/core/modules/search/config/schema/search.schema.yml index 9c7a44cd6113e621c06536c0d25dd00ec4cd2af4..f4284a0e12d5b394ae05e5b52f92161b2425411f 100644 --- a/core/modules/search/config/schema/search.schema.yml +++ b/core/modules/search/config/schema/search.schema.yml @@ -86,6 +86,10 @@ search.page.*: plugin: type: string label: 'Plugin' + constraints: + PluginExists: + manager: plugin.manager.search + interface: 'Drupal\search\Plugin\SearchInterface' configuration: type: search.plugin.[%parent.plugin] diff --git a/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php b/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php index eb35264fd3f1b373aa8c43501f75726d92521187..9dd113166404e7d51b2fbc600e2a937db93db0d0 100644 --- a/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php +++ b/core/modules/search/tests/src/Kernel/SearchPageValidationTest.php @@ -31,4 +31,14 @@ protected function setUp(): void { $this->entity->save(); } + /** + * Tests that the search plugin ID is validated. + */ + public function testInvalidPluginId(): void { + $this->entity->set('plugin', 'non_existent'); + $this->assertValidationErrors([ + 'plugin' => "The 'non_existent' plugin does not exist.", + ]); + } + } diff --git a/core/modules/system/config/schema/system.schema.yml b/core/modules/system/config/schema/system.schema.yml index 504acac218e62dcce68ca203446bfd11ca46aafe..d9f36cc507f9c6e68ddd66021b95549b2616ed0a 100644 --- a/core/modules/system/config/schema/system.schema.yml +++ b/core/modules/system/config/schema/system.schema.yml @@ -265,6 +265,10 @@ system.action.*: plugin: type: string label: 'Plugin' + constraints: + PluginExists: + manager: plugin.manager.action + interface: 'Drupal\Core\Action\ActionInterface' configuration: type: action.configuration.[%parent.plugin] @@ -316,6 +320,10 @@ system.mail: sequence: type: string label: 'Interface' + constraints: + PluginExists: + manager: plugin.manager.mail + interface: 'Drupal\Core\Mail\MailInterface' mailer_dsn: type: mapping label: 'Symfony mailer transport DSN' diff --git a/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php index eff928e210a611391ab11a9af5985fff27103ae4..4b034c82306ad37fa54b67067c9f6aee26f43723 100644 --- a/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php +++ b/core/modules/system/tests/src/Kernel/Entity/ActionValidationTest.php @@ -41,4 +41,14 @@ public function providerInvalidMachineNameCharacters(): array { return $cases; } + /** + * Tests that the action plugin ID is validated. + */ + public function testInvalidPluginId(): void { + $this->entity->set('plugin', 'non_existent'); + $this->assertValidationErrors([ + 'plugin' => "The 'non_existent' plugin does not exist.", + ]); + } + } diff --git a/core/modules/tour/config/schema/tour.schema.yml b/core/modules/tour/config/schema/tour.schema.yml index 898198b27be13c48436c744ea147f8a2025bb651..27cbcf7925a9299149bdcc6f4e96e4af992e49cd 100644 --- a/core/modules/tour/config/schema/tour.schema.yml +++ b/core/modules/tour/config/schema/tour.schema.yml @@ -41,6 +41,10 @@ tour.tip: plugin: type: string label: 'Plugin' + constraints: + PluginExists: + manager: plugin.manager.tour.tip + interface: '\Drupal\tour\TipPluginInterface' label: type: required_label label: 'Label' diff --git a/core/modules/views/config/schema/views.data_types.schema.yml b/core/modules/views/config/schema/views.data_types.schema.yml index 03f13c5fc6c2527b1321b9a3da8c7fbbf9209af7..3495ce93fa2bea0bec237620156df2c81404ee59 100644 --- a/core/modules/views/config/schema/views.data_types.schema.yml +++ b/core/modules/views/config/schema/views.data_types.schema.yml @@ -25,6 +25,10 @@ views_display: type: type: string label: 'Pager type' + constraints: + PluginExists: + manager: plugin.manager.views.pager + interface: 'Drupal\views\Plugin\views\pager\PagerPluginBase' options: type: views.pager.[%parent.type] exposed_form: @@ -34,6 +38,10 @@ views_display: type: type: string label: 'Exposed form type' + constraints: + PluginExists: + manager: plugin.manager.views.exposed_form + interface: 'Drupal\views\Plugin\views\exposed_form\ExposedFormPluginInterface' options: label: 'Options' type: views.exposed_form.[%parent.type] @@ -44,6 +52,9 @@ views_display: type: type: string label: 'Access type' + constraints: + PluginExists: + manager: plugin.manager.views.access options: type: views.access.[%parent.type] cache: @@ -88,6 +99,10 @@ views_display: type: type: string label: 'Type' + constraints: + PluginExists: + manager: plugin.manager.views.style + interface: 'Drupal\views\Plugin\views\style\StylePluginBase' options: type: views.style.[%parent.type] row: @@ -97,6 +112,9 @@ views_display: type: type: string label: 'Row type' + constraints: + PluginExists: + manager: plugin.manager.views.row options: type: views.row.[%parent.type] query: @@ -106,6 +124,9 @@ views_display: type: type: string label: 'Query type' + constraints: + PluginExists: + manager: plugin.manager.views.query options: type: views.query.[%parent.type] defaults: @@ -274,6 +295,12 @@ views_sort: plugin_id: type: string label: 'Plugin ID' + constraints: + PluginExists: + manager: plugin.manager.views.sort + # @todo Remove this line and fix all views in core which use invalid + # sort plugins in https://drupal.org/i/3387325. + allowFallback: true views_sort_expose: type: mapping @@ -298,6 +325,12 @@ views_area: plugin_id: type: string label: 'Plugin ID' + constraints: + PluginExists: + manager: plugin.manager.views.area + # @todo Remove this line and fix all views in core which use invalid + # area plugins in https://drupal.org/i/3387325. + allowFallback: true views_handler: type: mapping @@ -359,6 +392,9 @@ views_argument: default_argument_type: type: string label: 'Type' + constraints: + PluginExists: + manager: plugin.manager.views.argument_default default_argument_options: type: views.argument_default.[%parent.default_argument_type] label: 'Default argument options' @@ -388,6 +424,9 @@ views_argument: type: type: string label: 'Validator' + constraints: + PluginExists: + manager: plugin.manager.views.argument_validator fail: type: string label: 'Action to take if filter value does not validate' @@ -415,6 +454,12 @@ views_argument: plugin_id: type: string label: 'Plugin ID' + constraints: + PluginExists: + manager: plugin.manager.views.argument + # @todo Remove this line and fix all views in core which use invalid + # argument plugins in https://drupal.org/i/3387325. + allowFallback: true views_exposed_form: type: mapping @@ -574,6 +619,12 @@ views_field: plugin_id: type: string label: 'Plugin ID' + constraints: + PluginExists: + manager: plugin.manager.views.field + # @todo Remove this line and fix all views in core which use invalid + # field plugins in https://drupal.org/i/3387325. + allowFallback: true views_pager: type: mapping @@ -780,6 +831,12 @@ views_filter: plugin_id: type: string label: 'Plugin ID' + constraints: + PluginExists: + manager: plugin.manager.views.filter + # @todo Remove this line and fix all views in core which use invalid + # filter plugins in https://drupal.org/i/3387325. + allowFallback: true views_filter_group_item: type: mapping @@ -804,6 +861,15 @@ views_relationship: required: type: boolean label: 'Require this relationship' + plugin_id: + type: string + label: 'The plugin ID' + constraints: + PluginExists: + manager: plugin.manager.views.relationship + # @todo Remove this line and fix all views in core which use invalid + # relationship plugins in https://drupal.org/i/3387325. + allowFallback: true views_query: type: mapping @@ -831,6 +897,9 @@ views_cache: type: type: string label: 'Cache type' + constraints: + PluginExists: + manager: plugin.manager.views.cache views_display_extender: type: mapping diff --git a/core/modules/views/config/schema/views.schema.yml b/core/modules/views/config/schema/views.schema.yml index c279ee0d23f4622479a4eabe2b65c85f6d59fec8..8ddd73931f2c2d2c3ee2ed35efdddbf141daa5b8 100644 --- a/core/modules/views/config/schema/views.schema.yml +++ b/core/modules/views/config/schema/views.schema.yml @@ -108,6 +108,9 @@ views.view.*: display_plugin: type: string label: 'Display plugin' + constraints: + PluginExists: + manager: plugin.manager.views.display position: type: integer label: 'Position' diff --git a/core/modules/views/tests/modules/views_test_config/config/schema/views_test_config.views.schema.yml b/core/modules/views/tests/modules/views_test_config/config/schema/views_test_config.views.schema.yml index 199cdfcbb74f99a7da920110f30b536e84e7a530..d214c19cc22a5ad13d7194be6ef91b8dbe5733ec 100644 --- a/core/modules/views/tests/modules/views_test_config/config/schema/views_test_config.views.schema.yml +++ b/core/modules/views/tests/modules/views_test_config/config/schema/views_test_config.views.schema.yml @@ -10,3 +10,6 @@ views.area.test_example: custom_access: type: boolean label: 'Should access to the area be allowed' + +views.cache.non_existent: + type: views_cache diff --git a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml index 67c8ad03b24b5ce872f7e0790eab3d939fbae3bf..d68ce0ebe4b6d373208b8bbbdd9cacdd8074e3b0 100644 --- a/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml +++ b/core/modules/views/tests/modules/views_test_config/test_views/views.view.test_view.yml @@ -35,7 +35,7 @@ display: id: name relationship: none table: views_test_data - plugin_id: string + plugin_id: standard pager: options: offset: 0 @@ -47,7 +47,7 @@ display: order: ASC relationship: none table: views_test_data - plugin_id: numeric + plugin_id: standard display_plugin: default display_title: Default id: default diff --git a/core/modules/views/tests/modules/views_test_config/views_test_config.module b/core/modules/views/tests/modules/views_test_config/views_test_config.module index 88cbdbcaaf60b30785e81892e54a0d8daff448ae..6884c6e92efc691f7f197915d82ad54af0488c77 100644 --- a/core/modules/views/tests/modules/views_test_config/views_test_config.module +++ b/core/modules/views/tests/modules/views_test_config/views_test_config.module @@ -32,3 +32,33 @@ function views_test_config_views_post_render(ViewExecutable $view, &$output, Cac $output['#cache']['tags'][] = 'foo'; } } + +function _views_test_config_disable_broken_handler(array &$definitions, string $handler_type): void { + if (in_array($handler_type, \Drupal::state()->get('views_test_config_disable_broken_handler', []))) { + unset($definitions['broken']); + } +} + +function views_test_config_views_plugins_area_alter(array &$definitions): void { + _views_test_config_disable_broken_handler($definitions, 'area'); +} + +function views_test_config_views_plugins_argument_alter(array &$definitions): void { + _views_test_config_disable_broken_handler($definitions, 'argument'); +} + +function views_test_config_views_plugins_field_alter(array &$definitions): void { + _views_test_config_disable_broken_handler($definitions, 'field'); +} + +function views_test_config_views_plugins_filter_alter(array &$definitions): void { + _views_test_config_disable_broken_handler($definitions, 'filter'); +} + +function views_test_config_views_plugins_relationship_alter(array &$definitions): void { + _views_test_config_disable_broken_handler($definitions, 'relationship'); +} + +function views_test_config_views_plugins_sort_alter(array &$definitions): void { + _views_test_config_disable_broken_handler($definitions, 'sort'); +} diff --git a/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php b/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php index 2fe755f98a75202668903a36b428097dc0072dea..70bfc4335d180f693d92190f751a8bef37716875 100644 --- a/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php +++ b/core/modules/views/tests/src/Functional/Plugin/DisplayTest.php @@ -30,6 +30,19 @@ class DisplayTest extends ViewTestBase { */ protected static $modules = ['views_ui', 'node', 'block']; + /** + * {@inheritdoc} + */ + protected static $configSchemaCheckerExclusions = [ + // The availability of Views display plugins is validated by the config + // system, but one of our test cases saves a view with an invalid display + // plugin ID, to see how Views handles that. Therefore, allow that one view + // to be saved with an invalid display plugin without angering the config + // schema checker. + // @see ::testInvalidDisplayPlugins() + 'views.view.test_display_invalid', + ]; + /** * {@inheritdoc} */ diff --git a/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php b/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php index 199b2136a49de29d381fdf525da7a57eecc5387c..d6265bb2a14ac1d4028149e43790f1c61f786a00 100644 --- a/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php +++ b/core/modules/views/tests/src/Kernel/Entity/ViewValidationTest.php @@ -2,6 +2,7 @@ namespace Drupal\Tests\views\Kernel\Entity; +use Drupal\Component\Utility\NestedArray; use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase; use Drupal\views\Entity\View; @@ -16,7 +17,7 @@ class ViewValidationTest extends ConfigEntityValidationTestBase { /** * {@inheritdoc} */ - protected static $modules = ['views']; + protected static $modules = ['views', 'views_test_config']; /** * {@inheritdoc} @@ -40,4 +41,54 @@ public function testLabelsAreRequired(): void { $this->assertSame($this->entity->id(), $this->entity->label()); } + /** + * Tests that the various plugin IDs making up a view display are validated. + * + * @param string ...$parents + * The array parents of the property of the view's default display which + * will be set to `non_existent`. + * + * @testWith ["display_plugin"] + * ["display_options", "pager", "type"] + * ["display_options", "exposed_form", "type"] + * ["display_options", "access", "type"] + * ["display_options", "style", "type"] + * ["display_options", "row", "type"] + * ["display_options", "query", "type"] + * ["display_options", "cache", "type"] + * ["display_options", "header", "non_existent", "plugin_id"] + * ["display_options", "footer", "non_existent", "plugin_id"] + * ["display_options", "empty", "non_existent", "plugin_id"] + * ["display_options", "arguments", "non_existent", "plugin_id"] + * ["display_options", "sorts", "non_existent", "plugin_id"] + * ["display_options", "fields", "non_existent", "plugin_id"] + * ["display_options", "filters", "non_existent", "plugin_id"] + * ["display_options", "relationships", "non_existent", "plugin_id"] + */ + public function testInvalidPluginId(string ...$parents): void { + // Disable the `broken` handler plugin, which is used as a fallback for + // non-existent handler plugins. This ensures that when we use an + // invalid handler plugin ID, we will get the expected validation error. + // @todo Remove all this when fallback plugin IDs are not longer allowed by + // Views' config schema. + // @see views_test_config.module + $this->container->get('state') + ->set('views_test_config_disable_broken_handler', [ + 'area', + 'argument', + 'sort', + 'field', + 'filter', + 'relationship', + ]); + $this->container->get('plugin.cache_clearer')->clearCachedDefinitions(); + + $display = &$this->entity->getDisplay('default'); + NestedArray::setValue($display, $parents, 'non_existent'); + $property_path = 'display.default.' . implode('.', $parents); + $this->assertValidationErrors([ + $property_path => "The 'non_existent' plugin does not exist.", + ]); + } + } diff --git a/core/modules/views/tests/src/Kernel/TestViewsTest.php b/core/modules/views/tests/src/Kernel/TestViewsTest.php index 14267cb2dd8537b26458c48e2477335ae3e5fd60..fe9c70ffe0b52d863e3afa1d1e22f47079722abe 100644 --- a/core/modules/views/tests/src/Kernel/TestViewsTest.php +++ b/core/modules/views/tests/src/Kernel/TestViewsTest.php @@ -29,6 +29,7 @@ class TestViewsTest extends KernelTestBase { * @var array */ protected static $modules = [ + 'views', // For NodeType config entities to exist, its module must be installed. 'node', // The `DRUPAL_OPTIONAL` constant is used by the NodeType config entity type @@ -116,6 +117,9 @@ class TestViewsTest extends KernelTestBase { // `history` is a module dependency. // @see core/modules/views/tests/modules/views_test_config/test_views/views.view.test_history.yml 'history', + // The `image` module is required by at least one of the Node module's + // views. + 'image', ]; /** diff --git a/core/modules/views_ui/tests/src/Functional/OverrideDisplaysTest.php b/core/modules/views_ui/tests/src/Functional/OverrideDisplaysTest.php index 8208210e051a6553eb6150e5788e4d01857ef576..f27849f6923d97ea68500a8162529c75eb0ed1c8 100644 --- a/core/modules/views_ui/tests/src/Functional/OverrideDisplaysTest.php +++ b/core/modules/views_ui/tests/src/Functional/OverrideDisplaysTest.php @@ -67,6 +67,7 @@ public function testOverrideDisplays() { $this->assertSession()->pageTextContains($view['label']); // Place the block. + $this->container->get('plugin.manager.block')->clearCachedDefinitions(); $this->drupalPlaceBlock("views_block:{$view['id']}-block_1"); // Make sure the title appears in the block. @@ -132,6 +133,7 @@ public function testWizardMixedDefaultOverriddenDisplays() { // Put the block into the first sidebar region, and make sure it will not // display on the view's page display (since we will be searching for the // presence/absence of the view's title in both the page and the block). + $this->container->get('plugin.manager.block')->clearCachedDefinitions(); $this->drupalPlaceBlock("views_block:{$view['id']}-block_1", [ 'visibility' => [ 'request_path' => [ diff --git a/core/modules/workflows/config/schema/workflows.schema.yml b/core/modules/workflows/config/schema/workflows.schema.yml index 9c032204d8127020dbd702e740a206b87088daea..1d102d0ac442252652309fb9115fd9e9fd596442 100644 --- a/core/modules/workflows/config/schema/workflows.schema.yml +++ b/core/modules/workflows/config/schema/workflows.schema.yml @@ -11,6 +11,10 @@ workflows.workflow.*: type: type: string label: 'Workflow type' + constraints: + PluginExists: + manager: plugin.manager.workflows.type + interface: 'Drupal\workflows\WorkflowTypeInterface' type_settings: type: workflow.type_settings.[%parent.type] diff --git a/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php b/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php index 5f1c558e893fe0b2b494939a62bc8c69fd17c2a9..a840904fe2a7650a3e70a1c3d373588566908352 100644 --- a/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php +++ b/core/modules/workflows/tests/src/Kernel/WorkflowValidationTest.php @@ -32,4 +32,14 @@ protected function setUp(): void { $this->entity->save(); } + /** + * Tests that the workflow type plugin is validated. + */ + public function testTypePluginIsValidated(): void { + $this->entity->set('type', 'non_existent'); + $this->assertValidationErrors([ + 'type' => "The 'non_existent' plugin does not exist.", + ]); + } + } diff --git a/core/profiles/demo_umami/config/install/core.entity_form_display.node.recipe.default.yml b/core/profiles/demo_umami/config/install/core.entity_form_display.node.recipe.default.yml index bd9f57c3160ffbc4d7d373c937dfe34e6d9b6e63..77aaac869b3aca25019ba89f5c44447d7c3cfc47 100644 --- a/core/profiles/demo_umami/config/install/core.entity_form_display.node.recipe.default.yml +++ b/core/profiles/demo_umami/config/install/core.entity_form_display.node.recipe.default.yml @@ -112,12 +112,6 @@ content: settings: include_locked: true third_party_settings: { } - layout_builder__layout: - type: null - weight: 26 - region: content - settings: { } - third_party_settings: { } moderation_state: type: moderation_state_default weight: 20 @@ -174,4 +168,5 @@ content: size: 60 placeholder: '' third_party_settings: { } -hidden: { } +hidden: + layout_builder__layout: true diff --git a/core/profiles/demo_umami/config/install/views.view.frontpage.yml b/core/profiles/demo_umami/config/install/views.view.frontpage.yml index 0c3387aaa6f193fb0f35dbea39551a5321914f20..fbabf2664f69a8d96387502a502ab2a4d54aa8d4 100644 --- a/core/profiles/demo_umami/config/install/views.view.frontpage.yml +++ b/core/profiles/demo_umami/config/install/views.view.frontpage.yml @@ -91,7 +91,7 @@ display: admin_label: '' entity_type: node entity_field: sticky - plugin_id: boolean + plugin_id: standard order: DESC expose: label: '' diff --git a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php index c1c6d2fe6135552243b13bfacf479cbcab10ec0e..edef4e1f6c5f15cecbbf32c412d83b02b6496225 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/ConfigSchemaTest.php @@ -11,6 +11,7 @@ use Drupal\Core\TypedData\Plugin\DataType\StringData; use Drupal\Core\TypedData\Type\IntegerInterface; use Drupal\Core\TypedData\Type\StringInterface; +use Drupal\image\ImageEffectInterface; use Drupal\KernelTests\KernelTestBase; /** @@ -198,6 +199,12 @@ public function testSchemaMapping() { $expected['mapping']['effects']['type'] = 'sequence'; $expected['mapping']['effects']['sequence']['type'] = 'mapping'; $expected['mapping']['effects']['sequence']['mapping']['id']['type'] = 'string'; + $expected['mapping']['effects']['sequence']['mapping']['id']['constraints'] = [ + 'PluginExists' => [ + 'manager' => 'plugin.manager.image.effect', + 'interface' => ImageEffectInterface::class, + ], + ]; $expected['mapping']['effects']['sequence']['mapping']['data']['type'] = 'image.effect.[%parent.id]'; $expected['mapping']['effects']['sequence']['mapping']['weight']['type'] = 'integer'; $expected['mapping']['effects']['sequence']['mapping']['uuid']['type'] = 'uuid'; diff --git a/core/tests/Drupal/KernelTests/Core/Config/SimpleConfigValidationTest.php b/core/tests/Drupal/KernelTests/Core/Config/SimpleConfigValidationTest.php index 5b333b19ffb82b80a62c0c8fc9e19a8f2ed835c7..5da73d743391320ab0ebaa3705e94fb87f3e5b71 100644 --- a/core/tests/Drupal/KernelTests/Core/Config/SimpleConfigValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Config/SimpleConfigValidationTest.php @@ -149,4 +149,27 @@ public function testSpecialCharacters(string $config_name, string $property, str } } + /** + * Tests that plugin IDs in simple config are validated. + * + * @param string $config_name + * The name of the config object to validate. + * @param string $property + * The property path to set. This will receive the value 'non_existent' and + * is expected to raise a "plugin does not exist" error. + * + * @testWith ["system.mail", "interface.0"] + */ + public function testInvalidPluginId(string $config_name, string $property): void { + $config = $this->config($config_name); + + $violations = $this->container->get('config.typed') + ->createFromNameAndData($config_name, $config->set($property, 'non_existent')->get()) + ->validate(); + + $this->assertCount(1, $violations); + $this->assertSame($property, $violations[0]->getPropertyPath()); + $this->assertSame("The 'non_existent' plugin does not exist.", (string) $violations[0]->getMessage()); + } + } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php index bcc2a30396457e943fe2657c0586018f642bd666..124d0368809889c2cc8f0b1954ace86c983857fb 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/BaseFieldOverrideValidationTest.php @@ -38,7 +38,7 @@ protected function setUp(): void { $fields = $this->container->get('entity_field.manager') ->getBaseFieldDefinitions('node'); - $this->entity = BaseFieldOverride::createFromBaseFieldDefinition(reset($fields), 'one'); + $this->entity = BaseFieldOverride::createFromBaseFieldDefinition($fields['uuid'], 'one'); $this->entity->save(); } @@ -65,6 +65,20 @@ public function testImmutableProperties(array $valid_values = []): void { parent::testImmutableProperties([ 'entity_type' => 'entity_test_with_bundle', 'bundle' => 'another', + 'field_type' => 'string', + ]); + } + + /** + * Tests that the field type plugin's existence is validated. + */ + public function testFieldTypePluginIsValidated(): void { + // The `field_type` property is immutable, so we need to clone the entity in + // order to cleanly change its field_type property to some invalid value. + $this->entity = $this->entity->createDuplicate() + ->set('field_type', 'invalid'); + $this->assertValidationErrors([ + 'field_type' => "The 'invalid' plugin does not exist.", ]); } diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php index 435d2fdd5685281f850ab6d6ab78fcbfe4205c49..1302c0243b21fd8ded1cd2a7b4ebcfd230f240ea 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityDisplayBaseTest.php @@ -22,6 +22,7 @@ class EntityDisplayBaseTest extends KernelTestBase { 'entity_test', 'entity_test_third_party', 'field', + 'field_test', 'system', 'comment', 'user', @@ -47,9 +48,9 @@ public function testPreSave() { 'mode' => 'default', 'status' => TRUE, 'content' => [ - 'foo' => ['type' => 'visible'], + 'foo' => ['type' => 'field_no_settings'], 'bar' => ['region' => 'hidden'], - 'name' => ['type' => 'hidden', 'region' => 'content'], + 'name' => ['type' => 'field_no_settings', 'region' => 'content'], ], ]); diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php index 8596a96365fe98fe2be7b875e09f6822c373ee18..58c01c69bc25598fbc994055ed03d49a303f2b00 100644 --- a/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php +++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityViewDisplayValidationTest.php @@ -2,6 +2,8 @@ namespace Drupal\KernelTests\Core\Entity; +use Drupal\layout_builder\Entity\LayoutEntityDisplayInterface; +use Drupal\layout_builder\Section; use Drupal\Core\Entity\Entity\EntityViewMode; use Drupal\Core\Entity\EntityDisplayRepositoryInterface; use Drupal\entity_test\Entity\EntityTestBundle; @@ -52,6 +54,26 @@ protected function setUp(): void { $this->entity->save(); } + /** + * Tests that the plugin ID of a Layout Builder section is validated. + */ + public function testLayoutSectionPluginIdIsValidated(): void { + $this->enableModules(['layout_builder', 'layout_discovery']); + + $this->entity = $this->container->get('entity_display.repository') + ->getViewDisplay('user', 'user'); + $this->assertInstanceOf(LayoutEntityDisplayInterface::class, $this->entity); + $this->entity->enableLayoutBuilder()->save(); + $sections = array_map(fn(Section $section) => $section->toArray(), $this->entity->getSections()); + $this->assertCount(1, $sections); + $sections[0]['layout_id'] = 'non_existent'; + + $this->entity->setThirdPartySetting('layout_builder', 'sections', $sections); + $this->assertValidationErrors([ + 'third_party_settings.layout_builder.sections.0.layout_id' => "The 'non_existent' plugin does not exist.", + ]); + } + /** * Tests that the target bundle of the entity view display is checked. */ diff --git a/core/tests/Drupal/KernelTests/Core/Plugin/PluginExistsConstraintValidatorTest.php b/core/tests/Drupal/KernelTests/Core/Plugin/PluginExistsConstraintValidatorTest.php index 3106db3269687a7de687447d4a223e0a3010f7f4..e430c21448ee50cfa36b31640c549748dea9a17a 100644 --- a/core/tests/Drupal/KernelTests/Core/Plugin/PluginExistsConstraintValidatorTest.php +++ b/core/tests/Drupal/KernelTests/Core/Plugin/PluginExistsConstraintValidatorTest.php @@ -2,6 +2,8 @@ namespace Drupal\KernelTests\Core\Plugin; +use Drupal\Component\Plugin\FallbackPluginManagerInterface; +use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Action\ActionInterface; use Drupal\Core\TypedData\DataDefinition; use Drupal\KernelTests\KernelTestBase; @@ -68,4 +70,42 @@ public function testValidation(): void { $this->assertCount(0, $violations); } + /** + * Tests that fallback plugin IDs can be considered valid or invalid. + */ + public function testFallbackPluginIds(): void { + $plugin_manager = $this->prophesize(PluginManagerInterface::class) + ->willImplement(FallbackPluginManagerInterface::class); + $plugin_manager->getFallbackPluginId('non_existent') + ->shouldBeCalledOnce() + ->willReturn('broken'); + $plugin_manager->getDefinition('non_existent', FALSE) + ->shouldBeCalled() + ->willReturn(NULL); + $plugin_manager->getDefinition('broken', FALSE) + ->shouldBeCalled() + ->willReturn(['id' => 'broken']); + $this->container->set('plugin.manager.test_fallback', $plugin_manager->reveal()); + + // If fallback plugin IDs are allowed, then an invalid plugin ID should not + // raise an error. + $definition = DataDefinition::create('string') + ->addConstraint('PluginExists', [ + 'manager' => 'plugin.manager.test_fallback', + 'allowFallback' => TRUE, + ]); + $data = $this->container->get('typed_data_manager')->create($definition); + $data->setValue('non_existent'); + $this->assertCount(0, $data->validate()); + + // If fallback plugin IDs are not considered valid (the default behavior), + // then we should get a validation error. + $definition->addConstraint('PluginExists', [ + 'manager' => 'plugin.manager.test_fallback', + ]); + $violations = $data->validate(); + $this->assertCount(1, $violations); + $this->assertSame("The 'non_existent' plugin does not exist.", (string) $violations->get(0)->getMessage()); + } + }