*/ class Metasync_Activator { /** * 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', ]; /** * 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(); // 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'); } // 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 * * @since 2.5.0 */ public static function check_whitelabel_settings_update() { $json_file = self::get_whitelabel_settings_file(); if ($json_file === false) { return; } // Get current file modification time and content hash $file_mtime = filemtime($json_file); $file_hash = md5_file($json_file); // 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) { // File has changed, import settings self::import_whitelabel_settings(); // Store new file info update_option('metasync_whitelabel_file_mtime', $file_mtime); update_option('metasync_whitelabel_file_hash', $file_hash); } } /** * Import whitelabel settings from JSON file if available * Checks for whitelabel-settings.json in the plugin directory or extracted zip * * This method is public to allow calling during both activation and plugin updates. * @since 2.5.0 */ public static function import_whitelabel_settings() { $json_file = self::get_whitelabel_settings_file(); if ($json_file === false) { return; } // Read JSON file $json_content = file_get_contents($json_file); if ($json_content === false) { return; } // Decode JSON $import_data = json_decode($json_content, true); if ($import_data === null || json_last_error() !== JSON_ERROR_NONE) { return; } // Validate import data structure if (!isset($import_data['whitelabel_settings']) || !is_array($import_data['whitelabel_settings'])) { return; } // 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; } } // 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 update_option('metasync_options', $options); // Update the plugin file headers so whitelabel shows even when deactivated self::update_plugin_file_headers($import_data); // Optionally delete the JSON file after successful import (uncomment if desired) // unlink($json_file); } /** * 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() { $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, ); self::update_plugin_file_headers($import_data); } /** * 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 = plugin_dir_path(dirname(__FILE__)) . 'metasync.php'; if (!file_exists($plugin_file) || !is_writable($plugin_file)) { return; } $content = file_get_contents($plugin_file); if ($content === false) { return; } $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: The Premier AI SEO Plugin for Instant Optimization', ), '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; foreach ($header_map as $setting_key => $field_config) { $header_field = $field_config['header']; // Use whitelabel value if set, otherwise restore default $new_value = !empty($general[$setting_key]) ? $general[$setting_key] : $field_config['default']; // 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($pattern, $content, $matches)) { // Only replace if the value actually differs from what's in the file if (trim($matches[2]) !== trim($new_value)) { // Escape both backslashes and $ signs for preg_replace replacement string $escaped_value = str_replace(array('\\', '$'), array('\\\\', '\\$'), $new_value); $content = preg_replace($pattern, '${1}' . $escaped_value, $content, 1); $modified = true; } } } if ($modified) { file_put_contents($plugin_file, $content); // Clear WordPress plugin cache so it reads the updated headers wp_cache_delete('plugins', 'plugins'); } } }