# metasync/2.7.2/includes/class-metasync-activator.php

Search Atlas SEO – OTTO AI SEO Automation for WordPress, version 2.7.2. 963 lines.

- Page: https://pluginprobe.com/plugins/metasync/2.7.2/code/includes/class-metasync-activator.php
- Raw: https://pluginprobe.com/plugins/metasync/2.7.2/raw/includes/class-metasync-activator.php
- Modified: 2026-10-07T12:05:24+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.7.2/code/includes/class-metasync-activator.php#L10-L20`.

```php
<?php

/**
 * Fired during plugin activation
 *
 * @link       https://searchatlas.com
 * @since      1.0.0
 *
 * @package    Metasync
 * @subpackage Metasync/includes
 */

/**
 * Fired during plugin activation.
 *
 * This class defines all code necessary to run during the plugin's activation.
 *
 * @since      1.0.0
 * @package    Metasync
 * @subpackage Metasync/includes
 * @author     Engineering Team <support@searchatlas.com>
 */
class Metasync_Activator
{

	/**
	 * Option recording the last failed White Label JSON import.
	 * Stores array{file_hash: string, failed_at: int, error: string}.
	 *
	 * @var string
	 */
	 const IMPORT_FAILURE_OPTION = 'metasync_whitelabel_import_failure';

	/**
	 * Retry cadence (seconds) for a failing White Label import so a
	 * broken file cannot produce a silent retry storm on every admin request.
	 *
	 * @var int
	 */
	 const IMPORT_RETRY_BACKOFF_SECONDS = 3600;

	/**
	 * The white-label settings whose values are mirrored into the
	 * metasync.php plugin header.
	 *
	 * @var string[]
	 */
	const BRANDING_HEADER_FIELDS = [
		'white_label_plugin_name',
		'white_label_plugin_description',
		'white_label_plugin_author',
		'white_label_plugin_author_uri',
		'white_label_plugin_uri',
	];

	/**
	 * Machine-readable reason for the most recent import failure, consumed
	 * by render_whitelabel_import_failure_notice().
	 *
	 * @var string
	 */
	 private static $last_import_error = '';

	/**
	 * Field-specific validation errors for the most recent import failure,
	 * keyed by white-label setting name. Populated only when the import was
	 * rejected because a branding value fails the plugin-header contract.
	 *
	 * @var array<string, string>
	 */
	 private static $last_import_invalid_fields = array();

	/**
	 * Canonical list of MetaSync custom WP-Cron hooks.
	 * Shared with Metasync_Deactivator so deactivation cleans up every scheduled hook.
	 *
	 * @since 2.5.x
	 * @var string[]
	 */
	public static $cron_hooks = [
		'metasync_sync_log_daily_cleanup',
		'metasync_announce_cron',
		'metasync_rate_limit_cleanup',
		'metasync_heartbeat_cron_check',
		'metasync_burst_heartbeat',
		'metasync_check_debug_limits',
		'metasync_cleanup_transients',
		'metasync_hidden_post_check',
		'metasync_otto_recheck_404_exclusions',
		'metasync_db_cleanup',
		'metasync_media_batch_optimize_cron',
		'metasync_speed_cache_cleanup',
		'metasync_process_seo_job',
		'metasync_process_otto_crawl_url_job',
		'metasync_process_otto_batch_cache_job',
		'metasync_host_blocking_check',
		'metasync_host_blocking_weekly_check',
	];

	/**
	 * Short Description. (use period)
	 *
	 * Long Description.
	 *
	 * @since    1.0.0
	 */
	public static function activate()
	{
		// WordPress core sitemap functionality is required
		// if (wp_sitemaps_get_server()->sitemaps_enabled() == false) {
		// 	add_filter('wp_sitemaps_enabled', '__return_true');
		// }

		// Generate Plugin Auth Token on first activation
		self::ensure_plugin_auth_token();

		// Import whitelabel settings only if the JSON file is new or changed
		// (prevents overwriting admin UI changes on every deactivate/activate cycle)
		self::check_whitelabel_settings_update(true);

		// Pre-SSO announce: tell backend plugin is installed (PR4 - heartbeat reliability)
		update_option('metasync_announce_attempt_count', 0);
		self::send_announce_ping();
		update_option('metasync_announce_attempt_count', 1);

		// Schedule cron for announce pings 2-5 (every 10 minutes)
		if (!wp_next_scheduled('metasync_announce_cron')) {
			wp_schedule_event(time() + 10 * MINUTE_IN_SECONDS, 'metasync_every_10_minutes', 'metasync_announce_cron');
		}

		// Detect a host that blocks GET/POST between this site and Search Atlas.
		// Deferred to cron (~10 minutes out) rather than run here — a blocking HTTP call
		// inside the activation request is exactly what has taken slow hosts down before,
		// and a firewall-blocked request is the slowest kind there is.
		if (class_exists('Metasync_Host_Blocking_Check')) {
			Metasync_Host_Blocking_Check::schedule_initial_check();
		}

		// Set first activation flag for setup wizard
		if (!get_option('metasync_first_activation_time')) {
			update_option('metasync_first_activation_time', current_time('mysql'));
			update_option('metasync_show_wizard', true);
		}

		// Auto-disable WP core sitemap if a MetaSync sitemap already exists.
		// Skip on fresh installs where no sitemap has been generated yet so the site
		// doesn't end up with zero sitemaps.
		if (get_option('metasync_sitemap_auto_update', false)
			|| !empty(get_option('metasync_sitemap_files', []))
			|| file_exists(ABSPATH . 'sitemap_index.xml')
		) {
			update_option('metasync_disable_wp_sitemap', true);
		}

		// Soft flush only (no .htaccess rewrite). A hard flush calls
		// save_mod_rewrite_rules(), which takes an exclusive flock() on the site's
		// root .htaccess. On hosts whose cache/optimizer also holds that lock (e.g.
		// SiteGround + SG Optimizer), the activation request blocks until
		// max_execution_time and the host returns a 500. MetaSync's rewrite rules are
		// registered via add_rewrite_rule (stored in the `rewrite_rules` option, NOT
		// in .htaccess), so a soft flush is sufficient and never touches the file.
		flush_rewrite_rules(false);
	}

	/**
	 * Send pre-SSO announce ping to backend (zero-trust; backend rate-limits).
	 * POST /api/wp-plugin-announce/ with url + plugin_version; optional X-Plugin-Token for deduplication.
	 * Callable from activation and from init (rate-limited) when no API key yet.
	 *
	 * @since 2.5.x
	 */
	public static function send_announce_ping()
	{
		$base = 'https://ca.searchatlas.com';
		if (class_exists('Metasync_Endpoint_Manager')) {
			$base = Metasync_Endpoint_Manager::get_endpoint('CA_API_DOMAIN');
		} elseif (class_exists('Metasync')) {
			$base = Metasync::CA_API_DOMAIN;
		}
		$url = rtrim($base, '/') . '/api/wp-plugin-announce/';

		$body = wp_json_encode([
			'url' => get_home_url(),
			'plugin_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown',
		]);

		$options = get_option('metasync_options', []);
		$plugin_auth_token = $options['general']['apikey'] ?? '';
		$headers = [
			'Content-Type' => 'application/json',
		];
		if (!empty($plugin_auth_token)) {
			$headers['X-Plugin-Token'] = $plugin_auth_token;
		}

		wp_remote_post($url, [
			'body'    => $body,
			'headers' => $headers,
			'timeout' => 10,
			'blocking' => false,
		]);
	}

	/**
	 * Ensure Plugin Auth Token exists
	 * Generates a unique Plugin Auth Token during plugin activation
	 */
	private static function ensure_plugin_auth_token()
	{
		$options = get_option('metasync_options', []);

		if (empty($options['general']['apikey'])) {
			// Generate unique Plugin Auth Token (alphanumeric only)
			$plugin_auth_token = wp_generate_password(32, false, false);

			// Initialize options structure if needed
			if (!isset($options['general'])) {
				$options['general'] = [];
			}

			// Store Plugin Auth Token
			$options['general']['apikey'] = $plugin_auth_token;
			update_option('metasync_options', $options);
		}
	}

	/**
	 * Get the path to the whitelabel settings JSON file
	 *
	 * @return string|false Path to the file if it exists, false otherwise
	 * @since 2.5.0
	 */
	public static function get_whitelabel_settings_file()
	{
		$plugin_dir = plugin_dir_path(dirname(__FILE__));

		// Check for whitelabel-settings.json in plugin root
		$json_file = $plugin_dir . 'whitelabel-settings.json';

		// Also check in a common extracted zip location (if zip was extracted)
		if (!file_exists($json_file)) {
			$json_file = $plugin_dir . 'metasync/whitelabel-settings.json';
		}

		if (!file_exists($json_file)) {
			return false;
		}

		return $json_file;
	}

	/**
	 * Check if whitelabel settings file has been updated and import if needed
	 * This method should be called on init to detect plugin uploads/updates
	 *
	 * @param bool $trusted_update_context True only for core activation/upgrader hooks.
	 * @return bool Whether the file was already current or imported successfully.
	 * @since 2.5.0
	 */
	public static function check_whitelabel_settings_update($trusted_update_context = false)
	{
// Never run the import from AJAX handlers. admin-ajax requests
		// (notably the Forgot Password recovery call) run admin_init first, so an
		// import attempted there already holds the shared recovery lock when the
		// handler itself runs, and every recovery request returns HTTP 429.
		// Trusted activation/upgrader contexts run once per update, not per
		// request, and stay exempt.
		if (!$trusted_update_context && wp_doing_ajax()) {
			return false;
		}

		if (!$trusted_update_context && !current_user_can('manage_options')) {
			return false;
		}

		$json_file = self::get_whitelabel_settings_file();

		if ($json_file === false) {
			return false;
		}

		// Get current file modification time and content hash
		$file_mtime = filemtime($json_file);
		$file_hash = md5_file($json_file);
		if (!is_int($file_mtime) || !is_string($file_hash)) {
			return false;
		}

		// Get stored file info
		$stored_mtime = get_option('metasync_whitelabel_file_mtime', 0);
		$stored_hash = get_option('metasync_whitelabel_file_hash', '');

		// Check if file has changed (either modification time or content)
		if ($file_mtime > $stored_mtime || $file_hash !== $stored_hash) {
			// A failing import must not retry on every admin request.
			// Back off per failing file hash; a successful import or a changed
			// file clears the state and retries immediately.
			$failure = get_option(self::IMPORT_FAILURE_OPTION, array());
			if (is_array($failure)
				&& isset($failure['file_hash'], $failure['failed_at'])
				&& (string) $failure['file_hash'] === $file_hash
				&& (time() - (int) $failure['failed_at']) < self::IMPORT_RETRY_BACKOFF_SECONDS) {
				return false;
			}

			// File has changed, import settings
			if (!self::import_whitelabel_settings()) {
				$failure = array(
					'file_hash' => $file_hash,
					'failed_at' => time(),
					'error' => self::$last_import_error,
				);
				if (self::$last_import_error === 'branding_field_invalid' && !empty(self::$last_import_invalid_fields)) {
					$failure['invalid_fields'] = self::$last_import_invalid_fields;
				}
				update_option(self::IMPORT_FAILURE_OPTION, $failure);
				return false;
			}
			delete_option(self::IMPORT_FAILURE_OPTION);

			// Record the file only after the complete import succeeds.
			update_option('metasync_whitelabel_file_mtime', $file_mtime);
			update_option('metasync_whitelabel_file_hash', $file_hash);
		}

		return true;
	}

	/**
	 * Machine-readable reason of the most recent failed White Label import.
	 *
	 * @return string
	 */
	public static function get_last_import_error()
	{
		return self::$last_import_error;
	}

	/**
	 * Field-specific branding errors of the most recent failed White Label import.
	 *
	 * @return array<string, string>
	 */
	public static function get_last_import_invalid_fields()
	{
		return self::$last_import_invalid_fields;
	}

	/**
	 * Admin notice for a White Label JSON import that keeps failing.
	 * Registered from metasync_check_whitelabel_on_admin(); the state is set
	 * by check_whitelabel_settings_update() when an import fails.
	 *
	 * @return void
	 */
	public static function render_whitelabel_import_failure_notice()
	{
		if (!current_user_can('manage_options')) {
			return;
		}
		$failure = get_option(self::IMPORT_FAILURE_OPTION, array());
		if (!is_array($failure) || empty($failure['file_hash']) || empty($failure['failed_at'])) {
			return;
		}
		$reasons = array(
			'json_read_failed' => 'the file could not be read',
			'json_invalid' => 'the file is not valid JSON',
			'json_structure_invalid' => 'the file structure is invalid',
			'plugin_file_unreadable' => 'metasync.php could not be read',
			'branding_field_invalid' => 'the white-label branding values are not valid',
			'plugin_header_write_failed' => 'the plugin file headers could not be updated (is metasync.php writable?)',
			'recovery_lock_busy' => 'another password operation was in progress',
			'options_write_failed' => 'the settings could not be saved',
		);
		$reason = isset($failure['error'], $reasons[$failure['error']]) ? $reasons[$failure['error']] : 'an unexpected error occurred';

		// Field-specific detail for a content rejection: name the exact
		// branding field so the package can be fixed rather than retried blind.
		echo '<div class="notice notice-error"><p><strong>White Label settings import failed.</strong> '
			. 'MetaSync could not import <code>whitelabel-settings.json</code>: ' . esc_html($reason)
			. '. The import will be retried automatically; fix the file and reload to retry immediately.</p>';
		if (isset($failure['invalid_fields']) && is_array($failure['invalid_fields']) && !empty($failure['invalid_fields'])) {
			echo '<ul style="margin: 8px 0 0 0; list-style: disc; padding-left: 20px;">';
			foreach ($failure['invalid_fields'] as $message) {
				echo '<li>' . esc_html((string) $message) . '</li>';
			}
			echo '</ul>';
			echo '<p style="margin: 8px 0 0 0;">Allowed: single-line text (including valid Unicode) and http(s) URLs. Line breaks, control/invisible characters, comment-ending text, PHP tags, and non-http(s) URLs are rejected.</p>';
		}
		echo '</div>';
	}

	/**
	 * Import whitelabel settings from JSON file if available
	 * Checks for whitelabel-settings.json in the plugin directory or extracted zip
	 *
	 * @return bool Whether the complete import succeeded.
	 * @since 2.5.0
	 */
	private static function import_whitelabel_settings()
	{
		$json_file = self::get_whitelabel_settings_file();

		if ($json_file === false) {
			return false;
		}

		self::$last_import_error = 'unknown';
		self::$last_import_invalid_fields = array();

		// Read JSON file
		$json_content = file_get_contents($json_file);
		if ($json_content === false) {
			self::$last_import_error = 'json_read_failed';
			return false;
		}

		// Decode JSON
		$import_data = json_decode($json_content, true);
		if ($import_data === null || json_last_error() !== JSON_ERROR_NONE) {
			self::$last_import_error = 'json_invalid';
			return false;
		}

		// Validate import data structure
		if (!isset($import_data['whitelabel_settings']) || !is_array($import_data['whitelabel_settings'])) {
			self::$last_import_error = 'json_structure_invalid';
			return false;
		}

		$plugin_file = plugin_dir_path(dirname(__FILE__)) . 'metasync.php';
		$original_plugin_content = file_get_contents($plugin_file);
		if ($original_plugin_content === false) {
			self::$last_import_error = 'plugin_file_unreadable';
			return false;
		}

		// Get current options
		$options = get_option('metasync_options', array());

		// Import whitelabel settings
		if (isset($import_data['whitelabel_settings'])) {
			$whitelabel_settings = $import_data['whitelabel_settings'];

			// Initialize whitelabel array if needed
			if (!isset($options['whitelabel'])) {
				$options['whitelabel'] = array();
			}

			// Merge imported settings with existing (imported settings take precedence)
			$options['whitelabel'] = array_merge($options['whitelabel'], $whitelabel_settings);

			// The settings password travels as plaintext in the export file so it
			// can be imported on a site with different salts — encrypt it at rest.
			if (!empty($options['whitelabel']['settings_password']) && is_string($options['whitelabel']['settings_password'])) {
				$options['whitelabel']['settings_password'] = Metasync::encrypt_secret($options['whitelabel']['settings_password']);
			}

			// Update timestamp
			$options['whitelabel']['updated_at'] = time();
			$options['whitelabel']['imported_at'] = current_time('mysql');
		}

		// Import general settings related to whitelabel
		if (isset($import_data['general_settings']) && is_array($import_data['general_settings'])) {
			if (!isset($options['general'])) {
				$options['general'] = array();
			}

			// Merge general settings
			foreach ($import_data['general_settings'] as $key => $value) {
				$options['general'][$key] = $value;
			}
		}

		// Validate and persist the public plugin headers before any other import side effect.
		$header_data = array(
			'general_settings' => $options['general'] ?? array(),
		);
		if (!self::update_plugin_file_headers($header_data, $plugin_file)) {
			self::$last_import_error = 'plugin_header_write_failed';
			// Distinguish a content rejection (invalid branding values) from an
			// environmental failure (unwritable file, mangled header block) so the
			// admin notice can point at the offending field.
			$invalid_fields = self::validate_plugin_header_settings($options['general'] ?? array());
			if (!empty($invalid_fields)) {
				self::$last_import_error = 'branding_field_invalid';
				self::$last_import_invalid_fields = $invalid_fields;
			}
			return false;
		}

		// Restore bundled icon: if the icon value is a __bundled_icon__{ext} marker,
		// copy the bundled file from the plugin directory to uploads and update the URL.
		$icon_value = $options['general']['white_label_plugin_menu_icon'] ?? '';
		if (!empty($icon_value) && strpos($icon_value, '__bundled_icon__') === 0) {
			$ext             = substr($icon_value, strlen('__bundled_icon__'));
			$ext             = preg_replace('/[^a-z0-9]/', '', strtolower($ext)); // sanitize
			$bundled_file    = plugin_dir_path(dirname(__FILE__)) . 'whitelabel-icon.' . $ext;

			if (file_exists($bundled_file) && in_array($ext, ['png', 'svg'], true)) {
				$upload_dir  = wp_upload_dir();
				$dest_dir    = $upload_dir['basedir'] . '/metasync';
				if (!file_exists($dest_dir)) {
					wp_mkdir_p($dest_dir);
				}
				$dest_file = $dest_dir . '/whitelabel-icon.' . $ext;
				if (copy($bundled_file, $dest_file)) {
					$options['general']['white_label_plugin_menu_icon'] = $upload_dir['baseurl'] . '/metasync/whitelabel-icon.' . $ext;
				} else {
					// Could not copy — clear the broken marker so default icon shows
					$options['general']['white_label_plugin_menu_icon'] = '';
				}
			} else {
				// Bundled file missing or unsupported extension — clear the marker
				$options['general']['white_label_plugin_menu_icon'] = '';
			}
		}

		// Save updated options.
// when the package carries a settings password this is a
		// legitimate import write, but the recovery protection filter treats any
		// unauthenticated password write as a conflict and swaps it back to the
		// stored password — the verification below could then never pass once a
		// password is stored, and the retry loop would hold the shared recovery
		// lock on every admin request, blocking the Forgot Password flow.
		// Mirror the recovery flow's own persist path: take the lock and
		// authorize the write for the duration of the save.
		$carries_password = !empty($options['whitelabel']['settings_password']);
		$import_lock_owner = '';
		if ($carries_password) {
			require_once __DIR__ . '/class-metasync-admin-ajax.php';
			require_once __DIR__ . '/class-metasync-settings-registration.php';
			if (!self::class_supports_methods('Metasync_Admin_Ajax', array('acquire_recovery_lock', 'release_recovery_lock'))
				|| !self::class_supports_methods('Metasync_Settings_Registration', array('authorize_recovery_password_write'))) {
				// The updater can replace plugin files after the old classes were
				// loaded in this request. Defer the import to the next request.
				self::$last_import_error = 'recovery_api_unavailable';
				return false;
			}
			if (!Metasync_Admin_Ajax::instance()->acquire_recovery_lock($import_lock_owner)) {
				// A recovery or settings save is mid-write; retry via the normal
				// backoff instead of fighting for the lock.
				self::$last_import_error = 'recovery_lock_busy';
				return false;
			}
		}
		try {
			if ($carries_password) {
				Metasync_Settings_Registration::authorize_recovery_password_write(true);
			}
			update_option('metasync_options', $options);
		} finally {
			if ($carries_password) {
				Metasync_Settings_Registration::authorize_recovery_password_write(false);
				Metasync_Admin_Ajax::instance()->release_recovery_lock($import_lock_owner);
			}
		}
		if (get_option('metasync_options', null) !== $options) {
			if (self::atomically_replace_plugin_file($plugin_file, $original_plugin_content)) {
				wp_cache_delete('plugins', 'plugins');
			}
			self::$last_import_error = 'options_write_failed';
			return false;
		}

		// Optionally delete the JSON file after successful import (uncomment if desired)
		// unlink($json_file);

		return true;
	}

	/**
	 * Check runtime class compatibility without letting static analysis fold it.
	 *
	 * @param string   $class_name Class name loaded in the current request.
	 * @param string[] $methods Required method names.
	 * @return bool
	 */
	private static function class_supports_methods($class_name, $methods)
	{
		foreach ($methods as $method) {
			if (!method_exists($class_name, $method)) {
				return false;
			}
		}

		return true;
	}

	/**
	 * Sync plugin file headers from the current saved options in the database.
	 * Call this after saving whitelabel settings via the admin UI to ensure
	 * the plugin file headers reflect the latest whitelabel values.
	 *
	 * @since 2.5.0
	 */
	public static function sync_plugin_file_headers()
	{
		if (!current_user_can('manage_options')) {
			return false;
		}

		$options = get_option('metasync_options', array());
		$general = $options['general'] ?? array();

		// Build the import_data format expected by update_plugin_file_headers
		$import_data = array(
			'general_settings' => $general,
		);

		return self::update_plugin_file_headers($import_data);
	}

	/**
	 * Absolute path of the plugin file whose header carries the branding.
	 *
	 * @return string
	 */
	public static function plugin_header_file_path()
	{
		return plugin_dir_path(dirname(__FILE__)) . 'metasync.php';
	}

	/**
	 * Apply a proposed save's branding to the physical plugin header BEFORE
	 * the options write starts, so an unwritable or malformed target stops
	 * the save while nothing has been persisted yet.
	 *
	 * Returns null when none of the branding values changed (the file is left
	 * untouched — unrelated saves must not rewrite metasync.php), true when
	 * the header now matches the proposed values, and false when the update
	 * was rejected or failed (file unchanged, save must be aborted).
	 *
	 * @param mixed $proposed_general General section proposed for saving.
	 * @param mixed $stored_general   General section currently stored.
	 * @return bool|null
	 */
	public static function apply_plugin_file_headers_for_save($proposed_general, $stored_general)
	{
		$proposed_general = is_array($proposed_general) ? $proposed_general : array();
		$stored_general = is_array($stored_general) ? $stored_general : array();

		$changed = false;
		foreach (self::BRANDING_HEADER_FIELDS as $key) {
			if ((string) ($proposed_general[$key] ?? '') !== (string) ($stored_general[$key] ?? '')) {
				$changed = true;
				break;
			}
		}
		if (!$changed) {
			return null;
		}

		return self::update_plugin_file_headers(array('general_settings' => $proposed_general));
	}

	/**
	 * Restore a previous metasync.php content after a save that wrote new
	 * headers but then failed to persist the matching options.
	 *
	 * @param string $plugin_file       Absolute path to metasync.php.
	 * @param string|false $original_content  Content captured before the header write (file_get_contents() result).
	 * @return bool Whether the restore succeeded.
	 */
	public static function restore_plugin_file_content($plugin_file, $original_content)
	{
		if (!is_string($original_content) || $original_content === '') {
			return false;
		}
		if (!self::atomically_replace_plugin_file($plugin_file, $original_content)) {
			return false;
		}
		wp_cache_delete('plugins', 'plugins');
		return true;
	}

	/**
	 * Validate the five values that are written into metasync.php's plugin header.
	 *
	 * This is deliberately shared by administrator saves and package imports so
	 * both paths enforce the same contract before options are persisted.
	 *
	 * @param mixed $general General settings submitted or imported.
	 * @return array<string, string> Field-specific errors, keyed by setting name.
	 */
	public static function validate_plugin_header_settings($general)
	{
		$general = is_array($general) ? $general : array();
		$header_map = array(
			'white_label_plugin_name' => array(
				'label' => 'Plugin Name',
				'default' => 'Search Atlas SEO - OTTO AI SEO Automation for WordPress',
				'url' => false,
			),
			'white_label_plugin_description' => array(
				'label' => 'Plugin Description',
				'default' => '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.',
				'url' => false,
			),
			'white_label_plugin_author' => array(
				'label' => 'Author',
				'default' => 'Search Atlas',
				'url' => false,
			),
			'white_label_plugin_author_uri' => array(
				'label' => 'Author URI',
				'default' => 'https://searchatlas.com',
				'url' => true,
			),
			'white_label_plugin_uri' => array(
				'label' => 'Plugin URI',
				'default' => 'https://searchatlas.com/',
				'url' => true,
			),
		);
		$errors = array();

		foreach ($header_map as $key => $config) {
			// A present-but-empty string resets to the default; a scalar like
			// "0" is a real value and must not be conflated with unset (the
			// classic PHP !empty() trap). Malformed non-strings fall through
			// to the is_string rejection below.
			$value = $config['default'];
			if (array_key_exists($key, $general)) {
				$raw = $general[$key];
				if (is_string($raw)) {
					$value = $raw !== '' ? $raw : $config['default'];
				} elseif ($raw !== null) {
					$value = $raw;
				}
			}
			if (!is_string($value) || !self::is_valid_plugin_header_value($value, $config['url'])) {
				$errors[$key] = sprintf('%s contains invalid characters or format.', $config['label']);
			}
		}

		return $errors;
	}

	/**
	 * Update the main plugin file headers with whitelabel values
	 * WordPress reads plugin metadata directly from the file header comments,
	 * so modifying these ensures whitelabel shows even when the plugin is deactivated.
	 *
	 * @param array $import_data The imported whitelabel data
	 * @since 2.5.0
	 */
	private static function update_plugin_file_headers($import_data, $plugin_file = null)
	{
		$plugin_file = $plugin_file ?: plugin_dir_path(dirname(__FILE__)) . 'metasync.php';

		if (!file_exists($plugin_file) || !wp_is_writable($plugin_file)) {
			return false;
		}

		$content = file_get_contents($plugin_file);
		if ($content === false) {
			return false;
		}

		$general = $import_data['general_settings'] ?? array();

		// Map of whitelabel setting keys to plugin header field names
		// with default values to restore when whitelabel is cleared
		$header_map = array(
			'white_label_plugin_name'        => array(
				'header'  => 'Plugin Name',
				'default' => 'Search Atlas SEO - OTTO AI SEO Automation for WordPress',
			),
			'white_label_plugin_description'  => array(
				'header'  => 'Description',
				'default' => '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.',
			),
			'white_label_plugin_author'       => array(
				'header'  => 'Author',
				'default' => 'Search Atlas',
			),
			'white_label_plugin_author_uri'   => array(
				'header'  => 'Author URI',
				'default' => 'https://searchatlas.com',
			),
			'white_label_plugin_uri'          => array(
				'header'  => 'Plugin URI',
				'default' => 'https://searchatlas.com/',
			),
		);

		$modified = false;
		$original_content = $content;
		$url_fields = array('white_label_plugin_author_uri', 'white_label_plugin_uri');

		foreach ($header_map as $setting_key => $field_config) {
			$header_field = $field_config['header'];

			// Use whitelabel value if set, otherwise restore default. A scalar
			// like "0" is a real value, not an unset field; an empty string or
			// null restores the default; malformed non-strings are rejected by
			// the is_string check below.
			$new_value = $field_config['default'];
			if (array_key_exists($setting_key, $general)) {
				$raw_value = $general[$setting_key];
				if (is_string($raw_value)) {
					$new_value = $raw_value !== '' ? $raw_value : $field_config['default'];
				} elseif ($raw_value !== null) {
					$new_value = $raw_value;
				}
			}

			if (!is_string($new_value) || !self::is_valid_plugin_header_value($new_value, in_array($setting_key, $url_fields, true))) {
				return false;
			}

			// Match the header line: " * Field Name:       any value"
			// Handles varying whitespace between field name and value
			$pattern = '/^(\s*\*\s*' . preg_quote($header_field, '/') . ':\s*)(.+)$/m';

			if (preg_match_all($pattern, $content, $matches) !== 1) {
				return false;
			}

			// Only replace if the value actually differs from what's in the file.
			if (trim($matches[2][0]) !== trim($new_value)) {
				$content = preg_replace_callback(
					$pattern,
					static function ($match) use ($new_value) {
						return $match[1] . $new_value;
					},
					$content,
					1,
					$replacement_count
				);

				if (!is_string($content) || $replacement_count !== 1) {
					return false;
				}
				$modified = true;
			}
		}

		if (!self::is_valid_transformed_plugin_file($original_content, $content, $header_map)) {
			return false;
		}

		if (!$modified) {
			return true;
		}

		if (!self::atomically_replace_plugin_file($plugin_file, $content)) {
			return false;
		}

		// Clear WordPress plugin cache so it reads the updated headers.
		wp_cache_delete('plugins', 'plugins');

		return true;
	}

	/**
	 * Validate a value before placing it inside the plugin's PHP docblock.
	 *
	 * Public so form/import handlers can pre-validate a value with the exact
	 * contract used when metasync.php is physically rewritten.
	 *
	 * @param string $value Candidate header value.
	 * @param bool   $is_url Whether the value must be an http(s) URL.
	 * @return bool
	 */
	public static function is_valid_plugin_header_value($value, $is_url)
	{
		if (
			$value === ''
			|| preg_match('/^[\p{L}\p{M}\p{N}\p{P}\p{S}\p{Zs}]+$/u', $value) !== 1
			|| preg_match('/\*\/|<\?php|\?>/i', $value) !== 0
		) {
			return false;
		}

		if (!$is_url) {
			return true;
		}

		if (filter_var($value, FILTER_VALIDATE_URL) === false) {
			return false;
		}

		$scheme = strtolower((string) wp_parse_url($value, PHP_URL_SCHEME));

		return in_array($scheme, array('http', 'https'), true);
	}

	/**
	 * Validate the complete transformed PHP file and its header structure.
	 */
	private static function is_valid_transformed_plugin_file($original, $transformed, $header_map)
	{
		if (!is_string($transformed) || strpos($transformed, '<?php') !== 0) {
			return false;
		}

		if (
			substr_count($original, '<?php') !== substr_count($transformed, '<?php')
			|| substr_count($original, '?>') !== substr_count($transformed, '?>')
		) {
			return false;
		}

		foreach ($header_map as $field_config) {
			$pattern = '/^\s*\*\s*' . preg_quote($field_config['header'], '/') . ':\s*.+$/m';
			if (preg_match_all($pattern, $transformed) !== 1) {
				return false;
			}
		}

		try {
			$tokens = token_get_all($transformed, TOKEN_PARSE);
		} catch (ParseError $error) {
			return false;
		}

		return true;
	}

	/**
	 * Replace a plugin file from a same-directory temporary file.
	 */
	private static function atomically_replace_plugin_file($plugin_file, $content, $rename_file = null)
	{
		$directory = dirname($plugin_file);
		$temp_file = tempnam($directory, '.metasync-header-');
		if ($temp_file === false) {
			return false;
		}

		$permissions = fileperms($plugin_file);
		$written = file_put_contents($temp_file, $content, LOCK_EX);
		if ($written !== strlen($content)) {
			@wp_delete_file($temp_file);
			return false;
		}

		if ($permissions !== false) {
			@chmod($temp_file, $permissions & 0777); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- direct POSIX permission change; WP_Filesystem would prompt for FTP credentials on non-direct hosts
		}

		if ($rename_file === null) {
			$rename_file = static function ($source, $destination) {
				return @rename($source, $destination); // phpcs:ignore WordPress.WP.AlternativeFunctions.rename_rename -- atomic same-filesystem replace; WP_Filesystem::move() needs credential bootstrapping on non-direct hosts
			};
		}

		// The temporary file is on the same filesystem, so a successful rename is
		// atomic. On failure the original path has not been touched.
		if ($rename_file($temp_file, $plugin_file)) {
			return true;
		}

		@wp_delete_file($temp_file);

		return false;
	}
}

```
