Problem

Follow-up for #2002158: Convert form validation of comments to entity validation: In the original issue all validation logic for fields has been converted to the entity validation API, however conditional validation logic that validates field values in dependence of another field value has not been converted. See CommentForm::validate() for the logic that needs conversion.

Proposed resolution

Solve #2105797: Add CompositeConstraintBase so that constraints involving multiple fields, such as CommentNameConstraint, can be discovered and apply the solution by moving validation logic to the CommentNameConstraint or other respective constraints.

Current code with annotations

parent::validate($form, $form_state);
    $entity = $this->entity;

    if (!$entity->isNew()) {

/** Does this hunk belong in a pre-save submit handler, not validate? There is no validation **/

      // Verify the name in case it is being changed from being anonymous.
      $accounts = $this->entityManager->getStorage('user')->loadByProperties(array('name' => $form_state->getValue('name')));
      $account = reset($accounts);
      $form_state->setValue('uid', $account ? $account->id() : 0);

/** This needs to be a validation handler (handler 1) **/

      $date = $form_state->getValue('date');
      if ($date instanceOf DrupalDateTime && $date->hasErrors()) {
        $form_state->setErrorByName('date', $this->t('You have to specify a valid date.'));
      }

/** This needs to be another validation handler (handler 2) **/

      if ($form_state->getValue('name') && !$form_state->getValue('is_anonymous') && !$account) {
        $form_state->setErrorByName('name', $this->t('You have to specify a valid author.'));
      }
    }

/** This exists - CommentName **/

    elseif ($form_state->getValue('is_anonymous')) {
      // Validate anonymous comment author fields (if given). If the (original)
      // author of this comment was an anonymous user, verify that no registered
      // user with this name exists.
      if ($form_state->getValue('name')) {
        $accounts = $this->entityManager->getStorage('user')->loadByProperties(array('name' => $form_state->getValue('name')));
        if (!empty($accounts)) {
          $form_state->setErrorByName('name', $this->t('The name you used belongs to a registered user.'));
        }
      }
    }

Remaining tasks

All.

User interface changes

-

API changes

-

Beta phase evaluation

Reference: https://www.drupal.org/core/beta-changes
Issue category Bug because the conversion is supposed to be complete and required for REST validation to cover it.
Issue priority Critical, because REST request might miss some validation logic.
Disruption Not disruptive as comment and form validation works as before, comment entity validation just becomes more complete.
Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Berdir’s picture

#2227503: Apply formatters and widgets to Comment base fields is doing a lot there already, but we have issues with the uid/author behavior, I don't know how to fix that.

larowlan’s picture

Priority: Critical » Major
Status: Active » Postponed

Is this a critical if we have a critical meta? I'd think that'd make this a major?
Also postponed on issue in OP

larowlan’s picture

Priority: Major » Critical

Discussed with catch

larowlan’s picture

Issue summary: View changes

Added proposed handler to issue summary - I see there are three, and one hunk that belongs in a pre-save handler (is not to do with validaton)

larowlan’s picture

Assigned: Unassigned » larowlan
Status: Postponed » Active

Taking this on, doesn't need that issue to progress from what I can see

larowlan’s picture

Issue summary: View changes

handler 3 already exists

larowlan’s picture

larowlan’s picture

Status: Active » Needs review
Issue tags: +CriticalADay
FileSize
6.21 KB

WIP, doesn't touch 'date' yet - @Berdir indicated there are fixes in #2227503: Apply formatters and widgets to Comment base fields for that, so posting what I have and moving over there

kim.pepper’s picture

+++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
@@ -0,0 +1,34 @@
+      // @todo Properly inject dependency https://drupal.org/node/2197029
+      $accounts = \Drupal::entityManager()->getStorage('user')->loadByProperties(array('name' => $author_name));

If you want to inject dependencies, just implement \Drupal\Core\Plugin\ContainerFactoryPluginInterface::create

Status: Needs review » Needs work

The last submitted patch, 8: dynamic-constraints.wip_.1.patch, failed testing.

fago’s picture

+++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
@@ -0,0 +1,34 @@
+      // @todo Properly inject dependency https://drupal.org/node/2197029
+      $accounts = \Drupal::entityManager()->getStorage('user')->loadByProperties(array('name' => $author_name));

We need take account of the settings also, i.e. check CommentForm #required name.

      '#required' => ($this->currentUser->isAnonymous() && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT),

The same applies to email and homepage.

The form also uses "name" to assign the comment owner if it matches a user name. Imo, we can/should use that in buildEntity() to update the comment owner and name fields respectively, then comment validation should be able to run from there.

larowlan’s picture

Status: Needs work » Needs review
FileSize
5.75 KB
10.22 KB

more work

fixes #11
#9 see #2197029: Allow to inject dependencies into validation constraints

still to do - date

Also think #2227503: Apply formatters and widgets to Comment base fields will come back green now - but some questions around UX

larowlan’s picture

+++ b/core/modules/comment/src/Entity/Comment.php
@@ -541,7 +541,7 @@ public function getOwner() {
-    return $this->get('uid')->target_id;
+    return (int) $this->get('uid')->target_id;

this ensures the === 0 in the validator works, because "0" (from form submissions) !== 0

Status: Needs review » Needs work

The last submitted patch, 12: dynamic-constraints.wip_.2.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
4.66 KB
11.02 KB

Status: Needs review » Needs work

The last submitted patch, 15: dynamic-constraints.15.patch, failed testing.

andypost’s picture

+++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
@@ -21,19 +21,10 @@ class CommentAuthorConstraintValidator extends ConstraintValidator {
+      $anonymous_contact = $items->getEntity()->getCommentedEntity()->get($items->getEntity()->getFieldName())->getFieldDefinition()->getSettings()['anonymous'];

protected get6X() helper would be great!

  1. +++ b/core/modules/comment/src/Entity/Comment.php
    @@ -540,7 +541,7 @@ public function getOwner() {
       public function getOwnerId() {
    -    return $this->get('uid')->target_id;
    +    return (int) $this->get('uid')->target_id;
    

    needs comment/todo why limiting type

  2. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
    @@ -0,0 +1,33 @@
    +class CommentAuthorConstraintValidator extends ConstraintValidator {
    ...
    +  public function validate($items, Constraint $constraint) {
    ...
    +    if ($items->getEntity()->getOwnerId() === 0) {
    

    why not check that entity implements getOwnerId() method?

  3. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
    @@ -0,0 +1,33 @@
    +    /** @var \Drupal\Core\Field\FieldItemList $items */
    +    $author_name = $items->first()->value;
    ...
    +      if (empty($author_name) && $anonymous_contact == COMMENT_ANONYMOUS_MUST_CONTACT) {
    

    author name used latter and isEmpty() looks more reasonable

larowlan’s picture

Thanks @andypost will do

larowlan’s picture

Tagging for today's office hours

kim.pepper’s picture

Assigned: larowlan » kim.pepper

Taking a look

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
11.62 KB
2.62 KB

This (hopefully) addresses the points in #17 Didn't get time to look at the fail, sorry.

kim.pepper’s picture

Assigned: kim.pepper » Unassigned

Status: Needs review » Needs work

The last submitted patch, 21: 2403485-dynamic-constraints-21.patch, failed testing.

kim.pepper’s picture

Status: Needs work » Needs review
FileSize
11.67 KB
957 bytes

Rolled back the isEmpty() change as it caused more fails.

Status: Needs review » Needs work

The last submitted patch, 24: 2403485-dynamic-constraints-24.patch, failed testing.

amitgoyal’s picture

Issue tags: -Needs reroll
idebr’s picture

Status: Needs work » Needs review
FileSize
1.07 KB
12.52 KB

This should fix the failing test. Just date left now.

larowlan’s picture

Legend! thanks

+++ b/core/modules/comment/src/CommentForm.php
@@ -266,9 +266,11 @@ public function validate(array $form, FormStateInterface $form_state) {
+        $entity->name->value = $form_state->getValue('name');

This should happen in ::buildEntity? Which should in turn trigger the validation in ::validate(). I wonder why that isn't happening

larowlan’s picture

Status: Needs review » Reviewed & tested by the community
FileSize
1.5 KB
12.99 KB

Fixes #28
CommentForm wasn't using buildEntity in the same fashion as other entity forms, is now.

Now onto date

larowlan’s picture

Status: Reviewed & tested by the community » Needs review

didn't mean that

larowlan’s picture

Does date and the rest

jibran’s picture

Over are patch look good. Just some minor issues. I think it is ready.

  1. +++ b/core/modules/comment/src/CommentForm.php
    @@ -247,30 +247,29 @@ protected function actions(array $form, FormStateInterface $form_state) {
    +    $entity = $this->buildEntity($form, $form_state);
    

    Let's call it comment. Comment entity and commented entity is already very confusing imo.

  2. +++ b/core/modules/comment/src/Entity/Comment.php
    @@ -540,7 +541,9 @@ public function getOwner() {
    +    return (int) $this->get('uid')->target_id;
    

    I think it is fixed by #2232477: Fatal when adding new fields with NOT NULL constraints in a base table that contains existing entities.

  3. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
    @@ -0,0 +1,71 @@
    +    if (($entity = $items->getEntity()) &&
    

    This is a comment right? Can we rename it?

  4. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
    @@ -0,0 +1,71 @@
    +        $anonymous_contact = $this->getAnonymousContactSetting($entity);
    

    I confused it with core contact entity can we rename this?

  5. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
    @@ -0,0 +1,71 @@
    +      elseif (!$entity->isNew() || (!\Drupal::currentUser()->isAnonymous()) && $author_name) {
    

    You can use isAuthenticated instead.

  6. +++ b/core/modules/comment/src/Tests/CommentValidationTest.php
    @@ -36,6 +37,10 @@ protected function setUp() {
    +    $user = entity_create('user', array('name' => 'test'));
    

    User::create

larowlan’s picture

Fixed #32 except for #2 - see the comment in the code for the reasoning.

jibran’s picture

Status: Needs review » Reviewed & tested by the community

Thank you for the fixes.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work
  1. +++ b/core/modules/comment/src/CommentForm.php
    @@ -247,30 +247,29 @@ protected function actions(array $form, FormStateInterface $form_state) {
    -      if ($date instanceOf DrupalDateTime && $date->hasErrors()) {
    -        $form_state->setErrorByName('date', $this->t('You have to specify a valid date.'));
    -      }
    ...
    +      // @todo Remove when https://www.drupal.org/node/2395831 or
    +      //   https://www.drupal.org/node/2227503 is in.
    +      $result = $comment->created->validate();
    +      if ($result->count()) {
    +        $form_state->setErrorByName('date', $result->offsetGet(0)->getMessage());
           }
    ...
    +      if ($comment->name->value) {
    

    This looks untested.

  2. +++ b/core/modules/comment/src/CommentForm.php
    @@ -247,30 +247,29 @@ protected function actions(array $form, FormStateInterface $form_state) {
    -      if ($form_state->getValue('name') && !$form_state->getValue('is_anonymous') && !$account) {
    -        $form_state->setErrorByName('name', $this->t('You have to specify a valid author.'));
    

    Where has this validation gone?

fago’s picture

Assigned: Unassigned » fago
  1. +++ b/core/modules/comment/src/CommentForm.php
    @@ -247,30 +247,29 @@ protected function actions(array $form, FormStateInterface $form_state) {
    +    if (!$comment->isNew()) {
    +      // @todo Remove when https://www.drupal.org/node/2395831 or
    ...
    +      $result = $comment->created->validate();
    

    I think we should be able to do all validation logic with constraints now, and move conditional stuff into constraints.

    Then we need to solve #2395831: Entity forms skip validation of fields that are not in the EntityFormDisplay just to make sure the violations are not wrongly ignored. We might have to change constraints registration a bit, but as the logic won't change it makes sense to convert this now. I'll take a look at it.

  2. +++ b/core/modules/comment/src/CommentForm.php
    @@ -287,6 +287,15 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
    +      // Verify the name in case it is being changed from being anonymous.
    +      $accounts = $this->entityManager->getStorage('user')
    

    So invalid name validation will be done by the constraint, right? So let's comment that.
    Usually, this would be just a widget imo, and thus widget validation, but as we've got the $comment->name this case a bit different.

  3. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
    @@ -0,0 +1,71 @@
    + * Validates the CommentName constraint.
    ...
    +class CommentAuthorConstraintValidator extends ConstraintValidator {
    

    wrong comment.

  4. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentAuthorConstraintValidator.php
    @@ -0,0 +1,71 @@
    +      elseif (!$comment->isNew() || (\Drupal::currentUser()->isAuthenticated()) && $author_name) {
    

    I don't think validation should be different depending on the currently logged in user - imo we should avoid that as far as possible. It should be enough to verify the comment name matches the set owner, if given.

fago’s picture

Status: Needs work » Needs review
FileSize
14.78 KB

ok, worked over the patch to move all conditional logic intro constraints, which get always invoked. Also, I addressed my remarks. ad 2), this actually is not about validation but building the entity, thus I fixed the comment instead. I moved all name related validation logic in the comment name constraint, as I don't think it makes much sense to have to custom constraints for the same field - both wouldn't be re-usable anyway.

Finally, I extended validation a bit such that we ensure a stored author name matches a stored comment author. As usually, we store the comment name in addition to the comment author reference I guess it would make sense to enforce that via a presave hook. However, changing that behaviour seems to be out-of-scope for this issue, thus I just made sure any saved author name value matches (and added test coverage for that).

larowlan’s picture

FileSize
15.85 KB

Interdiff for 37

andypost’s picture

+++ b/core/modules/comment/src/CommentForm.php
@@ -201,12 +201,6 @@ public function form(array $form, FormStateInterface $form_state) {
-    $form['is_anonymous'] = array(
...
-      '#value' => ($comment->id() ? !$comment->getOwnerId() : $this->currentUser->isAnonymous()),

@@ -243,43 +237,10 @@ protected function actions(array $form, FormStateInterface $form_state) {
-    elseif ($form_state->getValue('is_anonymous')) {

is this really gone?

+++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentNameConstraintValidator.php
@@ -19,18 +20,60 @@ class CommentNameConstraintValidator extends ConstraintValidator {
+    if (isset($author_name) && $author_name !== '' && $comment->getOwnerId() === 0) {
...
+    elseif (isset($author_name) && $author_name !== '' && $comment->getOwnerId()) {
...
+    if ($comment->getOwnerId() === 0 && empty($author_name)) {

Type checking could fail. Suppose better to use isAnonymous() on author entity.
Also 0 has special constant.

larowlan’s picture

is this really gone?

grep says yes

andypost’s picture

so if current user switched somehow between form build and validate then form processing is wrong

larowlan’s picture

+++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentNameConstraintValidator.php
@@ -19,18 +20,60 @@ class CommentNameConstraintValidator extends ConstraintValidator {
+    if ($comment->getOwnerId() === 0 && empty($author_name)) {
+      if ($this->getAnonymousContactDetailsSetting($comment) == COMMENT_ANONYMOUS_MUST_CONTACT) {

Could be combined into one if.

So those two minor changes and I think this is RTBC again. I'll talk someone through it during tomorrow's critical office hours.

Manually tested removal of the old is_anonymous - works great

larowlan’s picture

or I'll just do it now :)
fixed #39 and #42

Status: Needs review » Needs work

The last submitted patch, 43: comment-constraints.43.patch, failed testing.

larowlan’s picture

Status: Needs work » Needs review
FileSize
1.43 KB
14.76 KB

We can't use getOwner()->isAnonymous() because sometimes the user entity is not set.

I think this is rtbc again

fago’s picture

so if current user switched somehow between form build and validate then form processing is wrong

Right. Seems wrong if you would do that to me though.

Thanks larowan for the changes, looks good. I agree that this is ready again.

jibran’s picture

So @fago +1ed it so nothing functional just extreme nits

  1. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentNameConstraint.php
    @@ -19,6 +19,8 @@
    +  public $messageNameTaken = 'The name you used (%name) belongs to a registered user.';
    +  public $messageRequired = 'You have to specify a valid author.';
    +  public $messageMatch = 'The specified author name does not match the comment author.';
    

    @var blocks missing.

  2. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentNameConstraintValidator.php
    @@ -19,18 +20,59 @@ class CommentNameConstraintValidator extends ConstraintValidator {
    +    $comment = $items->getEntity();
    +    /** @var CommentInterface $comment */
    

    These can switch places.

  3. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentNameConstraintValidator.php
    @@ -19,18 +20,59 @@ class CommentNameConstraintValidator extends ConstraintValidator {
    +    /** @var CommentNameConstraint $constraint */
    

    I don't see this var anywhere? Maybe stale comment. Or move it near the use of var :)

  4. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentNameConstraintValidator.php
    @@ -19,18 +20,59 @@ class CommentNameConstraintValidator extends ConstraintValidator {
    +      $this->getAnonymousContactDetailsSetting($comment) == COMMENT_ANONYMOUS_MUST_CONTACT) {
    

    Can we use === here?

  5. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentNameConstraintValidator.php
    @@ -19,18 +20,59 @@ class CommentNameConstraintValidator extends ConstraintValidator {
    +   * Gets the anonymous contact details setting for the entity.
    

    Gets the anonymous contact details setting form the comment.

  6. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentNameConstraintValidator.php
    @@ -19,18 +20,59 @@ class CommentNameConstraintValidator extends ConstraintValidator {
    +   * @param \Drupal\Core\Entity\EntityInterface $entity
    ...
    +  protected function getAnonymousContactDetailsSetting(CommentInterface $entity) {
    

    @param type mismatch. Can we please rename it to comment?

  7. +++ b/core/modules/comment/src/Plugin/Validation/Constraint/CommentNameConstraintValidator.php
    @@ -19,18 +20,59 @@ class CommentNameConstraintValidator extends ConstraintValidator {
    +      ->getSettings()['anonymous'];
    

    I think we can use getSetting('anonymous') here

larowlan’s picture

Fixes ##4

jibran’s picture

Thanks for the fixes. As per #46 it is RTBC.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

This issue addresses a critical bug and is allowed per https://www.drupal.org/core/beta-changes. Committed 91a0564 and pushed to 8.0.x. Thanks!

+++ b/core/modules/comment/src/CommentForm.php
@@ -243,43 +237,10 @@ protected function actions(array $form, FormStateInterface $form_state) {
   /**
-   * Overrides Drupal\Core\Entity\EntityForm::validate().
-   */
-  public function validate(array $form, FormStateInterface $form_state) {

Lovely.

  • alexpott committed 91a0564 on 8.0.x
    Issue #2403485 by larowlan, kim.pepper, idebr, fago: Complete conversion...
Wim Leers’s picture

This also opens the door to interesting new comment UIs and mechanisms in (headless) D8, without having to reimplement all validation :) Great work!

bzrudi71’s picture

Nice! This one fixed all remaining exceptions in #2356967: PostgreSQL: Fix tests in comment test group and just leaves 2 fails open in new CommentRestExportTest tests. Great!

fago’s picture

Assigned: fago » Unassigned

Done. :)

Status: Fixed » Closed (fixed)

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