403 ) ); }
}
public static function fingerprint() {
return hash( 'sha256', wp_json_encode( customfields_get_groups() ) );
}
private static function check_fingerprint( $value ) {
if ( ! is_string( $value ) || ! hash_equals( self::fingerprint(), $value ) ) {
wp_die( esc_html__( 'The schema changed in another session. Reload the editor or import preview before saving to avoid overwriting that work.', 'customfields-com' ), '', array( 'response' => 409, 'back_link' => true ) );
}
}
private static function fail( $error ) {
wp_die( esc_html( is_wp_error( $error ) ? $error->get_error_message() : (string) $error ), esc_html__( 'Schema not saved', 'customfields-com' ), array( 'response' => 400, 'back_link' => true ) );
}
private static function redirect( $status, $extra = array() ) {
wp_safe_redirect( add_query_arg( array_merge( array( 'page' => 'customfields', 'cf_status' => $status ), $extra ), admin_url( 'admin.php' ) ) );
exit;
}
public static function handle_save() {
self::require_schema_access();
$nonce = isset( $_POST['_wpnonce'] ) && is_string( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'customfields_save' ) ) {
wp_die( esc_html__( 'Your session expired. Reload this page before continuing.', 'customfields-com' ), '', array( 'response' => 403 ) );
}
$saved = self::save_request( wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- save_request checks the unchanged schema proof, bounded complete-form shape, and Schema::validate_group/validate_groups before any write; generic text sanitation would corrupt structured defaults.
if ( is_wp_error( $saved ) ) {
$data = $saved->get_error_data();
wp_die( esc_html( $saved->get_error_message() ), esc_html__( 'Schema not saved', 'customfields-com' ), array( 'response' => (int) ($data['status'] ?? 400), 'back_link' => true ) );
}
self::redirect( 'saved', array( 'edit' => $saved['group']['key'] ) );
}
public static function ajax_save() {
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'code' => 'forbidden', 'message' => __( 'You cannot manage field schemas.', 'customfields-com' ) ), 403 );
}
$nonce = isset( $_POST['_wpnonce'] ) && is_string( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'customfields_save' ) ) {
wp_send_json_error( array( 'code' => 'expired_nonce', 'message' => __( 'Your editing session expired. Keep this page open, sign in again in another tab, then reload before saving.', 'customfields-com' ) ), 403 );
}
$saved = self::save_request( wp_unslash( $_POST ) ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- save_request checks the unchanged schema proof, bounded complete-form shape, and Schema::validate_group/validate_groups before any write; generic text sanitation would corrupt structured defaults.
if ( is_wp_error( $saved ) ) {
$data = $saved->get_error_data();
wp_send_json_error( array( 'code' => $saved->get_error_code(), 'message' => $saved->get_error_message() ), (int) ($data['status'] ?? 400) );
}
wp_send_json_success( array(
'group' => $saved['group'],
'schema_hash' => $saved['schema_hash'],
'edit_url' => add_query_arg( array( 'page' => 'customfields', 'edit' => $saved['group']['key'] ), admin_url( 'admin.php' ) ),
'message' => __( 'Field group saved. Your fields are ready to use.', 'customfields-com' ),
'use_url' => Outcomes::guide_url($saved['group']['key']),
) );
}
/** Both transports share the same complete-form and full-registry validation. */
private static function save_request( $request ) {
global $wpdb;
// Read the actual row, not a possibly stale object cache, before validating a whole-registry update.
$row = $wpdb->get_row( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", 'customfields_groups' ), ARRAY_A ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Read authoritative workflow/metadata state for validation and conflict detection; cached values can miss concurrent edits or duplicate rows.
if ( $wpdb->last_error ) { return new \WP_Error( 'schema_read', __( 'The field groups could not be read. Please try again.', 'customfields-com' ) ); }
$before = $row ? maybe_unserialize( $row['option_value'] ) : array();
if ( ! is_array( $before ) ) { return new \WP_Error( 'schema_storage', __( 'The saved field registry is not valid. Export or restore it before editing.', 'customfields-com' ) ); }
$before = array_values( $before );
$hash = $request['schema_hash'] ?? '';
if ( ! is_string( $hash ) || ! hash_equals( hash( 'sha256', wp_json_encode( $before ) ), $hash ) ) { return self::save_conflict(); }
if ( isset( $request['group_json'] ) ) {
$json = $request['group_json'];
if ( ! is_string( $json ) || strlen( $json ) > Schema::MAX_BYTES ) { return new \WP_Error( 'schema_size', __( 'The group exceeds the 1 MB schema limit.', 'customfields-com' ) ); }
$raw = json_decode( $json, true, 24 );
if ( JSON_ERROR_NONE !== json_last_error() ) { return new \WP_Error( 'schema_json', __( 'Invalid group JSON.', 'customfields-com' ) ); }
} else {
$raw = $request['group'] ?? array();
$json = wp_json_encode( $raw );
if ( false === $json || strlen( $json ) > Schema::MAX_BYTES ) { return new \WP_Error( 'schema_size', __( 'The group exceeds the 1 MB schema limit.', 'customfields-com' ) ); }
}
if ( ! is_array( $raw ) || ( $raw['complete'] ?? '' ) !== '1' ) { return new \WP_Error( 'schema_incomplete', __( 'The form was incomplete. Nothing was changed. Try fewer fields or increase the server max_input_vars setting.', 'customfields-com' ) ); }
// A colon can be part of a JSON choice key, but the native "value : Label" textarea cannot represent it.
foreach ( $before as $existing ) {
if ( ! is_array( $existing ) || ( $existing['key'] ?? null ) !== ( $raw['key'] ?? null ) || ! is_array( $raw['fields'] ?? null ) || ! is_array( $existing['fields'] ?? null ) ) { continue; }
foreach ( $raw['fields'] as $incoming_field ) {
if ( ! is_array( $incoming_field ) || ! is_string( $incoming_field['choices'] ?? null ) ) { continue; }
foreach ( $existing['fields'] ?? array() as $stored_field ) {
if ( ! is_array( $stored_field ) || ! is_array( $stored_field['choices'] ?? null ) ) { continue; }
if ( ( $stored_field['key'] ?? null ) !== ( $incoming_field['key'] ?? null ) && ( $stored_field['name'] ?? null ) !== ( $incoming_field['name'] ?? null ) ) { continue; }
foreach ( array_keys( $stored_field['choices'] ?? array() ) as $choice_key ) {
if ( false !== strpos( (string) $choice_key, ':' ) ) {
return new \WP_Error( 'schema_choice_text', __( 'This group contains choice values with colons. Use the JavaScript choice editor to preserve them. The text-only form cannot safely represent these values, so nothing was saved.', 'customfields-com' ) );
}
}
}
}
}
$group = Schema::validate_group( $raw );
if ( is_wp_error( $group ) ) { return $group; }
$groups = $before;
$found = false;
foreach ( $groups as $index => $existing ) {
if ( $existing['key'] === $group['key'] ) { $groups[ $index ] = $group; $found = true; break; }
}
if ( ! $found ) { $groups[] = $group; }
$valid = Schema::validate_groups( $groups );
if ( is_wp_error( $valid ) ) { return $valid; }
$written = self::compare_and_write( $valid, $before, $row );
if ( is_wp_error( $written ) ) { return $written; }
// Bind the next browser request to what this save wrote, even if another writer follows immediately.
return array( 'group' => $group, 'schema_hash' => hash( 'sha256', wp_json_encode( $valid ) ) );
}
private static function save_conflict() {
return new \WP_Error( 'schema_conflict', __( 'The schema changed in another session. Reload the editor before saving to avoid overwriting that work.', 'customfields-com' ), array( 'status' => 409 ) );
}
/** Atomic browser saves; public write() remains available to existing transaction-owning workflows. */
private static function compare_and_write( $groups, $before, $row ) {
global $wpdb;
$name = 'customfields_groups';
$groups = array_values( $groups );
$serialized = maybe_serialize( $groups );
if ( $row ) {
do_action( 'update_option', $name, $before, $groups ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Intentionally emit the existing WordPress option lifecycle hook after the atomic option write.
$changed = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND BINARY option_value = BINARY %s", $serialized, $name, $row['option_value'] ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Atomic workflow write with explicit formats/prepared values; surrounding transaction performs cache invalidation after commit.
} else {
do_action( 'add_option', $name, $groups ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Intentionally emit the existing WordPress option lifecycle hook after the atomic option write.
// INSERT IGNORE never replaces a row created after the initial read.
$changed = $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO {$wpdb->options} (option_name, option_value, autoload) VALUES (%s, %s, 'no')", $name, $serialized ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Atomic workflow write with explicit formats/prepared values; surrounding transaction performs cache invalidation after commit.
}
wp_cache_delete( $name, 'options' );
wp_cache_delete( 'alloptions', 'options' );
wp_cache_delete( 'notoptions', 'options' );
if ( false === $changed ) { return new \WP_Error( 'schema_write', __( 'The field group could not be saved to the database. Your changes are still in this editor. Please try again.', 'customfields-com' ) ); }
if ( 1 !== $changed ) {
// MySQL reports zero affected rows for an unchanged value; verify that it is still the reviewed row.
if ( ! $row || $serialized !== $row['option_value'] ) { return self::save_conflict(); }
$current = $wpdb->get_var( $wpdb->prepare( "SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", $name ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Read authoritative workflow/metadata state for validation and conflict detection; cached values can miss concurrent edits or duplicate rows.
if ( $wpdb->last_error ) { return new \WP_Error( 'schema_read', __( 'The saved field group could not be verified. Reload before trying again.', 'customfields-com' ) ); }
if ( $current !== $serialized ) { return self::save_conflict(); }
} elseif ( $row ) {
do_action( 'update_option_' . $name, $before, $groups, $name ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Intentionally emit the existing WordPress option lifecycle hook after the atomic option write.
do_action( 'updated_option', $name, $before, $groups ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Intentionally emit the existing WordPress option lifecycle hook after the atomic option write.
} else {
do_action( 'add_option_' . $name, $name, $groups ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Intentionally emit the existing WordPress option lifecycle hook after the atomic option write.
do_action( 'added_option', $name, $groups ); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Intentionally emit the existing WordPress option lifecycle hook after the atomic option write.
}
do_action( 'customfields_schema_saved', $groups, $before );
return true;
}
/** Schema writes never delete, rename, or migrate content metadata. */
public static function write( $groups ) {
$before = customfields_get_groups();
update_option( 'customfields_groups', array_values( $groups ), false );
do_action( 'customfields_schema_saved', $groups, $before );
}
public static function handle_delete() {
self::require_schema_access();
$nonce = isset( $_POST['_wpnonce'] ) && is_string( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'customfields_delete' ) ) {
wp_die( esc_html__( 'Your session expired. Reload this page before continuing.', 'customfields-com' ), '', array( 'response' => 403 ) );
}
self::check_fingerprint( isset( $_POST['schema_hash'] ) && is_string( $_POST['schema_hash'] ) ? filter_var( wp_unslash( $_POST['schema_hash'] ), FILTER_VALIDATE_REGEXP, array( 'options' => array( 'regexp' => '/^[a-f0-9]{64}$/D' ) ) ) : '' );
$key = isset( $_POST['group_key'] ) && is_string( $_POST['group_key'] ) ? sanitize_text_field(wp_unslash( $_POST['group_key'] )) : '';
$groups = array_values( array_filter( customfields_get_groups(), static function( $group ) use ( $key ) { return $group['key'] !== $key; } ) );
self::write( $groups );
self::redirect( 'deleted' );
}
public static function handle_export() {
self::require_schema_access();
$nonce = isset( $_GET['_wpnonce'] ) && is_string( $_GET['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_GET['_wpnonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'customfields_export' ) ) {
wp_die( esc_html__( 'Your session expired. Reload this page before continuing.', 'customfields-com' ), '', array( 'response' => 403 ) );
}
nocache_headers();
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="customfields-schema.json"' );
header( 'X-Content-Type-Options: nosniff' );
echo Schema::encode( customfields_get_groups() ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- JSON download, not HTML.
exit;
}
public static function handle_preview() {
self::require_schema_access();
$nonce = isset( $_POST['_wpnonce'] ) && is_string( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'customfields_preview' ) ) {
wp_die( esc_html__( 'Your session expired. Reload this page before continuing.', 'customfields-com' ), '', array( 'response' => 403 ) );
}
$incoming = Schema::decode( isset( $_POST['schema_json'] ) && is_string( $_POST['schema_json'] ) ? wp_unslash( $_POST['schema_json'] ) : '' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Schema::decode bounds the JSON and immediately runs Schema::validate_groups, which sanitizes labels, defaults and every typed field; text sanitation would corrupt schema content.
if ( is_wp_error( $incoming ) ) { self::fail( $incoming ); }
$by_key = array();
foreach ( customfields_get_groups() as $group ) { $by_key[ $group['key'] ] = $group; }
$changes = array();
foreach ( $incoming as $group ) {
$changes[] = array( 'title' => $group['title'], 'operation' => isset( $by_key[ $group['key'] ] ) ? 'replace' : 'add', 'fields' => count( $group['fields'] ) );
$by_key[ $group['key'] ] = $group;
}
$merged = Schema::validate_groups( array_values( $by_key ) );
if ( is_wp_error( $merged ) ) { self::fail( $merged ); }
$token = wp_generate_password( 32, false, false );
set_transient( 'customfields_import_' . get_current_user_id(), array( 'groups' => $merged, 'changes' => $changes, 'hash' => self::fingerprint(), 'token' => $token ), 15 * MINUTE_IN_SECONDS );
self::redirect( 'preview', array( 'preview_token' => $token ) );
}
public static function handle_import() {
self::require_schema_access();
$nonce = isset( $_POST['_wpnonce'] ) && is_string( $_POST['_wpnonce'] ) ? sanitize_text_field( wp_unslash( $_POST['_wpnonce'] ) ) : '';
if ( ! wp_verify_nonce( $nonce, 'customfields_import' ) ) {
wp_die( esc_html__( 'Your session expired. Reload this page before continuing.', 'customfields-com' ), '', array( 'response' => 403 ) );
}
$preview = get_transient( 'customfields_import_' . get_current_user_id() );
if ( ! is_array( $preview ) ) { self::fail( __( 'The preview expired. Validate the JSON again.', 'customfields-com' ) ); }
if ( ! self::preview_matches( $preview, isset( $_POST['preview_token'] ) && is_string( $_POST['preview_token'] ) ? filter_var( wp_unslash( $_POST['preview_token'] ), FILTER_VALIDATE_REGEXP, array( 'options' => array( 'regexp' => '/^[a-zA-Z0-9]{32}$/D' ) ) ) : '' ) ) {
self::fail( __( 'This import preview was replaced in another tab. Review and validate the intended JSON again.', 'customfields-com' ) );
}
self::check_fingerprint( $preview['hash'] );
// Revalidate because an extension could have been disabled after the preview.
$groups = Schema::validate_groups( $preview['groups'] );
if ( is_wp_error( $groups ) ) { self::fail( $groups ); }
self::write( $groups );
delete_transient( 'customfields_import_' . get_current_user_id() );
self::redirect( 'imported' );
}
public static function page() {
if ( ! current_user_can( 'manage_options' ) ) { return; }
$edit = isset( $_GET['edit'] ) && is_string( $_GET['edit'] ) ? sanitize_text_field( wp_unslash( $_GET['edit'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$status = isset( $_GET['cf_status'] ) && is_string( $_GET['cf_status'] ) ? sanitize_key( wp_unslash( $_GET['cf_status'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$messages = array( 'saved' => __( 'Field group saved. Existing content values were kept.', 'customfields-com' ), 'deleted' => __( 'Field group removed. Stored content values were kept.', 'customfields-com' ), 'imported' => __( 'Schema imported. Content values were not changed.', 'customfields-com' ) );
echo '
';
if ( isset( $messages[ $status ] ) ) { echo '
' . esc_html( $messages[ $status ] ) . '
'; }
if ( $edit ) {
$group = array( 'key' => 'group_' . wp_generate_uuid4(), 'title' => '', 'target' => 'post', 'post_types' => array( 'post' ), 'fields' => array() );
$found = 'new' === $edit;
foreach ( customfields_get_groups() as $existing ) { if ( $existing['key'] === $edit ) { $group = $existing; $found = true; break; } }
if ( $found ) { self::editor( $group ); }
else { echo '
' . esc_html__( 'The requested group does not exist.', 'customfields-com' ) . '
'; self::listing(); }
} else {
self::listing();
self::import_panel( $status );
}
echo '
';
}
private static function listing() {
do_action('customfields_dashboard_top');
$groups = customfields_get_groups();
$new_url = admin_url( 'admin.php?page=customfields&edit=new' );
echo '';
echo '
' . esc_html__( 'Field groups', 'customfields-com' ) . ' ' . count( $groups ) . ' ' . esc_html__( 'Find a field group', 'customfields-com' ) . ' ';
if ( ! $groups ) {
echo '';
return;
}
$targets = Schema::targets();
$hash = self::fingerprint();
echo '';
foreach ( $groups as $group ) {
$target = $group['target'] ?? 'post';
$location = $targets[ $target ] ?? $target;
if ( 'post' === $target ) {
$labels = array();
foreach ( $group['post_types'] as $type ) { $object = get_post_type_object( $type ); $labels[] = $object ? $object->labels->name : $type; }
$location = implode( ', ', $labels );
} elseif ( 'term' === $target ) { $location = implode( ', ', $group['taxonomies'] ?? array() ); }
$edit_url = add_query_arg( array( 'page' => 'customfields', 'edit' => $group['key'] ), admin_url( 'admin.php' ) );
echo '
' . esc_html( $location ) . ' ' . esc_html( sprintf( /* translators: %s: Localized number of fields. */ _n( '%s field', '%s fields', count( $group['fields'] ), 'customfields-com' ), number_format_i18n( count( $group['fields'] ) ) ) ) . '
';
foreach ( array_slice( $group['fields'], 0, 4 ) as $field ) { echo '' . esc_html( $field['label'] ) . ' '; }
if ( count( $group['fields'] ) > 4 ) { echo '+' . ( count( $group['fields'] ) - 4 ) . ' '; }
if ( ! $group['fields'] ) { echo '' . esc_html__( 'Ready for your first field', 'customfields-com' ) . ' '; }
echo '
';
}
echo '
' . esc_html__( 'Removing a group keeps its saved content. Export JSON to keep a copy of your field definitions.', 'customfields-com' ) . '
';
}
/** Describe the current placement without requiring the settings panel to be open. */
private static function editor_location_summary( $group ) {
$target = $group['target'] ?? 'post';
$targets = Schema::targets();
$labels = array();
foreach ( $target === 'post' ? ( $group['post_types'] ?? array() ) : ( $target === 'term' ? ( $group['taxonomies'] ?? array() ) : array() ) as $name ) {
$object = $target === 'post' ? get_post_type_object( $name ) : get_taxonomy( $name );
$labels[] = $object ? $object->labels->name : $name;
}
$summary = $labels ? implode( ', ', $labels ) : ( $targets[ $target ] ?? $target );
if ( $target === 'post' && ( ! empty( $group['post_ids'] ) || ! empty( $group['page_templates'] ) ) ) { $summary .= ' · ' . __( 'Specific location rules', 'customfields-com' ); }
return $summary;
}
private static function editor( $group ) {
// Do not turn an unavailable extension's settings into a lossy core-only form.
if ( $group['title'] ) {
$compatible = Schema::validate_group( $group );
if ( is_wp_error( $compatible ) ) {
echo '' . esc_html( $compatible->get_error_message() ) . '
' . esc_html__( 'The group is read-only until its schema can be fully understood. Export JSON to keep all definitions.', 'customfields-com' ) . '
';
return;
}
}
echo '' . esc_html__( 'Build your field group', 'customfields-com' ) . ' ';
echo '
' . esc_html__( 'TRY IT OUT', 'customfields-com' ) . '
' . esc_html__( 'Live preview', 'customfields-com' ) . ' ' . esc_html__( 'Try the fields with sample values. These values are never saved.', 'customfields-com' ) . '
' . esc_html__( 'Your fields will appear here as you build.', 'customfields-com' ) . '
' . esc_html__( 'Reset preview', 'customfields-com' ) . ' ' . esc_html__( 'How to use your fields', 'customfields-com' ) . ' ' . esc_html__( 'Save the group, then open the content you selected above to enter real values.', 'customfields-com' ) . '
' . esc_html__( 'Display a value with the Custom Field block, a shortcode, or your theme.', 'customfields-com' ) . '
[customfield name="field_name"]' . esc_html__( 'BUILD YOUR CONTENT', 'customfields-com' ) . '
' . esc_html__( 'Choose a field type', 'customfields-com' ) . ' × ' . esc_html__( 'Search field types', 'customfields-com' ) . '
';
self::field_editor( array( 'key' => '__FIELD_KEY__', 'label' => '', 'name' => '', 'type' => 'text', 'required' => false, 'instructions' => '', 'choices' => array(), 'default' => '' ), '__INDEX__' );
echo ' ';
}
private static function field_editor( $field, $index ) {
$prefix = 'group[fields][' . $index . ']';
$id = 'cf-editor-' . $index;
$expanded = 0 === $index || '__INDEX__' === $index;
$choices = array();
foreach ( $field['choices'] as $key => $label ) { $choices[] = $key . ' : ' . $label; }
$types = Fields::types();
if ( ! isset( $types[ $field['type'] ] ) ) { $types[ $field['type'] ] = $field['type'] . ' (' . __( 'extension inactive', 'customfields-com' ) . ')'; }
echo '' . esc_html__( 'Choices', 'customfields-com' ) . ' ' . esc_textarea( implode( "\n", $choices ) ) . ' ' . esc_html__( 'One per line, written as value : Label.', 'customfields-com' ) . '
' . esc_html__( 'Make this a required field', 'customfields-com' ) . '' . esc_html__( 'Storage key, defaults & validation', 'customfields-com' ) . ' ' . esc_html__( 'Default value', 'customfields-com' ) . ' ' . esc_textarea( is_scalar( $field['default'] ) ? (string) $field['default'] : ( in_array( $field['type'], array( 'multiselect', 'checkboxes' ), true ) ? implode( "\n", $field['default'] ) : wp_json_encode( $field['default'] ) ) ) . ' ' . esc_html__( 'Checkbox: 1 or 0. Multiple choices: one value per line. References: an existing item ID. Defaults apply only when no value is stored.', 'customfields-com' ) . '
' . esc_html__( 'Help text for editors', 'customfields-com' ) . ' ' . esc_textarea( $field['instructions'] ) . ' ';
echo '' . esc_html__( 'Number limits', 'customfields-com' ) . ' ';
foreach ( array( 'min' => __( 'Minimum', 'customfields-com' ), 'max' => __( 'Maximum', 'customfields-com' ), 'step' => __( 'Step (or any)', 'customfields-com' ) ) as $setting => $label ) { echo '' . esc_html( $label ) . ' '; }
echo '
' . esc_html__( 'Text length', 'customfields-com' ) . ' ';
foreach ( array( 'minlength' => __( 'Minimum characters', 'customfields-com' ), 'maxlength' => __( 'Maximum characters', 'customfields-com' ) ) as $setting => $label ) { echo '' . esc_html( $label ) . ' '; }
echo '
' . esc_html__( 'Allowed post types (comma separated)', 'customfields-com' ) . ' ' . esc_html__( 'Taxonomy name', 'customfields-com' ) . ' ';
echo '';
do_action( 'customfields_field_editor_settings', $field, $prefix );
echo '
';
}
private static function import_panel( $status ) {
$preview = 'preview' === $status ? get_transient( 'customfields_import_' . get_current_user_id() ) : false;
$token = isset( $_GET['preview_token'] ) && is_string( $_GET['preview_token'] ) ? sanitize_text_field(wp_unslash( $_GET['preview_token'] )) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only, user-bound preview.
if ( is_array( $preview ) && ! self::preview_matches( $preview, $token ) ) {
echo '' . esc_html__( 'This import preview has been replaced by a newer preview. Validate the intended JSON again.', 'customfields-com' ) . '
';
$preview = false;
}
if ( is_array( $preview ) ) {
echo '' . esc_html__( 'Review import', 'customfields-com' ) . ' ' . esc_html__( 'Groups with matching keys will have their definitions replaced. Other groups and all content values will be kept.', 'customfields-com' ) . '
';
foreach ( $preview['changes'] as $change ) {
echo '' . esc_html( 'replace' === $change['operation'] ? __( 'Replace', 'customfields-com' ) : __( 'Add', 'customfields-com' ) ) . ': ' . esc_html( $change['title'] ) . ' (' . intval( $change['fields'] ) . ' ' . esc_html__( 'fields', 'customfields-com' ) . ') ';
}
echo ' ';
wp_nonce_field( 'customfields_import' );
echo ' ';
submit_button( __( 'Apply reviewed import', 'customfields-com' ), 'primary', 'submit', false );
echo ' ';
}
echo '' . esc_html__( 'Import a CustomFields.com JSON schema', 'customfields-com' ) . ' ' . esc_html__( 'Paste a CustomFields.com export. Validation and a review screen come before any changes. This imports field definitions, not content values.', 'customfields-com' ) . '
';
wp_nonce_field( 'customfields_preview' );
echo '' . esc_html__( 'Schema JSON', 'customfields-com' ) . ' ';
submit_button( __( 'Validate and preview', 'customfields-com' ), 'secondary' );
echo ' ';
}
public static function preview_matches( $preview, $token ) {
return is_array( $preview ) && isset( $preview['token'] ) && is_string( $preview['token'] ) && is_string( $token ) && hash_equals( $preview['token'], $token );
}
}