PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
2.7.1 2.7.2 2.7.0 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 All 141 releases
metasync / metasync.php

metasync.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.1, at metasync.php

793 lines 30.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * This file is read by WordPress to generate the plugin information in the plugin
5 * admin area. This file also includes all of the dependencies used by the plugin,
6 * registers the activation and deactivation functions, and defines a function
7 * that starts the plugin.
8 *
9 * @package Search Atlas SEO
10 * @copyright Copyright (C) 2021-2025, Search Atlas Group - [email protected]
11 * @link https://searchatlas.com/
12 * @since 1.0.0
13 *
14 * @wordpress-plugin
15 * Plugin Name: Search Atlas SEO - OTTO AI SEO Automation for WordPress
16 * Plugin URI: https://searchatlas.com/
17 * 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.
18 * Version: 2.7.1
19 * Requires PHP: 8.1
20 * Author: Search Atlas
21 * Author URI: https://searchatlas.com
22 * License: GPL v3
23 * License URI: https://www.gnu.org/licenses/gpl-3.0.txt
24 * Text Domain: metasync
25 */
26
27 // If this file is called directly, abort.
28 if (!defined('WPINC')) {
29 die;
30 }
31
32 // Composer classmap autoloader — loads all plugin classes on demand.
33 require_once __DIR__ . '/vendor/autoload.php';
34
35 // Canonical sanitizer — required explicitly (not via the committed classmap)
36 // so it is guaranteed loadable everywhere canonicals are read or written.
37 require_once __DIR__ . '/includes/class-metasync-canonical-sanitizer.php';
38
39 // Editor-settings feature flags — required explicitly for the same reason: they
40 // gate output in the OTTO buffer and in third-party filter callbacks, both of
41 // which can run before the classmap has resolved anything else.
42 require_once __DIR__ . '/includes/class-metasync-feature-flags.php';
43 Metasync_Feature_Flags::register_invalidation();
44
45 // Consent gate and write-once backup for third-party SEO storage — required
46 // explicitly for the same reason: every writer into Yoast / Rank Math / AIOSEO
47 // consults it, and a class that fails to load would read as "no consent" and
48 // silently stop those writes.
49 require_once __DIR__ . '/includes/class-metasync-seo-backup.php';
50
51 // Loaded beside the backup class because it is the only reader of what that
52 // class writes. Restore is inert until something calls it, so loading it early
53 // costs nothing and keeps the pair together.
54 require_once __DIR__ . '/includes/class-metasync-seo-restore.php';
55
56 /**
57 * Currently plugin version.
58 * Start at version 1.0.0 and use SemVer - https://semver.org
59 * Rename this for your plugin and update it as you release new versions.
60 */
61 $metasync_version = '2.7.1';
62 define('METASYNC_VERSION', preg_match('/^\d+\.\d+/', $metasync_version) ? $metasync_version : '9.9.9');
63 /**
64 * Define the current required php version
65 * This will be used to validate whether the user can install the plugin or not
66 */
67 define('METASYNC_MIN_PHP', '8.1');
68
69 /**
70 * Define the current required php version
71 * This will be used to validate whether the user can install the plugin or not
72 */
73 define('METASYNC_MIN_WP', '5.2');
74
75 /**
76 * Telemetry Configuration Constants
77 * These replace the old database options for better security and consistency
78 */
79 define('METASYNC_SENTRY_PROJECT_ID', '4509950439849985');
80 define('METASYNC_SENTRY_ENVIRONMENT', 'production');
81 define('METASYNC_SENTRY_RELEASE', METASYNC_VERSION);
82 define('METASYNC_SENTRY_SAMPLE_RATE', 1.0);
83
84 /**
85 * GA4 Analytics Configuration
86 * Measurement ID for Google Analytics 4 event tracking (format: G-XXXXXXXX)
87 *
88 * IMPORTANT: This constant is REQUIRED for GA4 tracking to function.
89 * If not defined or empty, all GA4 analytics tracking will be disabled.
90 *
91 * GA4_API_SECRET is required for server-side Measurement Protocol events
92 * (Content Genius, OTTO optimization). Generate it in GA4:
93 * Admin → Data Streams → (stream) → Measurement Protocol → Create
94 */
95 define('METASYNC_GA4_MEASUREMENT_ID', 'G-SBLWW1EMTJ');
96 define('METASYNC_GA4_API_SECRET', 'nMGs22mxQ3qVUy-aInqfZA');
97
98 /**
99 * Define whether to show the plugin status in WordPress admin top navigation bar
100 * Set to false to hide the status indicator
101 */
102 define('METASYNC_SHOW_ADMIN_BAR_STATUS', true);
103
104 /**
105 * Sanitize POST/GET/REQUEST data recursively
106 *
107 * @param array $data Data to sanitize
108 * @return array Sanitized data
109 */
110 if (!function_exists('metasync_sanitize_input_array')) {
111 function metasync_sanitize_input_array($data) {
112 if (!is_array($data)) {
113 return sanitize_text_field($data);
114 }
115
116 $sanitized = [];
117 foreach ($data as $key => $value) {
118 if (is_array($value)) {
119 $sanitized[$key] = metasync_sanitize_input_array($value);
120 } else {
121 // Check if it's a URL
122 if (filter_var($value, FILTER_VALIDATE_URL)) {
123 $sanitized[$key] = esc_url_raw($value);
124 } else {
125 $sanitized[$key] = sanitize_text_field($value);
126 }
127 }
128 }
129 return $sanitized;
130 }
131 }
132
133 // Skip heavy MetaSync init on admin-ajax requests that don't target our own actions (Sentry issue 7441226449).
134 if (!function_exists('metasync_is_non_metasync_admin_ajax')) {
135 function metasync_is_non_metasync_admin_ajax() {
136 static $result = null;
137 if ($result !== null) {
138 return $result;
139 }
140 // The MCP bridge intentionally boots WordPress with DOING_AJAX set, but it
141 // is a MetaSync entry point rather than an unrelated admin AJAX request.
142 // The constant is only ever defined as true by the bridge bootstraps.
143 if (defined('METASYNC_MCP_BRIDGE')) {
144 return ($result = false);
145 }
146 if (!defined('DOING_AJAX') || !DOING_AJAX) {
147 return ($result = false);
148 }
149 $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action'])) // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only action-name prefix check during boot
150 : (isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : ''); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only action-name prefix check during boot
151 if ($action === '') {
152 return ($result = true);
153 }
154 if (strpos($action, 'metasync_') === 0 || strpos($action, 'meta_sync_') === 0 || $action === 'sample-permalink') {
155 return ($result = false);
156 }
157 return ($result = true);
158 }
159 }
160
161 // Lazy-load guard: only initialise the MCP server when the request is actually targeting
162 // the MCP REST route (or its sibling SEO-inventory route, which depends on $metasync_mcp_server
163 // for its permission callback). Saves ~2-5 MB / 10-50 ms on the 99.9% of requests that never
164 // touch MCP.
165 if (!function_exists('metasync_is_mcp_rest_request')) {
166 function metasync_is_mcp_rest_request(): bool {
167 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
168 if ($uri === '') {
169 return false;
170 }
171 $prefix = function_exists('rest_get_url_prefix') ? rest_get_url_prefix() : 'wp-json';
172 $needles = [
173 '/' . $prefix . '/metasync/v1/mcp',
174 '/' . $prefix . '/metasync/v1/seo-inventory',
175 'rest_route=/metasync/v1/mcp',
176 'rest_route=/metasync/v1/seo-inventory',
177 'rest_route=%2Fmetasync%2Fv1%2Fmcp',
178 'rest_route=%2Fmetasync%2Fv1%2Fseo-inventory',
179 ];
180 foreach ($needles as $needle) {
181 if (stripos($uri, $needle) !== false) {
182 return true;
183 }
184 }
185 return false;
186 }
187 }
188
189 // Phase 2 — these files have file-level side effects (add_action outside class body)
190 // and must remain as explicit require_once until refactored.
191 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-api-backoff-rest.php';
192 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-data-store.php';
193 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-error-logger.php';
194
195 // Shared helpers (custom/LPS page detection + query exclusion) — loaded in all
196 // request contexts (admin, REST, MCP, AJAX) so every SEO surface uses one rule.
197 require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-helpers.php';
198
199 // Shared "results per page" selector helper used by the Redirections, 404
200 // Monitor, Changes Log and Media Library admin list tables. Loaded early so
201 // the helper class is available before any list table instantiates.
202 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-per-page-helper.php';
203
204 /**
205 * Include the Otto Pixel Php Code
206 */
207 if (!metasync_is_non_metasync_admin_ajax()) {
208 require_once plugin_dir_path( __FILE__ ) . '/otto/otto_pixel.php';
209 require_once plugin_dir_path( __FILE__ ) . '/otto/class-metasync-otto-clone-meta-cleaner.php';
210 Metasync_Otto_Clone_Meta_Cleaner::register();
211 }
212
213
214 /**
215 * Initialize OTTO Persistence Settings (registers REST API endpoints)
216 */
217 add_action('init', function() {
218 if (metasync_is_non_metasync_admin_ajax()) {
219 return;
220 }
221 Metasync_Otto_Persistence_Settings::init();
222 }, 5);
223
224 /**
225 * The code that runs during plugin activation.
226 * This action is documented in includes/class-metasync-activator.php
227 */
228 function activate_metasync()
229 {
230 # Include WordPress plugin functions to access plugin metadata
231 if (!function_exists('get_plugin_data')) {
232 require_once ABSPATH . 'wp-admin/includes/plugin.php';
233 }
234
235 # Get plugin data
236 $plugin_data = get_plugin_data(__FILE__);
237
238 # Get the plugin name
239 $plugin_name = $plugin_data['Name'];
240
241 # Get the current WordPress
242 global $wp_version;
243
244 # Get the current php version
245 $php_version = PHP_VERSION;
246
247 # Check for incompatible WordPress version
248 if (version_compare($wp_version, METASYNC_MIN_WP, '<')) {
249
250 #show error
251 wp_die(
252
253 #craft the message
254 sprintf(
255 '%s requires WordPress version %s or later. You are currently using version %s. Please update WordPress to activate this plugin.',
256 esc_html($plugin_name),
257 METASYNC_MIN_WP,
258 esc_html($wp_version)
259 ),
260
261 #the plugin title as page title
262 esc_html($plugin_name).'Plugin Activation Error',
263
264 #include the back link
265 ['back_link' => true]
266 );
267 }
268
269 # Check for incompatible PHP version
270 if (version_compare($php_version, METASYNC_MIN_PHP, '<')) {
271
272 #show error message
273 wp_die(
274
275 #craft the message
276 sprintf(
277 '%s requires PHP version %s or later. You are currently using version %s. Please update PHP to activate this plugin.',
278 esc_html($plugin_name),
279 METASYNC_MIN_PHP,
280 esc_html($php_version)
281 ),
282
283 #the plugin title as page title
284 esc_html($plugin_name).'Plugin Activation Error',
285
286 #include the back link
287 ['back_link' => true]
288 );
289 }
290
291 Metasync_Activator::activate();
292 // class name is changed at class-db-migrations.php
293 MetaSync_DBMigration::activation();
294
295 // Set initial version
296 update_option('metasync_version', METASYNC_VERSION);
297
298 // Record activation timestamp so Divi CSS fix transients
299 // auto-invalidate after plugin deactivate/reactivate cycles.
300 update_option('metasync_activated_at', (string) time());
301
302 // Clear all cache plugins to ensure fresh start
303 Metasync_Cache_Purge::purge_all('plugin_activation');
304
305 // Migrate physical sitemap files on activation.
306 metasync_migrate_physical_sitemaps();
307 }
308
309 // Log-sync removed - error monitoring now handled by Sentry
310 // require_once plugin_dir_path(__FILE__) . 'log-sync/log-sync.php';
311
312 /**
313 * Initialize telemetry system for error monitoring and Sentry integration
314 */
315 if (!metasync_is_non_metasync_admin_ajax()) {
316 require_once plugin_dir_path(__FILE__) . 'telemetry/telemetry-init.php';
317 }
318
319 /**
320 * The code that runs during plugin deactivation.
321 * This action is documented in includes/class-metasync-deactivator.php
322 */
323 function deactivate_metasync()
324 {
325 Metasync_Deactivator::deactivate();
326 // class name is changed at class-db-migrations.php
327 MetaSync_DBMigration::deactivation();
328
329 // Clear news/video sitemap caches
330 delete_transient('metasync_vsm_' . md5('news-sitemap.xml'));
331 delete_transient('metasync_vsm_' . md5('video-sitemap.xml'));
332 delete_option('metasync_sitemap_virtual_index');
333
334 // Clear OTTO JS detection cache so re-activation gets a fresh check
335 delete_transient('metasync_otto_js_detected');
336 }
337
338 register_activation_hook(__FILE__, 'activate_metasync');
339 register_deactivation_hook(__FILE__, 'deactivate_metasync');
340
341 /**
342 * Migrate physical sitemap .xml files into transients and delete them.
343 *
344 * Physical files in ABSPATH cause nginx/Plesk to 403 before WordPress can
345 * serve them. Called from both the activation hook and the version-gate
346 * migration so it covers fresh activations and auto-updates.
347 */
348 function metasync_migrate_physical_sitemaps()
349 {
350 update_option('metasync_sitemap_virtual_mode', true, false);
351
352 $candidates = [];
353 $globbed = glob(ABSPATH . 'sitemap*.xml');
354 if (is_array($globbed)) {
355 foreach ($globbed as $file) {
356 $basename = basename($file);
357 if ($basename === 'sitemap_index.xml' || preg_match('/^sitemap\d*\.xml$/', $basename)) {
358 $candidates[] = $file;
359 }
360 }
361 }
362 foreach (['news-sitemap.xml', 'video-sitemap.xml'] as $extra) {
363 $path = ABSPATH . $extra;
364 if (file_exists($path)) {
365 $candidates[] = $path;
366 }
367 }
368
369 if (empty($candidates)) {
370 return;
371 }
372
373 // Ownership gate: other plugins (Yoast and friends) write files with the
374 // exact same names (sitemap_index.xml, sitemap1.xml, ...), so a name match
375 // alone must never authorize deleting a physical file. A file is only
376 // migrated when this plugin left evidence it wrote it.
377 $tracked_names = [];
378 $tracked_files = get_option('metasync_sitemap_files', []);
379 if (is_array($tracked_files)) {
380 foreach ($tracked_files as $entry) {
381 $name = is_array($entry) && isset($entry['filename']) ? $entry['filename'] : $entry;
382 if (is_string($name) && '' !== $name) {
383 $tracked_names[] = $name;
384 }
385 }
386 }
387 $virtual_index = get_option('metasync_sitemap_virtual_index', []);
388 if (!is_array($virtual_index)) {
389 $virtual_index = [];
390 }
391 $parked_names = array_keys($virtual_index);
392 $generator_ran = !empty($tracked_names) && false !== get_option('metasync_sitemap_last_generated');
393
394 $ours = [];
395 $metasync_files_found = false;
396 foreach ($candidates as $file) {
397 $bn = basename($file);
398
399 // A parked name proves MetaSync owned the file at an earlier point, but
400 // an expired transient no longer proves that a file currently on disk
401 // is ours. A different plugin may have recreated the same basename.
402 if (in_array($bn, $parked_names, true)) {
403 if (false !== get_transient('metasync_vsm_' . md5($bn))) {
404 continue;
405 }
406 continue;
407 }
408
409 // Chunk, index, news, and video files are migratable only when their
410 // exact basename is recorded by MetaSync's generator/configuration.
411 if (in_array($bn, $tracked_names, true) && $generator_ran) {
412 $ours[] = $file;
413 $metasync_files_found = true;
414 continue;
415 }
416
417 if ($bn === 'news-sitemap.xml' && null !== get_option('metasync_news_sitemap_settings', null)) {
418 $ours[] = $file;
419 $metasync_files_found = true;
420 continue;
421 }
422 if ($bn === 'video-sitemap.xml' && null !== get_option('metasync_video_sitemap_settings', null)) {
423 $ours[] = $file;
424 $metasync_files_found = true;
425 }
426 }
427
428 // Do not infer ownership of sitemap_index.xml from another sitemap: Yoast,
429 // Rank Math, and other SEO plugins commonly use the same root filename.
430 // It must be explicitly recorded in MetaSync's tracked file list.
431 $ours = array_values(array_unique($ours));
432
433 if (empty($ours)) {
434 return;
435 }
436
437 $migrated_files = [];
438
439 foreach ($ours as $file) {
440 $bn = basename($file);
441 $content = @file_get_contents($file);
442 if (false !== $content) {
443 $tkey = 'metasync_vsm_' . md5($bn);
444 // The news sitemap's entries are only valid inside Google News'
445 // 48-hour window, so it must not be migrated in under a 30-day
446 // TTL — that would re-introduce the staleness the generator's
447 // own bounded TTL exists to prevent. Regenerate-on-miss rebuilds
448 // it with a fresh date_query when this expires.
449 $ttl = ('news-sitemap.xml' === $bn) ? (int) (DAY_IN_SECONDS / 4) : 30 * DAY_IN_SECONDS;
450 set_transient($tkey, $content, $ttl);
451 if (false !== get_transient($tkey)) {
452 // The parked transient doubles as a 30-day backup of the
453 // content before the file is removed from disk.
454 @wp_delete_file($file);
455 $virtual_index[$bn] = $tkey;
456 if ($bn !== 'sitemap_index.xml' && $bn !== 'news-sitemap.xml' && $bn !== 'video-sitemap.xml') {
457 $migrated_files[] = [
458 'filename' => $bn,
459 'url' => home_url('/' . $bn),
460 'lastmod' => current_time('mysql', true),
461 ];
462 }
463 }
464 }
465 }
466
467 update_option('metasync_sitemap_virtual_index', $virtual_index, false);
468
469 if (!empty($migrated_files)) {
470 update_option('metasync_sitemap_files', $migrated_files);
471 update_option('metasync_sitemap_last_generated', current_time('mysql'));
472 }
473 }
474
475 /**
476 * Check for plugin updates and run migrations if needed
477 */
478 function check_metasync_updates()
479 {
480 static $checked = false;
481 if ($checked) return;
482 $checked = true;
483
484 $current_version = get_option('metasync_version', '0.0.0');
485 $plugin_version = METASYNC_VERSION;
486
487 // If versions don't match, run migration. Dev builds store the 9.9.9
488 // placeholder version, which compares greater than every real release —
489 // once stored, a plain "<" comparison never fires again and migrations
490 // are silently skipped forever. Also enter when the stored version is the
491 // placeholder but the running version is a real release, so the migration
492 // runner's dev-build escape hatch becomes reachable and the stored version
493 // is corrected to the real release.
494 if (version_compare($current_version, $plugin_version, '<')
495 || ('9.9.9' === $current_version && '9.9.9' !== $plugin_version)) {
496 // Import whitelabel settings only if the JSON file is new or changed
497 // (prevents overwriting admin UI changes on every version check)
498 Metasync_Activator::check_whitelabel_settings_update();
499
500 // Run version-specific migrations first
501 MetaSync_DBMigration::run_version_migrations($current_version, $plugin_version);
502
503 // Migration for v2.7.0+: Remove AI Agent, switch to plugin auth token, make MCP always-on
504 if (version_compare($current_version, '2.7.0', '<')) {
505 // Remove old MCP API key option
506 delete_option('metasync_mcp_api_key');
507
508 // Remove MCP enabled/disabled toggle option (MCP is now always enabled)
509 delete_option('metasync_mcp_enabled');
510
511 // Remove AI Agent settings (AI Agent has been removed)
512 delete_option('metasync_ai_agent_mcp_config');
513 delete_option('metasync_ai_agent_ai_config');
514 delete_option('metasync_ai_agent_enabled');
515
516 // Ensure plugin auth token exists
517 $options = get_option('metasync_options', []);
518 if (empty($options['general']['apikey'])) {
519 $options['general']['apikey'] = wp_generate_password(32, false, false);
520 update_option('metasync_options', $options);
521 }
522 }
523
524 // One-time purge of stale OTTO SEO cron backlog.
525 // Prior versions could accumulate thousands of metasync_process_seo_job and
526 // metasync_process_otto_crawl_url_job events due to unbounded rescheduling.
527 // Clear the backlog once on update; the new code prevents re-accumulation.
528 if (!get_option('metasync_wp299_cron_cleanup_done')) {
529 wp_unschedule_hook('metasync_process_seo_job');
530 wp_unschedule_hook('metasync_process_otto_crawl_url_job');
531 wp_unschedule_hook('metasync_process_otto_batch_cache_job');
532 update_option('metasync_wp299_cron_cleanup_done', true, false);
533 }
534
535 // One-time cleanup of canonical values corrupted to the literal
536 // "Array" (emitted as http://Array once the 2.6.16 canonical filters
537 // started reading them). The sanitizer prevents new corruption; this
538 // repairs the rows already in the database. Cache purge below pushes
539 // the clean pages live. Claimed via add_option() — it fails if the row
540 // already exists, so concurrent requests can't run the cleanup twice,
541 // and the claim lands BEFORE the work: everything inside is idempotent
542 // and the read-side sanitizer already protects output if a run is
543 // interrupted.
544 if (false === get_option('metasync_canonical_cleanup_done')
545 && add_option('metasync_canonical_cleanup_done', 'running', '', false)) {
546 MetaSync_DBMigration::cleanup_corrupted_canonicals();
547 update_option('metasync_canonical_cleanup_done', 'done', false);
548 }
549
550 // One-time repair of Local Business logos corrupted to "http://<id>".
551 // The sanitizer fix stopped new corruption; this restores the
552 // attachment ID on sites that saved a logo before it. The corrupted
553 // value encodes the original ID exactly, so the rewrite is lossless.
554 // Claimed via add_option() — it fails when the row already exists, so
555 // concurrent requests can't run the repair twice, and the claim lands
556 // BEFORE the work: the repair itself is idempotent, and the read-side
557 // normalisation in the schema output and admin preview already
558 // protects output if a run is interrupted.
559 if (false === get_option('metasync_local_seo_logo_repair_done')
560 && add_option('metasync_local_seo_logo_repair_done', 'running', '', false)) {
561 MetaSync_DBMigration::repair_corrupted_local_seo_logo();
562 update_option('metasync_local_seo_logo_repair_done', 'done', false);
563 }
564
565 // Migrate physical sitemap files on version update.
566 metasync_migrate_physical_sitemaps();
567
568 // Run full migration to ensure all tables are up to date
569 MetaSync_DBMigration::run_migrations();
570
571 // Update stored version
572 update_option('metasync_version', $plugin_version);
573
574 // Clear all cache plugins after update
575 Metasync_Cache_Purge::purge_all('plugin_update');
576
577 // Log the update
578 //error_log("MetaSync: Plugin updated from {$current_version} to {$plugin_version}. Database migration completed.");
579 }
580 }
581
582 // Hook into WordPress init to check for updates
583 add_action('init', 'check_metasync_updates', 1);
584
585 /**
586 * Handle whitelabel settings import after plugin is updated via WordPress admin
587 * This hook fires when plugins are installed/updated through the WordPress updater
588 *
589 * @param WP_Upgrader $upgrader WP_Upgrader instance
590 * @param array $hook_extra Extra arguments passed to hooked filters
591 */
592 function metasync_handle_plugin_upgrade($upgrader, $hook_extra)
593 {
594 // Only process plugin updates/installs
595 if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') {
596 return;
597 }
598
599 // By the time upgrader_process_complete fires, the upgrader may have
600 // deleted the directory this (old, still-in-memory) copy of the plugin was
601 // loaded from — e.g. when the installed dir name differs from the package's
602 // root dir ('metasync-develop' vs 'metasync'). The Composer classmap then
603 // points at files that no longer exist, so autoloading Metasync_Activator
604 // below would fatal. Bail instead; the whitelabel re-import runs on the next
605 // request via check_metasync_updates() once the new copy is active.
606 if (!class_exists('Metasync_Activator', false)) {
607 $activator = __DIR__ . '/includes/class-metasync-activator.php';
608 if (!is_file($activator)) {
609 return;
610 }
611 require_once $activator;
612 }
613
614 // Only process install and update actions
615 if (!isset($hook_extra['action']) || !in_array($hook_extra['action'], ['install', 'update'], true)) {
616 return;
617 }
618
619 $this_plugin = plugin_basename(__FILE__);
620 $this_plugin_slug = dirname($this_plugin); // Get 'metasync' from 'metasync/metasync.php'
621 $should_import = false;
622
623 // Handle bulk updates
624 if (isset($hook_extra['bulk']) && $hook_extra['bulk'] === true && isset($hook_extra['plugins'])) {
625 foreach ($hook_extra['plugins'] as $plugin) {
626 // Match by exact path OR by plugin slug/directory
627 if ($plugin === $this_plugin || dirname($plugin) === $this_plugin_slug) {
628 $should_import = true;
629 break;
630 }
631 }
632 }
633
634 // Handle single plugin update/install
635 if (isset($hook_extra['plugin'])) {
636 $plugin = $hook_extra['plugin'];
637 // Match by exact path OR by plugin slug/directory
638 if ($plugin === $this_plugin || dirname($plugin) === $this_plugin_slug) {
639 $should_import = true;
640 }
641 }
642
643 // SPECIAL CASE: When uploading plugin via "Add New > Upload Plugin",
644 // WordPress doesn't set the 'plugin' parameter during 'install' action.
645 // Check if we can get the plugin info from the upgrader result or whitelabel file exists.
646 if (!$should_import && $hook_extra['action'] === 'install') {
647 // Check upgrader result for destination
648 if (isset($upgrader->result) && isset($upgrader->result['destination'])) {
649 $destination = $upgrader->result['destination'];
650 // Check if destination contains our plugin slug
651 if (strpos($destination, $this_plugin_slug) !== false) {
652 $should_import = true;
653 }
654 }
655
656 // Fallback: Check if whitelabel file exists in our plugin directory
657 // This means our plugin was just installed/updated with whitelabel settings
658 if (!$should_import) {
659 $whitelabel_file = Metasync_Activator::get_whitelabel_settings_file();
660 if ($whitelabel_file !== false) {
661 $should_import = true;
662 }
663 }
664 }
665
666 if ($should_import) {
667 Metasync_Activator::check_whitelabel_settings_update(true);
668 }
669 }
670
671 // Hook into WordPress upgrader to detect plugin updates
672 add_action('upgrader_process_complete', 'metasync_handle_plugin_upgrade', 10, 2);
673
674 /**
675 * Fallback: Check for whitelabel file changes on admin pages
676 * This handles edge cases where upgrader_process_complete doesn't fire
677 * (e.g., FTP uploads, manual file replacements)
678 * Only checks once per admin request to minimize performance impact
679 */
680 function metasync_check_whitelabel_on_admin()
681 {
682 static $checked = false;
683 if ($checked || !current_user_can('manage_options')) {
684 return;
685 }
686 $checked = true;
687
688 require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
689
690 // Surface a persistently failing White Label import instead of
691 // retrying it silently on every admin request.
692 add_action('admin_notices', array('Metasync_Activator', 'render_whitelabel_import_failure_notice'));
693
694 Metasync_Activator::check_whitelabel_settings_update();
695 }
696
697 // Retry package imports on an authorized admin request when an update completed
698 // through the public version-check or stale-classmap fallback path.
699 add_action('admin_init', 'metasync_check_whitelabel_on_admin', 1);
700
701 // Include Media Optimization Module
702 if (!metasync_is_non_metasync_admin_ajax()) {
703 require_once plugin_dir_path(__FILE__) . 'media-optimization/media-optimization-loader.php';
704 }
705
706 // Include Code Minification & Delivery Module
707 if (!metasync_is_non_metasync_admin_ajax()) {
708 require_once plugin_dir_path(__FILE__) . 'code-minification/code-minification-loader.php';
709 }
710
711
712 function run_metasync()
713 {
714 $plugin = new Metasync();
715 $plugin->run();
716 }
717 run_metasync();
718
719 // MCP server bootstrap (server + tool registration) extracted to keep this entry file lean.
720 require_once plugin_dir_path( __FILE__ ) . 'includes/mcp-server-bootstrap.php';
721
722 /**
723 * Schedule a daily cron event to auto-purge Sync History records older than 90 days.
724 */
725 function metasync_schedule_sync_log_cleanup() {
726 if (!wp_next_scheduled('metasync_sync_log_daily_cleanup')) {
727 wp_schedule_event(time(), 'daily', 'metasync_sync_log_daily_cleanup');
728 }
729 }
730 add_action('wp', 'metasync_schedule_sync_log_cleanup');
731
732 /**
733 * Cron callback: delete Sync History records older than 90 days.
734 */
735 function metasync_sync_log_cleanup_handler() {
736 $sync_db = new Metasync_Sync_History_Database();
737 $sync_db->delete_older_than_days(90);
738 }
739 add_action('metasync_sync_log_daily_cleanup', 'metasync_sync_log_cleanup_handler');
740
741 /**
742 * Output DYO initialization flag to the frontend
743 * Makes window.__SA_DYO_INITIALIZED__ = true available in the DOM
744 * This indicates the Search Atlas plugin is active and initialized
745 */
746 function metasync_output_dyo_init_flag() {
747 echo '<script>window.__SA_DYO_INITIALIZED__=true;</script>' . "\n";
748 }
749 add_action('wp_head', 'metasync_output_dyo_init_flag', 1);
750
751 // Runtime feature initialisers (GA4, API backoff, review notice, JWT accessor, debug mode) extracted to keep this entry file lean.
752 require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-runtime-init.php';
753
754 /**
755 * Append a "Website Studio" post state to LPS-synced / MetaSync custom pages in
756 * the admin Pages list, so site owners can tell at a glance which pages are
757 * managed by Website Studio and shouldn't be hand-edited.
758 *
759 * Hooks WordPress core's display_post_states filter — the same mechanism that
760 * renders the grey inline tags like "— Front Page" / "— Draft" — so the label
761 * is native-styled and only appears next to relevant page titles, with no
762 * custom admin column.
763 *
764 * @param string[] $post_states Existing post-state labels keyed by slug.
765 * @param WP_Post $post The post being listed.
766 * @return string[] Possibly-augmented post states.
767 */
768 function metasync_add_lps_post_state($post_states, $post) {
769 // metasync_is_custom_or_lps_page() lives in otto/otto_pixel.php, which is NOT
770 // loaded on non-MetaSync admin-ajax requests (e.g. Quick Edit's inline-save,
771 // where this filter still fires), so guard against the undefined function.
772 if (!function_exists('metasync_is_custom_or_lps_page')) {
773 return $post_states;
774 }
775 if (!metasync_is_custom_or_lps_page($post->ID)) {
776 return $post_states;
777 }
778 $post_states['metasync_website_studio'] = __('Website Studio', 'metasync');
779 return $post_states;
780 }
781
782
783 /**
784 * Oxygen Builder Compatibility
785 * Auto re-signs [oxygen] dynamic-data shortcodes when their HMAC signatures
786 * are invalid (e.g. after design-set import or site migration).
787 * Runs once on admin_init; skips entirely when Oxygen is inactive.
788 */
789 if (is_admin()) {
790 add_action('admin_init', ['Metasync_Oxygen_Compat', 'maybe_resign_shortcodes'], 20);
791 add_filter('display_post_states', 'metasync_add_lps_post_state', 10, 2);
792 }
793