Commit 0c1de8e4 authored by larowlan's avatar larowlan

Issue #2671964 by EclipseGc, tim.plunkett, Jo Fitzgerald, larowlan, fago,...

Issue #2671964 by EclipseGc, tim.plunkett, Jo Fitzgerald, larowlan, fago, dawehner, Berdir, phenaproxima: ContextHandler cannot validate constraints
parent ee7f7f2a
......@@ -3,6 +3,11 @@
namespace Drupal\Core\Plugin\Context;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\Plugin\DataType\EntityAdapter;
use Drupal\Core\Entity\Plugin\Validation\Constraint\BundleConstraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\EntityTypeConstraint;
use Drupal\Core\Entity\TypedData\EntityDataDefinition;
use Drupal\Core\TypedData\TypedDataTrait;
/**
......@@ -252,4 +257,105 @@ public function getDataDefinition() {
return $definition;
}
/**
* {@inheritdoc}
*/
public function isSatisfiedBy(ContextInterface $context) {
$definition = $context->getContextDefinition();
// If the data types do not match, this context is invalid unless the
// expected data type is any, which means all data types are supported.
if ($this->getDataType() != 'any' && $definition->getDataType() != $this->getDataType()) {
return FALSE;
}
// Get the value for this context, either directly if possible or by
// introspecting the definition.
if ($context->hasContextValue()) {
$values = [$context->getContextData()];
}
elseif ($definition instanceof static) {
$values = $definition->getSampleValues();
}
else {
$values = [];
}
$validator = $this->getTypedDataManager()->getValidator();
foreach ($values as $value) {
$violations = $validator->validate($value, array_values($this->getConstraintObjects()));
// If a value has no violations then the requirement is satisfied.
if (!$violations->count()) {
return TRUE;
}
}
return FALSE;
}
/**
* Returns typed data objects representing this context definition.
*
* This should return as many objects as needed to reflect the variations of
* the constraints it supports.
*
* @yield \Drupal\Core\TypedData\TypedDataInterface
* The set of typed data object.
*/
protected function getSampleValues() {
// @todo Move the entity specific logic out of this class in
// https://www.drupal.org/node/2932462.
// Get the constraints from the context's definition.
$constraints = $this->getConstraintObjects();
// If constraints include EntityType, we generate an entity or adapter.
if (!empty($constraints['EntityType']) && $constraints['EntityType'] instanceof EntityTypeConstraint) {
$entity_type_manager = \Drupal::entityTypeManager();
$entity_type_id = $constraints['EntityType']->type;
$storage = $entity_type_manager->getStorage($entity_type_id);
// If the storage can generate a sample entity we might delegate to that.
if ($storage instanceof ContentEntityStorageInterface) {
if (!empty($constraints['Bundle']) && $constraints['Bundle'] instanceof BundleConstraint) {
foreach ($constraints['Bundle']->bundle as $bundle) {
// We have a bundle, we are bundleable and we can generate a sample.
yield EntityAdapter::createFromEntity($storage->createWithSampleValues($bundle));
}
return;
}
}
// Either no bundle, or not bundleable, so generate an entity adapter.
$definition = EntityDataDefinition::create($entity_type_id);
yield new EntityAdapter($definition);
return;
}
// No entity related constraints, so generate a basic typed data object.
yield $this->getTypedDataManager()->create($this->getDataDefinition());
}
/**
* Extracts an array of constraints for a context definition object.
*
* @return \Symfony\Component\Validator\Constraint[]
* A list of applied constraints for the context definition.
*/
protected function getConstraintObjects() {
$constraint_definitions = $this->getConstraints();
// @todo Move the entity specific logic out of this class in
// https://www.drupal.org/node/2932462.
// If the data type is an entity, manually add one to the constraints array.
if (strpos($this->getDataType(), 'entity:') === 0) {
$entity_type_id = substr($this->getDataType(), 7);
$constraint_definitions['EntityType'] = ['type' => $entity_type_id];
}
$validation_constraint_manager = $this->getTypedDataManager()->getValidationConstraintManager();
$constraints = [];
foreach ($constraint_definitions as $constraint_name => $constraint_definition) {
$constraints[$constraint_name] = $validation_constraint_manager->create($constraint_name, $constraint_definition);
}
return $constraints;
}
}
......@@ -20,4 +20,16 @@ interface ContextDefinitionInterface extends ComponentContextDefinitionInterface
*/
public function getDataDefinition();
/**
* Determines if this definition is satisfied by a context object.
*
* @param \Drupal\Core\Plugin\Context\ContextInterface $context
* The context object.
*
* @return bool
* TRUE if this definition is satisfiable by the context object, FALSE
* otherwise.
*/
public function isSatisfiedBy(ContextInterface $context);
}
......@@ -43,23 +43,7 @@ public function checkRequirements(array $contexts, array $requirements) {
*/
public function getMatchingContexts(array $contexts, ContextDefinitionInterface $definition) {
return array_filter($contexts, function (ContextInterface $context) use ($definition) {
$context_definition = $context->getContextDefinition();
// If the data types do not match, this context is invalid unless the
// expected data type is any, which means all data types are supported.
if ($definition->getDataType() != 'any' && $definition->getDataType() != $context_definition->getDataType()) {
return FALSE;
}
// If any constraint does not match, this context is invalid.
foreach ($definition->getConstraints() as $constraint_name => $constraint) {
if ($context_definition->getConstraint($constraint_name) != $constraint) {
return FALSE;
}
}
// All contexts with matching data type and contexts are valid.
return TRUE;
return $definition->isSatisfiedBy($context);
});
}
......
......@@ -50,6 +50,10 @@ public function __construct(AccountInterface $account, EntityManagerInterface $e
public function getRuntimeContexts(array $unqualified_context_ids) {
$current_user = $this->userStorage->load($this->account->id());
// @todo Do not validate protected fields to avoid bug in TypedData, remove
// this in https://www.drupal.org/project/drupal/issues/2934192.
$current_user->_skipProtectedUserFieldConstraint = TRUE;
$context = new Context(new ContextDefinition('entity:user', $this->t('Current user')), $current_user);
$cacheability = new CacheableMetadata();
$cacheability->setCacheContexts(['user']);
......
......@@ -9,12 +9,19 @@
use Drupal\Component\Plugin\ConfigurablePluginInterface;
use Drupal\Component\Plugin\Exception\ContextException;
use Drupal\Core\Cache\NullBackend;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\Plugin\Context\ContextHandler;
use Drupal\Core\Plugin\ContextAwarePluginInterface;
use Drupal\Core\TypedData\DataDefinition;
use Drupal\Core\TypedData\Plugin\DataType\StringData;
use Drupal\Core\TypedData\TypedDataManager;
use Drupal\Core\Validation\ConstraintManager;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\Core\Plugin\Context\ContextHandler
......@@ -36,6 +43,26 @@ protected function setUp() {
parent::setUp();
$this->contextHandler = new ContextHandler();
$namespaces = new \ArrayObject([
'Drupal\\Core\\TypedData' => $this->root . '/core/lib/Drupal/Core/TypedData',
'Drupal\\Core\\Validation' => $this->root . '/core/lib/Drupal/Core/Validation',
]);
$cache_backend = new NullBackend('cache');
$module_handler = $this->prophesize(ModuleHandlerInterface::class);
$class_resolver = $this->prophesize(ClassResolverInterface::class);
$class_resolver->getInstanceFromDefinition(Argument::type('string'))->will(function ($arguments) {
$class_name = $arguments[0];
return new $class_name();
});
$type_data_manager = new TypedDataManager($namespaces, $cache_backend, $module_handler->reveal(), $class_resolver->reveal());
$type_data_manager->setValidationConstraintManager(
new ConstraintManager($namespaces, $cache_backend, $module_handler->reveal())
);
$container = new ContainerBuilder();
$container->set('typed_data_manager', $type_data_manager);
\Drupal::setContainer($container);
}
/**
......@@ -60,10 +87,10 @@ public function providerTestCheckRequirements() {
$context_any = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
$context_any->expects($this->atLeastOnce())
->method('getContextDefinition')
->will($this->returnValue(new ContextDefinition('empty')));
->will($this->returnValue(new ContextDefinition('any')));
$requirement_specific = new ContextDefinition('specific');
$requirement_specific->setConstraints(['bar' => 'baz']);
$requirement_specific = new ContextDefinition('string');
$requirement_specific->setConstraints(['Blank' => []]);
$context_constraint_mismatch = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
$context_constraint_mismatch->expects($this->atLeastOnce())
......@@ -74,8 +101,8 @@ public function providerTestCheckRequirements() {
->method('getContextDefinition')
->will($this->returnValue(new ContextDefinition('fuzzy')));
$context_definition_specific = new ContextDefinition('specific');
$context_definition_specific->setConstraints(['bar' => 'baz']);
$context_definition_specific = new ContextDefinition('string');
$context_definition_specific->setConstraints(['Blank' => []]);
$context_specific = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
$context_specific->expects($this->atLeastOnce())
->method('getContextDefinition')
......@@ -112,13 +139,13 @@ public function testGetMatchingContexts($contexts, $requirement, $expected = NUL
public function providerTestGetMatchingContexts() {
$requirement_any = new ContextDefinition();
$requirement_specific = new ContextDefinition('specific');
$requirement_specific->setConstraints(['bar' => 'baz']);
$requirement_specific = new ContextDefinition('string');
$requirement_specific->setConstraints(['Blank' => []]);
$context_any = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
$context_any->expects($this->atLeastOnce())
->method('getContextDefinition')
->will($this->returnValue(new ContextDefinition('empty')));
->will($this->returnValue(new ContextDefinition('any')));
$context_constraint_mismatch = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
$context_constraint_mismatch->expects($this->atLeastOnce())
->method('getContextDefinition')
......@@ -127,8 +154,8 @@ public function providerTestGetMatchingContexts() {
$context_datatype_mismatch->expects($this->atLeastOnce())
->method('getContextDefinition')
->will($this->returnValue(new ContextDefinition('fuzzy')));
$context_definition_specific = new ContextDefinition('specific');
$context_definition_specific->setConstraints(['bar' => 'baz']);
$context_definition_specific = new ContextDefinition('string');
$context_definition_specific->setConstraints(['Blank' => []]);
$context_specific = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
$context_specific->expects($this->atLeastOnce())
->method('getContextDefinition')
......@@ -158,7 +185,7 @@ public function providerTestGetMatchingContexts() {
public function testFilterPluginDefinitionsByContexts($has_context, $definitions, $expected) {
if ($has_context) {
$context = $this->getMock('Drupal\Core\Plugin\Context\ContextInterface');
$expected_context_definition = (new ContextDefinition('expected_data_type'))->setConstraints(['expected_constraint_name' => 'expected_constraint_value']);
$expected_context_definition = (new ContextDefinition('string'))->setConstraints(['Blank' => []]);
$context->expects($this->atLeastOnce())
->method('getContextDefinition')
->will($this->returnValue($expected_context_definition));
......@@ -189,7 +216,7 @@ public function providerTestFilterPluginDefinitionsByContexts() {
// No context, all plugins available.
$data[] = [FALSE, $plugins, $plugins];
$plugins = ['expected_plugin' => ['context' => ['context1' => new ContextDefinition('expected_data_type')]]];
$plugins = ['expected_plugin' => ['context' => ['context1' => new ContextDefinition('string')]]];
// Missing context, no plugins available.
$data[] = [FALSE, $plugins, []];
// Satisfied context, all plugins available.
......@@ -206,7 +233,7 @@ public function providerTestFilterPluginDefinitionsByContexts() {
// Optional mismatched constraint, all plugins available.
$data[] = [FALSE, $plugins, $plugins];
$expected_context_definition = (new ContextDefinition('expected_data_type'))->setConstraints(['expected_constraint_name' => 'expected_constraint_value']);
$expected_context_definition = (new ContextDefinition('string'))->setConstraints(['Blank' => []]);
$plugins = ['expected_plugin' => ['context' => ['context1' => $expected_context_definition]]];
// Satisfied context with constraint, all plugins available.
$data[] = [TRUE, $plugins, $plugins];
......@@ -220,7 +247,7 @@ public function providerTestFilterPluginDefinitionsByContexts() {
$unexpected_context_definition = (new ContextDefinition('unexpected_data_type'))->setConstraints(['mismatched_constraint_name' => 'mismatched_constraint_value']);
$plugins = [
'unexpected_plugin' => ['context' => ['context1' => $unexpected_context_definition]],
'expected_plugin' => ['context' => ['context2' => new ContextDefinition('expected_data_type')]],
'expected_plugin' => ['context' => ['context2' => new ContextDefinition('string')]],
];
// Context only satisfies one plugin.
$data[] = [TRUE, $plugins, ['expected_plugin' => $plugins['expected_plugin']]];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment