PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.22
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.22
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 2.5.24 All 137 releases
metasync / metasync.php

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

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