PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.24
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.24
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.24, at metasync.php

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