Adds a webhook destination option to the site-builder contact form: destination selector, URL field, auth-mode selector, and a write-only secret field that POSTs to the panel and persists only the returned id. The raw secret never reaches a Craft prop.
Legacy markers stay byte-identical, proven by differential execution against the base across 30,240 cases. All four new marker attributes go through the existing escape helper, which is what keeps whole classes of publish-time strip failure unreachable.
Pairs with the whp-side branch of the same name. Ship order matters: the panel parser must land before or with the rebuilt bundle, or the feature ships inert.
Adds a webhook destination option to the site-builder contact form: destination selector, URL field, auth-mode selector, and a write-only secret field that POSTs to the panel and persists only the returned id. The raw secret never reaches a Craft prop.
Legacy markers stay byte-identical, proven by differential execution against the base across 30,240 cases. All four new marker attributes go through the existing escape helper, which is what keeps whole classes of publish-time strip failure unreachable.
Pairs with the whp-side branch of the same name. Ship order matters: the panel parser must land before or with the rebuilt bundle, or the feature ships inert.
Custom HTML block renders arbitrary markup on the published site.
Scripts and event handlers are stripped, but forms, iframes, and
images survive and can send data elsewhere. Add a plain, always-shown
advisory (no content detection) alongside the existing wrapper-styling
note.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ContactForm gains four craft props (destinationType, webhookUrl,
webhookSecretId, webhookAuthMode), all present in craft.props defaults so
FormStylePanel's `nodeProps.X !== undefined` gates actually render their
controls. The controls live in FormStylePanel (RightPanel renders only
GuidedStyles, so related.settings would be dead UI).
relayFormWiring widens the marker to optionally carry type/url/secret/
authmode BETWEEN `id` and `recipient`, which is where FormRelayRewrite.php's
parser looks. A marker with no type is byte-identical to what shipped before
-- pinned by a test that diffs an explicit-email form against one with no
destination props at all, since every already-published site depends on that
shape continuing to provision an email endpoint.
Every optional attribute value goes through one escaping site (markerAttr ->
escapeAttr); type and authmode are additionally allowlisted, so a case-drifted
"Bearer" reaches the relay as the exact literal it compares against instead of
being silently downgraded to unsigned.
The raw shared secret is never a prop: it is held in WebhookSecretField's
local state, POSTed to /api/form-webhook-secret.php on blur, and only the
returned opaque id is persisted. The field is write-only (set / replace /
remove, never view) because the endpoint has no read route, and the endpoint's
429 cap message is surfaced verbatim so a customer can act on it.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
I-1: WebhookSecretField now mounts with key={selectedId}. GuidedStyles renders
FormStylePanel with no key, so a selection change re-rendered rather than
remounted it and React kept `draft`/`status`. Since a failed store deliberately
retains the draft, clicking a second contact form showed node A's raw secret in
node B's field, and the next blur assigned the returned secret_id to the wrong
form (burning a slot against the per-site cap). The status banner leaked the
same way. Both pinned by the reviewer's repro sequence.
I-2: the button is "Clear", not "Remove", and says so -- nothing deletes a
stored key file, so the 50-per-site cap counts stores-ever. Labelling it Remove
told the customer they had reclaimed a slot right up until the 429 that said
otherwise. Actually deleting the file is Task 8's territory.
M-1: inline warning when the webhook URL is blank or not absolute https. The
publish step does refuse these, but into an error_log the customer never reads.
Warning only -- isHttpsWebhookUrl never edits the value or blocks the publish,
since either would trade a loud server-side refusal for a silently inert form.
M-2: the BYTE-IDENTITY test now compares against a literal marker captured by
executing the emitter at 071f3447, not against another head-revision output. The
self-comparison could only catch a drift affecting one side; a uniform one
passed it. Verified: a uniform `<!-- WHP-FORM` drift now fails this assertion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jknapp
merged commit a684d51996 into main2026-08-11 00:42:03 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Adds a webhook destination option to the site-builder contact form: destination selector, URL field, auth-mode selector, and a write-only secret field that POSTs to the panel and persists only the returned id. The raw secret never reaches a Craft prop.
Legacy markers stay byte-identical, proven by differential execution against the base across 30,240 cases. All four new marker attributes go through the existing escape helper, which is what keeps whole classes of publish-time strip failure unreachable.
Pairs with the whp-side branch of the same name. Ship order matters: the panel parser must land before or with the rebuilt bundle, or the feature ships inert.
I-1: WebhookSecretField now mounts with key={selectedId}. GuidedStyles renders FormStylePanel with no key, so a selection change re-rendered rather than remounted it and React kept `draft`/`status`. Since a failed store deliberately retains the draft, clicking a second contact form showed node A's raw secret in node B's field, and the next blur assigned the returned secret_id to the wrong form (burning a slot against the per-site cap). The status banner leaked the same way. Both pinned by the reviewer's repro sequence. I-2: the button is "Clear", not "Remove", and says so -- nothing deletes a stored key file, so the 50-per-site cap counts stores-ever. Labelling it Remove told the customer they had reclaimed a slot right up until the 429 that said otherwise. Actually deleting the file is Task 8's territory. M-1: inline warning when the webhook URL is blank or not absolute https. The publish step does refuse these, but into an error_log the customer never reads. Warning only -- isHttpsWebhookUrl never edits the value or blocks the publish, since either would trade a loud server-side refusal for a silently inert form. M-2: the BYTE-IDENTITY test now compares against a literal marker captured by executing the emitter at071f3447, not against another head-revision output. The self-comparison could only catch a drift affecting one side; a uniform one passed it. Verified: a uniform `<!-- WHP-FORM` drift now fails this assertion. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>