handle_session_management_early();
Metasync_Connect_Manager::instance()->handle_whitelabel_password_early();
Metasync_Debug_Manager::instance()->handle_debug_mode_operations();
Metasync_Debug_Manager::instance()->handle_error_log_operations();
Metasync_Debug_Manager::instance()->handle_clear_all_settings();
$this->handle_plugin_access_roles_save();
$SECTION_FEATURES = Metasync_Admin::SECTION_FEATURES;
$SECTION_METASYNC = Metasync_Admin::SECTION_METASYNC;
$SECTION_SEARCHENGINE = Metasync_Admin::SECTION_SEARCHENGINE;
$SECTION_LOCALSEO = Metasync_Admin::SECTION_LOCALSEO;
$SECTION_CODESNIPPETS = Metasync_Admin::SECTION_CODESNIPPETS;
$SECTION_OPTIMAL_SETTINGS = Metasync_Admin::SECTION_OPTIMAL_SETTINGS;
$SECTION_SITE_SETTINGS = Metasync_Admin::SECTION_SITE_SETTINGS;
$SECTION_COMMON_SETTINGS = Metasync_Admin::SECTION_COMMON_SETTINGS;
$SECTION_COMMON_META_SETTINGS = Metasync_Admin::SECTION_COMMON_META_SETTINGS;
$SECTION_SOCIAL_META = Metasync_Admin::SECTION_SOCIAL_META;
$SECTION_SEO_CONTROLS = Metasync_Admin::SECTION_SEO_CONTROLS;
$SECTION_SEO_CONTROLS_ADVANCED = Metasync_Admin::SECTION_SEO_CONTROLS_ADVANCED;
$SECTION_SEO_CONTROLS_INSTANT_INDEX = Metasync_Admin::SECTION_SEO_CONTROLS_INSTANT_INDEX;
$SECTION_PLUGIN_VISIBILITY = Metasync_Admin::SECTION_PLUGIN_VISIBILITY;
$SECTION_BREADCRUMBS = Metasync_Admin::SECTION_BREADCRUMBS;
$SECTION_LLMS_TXT = Metasync_Admin::SECTION_LLMS_TXT;
$option_key = Metasync_Admin::option_key;
$page_slug = Metasync_Admin::$page_slug;
# Use whitelabel OTTO name if configured, fallback to 'OTTO'
$whitelabel_otto_name = Metasync::get_whitelabel_otto_name();
register_setting(
Metasync_Admin::option_group,
Metasync_Admin::option_key,
array(self::instance(), 'sanitize')
);
// LLMs.txt settings are stored in a dedicated option so MCP tools and
// the admin UI share one source of truth.
register_setting(
Metasync_Admin::option_group,
'metasync_llms_txt_settings',
array(
'sanitize_callback' => array(self::instance(), 'sanitize_llms_txt_settings'),
'default' => array(),
)
);
add_settings_section(
$SECTION_METASYNC, // ID
'', // Title - removed to prevent duplication with dashboard card
function(){}, // Callback
$page_slug . '_general' // Page
);
add_settings_section(
$SECTION_LLMS_TXT,
esc_html__('LLMs.txt Settings', 'metasync'),
function () {
echo '
' . esc_html__('Publish a standards-compliant /llms.txt so AI search engines and assistants can discover your most important pages.', 'metasync') . '
';
},
$page_slug . '_general'
);
add_settings_field(
'llms_txt_enabled',
esc_html__('Enable /llms.txt', 'metasync'),
function () {
$settings = get_option('metasync_llms_txt_settings', array());
$checked = !empty($settings['enabled']);
printf(
'';
foreach ($public_types as $type) {
if (in_array($type->name, array('attachment'), true)) {
continue;
}
printf(
' %3$s ',
esc_attr($type->name),
in_array($type->name, $selected, true) ? 'checked' : '',
esc_html($type->labels->singular_name ?: $type->name)
);
}
echo '
';
},
$page_slug . '_general',
$SECTION_LLMS_TXT
);
add_settings_field(
'llms_txt_max_posts',
esc_html__('Max posts to include', 'metasync'),
function () {
$settings = get_option('metasync_llms_txt_settings', array());
$value = isset($settings['max_posts']) ? (int) $settings['max_posts'] : 50;
echo '';
echo '
Bing Instant Indexing (IndexNow) ';
echo '
Configure IndexNow API for instant URL submission to Bing, Yandex, and other search engines that support the IndexNow protocol.
';
},
$page_slug . '_seo-controls' // Page
);
add_settings_field(
'enable_binginstantindex',
'Enable Bing Instant Indexing',
function() use ($option_key) {
// Auto-enable if API key is configured
$bing_settings = get_option('metasync_options_bing_instant_indexing', []);
$has_api_key = !empty($bing_settings['api_key']);
$enable_binginstantindex = Metasync::get_option('seo_controls')['enable_binginstantindex'] ?? 'false';
// Auto-check if API key exists
if ($has_api_key && $enable_binginstantindex !== 'true') {
$enable_binginstantindex = 'true';
}
printf(
'
',
isset($enable_binginstantindex) && $enable_binginstantindex == 'true' ? 'checked' : '',
$has_api_key ? 'data-auto-enabled="true"' : ''
);
printf('
Enable Bing Instant Indexing: Automatically enabled when you configure an API key below. When enabled, this activates the IndexNow protocol to instantly notify Bing, Yandex, and other search engines about URL changes.');
},
$page_slug . '_seo-controls',
'seo_controls_bing_instant_index'
);
// Bing Index API configuration
add_settings_field(
'bing_index_api_config',
'IndexNow API Configuration',
array(Metasync_Settings_Fields::instance(), 'render_bing_index_section'),
$page_slug . '_seo-controls',
'seo_controls_bing_instant_index'
);
add_settings_field(
'searchatlas_api_key',
$this->get_effective_menu_title() . ' API Key',
array(Metasync_Settings_Fields::instance(), 'searchatlas_api_key_callback'),
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'apikey', // ID
'Plugin Auth Token', // Title
array(Metasync_Settings_Fields::instance(), 'metasync_settings_genkey_callback'), // Callback
$page_slug . '_general', // Page
$SECTION_METASYNC // Section
);
/**
* SERVER SIDE RENDERING SETTING OPTIONS
* @see SSR functionality
* Note: OTTO SSR is always enabled by default when connected
*/
# field to accept the otto pixel
add_settings_field(
'otto_pixel_uuid',
$whitelabel_otto_name . ' Pixel UUID',
function() use ($option_key) {
$value = Metasync::get_option('general')['otto_pixel_uuid'] ?? '';
printf('
');
},
$page_slug . '_general',
$SECTION_METASYNC
);
# check box to toggle on and off disabling OTTO for logged in users
add_settings_field(
'otto_disable_on_loggedin',
'Disable ' . $whitelabel_otto_name . ' for Logged in Users',
function() use ($whitelabel_otto_name, $option_key) {
$otto_disable_on_loggedin = Metasync::get_option('general')['otto_disable_on_loggedin'] ?? '';
printf(
'
',
isset($otto_disable_on_loggedin) && $otto_disable_on_loggedin == 'true' ? 'checked' : ''
);
printf('
This disables '.$whitelabel_otto_name.' when logged in to allow editing original page contents ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
# check box to disable OTTO frontend toolbar
add_settings_field(
'otto_disable_preview_button',
'Disable ' . $whitelabel_otto_name . ' Frontend Toolbar',
function() use ($whitelabel_otto_name, $option_key) {
$otto_disable_toolbar = Metasync::get_option('general')['otto_disable_preview_button'] ?? false;
printf(
'
',
filter_var($otto_disable_toolbar, FILTER_VALIDATE_BOOLEAN) ? 'checked' : ''
);
printf('
Hide the entire frontend toolbar (status indicator, preview button, and debug button) on the frontend. %s functionality will still work, but the toolbar controls will be hidden. ', esc_html($whitelabel_otto_name));
},
$page_slug . '_general',
$SECTION_METASYNC
);
# WP Rocket compatibility mode setting
add_settings_field(
'otto_wp_rocket_compat',
'WP Rocket Compatibility',
function() use ($whitelabel_otto_name, $option_key) {
$value = Metasync::get_option('general')['otto_wp_rocket_compat'] ?? 'auto';
$wp_rocket_active = class_exists('WP_Rocket');
echo '
';
echo 'Auto (Recommended) ';
echo 'Buffer Mode (Faster) ';
echo 'HTTP Mode (Safer) ';
echo 'Disable ' . esc_html($whitelabel_otto_name) . ' when WP Rocket is active ';
echo ' ';
if ($wp_rocket_active) {
echo '
✓ WP Rocket detected - compatibility mode active
';
}
echo '
';
echo 'Auto (Recommended): Allows both ' . esc_html($whitelabel_otto_name) . ' and WP Rocket to work together. Does not set DONOTCACHEPAGE unless required (Brizy pages, SG Optimizer conflicts). ';
echo 'Buffer Mode: Forces output buffering method. Fastest but may conflict with some configurations. ';
echo 'HTTP Mode: Forces internal HTTP fetch method. Slower but more compatible. ';
echo 'Disable ' . esc_html($whitelabel_otto_name) . ': Completely disables ' . esc_html($whitelabel_otto_name) . ' when WP Rocket is detected. Use if issues persist.';
echo '
';
},
$page_slug . '_general',
$SECTION_METASYNC
);
/**
* BOT DETECTION SETTINGS FOR OTTO
* @since 1.0.0
*/
# Checkbox to disable OTTO for bot traffic
add_settings_field(
'otto_disable_for_bots',
'Disable ' . $whitelabel_otto_name . ' for Bot Traffic',
function() use ($whitelabel_otto_name, $option_key) {
$otto_disable_for_bots = Metasync::get_option('general')['otto_disable_for_bots'] ?? false;
printf(
'
',
filter_var($otto_disable_for_bots, FILTER_VALIDATE_BOOLEAN) ? 'checked' : ''
);
printf('
Skip %s processing for detected bots (search engines, crawlers, SEO tools) to reduce unnecessary API calls. View bot statistics in the Bot Statistics page under the SEO dropdown. ', esc_html($whitelabel_otto_name));
},
$page_slug . '_general',
$SECTION_METASYNC
);
# Textarea for bot whitelist
add_settings_field(
'otto_bot_whitelist',
'Bot Whitelist',
function() use ($whitelabel_otto_name, $option_key) {
$otto_bot_whitelist = Metasync::get_option('general')['otto_bot_whitelist'] ?? '';
printf(
'
',
esc_textarea($otto_bot_whitelist)
);
echo '
';
echo 'Enter bot names or user-agent patterns (one per line) that should always be processed by ' . esc_html($whitelabel_otto_name) . ', even when "Disable for Bots" is enabled. ';
echo 'Example: ';
echo 'Googlebot ';
echo 'Bingbot';
echo '
';
},
$page_slug . '_general',
$SECTION_METASYNC
);
# Textarea for bot blacklist
add_settings_field(
'otto_bot_blacklist',
'Bot Blacklist',
function() use ($whitelabel_otto_name, $option_key) {
$otto_bot_blacklist = Metasync::get_option('general')['otto_bot_blacklist'] ?? '';
printf(
'
',
esc_textarea($otto_bot_blacklist)
);
echo '
';
echo 'Enter bot names or user-agent patterns (one per line) that should always be blocked from ' . esc_html($whitelabel_otto_name) . ' processing, regardless of other settings. ';
echo 'Example: ';
echo 'BadBot ';
echo 'MaliciousCrawler';
echo '
';
},
$page_slug . '_general',
$SECTION_METASYNC
);
# Link button to Bot Statistics page
add_settings_field(
'otto_bot_statistics_link',
'Bot Detection Statistics',
function() use ($page_slug) {
printf(
'
View Bot Statistics',
esc_url(admin_url('admin.php?page=' . $page_slug . '-bot-statistics'))
);
printf('
View detailed bot detection statistics, breakdown by bot type, and unique bot entries with hit counts.
');
},
$page_slug . '_general',
$SECTION_METASYNC
);
# END BOT DETECTION SETTINGS
add_settings_field(
'periodic_clear_ottopage_cache',
'Clear Page Cache',
function() use ($option_key) {
$periodic_clear_ottopage_cache = Metasync::get_option('general')['periodic_clear_ottopage_cache'] ?? 'default';
printf('
');
printf('Clear Daily ');
printf('Clear Every 2 days ');
printf('Clear Weekly ');
printf('Clear Monthly (Default) ');
printf(' ');
# get last cleared timestamp
$timestamp = get_option('metasync_refresh_all_caches')['pages'] ?? false;
if($timestamp > 0){
printf(
'
last Cleared : '.date('y-m-d H:i:s', $timestamp).'
'
);
}
},
$page_slug . '_otto_cache',
$SECTION_METASYNC
);
add_settings_field(
'periodic_clear_ottopost_cache',
'Clear Post Cache',
function() use ($option_key) {
$periodic_clear_ottopost_cache = Metasync::get_option('general')['periodic_clear_ottopost_cache'] ?? 'default';
printf('
');
printf('Clear Daily ');
printf('Clear Every 2 days ');
printf('Clear Weekly ');
printf('Clear Monthly (Default) ');
printf(' ');
# get last cleared timestamp
$timestamp = get_option('metasync_refresh_all_caches')['posts'] ?? false;
if($timestamp > 0){
printf(
'
last Cleared : '.date('y-m-d H:i:s', $timestamp).'
'
);
}
},
$page_slug . '_otto_cache',
$SECTION_METASYNC
);
add_settings_field(
'periodic_clear_otto_cache',
'Clear all cache',
function() use ($option_key) {
$periodic_clear_otto_cache = Metasync::get_option('general')['periodic_clear_otto_cache'] ?? 'default';
printf('
');
printf('Clear Daily ');
printf('Clear Every 2 days ');
printf('Clear Weekly ');
printf('Clear Monthly (Default) ');
printf(' ');
# get last cleared timestamp
$timestamp = get_option('metasync_refresh_all_caches')['general'] ?? false;
if($timestamp > 0){
printf(
'
last Cleared : '.date('y-m-d H:i:s', $timestamp).'
'
);
}
},
$page_slug . '_otto_cache',
$SECTION_METASYNC
);
# END SERVER SIDE RENDERING SETTINGS
// Meta Box Visibility Controls
add_settings_field(
'disable_common_robots_metabox',
'Disable Common Robots Meta Box',
function() use ($option_key) {
$disabled = Metasync::get_option('general')['disable_common_robots_metabox'] ?? false;
printf(
'
',
$disabled ? 'checked' : ''
);
printf('
Hide the Common Robots Meta box on post/page edit screens ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'disable_advance_robots_metabox',
'Disable Advance Robots Meta Box',
function() use ($option_key) {
$disabled = Metasync::get_option('general')['disable_advance_robots_metabox'] ?? false;
printf(
'
',
$disabled ? 'checked' : ''
);
printf('
Hide the Advance Robots Meta box on post/page edit screens ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'disable_redirection_metabox',
'Disable Redirection Meta Box',
function() use ($option_key) {
$disabled = Metasync::get_option('general')['disable_redirection_metabox'] ?? false;
printf(
'
',
$disabled ? 'checked' : ''
);
printf('
Hide the Redirection meta box on post/page edit screens ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'disable_canonical_metabox',
'Disable Canonical Meta Box',
function() use ($option_key) {
$disabled = Metasync::get_option('general')['disable_canonical_metabox'] ?? false;
printf(
'
',
$disabled ? 'checked' : ''
);
printf('
Hide the Canonical meta box on post/page edit screens ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'disable_social_opengraph_metabox',
'Disable Social Media & Open Graph Meta Box',
function() use ($option_key) {
$disabled = Metasync::get_option('general')['disable_social_opengraph_metabox'] ?? false;
printf(
'
',
$disabled ? 'checked' : ''
);
printf('
Hide the Social Media & Open Graph meta box on post/page edit screens ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'disable_schema_markup_metabox',
'Disable Schema Markup Meta Box',
function() use ($option_key) {
$disabled = Metasync::get_option('general')['disable_schema_markup_metabox'] ?? false;
printf(
'
',
$disabled ? 'checked' : ''
);
printf('
Hide the Schema Markup meta box on post/page edit screens ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'disable_seo_metabox',
'Disable SEO Meta Box',
function() use ($option_key) {
$disabled = Metasync::get_option('general')['disable_seo_metabox'] ?? false;
printf(
'
',
$disabled ? 'checked' : ''
);
printf('
Hide the SEO Title & Meta Description meta box on post/page edit screens ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'open_external_links',
'Open External Links in New Tab/Window',
function() use ($option_key) {
$enabled = Metasync::get_option('general')['open_external_links'] ?? false;
printf(
'
',
$enabled ? 'checked' : ''
);
printf('
Automatically add target="_blank" attribute to external links appearing in your posts, pages, and other post types when rendered by Otto. The attribute is applied when the url is displayed. ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'permalink_structure',
'The Permalink setting of website',
function() {
$current_permalink_structure = get_option('permalink_structure');
$current_rewrite_rules = get_option('rewrite_rules');
// Check if the current permalink structure is set to "Plain"
if (($current_permalink_structure == '/%post_id%/' || $current_permalink_structure == '') && $current_rewrite_rules == '') {
printf('
To ensure compatibility, Please Update your Permalink structure to any option other than "plain. For any Inquiries contact support Check Setting ');
} else {
printf('
Permalink is Okay ');
}
},
$page_slug . '_general',
$SECTION_METASYNC
);
# Add Hide Dashboard Framework setting
add_settings_field(
'hide_dashboard_framework',
'Hide Dashboard',
function() use ($option_key) {
$hide_dashboard = Metasync::get_option('general')['hide_dashboard_framework'] ?? '';
printf(
'
',
isset($hide_dashboard) && $hide_dashboard == 'true' ? 'checked' : ''
);
printf('
Hide the %s dashboard ', esc_html(Metasync_Settings_Registration::instance()->get_effective_menu_title()));
},
$page_slug . '_general',
$SECTION_METASYNC
);
// REMOVED (CVE-2025-14386): disable_single_signup_login setting was removed
# Adding the "Show Admin Bar Status" setting
add_settings_field(
'show_admin_bar_status',
'Show ' . $this->get_effective_menu_title() . ' Status in Admin Bar',
function() use ($option_key) {
$show_admin_bar = Metasync::get_option('general')['show_admin_bar_status'] ?? true;
printf(
'
',
$show_admin_bar ? 'checked' : ''
);
printf('
Show the %s status indicator in the WordPress admin bar. ', esc_html(Metasync::get_effective_plugin_name()));
},
$page_slug . '_general',
$SECTION_METASYNC
);
# Adding the "Enable Auto-updates" setting
add_settings_field(
'enable_auto_updates',
'Enable Automatic Updates',
function() use ($option_key) {
$enable_auto_updates = Metasync::get_option('general')['enable_auto_updates'] ?? false;
printf(
'
',
$enable_auto_updates ? 'checked' : ''
);
printf('
Allow WordPress to automatically update this plugin when new versions are available. ');
},
$page_slug . '_general',
$SECTION_METASYNC
);
# Add User Role Sync Setting
add_settings_field(
'content_genius_sync_roles',
'Content Genius User Roles to Sync',
function() use ($option_key) {
$selected_roles = Metasync::get_option('general')['content_genius_sync_roles'] ?? array();
# If it's a string (single role from old version), convert to array
if (!is_array($selected_roles)) {
$selected_roles = array($selected_roles);
}
# Get all WordPress user roles safely
if (!function_exists('wp_roles')) {
require_once(ABSPATH . 'wp-admin/includes/user.php');
}
global $wp_roles;
# Safety check for $wp_roles
if (!isset($wp_roles) || !is_object($wp_roles)) {
$wp_roles = new WP_Roles();
}
$all_roles = $wp_roles->roles;
# Start container
?>
/>
All Roles
$role_details) {
# Safety check for role data
if (!isset($role_details['name'])) {
continue;
}
$is_checked = in_array($role_key, $selected_roles);
$role_name = translate_user_role($role_details['name']);
?>
/>
How it works:
Select which user roles should be synced with Content Genius.
If "All Roles" is selected or none are selected, all users will be synced.
Otherwise, only users with the selected roles will be included in the sync.
';
echo '';
echo '
';
foreach ($builders as $key => $builder) {
$is_detected = $builder['detected'];
$label = esc_html($builder['label']);
if ($is_detected && $key !== 'gutenberg') {
$label .= ' — Detected';
} elseif (!$is_detected) {
$label .= ' — Not Detected';
}
if ($key === 'gutenberg' && empty($current)) {
$label .= ' — Default';
}
printf(
'%s ',
esc_attr($key),
selected(empty($current) ? 'gutenberg' : $current, $key, false),
$is_detected ? '' : ' disabled',
$label
);
}
echo ' ';
echo '
Only theme builders detected on your site can be selected. Undetected builders are shown as disabled.
';
# Show current auto-detection result
if (!empty($builders[$auto_detected]) && $auto_detected !== 'gutenberg') {
printf(
'
🔍 Auto-detected: %s is active on this site.
',
esc_html($builders[$auto_detected]['label'])
);
}
# Show description for the selected builder
echo '
';
# Inline JS to toggle description on select change
?>
Import from SEO Plugins',
esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-import-external'))
);
printf('
Import settings and data from other SEO plugins (Yoast, Rank Math, AIOSEO, etc).
');
},
$page_slug . '_general',
$SECTION_METASYNC
);
/*
add field to save color for elementor and assign custom color to the heading
*/
if(is_admin()){
add_settings_field(
'white_label_plugin_name',
'Plugin Name',
function() use ($option_key) {
$value = Metasync::get_option('general')['white_label_plugin_name'] ?? '';
printf('
');
printf('
This name will be used for general plugin branding (WordPress menus, page titles, and system messages). Maximum 16 characters.
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'whitelabel_otto_name',
'OTTO Name',
function() use ($option_key) {
$value = Metasync::get_option('general')['whitelabel_otto_name'] ?? '';
printf('
');
$example_name = !empty($value) ? $value : 'OTTO';
printf('
This name will be used for OTTO feature references (e.g., "Enable %s Server Side Rendering").
', esc_html($example_name));
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'whitelabel_logo_light_url',
'Logo (Light Theme)',
function() use ($option_key) {
$whitelabel_settings = Metasync::get_whitelabel_settings();
$value = $whitelabel_settings['logo_light'] ?? '';
printf('
');
printf('
Displayed when the admin UI is in light mode. Leave blank to use the default %s logo.
', esc_html(Metasync::get_effective_plugin_name()));
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'whitelabel_logo_dark_url',
'Logo (Dark Theme)',
function() use ($option_key) {
$whitelabel_settings = Metasync::get_whitelabel_settings();
$value = $whitelabel_settings['logo_dark'] ?? '';
printf('
');
printf('
Displayed when the admin UI is in dark mode. Leave blank to use the default %s logo.
', esc_html(Metasync::get_effective_plugin_name()));
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'whitelabel_domain_url',
'Dashboard URL',
function() use ($option_key) {
$whitelabel_settings = Metasync::get_whitelabel_settings();
$value = $whitelabel_settings['domain'] ?? '';
printf('
');
printf('
Enter your whitelabel dashboard URL (e.g., https://yourdashboard.com). Used for branding purposes.
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'white_label_plugin_description',
'Plugin Description',
function() use ($option_key) {
$value = Metasync::get_option('general')['white_label_plugin_description'] ?? '';
printf('
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'white_label_plugin_author',
'Author',
function() use ($option_key) {
$value = Metasync::get_option('general')['white_label_plugin_author'] ?? '';
printf('
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'white_label_plugin_author_uri',
'Author URI',
function() use ($option_key) {
$value = Metasync::get_option('general')['white_label_plugin_author_uri'] ?? '';
# printf('
');
# Fixed printf usage
printf('
', esc_attr($option_key . '[general][white_label_plugin_author_uri]'), esc_attr($value) );
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'white_label_plugin_uri',
'Plugin URI',
function() use ($option_key) {
$value = Metasync::get_option('general')['white_label_plugin_uri'] ?? ''; // New option for Plugin URI
printf('
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
register_setting(Metasync_Admin::option_group,
Metasync_Admin::option_key,
array(self::instance(), 'sanitize')
);
// DEPRECATED: Menu Name and Menu Title fields removed
add_settings_field(
'white_label_plugin_menu_slug',
'Menu Slug',
function() use ($option_key) {
$value = Metasync::get_option('general')['white_label_plugin_menu_slug'] ?? '';
printf('
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'white_label_plugin_menu_icon',
'Menu Icon',
function() use ($option_key) {
$value = Metasync::get_option('general')['white_label_plugin_menu_icon'] ?? '';
printf('
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
}
// HIDDEN: Choose Style Option setting
/*
add_settings_field(
'enabled_plugin_css',
'Choose Style Option',
function() use ($option_key) {
$enabled_plugin_css = Metasync::get_option('general')['enabled_plugin_css'] ?? '';
// Output radio button for Default Style.css active
printf(
'
',
($enabled_plugin_css == 'default'||$enabled_plugin_css =='') ? 'checked' : ''
);
printf('
Default ');
// Output radio button for Metasync Style
printf(
'
',
($enabled_plugin_css == 'metasync') ? 'checked' : ''
);
printf('
Metasync Style ');
printf('
Choose the default page Style Sheet: Default or MetaSync.
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
*/
add_settings_field(
'enabled_elementor_plugin_css',
'Choose Elementor Font Style',
function() use ($option_key) {
$enabled_elementor_plugin_css = Metasync::get_option('general')['enabled_elementor_plugin_css'] ?? 'default';
printf('
');
printf('Default ');
printf('Custom ');
printf(' ');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'enabled_elementor_plugin_css_color',
'Choose Elementor Font Color',
function() use ($option_key) {
$enabled_elementor_plugin_css_color = Metasync::get_option('general')['enabled_elementor_plugin_css_color'] ?? '#000000';
printf('
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'whitelabel_settings_password',
'Settings Password',
function() use ($option_key) {
$whitelabel_settings = Metasync::get_whitelabel_settings();
$value = $whitelabel_settings['settings_password'] ?? '';
printf('
');
printf('
Set a custom password to protect the branding settings section.
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'whitelabel_recovery_email',
'Recovery Email',
function() use ($option_key) {
$whitelabel_settings = Metasync::get_whitelabel_settings();
$value = $whitelabel_settings['recovery_email'] ?? '';
$has_password = !empty($whitelabel_settings['settings_password']);
printf('
', $has_password ? 'required' : '');
printf('
Email address to receive password recovery. Required when password is set.
');
},
$page_slug . '_branding',
$SECTION_METASYNC
);
add_settings_field(
'whitelabel_quick_links',
'Quick Links',
function() use ($option_key) {
$whitelabel_settings = Metasync::get_whitelabel_settings();
$links = isset($whitelabel_settings['quick_links']) && is_array($whitelabel_settings['quick_links'])
? $whitelabel_settings['quick_links']
: [];
// Ensure at least 5 rows
while (count($links) < 5) {
$links[] = ['label' => '', 'url' => '', 'external' => false];
}
echo '
Add up to 5 links to show in the sidebar Quick Links widget. Leave blank to hide the widget when whitelabeling is active.
';
echo '';
echo '
';
},
$page_slug . '_branding',
$SECTION_METASYNC
);
// ======================================================================
// Schema Markup Settings — dedicated page (SEO → Schema Markup)
// ======================================================================
$schema_page = $page_slug . '_schema-markup';
add_settings_section(
'metasync_schema_org',
'Organization Schema (Site-Wide)',
function() {
echo '
This Organization schema is injected on every page of your site.
';
},
$schema_page
);
add_settings_field(
'schema_org_name',
'Organization Name',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['org_name'] ?? '';
printf('
', esc_attr($value));
},
$schema_page,
'metasync_schema_org'
);
add_settings_field(
'schema_org_url',
'Organization URL',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['org_url'] ?? '';
printf('
', esc_attr($value));
},
$schema_page,
'metasync_schema_org'
);
add_settings_field(
'schema_org_logo',
'Organization Logo URL',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['org_logo'] ?? '';
printf('
', esc_attr($value));
},
$schema_page,
'metasync_schema_org'
);
add_settings_field(
'schema_org_contact_telephone',
'Contact Telephone',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['org_contact_telephone'] ?? '';
printf('
', esc_attr($value));
},
$schema_page,
'metasync_schema_org'
);
add_settings_field(
'schema_org_contact_type',
'Contact Type',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['org_contact_type'] ?? '';
printf('
', esc_attr($value));
},
$schema_page,
'metasync_schema_org'
);
add_settings_field(
'schema_org_same_as',
'Social Profile URLs',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['org_same_as'] ?? '';
printf('
', esc_textarea($value));
echo '
Enter one social profile URL per line.
';
},
$schema_page,
'metasync_schema_org'
);
add_settings_section(
'metasync_schema_website',
'WebSite Schema (Homepage Only)',
function() {
echo '
This WebSite schema is injected on the homepage only. Enables the Google Sitelinks Searchbox.
';
},
$schema_page
);
add_settings_field(
'schema_website_name',
'WebSite Name',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['website_name'] ?? '';
printf('
', esc_attr($value));
},
$schema_page,
'metasync_schema_website'
);
add_settings_field(
'schema_website_url',
'WebSite URL',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['website_url'] ?? '';
printf('
', esc_attr($value));
},
$schema_page,
'metasync_schema_website'
);
add_settings_field(
'schema_website_searchbox',
'Enable Sitelinks Searchbox',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['website_searchbox'] ?? false;
printf(
'
',
checked($value, true, false)
);
echo '
Adds SearchAction to WebSite schema for Google Sitelinks Searchbox. ';
},
$schema_page,
'metasync_schema_website'
);
add_settings_section(
'metasync_schema_compat',
'Compatibility',
function() {
echo '
Control how MetaSync interacts with other plugins that output structured data.
';
},
$schema_page
);
add_settings_field(
'schema_override_woocommerce',
'Override WooCommerce Product Schema',
function() use ($option_key) {
$schema = Metasync::get_option('schema') ?? [];
$value = $schema['override_woocommerce_schema'] ?? false;
printf(
'
',
checked($value, true, false)
);
echo '
When unchecked (default), MetaSync suppresses its Product schema when WooCommerce is active to avoid duplication. Check to override WooCommerce\'s native Product schema. ';
},
$schema_page,
'metasync_schema_compat'
);
// ----------------------------------------------------------------
// Site Verification page
// ----------------------------------------------------------------
add_settings_section(
$SECTION_SEARCHENGINE,
'',
function() {},
$page_slug . '_searchengines-verification'
);
add_settings_field(
'google_site_verification',
'Google Search Console',
array( Metasync_Settings_Fields::instance(), 'google_site_verification_callback' ),
$page_slug . '_searchengines-verification',
$SECTION_SEARCHENGINE
);
add_settings_field(
'bing_site_verification',
'Bing Webmaster Tools',
array( Metasync_Settings_Fields::instance(), 'bing_site_verification_callback' ),
$page_slug . '_searchengines-verification',
$SECTION_SEARCHENGINE
);
add_settings_field(
'yandex_site_verification',
'Yandex Webmaster',
array( Metasync_Settings_Fields::instance(), 'yandex_site_verification_callback' ),
$page_slug . '_searchengines-verification',
$SECTION_SEARCHENGINE
);
add_settings_field(
'pinterest_site_verification',
'Pinterest',
array( Metasync_Settings_Fields::instance(), 'pinterest_site_verification_callback' ),
$page_slug . '_searchengines-verification',
$SECTION_SEARCHENGINE
);
add_settings_field(
'baidu_site_verification',
'Baidu',
function() use ($option_key) {
$value = Metasync::get_option('searchengines')['baidu_site_verification'] ?? '';
printf( '
', esc_attr( $value ) );
},
$page_slug . '_searchengines-verification',
$SECTION_SEARCHENGINE
);
add_settings_field(
'alexa_site_verification',
'Alexa',
function() use ($option_key) {
$value = Metasync::get_option('searchengines')['alexa_site_verification'] ?? '';
printf( '
', esc_attr( $value ) );
},
$page_slug . '_searchengines-verification',
$SECTION_SEARCHENGINE
);
add_settings_field(
'norton_save_site_verification',
'Norton Safe Web',
function() use ($option_key) {
$value = Metasync::get_option('searchengines')['norton_save_site_verification'] ?? '';
printf( '
', esc_attr( $value ) );
},
$page_slug . '_searchengines-verification',
$SECTION_SEARCHENGINE
);
// ----------------------------------------------------------------
// Code Snippets page
// ----------------------------------------------------------------
add_settings_section(
$SECTION_CODESNIPPETS,
'',
function() {},
$page_slug . '_code-snippets'
);
add_settings_field(
'header_snippet',
'Header Code',
array( Metasync_Settings_Fields::instance(), 'header_snippets_callback' ),
$page_slug . '_code-snippets',
$SECTION_CODESNIPPETS
);
add_settings_field(
'footer_snippet',
'Footer Code',
array( Metasync_Settings_Fields::instance(), 'footer_snippets_callback' ),
$page_slug . '_code-snippets',
$SECTION_CODESNIPPETS
);
// ----------------------------------------------------------------
// Local Business page
// ----------------------------------------------------------------
add_settings_section(
$SECTION_LOCALSEO,
'',
function() {},
$page_slug . '_local-seo'
);
$fields_instance = Metasync_Settings_Fields::instance();
add_settings_field( 'local_seo_person_organization', 'Type', array( $fields_instance, 'local_seo_person_organization_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_name', 'Name', array( $fields_instance, 'local_seo_name_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_logo', 'Logo', array( $fields_instance, 'local_seo_logo_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_url', 'Website URL', array( $fields_instance, 'local_seo_url_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_email', 'Email', array( $fields_instance, 'local_seo_email_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_phone', 'Phone', array( $fields_instance, 'local_seo_phone_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_address', 'Address', array( $fields_instance, 'local_seo_address_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_business_type', 'Business Type', array( $fields_instance, 'local_seo_business_type_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_opening_hours', 'Opening Hours', array( $fields_instance, 'local_seo_opening_hours_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_phone_numbers', 'Phone Numbers', array( $fields_instance, 'local_seo_phone_numbers_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_price_range', 'Price Range', array( $fields_instance, 'local_seo_price_range_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_about_page', 'About Page', array( $fields_instance, 'local_seo_about_page_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_contact_page', 'Contact Page', array( $fields_instance, 'local_seo_contact_page_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_map_key', 'Google Maps Key', array( $fields_instance, 'local_seo_map_key_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
add_settings_field( 'local_seo_geo_coordinates', 'Geo Coordinates', array( $fields_instance, 'local_seo_geo_coordinates_callback' ), $page_slug . '_local-seo', $SECTION_LOCALSEO );
// ----------------------------------------------------------------
// Breadcrumbs page
// ----------------------------------------------------------------
add_settings_section(
$SECTION_BREADCRUMBS,
'',
function() {},
$page_slug . '_breadcrumbs'
);
add_settings_field(
'breadcrumbs_enabled',
'Enable Breadcrumbs',
function() use ($option_key) {
$value = Metasync::get_option('breadcrumbs')['enabled'] ?? true;
printf(
'
',
checked(1, $value, false)
);
echo '
Output breadcrumb trail HTML on your site.
';
},
$page_slug . '_breadcrumbs',
$SECTION_BREADCRUMBS
);
add_settings_field(
'breadcrumbs_separator',
'Separator',
function() use ($option_key) {
$value = Metasync::get_option('breadcrumbs')['separator'] ?? '»';
printf(
'
',
esc_attr($value)
);
echo '
Character or HTML entity shown between crumbs (e.g. » or /).
';
},
$page_slug . '_breadcrumbs',
$SECTION_BREADCRUMBS
);
add_settings_field(
'breadcrumbs_home_label',
'Home Label',
function() use ($option_key) {
$value = Metasync::get_option('breadcrumbs')['home_label'] ?? 'Home';
printf(
'
',
esc_attr($value)
);
},
$page_slug . '_breadcrumbs',
$SECTION_BREADCRUMBS
);
add_settings_field(
'breadcrumbs_home_url',
'Home URL',
function() use ($option_key) {
$value = Metasync::get_option('breadcrumbs')['home_url'] ?? '';
printf(
'
',
esc_attr($value)
);
echo '
Leave blank to use the site home URL.
';
},
$page_slug . '_breadcrumbs',
$SECTION_BREADCRUMBS
);
add_settings_field(
'breadcrumbs_show_current_page',
'Show Current Page',
function() use ($option_key) {
$value = Metasync::get_option('breadcrumbs')['show_current_page'] ?? true;
printf(
'
',
checked(1, $value, false)
);
echo '
Include the current page as the last (non-linked) crumb.
';
},
$page_slug . '_breadcrumbs',
$SECTION_BREADCRUMBS
);
add_settings_field(
'breadcrumbs_prefix_text',
'Prefix Text',
function() use ($option_key) {
$value = Metasync::get_option('breadcrumbs')['prefix_text'] ?? '';
printf(
'
',
esc_attr($value)
);
echo '
Optional text before the breadcrumb trail (e.g. "You are here:").
';
},
$page_slug . '_breadcrumbs',
$SECTION_BREADCRUMBS
);
add_settings_field(
'breadcrumbs_archive_label_format',
'Archive Label Format',
function() use ($option_key) {
$value = Metasync::get_option('breadcrumbs')['archive_label_format'] ?? '{name}';
printf(
'
',
esc_attr($value)
);
echo '
Format for archive crumb labels. Use {name} as placeholder for the archive name.
';
},
$page_slug . '_breadcrumbs',
$SECTION_BREADCRUMBS
);
# Open Graph / Article meta toggles — surface each new tag family as its
# own opt-out checkbox on the main Settings page (Open Graph accordion).
add_settings_field(
'og_image_dimensions',
'OG Image Dimensions',
array(Metasync_Settings_Fields::instance(), 'og_image_dimensions_callback'),
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'article_timestamps',
'Article Timestamps',
array(Metasync_Settings_Fields::instance(), 'article_timestamps_callback'),
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'article_author',
'Article Author',
array(Metasync_Settings_Fields::instance(), 'article_author_callback'),
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'article_section',
'Article Section',
array(Metasync_Settings_Fields::instance(), 'article_section_callback'),
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'article_tags',
'Article Tags',
array(Metasync_Settings_Fields::instance(), 'article_tags_callback'),
$page_slug . '_general',
$SECTION_METASYNC
);
add_settings_field(
'twitter_image_alt',
'Twitter Image Alt',
array(Metasync_Settings_Fields::instance(), 'twitter_image_alt_callback'),
$page_slug . '_general',
$SECTION_METASYNC
);
}
/**
* Sanitize each setting field as needed.
*
* Formerly Metasync_Admin::sanitize().
*
* @param array $input Contains all settings fields as array keys
* @return array Sanitized settings
*/
public function sanitize($input)
{
$new_input = Metasync::get_option();
$whitelabel_validation_failed = false;
# Determine which tab is being submitted (same logic as AJAX handler)
# Check POST first (from AJAX), then GET (from form action URL)
$active_tab = 'general';
if (isset($_POST['active_tab'])) {
$active_tab = sanitize_text_field($_POST['active_tab']);
} elseif (isset($_GET['tab'])) {
$active_tab = sanitize_text_field($_GET['tab']);
} elseif (isset($_POST['_wp_http_referer'])) {
# Parse referer URL to get tab parameter
$referer = wp_parse_url($_POST['_wp_http_referer']);
if (isset($referer['query'])) {
parse_str($referer['query'], $referer_params);
if (isset($referer_params['tab'])) {
$active_tab = sanitize_text_field($referer_params['tab']);
}
}
}
$general_tab_submitted = ($active_tab === 'general');
// General Settings
if (isset($input['general']['apikey'])) {
$new_input['general']['apikey'] = sanitize_text_field($input['general']['apikey']);
}
// Meta Box Visibility Settings
if (isset($input['general']) && $general_tab_submitted) {
$checkbox_fields = [
'disable_common_robots_metabox',
'disable_advance_robots_metabox',
'disable_redirection_metabox',
'disable_canonical_metabox',
'disable_social_opengraph_metabox',
'disable_schema_markup_metabox',
'open_external_links'
];
foreach ($checkbox_fields as $field) {
if (isset($input['general'][$field])) {
$new_input['general'][$field] = filter_var($input['general'][$field], FILTER_VALIDATE_BOOLEAN);
} else {
$new_input['general'][$field] = false;
}
// The final array_merge($new_input, $input) is shallow, so
// $input['general'] replaces $new_input['general'] wholesale.
// Write the sanitized boolean back into $input so it survives.
$input['general'][$field] = $new_input['general'][$field];
}
}
// Site Verification Settings
if (isset($input['searchengines']['bing_site_verification'])) {
$new_input['searchengines']['bing_site_verification'] = sanitize_text_field($input['searchengines']['bing_site_verification']);
}
if (isset($input['searchengines']['baidu_site_verification'])) {
$new_input['searchengines']['baidu_site_verification'] = sanitize_text_field($input['searchengines']['baidu_site_verification']);
}
if (isset($input['searchengines']['alexa_site_verification'])) {
$new_input['searchengines']['alexa_site_verification'] = sanitize_text_field($input['searchengines']['alexa_site_verification']);
}
if (isset($input['searchengines']['yandex_site_verification'])) {
$new_input['searchengines']['yandex_site_verification'] = sanitize_text_field($input['searchengines']['yandex_site_verification']);
}
if (isset($input['searchengines']['google_site_verification'])) {
$new_input['searchengines']['google_site_verification'] = sanitize_text_field($input['searchengines']['google_site_verification']);
}
if (isset($input['searchengines']['pinterest_site_verification'])) {
$new_input['searchengines']['pinterest_site_verification'] = sanitize_text_field($input['searchengines']['pinterest_site_verification']);
}
if (isset($input['searchengines']['norton_save_site_verification'])) {
$new_input['searchengines']['norton_save_site_verification'] = sanitize_text_field($input['searchengines']['norton_save_site_verification']);
}
// Local Business SEO Settings
if (isset($input['localseo']['local_seo_person_organization'])) {
$new_input['localseo']['local_seo_person_organization'] = sanitize_text_field($input['localseo']['local_seo_person_organization']);
}
if (isset($input['localseo']['local_seo_name'])) {
$new_input['localseo']['local_seo_name'] = sanitize_text_field($input['localseo']['local_seo_name']);
}
if (isset($input['localseo']['local_seo_logo'])) {
$new_input['localseo']['local_seo_logo'] = sanitize_url($input['localseo']['local_seo_logo']);
}
if (isset($input['localseo']['local_seo_url'])) {
$new_input['localseo']['local_seo_url'] = sanitize_url($input['localseo']['local_seo_url']);
}
if (isset($input['localseo']['local_seo_email'])) {
$new_input['localseo']['local_seo_email'] = sanitize_email($input['localseo']['local_seo_email']);
}
if (isset($input['localseo']['local_seo_phone'])) {
$new_input['localseo']['local_seo_phone'] = sanitize_text_field($input['localseo']['local_seo_phone']);
}
if (isset($input['localseo']['address']['street'])) {
$new_input['localseo']['address']['street'] = sanitize_text_field($input['localseo']['address']['street']);
}
if (isset($input['localseo']['address']['locality'])) {
$new_input['localseo']['address']['locality'] = sanitize_text_field($input['localseo']['address']['locality']);
}
if (isset($input['localseo']['address']['region'])) {
$new_input['localseo']['address']['region'] = sanitize_text_field($input['localseo']['address']['region']);
}
if (isset($input['localseo']['address']['postancode'])) {
$new_input['localseo']['address']['postancode'] = sanitize_text_field($input['localseo']['address']['postancode']);
}
if (isset($input['localseo']['address']['country'])) {
$new_input['localseo']['address']['country'] = sanitize_text_field($input['localseo']['address']['country']);
}
if (isset($input['localseo']['local_seo_business_type'])) {
$new_input['localseo']['local_seo_business_type'] = sanitize_text_field($input['localseo']['local_seo_business_type']);
}
if (isset($input['localseo']['local_seo_hours_format'])) {
$new_input['localseo']['local_seo_hours_format'] = sanitize_text_field($input['localseo']['local_seo_hours_format']);
}
if (isset($input['localseo']['days'])) {
$new_input['localseo']['days'] = sanitize_text_field($input['localseo']['days']);
}
if (isset($input['localseo']['times'])) {
$new_input['localseo']['times'] = sanitize_text_field($input['localseo']['times']);
}
if (isset($input['localseo']['phonetype'])) {
$new_input['localseo']['phonetype'] = sanitize_text_field($input['localseo']['phonetype']);
}
if (isset($input['localseo']['phonenumber'])) {
$new_input['localseo']['phonenumber'] = sanitize_text_field($input['localseo']['phonenumber']);
}
if (isset($input['localseo']['local_seo_price_range'])) {
$new_input['localseo']['local_seo_price_range'] = sanitize_text_field($input['localseo']['local_seo_price_range']);
}
if (isset($input['localseo']['local_seo_about_page'])) {
$new_input['localseo']['local_seo_about_page'] = sanitize_text_field($input['localseo']['local_seo_about_page']);
}
if (isset($input['localseo']['local_seo_contact_page'])) {
$new_input['localseo']['local_seo_contact_page'] = sanitize_text_field($input['localseo']['local_seo_contact_page']);
}
if (isset($input['localseo']['local_seo_map_key'])) {
$new_input['localseo']['local_seo_map_key'] = sanitize_text_field($input['localseo']['local_seo_map_key']);
}
if (isset($input['localseo']['local_seo_geo_coordinates'])) {
$new_input['localseo']['local_seo_geo_coordinates'] = sanitize_text_field($input['localseo']['local_seo_geo_coordinates']);
}
// Breadcrumbs Settings
if (isset($input['breadcrumbs'])) {
$new_input['breadcrumbs']['enabled'] = !empty($input['breadcrumbs']['enabled']);
$new_input['breadcrumbs']['show_current_page'] = !empty($input['breadcrumbs']['show_current_page']);
$new_input['breadcrumbs']['disable_schema'] = !empty($input['breadcrumbs']['disable_schema']);
if (isset($input['breadcrumbs']['separator'])) {
$new_input['breadcrumbs']['separator'] = sanitize_text_field($input['breadcrumbs']['separator']);
}
if (isset($input['breadcrumbs']['home_label'])) {
$new_input['breadcrumbs']['home_label'] = sanitize_text_field($input['breadcrumbs']['home_label']);
}
if (isset($input['breadcrumbs']['home_url'])) {
$new_input['breadcrumbs']['home_url'] = esc_url_raw($input['breadcrumbs']['home_url']);
}
if (isset($input['breadcrumbs']['prefix_text'])) {
$new_input['breadcrumbs']['prefix_text'] = sanitize_text_field($input['breadcrumbs']['prefix_text']);
}
if (isset($input['breadcrumbs']['archive_label_format'])) {
$new_input['breadcrumbs']['archive_label_format'] = sanitize_text_field($input['breadcrumbs']['archive_label_format']);
}
// Remove raw breadcrumbs so sanitized values (especially checkbox
// booleans) survive the shallow array_merge($new_input, $input).
unset($input['breadcrumbs']);
}
// Code Snippets Settings
if (isset($input['codesnippets']['header_snippet'])) {
$new_input['codesnippets']['header_snippet'] = sanitize_text_field($input['codesnippets']['header_snippet']);
}
if (isset($input['codesnippets']['footer_snippet'])) {
$new_input['codesnippets']['footer_snippet'] = sanitize_text_field($input['codesnippets']['footer_snippet']);
}
// Optimal Settings
if (isset($input['optimal_settings']['no_index_posts'])) {
$new_input['optimal_settings']['no_index_posts'] = boolval($input['optimal_settings']['no_index_posts']);
}
if (isset($input['optimal_settings']['no_follow_links'])) {
$new_input['optimal_settings']['no_follow_links'] = boolval($input['optimal_settings']['no_follow_links']);
}
if (isset($input['optimal_settings']['open_external_links'])) {
$new_input['optimal_settings']['open_external_links'] = boolval($input['optimal_settings']['open_external_links']);
}
if (isset($input['optimal_settings']['add_alt_image_tags'])) {
$new_input['optimal_settings']['add_alt_image_tags'] = boolval($input['optimal_settings']['add_alt_image_tags']);
}
if (isset($input['optimal_settings']['add_title_image_tags'])) {
$new_input['optimal_settings']['add_title_image_tags'] = boolval($input['optimal_settings']['add_title_image_tags']);
}
// Site Information - Optimal Settings
if (isset($input['optimal_settings']['site_info']['type'])) {
$new_input['optimal_settings']['site_info']['type'] = sanitize_text_field($input['optimal_settings']['site_info']['type']);
}
if (isset($input['optimal_settings']['site_info']['business_type'])) {
$new_input['optimal_settings']['site_info']['business_type'] = sanitize_text_field($input['optimal_settings']['site_info']['business_type']);
}
if (isset($input['optimal_settings']['site_info']['company_name'])) {
$new_input['optimal_settings']['site_info']['company_name'] = sanitize_text_field($input['optimal_settings']['site_info']['company_name']);
}
if (isset($input['optimal_settings']['site_info']['google_logo'])) {
$new_input['optimal_settings']['site_info']['google_logo'] = sanitize_url($input['optimal_settings']['site_info']['google_logo']);
}
if (isset($input['optimal_settings']['site_info']['social_share_image'])) {
$new_input['optimal_settings']['site_info']['social_share_image'] = sanitize_url($input['optimal_settings']['site_info']['social_share_image']);
}
// Common Setting - Global Settings
$common_robots_field = isset($input['common_robots_meta']) ? 'common_robots_meta' : 'common_robots_mata';
if (isset($input[$common_robots_field]['index'])) {
$new_input['common_robots_meta']['index'] = boolval($input[$common_robots_field]['index']);
}
if (isset($input[$common_robots_field]['noindex'])) {
$new_input['common_robots_meta']['noindex'] = boolval($input[$common_robots_field]['noindex']);
}
if (isset($input[$common_robots_field]['nofollow'])) {
$new_input['common_robots_meta']['nofollow'] = boolval($input[$common_robots_field]['nofollow']);
}
if (isset($input[$common_robots_field]['noarchive'])) {
$new_input['common_robots_meta']['noarchive'] = boolval($input[$common_robots_field]['noarchive']);
}
if (isset($input[$common_robots_field]['noimageindex'])) {
$new_input['common_robots_meta']['noimageindex'] = boolval($input[$common_robots_field]['noimageindex']);
}
if (isset($input[$common_robots_field]['nosnippet'])) {
$new_input['common_robots_meta']['nosnippet'] = boolval($input[$common_robots_field]['nosnippet']);
}
// Advance Setting - Global Settings
$advance_robots_field = isset($input['advance_robots_meta']) ? 'advance_robots_meta' : 'advance_robots_mata';
if (isset($input[$advance_robots_field]['max-snippet']['enable'])) {
$new_input['advance_robots_meta']['max-snippet']['enable'] = boolval($input[$advance_robots_field]['max-snippet']['enable']);
}
if (isset($input[$advance_robots_field]['max-snippet']['length'])) {
$new_input['advance_robots_meta']['max-snippet']['length'] = sanitize_text_field($input[$advance_robots_field]['max-snippet']['length']);
}
if (isset($input[$advance_robots_field]['max-video-preview']['enable'])) {
$new_input['advance_robots_meta']['max-video-preview']['enable'] = boolval($input[$advance_robots_field]['max-video-preview']['enable']);
}
if (isset($input[$advance_robots_field]['max-video-preview']['length'])) {
$new_input['advance_robots_meta']['max-video-preview']['length'] = sanitize_text_field($input[$advance_robots_field]['max-video-preview']['length']);
}
if (isset($input[$advance_robots_field]['max-image-preview']['enable'])) {
$new_input['advance_robots_meta']['max-image-preview']['enable'] = boolval($input[$advance_robots_field]['max-image-preview']['enable']);
}
if (isset($input[$advance_robots_field]['max-image-preview']['length'])) {
$new_input['advance_robots_meta']['max-image-preview']['length'] = sanitize_text_field($input[$advance_robots_field]['max-image-preview']['length']);
}
// Social meta settings
if (isset($input['social_meta']['facebook_page_url'])) {
$new_input['social_meta']['facebook_page_url'] = sanitize_text_field($input['social_meta']['facebook_page_url']);
}
if (isset($input['social_meta']['facebook_authorship'])) {
$new_input['social_meta']['facebook_authorship'] = sanitize_text_field($input['social_meta']['facebook_authorship']);
}
if (isset($input['social_meta']['facebook_admin'])) {
$new_input['social_meta']['facebook_admin'] = sanitize_text_field($input['social_meta']['facebook_admin']);
}
if (isset($input['social_meta']['facebook_app'])) {
$new_input['social_meta']['facebook_app'] = sanitize_text_field($input['social_meta']['facebook_app']);
}
if (isset($input['social_meta']['facebook_secret'])) {
$new_input['social_meta']['facebook_secret'] = sanitize_text_field($input['social_meta']['facebook_secret']);
}
if (isset($input['social_meta']['twitter_username'])) {
$new_input['social_meta']['twitter_username'] = sanitize_text_field($input['social_meta']['twitter_username']);
}
// Common Meta Settings — Open Graph / article toggle checkboxes.
// Unchecked HTML checkboxes are absent from $input, so an explicit
// 'true'/'false' round-trip is required to persist opt-outs.
if (isset($input['common_meta_settings'])) {
$og_toggle_fields = [
'og_image_dimensions',
'article_timestamps',
'article_author',
'article_section',
'article_tags',
'twitter_image_alt',
];
foreach ($og_toggle_fields as $field) {
$new_input['common_meta_settings'][$field] = (isset($input['common_meta_settings'][$field]) && $input['common_meta_settings'][$field] === 'true') ? 'true' : 'false';
}
// Remove raw common_meta_settings so sanitized toggle values
// survive the shallow array_merge($new_input, $input).
unset($input['common_meta_settings']);
}
# Handle whitelabel URL fields with improved empty value handling
if (isset($input['whitelabel'])) {
$existing_whitelabel = Metasync::get_option()['whitelabel'] ?? [];
$general_input = $input['general'] ?? [];
$plugin_name = isset($general_input['white_label_plugin_name']) ? trim((string) $general_input['white_label_plugin_name']) : '';
$logo_light = isset($input['whitelabel']['logo_light']) ? trim((string) $input['whitelabel']['logo_light']) : '';
$logo_dark = isset($input['whitelabel']['logo_dark']) ? trim((string) $input['whitelabel']['logo_dark']) : '';
$author = isset($general_input['white_label_plugin_author']) ? trim((string) $general_input['white_label_plugin_author']) : '';
$author_uri = isset($general_input['white_label_plugin_author_uri']) ? trim((string) $general_input['white_label_plugin_author_uri']) : '';
$plugin_uri = isset($general_input['white_label_plugin_uri']) ? trim((string) $general_input['white_label_plugin_uri']) : '';
$domain = isset($input['whitelabel']['domain']) ? trim((string) $input['whitelabel']['domain']) : '';
$description = isset($general_input['white_label_plugin_description']) ? trim((string) $general_input['white_label_plugin_description']) : '';
$has_core_field = (!empty($plugin_name) || (!empty($logo_light) && filter_var($logo_light, FILTER_VALIDATE_URL)) || (!empty($logo_dark) && filter_var($logo_dark, FILTER_VALIDATE_URL)) || !empty($author));
$has_optional_only = (!empty($author_uri) || !empty($plugin_uri) || (!empty($domain) && filter_var($domain, FILTER_VALIDATE_URL)) || !empty($description));
if ($has_optional_only && !$has_core_field) {
add_settings_error(
'metasync_options',
'whitelabel_partial',
'Add at least one of: Plugin Name, Logo URL, or Author to save whitelabel settings.',
'error'
);
$new_input['whitelabel'] = $existing_whitelabel;
$whitelabel_validation_failed = true;
} else {
$new_input['whitelabel'] = $existing_whitelabel;
if (isset($input['whitelabel']['logo'])) {
$logo_value = trim($input['whitelabel']['logo']);
if (!empty($logo_value) && filter_var($logo_value, FILTER_VALIDATE_URL)) {
$new_input['whitelabel']['logo'] = esc_url_raw($logo_value);
} else {
$new_input['whitelabel']['logo'] = '';
}
}
if (isset($input['whitelabel']['logo_light'])) {
$logo_light_value = trim($input['whitelabel']['logo_light']);
if (!empty($logo_light_value) && filter_var($logo_light_value, FILTER_VALIDATE_URL)) {
$new_input['whitelabel']['logo_light'] = esc_url_raw($logo_light_value);
} else {
$new_input['whitelabel']['logo_light'] = '';
}
}
if (isset($input['whitelabel']['logo_dark'])) {
$logo_dark_value = trim($input['whitelabel']['logo_dark']);
if (!empty($logo_dark_value) && filter_var($logo_dark_value, FILTER_VALIDATE_URL)) {
$new_input['whitelabel']['logo_dark'] = esc_url_raw($logo_dark_value);
} else {
$new_input['whitelabel']['logo_dark'] = '';
}
}
if (isset($input['whitelabel']['domain'])) {
$domain_value = trim($input['whitelabel']['domain']);
if (!empty($domain_value) && filter_var($domain_value, FILTER_VALIDATE_URL)) {
$new_input['whitelabel']['domain'] = esc_url_raw($domain_value);
} else {
$old_domain = $existing_whitelabel['domain'] ?? '';
$new_input['whitelabel']['domain'] = '';
if (!empty($old_domain)) {
$new_input['_trigger_heartbeat_after_save'] = 'Domain cleared from: ' . $old_domain;
}
}
}
if (isset($input['whitelabel']['settings_password'])) {
$password_value = trim($input['whitelabel']['settings_password']);
$new_input['whitelabel']['settings_password'] = sanitize_text_field($password_value);
} else {
if (isset($existing_whitelabel['settings_password'])) {
$new_input['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
}
}
if (isset($input['whitelabel']['recovery_email'])) {
$recovery_email = trim($input['whitelabel']['recovery_email']);
if (!empty($recovery_email)) {
$sanitized_email = sanitize_email($recovery_email);
if (is_email($sanitized_email)) {
$new_input['whitelabel']['recovery_email'] = $sanitized_email;
} else {
add_settings_error(
'metasync_messages',
'metasync_message',
__('Invalid recovery email address.', 'metasync'),
'error'
);
}
} else {
$new_input['whitelabel']['recovery_email'] = '';
}
} else {
if (isset($existing_whitelabel['recovery_email'])) {
$new_input['whitelabel']['recovery_email'] = $existing_whitelabel['recovery_email'];
}
}
$has_password = !empty($new_input['whitelabel']['settings_password']);
$has_recovery_email = !empty($new_input['whitelabel']['recovery_email']);
if ($has_password && !$has_recovery_email) {
add_settings_error(
'metasync_messages',
'metasync_message',
__('Password Recovery Email is required when a password is set.', 'metasync'),
'error'
);
$new_input['whitelabel']['settings_password'] = '';
}
if (isset($input['whitelabel']['access_control'])) {
$new_input['whitelabel']['access_control'] = Metasync_Access_Control::sanitize_access_control($input['whitelabel']['access_control']);
}
// Sanitize quick_links (up to 5 entries)
if (isset($input['whitelabel']['quick_links']) && is_array($input['whitelabel']['quick_links'])) {
$sanitized_links = [];
foreach (array_slice($input['whitelabel']['quick_links'], 0, 5) as $link) {
$url = isset($link['url']) ? esc_url_raw(trim((string) $link['url'])) : '';
if (empty($url)) {
continue; // skip blank rows
}
$sanitized_links[] = [
'label' => sanitize_text_field($link['label'] ?? ''),
'url' => $url,
'external' => !empty($link['external']),
];
}
$new_input['whitelabel']['quick_links'] = $sanitized_links;
} else {
// Preserve existing links when not submitted
if (isset($existing_whitelabel['quick_links'])) {
$new_input['whitelabel']['quick_links'] = $existing_whitelabel['quick_links'];
}
}
// Sanitize color palette
if (isset($input['whitelabel']['color_palette']) && is_array($input['whitelabel']['color_palette'])) {
$allowed_vars = array(
'dashboard-bg', 'dashboard-card-bg', 'dashboard-card-hover',
'dashboard-text-primary', 'dashboard-text-secondary',
'dashboard-accent', 'dashboard-accent-hover',
'dashboard-success', 'dashboard-warning', 'dashboard-error',
'dashboard-border',
'dashboard-gradient-primary-from', 'dashboard-gradient-primary-to',
'dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'
);
$sanitized_palette = array();
foreach (array('dark', 'light') as $theme_key) {
if (isset($input['whitelabel']['color_palette'][$theme_key]) && is_array($input['whitelabel']['color_palette'][$theme_key])) {
$sanitized_palette[$theme_key] = array();
foreach ($input['whitelabel']['color_palette'][$theme_key] as $var_name => $color_value) {
if (in_array($var_name, $allowed_vars, true)) {
$color_value = trim(sanitize_text_field($color_value));
if ($color_value !== '' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color_value)) {
$sanitized_palette[$theme_key][$var_name] = $color_value;
}
}
}
}
}
$new_input['whitelabel']['color_palette'] = $sanitized_palette;
} else {
if (isset($existing_whitelabel['color_palette'])) {
$new_input['whitelabel']['color_palette'] = $existing_whitelabel['color_palette'];
}
}
$new_input['whitelabel']['updated_at'] = time();
}
} else {
$existing_whitelabel = Metasync::get_option()['whitelabel'] ?? [];
$has_existing_whitelabel = !empty($existing_whitelabel['domain']) || !empty($existing_whitelabel['logo']);
if ($has_existing_whitelabel) {
$new_input['whitelabel'] = [
'is_whitelabel' => false,
'domain' => '',
'logo' => '',
'company_name' => '',
'updated_at' => time()
];
if (!empty($existing_whitelabel['domain'])) {
delete_transient('metasync_heartbeat_status_cache');
do_action('metasync_trigger_immediate_heartbeat', 'Whitelabel settings cleared - domain changed to default');
}
}
}
// Indexation Control Settings
if (isset($input['seo_controls']['index_date_archives'])) {
$new_input['seo_controls']['index_date_archives'] = boolval($input['seo_controls']['index_date_archives']);
}
if (isset($input['seo_controls']['index_tag_archives'])) {
$new_input['seo_controls']['index_tag_archives'] = boolval($input['seo_controls']['index_tag_archives']);
}
if (isset($input['seo_controls']['index_author_archives'])) {
$new_input['seo_controls']['index_author_archives'] = boolval($input['seo_controls']['index_author_archives']);
}
if (isset($input['seo_controls']['index_format_archives'])) {
$new_input['seo_controls']['index_format_archives'] = boolval($input['seo_controls']['index_format_archives']);
}
// Handle post-save heartbeat trigger for domain changes
if (isset($new_input['_trigger_heartbeat_after_save'])) {
$context = $new_input['_trigger_heartbeat_after_save'];
unset($new_input['_trigger_heartbeat_after_save']);
add_action('updated_option_metasync_options', function() use ($context) {
delete_transient('metasync_heartbeat_status_cache');
do_action('metasync_trigger_immediate_heartbeat', 'Whitelabel domain change - ' . $context);
});
}
$result = array_merge($new_input, $input);
if ($whitelabel_validation_failed) {
$result['whitelabel'] = Metasync::get_option()['whitelabel'] ?? [];
}
delete_transient('metasync_otto_js_detected');
return $result;
}
/**
* AJAX handler for saving general/whitelabel/advanced settings.
*
* Moved from Metasync_Admin – the admin class now delegates here.
*/
public function meta_sync_save_settings()
{
if (!isset($_POST['meta_sync_nonce']) || !wp_verify_nonce($_POST['meta_sync_nonce'], 'meta_sync_general_setting_nonce')) {
wp_send_json_error(array('message' => 'Invalid nonce'));
return;
}
if (!Metasync::current_user_has_plugin_access()) {
wp_send_json_error(['message' => 'Insufficient permissions.'], 403);
return;
}
$text_fields = [
'searchatlas_api_key', 'apikey',
'white_label_plugin_name', 'white_label_plugin_description',
'white_label_plugin_author', 'white_label_plugin_menu_slug',
'white_label_plugin_menu_icon', 'enabled_plugin_css',
'enabled_elementor_plugin_css_color','enabled_elementor_plugin_css',
'otto_pixel_uuid','periodic_clear_otto_cache','periodic_clear_ottopage_cache',
'periodic_clear_ottopost_cache', 'whitelabel_otto_name', 'otto_wp_rocket_compat'
];
$url_fields = [
'white_label_plugin_author_uri', 'white_label_plugin_uri'
];
$bool_fields = ['otto_disable_on_loggedin', 'otto_disable_preview_button', 'otto_disable_for_bots' , 'hide_dashboard_framework', 'show_admin_bar_status', 'enable_auto_updates', 'disable_common_robots_metabox', 'disable_advance_robots_metabox', 'disable_redirection_metabox', 'disable_canonical_metabox', 'disable_social_opengraph_metabox', 'disable_schema_markup_metabox', 'open_external_links'];
$url_fields = ['white_label_plugin_author_uri', 'white_label_plugin_uri'];
$metasync_options = Metasync::get_option();
if (!is_array($metasync_options)) {
$metasync_options = array();
}
if (!isset($metasync_options['general']) || !is_array($metasync_options['general'])) {
$metasync_options['general'] = array();
}
$validation_errors = [];
$active_tab = isset($_POST['active_tab']) ? sanitize_text_field($_POST['active_tab']) : 'general';
$general_tab_submitted = ($active_tab === 'general');
$whitelabel_tab_submitted = ($active_tab === 'whitelabel');
foreach ($text_fields as $field) {
if (isset($_POST['metasync_options']['general'][$field])) {
$value = trim($_POST['metasync_options']['general'][$field]);
// searchatlas_api_key must be clearable to support key removal (WP-334)
$clearable_fields = [
'searchatlas_api_key',
'white_label_plugin_name',
'white_label_plugin_description',
'white_label_plugin_author',
'white_label_plugin_menu_slug',
'white_label_plugin_menu_icon',
'whitelabel_otto_name',
'otto_pixel_uuid'
];
if ($value === '' && !in_array($field, $clearable_fields)) {
continue;
}
if ($field === 'white_label_plugin_name') {
if (strlen($value) > 16) {
$validation_errors[$field] = 'Plugin name must not exceed 16 characters';
continue;
}
}
if ($field === 'white_label_plugin_menu_icon') {
if ($value === '') {
$metasync_options['general'][$field] = '';
} elseif (filter_var($value, FILTER_VALIDATE_URL)) {
$image_extensions = ['png', 'svg'];
$path = parse_url($value, PHP_URL_PATH);
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
if (in_array($extension, $image_extensions, true)) {
$metasync_options['general'][$field] = esc_url_raw($value);
} else {
$validation_errors[] = 'Invalid Menu icon format. Only PNG and SVG are allowed.';
}
} else {
$validation_errors[] = 'Invalid Menu icon URL format.';
}
} else {
$metasync_options['general'][$field] = sanitize_text_field($_POST['metasync_options']['general'][$field]);
}
}
}
$textarea_fields = ['otto_bot_whitelist', 'otto_bot_blacklist'];
if ($general_tab_submitted) {
foreach ($textarea_fields as $field) {
if (isset($_POST['metasync_options']['general'][$field])) {
$metasync_options['general'][$field] = sanitize_textarea_field($_POST['metasync_options']['general'][$field]);
} else {
$metasync_options['general'][$field] = '';
}
}
}
if ($general_tab_submitted) {
foreach ($bool_fields as $field) {
if (isset($_POST['metasync_options']['general'][$field])) {
$metasync_options['general'][$field] = filter_var($_POST['metasync_options']['general'][$field], FILTER_VALIDATE_BOOLEAN);
}else {
$metasync_options['general'][$field] = false;
}
}
}
if ($general_tab_submitted || $whitelabel_tab_submitted) {
foreach ($url_fields as $field) {
if (isset($_POST['metasync_options']['general'][$field])) {
$value = trim($_POST['metasync_options']['general'][$field]);
$whitelabel_clearable_url_fields = [
'white_label_plugin_author_uri',
'white_label_plugin_uri'
];
if ($value === '' && !in_array($field, $whitelabel_clearable_url_fields)) {
continue;
}
if ($value === '' && in_array($field, $whitelabel_clearable_url_fields)) {
$metasync_options['general'][$field] = '';
continue;
}
if (filter_var($value, FILTER_VALIDATE_URL)) {
$parsed_url = parse_url($value);
$host = isset($parsed_url['host']) ? $parsed_url['host'] : '';
if (strpos($host, '.') !== false || filter_var($host, FILTER_VALIDATE_IP)) {
$metasync_options['general'][$field] = esc_url_raw($value);
} else {
$field_name = ($field === 'white_label_plugin_author_uri') ? 'Author URL' : 'Plugin URL';
$validation_errors[] = 'Invalid ' . $field_name . ' format. Please use a proper domain name (e.g., example.com).';
}
} else {
$field_name = ($field === 'white_label_plugin_author_uri') ? 'Author URL' : 'Plugin URL';
$validation_errors[] = 'Invalid ' . $field_name . ' format.';
}
}
}
}
if ($general_tab_submitted) {
if (isset($_POST['metasync_options']['general']['content_genius_sync_roles']) && is_array($_POST['metasync_options']['general']['content_genius_sync_roles'])) {
$metasync_options['general']['content_genius_sync_roles'] = array_map('sanitize_text_field', $_POST['metasync_options']['general']['content_genius_sync_roles']);
} else {
$metasync_options['general']['content_genius_sync_roles'] = array();
}
}
if ($general_tab_submitted) {
$breadcrumbs_input = isset($_POST['metasync_options']['breadcrumbs']) && is_array($_POST['metasync_options']['breadcrumbs'])
? $_POST['metasync_options']['breadcrumbs']
: array();
if (!isset($metasync_options['breadcrumbs']) || !is_array($metasync_options['breadcrumbs'])) {
$metasync_options['breadcrumbs'] = array();
}
$metasync_options['breadcrumbs']['enabled'] = !empty($breadcrumbs_input['enabled']);
$metasync_options['breadcrumbs']['disable_schema'] = !empty($breadcrumbs_input['disable_schema']);
$metasync_options['breadcrumbs']['show_current_page'] = !empty($breadcrumbs_input['show_current_page']);
$metasync_options['breadcrumbs']['separator'] = isset($breadcrumbs_input['separator']) ? sanitize_text_field($breadcrumbs_input['separator']) : '»';
$metasync_options['breadcrumbs']['home_label'] = isset($breadcrumbs_input['home_label']) ? sanitize_text_field($breadcrumbs_input['home_label']) : 'Home';
$metasync_options['breadcrumbs']['home_url'] = isset($breadcrumbs_input['home_url']) ? esc_url_raw($breadcrumbs_input['home_url']) : '';
$metasync_options['breadcrumbs']['prefix_text'] = isset($breadcrumbs_input['prefix_text']) ? sanitize_text_field($breadcrumbs_input['prefix_text']) : '';
$metasync_options['breadcrumbs']['archive_label_format'] = isset($breadcrumbs_input['archive_label_format']) ? sanitize_text_field($breadcrumbs_input['archive_label_format']) : '{name}';
}
// Open Graph / article toggle checkboxes live under common_meta_settings.
// Unchecked HTML checkboxes are absent from $_POST, so we explicitly
// write 'false' for any missing toggle to persist opt-outs.
if ($general_tab_submitted) {
$og_toggle_fields = [
'open_graph_meta_tags',
'facebook_meta_tags',
'twitter_meta_tags',
'og_image_dimensions',
'article_timestamps',
'article_author',
'article_section',
'article_tags',
'twitter_image_alt',
];
if (!isset($metasync_options['common_meta_settings']) || !is_array($metasync_options['common_meta_settings'])) {
$metasync_options['common_meta_settings'] = [];
}
foreach ($og_toggle_fields as $field) {
$posted = isset($_POST['metasync_options']['common_meta_settings'][$field])
? sanitize_text_field($_POST['metasync_options']['common_meta_settings'][$field])
: '';
$metasync_options['common_meta_settings'][$field] = ($posted === 'true') ? 'true' : 'false';
}
}
if ($general_tab_submitted && isset($_POST['metasync_options']['general']['default_page_builder'])) {
require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php';
$available_builders = Metasync_HTML_To_Builder_Converter::get_available_builders();
$builder_value = sanitize_text_field($_POST['metasync_options']['general']['default_page_builder']);
if (isset($available_builders[$builder_value]) && $available_builders[$builder_value]['detected']) {
$metasync_options['general']['default_page_builder'] = $builder_value;
} else {
$metasync_options['general']['default_page_builder'] = 'gutenberg';
if (isset($available_builders[$builder_value]) && !$available_builders[$builder_value]['detected']) {
$validation_errors[] = sprintf(__('Builder "%s" is not detected on this site. Reverted to Gutenberg.', 'metasync'), $available_builders[$builder_value]['label']);
}
}
}
if ($whitelabel_tab_submitted && isset($_POST['metasync_options']['whitelabel'])) {
$gp = isset($_POST['metasync_options']['general']) && is_array($_POST['metasync_options']['general']) ? $_POST['metasync_options']['general'] : [];
$wp = isset($_POST['metasync_options']['whitelabel']) && is_array($_POST['metasync_options']['whitelabel']) ? $_POST['metasync_options']['whitelabel'] : [];
$plugin_name = isset($gp['white_label_plugin_name']) ? trim((string) $gp['white_label_plugin_name']) : '';
$logo_light = isset($wp['logo_light']) ? trim((string) $wp['logo_light']) : '';
$logo_dark = isset($wp['logo_dark']) ? trim((string) $wp['logo_dark']) : '';
$author = isset($gp['white_label_plugin_author']) ? trim((string) $gp['white_label_plugin_author']) : '';
$author_uri = isset($gp['white_label_plugin_author_uri']) ? trim((string) $gp['white_label_plugin_author_uri']) : '';
$plugin_uri = isset($gp['white_label_plugin_uri']) ? trim((string) $gp['white_label_plugin_uri']) : '';
$domain = isset($wp['domain']) ? trim((string) $wp['domain']) : '';
$description = isset($gp['white_label_plugin_description']) ? trim((string) $gp['white_label_plugin_description']) : '';
$has_core_field = (!empty($plugin_name) || (!empty($logo_light) && filter_var($logo_light, FILTER_VALIDATE_URL)) || (!empty($logo_dark) && filter_var($logo_dark, FILTER_VALIDATE_URL)) || !empty($author));
$has_optional_only = (!empty($author_uri) || !empty($plugin_uri) || (!empty($domain) && filter_var($domain, FILTER_VALIDATE_URL)) || !empty($description));
if ($has_optional_only && !$has_core_field) {
$validation_errors[] = 'Add at least one of: Plugin Name, Logo URL, or Author to save whitelabel settings.';
}
}
if (!empty($validation_errors)) {
wp_send_json_error([
'errors' => $validation_errors
]);
return;
}
$old_options = Metasync::get_option('general') ?? [];
$old_api_key = $old_options['searchatlas_api_key'] ?? '';
$api_key_field_present = isset( $_POST['metasync_options']['general']['searchatlas_api_key'] );
$api_key_validated = null; // null = not checked, true = valid, false = rejected
$is_connected = null;
$api_key_warning = null;
$proposed_new_api_key = $metasync_options['general']['searchatlas_api_key'] ?? '';
// WP-308: When the API key changes, save it first, then POST heartbeat to register
// the WPWebsiteHeartbeat row on the backend, then GET ping to verify.
// The old flow (WP-239) did GET ping first, which failed because the row didn't exist yet.
if ( $api_key_field_present && $proposed_new_api_key !== '' && $proposed_new_api_key !== $old_api_key ) {
// Step 1: Save the new key so SyncCustomerParams can use it
Metasync::set_option($metasync_options);
// Step 2: POST heartbeat to register the site (creates WPWebsiteHeartbeat row)
$sync_request = new Metasync_Sync_Requests();
$sync_response = $sync_request->SyncCustomerParams();
$sync_status = Metasync_Sync_Requests::get_response_code($sync_response);
if ( $sync_status == 200 ) {
// Step 3: GET ping to verify registration succeeded
$ping = Metasync_Heartbeat_Manager::instance()->validate_api_key( $proposed_new_api_key );
if ( $ping['connected'] === true ) {
$api_key_validated = true;
$is_connected = true;
if ( ! empty( $ping['otto_pixel_uuid'] ) ) {
$existing_uuid = $metasync_options['general']['otto_pixel_uuid'] ?? '';
if ( empty( $existing_uuid ) || $existing_uuid !== $ping['otto_pixel_uuid'] ) {
$metasync_options['general']['otto_pixel_uuid'] = sanitize_text_field( $ping['otto_pixel_uuid'] );
Metasync::set_option($metasync_options);
}
}
} else {
// POST succeeded but ping didn't confirm — keep the key, warn user
$api_key_validated = true;
$is_connected = true;
$api_key_warning = 'API key saved and heartbeat sent. Connection verification will complete on the next heartbeat.';
}
} elseif ( is_wp_error($sync_response) ) {
// Network error during POST — keep the key, warn user
$api_key_warning = 'Could not register with the server (network error). The key was saved — it will be verified on the next heartbeat.';
} else {
// POST returned non-200 — the key is likely invalid, revert
$metasync_options['general']['searchatlas_api_key'] = $old_api_key;
Metasync::set_option($metasync_options);
$api_key_validated = false;
$is_connected = false;
$api_key_warning = empty( $old_api_key )
? 'The API key could not be verified. Please check it and try again.'
: 'The API key could not be verified. Your API key has been reverted to the previously saved key.';
}
} else {
// Key unchanged — just save options
Metasync::set_option($metasync_options);
}
// Update connection state immediately when validated
if ( $api_key_validated === true ) {
update_option( 'metasync_last_known_connection_state', true );
set_transient( 'metasync_heartbeat_status_cache', [
'status' => true,
'timestamp' => time(),
'cached_until' => time() + 300,
'updated_by' => 'manual_save_ping_connected',
], 300 );
}
$data = Metasync::get_option('general');
$new_api_key = $data['searchatlas_api_key'] ?? '';
$api_key_changed = $old_api_key !== $new_api_key;
$api_key_added = empty($old_api_key) && !empty($new_api_key);
$api_key_removed = !empty($old_api_key) && empty($new_api_key);
if ( $api_key_validated === true ) {
# Use current_time('mysql') so the value is consistent with the
# cron heartbeat writer and renders correctly via time_elapsed_string.
$send_auth_token_timestamp = Metasync::get_option();
$send_auth_token_timestamp['general']['send_auth_token_timestamp'] = current_time('mysql');
Metasync::set_option($send_auth_token_timestamp);
Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - validated via ping');
} else {
// Key unchanged or removed — run sync as before
$sync_request = new Metasync_Sync_Requests();
$response = $sync_request->SyncCustomerParams();
$responseCode = Metasync_Sync_Requests::get_response_code($response);
if ($responseCode == 200) {
# Use current_time('mysql') for consistency with cron heartbeat.
$send_auth_token_timestamp = Metasync::get_option();
$send_auth_token_timestamp['general']['send_auth_token_timestamp'] = current_time('mysql');
Metasync::set_option($send_auth_token_timestamp);
if ($api_key_added) {
Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron();
do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - new key added');
} elseif ($api_key_removed) {
Metasync_Heartbeat_Manager::instance()->unschedule_heartbeat_cron();
delete_transient('metasync_heartbeat_status_cache');
} elseif ($api_key_changed && !empty($new_api_key)) {
do_action('metasync_trigger_immediate_heartbeat', 'Manual API key update - key changed');
}
}
}
if (isset($_POST['metasync_options']['whitelabel'])) {
$whitelabel_data = $_POST['metasync_options']['whitelabel'];
$existing_whitelabel = $metasync_options['whitelabel'] ?? [];
if (isset($whitelabel_data['logo'])) {
$logo_value = trim($whitelabel_data['logo']);
if (!empty($logo_value) && filter_var($logo_value, FILTER_VALIDATE_URL)) {
$metasync_options['whitelabel']['logo'] = esc_url_raw($logo_value);
} else {
$metasync_options['whitelabel']['logo'] = '';
}
}
if (isset($whitelabel_data['logo_light'])) {
$logo_light_value = trim($whitelabel_data['logo_light']);
if (!empty($logo_light_value) && filter_var($logo_light_value, FILTER_VALIDATE_URL)) {
$metasync_options['whitelabel']['logo_light'] = esc_url_raw($logo_light_value);
} else {
$metasync_options['whitelabel']['logo_light'] = '';
}
}
if (isset($whitelabel_data['logo_dark'])) {
$logo_dark_value = trim($whitelabel_data['logo_dark']);
if (!empty($logo_dark_value) && filter_var($logo_dark_value, FILTER_VALIDATE_URL)) {
$metasync_options['whitelabel']['logo_dark'] = esc_url_raw($logo_dark_value);
} else {
$metasync_options['whitelabel']['logo_dark'] = '';
}
}
if (isset($whitelabel_data['domain'])) {
$domain_value = trim($whitelabel_data['domain']);
if (!empty($domain_value) && filter_var($domain_value, FILTER_VALIDATE_URL)) {
$metasync_options['whitelabel']['domain'] = esc_url_raw($domain_value);
} else {
$metasync_options['whitelabel']['domain'] = '';
}
}
if (isset($whitelabel_data['settings_password'])) {
$password_value = trim($whitelabel_data['settings_password']);
$hide_settings_enabled = isset($whitelabel_data['hide_settings']) && $whitelabel_data['hide_settings'] == '1';
if ($hide_settings_enabled && empty($password_value)) {
if (isset($existing_whitelabel['settings_password'])) {
$metasync_options['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
}
} else {
$metasync_options['whitelabel']['settings_password'] = sanitize_text_field($password_value);
}
} else {
if (isset($existing_whitelabel['settings_password'])) {
$metasync_options['whitelabel']['settings_password'] = $existing_whitelabel['settings_password'];
}
}
if (isset($whitelabel_data['hide_settings'])) {
$metasync_options['whitelabel']['hide_settings'] = filter_var($whitelabel_data['hide_settings'], FILTER_VALIDATE_BOOLEAN) ? 1 : 0;
} else {
$metasync_options['whitelabel']['hide_settings'] = 0;
}
if (isset($whitelabel_data['recovery_email'])) {
$recovery_email = trim($whitelabel_data['recovery_email']);
if (!empty($recovery_email)) {
$sanitized_email = sanitize_email($recovery_email);
if (is_email($sanitized_email)) {
$metasync_options['whitelabel']['recovery_email'] = $sanitized_email;
}
} else {
$metasync_options['whitelabel']['recovery_email'] = '';
}
} else {
if (isset($existing_whitelabel['recovery_email'])) {
$metasync_options['whitelabel']['recovery_email'] = $existing_whitelabel['recovery_email'];
}
}
$has_password = !empty($metasync_options['whitelabel']['settings_password']);
$has_recovery_email = !empty($metasync_options['whitelabel']['recovery_email']);
if ($has_password && !$has_recovery_email) {
$metasync_options['whitelabel']['settings_password'] = '';
}
if (isset($whitelabel_data['access_control'])) {
$metasync_options['whitelabel']['access_control'] = Metasync_Access_Control::sanitize_access_control($whitelabel_data['access_control']);
}
if (!empty($metasync_options['whitelabel']['hide_settings']) && empty($metasync_options['whitelabel']['settings_password'])) {
$metasync_options['whitelabel']['hide_settings'] = 0;
}
// Sanitize color palette
if (isset($whitelabel_data['color_palette']) && is_array($whitelabel_data['color_palette'])) {
$allowed_vars = array(
'dashboard-bg', 'dashboard-card-bg', 'dashboard-card-hover',
'dashboard-text-primary', 'dashboard-text-secondary',
'dashboard-accent', 'dashboard-accent-hover',
'dashboard-success', 'dashboard-warning', 'dashboard-error',
'dashboard-border',
'dashboard-gradient-primary-from', 'dashboard-gradient-primary-to',
'dashboard-gradient-accent-from', 'dashboard-gradient-accent-to'
);
$sanitized_palette = array();
foreach (array('dark', 'light') as $theme_key) {
if (isset($whitelabel_data['color_palette'][$theme_key]) && is_array($whitelabel_data['color_palette'][$theme_key])) {
$sanitized_palette[$theme_key] = array();
foreach ($whitelabel_data['color_palette'][$theme_key] as $var_name => $color_value) {
if (in_array($var_name, $allowed_vars, true)) {
$color_value = trim(sanitize_text_field($color_value));
if ($color_value !== '' && preg_match('/^#[0-9a-fA-F]{3,8}$/', $color_value)) {
$sanitized_palette[$theme_key][$var_name] = $color_value;
}
}
}
}
}
$metasync_options['whitelabel']['color_palette'] = $sanitized_palette;
} else {
if (isset($existing_whitelabel['color_palette'])) {
$metasync_options['whitelabel']['color_palette'] = $existing_whitelabel['color_palette'];
}
}
$metasync_options['whitelabel']['updated_at'] = time();
Metasync::set_option($metasync_options);
}
$redirect_url = isset($_GET['tab']) ?
admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-settings&tab='.$_GET['tab']) :
admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-settings');
// Also save edge cache settings if present in the payload (section renders inside this form)
if (class_exists('Metasync_Edge_Cache_Settings')) {
Metasync_Edge_Cache_Settings::save_from_post();
}
if ($general_tab_submitted && isset($_POST['metasync_llms_txt_settings'])) {
$llms_input = wp_unslash($_POST['metasync_llms_txt_settings']);
if (is_array($llms_input)) {
update_option('metasync_llms_txt_settings', $this->sanitize_llms_txt_settings($llms_input));
}
}
$response_payload = array(
'message' => 'Settings saved successfully!',
'redirect_url' => $redirect_url,
);
if ( $api_key_field_present && $proposed_new_api_key !== '' && $proposed_new_api_key !== $old_api_key ) {
if ( $api_key_validated !== null ) {
$response_payload['api_key_validated'] = $api_key_validated;
$response_payload['is_connected'] = $is_connected;
}
if ( $api_key_warning !== null ) {
$response_payload['warning'] = $api_key_warning;
}
if ( $api_key_validated === false ) {
$response_payload['previous_api_key'] = $old_api_key;
}
} elseif ( $api_key_field_present && $proposed_new_api_key === '' && $old_api_key !== '' ) {
$response_payload['api_key_removed'] = true;
$response_payload['warning'] = 'The API key has been removed. The plugin will no longer sync with the server.';
}
delete_transient('metasync_otto_js_detected');
wp_send_json_success( $response_payload );
}
/**
* AJAX handler for saving execution settings.
*
* Moved from Metasync_Admin – the admin class now delegates here.
*/
public function ajax_save_execution_settings()
{
if (!isset($_POST['execution_settings_nonce']) || !wp_verify_nonce($_POST['execution_settings_nonce'], 'metasync_execution_settings_nonce')) {
wp_send_json_error(array('message' => 'Invalid security token. Please refresh the page and try again.'));
return;
}
if (!Metasync::current_user_has_plugin_access()) {
wp_send_json_error(array('message' => 'Insufficient permissions to save settings.'));
return;
}
$existing_settings = get_option('metasync_execution_settings', array());
$settings = array(
'max_execution_time' => isset($_POST['max_execution_time']) ? absint($_POST['max_execution_time']) : 30,
'max_memory_limit' => isset($_POST['max_memory_limit']) ? absint($_POST['max_memory_limit']) : 256,
'log_batch_size' => isset($_POST['log_batch_size']) ? absint($_POST['log_batch_size']) : 1000,
'action_scheduler_batches' => isset($_POST['action_scheduler_batches']) ? absint($_POST['action_scheduler_batches']) : 1,
'otto_rate_limit' => isset($_POST['otto_rate_limit']) ? absint($_POST['otto_rate_limit']) : 10,
'otto_cache_ttl' => isset($existing_settings['otto_cache_ttl']) ? absint($existing_settings['otto_cache_ttl']) : 30,
'queue_cleanup_days' => isset($_POST['queue_cleanup_days']) ? absint($_POST['queue_cleanup_days']) : 31
);
$server_limits = Metasync_Settings_Fields::instance()->get_server_limits();
if ($settings['max_execution_time'] < 1 || $settings['max_execution_time'] > 300) {
wp_send_json_error(array('message' => 'Max Execution Time must be between 1 and 300 seconds.'));
return;
}
if ((int)$server_limits['max_execution_time_raw'] !== -1 && (int)$settings['max_execution_time'] > (int)$server_limits['max_execution_time_raw']) {
wp_send_json_error(array('message' => sprintf('Max Execution Time exceeds server limit of %d seconds. Please reduce the value.', (int)$server_limits['max_execution_time_raw'])));
return;
}
$can_change_memory = Metasync_Settings_Fields::instance()->can_change_memory_limit();
if ($can_change_memory && ($settings['max_memory_limit'] < 64 || $settings['max_memory_limit'] > 512)) {
wp_send_json_error(array('message' => 'Max Memory Limit must be between 64 and 512 MB.'));
return;
}
if ($can_change_memory && $server_limits['memory_limit_raw'] != -1 && $settings['max_memory_limit'] > $server_limits['memory_limit_raw']) {
wp_send_json_error(array('message' => sprintf('Max Memory Limit exceeds server limit of %d MB. Please reduce the value.', $server_limits['memory_limit_raw'])));
return;
}
if (!$can_change_memory) {
$settings['max_memory_limit'] = isset($existing_settings['max_memory_limit'])
? $existing_settings['max_memory_limit']
: 256;
}
if ($settings['log_batch_size'] < 100 || $settings['log_batch_size'] > 5000) {
wp_send_json_error(array('message' => 'Log Batch Size must be between 100 and 5000 lines.'));
return;
}
if ($settings['action_scheduler_batches'] < 1 || $settings['action_scheduler_batches'] > 10) {
wp_send_json_error(array('message' => 'Action Scheduler Concurrent Batches must be between 1 and 10.'));
return;
}
if ($settings['otto_rate_limit'] < 1 || $settings['otto_rate_limit'] > 60) {
wp_send_json_error(array('message' => 'OTTO API Calls Per Minute must be between 1 and 60.'));
return;
}
if ($settings['queue_cleanup_days'] < 7 || $settings['queue_cleanup_days'] > 90) {
wp_send_json_error(array('message' => 'Queue Auto-Cleanup Days must be between 7 and 90 days.'));
return;
}
$saved = update_option('metasync_execution_settings', $settings);
if ($saved !== false) {
wp_send_json_success(array(
'message' => 'Execution settings saved successfully!'
));
} else {
wp_send_json_error(array('message' => 'Failed to save settings. Please try again.'));
}
}
/**
* AJAX handler for saving SEO / Indexation Control settings.
*
* Moved from Metasync_Admin – the admin class now delegates here.
*/
public function meta_sync_save_seo_controls()
{
try {
if (!isset($_POST['meta_sync_seo_controls_nonce']) || !wp_verify_nonce($_POST['meta_sync_seo_controls_nonce'], 'meta_sync_seo_controls_nonce')) {
wp_send_json_error(array('message' => 'Invalid nonce'));
return;
}
$current_options = Metasync::get_option();
$original_options = json_decode(json_encode($current_options), true);
if (!isset($current_options['seo_controls'])) {
$current_options['seo_controls'] = [];
}
$seo_control_fields = [
'index_date_archives',
'index_tag_archives',
'index_author_archives',
'index_category_archives',
'index_format_archives',
'override_robots_tags',
'add_nofollow_to_external_links',
'enable_googleinstantindex',
'enable_binginstantindex'
];
foreach ($seo_control_fields as $field) {
if (isset($_POST['metasync_options']['seo_controls'][$field]) && $_POST['metasync_options']['seo_controls'][$field] === 'true') {
$current_options['seo_controls'][$field] = 'true';
} else {
$current_options['seo_controls'][$field] = 'false';
}
}
$bing_save_result = true;
if (isset($_POST['metasync_bing_api_key_inline'])) {
$bing_save_result = $this->save_bing_inline_settings_ajax();
}
// Save Google Instant Indexing post types
if (isset($_POST['metasync_post_types']) && is_array($_POST['metasync_post_types'])) {
$google_post_types = array_values(array_map('sanitize_title', wp_unslash($_POST['metasync_post_types'])));
$google_settings = get_option('metasync_options_instant_indexing', ['post_types' => []]);
$google_settings['post_types'] = $google_post_types;
update_option('metasync_options_instant_indexing', $google_settings);
}
$options_changed = (json_encode($original_options) !== json_encode($current_options));
$result = Metasync::set_option($current_options);
if (!$result && !$options_changed) {
$result = true;
}
if ($result && $bing_save_result) {
$success_message = 'Indexation Control settings saved successfully!';
if (isset($_POST['metasync_bing_api_key_inline'])) {
$success_message = 'Indexation Control and Bing Instant Indexing settings saved successfully!';
}
wp_send_json_success(array(
'message' => $success_message,
'saved_data' => $current_options['seo_controls']
));
} else {
$error_message = 'Failed to save Indexation Control settings';
if (!$bing_save_result) {
$error_message = 'Failed to save Bing Instant Indexing settings';
}
wp_send_json_error(array('message' => $error_message));
}
} catch (Exception $e) {
error_log('Indexation Control AJAX Exception: ' . $e->getMessage());
error_log('Indexation Control AJAX Stack trace: ' . $e->getTraceAsString());
wp_send_json_error(array('message' => 'Error saving settings: ' . $e->getMessage()));
}
}
/**
* Save Bing IndexNow inline settings from SEO Controls page.
*
* Moved from Metasync_Admin – the admin class now delegates here.
*
* @return bool True on success, false on failure
*/
public function save_bing_inline_settings_ajax()
{
$api_key = isset($_POST['metasync_bing_api_key_inline']) ? sanitize_text_field(wp_unslash($_POST['metasync_bing_api_key_inline'])) : '';
$endpoint = isset($_POST['metasync_bing_endpoint_inline']) ? sanitize_text_field(wp_unslash($_POST['metasync_bing_endpoint_inline'])) : 'indexnow';
$post_types = isset($_POST['metasync_bing_post_types_inline']) && is_array($_POST['metasync_bing_post_types_inline']) ? array_map('sanitize_title', wp_unslash($_POST['metasync_bing_post_types_inline'])) : [];
$disable_other_plugins = isset($_POST['metasync_bing_disable_other_plugins_inline']) ? true : false;
$existing_settings = get_option('metasync_options_bing_instant_indexing', []);
$new_settings = [
'api_key' => $api_key,
'endpoint' => $endpoint,
'post_types' => array_values($post_types),
'disable_other_plugins' => $disable_other_plugins,
];
$settings = array_merge($existing_settings, $new_settings);
$settings_changed = (json_encode($existing_settings) !== json_encode($settings));
$result = update_option('metasync_options_bing_instant_indexing', $settings);
if (!$result && !$settings_changed) {
$result = true;
}
if (!empty($api_key)) {
$safe_key = sanitize_file_name( $api_key );
$file_path = ABSPATH . $safe_key . '.txt';
$real_path = realpath( ABSPATH );
if ( false === $real_path || 0 !== strpos( realpath( dirname( $file_path ) ), $real_path ) ) {
error_log( 'Bing IndexNow: Refusing to write outside ABSPATH' );
return false;
}
$file_result = file_put_contents( $file_path, $safe_key );
if ($file_result === false) {
error_log('Bing IndexNow: Failed to create API key verification file at ' . $file_path);
return false;
}
$current_options = Metasync::get_option();
if (!isset($current_options['seo_controls']['enable_binginstantindex']) || $current_options['seo_controls']['enable_binginstantindex'] !== 'true') {
$current_options['seo_controls']['enable_binginstantindex'] = 'true';
Metasync::set_option($current_options);
}
}
return $result;
}
/**
* Sanitize the metasync_llms_txt_settings option.
*
* The Settings API delivers raw form data; we coerce it into a predictable
* shape so the generator and MCP tools can rely on the structure.
*
* @param mixed $input Raw form data.
* @return array
*/
public function sanitize_llms_txt_settings($input)
{
if (!is_array($input)) {
$input = array();
}
$existing = get_option('metasync_llms_txt_settings', array());
if (!is_array($existing)) {
$existing = array();
}
$output = $existing;
$output['enabled'] = !empty($input['enabled'])
? (bool) filter_var($input['enabled'], FILTER_VALIDATE_BOOLEAN)
: false;
if (isset($input['post_types']) && is_array($input['post_types'])) {
$output['post_types'] = array_values(array_filter(array_map('sanitize_text_field', $input['post_types'])));
} else {
$output['post_types'] = array('page', 'post');
}
if (isset($input['max_posts'])) {
$max = absint($input['max_posts']);
if ($max < 1) {
$max = 1;
}
if ($max > 500) {
$max = 500;
}
$output['max_posts'] = $max;
}
if (isset($input['excluded_ids'])) {
if (is_array($input['excluded_ids'])) {
$ids = $input['excluded_ids'];
} else {
$ids = preg_split('/[\s,]+/', (string) $input['excluded_ids']);
}
$output['excluded_ids'] = array_values(array_filter(array_map('absint', (array) $ids)));
}
if (isset($input['custom_description'])) {
$output['custom_description'] = sanitize_text_field((string) $input['custom_description']);
}
$output['llms_full_enabled'] = !empty($input['llms_full_enabled'])
? (bool) filter_var($input['llms_full_enabled'], FILTER_VALIDATE_BOOLEAN)
: false;
if (isset($input['max_posts_full'])) {
$max_full = absint($input['max_posts_full']);
if ($max_full < 1) {
$max_full = 1;
}
if ($max_full > 500) {
$max_full = 500;
}
$output['max_posts_full'] = $max_full;
}
return $output;
}
}