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 '
' . esc_html__( 'CustomFields.com', 'customfields-com' ) . '' . esc_html__( 'Content studio', 'customfields-com' ) . '' . esc_html( Workspace::pro_available() ? __( 'Pro', 'customfields-com' ) : ( defined( 'CUSTOMFIELDS_PRO_VERSION' ) ? __( 'Pro · activate licence', 'customfields-com' ) : __( 'Free', 'customfields-com' ) ) ) . '
'; 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 '

' . esc_html__( 'YOUR WORKSPACE', 'customfields-com' ) . '

' . esc_html__( 'Field groups', 'customfields-com' ) . '

' . esc_html__( 'Create fields, choose where they appear, then enter values in your WordPress content.', 'customfields-com' ) . '

' . esc_html__( 'New field group', 'customfields-com' ) . '
'; echo '

' . esc_html__( 'Field groups', 'customfields-com' ) . ' ' . count( $groups ) . '

'; if ( ! $groups ) { echo '

' . esc_html__( 'Your first field group starts here.', 'customfields-com' ) . '

' . esc_html__( 'A field group is a small collection of extra details. Try a project brief, event details, or a team profile.', 'customfields-com' ) . '

' . esc_html__( 'Create a field group', 'customfields-com' ) . '
'; 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'] ) ) ) ) . '

' . esc_html( $group['title'] ) . '

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

'; wp_nonce_field( 'customfields_save' ); echo '
' . esc_html__( 'Field groups', 'customfields-com' ) . '
' . esc_html( $group['title'] ? __( 'All changes saved', 'customfields-com' ) : __( 'New field group', 'customfields-com' ) ) . '
'; echo '
' . esc_html__('Your saved group is ready.', 'customfields-com') . '' . esc_html__('Next: enter real values', 'customfields-com') . '' . esc_html__(' (opens in a new tab)', 'customfields-com') . ' →
'; echo '
' . ( $group['title'] ? '' : '

' . esc_html__( 'For example: Project details or Team profile.', 'customfields-com' ) . '

' ) . '
'; echo '
' . esc_html__( 'Show on', 'customfields-com' ) . '' . esc_html( self::editor_location_summary( $group ) ) . '' . esc_html__( 'Change', 'customfields-com' ) . '
' . esc_html__( 'Content types', 'customfields-com' ) . '
'; $post_types = get_post_types( array( 'show_ui' => true ), 'objects' ); foreach ( $group['post_types'] as $type ) { if ( ! isset( $post_types[ $type ] ) ) { $post_types[ $type ] = (object) array( 'name' => $type, 'label' => $type . ' (' . __( 'not registered', 'customfields-com' ) . ')' ); } } foreach ( $post_types as $type ) { if ( in_array( $type->name, array( 'attachment', 'wp_block', 'wp_template', 'wp_template_part', 'wp_navigation', 'wp_font_family', 'wp_font_face' ), true ) ) { continue; } echo ''; } echo '
' . esc_html__( 'Narrow to specific posts or templates', 'customfields-com' ) . '

' . esc_html__( 'Optional. Separate IDs with commas and template filenames with new lines. When both are supplied, both must match.', 'customfields-com' ) . '

' . esc_html__( 'Taxonomies', 'customfields-com' ) . '
'; $taxonomies = get_taxonomies( array( 'show_ui' => true ), 'objects' ); foreach ( $group['taxonomies'] ?? array() as $tax ) { if ( ! isset( $taxonomies[$tax] ) ) { $taxonomies[$tax] = (object) array( 'name' => $tax, 'label' => $tax ); } } foreach ( $taxonomies as $tax ) { echo ''; } echo '
'; echo '

' . esc_html__( 'Your fields', 'customfields-com' ) . ' ' . count( $group['fields'] ) . '

' . esc_html__( 'Add a field, edit its settings, or drag to reorder.', 'customfields-com' ) . '

' . esc_html__( 'Add your first field', 'customfields-com' ) . '

' . esc_html__( 'Start with a ready-made kit above, or add your first field.', 'customfields-com' ) . '

'; foreach ( $group['fields'] as $index => $field ) { self::field_editor( $field, $index ); } echo '
'; echo '
' . esc_html__( 'Storage & group options', 'customfields-com' ) . '' . esc_html( ( $group['storage'] ?? 'metadata' ) === 'custom_table' ? __( 'Custom table + compatibility mirror', 'customfields-com' ) : __( 'WordPress metadata', 'customfields-com' ) ) . '

' . esc_html__( 'The default storage works for ordinary field groups. Change it only when you have prepared a different storage setup.', 'customfields-com' ) . '

'; do_action( 'customfields_group_editor_settings', $group ); echo '

'; echo '

' . esc_html__( 'BUILD YOUR CONTENT', 'customfields-com' ) . '

' . esc_html__( 'Choose a field type', 'customfields-com' ) . '

'; } 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( $field['label'] ?: __( 'Untitled field', 'customfields-com' ) ) . '' . esc_html( $types[ $field['type'] ] ) . '

' . esc_html__( 'One per line, written as value : Label.', 'customfields-com' ) . '

' . esc_html__( 'Storage key, defaults & validation', 'customfields-com' ) . '

' . esc_html__( 'Generated from the label. Changing an existing key does not move saved content.', 'customfields-com' ) . '

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

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

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