# metasync/2.6.21/metasync.php

Search Atlas SEO – OTTO AI SEO Automation for WordPress, version 2.6.21. 666 lines.

- Page: https://pluginprobe.com/plugins/metasync/2.6.21/code/metasync.php
- Raw: https://pluginprobe.com/plugins/metasync/2.6.21/raw/metasync.php
- Modified: 2026-08-04T00:06:00+00:00

Line numbers below start at 1. Link to a line or a range by appending a fragment to the
page URL, for example `https://pluginprobe.com/plugins/metasync/2.6.21/code/metasync.php#L10-L20`.

```php
<?php

/**
 * This file is read by WordPress to generate the plugin information in the plugin
 * admin area. This file also includes all of the dependencies used by the plugin,
 * registers the activation and deactivation functions, and defines a function
 * that starts the plugin.
 *
 * @package     Search Atlas SEO
 * @copyright   Copyright (C) 2021-2025, Search Atlas Group - support@searchatlas.com
 * @link		https://searchatlas.com/
 * @since		1.0.0
 *
 * @wordpress-plugin
 * Plugin Name:       Search Atlas: The Premier AI SEO Plugin for Instant Optimization
 * Plugin URI:        https://searchatlas.com/
 * Description:       Search Atlas SEO is an intuitive WordPress Plugin that transforms the most complicated, most labor-intensive SEO tasks into streamlined, straightforward processes. With a few clicks, the meta-bulk update feature automates the re-optimization of meta tags using AI to increase clicks. Stay up-to-date with the freshest Google Search data for your entire site or targeted URLs within the Meta Sync plug-in page.
 * Version:           2.6.21
 * Requires PHP:      8.2
 * Author:            Search Atlas
 * Author URI:        https://searchatlas.com
 * License:           GPL v3
 * License URI:       https://www.gnu.org/licenses/gpl-3.0.txt
 * Text Domain:       metasync
 */

// If this file is called directly, abort.
if (!defined('WPINC')) {
	die;
}

// Composer classmap autoloader — loads all plugin classes on demand.
require_once __DIR__ . '/vendor/autoload.php';

// Canonical sanitizer — required explicitly (not via the committed classmap)
// so it is guaranteed loadable everywhere canonicals are read or written.
require_once __DIR__ . '/includes/class-metasync-canonical-sanitizer.php';

/**
 * Currently plugin version.
 * Start at version 1.0.0 and use SemVer - https://semver.org
 * Rename this for your plugin and update it as you release new versions.
 */
$metasync_version = '2.6.21';
define('METASYNC_VERSION', preg_match('/^\d+\.\d+/', $metasync_version) ? $metasync_version : '9.9.9');
/**
 * Define the current required php version 
 * This will be used to validate whether the user can install the plugin or not
 */
define('METASYNC_MIN_PHP', '8.2');

/**
 * Define the current required php version 
 * This will be used to validate whether the user can install the plugin or not
 */
define('METASYNC_MIN_WP', '5.2');

/**
 * Telemetry Configuration Constants
 * These replace the old database options for better security and consistency
 */
define('METASYNC_SENTRY_PROJECT_ID', '4509950439849985');
define('METASYNC_SENTRY_ENVIRONMENT', 'production');
define('METASYNC_SENTRY_RELEASE', METASYNC_VERSION);
define('METASYNC_SENTRY_SAMPLE_RATE', 1.0);

/**
 * GA4 Analytics Configuration
 * Measurement ID for Google Analytics 4 event tracking (format: G-XXXXXXXX)
 *
 * IMPORTANT: This constant is REQUIRED for GA4 tracking to function.
 * If not defined or empty, all GA4 analytics tracking will be disabled.
 *
 * GA4_API_SECRET is required for server-side Measurement Protocol events
 * (Content Genius, OTTO optimization). Generate it in GA4:
 * Admin → Data Streams → (stream) → Measurement Protocol → Create
 */
define('METASYNC_GA4_MEASUREMENT_ID', 'G-SBLWW1EMTJ');
define('METASYNC_GA4_API_SECRET', 'nMGs22mxQ3qVUy-aInqfZA');

/**
 * Define whether to show the plugin status in WordPress admin top navigation bar
 * Set to false to hide the status indicator
 */
define('METASYNC_SHOW_ADMIN_BAR_STATUS', true);

/**
 * Sanitize POST/GET/REQUEST data recursively
 * 
 * @param array $data Data to sanitize
 * @return array Sanitized data
 */
if (!function_exists('metasync_sanitize_input_array')) {
	function metasync_sanitize_input_array($data) {
		if (!is_array($data)) {
			return sanitize_text_field($data);
		}

		$sanitized = [];
		foreach ($data as $key => $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 '<script>window.__SA_DYO_INITIALIZED__=true;</script>' . "\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);
}

```
