$value) { if (is_array($value)) { $sanitized[$key] = metasync_sanitize_input_array($value); } else { // Check if it's a URL if (filter_var($value, FILTER_VALIDATE_URL)) { $sanitized[$key] = esc_url_raw($value); } else { $sanitized[$key] = sanitize_text_field($value); } } } return $sanitized; } } // Skip heavy MetaSync init on admin-ajax requests that don't target our own actions (Sentry issue 7441226449). if (!function_exists('metasync_is_non_metasync_admin_ajax')) { function metasync_is_non_metasync_admin_ajax() { static $result = null; if ($result !== null) { return $result; } if (!defined('DOING_AJAX') || !DOING_AJAX) { return ($result = false); } $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action'])) : (isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : ''); if ($action === '') { return ($result = true); } if (strpos($action, 'metasync_') === 0 || strpos($action, 'meta_sync_') === 0 || $action === 'sample-permalink') { return ($result = false); } return ($result = true); } } // Lazy-load guard: only initialise the MCP server when the request is actually targeting // the MCP REST route (or its sibling SEO-inventory route, which depends on $metasync_mcp_server // for its permission callback). Saves ~2-5 MB / 10-50 ms on the 99.9% of requests that never // touch MCP. if (!function_exists('metasync_is_mcp_rest_request')) { function metasync_is_mcp_rest_request(): bool { $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : ''; if ($uri === '') { return false; } $prefix = function_exists('rest_get_url_prefix') ? rest_get_url_prefix() : 'wp-json'; $needles = [ '/' . $prefix . '/metasync/v1/mcp', '/' . $prefix . '/metasync/v1/seo-inventory', 'rest_route=/metasync/v1/mcp', 'rest_route=/metasync/v1/seo-inventory', 'rest_route=%2Fmetasync%2Fv1%2Fmcp', 'rest_route=%2Fmetasync%2Fv1%2Fseo-inventory', ]; foreach ($needles as $needle) { if (stripos($uri, $needle) !== false) { return true; } } return false; } } // Phase 2 — these files have file-level side effects (add_action outside class body) // and must remain as explicit require_once until refactored. require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-api-backoff-rest.php'; require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-error-logger.php'; // Shared helpers (custom/LPS page detection + query exclusion) — loaded in all // request contexts (admin, REST, MCP, AJAX) so every SEO surface uses one rule. require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-helpers.php'; /** * Include the Otto Pixel Php Code */ if (!metasync_is_non_metasync_admin_ajax()) { require_once plugin_dir_path( __FILE__ ) . '/otto/otto_pixel.php'; require_once plugin_dir_path( __FILE__ ) . '/otto/class-metasync-otto-clone-meta-cleaner.php'; Metasync_Otto_Clone_Meta_Cleaner::register(); } /** * Initialize OTTO Persistence Settings (registers REST API endpoints) */ add_action('init', function() { if (metasync_is_non_metasync_admin_ajax()) { return; } Metasync_Otto_Persistence_Settings::init(); }, 5); /** * The code that runs during plugin activation. * This action is documented in includes/class-metasync-activator.php */ function activate_metasync() { # Include WordPress plugin functions to access plugin metadata if (!function_exists('get_plugin_data')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } # Get plugin data $plugin_data = get_plugin_data(__FILE__); # Get the plugin name $plugin_name = $plugin_data['Name']; # Get the current WordPress global $wp_version; # Get the current php version $php_version = PHP_VERSION; # Check for incompatible WordPress version if (version_compare($wp_version, METASYNC_MIN_WP, '<')) { #show error wp_die( #craft the message sprintf( '%s requires WordPress version %s or later. You are currently using version %s. Please update WordPress to activate this plugin.', esc_html($plugin_name), METASYNC_MIN_WP, esc_html($wp_version) ), #the plugin title as page title esc_html($plugin_name).'Plugin Activation Error', #include the back link ['back_link' => true] ); } # Check for incompatible PHP version if (version_compare($php_version, METASYNC_MIN_PHP, '<')) { #show error message wp_die( #craft the message sprintf( '%s requires PHP version %s or later. You are currently using version %s. Please update PHP to activate this plugin.', esc_html($plugin_name), METASYNC_MIN_PHP, esc_html($php_version) ), #the plugin title as page title esc_html($plugin_name).'Plugin Activation Error', #include the back link ['back_link' => true] ); } Metasync_Activator::activate(); // class name is changed at class-db-migrations.php MetaSync_DBMigration::activation(); // Set initial version update_option('metasync_version', METASYNC_VERSION); // Record activation timestamp so Divi CSS fix transients // auto-invalidate after plugin deactivate/reactivate cycles. update_option('metasync_activated_at', (string) time()); // Clear all cache plugins to ensure fresh start Metasync_Cache_Purge::purge_all('plugin_activation'); // Migrate physical sitemap files on activation. metasync_migrate_physical_sitemaps(); } // Log-sync removed - error monitoring now handled by Sentry // require_once plugin_dir_path(__FILE__) . 'log-sync/log-sync.php'; /** * Initialize telemetry system for error monitoring and Sentry integration */ if (!metasync_is_non_metasync_admin_ajax()) { require_once plugin_dir_path(__FILE__) . 'telemetry/telemetry-init.php'; } /** * The code that runs during plugin deactivation. * This action is documented in includes/class-metasync-deactivator.php */ function deactivate_metasync() { Metasync_Deactivator::deactivate(); // class name is changed at class-db-migrations.php MetaSync_DBMigration::deactivation(); // Clear news/video sitemap caches delete_transient('metasync_vsm_' . md5('news-sitemap.xml')); delete_transient('metasync_vsm_' . md5('video-sitemap.xml')); delete_option('metasync_sitemap_virtual_index'); // Clear OTTO JS detection cache so re-activation gets a fresh check delete_transient('metasync_otto_js_detected'); } register_activation_hook(__FILE__, 'activate_metasync'); register_deactivation_hook(__FILE__, 'deactivate_metasync'); /** * Migrate physical sitemap .xml files into transients and delete them. * * Physical files in ABSPATH cause nginx/Plesk to 403 before WordPress can * serve them. Called from both the activation hook and the version-gate * migration so it covers fresh activations and auto-updates. */ function metasync_migrate_physical_sitemaps() { update_option('metasync_sitemap_virtual_mode', true, false); $candidates = []; $globbed = glob(ABSPATH . 'sitemap*.xml'); if (is_array($globbed)) { foreach ($globbed as $file) { $basename = basename($file); if ($basename === 'sitemap_index.xml' || preg_match('/^sitemap\d*\.xml$/', $basename)) { $candidates[] = $file; } } } foreach (['news-sitemap.xml', 'video-sitemap.xml'] as $extra) { $path = ABSPATH . $extra; if (file_exists($path)) { $candidates[] = $path; } } if (empty($candidates)) { return; } $virtual_index = get_option('metasync_sitemap_virtual_index', []); $migrated_files = []; foreach ($candidates as $file) { $bn = basename($file); $content = @file_get_contents($file); if (false !== $content) { $tkey = 'metasync_vsm_' . md5($bn); set_transient($tkey, $content, 30 * DAY_IN_SECONDS); if (false !== get_transient($tkey)) { @unlink($file); $virtual_index[$bn] = $tkey; if ($bn !== 'sitemap_index.xml' && $bn !== 'news-sitemap.xml' && $bn !== 'video-sitemap.xml') { $migrated_files[] = [ 'filename' => $bn, 'url' => home_url('/' . $bn), 'lastmod' => current_time('mysql', true), ]; } } } } update_option('metasync_sitemap_virtual_index', $virtual_index, false); if (!empty($migrated_files)) { update_option('metasync_sitemap_files', $migrated_files); update_option('metasync_sitemap_last_generated', current_time('mysql')); } } /** * Check for plugin updates and run migrations if needed */ function check_metasync_updates() { static $checked = false; if ($checked) return; $checked = true; $current_version = get_option('metasync_version', '0.0.0'); $plugin_version = METASYNC_VERSION; // If versions don't match, run migration if (version_compare($current_version, $plugin_version, '<')) { // Import whitelabel settings only if the JSON file is new or changed // (prevents overwriting admin UI changes on every version check) Metasync_Activator::check_whitelabel_settings_update(); // Run version-specific migrations first MetaSync_DBMigration::run_version_migrations($current_version, $plugin_version); // Migration for v2.7.0+: Remove AI Agent, switch to plugin auth token, make MCP always-on if (version_compare($current_version, '2.7.0', '<')) { // Remove old MCP API key option delete_option('metasync_mcp_api_key'); // Remove MCP enabled/disabled toggle option (MCP is now always enabled) delete_option('metasync_mcp_enabled'); // Remove AI Agent settings (AI Agent has been removed) delete_option('metasync_ai_agent_mcp_config'); delete_option('metasync_ai_agent_ai_config'); delete_option('metasync_ai_agent_enabled'); // Ensure plugin auth token exists $options = get_option('metasync_options', []); if (empty($options['general']['apikey'])) { $options['general']['apikey'] = wp_generate_password(32, false, false); update_option('metasync_options', $options); } } // One-time purge of stale OTTO SEO cron backlog. // Prior versions could accumulate thousands of metasync_process_seo_job and // metasync_process_otto_crawl_url_job events due to unbounded rescheduling. // Clear the backlog once on update; the new code prevents re-accumulation. if (!get_option('metasync_wp299_cron_cleanup_done')) { wp_unschedule_hook('metasync_process_seo_job'); wp_unschedule_hook('metasync_process_otto_crawl_url_job'); wp_unschedule_hook('metasync_process_otto_batch_cache_job'); update_option('metasync_wp299_cron_cleanup_done', true, false); } // One-time cleanup of canonical values corrupted to the literal // "Array" (emitted as http://Array once the 2.6.16 canonical filters // started reading them). The sanitizer prevents new corruption; this // repairs the rows already in the database. Cache purge below pushes // the clean pages live. Claimed via add_option() — it fails if the row // already exists, so concurrent requests can't run the cleanup twice, // and the claim lands BEFORE the work: everything inside is idempotent // and the read-side sanitizer already protects output if a run is // interrupted. if (false === get_option('metasync_canonical_cleanup_done') && add_option('metasync_canonical_cleanup_done', 'running', '', false)) { MetaSync_DBMigration::cleanup_corrupted_canonicals(); update_option('metasync_canonical_cleanup_done', 'done', false); } // Migrate physical sitemap files on version update. metasync_migrate_physical_sitemaps(); // Run full migration to ensure all tables are up to date MetaSync_DBMigration::run_migrations(); // Update stored version update_option('metasync_version', $plugin_version); // Clear all cache plugins after update Metasync_Cache_Purge::purge_all('plugin_update'); // Log the update //error_log("MetaSync: Plugin updated from {$current_version} to {$plugin_version}. Database migration completed."); } } // Hook into WordPress init to check for updates add_action('init', 'check_metasync_updates', 1); /** * Handle whitelabel settings import after plugin is updated via WordPress admin * This hook fires when plugins are installed/updated through the WordPress updater * * @param WP_Upgrader $upgrader WP_Upgrader instance * @param array $hook_extra Extra arguments passed to hooked filters */ function metasync_handle_plugin_upgrade($upgrader, $hook_extra) { // Only process plugin updates/installs if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') { return; } // By the time upgrader_process_complete fires, the upgrader may have // deleted the directory this (old, still-in-memory) copy of the plugin was // loaded from — e.g. when the installed dir name differs from the package's // root dir ('metasync-develop' vs 'metasync'). The Composer classmap then // points at files that no longer exist, so autoloading Metasync_Activator // below would fatal. Bail instead; the whitelabel re-import runs on the next // request via check_metasync_updates() once the new copy is active. if (!class_exists('Metasync_Activator', false)) { $activator = __DIR__ . '/includes/class-metasync-activator.php'; if (!is_file($activator)) { return; } require_once $activator; } // Only process install and update actions if (!isset($hook_extra['action']) || !in_array($hook_extra['action'], ['install', 'update'], true)) { return; } $this_plugin = plugin_basename(__FILE__); $this_plugin_slug = dirname($this_plugin); // Get 'metasync' from 'metasync/metasync.php' $should_import = false; // Handle bulk updates if (isset($hook_extra['bulk']) && $hook_extra['bulk'] === true && isset($hook_extra['plugins'])) { foreach ($hook_extra['plugins'] as $plugin) { // Match by exact path OR by plugin slug/directory if ($plugin === $this_plugin || dirname($plugin) === $this_plugin_slug) { $should_import = true; break; } } } // Handle single plugin update/install if (isset($hook_extra['plugin'])) { $plugin = $hook_extra['plugin']; // Match by exact path OR by plugin slug/directory if ($plugin === $this_plugin || dirname($plugin) === $this_plugin_slug) { $should_import = true; } } // SPECIAL CASE: When uploading plugin via "Add New > Upload Plugin", // WordPress doesn't set the 'plugin' parameter during 'install' action. // Check if we can get the plugin info from the upgrader result or whitelabel file exists. if (!$should_import && $hook_extra['action'] === 'install') { // Check upgrader result for destination if (isset($upgrader->result) && isset($upgrader->result['destination'])) { $destination = $upgrader->result['destination']; // Check if destination contains our plugin slug if (strpos($destination, $this_plugin_slug) !== false) { $should_import = true; } } // Fallback: Check if whitelabel file exists in our plugin directory // This means our plugin was just installed/updated with whitelabel settings if (!$should_import) { $whitelabel_file = Metasync_Activator::get_whitelabel_settings_file(); if ($whitelabel_file !== false) { $should_import = true; } } } if ($should_import) { Metasync_Activator::check_whitelabel_settings_update(); } } // Hook into WordPress upgrader to detect plugin updates add_action('upgrader_process_complete', 'metasync_handle_plugin_upgrade', 10, 2); /** * Fallback: Check for whitelabel file changes on admin pages * This handles edge cases where upgrader_process_complete doesn't fire * (e.g., FTP uploads, manual file replacements) * Only checks once per admin session to minimize performance impact */ // function metasync_check_whitelabel_on_admin() // { // // Only check once per admin session to avoid overhead // static $checked = false; // if ($checked) { // return; // } // $checked = true; // require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php'; // Metasync_Activator::check_whitelabel_settings_update(); // } // Check for whitelabel changes on admin pages (fallback for edge cases) // add_action('admin_init', 'metasync_check_whitelabel_on_admin', 1); // Include Media Optimization Module if (!metasync_is_non_metasync_admin_ajax()) { require_once plugin_dir_path(__FILE__) . 'media-optimization/media-optimization-loader.php'; } // Include Code Minification & Delivery Module if (!metasync_is_non_metasync_admin_ajax()) { require_once plugin_dir_path(__FILE__) . 'code-minification/code-minification-loader.php'; } function run_metasync() { $plugin = new Metasync(); $plugin->run(); } run_metasync(); // MCP server bootstrap (server + tool registration) extracted to keep this entry file lean. require_once plugin_dir_path( __FILE__ ) . 'includes/mcp-server-bootstrap.php'; /** * Schedule a daily cron event to auto-purge Sync History records older than 90 days. */ function metasync_schedule_sync_log_cleanup() { if (!wp_next_scheduled('metasync_sync_log_daily_cleanup')) { wp_schedule_event(time(), 'daily', 'metasync_sync_log_daily_cleanup'); } } add_action('wp', 'metasync_schedule_sync_log_cleanup'); /** * Cron callback: delete Sync History records older than 90 days. */ function metasync_sync_log_cleanup_handler() { $sync_db = new Metasync_Sync_History_Database(); $sync_db->delete_older_than_days(90); } add_action('metasync_sync_log_daily_cleanup', 'metasync_sync_log_cleanup_handler'); /** * Output DYO initialization flag to the frontend * Makes window.__SA_DYO_INITIALIZED__ = true available in the DOM * This indicates the Search Atlas plugin is active and initialized */ function metasync_output_dyo_init_flag() { echo '' . "\n"; } add_action('wp_head', 'metasync_output_dyo_init_flag', 1); // Runtime feature initialisers (GA4, API backoff, review notice, JWT accessor, debug mode) extracted to keep this entry file lean. require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-runtime-init.php'; /** * Append a "Website Studio" post state to LPS-synced / MetaSync custom pages in * the admin Pages list, so site owners can tell at a glance which pages are * managed by Website Studio and shouldn't be hand-edited. * * Hooks WordPress core's display_post_states filter — the same mechanism that * renders the grey inline tags like "— Front Page" / "— Draft" — so the label * is native-styled and only appears next to relevant page titles, with no * custom admin column. * * @param string[] $post_states Existing post-state labels keyed by slug. * @param WP_Post $post The post being listed. * @return string[] Possibly-augmented post states. */ function metasync_add_lps_post_state($post_states, $post) { // metasync_is_custom_or_lps_page() lives in otto/otto_pixel.php, which is NOT // loaded on non-MetaSync admin-ajax requests (e.g. Quick Edit's inline-save, // where this filter still fires), so guard against the undefined function. if (!function_exists('metasync_is_custom_or_lps_page')) { return $post_states; } if (!metasync_is_custom_or_lps_page($post->ID)) { return $post_states; } $post_states['metasync_website_studio'] = __('Website Studio', 'metasync'); return $post_states; } /** * Oxygen Builder Compatibility * Auto re-signs [oxygen] dynamic-data shortcodes when their HMAC signatures * are invalid (e.g. after design-set import or site migration). * Runs once on admin_init; skips entirely when Oxygen is inactive. */ if (is_admin()) { add_action('admin_init', ['Metasync_Oxygen_Compat', 'maybe_resign_shortcodes'], 20); add_filter('display_post_states', 'metasync_add_lps_post_state', 10, 2); }