Unverified 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']);
......
<?php
namespace Drupal\Tests\Core\Plugin\Context;
use Drupal\Core\Cache\NullBackend;
use Drupal\Core\DependencyInjection\ClassResolverInterface;
use Drupal\Core\DependencyInjection\ContainerBuilder;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\ContentEntityStorageInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityType;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Plugin\Context\Context;
use Drupal\Core\Plugin\Context\ContextDefinition;
use Drupal\Core\TypedData\TypedDataManager;
use Drupal\Core\Validation\ConstraintManager;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
/**
* @coversDefaultClass \Drupal\Core\Plugin\Context\ContextDefinition
* @group Plugin
*/
class ContextDefinitionIsSatisfiedTest extends UnitTestCase {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity manager.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The entity type bundle info.
*
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
*/
protected $entityTypeBundleInfo;
/**
* {@inheritdoc}
*/
protected function setUp() {
parent::setUp();
$namespaces = new \ArrayObject([
'Drupal\\Core\\TypedData' => $this->root . '/core/lib/Drupal/Core/TypedData',
'Drupal\\Core\\Validation' => $this->root . '/core/lib/Drupal/Core/Validation',
'Drupal\\Core\\Entity' => $this->root . '/core/lib/Drupal/Core/Entity',
]);
$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()));
$this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class);
$this->entityManager = $this->prophesize(EntityManagerInterface::class);
$this->entityTypeBundleInfo = $this->prophesize(EntityTypeBundleInfoInterface::class);
$container = new ContainerBuilder();
$container->set('typed_data_manager', $type_data_manager);
$container->set('entity_type.manager', $this->entityTypeManager->reveal());
$container->set('entity.manager', $this->entityManager->reveal());
$container->set('entity_type.bundle.info', $this->entityTypeBundleInfo->reveal());
\Drupal::setContainer($container);
}
/**
* Asserts that the requirement is satisfied as expected.
*
* @param bool $expected
* The expected outcome.
* @param \Drupal\Core\Plugin\Context\ContextDefinition $requirement
* The requirement to check against.
* @param \Drupal\Core\Plugin\Context\ContextDefinition $definition
* The context definition to check.
* @param mixed $value
* (optional) The value to set on the context, defaults to NULL.
*/
protected function assertRequirementIsSatisfied($expected, ContextDefinition $requirement, ContextDefinition $definition, $value = NULL) {
$context = new Context($definition, $value);
$this->assertSame($expected, $requirement->isSatisfiedBy($context));
}
/**
* @covers ::isSatisfiedBy
* @covers ::getSampleValues
* @covers ::getConstraintObjects
*
* @dataProvider providerTestIsSatisfiedBy
*/
public function testIsSatisfiedBy($expected, ContextDefinition $requirement, ContextDefinition $definition, $value = NULL) {
$entity_storage = $this->prophesize(EntityStorageInterface::class);
$content_entity_storage = $this->prophesize(ContentEntityStorageInterface::class);
$this->entityTypeManager->getStorage('test_config')->willReturn($entity_storage->reveal());
$this->entityTypeManager->getStorage('test_content')->willReturn($content_entity_storage->reveal());
$this->entityManager->getDefinitions()->willReturn([
'test_config' => new EntityType(['id' => 'test_config']),
'test_content' => new EntityType(['id' => 'test_content']),
]);
$this->entityTypeBundleInfo->getBundleInfo('test_config')->willReturn([
'test_config' => ['label' => 'test_config'],
]);
$this->entityTypeBundleInfo->getBundleInfo('test_content')->willReturn([
'test_content' => ['label' => 'test_content'],
]);
$this->assertRequirementIsSatisfied($expected, $requirement, $definition, $value);
}
/**
* Provides test data for ::testIsSatisfiedBy().
*/
public function providerTestIsSatisfiedBy() {
$data = [];
// Simple data types.
$data['both any'] = [
TRUE,
new ContextDefinition('any'),
new ContextDefinition('any'),
];
$data['requirement any'] = [
TRUE,
new ContextDefinition('any'),
new ContextDefinition('integer'),
];
$data['integer, out of range'] = [
FALSE,
(new ContextDefinition('integer'))->addConstraint('Range', ['min' => 0, 'max' => 10]),
new ContextDefinition('integer'),
20,
];
$data['integer, within range'] = [
TRUE,
(new ContextDefinition('integer'))->addConstraint('Range', ['min' => 0, 'max' => 10]),
new ContextDefinition('integer'),
5,
];
$data['integer, no value'] = [
TRUE,
(new ContextDefinition('integer'))->addConstraint('Range', ['min' => 0, 'max' => 10]),
new ContextDefinition('integer'),
];
$data['non-integer, within range'] = [
FALSE,
(new ContextDefinition('integer'))->addConstraint('Range', ['min' => 0, 'max' => 10]),
new ContextDefinition('any'),
5,
];
// Entities without bundles.
$data['content entity, matching type, no value'] = [
TRUE,
new ContextDefinition('entity:test_content'),
new ContextDefinition('entity:test_content'),
];
$entity = $this->prophesize(ContentEntityInterface::class)->willImplement(\IteratorAggregate::class);
$entity->getIterator()->willReturn(new \ArrayIterator([]));
$entity->getCacheContexts()->willReturn([]);
$entity->getCacheTags()->willReturn([]);
$entity->getCacheMaxAge()->willReturn(0);
$entity->getEntityTypeId()->willReturn('test_content');
$data['content entity, matching type, correct value'] = [
TRUE,
new ContextDefinition('entity:test_content'),
new ContextDefinition('entity:test_content'),
$entity->reveal(),
];
$data['content entity, incorrect manual constraint'] = [
TRUE,
new ContextDefinition('entity:test_content'),
(new ContextDefinition('entity:test_content'))->addConstraint('EntityType', 'test_config'),
];
$data['config entity, matching type, no value'] = [
TRUE,
new ContextDefinition('entity:test_config'),
new ContextDefinition('entity:test_config'),
];
return $data;
}
/**
* @covers ::isSatisfiedBy
* @covers ::getSampleValues
* @covers ::getConstraintObjects
*
* @dataProvider providerTestIsSatisfiedByGenerateBundledEntity
*/
public function testIsSatisfiedByGenerateBundledEntity($expected, array $requirement_bundles, array $candidate_bundles, array $bundles_to_instantiate = NULL) {
// If no bundles are explicitly specified, instantiate all bundles.
if (!$bundles_to_instantiate) {
$bundles_to_instantiate = $candidate_bundles;
}
$content_entity_storage = $this->prophesize(ContentEntityStorageInterface::class);
foreach ($bundles_to_instantiate as $bundle) {
$entity = $this->prophesize(ContentEntityInterface::class)->willImplement(\IteratorAggregate::class);
$entity->getEntityTypeId()->willReturn('test_content');
$entity->getIterator()->willReturn(new \ArrayIterator([]));
$entity->bundle()->willReturn($bundle);
$content_entity_storage->createWithSampleValues($bundle)
->willReturn($entity->reveal())
->shouldBeCalled();
}
$this->entityTypeManager->getStorage('test_content')->willReturn($content_entity_storage->reveal());
$this->entityManager->getDefinitions()->willReturn([
'test_content' => new EntityType(['id' => 'test_content']),
]);
$this->entityTypeBundleInfo->getBundleInfo('test_content')->willReturn([
'first_bundle' => ['label' => 'First bundle'],
'second_bundle' => ['label' => 'Second bundle'],
'third_bundle' => ['label' => 'Third bundle'],
]);
$requirement = new ContextDefinition('entity:test_content');
if ($requirement_bundles) {
$requirement->addConstraint('Bundle', $requirement_bundles);
}
$definition = (new ContextDefinition('entity:test_content'))->addConstraint('Bundle', $candidate_bundles);
$this->assertRequirementIsSatisfied($expected, $requirement, $definition);
}
/**
* Provides test data for ::testIsSatisfiedByGenerateBundledEntity().
*/
public function providerTestIsSatisfiedByGenerateBundledEntity() {
$data = [];
$data['no requirement'] = [
TRUE,
[],
['first_bundle'],
];
$data['single requirement'] = [
TRUE,
['first_bundle'],
['first_bundle'],
];
$data['single requirement, multiple candidates, satisfies last candidate'] = [
TRUE,
['third_bundle'],
['first_bundle', 'second_bundle', 'third_bundle'],
];
$data['single requirement, multiple candidates, satisfies first candidate'] = [
TRUE,
['first_bundle'],
['first_bundle', 'second_bundle', 'third_bundle'],
// Once the first match is found, subsequent candidates are not checked.
['first_bundle'],
];
$data['unsatisfied requirement'] = [
FALSE,
['second_bundle'],
['first_bundle', 'third_bundle'],
];
$data['multiple requirements'] = [
TRUE,
['first_bundle', 'second_bundle'],
['first_bundle'],
];
return $data;
}
/**
* @covers ::isSatisfiedBy
* @covers ::getSampleValues
* @covers ::getConstraintObjects
*
* @dataProvider providerTestIsSatisfiedByPassBundledEntity
*/
public function testIsSatisfiedByPassBundledEntity($expected, $requirement_constraint) {
$this->entityManager->getDefinitions()->willReturn([
'test_content' => new EntityType(['id' => 'test_content']),
]);
$this->entityTypeManager->getStorage('test_content')->shouldNotBeCalled();
$this->entityTypeBundleInfo->getBundleInfo('test_content')->willReturn([
'first_bundle' => ['label' => 'First bundle'],
'second_bundle' => ['label' => 'Second bundle'],
'third_bundle' => ['label' => 'Third bundle'],
]);
$entity = $this->prophesize(ContentEntityInterface::class)->willImplement(\IteratorAggregate::class);
$entity->getEntityTypeId()->willReturn('test_content');
$entity->getIterator()->willReturn(new \ArrayIterator([]));
$entity->getCacheContexts()->willReturn([]);
$entity->getCacheTags()->willReturn([]);
$entity->getCacheMaxAge()->willReturn(0);
$entity->bundle()->willReturn('third_bundle');
$requirement = new ContextDefinition('entity:test_content');
if ($requirement_constraint) {
$requirement->addConstraint('Bundle', $requirement_constraint);
}
$definition = new ContextDefinition('entity:test_content');
$this->assertRequirementIsSatisfied($expected, $requirement, $definition, $entity->reveal());
}
/**
* Provides test data for ::testIsSatisfiedByPassBundledEntity().
*/
public function providerTestIsSatisfiedByPassBundledEntity() {
$data = [];
$data[] = [TRUE, []];
$data[] = [FALSE, ['first_bundle']];
$data[] = [FALSE, ['second_bundle']];
$data[] = [TRUE, ['third_bundle']];
$data[] = [TRUE, ['first_bundle', 'second_bundle', 'third_bundle']];
$data[] = [FALSE, ['first_bundle', 'second_bundle']];
$data[] = [TRUE, ['first_bundle', 'third_bundle']];
$data[] = [TRUE, ['second_bundle', 'third_bundle']];
return $data;
}
}
namespace Drupal\Core\Validation;
if (!function_exists('t')) {
function t($string, array $args = []) {
return strtr($string, $args);
}
}
......@@ -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');