PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.21
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.21
2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 2.6.4 2.6.3 2.5.23 All 138 releases
metasync / includes / class-metasync-activator.php

class-metasync-activator.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.21, at includes/class-metasync-activator.php

436 lines 14.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Fired during plugin activation
5 *
6 * @link https://searchatlas.com
7 * @since 1.0.0
8 *
9 * @package Metasync
10 * @subpackage Metasync/includes
11 */
12
13 /**
14 * Fired during plugin activation.
15 *
16 * This class defines all code necessary to run during the plugin's activation.
17 *
18 * @since 1.0.0
19 * @package Metasync
20 * @subpackage Metasync/includes
21 * @author Engineering Team <support@searchatlas.com>
22 */
23 class Metasync_Activator
24 {
25
26 /**
27 * Canonical list of MetaSync custom WP-Cron hooks.
28 * Shared with Metasync_Deactivator so deactivation cleans up every scheduled hook.
29 *
30 * @since 2.5.x
31 * @var string[]
32 */
33 public static $cron_hooks = [
34 'metasync_sync_log_daily_cleanup',
35 'metasync_announce_cron',
36 'metasync_rate_limit_cleanup',
37 'metasync_heartbeat_cron_check',
38 'metasync_burst_heartbeat',
39 'metasync_check_debug_limits',
40 'metasync_cleanup_transients',
41 'metasync_hidden_post_check',
42 'metasync_otto_recheck_404_exclusions',
43 'metasync_db_cleanup',
44 'metasync_media_batch_optimize_cron',
45 'metasync_speed_cache_cleanup',
46 'metasync_process_seo_job',
47 'metasync_process_otto_crawl_url_job',
48 'metasync_process_otto_batch_cache_job',
49 ];
50
51 /**
52 * Short Description. (use period)
53 *
54 * Long Description.
55 *
56 * @since 1.0.0
57 */
58 public static function activate()
59 {
60 // WordPress core sitemap functionality is required
61 // if (wp_sitemaps_get_server()->sitemaps_enabled() == false) {
62 // add_filter('wp_sitemaps_enabled', '__return_true');
63 // }
64
65 // Generate Plugin Auth Token on first activation
66 self::ensure_plugin_auth_token();
67
68 // Import whitelabel settings only if the JSON file is new or changed
69 // (prevents overwriting admin UI changes on every deactivate/activate cycle)
70 self::check_whitelabel_settings_update();
71
72 // Pre-SSO announce: tell backend plugin is installed (PR4 - heartbeat reliability)
73 update_option('metasync_announce_attempt_count', 0);
74 self::send_announce_ping();
75 update_option('metasync_announce_attempt_count', 1);
76
77 // Schedule cron for announce pings 2-5 (every 10 minutes)
78 if (!wp_next_scheduled('metasync_announce_cron')) {
79 wp_schedule_event(time() + 10 * MINUTE_IN_SECONDS, 'metasync_every_10_minutes', 'metasync_announce_cron');
80 }
81
82 // Set first activation flag for setup wizard
83 if (!get_option('metasync_first_activation_time')) {
84 update_option('metasync_first_activation_time', current_time('mysql'));
85 update_option('metasync_show_wizard', true);
86 }
87
88 // Auto-disable WP core sitemap if a MetaSync sitemap already exists.
89 // Skip on fresh installs where no sitemap has been generated yet so the site
90 // doesn't end up with zero sitemaps.
91 if (get_option('metasync_sitemap_auto_update', false)
92 || !empty(get_option('metasync_sitemap_files', []))
93 || file_exists(ABSPATH . 'sitemap_index.xml')
94 ) {
95 update_option('metasync_disable_wp_sitemap', true);
96 }
97
98 // Soft flush only (no .htaccess rewrite). A hard flush calls
99 // save_mod_rewrite_rules(), which takes an exclusive flock() on the site's
100 // root .htaccess. On hosts whose cache/optimizer also holds that lock (e.g.
101 // SiteGround + SG Optimizer), the activation request blocks until
102 // max_execution_time and the host returns a 500. MetaSync's rewrite rules are
103 // registered via add_rewrite_rule (stored in the `rewrite_rules` option, NOT
104 // in .htaccess), so a soft flush is sufficient and never touches the file.
105 flush_rewrite_rules(false);
106 }
107
108 /**
109 * Send pre-SSO announce ping to backend (zero-trust; backend rate-limits).
110 * POST /api/wp-plugin-announce/ with url + plugin_version; optional X-Plugin-Token for deduplication.
111 * Callable from activation and from init (rate-limited) when no API key yet.
112 *
113 * @since 2.5.x
114 */
115 public static function send_announce_ping()
116 {
117 $base = 'https://ca.searchatlas.com';
118 if (class_exists('Metasync_Endpoint_Manager')) {
119 $base = Metasync_Endpoint_Manager::get_endpoint('CA_API_DOMAIN');
120 } elseif (class_exists('Metasync')) {
121 $base = Metasync::CA_API_DOMAIN;
122 }
123 $url = rtrim($base, '/') . '/api/wp-plugin-announce/';
124
125 $body = wp_json_encode([
126 'url' => get_home_url(),
127 'plugin_version' => defined('METASYNC_VERSION') ? METASYNC_VERSION : 'unknown',
128 ]);
129
130 $options = get_option('metasync_options', []);
131 $plugin_auth_token = $options['general']['apikey'] ?? '';
132 $headers = [
133 'Content-Type' => 'application/json',
134 ];
135 if (!empty($plugin_auth_token)) {
136 $headers['X-Plugin-Token'] = $plugin_auth_token;
137 }
138
139 wp_remote_post($url, [
140 'body' => $body,
141 'headers' => $headers,
142 'timeout' => 10,
143 'blocking' => false,
144 ]);
145 }
146
147 /**
148 * Ensure Plugin Auth Token exists
149 * Generates a unique Plugin Auth Token during plugin activation
150 */
151 private static function ensure_plugin_auth_token()
152 {
153 $options = get_option('metasync_options', []);
154
155 if (empty($options['general']['apikey'])) {
156 // Generate unique Plugin Auth Token (alphanumeric only)
157 $plugin_auth_token = wp_generate_password(32, false, false);
158
159 // Initialize options structure if needed
160 if (!isset($options['general'])) {
161 $options['general'] = [];
162 }
163
164 // Store Plugin Auth Token
165 $options['general']['apikey'] = $plugin_auth_token;
166 update_option('metasync_options', $options);
167 }
168 }
169
170 /**
171 * Get the path to the whitelabel settings JSON file
172 *
173 * @return string|false Path to the file if it exists, false otherwise
174 * @since 2.5.0
175 */
176 public static function get_whitelabel_settings_file()
177 {
178 $plugin_dir = plugin_dir_path(dirname(__FILE__));
179
180 // Check for whitelabel-settings.json in plugin root
181 $json_file = $plugin_dir . 'whitelabel-settings.json';
182
183 // Also check in a common extracted zip location (if zip was extracted)
184 if (!file_exists($json_file)) {
185 $json_file = $plugin_dir . 'metasync/whitelabel-settings.json';
186 }
187
188 if (!file_exists($json_file)) {
189 return false;
190 }
191
192 return $json_file;
193 }
194
195 /**
196 * Check if whitelabel settings file has been updated and import if needed
197 * This method should be called on init to detect plugin uploads/updates
198 *
199 * @since 2.5.0
200 */
201 public static function check_whitelabel_settings_update()
202 {
203 $json_file = self::get_whitelabel_settings_file();
204
205 if ($json_file === false) {
206 return;
207 }
208
209 // Get current file modification time and content hash
210 $file_mtime = filemtime($json_file);
211 $file_hash = md5_file($json_file);
212
213 // Get stored file info
214 $stored_mtime = get_option('metasync_whitelabel_file_mtime', 0);
215 $stored_hash = get_option('metasync_whitelabel_file_hash', '');
216
217 // Check if file has changed (either modification time or content)
218 if ($file_mtime > $stored_mtime || $file_hash !== $stored_hash) {
219 // File has changed, import settings
220 self::import_whitelabel_settings();
221
222 // Store new file info
223 update_option('metasync_whitelabel_file_mtime', $file_mtime);
224 update_option('metasync_whitelabel_file_hash', $file_hash);
225 }
226 }
227
228 /**
229 * Import whitelabel settings from JSON file if available
230 * Checks for whitelabel-settings.json in the plugin directory or extracted zip
231 *
232 * This method is public to allow calling during both activation and plugin updates.
233 * @since 2.5.0
234 */
235 public static function import_whitelabel_settings()
236 {
237 $json_file = self::get_whitelabel_settings_file();
238
239 if ($json_file === false) {
240 return;
241 }
242
243 // Read JSON file
244 $json_content = file_get_contents($json_file);
245 if ($json_content === false) {
246 return;
247 }
248
249 // Decode JSON
250 $import_data = json_decode($json_content, true);
251 if ($import_data === null || json_last_error() !== JSON_ERROR_NONE) {
252 return;
253 }
254
255 // Validate import data structure
256 if (!isset($import_data['whitelabel_settings']) || !is_array($import_data['whitelabel_settings'])) {
257 return;
258 }
259
260 // Get current options
261 $options = get_option('metasync_options', array());
262
263 // Import whitelabel settings
264 if (isset($import_data['whitelabel_settings'])) {
265 $whitelabel_settings = $import_data['whitelabel_settings'];
266
267 // Initialize whitelabel array if needed
268 if (!isset($options['whitelabel'])) {
269 $options['whitelabel'] = array();
270 }
271
272 // Merge imported settings with existing (imported settings take precedence)
273 $options['whitelabel'] = array_merge($options['whitelabel'], $whitelabel_settings);
274
275 // The settings password travels as plaintext in the export file so it
276 // can be imported on a site with different salts — encrypt it at rest.
277 if (!empty($options['whitelabel']['settings_password']) && is_string($options['whitelabel']['settings_password'])) {
278 $options['whitelabel']['settings_password'] = Metasync::encrypt_secret($options['whitelabel']['settings_password']);
279 }
280
281 // Update timestamp
282 $options['whitelabel']['updated_at'] = time();
283 $options['whitelabel']['imported_at'] = current_time('mysql');
284 }
285
286 // Import general settings related to whitelabel
287 if (isset($import_data['general_settings']) && is_array($import_data['general_settings'])) {
288 if (!isset($options['general'])) {
289 $options['general'] = array();
290 }
291
292 // Merge general settings
293 foreach ($import_data['general_settings'] as $key => $value) {
294 $options['general'][$key] = $value;
295 }
296 }
297
298 // Restore bundled icon: if the icon value is a __bundled_icon__{ext} marker,
299 // copy the bundled file from the plugin directory to uploads and update the URL.
300 $icon_value = $options['general']['white_label_plugin_menu_icon'] ?? '';
301 if (!empty($icon_value) && strpos($icon_value, '__bundled_icon__') === 0) {
302 $ext = substr($icon_value, strlen('__bundled_icon__'));
303 $ext = preg_replace('/[^a-z0-9]/', '', strtolower($ext)); // sanitize
304 $bundled_file = plugin_dir_path(dirname(__FILE__)) . 'whitelabel-icon.' . $ext;
305
306 if (file_exists($bundled_file) && in_array($ext, ['png', 'svg'], true)) {
307 $upload_dir = wp_upload_dir();
308 $dest_dir = $upload_dir['basedir'] . '/metasync';
309 if (!file_exists($dest_dir)) {
310 wp_mkdir_p($dest_dir);
311 }
312 $dest_file = $dest_dir . '/whitelabel-icon.' . $ext;
313 if (copy($bundled_file, $dest_file)) {
314 $options['general']['white_label_plugin_menu_icon'] = $upload_dir['baseurl'] . '/metasync/whitelabel-icon.' . $ext;
315 } else {
316 // Could not copy — clear the broken marker so default icon shows
317 $options['general']['white_label_plugin_menu_icon'] = '';
318 }
319 } else {
320 // Bundled file missing or unsupported extension — clear the marker
321 $options['general']['white_label_plugin_menu_icon'] = '';
322 }
323 }
324
325 // Save updated options
326 update_option('metasync_options', $options);
327
328 // Update the plugin file headers so whitelabel shows even when deactivated
329 self::update_plugin_file_headers($import_data);
330
331 // Optionally delete the JSON file after successful import (uncomment if desired)
332 // unlink($json_file);
333 }
334
335 /**
336 * Sync plugin file headers from the current saved options in the database.
337 * Call this after saving whitelabel settings via the admin UI to ensure
338 * the plugin file headers reflect the latest whitelabel values.
339 *
340 * @since 2.5.0
341 */
342 public static function sync_plugin_file_headers()
343 {
344 $options = get_option('metasync_options', array());
345 $general = $options['general'] ?? array();
346
347 // Build the import_data format expected by update_plugin_file_headers
348 $import_data = array(
349 'general_settings' => $general,
350 );
351
352 self::update_plugin_file_headers($import_data);
353 }
354
355 /**
356 * Update the main plugin file headers with whitelabel values
357 * WordPress reads plugin metadata directly from the file header comments,
358 * so modifying these ensures whitelabel shows even when the plugin is deactivated.
359 *
360 * @param array $import_data The imported whitelabel data
361 * @since 2.5.0
362 */
363 private static function update_plugin_file_headers($import_data)
364 {
365 $plugin_file = plugin_dir_path(dirname(__FILE__)) . 'metasync.php';
366
367 if (!file_exists($plugin_file) || !is_writable($plugin_file)) {
368 return;
369 }
370
371 $content = file_get_contents($plugin_file);
372 if ($content === false) {
373 return;
374 }
375
376 $general = $import_data['general_settings'] ?? array();
377
378 // Map of whitelabel setting keys to plugin header field names
379 // with default values to restore when whitelabel is cleared
380 $header_map = array(
381 'white_label_plugin_name' => array(
382 'header' => 'Plugin Name',
383 'default' => 'Search Atlas: The Premier AI SEO Plugin for Instant Optimization',
384 ),
385 'white_label_plugin_description' => array(
386 'header' => 'Description',
387 '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.',
388 ),
389 'white_label_plugin_author' => array(
390 'header' => 'Author',
391 'default' => 'Search Atlas',
392 ),
393 'white_label_plugin_author_uri' => array(
394 'header' => 'Author URI',
395 'default' => 'https://searchatlas.com',
396 ),
397 'white_label_plugin_uri' => array(
398 'header' => 'Plugin URI',
399 'default' => 'https://searchatlas.com/',
400 ),
401 );
402
403 $modified = false;
404
405 foreach ($header_map as $setting_key => $field_config) {
406 $header_field = $field_config['header'];
407
408 // Use whitelabel value if set, otherwise restore default
409 $new_value = !empty($general[$setting_key])
410 ? $general[$setting_key]
411 : $field_config['default'];
412
413 // Match the header line: " * Field Name: any value"
414 // Handles varying whitespace between field name and value
415 $pattern = '/^(\s*\*\s*' . preg_quote($header_field, '/') . ':\s*)(.+)$/m';
416
417 if (preg_match($pattern, $content, $matches)) {
418 // Only replace if the value actually differs from what's in the file
419 if (trim($matches[2]) !== trim($new_value)) {
420 // Escape both backslashes and $ signs for preg_replace replacement string
421 $escaped_value = str_replace(array('\\', '$'), array('\\\\', '\\$'), $new_value);
422 $content = preg_replace($pattern, '${1}' . $escaped_value, $content, 1);
423 $modified = true;
424 }
425 }
426 }
427
428 if ($modified) {
429 file_put_contents($plugin_file, $content);
430
431 // Clear WordPress plugin cache so it reads the updated headers
432 wp_cache_delete('plugins', 'plugins');
433 }
434 }
435 }
436