diff --git a/django_forms_workflows/static/django_forms_workflows/js/workflow-builder.js b/django_forms_workflows/static/django_forms_workflows/js/workflow-builder.js index 5475224..91727fd 100644 --- a/django_forms_workflows/static/django_forms_workflows/js/workflow-builder.js +++ b/django_forms_workflows/static/django_forms_workflows/js/workflow-builder.js @@ -4,7 +4,7 @@ * Drag-and-drop workflow builder for post-submission actions and approvals. */ -class WorkflowBuilder { +export class WorkflowBuilder { constructor(config) { this.config = config; this.nodes = []; @@ -1784,7 +1784,7 @@ class WorkflowBuilder { fields.forEach(field => { const prefillBadge = field.prefill_source ? - ` ${field.prefill_source}` : ''; + ` ${this.escapeHtml(field.prefill_source)}` : ''; const requiredBadge = field.required ? `Required` : ''; @@ -1793,7 +1793,7 @@ class WorkflowBuilder {
${this.escapeHtml(field.label)} - ${field.name} (${field.type}) + ${this.escapeHtml(field.name)} (${this.escapeHtml(field.type)})
${requiredBadge} @@ -2893,10 +2893,10 @@ class WorkflowBuilder { return `${badgeHtml}${fieldCount} field${fieldCount !== 1 ? 's' : ''} • Edit Form`; case 'workflow_settings': const parts_ws = []; - if (node.data.name_label) parts_ws.push(node.data.name_label); + if (node.data.name_label) parts_ws.push(this.escapeHtml(node.data.name_label)); if (node.data.approval_deadline_days) parts_ws.push(`Deadline: ${node.data.approval_deadline_days}d`); - if (node.data.notification_cadence && node.data.notification_cadence !== 'immediate') parts_ws.push(`Cadence: ${node.data.notification_cadence}`); - if (node.data.notification_cadence_form_field && node.data.notification_cadence === 'form_field_date') parts_ws.push(`Date field: ${node.data.notification_cadence_form_field}`); + if (node.data.notification_cadence && node.data.notification_cadence !== 'immediate') parts_ws.push(`Cadence: ${this.escapeHtml(node.data.notification_cadence)}`); + if (node.data.notification_cadence_form_field && node.data.notification_cadence === 'form_field_date') parts_ws.push(`Date field: ${this.escapeHtml(node.data.notification_cadence_form_field)}`); if (node.data.notification_rules && node.data.notification_rules.length > 0) parts_ws.push(`Notifications: ${node.data.notification_rules.length}`); if (node.data.trigger_conditions && node.data.trigger_conditions.conditions && node.data.trigger_conditions.conditions.length > 0) parts_ws.push('Conditional'); return parts_ws.length > 0 ? @@ -2908,14 +2908,14 @@ class WorkflowBuilder { if (node.data.allow_send_back) stageParts.push('Send Back target'); if (node.data.allow_reassign) stageParts.push('Reassign'); if (node.data.allow_edit_form_data) stageParts.push('Editable'); - if (node.data.assignee_form_field) stageParts.push(`Dynamic: ${node.data.assignee_form_field}`); + if (node.data.assignee_form_field) stageParts.push(`Dynamic: ${this.escapeHtml(node.data.assignee_form_field)}`); if (node.data.approval_fields && node.data.approval_fields.length > 0) stageParts.push(`Stage fields: ${node.data.approval_fields.length}`); if (node.data.trigger_conditions && node.data.trigger_conditions.conditions && node.data.trigger_conditions.conditions.length > 0) stageParts.push('Conditional'); if (node.data.approval_groups && node.data.approval_groups.length > 0) { const gc = node.data.approval_groups.length; - stageParts.push(`${gc} group${gc > 1 ? 's' : ''} (${node.data.approval_logic || 'all'})`); + stageParts.push(`${gc} group${gc > 1 ? 's' : ''} (${this.escapeHtml(node.data.approval_logic || 'all')})`); } - const label = node.data.approve_label ? ` • "${node.data.approve_label}"` : ''; + const label = node.data.approve_label ? ` • "${this.escapeHtml(node.data.approve_label)}"` : ''; return stageParts.length > 0 ? `Stage ${node.data.order || '?'}
${stageParts.join(' + ')}${label}` : `Stage ${node.data.order || '?'}
No approvers configured`; @@ -2934,22 +2934,22 @@ class WorkflowBuilder { 'not_empty': 'is not empty' }; const op = operatorSymbols[node.data.operator] || node.data.operator; - return `If ${node.data.field} ${op} ${node.data.value || ''}`; + return `If ${this.escapeHtml(node.data.field)} ${this.escapeHtml(op)} ${this.escapeHtml(node.data.value || '')}`; } return 'Configure condition'; case 'action': - return node.data.action_type ? `${node.data.action_type.toUpperCase()}: ${node.data.trigger || ''}` : 'Configure action'; + return node.data.action_type ? `${this.escapeHtml(node.data.action_type.toUpperCase())}: ${this.escapeHtml(node.data.trigger || '')}` : 'Configure action'; case 'email': - if (node.data.email_to && node.data.email_to_field) return `Send to: ${node.data.email_to} + field ${node.data.email_to_field}`; - if (node.data.email_to) return `Send to: ${node.data.email_to}`; - if (node.data.email_to_field) return `Send to field: ${node.data.email_to_field}`; + if (node.data.email_to && node.data.email_to_field) return `Send to: ${this.escapeHtml(node.data.email_to)} + field ${this.escapeHtml(node.data.email_to_field)}`; + if (node.data.email_to) return `Send to: ${this.escapeHtml(node.data.email_to)}`; + if (node.data.email_to_field) return `Send to field: ${this.escapeHtml(node.data.email_to_field)}`; return 'Configure email'; case 'join': return 'Parallel stages merge here'; case 'sub_workflow': const swParts = []; - if (node.data.sub_workflow_name) swParts.push(node.data.sub_workflow_name); - if (node.data.count_field) swParts.push(`Count: ${node.data.count_field}`); + if (node.data.sub_workflow_name) swParts.push(this.escapeHtml(node.data.sub_workflow_name)); + if (node.data.count_field) swParts.push(`Count: ${this.escapeHtml(node.data.count_field)}`); if (node.data.trigger) swParts.push(node.data.trigger === 'on_approval' ? 'After approval' : 'On submission'); if (node.data.detached) swParts.push('Detached'); if (node.data.reject_parent) swParts.push('Rejects parent'); @@ -3162,6 +3162,3 @@ class WorkflowBuilder { return `M ${x1} ${y1} C ${cx1} ${y1}, ${cx2} ${y2}, ${x2} ${y2}`; } } - -// This script is loaded before the inline admin bootstrap code. -window.WorkflowBuilder = WorkflowBuilder; diff --git a/django_forms_workflows/templates/admin/django_forms_workflows/workflow_builder.html b/django_forms_workflows/templates/admin/django_forms_workflows/workflow_builder.html index 08a7705..7c3d90a 100644 --- a/django_forms_workflows/templates/admin/django_forms_workflows/workflow_builder.html +++ b/django_forms_workflows/templates/admin/django_forms_workflows/workflow_builder.html @@ -1119,8 +1119,9 @@
Visual Builder Scope
{% block extrahead %} - -'; + + it('escapes workflow_settings name_label/notification_cadence/notification_cadence_form_field', () => { + const instance = createInstance(); + const node = { + type: 'workflow_settings', + data: { + name_label: payload, + notification_cadence: 'form_field_date', + notification_cadence_form_field: payload, + }, + }; + + const html = instance.getNodeDescription(node); + + expect(html).not.toContain(''; + const node = { + id: 'form_1', + data: { + is_initial: true, + form_name: 'Test Form', + field_count: 1, + fields: [ + { + label: payload, + name: payload, + type: payload, + prefill_source: payload, + }, + ], + has_more_fields: false, + }, + }; + + const html = instance.buildFormProperties(node); + + expect(html).not.toContain('