Skip to content

ConditionAwareValidator bypasses mandatory validation when AJAX condition call fails #127

Description

@maryahcm

The ConditionAwareValidator reads the session (tx_powermail_cond) to determine whether a field is hidden and should skip mandatory validation. This session is populated by the AJAX call in PowermailConditions.js on every field change.

If the AJAX call fails due to a brief connection interruption, the session retains a stale state. The validator then believes a field is hidden when it is should be actually visible, skips mandatory validation, and the form submits successfully with missing required fields.

Steps to reproduce

  1. Open a form with a conditional mandatory field (e.g. an email field shown only when "By email" is checked)
  2. Fill out the form without checking the conditional option
  3. Trigger a brief connection interruption (unstable Wi-Fi, mobile network handoff, VPN reconnect, etc.)
  4. While disconnected, check the conditional option — the AJAX call fails silently
  5. Connection resumes
  6. Submit the form

Result: Form submits without the mandatory email field.
Expected: Server-side validation rejects the submission.

Root cause
ConditionAwareValidator::isValidFieldInMandatoryValidation() trusts the session blindly:

$arguments = $GLOBALS['TYPO3_REQUEST']->getAttribute('frontend.user')->getSessionData('tx_powermail_cond');

If the AJAX call never updated the session (connection lost at that moment), the validator uses stale data.

Suggested fix
Recalculate conditions from the submitted POST data at validation time, instead of relying on the session. The POST contains the same tx_powermail_pi1 payload that the AJAX call would have sent:

protected function isValidFieldInMandatoryValidation(Field $field, $value): void
{
    $parsedBody = $GLOBALS['TYPO3_REQUEST']->getParsedBody() ?? [];
    $powermailArgs = $parsedBody['tx_powermail_pi1'] ?? [];

    // Fallback to session if POST data is unavailable
    if (empty($powermailArgs['field']) || empty($powermailArgs['mail']['form'])) {
        parent::isValidFieldInMandatoryValidation($field, $value);
        return;
    }

    // Recalculate conditions from submitted values
    $conditionContainer = $this->conditionContainerRepository->findOneByForm((int)$powermailArgs['mail']['form']);
    if ($conditionContainer === null) {
        parent::isValidFieldInMandatoryValidation($field, $value);
        return;
    }

    $form = $field->getPage()?->getForm();
    // Hydrate fields with POST values
    foreach ($form->getPages() as $page) {
        foreach ($page->getFields() as $f) {
            $fields[$f->getMarker()] = $f;
        }
    }
    foreach ($powermailArgs['field'] as $name => $val) {
        if (isset($fields[$name])) {
            $fields[$name]->setText(is_array($val) ? json_encode($val) : (string)$val);
        }
    }

    $arguments = $conditionContainer->applyConditions($form, $powermailArgs);

    // Check if field is hidden
    $formUid = $form->getUid();
    $pageUid = $field->getPage()->getUid();
    $action = $arguments[Condition::INDEX_TODO][$formUid][$pageUid][$field->getMarker()][Condition::INDEX_ACTION] ?? null;
    if ($action === Condition::ACTION_HIDE_STRING) {
        return;
    }

    // Field is visible — apply standard mandatory validation
    if (in_array($field->getType(), $this->mandatoryValidationFieldTypes)
        && $field->isMandatory()
        && !$this->validateMandatory($value)
    ) {
        $this->setErrorAndMessage($field, 'mandatory');
    }
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions