Skip to content

Commit ddc42df

Browse files
matteiusclaude
andcommitted
feat: embeddable forms, WordPress plugin, embed code panel + fix CodeQL alerts
Embeddable Forms: - form_embed view (@xframe_options_exempt) with embed_base.html minimal layout - dfw-embed.js loader: iframe auto-resize via postMessage, theme/accent color, data-on-submit/data-on-load callbacks - embed_enabled BooleanField on FormDefinition - Inline success rendering, SameSite=None CSRF cookie for cross-origin - Admin embed code panel with JS/iframe/WordPress tabs + copy-to-clipboard - Form builder: Embeddable checkbox in Submission Controls - sync_api export/import and clone support for embed_enabled - Migration 0086, 14 new tests WordPress Plugin (wordpress/dfw-forms/): - [dfw_form] shortcode with sanitized attributes - Gutenberg block (apiVersion 3, no build step) with live preview - Settings page with server URL and Test Connection button - JS and iframe embed modes Security fixes (CodeQL #23, #25, #26-28, #29-30): - dfw-embed.js: validate data-server as http(s) URL via new URL() before setting iframe.src (fixes #29, #30) - workflow-builder.js: validate workflowId as integer, use URL() constructor instead of template literal (fixes #25) - sync_api.py: remove user-supplied slugs from log messages (fixes #26-28) - workflow_builder_views.py: stop exposing ValidationError messages in JSON response, log server-side only (fixes #23) - views.py: sanitize accent_color query param with hex regex Documentation: - docs/EMBEDDING.md: full guide with JS loader, iframe, WordPress plugin, security considerations - CHANGELOG.md: v0.59.0 entry - README.md: embeddable forms feature highlight and detailed section Co-authored-by: Claude Code <noreply@anthropic.com>
1 parent 6bfd124 commit ddc42df

17 files changed

Lines changed: 1305 additions & 17 deletions

File tree

CHANGELOG.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.59.0] - 2026-04-02
11+
12+
### Added
13+
- **Embeddable Forms** — Embed DFW forms on any external website via iframe:
14+
- `dfw-embed.js` loader script: creates responsive iframe with auto-resize via `postMessage` (`dfw:loaded`, `dfw:resize`, `dfw:submitted`), configurable theme, accent color, callbacks.
15+
- `embed_base.html` minimal layout (no navbar/footer) + `form_embed.html` with full field JS support + `embed_success.html` inline success state.
16+
- `form_embed` view with `@xframe_options_exempt`, `SameSite=None; Secure` CSRF cookie, rate limiting for anonymous submissions, submission controls (close date, max submissions).
17+
- `embed_enabled` BooleanField on `FormDefinition`.
18+
- Admin: embed code panel on FormDefinition change form with three tabs (JS Embed, iframe Fallback, WordPress Shortcode) with copy-to-clipboard buttons.
19+
- Form builder: "Embeddable" checkbox in Submission Controls.
20+
- sync_api export/import and clone support.
21+
- Migration `0086`.
22+
- **WordPress Plugin** (`wordpress/dfw-forms/`):
23+
- `[dfw_form]` shortcode with full attribute sanitization.
24+
- Gutenberg block (apiVersion 3, no build step) with live preview and sidebar controls.
25+
- Settings page at Settings > DFW Forms with server URL and "Test Connection" button.
26+
- JS and iframe embed modes; WordPress.com compatibility notes.
27+
28+
### Fixed
29+
- **CodeQL #25**: DOM text reinterpreted as HTML in workflow-builder.js — validate workflowId and use `URL()` constructor.
30+
- **CodeQL #26-28**: Clear-text logging of form slugs in sync_api.py — removed user-supplied values from log messages.
31+
- **CodeQL #23**: Information exposure via ValidationError in workflow_builder_views.py — log server-side only, return generic error.
32+
33+
### Documentation
34+
- `docs/EMBEDDING.md` — full embedding guide: JS loader, iframe fallback, WordPress plugin, security considerations (CORS, CSP, CSRF, rate limiting).
35+
36+
### Tests
37+
- 14 new embed tests: GET/POST, disabled/inactive, theme, accent color sanitisation, closed form, max submissions, audit log, success message piping, no-redirect behavior.
38+
1039
## [0.58.0] - 2026-04-02
1140

1241
### Added

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Django Forms Workflows bridges the gap between simple form libraries (like Crisp
2121
- 🔄 **Cross-Instance Sync** — Push/pull form definitions between environments directly from the Django Admin.
2222
- 🔒 **Enterprise Security** — LDAP/AD & SSO authentication, RBAC with four permission tiers, complete audit trails.
2323
- 🌐 **REST API** — Opt-in Bearer-token API: list forms, fetch field schema, submit (or save draft), poll status. OpenAPI 3.0 schema + Swagger UI included.
24+
- 🖼️ **Embeddable Forms** — Embed forms on any website via a `<script>` tag or `<iframe>`. Auto-resize, theme/accent color support, postMessage callbacks. WordPress plugin included.
2425
- 💳 **Pluggable Payments** — Collect payments during form submission with a pluggable provider system. Stripe ships built-in; add custom providers via `register_provider()`.
2526
- 📋 **Shared Option Lists** — Define reusable choice lists once, reference from any form field. Update the list and every field reflects the change.
2627
- 🔗 **Dependent Workflows** — Workflows that start only after all other workflows on the form complete, enabling convergence gates after parallel tracks.
@@ -75,6 +76,17 @@ Spawn child workflow instances from a parent submission:
7576

7677
See [Sub-Workflows Guide](docs/SUB_WORKFLOWS.md) for a full walkthrough.
7778

79+
### 🖼️ Embeddable Forms
80+
Embed DFW forms on any external website:
81+
- Single `<script>` tag creates a responsive iframe with auto-resize via `postMessage`
82+
- Configurable theme (`light`/`dark`), accent color, min height
83+
- Callbacks: `data-on-submit`, `data-on-load` for parent page integration
84+
- WordPress plugin included (`[dfw_form]` shortcode + Gutenberg block)
85+
- Admin embed code panel with copy-to-clipboard snippets
86+
- Inline success state — no redirects, iframe stays self-contained
87+
88+
See [Embedding Guide](docs/EMBEDDING.md).
89+
7890
### 💳 Payment Collection
7991
Collect payments as part of the form submission flow:
8092
- Pluggable provider architecture: `PaymentProvider` ABC, provider registry, `PaymentResult` dataclass

django_forms_workflows/static/django_forms_workflows/js/dfw-embed.js

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -47,11 +47,22 @@
4747
var minHeight = parseInt(script.getAttribute('data-min-height')) || 300;
4848
var loadingText = script.getAttribute('data-loading-text') || 'Loading form...';
4949

50-
// Normalize server URL (remove trailing slash)
51-
server = server.replace(/\/+$/, '');
50+
// Validate server is a proper HTTP(S) URL to prevent script injection
51+
var sanitisedServer;
52+
try {
53+
var parsedServer = new URL(server);
54+
if (parsedServer.protocol !== 'https:' && parsedServer.protocol !== 'http:') {
55+
console.error('[dfw-embed] data-server must be an http(s) URL');
56+
return;
57+
}
58+
sanitisedServer = parsedServer.origin;
59+
} catch (e) {
60+
console.error('[dfw-embed] Invalid data-server URL:', e.message);
61+
return;
62+
}
5263

53-
// Build embed URL
54-
var embedUrl = server + '/forms/' + encodeURIComponent(formSlug) + '/embed/';
64+
// Build embed URL using validated origin
65+
var embedUrl = sanitisedServer + '/forms/' + encodeURIComponent(formSlug) + '/embed/';
5566
var params = [];
5667
if (theme) params.push('theme=' + encodeURIComponent(theme));
5768
if (accentColor) params.push('accent_color=' + encodeURIComponent(accentColor));
@@ -90,15 +101,8 @@
90101
script.parentNode.insertBefore(container, script.nextSibling);
91102
}
92103

93-
// Parse the server origin for message validation
94-
var serverOrigin;
95-
try {
96-
var a = document.createElement('a');
97-
a.href = server;
98-
serverOrigin = a.protocol + '//' + a.host;
99-
} catch (e) {
100-
serverOrigin = server;
101-
}
104+
// Use the already-validated origin for message validation
105+
var serverOrigin = sanitisedServer;
102106

103107
// Listen for postMessage events from the iframe
104108
window.addEventListener('message', function (event) {

django_forms_workflows/templates/admin/django_forms_workflows/formdef_change_form.html

Lines changed: 173 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,92 @@
8989
display: none;
9090
margin-top: 4px;
9191
}
92+
/* Embed code panel */
93+
.embed-code-panel {
94+
background: #f8f9fa;
95+
border: 1px solid #dee2e6;
96+
border-radius: 8px;
97+
padding: 20px;
98+
margin: 20px 0;
99+
}
100+
.embed-code-panel h3 {
101+
margin: 0 0 8px 0;
102+
font-size: 14px;
103+
color: #333;
104+
}
105+
.embed-code-panel .embed-warning {
106+
background: #fff3cd;
107+
border: 1px solid #ffc107;
108+
border-radius: 4px;
109+
padding: 8px 12px;
110+
margin-bottom: 12px;
111+
font-size: 13px;
112+
color: #664d03;
113+
}
114+
.embed-code-tabs {
115+
display: flex;
116+
gap: 0;
117+
border-bottom: 2px solid #dee2e6;
118+
margin-bottom: 16px;
119+
}
120+
.embed-code-tab {
121+
padding: 8px 16px;
122+
border: none;
123+
background: none;
124+
font-size: 13px;
125+
font-weight: 500;
126+
color: #666;
127+
cursor: pointer;
128+
border-bottom: 2px solid transparent;
129+
margin-bottom: -2px;
130+
}
131+
.embed-code-tab:hover {
132+
color: #333;
133+
}
134+
.embed-code-tab.active {
135+
color: #0d6efd;
136+
border-bottom-color: #0d6efd;
137+
}
138+
.embed-code-content {
139+
display: none;
140+
}
141+
.embed-code-content.active {
142+
display: block;
143+
}
144+
.embed-code-content pre {
145+
background: #1e1e1e;
146+
color: #d4d4d4;
147+
padding: 14px 16px;
148+
border-radius: 6px;
149+
font-size: 12px;
150+
line-height: 1.5;
151+
overflow-x: auto;
152+
margin: 0 0 8px 0;
153+
white-space: pre-wrap;
154+
word-break: break-all;
155+
}
156+
.embed-code-content .embed-note {
157+
font-size: 12px;
158+
color: #666;
159+
margin-top: 8px;
160+
}
161+
.embed-copy-btn {
162+
padding: 6px 12px;
163+
border: 1px solid #ccc;
164+
border-radius: 4px;
165+
background: #fff;
166+
cursor: pointer;
167+
font-size: 12px;
168+
}
169+
.embed-copy-btn:hover {
170+
background: #f0f0f0;
171+
}
172+
.embed-copy-feedback {
173+
font-size: 12px;
174+
color: #28a745;
175+
display: none;
176+
margin-left: 8px;
177+
}
92178
</style>
93179
{% endblock %}
94180

@@ -140,23 +226,75 @@ <h3><i class="bi bi-qr-code"></i> Share Form via QR Code</h3>
140226
</div>
141227
</div>
142228
</div>
229+
<div class="embed-code-panel">
230+
<h3><i class="bi bi-code-slash"></i> Embed Code</h3>
231+
<p style="font-size: 13px; color: #666; margin-bottom: 12px;">
232+
Copy a code snippet to embed this form on any website.
233+
</p>
234+
{% if not original.embed_enabled %}
235+
<div class="embed-warning">
236+
<i class="bi bi-exclamation-triangle"></i>
237+
Embedding is currently disabled for this form. Enable it in the "API &amp; Embedding" section above.
238+
</div>
239+
{% endif %}
240+
<div class="embed-code-tabs">
241+
<button type="button" class="embed-code-tab active" data-tab="js">JS Embed</button>
242+
<button type="button" class="embed-code-tab" data-tab="iframe">iframe Fallback</button>
243+
<button type="button" class="embed-code-tab" data-tab="wordpress">WordPress</button>
244+
</div>
245+
246+
<div class="embed-code-content active" id="embed-tab-js">
247+
<pre id="embed-snippet-js"></pre>
248+
<button type="button" class="embed-copy-btn" data-target="embed-snippet-js">
249+
<i class="bi bi-clipboard"></i> Copy to Clipboard
250+
</button>
251+
<span class="embed-copy-feedback">Copied!</span>
252+
<p class="embed-note">
253+
Best option for most websites. The form auto-resizes to fit its content.
254+
</p>
255+
</div>
256+
257+
<div class="embed-code-content" id="embed-tab-iframe">
258+
<pre id="embed-snippet-iframe"></pre>
259+
<button type="button" class="embed-copy-btn" data-target="embed-snippet-iframe">
260+
<i class="bi bi-clipboard"></i> Copy to Clipboard
261+
</button>
262+
<span class="embed-copy-feedback">Copied!</span>
263+
<p class="embed-note">
264+
Use this if external scripts are restricted (e.g., WordPress.com). You may need to set a fixed height.
265+
</p>
266+
</div>
267+
268+
<div class="embed-code-content" id="embed-tab-wordpress">
269+
<pre id="embed-snippet-wordpress"></pre>
270+
<button type="button" class="embed-copy-btn" data-target="embed-snippet-wordpress">
271+
<i class="bi bi-clipboard"></i> Copy to Clipboard
272+
</button>
273+
<span class="embed-copy-feedback">Copied!</span>
274+
<p class="embed-note">
275+
Requires the <strong>DFW Forms</strong> WordPress plugin. Configure the server URL under Settings &gt; DFW Forms.
276+
</p>
277+
</div>
278+
</div>
279+
143280
<script>
144281
document.addEventListener('DOMContentLoaded', function() {
145282
var qrUrl = '{% url "forms_workflows:form_qr_code" original.slug %}';
146283
var submitUrl = '{% url "forms_workflows:form_submit" original.slug %}';
284+
var embedUrl = '{% url "forms_workflows:form_embed" original.slug %}';
147285
var absUrl = window.location.origin + submitUrl;
286+
var absEmbedUrl = window.location.origin + embedUrl;
287+
var absStaticBase = window.location.origin;
148288
var slug = '{{ original.slug }}';
149289

150-
// Set URL field
290+
// -- QR code panel --
151291
document.getElementById('adminQrUrl').value = absUrl;
152292

153-
// Set download links
154293
document.getElementById('adminQrDownloadSvg').href = qrUrl + '?format=svg';
155294
document.getElementById('adminQrDownloadSvg').download = slug + '-qr.svg';
156295
document.getElementById('adminQrDownloadPng').href = qrUrl + '?format=png&size=12';
157296
document.getElementById('adminQrDownloadPng').download = slug + '-qr.png';
158297

159-
// Load QR preview
160298
var preview = document.getElementById('adminQrPreview');
161299
fetch(qrUrl + '?format=svg')
162300
.then(function(r) { return r.text(); })
@@ -172,14 +310,45 @@ <h3><i class="bi bi-qr-code"></i> Share Form via QR Code</h3>
172310
preview.innerHTML = '<span style="color: #dc3545;">Failed to load QR code.<br>Is segno installed?</span>';
173311
});
174312

175-
// Copy button
176313
document.getElementById('adminQrCopyBtn').addEventListener('click', function() {
177314
navigator.clipboard.writeText(absUrl).then(function() {
178315
var fb = document.getElementById('adminQrCopyFeedback');
179316
fb.style.display = 'block';
180317
setTimeout(function() { fb.style.display = 'none'; }, 2500);
181318
});
182319
});
320+
321+
// -- Embed code panel --
322+
var jsSnippet = '<script src="' + absStaticBase + '/static/django_forms_workflows/js/dfw-embed.js"\n data-form="' + slug + '"\n data-server="' + absStaticBase + '"\n><\/script>';
323+
var iframeSnippet = '<iframe src="' + absEmbedUrl + '"\n style="width:100%;border:none;min-height:500px;"\n title="Form: ' + slug + '"\n loading="lazy"\n allowtransparency="true"\n></iframe>';
324+
var wpSnippet = '[dfw_form slug="' + slug + '"]';
325+
326+
document.getElementById('embed-snippet-js').textContent = jsSnippet;
327+
document.getElementById('embed-snippet-iframe').textContent = iframeSnippet;
328+
document.getElementById('embed-snippet-wordpress').textContent = wpSnippet;
329+
330+
// Tab switching
331+
document.querySelectorAll('.embed-code-tab').forEach(function(tab) {
332+
tab.addEventListener('click', function() {
333+
document.querySelectorAll('.embed-code-tab').forEach(function(t) { t.classList.remove('active'); });
334+
document.querySelectorAll('.embed-code-content').forEach(function(c) { c.classList.remove('active'); });
335+
tab.classList.add('active');
336+
document.getElementById('embed-tab-' + tab.getAttribute('data-tab')).classList.add('active');
337+
});
338+
});
339+
340+
// Copy buttons
341+
document.querySelectorAll('.embed-copy-btn').forEach(function(btn) {
342+
btn.addEventListener('click', function() {
343+
var targetId = btn.getAttribute('data-target');
344+
var text = document.getElementById(targetId).textContent;
345+
navigator.clipboard.writeText(text).then(function() {
346+
var fb = btn.nextElementSibling;
347+
fb.style.display = 'inline';
348+
setTimeout(function() { fb.style.display = 'none'; }, 2500);
349+
});
350+
});
351+
});
183352
});
184353
</script>
185354
{% endif %}

0 commit comments

Comments
 (0)