Problem/Motivation

When Mosparo is used via the CAPTCHA module (mosparo_captcha) on entity forms (e.g. node_*_form), server-side validation fails with the generic CAPTCHA error and the log message "The form is not submittable." from mosparo_captcha.

The same Mosparo connection works correctly on simple core forms such as user_login_form.

The root cause appears to be in MosparoService::extractFieldInformation(). That method does not correctly discover fields on typical Drupal entity edit forms:

Entity field wrappers use #type => container. The method only inspects direct children of containers. The actual input is under widget, so fields like field_member_email are never added to the field information array used for API verification.

Field Group / fieldset elements use #type => fieldset or details, not container. These are registered as a single field entry (e.g. group_main) and their nested fields are not extracted recursively.

As a result, the Mosparo frontend widget validates visible inputs in the browser, but mosparo_captcha_captcha_validation() calls prepareFormData() with an incomplete or empty set of required/verifiable fields. verifySubmission() then returns a result where isSubmittable() is FALSE, or required/verifiable field checks do not match what Mosparo verified on the client.

Example: Member registration form node_app_member_form at /register (anonymous user, CAPTCHA point enabled for that form ID). Login with the same Mosparo connection works; node add does not.

Environment: Drupal 11, mosparo_integration + mosparo_captcha, CAPTCHA module, Field Group on entity forms.

Steps to reproduce

Install and enable mosparo_integration, mosparo_captcha, and captcha.
Configure a Mosparo connection and set it as the default CAPTCHA challenge (captcha.settings: default_challenge = mosparo_captcha/mosparo (connection: …)).
Enable a CAPTCHA point for an entity form with multiple fields and Field Groups, e.g. node_app_member_form (or any node_*_form with field widgets inside fieldsets).
Optionally compare with a CAPTCHA point on user_login_form (should work).
As an anonymous user, open the entity create form (e.g. /node/add/app_member or a custom route that builds the same form).
Fill in visible required fields and complete the Mosparo check.
Submit the form.
Expected: Form submits successfully when Mosparo validation passes.

Actual: Form does not submit; user sees the CAPTCHA failure message (e.g. “The answer you entered for the CAPTCHA was not correct.”). Watchdog/log contains: mosparo_captcha: The form is not submittable.

Verification via Drush (optional):

$form = \Drupal::service('entity.form_builder')->getForm($node, 'default');
$fs = new \Drupal\Core\Form\FormState();
$fs->setCompleteForm($form);
$info = \Drupal::service('mosparo_integration.service')->extractFieldInformation($fs);
// field_member_email (and most entity fields) are missing from $info
// Compare with user_login_form where 'name' is present.

On user_login_form, name is extracted with type => textfield. On entity forms, fields with #type => container and widgets under widget are not extracted.

Proposed resolution

Improve MosparoService::extractFieldInformation() to support common Drupal form structures used by entity forms:

1. Recursive traversal of form elements (fieldset, details, container, and other grouping types used by Field Group).
2. Entity field containers: when an element has a widget child, derive field metadata from the widget (and field definition where available), and use the correct value key (e.g. field_name[0][value] for standard fields; special handling for address, link, etc.).
3. Respect #access: skip elements (and descendants) where #access is FALSE, so hidden/admin-only fields are not included in required/verifiable field lists.
4. Align with prepareFormData() / prepareFlatStructure() so server-side field keys match what Mosparo validates in the browser.

Consider adding or documenting use of MosparoIntegrationFilterFormDataEvent only as a fallback; the primary fix should be in field extraction so CAPTCHA, Contact, and Webform integrations behave consistently.

Add functional or kernel test coverage for an entity form with at least one field inside a fieldset and one top-level entity field widget.

Remaining tasks

Implement recursive field extraction in MosparoService::extractFieldInformation()

Handle entity widget containers and common field types (email, string, textfield, textarea, address, telephone, etc.)

Skip inaccessible fields (#access === FALSE)

Add automated test(s) for entity form + CAPTCHA validation path

Verify mosparo_webform / mosparo_contact are unaffected or benefit from the same logic

Update module documentation if integrators relied on workarounds

User interface changes

None expected. End users should only see correct Mosparo/CAPTCHA behaviour on entity forms (successful submit when the challenge is passed). No new UI components required for the fix.

API changes

Internal / service behaviour (backward compatible if done correctly):
- MosparoService::extractFieldInformation() would return a more complete and accurate field map for entity forms and nested field groups.
- prepareFormData() would receive correct $elements, improving requiredFields and verifiableFields passed to verifySubmission() without requiring custom code in consuming projects.
Public API:
- No breaking changes to entity/config schema anticipated.
- Existing events (mosparo_integration.filter_field_types, mosparo_integration.filter_form_data) remain available for edge-case adjustments.

Data model changes

None.

Comments

cola created an issue. See original summary.

zepich’s picture

Hi @cola

Thank you very much for reporting this issue and for all the details you included.

I'll test it and will adjust the logic. The idea, as you describe, is that the functionality can handle these forms as well.

I'll inform you as soon as I have more information.

Kind regards,
zepich

cola’s picture

Hi @zepich

exactly, because drupal allow to also put "captcha" on this forms. let me know and we will test it.

regards
cola

  • zepich committed 31e379e1 on 1.0.x
    Adjusted the logic to collect the form fields in containers correctly,...
zepich’s picture

Hi @cola

I've tested it and reproduced the issue.

I'm not sure exactly what happened. Either there was a logical issue in my initial implementation, or something changed in Drupal. But the logic was already trying to handle the container fields, just in the wrong way.

I now understand the logic of that one and have extended the MosparoService to catch the container fields, too.

Commit: https://git.drupalcode.org/project/mosparo_integration/-/commit/31e379e1...

I've tested it with a node form, a Contact form, and a Webform form. Please take a look and let me know what you think.

Do you know if there can be more than three levels of the structure? If yes, we would have to implement a real recursive solution. In my tests, I was unable to have more than 3 levels.

Thank you very much for your feedback!

Kind regards,
zepich

cola’s picture

Status: Active » Needs work

thank you for the fast patch!

Client-side validation works (checkbox checked, both tokens present, mosparo__checked). Submission still fails with "The form is not submittable."

On our node_app_member_form, Mosparo frontend validates fields including:

field_member_gender, field_member_nationality, field_member_civil_status (options_select, #name without [0][value])
field_member_birthday[0][value][date] (datetime)
field_member_address[0][address][address_line1], [postal_code], [locality] (address)
field_member_phone[0][value] (tel)
After commit 31e379e1, extractFieldInformation() still misses options_select widgets and uses wrong keys for address/datetime. prepareFormData() therefore sends incomplete data to verifySubmission() although the browser tokens are valid.

zepich’s picture

Status: Needs work » Needs review

Hi cola

Thank you very much for your feedback.

I discovered I was missing some modules, so I didn't have the field types (date, date range, address). When I tested, I only had the simple ones.

After I had these types, I found my problem with the logic, the widget structure, and what I did wrong. Before, I tried to make things more complicated by manually rebuilding the field names. But the name is in the data structure, so it's pretty easy to build the list of fields. So, I've now refactored the logic to collect all fields with the correct field names.

Commit: https://git.drupalcode.org/project/mosparo_integration/-/commit/efafeeff...

I'm looking forward to your feedback!

Kind regards,
zepich

cola’s picture

Status: Needs review » Needs work

Hi zepich

After the refactor (findFields()), most fields work on our node_app_member_form. Mosparo backend still shows verification failed for field_member_gender only.

Cause: For required options_select widgets, the structure is widget → #name + delta 0 → value. findFields() recurses into 0 and never registers #name on the parent select element.

Works: field_member_nationality, field_member_civil_status (no delta child).

Fails: field_member_gender (has widget[0]).

Suggested fix: Before recursing, if #name and #type exist on the current element, register it. Or treat numeric delta keys like 0 as part of field widgets, not as “go deeper only”.

Drush check: extractFieldInformation() returns no entry for field_member_gender while POST contains field_member_gender=male.

Regards

zepich’s picture

Hi cola

Thank you very much for your feedback.

Unfortunately, I was not able to reproduce the issue with my forms, but I fully agree with you. We can switch the logic to collect the field as soon as the element has a #name and #type set. Collecting more fields than we need in this method doesn't hurt us, so why not do it?

While testing the forms, I discovered an issue with selects that allow multiple selected values. I've adjusted that as well.

Both fixes are in this commit: https://git.drupalcode.org/project/mosparo_integration/-/commit/731689b7...

I'm looking forward to your feedback!

Kind regards,
zepich

cola’s picture

Hi zepich,

Thank you for the fixes in commit 731689b7 — field_member_gender is now correctly extracted and Mosparo backend shows Is valid: Yes for our submission. However, the Drupal form still fails with the CAPTCHA error and the log message "The form is not submittable." from mosparo_captcha.

Root cause (verified on our site):

The Mosparo API verification itself succeeds (isSubmittable() is true, backend shows valid). The failure happens in the additional field-list check in mosparo_captcha_captcha_validation():

$requiredFieldDifference = array_diff($requiredFields, $verifiedFields);
$verifiableFieldDifference = array_diff($verifiableFields, $verifiedFields);
if ($res->isSubmittable() && empty($requiredFieldDifference) && empty($verifiableFieldDifference)) {

On our node_app_member_form (member registration with Address field), extractFieldInformation() / findFields() collects the hidden address country field:

field_member_address[0][address][country_code] — type address_country, marked as required (parent address fieldset is required)
This field is a hidden input (). The Mosparo frontend widget does not verify it, and it does not appear in the verified fields returned by the API.

All visible fields from our form are present in the Mosparo submission (nationality, gender, name, birthday, email, phone, address lines, bank fields, etc.). Only country_code causes the mismatch:

requiredFieldDifference: field_member_address[0][address][country_code]
verifiableFieldDifference: (empty)
So Mosparo says the submission is valid, but Drupal rejects it because one server-side "required" field was never verified client-side.

Suggested fix:

In findFields() / extractFieldInformation(), skip fields that are not verified by the Mosparo frontend, for example:

#type = hidden
#type = address_country (and possibly other address sub-components that are hidden)
elements with #access === FALSE (including nested ones like revision_log in hidden containers)
Alternatively, adjust the CAPTCHA validation to only compare fields that were actually included in $formData sent to verifySubmission(), or treat hidden/system fields as non-required for the array_diff check.

Separate issue — PHP 8.4 deprecations:

We also see deprecation warnings from mosparo/php-api-client v1.1.0 when submitting:

Implicitly marking parameter $submitToken as nullable is deprecated
Implicitly marking parameter $validationToken as nullable is deprecated
In Mosparo\ApiClient\Client, parameters like string $submitToken = null should be ?string $submitToken = null (same for validateSubmission() and getStatisticByDate()). This does not cause the submission failure, but it floods our logs on PHP 8.4+.

Environment: Drupal 11, PHP 8.4, mosparo_integration 1.0.x-dev (731689b7), mosparo/php-api-client v1.1.0, Address module, Field Group, CAPTCHA on node_app_member_form.

Happy to test again once hidden/internal address fields are excluded from the required-field comparison.

Kind regards,
cola

zepich’s picture

Hi cola

Thank you very much for all your help, testing, and feedback!

I had a look at the logic and found the problem. Since we switched the order in which we collect the fields, we ended up with one check too many. In the case of the country code field (and maybe other fields as well), a lower-level widget contains the required information, such as `#type => hidden` and `#access => false`.

But because of the switched logic, we didn't overwrite the information from the country_code widget at the first level (where the type is `address_country`) with the lower-level widget (where the type is `hidden`).

https://git.drupalcode.org/project/mosparo_integration/-/commit/22e68e43...

I've removed that extra check, and now the field information is collected correctly, and the country code select is ignored when it is invisible.

That fix should work with the other fields too, because before this logic was below the foreach with the subfields and ensured that the higher level didn't overwrite any fields from the lower level.

Deprecation messages:

Thank you very much for pointing that out. I've just released v1.1.1, which fixes these messages.

Kind regards,
zepich

cola’s picture

Hi zepich

Is working (last DEV Version)... Did you release a new version with this changes?

Regards
cola

zepich’s picture

Assigned: Unassigned » zepich
Status: Needs work » Fixed

Hi cola

Awesome! I just did, v1.0.4.

Thank you very much for reporting this issue and for your awesome help in fixing it!

Kind regards,
zepich

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

zepich’s picture

Status: Fixed » Closed (fixed)