base, array('post', 'profile', 'user-edit', 'term', 'edit-tags'), true);
$field_page = $hook === 'toplevel_page_customfields' || strpos($hook, 'customfields-pro-options') !== false || strpos($hook, 'customfields-bulk') !== false;
if (!$field_screen && !$field_page) { return; }
wp_enqueue_media();
wp_enqueue_editor();
wp_enqueue_script('customfields-richtext', CUSTOMFIELDS_URL . 'assets/richtext.js', array('editor', 'wp-i18n'), CUSTOMFIELDS_VERSION, true);
wp_set_script_translations('customfields-richtext', 'customfields-com', CUSTOMFIELDS_PATH . 'languages');
wp_enqueue_style('customfields-pro', CUSTOMFIELDS_LOCAL_URL . 'admin.css', array(), CUSTOMFIELDS_LOCAL_VERSION);
wp_enqueue_script('customfields-pro', CUSTOMFIELDS_LOCAL_URL . 'admin.js', array('wp-i18n', 'customfields-richtext'), CUSTOMFIELDS_LOCAL_VERSION, true);
wp_set_script_translations('customfields-pro', 'customfields-com', CUSTOMFIELDS_PATH . 'languages');
if (strpos($hook, 'customfields-pro-options') !== false || isset($_POST['options_page_slug'])) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Display-only options-page selection; save handlers independently verify page capability and action nonce.
wp_enqueue_script('customfields-pro-options', CUSTOMFIELDS_LOCAL_URL . 'options.js', array('customfields-pro', 'wp-i18n'), CUSTOMFIELDS_LOCAL_VERSION, true);
wp_set_script_translations('customfields-pro-options', 'customfields-com', CUSTOMFIELDS_PATH . 'languages');
wp_localize_script('customfields-pro-options', 'CustomFieldsOptions', array('ajaxUrl' => admin_url('admin-ajax.php')));
}
wp_localize_script('customfields-pro', 'customfieldsProLabels', array(
'row' => __('Row', 'customfields-com'), 'rows' => __('rows', 'customfields-com'), 'maximum' => __('Maximum reached', 'customfields-com'),
'up' => __('Move up', 'customfields-com'), 'down' => __('Move down', 'customfields-com'), 'remove' => __('Remove', 'customfields-com'),
'collapse' => __('Collapse', 'customfields-com'), 'expand' => __('Expand', 'customfields-com'), 'chooseImages' => __('Choose gallery images', 'customfields-com'),
'newField' => __('New child field', 'customfields-com'), 'fieldLabel' => __('Label', 'customfields-com'), 'fieldName' => __('Name', 'customfields-com'),
'fieldType' => __('Type', 'customfields-com'), 'required' => __('Required', 'customfields-com'), 'choices' => __('Choices (value : label, one per line)', 'customfields-com'),
'defaultValue' => __('Default value', 'customfields-com'), 'minRows' => __('Minimum rows', 'customfields-com'), 'maxRows' => __('Maximum rows', 'customfields-com'),
'addChild' => __('Add child field', 'customfields-com'), 'maxChildren' => __('A container supports up to 20 child fields.', 'customfields-com'),
'invalidJSON' => __('Provide an array of child fields.', 'customfields-com'),
));
}
private static function authorize() {
if (!current_user_can('manage_options')) {
wp_die(esc_html__('You cannot manage these settings.', 'customfields-com'), '', array('response' => 403));
}
}
private static function page_config($group) {
return array_merge(array('slug' => 'site', 'title' => __('Site options', 'customfields-com'), 'capability' => 'manage_options'), $group['options_page'] ?? array());
}
private static function menu_slug($slug) {
return $slug === 'site' ? 'customfields-pro-options' : 'customfields-pro-options-' . $slug;
}
private static function selected_page() {
$slug = isset($_POST['options_page_slug']) && is_string($_POST['options_page_slug']) ? sanitize_key(wp_unslash($_POST['options_page_slug'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Missing -- Display-only options-page selection; save handlers independently verify page capability and action nonce.
if ($slug === '') {
$menu = isset($_GET['page']) && is_string($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only routing, filtering or pagination; no mutation is authorized by this query parameter. Object access remains checked by the renderer.
$slug = is_string($menu) && strpos($menu, 'customfields-pro-options-') === 0 ? substr($menu, strlen('customfields-pro-options-')) : 'site';
}
return is_string($slug) && preg_match('/^[a-z][a-z0-9_-]{0,39}$/D', $slug) ? $slug : '';
}
/** Only groups explicitly delegated to the current user are rendered or written. */
private static function groups($slug = null) {
if ($slug === null) { $slug = self::selected_page(); }
return array_values(array_filter(customfields_get_groups(), static function ($group) use ($slug) {
$config = self::page_config($group);
return ($group['target'] ?? 'post') === 'options' && $config['slug'] === $slug && current_user_can($config['capability']);
}));
}
private static function authorize_options() {
if (self::selected_page() === '' || (!self::groups() && !(self::selected_page() === 'site' && current_user_can('manage_options')))) {
wp_die(esc_html__('You cannot manage this options page.', 'customfields-com'), '', array('response' => 403));
}
}
private static function values_hash($values, $groups) {
$visible = array();
foreach ($groups as $group) {
foreach ($group['fields'] as $field) {
if (array_key_exists($field['name'], $values)) { $visible[$field['name']] = $values[$field['name']]; }
}
}
return hash('sha256', serialize($visible));
}
/** Optimistic compare-and-swap preserves changes to other options pages. */
private static function write_values($before, $updates, $groups) {
global $wpdb;
$name = 'customfields_pro_values';
for ($attempt = 0; $attempt < 3; $attempt++) {
$row = $wpdb->get_row($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name = %s", $name), 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('options_read', __('Site options could not be read. Nothing was saved.', 'customfields-com')); }
$latest = $row ? maybe_unserialize($row['option_value']) : array();
if (!is_array($latest) || self::values_hash($latest, $groups) !== self::values_hash($before, $groups)) {
return new \WP_Error('options_conflict', __('Values on this page changed while you were editing. Reload the page and review them before saving.', 'customfields-com'));
}
$next = array_replace($latest, $updates);
if ($next === $latest) { return true; }
if (!$row) {
wp_cache_delete($name, 'options');
wp_cache_delete('notoptions', 'options');
if (add_option($name, $next, '', false)) { return true; }
continue;
}
$changed = $wpdb->query($wpdb->prepare("UPDATE {$wpdb->options} SET option_value = %s WHERE option_name = %s AND BINARY option_value = BINARY %s", maybe_serialize($next), $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.
wp_cache_delete($name, 'options');
wp_cache_delete('alloptions', 'options');
if ($changed === 1) {
do_action('updated_option', $name, $latest, $next); // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- Intentionally emit the existing WordPress option lifecycle hook after the atomic option write.
return true;
}
if ($changed === false) { return new \WP_Error('options_write', __('Site options could not be saved. Please try again.', 'customfields-com')); }
}
return new \WP_Error('options_busy', __('Site options changed during saving. Reload and try again.', 'customfields-com'));
}
private static function notice($kind, $message) {
set_transient('customfields_pro_notice_' . get_current_user_id(), array('kind' => $kind, 'message' => $message), 120);
}
private static function show_notice() {
$key = 'customfields_pro_notice_' . get_current_user_id();
$notice = get_transient($key);
if (!is_array($notice)) { return; }
delete_transient($key);
echo '
' . esc_html($notice['message']) . '
';
}
private static function redirect($page) {
wp_safe_redirect(admin_url('admin.php?page=' . $page));
exit;
}
/** Echo only this user's submitted work; never fetch or retain removed fields. */
public static function recovery_panel($request, $kind = 'options') {
if (!is_array($request)) { return; }
if (isset($request['values_json']) && is_string($request['values_json'])) { $text = $request['values_json']; }
else {
$values = $kind === 'options' ? ($request['customfields_pro_values'] ?? array()) : ($request['customfields_frontend_values'][absint($request['post_id'] ?? 0)][$request['group_key'] ?? ''] ?? array());
$text = wp_json_encode($values, JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE);
}
$recovered = $request[$kind === 'options' ? 'customfields_pro_recovery_json' : 'cfp_recovered'] ?? array();
if ($recovered) { $text = wp_json_encode(array('submitted_json' => $text, 'recovered_json' => $recovered), JSON_PRETTY_PRINT | JSON_INVALID_UTF8_SUBSTITUTE); }
$text = self::recovery_text($text);
if ($text === '' || $text === '[]') { return; }
echo '' . esc_html__('Copy of your submitted work', 'customfields-com') . '
' . esc_html__('Copy these values before leaving this page. Fields that were removed or are no longer editable are included only for recovery and cannot be saved here.', 'customfields-com') . '
';
}
/** Recovery may retain an old conflict token, but must never repair it into a valid one. */
private static function recovery_hash($value) {
if (!is_string($value)) { return ''; }
$clean = sanitize_text_field($value);
return $clean === $value && preg_match('/^[a-f0-9]{64}$/D', $clean) ? $clean : '';
}
/**
* Recovery is editable source text, not HTML or a value ready for storage.
* Validate its byte envelope without destroying JSON strings containing rich
* text, percent signs or newlines. Invalid JSON remains available to correct;
* options_input()/Forms::submitted() parse it and field sanitizers run on save.
*/
public static function recovery_text($value) {
if (!is_string($value) || strlen($value) > 1048576) { return ''; }
if (wp_check_invalid_utf8($value) !== $value || preg_match('/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', $value)) { return ''; }
return $value;
}
/** Read only recognized options controls; nested fields are sanitized by schema. */
private static function options_http_request() {
if (!isset($_POST['_wpnonce']) || !is_string($_POST['_wpnonce'])) { return array(); }
if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'customfields_pro_options')) { return array(); }
$request = array();
foreach (array('_wpnonce', 'options_page_slug', 'customfields_pro_schema', 'customfields_pro_values_hash', 'customfields_pro_complete', 'options_rebase', 'options_confirm_review', 'options_intent', 'intent') as $name) {
if (!isset($_POST[$name])) { continue; }
if (!is_string($_POST[$name])) { $request[$name] = null; continue; }
$clean = sanitize_text_field(wp_unslash($_POST[$name]));
// A malformed proof or completion marker must never be repaired into a valid one.
$request[$name] = $name === '_wpnonce' || $clean === wp_unslash($_POST[$name]) ? $clean : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The second read is used only for exact comparison with the sanitized control, never output or storage.
}
if (isset($_POST['values_json'])) {
$request['values_json'] = is_string($_POST['values_json']) ? wp_unslash($_POST['values_json']) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Authenticated JSON transport; options_input enforces the 1 MB/depth/array limits, then options_request_validated sanitizes every field against its registered schema before the atomic write.
}
foreach (array('customfields_pro_values', 'customfields_pro_recovery_json') as $name) {
if (isset($_POST[$name])) {
$request[$name] = is_array($_POST[$name]) ? wp_unslash($_POST[$name]) : null; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Authenticated structured fields; options_input validates the complete envelope and every persisted value passes its field sanitizer. Generic text sanitation would corrupt rich/nested fields.
}
}
return $request;
}
public static function options_page($recovery = null, $error = null, $review = false) {
$groups = self::groups();
if ($recovery && !$groups && is_user_logged_in() && is_string($recovery['_wpnonce'] ?? null) && wp_verify_nonce(sanitize_text_field($recovery['_wpnonce']), 'customfields_pro_options')) {
echo '
' . esc_html__('Unsaved site options', 'customfields-com') . '
' . esc_html__('This options page is no longer available to you. Nothing was saved.', 'customfields-com') . '
';
return;
}
$values = get_option('customfields_pro_values', array());
if (!is_array($values)) { $values = array(); }
$submitted = is_array($recovery) ? self::options_input($recovery) : array();
$raw_recovery = array();
if (is_wp_error($submitted)) {
$submitted = is_array($recovery['customfields_pro_values'] ?? null) && strlen(wp_json_encode($recovery['customfields_pro_values'])) <= 1048576 ? $recovery['customfields_pro_values'] : array();
$raw_recovery = is_array($recovery['customfields_pro_recovery_json'] ?? null) && strlen(wp_json_encode($recovery['customfields_pro_recovery_json'])) <= 1048576 ? $recovery['customfields_pro_recovery_json'] : array();
}
$schema_hash = hash('sha256', wp_json_encode($groups));
$recovery_schema_hash = self::recovery_hash($recovery['customfields_pro_schema'] ?? null);
$recovery_values_hash = self::recovery_hash($recovery['customfields_pro_values_hash'] ?? null);
$same_schema = !$recovery || $recovery_schema_hash === $schema_hash;
$review = $review && $same_schema;
echo '';
}
private static function option_error($code, $message, $status = 400, $field = '') {
return new \WP_Error($code, $message, array('status' => $status, 'field' => $field));
}
private static function options_input($request) {
$input = array_key_exists('customfields_pro_values', $request) ? $request['customfields_pro_values'] : array();
if (array_key_exists('values_json', $request)) {
if (!is_string($request['values_json']) || strlen($request['values_json']) > 1048576) { return self::option_error('options_size', __('This options form exceeds the 1 MB limit.', 'customfields-com')); }
$input = json_decode($request['values_json'], true, 16);
if (json_last_error() !== JSON_ERROR_NONE) { return self::option_error('options_json', __('The submitted values could not be read. Your work is still in this form.', 'customfields-com')); }
}
if (!is_array($input) || strlen(wp_json_encode($input)) > 1048576) { return self::option_error('options_size', __('Provide an options form of at most 1 MB.', 'customfields-com')); }
$recovered = array_key_exists('customfields_pro_recovery_json', $request) ? $request['customfields_pro_recovery_json'] : array();
if (!is_array($recovered)) { return self::option_error('options_json', __('The recovered values are malformed.', 'customfields-com')); }
foreach ($recovered as $name => $json) {
if (!is_string($json) || strlen($json) > 1048576) { return self::option_error('options_json', __('A recovered value is too large.', 'customfields-com')); }
$value = json_decode($json, true, 16);
if (json_last_error() !== JSON_ERROR_NONE) { return self::option_error('options_json', __('Correct the recovered value JSON before saving.', 'customfields-com'), 400, $name); }
$input[$name] = $value;
}
$encoded = wp_json_encode($input);
if (false === $encoded || strlen($encoded) > 1048576) { return self::option_error('options_size', __('The recovered form exceeds the 1 MB limit.', 'customfields-com')); }
return $input;
}
/** Same validation and optimistic write for native and AJAX transports. */
private static function options_request($request) {
global $wpdb;
$slug = $request['options_page_slug'] ?? '';
if (!is_user_logged_in() || !is_string($slug) || !preg_match('/^[a-z][a-z0-9_-]{0,39}$/D', $slug) || !self::groups($slug)) { return self::option_error('options_forbidden', __('You cannot edit this options page.', 'customfields-com'), 403); }
$table = $wpdb->get_row($wpdb->prepare('SHOW TABLE STATUS LIKE %s', $wpdb->esc_like($wpdb->options)), ARRAY_A); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Inspect/provision the actual database schema; cached application metadata cannot establish storage-engine or column guarantees.
if (!$table || strtolower($table['Engine'] ?? '') !== 'innodb') { return self::option_error('options_database', __('Options editing requires a transactional options table.', 'customfields-com'), 500); }
if (false === $wpdb->query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE') || false === $wpdb->query('START TRANSACTION')) { return self::option_error('options_database', __('A protected save could not be started.', 'customfields-com'), 500); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Database transaction/advisory-lock operation; WordPress has no equivalent API and lock results must never be cached.
try {
foreach (array('customfields_groups', 'customfields_pro_values') as $name) {
if (false === $wpdb->query($wpdb->prepare("SELECT option_value FROM {$wpdb->options} WHERE option_name=%s FOR UPDATE", $name))) { throw new \RuntimeException(__('The options could not be locked. Nothing was saved.', 'customfields-com')); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Read or lock authoritative rows for the transaction; an object-cache result cannot provide database locking or conflict detection.
wp_cache_delete($name, 'options');
}
wp_cache_delete('alloptions', 'options'); wp_cache_delete('notoptions', 'options');
// Resolve current group capabilities and schema only after the schema row is locked.
$result = self::options_request_validated($request);
if (is_wp_error($result)) { $wpdb->query('ROLLBACK'); wp_cache_delete('customfields_pro_values', 'options'); wp_cache_delete('alloptions', 'options'); wp_cache_delete('notoptions', 'options'); return $result; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Database transaction/advisory-lock operation; WordPress has no equivalent API and lock results must never be cached.
if (false === $wpdb->query('COMMIT')) { throw new \RuntimeException(__('The options save could not be committed. Reload and verify before retrying.', 'customfields-com')); } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Database transaction/advisory-lock operation; WordPress has no equivalent API and lock results must never be cached.
wp_cache_delete('customfields_pro_values', 'options'); wp_cache_delete('alloptions', 'options'); wp_cache_delete('notoptions', 'options');
return $result;
} catch (\Throwable $error) {
$wpdb->query('ROLLBACK'); wp_cache_delete('customfields_pro_values', 'options'); wp_cache_delete('alloptions', 'options'); wp_cache_delete('notoptions', 'options'); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery -- Database transaction/advisory-lock operation; WordPress has no equivalent API and lock results must never be cached.
return self::option_error('options_write', $error->getMessage(), 500);
}
}
private static function options_request_validated($request) {
$slug = $request['options_page_slug'] ?? '';
if (!is_string($slug) || !preg_match('/^[a-z][a-z0-9_-]{0,39}$/D', $slug)) { return self::option_error('options_forbidden', __('This options page is unavailable.', 'customfields-com'), 403); }
$groups = self::groups($slug);
if (!$groups) { return self::option_error('options_forbidden', __('You cannot edit this options page.', 'customfields-com'), 403); }
$schema = hash('sha256', wp_json_encode($groups));
if (!is_string($request['customfields_pro_schema'] ?? null) || !hash_equals($schema, $request['customfields_pro_schema'])) { return self::option_error('options_schema', __('The field definitions changed. Keep this form open and review the updated page in a new tab before transferring your edits.', 'customfields-com'), 409); }
if (($request['customfields_pro_complete'] ?? '') !== '1') { return self::option_error('options_incomplete', __('The form was incomplete. Nothing was saved. Check the PHP input limit.', 'customfields-com')); }
if (isset($request['options_rebase']) && ($request['options_confirm_review'] ?? '') !== '1') { return self::option_error('options_review', __('Confirm that you reviewed the current saved values before saving your edits.', 'customfields-com')); }
$submitted = self::options_input($request);
if (is_wp_error($submitted)) { return $submitted; }
$values = get_option('customfields_pro_values', array());
if (!is_array($values)) { return self::option_error('options_storage', __('The saved options cannot be read safely.', 'customfields-com')); }
if (!is_string($request['customfields_pro_values_hash'] ?? null) || !hash_equals(self::values_hash($values, $groups), $request['customfields_pro_values_hash'])) { return self::option_error('options_conflict', __('Someone saved this page after you opened it. Your edits are still here. Review the current values before choosing what to save.', 'customfields-com'), 409); }
$updates = array();
foreach ($groups as $group) {
$clean_group = \CustomFields\Conditions::resolve_values($group['fields'], $submitted, $values, static function($field) use ($submitted, $values) {
if (array_key_exists($field['name'], $values)) {
$stored = \CustomFields\Fields::storage_error($values[$field['name']], $field);
if (is_wp_error($stored)) { return $stored; }
}
if (!array_key_exists($field['name'], $submitted)) { return new \WP_Error('options_incomplete', __('This field is missing. Nothing was saved.', 'customfields-com')); }
return \CustomFields\Fields::sanitize($submitted[$field['name']], $field, array('has_stored_value' => array_key_exists($field['name'], $values), 'stored_value' => $values[$field['name']] ?? null));
});
if (is_wp_error($clean_group)) { return self::option_error('options_invalid', implode(' ', $clean_group->get_error_messages()), 400, $clean_group->get_error_code()); }
$updates = array_replace($updates, $clean_group);
}
$saved = self::write_values($values, $updates, $groups);
if (is_wp_error($saved)) { return self::option_error($saved->get_error_code(), $saved->get_error_message(), in_array($saved->get_error_code(), array('options_conflict', 'options_busy'), true) ? 409 : 500); }
// Bind the next request to this write, not to another writer's later result.
return array('schema_hash' => $schema, 'values_hash' => self::values_hash(array_replace($values, $updates), $groups), 'contexts' => self::options_contexts($groups, $updates), 'condition_values' => \CustomFields\Conditions::option_values($groups, $updates), 'field_hashes' => array_map(array('CustomFields\\Local\\Fields', 'fingerprint'), $updates), 'message' => __('Site options saved.', 'customfields-com'));
}
/** JSON lists preserve PHP iteration order; native baselines remain unchanged. */
public static function editor_value($field, $value) {
if (!is_array($value) || !in_array($field['type'], array('repeater', 'group', 'flexible', 'clone'), true)) { return $value; }
$multiple = in_array($field['type'], array('repeater', 'flexible'), true);
$rows = $multiple ? array_values($value) : array($value);
$layouts = array_column($field['layouts'] ?? array(), null, 'name');
foreach ($rows as &$row) {
if (!is_array($row)) { continue; }
$children = $field['type'] === 'flexible' ? ($layouts[$row['__layout'] ?? '']['sub_fields'] ?? array()) : ($field['sub_fields'] ?? array());
foreach ($children as $child) { if (array_key_exists($child['name'], $row)) { $row[$child['name']] = self::editor_value($child, $row[$child['name']]); } }
}
unset($row);
return $multiple ? $rows : $rows[0];
}
public static function value_contexts($fields, $values, $base) {
$contexts = array();
$visit = function($field, $value, $prefix) use (&$visit, &$contexts) {
if ($field['type'] === 'relationship' && class_exists('CustomFields\\Relationships')) { $contexts[$prefix] = array('fingerprint' => \CustomFields\Relationships::hash($value), 'stored' => $value); return; }
if (!in_array($field['type'], array('repeater', 'group', 'flexible', 'clone'), true) || !is_array($value)) { return; }
$contexts[$prefix] = array('fingerprint' => Fields::fingerprint($value), 'stored' => $value);
if (in_array($field['type'], array('repeater', 'flexible'), true)) { $contexts[$prefix]['row_sources'] = array_keys($value); }
$multiple = in_array($field['type'], array('repeater', 'flexible'), true);
$rows = $multiple ? array_values($value) : array($value);
$layouts = array_column($field['layouts'] ?? array(), null, 'name');
foreach ($rows as $index => $row) {
$children = $field['type'] === 'flexible' ? ($layouts[$row['__layout'] ?? '']['sub_fields'] ?? array()) : ($field['sub_fields'] ?? array());
foreach ($children as $child) { if (array_key_exists($child['name'], $row)) { $visit($child, $row[$child['name']], $prefix . ($multiple ? '[' . $index . ']' : '') . '[' . $child['name'] . ']'); } }
}
};
foreach ($fields as $field) { if (array_key_exists($field['name'], $values)) { $visit($field, $values[$field['name']], $base . '[' . $field['name'] . ']'); } }
return $contexts;
}
private static function options_contexts($groups, $updates) {
$contexts = array();
foreach ($groups as $group) { $contexts = array_merge($contexts, self::value_contexts($group['fields'], $updates, 'customfields_pro_values')); }
return $contexts;
}
public static function ajax_options() {
if (!isset($_POST['_wpnonce']) || !is_string($_POST['_wpnonce'])) { wp_send_json_error(array('code' => 'options_nonce', 'message' => __('Your session expired. Keep your edits here and sign in again in another tab.', 'customfields-com')), 403); }
if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'customfields_pro_options')) { wp_send_json_error(array('code' => 'options_nonce', 'message' => __('Your session expired. Keep your edits here and sign in again in another tab.', 'customfields-com')), 403); }
$request = self::options_http_request();
$slug = $request['options_page_slug'] ?? '';
if (!is_string($slug) || !preg_match('/^[a-z][a-z0-9_-]{0,39}$/D', $slug) || !self::groups($slug)) { wp_send_json_error(array('code' => 'options_forbidden', 'message' => __('You cannot edit this options page.', 'customfields-com')), 403); }
if (($request['intent'] ?? '') === 'review') {
$groups = self::groups($slug);
$values = get_option('customfields_pro_values', array());
if (!is_array($values)) { wp_send_json_error(array('code' => 'options_storage', 'message' => __('The saved options cannot be read safely.', 'customfields-com')), 500); }
$visible = array();
foreach ($groups as $group) { foreach ($group['fields'] as $field) { $visible[] = array('name' => $field['name'], 'label' => $field['label'], 'type' => $field['type'], 'fingerprint' => Fields::fingerprint($values[$field['name']] ?? $field['default']), 'value' => $values[$field['name']] ?? $field['default'], 'editor_value' => self::editor_value($field, $values[$field['name']] ?? $field['default'])); } }
wp_send_json_success(array('schema_hash' => hash('sha256', wp_json_encode($groups)), 'values_hash' => self::values_hash($values, $groups), 'fields' => $visible, 'contexts' => self::options_contexts($groups, $values)));
}
$result = self::options_request($request);
if (is_wp_error($result)) { $data = $result->get_error_data(); wp_send_json_error(array('code' => $result->get_error_code(), 'message' => $result->get_error_message(), 'field' => $data['field'] ?? ''), (int) ($data['status'] ?? 400)); }
wp_send_json_success($result);
}
public static function save_options() {
if (!isset($_POST['_wpnonce']) || !is_string($_POST['_wpnonce'])) { wp_die(esc_html__('Your session expired. Nothing was saved.', 'customfields-com'), '', array('response' => 403)); }
if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'customfields_pro_options')) { wp_die(esc_html__('Your session expired. Nothing was saved.', 'customfields-com'), '', array('response' => 403)); }
$request = self::options_http_request();
$review = ($request['options_intent'] ?? '') === 'review';
$result = $review ? self::option_error('options_review', __('Review the saved values below. Your edits have not been saved.', 'customfields-com')) : self::options_request($request);
if (is_wp_error($result)) {
// Render within this authenticated response. No invalid values are stored in options, transients or browser storage.
nocache_headers();
$GLOBALS['title'] = __('Review site options', 'customfields-com');
require_once ABSPATH . 'wp-admin/admin-header.php';
self::options_page($request, $result, $review);
require_once ABSPATH . 'wp-admin/admin-footer.php';
exit;
}
self::notice('success', $result['message']);
self::redirect(self::menu_slug(self::selected_page()));
}
public static function analyze() {
self::authorize();
if (!isset($_POST['_wpnonce']) || !is_string($_POST['_wpnonce'])) { wp_die(esc_html__('Your session expired. Run the analysis again.', 'customfields-com'), '', array('response' => 403)); }
if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'customfields_pro_analyze')) { wp_die(esc_html__('Your session expired. Run the analysis again.', 'customfields-com'), '', array('response' => 403)); }
$json = isset($_POST['acf_json']) && is_string($_POST['acf_json']) ? wp_unslash($_POST['acf_json']) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The capability and nonce were verified above. Migrator::analyze bounds/parses the JSON, then Schema::validate_groups sanitizes and validates every candidate. Text sanitation would corrupt JSON and rich defaults.
$report = Migrator::analyze($json, customfields_get_groups());
$token = wp_generate_password(32, false, false);
set_transient('customfields_pro_import_' . get_current_user_id(), array('token' => $token, 'report' => $report, 'schema_hash' => hash('sha256', wp_json_encode(get_option('customfields_groups', array())))), 15 * MINUTE_IN_SECONDS);
self::redirect('customfields-pro-import');
}
public static function import() {
self::authorize();
if (!isset($_POST['_wpnonce']) || !is_string($_POST['_wpnonce'])) { wp_die(esc_html__('Your session expired. Analyze the export again.', 'customfields-com'), '', array('response' => 403)); }
if (!wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['_wpnonce'])), 'customfields_pro_import')) { wp_die(esc_html__('Your session expired. Analyze the export again.', 'customfields-com'), '', array('response' => 403)); }
$key = 'customfields_pro_import_' . get_current_user_id();
$preview = get_transient($key);
$token = isset($_POST['preview_token']) && is_string($_POST['preview_token']) ? sanitize_text_field(wp_unslash($_POST['preview_token'])) : '';
if (!is_array($preview) || !hash_equals($preview['token'], $token) || !empty($preview['report']['errors']) || empty($preview['report']['candidates']) || ($_POST['acknowledge'] ?? '') !== '1') { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- The capability and nonce were verified above. Exact comparison to the expected completion/confirmation or form-identity value; invalid strings and arrays are rejected, not rendered or persisted.
self::notice('error', __('Run the schema analysis and confirm its limitations before importing. Previews expire after 15 minutes.', 'customfields-com'));
self::redirect('customfields-pro-import');
}
if (!hash_equals($preview['schema_hash'], hash('sha256', wp_json_encode(get_option('customfields_groups', array()))))) {
self::notice('error', __('Field groups changed since this preview. Analyze the export again.', 'customfields-com'));
self::redirect('customfields-pro-import');
}
$groups = \CustomFields\Schema::validate_groups(array_merge(customfields_get_groups(), $preview['report']['candidates']));
if (is_wp_error($groups)) {
self::notice('error', $groups->get_error_message());
self::redirect('customfields-pro-import');
}
$before = customfields_get_groups();
update_option('customfields_groups', $groups, false);
do_action('customfields_schema_saved', $groups, $before);
delete_transient($key);
$imported_count = count($preview['report']['candidates']);
self::notice('success', sprintf(/* translators: %s: Imported group count. */ _n('%s field group schema imported. Existing content was not migrated or changed.', '%s field group schemas imported. Existing content was not migrated or changed.', $imported_count, 'customfields-com'), number_format_i18n($imported_count)));
self::redirect('customfields-pro-import');
}
public static function import_page() {
self::authorize();
echo '
' . esc_html__('Import field definitions', 'customfields-com') . '
' . esc_html__('Analyze an ACF JSON export before making changes. This imports supported field definitions only. It does not migrate existing field values, templates, or PHP API calls.', 'customfields-com') . '