Skip to content
Snippets Groups Projects
Verified Commit 529550a4 authored by Lee Rowlands's avatar Lee Rowlands
Browse files

Issue #2920682 by phenaproxima, alexpott, Sam152, borisson_, Wim Leers,...

Issue #2920682 by phenaproxima, alexpott, Sam152, borisson_, Wim Leers, larowlan: Add config validation for plugin IDs
parent 9cc7cfaa
No related branches found
No related tags found
32 merge requests!7452Issue #1797438. HTML5 validation is preventing form submit and not fully...,!54479.5.x SF update,!5014Issue #3071143: Table Render Array Example Is Incorrect,!4868Issue #1428520: Improve menu parent link selection,!4289Issue #1344552 by marcingy, Niklas Fiekas, Ravi.J, aleevas, Eduardo Morales...,!4114Issue #2707291: Disable body-level scrolling when a dialog is open as a modal,!4100Issue #3249600: Add support for PHP 8.1 Enums as allowed values for list_* data types,!3630Issue #2815301 by Chi, DanielVeza, kostyashupenko, smustgrave: Allow to create...,!3600Issue #3344629: Passing null to parameter #1 ($haystack) of type string is deprecated,!3291Issue #3336463: Rewrite rules for gzipped CSS and JavaScript aggregates never match,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2334Issue #3228209: Add hasRole() method to AccountInterface,!2074Issue #2707689: NodeForm::actions() checks for delete access on new entities,!2062Issue #3246454: Add weekly granularity to views date sort,!1591Issue #3199697: Add JSON:API Translation experimental module,!1484Exposed filters get values from URL when Ajax is on,!1255Issue #3238922: Refactor (if feasible) uses of the jQuery serialize function to use vanillaJS,!1162Issue #3100350: Unable to save '/' root path alias,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!957Added throwing of InvalidPluginDefinitionException from getDefinition().,!925Issue #2339235: Remove taxonomy hard dependency on node module,!877Issue #2708101: Default value for link text is not saved,!872Draft: Issue #3221319: Race condition when creating menu links and editing content deletes menu links,!844Resolve #3036010 "Updaters",!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493,!485Sets the autocomplete attribute for username/password input field on login form.,!30Issue #3182188: Updates composer usage to point at ./vendor/bin/composer
<?php
declare(strict_types = 1);
namespace Drupal\Core\Plugin\Plugin\Validation\Constraint;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\MissingOptionsException;
/**
* Checks if a plugin exists and optionally implements a particular interface.
*
* @Constraint(
* id = "PluginExists",
* label = @Translation("Plugin exists", context = "Validation"),
* )
*/
class PluginExistsConstraint extends Constraint implements ContainerFactoryPluginInterface {
/**
* The error message if a plugin does not exist.
*
* @var string
*/
public string $unknownPluginMessage = "The '@plugin_id' plugin does not exist.";
/**
* The error message if a plugin does not implement the expected interface.
*
* @var string
*/
public string $invalidInterfaceMessage = "The '@plugin_id' plugin must implement or extend @interface.";
/**
* The ID of the plugin manager service.
*
* @var string
*/
protected string $manager;
/**
* Optional name of the interface that the plugin must implement.
*
* @var string|null
*/
public ?string $interface = NULL;
/**
* Constructs a PluginExistsConstraint.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $pluginManager
* The plugin manager associated with the constraint.
* @param mixed|null $options
* The options (as associative array) or the value for the default option
* (any other type).
* @param array|null $groups
* An array of validation groups.
* @param mixed|null $payload
* Domain-specific data attached to a constraint.
*/
public function __construct(public readonly PluginManagerInterface $pluginManager, mixed $options = NULL, array $groups = NULL, mixed $payload = NULL) {
parent::__construct($options, $groups, $payload);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
$plugin_manager_id = $configuration['manager'] ?? $configuration['value'] ?? NULL;
if ($plugin_manager_id === NULL) {
throw new MissingOptionsException(sprintf('The option "manager" must be set for constraint "%s".', static::class), ['manager']);
}
return new static($container->get($plugin_manager_id), $configuration);
}
/**
* {@inheritdoc}
*/
public function getDefaultOption(): ?string {
return 'manager';
}
/**
* {@inheritdoc}
*/
public function getRequiredOptions(): array {
return ['manager'];
}
}
<?php
declare(strict_types = 1);
namespace Drupal\Core\Plugin\Plugin\Validation\Constraint;
use Drupal\Component\Plugin\Factory\DefaultFactory;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
/**
* Validates the PluginExists constraint.
*/
class PluginExistsConstraintValidator extends ConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate(mixed $plugin_id, Constraint $constraint) {
assert($constraint instanceof PluginExistsConstraint);
$definition = $constraint->pluginManager->getDefinition($plugin_id, FALSE);
if (empty($definition)) {
$this->context->addViolation($constraint->unknownPluginMessage, [
'@plugin_id' => $plugin_id,
]);
return;
}
// If we don't need to validate the plugin class's interface, we're done.
if (empty($constraint->interface)) {
return;
}
if (!is_a(DefaultFactory::getPluginClass($plugin_id, $definition), $constraint->interface, TRUE)) {
$this->context->addViolation($constraint->invalidInterfaceMessage, [
'@plugin_id' => $plugin_id,
'@interface' => $constraint->interface,
]);
}
}
}
......@@ -22,6 +22,10 @@ block.block.*:
plugin:
type: string
label: 'Plugin'
constraints:
PluginExists:
manager: plugin.manager.block
interface: Drupal\Core\Block\BlockPluginInterface
settings:
type: block.settings.[%parent.plugin]
visibility:
......
<?php
namespace Drupal\Tests\block\Kernel;
use Drupal\block\Entity\Block;
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
/**
* Tests validation of block entities.
*
* @group block
*/
class BlockValidationTest extends ConfigEntityValidationTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['block'];
/**
* {@inheritdoc}
*/
protected function setUp(): void {
parent::setUp();
$this->entity = Block::create([
'id' => 'test_block',
'theme' => 'stark',
'plugin' => 'system_powered_by_block',
]);
$this->entity->save();
}
/**
* Tests validating a block with an unknown plugin ID.
*/
public function testInvalidPluginId(): void {
$this->entity->set('plugin', 'non_existent');
$this->assertValidationErrors(["The 'non_existent' plugin does not exist."]);
}
}
......@@ -10,6 +10,10 @@ editor.editor.*:
editor:
type: string
label: 'Text editor'
constraints:
PluginExists:
manager: plugin.manager.editor
interface: Drupal\editor\Plugin\EditorPluginInterface
settings:
type: editor.settings.[%parent.editor]
image_upload:
......
......@@ -62,4 +62,12 @@ public function testInvalidDependencies(): void {
]);
}
/**
* Tests validating an editor with an unknown plugin ID.
*/
public function testInvalidPluginId(): void {
$this->entity->setEditor('non_existent');
$this->assertValidationErrors(["The 'non_existent' plugin does not exist."]);
}
}
<?php
namespace Drupal\KernelTests\Core\Plugin;
use Drupal\Core\Action\ActionInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\KernelTests\KernelTestBase;
use Drupal\system\MenuInterface;
/**
* @group Plugin
* @group Validation
*
* @covers \Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint
* @covers \Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraintValidator
*/
class PluginExistsConstraintValidatorTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
protected static $modules = ['action_test', 'system'];
/**
* Tests validation of plugin existence.
*/
public function testValidation(): void {
$definition = DataDefinition::create('string')
->addConstraint('PluginExists', 'plugin.manager.action');
// An existing action plugin should pass validation.
$data = $this->container->get('typed_data_manager')->create($definition);
$data->setValue('action_test_save_entity');
$this->assertCount(0, $data->validate());
// It should also pass validation if we check for an interface it actually
// implements.
$definition->setConstraints([
'PluginExists' => [
'manager' => 'plugin.manager.action',
'interface' => ActionInterface::class,
],
]);
$this->assertCount(0, $data->validate());
// A non-existent plugin should be invalid, regardless of interface.
$data->setValue('non_existent_plugin');
$violations = $data->validate();
$this->assertCount(1, $violations);
$this->assertSame("The 'non_existent_plugin' plugin does not exist.", (string) $violations->get(0)->getMessage());
// An existing plugin that doesn't implement the specified interface should
// raise an error.
$definition->setConstraints([
'PluginExists' => [
'manager' => 'plugin.manager.action',
'interface' => MenuInterface::class,
],
]);
$data->setValue('action_test_save_entity');
$violations = $data->validate();
$this->assertCount(1, $violations);
$this->assertSame("The 'action_test_save_entity' plugin must implement or extend " . MenuInterface::class . '.', (string) $violations->get(0)->getMessage());
}
}
<?php
namespace Drupal\Tests\Core\Plugin;
use Drupal\Component\DependencyInjection\ContainerInterface;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint;
use Drupal\Tests\UnitTestCase;
use Symfony\Component\Validator\Exception\MissingOptionsException;
/**
* @group Plugin
* @group Validation
*
* @coversDefaultClass \Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint
*/
class PluginExistsConstraintTest extends UnitTestCase {
/**
* Tests missing option.
*
* @covers ::create
*/
public function testMissingOption(): void {
$this->expectException(MissingOptionsException::class);
$this->expectExceptionMessage('The option "manager" must be set for constraint "Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint".');
$container = $this->createMock(ContainerInterface::class);
PluginExistsConstraint::create($container, [], 'test_plugin_id', []);
}
/**
* Tests with different option keys.
*
* @testWith ["value"]
* ["manager"]
*
* @covers ::create
* @covers ::__construct
*/
public function testOption(string $option_key): void {
$container = $this->createMock(ContainerInterface::class);
$manager = $this->createMock(PluginManagerInterface::class);
$container->expects($this->any())
->method('get')
->with('plugin.manager.mock')
->willReturn($manager);
$constraint = PluginExistsConstraint::create($container, [$option_key => 'plugin.manager.mock'], 'test_plugin_id', []);
$this->assertSame($manager, $constraint->pluginManager);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment