= ({
)}
))}
+ {captcha === true && }
= ({
action = '#',
method = 'POST',
style = {},
+ captcha,
}) => {
const { connectors: { connect, drag } } = useNode();
@@ -46,6 +50,7 @@ export const FormContainer: UserComponent = ({
style={{ display: 'flex', flexDirection: 'column', gap: '16px', padding: '0' }}
tag="div"
/>
+ {captcha === true && }
);
};
@@ -59,6 +64,7 @@ FormContainer.craft = {
method: 'POST',
recipientEmail: '',
thankYouUrl: '',
+ captcha: true,
style: {
padding: '24px',
backgroundColor: tokenRef('bg'),
@@ -85,7 +91,7 @@ FormContainer.craft = {
padding: '24px',
...props.style,
});
- const { useRelay, marker, actionAttr, honeypot } = relayFormWiring(props.recipientEmail, props.thankYouUrl, props.action, nodeId);
+ const { useRelay, marker, actionAttr, honeypot } = relayFormWiring(props.recipientEmail, props.thankYouUrl, props.action, nodeId, undefined, props.captcha);
const method = useRelay ? 'POST' : sanitizeFormMethod(props.method); // relay requires POST
const body = honeypot + childrenHtml; // honeypot as first child
return {
diff --git a/craft/src/components/forms/SubscribeForm.tsx b/craft/src/components/forms/SubscribeForm.tsx
index f9c5fc6..ebb9db4 100644
--- a/craft/src/components/forms/SubscribeForm.tsx
+++ b/craft/src/components/forms/SubscribeForm.tsx
@@ -1,6 +1,7 @@
import React, { CSSProperties } from 'react';
import { useNode, UserComponent } from '@craftjs/core';
import { cssPropsToString } from '../../utils/style-helpers';
+import { CaptchaMock } from './CaptchaMock';
import { relayFormWiring } from '../../utils/form-relay-wiring';
import { escapeHtml, escapeAttr } from '../../utils/escape';
import { tokenRef } from '../../utils/tokens';
@@ -25,6 +26,8 @@ interface SubscribeFormProps {
* published form then has no working action at all, which is the bug
* this prop exists to fix. */
recipientEmail?: string;
+ /** ALTCHA captcha (relay path). undefined = legacy design: no marker attr, stored flag untouched. */
+ captcha?: boolean;
thankYouUrl?: string;
animation?: string;
animationDelay?: string;
@@ -40,6 +43,7 @@ export const SubscribeForm: UserComponent = ({
buttonColor = tokenRef('primary'),
layout = 'inline',
style = {},
+ captcha,
}) => {
const {
connectors: { connect, drag },
@@ -116,6 +120,7 @@ export const SubscribeForm: UserComponent = ({
{buttonText}
+ {captcha === true && }
);
};
@@ -133,6 +138,7 @@ SubscribeForm.craft = {
style: { backgroundColor: tokenRef('bg') },
recipientEmail: '',
thankYouUrl: '',
+ captcha: true,
animation: '',
animationDelay: '',
hideOnDesktop: false,
@@ -202,7 +208,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);
+ const { marker, actionAttr, honeypot } = relayFormWiring(props.recipientEmail, props.thankYouUrl, undefined, nodeId, undefined, props.captcha);
return {
html: `${marker}
diff --git a/craft/src/components/forms/captcha.toHtml.test.ts b/craft/src/components/forms/captcha.toHtml.test.ts
new file mode 100644
index 0000000..d87d751
--- /dev/null
+++ b/craft/src/components/forms/captcha.toHtml.test.ts
@@ -0,0 +1,49 @@
+import { describe, test, expect } from 'vitest';
+import { ContactForm } from './ContactForm';
+import { SubscribeForm } from './SubscribeForm';
+import { FormContainer } from './FormContainer';
+import { relayFormWiring } from '../../utils/form-relay-wiring';
+
+const cases: [string, any, any][] = [
+ ['ContactForm', (ContactForm as any).toHtml, { fields: [] }],
+ ['SubscribeForm', (SubscribeForm as any).toHtml, {}],
+ ['FormContainer', (FormContainer as any).toHtml, {}],
+];
+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(//);
+ });
+ 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 relay (no recipient): no marker at all, even with captcha true', () => {
+ expect(toHtml({ ...extra, captcha: true }, '', 'n').html).not.toContain('WHP-FORM');
+ });
+ test('never emits widget markup (injected at publish)', () => {
+ expect(run({ captcha: true })).not.toMatch(/altcha-widget|challenge\.php|widget\.php/);
+ });
+});
+
+test('webhook marker keeps captcha attr inside the strict-regex attribute zone', () => {
+ const m = relayFormWiring('', '', undefined, 'n', { type: 'webhook', url: 'https://x.y/h' }, true).marker;
+ expect(m).toMatch(/^`,
+ marker: ``,
actionAttr: `__WHP_FORM_ACTION__${fid}__`,
honeypot: ` `,
};