admin_url( 'admin-ajax.php' ),
'saveAction' => 'customfields_builder_save',
'reservedNames' => $reserved[ $target ] ?? array(),
'reservedNamesByTarget' => $reserved,
'types' => Fields::types(),
'isNew' => 'new' === $edit,
'propertyLabels' => array(
'label' => __( 'Field label', 'customfields-com' ),
'name' => __( 'Storage key', 'customfields-com' ),
'type' => __( 'Field type', 'customfields-com' ),
'required' => __( 'Required', 'customfields-com' ),
'default' => __( 'Default value', 'customfields-com' ),
'instructions' => __( 'Help text', 'customfields-com' ),
'choices' => __( 'Choices', 'customfields-com' ),
),
'removeField' => __( 'Remove this field from the schema? Existing content values will be kept.', 'customfields-com' ),
'newField' => __( 'Untitled field', 'customfields-com' ),
'unsaved' => __( 'You have unsaved field changes.', 'customfields-com' ),
'moved' => __( 'Field order updated.', 'customfields-com' ),
'saving' => __( 'Saving…', 'customfields-com' ),
'saved' => __( 'All changes saved', 'customfields-com' ),
'saveFailed' => __( 'We could not save this group. Your changes are still here. Please try again.', 'customfields-com' ),
) );
}
}
public static function meta_boxes( $post_type, $post ) {
if ( ! $post || ! current_user_can( 'edit_post', $post->ID ) ) { return; }
foreach ( customfields_get_groups() as $group ) {
if ( empty( $group['fields'] ) ) { continue; }
if ( ! Editor::matches( $group, $post ) ) { continue; }
add_meta_box( 'customfields-' . $group['key'], $group['title'], array( __CLASS__, 'render_group' ), $post_type, 'normal', 'default', array( 'group' => $group, '__block_editor_compatible_meta_box' => true ) );
}
}
public static function render_group( $post, $box ) {
$group = $box['args']['group'];
wp_nonce_field( 'customfields_save_' . $post->ID . '_' . $group['key'], 'customfields_nonce[' . $group['key'] . ']', false );
echo '
';
foreach ( $group['fields'] as $field ) {
if ( ! Editor::can_edit( 'post', $post->ID, $field['name'] ) ) { continue; }
$input_name = 'customfields_values[' . $group['key'] . '][' . $field['name'] . ']';
$id = 'cf-' . substr( md5( $input_name ), 0, 16 );
// Editors work with raw storage, not template formatting filters or helper type coercion.
$value = metadata_exists( 'post', $post->ID, $field['name'] ) ? get_post_meta( $post->ID, $field['name'], true ) : ( $field['default'] ?? '' );
echo '
';
echo '
';
Fields::render( $field, $value, $input_name );
if ( $field['instructions'] ) {
echo '
' . esc_html( $field['instructions'] ) . '
';
}
echo '
';
}
echo '
';
do_action('customfields_editor_next', $group, 'post', (int) $post->ID);
}
public static function save_post( $post_id, $post ) {
if ( ! $post || wp_is_post_revision( $post_id ) || wp_is_post_autosave( $post_id ) || ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ! current_user_can( 'edit_post', $post_id ) ) { return; }
// These nonces are checked per group. Requests without our metabox payload never change fields.
if ( ! isset( $_POST['customfields_nonce'] ) || ! is_array( $_POST['customfields_nonce'] ) ) { return; }
$payload = isset( $_POST['customfields_values'] ) && is_array( $_POST['customfields_values'] ) ? wp_unslash( $_POST['customfields_values'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Complete structured payload is validated per field by validate_values/Fields::sanitize; nonces use wp_verify_nonce and completion markers require exact equality. Generic text sanitation would corrupt rich/nested values.
$complete = isset( $_POST['customfields_complete'] ) && is_array( $_POST['customfields_complete'] ) ? wp_unslash( $_POST['customfields_complete'] ) : array(); // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Complete structured payload is validated per field by validate_values/Fields::sanitize; nonces use wp_verify_nonce and completion markers require exact equality. Generic text sanitation would corrupt rich/nested values.
$errors = array();
Editor::start_save( $post_id );
foreach ( customfields_get_groups() as $group ) {
$key = $group['key'];
if ( ! Editor::matches( $group, $post ) || ! isset( $_POST['customfields_nonce'][ $key ] ) || ! is_string( $_POST['customfields_nonce'][ $key ] ) ) { continue; }
$nonce = sanitize_text_field( wp_unslash( $_POST['customfields_nonce'][ $key ] ) );
if ( ! wp_verify_nonce( $nonce, 'customfields_save_' . $post_id . '_' . $key ) ) { continue; }
if ( ( $complete[ $key ] ?? null ) !== '1' || ! isset( $payload[ $key ] ) || ! is_array( $payload[ $key ] ) ) {
$errors[] = sprintf( /* translators: %s: Field group title. */ __( '%s: the submitted fields were incomplete. No values in this group were changed. Check your server max_input_vars setting for large forms.', 'customfields-com' ), $group['title'] );
continue;
}
$updates = Editor::validate_values( $group, $payload[$key], 'post', $post_id );
if ( is_wp_error( $updates ) ) {
$errors[] = $group['title'] . ': ' . implode( ' ', $updates->get_error_messages() );
continue;
}
Editor::before_save( $post_id );
if ( class_exists( ContentStore::class ) ) {
$saved = ContentStore::save_group( $group, 'post', $post_id, $updates );
if ( is_wp_error( $saved ) ) { $errors[] = $group['title'] . ': ' . $saved->get_error_message(); continue; }
} else {
foreach ( $updates as $name => $value ) { update_post_meta( $post_id, $name, wp_slash( $value ) ); }
}
do_action( 'customfields_group_saved', $group, $post_id, $updates );
}
if ( $errors ) { set_transient( 'customfields_errors_' . get_current_user_id() . '_' . $post_id, $errors, 120 ); }
}
public static function notices() {
$post_id = isset( $_GET['post'] ) && is_scalar( $_GET['post'] ) ? absint( $_GET['post'] ) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice.
if ( ! $post_id ) { return; }
$key = 'customfields_errors_' . get_current_user_id() . '_' . $post_id;
$errors = get_transient( $key );
if ( ! is_array( $errors ) ) { return; }
delete_transient( $key );
echo '' . esc_html__( 'Some custom fields were not saved. The post itself may have saved.', 'customfields-com' ) . '
';
foreach ( $errors as $error ) { echo '- ' . esc_html( $error ) . '
'; }
echo '
';
}
/** The shortcode only prints scalar, registered values on posts the reader can access. */
public static function shortcode( $attributes ) {
$attributes = shortcode_atts( array( 'name' => '', 'post_id' => 0 ), $attributes, 'customfield' );
if ( ! is_scalar( $attributes['post_id'] ) || ! is_string( $attributes['name'] ) ) { return ''; }
$post_id = $attributes['post_id'] ? absint( $attributes['post_id'] ) : get_the_ID();
$post = get_post( $post_id );
if ( ! $post || post_password_required( $post ) ) { return ''; }
if ( ! is_post_publicly_viewable( $post ) && ! current_user_can( 'read_post', $post_id ) ) { return ''; }
$value = customfields_get( $attributes['name'], $post_id );
if ( is_bool( $value ) ) { return $value ? '1' : '0'; }
return is_scalar( $value ) ? esc_html( (string) $value ) : '';
}
}