Repository navigation
Forms
oCMS includes a built-in form builder for creating contact forms, surveys, and other data collection forms. Forms support multiple field types, multi-language translations, honeypot spam protection, hCaptcha integration, webhook notifications, and CSV export.
The forms system consists of three parts:
- Form builder -- admin UI for creating forms and configuring fields
-
Public rendering -- themed form pages at
/forms/{slug}with validation and spam protection - Submission management -- admin UI for viewing, filtering, and exporting submissions
| Type | Description | Options | Validation |
|---|---|---|---|
text |
Single-line text input | -- | minLength, maxLength, pattern |
email |
Email input | -- | Built-in email format check |
textarea |
Multi-line text (5 rows) | -- | minLength, maxLength |
number |
Numeric input | -- | Built-in float parsing |
date |
Date picker (YYYY-MM-DD) | -- | Built-in date format check |
select |
Dropdown menu | JSON array of options | -- |
radio |
Radio button group | JSON array of options | -- |
checkbox |
Checkbox group (every checked option is stored, joined with , ; a value that is not one of the options is refused) |
JSON array of options | -- |
file |
File upload input | -- | Parsed but not processed |
captcha |
hCaptcha widget | -- | Verified via hook (not stored) |
For select, radio, and checkbox fields, options are stored as a JSON array:
["Option 1", "Option 2", "Option 3"]Custom validation rules are stored as a JSON object:
{
"minLength": 5,
"maxLength": 200,
"pattern": "^[a-zA-Z ]+$"
}-
minLength-- minimum character count -
maxLength-- maximum character count -
pattern-- regex pattern the value must match
Validation is applied server-side. Required fields use the is_required flag.
Forms in the default language are accessible at GET /forms/{slug} and submitted via POST /forms/{slug}; a form in another active language lives under that language's prefix, as /{lang}/forms/{slug}.
- User loads form page (rendered via active theme)
- User fills in fields and submits
- Server validates payload size, honeypot, captcha, and field rules
- On success: stores submission, dispatches webhook, renders success message
- On failure: re-renders form with errors and preserved field values
Each form has a configurable success_message field. Default: "Thank you for your submission."
All public forms include a hidden _website field (invisible to users via CSS). Bots that fill this field are silently rejected -- they receive a fake success response.
When triggered:
- A
WARNING-level security event is logged - The
security.honeypot_triggeredhook fires - If the Module: Sentinel is active, the IP is automatically banned
Forms use the global CSRF middleware which validates requests via Fetch metadata headers. No token is embedded in the form HTML. See CSRF Protection for details.
Public form submissions are rate-limited at 1 request per second with a burst of 5 per IP address. Excess submissions receive a 429 Too Many Requests response.
| Limit | Value | Description |
|---|---|---|
| Total body | 64 KB | Maximum request body size |
| Per field | 4 KB | Maximum single field value |
| Total data | 16 KB | Maximum combined submission data |
Oversized submissions are rejected with a security warning event logged.
Forms support hCaptcha via the optional hCaptcha module. See hCaptcha for configuration.
- Enable the hCaptcha module and configure site key / secret key
- Add a
captchafield to the form (only one per form) - Optionally set
OCMS_REQUIRE_FORM_CAPTCHA=true(the default in production when unset) to require a captcha on every public form
- The captcha widget is rendered via the
form.captcha_widgethook - Verification uses the
form.captcha_verifyhook - Captcha responses are not stored in submission data
- If
OCMS_REQUIRE_FORM_CAPTCHAis enabled but the captcha field or hCaptcha module is missing, submissions return 503
Access submissions at Admin > Forms > [Form] > Submissions.
Features:
- Sortable by: ID, created date, read status, IP address
- Default sort: newest first
- Pagination with configurable per-page count
- Preview of first 2 field values (truncated to 30 chars)
- Unread submission count badge
- Read status tracking (auto-marked when viewed)
Viewing a submission displays all field labels and values, IP address, user agent, and timestamp. The submission is automatically marked as read.
Select multiple submissions and delete them in bulk from the submissions list.
Export all submissions for a form as CSV. Columns include:
- ID, Submitted At, IP Address, Read status
- All field labels as column headers with corresponding values
Form submissions trigger a form.submitted webhook event. See Webhooks for general webhook configuration.
Controlled by the OCMS_WEBHOOK_FORM_DATA_MODE environment variable:
| Mode | Description |
|---|---|
redacted (default) |
Sensitive fields masked as [REDACTED], others truncated to 1024 chars |
none |
No form data included in webhook payload |
full |
All data included, values truncated to 1024 chars |
Fields are considered sensitive if their name (lowercased, hyphens replaced with underscores) contains any of these tokens:
password, passwd, token, secret, api_key, apikey, authorization, auth, ssn, credit, card, cvv
Set OCMS_REQUIRE_WEBHOOK_FORM_DATA_MINIMIZATION=true to prevent startup when OCMS_WEBHOOK_FORM_DATA_MODE=full in production environments. This is enabled by default in production when the variable is unset.
Forms support translations via the language_code field. See Multi-Language for the general multi-language system.
- Each form belongs to a specific language
- The same slug can exist across different languages
- Translations are linked via the
translationstable - Creating a translation copies the form structure to the target language
- The admin UI shows available and missing translations per form
| Method | Path | Description |
|---|---|---|
| GET | /admin/forms |
List all forms |
| GET | /admin/forms/new |
New form page |
| POST | /admin/forms |
Create form |
| GET | /admin/forms/{id} |
Edit form / field builder |
| PUT | /admin/forms/{id} |
Update form metadata |
| DELETE | /admin/forms/{id} |
Delete form |
| POST | /admin/forms/{id}/fields |
Add field (JSON) |
| PUT | /admin/forms/{id}/fields/{fieldId} |
Update field (JSON) |
| DELETE | /admin/forms/{id}/fields/{fieldId} |
Delete field |
| POST | /admin/forms/{id}/fields/reorder |
Reorder fields (JSON) |
| GET | /admin/forms/{id}/submissions |
List submissions |
| GET | /admin/forms/{id}/submissions/{subId} |
View submission |
| DELETE | /admin/forms/{id}/submissions/{subId} |
Delete submission |
| POST | /admin/forms/{id}/submissions/bulk-delete |
Bulk delete submissions |
| POST | /admin/forms/{id}/submissions/export |
Export submissions as CSV |
| POST | /admin/forms/{id}/translate/{langCode} |
Create form translation |
| Method | Path | Rate Limited | Description |
|---|---|---|---|
| GET | /forms/{slug} |
No | Display form |
| POST | /forms/{slug} |
Yes (1/s, burst 5) | Submit form |
| GET | /{lang}/forms/{slug} |
No | Display a form in a non-default language |
| POST | /{lang}/forms/{slug} |
Yes (1/s, burst 5) | Submit a form in a non-default language |
| Column | Type | Description |
|---|---|---|
id |
INTEGER PK | Auto-increment ID |
name |
TEXT | Form display name |
slug |
TEXT | URL slug (unique per language) |
title |
TEXT | Title shown to users |
description |
TEXT | Optional subtitle/description |
success_message |
TEXT | Message after submission |
email_to |
TEXT | Notification email (reserved) |
is_active |
BOOLEAN | Whether publicly accessible |
language_code |
TEXT | ISO language code |
created_at |
DATETIME | Creation timestamp |
updated_at |
DATETIME | Last update timestamp |
| Column | Type | Description |
|---|---|---|
id |
INTEGER PK | Auto-increment ID |
form_id |
INTEGER FK | Reference to forms table |
type |
TEXT | Field type (see supported types) |
name |
TEXT | HTML field name |
label |
TEXT | Display label |
placeholder |
TEXT | Placeholder text |
help_text |
TEXT | Helper text below field |
options |
TEXT | JSON array for select/radio/checkbox |
validation |
TEXT | JSON validation rules |
is_required |
BOOLEAN | Whether field is required |
position |
INTEGER | Sort order |
language_code |
TEXT | ISO language code |
created_at |
DATETIME | Creation timestamp |
updated_at |
DATETIME | Last update timestamp |
| Column | Type | Description |
|---|---|---|
id |
INTEGER PK | Auto-increment ID |
form_id |
INTEGER FK | Reference to forms table |
data |
TEXT | JSON string of field values |
ip_address |
TEXT | Submitter's IP address |
user_agent |
TEXT | Submitter's user agent |
is_read |
BOOLEAN | Read status |
language_code |
TEXT | ISO language code |
created_at |
DATETIME | Submission timestamp |
| Variable | Default | Description |
|---|---|---|
OCMS_WEBHOOK_FORM_DATA_MODE |
redacted |
Webhook payload mode: redacted, none, full
|
OCMS_REQUIRE_WEBHOOK_FORM_DATA_MINIMIZATION |
true in prod |
Block startup if mode is full in production |
OCMS_REQUIRE_FORM_CAPTCHA |
true in prod |
Require a captcha field on every public form; submissions to a form without one return 503, and production startup fails if hCaptcha is not configured or an active form has no captcha field |
Getting Started
Core Features
- Content Management
- Media Library
- Taxonomy
- Menu Builder
- Forms
- Multi-Language
- SEO
- Feeds
- Video Embedding
- Agent-Ready
Administration
API & Integrations
Security
Theme Development
Module Development
Built-in Modules
- Module: Sentinel
- Module: Developer
- Module: DB Manager
- Module: Migrator
- Module: Informer
- Module: Demo Mode
- Module: Internal Analytics
- Module: External Analytics
- Module: Privacy
- Module: Embed
- Module: MCP Server
Deployment