diff --git a/craft/src/components/forms/ContactForm.toHtml.test.ts b/craft/src/components/forms/ContactForm.toHtml.test.ts index bdb091c..210dbcb 100644 --- a/craft/src/components/forms/ContactForm.toHtml.test.ts +++ b/craft/src/components/forms/ContactForm.toHtml.test.ts @@ -6,7 +6,7 @@ const toHtml = (ContactForm as any).toHtml; describe('ContactForm.toHtml relay wiring', () => { test('with recipientEmail: emits marker, placeholder action, honeypot', () => { const { html } = toHtml({ recipientEmail: 'a@b.com', thankYouUrl: '/thx', fields: [] }, ''); - expect(html).toMatch(//); + expect(html).toMatch(//); expect(html).toMatch(/action="__WHP_FORM_ACTION__F_[0-9a-z]+__"/); expect(html).toContain('method="POST"'); expect(html).toContain('name="_gotcha"'); @@ -244,7 +244,9 @@ describe('ContactForm.toHtml destination marker', () => { // if it has to be edited, every already-published site's forms have changed // shape and that is the thing to stop, not the test. const FROZEN_LEGACY_MARKER = - ''; + ''; + // (captcha="1" was added when captcha became mandatory on every relay form: that is the one deliberate + // shape change since 071f3447; the fid and everything else is unchanged.) const legacy = toHtml({ ...defaultProps, recipientEmail: 'a@example.com', thankYouUrl: '/thx' }, '', 'n1'); expect(legacy.html.startsWith(`${FROZEN_LEGACY_MARKER}
= ({ submitText = 'Send Message', submitColor = tokenRef('primary'), formAction = '#', - captcha, + recipientEmail, + destinationType, successMessage = 'Thank you! We\'ll get back to you soon.', style = {}, labelColor = tokenRef('text'), @@ -154,7 +155,7 @@ export const ContactForm: UserComponent = ({ )} ))} - {captcha === true && } + {isRelayWired(recipientEmail, destinationType) && } - {captcha === true && } + {isRelayWired(recipientEmail) && } ); }; @@ -138,7 +138,6 @@ SubscribeForm.craft = { style: { backgroundColor: tokenRef('bg') }, recipientEmail: '', thankYouUrl: '', - captcha: true, animation: '', animationDelay: '', hideOnDesktop: false, @@ -208,7 +207,7 @@ SubscribeForm.craft = { // `formAction`-less relay wiring (fallbackAction undefined -> '#') keeps // the old no-recipient case visually identical (action="#") while making // the relay path actually functional once an admin sets an email. - const { marker, actionAttr, honeypot } = relayFormWiring(props.recipientEmail, props.thankYouUrl, undefined, nodeId, undefined, props.captcha); + const { marker, actionAttr, honeypot } = relayFormWiring(props.recipientEmail, props.thankYouUrl, undefined, nodeId); return { html: `${marker} diff --git a/craft/src/components/forms/captcha.toHtml.test.ts b/craft/src/components/forms/captcha.toHtml.test.ts index d87d751..d7e298b 100644 --- a/craft/src/components/forms/captcha.toHtml.test.ts +++ b/craft/src/components/forms/captcha.toHtml.test.ts @@ -13,24 +13,22 @@ const base = { recipientEmail: 'a@b.com', thankYouUrl: '/thx' }; describe.each(cases)('%s captcha marker attribute', (_n, toHtml, extra) => { const run = (p: any) => toHtml({ ...extra, ...base, ...p }, '', 'node1').html as string; - test('true emits captcha="1" between id and recipient', () => { - expect(run({ captcha: true })).toMatch(//); + const MARK = //; + test('relay form always emits captcha="1" between id and recipient', () => { + expect(run({ captcha: true })).toMatch(MARK); }); - test('explicit false emits captcha="0"', () => { - expect(run({ captcha: false })).toMatch(//); - expect(html).not.toContain('captcha'); - expect(run({ captcha: undefined })).toBe(html); - }); - test('non-boolean junk emits nothing', () => { - expect(run({ captcha: 'yes' })).not.toContain('captcha'); - expect(run({ captcha: 1 })).not.toContain('captcha'); + test('no captcha prop at all (new design) emits captcha="1"', () => { + expect(run({})).toMatch(MARK); }); test('no relay (no recipient): no marker at all, even with captcha true', () => { expect(toHtml({ ...extra, captcha: true }, '', 'n').html).not.toContain('WHP-FORM'); + expect(toHtml({ ...extra, captcha: false }, '', 'n').html).not.toContain('captcha'); }); test('never emits widget markup (injected at publish)', () => { expect(run({ captcha: true })).not.toMatch(/altcha-widget|challenge\.php|widget\.php/); @@ -42,8 +40,18 @@ test('webhook marker keeps captcha attr inside the strict-regex attribute zone', expect(m).toMatch(/^$/); + expect(legacy.marker).toMatch(/^$/); }); test('an email form with no recipient is still not a relay at all', () => { @@ -78,7 +78,7 @@ describe('relayFormWiring webhook marker matches the publish-side parser exactly test('the optional attributes sit BETWEEN id and recipient, lowercase-named', () => { const attrs = w.marker.match(PUBLISH_MARKER_RE)![2]; - expect(attrs).toBe(' type="webhook" url="https://hooks.example.com/x" secret="whs_7_abc123" authmode="bearer"'); + expect(attrs).toBe(' type="webhook" url="https://hooks.example.com/x" secret="whs_7_abc123" authmode="bearer" captcha="1"'); }); test('a webhook with no recipient still emits a relay marker', () => { diff --git a/craft/src/utils/form-relay-wiring.ts b/craft/src/utils/form-relay-wiring.ts index 2970c62..bafb316 100644 --- a/craft/src/utils/form-relay-wiring.ts +++ b/craft/src/utils/form-relay-wiring.ts @@ -63,6 +63,11 @@ function markerAttr(name: string, value: string): string { return ` ${name}="${escapeAttr(value)}"`; } +/** Is a form wired to the relay (recipient set, or webhook destination)? Captcha is always on for these. */ +export function isRelayWired(recipientEmail?: string, destinationType?: string): boolean { + return !!recipientEmail || allowlist(destinationType, DESTINATION_TYPES, 'email') === 'webhook'; +} + export interface RelayWiring { /** true when a recipient is set (relay path); false = legacy formAction fallback */ useRelay: boolean; @@ -82,10 +87,10 @@ export interface RelayWiring { * the marker/placeholder id deterministically and uniquely -- * see `scopeId` in ./escape. Falls back to a stable hash of the * recipient/thankYouUrl/fallbackAction when omitted (never random). - * @param captcha ALTCHA captcha intent for the relay (whp: FormRelayRewrite). `true` emits - * `captcha="1"`, EXPLICIT `false` emits `captcha="0"` (user un-ticked: the - * relay must clear a stored flag), anything else (undefined/legacy) emits NO - * attribute, so legacy saved designs stay byte-identical and keep their stored flag. + * @param _legacyCaptcha IGNORED. Captcha is MANDATORY on every relay form (no opt-out): a relay-wired form + * always emits `captcha="1"`, whatever an old saved design holds in its `captcha` prop + * (undefined, true, false or junk). Kept as a parameter only so existing call sites and + * old projects keep compiling/loading; the publish step ignores the attribute value too. * @param destination optional destination descriptor. Omitted, or `type: 'email'`, * yields the LEGACY narrow marker byte for byte -- every * already-published site depends on that shape continuing to @@ -111,7 +116,7 @@ export function relayFormWiring( fallbackAction: string | undefined, nodeId?: string, destination?: FormDestination, - captcha?: boolean, + _legacyCaptcha?: unknown, ): RelayWiring { const destType = allowlist(destination?.type, DESTINATION_TYPES, 'email'); const isWebhook = destType === 'webhook'; @@ -141,8 +146,8 @@ export function relayFormWiring( + markerAttr('authmode', allowlist(destination?.authMode, AUTH_MODES, 'signature')) : ''; - const captchaAttr = captcha === true ? markerAttr('captcha', '1') - : captcha === false ? markerAttr('captcha', '0') : ''; + // Always on: the widget, flag and script are applied together at publish (whp: FormRelayRewrite). + const captchaAttr = markerAttr('captcha', '1'); return { useRelay: true,