# CustomFields.com – Fields & Content Builder — developer guide

Version `1.1.4`, GPL-2.0-or-later. Requires WordPress 6.5+ and PHP 7.4+. This documentation and release-metadata update inherits the runtime, browser and compatibility qualification from 1.1.3; fresh 1.1.4 checks cover the readme and release packages. It adds no new compatibility claims. The inherited test matrix does not establish compatibility with every theme, builder, plugin, multisite configuration, or hosting environment.

## Install and configure

Install the `customfields-com` directory in `wp-content/plugins`, or upload its ZIP through WordPress, then activate **CustomFields.com – Fields & Content Builder**. Administrators manage schemas in **CustomFields.com**. Free retains all 26 field types and WordPress-admin content tools; Pro supplies the visual directory/listing builder, configurable listing layouts and queries, frontend edit/create forms and interactive listing search. Existing saved listings continue rendering in Free. Use matching Free and Pro `1.1.4`; Pro 1.1.4 requires Free 1.1.3 or later. When updating both editions, update Free first, then Pro without uninstalling. Back up the database and export the existing schema before updating a staging copy.

When moving from the earlier `customfields` folder, back up the database and test on staging. Upload the renamed Free ZIP, deactivate **CustomFields** (the earlier Free plugin), then activate **CustomFields.com** and update Pro. Keep the earlier Free installation inactive; do not uninstall it during the transition. Both editions keep the existing definitions, content, options, hooks and PHP APIs. The renamed Free plugin refuses activation while the earlier Free installation is active.

The shared workspace supplies Build, Publish and Move data navigation, a keyboard-accessible tool finder (Alt+K), guided content-type and publishing forms, local JSON file selection and readable reviews. Enhancements retain the existing nonce-protected server handlers; selecting a file alone does not import it.

The visual builder supports adding, removing, reordering, duplicating, finding, and collapsing fields, with quick presets for common types. Configure labels, stable keys, metadata names, instructions, defaults, required flags, choices, constraints, and conditional visibility. Duplication creates a new field identity and storage name; it does not copy content. JavaScript is needed for the interactive builder, media modal, condition selectors, and recovery interface. Existing compatible definitions can also be edited through validated JSON.

### Twenty core field types

| Type | Value and behavior |
| --- | --- |
| `text`, `textarea` | Plain text; optional minimum/maximum character limits. |
| `wysiwyg` | WordPress rich-text editor; HTML restricted with `wp_kses_post`, including for administrators. Length limits count text rather than markup. |
| `number` | Finite floating-point value, optional `min`, `max`, and positive `step` or `any`. Not an arbitrary-precision financial type. |
| `email`, `url` | Valid email or absolute HTTP/HTTPS URL; optional character limits. |
| `select`, `radio` | One configured choice value, or an optional empty value. |
| `multiselect`, `checkboxes` | An array of distinct configured choice values. Defaults in the builder use one value per line. |
| `checkbox` | One boolean, distinct from checkbox choices. Schema defaults use `1` or `0`. |
| `date` | Real calendar date, `YYYY-MM-DD`. |
| `time` | 24-hour local time, `HH:MM`. |
| `datetime` | Local date/time, `YYYY-MM-DDTHH:MM`; no timezone conversion or seconds. |
| `color` | Optional six-digit hex value, such as `#3366ff`. |
| `image`, `file` | Existing attachment ID, with a Media Library selector. Images must be image attachments. Clearing a field does not delete its attachment. |
| `post` | Existing post ID restricted to configured `post_types` and the editor's read permissions. |
| `user` | Existing user ID. Listing other users requires `list_users`; an editor can select their own user ID. |
| `taxonomy` | Existing term ID restricted to a configured `taxonomy` and its assign-terms capability. |

Reference selectors show at most 100 permitted items plus the current selection; they are not an AJAX search interface for a large catalog. Referenced objects and attachments must exist when values are validated. Stored references that become unavailable are protected from being silently replaced. The media modal requires WordPress media permissions.

### Locations and conditions

Core targets are `post`, `user`, and `term`. Post groups require `post_types` and can optionally restrict `post_ids` and `page_templates`. An empty filter matches all posts of the selected types; supplied filters must both match. Template values are relative filenames or `default`. Matching uses the object's saved location; save a changed template/location and reload before editing newly applicable groups.

User groups appear on permitted user profile/edit screens. Term groups require an explicit `taxonomies` list and appear on existing term edit screens. Adding a term first, then editing it, makes its field panel available. Attachment editors, comments, menus, and network settings are not supported targets.

Free conditional visibility supports up to 20 same-group rules, matching `all` or `any`, with `==`, `!=`, `contains`, `empty`, `!empty`, `>`, and `<`. The visual selector refers to field names. Existing Pro beta flat AND rules remain valid: omit `condition_relation` to use `all`. Collection equality/contains means membership; `empty` means an empty string or collection, so `0` and unchecked boolean `0` are not empty. Cycles and unavailable controllers are rejected. Missing or malformed controller submissions leave fields active for validation. Hidden fields retain their saved values. Visibility is not access control.

## Display and PHP helpers

Insert the **Custom field** block and choose a matching registered field. It renders saved text, rich text, images, file links, and multiple-choice labels. The block checks post visibility and password protection, and validates stored shapes before rendering. This existing block remains a dynamic output block. The separate **Blocks & listings** workspace can now author named Gutenberg blocks with bounded scalar attributes and fixed safe layouts; it does not execute template PHP/JavaScript.

The registered `customfields/value` block-binding source is available when WordPress supports the bindings API. Supported output attributes are `content`, `url`, `title`, and `alt`; an image field can supply its attachment URL. Binding configuration is for developers and uses `args.name` to identify the field.

```php
if ( function_exists( 'customfields_get' ) ) {
    echo esc_html( customfields_get( 'project_client', get_the_ID() ) );
    echo wp_get_attachment_image( (int) customfields_get( 'project_image' ), 'large' );
    echo wp_kses_post( customfields_get( 'project_description' ) );
}

// Trusted template code must authorize any user/term values it publishes.
echo esc_html( customfields_get_object( 'profile_title', 'user', $user_id, '' ) );
echo esc_html( customfields_get_object( 'category_note', 'term', $term_id, '' ) );
```

`customfields_get($name, $post_id = 0)` only reads registered fields whose current group locations match the post. An absent metadata row returns its default; an existing empty value remains empty. Boolean fields return booleans, and numeric post fields return floats or `''`. Multiple choices return arrays; references are IDs and should be cast to integers when passed to WordPress APIs. WordPress may return stored scalar IDs as strings. Missing/unregistered fields return `null`.

`customfields_get_object($name, $kind, $object_id, $fallback = null)` supports `user` and `term`, checks configured group/taxonomy matching, and returns the fallback when no applicable field exists. These PHP helpers are trusted template APIs, not permission-enforcing public endpoints: authorize reads and escape output for its context.

The shortcode `[customfield name="project_client" post_id="123"]` returns escaped scalar text. The ID is optional. It checks post visibility/password protection and cannot access unregistered metadata. It does not render rich HTML or arrays.

## Editing, validation, and recovery

Supported editor submissions validate values and permissions on the server. Classic post forms use per-group nonces and completeness markers; the block editor sends a registered `customfields` REST attribute. A stale form missing an active field, an invalid value, or an incompatible stored value prevents that group's updates. Group validation happens before writes. Ordinary native-metadata groups retain WordPress update hooks and do not promise transaction rollback after a database failure. Opted-in custom-table groups and reciprocal relationships use the protected ContentStore transaction. Other valid groups or ordinary post fields can still save.

Within the plugin's supported Classic/Gutenberg editing flow, invalid submitted fields block a new publish/schedule transition. An already published post is not automatically unpublished. Browser controls provide immediate validation where available; server checks cover constraints such as checkbox-group requirements and rich-text emptiness. This is not a site-wide publication approval system: unrelated REST clients, imports, direct metadata calls, and requests without the plugin payload do not become subject to a global publication gate.

After valid edits, the post editor saves a recovery draft through an authenticated, nonce-protected request. Drafts are local to the current user and post, last up to 24 hours, and are offered only while the schema fingerprint and current permissions match. Recovery stores valid groups only; invalid groups are excluded and reported. Restoring a draft repopulates the editor for review. Saving removes matching saved groups from the recovery draft; different, newer, or unsaved groups remain available. This supplements ordinary WordPress autosaving; it is not a promise to recover every invalid keystroke. There are no recovery drafts for user or term forms.

Post revisions include registered field snapshots, including field-only edits while WordPress revisions are enabled for that post type. Restoring a revision verifies its parent, current field identities, compatibility, and edit permissions before changing field metadata. Changed names or absent snapshot identities are not mapped automatically. Incompatible values or permissions block the field restoration and produce a notice. Disabling WordPress revisions disables creation of these field revisions. User and term fields do not have this revision integration.

### REST contract

A `customfields` object is registered on REST-enabled post types in authenticated `edit` context. Its shape is `group_key -> field_name -> value`. Reading requires permission to edit the post and individual metadata; writing validates applicable groups, fields, and metadata capabilities. It is not public REST exposure. Save a post draft first to obtain its ID before writing these fields through REST.

A read-only `customfields_contexts` object accompanies permitted editor values when structured local fields are available. Its opaque fingerprints and original row identities keep hidden structured values attached to the correct rows across AJAX saves and recovery. Custom integrations should use the matching editor transport or retain these contexts when submitting conditional/reordered composites.

For each included group, send every active editable field. Omitted groups are unchanged. Unknown/unavailable groups are rejected. Integrations that omit the `customfields` attribute are outside this validation path. Other plugins can independently register or expose metadata.

## Storage, JSON, and developer workflows

The non-autoloaded `customfields_groups` option contains field definitions. Content normally uses native post, user, or term metadata under the field name. Optional custom-table storage is authoritative and keeps a mandatory transactional native-metadata compatibility mirror. Names must match `[a-z][a-z0-9_]{0,63}` and be unique across groups for the same target. Protected underscore-prefixed WordPress keys cannot be configured. User-profile top-level account-control keys (roles, levels, sessions and application passwords) are also rejected; legacy definitions cannot expose them through the object helper or edit path.

```json
{
  "format": "customfields",
  "schema_version": 1,
  "groups": [{
    "key": "group_project",
    "title": "Project details",
    "target": "post",
    "post_types": ["post", "page"],
    "post_ids": [],
    "page_templates": [],
    "fields": [{
      "key": "field_project_client",
      "name": "project_client",
      "label": "Client",
      "type": "text",
      "required": false,
      "instructions": "Use the public organization name.",
      "choices": {},
      "default": "",
      "maxlength": 200,
      "conditional_logic": [],
      "condition_relation": "all"
    }]
  }]
}
```

`Schema::validate_groups`, `validate_group`, and `validate_field($field, $depth = 0)` return normalized arrays or `WP_Error`. New keys can be generated; existing stable keys survive exports. Limits include 1 MB/import, 100 groups, 200 fields/group, 200 choices/field, and 100 KB/scalar field value. Server request limits can be smaller. Dates, finite numbers, limits, defaults, choice membership, IDs, rule dependencies, and extension settings are checked.

The administrator import UI accepts only the versioned CustomFields.com envelope. It merges by group key, previews additions/replacements, and applies after checking preview identity and the current schema fingerprint. Unmentioned groups and all content values remain. Export preserves definitions, including unavailable extension settings. An incompatible stored schema is read-only; registry-wide validation can block other schema writes until the required extension is reactivated.

Use **Definition files → Download JSON** to save a schema copy through your browser. Schema saves and imports do not write files on the server. No uploads directory, server rule or configuration edit is needed.

An existing `CUSTOMFIELDS_JSON_DIR` setting remains supported as a **read-only legacy import source**. The workspace reads `customfields-schema.json` (maximum 1 MB) and shows a semantic review before replacing the registry. Applying rechecks the exact file bytes, database schema and affected saved values. File paths with symlink components and special files are rejected. Keep the source private and publish external changes atomically. There is no automatic export, background watcher, silent import or PHP execution. Existing source files are never changed by this plugin.

WP-CLI provides explicit portability commands:

```bash
wp customfields schema export
wp customfields schema check customfields-schema.json
wp customfields schema import customfields-schema.json
wp customfields schema import customfields-schema.json --apply --expected-hash=HASH_FROM_CHECK --user=ADMINISTRATOR_LOGIN
```

`check` and an import without `--apply` validate and print the current hash without changing data. **An applied CLI import replaces the complete registry**, unlike the administrator UI's merge. Review the full file and use the exact reported current hash; applying requires a user with `manage_options`. Content metadata is retained even when definitions are removed. Local file import is explicitly reviewed; CLI import is deliberate. Neither is automatic content conversion.

Removing definitions, deactivating, or uninstalling preserves stored content intentionally. Changing a storage name does not move old values. Incompatible stored types, removed choices, and other schema conflicts stay locked until the definition is restored or the data is deliberately migrated. Content moves only through explicit, reviewed value-conversion workflows; saving definitions does not migrate or rename content.

## Extension contract

Extensions are trusted PHP. They must escape rendered output, validate owned settings, and preserve errors for types they do not own.

| Hook | Arguments | Contract |
| --- | --- | --- |
| `customfields_field_types` filter | type/label map | Add supported field types. |
| `customfields_group_targets` filter | target/label map | Free supplies `post`, `user`, `term`, and `options`. |
| `customfields_validate_field_schema` filter | normalized field, raw field, depth | Normalize owned settings or return `WP_Error`. |
| `customfields_supported_field_settings` filter | setting names, normalized field, raw field | Declare only settings an active extension understands. |
| `customfields_validate_group_schema` filter | normalized group, raw group | Validate target configuration and relationships. |
| `customfields_supported_group_settings` filter | setting names, normalized group, raw group | Declare understood group-level extension settings. |
| `customfields_sanitize_value` filter | core result, field, raw value, context | Replace the result only for owned types; required validation follows. |
| `customfields_storage_error` filter | error/null, stored value, field | Return `WP_Error` to lock incompatible stored content. |
| `customfields_render_field` action | field, value, input name | Render an extension input, not a form. |
| `customfields_after_render_field` action | field, value, input name | Add per-input metadata; Free uses it for conditions. |
| `customfields_field_editor_settings` action | field, input prefix | Add bracket-named field builder controls. |
| `customfields_group_editor_settings` action | group | Add group builder controls. |
| `customfields_field_is_active` filter | active, field, submitted values, object ID | False preserves an inactive field; never grants authorization. |
| `customfields_group_saved` action | group, post ID, sanitized updates | Runs after a post group's valid updates are written. |
| `customfields_schema_saved` action | new groups, previous groups | Runs after supported schema writes; used for export and local history. |
| `customfields_get_value` filter | value, field, post ID | Formats a registered post field for PHP retrieval. |
| `customfields_loaded` action | none | Core hooks are registered. |

`Fields::sanitize` returns a value or `WP_Error`. Internal `_validating_default` and `_checking_storage` flags are for trusted schema/storage code only; never derive them from submitted input. `_checking_storage` checks structure/existence without treating a current user's changed reference permissions as a data-format change.

`Fields::render` outputs an input. The wrapper is `.customfields-field[data-customfields-name="name"]`; standard input IDs are `cf-` plus the first 16 hex characters of `md5($input_name)`. Rich editors substitute underscores because WordPress editor IDs cannot contain dashes. Descriptions use the standard ID plus `-description`.

The builder dispatches `customfields:field-added` with `.detail.field` and `.detail.card`. Runtime controls can dispatch `customfields:rows-changed` after structured edits so conditions/recovery refresh. Reordering rewrites form names; extensions should maintain their own unique control IDs. Unknown nonempty field/group settings fail closed rather than disappearing.

## Tests and boundaries

These maintainer-only examples require a separate checkout containing the test suite; `tests/` is not included in the installation ZIP. Run the suites sequentially against an installed disposable WordPress site with the matching plugin code active:

```bash
wp --path=/path/to/wordpress eval-file /path/to/maintainer-checkout/customfields/tests/integration.php
wp --path=/path/to/wordpress eval-file /path/to/maintainer-checkout/customfields/tests/features-fields.php
wp --path=/path/to/wordpress eval-file /path/to/maintainer-checkout/customfields/tests/editor-integration.php
```

The suites create temporary users, posts, terms, metadata, and schemas and restore their fixtures in cleanup. They cover schema portability, scalar/collection validation, media/reference permissions, conditions, preservation locks, editor/REST authorization, display escaping, and recovery/revisions. Do not run them concurrently with other schema mutations or on a live site.

Free works without an account or external service. Optional domain-only registration and separately confirmed email signup require explicit unchecked choices; see readme.txt for the exact data, retention and withdrawal controls. There are no automatic registration or status requests. Local request records remain until the choice is removed; a matching WordPress personal-data erasure can also remove the independent email choice. Remote expiry does not erase them locally. Use Remove site registration or Stop these emails. A failed removal clears the selected local payload while retaining protected retry controls; it never renews enrollment consent. Local Pro workflow intent expires after seven days; opening previews creates no intent or network request. Authenticated local forms, searches and recovery use the site’s own WordPress endpoints. Display/forms enqueue their own local assets, and optional dedicated storage creates a local values table. WordPress and separately installed builders may make their normal network requests. Comments/menu/network field targets, arbitrary precision numbers, complete competitor migrations and universal builder compatibility remain outside the current supported scope.

## Additional Free 0.5 capabilities

- **Structured content and options:** repeater, group, gallery, named flexible layouts and independent or linked reusable groups. Named options pages delegate permitted value editing while schemas remain administrator-owned. See [local feature details](docs/LOCAL-FEATURES.md) for bounds, preserved formats and conditional row identities.
- **Frontend forms:** authenticated edits of an existing post and creation of an owned draft with title, complete validation, protected transaction and duplicate-retry receipt. See [form integration](docs/FRONTEND-FORMS.md).
- **Content types:** administrator UI for owned CPT/taxonomy definitions; changing definitions does not migrate existing post types or taxonomy content. Reserved/internal registrations and name collisions are rejected.
- **Searchable relationships:** ordered collections of permitted post/user/term IDs, 1–100 maximum items, AJAX search and optional explicitly paired inverse fields. Reciprocal updates require permissions and compatible top-level owner fields; no arbitrary nested inverse graph. Existing single-reference field types retain their simpler selectors.
- **Optional dedicated storage:** explicitly provision the values table, then opt a post/user/term group into custom-table storage. A generation token prevents old table rows resurfacing after switching back and re-enabling. A mandatory metadata mirror supports ordinary WordPress queries, revisions and fallback. This is not a claim of reduced metadata storage or benchmarked large-scale performance.
- **Named block authoring (Free) and listing authoring (Pro):** configure a named block with scalar controls and fixed card/stack/banner layouts; with activated Pro, configure a public-content listing with bounded filters, sorting, cards and pagination, then insert its Gutenberg block or `[customfields_listing id="saved_slug"]`. Queries exclude inaccessible/password-protected content. The layouts are fixed renderers, not user-authored PHP/JavaScript or an unrestricted SQL/query builder.
- **Elementor connectors:** field and listing widgets plus a scalar dynamic-tag connector load only when Elementor supplies the host APIs. Elementor is a separate dependency; dynamic-tag availability follows the installed host edition. This does not establish compatibility with Bricks, Divi, Oxygen or every Elementor release.
- **History and conversion:** bounded schema snapshots and reviewed restoration; copy registered native post/user/term/options values, or convert supported active ACF/SCF and Meta Box sources to a separately named destination. ACF/SCF supports bounded group/repeater/flexible layouts, galleries, mapped scalar/date fields, single and ordered multiple post/user/term references, modern user/term metadata and explicitly registered default or custom-prefix options pages. Meta Box supports registered standard post/user/term metadata, repeated collection rows, serialized and `clone_as_multiple` scalar clones, Group/clone-Group structures, references and supported declared numeric date/time formats. Scalar clones map to a repeater child named `value`. A narrow MB Custom Table adapter binds one applicable active group to its dedicated InnoDB table, integer primary ID and direct field-ID column; no arbitrary table names or custom models. Paid MB Group/Custom Table qualification remains contract-fixture evidence, separate from actual core-plugin tests. Source rows stay intact; guarded rollback preserves later destination edits. Options journal the whole shared native container and refuse sibling changes. The conservative ACF JSON schema importer remains separate.

Local implementations live under `CustomFields\Local`; normal 0.5 boots provide legacy `CustomFieldsPro` class aliases. Persisted `customfields_pro_values`, schema history/journal options, actions, transport prefixes and public helper names intentionally retain their existing names. Those names do not imply a paid requirement. A mixed old-Pro/new-Free upgrade suppresses the old Pro bootstrap only; existing stored data is not converted during activation.

For extension writers, `Fields::sanitize($value,$field,$context=array())` forwards the fourth hook argument with trusted `has_stored_value`, `stored_value`, `object_kind` and `object_id` context. Composite inputs also carry original-row identities and opaque fingerprints; never manufacture them from untrusted stored-object identifiers. `customfields_metadata_transaction_written($group,$kind,$id,$updates,$before_raw_rows)` runs before supported direct-SQL commits so local storage and reciprocal updates join the transaction. Exceptions abort it. These internal hooks are trusted PHP integration points, not public write endpoints.


## Additional migration formats in 0.7

The guided workflow separates source/destination selection, content selection, exact review and history. Search and pagination change only the visible rows, never the operation scope. Native forms still work without JavaScript.

- ACF/SCF Clone uses the active vendor-resolved definition, including supported group/seamless and name-prefix modes. Destination groups use original unprefixed child names. Cycles, ambiguous definitions and unsupported descendants are refused.
- Link values use a Group with `url` (URL), `title` (text) and `target` (text or compatible select). Unknown properties or changed values are refused.
- Google Map values use a matching Group whose keys preserve the stored properties. `lat`, `lng` and `zoom` use Number fields; address/name details use text. Unknown keys, invalid shapes or numeric precision loss are refused. This transfers stored data without requesting a map service.
- Custom option prefixes must belong to a registered ACF/SCF options page. Arbitrary option names are not accepted.
- Meta Box Date/Datetime timestamp storage follows the vendor's UTC semantics and empty-zero behavior. Time's actual core normalizer disables timestamps; timestamp-Time support is not claimed. Taxonomy fields with `load_terms` read the effective relationships and stop if those relationships change after review.

Paid Meta Box Group/Custom Table extension binaries were unavailable. Existing contract fixtures are not actual paid-extension integration tests. MB Settings Page containers, arbitrary custom models, unsupported vendor formats, templates and reciprocal relationship graphs remain outside the adapters. Synthetic qualification sites are not customer-site evidence.

## Local Pro discovery

Explore Pro includes labelled fictional workflow examples. Trying an example does not save content or make an external request. Suggestions and preview visibility can be changed per administrator and site. Choosing View Pro plans remembers the selected workflow locally for seven days and opens the pricing page; opening a preview alone records no purchase intent. Installed customers see Activate your licence or Continue your task as appropriate. Marketing choices remain separate, and all existing Free capabilities remain available.

## Use your fields and local diagnosis

Use your fields carries the selected group and record through read-only navigation without requiring a saved goal. It filters the record selector through WordPress permissions and the group's locations, opens the permitted native editor, and previews real saved values with the matching output instructions. Nothing is created or published by the guide. Existing local task preferences remain supported, and group creation remains directly accessible.

Compare a CSV provides useful read-only analysis without the Pro add-on. Its supported row outcomes, issues, exports and manual editor links stay available. A retained task can be opened after installing Pro, which rechecks source, schema and values before separate execution approval. No read-only result grants content-write authorization.

Content health captures explicit bounded scopes, checks required values/configured choices/known references, and keeps every supported finding visible with manual links. Additional scopes remain Free. Nested/conditional fields and unsupported storage are labelled rather than called healthy. Definition baselines compare field groups and known top-level consumers; external templates and nested consumers remain unknown. Retained native-value counts are bounded observations, not proof a field is safe to remove.

Optional workflow measurement starts off. Enabling it retains only allowlisted event counts by UTC day for this administrator/site, up to 90 active days. Export or clear them from Use your fields. It makes no external request and cannot establish purchases or a Free-to-Pro conversion rate. Programmatic completion hooks are approximate observations, not financial evidence.

## Translation files

The `languages/customfields-com.pot` template includes PHP and JavaScript interface strings. JavaScript uses the WordPress i18n package and standard per-script translation catalogs. Create locale PO/MO files and generate the matching JSON catalogs with `wp i18n make-json`; keep the source-file references so WordPress can identify each script. No community language pack is bundled. User-authored labels, values and saved identifiers remain unchanged by interface translation.

## Source code and development

The readable JavaScript and CSS in `assets/` are edited and loaded directly; there is no compilation or bundling step. See [SOURCE.txt](SOURCE.txt) for the source map, development and packaging workflow, optional checks, translations and dependency attribution.
