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' ) . '

'; } /** 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 ) : ''; } }