*/ class Metasync_Admin { // Section constants for settings const SECTION_FEATURES = "features_settings"; const SECTION_METASYNC = "metasync_settings"; const SECTION_SEARCHENGINE = "searchengine_settings"; const SECTION_LOCALSEO = "local_seo"; const SECTION_CODESNIPPETS = "code_snippets"; const SECTION_SITE_SETTINGS = "site_settings"; const SECTION_COMMON_SETTINGS = "common_settings"; const SECTION_COMMON_META_SETTINGS = "common_meta_settings"; const SECTION_SOCIAL_META = "social_meta"; const SECTION_SEO_CONTROLS = "seo_controls"; const SECTION_SEO_CONTROLS_ADVANCED = "seo_controls_advanced"; const SECTION_SEO_CONTROLS_INSTANT_INDEX = "seo_controls_instant_index"; const SECTION_PLUGIN_VISIBILITY = "plugin_visibility_settings"; const SECTION_LLMS_TXT = "llms_txt_settings"; /** * The ID of this plugin. * * @since 1.0.0 * @access private * @var string $plugin_name The ID of this plugin. */ private $plugin_name; /** * The version of this plugin. * * @since 1.0.0 * @access private * @var string $version The current version of this plugin. */ private $version; /** * Holds the values to be used in the fields callbacks */ private $options; public $menu_title = "Search Atlas"; // Default, overridden by get_effective_menu_title() public const page_title = "MetaSync Settings"; /** * Get effective menu title with whitelabel company name */ public function get_effective_menu_title() { $whitelabel_company_name = Metasync::get_whitelabel_company_name(); if ($whitelabel_company_name) { return $whitelabel_company_name . ' SEO'; } // Use centralized method for getting effective plugin name return Metasync::get_effective_plugin_name(); } public const option_group = "metasync_group"; public const option_key = "metasync_options"; public static $page_slug = "searchatlas"; /** * Get the effective dashboard domain * Returns whitelabel domain if set, otherwise returns default production domain * Uses centralized constant from main Metasync class */ public static function get_effective_dashboard_domain() { // Delegate to main Metasync class for consistent domain resolution return Metasync::get_dashboard_domain(); } /** * Static method to render header and navigation for external pages (like AI Agent) * This creates a minimal instance just for rendering */ public static function render_standard_header_nav($page_title = null, $current_page = null) { Metasync_Admin_Navigation::instance()->render_standard_header_nav($page_title, $current_page); } /** * Static header render */ public static function render_static_header($page_title = null) { Metasync_Admin_Navigation::instance()->render_static_header($page_title); } /** * Static navigation render */ public static function render_static_navigation($current_page = null) { Metasync_Admin_Navigation::instance()->render_static_navigation($current_page); } /** * Get dashboard URL with authentication tokens and tracking parameters * Returns the complete URL for accessing the Search Atlas dashboard */ public function get_dashboard_url() { // Get the effective dashboard domain (whitelabel or production) $dashboard_url = self::get_effective_dashboard_domain(); // Get current options for token inclusion $general_options = Metasync::get_option('general'); // Add JWT token if available for seamless login if (isset($general_options['linkgraph_token']) && !empty($general_options['linkgraph_token'])) { $dashboard_url .= '/?jwtToken=' . urlencode($general_options['linkgraph_token']); } // Add source tracking parameter $dashboard_url .= (strpos($dashboard_url, '?') !== false ? '&' : '?') . 'source=wordpress-plugin'; // Add whitelabel identification if in whitelabel mode $whitelabel_company_name = Metasync::get_whitelabel_company_name(); if ($whitelabel_company_name) { $dashboard_url .= '&whitelabel=' . urlencode($whitelabel_company_name); } return $dashboard_url; } public const feature_sections = array( 'enable_404monitor' => 'Enable 404 Monitor', 'enable_siteverification' => 'Enable Site Verification', 'enable_localbusiness' => 'Enable Local Business', 'enable_codesnippets' => 'Enable Code Snippets', 'enable_googleconsole' => 'Enable Google Console', 'enable_optimalsettings' => 'Enable Optimal Settings', 'enable_globalsettings' => 'Enable Global Settings', 'enable_commonmetastatus' => 'Enable Common Meta Status', 'enable_socialmeta' => 'Enable Social Meta', 'enable_redirections' => 'Enable Redirections', 'enable_errorlogs' => 'Enable Error Logs' ); private $database; private $db_redirection; public $db_heartbeat_errors; public $setup_wizard; /** * Initialize the class and set its properties. * * @since 1.0.0 * @param string $plugin_name The name of this plugin. * @param string $version The version of this plugin. */ public function __construct($plugin_name, $version, &$database, $db_redirection, $db_heartbeat_errors) // , $data_error_log_list { $this->plugin_name = $plugin_name; $this->version = $version; $this->database = $database; $this->db_redirection = $db_redirection; $this->db_heartbeat_errors = $db_heartbeat_errors; // $this->data_error_log_list = $data_error_log_list; // Initialize setup wizard $this->setup_wizard = new Metasync_Setup_Wizard($plugin_name, $version); // Wire up the extracted settings-fields singleton Metasync_Settings_Fields::instance()->set_admin_instance($this); // Get data first for menu configuration $data = Metasync::get_option('general'); // Set menu title using the effective title (includes whitelabel company name if available) $this->menu_title = $this->get_effective_menu_title(); $raw_slug = isset($data['white_label_plugin_menu_slug']) ? $data['white_label_plugin_menu_slug'] : ''; $clean_slug = sanitize_title($raw_slug); # Self-heal a legacy URL-shaped slug: persist the sanitized value so every # reader that builds admin links from the stored option gets a valid WP menu slug. if ($raw_slug !== '' && $clean_slug !== $raw_slug) { $options = Metasync::get_option(); $options['general']['white_label_plugin_menu_slug'] = $clean_slug; Metasync::set_option($options); } self::$page_slug = $clean_slug === '' ? 'searchatlas' : $clean_slug; add_action('admin_menu', array($this, 'add_plugin_settings_page')); add_action('admin_menu', array($this, 'add_import_external_data_page')); add_action('admin_init', array($this, 'settings_page_init')); add_filter('all_plugins', array($this,'metasync_plugin_white_label')); add_filter( 'plugin_row_meta',array($this,'metasync_view_detials_url'),10,3); add_filter('site_transient_update_plugins', array($this, 'inject_whitelabel_icon_into_update_transient')); // Display transient error/success messages for redirections add_action('admin_notices', array($this, 'display_redirection_messages')); // Display CPU deferral notices when batch processing is deferred add_action('admin_notices', array($this, 'display_cpu_deferral_notice')); // Display LLMs.txt cross-plugin conflict notice add_action('admin_notices', array($this, 'display_llms_txt_conflict_notice')); // Add custom column for HTML-converted pages add_filter('manage_posts_columns', array($this, 'add_html_converted_column')); add_filter('manage_pages_columns', array($this, 'add_html_converted_column')); add_action('manage_posts_custom_column', array($this, 'render_html_converted_column'), 10, 2); add_action('manage_pages_custom_column', array($this, 'render_html_converted_column'), 10, 2); // Add badge to page editor screen add_action('edit_form_after_title', array($this, 'add_editor_source_notice')); // Add badge to quick edit panel add_action('quick_edit_custom_box', array($this, 'add_quick_edit_source_display'), 10, 2); // Add dashboard widget add_action('wp_dashboard_setup', array($this, 'add_html_pages_dashboard_widget')); // Add Search Atlas status to WordPress admin bar (priority 999 to ensure plugin is fully loaded) // Always add the action - the method will check the setting internally add_action('admin_bar_menu', array($this, 'add_searchatlas_admin_bar_status'), 999); #add css into admin header for icon image add_action('admin_head', array($this,'metasync_admin_icon_style')); add_action('admin_head', array($this, 'metasync_fouc_prevention_style')); add_action('admin_head', array($this, 'suppress_notices_on_wizard_page'), 1); // Always add admin bar styles - the method will check the setting internally add_action('wp_head', array($this,'metasync_admin_bar_style')); // For frontend admin bar add_action('admin_head', array($this,'metasync_admin_bar_style')); // For backend admin bar // removing this as we don't need it anymore because we are using wp-ajax to implement the white label // add_action('update_option_metasync_options', array($this, 'check_and_redirect_slug'), 10, 3); // Sync plugin file headers whenever metasync_options is updated (covers AJAX save, Settings API, import) add_action('update_option_metasync_options', array($this, 'on_options_updated_sync_file_headers'), 10, 2); // Invalidate the admin bar status cache when settings change or the API key is rotated. add_action('update_option_metasync_options', array('Metasync_Admin_Navigation', 'invalidate_admin_bar_status_cache'), 10, 0); add_action('metasync_api_key_changed', array('Metasync_Admin_Navigation', 'invalidate_admin_bar_status_cache'), 10, 0); add_action('admin_init', array($this, 'initialize_cookie')); add_action('admin_init', array($this, 'maybe_redirect_to_wizard')); // Add admin_post hooks for form submissions (WordPress standard way - no output buffering needed) add_action('admin_post_metasync_clear_all_cache_plugins', array($this, 'handle_clear_all_cache_plugins')); add_action('admin_post_metasync_clear_otto_cache_all', array($this, 'handle_clear_otto_cache_all')); add_action('admin_post_metasync_clear_otto_cache_url', array($this, 'handle_clear_otto_cache_url')); add_action('admin_post_metasync_purge_hosting_cache', array($this, 'handle_purge_hosting_cache')); // Add AJAX for saving general settings add_action( 'wp_ajax_meta_sync_save_settings', array($this,'meta_sync_save_settings') ); // Add AJAX for saving Indexation Control settings add_action( 'wp_ajax_meta_sync_save_seo_controls', array($this,'meta_sync_save_seo_controls') ); // Add AJAX handler for saving Performance (CPU Load) settings add_action('wp_ajax_metasync_save_performance_settings', array($this, 'ajax_save_performance_settings')); // Add AJAX for saving execution settings add_action( 'wp_ajax_metasync_save_execution_settings', array($this, 'ajax_save_execution_settings') ); // Add AJAX for saving hosting cache settings add_action( 'wp_ajax_metasync_save_hosting_cache_settings', array($this, 'ajax_save_hosting_cache_settings') ); // Add AJAX for saving OTTO Cache TTL add_action('wp_ajax_metasync_save_otto_cache_ttl', array($this, 'ajax_save_otto_cache_ttl')); add_action( 'wp_ajax_metasync_save_object_cache_settings', array($this, 'ajax_save_object_cache_settings') ); // Add AJAX for saving edge cache / CDN settings add_action( 'wp_ajax_metasync_save_edge_cache_settings', array('Metasync_Edge_Cache_Settings', 'ajax_save') ); // Add AJAX handler for Plugin Auth Token refresh add_action('wp_ajax_metasync_refresh_plugin_auth_token', array($this, 'refresh_plugin_auth_token')); // Add AJAX handler to get current Plugin Auth Token (for UI updates) add_action('wp_ajax_metasync_get_plugin_auth_token', array($this, 'get_plugin_auth_token')); // Add AJAX handler for creating redirects from 404 suggestions add_action('wp_ajax_metasync_create_redirect_from_404', array($this, 'ajax_create_redirect_from_404')); // Add AJAX handler for updating database structure add_action('wp_ajax_metasync_update_db_structure', array($this, 'ajax_update_db_structure')); // Add AJAX handlers for setup wizard add_action('wp_ajax_metasync_save_wizard_progress', array($this, 'ajax_save_wizard_progress')); add_action('wp_ajax_metasync_complete_wizard', array($this, 'ajax_complete_wizard')); // Add AJAX handlers for robots.txt add_action('wp_ajax_metasync_validate_robots', array($this, 'ajax_validate_robots')); add_action('wp_ajax_metasync_get_default_robots', array($this, 'ajax_get_default_robots')); add_action('wp_ajax_metasync_preview_robots_backup', array($this, 'ajax_preview_robots_backup')); add_action('wp_ajax_metasync_delete_robots_backup', array($this, 'ajax_delete_robots_backup')); add_action('wp_ajax_metasync_restore_robots_backup', array($this, 'ajax_restore_robots_backup')); add_action('wp_ajax_metasync_get_robots_backups', array($this, 'ajax_get_robots_backups')); // Add AJAX handlers for host blocking test add_action('wp_ajax_metasync_test_host_blocking_get', array($this, 'ajax_test_host_blocking_get')); add_action('wp_ajax_metasync_test_host_blocking_post', array($this, 'ajax_test_host_blocking_post')); // Add AJAX handlers for OTTO excluded URLs add_action('wp_ajax_metasync_otto_add_excluded_url', array($this, 'ajax_otto_add_excluded_url')); add_action('wp_ajax_metasync_otto_delete_excluded_url', array($this, 'ajax_otto_delete_excluded_url')); add_action('wp_ajax_metasync_burst_ping', array($this, 'ajax_burst_ping')); add_action('wp_ajax_metasync_otto_get_excluded_urls', array($this, 'ajax_otto_get_excluded_urls')); add_action('wp_ajax_metasync_otto_recheck_excluded_url', array($this, 'ajax_otto_recheck_excluded_url')); # Add AJAX handler for GA4 analytics tracking add_action('wp_ajax_metasync_track_one_click_activation', array($this, 'ajax_track_one_click_activation')); # Add AJAX handler for submitting issue reports add_action('wp_ajax_metasync_submit_issue_report', array($this, 'ajax_submit_issue_report')); # Add AJAX handlers for support token management # Add AJAX handler for theme switcher add_action('wp_ajax_metasync_save_theme', array($this, 'ajax_save_theme')); # Add AJAX handlers for Sync Log management add_action('wp_ajax_metasync_clear_sync_log', array($this, 'ajax_clear_sync_log')); add_action('wp_ajax_metasync_rollback_mcp_change', array($this, 'ajax_rollback_mcp_change')); # Add AJAX handler for external data import add_action('wp_ajax_metasync_import_external_data', array($this, 'ajax_import_external_data')); # Add AJAX handler for SEO metadata batch import add_action('wp_ajax_metasync_import_seo_metadata', array($this, 'ajax_import_seo_metadata')); # Add AJAX handler for password recovery add_action('wp_ajax_metasync_recover_password', array($this, 'ajax_recover_password')); # Add AJAX handler for resetting bot statistics add_action('wp_ajax_metasync_reset_bot_stats', array($this, 'ajax_reset_bot_stats')); # Add AJAX handlers for DB cleanup add_action('wp_ajax_metasync_run_db_cleanup', array($this, 'ajax_run_db_cleanup')); add_action('wp_ajax_metasync_save_db_cleanup_settings', array($this, 'ajax_save_db_cleanup_settings')); # Add admin-post handler for exporting whitelabel settings (file download) add_action('admin_post_metasync_export_whitelabel_settings', array($this, 'handle_export_whitelabel_settings')); # Media Optimization AJAX handlers add_action('wp_ajax_metasync_optimize_single_image', array($this, 'ajax_optimize_single_image')); add_action('wp_ajax_metasync_revert_single_image', array($this, 'ajax_revert_single_image')); add_action('wp_ajax_metasync_start_batch_optimize', array($this, 'ajax_start_batch_optimize')); add_action('wp_ajax_metasync_cancel_batch_optimize', array($this, 'ajax_cancel_batch_optimize')); add_action('wp_ajax_metasync_batch_progress', array($this, 'ajax_batch_progress')); add_action('wp_ajax_metasync_bulk_optimize_selected', array($this, 'ajax_bulk_optimize_selected')); add_action('wp_ajax_metasync_bulk_unoptimize_selected', array($this, 'ajax_bulk_unoptimize_selected')); add_action('wp_ajax_metasync_process_batch_tick', array($this, 'ajax_process_batch_tick')); add_action('wp_ajax_metasync_delete_orphaned_image', array($this, 'ajax_delete_orphaned_image')); add_action('metasync_media_batch_optimize_cron', array($this, 'handle_media_batch_cron')); # Add AJAX handlers for Google Instant Indexing add_action('wp_ajax_metasync_send_giapi', array($this, 'ajax_send_giapi')); # Add AJAX handlers for Bing Instant Indexing (IndexNow) add_action('wp_ajax_metasync_send_bing_indexnow', array($this, 'ajax_send_bing_indexnow')); # Add hooks for instant indexing settings saves add_action('admin_init', array($this, 'save_instant_indexing_settings')); # Add hooks for instant indexing post actions add_filter('post_row_actions', array($this, 'add_instant_indexing_post_actions'), 10, 2); add_filter('page_row_actions', array($this, 'add_instant_indexing_post_actions'), 10, 2); # Add hooks for auto-submission on post publish add_action('save_post', array($this, 'auto_submit_to_instant_indexing'), 10, 3); // Add REST API endpoint for ping add_action('rest_api_init', array($this, 'register_ping_rest_endpoint')); // Add heartbeat cron functionality add_filter('cron_schedules', array($this, 'add_heartbeat_cron_schedule')); add_action('metasync_heartbeat_cron_check', array($this, 'execute_heartbeat_cron_check')); add_action('metasync_burst_heartbeat', array($this, 'execute_burst_heartbeat')); add_action('metasync_announce_cron', array($this, 'execute_announce_cron')); // PR3: Transition to KEY_PENDING when API key is added or rotated add_action('metasync_heartbeat_state_key_pending', array($this, 'set_heartbeat_state_key_pending')); // Add transient cleanup cron functionality add_action('metasync_cleanup_transients', array($this, 'execute_transient_cleanup')); // Add DB cleanup cron functionality add_action('metasync_db_cleanup', array($this, 'execute_db_cleanup')); // Schedule heartbeat cron on plugin load (if not already scheduled) add_action('init', array($this, 'maybe_schedule_heartbeat_cron')); // Schedule transient cleanup cron on plugin load add_action('init', array($this, 'maybe_schedule_transient_cleanup_cron')); // Schedule DB cleanup cron on plugin load (only when enabled) add_action('init', array($this, 'maybe_schedule_db_cleanup_cron')); # Schedule hidden post manager cron on plugin load (runs every 7 days) add_action('init', array($this, 'maybe_schedule_hidden_post_check')); # Schedule OTTO 404 exclusion recheck (runs daily to recheck URLs excluded 7+ days ago) add_action('init', array($this, 'maybe_schedule_otto_recheck_404_cron')); # Schedule support token cleanup cron on plugin load (runs daily) // Listen for immediate heartbeat trigger requests from other parts of the plugin add_action('metasync_trigger_immediate_heartbeat', array($this, 'handle_immediate_heartbeat_trigger')); // Listen for cron scheduling requests (after Search Atlas connect authentication) add_action('metasync_ensure_heartbeat_cron_scheduled', array($this, 'maybe_schedule_heartbeat_cron')); // Action Scheduler configuration filters add_filter('action_scheduler_queue_runner_concurrent_batches', array($this, 'get_action_scheduler_batches')); add_filter('action_scheduler_retention_period', array($this, 'get_action_scheduler_retention_period')); // Note: Option change monitoring is now handled by the centralized API Key Monitor class // This provides more comprehensive and intelligent monitoring of API key changes #-------------------------- # we are disabling this code # To prevent enabling debuging on every pluging Update # This causes Issue #102 on gilab #-------------------------- # # NOTE: # Do not delete this as we may need it in implementing universal logging # add_action('upgrader_process_complete', array($this,'metasync_plugin_updated_action'), 10, 2); # Hook into post category creation and update (AFTER they are saved) add_action('saved_term', array($this,'admin_crud_term'), 10, 3); # Hook into post category deletion (AFTER it is deleted) // add_action('pre_delete_term', array($this,'admin_delete_term'), 10, 2); # Using delete_category hook which fires AFTER category is fully deleted and provides correct data add_action('delete_category', array($this,'admin_delete_category'), 10, 1); } /* This function add css to wp admin header */ public function metasync_admin_icon_style(){ # Get Metasync Option $data= Metasync::get_option('general'); # Get white label menu slug # Sanitize so a URL-shaped legacy value still yields a valid WP menu slug $menu_slug = empty($data['white_label_plugin_menu_slug']) ? self::$page_slug : sanitize_title($data['white_label_plugin_menu_slug']); $menu_slug = $menu_slug === '' ? self::$page_slug : $menu_slug; ?> remove_third_party_admin_notices(); } } /** * Remove admin-notice callbacks that don't belong to this plugin. * * Third-party plugins (e.g. Rank Math, Yoast) register their banners on the * global admin_notices / all_admin_notices actions, which WordPress fires on * every admin screen — so their notices leak onto our settings pages. We walk * the registered callbacks and drop any whose code lives outside this plugin's * directory, leaving every one of our own notices intact. */ private function remove_third_party_admin_notices() { global $wp_filter; foreach ( array( 'admin_notices', 'all_admin_notices' ) as $hook ) { if ( empty( $wp_filter[ $hook ] ) || ! ( $wp_filter[ $hook ] instanceof WP_Hook ) ) { continue; } // Collect first, then remove — avoids mutating the callbacks array mid-walk. $to_remove = array(); foreach ( $wp_filter[ $hook ]->callbacks as $priority => $callbacks ) { foreach ( $callbacks as $callback ) { if ( ! $this->is_own_admin_notice_callback( $callback['function'] ) ) { $to_remove[] = array( $callback['function'], $priority ); } } } foreach ( $to_remove as $entry ) { remove_action( $hook, $entry[0], $entry[1] ); } } } /** * Determine whether an admin-notice callback is defined by this plugin. * * Ownership is decided by where the callback's code physically lives, not by * class name — our notice owners use inconsistent prefixes (Metasync_Admin, * Google_Index_Admin, ConfigControllerMetaSync), and a white-label install can * rename the plugin folder. Resolving the declaring file and checking it sits * inside this plugin's directory is robust against all of that. If a callback * can't be introspected, we keep it rather than risk hiding a legitimate notice. * * @param string|array|Closure $function The registered callback. * @return bool True when the callback's code lives inside this plugin. */ private function is_own_admin_notice_callback( $function ) { // Plugin root: this file lives in /admin, so its parent is the root. $plugin_dir = wp_normalize_path( dirname( __DIR__ ) ) . '/'; try { if ( is_array( $function ) && isset( $function[0], $function[1] ) ) { $class = is_object( $function[0] ) ? get_class( $function[0] ) : $function[0]; $ref = new ReflectionMethod( $class, $function[1] ); } elseif ( $function instanceof Closure ) { $ref = new ReflectionFunction( $function ); } elseif ( is_string( $function ) && strpos( $function, '::' ) !== false ) { list( $class, $method ) = explode( '::', $function, 2 ); $ref = new ReflectionMethod( $class, $method ); } elseif ( is_string( $function ) ) { $ref = new ReflectionFunction( $function ); } else { return false; } } catch ( \Throwable $e ) { // Unintrospectable callback — keep it; never hide a notice we can't classify. return true; } $file = $ref->getFileName(); if ( ! $file ) { // Internal/built-in callback (no source file) — not ours. return false; } return strpos( wp_normalize_path( $file ), $plugin_dir ) === 0; } #---------fixes issue : #95 ---------- #This function is to redirect in case client changes slug on fresh install #It is called by the add_option hook public function redirect_slug_for_freshinstalls(){ #get the db menu slug $plugin_menu_slug = Metasync::get_option('general')['white_label_plugin_menu_slug'] ?? ''; #check that the slug is set or set it to the defaul class slug usually ('searchatlas') $current_slug = empty($plugin_menu_slug) ? self::$page_slug : $plugin_menu_slug; #check if we have the cookie set and check if the slug has changed if (isset($_COOKIE['metasync_previous_slug']) && $_COOKIE['metasync_previous_slug'] !== $current_slug) { # the slug changed so we need to update the cookie setcookie('metasync_previous_slug', $current_slug, [ 'expires' => time() + 3600, 'path' => COOKIEPATH, 'domain' => COOKIE_DOMAIN, 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ]); $_COOKIE['metasync_previous_slug'] = $current_slug; #Redirect url to the new slug $redirect_url = admin_url('admin.php?page=' . $current_slug); wp_redirect($redirect_url); exit; } } public function metasync_plugin_updated_action($upgrader_object, $options){ if ($options['action'] == 'update' && $options['type'] == 'plugin') { // List of plugins being updated $updated_plugins = $options['plugins']; // Loop through plugins and check if your plugin is updated if (in_array(plugin_basename(dirname(__DIR__) . '/metasync.php'), $updated_plugins, true)) { // Only set debug options if they don't already exist (first install/update) if (get_option('wp_debug_enabled') === false) { update_option('wp_debug_enabled', 'true'); } if (get_option('wp_debug_log_enabled') === false) { update_option('wp_debug_log_enabled', 'true'); } if (get_option('wp_debug_display_enabled') === false) { update_option('wp_debug_display_enabled', 'false'); } } } } public function initialize_cookie() { // Check if cookie is already set if (!isset($_COOKIE['metasync_previous_slug'])) { // Only set cookie if headers haven't been sent yet if (!headers_sent()) { $data = Metasync::get_option('general'); // Retrieve the current slug $initial_slug = isset($data['white_label_plugin_menu_slug']) ? $data['white_label_plugin_menu_slug'] : self::$page_slug; // Set the cookie setcookie('metasync_previous_slug', $initial_slug, [ 'expires' => time() + 3600, 'path' => COOKIEPATH, 'domain' => COOKIE_DOMAIN, 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ]); } } } /** * Redirect to setup wizard on first activation * * @since 1.0.0 */ public function maybe_redirect_to_wizard() { // Only redirect if wizard should be shown if (get_option('metasync_show_wizard') && !isset($_GET['page'])) { delete_option('metasync_show_wizard'); // Don't redirect during AJAX, cron, or bulk activation if (wp_doing_ajax() || wp_doing_cron() || isset($_GET['activate-multi'])) { return; } // Only redirect if user has access to the plugin if (!Metasync::current_user_has_plugin_access()) { return; } wp_safe_redirect(admin_url('admin.php?page=' . self::$page_slug . '-setup-wizard')); exit; } } public function metasync_display_error_log() { Metasync_Debug_Manager::instance()->metasync_display_error_log($this); } public function metasync_update_wp_config() { Metasync_Debug_Manager::instance()->metasync_update_wp_config(); } /** * Sync plugin file headers when metasync_options is updated. * This ensures whitelabel values are written to the plugin file header * so they persist even when the plugin is deactivated. * * @param mixed $old_value The old option value. * @param mixed $new_value The new option value. * @since 2.5.0 */ public function on_options_updated_sync_file_headers($old_value, $new_value) { Metasync_Debug_Manager::instance()->on_options_updated_sync_file_headers($old_value, $new_value); } public function check_and_redirect_slug($option, $old_value, $new_value) { // Ensure this hook is only triggered for your specific option group if (!isset($option['general'] ) && !isset($option['general']['white_label_plugin_menu_slug'])) { return; } $new_slug = $new_value['general']['white_label_plugin_menu_slug'] ?? self::$page_slug; $old_slug = $old_value['general']['white_label_plugin_menu_slug'] ?? self::$page_slug; if ($new_slug !== $old_slug && $old_slug !=='' ){ // Set a new cookie setcookie('metasync_previous_slug', $new_slug, [ 'expires' => time() + 3600, 'path' => COOKIEPATH, 'domain' => COOKIE_DOMAIN, 'secure' => is_ssl(), 'httponly' => true, 'samesite' => 'Lax', ]); $_COOKIE['metasync_previous_slug'] = $new_slug; // Redirect to the new slug $redirect_url = admin_url('admin.php?page=' . $old_slug); wp_redirect($redirect_url); exit; }else{ self::$page_slug = Metasync::get_option('general')['white_label_plugin_menu_slug']=="" ? "searchatlas":Metasync::get_option('general')['white_label_plugin_menu_slug']; $redirect_url = admin_url('admin.php?page=' . self::$page_slug); #add redirection for when the old slug is not defined #this fixes the redirect issue # wp_redirect($redirect_url); exit; } } public function metasync_view_detials_url( $plugin_meta, $plugin_file, $plugin_data ) { $plugin_uri = Metasync::get_option('general')['white_label_plugin_uri'] ?? ''; $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php'); if ($this_plugin === $plugin_file && $plugin_uri !== '') { foreach ($plugin_meta as &$meta) { if (strpos($meta, 'open-plugin-details-modal') !== false) { $meta = sprintf( '%s', add_query_arg('TB_iframe', 'true', $plugin_uri), esc_attr(sprintf(esc_html__('More information about %s'), $plugin_data['Name'])), esc_attr($plugin_data['Name']), esc_html__('View details', 'metasync') ); break; // Exit loop after replacing the link } } } return $plugin_meta; } public function metasync_plugin_white_label($all_plugins) { $general = Metasync::get_option('general'); if (!is_array($general)) { return $all_plugins; } $plugin_name = $general['white_label_plugin_name'] ?? ''; $plugin_description = $general['white_label_plugin_description'] ?? ''; $plugin_author = $general['white_label_plugin_author'] ?? ''; $plugin_author_uri = $general['white_label_plugin_author_uri'] ?? ''; $plugin_uri = $general['white_label_plugin_uri'] ?? ''; // Dynamically resolve the plugin basename to handle renamed plugin folders $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php'); if (isset($all_plugins[$this_plugin])) { if ($plugin_name !== '') { $all_plugins[$this_plugin]['Name'] = $plugin_name; $all_plugins[$this_plugin]['Title'] = $plugin_name; } if ($plugin_description !== '') { $all_plugins[$this_plugin]['Description'] = $plugin_description; } if ($plugin_author !== '') { $all_plugins[$this_plugin]['Author'] = $plugin_author; $all_plugins[$this_plugin]['AuthorName'] = $plugin_author; } if ($plugin_author_uri !== '') { $all_plugins[$this_plugin]['AuthorURI'] = $plugin_author_uri; } if ($plugin_uri !== '') { $all_plugins[$this_plugin]['PluginURI'] = $plugin_uri; } } return $all_plugins; } /** * Inject the whitelabel icon into the update_plugins transient so that * /wp-admin/update-core.php (Dashboard → Updates) shows the WL icon * instead of the SearchAtlas icon returned by the update API. * * @param object $transient The site transient object. * @return object */ public function inject_whitelabel_icon_into_update_transient($transient) { if (empty($transient) || !is_object($transient)) { return $transient; } $general = Metasync::get_option('general'); if (!is_array($general)) { return $transient; } $icon_url = $general['white_label_plugin_menu_icon'] ?? ''; if (empty($icon_url)) { return $transient; } $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php'); // Inject into pending updates list if (!empty($transient->response) && isset($transient->response[$this_plugin])) { $transient->response[$this_plugin]->icons = [ '1x' => $icon_url, '2x' => $icon_url, ]; } // Also inject into the "no update needed" list so the icon appears // on the updates screen even when the plugin is up-to-date if (!empty($transient->no_update) && isset($transient->no_update[$this_plugin])) { $transient->no_update[$this_plugin]->icons = [ '1x' => $icon_url, '2x' => $icon_url, ]; } return $transient; } /** * Register the stylesheets for the admin area. * * @since 1.0.0 */ public function enqueue_styles() { $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; if (strpos($current_page, self::$page_slug) === 0) { wp_enqueue_style( $this->plugin_name, plugin_dir_url(__FILE__) . 'css/metasync-admin.css', array(), $this->version, 'all' ); // Enqueue dashboard-style CSS for admin pages wp_enqueue_style( $this->plugin_name . '-dashboard', plugin_dir_url(__FILE__) . 'css/metasync-dashboard.css', array($this->plugin_name), $this->version, 'all' ); // Enqueue 3-column layout CSS wp_enqueue_style( $this->plugin_name . '-layout', plugin_dir_url(__FILE__) . 'css/metasync-layout.css', array($this->plugin_name . '-dashboard'), $this->version, 'all' ); } // Enqueue wizard CSS if on wizard page if (isset($_GET['page']) && strpos($_GET['page'], '-setup-wizard') !== false) { wp_enqueue_style( $this->plugin_name . '-setup-wizard', plugin_dir_url(__FILE__) . 'css/metasync-setup-wizard.css', array($this->plugin_name . '-dashboard'), $this->version, 'all' ); } // Enqueue SEO Health CSS if on the SEO Health page if (isset($_GET['page']) && strpos($_GET['page'], '-seo-health') !== false) { wp_enqueue_style( $this->plugin_name . '-seo-health', plugin_dir_url(__FILE__) . 'css/metasync-seo-health.css', array($this->plugin_name . '-dashboard'), $this->version, 'all' ); } } /** * Register the JavaScript for the admin area. * * @since 1.0.0 */ public function enqueue_scripts() { // --- Phase 5 (#887): Extracted inline JS files --- $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; $plugin_root_url = plugin_dir_url(dirname(__FILE__)); $is_metasync_page = ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0); if ($is_metasync_page) { wp_enqueue_media(); wp_enqueue_script( $this->plugin_name, plugin_dir_url(__FILE__) . 'js/metasync-admin.js', array('jquery'), $this->version, false ); // Enqueue dashboard-style JavaScript for enhanced interactions wp_enqueue_script( $this->plugin_name . '-dashboard', plugin_dir_url(__FILE__) . 'js/metasync-dashboard.js', array('jquery', $this->plugin_name), $this->version, true ); # Enqueue theme switcher wp_enqueue_script( $this->plugin_name . '-theme-switcher', plugin_dir_url(__FILE__) . 'js/metasync-theme-switcher.js', array('jquery'), $this->version, true ); // Keeps core's admin-menu pinning in step with pages whose height // changes after load. Depends on 'common' so core has bound its // own handlers before this runs. wp_enqueue_script( $this->plugin_name . '-admin-menu-height', plugin_dir_url(__FILE__) . 'js/metasync-admin-menu-height.js', array('jquery', 'common'), $this->version, true ); } $per_page_screens = [ self::$page_slug . '-redirections', self::$page_slug . '-404-monitor', self::$page_slug . '-sync-log', self::$page_slug . '-media-optimization', ]; if (in_array($current_page, $per_page_screens, true)) { // Results-per-page selectors use this external handler instead of // an inline event attribute so changing a page size always reloads // the current admin page. wp_enqueue_script( $this->plugin_name . '-per-page', plugin_dir_url(__FILE__) . 'js/metasync-per-page.js', array(), $this->version, true ); } // Dashboard iframe height (only on dashboard page) if ($current_page === self::$page_slug . '-dashboard' || $current_page === self::$page_slug) { wp_enqueue_script( $this->plugin_name . '-iframe', plugin_dir_url(__FILE__) . 'js/metasync-iframe.js', array(), $this->version, false // Load in head — needed before iframe renders ); } // Settings page scripts (save btn, clear settings, bing key, access roles) if ($current_page === self::$page_slug) { wp_enqueue_script( $this->plugin_name . '-settings', plugin_dir_url(__FILE__) . 'js/metasync-settings.js', array('jquery'), $this->version, true ); } // Tab switcher (redirections / 404-monitor page) if ($current_page === self::$page_slug . '-redirections') { wp_enqueue_script( $this->plugin_name . '-tab-switcher', plugin_dir_url(__FILE__) . 'js/metasync-tab-switcher.js', array('jquery'), $this->version, true ); } // Sitemap tabs (xml-sitemap page) if ($current_page === self::$page_slug . '-xml-sitemap') { wp_enqueue_style( $this->plugin_name . '-sitemap-tabs', plugin_dir_url(__FILE__) . 'css/metasync-sitemap-tabs.css', array($this->plugin_name . '-dashboard'), $this->version ); wp_enqueue_script( $this->plugin_name . '-sitemap-tabs', plugin_dir_url(__FILE__) . 'js/metasync-sitemap-tabs.js', array('jquery'), $this->version, true ); // Pass the active tab from server (handles POST redirect) $sitemap_active_tab = 'general'; if (isset($_GET['tab']) && in_array($_GET['tab'], ['general', 'news', 'video'], true)) { $sitemap_active_tab = sanitize_text_field(wp_unslash($_GET['tab'])); } elseif (isset($_POST['redirect_tab']) && in_array($_POST['redirect_tab'], ['general', 'news', 'video'], true)) { $sitemap_active_tab = sanitize_text_field(wp_unslash($_POST['redirect_tab'])); } wp_localize_script($this->plugin_name . '-sitemap-tabs', 'metasyncSitemapTabs', [ 'activeTab' => $sitemap_active_tab, ]); } // Error logs — copy to clipboard if ($current_page === self::$page_slug) { wp_enqueue_script( $this->plugin_name . '-error-logs', $plugin_root_url . 'site-error-logs/js/metasync-error-logs.js', array(), $this->version, true ); } // Access control UI if ($current_page === self::$page_slug) { wp_enqueue_script( $this->plugin_name . '-access-control', $plugin_root_url . 'includes/js/metasync-access-control.js', array(), $this->version, true ); } // 404 monitor filter if ($current_page === self::$page_slug . '-redirections' || $current_page === self::$page_slug . '-404-monitor') { wp_enqueue_script( $this->plugin_name . '-404-monitor', $plugin_root_url . 'views/js/metasync-404-monitor.js', array(), $this->version, true ); } // Redirections filter if ($current_page === self::$page_slug . '-redirections') { wp_enqueue_script( $this->plugin_name . '-redirections', $plugin_root_url . 'views/js/metasync-redirections.js', array(), $this->version, true ); wp_localize_script($this->plugin_name . '-redirections', 'metasyncHealthCheck', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'healthNonce' => wp_create_nonce('metasync_redirect_health_check'), )); } // --- Phase 5 Part B: Extracted inline JS with wp_localize_script --- // Navigation portal menus (top bar + settings inner page) $whitelabel_data = Metasync::get_option('whitelabel'); if ($is_metasync_page) { wp_enqueue_script( $this->plugin_name . '-navigation', plugin_dir_url(__FILE__) . 'js/metasync-navigation.js', array(), $this->version, false // Load in head — global functions called from onclick attributes ); wp_localize_script($this->plugin_name . '-navigation', 'metasyncNavData', array( 'hideAdvanced' => !empty($whitelabel_data['hide_advanced']), 'showGeneral' => Metasync_Access_Control::user_can_access('hide_settings'), 'pageSlug' => self::$page_slug, )); } // Debug mode timer if ($current_page === self::$page_slug) { $debug_manager = Metasync_Debug_Mode_Manager::get_instance(); $debug_status = $debug_manager->get_status(); wp_enqueue_script( $this->plugin_name . '-debug-mode', plugin_dir_url(__FILE__) . 'js/metasync-debug-mode.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-debug-mode', 'metasyncDebugData', array( 'enabled' => !empty($debug_status['enabled']), 'indefinite' => !empty($debug_status['indefinite']), 'timeRemaining' => isset($debug_status['time_remaining']) ? (int) $debug_status['time_remaining'] : 0, 'statusUrl' => rest_url('metasync/v1/debug-mode/status'), 'restNonce' => wp_create_nonce('wp_rest'), )); } // MetasyncConfig + admin bar sync if ($is_metasync_page) { wp_enqueue_script( $this->plugin_name . '-config', plugin_dir_url(__FILE__) . 'js/metasync-config.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-config', 'metasyncConfigData', array( 'pluginName' => Metasync::get_effective_plugin_name(), 'ottoName' => Metasync::get_whitelabel_otto_name(), )); } // Whitelabel password (forgot password handler) if ($current_page === self::$page_slug) { wp_enqueue_script( $this->plugin_name . '-whitelabel', plugin_dir_url(__FILE__) . 'js/metasync-whitelabel.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-whitelabel', 'metasyncWhitelabelData', array( 'recoverNonce' => wp_create_nonce('metasync_recover_password_nonce'), )); } // Whitelabel connect (validation modal + lock section + export) if ($current_page === self::$page_slug) { wp_enqueue_script( $this->plugin_name . '-connect', plugin_dir_url(__FILE__) . 'js/metasync-connect.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-connect', 'metasyncConnectData', array( 'optionKey' => self::option_key, 'adminPostUrl' => admin_url('admin-post.php'), 'exportNonce' => wp_create_nonce('metasync_export_whitelabel'), 'ajaxUrl' => admin_url('admin-ajax.php'), 'logoutNonceField' => wp_nonce_field('whitelabel_logout_nonce', 'whitelabel_logout_nonce', true, false), )); } // Host blocking test (settings + dashboard) if ($current_page === self::$page_slug || $current_page === self::$page_slug . '-dashboard') { wp_enqueue_script( $this->plugin_name . '-host-blocking', plugin_dir_url(__FILE__) . 'js/metasync-host-blocking.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-host-blocking', 'metasyncHostBlockingData', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('metasync_nonce'), )); } // OTTO excluded URLs (lives on the Compatibility page) if ($current_page === self::$page_slug . '-compatibility') { wp_enqueue_script( $this->plugin_name . '-excluded-urls', plugin_dir_url(__FILE__) . 'js/metasync-excluded-urls.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-excluded-urls', 'metasyncExcludedUrlsData', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('metasync_otto_excluded_urls'), )); } // Execution settings form if ($current_page === self::$page_slug) { $server_limits = $this->get_server_limits(); wp_enqueue_script( $this->plugin_name . '-execution-settings', plugin_dir_url(__FILE__) . 'js/metasync-execution-settings.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-execution-settings', 'metasyncExecSettingsData', array( 'serverMaxExecTime' => ($server_limits['max_execution_time_raw'] == -1) ? 'Infinity' : (int) $server_limits['max_execution_time_raw'], 'serverMaxMemory' => ($server_limits['memory_limit_raw'] == -1) ? 'Infinity' : (int) $server_limits['memory_limit_raw'], 'canChangeMemory' => !empty($server_limits['can_change_memory']), )); } // Quick edit badge (custom pages) global $pagenow; if ($pagenow === 'edit.php') { wp_enqueue_script( $this->plugin_name . '-quick-edit', plugin_dir_url(__FILE__) . 'js/metasync-quick-edit.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-quick-edit', 'metasyncQuickEditData', array( 'standardPageLabel' => __('Standard page', 'metasync'), )); } // Report issue form if ($current_page === self::$page_slug . '-report-issue') { wp_enqueue_script( $this->plugin_name . '-report-issue', $plugin_root_url . 'views/js/metasync-report-issue.js', array('jquery'), $this->version, true ); } // Bing console if ($current_page === self::$page_slug . '-bing-console') { wp_enqueue_script( $this->plugin_name . '-bing-console', $plugin_root_url . 'views/js/metasync-bing-console.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-bing-console', 'metasyncBingConsoleData', array( 'nonce' => wp_create_nonce('metasync_nonce'), )); } // Add redirection form if ($current_page === self::$page_slug . '-redirections') { wp_enqueue_script( $this->plugin_name . '-add-redirection', $plugin_root_url . 'views/js/metasync-add-redirection.js', array(), $this->version, true ); } // Import redirections if ($current_page === self::$page_slug . '-redirections') { wp_enqueue_script( $this->plugin_name . '-import-redirections', $plugin_root_url . 'views/js/metasync-import-redirections.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-import-redirections', 'metasyncImportRedirData', array( 'nonce' => wp_create_nonce('metasync_import_redirections'), 'redirectUrl' => admin_url('admin.php?page=' . self::$page_slug . '-redirections'), )); } // Import external data (SEO metadata) if ($current_page === self::$page_slug . '-import-external') { wp_enqueue_script( $this->plugin_name . '-import-external-data', $plugin_root_url . 'views/js/metasync-import-external-data.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-import-external-data', 'metasyncImportData', array( 'importNonce' => wp_create_nonce('metasync_import_external_data'), 'seoImportNonce' => wp_create_nonce('metasync_import_seo_metadata'), )); } // OTTO bot statistics if ($current_page === self::$page_slug . '-bot-statistics') { wp_enqueue_script( $this->plugin_name . '-bot-statistics', $plugin_root_url . 'views/js/metasync-bot-statistics.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-bot-statistics', 'metasyncBotStatsData', array( 'resetNonce' => wp_create_nonce('metasync_reset_bot_stats'), )); } // OTTO debug page if ($current_page === self::$page_slug . '-otto-debug') { wp_enqueue_script( $this->plugin_name . '-otto-debug', plugin_dir_url(__FILE__) . 'js/metasync-otto-debug.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-otto-debug', 'metasyncOttoDebugData', array( 'nonce' => wp_create_nonce('metasync_otto_debug'), )); } // --- End Phase 5 enqueues --- if ($is_metasync_page) { # Localize theme switcher script wp_localize_script( $this->plugin_name . '-theme-switcher', 'metasyncThemeData', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('metasync_theme_nonce'), 'currentTheme' => get_option('metasync_theme', 'dark') ) ); // Get connection status for JavaScript $general_settings = Metasync::get_option('general'); $searchatlas_api_key = isset($general_settings['searchatlas_api_key']) ? $general_settings['searchatlas_api_key'] : ''; $otto_pixel_uuid = isset($general_settings['otto_pixel_uuid']) ? $general_settings['otto_pixel_uuid'] : ''; // SECURITY FIX (CVE-2025-14386): Only generate Search Atlas connect nonce for administrators // Using strict capability check instead of plugin access roles $sa_connect_nonce = ''; if (current_user_can('manage_options')) { $sa_connect_nonce = wp_create_nonce('metasync_sa_connect_nonce'); } $heartbeat_state = $this->get_heartbeat_state(); wp_localize_script( $this->plugin_name, 'metaSync', array( 'ajax_url' => admin_url( 'admin-ajax.php' ), 'admin_url'=>admin_url('admin.php'), 'nonce' => wp_create_nonce('metasync_nonce'), 'sa_connect_nonce' => $sa_connect_nonce, 'reset_auth_nonce' => wp_create_nonce('metasync_reset_auth_nonce'), 'burst_ping_nonce' => wp_create_nonce('metasync_burst_ping'), 'heartbeat_state' => $heartbeat_state, 'dashboard_domain' => self::get_effective_dashboard_domain(), 'support_email' => Metasync::SUPPORT_EMAIL, 'documentation_domain' => Metasync::DOCUMENTATION_DOMAIN, 'debug_enabled' => WP_DEBUG || (defined('METASYNC_DEBUG') && constant('METASYNC_DEBUG')), 'searchatlas_api_key' => !empty($searchatlas_api_key), 'otto_pixel_uuid' => $otto_pixel_uuid, 'is_connected' => (bool)$this->is_heartbeat_connected() )); // Ensure ajaxurl is available for admin pages (WordPress standard) // This creates a global ajaxurl variable for JavaScript wp_enqueue_script('wp-util'); // Add inline script to ensure ajaxurl is defined $inline_script = " if (typeof ajaxurl === 'undefined') { var ajaxurl = '" . esc_js(admin_url('admin-ajax.php')) . "'; } // Add Plugin Auth Token refresh functionality jQuery(document).ready(function($) { $('#refresh-plugin-auth-token').click(function() { var button = $(this); var originalText = button.text(); if (confirm('Are you sure you want to refresh the Plugin Auth Token? This will generate a new token and update the heartbeat API.')) { // Disable button and show loading button.prop('disabled', true).text('🔄 Refreshing...'); $.post(ajaxurl, { action: 'metasync_refresh_plugin_auth_token', nonce: '" . wp_create_nonce('metasync_refresh_plugin_auth_token') . "' }) .done(function(response) { if (response.success && response.data && response.data.new_token) { // Update the field value immediately $('#apikey').val(response.data.new_token); // Visual feedback with green border $('#apikey').css('border', '2px solid #28a745').animate({borderColor: '#ddd'}, 2000); alert('✅ Plugin Auth Token refreshed successfully!\\n\\nNew token: ' + response.data.new_token.substring(0, 8) + '...'); } else { alert('❌ Error refreshing token: ' + (response.data ? response.data.message : 'Unknown error')); } }) .fail(function() { alert('❌ Network error while refreshing token'); }) .always(function() { // Re-enable button button.prop('disabled', false).text(originalText); }); } }); }); "; wp_add_inline_script($this->plugin_name, $inline_script); } add_action('admin_notices', array($this, 'permalink_structure_dashboard_warning')); add_action('admin_notices', array($this, 'display_page_builder_notice')); // Display update warning banner if plugin update is available add_action('admin_notices', array($this, 'display_update_warning_banner')); // Enqueue wizard assets if on wizard page if (isset($_GET['page']) && strpos($_GET['page'], '-setup-wizard') !== false) { wp_enqueue_script( $this->plugin_name . '-setup-wizard', plugin_dir_url(__FILE__) . 'js/metasync-setup-wizard.js', array('jquery'), $this->version, true ); wp_localize_script($this->plugin_name . '-setup-wizard', 'metasyncWizardData', array( 'nonce' => wp_create_nonce('metasync_wizard'), 'ssoNonce' => wp_create_nonce('metasync_sso_nonce'), 'saConnectNonce' => wp_create_nonce('metasync_sa_connect_nonce'), 'importNonce' => wp_create_nonce('metasync_import_external_data'), 'dashboardUrl' => admin_url('admin.php?page=' . self::$page_slug . '-dashboard'), 'currentStep' => 1, 'totalSteps' => 6, 'pluginName' => Metasync::get_effective_plugin_name() )); } wp_enqueue_script('heartbeat'); } /** * Settings of HeartBeat API for admin area. * * The only thing the plugin rides the beat for is a periodic admin * telemetry ping (the `heartbeat-send` handler in admin/js/metasync-admin.js), * so it slows the beat down rather than speeding it up. Two contexts are * left on whatever core decided, because core leans on the beat there: * * - The front end, where the beat drives wp-auth-check — the "your session * has expired, log in again" prompt. * - The editor screens, where each tick renews the post lock. A lock only * lives for wp_check_post_lock_window() seconds (150 by default), so any * interval longer than that drops the lock between beats and lets a * second editor silently take over a post someone still has open. * * Core clamps this to 1-3600 seconds in wp-includes/js/heartbeat.js, so an * over-long interval is not corrected on our behalf. * * @param array $settings Heartbeat settings array. * @return array Heartbeat settings array. */ function metasync_heartbeat_settings($settings) { if (!is_admin()) { return $settings; } # $pagenow is set in wp-includes/vars.php, long before this filter runs; # core's own wp_heartbeat_set_suspension() reads it exactly this way. global $pagenow; $lock_dependent_screens = array('post.php', 'post-new.php', 'customize.php'); if (isset($pagenow) && in_array($pagenow, $lock_dependent_screens, true)) { return $settings; } # 60s matches heartbeat.js's own default and stays well inside the # 150s post-lock window, so nothing core does can fall through a gap. $settings['interval'] = 60; return $settings; } /** * Data or Response received from HeartBeat API for admin area. */ function metasync_received_data($response, $data) { // if ($data['client'] == 'marco') $response['server'] = wp_json_encode($data); return $response; } /** * Add Import External Data page */ public function add_import_external_data_page() { # Check if current user has plugin access based on role settings if (!$this->current_user_has_plugin_access()) { return; // Don't add this page for users without access } // Use 'read' capability since actual access is controlled by current_user_has_plugin_access() check above add_submenu_page( '', // Hidden from menu, linked from other pages 'Import External Data', 'Import External Data', 'read', self::$page_slug . '-import-external', array($this, 'render_import_external_data_page') ); } /** * Render Import External Data page */ public function render_import_external_data_page() { require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-external-importer.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-import-external-data.php'; } /** * AJAX handler for external data import */ public function ajax_import_external_data() { Metasync_Admin_Ajax::instance()->ajax_import_external_data(); } /** * AJAX handler for SEO metadata batch import * Supports batch processing with progress tracking */ public function ajax_import_seo_metadata() { Metasync_Admin_Ajax::instance()->ajax_import_seo_metadata(); } /** * Additional context validation for Search Atlas connect tokens. * * Validates site URL and optional IP/user-agent context for connect tokens * used during the Search Atlas API key + Otto UUID retrieval flow. * This does NOT create WordPress login sessions. */ private function validate_searchatlas_context($token_data) { return Metasync_Connect_Manager::instance()->validate_searchatlas_context($token_data); } private function should_validate_ip() { return Metasync_Connect_Manager::instance()->should_validate_ip(); } private function are_user_agents_incompatible($old_ua, $new_ua) { return Metasync_Connect_Manager::instance()->are_user_agents_incompatible($old_ua, $new_ua); } private function extract_browser_name($ua) { return Metasync_Connect_Manager::instance()->extract_browser_name($ua); } public function generate_searchatlas_wp_connect_token($regenerate = false) { return Metasync_Connect_Manager::instance()->generate_searchatlas_wp_connect_token($regenerate); } private function ensure_plugin_auth_token_exists() { Metasync_Connect_Manager::instance()->ensure_plugin_auth_token_exists(); } public function generate_searchatlas_connect_url() { Metasync_Connect_Manager::instance()->generate_searchatlas_connect_url(); } public function check_searchatlas_connect_status() { Metasync_Connect_Manager::instance()->check_searchatlas_connect_status(); } private function create_searchatlas_nonce_token() { return Metasync_Connect_Manager::instance()->create_searchatlas_nonce_token(); } private function get_client_ip() { return Metasync_Connect_Manager::instance()->get_client_ip(); } private function create_encrypted_searchatlas_token($metadata = array()) { return Metasync_Connect_Manager::instance()->create_encrypted_searchatlas_token($metadata); } private function wp_encrypt_token($payload) { return Metasync_Connect_Manager::instance()->wp_encrypt_token($payload); } public function test_enhanced_searchatlas_tokens() { return Metasync_Connect_Manager::instance()->test_enhanced_searchatlas_tokens(); } public function test_searchatlas_ajax_endpoint() { Metasync_Connect_Manager::instance()->test_searchatlas_ajax_endpoint(); } public function simple_ajax_test() { Metasync_Connect_Manager::instance()->simple_ajax_test(); } /** * Create Admin Dashboard Iframe Page * Embeds the Search Atlas dashboard directly in WordPress admin */ public function create_admin_dashboard_iframe() { Metasync_Admin_Pages::get_instance($this)->create_admin_dashboard_iframe(); } /** * Render OTTO cache management interface */ public function render_otto_cache_management() { // Check if OTTO transient cache class exists if (!class_exists('Metasync_Otto_Transient_Cache')) { echo '

'; echo '❌ Error: Transient Cache class not found.'; echo '

'; return; } // Get cache count $cache_count = Metasync_Otto_Transient_Cache::get_cache_count(); ?>

Cache TTL

Configure how long API suggestions are cached before a fresh API call. The stale fallback expires after 12 hours.

min 30 · max 1440

Clear All Cache Plugins

Clear all cache plugins to ensure changes are visible immediately.

get_active_cache_plugins(); if (!empty($active_cache_plugins)) { echo '

Active Cache Plugins Detected:

'; echo ''; } else { echo '

ℹ️ No cache plugins detected.

'; } } catch (Exception $e) { error_log('MetaSync Cache Status Error: ' . $e->getMessage()); echo '

⚠️ An error occurred while retrieving cache plugin status.

'; } } else { echo '

⚠️ Cache Purge class not loaded.

'; } ?>

This will clear cache from WP Rocket, LiteSpeed, W3 Total Cache, and all other detected cache plugins.

0) { echo '

'; echo '✅ Success! Cleared cache for ' . intval( $cleared ) . ' plugin(s)'; if ($plugins) { echo ': ' . esc_html($plugins); } echo '

'; } else { echo '

'; echo 'ℹ️ No cache plugins found to clear. WordPress object cache was cleared.'; echo '

'; } if ($failed > 0) { echo '

'; echo '⚠️ Failed to clear ' . intval( $failed ) . ' plugin(s).'; echo '

'; } } if (isset($_GET['cache_error']) && $_GET['cache_error'] == '1') { $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : ''; if (empty($message)) { $message = 'An unknown error occurred while clearing cache. Please check error logs for details.'; } echo '

'; echo '❌ Error clearing cache: ' . esc_html($message); echo '

'; } ?>
get_hosting_cache_settings(); $wpe_detected = class_exists('WpeCommon'); $kinsta_detected = class_exists('KinstaCache'); ?>

Hosting Cache Integration

Use your hosting provider's native API to purge the entire site cache in one click. These options are independent of cache plugins and target the server-level cache layer.

WP Engine Kinsta
Enable Native Cache Purge

Triggers a full-site cache purge using the native WP Engine and/or Kinsta APIs (based on toggles above).

'; echo '✅ Success! Purged hosting cache on: ' . esc_html($hc_cleared); echo '

'; } if ($hc_failed) { echo '

'; echo '❌ Failed to purge: ' . esc_html($hc_failed); echo '

'; } if ($hc_not_detected && !$hc_cleared && !$hc_failed) { echo '

'; echo 'ℹ️ No enabled hosting providers were detected on this server (' . esc_html($hc_not_detected) . ').'; echo '

'; } } ?>

Object Cache Behaviour

Controls how the WordPress object cache (Redis/Memcached) is cleared when OTTO updates pages.

Transient Cache

Manage suggestions cache. Clearing cache will force fresh API calls on next page load.

Current Cache Status: cached entries

'; if (!empty($url)) { echo '✅ Success! Cleared cache for URL: ' . esc_html($url) . ' (' . intval( $cleared_count ) . ' entries)'; } else { echo '✅ Success! Cleared entire transient cache (' . intval( $cleared_count ) . ' entries)'; } echo '

'; } if (isset($_GET['otto_cache_error']) && $_GET['otto_cache_error'] == '1') { $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : 'An unknown error occurred.'; echo '

'; echo '❌ Error: ' . esc_html($message); echo '

'; } ?>
Clear Entire Transient Cache

This will clear all transient cache entries (suggestions, locks, stale cache, rate limits).

Clear Cache by URL

Enter a specific URL to clear its cached suggestions. Use the full URL including protocol (e.g., https://example.com/page/).

Enter the full URL of the page whose cache you want to clear.

render_debug_mode_section(); } /** * Render error log content for inclusion in Advanced settings */ public function render_error_log_content() { Metasync_Debug_Manager::instance()->render_error_log_content(); } /** * WordPress standard handler for clearing all cache plugins (admin_post hook) * This method runs early and prevents any output before redirect */ public function handle_clear_all_cache_plugins() { Metasync_Otto_Cache_Manager::instance()->handle_clear_all_cache_plugins(); } /** * WordPress standard handler for clearing OTTO cache (admin_post hook) */ public function handle_clear_otto_cache_all() { Metasync_Otto_Cache_Manager::instance()->handle_clear_otto_cache_all(); } /** * WordPress standard handler for clearing OTTO cache by URL (admin_post hook) */ public function handle_clear_otto_cache_url() { Metasync_Otto_Cache_Manager::instance()->handle_clear_otto_cache_url(); } /** * Get hosting cache integration settings with defaults * * @return array Settings array with 'wpengine_enabled' and 'kinsta_enabled' keys */ private function get_hosting_cache_settings() { $defaults = array( 'wpengine_enabled' => true, 'kinsta_enabled' => true, ); $saved = get_option('metasync_hosting_cache_options', array()); return wp_parse_args($saved, $defaults); } /** * AJAX handler for saving hosting cache settings */ public function ajax_save_hosting_cache_settings() { if (!isset($_POST['hosting_cache_nonce']) || !wp_verify_nonce($_POST['hosting_cache_nonce'], 'metasync_hosting_cache_nonce')) { wp_send_json_error(array('message' => 'Security check failed')); return; } if (!Metasync::current_user_has_plugin_access()) { wp_send_json_error(array('message' => 'Insufficient permissions')); return; } $settings = array( 'wpengine_enabled' => !empty($_POST['wpengine_enabled']) && $_POST['wpengine_enabled'] === '1', 'kinsta_enabled' => !empty($_POST['kinsta_enabled']) && $_POST['kinsta_enabled'] === '1', ); update_option('metasync_hosting_cache_options', $settings); wp_send_json_success(array('message' => 'Hosting cache settings saved')); } /** * AJAX handler: save object cache behaviour settings */ public function ajax_save_object_cache_settings() { if (!isset($_POST['object_cache_nonce']) || !wp_verify_nonce($_POST['object_cache_nonce'], 'metasync_object_cache_nonce')) { wp_send_json_error(array('message' => 'Security check failed')); return; } if (!Metasync::current_user_has_plugin_access()) { wp_send_json_error(array('message' => 'Insufficient permissions')); return; } $targeted = (!empty($_POST['targeted_object_cache']) && $_POST['targeted_object_cache'] === '1') ? '1' : '0'; update_option('metasync_targeted_object_cache', $targeted); wp_send_json_success(array('message' => 'Object cache settings saved')); } /** * Get OTTO Cache TTL value in minutes from execution settings. * * @return int */ private function get_otto_cache_ttl_minutes() { $execution_settings = get_option('metasync_execution_settings', array()); return isset($execution_settings['otto_cache_ttl']) ? absint($execution_settings['otto_cache_ttl']) : 30; } /** * AJAX handler for saving OTTO Cache TTL */ public function ajax_save_otto_cache_ttl() { if (!isset($_POST['otto_cache_ttl_nonce']) || !wp_verify_nonce($_POST['otto_cache_ttl_nonce'], 'metasync_otto_cache_ttl_nonce')) { wp_send_json_error(array('message' => 'Security check failed.')); return; } if (!current_user_can('manage_options')) { wp_send_json_error(array('message' => 'Insufficient permissions.')); return; } $ttl = isset($_POST['otto_cache_ttl']) ? absint($_POST['otto_cache_ttl']) : 30; if ($ttl < 30 || $ttl > 1440) { wp_send_json_error(array('message' => sprintf('%s Cache TTL must be between 30 and 1440 minutes.', Metasync::get_whitelabel_otto_name()))); return; } $settings = get_option('metasync_execution_settings', array()); $settings['otto_cache_ttl'] = $ttl; update_option('metasync_execution_settings', $settings); wp_send_json_success(array('message' => sprintf('%s Cache TTL saved.', Metasync::get_whitelabel_otto_name()))); } /** * admin_post handler: purge WP Engine and Kinsta hosting-level caches */ public function handle_purge_hosting_cache() { if (!isset($_POST['hosting_cache_purge_nonce']) || !wp_verify_nonce($_POST['hosting_cache_purge_nonce'], 'metasync_hosting_cache_purge_nonce')) { wp_die('Security check failed'); } if (!Metasync::current_user_has_plugin_access()) { wp_die('You do not have permission to perform this action'); } $redirect_url = admin_url('admin.php?page=' . self::$page_slug . '&tab=advanced'); $settings = $this->get_hosting_cache_settings(); $cleared = array(); $failed = array(); $not_detected = array(); // WP Engine native purge (Varnish + Memcached) if (!empty($settings['wpengine_enabled'])) { if (class_exists('WpeCommon')) { try { WpeCommon::purge_varnish_cache(); WpeCommon::purge_memcached(); $cleared[] = 'WP Engine'; } catch (Exception $e) { error_log('MetaSync: WP Engine hosting cache purge failed - ' . $e->getMessage()); $failed[] = 'WP Engine'; } } else { $not_detected[] = 'WP Engine'; } } // Kinsta native purge (full site) if (!empty($settings['kinsta_enabled'])) { if (class_exists('KinstaCache')) { try { KinstaCache::get_instance()->kinsta_cache_purge_full(); $cleared[] = 'Kinsta'; } catch (Exception $e) { error_log('MetaSync: Kinsta hosting cache purge failed - ' . $e->getMessage()); $failed[] = 'Kinsta'; } } else { $not_detected[] = 'Kinsta'; } } $redirect_url .= '&hosting_cache_cleared=1'; if (!empty($cleared)) { $redirect_url .= '&hc_cleared=' . urlencode(implode(',', $cleared)); } if (!empty($failed)) { $redirect_url .= '&hc_failed=' . urlencode(implode(',', $failed)); } if (!empty($not_detected)) { $redirect_url .= '&hc_not_detected=' . urlencode(implode(',', $not_detected)); } wp_safe_redirect($redirect_url); exit; } /** * Handle debug mode operations (enable/disable/extend) */ private function handle_debug_mode_operations() { Metasync_Debug_Manager::instance()->handle_debug_mode_operations(); } /** * Handle error log operations (clear) */ private function handle_error_log_operations() { Metasync_Debug_Manager::instance()->handle_error_log_operations(); } /** * Handle clear all settings operations */ private function handle_clear_all_settings() { Metasync_Debug_Manager::instance()->handle_clear_all_settings(); } /** * Handle saving plugin access roles from Advanced Settings * @deprecated Now handled by Metasync_Settings_Registration::settings_page_init() */ private function handle_plugin_access_roles_save() { } /** * Get error log content for display */ private function get_error_log_content() { return Metasync_Debug_Manager::instance()->get_error_log_content(); } /** * Memory-efficient function to get last N lines from a large file */ private function get_log_tail($file_path, $lines = null) { return Metasync_Debug_Manager::instance()->get_log_tail($file_path, $lines); } /** * Test whitelabel domain functionality (development/debugging) */ public function test_whitelabel_domain() { Metasync_Connect_Manager::instance()->test_whitelabel_domain(); } /** * Decrypt token using WordPress SALTs */ private function wp_decrypt_token($encrypted_token) { return Metasync_Connect_Manager::instance()->wp_decrypt_token($encrypted_token); } /** * Get active JWT token for the plugin * Public static method accessible from anywhere in the plugin * * @param bool $force_refresh Force generation of new token even if cached one exists * @return string|false JWT token on success, false on failure */ public static function get_active_jwt_token($force_refresh = false) { return Metasync_Connect_Manager::instance()->get_active_jwt_token($force_refresh); } /** * Get fresh JWT token from Search Atlas API with caching * Generates and caches JWT tokens to avoid repeated API calls * * @return string|false JWT token on success, false on failure */ public function get_fresh_jwt_token() { return Metasync_Connect_Manager::instance()->get_fresh_jwt_token(); } /** * Clear cached JWT tokens * Useful when authentication is reset or API key changes */ private function clear_jwt_token_cache() { Metasync_Connect_Manager::instance()->clear_jwt_token_cache(); } /** * Data or Response received from HeartBeat API for admin area. */ public function lgSendCustomerParams() { Metasync_Admin_Ajax::instance()->lgSendCustomerParams(); } /** * Add CSS styles for Search Atlas admin bar status indicator */ public function metasync_admin_bar_style() { Metasync_Admin_Navigation::instance()->metasync_admin_bar_style(); } /** * Add Search Atlas status indicator to WordPress admin bar * Shows sync status with green/red emoji */ public function add_searchatlas_admin_bar_status($wp_admin_bar) { Metasync_Admin_Navigation::instance()->add_searchatlas_admin_bar_status($wp_admin_bar); } // ------------------------------------------------------------------ // Heartbeat / connection-monitoring – delegated to Metasync_Heartbeat_Manager // ------------------------------------------------------------------ public function is_heartbeat_connected($general_settings = null) { return Metasync_Heartbeat_Manager::instance()->is_heartbeat_connected($general_settings); } public function fetch_public_hash($otto_pixel_uuid, $jwt_token) { return Metasync_Heartbeat_Manager::instance()->fetch_public_hash($otto_pixel_uuid, $jwt_token); } public function schedule_heartbeat_cron() { Metasync_Heartbeat_Manager::instance()->schedule_heartbeat_cron(); } public function unschedule_heartbeat_cron() { Metasync_Heartbeat_Manager::instance()->unschedule_heartbeat_cron(); } public function execute_heartbeat_cron_check() { return Metasync_Heartbeat_Manager::instance()->execute_heartbeat_cron_check(); } public function add_heartbeat_cron_schedule($schedules) { return Metasync_Heartbeat_Manager::instance()->add_heartbeat_cron_schedule($schedules); } public function get_heartbeat_state() { return Metasync_Heartbeat_Manager::instance()->get_heartbeat_state(); } public function set_heartbeat_state_key_pending() { Metasync_Heartbeat_Manager::instance()->set_heartbeat_state_key_pending(); } public function execute_burst_heartbeat() { Metasync_Heartbeat_Manager::instance()->execute_burst_heartbeat(); } public function execute_announce_cron() { Metasync_Heartbeat_Manager::instance()->execute_announce_cron(); } public function unschedule_burst_heartbeat_cron() { Metasync_Heartbeat_Manager::instance()->unschedule_burst_heartbeat_cron(); } public function unschedule_announce_cron() { Metasync_Heartbeat_Manager::instance()->unschedule_announce_cron(); } public function maybe_schedule_heartbeat_cron() { Metasync_Heartbeat_Manager::instance()->maybe_schedule_heartbeat_cron(); } public function trigger_immediate_heartbeat_check($context = 'Manual trigger') { return Metasync_Heartbeat_Manager::instance()->trigger_immediate_heartbeat_check($context); } public function handle_immediate_heartbeat_trigger($context = 'WordPress action trigger') { Metasync_Heartbeat_Manager::instance()->handle_immediate_heartbeat_trigger($context); } public function ajax_burst_ping() { Metasync_Heartbeat_Manager::instance()->ajax_burst_ping(); } public function update_heartbeat_cache_after_sync($is_connected, $context = 'Sync operation') { return Metasync_Heartbeat_Manager::instance()->update_heartbeat_cache_after_sync($is_connected, $context); } /** * Refresh Plugin Auth Token * Generates a new Plugin Auth Token and updates heartbeat API */ public function refresh_plugin_auth_token() { Metasync_Connect_Manager::instance()->refresh_plugin_auth_token(); } public function get_plugin_auth_token() { Metasync_Connect_Manager::instance()->get_plugin_auth_token(); } public function reset_searchatlas_authentication() { Metasync_Connect_Manager::instance()->reset_searchatlas_authentication(); } private function cleanup_searchatlas_nonce_tokens() { return Metasync_Connect_Manager::instance()->cleanup_searchatlas_nonce_tokens(); } private function cleanup_searchatlas_rate_limits() { return Metasync_Connect_Manager::instance()->cleanup_searchatlas_rate_limits(); } private function get_available_menu_items() { return Metasync_Admin_Navigation::instance()->get_available_menu_items(); } /** * Add options page */ public function add_plugin_settings_page() { Metasync_Admin_Navigation::instance()->add_plugin_settings_page($this); } /** * General Options page callback */ public function create_admin_settings_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_settings_page(); } public function render_navigation_menu($current_page = null) { Metasync_Admin_Navigation::instance()->render_navigation_menu($current_page); } public function render_plugin_header($page_title = null) { Metasync_Admin_Navigation::instance()->render_plugin_header($page_title); } /** * Open the Yoast-style 3-column page layout. * Must be paired with render_layout_close(). */ public function render_layout_open($page_title = '', $current_page = '', $description = '') { Metasync_Admin_Navigation::instance()->render_layout_open($page_title, $current_page, $description); } /** * Close the 3-column layout opened by render_layout_open(). * * @param bool $show_promo Whether to render the right promo sidebar. Default true. */ public function render_layout_close($show_promo = true) { Metasync_Admin_Navigation::instance()->render_layout_close($show_promo); } /* Method to handle Ajax request from "General Settings" page */ public function meta_sync_save_settings() { Metasync_Settings_Registration::instance()->meta_sync_save_settings(); } /** * AJAX handler for saving execution settings */ public function ajax_save_execution_settings() { Metasync_Settings_Registration::instance()->ajax_save_execution_settings(); } /** * Get Action Scheduler concurrent batches from execution settings * * @param int $default_batches Default concurrent batches * @return int Configured concurrent batches */ public function get_action_scheduler_batches($default_batches) { // Only apply if Action Scheduler is active if (!class_exists('ActionScheduler')) { return $default_batches; } return $this->get_execution_setting('action_scheduler_batches'); } /** * Get Action Scheduler retention period from execution settings * Converts days to seconds for Action Scheduler * * @param int $default_seconds Default retention period in seconds * @return int Configured retention period in seconds */ public function get_action_scheduler_retention_period($default_seconds) { // Only apply if Action Scheduler is active if (!class_exists('ActionScheduler')) { return $default_seconds; // Default is 30 days = 2592000 seconds } $cleanup_days = $this->get_execution_setting('queue_cleanup_days'); return $cleanup_days * DAY_IN_SECONDS; } /* * Sync setting on CRUD term category */ public function admin_crud_term($term_id,$term_tax_id,$taxonomy) { # Handle term creation, update, or deletion $this->sync_term($term_id, $taxonomy); } /* * Sync setting on Delete term category */ public function admin_delete_term($term_id,$taxonomy) { # Handle term deletion $this->sync_term($term_id, $taxonomy); } /* * Handle category deletion - Sync after category is deleted * This hook fires AFTER the category is fully deleted from the database */ public function admin_delete_category($term_id) { try { # Initialize MetaSync API request class and trigger synchronization (new Metasync_Sync_Requests())->SyncCustomerParams(); } catch (Exception $e) { # Log any API request errors for debugging error_log('Metasync API Error: ' . $e->getMessage()); } } /* * Call the SYNC API */ private function sync_term($term_id, $taxonomy) { # Ensure the term belongs to the 'category' taxonomy and is not an error if ($taxonomy !== 'category' ) return; try { # Initialize MetaSync API request class and trigger synchronization (new Metasync_Sync_Requests())->SyncCustomerParams(); } catch (Exception $e) { # Log any API request errors for debugging error_log('Metasync API Error: ' . $e->getMessage()); } } /** * Dashboard page callback */ public function create_admin_dashboard_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_dashboard_page(); } /** * Robots.txt page callback */ public function create_admin_robots_txt_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_robots_txt_page(); } /** * Media Optimization page callback */ public function create_admin_media_optimization_page() { $this->render_layout_open('Media Optimization', 'media_optimization', 'Compress and optimize images to improve page load speed.'); // Load media optimization settings class require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php'; $save_success = false; // Handle form submissions if (isset($_POST['metasync_media_optimization_nonce'])) { check_admin_referer('metasync_save_media_optimization', 'metasync_media_optimization_nonce'); // Settings shape the whole site's conversion pipeline — restrict // the save (and the reset) to site admins, not just anyone who can // open the page via plugin_access_roles. if (!current_user_can('manage_options')) { wp_die(__('Sorry, you are not allowed to manage media optimization settings.', 'metasync')); } // Handle reset to defaults if (!empty($_POST['metasync_media_reset'])) { $defaults = Metasync_Media_Settings::get_defaults(); Metasync_Media_Settings::save_settings($defaults); $save_success = true; } elseif (isset($_POST['metasync_media'])) { $input = wp_unslash($_POST['metasync_media']); Metasync_Media_Settings::save_settings($input); $save_success = true; } } // Tab handling $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'settings'; // Prepare image library data $list_table = null; $stats = null; $batch_progress = null; require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; $list_table = new Metasync_Media_Library_List_Table(); $list_table->prepare_items(); $stats = Metasync_Media_Library_List_Table::get_stats(); $batch_progress = Metasync_Media_Batch_Optimizer::get_progress(); // Render the admin page view require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/views/admin-page.php'; $this->render_layout_close(); } /** * Code Minification page callback */ public function create_admin_code_minification_page() { $this->render_layout_open('Code Minification', 'code_minification', 'Minify CSS, JavaScript, and HTML to improve performance.'); // Load settings and compatibility classes require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-minification-settings.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-minification-cache.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-compatibility-guard.php'; $save_success = false; // Handle form submissions if (isset($_POST['metasync_code_minification_nonce'])) { check_admin_referer('metasync_save_code_minification', 'metasync_code_minification_nonce'); // Handle reset to defaults if (!empty($_POST['metasync_code_min_reset'])) { $defaults = Metasync_Minification_Settings::get_defaults(); Metasync_Minification_Settings::save_settings($defaults); $save_success = true; } elseif (isset($_POST['metasync_code_min'])) { $input = (array) wp_unslash($_POST['metasync_code_min']); Metasync_Minification_Settings::save_settings($input); $save_success = true; } } // Tab handling $current_tab = isset($_GET['tab']) ? sanitize_text_field(wp_unslash($_GET['tab'])) : 'settings'; $settings = Metasync_Minification_Settings::get_settings(); $conflicts = Metasync_Compatibility_Guard::get_active_conflicts(); // Render the admin page view require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/views/admin-page.php'; $this->render_layout_close(); } // ── Media Optimization AJAX Handlers ── /** * AJAX: Optimize a single image. */ public function ajax_optimize_single_image() { check_ajax_referer('metasync_media_opt_nonce', 'nonce'); if (!current_user_can('upload_files')) { wp_send_json_error(__('Permission denied.', 'metasync')); } $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0; if (!$attachment_id) { wp_send_json_error(__('Invalid attachment ID.', 'metasync')); } // Ownership: under the replace strategy this permanently deletes the // attachment's original file, so upload_files alone is not enough — // the caller must be allowed to edit this specific attachment. if (!current_user_can('edit_post', $attachment_id)) { wp_send_json_error(__('Permission denied.', 'metasync')); } require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-image-converter.php'; $settings = Metasync_Media_Settings::get_settings(); $file = get_attached_file($attachment_id); $mime = get_post_mime_type($attachment_id); if (!$file || !file_exists($file)) { wp_send_json_error(__('Optimization failed: file not found.', 'metasync')); } $max_bytes = 10 * 1024 * 1024; if (filesize($file) > $max_bytes) { $size_mb = round(filesize($file) / 1024 / 1024, 1); wp_send_json_error(sprintf( __('Optimization skipped: file size (%s MB) exceeds the 10 MB safety limit to prevent memory issues.', 'metasync'), $size_mb )); } if (!in_array($mime, ['image/jpeg', 'image/png'], true)) { wp_send_json_error(__('Optimization failed: unsupported image format.', 'metasync')); } $success = Metasync_Image_Converter::convert_attachment($attachment_id, $settings); if ($success) { require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; wp_send_json_success([ 'status_html' => Metasync_Media_Library_List_Table::render_status_html($attachment_id), 'can_revert' => Metasync_Image_Converter::can_revert($attachment_id), ]); } wp_send_json_error(__('Optimization failed: conversion could not be completed.', 'metasync')); } /** * AJAX: Revert a single image. */ public function ajax_revert_single_image() { check_ajax_referer('metasync_media_opt_nonce', 'nonce'); if (!current_user_can('upload_files')) { wp_send_json_error(__('Permission denied.', 'metasync')); } $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0; if (!$attachment_id) { wp_send_json_error(__('Invalid attachment ID.', 'metasync')); } // Ownership: reverting deletes the attachment's converted files, so // restrict it to users who may edit this specific attachment. if (!current_user_can('edit_post', $attachment_id)) { wp_send_json_error(__('Permission denied.', 'metasync')); } // Replace-strategy conversions have no original to restore — reverting // would delete the attachment's only file. Refuse up front, like bulk revert does. if (!Metasync_Image_Converter::can_revert($attachment_id)) { wp_send_json_error(__('This image cannot be reverted. The original file no longer exists (replace strategy).', 'metasync')); } $success = Metasync_Image_Converter::revert_attachment($attachment_id); if ($success) { wp_send_json_success(); } wp_send_json_error(__('Revert failed. Original file may not exist (replace strategy).', 'metasync')); } /** * AJAX: Start batch optimization. */ public function ajax_start_batch_optimize() { check_ajax_referer('metasync_media_opt_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Permission denied.', 'metasync')); } require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; $settings = Metasync_Media_Settings::get_settings(); $progress = Metasync_Media_Batch_Optimizer::start_batch($settings); wp_send_json_success($progress); } /** * AJAX: Cancel batch optimization. */ public function ajax_cancel_batch_optimize() { check_ajax_referer('metasync_media_opt_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Permission denied.', 'metasync')); } require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; Metasync_Media_Batch_Optimizer::cancel_batch(); wp_send_json_success(); } /** * AJAX: Get batch progress + stats. */ public function ajax_batch_progress() { check_ajax_referer('metasync_media_opt_nonce', 'nonce'); // Read-only progress/stats polling — the same capability that can // open the media optimization page is enough, but a nonce alone // never was. if (!current_user_can('upload_files')) { wp_send_json_error(__('Permission denied.', 'metasync')); } require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; $progress = Metasync_Media_Batch_Optimizer::get_progress(); $progress['stats'] = Metasync_Media_Library_List_Table::get_stats(); wp_send_json_success($progress); } /** * AJAX: Bulk optimize selected images. */ public function ajax_bulk_optimize_selected() { check_ajax_referer('metasync_media_opt_nonce', 'nonce'); if (!current_user_can('upload_files')) { wp_send_json_error(__('Permission denied.', 'metasync')); } $ids = isset($_POST['ids']) ? array_map('absint', explode(',', sanitize_text_field(wp_unslash($_POST['ids'])))) : []; $ids = array_filter($ids); if (empty($ids)) { wp_send_json_error(__('No images selected.', 'metasync')); } require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php'; $settings = Metasync_Media_Settings::get_settings(); $success = 0; $failed = 0; $denied = 0; foreach ($ids as $id) { // Per-attachment ownership: replace mode deletes originals, so // upload_files alone must not allow converting images the caller // does not own/cannot edit. if (!current_user_can('edit_post', $id)) { $denied++; continue; } if (Metasync_Image_Converter::convert_attachment($id, $settings)) { $success++; } else { $failed++; } } wp_send_json_success([ 'success' => $success, 'failed' => $failed, 'denied' => $denied, ]); } /** * AJAX: Bulk unoptimize (revert) selected images. */ public function ajax_bulk_unoptimize_selected() { check_ajax_referer('metasync_media_opt_nonce', 'nonce'); if (!current_user_can('upload_files')) { wp_send_json_error(__('Permission denied.', 'metasync')); } $ids = isset($_POST['ids']) ? array_map('absint', explode(',', sanitize_text_field(wp_unslash($_POST['ids'])))) : []; $ids = array_filter($ids); if (empty($ids)) { wp_send_json_error(__('No images selected.', 'metasync')); } $success = 0; $failed = 0; $skipped = 0; $denied = 0; $errors = []; foreach ($ids as $id) { // Per-attachment ownership: reverting deletes the attachment's // converted files, so restrict it per image, not per role. if (!current_user_can('edit_post', $id)) { $denied++; continue; } $format = get_post_meta($id, '_metasync_converted_format', true); if (!$format) { $skipped++; continue; } if (!Metasync_Image_Converter::can_revert($id)) { $skipped++; $file = get_attached_file($id); $name = $file ? basename($file) : "ID {$id}"; $errors[] = sprintf(__('%s: skipped — original image unavailable.', 'metasync'), $name); continue; } if (Metasync_Image_Converter::revert_attachment($id)) { $success++; } else { $failed++; $file = get_attached_file($id); $name = $file ? basename($file) : "ID {$id}"; $errors[] = sprintf(__('%s: revert failed (original file may not exist).', 'metasync'), $name); } } wp_send_json_success([ 'success' => $success, 'failed' => $failed, 'skipped' => $skipped, 'denied' => $denied, 'errors' => $errors, ]); } /** * AJAX: Process one batch tick (browser-driven chaining). */ public function ajax_process_batch_tick() { check_ajax_referer('metasync_media_opt_nonce', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(__('Permission denied.', 'metasync')); } require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; $progress = Metasync_Media_Batch_Optimizer::process_ajax_tick(); $progress['stats'] = Metasync_Media_Library_List_Table::get_stats(); wp_send_json_success($progress); } /** * AJAX: Delete an orphaned media record (attachment whose file is missing). * * Only deletes when the file is confirmed missing on disk, so valid * attachments can never be removed through this endpoint. */ public function ajax_delete_orphaned_image() { check_ajax_referer('metasync_media_opt_nonce', 'nonce'); $attachment_id = isset($_POST['attachment_id']) ? absint($_POST['attachment_id']) : 0; if (!$attachment_id) { wp_send_json_error(__('Invalid attachment ID.', 'metasync')); } if (!current_user_can('delete_post', $attachment_id)) { wp_send_json_error(__('Permission denied.', 'metasync')); } if (get_post_type($attachment_id) !== 'attachment') { wp_send_json_error(__('Not an attachment.', 'metasync')); } require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-library-list-table.php'; // Guard: only orphaned records (missing file) may be deleted here. if (!Metasync_Media_Library_List_Table::is_file_missing($attachment_id)) { wp_send_json_error(__('The image file exists; refusing to delete a valid attachment.', 'metasync')); } $deleted = wp_delete_attachment($attachment_id, true); if ($deleted) { wp_send_json_success([ 'stats' => Metasync_Media_Library_List_Table::get_stats(), ]); } wp_send_json_error(__('Failed to delete the orphaned media record.', 'metasync')); } /** * Cron handler: Process batch optimization tick. */ public function handle_media_batch_cron() { require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-settings.php'; require_once plugin_dir_path(dirname(__FILE__)) . 'media-optimization/class-media-batch-optimizer.php'; Metasync_Media_Batch_Optimizer::process_batch_tick(); } /** * Report Issue page callback */ public function create_admin_report_issue_page() { // Load the Report Issue view require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php'; } /** * XML Sitemap page callback */ public function create_admin_xml_sitemap_page() { // Load sitemap generator class require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php'; $sitemap_generator = new Metasync_Sitemap_Generator(); // Determine active tab (from GET param or POST redirect) $active_tab = 'general'; if (isset($_GET['tab']) && in_array($_GET['tab'], ['general', 'news', 'video'], true)) { $active_tab = sanitize_text_field(wp_unslash($_GET['tab'])); } elseif (isset($_POST['redirect_tab']) && in_array($_POST['redirect_tab'], ['general', 'news', 'video'], true)) { $active_tab = sanitize_text_field(wp_unslash($_POST['redirect_tab'])); } // Handle Main Sitemap settings form submission if (isset($_POST['metasync_sitemap_settings_nonce']) && isset($_POST['save_sitemap_settings'])) { check_admin_referer('metasync_sitemap_settings_action', 'metasync_sitemap_settings_nonce'); $sitemap_settings = [ '_configured' => true, 'post_types' => array_map('sanitize_key', (array) ($_POST['sitemap_post_types'] ?? [])), 'categories' => array_map('absint', (array) ($_POST['sitemap_categories'] ?? [])), 'tags' => array_map('absint', (array) ($_POST['sitemap_tags'] ?? [])), 'taxonomies' => array_map('sanitize_key', (array) ($_POST['sitemap_taxonomies'] ?? [])), 'excluded_urls' => sanitize_textarea_field(wp_unslash($_POST['sitemap_excluded_urls'] ?? '')), ]; update_option('metasync_sitemap_settings', $sitemap_settings); // Regenerate sitemap with new content settings $result = $sitemap_generator->generate_sitemap(); if (is_wp_error($result)) { echo '

' . esc_html( sprintf(__('Settings saved but sitemap generation failed: %s', 'metasync'), $result->get_error_message()) ) . '

'; } elseif (false === $result) { echo '

' . esc_html__('Settings saved but sitemap generation failed: the sitemap data could not be stored, so no sitemap is being served. Check your object cache and database write settings, then try again.', 'metasync') . '

'; } else { echo '

' . esc_html__('Sitemap content settings saved and sitemap regenerated!', 'metasync') . '

'; } } // Handle News Sitemap settings form submission if (isset($_POST['metasync_news_sitemap_nonce']) && isset($_POST['save_news_sitemap'])) { check_admin_referer('metasync_news_sitemap_action', 'metasync_news_sitemap_nonce'); // Build generic taxonomy filters $news_taxonomies = []; if (!empty($_POST['news_taxonomies']) && is_array($_POST['news_taxonomies'])) { foreach ($_POST['news_taxonomies'] as $tax_name => $term_ids) { $news_taxonomies[sanitize_key($tax_name)] = array_map('absint', (array) $term_ids); } } $news_settings = [ 'enabled' => isset($_POST['news_enabled']), 'post_types' => array_map('sanitize_key', (array) ($_POST['news_post_types'] ?? ['post'])), 'categories' => array_map('absint', (array) ($_POST['news_categories'] ?? [])), 'tags' => array_map('absint', (array) ($_POST['news_tags'] ?? [])), 'taxonomies' => $news_taxonomies, 'excluded_urls' => sanitize_textarea_field(wp_unslash($_POST['news_excluded_urls'] ?? '')), 'publication_name' => sanitize_text_field(wp_unslash($_POST['publication_name'] ?? '')), 'publication_language' => sanitize_text_field(wp_unslash($_POST['publication_language'] ?? '')), ]; // Always invalidate old cache before saving new settings delete_transient('metasync_vsm_' . md5('news-sitemap.xml')); update_option('metasync_news_sitemap_settings', $news_settings); if ($news_settings['enabled']) { $sitemap_generator->generate_news_sitemap(); update_option('metasync_sitemap_last_generated', current_time('mysql')); echo '

' . esc_html__('News sitemap settings saved and sitemap regenerated!', 'metasync') . '

'; } else { // Also remove physical file if it exists if (file_exists(ABSPATH . 'news-sitemap.xml')) { @unlink(ABSPATH . 'news-sitemap.xml'); } echo '

' . esc_html__('News sitemap settings saved. Sitemap cache cleared.', 'metasync') . '

'; } } // Handle Video Sitemap settings form submission if (isset($_POST['metasync_video_sitemap_nonce']) && isset($_POST['save_video_sitemap'])) { check_admin_referer('metasync_video_sitemap_action', 'metasync_video_sitemap_nonce'); // Build generic taxonomy filters $video_taxonomies = []; if (!empty($_POST['video_taxonomies']) && is_array($_POST['video_taxonomies'])) { foreach ($_POST['video_taxonomies'] as $tax_name => $term_ids) { $video_taxonomies[sanitize_key($tax_name)] = array_map('absint', (array) $term_ids); } } $video_settings = [ 'enabled' => isset($_POST['video_enabled']), 'post_types' => array_map('sanitize_key', (array) ($_POST['video_post_types'] ?? ['post', 'page'])), 'auto_detect' => isset($_POST['auto_detect']), 'taxonomies' => $video_taxonomies, 'excluded_urls' => sanitize_textarea_field(wp_unslash($_POST['video_excluded_urls'] ?? '')), // This array replaces the stored option wholesale, so every // setting must be represented here or it is wiped on save. 'video_url_meta_keys' => sanitize_textarea_field(wp_unslash($_POST['video_url_meta_keys'] ?? '')), 'video_thumbnail_meta_keys' => sanitize_textarea_field(wp_unslash($_POST['video_thumbnail_meta_keys'] ?? '')), ]; // Always invalidate old cache before saving new settings delete_transient('metasync_vsm_' . md5('video-sitemap.xml')); update_option('metasync_video_sitemap_settings', $video_settings); if ($video_settings['enabled']) { $sitemap_generator->generate_video_sitemap(); update_option('metasync_sitemap_last_generated', current_time('mysql')); echo '

' . esc_html__('Video sitemap settings saved and sitemap regenerated!', 'metasync') . '

'; } else { // Also remove physical file if it exists if (file_exists(ABSPATH . 'video-sitemap.xml')) { @unlink(ABSPATH . 'video-sitemap.xml'); } echo '

' . esc_html__('Video sitemap settings saved. Sitemap cache cleared.', 'metasync') . '

'; } } // Handle News Sitemap generate action if (isset($_POST['metasync_news_sitemap_nonce']) && isset($_POST['generate_news_sitemap'])) { check_admin_referer('metasync_news_sitemap_action', 'metasync_news_sitemap_nonce'); $result = $sitemap_generator->generate_news_sitemap(); if ($result) { echo '

' . esc_html__('News sitemap generated successfully!', 'metasync') . '

'; } else { echo '

' . esc_html__('News sitemap generation failed. Check if it is enabled and no conflicting plugins are active.', 'metasync') . '

'; } } // Handle Video Sitemap generate action if (isset($_POST['metasync_video_sitemap_nonce']) && isset($_POST['generate_video_sitemap'])) { check_admin_referer('metasync_video_sitemap_action', 'metasync_video_sitemap_nonce'); $result = $sitemap_generator->generate_video_sitemap(); if ($result) { echo '

' . esc_html__('Video sitemap generated successfully!', 'metasync') . '

'; } else { echo '

' . esc_html__('Video sitemap generation failed. Check if it is enabled and no conflicting plugins are active.', 'metasync') . '

'; } } // Handle form submissions if (isset($_POST['metasync_sitemap_nonce'])) { check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce'); if (isset($_POST['generate_sitemap'])) { // Auto-disable other sitemap generators before generating $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators(); // Generate news/video sitemaps FIRST so they exist when the main sitemap builds its index $news_opts = get_option('metasync_news_sitemap_settings', []); $video_opts = get_option('metasync_video_sitemap_settings', []); $extras = []; if (!empty($news_opts['enabled'])) { if ($sitemap_generator->generate_news_sitemap()) { $extras[] = 'news'; } } // Reclaim unreferenced memory between the news and video passes // so the video generator starts with more headroom (they share // one request). Each generator already releases its own object // cache via clean_post_cache(), so no cache flush is needed here. if (function_exists('gc_collect_cycles')) { gc_collect_cycles(); } if (!empty($video_opts['enabled'])) { if ($sitemap_generator->generate_video_sitemap()) { $extras[] = 'video'; } } // Same inter-pass GC before the main sitemap pass begins. if (function_exists('gc_collect_cycles')) { gc_collect_cycles(); } // Generate main sitemap (its index will include news/video since they now exist) $result = $sitemap_generator->generate_sitemap(); if (is_wp_error($result)) { $error_msg = $result->get_error_message(); error_log('[MetaSync] Sitemap generation failed: ' . $error_msg); echo '

' . esc_html( sprintf(__('Sitemap generation failed: %s', 'metasync'), $error_msg) ) . '

'; } elseif (false === $result) { echo '

' . esc_html__('Sitemap generation failed: the sitemap data could not be stored, so no sitemap is being served. Check your object cache and database write settings, then try again.', 'metasync') . '

'; } else { $message = esc_html__('Sitemap generated successfully!', 'metasync'); if (!empty($extras)) { $message .= ' ' . sprintf( esc_html__('Also generated %s sitemap(s).', 'metasync'), implode(' & ', $extras) ); } if ($disabled_plugins) { $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync'); } // Check if robots.txt was updated $robots_result = get_transient('metasync_sitemap_robots_updated'); if ($robots_result && $robots_result['success']) { if ($robots_result['action'] === 'added') { $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync'); } elseif ($robots_result['action'] === 'updated') { $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync'); } elseif ($robots_result['action'] === 'created') { $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync'); } delete_transient('metasync_sitemap_robots_updated'); } echo '

' . esc_html($message) . '

'; } } elseif (isset($_POST['enable_auto_update'])) { update_option('metasync_sitemap_auto_update', true); $sitemap_generator->setup_auto_update_hooks(); echo '

' . esc_html__('Auto-update enabled!', 'metasync') . '

'; } elseif (isset($_POST['disable_auto_update'])) { update_option('metasync_sitemap_auto_update', false); echo '

' . esc_html__('Auto-update disabled!', 'metasync') . '

'; } elseif (isset($_POST['delete_general_sitemap'])) { $deleted = $sitemap_generator->delete_sitemap('general'); if ($deleted) { // Disable auto-update only when the general sitemap is removed update_option('metasync_sitemap_auto_update', false); // Re-enable WP core sitemap only if no other MetaSync sitemaps remain if (!$sitemap_generator->sitemap_exists()) { delete_option('metasync_disable_wp_sitemap'); } echo '

' . esc_html__('General sitemap deleted successfully!', 'metasync') . '

'; } else { echo '

' . esc_html__('Failed to delete general sitemap. The files may not exist or are not writable.', 'metasync') . '

'; } } elseif (isset($_POST['delete_news_sitemap'])) { $deleted = $sitemap_generator->delete_sitemap('news'); if ($deleted) { echo '

' . esc_html__('News sitemap deleted successfully!', 'metasync') . '

'; } else { echo '

' . esc_html__('Failed to delete news sitemap. The file may not exist or is not writable.', 'metasync') . '

'; } } elseif (isset($_POST['delete_video_sitemap'])) { $deleted = $sitemap_generator->delete_sitemap('video'); if ($deleted) { echo '

' . esc_html__('Video sitemap deleted successfully!', 'metasync') . '

'; } else { echo '

' . esc_html__('Failed to delete video sitemap. The file may not exist or is not writable.', 'metasync') . '

'; } } elseif (isset($_POST['delete_sitemap'])) { // Delete all sitemaps: main + news + video (handled by delete_sitemap) $deleted = $sitemap_generator->delete_sitemap(); if ($deleted) { // Also disable auto-update when deleting update_option('metasync_sitemap_auto_update', false); // Re-enable WP core sitemap so the site isn't left with zero sitemaps delete_option('metasync_disable_wp_sitemap'); echo '

' . esc_html__('All sitemaps deleted successfully!', 'metasync') . '

'; } else { echo '

' . esc_html__('Failed to delete sitemaps. The files may not exist or are not writable.', 'metasync') . '

'; } } elseif (isset($_POST['enable_other_sitemaps'])) { // Re-enable other sitemap plugins $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators(); if ($enabled_plugins) { echo '

' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '

'; } else { echo '

' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '

'; } } } // Get sitemap info $sitemap_exists = $sitemap_generator->sitemap_exists(); $sitemap_url = $sitemap_generator->get_sitemap_url(); $url_count = $sitemap_generator->count_urls(); $last_generated = $sitemap_generator->get_last_generated_time(); $auto_update_enabled = get_option('metasync_sitemap_auto_update', false); $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins(); // Main sitemap content settings $sitemap_settings = get_option('metasync_sitemap_settings', [ 'post_types' => [], 'categories' => [], 'tags' => [], 'taxonomies' => [], 'excluded_urls' => '', ]); // News and video sitemap settings for tabs $news_settings = get_option('metasync_news_sitemap_settings', [ 'enabled' => false, 'post_types' => ['post'], 'categories' => [], 'tags' => [], 'taxonomies' => [], 'excluded_urls' => '', 'publication_name' => '', 'publication_language' => '', ]); $video_settings = get_option('metasync_video_sitemap_settings', [ 'enabled' => false, 'post_types' => ['post', 'page'], 'auto_detect' => true, 'taxonomies' => [], 'excluded_urls' => '', ]); // Load view require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php'; } /** * Custom Pages page callback */ public function create_admin_custom_pages_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_custom_pages_page(); } /** * 404 Monitor page callback */ public function create_admin_404_monitor_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_404_monitor_page(); } /** * SEO Health dashboard page callback */ public function create_admin_seo_health_page() { require_once plugin_dir_path(__FILE__) . 'class-metasync-seo-health.php'; Metasync_SEO_Health::get_instance()->render_page(); } /** * Site Verification page callback */ public function create_admin_search_engine_verification_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_search_engine_verification_page(); } /** * Local Business page callback */ public function create_admin_local_business_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_local_business_page(); } /** * Code Snippets page callback */ public function create_admin_code_snippets_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_code_snippets_page(); } /** * Schema Markup settings page callback */ public function create_admin_schema_markup_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_schema_markup_page(); } /** * Breadcrumbs settings page callback */ public function create_admin_breadcrumbs_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_breadcrumbs_page(); } /** * Google Instant Index Setting page callback */ public function create_admin_google_instant_index_page() { $this->render_layout_open('Instant Indexing', 'instant_index', 'Submit URLs to Google for instant indexing via the Indexing API.'); // Render shared Google Index credentials section if (!function_exists('google_index_direct')) { if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) { require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'; } else { error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'); return; } } $google_index = google_index_direct(); $service_info = $google_index->get_service_account_info(); $is_configured = !isset($service_info['error']); $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : ''; include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php'; // Render post types selection with save form $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; ?>
'button button-primary')); ?>
render_layout_close(); } /** * Google Console page callback */ public function create_admin_google_console_page() { $this->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.'); $service_info = function_exists('google_index_direct') ? google_index_direct()->get_service_account_info() : ['error' => 'Module not loaded']; $is_configured = !isset($service_info['error']); include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php'; $this->render_layout_close(); } /** * Bing Console page callback */ public function create_admin_bing_console_page() { $this->render_layout_open('Bing Console', 'bing_console', 'Submit URLs to Bing for instant indexing via IndexNow.'); require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php'; $bing_instant_index = new Metasync_Bing_Instant_Index(); $bing_instant_index->show_bing_instant_indexing_console(); $this->render_layout_close(); } /** * Global Options page callback */ public function create_admin_global_settings_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_global_settings_page(); } /** * Common Meta Options page callback */ public function create_admin_common_meta_settings_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_common_meta_settings_page(); } /** * Social meta page callback */ public function create_admin_social_meta_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_social_meta_page(); } /** * Indexation Control page callback */ public function create_admin_seo_controls_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_seo_controls_page(); } /** * redirection page callback with tabs */ public function create_admin_redirections_page() { Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->create_admin_redirections_page(); } /** * Display transient error/success messages for redirections */ public function display_redirection_messages() { Metasync_Redirections_Admin::get_instance($this->db_redirection, $this)->display_redirection_messages(); } /** * Display admin notice when batch processing was deferred due to high CPU load. * Reads transient set by Metasync_CPU_Monitor::record_deferral() and clears it. */ public function display_cpu_deferral_notice() { $data = get_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT ); if ( ! $data || ! is_array( $data ) ) { return; } delete_transient( Metasync_CPU_Monitor::DEFER_NOTICE_TRANSIENT ); echo '

'; printf( /* translators: 1: plugin name, 2: current load, 3: threshold, 4: core count */ esc_html__( '%1$s: Batch processing was deferred — server CPU load (%2$s) exceeded the threshold (%3$s on %4$s cores). Processing will resume automatically.', 'metasync' ), esc_html( Metasync::get_effective_plugin_name() ), esc_html( $data['load'] ), esc_html( $data['threshold'] ), esc_html( $data['cores'] ) ); echo '

'; } /** * Display info notice when another SEO plugin also generates /llms.txt. * * MetaSync always serves its own version when enabled (priority 1). This * notice simply informs the admin that another plugin was detected. */ public function display_llms_txt_conflict_notice() { if (!get_transient('metasync_llms_conflict')) { return; } echo '

'; echo esc_html(sprintf(__('Note: Another SEO plugin (Yoast, Rank Math, or AIOSEO) may also be generating /llms.txt. %s\'s version takes priority when enabled.', 'metasync'), Metasync::get_effective_plugin_name())); echo '

'; } /** * AJAX handler for updating database structure */ public function ajax_update_db_structure() { Metasync_Admin_Ajax::instance()->ajax_update_db_structure(); } /** * AJAX handler to save wizard progress * * @since 1.0.0 */ public function ajax_save_wizard_progress() { Metasync_Admin_Ajax::instance()->ajax_save_wizard_progress(); } /** * AJAX handler to complete wizard * * @since 1.0.0 */ public function ajax_complete_wizard() { Metasync_Admin_Ajax::instance()->ajax_complete_wizard(); } /** * AJAX handler to validate robots.txt content */ public function ajax_validate_robots() { Metasync_Admin_Ajax::instance()->ajax_validate_robots(); } /** * AJAX handler to get default robots.txt content */ public function ajax_get_default_robots() { Metasync_Admin_Ajax::instance()->ajax_get_default_robots(); } /** * AJAX handler to preview robots.txt backup content */ public function ajax_preview_robots_backup() { Metasync_Admin_Ajax::instance()->ajax_preview_robots_backup(); } /** * AJAX handler to delete robots.txt backup */ public function ajax_delete_robots_backup() { Metasync_Admin_Ajax::instance()->ajax_delete_robots_backup(); } /** * AJAX handler to restore robots.txt backup */ public function ajax_restore_robots_backup() { Metasync_Admin_Ajax::instance()->ajax_restore_robots_backup(); } /** * AJAX handler to fetch a paginated page of robots.txt backups */ public function ajax_get_robots_backups() { Metasync_Admin_Ajax::instance()->ajax_get_robots_backups(); } public function ajax_create_redirect_from_404() { Metasync_Admin_Ajax::instance()->ajax_create_redirect_from_404(); } /** * AJAX handler for testing host blocking with GET request */ public function ajax_test_host_blocking_get() { Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_get(); } /** * AJAX handler for testing host blocking with POST request */ public function ajax_test_host_blocking_post() { Metasync_Admin_Ajax::instance()->ajax_test_host_blocking_post(); } /** * Register REST API endpoint for ping */ public function register_ping_rest_endpoint() { register_rest_route('metasync/v1', '/ping', array( 'methods' => array('GET', 'POST'), 'callback' => array($this, 'handle_ping_rest_endpoint'), 'permission_callback' => '__return_true', // Allow public access 'args' => array( 'test' => array( 'description' => 'Optional test parameter', 'type' => 'string', 'sanitize_callback' => 'sanitize_text_field', ), ), )); } /** * Handle REST API ping endpoint */ public function handle_ping_rest_endpoint($request) { // Get request method $method = $request->get_method(); // Prepare response data $response_data = array( 'response' => 'pong', 'method' => $method, 'timestamp' => current_time('mysql'), 'site_url' => home_url(), 'plugin_version' => METASYNC_VERSION ); // Add request data for POST requests if ($method === 'POST') { $body = $request->get_body(); if (!empty($body)) { $response_data['received_data'] = json_decode($body, true); } // Add any query parameters $params = $request->get_params(); if (!empty($params)) { $response_data['query_params'] = $params; } } // Add test parameter if provided $test_param = $request->get_param('test'); if (!empty($test_param)) { $response_data['test_param'] = $test_param; } return new WP_REST_Response($response_data, 200); } /** * Site error logs page callback */ public function create_admin_error_logs_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_error_logs_page(); } /** * Compatibility page callback */ public function create_admin_compatibility_page() { Metasync_Compatibility_Checker::instance()->create_admin_compatibility_page($this); } /** * Sync Log page callback */ public function create_admin_sync_log_page() { // Classes are now autoloaded $sync_db = new Metasync_Sync_History_Database(); // Handle AJAX requests for sync log data if (wp_doing_ajax()) { $this->handle_sync_log_ajax(); return; } // Get pagination parameters $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; $per_page = Metasync_Per_Page_Helper::resolve('sync_log', 10); $offset = ($page - 1) * $per_page; // Get filters $filters = [ // UI exposes date_range and status only. We compute date_from/date_to based on date_range 'date_range' => isset($_GET['date_range']) ? sanitize_text_field(wp_unslash($_GET['date_range'])) : '', 'status' => isset($_GET['status']) ? sanitize_text_field(wp_unslash($_GET['status'])) : '', ]; // Map date_range to concrete date_from/date_to for DB queries $date_range = $filters['date_range']; $wp_now_ts = current_time('timestamp'); $date_from = ''; $date_to = ''; if (!empty($date_range)) { // End boundary is now by default $date_to = date('Y-m-d H:i:s', $wp_now_ts); if ($date_range === 'today') { $start_ts = strtotime('today', $wp_now_ts); $date_from = date('Y-m-d H:i:s', $start_ts); } elseif ($date_range === 'yesterday') { $start_ts = strtotime('yesterday', $wp_now_ts); $end_ts = strtotime('today', $wp_now_ts) - 1; // end of yesterday $date_from = date('Y-m-d H:i:s', $start_ts); $date_to = date('Y-m-d H:i:s', $end_ts); } elseif ($date_range === 'this_week') { $start_of_week = (int) get_option('start_of_week', 1); // 0=Sun, 1=Mon $day_of_week = (int) date('w', $wp_now_ts); // 0=Sun..6=Sat // Convert start_of_week to PHP's 0..6 where 0=Sunday $delta_days = ($day_of_week - $start_of_week + 7) % 7; $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts)); $date_from = date('Y-m-d H:i:s', $start_ts); } elseif ($date_range === 'this_month') { $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts)); $date_from = date('Y-m-d H:i:s', $start_ts); } elseif ($date_range === 'all') { // no bounds } } if (!empty($date_from)) { $filters['date_from'] = $date_from; } if (!empty($date_to)) { $filters['date_to'] = $date_to; } // Remove empty filters $filters = array_filter($filters); // Get sync history records $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters); $total_records = $sync_db->get_count($filters); $total_pages = ceil($total_records / $per_page); // Get statistics $stats = $sync_db->get_statistics(); $this->render_layout_open('Changes Log', 'sync_log', 'Track recent content synchronizations and changes from external tools.'); ?>

Changes Log

Recent content synchronizations from external tools. Records are automatically removed after 90 days.

No sync records found

Sync records will appear here when content/pages receive new updates.

source === 'MCP Client') ? '' : ''; ?>
title); ?> url)): ?>
time_elapsed_string($record->created_at) ); ?> source)): ?>  · source); ?>
status; $b_label = ucfirst($st); $b_bg = '#6b7280'; $b_icon = 'dashicons-info-outline'; if ($st === 'published' || $st === 'publish' || $st === 'success' || $st === 'partial') { $b_label = 'Published'; $b_bg = '#16a34a'; $b_icon = 'dashicons-yes'; } elseif ($st === 'updated') { $b_label = 'Updated'; $b_bg = '#0d9488'; $b_icon = 'dashicons-update'; } elseif ($st === 'failed' || $st === 'conflict' || $st === 'locked') { $b_label = 'Not imported'; $b_bg = '#64748b'; $b_icon = 'dashicons-minus'; } elseif ($st === 'draft') { $b_label = 'Draft'; $b_bg = '#6b7280'; $b_icon = 'dashicons-info-outline'; } ?> source === 'MCP Client'): ?>
Total records: 0): ?> | Showing -
1): ?>
1): ?>
render_layout_close(); ?> $value) { if (!empty($value)) { $query_parts[] = $key . '=' . urlencode($value); } } // Preserve the user-selected results-per-page value across page // navigation so a non-default page size survives clicking a page link. $per_page_key = Metasync_Per_Page_Helper::request_key('sync_log'); if (isset($_GET[$per_page_key])) { $per_page = (int) $_GET[$per_page_key]; if (in_array($per_page, Metasync_Per_Page_Helper::allowed_values(), true)) { $query_parts[] = $per_page_key . '=' . $per_page; } } return !empty($query_parts) ? '&' . implode('&', $query_parts) : ''; } /** * Handle AJAX requests for sync log data */ private function handle_sync_log_ajax() { // This can be used for future AJAX functionality like real-time updates wp_die(); } /** * AJAX: Clear all Sync Log records (admin-only, nonce protected). */ public function ajax_clear_sync_log() { check_ajax_referer('metasync_clear_sync_log', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => 'Insufficient permissions.'], 403); } $sync_db = new Metasync_Sync_History_Database(); $sync_db->clear_logs(); wp_send_json_success(['message' => 'Sync log cleared successfully.']); } /** * AJAX: Rollback a single MCP Client sync history entry. */ public function ajax_rollback_mcp_change() { check_ajax_referer('metasync_rollback_mcp_change', 'nonce'); if (!current_user_can('manage_options')) { wp_send_json_error(['message' => 'Insufficient permissions.'], 403); } $id = isset($_POST['sync_history_id']) ? intval($_POST['sync_history_id']) : 0; if (!$id) { wp_send_json_error(['message' => 'Invalid sync history ID.']); } $result = Metasync_MCP_Sync_Logger::rollback($id); if ($result['success']) { wp_send_json_success(['message' => $result['message']]); } else { wp_send_json_error(['message' => $result['message']]); } } /** * Render compatibility sections */ private function render_compatibility_sections() { Metasync_Compatibility_Checker::instance()->render_compatibility_sections(); } /** * Render Page Builders section */ private function render_page_builders_section() { Metasync_Compatibility_Checker::instance()->render_page_builders_section(); } /** * Render SEO Plugins section */ private function render_seo_plugins_section() { Metasync_Compatibility_Checker::instance()->render_seo_plugins_section(); } /** * Render Cache Plugins section */ private function render_cache_plugins_section() { Metasync_Compatibility_Checker::instance()->render_cache_plugins_section(); } /** * Render Lock Section button for protected tabs * * @param string $tab The tab identifier (general, whitelabel, advanced) */ private function render_lock_button($tab) { Metasync_Compatibility_Checker::instance()->render_lock_button($tab); } /** * Get Page Builders compatibility information */ private function get_page_builders_compatibility() { return Metasync_Compatibility_Checker::instance()->get_page_builders_compatibility(); } /** * Get SEO Plugins compatibility information */ private function get_seo_plugins_compatibility() { return Metasync_Compatibility_Checker::instance()->get_seo_plugins_compatibility(); } /** * Get Cache Plugins compatibility information */ private function get_cache_plugins_compatibility() { return Metasync_Compatibility_Checker::instance()->get_cache_plugins_compatibility(); } /** * Check if a plugin is installed and active * @deprecated Use get_plugin_status() instead */ private function is_plugin_installed($plugin_file) { return Metasync_Compatibility_Checker::instance()->is_plugin_installed($plugin_file); } /** * Get detailed plugin status (installed and/or active) * Checks multiple plugin file paths (e.g., free and premium versions) * * @param array $plugin_files Array of plugin file paths to check (e.g., ['free/plugin.php', 'pro/plugin.php']) * @param bool $is_core Whether this is a WordPress core feature (always installed/active) * @param string $theme_name Optional theme name to check if it's a theme instead of plugin * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null] */ private function get_plugin_status($plugin_files, $is_core = false, $theme_name = null) { return Metasync_Compatibility_Checker::instance()->get_plugin_status($plugin_files, $is_core, $theme_name); } /** * Get plugin logo URL (optimized for performance) */ private function get_plugin_logo($plugin_key, $type) { return Metasync_Compatibility_Checker::instance()->get_plugin_logo($plugin_key, $type); } public function creat_error_Logs_List() { Metasync_Admin_Pages::get_instance($this)->creat_error_Logs_List(); } /** * Site error logs page callback */ public function create_admin_heartbeat_error_logs_page() { Metasync_Admin_Pages::get_instance($this)->create_admin_heartbeat_error_logs_page(); } /** * Handle session management early for whitelabel functionality */ private function handle_session_management_early() { Metasync_Connect_Manager::instance()->handle_session_management_early(); } /** * @deprecated 2.5.12 Use Metasync_Auth_Manager instead of sessions for authentication */ private function safe_session_start() { // This method is deprecated and no longer used // Authentication now uses Metasync_Auth_Manager with WordPress transients and user meta _deprecated_function(__METHOD__, '2.5.12', 'Metasync_Auth_Manager'); return Metasync_Session_Helper::safe_start(); } private function handle_whitelabel_session_logic() { Metasync_Connect_Manager::instance()->handle_whitelabel_session_logic(); } private function handle_whitelabel_password_early() { Metasync_Connect_Manager::instance()->handle_whitelabel_password_early(); } /** * Get accordion sections configuration for General Settings * * @return array Accordion sections with field IDs, icons, and descriptions */ private function get_accordion_sections_config() { return Metasync_Settings_Fields::instance()->get_accordion_sections_config(); } /** * Get accordion sections configuration for Advanced Settings Tab * * @return array Accordion sections configuration */ private function get_advanced_accordion_config() { return Metasync_Settings_Fields::instance()->get_advanced_accordion_config(); } /** * Render accordion sections for Advanced Settings Tab */ public function render_advanced_accordion() { Metasync_Settings_Fields::instance()->render_advanced_accordion(); } /** * Render reset settings section for Advanced tab accordion */ /** * Render CPU Monitor section for Performance accordion */ public function render_cpu_monitor_section() { $cpu_monitor = new Metasync_CPU_Monitor(); $stats = Metasync_CPU_Monitor::get_stats(); $per_core_threshold = Metasync_CPU_Monitor::get_per_core_threshold(); $cores = Metasync_CPU_Monitor::get_cpu_core_count(); $effective_threshold = Metasync_CPU_Monitor::get_effective_threshold(); $detection_reliable = Metasync_CPU_Monitor::is_core_detection_reliable(); ?>
core Not detected

Automatically detected on this system. Core detection is not available on this hosting environment.

Set the load average per CPU core (0.5–10.0). Default: 2.0

Calculated as: cores × per-core threshold

CPU Load Statistics

Total Deferrals
Max Load Observed
Average Load
render_reset_settings_section(); } /** * Render Google Index API section for Indexation Control page */ public function render_google_index_section() { Metasync_Settings_Fields::instance()->render_google_index_section(); } /** * Render Bing Index (IndexNow) section * * @since 2.6.0 * @return void */ public function render_bing_index_section() { Metasync_Settings_Fields::instance()->render_bing_index_section(); } /** * Render Plugin Access Roles section for Advanced tab accordion */ private function render_plugin_access_roles_section() { Metasync_Settings_Fields::instance()->render_plugin_access_roles_section(); } /** * Check if the current user has access to the plugin based on role settings * Wrapper method that delegates to the common Metasync::current_user_has_plugin_access() * but also requires manage_options capability for admin area access * * @return bool True if user has access, false otherwise */ public function current_user_has_plugin_access() { return Metasync::current_user_has_plugin_access(); } /** * Get default execution settings * * @return array Default execution settings */ private function get_default_execution_settings() { return Metasync_Settings_Fields::instance()->get_default_execution_settings(); } /** * Get execution setting value * * @param string $key Setting key * @param mixed $default Default value if setting doesn't exist * @return mixed Setting value or default */ public function get_execution_setting($key, $default = null) { return Metasync_Settings_Fields::instance()->get_execution_setting($key, $default); } /** * Get all execution settings * * @return array All execution settings with defaults merged */ public function get_all_execution_settings() { return Metasync_Settings_Fields::instance()->get_all_execution_settings(); } /** * Check if server allows changing memory limit * Tests if ini_set('memory_limit') is allowed * * @return bool True if memory limit can be changed, false otherwise */ private function can_change_memory_limit() { return Metasync_Settings_Fields::instance()->can_change_memory_limit(); } /** * Get PHP server limits for display * * @return array Server limits (execution_time, memory_limit, can_change_memory) */ private function get_server_limits() { return Metasync_Settings_Fields::instance()->get_server_limits(); } /** * Apply memory limit from execution settings * Only applies if server allows changing memory limit * * @return bool True if memory limit was applied, false otherwise */ public function apply_memory_limit() { return Metasync_Settings_Fields::instance()->apply_memory_limit(); } /** * Parse memory limit string to MB * * @param string $memory_limit Memory limit string (e.g., "256M", "1G") * @return int Memory limit in MB */ private function parse_memory_limit_to_mb($memory_limit) { return Metasync_Settings_Fields::instance()->parse_memory_limit_to_mb($memory_limit); } /** * Render Execution Settings section for Advanced tab accordion */ private function render_execution_settings_section() { Metasync_Settings_Fields::instance()->render_execution_settings_section(); } /** * Get tooltip content for settings fields * * @return array Field ID => Tooltip text mapping */ private function get_field_tooltips() { return Metasync_Settings_Fields::instance()->get_field_tooltips(); } /** * Get the section key for a given field ID * * @param string $field_id The settings field ID * @return string|null Section key or null if not found */ private function get_field_section($field_id) { return Metasync_Settings_Fields::instance()->get_field_section($field_id); } /** * Render accordion sections for General Settings * * @param string $page The settings page slug */ public function render_accordion_sections($page) { Metasync_Settings_Fields::instance()->render_accordion_sections($page); } /** * Register and add settings */ public function settings_page_init() { Metasync_Settings_Registration::instance()->settings_page_init(); } /** * Sanitize each setting field as needed * * @param array $input Contains all settings fields as array keys */ public function sanitize($input) { return Metasync_Settings_Registration::instance()->sanitize($input); } public function metasync_settings_genkey_callback() { Metasync_Settings_Fields::instance()->metasync_settings_genkey_callback(); } /** * Get the settings option array and print one of its values */ public function linkgraph_token_callback() { Metasync_Settings_Fields::instance()->linkgraph_token_callback(); } private function time_elapsed_string($datetime, $full = false) { return Metasync_Settings_Fields::instance()->time_elapsed_string($datetime, $full); } /** * Get the settings option array and print one of its values */ public function searchatlas_api_key_callback() { Metasync_Settings_Fields::instance()->searchatlas_api_key_callback(); } /** * Site Verification Tools * * Bing Site Verification * Baidu Site Verification * Alexa Site Verification * Yandex Site Verification * Google Site Verification * Pinterest Site Verification * Norton Safe Web Site Verification */ /** * Get the settings option array and print one of its values */ public function bing_site_verification_callback() { Metasync_Settings_Fields::instance()->bing_site_verification_callback(); } /** * Get the settings option array and print one of its values */ public function yandex_site_verification_callback() { Metasync_Settings_Fields::instance()->yandex_site_verification_callback(); } /** * Get the settings option array and print one of its values */ public function google_site_verification_callback() { Metasync_Settings_Fields::instance()->google_site_verification_callback(); } /** * Get the settings option array and print one of its values */ public function pinterest_site_verification_callback() { Metasync_Settings_Fields::instance()->pinterest_site_verification_callback(); } /** * Local SEO for business and person * */ /** * Get the settings option array and print one of its values */ public function local_seo_person_organization_callback() { Metasync_Settings_Fields::instance()->local_seo_person_organization_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_name_callback() { Metasync_Settings_Fields::instance()->local_seo_name_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_logo_callback() { Metasync_Settings_Fields::instance()->local_seo_logo_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_url_callback() { Metasync_Settings_Fields::instance()->local_seo_url_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_email_callback() { Metasync_Settings_Fields::instance()->local_seo_email_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_phone_callback() { Metasync_Settings_Fields::instance()->local_seo_phone_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_address_callback() { Metasync_Settings_Fields::instance()->local_seo_address_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_business_type_callback() { Metasync_Settings_Fields::instance()->local_seo_business_type_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_opening_hours_callback() { Metasync_Settings_Fields::instance()->local_seo_opening_hours_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_phone_numbers_callback() { Metasync_Settings_Fields::instance()->local_seo_phone_numbers_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_price_range_callback() { Metasync_Settings_Fields::instance()->local_seo_price_range_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_about_page_callback() { Metasync_Settings_Fields::instance()->local_seo_about_page_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_contact_page_callback() { Metasync_Settings_Fields::instance()->local_seo_contact_page_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_map_key_callback() { Metasync_Settings_Fields::instance()->local_seo_map_key_callback(); } /** * Get the settings option array and print one of its values */ public function local_seo_geo_coordinates_callback() { Metasync_Settings_Fields::instance()->local_seo_geo_coordinates_callback(); } /** * Get the settings option array and print one of its values */ public function header_snippets_callback() { Metasync_Settings_Fields::instance()->header_snippets_callback(); } /** * Get the settings option array and print one of its values */ public function footer_snippets_callback() { Metasync_Settings_Fields::instance()->footer_snippets_callback(); } /** * Get the settings option array and print one of its values */ public function common_robot_meta_tags_callback() { Metasync_Settings_Fields::instance()->common_robot_meta_tags_callback(); } /** * Backward compatibility alias for common_robot_mata_tags_callback * @deprecated Use common_robot_meta_tags_callback() instead */ public function common_robot_mata_tags_callback() { Metasync_Settings_Fields::instance()->common_robot_mata_tags_callback(); } /** * Get the settings option array and print one of its values */ public function advance_robot_meta_tags_callback() { Metasync_Settings_Fields::instance()->advance_robot_meta_tags_callback(); } /** * Backward compatibility alias for advance_robot_mata_tags_callback * @deprecated Use advance_robot_meta_tags_callback() instead */ public function advance_robot_mata_tags_callback() { Metasync_Settings_Fields::instance()->advance_robot_mata_tags_callback(); } /** * Get the settings option array and print one of its values */ public function global_twitter_card_type_callback() { Metasync_Settings_Fields::instance()->global_twitter_card_type_callback(); } /** * Get the settings option array and print one of its values */ public function global_open_graph_meta_callback() { Metasync_Settings_Fields::instance()->global_open_graph_meta_callback(); } /** * Get the settings option array and print one of its values */ public function global_facebook_meta_callback() { Metasync_Settings_Fields::instance()->global_facebook_meta_callback(); } /** * Get the settings option array and print one of its values */ public function global_twitter_meta_callback() { Metasync_Settings_Fields::instance()->global_twitter_meta_callback(); } /** * Get the settings option array and print one of its values */ public function og_image_dimensions_callback() { Metasync_Settings_Fields::instance()->og_image_dimensions_callback(); } /** * Get the settings option array and print one of its values */ public function article_timestamps_callback() { Metasync_Settings_Fields::instance()->article_timestamps_callback(); } /** * Get the settings option array and print one of its values */ public function article_author_callback() { Metasync_Settings_Fields::instance()->article_author_callback(); } /** * Get the settings option array and print one of its values */ public function article_section_callback() { Metasync_Settings_Fields::instance()->article_section_callback(); } /** * Get the settings option array and print one of its values */ public function article_tags_callback() { Metasync_Settings_Fields::instance()->article_tags_callback(); } /** * Get the settings option array and print one of its values */ public function twitter_image_alt_callback() { Metasync_Settings_Fields::instance()->twitter_image_alt_callback(); } /** * Get the settings option array and print one of its values */ public function facebook_page_url_callback() { Metasync_Settings_Fields::instance()->facebook_page_url_callback(); } /** * Get the settings option array and print one of its values */ public function facebook_authorship_callback() { Metasync_Settings_Fields::instance()->facebook_authorship_callback(); } /** * Get the settings option array and print one of its values */ public function facebook_admin_callback() { Metasync_Settings_Fields::instance()->facebook_admin_callback(); } /** * Get the settings option array and print one of its values */ public function facebook_app_callback() { Metasync_Settings_Fields::instance()->facebook_app_callback(); } /** * Get the settings option array and print one of its values */ public function facebook_secret_callback() { Metasync_Settings_Fields::instance()->facebook_secret_callback(); } /** * Get the settings option array and print one of its values */ public function twitter_username_callback() { Metasync_Settings_Fields::instance()->twitter_username_callback(); } /** * Get business types as choices in local business. * * @return array */ public static function get_business_types() { return Metasync_Settings_Fields::get_business_types(); } /** * Display a dashboard warning when using the plain permalink structure. * @param $data An array of data passed. */ public function permalink_structure_dashboard_warning() { $current_permalink_structure = get_option('permalink_structure'); # Get the plugin name using centralized method $plugin_name = Metasync::get_effective_plugin_name(); # Check if the current permalink structure is set to "Plain" if ($current_permalink_structure === '/%post_id%/' || $current_permalink_structure === '') { printf( '

Warning from %s
To ensure compatibility, please update your permalink structure to any option other than "Plain". For any inquiries, contact support.

', esc_html($plugin_name) ); } } /** * Show a one-time admin notice when a page builder is detected but the * "Default Page Builder" setting has never been explicitly saved. */ public function display_page_builder_notice() { // Only relevant on the plugin's own settings page — avoid repeating // this notice across every admin screen. if (!isset($_GET['page']) || $_GET['page'] !== self::$page_slug) { return; } $configured = Metasync::get_option('general')['default_page_builder'] ?? ''; // Setting already saved — nothing to warn about if (!empty($configured)) { return; } // Check if user dismissed this notice $dismissed = get_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', true); if ($dismissed) { return; } // Handle dismiss action if (isset($_GET['metasync_dismiss_builder_notice']) && wp_verify_nonce($_GET['_wpnonce'] ?? '', 'metasync_dismiss_builder')) { update_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', '1'); return; } require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-html-to-builder-converter.php'; $detected = Metasync_HTML_To_Builder_Converter::auto_detect_builder(); // No non-Gutenberg builder detected — no need to warn if ($detected === 'gutenberg') { return; } $builders = Metasync_HTML_To_Builder_Converter::get_available_builders(); $builder_label = $builders[$detected]['label'] ?? $detected; $plugin_name = Metasync::get_effective_plugin_name(); $settings_url = admin_url('admin.php?page=' . self::$page_slug . '&tab=general#metasync-section-content_rendering'); $dismiss_url = wp_nonce_url(add_query_arg('metasync_dismiss_builder_notice', '1'), 'metasync_dismiss_builder'); printf( '

%s — Page Builder Detected
%s is active on this site. Content synced by Content Genius currently uses Gutenberg (WordPress Block Editor) format by default.

If you want synced content to use %s\'s native widget format instead, you can change this in Settings → Content Rendering → Default Page Builder.

Dismiss this notice

', esc_html($plugin_name), esc_html($builder_label), esc_html($builder_label), esc_url($settings_url), esc_url($dismiss_url) ); } /** * Display update warning banner if plugin update is available * Checks WordPress update API to see if a newer version is available * * @since 1.0.0 */ public function display_update_warning_banner() { // Get the installed version from database $installed_version = get_option('metasync_version', '0.0.0'); // Get plugin basename for WordPress update API check // This is the plugin file path relative to plugins directory (e.g., 'metasync/metasync.php') $plugin_file = plugin_basename(plugin_dir_path(dirname(__FILE__)) . 'metasync.php'); // Get WordPress update plugins transient (contains available updates) $update_plugins = get_site_transient('update_plugins'); // Check if update information exists and if our plugin has an update available if ($update_plugins && isset($update_plugins->response) && isset($update_plugins->response[$plugin_file])) { $update_info = $update_plugins->response[$plugin_file]; $latest_version = isset($update_info->new_version) ? $update_info->new_version : ''; // Compare installed version with latest available version if ($latest_version && version_compare($installed_version, $latest_version, '<')) { // Get the plugin name using centralized method $plugin_name = Metasync::get_effective_plugin_name(); // Show admin notice with plugin name included in the message printf( '

Warning from %s
A new version of %s is available. Please update to the latest version to ensure compatibility and access new features. For any inquiries, contact support.

', esc_html($plugin_name), esc_html($plugin_name) ); } } } /* Method to handle Ajax request from "Indexation Control" page */ public function meta_sync_save_seo_controls() { Metasync_Settings_Registration::instance()->meta_sync_save_seo_controls(); } /** * AJAX handler for saving Performance (CPU Load) settings * * Saves the CPU load threshold and returns statistics */ public function ajax_save_performance_settings() { # Check nonce for security and return early if invalid 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; } # Check user capabilities if (!Metasync::current_user_has_plugin_access()) { wp_send_json_error(array('message' => 'Insufficient permissions')); return; } # Get current options $current_options = Metasync::get_option(); if (!is_array($current_options)) { $current_options = array(); } # Initialize performance section if it doesn't exist if (!isset($current_options['performance']) || !is_array($current_options['performance'])) { $current_options['performance'] = array(); } # Validate and sanitize CPU load threshold if (isset($_POST['metasync_options']['performance']['cpu_load_per_core_threshold'])) { $threshold = floatval($_POST['metasync_options']['performance']['cpu_load_per_core_threshold']); # Clamp value between 0.5 and 10.0 $threshold = max(0.5, min(10.0, $threshold)); $current_options['performance']['cpu_load_per_core_threshold'] = $threshold; } else { # Ensure default value exists if (!isset($current_options['performance']['cpu_load_per_core_threshold'])) { $current_options['performance']['cpu_load_per_core_threshold'] = Metasync_CPU_Monitor::DEFAULT_PER_CORE; } } # Save the updated options $result = Metasync::set_option($current_options); if ($result) { # Get current statistics to return $stats = Metasync_CPU_Monitor::get_stats(); $cores = Metasync_CPU_Monitor::get_cpu_core_count(); $effective_threshold = Metasync_CPU_Monitor::get_effective_threshold(); wp_send_json_success(array( 'message' => 'Performance settings saved successfully!', 'cpu_load_per_core_threshold' => $current_options['performance']['cpu_load_per_core_threshold'], 'effective_threshold' => $effective_threshold, 'cores' => $cores, 'stats' => $stats )); } else { wp_send_json_error(array('message' => 'Failed to save Performance settings')); } } /** * Schedule transient cleanup cron job * Runs daily to clean up expired transients and reduce database load */ public function schedule_transient_cleanup_cron() { // Clear any existing scheduled event first $this->unschedule_transient_cleanup_cron(); // Schedule new cron job daily if (!wp_next_scheduled('metasync_cleanup_transients')) { $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_cleanup_transients'); if (!$scheduled) { error_log('MetaSync: Failed to schedule transient cleanup cron job'); } } } /** * Unschedule transient cleanup cron job */ public function unschedule_transient_cleanup_cron() { $timestamp = wp_next_scheduled('metasync_cleanup_transients'); if ($timestamp) { wp_unschedule_event($timestamp, 'metasync_cleanup_transients'); error_log('MetaSync: Transient cleanup cron job unscheduled'); } } /** * Maybe schedule transient cleanup cron job * Called on init hook - always schedules for database maintenance */ public function maybe_schedule_transient_cleanup_cron() { if (!wp_next_scheduled('metasync_cleanup_transients')) { $this->schedule_transient_cleanup_cron(); } } /** * Schedule hidden post manager cron job (runs every 7 days) * Called on init hook - always schedules for template checking */ public function maybe_schedule_hidden_post_check() { if (!wp_next_scheduled('metasync_hidden_post_check')) { $scheduled = wp_schedule_event(time(), 'metasync_weekly', 'metasync_hidden_post_check'); if ($scheduled) { error_log('MetaSync: Hidden post manager cron job scheduled successfully (runs every 7 days)'); } else { error_log('MetaSync: Failed to schedule hidden post manager cron job'); } } } /** * Schedule OTTO 404 exclusion recheck cron job (runs daily) * Rechecks URLs auto-excluded due to 404 after 7 days; removes from exclusion if now available */ public function maybe_schedule_otto_recheck_404_cron() { if (!wp_next_scheduled('metasync_otto_recheck_404_exclusions')) { $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_otto_recheck_404_exclusions'); if ($scheduled) { error_log('MetaSync: OTTO 404 recheck cron job scheduled successfully (runs daily)'); } else { error_log('MetaSync: Failed to schedule OTTO 404 recheck cron job'); } } } /** * Execute transient cleanup cron job * Cleans up expired transients and plugin-specific transients to reduce database load */ public function execute_transient_cleanup() { Metasync_Admin_Ajax::instance()->execute_transient_cleanup(); } // ------------------------------------------------------------------------- // DB CLEANUP — cron scheduling, execution, render, AJAX // ------------------------------------------------------------------------- /** * Returns saved DB cleanup settings with defaults merged in. */ private function get_db_cleanup_settings() { $defaults = array( 'enabled' => false, 'clean_post_revisions' => true, 'clean_trashed_posts' => true, 'clean_trashed_comments' => true, 'clean_spam_comments' => true, 'clean_expired_transients' => true, 'clean_orphaned_postmeta' => true, 'last_run_at' => 0, 'last_run_stats' => array(), ); $saved = get_option('metasync_db_cleanup_settings', array()); return array_merge($defaults, $saved); } /** * Schedules the weekly DB cleanup cron if the feature is enabled and not yet scheduled. */ public function maybe_schedule_db_cleanup_cron() { $settings = $this->get_db_cleanup_settings(); if (!empty($settings['enabled'])) { if (!wp_next_scheduled('metasync_db_cleanup')) { wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup'); } } else { $this->unschedule_db_cleanup_cron(); } } /** * Removes the DB cleanup cron event. */ public function unschedule_db_cleanup_cron() { $timestamp = wp_next_scheduled('metasync_db_cleanup'); if ($timestamp) { wp_unschedule_event($timestamp, 'metasync_db_cleanup'); } } /** * Cron callback: runs each enabled cleanup task and records stats. * Never calls wp_cache_flush() — only targeted DB deletes. */ public function execute_db_cleanup() { global $wpdb; $settings = $this->get_db_cleanup_settings(); $stats = array(); $start = microtime(true); try { // 1. Post revisions if (!empty($settings['clean_post_revisions'])) { $stats['post_revisions'] = (int) $wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_type = 'revision'" ); // Remove postmeta left behind by deleted revisions $wpdb->query( "DELETE pm FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE p.ID IS NULL" ); } // 2. Trashed posts + their postmeta if (!empty($settings['clean_trashed_posts'])) { // Collect IDs first to cleanly remove postmeta $trashed_ids = $wpdb->get_col( "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'trash'" ); if (!empty($trashed_ids)) { $placeholders = implode(',', array_fill(0, count($trashed_ids), '%d')); $wpdb->query( $wpdb->prepare( "DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($placeholders)", $trashed_ids ) ); $stats['trashed_posts'] = (int) $wpdb->query( "DELETE FROM {$wpdb->posts} WHERE post_status = 'trash'" ); } else { $stats['trashed_posts'] = 0; } } // 3. Trashed comments if (!empty($settings['clean_trashed_comments'])) { $stats['trashed_comments'] = (int) $wpdb->query( "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'trash'" ); } // 4. Spam comments if (!empty($settings['clean_spam_comments'])) { $stats['spam_comments'] = (int) $wpdb->query( "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'spam'" ); } // 5. Expired transients — direct SQL, no cache flush if (!empty($settings['clean_expired_transients'])) { // Delete timeout rows that have already expired $wpdb->query( "DELETE FROM {$wpdb->options} WHERE option_name LIKE '\_transient\_timeout\_%' AND option_value + 0 < UNIX_TIMESTAMP()" ); // Delete value rows whose timeout row no longer exists $stats['expired_transients'] = (int) $wpdb->query( "DELETE o FROM {$wpdb->options} o LEFT JOIN {$wpdb->options} t ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 12)) WHERE o.option_name LIKE '\_transient\_%' AND o.option_name NOT LIKE '\_transient\_timeout\_%' AND t.option_id IS NULL" ); } // 6. Orphaned postmeta (post_id references a post that no longer exists) if (!empty($settings['clean_orphaned_postmeta'])) { $stats['orphaned_postmeta'] = (int) $wpdb->query( "DELETE pm FROM {$wpdb->postmeta} pm LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id WHERE p.ID IS NULL" ); } $stats['execution_ms'] = round((microtime(true) - $start) * 1000, 2); // Persist last-run timestamp and stats $settings['last_run_at'] = time(); $settings['last_run_stats'] = $stats; update_option('metasync_db_cleanup_settings', $settings); error_log('MetaSync: DB cleanup completed — ' . json_encode($stats)); } catch (Exception $e) { error_log('MetaSync: DB cleanup failed — ' . $e->getMessage()); } } /** * Renders the Database Cleanup accordion section in Advanced Settings. */ public function render_db_cleanup_section() { $settings = $this->get_db_cleanup_settings(); $last_run = !empty($settings['last_run_at']) ? $settings['last_run_at'] : 0; $stats = !empty($settings['last_run_stats']) ? $settings['last_run_stats'] : array(); $next_run = wp_next_scheduled('metasync_db_cleanup'); $task_labels = array( 'clean_post_revisions' => 'Post revisions', 'clean_trashed_posts' => 'Trashed posts', 'clean_trashed_comments' => 'Trashed comments', 'clean_spam_comments' => 'Spam comments', 'clean_expired_transients' => 'Expired transients', 'clean_orphaned_postmeta' => 'Orphaned post meta', ); ?>

Remove orphaned database rows that accumulate over time and slow down queries. Runs weekly via WP-Cron when enabled.

Schedules an automatic cleanup every 7 days via WP-Cron. Next run:

Cleanup Tasks

$label): ?>

Last Cleanup

$count): ?> : in ms
'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.')); return; } $existing = $this->get_db_cleanup_settings(); $task_keys = array( 'clean_post_revisions', 'clean_trashed_posts', 'clean_trashed_comments', 'clean_spam_comments', 'clean_expired_transients', 'clean_orphaned_postmeta', ); $new_settings = array( 'enabled' => !empty($_POST['enabled']), 'last_run_at' => $existing['last_run_at'], 'last_run_stats' => $existing['last_run_stats'], ); foreach ($task_keys as $key) { $new_settings[$key] = !empty($_POST[$key]); } update_option('metasync_db_cleanup_settings', $new_settings); // Reschedule based on new enabled state if (!empty($new_settings['enabled'])) { if (!wp_next_scheduled('metasync_db_cleanup')) { wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup'); } $next_run = wp_next_scheduled('metasync_db_cleanup'); $next_run_label = $next_run ? date_i18n(get_option('date_format') . ' ' . get_option('time_format'), $next_run) : ''; wp_send_json_success(array( 'message' => 'Settings saved. Weekly cleanup is enabled.', 'next_run_label' => $next_run_label, )); } else { $this->unschedule_db_cleanup_cron(); wp_send_json_success(array( 'message' => 'Settings saved. Weekly cleanup is disabled.', )); } } /** * AJAX: Manually trigger the DB cleanup and return stats for the UI. * Persists current form state first so unsaved checkbox changes are respected. */ public function ajax_run_db_cleanup() { if (!isset($_POST['db_cleanup_settings_nonce']) || !wp_verify_nonce($_POST['db_cleanup_settings_nonce'], 'metasync_db_cleanup_settings_nonce')) { wp_send_json_error(array('message' => 'Invalid security token.')); return; } if (!Metasync::current_user_has_plugin_access()) { wp_send_json_error(array('message' => 'Insufficient permissions.')); return; } // Save current form state before running so the cleanup uses what the user sees $existing = $this->get_db_cleanup_settings(); $task_keys = array( 'clean_post_revisions', 'clean_trashed_posts', 'clean_trashed_comments', 'clean_spam_comments', 'clean_expired_transients', 'clean_orphaned_postmeta', ); $to_save = array( 'enabled' => !empty($_POST['enabled']), 'last_run_at' => $existing['last_run_at'], 'last_run_stats' => $existing['last_run_stats'], ); foreach ($task_keys as $key) { $to_save[$key] = !empty($_POST[$key]); } update_option('metasync_db_cleanup_settings', $to_save); // Reschedule cron to match the (possibly updated) enabled state if (!empty($to_save['enabled'])) { if (!wp_next_scheduled('metasync_db_cleanup')) { wp_schedule_event(time(), 'metasync_weekly', 'metasync_db_cleanup'); } } else { $this->unschedule_db_cleanup_cron(); } $this->execute_db_cleanup(); $settings = $this->get_db_cleanup_settings(); $stats = $settings['last_run_stats']; $timestamp_label = date_i18n( get_option('date_format') . ' ' . get_option('time_format'), $settings['last_run_at'] ); // Build stats badges HTML $stat_labels = array( 'post_revisions' => 'Post revisions', 'trashed_posts' => 'Trashed posts', 'trashed_comments' => 'Trashed comments', 'spam_comments' => 'Spam comments', 'expired_transients' => 'Expired transients', 'orphaned_postmeta' => 'Orphaned post meta', ); $stats_html = ''; foreach ($stat_labels as $key => $label) { if (isset($stats[$key])) { $stats_html .= '' . esc_html($label) . ': ' . intval($stats[$key]) . ' '; } } if (!empty($stats['execution_ms'])) { $stats_html .= 'in ' . esc_html($stats['execution_ms']) . 'ms'; } wp_send_json_success(array( 'message' => 'Cleanup completed successfully.', 'timestamp_label' => $timestamp_label, 'stats_html' => $stats_html, )); } /** * Control plugin auto-updates based on user setting * * @param bool $update Whether to update * @param object $item Update offer * @return bool Whether to allow auto-update */ public function control_plugin_auto_updates($update, $item) { # Check if the item object has the slug property if (!isset($item->slug)) { return $update; } // Check if this is our plugin if ($item->slug === 'metasync') { $general_settings = Metasync::get_option('general') ?? []; $enable_auto_updates = $general_settings['enable_auto_updates'] ?? false; // Return the user's preference (true = allow auto-updates, false = prevent) return $enable_auto_updates === 'true' || $enable_auto_updates === true; } // For other plugins, don't interfere with their auto-update settings return $update; } /** * AJAX handler to add excluded URL for OTTO */ public function ajax_otto_add_excluded_url() { Metasync_Otto_Cache_Manager::instance()->ajax_otto_add_excluded_url(); } /** * AJAX handler to delete excluded URL for OTTO */ public function ajax_otto_delete_excluded_url() { Metasync_Otto_Cache_Manager::instance()->ajax_otto_delete_excluded_url(); } /** * AJAX handler to recheck if an excluded URL is now available * Used for "Recheck" action on Excluded URLs list */ public function ajax_otto_recheck_excluded_url() { Metasync_Otto_Cache_Manager::instance()->ajax_otto_recheck_excluded_url(); } /** * AJAX handler to get excluded URLs with pagination */ public function ajax_otto_get_excluded_urls() { Metasync_Otto_Cache_Manager::instance()->ajax_otto_get_excluded_urls(); } /** * AJAX handler for submitting issue reports to Sentry * * @since 2.5.10 * @return void Sends JSON response and exits */ public function ajax_submit_issue_report() { Metasync_Admin_Ajax::instance()->ajax_submit_issue_report(); } /** * Format duration seconds into human-readable label * * @since 2.5.11 * @param int $seconds Duration in seconds * @return string Human-readable duration */ private function format_duration_label($seconds) { $labels = array( 3600 => '1 hour', 14400 => '4 hours', 28800 => '8 hours', 86400 => '24 hours', 172800 => '48 hours', 604800 => '7 days', 1209600 => '14 days', 2592000 => '30 days' ); if (isset($labels[$seconds])) { return $labels[$seconds]; } # Calculate hours if not a standard duration $hours = round($seconds / 3600); return $hours . ' hours'; } /** * AJAX handler for password recovery * Sends the whitelabel settings password to the configured recovery email */ public function ajax_recover_password() { Metasync_Admin_Ajax::instance()->ajax_recover_password(); } /** * AJAX handler for saving theme preference * Saves the user's theme choice (light/dark) to WordPress options */ public function ajax_save_theme() { Metasync_Admin_Ajax::instance()->ajax_save_theme(); } /** * AJAX handler for tracking 1-click activation in GA4 */ public function ajax_track_one_click_activation() { Metasync_Admin_Ajax::instance()->ajax_track_one_click_activation(); } /** * Handler for exporting whitelabel settings to a zip file * Uses admin-post action for file downloads */ public function handle_export_whitelabel_settings() { Metasync_Admin_Ajax::instance()->handle_export_whitelabel_settings(); } /** * Add custom column to posts/pages list for HTML-converted pages * * @param array $columns Existing columns * @return array Modified columns */ public function add_html_converted_column($columns) { // Add column after the title column $new_columns = array(); foreach ($columns as $key => $value) { $new_columns[$key] = $value; if ($key === 'title') { $new_columns['metasync_html_source'] = __('Source', 'metasync'); } } return $new_columns; } /** * Render content for the HTML-converted column * * @param string $column_name Name of the column * @param int $post_id Post ID */ public function render_html_converted_column($column_name, $post_id) { if ($column_name !== 'metasync_html_source') { return; } // Check if this is an HTML-converted page $has_raw_html = get_post_meta($post_id, '_metasync_raw_html_enabled', true); $has_custom_css = get_post_meta($post_id, '_metasync_custom_css', true); // If page has raw HTML or custom CSS from conversion, show badge if ($has_raw_html || !empty($has_custom_css)) { $label = $this->get_html_source_label(); $tooltip = sprintf( __('This page was created using %s HTML-to-Builder converter', 'metasync'), $label ); echo sprintf( ' %s ', esc_attr($tooltip), esc_html($label) ); } } /** * Get the label for HTML-converted pages (respects whitelabel settings) * * @return string Label to display */ private function get_html_source_label() { $whitelabel_company = Metasync::get_whitelabel_company_name(); if (!empty($whitelabel_company)) { return $whitelabel_company . ' AI'; } return Metasync::get_effective_plugin_name() . ' AI'; } /** * Add source notice banner in the page editor * * @param WP_Post $post Current post object */ public function add_editor_source_notice($post) { if (!$post || !in_array($post->post_type, array('post', 'page'))) { return; } // Don't show the "HTML-to-Builder converter" banner on LPS-synced / custom-HTML // pages: those are raw-HTML store-and-serve pages, NOT actually converted to a // page builder, so the banner mislabels them. It still shows for pages genuinely // produced by the converter. if (function_exists('metasync_is_custom_or_lps_page') && metasync_is_custom_or_lps_page($post->ID)) { return; } $has_raw_html = get_post_meta($post->ID, '_metasync_raw_html_enabled', true); $has_custom_css = get_post_meta($post->ID, '_metasync_custom_css', true); if ($has_raw_html || !empty($has_custom_css)) { $label = $this->get_html_source_label(); $message = sprintf( __('This page was created using %s HTML-to-Builder converter. The design is preserved with custom CSS and inline styles.', 'metasync'), '' . esc_html($label) . '' ); echo sprintf( '
%s

%s

', esc_html($label), $message ); } } /** * Add source display in quick edit panel * * @param string $column_name Column name * @param string $post_type Post type */ public function add_quick_edit_source_display($column_name, $post_type) { if ($column_name !== 'metasync_html_source') { return; } if (!in_array($post_type, array('post', 'page'))) { return; } ?>
get_html_source_label(); $widget_title = sprintf(__('%s Pages', 'metasync'), $label); wp_add_dashboard_widget( 'metasync_html_pages_widget', $widget_title, array($this, 'render_html_pages_dashboard_widget') ); } /** * Render the dashboard widget content */ public function render_html_pages_dashboard_widget() { Metasync_Admin_Ajax::instance()->render_html_pages_dashboard_widget(); } /** * Bot Statistics page callback * Displays bot detection statistics and logs */ public function create_admin_bot_statistics_page() { require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php'; } /** * AJAX handler for resetting bot statistics */ public function ajax_reset_bot_stats() { check_ajax_referer('metasync_reset_bot_stats', 'nonce'); if (!Metasync::current_user_has_plugin_access()) { wp_send_json_error(['message' => 'Insufficient permissions.']); } require_once plugin_dir_path(dirname(__FILE__)) . 'otto/class-metasync-otto-bot-statistics-database.php'; $db = Metasync_Otto_Bot_Statistics_Database::get_instance(); $result = $db->reset_statistics(); if ($result) { wp_send_json_success(['message' => 'Statistics reset successfully.']); } else { wp_send_json_error(['message' => 'Failed to reset statistics.']); } } /** * AJAX handler for sending URLs to Google Instant Indexing API * * @since 2.6.0 * @return void Sends JSON response and exits */ public function ajax_send_giapi() { check_ajax_referer('metasync_nonce', 'nonce'); if (!Metasync::current_user_has_plugin_access()) { wp_send_json_error(['message' => 'Insufficient permissions.'], 403); } $post_data = metasync_sanitize_input_array($_POST); if (!isset($post_data['metasync_giapi_url'])) { return; } // Parse URLs from textarea input (one per line) $urls = array_values(array_filter(array_map('trim', explode("\n", sanitize_textarea_field(wp_unslash($post_data['metasync_giapi_url'])))))); if (empty($urls)) { return; } if (!isset($post_data['metasync_giapi_action'])) { return; } $action = sanitize_title($post_data['metasync_giapi_action']); // Map form action values to google_index_direct action values if ($action === 'remove') { $action = 'delete'; } header('Content-type: application/json'); $result_data = []; foreach ($urls as $i => $url) { $url = esc_url_raw($url); if (empty($url)) { continue; } if ($action === 'status') { $result = google_index_direct()->get_url_status($url); } else { $result = google_index_direct()->index_url($url, $action); } $key = 'url-' . $i; if (!empty($result['success'])) { $result_data[$key] = $result['data']; } else { $result_data[$key] = (object) [ 'error' => (object) [ 'code' => isset($result['error']['code']) ? $result['error']['code'] : 400, 'message' => isset($result['error']['message']) ? $result['error']['message'] : 'Unknown error', ] ]; } } // For single URL, unwrap from the batch format (matches old behavior) if (count($result_data) === 1) { $result_data = reset($result_data); } wp_send_json($result_data); wp_die(); } /** * AJAX handler for sending URLs to Bing via IndexNow API * * @since 2.6.0 * @return void Sends JSON response and exits */ public function ajax_send_bing_indexnow() { check_ajax_referer('metasync_nonce', 'nonce'); if (!Metasync::current_user_has_plugin_access()) { wp_send_json_error(['message' => 'Insufficient permissions.'], 403); } require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php'; $bing_instant_index = new Metasync_Bing_Instant_Index(); $bing_instant_index->send(); } /** * Save instant indexing settings (Google and Bing) * * @since 2.6.0 * @return void */ public function save_instant_indexing_settings() { // Check if this is a settings submission if (!isset($_POST['submit'])) { return; } // This handler runs on admin_init, which also fires inside // admin-ajax.php before its login gate, so a bare isset() check let // any request (including logged-out ones) rewrite the auto-submit // post types. Require the nonce and plugin access before writing. if (!isset($_POST['metasync_instant_indexing_nonce']) || !wp_verify_nonce(sanitize_key(wp_unslash($_POST['metasync_instant_indexing_nonce'])), 'metasync_instant_indexing_settings') || !Metasync::current_user_has_plugin_access()) { return; } // Save post types for Google Instant Indexing auto-submit if (isset($_POST['metasync_post_types'])) { $post_data = metasync_sanitize_input_array($_POST); $post_types = is_array($post_data['metasync_post_types']) ? array_map('sanitize_title', $post_data['metasync_post_types']) : []; $settings = get_option('metasync_options_instant_indexing', ['post_types' => []]); $settings['post_types'] = array_values($post_types); update_option('metasync_options_instant_indexing', $settings); } // Note: Bing Instant Indexing settings are saved via AJAX in save_bing_inline_settings_ajax() } /** * Save Bing instant indexing settings from inline form (Indexation Control page) * * @since 2.6.0 * @return bool True on success, false on failure */ private function save_bing_inline_settings_ajax() { return Metasync_Settings_Registration::instance()->save_bing_inline_settings_ajax(); } /** * Add instant indexing action links to post/page rows * * @since 2.6.0 * @param array $actions Current actions * @param WP_Post $post Current post object * @return array Modified actions */ public function add_instant_indexing_post_actions($actions, $post) { // Add Google Instant Indexing links $options = get_option('metasync_options_instant_indexing', ['json_key' => '', 'post_types' => []]); $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; if (in_array($post->post_type, $post_types) && $post->post_status == 'publish') { $link = get_permalink($post); // Get menu slug (support white label) $general_options = Metasync::get_option('general') ?? []; $menu_slug = !empty($general_options['white_label_plugin_menu_slug']) ? $general_options['white_label_plugin_menu_slug'] : 'searchatlas'; $page_slug = $menu_slug . '-google-console'; $actions['index-update'] = 'Update Google Index'; $actions['index-status'] = 'Status Google Index'; } // Add Bing Instant Indexing links require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php'; $bing_instant_index = new Metasync_Bing_Instant_Index(); $actions = $bing_instant_index->bing_instant_index_post_link($actions, $post); return $actions; } /** * Auto-submit post to instant indexing services when published * * @since 2.6.0 * @param int $post_id Post ID * @param WP_Post $post Post object * @param bool $update Whether this is an update * @return void */ public function auto_submit_to_instant_indexing($post_id, $post, $update) { // Skip revisions, autosaves, and non-published posts if (wp_is_post_revision($post_id) || wp_is_post_autosave($post_id)) { return; } if ($post->post_status !== 'publish') { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } // Auto-submit to Google Instant Indexing $seo_controls = Metasync::get_option('seo_controls'); if (!empty($seo_controls['enable_googleinstantindex']) && $seo_controls['enable_googleinstantindex'] === 'true') { $options = get_option('metasync_options_instant_indexing', ['post_types' => []]); $post_types = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : []; if (in_array($post->post_type, $post_types)) { // save_post is the single owner of auto-submit and also fires // on REST-created posts, where nothing else may have loaded // the Google Index helpers yet — load them on demand instead // of silently skipping. if (!function_exists('google_index_direct')) { $google_index_path = plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'; if (file_exists($google_index_path)) { require_once $google_index_path; } } if (function_exists('google_index_direct')) { $service_info = google_index_direct()->get_service_account_info(); if (!isset($service_info['error'])) { google_index_direct()->index_post($post_id, $post->post_type, 'update'); } } } } // Auto-submit to Bing Instant Indexing require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php'; $bing_instant_index = new Metasync_Bing_Instant_Index(); $bing_instant_index->auto_submit_on_publish($post_id, $post, $update); } }