Guide

Block Forms architecture

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)
Flow

Submission path

5 relationships
Public submit flow for CPT-backed and inline forms.

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/form block.
  • Declares presence and prc-publish-workflows support 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/.

  1. SSR (render_form_callback) wraps markup in data-wp-interactive="prc-block/form" with context (formId, formName, method, namespace, action, actionConfig, captcha/submission flags, formPostId when present).
  2. Conditional fields get data-wp-bind--hidden via handle_conditional_form_field_display.
  3. Client store (view/index.js) owns field values, validation, captcha handoff, and sendSubmission.
  4. 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.

LayerMechanism
Bot protectionCloudflare Turnstile via prc-block/form-captcha
Abuse throttlingPRC\Platform\rate_limit_hit() (see class-form-send-email.php and log-response throttle filter)
User-scoped actionsFirebase 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.

ContextResolution
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.

PieceLocation
Schema (versioned dbDelta)includes/class-form-response-schema.php
Repositoryincludes/class-form-response-repository.php
Logger + logResponse RESTincludes/class-form-response-log.php
Admin RESTincludes/class-form-responses-rest-controller.php
DataViews UIincludes/class-form-responses-admin.php + src/response-admin/
Retention purgeincludes/class-form-response-retention.php

Notable behavior:

  • sendToEmail logs automatically (sent / send_failed).
  • logResponse stores 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; 0 disables). Action Scheduler job prc_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.

Was this helpful?