[], 'meta' => [], 'acf' => [], 'terms' => []];
foreach ($catalog['post_types'] ?? [] as $type) {
foreach ($type['taxonomies'] ?? [] as $taxonomy) {
$key = $taxonomy['slug'] ?? '';
if (!$key) continue;
$options['taxonomy'][$key] = ['label' => display_label($taxonomy['label'] ?? $key), 'public' => $taxonomy['public'] ?? true];
foreach ($taxonomy['terms'] ?? [] as $term) {
$slug = $term['slug'] ?? '';
if ($slug !== '') $options['terms'][$key][$slug] = ['label' => display_label($term['name'] ?? $slug)];
}
}
foreach (['meta' => 'meta_fields', 'acf' => 'acf_fields'] as $kind => $field_key) {
foreach ($type[$field_key] ?? [] as $field) {
$key = $kind === 'meta' ? ($field['key'] ?? '') : ($field['name'] ?? '');
if ($key !== '') $options[$kind][$key] = ['label' => display_label($field['label'] ?? $key)];
}
}
}
foreach (['taxonomy', 'meta', 'acf'] as $kind) uasort($options[$kind], static fn($a, $b) => strnatcasecmp($a['label'], $b['label']));
ksort($options['terms'], SORT_NATURAL | SORT_FLAG_CASE);
foreach ($options['terms'] as &$terms) uasort($terms, static fn($a, $b) => strnatcasecmp($a['label'], $b['label']));
unset($terms);
return $options;
}
function render_option($name, $value, $key, $option, $excluded)
{
?>
__('Global exclusions saved. They apply immediately; no catalog rebuild is needed.', 'accomplice-query-loop-filters'),
'defaults' => __('Default global exclusions restored. No catalog rebuild is needed.', 'accomplice-query-loop-filters'),
];
if (isset($messages[$notice])) printf('
', esc_html($messages[$notice]));
?>
[], 'meta' => [], 'acf' => [], 'terms' => []];
foreach (['taxonomy', 'meta', 'acf'] as $kind) $settings[$kind] = isset($posted[$kind]) && is_array($posted[$kind]) ? $posted[$kind] : [];
foreach (isset($posted['terms']) && is_array($posted['terms']) ? $posted['terms'] : [] as $encoded) {
$term = is_string($encoded) ? json_decode($encoded, true) : null;
if (is_array($term)) $settings['terms'][] = $term;
}
return Exclusions\sanitize_settings($settings);
}
function handle_save()
{
if (!current_user_can('manage_options')) wp_die(esc_html__('You cannot change these settings.', 'accomplice-query-loop-filters'));
check_admin_referer('fqbpro_catalog_save_exclusions');
$operation = isset($_POST['operation']) && is_string($_POST['operation']) ? sanitize_key(wp_unslash($_POST['operation'])) : '';
if ($operation === 'defaults') {
Exclusions\save_settings(Exclusions\defaults());
} elseif ($operation === 'save') {
$complete = isset($_POST['fqbpro_exclusions_complete']) && is_string($_POST['fqbpro_exclusions_complete']) ? sanitize_text_field(wp_unslash($_POST['fqbpro_exclusions_complete'])) : '';
if ($complete !== '1') wp_die(esc_html__('The exclusion form was incomplete. Return to Catalog Data and save a smaller selection, or ask your administrator to increase the PHP input limit. No exclusions were changed.', 'accomplice-query-loop-filters'));
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitize_posted_settings decodes term JSON and validates every field through the shared structured normalizer, preserving exact metadata keys and encoded term slugs.
$settings = sanitize_posted_settings(isset($_POST['exclusions']) && is_array($_POST['exclusions']) ? wp_unslash($_POST['exclusions']) : []);
Exclusions\save_settings($settings);
} else {
wp_die(esc_html__('Unknown exclusions action.', 'accomplice-query-loop-filters'));
}
wp_safe_redirect(add_query_arg(['page' => 'fqbpro-catalog', 'tab' => 'catalog', 'fqbpro_exclusions_notice' => $operation === 'defaults' ? 'defaults' : 'saved'], admin_url('admin.php')));
exit;
}