Maintainer overview of the Forms CPT, render path, submission security, and response logging.
System pieces
Form CPT (form) ──► Synced Form / Form_Renderer ──► prc-block/form (SSR + Interactivity)
│
▼
REST actions (sendToEmail, logResponse, …)
│
▼
prc_form_responses table ──► Forms → Responses (DataViews)
Submission path
Forms CPT
Registered in includes/class-forms.php. Top-level Forms admin menu.
- Not publicly queryable — forms render only where embedded or rendered in PHP.
- Editor template locked to a single
prc-block/formblock. - Declares
presenceandprc-publish-workflowssupport so All Forms can show active-editor avatars, the Active editors filter, and Working on / Watchers. - Action-agnostic: any registered action (
sendToEmail,sendSystemEmail, Mailchimp subscribe,logResponse, and others from consumer plugins).
List meta (_prc_form_action, _prc_form_method, _prc_form_field_count) is mirrored on save for the Forms library on the shared Admin DataViews shell (includes/class-form-list.php + src/admin-dataview/).
Synced Form and Form_Renderer
prc-block/synced-form embeds a form by ref (form post ID). Server-side rendering parses the form post’s blocks and renders them through WP_Block with prc-block/form/formPostId context so submissions tie back to the CPT.
Form_Renderer (includes/class-form-renderer.php) renders a form by ID or slug from PHP without placing a Synced Form block:
use PRC\Platform\Block_Forms\Form_Renderer;
$html = Form_Renderer::render_by_slug( 'account-login' );
$html = Form_Renderer::render_by_id( 123 );
It uses parse_blocks() + WP_Block::render() (not do_blocks()) so formPostId context propagates. Synced_Form::render_block_callback() delegates to Form_Renderer::render_by_id().
Form block runtime
Source: src/form/.
- SSR (
render_form_callback) wraps markup indata-wp-interactive="prc-block/form"with context (formId,formName,method,namespace,action,actionConfig, captcha/submission flags,formPostIdwhen present). - Conditional fields get
data-wp-bind--hiddenviahandle_conditional_form_field_display. - Client store (
view/index.js) owns field values, validation, captcha handoff, andsendSubmission. - Field values persist to localStorage under
prc-form-{formName}for 24 hours.
Form actions register in the prc-block-library/forms data store (consumed by this plugin’s Form editor). Consumer plugins such as prc-user-accounts, prc-email-builder, prc-user-surveys, and prc-quiz-builder declare prc-block-forms in Requires Plugins.
Public form security
Forms often render on VIP edge-cached pages. Page-baked WordPress nonces expire before many visitors submit, so nonceToken is not the primary auth gate for public forms.
| Layer | Mechanism |
|---|---|
| Bot protection | Cloudflare Turnstile via prc-block/form-captcha |
| Abuse throttling | PRC\Platform\rate_limit_hit() (see class-form-send-email.php and log-response throttle filter) |
| User-scoped actions | Firebase X-PRC-User-Id / X-PRC-User-Token where applicable |
sendToEmail recipient resolution
Handler: includes/class-form-send-email.php. Client-supplied forwardTo is never trusted alone.
| Context | Resolution |
|---|---|
Synced form / Forms CPT (formPostId > 0) | Reads actionConfig.forwardTo from the saved form post. Client forwardTo / forwardToSig ignored. |
Inline form (no formPostId) | Requires actionConfig.forwardTo and matching forwardToSig HMAC (Form_Send_Email::sign_forward_to(), keyed with wp_salt('auth')). |
Draft form posts resolve forwardTo only for users who can edit that form. Abilities that expose form definitions redact forwardTo to a presence-only token.
Response logging
Custom table {$wpdb->prefix}prc_form_responses.
| Piece | Location |
|---|---|
| Schema (versioned dbDelta) | includes/class-form-response-schema.php |
| Repository | includes/class-form-response-repository.php |
Logger + logResponse REST | includes/class-form-response-log.php |
| Admin REST | includes/class-form-responses-rest-controller.php |
| DataViews UI | includes/class-form-responses-admin.php + src/response-admin/ |
| Retention purge | includes/class-form-response-retention.php |
Notable behavior:
sendToEmaillogs automatically (sent/send_failed).logResponsestores without email; verifies Turnstile; rate-limits per IP (prc_block_form_log_response_throttle).- System fields (
captchaToken,nonceToken) stripped before storage. - Spam classification at log time (link flood, term blocklist, Akismet when active); Spam folder rather than drop.
- Unread defaults on; viewing does not auto-mark read.
- Retention: 365 days inbox, 30 days spam (filters
prc_form_responses_retention_days/prc_form_responses_spam_retention_days;0disables). Action Scheduler jobprc_form_responses_retention_purge.
Filters/actions: prc_block_form_response_logging_enabled, prc_platform_form_response_logged, prc_form_responses_purged, plus spam classification filters in the response log source.
Jetpack compatibility
The form render path disables Jetpack contact form conflicts. Jetpack Forms abilities (jetpack-forms/*) are unregistered so agents use prc-block-forms/*. Extend disabled slugs with prc_block_forms_disabled_jetpack_abilities.