Problem/Motivation

As discovered in #2438017: Entity reference does not validate auto-created entities:

The current validation context is getting injected into constraint validator objects and in a case of a recursive validation where a validator triggers a validation chain leading to the same validator object the context of the first call will be exchanged with the one of the subsequent validation chain.

This is critical because it concerns data integrity by preventing validation errors to propagate, which might result into storing something that shouldn't have been stored.

The problem occurs at \Drupal\Core\TypedData\Validation\RecursiveContextualValidator::validateConstraints() where we retrieve the constraint validator and intialize it the current context:

$validator = $this->constraintValidatorFactory->getInstance($constraint);
$validator->initialize($this->context);

Proposed resolution

There are 2 considerable solutions:

  1. Temporary switch the context on the validator by preserving the previous one.
  2. Stop reusing initialized constraint validator objects.

The first approach is better, but should be done in Symfony, as the base class and interface for constraint validators are provided by Symfony and they lack the ability to retrieve the context. The only possible way would be to use reflection for retrieving the previous context of the validator object.

If we want to solve the problem without reflection then we should go with the second approach.

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

hchonov created an issue. See original summary.

hchonov’s picture

Issue summary: View changes
gease’s picture

Uploading patch with the solution and test demonstrating the problem (recursive validation of entities, and the same field being successfully validated on the child entity eliminates error on this field for parent entity).

The last submitted patch, 3: 3096811-test-only.patch, failed testing. View results
- codesniffer_fixes.patch Interdiff of automated coding standards fixes only.

hchonov’s picture

Status: Needs review » Needs work
  1. +++ b/core/lib/Drupal/Core/Validation/ConstraintValidatorFactory.php
    @@ -25,12 +25,13 @@ public function __construct(ClassResolverInterface $class_resolver) {
    +    // Constraint validator instances should always be initialized newly and
    +    // never shared, because the current validation context is getting injected
    +    // into them through setter injection and in a case of a recursive
    +    // validation where a validator triggers a validation chain leading to the
    +    // same validator the context of the first call would be exchanged with the
    +    // one of the subsequent validation chain.
    +    return $this->classResolver->getInstanceFromDefinition($class_name);
    

    As suggested in #2438017-65: Entity reference does not validate auto-created entities.2 we might need to get right of this class. Or if Symfony introduces a getter method on the constraint validator we might revert this change later and only temporarily exchange the context on each validator object. Please create a follow-up issue about this and reference it from here.

  2. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/ReferenceRecursiveConstraint.php
    @@ -0,0 +1,19 @@
    + * Supports recursive validation of entity references.
    

    "Entity Reference valid reference constraint." would be a better fit.

  3. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/ReferenceRecursiveConstraint.php
    @@ -0,0 +1,19 @@
    + *   id = "ReferenceRecursiveConstraint",
    ...
    +class ReferenceRecursiveConstraint extends Constraint {
    

    I think that TestRecursiveValidationConstraint would be a better fit as an ID and class name.

  4. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/ReferenceRecursiveConstraint.php
    @@ -0,0 +1,19 @@
    +  public $message = 'Invalid referenced entity.';
    

    This looks mixed across core, but let's document the class variable like this:

      /**
       * The default violation message.
       *
       * @var string
       */
    
  5. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/ReferenceRecursiveConstraintValidator.php
    @@ -0,0 +1,31 @@
    +  public function validate($value, Constraint $constraint) {
    ...
    +    foreach ($value as $delta => $item) {
    

    $value => items

  6. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/ReferenceRecursiveConstraintValidator.php
    @@ -0,0 +1,31 @@
    +      if ($violations->count() !== 0) {
    

    Is this check really needed?

  7. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/ReferenceRecursiveConstraintValidator.php
    index 97b8215330..6fca8d0b6e 100644
    --- a/core/tests/Drupal/KernelTests/Core/Entity/ValidReferenceConstraintValidatorTest.php
    
    --- a/core/tests/Drupal/KernelTests/Core/Entity/ValidReferenceConstraintValidatorTest.php
    +++ b/core/tests/Drupal/KernelTests/Core/Entity/ValidReferenceConstraintValidatorTest.php
    

    This test file tests the ValidReferenceConstraint. But here we introduce a new test dealing with the recursive validation and therefore should be placed under \Drupal\KernelTests\Core\TypedData\RecursiveContextualValidatorTest

  8. +++ b/core/tests/Drupal/KernelTests/Core/Entity/ValidReferenceConstraintValidatorTest.php
    @@ -231,4 +231,41 @@ public function testPreExistingItemsValidation() {
    +   * Validation of referenced entity does not break validation of parent entity.
    

    Test recursive validation.

  9. +++ b/core/tests/Drupal/KernelTests/Core/Entity/ValidReferenceConstraintValidatorTest.php
    @@ -231,4 +231,41 @@ public function testPreExistingItemsValidation() {
    +
    

    Not needed empty line at the beginning. Here we should explain why we are creating the 2 fields and how the recursive validation is triggered.

  10. +++ b/core/tests/Drupal/KernelTests/Core/Entity/ValidReferenceConstraintValidatorTest.php
    @@ -231,4 +231,41 @@ public function testPreExistingItemsValidation() {
    +      'user_id' => ['target_id' => 0],
    ...
    +      'user_id' => ['target_id' => 0],
    

    Why is this needed?

  11. +++ b/core/tests/Drupal/KernelTests/Core/Entity/ValidReferenceConstraintValidatorTest.php
    @@ -231,4 +231,41 @@ public function testPreExistingItemsValidation() {
    +      //'string_required' => 'some string',
    

    This is unnecessary.

  12. +++ b/core/tests/Drupal/KernelTests/Core/Entity/ValidReferenceConstraintValidatorTest.php
    @@ -231,4 +231,41 @@ public function testPreExistingItemsValidation() {
    +    $this->assertCount(1, $violations);
    +    $this->assertEquals('field_test', $violations[0]->getPropertyPath());
    

    Document what we except here and why.

gease’s picture

Status: Needs work » Needs review
FileSize
5.96 KB
6.98 KB

Entity Reference valid reference constraint

It's not really a valid reference constraint. It checks that the referenced entity is valid itself, but not if it can be a valid reference for this field. It's just a test. Let it be "Validates referenced entities".

I think that TestRecursiveValidationConstraint would be a better fit as an ID and class name.

I would avoid repeating word Validation. But since the constraint is actually focused on the thing that referenced entity is validated itself, let it be TestValidatedReferenceConstraint.

This looks mixed across core, but let's document the class variable like this:

Ok.

$value => items

Ok.

This test file tests the ValidReferenceConstraint. But here we introduce a new test dealing with the recursive validation and therefore should be placed under \Drupal\KernelTests\Core\TypedData\RecursiveContextualValidatorTest

I have moved it there. And now we have in that file two tests with ridiculously differing names testRecursiveValidate and testRecursiveValidation that test almost opposite functionality: first one tests that missing referenced entity doesn't break the validation and second that referenced entity not passing the validation will also raise violation at referencing one. I'm confused.

Not needed empty line at the beginning. Here we should explain why we are creating the 2 fields and how the recursive validation is triggered.

Tried to lay out an explanation.

Why is this needed?

To avoid violation at this field :-)

This is unnecessary.

But could be helpful ). I left to show that we don't set this field, but ok, removed.

Document what we except here and why.

I guess, expect? I put the whole explanation at the beginning of the test.

hchonov’s picture

Status: Needs review » Needs work

#5.1 still missing.

This test file tests the ValidReferenceConstraint. But here we introduce a new test dealing with the recursive validation and therefore should be placed under \Drupal\KernelTests\Core\TypedData\RecursiveContextualValidatorTest

I have moved it there. And now we have in that file two tests with ridiculously differing names testRecursiveValidate and testRecursiveValidation that test almost opposite functionality: first one tests that missing referenced entity doesn't break the validation and second that referenced entity not passing the validation will also raise violation at referencing one. I'm confused.

Then you can rename the new method to for example ::testRecursiveViolationPropagation().

  1. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/TestValidatedReferenceConstraint.php
    @@ -0,0 +1,24 @@
    + *   id = "TestValidatedReferenceConstraint",
    + *   label = @Translation("Reference recursive constraint.")
    

    The label needs to be updated now as well.

  2. +++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Validation/Constraint/TestValidatedReferenceConstraintValidator.php
    @@ -0,0 +1,29 @@
    + * Simple recursive validator.
    

    This should match the constraint - "Validates referenced entities".

  3. +++ b/core/tests/Drupal/KernelTests/Core/TypedData/RecursiveContextualValidatorTest.php
    @@ -41,4 +42,49 @@ public function testRecursiveValidate() {
    +    // We create 2 fields: one to hold entity reference and the second required
    +    // string field which should trigger validation failure. To the entity
    +    // reference field we attach a constraint that triggers validation of
    +    // referenced entity.
    +    // We will set required field on parent entity and not set on referenced
    +    // one. If things work correct, validating of parent entity will trigger
    +    // validation of referenced one, which should fail and trigger failure on
    +    // parent entity with violation path of reference field. If not, the
    +    // correct validation of required field for entity 1 will cancel its failure
    +    // for entity 2 and there will not be validation errors on entity 1.
    

    When documenting tests we don't need to explain what happens when something does not behave like expected. We document only what should happen. The reason is that today it fails for one reason, but tomorrow it might fail for another the the "if things don't work there will be.." might not be correct anymore. What about the following a little bit shorter text:

    "We create an entity reference field with a constraint which will trigger the validation of the referenced entities. Then we add a required field and populate it only on the parent entity, so that the child entity fails the validation."

  4. +++ b/core/tests/Drupal/KernelTests/Core/TypedData/RecursiveContextualValidatorTest.php
    @@ -41,4 +42,49 @@ public function testRecursiveValidate() {
    +    $entity_1 = EntityTest::create([
    ...
    +    $entity_2 = EntityTest::create([
    

    I think it makes sense to rename the entities to $parent and $child.

  5. +++ b/core/tests/Drupal/KernelTests/Core/TypedData/RecursiveContextualValidatorTest.php
    @@ -41,4 +42,49 @@ public function testRecursiveValidate() {
    +
    ...
    +
    ...
    +
    

    The new lines aren't really needed.

  6. +++ b/core/tests/Drupal/KernelTests/Core/TypedData/RecursiveContextualValidatorTest.php
    @@ -41,4 +42,49 @@ public function testRecursiveValidate() {
    +    $entity_1->field_test = $entity_2;
    

    Instead of doing this, we could first create the $child entity and then pass it to the values of $parent when creating it.

  7. +++ b/core/tests/Drupal/KernelTests/Core/TypedData/RecursiveContextualValidatorTest.php
    @@ -41,4 +42,49 @@ public function testRecursiveValidate() {
    +    $violations = $entity_1->validate();
    

    Let's document this: "The child entity should fail the validation and the violation should propagate to the parent."

  8. +++ b/core/tests/Drupal/KernelTests/Core/TypedData/RecursiveContextualValidatorTest.php
    @@ -41,4 +42,49 @@ public function testRecursiveValidate() {
    +    $this->assertEquals('field_test', $violations[0]->getPropertyPath());
    

    Also let's assert that the violation messages matches the expected message.

MeenakshiG’s picture

Status: Needs work » Needs review
FileSize
5.62 KB
3.88 KB

1. Updated label
2. Fixed comment
3. Adding suggested documentation
4. Renamed the entities to $parent and $child
5. Not fixed.
6. Not fixed.
7. Added documentation.
8. Not fixed.

gease’s picture

Please create a follow-up issue about this and reference it from here.

#3097071: Find out how to reuse validator objects in recursive validation

gease’s picture

The rest of comments addressed.

hchonov’s picture

Looks good now. Thank you!

hchonov’s picture

Status: Needs review » Reviewed & tested by the community
alexpott’s picture

Version: 8.9.x-dev » 8.8.x-dev
Status: Reviewed & tested by the community » Patch (to be ported)
+++ b/core/lib/Drupal/Core/Validation/ConstraintValidatorFactory.php
@@ -25,12 +28,13 @@ public function __construct(ClassResolverInterface $class_resolver) {
-    if (!isset($this->validators[$class_name])) {

The static cache was added in the first fix patch of #2197029: Allow to inject dependencies into validation constraints with no comment as to why it was necessary. I think we could ask for some performance testing here BUT still think this is probably the way to go because it's obvious from Symfony's current base class that these were not designed to be re-used like this. If we want to fix the bug then this is a path forward. If we can optimise this in the future that that can happen in #3097071: Find out how to reuse validator objects in recursive validation

alexpott’s picture

Committed and pushed 3cc125752b to 9.0.x and 5541aeb86a to 8.9.x. Thanks!

Going to discuss with other committers about backporting to 8.8.x

  • alexpott committed 3cc1257 on 9.0.x
    Issue #3096811 by gease, Meenakshi.g, hchonov: Reusing initialized...

  • alexpott committed 5541aeb on 8.9.x
    Issue #3096811 by gease, Meenakshi.g, hchonov: Reusing initialized...
Wim Leers’s picture

Issue tags: +Entity validation

Woah! 😨

This is critical because it concerns data integrity by preventing validation errors to propagate, which might result into storing something that shouldn't have been stored.

Does this have security implications?

hchonov’s picture

Yes, but only in the case of nested entity references, where the constraint validator of the one level triggers the validation of the next level.

catch’s picture

+1 to backporting this.

alexpott’s picture

Status: Patch (to be ported) » Fixed

  • alexpott committed 013f7f4 on 8.8.x
    Issue #3096811 by gease, Meenakshi.g, hchonov: Reusing initialized...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.