PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.26
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.26
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 / metasync.php

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

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