diff --git a/pos-module-captchas-hcaptcha/CHANGELOG.md b/pos-module-captchas-hcaptcha/CHANGELOG.md index 761651f..a4d677c 100644 --- a/pos-module-captchas-hcaptcha/CHANGELOG.md +++ b/pos-module-captchas-hcaptcha/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.1.0 + +- Errors are now recorded with `modules/captchas/helpers/add_error`, so `result.errors` holds + translated messages instead of `modules/captchas/errors.*` keys — drop any `| t` you apply + to them. Requires `captchas ^1.1.0`. +- Other code improvements: internal cleanups with no change to behavior or the public API. + ## 1.0.0 Initial release. hCaptcha provider for the diff --git a/pos-module-captchas-hcaptcha/README.md b/pos-module-captchas-hcaptcha/README.md index c583264..41dbfda 100644 --- a/pos-module-captchas-hcaptcha/README.md +++ b/pos-module-captchas-hcaptcha/README.md @@ -95,6 +95,9 @@ non-JSON bodies (`modules/captchas/errors.request_failed`). A `success: false` a `modules/captchas/errors.verification_failed`. `expected_hostname` (handled by the abstraction) works with the `hostname` field hCaptcha returns. +Errors are stored via `modules/captchas/helpers/add_error`, which translates the message as it +is set — `result.errors.captcha` holds display-ready text, not translation keys. + > ⚠️ **Billing** — the server-side verify call is a normal, billable platformOS API call; > each `verify` performs one outbound `siteverify` request. diff --git a/pos-module-captchas-hcaptcha/app/lib/test/captchas_hcaptcha/verify_test.liquid b/pos-module-captchas-hcaptcha/app/lib/test/captchas_hcaptcha/verify_test.liquid index d973b51..b4bdd1a 100644 --- a/pos-module-captchas-hcaptcha/app/lib/test/captchas_hcaptcha/verify_test.liquid +++ b/pos-module-captchas-hcaptcha/app/lib/test/captchas_hcaptcha/verify_test.liquid @@ -13,4 +13,21 @@ function r_fail = 'modules/captchas/commands/captcha/verify', provider: 'hcaptcha', secret: '0x0000000000000000000000000000000000000000', token: 'not-a-real-token' function contract = 'modules/tests/assertions/not_valid_object', contract: contract, field_name: 'garbage_token_fails', object: r_fail function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'captcha', object: r_fail.errors + + # The message is stored translated, not as a raw key: this provider module sets the error via + # modules/captchas/helpers/add_error, which resolves a modules/captchas/* key from inside + # modules/captchas_hcaptcha. Callers render errors verbatim, so a raw key would leak to users. + # Reuses r_fail — no extra billable call. + assign fail_key = 'modules/captchas/errors.verification_failed' + assign fail_message = r_fail.errors.captcha | first + assign expected_message = fail_key | t + function contract = 'modules/tests/assertions/equal', contract: contract, given: fail_message, expected: expected_message, field_name: 'garbage_token_message_translated' + + # The check above alone would pass if `| t` silently returned the key on both sides, so assert + # the stored value is not the key itself — that is the failure mode this guards against. + assign is_raw_key = false + if fail_message == fail_key + assign is_raw_key = true + endif + function contract = 'modules/tests/assertions/equal', contract: contract, given: is_raw_key, expected: false, field_name: 'garbage_token_message_not_raw_key' %} diff --git a/pos-module-captchas-hcaptcha/app/views/pages/verify.liquid b/pos-module-captchas-hcaptcha/app/views/pages/verify.liquid index 4802697..d36c140 100644 --- a/pos-module-captchas-hcaptcha/app/views/pages/verify.liquid +++ b/pos-module-captchas-hcaptcha/app/views/pages/verify.liquid @@ -25,10 +25,14 @@ method: post

Submitted message: {{ context.params.message }}

{% else %}

❌ Captcha failed ({{ result.provider }})

+ {% comment %} + result.errors holds already-translated messages (the commands translate at set time, via + modules/captchas/helpers/add_error), so they render as-is — no `| t` here. + {% endcomment %} diff --git a/pos-module-captchas-hcaptcha/modules/captchas_hcaptcha/public/lib/commands/verify.liquid b/pos-module-captchas-hcaptcha/modules/captchas_hcaptcha/public/lib/commands/verify.liquid index 7465baa..96115f7 100644 --- a/pos-module-captchas-hcaptcha/modules/captchas_hcaptcha/public/lib/commands/verify.liquid +++ b/pos-module-captchas-hcaptcha/modules/captchas_hcaptcha/public/lib/commands/verify.liquid @@ -5,20 +5,19 @@ @param {object} object - validated object: { secret, token, remote_ip, expected_sitekey, errors }. expected_sitekey (optional) is forwarded as hCaptcha's `sitekey` param — hCaptcha secrets are account-wide, so this is what stops a token issued for another sitekey from being redeemed here. - @returns {object} object with success (boolean), response (hash), valid, and errors. + @returns {object} object with success (boolean), response (hash), valid, and errors + (translated messages, via modules/captchas/helpers/add_error). {% enddoc %} {% liquid - assign payload = '{}' | parse_json - assign payload = payload | hash_merge: secret: object.secret, token: object.token, remote_ip: object.remote_ip, sitekey: object.expected_sitekey + assign payload = { "secret": object.secret, "token": object.token, "remote_ip": object.remote_ip, "sitekey": object.expected_sitekey } graphql result = 'modules/captchas_hcaptcha/api_call/send', data: payload, name: 'modules/captchas_hcaptcha/siteverify' assign call = result.api_call_send - function c = 'modules/captchas/helpers/errors_hash', errors: object.errors if call.errors != blank - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -26,8 +25,8 @@ assign status = call.response.status if status != 200 - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -39,8 +38,8 @@ assign raw_body = call.response.body | strip assign first_char = raw_body | slice: 0, 1 if first_char != '{' - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -53,8 +52,8 @@ if body.success assign object.valid = true else - assign messages = 'modules/captchas/errors.verification_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.verification_failed' + assign object.errors = updated_errors assign object.valid = false endif diff --git a/pos-module-captchas-hcaptcha/pos-module.json b/pos-module-captchas-hcaptcha/pos-module.json index 1e5e3a5..8587c30 100644 --- a/pos-module-captchas-hcaptcha/pos-module.json +++ b/pos-module-captchas-hcaptcha/pos-module.json @@ -1,9 +1,9 @@ { "name": "pOS Captchas hCaptcha", "machine_name": "captchas_hcaptcha", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { - "captchas": "^1.0.0" + "captchas": "^1.1.0" }, "devDependencies": { "tests": "^1.3.4" diff --git a/pos-module-captchas-recaptcha/CHANGELOG.md b/pos-module-captchas-recaptcha/CHANGELOG.md index ccbc8e2..0d28ff2 100644 --- a/pos-module-captchas-recaptcha/CHANGELOG.md +++ b/pos-module-captchas-recaptcha/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.1.0 + +- Errors are now recorded with `modules/captchas/helpers/add_error`, so `result.errors` holds + translated messages instead of `modules/captchas/errors.*` keys — drop any `| t` you apply + to them. Requires `captchas ^1.1.0`. +- Other code improvements: internal cleanups with no change to behavior or the public API. + ## 1.0.0 Initial release. Google reCAPTCHA v2 (checkbox / invisible) provider for the diff --git a/pos-module-captchas-recaptcha/README.md b/pos-module-captchas-recaptcha/README.md index 4ed1455..85e4985 100644 --- a/pos-module-captchas-recaptcha/README.md +++ b/pos-module-captchas-recaptcha/README.md @@ -100,6 +100,9 @@ responses, and non-JSON bodies (`modules/captchas/errors.request_failed`). A `expected_hostname` (handled by the abstraction) works with the `hostname` field reCAPTCHA returns. +Errors are stored via `modules/captchas/helpers/add_error`, which translates the message as it +is set — `result.errors.captcha` holds display-ready text, not translation keys. + > ⚠️ **Billing** — the server-side verify call is a normal, billable platformOS API call; > each `verify` performs one outbound `siteverify` request. diff --git a/pos-module-captchas-recaptcha/app/lib/test/captchas_recaptcha/verify_test.liquid b/pos-module-captchas-recaptcha/app/lib/test/captchas_recaptcha/verify_test.liquid index 0308e9e..0424857 100644 --- a/pos-module-captchas-recaptcha/app/lib/test/captchas_recaptcha/verify_test.liquid +++ b/pos-module-captchas-recaptcha/app/lib/test/captchas_recaptcha/verify_test.liquid @@ -13,4 +13,21 @@ function r_fail = 'modules/captchas/commands/captcha/verify', provider: 'recaptcha', secret: 'not-a-real-secret', token: 'dummy-token-for-test-keys' function contract = 'modules/tests/assertions/not_valid_object', contract: contract, field_name: 'bad_secret_fails', object: r_fail function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'captcha', object: r_fail.errors + + # The message is stored translated, not as a raw key: this provider module sets the error via + # modules/captchas/helpers/add_error, which resolves a modules/captchas/* key from inside + # modules/captchas_recaptcha. Callers render errors verbatim, so a raw key would leak to users. + # Reuses r_fail — no extra billable call. + assign fail_key = 'modules/captchas/errors.verification_failed' + assign fail_message = r_fail.errors.captcha | first + assign expected_message = fail_key | t + function contract = 'modules/tests/assertions/equal', contract: contract, given: fail_message, expected: expected_message, field_name: 'bad_secret_message_translated' + + # The check above alone would pass if `| t` silently returned the key on both sides, so assert + # the stored value is not the key itself — that is the failure mode this guards against. + assign is_raw_key = false + if fail_message == fail_key + assign is_raw_key = true + endif + function contract = 'modules/tests/assertions/equal', contract: contract, given: is_raw_key, expected: false, field_name: 'bad_secret_message_not_raw_key' %} diff --git a/pos-module-captchas-recaptcha/app/views/pages/verify.liquid b/pos-module-captchas-recaptcha/app/views/pages/verify.liquid index 5bab788..bb29dff 100644 --- a/pos-module-captchas-recaptcha/app/views/pages/verify.liquid +++ b/pos-module-captchas-recaptcha/app/views/pages/verify.liquid @@ -21,10 +21,14 @@ method: post

Submitted message: {{ context.params.message }}

{% else %}

❌ Captcha failed ({{ result.provider }})

+ {% comment %} + result.errors holds already-translated messages (the commands translate at set time, via + modules/captchas/helpers/add_error), so they render as-is — no `| t` here. + {% endcomment %} diff --git a/pos-module-captchas-recaptcha/modules/captchas_recaptcha/public/lib/commands/verify.liquid b/pos-module-captchas-recaptcha/modules/captchas_recaptcha/public/lib/commands/verify.liquid index 633a459..6fe0f58 100644 --- a/pos-module-captchas-recaptcha/modules/captchas_recaptcha/public/lib/commands/verify.liquid +++ b/pos-module-captchas-recaptcha/modules/captchas_recaptcha/public/lib/commands/verify.liquid @@ -3,20 +3,19 @@ api_call_send and normalize the result. Part of the captchas provider contract — dispatched by modules/captchas/commands/captcha/verify/execute. @param {object} object - validated object: { secret, token, remote_ip, errors }. - @returns {object} object with success (boolean), response (hash), valid, and errors. + @returns {object} object with success (boolean), response (hash), valid, and errors + (translated messages, via modules/captchas/helpers/add_error). {% enddoc %} {% liquid - assign payload = '{}' | parse_json - assign payload = payload | hash_merge: secret: object.secret, token: object.token, remote_ip: object.remote_ip + assign payload = { "secret": object.secret, "token": object.token, "remote_ip": object.remote_ip } graphql result = 'modules/captchas_recaptcha/api_call/send', data: payload, name: 'modules/captchas_recaptcha/siteverify' assign call = result.api_call_send - function c = 'modules/captchas/helpers/errors_hash', errors: object.errors if call.errors != blank - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -24,8 +23,8 @@ assign status = call.response.status if status != 200 - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -37,8 +36,8 @@ assign raw_body = call.response.body | strip assign first_char = raw_body | slice: 0, 1 if first_char != '{' - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -51,8 +50,8 @@ if body.success assign object.valid = true else - assign messages = 'modules/captchas/errors.verification_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.verification_failed' + assign object.errors = updated_errors assign object.valid = false endif diff --git a/pos-module-captchas-recaptcha/pos-module.json b/pos-module-captchas-recaptcha/pos-module.json index 12e0ec8..2dc472c 100644 --- a/pos-module-captchas-recaptcha/pos-module.json +++ b/pos-module-captchas-recaptcha/pos-module.json @@ -1,9 +1,9 @@ { "name": "pOS Captchas reCAPTCHA v2", "machine_name": "captchas_recaptcha", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { - "captchas": "^1.0.0" + "captchas": "^1.1.0" }, "devDependencies": { "tests": "^1.3.4" diff --git a/pos-module-captchas-recaptcha3/CHANGELOG.md b/pos-module-captchas-recaptcha3/CHANGELOG.md index c87de6d..f645152 100644 --- a/pos-module-captchas-recaptcha3/CHANGELOG.md +++ b/pos-module-captchas-recaptcha3/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## 1.1.0 + +- Errors are now recorded with `modules/captchas/helpers/add_error` (in both `commands/verify` + and `helpers/score_check`), so `result.errors` holds translated messages instead of + `modules/captchas/errors.*` keys — drop any `| t` you apply to them. Requires + `captchas ^1.1.0`. +- Other code improvements: internal cleanups with no change to behavior or the public API. + ## 1.0.0 Initial release. Google reCAPTCHA v3 (invisible, score-based) provider for the diff --git a/pos-module-captchas-recaptcha3/README.md b/pos-module-captchas-recaptcha3/README.md index 89d80f3..236ce23 100644 --- a/pos-module-captchas-recaptcha3/README.md +++ b/pos-module-captchas-recaptcha3/README.md @@ -96,12 +96,15 @@ challenge fails client-side, the form submits token-less and verify fails closed and non-JSON bodies (`modules/captchas/errors.request_failed`), then applies the score/action gate (`modules/captchas_recaptcha3/helpers/score_check`): -| Outcome | Error key | +| Outcome | Message key | |---|---| | provider `success` false/missing | `modules/captchas/errors.verification_failed` | | action ≠ `expected_action` | `modules/captchas/errors.action_mismatch` | | score < `min_score` | `modules/captchas/errors.low_score` | +Errors are stored via `modules/captchas/helpers/add_error`, which translates the message as it +is set — `result.errors.captcha` holds display-ready text, not the keys above. + `expected_hostname` (handled by the abstraction) works with the `hostname` field the response includes. diff --git a/pos-module-captchas-recaptcha3/app/lib/test/captchas_recaptcha3/score_check_test.liquid b/pos-module-captchas-recaptcha3/app/lib/test/captchas_recaptcha3/score_check_test.liquid index d651074..ec3431f 100644 --- a/pos-module-captchas-recaptcha3/app/lib/test/captchas_recaptcha3/score_check_test.liquid +++ b/pos-module-captchas-recaptcha3/app/lib/test/captchas_recaptcha3/score_check_test.liquid @@ -17,6 +17,22 @@ function contract = 'modules/tests/assertions/equal', contract: contract, given: r2.valid, expected: false, field_name: 'low_score_fails' function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'captcha', object: r2.errors + # The message is stored translated, not as a raw key: score_check sets the error via + # modules/captchas/helpers/add_error, which resolves a modules/captchas/* key from inside + # modules/captchas_recaptcha3. Callers render errors verbatim, so a raw key would leak to users. + assign low_score_key = 'modules/captchas/errors.low_score' + assign low_score_message = r2.errors.captcha | first + assign expected_message = low_score_key | t + function contract = 'modules/tests/assertions/equal', contract: contract, given: low_score_message, expected: expected_message, field_name: 'low_score_message_translated' + + # The check above alone would pass if `| t` silently returned the key on both sides, so assert + # the stored value is not the key itself — that is the failure mode this guards against. + assign is_raw_key = false + if low_score_message == low_score_key + assign is_raw_key = true + endif + function contract = 'modules/tests/assertions/equal', contract: contract, given: is_raw_key, expected: false, field_name: 'low_score_message_not_raw_key' + # a custom min_score lowers the bar function r3 = 'modules/captchas_recaptcha3/helpers/score_check', body: body_low, min_score: 0.2, errors: no_errors function contract = 'modules/tests/assertions/equal', contract: contract, given: r3.valid, expected: true, field_name: 'custom_min_score' diff --git a/pos-module-captchas-recaptcha3/app/views/pages/verify.liquid b/pos-module-captchas-recaptcha3/app/views/pages/verify.liquid index 3af583c..a65d5b3 100644 --- a/pos-module-captchas-recaptcha3/app/views/pages/verify.liquid +++ b/pos-module-captchas-recaptcha3/app/views/pages/verify.liquid @@ -23,10 +23,14 @@ method: post

Submitted message: {{ context.params.message }}

{% else %}

❌ Captcha failed ({{ result.provider }})

+ {% comment %} + result.errors holds already-translated messages (the commands translate at set time, via + modules/captchas/helpers/add_error), so they render as-is — no `| t` here. + {% endcomment %} diff --git a/pos-module-captchas-recaptcha3/modules/captchas_recaptcha3/public/lib/commands/verify.liquid b/pos-module-captchas-recaptcha3/modules/captchas_recaptcha3/public/lib/commands/verify.liquid index 84e5343..07aa86f 100644 --- a/pos-module-captchas-recaptcha3/modules/captchas_recaptcha3/public/lib/commands/verify.liquid +++ b/pos-module-captchas-recaptcha3/modules/captchas_recaptcha3/public/lib/commands/verify.liquid @@ -6,20 +6,19 @@ modules/captchas/commands/captcha/verify/execute. @param {object} object - validated object: { secret, token, remote_ip, errors, min_score, expected_action }. min_score defaults to 0.5; expected_action is optional. - @returns {object} object with success (boolean), score (number), action (string), response (hash), valid, and errors. + @returns {object} object with success (boolean), score (number), action (string), response (hash), + valid, and errors (translated messages, via modules/captchas/helpers/add_error). {% enddoc %} {% liquid - assign payload = '{}' | parse_json - assign payload = payload | hash_merge: secret: object.secret, token: object.token, remote_ip: object.remote_ip + assign payload = { "secret": object.secret, "token": object.token, "remote_ip": object.remote_ip } graphql result = 'modules/captchas_recaptcha3/api_call/send', data: payload, name: 'modules/captchas_recaptcha3/siteverify' assign call = result.api_call_send - function c = 'modules/captchas/helpers/errors_hash', errors: object.errors if call.errors != blank - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -27,8 +26,8 @@ assign status = call.response.status if status != 200 - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -40,8 +39,8 @@ assign raw_body = call.response.body | strip assign first_char = raw_body | slice: 0, 1 if first_char != '{' - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object diff --git a/pos-module-captchas-recaptcha3/modules/captchas_recaptcha3/public/lib/helpers/score_check.liquid b/pos-module-captchas-recaptcha3/modules/captchas_recaptcha3/public/lib/helpers/score_check.liquid index 0fd0907..91b4a3a 100644 --- a/pos-module-captchas-recaptcha3/modules/captchas_recaptcha3/public/lib/helpers/score_check.liquid +++ b/pos-module-captchas-recaptcha3/modules/captchas_recaptcha3/public/lib/helpers/score_check.liquid @@ -6,27 +6,23 @@ @param {number} [min_score] - minimum score required to pass (default 0.5) @param {string} [expected_action] - when present, body.action must equal it @param {object} [errors] - existing errors hash to merge into - @returns {object} { valid (boolean), errors (hash) } + @returns {object} { valid (boolean), errors (hash of translated messages, via + modules/captchas/helpers/add_error) } {% enddoc %} {% liquid - function c = 'modules/captchas/helpers/errors_hash', errors: errors - assign result = '{}' | parse_json - # `!= true` (not `== false`): a nil/missing success field must fail closed too — otherwise a # 200 JSON body without `success` would fall through to the score check and, with min_score 0, # pass without any provider verdict. if body.success != true - assign messages = 'modules/captchas/errors.verification_failed' | split: '|' - assign merged = c | hash_merge: captcha: messages - assign result = result | hash_merge: valid: false, errors: merged + function merged = 'modules/captchas/helpers/add_error', errors: errors, field_name: 'captcha', key: 'modules/captchas/errors.verification_failed' + assign result = { "valid": false, "errors": merged } return result endif if expected_action != blank if body.action != expected_action - assign messages = 'modules/captchas/errors.action_mismatch' | split: '|' - assign merged = c | hash_merge: captcha: messages - assign result = result | hash_merge: valid: false, errors: merged + function merged = 'modules/captchas/helpers/add_error', errors: errors, field_name: 'captcha', key: 'modules/captchas/errors.action_mismatch' + assign result = { "valid": false, "errors": merged } return result endif endif @@ -35,11 +31,12 @@ assign score = body.score | default: 0.0 | plus: 0.0 if score >= threshold - assign result = result | hash_merge: valid: true, errors: c + # The pass path still normalizes: errors may arrive nil/empty and callers merge into it. + function c = 'modules/captchas/helpers/errors_hash', errors: errors + assign result = { "valid": true, "errors": c } else - assign messages = 'modules/captchas/errors.low_score' | split: '|' - assign merged = c | hash_merge: captcha: messages - assign result = result | hash_merge: valid: false, errors: merged + function merged = 'modules/captchas/helpers/add_error', errors: errors, field_name: 'captcha', key: 'modules/captchas/errors.low_score' + assign result = { "valid": false, "errors": merged } endif return result diff --git a/pos-module-captchas-recaptcha3/pos-module.json b/pos-module-captchas-recaptcha3/pos-module.json index bde02b7..589c5da 100644 --- a/pos-module-captchas-recaptcha3/pos-module.json +++ b/pos-module-captchas-recaptcha3/pos-module.json @@ -1,9 +1,9 @@ { "name": "pOS Captchas reCAPTCHA v3", "machine_name": "captchas_recaptcha3", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { - "captchas": "^1.0.0" + "captchas": "^1.1.0" }, "devDependencies": { "tests": "^1.3.4" diff --git a/pos-module-captchas-turnstile/CHANGELOG.md b/pos-module-captchas-turnstile/CHANGELOG.md index b1a9090..72199b3 100644 --- a/pos-module-captchas-turnstile/CHANGELOG.md +++ b/pos-module-captchas-turnstile/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 1.1.0 + +- Errors are now recorded with `modules/captchas/helpers/add_error`, so `result.errors` holds + translated messages instead of `modules/captchas/errors.*` keys — drop any `| t` you apply + to them. Requires `captchas ^1.1.0`. +- Other code improvements: internal cleanups with no change to behavior or the public API. + ## 1.0.0 Initial release. Cloudflare Turnstile provider for the diff --git a/pos-module-captchas-turnstile/README.md b/pos-module-captchas-turnstile/README.md index de13074..2121e52 100644 --- a/pos-module-captchas-turnstile/README.md +++ b/pos-module-captchas-turnstile/README.md @@ -94,6 +94,9 @@ answer maps to `modules/captchas/errors.verification_failed`. Turnstile secrets per-widget, so `expected_sitekey` is not needed; `expected_hostname` (handled by the abstraction) works with the `hostname` field Turnstile returns. +Errors are stored via `modules/captchas/helpers/add_error`, which translates the message as it +is set — `result.errors.captcha` holds display-ready text, not translation keys. + > ⚠️ **Billing** — the server-side verify call is a normal, billable platformOS API call; > each `verify` performs one outbound `siteverify` request. diff --git a/pos-module-captchas-turnstile/app/lib/test/captchas_turnstile/verify_test.liquid b/pos-module-captchas-turnstile/app/lib/test/captchas_turnstile/verify_test.liquid index 1ae616d..cce7614 100644 --- a/pos-module-captchas-turnstile/app/lib/test/captchas_turnstile/verify_test.liquid +++ b/pos-module-captchas-turnstile/app/lib/test/captchas_turnstile/verify_test.liquid @@ -13,4 +13,21 @@ function r_fail = 'modules/captchas/commands/captcha/verify', provider: 'turnstile', secret: '2x0000000000000000000000000000000AA', token: 'XXXX.DUMMY.TOKEN.XXXX' function contract = 'modules/tests/assertions/not_valid_object', contract: contract, field_name: 'testkey_fail', object: r_fail function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'captcha', object: r_fail.errors + + # The message is stored translated, not as a raw key: this provider module sets the error via + # modules/captchas/helpers/add_error, which resolves a modules/captchas/* key from inside + # modules/captchas_turnstile. Callers render errors verbatim, so a raw key would leak to users. + # Reuses r_fail — no extra billable call. + assign fail_key = 'modules/captchas/errors.verification_failed' + assign fail_message = r_fail.errors.captcha | first + assign expected_message = fail_key | t + function contract = 'modules/tests/assertions/equal', contract: contract, given: fail_message, expected: expected_message, field_name: 'testkey_fail_message_translated' + + # The check above alone would pass if `| t` silently returned the key on both sides, so assert + # the stored value is not the key itself — that is the failure mode this guards against. + assign is_raw_key = false + if fail_message == fail_key + assign is_raw_key = true + endif + function contract = 'modules/tests/assertions/equal', contract: contract, given: is_raw_key, expected: false, field_name: 'testkey_fail_message_not_raw_key' %} diff --git a/pos-module-captchas-turnstile/app/views/pages/verify.liquid b/pos-module-captchas-turnstile/app/views/pages/verify.liquid index 47a1436..1449cf8 100644 --- a/pos-module-captchas-turnstile/app/views/pages/verify.liquid +++ b/pos-module-captchas-turnstile/app/views/pages/verify.liquid @@ -22,10 +22,14 @@ method: post

Submitted message: {{ context.params.message }}

{% else %}

❌ Captcha failed ({{ result.provider }})

+ {% comment %} + result.errors holds already-translated messages (the commands translate at set time, via + modules/captchas/helpers/add_error), so they render as-is — no `| t` here. + {% endcomment %} diff --git a/pos-module-captchas-turnstile/modules/captchas_turnstile/public/lib/commands/verify.liquid b/pos-module-captchas-turnstile/modules/captchas_turnstile/public/lib/commands/verify.liquid index 384608e..caf5c94 100644 --- a/pos-module-captchas-turnstile/modules/captchas_turnstile/public/lib/commands/verify.liquid +++ b/pos-module-captchas-turnstile/modules/captchas_turnstile/public/lib/commands/verify.liquid @@ -3,20 +3,19 @@ api_call_send and normalize the result. Part of the captchas provider contract — dispatched by modules/captchas/commands/captcha/verify/execute. @param {object} object - validated object: { secret, token, remote_ip, errors }. - @returns {object} object with success (boolean), response (hash), valid, and errors. + @returns {object} object with success (boolean), response (hash), valid, and errors + (translated messages, via modules/captchas/helpers/add_error). {% enddoc %} {% liquid - assign payload = '{}' | parse_json - assign payload = payload | hash_merge: secret: object.secret, token: object.token, remote_ip: object.remote_ip + assign payload = { "secret": object.secret, "token": object.token, "remote_ip": object.remote_ip } graphql result = 'modules/captchas_turnstile/api_call/send', data: payload, name: 'modules/captchas_turnstile/siteverify' assign call = result.api_call_send - function c = 'modules/captchas/helpers/errors_hash', errors: object.errors if call.errors != blank - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -24,8 +23,8 @@ assign status = call.response.status if status != 200 - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -37,8 +36,8 @@ assign raw_body = call.response.body | strip assign first_char = raw_body | slice: 0, 1 if first_char != '{' - assign messages = 'modules/captchas/errors.request_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors assign object.success = false assign object.valid = false return object @@ -51,8 +50,8 @@ if body.success assign object.valid = true else - assign messages = 'modules/captchas/errors.verification_failed' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.verification_failed' + assign object.errors = updated_errors assign object.valid = false endif diff --git a/pos-module-captchas-turnstile/pos-module.json b/pos-module-captchas-turnstile/pos-module.json index 4d7056f..7eebac4 100644 --- a/pos-module-captchas-turnstile/pos-module.json +++ b/pos-module-captchas-turnstile/pos-module.json @@ -1,9 +1,9 @@ { "name": "pOS Captchas Turnstile", "machine_name": "captchas_turnstile", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { - "captchas": "^1.0.0" + "captchas": "^1.1.0" }, "devDependencies": { "tests": "^1.3.4" diff --git a/pos-module-captchas/CHANGELOG.md b/pos-module-captchas/CHANGELOG.md index f8d5fb9..b3a4745 100644 --- a/pos-module-captchas/CHANGELOG.md +++ b/pos-module-captchas/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog +## 1.1.0 + +- `object.errors` now holds **translated messages** instead of `modules/captchas/errors.*` + translation keys, matching `modules/core/helpers/register_error` and the rest of the + platformOS modules. Callers can pass `result.errors[field]` straight to + `modules/common-styling/forms/error_list` (which renders each entry raw); previously that + printed a raw key to end users. +- New `modules/captchas/helpers/add_error` — translates a `modules/captchas/errors.*` key and + **appends** it to a field, so multiple errors can accumulate on one field (the previous + `hash_merge` replaced the field's array). This is now the documented way for provider + modules to record errors; see the provider contract in the README. +- **Migration:** drop any `| t` you apply to values from `result.errors` — the strings are + already translated. Provider modules must depend on `captchas ^1.1.0`. +- Other code improvements: internal cleanups with no change to behavior or the public API. + ## 1.0.0 Initial release. diff --git a/pos-module-captchas/README.md b/pos-module-captchas/README.md index 6f26bdd..b999900 100644 --- a/pos-module-captchas/README.md +++ b/pos-module-captchas/README.md @@ -105,11 +105,19 @@ provider or key (see [Keys are caller-supplied](#keys-are-caller-supplied)): %} ``` -Render errors with `| t` — `result.errors` maps fields to arrays of translation keys: +`result.errors` maps fields to arrays of **already-translated messages** (the commands +translate at set time, like `modules/core/helpers/register_error`), so they render as-is — no +`| t` at the call site, and they can go straight into the shared form-error partial: + +```liquid +{% render 'modules/common-styling/forms/error_list', name: 'captcha', errors: result.errors.captcha %} +``` + +Or render them all: ```liquid {% for error in result.errors %} - {% for key in error[1] %}

{{ key | t }}

{% endfor %} + {% for message in error[1] %}

{{ message }}

{% endfor %} {% endfor %} ``` @@ -196,11 +204,14 @@ Returns: | `token_field` | The form field the token was read from. | | `response` | Parsed provider response (hash). | | `score` / `action` | Score-based providers only. | -| `errors` | Hash of field → array of translation keys (`modules/captchas/errors.*`). | +| `errors` | Hash of field (`token`, `secret`, `provider`, `captcha`) → array of translated messages, ready to display. | -Error keys — all shipped by this module (en + pl), so callers deal with one namespace -regardless of provider: `token_missing`, `secret_missing`, `unsupported_provider`, -`request_failed`, `verification_failed`, `hostname_mismatch`, `low_score`, `action_mismatch`. +Messages are translated when the error is set, so no `| t` is needed at the call site — see +[Usage](#usage). The underlying keys (`modules/captchas/errors.*`) are all +shipped by this module (en + pl), so the copy is one namespace regardless of provider: +`token_missing`, `secret_missing`, `unsupported_provider`, `request_failed`, +`verification_failed`, `hostname_mismatch`, `low_score`, `action_mismatch`. Override any of +them the usual way — copy `modules/captchas/public/translations/.yml` into your app. ## How verification works @@ -226,7 +237,7 @@ exempt** from API usage billing. Each `verify` performs one outbound `siteverify ## Writing a provider module A provider is a separate platformOS module named `captchas_` (``: lowercase -`a-z0-9_`), with `"dependencies": { "captchas": "^1.0.0" }`. The abstraction dispatches to +`a-z0-9_`), with `"dependencies": { "captchas": "^1.1.0" }`. The abstraction dispatches to it purely by naming convention — no registration anywhere. It must expose: 1. **`modules/captchas_/widget`** (`public/views/partials/widget.liquid`) — receives @@ -244,9 +255,18 @@ it purely by naming convention — no registration anywhere. It must expose: - `success` — the provider's raw verdict, - `valid` — the final boolean; **fail closed** on transport errors, non-200 responses, non-JSON bodies, and a missing success field, - - `errors` — merged via `modules/captchas/helpers/errors_hash`, using - `modules/captchas/errors.*` keys (`request_failed`, `verification_failed`, and for - score-based providers `low_score` / `action_mismatch`). + - `errors` — set via `modules/captchas/helpers/add_error`, which translates the message and + appends it to the field. Pass `errors: object.errors`, `field_name: 'captcha'`, and a + `modules/captchas/errors.*` key (`request_failed`, `verification_failed`, and for + score-based providers `low_score` / `action_mismatch`): + + ```liquid + function updated_errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.request_failed' + assign object.errors = updated_errors + ``` + + Do not store the raw key: callers display `object.errors` directly. Add your own keys in + your module's translations if you need provider-specific copy. The incoming `object` carries `secret`, `token`, `remote_ip`, `expected_sitekey`, `min_score`, `expected_action`, and the current `errors` — read what applies. diff --git a/pos-module-captchas/app/lib/test/captcha/add_error_test.liquid b/pos-module-captchas/app/lib/test/captcha/add_error_test.liquid new file mode 100644 index 0000000..2902edc --- /dev/null +++ b/pos-module-captchas/app/lib/test/captcha/add_error_test.liquid @@ -0,0 +1,36 @@ +{% comment %} + Unit test (no network): the add_error helper. The contract it locks in is that errors hold + *translated messages*, not translation keys — the same convention as + modules/core/helpers/register_error. That is what lets a caller pass object.errors[field] + straight to modules/common-styling/forms/error_list, which renders each entry raw: storing + keys here would print "modules/captchas/errors.token_missing" to end users. + Also covers appending, since several errors can land on one field. +{% endcomment %} +{% liquid + assign token_key = 'modules/captchas/errors.token_missing' + + # nil errors input -> a real hash with the field populated + function e1 = 'modules/captchas/helpers/add_error', field_name: 'token', key: token_key + function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'token', object: e1 + + # the stored value is the translated message + assign stored = e1.token | first + assign translated = token_key | t + function contract = 'modules/tests/assertions/equal', contract: contract, given: stored, expected: translated, field_name: 'stores_translated_message' + + # ...and specifically not the raw key + assign is_raw_key = false + if stored == token_key + assign is_raw_key = true + endif + function contract = 'modules/tests/assertions/equal', contract: contract, given: is_raw_key, expected: false, field_name: 'does_not_store_raw_key' + + # appends rather than replaces: two errors on the same field both survive + function e2 = 'modules/captchas/helpers/add_error', errors: e1, field_name: 'token', key: 'modules/captchas/errors.verification_failed' + function contract = 'modules/tests/assertions/equal', contract: contract, given: e2.token.size, expected: 2, field_name: 'appends_to_same_field' + + # an existing field survives a second field being added + function e3 = 'modules/captchas/helpers/add_error', errors: e2, field_name: 'secret', key: 'modules/captchas/errors.secret_missing' + function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'token', object: e3 + function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'secret', object: e3 +%} diff --git a/pos-module-captchas/app/lib/test/captcha/errors_hash_test.liquid b/pos-module-captchas/app/lib/test/captcha/errors_hash_test.liquid index 34f4451..465da01 100644 --- a/pos-module-captchas/app/lib/test/captcha/errors_hash_test.liquid +++ b/pos-module-captchas/app/lib/test/captcha/errors_hash_test.liquid @@ -17,7 +17,7 @@ function contract = 'modules/tests/assertions/equal', contract: contract, given: merged_empty.secret, expected: 'y', field_name: 'empty_hash_mergeable' # a populated hash passes through: existing keys survive a subsequent merge - assign existing = '{"token":["modules/captchas/errors.token_missing"]}' | parse_json + assign existing = '{"token":["The captcha challenge was not completed."]}' | parse_json function preserved = 'modules/captchas/helpers/errors_hash', errors: existing function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'token', object: preserved assign merged_more = preserved | hash_merge: secret: 'z' diff --git a/pos-module-captchas/app/lib/test/captcha/translations_test.liquid b/pos-module-captchas/app/lib/test/captcha/translations_test.liquid index b168d7d..4dcfc0f 100644 --- a/pos-module-captchas/app/lib/test/captcha/translations_test.liquid +++ b/pos-module-captchas/app/lib/test/captcha/translations_test.liquid @@ -1,13 +1,13 @@ {% comment %} - Unit test (no network): every error message key the captcha commands store in - object.errors must resolve to a real translation. Module translation keys are + Unit test (no network): every error message key the captcha commands hand to + helpers/add_error must resolve to a real translation. Module translation keys are auto-prefixed (modules/captchas/) and a missing key fails only at runtime — the - t filter silently returns the key itself — so a typo here would show a raw - 'modules/captchas/errors.*' string to end users at display time. + t filter silently returns the key itself — so a typo here would put a raw + 'modules/captchas/errors.*' string into object.errors, which callers display verbatim. Keep this list in sync with the keys used in commands/captcha and translations/en.yml. {% endcomment %} {% liquid - assign keys = 'token_missing,secret_missing,unsupported_provider,verification_failed,request_failed,low_score,action_mismatch,hostname_mismatch' | split: ',' + assign keys = ["token_missing", "secret_missing", "unsupported_provider", "verification_failed", "request_failed", "low_score", "action_mismatch", "hostname_mismatch"] for key in keys assign full_key = 'modules/captchas/errors.' | append: key diff --git a/pos-module-captchas/app/lib/test/captcha/verify_build_test.liquid b/pos-module-captchas/app/lib/test/captcha/verify_build_test.liquid index cf48d25..9210f17 100644 --- a/pos-module-captchas/app/lib/test/captcha/verify_build_test.liquid +++ b/pos-module-captchas/app/lib/test/captcha/verify_build_test.liquid @@ -16,29 +16,29 @@ function contract = 'modules/tests/assertions/equal', contract: contract, given: b1.secret, expected: '', field_name: 'no_secret_is_blank' # an explicit secret is preserved through build - assign o1b = '{}' | parse_json | hash_merge: secret: 'sk_explicit_test' + assign o1b = { "secret": "sk_explicit_test" } function b1b = 'modules/captchas/commands/captcha/verify/build', object: o1b function contract = 'modules/tests/assertions/equal', contract: contract, given: b1b.secret, expected: 'sk_explicit_test', field_name: 'explicit_secret_preserved' # an explicit provider is preserved, and the default token field comes from the provider # module's config helper (dynamic dispatch) - assign o2 = '{}' | parse_json | hash_merge: provider: 'test' + assign o2 = { "provider": "test" } function b2 = 'modules/captchas/commands/captcha/verify/build', object: o2 function contract = 'modules/tests/assertions/equal', contract: contract, given: b2.provider, expected: 'test', field_name: 'explicit_provider' function contract = 'modules/tests/assertions/equal', contract: contract, given: b2.token_field, expected: 'x-test-captcha-response', field_name: 'token_field_from_provider_config' # an unavailable provider must not be dispatched to for config: token_field stays blank - assign o3 = '{}' | parse_json | hash_merge: provider: 'bogus' + assign o3 = { "provider": "bogus" } function b3 = 'modules/captchas/commands/captcha/verify/build', object: o3 function contract = 'modules/tests/assertions/blank', contract: contract, field_name: 'token_field', object: b3 # an explicit response_field_name overrides the provider default - assign o4 = '{}' | parse_json | hash_merge: provider: 'test', response_field_name: 'my-custom-field' + assign o4 = { "provider": "test", "response_field_name": "my-custom-field" } function b4 = 'modules/captchas/commands/captcha/verify/build', object: o4 function contract = 'modules/tests/assertions/equal', contract: contract, given: b4.token_field, expected: 'my-custom-field', field_name: 'explicit_response_field_name' # an explicit token is honoured (used instead of reading context.params) - assign o5 = '{}' | parse_json | hash_merge: provider: 'test', token: 'explicit-token-123' + assign o5 = { "provider": "test", "token": "explicit-token-123" } function b5 = 'modules/captchas/commands/captcha/verify/build', object: o5 function contract = 'modules/tests/assertions/equal', contract: contract, given: b5.token, expected: 'explicit-token-123', field_name: 'explicit_token' %} diff --git a/pos-module-captchas/app/lib/test/captcha/verify_check_test.liquid b/pos-module-captchas/app/lib/test/captcha/verify_check_test.liquid index 061fad3..7a71e5f 100644 --- a/pos-module-captchas/app/lib/test/captcha/verify_check_test.liquid +++ b/pos-module-captchas/app/lib/test/captcha/verify_check_test.liquid @@ -5,24 +5,24 @@ {% endcomment %} {% liquid # happy path: token + secret + available provider -> valid - assign ok = '{}' | parse_json | hash_merge: provider: 'test', secret: 'sk_test', token: 'tok_test' + assign ok = { "provider": "test", "secret": "sk_test", "token": "tok_test" } function ok = 'modules/captchas/commands/captcha/verify/check', object: ok function contract = 'modules/tests/assertions/valid_object', contract: contract, object: ok, field_name: 'all_present' # missing token -> invalid, with a token error - assign no_token = '{}' | parse_json | hash_merge: provider: 'test', secret: 'sk_test' + assign no_token = { "provider": "test", "secret": "sk_test" } function no_token = 'modules/captchas/commands/captcha/verify/check', object: no_token function contract = 'modules/tests/assertions/not_valid_object', contract: contract, field_name: 'missing_token', object: no_token function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'token', object: no_token.errors # missing secret -> invalid, with a secret error - assign no_secret = '{}' | parse_json | hash_merge: provider: 'test', token: 'tok_test' + assign no_secret = { "provider": "test", "token": "tok_test" } function no_secret = 'modules/captchas/commands/captcha/verify/check', object: no_secret function contract = 'modules/tests/assertions/not_valid_object', contract: contract, field_name: 'missing_secret', object: no_secret function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'secret', object: no_secret.errors # unavailable provider (no such module installed) -> invalid, with a provider error - assign bad = '{}' | parse_json | hash_merge: provider: 'bogus', secret: 'sk_test', token: 'tok_test' + assign bad = { "provider": "bogus", "secret": "sk_test", "token": "tok_test" } function bad = 'modules/captchas/commands/captcha/verify/check', object: bad function contract = 'modules/tests/assertions/not_valid_object', contract: contract, field_name: 'unsupported_provider', object: bad function contract = 'modules/tests/assertions/presence', contract: contract, field_name: 'provider', object: bad.errors diff --git a/pos-module-captchas/app/lib/test/captcha/verify_execute_fail_closed_test.liquid b/pos-module-captchas/app/lib/test/captcha/verify_execute_fail_closed_test.liquid index 7831017..514bd27 100644 --- a/pos-module-captchas/app/lib/test/captcha/verify_execute_fail_closed_test.liquid +++ b/pos-module-captchas/app/lib/test/captcha/verify_execute_fail_closed_test.liquid @@ -8,7 +8,7 @@ {% endcomment %} {% liquid # simulate an object that slipped past check: unknown provider, yet valid: true - assign slipped = '{}' | parse_json | hash_merge: provider: 'bogus', secret: 'sk_test', token: 'tok_test', valid: true + assign slipped = { "provider": "bogus", "secret": "sk_test", "token": "tok_test", "valid": true } function result = 'modules/captchas/commands/captcha/verify/execute', object: slipped function contract = 'modules/tests/assertions/not_valid_object', contract: contract, field_name: 'execute_fails_closed', object: result diff --git a/pos-module-captchas/app/modules/captchas_test/public/lib/commands/verify.liquid b/pos-module-captchas/app/modules/captchas_test/public/lib/commands/verify.liquid index 1d18af6..46eb4fd 100644 --- a/pos-module-captchas/app/modules/captchas_test/public/lib/commands/verify.liquid +++ b/pos-module-captchas/app/modules/captchas_test/public/lib/commands/verify.liquid @@ -7,8 +7,6 @@ @returns {object} object with response, success, valid and errors set {% enddoc %} {% liquid - function c = 'modules/captchas/helpers/errors_hash', errors: object.errors - if object.token == 'pass' assign response = '{"success": true, "hostname": "test.example.com"}' | parse_json assign object = object | hash_merge: response: response, success: true, valid: true @@ -16,8 +14,7 @@ endif assign response = '{"success": false}' | parse_json - assign messages = 'modules/captchas/errors.verification_failed' | split: '|' - assign errors = c | hash_merge: captcha: messages + function errors = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.verification_failed' assign object = object | hash_merge: response: response, success: false, valid: false, errors: errors return object %} diff --git a/pos-module-captchas/app/views/pages/captcha-verify.liquid b/pos-module-captchas/app/views/pages/captcha-verify.liquid index 28c73ea..7d4afaf 100644 --- a/pos-module-captchas/app/views/pages/captcha-verify.liquid +++ b/pos-module-captchas/app/views/pages/captcha-verify.liquid @@ -27,10 +27,15 @@ method: post

Submitted message: {{ context.params.message }}

{% else %}

❌ Captcha failed ({{ result.provider }})

+ {% comment %} + result.errors holds already-translated messages (the commands translate at set time, like + modules/core/helpers/register_error), so they render as-is — no `| t` here. This is also why + a caller can pass result.errors[field] straight to modules/common-styling/forms/error_list. + {% endcomment %}
    {% for error in result.errors %} - {% for key in error[1] %} -
  • {{ key | t }}
  • + {% for message in error[1] %} +
  • {{ message }}
  • {% endfor %} {% endfor %}
diff --git a/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify.liquid b/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify.liquid index b7e3edc..7f2806b 100644 --- a/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify.liquid +++ b/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify.liquid @@ -23,8 +23,7 @@ @returns {object} { valid, success, provider, response, errors } (plus score/action for score-based providers). {% enddoc %} {% liquid - assign object = '{}' | parse_json - assign object = object | hash_merge: provider: provider, secret: secret, token: token, response_field_name: response_field_name, remote_ip: remote_ip, expected_sitekey: expected_sitekey, expected_hostname: expected_hostname, min_score: min_score, expected_action: expected_action + assign object = { "provider": provider, "secret": secret, "token": token, "response_field_name": response_field_name, "remote_ip": remote_ip, "expected_sitekey": expected_sitekey, "expected_hostname": expected_hostname, "min_score": min_score, "expected_action": expected_action } function object = 'modules/captchas/commands/captcha/verify/build', object: object function object = 'modules/captchas/commands/captcha/verify/check', object: object diff --git a/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify/check.liquid b/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify/check.liquid index 5b09160..1e9f0ec 100644 --- a/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify/check.liquid +++ b/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify/check.liquid @@ -2,25 +2,23 @@ @description Check phase for captcha verify: require a token + secret and an available provider (installed provider module, permitted by the optional allow-list). @param {object} object - normalized object from the build phase. - @returns {object} object with errors populated and valid set. + @returns {object} object with errors populated (translated messages, via helpers/add_error) + and valid set. {% enddoc %} {% liquid function c = 'modules/captchas/helpers/errors_hash', errors: object.errors function provider_supported = 'modules/captchas/helpers/provider_available', provider: object.provider if object.token == blank - assign messages = 'modules/captchas/errors.token_missing' | split: '|' - assign c = c | hash_merge: token: messages + function c = 'modules/captchas/helpers/add_error', errors: c, field_name: 'token', key: 'modules/captchas/errors.token_missing' endif if object.secret == blank - assign messages = 'modules/captchas/errors.secret_missing' | split: '|' - assign c = c | hash_merge: secret: messages + function c = 'modules/captchas/helpers/add_error', errors: c, field_name: 'secret', key: 'modules/captchas/errors.secret_missing' endif unless provider_supported - assign messages = 'modules/captchas/errors.unsupported_provider' | split: '|' - assign c = c | hash_merge: provider: messages + function c = 'modules/captchas/helpers/add_error', errors: c, field_name: 'provider', key: 'modules/captchas/errors.unsupported_provider' endunless assign object.errors = c diff --git a/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify/execute.liquid b/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify/execute.liquid index dc2da84..e1e1373 100644 --- a/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify/execute.liquid +++ b/pos-module-captchas/modules/captchas/public/lib/commands/captcha/verify/execute.liquid @@ -5,7 +5,7 @@ @param {object} object - validated object: { provider, secret, token, remote_ip, expected_sitekey, expected_hostname, min_score, expected_action, errors }. @returns {object} object with success (boolean), response (hash), valid, and errors - (plus score/action for score-based providers). + (translated messages; plus score/action for score-based providers). {% enddoc %} {% liquid # Re-check availability even though the check phase already did (defense in depth: this @@ -14,9 +14,8 @@ # siteverify endpoint — never leave object.valid true here. function provider_is_available = 'modules/captchas/helpers/provider_available', provider: object.provider unless provider_is_available - function c = 'modules/captchas/helpers/errors_hash', errors: object.errors - assign messages = 'modules/captchas/errors.unsupported_provider' | split: '|' - assign object.errors = c | hash_merge: provider: messages + function c = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'provider', key: 'modules/captchas/errors.unsupported_provider' + assign object.errors = c assign object.success = false assign object.valid = false return object @@ -28,9 +27,8 @@ if object.valid function hostname_allowed = 'modules/captchas/helpers/hostname_allowed', hostname: object.response.hostname, expected_hostname: object.expected_hostname unless hostname_allowed - function c = 'modules/captchas/helpers/errors_hash', errors: object.errors - assign messages = 'modules/captchas/errors.hostname_mismatch' | split: '|' - assign object.errors = c | hash_merge: captcha: messages + function c = 'modules/captchas/helpers/add_error', errors: object.errors, field_name: 'captcha', key: 'modules/captchas/errors.hostname_mismatch' + assign object.errors = c assign object.valid = false endunless endif diff --git a/pos-module-captchas/modules/captchas/public/lib/helpers/add_error.liquid b/pos-module-captchas/modules/captchas/public/lib/helpers/add_error.liquid new file mode 100644 index 0000000..5d877a2 --- /dev/null +++ b/pos-module-captchas/modules/captchas/public/lib/helpers/add_error.liquid @@ -0,0 +1,24 @@ +{% doc %} + @description Append a translated error message for a field to an errors hash. Mirrors + modules/core/helpers/register_error: the message is translated at set time, so object.errors + holds ready-to-display strings — callers can pass them straight to + modules/common-styling/forms/error_list, which renders each entry raw. Appends rather than + replaces, so several errors can accumulate on the same field. + @param {object} [errors] - existing errors container; may be nil, empty, or a populated hash. + @param {string} field_name - the field the error belongs to (token, secret, provider, captcha). + @param {string} key - translation key, e.g. 'modules/captchas/errors.token_missing'. Module + translation keys are auto-prefixed, so the modules/captchas/ prefix is required — including + from provider modules, which store this module's error messages. + @returns {object} the errors hash with the translated message appended under field_name. +{% enddoc %} +{% liquid + function result = 'modules/captchas/helpers/errors_hash', errors: errors + assign message = key | t + + assign empty_list = [] + assign field_errors = result[field_name] | default: empty_list + assign field_errors << message + assign result[field_name] = field_errors + + return result +%} diff --git a/pos-module-captchas/pos-module.json b/pos-module-captchas/pos-module.json index df6ed71..3ecb527 100644 --- a/pos-module-captchas/pos-module.json +++ b/pos-module-captchas/pos-module.json @@ -1,7 +1,7 @@ { "name": "Pos Module Captchas", "machine_name": "captchas", - "version": "1.0.0", + "version": "1.1.0", "dependencies": {}, "devDependencies": { "tests": "^1.3.4"