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
← All changes | metasync.php +305 -366 2.6.6 → 2.7.1 View file →
@@ -11,12 +11,13 @@
11 11 * @link https://searchatlas.com/
12 12 * @since 1.0.0
13 13 *
14 14 * @wordpress-plugin
15 - * Plugin Name: Search Atlas: The Premier AI SEO Plugin for Instant Optimization
15 + * Plugin Name: Search Atlas SEO - OTTO AI SEO Automation for WordPress
16 16 * Plugin URI: https://searchatlas.com/
17 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.6
18 + * Version: 2.7.1
19 + * Requires PHP: 8.1
19 20 * Author: Search Atlas
20 21 * Author URI: https://searchatlas.com
21 22 * License: GPL v3
22 23 * License URI: https://www.gnu.org/licenses/gpl-3.0.txt
@@ -30,20 +31,41 @@
30 31
31 32 // Composer classmap autoloader — loads all plugin classes on demand.
32 33 require_once __DIR__ . '/vendor/autoload.php';
33 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 +
34 56 /**
35 57 * Currently plugin version.
36 58 * Start at version 1.0.0 and use SemVer - https://semver.org
37 59 * Rename this for your plugin and update it as you release new versions.
38 60 */
39 -$metasync_version = '2.6.6';
61 +$metasync_version = '2.7.1';
40 62 define('METASYNC_VERSION', preg_match('/^\d+\.\d+/', $metasync_version) ? $metasync_version : '9.9.9');
41 63 /**
42 64 * Define the current required php version
43 65 * This will be used to validate whether the user can install the plugin or not
44 66 */
45 -define('METASYNC_MIN_PHP', '8.2');
67 +define('METASYNC_MIN_PHP', '8.1');
46 68
47 69 /**
48 70 * Define the current required php version
49 71 * This will be used to validate whether the user can install the plugin or not
@@ -107,9 +129,9 @@
107 129 return $sanitized;
108 130 }
109 131 }
110 132
111 -// Skip heavy MetaSync init on admin-ajax requests that don't target our own actions (Sentry issue 7441226449 / WP-227).
133 +// Skip heavy MetaSync init on admin-ajax requests that don't target our own actions (Sentry issue 7441226449).
112 134 if (!function_exists('metasync_is_non_metasync_admin_ajax')) {
113 135 function metasync_is_non_metasync_admin_ajax() {
114 136 static $result = null;
115 137 if ($result !== null) {
@@ -114,13 +136,19 @@
114 136 static $result = null;
115 137 if ($result !== null) {
116 138 return $result;
117 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 + }
118 146 if (!defined('DOING_AJAX') || !DOING_AJAX) {
119 147 return ($result = false);
120 148 }
121 - $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action']))
122 - : (isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : '');
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
123 151 if ($action === '') {
124 152 return ($result = true);
125 153 }
126 154 if (strpos($action, 'metasync_') === 0 || strpos($action, 'meta_sync_') === 0 || $action === 'sample-permalink') {
@@ -132,9 +160,9 @@
132 160
133 161 // Lazy-load guard: only initialise the MCP server when the request is actually targeting
134 162 // the MCP REST route (or its sibling SEO-inventory route, which depends on $metasync_mcp_server
135 163 // for its permission callback). Saves ~2-5 MB / 10-50 ms on the 99.9% of requests that never
136 -// touch MCP. See WP-255.
164 +// touch MCP.
137 165 if (!function_exists('metasync_is_mcp_rest_request')) {
138 166 function metasync_is_mcp_rest_request(): bool {
139 167 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
140 168 if ($uri === '') {
@@ -160,15 +188,27 @@
160 188
161 189 // Phase 2 — these files have file-level side effects (add_action outside class body)
162 190 // and must remain as explicit require_once until refactored.
163 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';
164 193 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-error-logger.php';
165 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 +
166 204 /**
167 205 * Include the Otto Pixel Php Code
168 206 */
169 207 if (!metasync_is_non_metasync_admin_ajax()) {
170 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();
171 211 }
172 212
173 213
174 214 /**
@@ -253,11 +293,18 @@
253 293 MetaSync_DBMigration::activation();
254 294
255 295 // Set initial version
256 296 update_option('metasync_version', METASYNC_VERSION);
257 -
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 +
258 302 // Clear all cache plugins to ensure fresh start
259 303 Metasync_Cache_Purge::purge_all('plugin_activation');
304 +
305 + // Migrate physical sitemap files on activation.
306 + metasync_migrate_physical_sitemaps();
260 307 }
261 308
262 309 // Log-sync removed - error monitoring now handled by Sentry
263 310 // require_once plugin_dir_path(__FILE__) . 'log-sync/log-sync.php';
@@ -278,14 +325,8 @@
278 325 Metasync_Deactivator::deactivate();
279 326 // class name is changed at class-db-migrations.php
280 327 MetaSync_DBMigration::deactivation();
281 328
282 - // Unschedule the sync log cleanup cron to avoid orphaned events.
283 - $timestamp = wp_next_scheduled('metasync_sync_log_daily_cleanup');
284 - if ($timestamp) {
285 - wp_unschedule_event($timestamp, 'metasync_sync_log_daily_cleanup');
286 - }
287 -
288 329 // Clear news/video sitemap caches
289 330 delete_transient('metasync_vsm_' . md5('news-sitemap.xml'));
290 331 delete_transient('metasync_vsm_' . md5('video-sitemap.xml'));
291 332 delete_option('metasync_sitemap_virtual_index');
@@ -297,8 +338,142 @@
297 338 register_activation_hook(__FILE__, 'activate_metasync');
298 339 register_deactivation_hook(__FILE__, 'deactivate_metasync');
299 340
300 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 +/**
301 476 * Check for plugin updates and run migrations if needed
302 477 */
303 478 function check_metasync_updates()
304 479 {
@@ -308,10 +483,17 @@
308 483
309 484 $current_version = get_option('metasync_version', '0.0.0');
310 485 $plugin_version = METASYNC_VERSION;
311 486
312 - // If versions don't match, run migration
313 - if (version_compare($current_version, $plugin_version, '<')) {
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)) {
314 496 // Import whitelabel settings only if the JSON file is new or changed
315 497 // (prevents overwriting admin UI changes on every version check)
316 498 Metasync_Activator::check_whitelabel_settings_update();
317 499
@@ -338,8 +520,52 @@
338 520 update_option('metasync_options', $options);
339 521 }
340 522 }
341 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 +
342 568 // Run full migration to ensure all tables are up to date
343 569 MetaSync_DBMigration::run_migrations();
344 570
345 571 // Update stored version
@@ -369,8 +595,23 @@
369 595 if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') {
370 596 return;
371 597 }
372 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 +
373 614 // Only process install and update actions
374 615 if (!isset($hook_extra['action']) || !in_array($hook_extra['action'], ['install', 'update'], true)) {
375 616 return;
376 617 }
@@ -422,9 +663,9 @@
422 663 }
423 664 }
424 665
425 666 if ($should_import) {
426 - Metasync_Activator::check_whitelabel_settings_update();
667 + Metasync_Activator::check_whitelabel_settings_update(true);
427 668 }
428 669 }
429 670
430 671 // Hook into WordPress upgrader to detect plugin updates
@@ -433,26 +674,31 @@
433 674 /**
434 675 * Fallback: Check for whitelabel file changes on admin pages
435 676 * This handles edge cases where upgrader_process_complete doesn't fire
436 677 * (e.g., FTP uploads, manual file replacements)
437 - * Only checks once per admin session to minimize performance impact
678 + * Only checks once per admin request to minimize performance impact
438 679 */
439 -// function metasync_check_whitelabel_on_admin()
440 -// {
441 -// // Only check once per admin session to avoid overhead
442 -// static $checked = false;
443 -// if ($checked) {
444 -// return;
445 -// }
446 -// $checked = true;
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;
447 687
448 -// require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
449 -// Metasync_Activator::check_whitelabel_settings_update();
450 -// }
688 + require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
451 689
452 -// Check for whitelabel changes on admin pages (fallback for edge cases)
453 -// add_action('admin_init', 'metasync_check_whitelabel_on_admin', 1);
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'));
454 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 +
455 701 // Include Media Optimization Module
456 702 if (!metasync_is_non_metasync_admin_ajax()) {
457 703 require_once plugin_dir_path(__FILE__) . 'media-optimization/media-optimization-loader.php';
458 704 }
@@ -469,276 +715,12 @@
469 715 $plugin->run();
470 716 }
471 717 run_metasync();
472 718
473 -/**
474 - * Initialize WordPress MCP Server
475 - *
476 - * Safely register a single MCP tool class, logging failures without aborting
477 - * subsequent registrations. Extracted as a named function so both production
478 - * code and unit tests exercise the same logic (WP-265).
479 - *
480 - * @param object $server The MCP server instance (must have register_tool()).
481 - * @param string $class_name Fully-qualified tool class name.
482 - */
483 -function metasync_safe_register_mcp_tool($server, $class_name) {
484 - static $logged_missing = [];
485 - if (!class_exists($class_name)) {
486 - if (empty($logged_missing[$class_name])) {
487 - error_log('MetaSync MCP: ' . $class_name . ' class not found — skipping registration. Run composer dump-autoload to regenerate the classmap.');
488 - $logged_missing[$class_name] = true;
489 - }
490 - return;
491 - }
492 - try {
493 - $server->register_tool(new $class_name());
494 - } catch (\Throwable $e) {
495 - error_log('MetaSync MCP: Failed to register tool ' . $class_name . ': ' . $e->getMessage());
496 - }
497 -}
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';
498 721
499 722 /**
500 - * Creates and configures the MCP server instance,
501 - * registers all available MCP tools for WordPress operations.
502 - * Hooked to 'init' for proper WordPress lifecycle integration.
503 - */
504 -function metasync_init_mcp_server() {
505 - if (metasync_is_non_metasync_admin_ajax()) {
506 - return;
507 - }
508 - // Skip the heavy MCP boot (server + sync logger + REST inventory + 100+ tool objects)
509 - // unless this request is actually targeting the MCP REST route or was loaded via
510 - // the stdio/HTTP bridge. WP-255.
511 - if (!metasync_is_mcp_rest_request() && !defined('METASYNC_MCP_BRIDGE')) {
512 - return;
513 - }
514 - // Initialize MCP server
515 - global $metasync_mcp_server;
516 -
517 - try {
518 - $metasync_mcp_server = new Metasync_MCP_Server();
519 -
520 - // Attach MCP sync logger so all write tool calls are recorded in Sync History.
521 - new Metasync_MCP_Sync_Logger();
522 -
523 - // SEO Inventory: shared builder + standalone REST endpoint (WP-135)
524 - new Metasync_REST_SEO_Inventory();
525 - } catch (\Throwable $e) {
526 - error_log('MCP Server Initialization Error: ' . $e->getMessage());
527 - return;
528 - }
529 -
530 - // Helper: register a single tool, logging failures without aborting subsequent registrations
531 - $safe_register = function($class_name) use ($metasync_mcp_server) {
532 - metasync_safe_register_mcp_tool($metasync_mcp_server, $class_name);
533 - };
534 -
535 - // Register MCP Tools (Total: 92 existing + 8 new = 100 tools total!)
536 - // NEW in v2.8.0: +4 Taxonomy Meta tools, +4 Bulk Alt Text tools
537 -
538 - // Post Meta Operations (4 tools)
539 - $safe_register('MCP_Tool_Update_Post_Meta');
540 - $safe_register('MCP_Tool_Get_Post_Meta');
541 - $safe_register('MCP_Tool_Get_SEO_Meta');
542 - $safe_register('MCP_Tool_Get_Hreflang_Links');
543 -
544 - // Post Operations (4 tools)
545 - $safe_register('MCP_Tool_Get_Post');
546 - $safe_register('MCP_Tool_Get_Post_By_URL');
547 - $safe_register('MCP_Tool_List_Posts');
548 - $safe_register('MCP_Tool_Update_Post');
549 - $safe_register('MCP_Tool_Get_Post_Types');
550 -
551 - // SEO Analysis (3 tools)
552 - $safe_register('MCP_Tool_Analyze_SEO');
553 - $safe_register('MCP_Tool_Check_Indexability');
554 - $safe_register('MCP_Tool_SEO_Health_Report');
555 -
556 - // Search Operations (3 tools)
557 - $safe_register('MCP_Tool_Search_Posts');
558 - $safe_register('MCP_Tool_Search_By_Keyword');
559 - $safe_register('MCP_Tool_Find_Missing_Meta');
560 -
561 - // Redirect Management (5 tools)
562 - $safe_register('MCP_Tool_Create_Redirect');
563 - $safe_register('MCP_Tool_List_Redirects');
564 - $safe_register('MCP_Tool_Delete_Redirect');
565 - $safe_register('MCP_Tool_Update_Redirect');
566 - $safe_register('MCP_Tool_Check_Redirects_Health');
567 -
568 - // 404 Error Monitoring (5 tools)
569 - $safe_register('MCP_Tool_List_404_Errors');
570 - $safe_register('MCP_Tool_Get_404_Stats');
571 - $safe_register('MCP_Tool_Delete_404_Error');
572 - $safe_register('MCP_Tool_Clear_404_Errors');
573 - $safe_register('MCP_Tool_Create_Redirect_From_404');
574 -
575 - // Robots.txt & Sitemap Management (11 tools - 7 existing + 4 new)
576 - $safe_register('MCP_Tool_Get_Robots_Txt');
577 - $safe_register('MCP_Tool_Update_Robots_Txt');
578 - $safe_register('MCP_Tool_Get_Sitemap_Status');
579 - $safe_register('MCP_Tool_Regenerate_Sitemap');
580 - $safe_register('MCP_Tool_Exclude_From_Sitemap');
581 - $safe_register('MCP_Tool_Add_Robots_Rule');
582 - $safe_register('MCP_Tool_Remove_Robots_Rule');
583 - $safe_register('MCP_Tool_Parse_Robots_Txt');
584 - $safe_register('MCP_Tool_Validate_Robots_Txt');
585 - $safe_register('MCP_Tool_Get_News_Sitemap');
586 - $safe_register('MCP_Tool_Get_Video_Sitemap');
587 -
588 - // Plugin Settings Management (4 tools)
589 - $safe_register('MCP_Tool_Get_Plugin_Settings');
590 - $safe_register('MCP_Tool_Update_Plugin_Settings');
591 - $safe_register('MCP_Tool_List_Plugin_Settings_Schema');
592 - $safe_register('MCP_Tool_Get_MCP_Settings');
593 -
594 - // Schema Markup Management (7 tools)
595 - $safe_register('MCP_Tool_Get_Schema_Markup');
596 - $safe_register('MCP_Tool_Update_Schema_Markup');
597 - $safe_register('MCP_Tool_Add_Schema_Type');
598 - $safe_register('MCP_Tool_Remove_Schema_Type');
599 - $safe_register('MCP_Tool_Validate_Schema');
600 - $safe_register('MCP_Tool_Get_Schema_Content');
601 - $safe_register('MCP_Tool_Set_Schema_Content');
602 -
603 - // Google Instant Index (6 tools)
604 - $safe_register('MCP_Tool_Instant_Index_Update');
605 - $safe_register('MCP_Tool_Instant_Index_Delete');
606 - $safe_register('MCP_Tool_Instant_Index_Status');
607 - $safe_register('MCP_Tool_Instant_Index_Bulk_Update');
608 - $safe_register('MCP_Tool_Get_Instant_Index_Settings');
609 - $safe_register('MCP_Tool_Update_Instant_Index_Settings');
610 -
611 - // Custom HTML Pages (5 tools)
612 - $safe_register('MCP_Tool_Create_Custom_Page');
613 - $safe_register('MCP_Tool_Get_Custom_Page');
614 - $safe_register('MCP_Tool_List_Custom_Pages');
615 - $safe_register('MCP_Tool_Update_Custom_Page');
616 - $safe_register('MCP_Tool_Delete_Custom_Page');
617 -
618 - // HTML to Builder Converter (3 tools)
619 - $safe_register('MCP_Tool_Convert_HTML_To_Builder');
620 - $safe_register('MCP_Tool_Create_Builder_Page_From_HTML');
621 - $safe_register('MCP_Tool_Convert_Custom_Page_To_Builder');
622 -
623 - // Code Snippets (6 tools)
624 - $safe_register('MCP_Tool_Get_Header_Snippet');
625 - $safe_register('MCP_Tool_Update_Header_Snippet');
626 - $safe_register('MCP_Tool_Get_Footer_Snippet');
627 - $safe_register('MCP_Tool_Update_Footer_Snippet');
628 - $safe_register('MCP_Tool_Get_Post_Snippets');
629 - $safe_register('MCP_Tool_Update_Post_Snippets');
630 -
631 - // Categories & Taxonomies (15 tools)
632 - $safe_register('MCP_Tool_List_Categories');
633 - $safe_register('MCP_Tool_Get_Category');
634 - $safe_register('MCP_Tool_Create_Category');
635 - $safe_register('MCP_Tool_Update_Category');
636 - $safe_register('MCP_Tool_Delete_Category');
637 - $safe_register('MCP_Tool_Get_Post_Categories');
638 - $safe_register('MCP_Tool_Set_Post_Categories');
639 -
640 - // Tags (8 tools)
641 - $safe_register('MCP_Tool_List_Tags');
642 - $safe_register('MCP_Tool_Get_Tag');
643 - $safe_register('MCP_Tool_Create_Tag');
644 - $safe_register('MCP_Tool_Update_Tag');
645 - $safe_register('MCP_Tool_Delete_Tag');
646 - $safe_register('MCP_Tool_Get_Post_Tags');
647 - $safe_register('MCP_Tool_Set_Post_Tags');
648 -
649 - // Featured Images & Media (6 tools)
650 - $safe_register('MCP_Tool_Get_Featured_Image');
651 - $safe_register('MCP_Tool_Set_Featured_Image');
652 - $safe_register('MCP_Tool_Upload_Featured_Image');
653 - $safe_register('MCP_Tool_Remove_Featured_Image');
654 - $safe_register('MCP_Tool_List_Media');
655 - $safe_register('MCP_Tool_Get_Media_Details');
656 -
657 - // Post CRUD Operations (1 tool - delete/restore disabled for safety)
658 - $safe_register('MCP_Tool_Create_Post');
659 - // $safe_register('MCP_Tool_Delete_Post'); // DISABLED - safety
660 - // $safe_register('MCP_Tool_Restore_Post'); // DISABLED - safety
661 -
662 - // Bulk Operations (3 tools - bulk delete disabled for safety)
663 - $safe_register('MCP_Tool_Bulk_Update_Meta');
664 - $safe_register('MCP_Tool_Bulk_Set_Categories');
665 - $safe_register('MCP_Tool_Bulk_Change_Status');
666 - // $safe_register('MCP_Tool_Bulk_Delete_Posts'); // DISABLED - safety
667 -
668 - // WordPress Core SEO Settings (10 tools)
669 - $safe_register('MCP_Tool_Get_Site_Info');
670 - $safe_register('MCP_Tool_Update_Site_Info');
671 - $safe_register('MCP_Tool_Get_Permalink_Structure');
672 - $safe_register('MCP_Tool_Update_Permalink_Structure');
673 - $safe_register('MCP_Tool_Get_Reading_Settings');
674 - $safe_register('MCP_Tool_Update_Reading_Settings');
675 - $safe_register('MCP_Tool_Get_Search_Visibility');
676 - $safe_register('MCP_Tool_Update_Search_Visibility');
677 - $safe_register('MCP_Tool_Get_Date_Format');
678 - $safe_register('MCP_Tool_Get_Discussion_Settings');
679 -
680 - // Taxonomy Meta Operations (4 tools - NEW in v2.8.0)
681 - $safe_register('MCP_Tool_Get_Term_Meta');
682 - $safe_register('MCP_Tool_Update_Term_Meta');
683 - $safe_register('MCP_Tool_Bulk_Update_Term_Meta');
684 - $safe_register('MCP_Tool_List_Terms_With_Meta');
685 -
686 - // Bulk Alt Text Operations (4 tools - NEW in v2.8.0)
687 - $safe_register('MCP_Tool_Audit_Alt_Text');
688 - $safe_register('MCP_Tool_Bulk_Update_Alt_Text');
689 - $safe_register('MCP_Tool_Generate_Alt_Text');
690 - $safe_register('MCP_Tool_Validate_Alt_Text');
691 -
692 - // OTTO Persistence Settings (2 tools)
693 - $safe_register('MCP_Tool_Get_Otto_Persistence_Settings');
694 - $safe_register('MCP_Tool_Update_Otto_Persistence_Settings');
695 -
696 - // OTTO Pipeline Tools (3 tools)
697 - $safe_register('MCP_Tool_Trigger_Otto_Optimization');
698 - $safe_register('MCP_Tool_Get_Otto_Status');
699 - $safe_register('MCP_Tool_Verify_SEO_Output');
700 -
701 - // System Diagnostics & Plugin Info (4 tools)
702 - $safe_register('MCP_Tool_System_Diagnostics');
703 - $safe_register('MCP_Tool_List_All_Plugins');
704 - $safe_register('MCP_Tool_Get_Cron_Jobs');
705 - $safe_register('MCP_Tool_Get_WP_Option');
706 -
707 - // SEO Inventory (1 tool — WP-135)
708 - $safe_register('MCP_Tool_List_Posts_SEO_Inventory');
709 -
710 - // Read-Only Database Access (3 tools)
711 - $safe_register('MCP_Tool_DB_Tables');
712 - $safe_register('MCP_Tool_DB_Describe');
713 - $safe_register('MCP_Tool_DB_Select');
714 -
715 - // Breadcrumb Tools (1 tool)
716 - $safe_register('MCP_Tool_Get_Breadcrumb_Path');
717 -
718 - // Cache Purge (2 tools)
719 - $safe_register('MCP_Tool_Cache_Purge_All');
720 - $safe_register('MCP_Tool_Cache_Purge_URL');
721 -
722 - // LLMs.txt Tools (5 tools)
723 - $safe_register('MCP_Tool_Get_LLMs_Txt');
724 - $safe_register('MCP_Tool_Regenerate_LLMs_Txt');
725 - $safe_register('MCP_Tool_Get_LLMs_Txt_Settings');
726 - $safe_register('MCP_Tool_Update_LLMs_Txt_Settings');
727 - $safe_register('MCP_Tool_Get_Post_Markdown');
728 -
729 - // SEO Plugin Audit (4 tools — WP-202)
730 - $safe_register('MCP_Tool_Read_SEO_Plugin_Data');
731 - $safe_register('MCP_Tool_SEO_Plugin_Diff');
732 - $safe_register('MCP_Tool_Sync_To_Active_Plugins');
733 - $safe_register('MCP_Tool_Detect_SEO_Conflicts');
734 -
735 - // Allow other plugins/themes to register tools
736 - do_action('metasync_mcp_register_tools', $metasync_mcp_server);
737 -}
738 -add_action('init', 'metasync_init_mcp_server', 5);
739 -
740 -/**
741 723 * Schedule a daily cron event to auto-purge Sync History records older than 90 days.
742 724 */
743 725 function metasync_schedule_sync_log_cleanup() {
744 726 if (!wp_next_scheduled('metasync_sync_log_daily_cleanup')) {
@@ -765,85 +747,41 @@
765 747 echo '<script>window.__SA_DYO_INITIALIZED__=true;</script>' . "\n";
766 748 }
767 749 add_action('wp_head', 'metasync_output_dyo_init_flag', 1);
768 750
769 -/**
770 - * Initialize GA4 Analytics for Admin Area
771 - * Hooked to admin_init for proper WordPress lifecycle integration
772 - * Only loads after WordPress, plugins, and themes are fully loaded
773 - */
774 -function metasync_init_analytics() {
775 - // Only initialize in admin area (excludes AJAX and REST API requests)
776 - if (is_admin() && !wp_doing_ajax() && !defined('REST_REQUEST')) {
777 - Metasync_GA4::get_instance();
778 - }
779 -}
780 -add_action('admin_init', 'metasync_init_analytics', 10);
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';
781 753
782 754 /**
783 - * Initialize API Backoff System
784 - * Hooked to init for proper WordPress lifecycle integration
785 - * Monitors API responses and manages exponential backoff for rate limiting
786 - * @since 2.7.1
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.
787 767 */
788 -function metasync_init_api_backoff() {
789 - if (metasync_is_non_metasync_admin_ajax()) {
790 - return;
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;
791 774 }
792 - // Initialize backoff manager (registers HTTP response filters)
793 - Metasync_API_Backoff_Manager::get_instance();
794 -
795 - // Initialize admin notices (only in admin area)
796 - if (is_admin()) {
797 - Metasync_API_Backoff_Notices::get_instance();
775 + if (!metasync_is_custom_or_lps_page($post->ID)) {
776 + return $post_states;
798 777 }
778 + $post_states['metasync_website_studio'] = __('Website Studio', 'metasync');
779 + return $post_states;
799 780 }
800 -add_action('init', 'metasync_init_api_backoff', 5);
801 781
802 -/**
803 - * Initialize Review Notice
804 - * Shows a dismissible notice asking users to rate the plugin after a usage period
805 - * @since 2.8.0
806 - */
807 -function metasync_init_review_notice() {
808 - if (metasync_is_non_metasync_admin_ajax()) {
809 - return;
810 - }
811 - if (is_admin()) {
812 - Metasync_Review_Notice::get_instance();
813 - }
814 -}
815 -add_action('init', 'metasync_init_review_notice');
816 782
817 783 /**
818 - * Global convenience function to get active JWT token
819 - * Can be called from anywhere in WordPress (themes, other plugins, etc.)
820 - *
821 - * @param bool $force_refresh Force generation of new token even if cached one exists
822 - * @return string|false JWT token on success, false on failure
823 - */
824 -if (!function_exists('metasync_get_jwt_token')) {
825 - function metasync_get_jwt_token($force_refresh = false)
826 - {
827 - return Metasync::get_jwt_token($force_refresh);
828 - }
829 -}
830 -
831 -
832 -/**
833 - * Initialize Debug Mode Manager
834 - * Hooked to 'init' for proper WordPress lifecycle integration
835 - */
836 -function metasync_init_debug_mode_manager() {
837 - if (metasync_is_non_metasync_admin_ajax()) {
838 - return;
839 - }
840 - // Initialize the Debug Mode Manager singleton
841 - Metasync_Debug_Mode_Manager::get_instance();
842 -}
843 -add_action('init', 'metasync_init_debug_mode_manager', 10);
844 -
845 -/**
846 784 * Oxygen Builder Compatibility
847 785 * Auto re-signs [oxygen] dynamic-data shortcodes when their HMAC signatures
848 786 * are invalid (e.g. after design-set import or site migration).
849 787 * Runs once on admin_init; skips entirely when Oxygen is inactive.
@@ -849,5 +787,6 @@
849 787 * Runs once on admin_init; skips entirely when Oxygen is inactive.
850 788 */
851 789 if (is_admin()) {
852 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);
853 792 }