PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
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 +238 -368 2.6.14 → 2.7.2 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.14
18 + * Version: 2.7.2
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.14';
61 +$metasync_version = '2.7.2';
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
@@ -73,8 +95,18 @@
73 95 define('METASYNC_GA4_MEASUREMENT_ID', 'G-SBLWW1EMTJ');
74 96 define('METASYNC_GA4_API_SECRET', 'nMGs22mxQ3qVUy-aInqfZA');
75 97
76 98 /**
99 + * PostHog product analytics configuration.
100 + * The write key is a public project key embedded by design (same precedent
101 + * as the GA4 secret above); set METASYNC_POSTHOG_API_KEY in the environment
102 + * to override it - including to an empty string, which disables PostHog.
103 + */
104 +define('METASYNC_POSTHOG_HOST', 'https://posthog.internal.searchatlas.com');
105 +$metasync_posthog_key = getenv('METASYNC_POSTHOG_API_KEY');
106 +define('METASYNC_POSTHOG_API_KEY', false !== $metasync_posthog_key ? $metasync_posthog_key : 'phc_ntSayCJE7Ad9eBHRG6ygBaY4BJMa8rFjXcXESun9ftkh'); // gitleaks:allow -- public write-only PostHog project key
107 +
108 +/**
77 109 * Define whether to show the plugin status in WordPress admin top navigation bar
78 110 * Set to false to hide the status indicator
79 111 */
80 112 define('METASYNC_SHOW_ADMIN_BAR_STATUS', true);
@@ -107,9 +139,9 @@
107 139 return $sanitized;
108 140 }
109 141 }
110 142
111 -// Skip heavy MetaSync init on admin-ajax requests that don't target our own actions (Sentry issue 7441226449 / WP-227).
143 +// Skip heavy MetaSync init on admin-ajax requests that don't target our own actions (Sentry issue 7441226449).
112 144 if (!function_exists('metasync_is_non_metasync_admin_ajax')) {
113 145 function metasync_is_non_metasync_admin_ajax() {
114 146 static $result = null;
115 147 if ($result !== null) {
@@ -114,13 +146,19 @@
114 146 static $result = null;
115 147 if ($result !== null) {
116 148 return $result;
117 149 }
150 + // The MCP bridge intentionally boots WordPress with DOING_AJAX set, but it
151 + // is a MetaSync entry point rather than an unrelated admin AJAX request.
152 + // The constant is only ever defined as true by the bridge bootstraps.
153 + if (defined('METASYNC_MCP_BRIDGE')) {
154 + return ($result = false);
155 + }
118 156 if (!defined('DOING_AJAX') || !DOING_AJAX) {
119 157 return ($result = false);
120 158 }
121 - $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action']))
122 - : (isset($_GET['action']) ? sanitize_text_field(wp_unslash($_GET['action'])) : '');
159 + $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
160 + : (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 161 if ($action === '') {
124 162 return ($result = true);
125 163 }
126 164 if (strpos($action, 'metasync_') === 0 || strpos($action, 'meta_sync_') === 0 || $action === 'sample-permalink') {
@@ -132,9 +170,9 @@
132 170
133 171 // Lazy-load guard: only initialise the MCP server when the request is actually targeting
134 172 // the MCP REST route (or its sibling SEO-inventory route, which depends on $metasync_mcp_server
135 173 // 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.
174 +// touch MCP.
137 175 if (!function_exists('metasync_is_mcp_rest_request')) {
138 176 function metasync_is_mcp_rest_request(): bool {
139 177 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
140 178 if ($uri === '') {
@@ -160,10 +198,20 @@
160 198
161 199 // Phase 2 — these files have file-level side effects (add_action outside class body)
162 200 // and must remain as explicit require_once until refactored.
163 201 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-api-backoff-rest.php';
202 +require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-data-store.php';
164 203 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-error-logger.php';
165 204
205 +// Shared helpers (custom/LPS page detection + query exclusion) — loaded in all
206 +// request contexts (admin, REST, MCP, AJAX) so every SEO surface uses one rule.
207 +require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-helpers.php';
208 +
209 +// Shared "results per page" selector helper used by the Redirections, 404
210 +// Monitor, Changes Log and Media Library admin list tables. Loaded early so
211 +// the helper class is available before any list table instantiates.
212 +require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-per-page-helper.php';
213 +
166 214 /**
167 215 * Include the Otto Pixel Php Code
168 216 */
169 217 if (!metasync_is_non_metasync_admin_ajax()) {
@@ -215,9 +263,9 @@
215 263 #craft the message
216 264 sprintf(
217 265 '%s requires WordPress version %s or later. You are currently using version %s. Please update WordPress to activate this plugin.',
218 266 esc_html($plugin_name),
219 - METASYNC_MIN_WP,
267 + esc_html(METASYNC_MIN_WP),
220 268 esc_html($wp_version)
221 269 ),
222 270
223 271 #the plugin title as page title
@@ -237,9 +285,9 @@
237 285 #craft the message
238 286 sprintf(
239 287 '%s requires PHP version %s or later. You are currently using version %s. Please update PHP to activate this plugin.',
240 288 esc_html($plugin_name),
241 - METASYNC_MIN_PHP,
289 + esc_html(METASYNC_MIN_PHP),
242 290 esc_html($php_version)
243 291 ),
244 292
245 293 #the plugin title as page title
@@ -256,9 +304,9 @@
256 304
257 305 // Set initial version
258 306 update_option('metasync_version', METASYNC_VERSION);
259 307
260 - // WP-315: Record activation timestamp so Divi CSS fix transients
308 + // Record activation timestamp so Divi CSS fix transients
261 309 // auto-invalidate after plugin deactivate/reactivate cycles.
262 310 update_option('metasync_activated_at', (string) time());
263 311
264 312 // Clear all cache plugins to ensure fresh start
@@ -263,10 +311,14 @@
263 311
264 312 // Clear all cache plugins to ensure fresh start
265 313 Metasync_Cache_Purge::purge_all('plugin_activation');
266 314
267 - // WP-332: Migrate physical sitemap files on activation.
315 + // Migrate physical sitemap files on activation.
268 316 metasync_migrate_physical_sitemaps();
317 +
318 + // Consent-gated analytics: the activation event only fires if the user
319 + // previously opted in (re-activation) — a brand-new install is OFF.
320 + Metasync_PostHog::get_instance()->track('plugin_activated', array());
269 321 }
270 322
271 323 // Log-sync removed - error monitoring now handled by Sentry
272 324 // require_once plugin_dir_path(__FILE__) . 'log-sync/log-sync.php';
@@ -300,9 +352,9 @@
300 352 register_activation_hook(__FILE__, 'activate_metasync');
301 353 register_deactivation_hook(__FILE__, 'deactivate_metasync');
302 354
303 355 /**
304 - * WP-332: Migrate physical sitemap .xml files into transients and delete them.
356 + * Migrate physical sitemap .xml files into transients and delete them.
305 357 *
306 358 * Physical files in ABSPATH cause nginx/Plesk to 403 before WordPress can
307 359 * serve them. Called from both the activation hook and the version-gate
308 360 * migration so it covers fresh activations and auto-updates.
@@ -331,19 +383,90 @@
331 383 if (empty($candidates)) {
332 384 return;
333 385 }
334 386
387 + // Ownership gate: other plugins (Yoast and friends) write files with the
388 + // exact same names (sitemap_index.xml, sitemap1.xml, ...), so a name match
389 + // alone must never authorize deleting a physical file. A file is only
390 + // migrated when this plugin left evidence it wrote it.
391 + $tracked_names = [];
392 + $tracked_files = get_option('metasync_sitemap_files', []);
393 + if (is_array($tracked_files)) {
394 + foreach ($tracked_files as $entry) {
395 + $name = is_array($entry) && isset($entry['filename']) ? $entry['filename'] : $entry;
396 + if (is_string($name) && '' !== $name) {
397 + $tracked_names[] = $name;
398 + }
399 + }
400 + }
335 401 $virtual_index = get_option('metasync_sitemap_virtual_index', []);
402 + if (!is_array($virtual_index)) {
403 + $virtual_index = [];
404 + }
405 + $parked_names = array_keys($virtual_index);
406 + $generator_ran = !empty($tracked_names) && false !== get_option('metasync_sitemap_last_generated');
407 +
408 + $ours = [];
409 + $metasync_files_found = false;
410 + foreach ($candidates as $file) {
411 + $bn = basename($file);
412 +
413 + // A parked name proves MetaSync owned the file at an earlier point, but
414 + // an expired transient no longer proves that a file currently on disk
415 + // is ours. A different plugin may have recreated the same basename.
416 + if (in_array($bn, $parked_names, true)) {
417 + if (false !== get_transient('metasync_vsm_' . md5($bn))) {
418 + continue;
419 + }
420 + continue;
421 + }
422 +
423 + // Chunk, index, news, and video files are migratable only when their
424 + // exact basename is recorded by MetaSync's generator/configuration.
425 + if (in_array($bn, $tracked_names, true) && $generator_ran) {
426 + $ours[] = $file;
427 + $metasync_files_found = true;
428 + continue;
429 + }
430 +
431 + if ($bn === 'news-sitemap.xml' && null !== get_option('metasync_news_sitemap_settings', null)) {
432 + $ours[] = $file;
433 + $metasync_files_found = true;
434 + continue;
435 + }
436 + if ($bn === 'video-sitemap.xml' && null !== get_option('metasync_video_sitemap_settings', null)) {
437 + $ours[] = $file;
438 + $metasync_files_found = true;
439 + }
440 + }
441 +
442 + // Do not infer ownership of sitemap_index.xml from another sitemap: Yoast,
443 + // Rank Math, and other SEO plugins commonly use the same root filename.
444 + // It must be explicitly recorded in MetaSync's tracked file list.
445 + $ours = array_values(array_unique($ours));
446 +
447 + if (empty($ours)) {
448 + return;
449 + }
450 +
336 451 $migrated_files = [];
337 452
338 - foreach ($candidates as $file) {
453 + foreach ($ours as $file) {
339 454 $bn = basename($file);
340 455 $content = @file_get_contents($file);
341 456 if (false !== $content) {
342 457 $tkey = 'metasync_vsm_' . md5($bn);
343 - set_transient($tkey, $content, 30 * DAY_IN_SECONDS);
458 + // The news sitemap's entries are only valid inside Google News'
459 + // 48-hour window, so it must not be migrated in under a 30-day
460 + // TTL — that would re-introduce the staleness the generator's
461 + // own bounded TTL exists to prevent. Regenerate-on-miss rebuilds
462 + // it with a fresh date_query when this expires.
463 + $ttl = ('news-sitemap.xml' === $bn) ? (int) (DAY_IN_SECONDS / 4) : 30 * DAY_IN_SECONDS;
464 + set_transient($tkey, $content, $ttl);
344 465 if (false !== get_transient($tkey)) {
345 - @unlink($file);
466 + // The parked transient doubles as a 30-day backup of the
467 + // content before the file is removed from disk.
468 + @wp_delete_file($file);
346 469 $virtual_index[$bn] = $tkey;
347 470 if ($bn !== 'sitemap_index.xml' && $bn !== 'news-sitemap.xml' && $bn !== 'video-sitemap.xml') {
348 471 $migrated_files[] = [
349 472 'filename' => $bn,
@@ -374,10 +497,17 @@
374 497
375 498 $current_version = get_option('metasync_version', '0.0.0');
376 499 $plugin_version = METASYNC_VERSION;
377 500
378 - // If versions don't match, run migration
379 - if (version_compare($current_version, $plugin_version, '<')) {
501 + // If versions don't match, run migration. Dev builds store the 9.9.9
502 + // placeholder version, which compares greater than every real release —
503 + // once stored, a plain "<" comparison never fires again and migrations
504 + // are silently skipped forever. Also enter when the stored version is the
505 + // placeholder but the running version is a real release, so the migration
506 + // runner's dev-build escape hatch becomes reachable and the stored version
507 + // is corrected to the real release.
508 + if (version_compare($current_version, $plugin_version, '<')
509 + || ('9.9.9' === $current_version && '9.9.9' !== $plugin_version)) {
380 510 // Import whitelabel settings only if the JSON file is new or changed
381 511 // (prevents overwriting admin UI changes on every version check)
382 512 Metasync_Activator::check_whitelabel_settings_update();
383 513
@@ -383,8 +513,13 @@
383 513
384 514 // Run version-specific migrations first
385 515 MetaSync_DBMigration::run_version_migrations($current_version, $plugin_version);
386 516
517 + // The developer endpoint-switching panel has been removed. Reset its
518 + // persisted staging mode so upgrades cannot keep routing API requests
519 + // to staging without a visible warning or a supported recovery path.
520 + delete_option('metasync_dev_endpoint_mode');
521 +
387 522 // Migration for v2.7.0+: Remove AI Agent, switch to plugin auth token, make MCP always-on
388 523 if (version_compare($current_version, '2.7.0', '<')) {
389 524 // Remove old MCP API key option
390 525 delete_option('metasync_mcp_api_key');
@@ -404,9 +539,9 @@
404 539 update_option('metasync_options', $options);
405 540 }
406 541 }
407 542
408 - // WP-299: One-time purge of stale OTTO SEO cron backlog.
543 + // One-time purge of stale OTTO SEO cron backlog.
409 544 // Prior versions could accumulate thousands of metasync_process_seo_job and
410 545 // metasync_process_otto_crawl_url_job events due to unbounded rescheduling.
411 546 // Clear the backlog once on update; the new code prevents re-accumulation.
412 547 if (!get_option('metasync_wp299_cron_cleanup_done')) {
@@ -415,9 +550,39 @@
415 550 wp_unschedule_hook('metasync_process_otto_batch_cache_job');
416 551 update_option('metasync_wp299_cron_cleanup_done', true, false);
417 552 }
418 553
419 - // WP-332: Migrate physical sitemap files on version update.
554 + // One-time cleanup of canonical values corrupted to the literal
555 + // "Array" (emitted as http://Array once the 2.6.16 canonical filters
556 + // started reading them). The sanitizer prevents new corruption; this
557 + // repairs the rows already in the database. Cache purge below pushes
558 + // the clean pages live. Claimed via add_option() — it fails if the row
559 + // already exists, so concurrent requests can't run the cleanup twice,
560 + // and the claim lands BEFORE the work: everything inside is idempotent
561 + // and the read-side sanitizer already protects output if a run is
562 + // interrupted.
563 + if (false === get_option('metasync_canonical_cleanup_done')
564 + && add_option('metasync_canonical_cleanup_done', 'running', '', false)) {
565 + MetaSync_DBMigration::cleanup_corrupted_canonicals();
566 + update_option('metasync_canonical_cleanup_done', 'done', false);
567 + }
568 +
569 + // One-time repair of Local Business logos corrupted to "http://<id>".
570 + // The sanitizer fix stopped new corruption; this restores the
571 + // attachment ID on sites that saved a logo before it. The corrupted
572 + // value encodes the original ID exactly, so the rewrite is lossless.
573 + // Claimed via add_option() — it fails when the row already exists, so
574 + // concurrent requests can't run the repair twice, and the claim lands
575 + // BEFORE the work: the repair itself is idempotent, and the read-side
576 + // normalisation in the schema output and admin preview already
577 + // protects output if a run is interrupted.
578 + if (false === get_option('metasync_local_seo_logo_repair_done')
579 + && add_option('metasync_local_seo_logo_repair_done', 'running', '', false)) {
580 + MetaSync_DBMigration::repair_corrupted_local_seo_logo();
581 + update_option('metasync_local_seo_logo_repair_done', 'done', false);
582 + }
583 +
584 + // Migrate physical sitemap files on version update.
420 585 metasync_migrate_physical_sitemaps();
421 586
422 587 // Run full migration to ensure all tables are up to date
423 588 MetaSync_DBMigration::run_migrations();
@@ -449,9 +614,9 @@
449 614 if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') {
450 615 return;
451 616 }
452 617
453 - // WP-427: By the time upgrader_process_complete fires, the upgrader may have
618 + // By the time upgrader_process_complete fires, the upgrader may have
454 619 // deleted the directory this (old, still-in-memory) copy of the plugin was
455 620 // loaded from — e.g. when the installed dir name differs from the package's
456 621 // root dir ('metasync-develop' vs 'metasync'). The Composer classmap then
457 622 // points at files that no longer exist, so autoloading Metasync_Activator
@@ -517,9 +682,9 @@
517 682 }
518 683 }
519 684
520 685 if ($should_import) {
521 - Metasync_Activator::check_whitelabel_settings_update();
686 + Metasync_Activator::check_whitelabel_settings_update(true);
522 687 }
523 688 }
524 689
525 690 // Hook into WordPress upgrader to detect plugin updates
@@ -528,26 +693,31 @@
528 693 /**
529 694 * Fallback: Check for whitelabel file changes on admin pages
530 695 * This handles edge cases where upgrader_process_complete doesn't fire
531 696 * (e.g., FTP uploads, manual file replacements)
532 - * Only checks once per admin session to minimize performance impact
697 + * Only checks once per admin request to minimize performance impact
533 698 */
534 -// function metasync_check_whitelabel_on_admin()
535 -// {
536 -// // Only check once per admin session to avoid overhead
537 -// static $checked = false;
538 -// if ($checked) {
539 -// return;
540 -// }
541 -// $checked = true;
699 +function metasync_check_whitelabel_on_admin()
700 +{
701 + static $checked = false;
702 + if ($checked || !current_user_can('manage_options')) {
703 + return;
704 + }
705 + $checked = true;
542 706
543 -// require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
544 -// Metasync_Activator::check_whitelabel_settings_update();
545 -// }
707 + require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
546 708
547 -// Check for whitelabel changes on admin pages (fallback for edge cases)
548 -// add_action('admin_init', 'metasync_check_whitelabel_on_admin', 1);
709 + // Surface a persistently failing White Label import instead of
710 + // retrying it silently on every admin request.
711 + add_action('admin_notices', array('Metasync_Activator', 'render_whitelabel_import_failure_notice'));
549 712
713 + Metasync_Activator::check_whitelabel_settings_update();
714 +}
715 +
716 +// Retry package imports on an authorized admin request when an update completed
717 +// through the public version-check or stale-classmap fallback path.
718 +add_action('admin_init', 'metasync_check_whitelabel_on_admin', 1);
719 +
550 720 // Include Media Optimization Module
551 721 if (!metasync_is_non_metasync_admin_ajax()) {
552 722 require_once plugin_dir_path(__FILE__) . 'media-optimization/media-optimization-loader.php';
553 723 }
@@ -564,277 +734,12 @@
564 734 $plugin->run();
565 735 }
566 736 run_metasync();
567 737
568 -/**
569 - * Initialize WordPress MCP Server
570 - *
571 - * Safely register a single MCP tool class, logging failures without aborting
572 - * subsequent registrations. Extracted as a named function so both production
573 - * code and unit tests exercise the same logic (WP-265).
574 - *
575 - * @param object $server The MCP server instance (must have register_tool()).
576 - * @param string $class_name Fully-qualified tool class name.
577 - */
578 -function metasync_safe_register_mcp_tool($server, $class_name) {
579 - static $logged_missing = [];
580 - if (!class_exists($class_name)) {
581 - if (empty($logged_missing[$class_name])) {
582 - error_log('MetaSync MCP: ' . $class_name . ' class not found — skipping registration. Run composer dump-autoload to regenerate the classmap.');
583 - $logged_missing[$class_name] = true;
584 - }
585 - return;
586 - }
587 - try {
588 - $server->register_tool(new $class_name());
589 - } catch (\Throwable $e) {
590 - error_log('MetaSync MCP: Failed to register tool ' . $class_name . ': ' . $e->getMessage());
591 - }
592 -}
738 +// MCP server bootstrap (server + tool registration) extracted to keep this entry file lean.
739 +require_once plugin_dir_path( __FILE__ ) . 'includes/mcp-server-bootstrap.php';
593 740
594 741 /**
595 - * Creates and configures the MCP server instance,
596 - * registers all available MCP tools for WordPress operations.
597 - * Hooked to 'init' for proper WordPress lifecycle integration.
598 - */
599 -function metasync_init_mcp_server() {
600 - if (metasync_is_non_metasync_admin_ajax()) {
601 - return;
602 - }
603 - // Skip the heavy MCP boot (server + sync logger + REST inventory + 100+ tool objects)
604 - // unless this request is actually targeting the MCP REST route or was loaded via
605 - // the stdio/HTTP bridge. WP-255.
606 - if (!metasync_is_mcp_rest_request() && !defined('METASYNC_MCP_BRIDGE')) {
607 - return;
608 - }
609 - // Initialize MCP server
610 - global $metasync_mcp_server;
611 -
612 - try {
613 - $metasync_mcp_server = new Metasync_MCP_Server();
614 -
615 - // Attach MCP sync logger so all write tool calls are recorded in Sync History.
616 - new Metasync_MCP_Sync_Logger();
617 -
618 - // SEO Inventory: shared builder + standalone REST endpoint (WP-135)
619 - new Metasync_REST_SEO_Inventory();
620 - } catch (\Throwable $e) {
621 - error_log('MCP Server Initialization Error: ' . $e->getMessage());
622 - return;
623 - }
624 -
625 - // Helper: register a single tool, logging failures without aborting subsequent registrations
626 - $safe_register = function($class_name) use ($metasync_mcp_server) {
627 - metasync_safe_register_mcp_tool($metasync_mcp_server, $class_name);
628 - };
629 -
630 - // Register MCP Tools (Total: 92 existing + 8 new = 100 tools total!)
631 - // NEW in v2.8.0: +4 Taxonomy Meta tools, +4 Bulk Alt Text tools
632 -
633 - // Post Meta Operations (4 tools)
634 - $safe_register('MCP_Tool_Update_Post_Meta');
635 - $safe_register('MCP_Tool_Get_Post_Meta');
636 - $safe_register('MCP_Tool_Get_SEO_Meta');
637 - $safe_register('MCP_Tool_Get_Hreflang_Links');
638 -
639 - // Post Operations (4 tools)
640 - $safe_register('MCP_Tool_Get_Post');
641 - $safe_register('MCP_Tool_Get_Post_By_URL');
642 - $safe_register('MCP_Tool_List_Posts');
643 - $safe_register('MCP_Tool_Update_Post');
644 - $safe_register('MCP_Tool_Get_Post_Types');
645 -
646 - // SEO Analysis (3 tools)
647 - $safe_register('MCP_Tool_Analyze_SEO');
648 - $safe_register('MCP_Tool_Check_Indexability');
649 - $safe_register('MCP_Tool_SEO_Health_Report');
650 -
651 - // Search Operations (3 tools)
652 - $safe_register('MCP_Tool_Search_Posts');
653 - $safe_register('MCP_Tool_Search_By_Keyword');
654 - $safe_register('MCP_Tool_Find_Missing_Meta');
655 -
656 - // Redirect Management (5 tools)
657 - $safe_register('MCP_Tool_Create_Redirect');
658 - $safe_register('MCP_Tool_List_Redirects');
659 - $safe_register('MCP_Tool_Delete_Redirect');
660 - $safe_register('MCP_Tool_Update_Redirect');
661 - $safe_register('MCP_Tool_Check_Redirects_Health');
662 -
663 - // 404 Error Monitoring (5 tools)
664 - $safe_register('MCP_Tool_List_404_Errors');
665 - $safe_register('MCP_Tool_Get_404_Stats');
666 - $safe_register('MCP_Tool_Delete_404_Error');
667 - $safe_register('MCP_Tool_Clear_404_Errors');
668 - $safe_register('MCP_Tool_Create_Redirect_From_404');
669 -
670 - // Robots.txt & Sitemap Management (11 tools - 7 existing + 4 new)
671 - $safe_register('MCP_Tool_Get_Robots_Txt');
672 - $safe_register('MCP_Tool_Update_Robots_Txt');
673 - $safe_register('MCP_Tool_Get_Sitemap_Status');
674 - $safe_register('MCP_Tool_Regenerate_Sitemap');
675 - $safe_register('MCP_Tool_Exclude_From_Sitemap');
676 - $safe_register('MCP_Tool_Add_Robots_Rule');
677 - $safe_register('MCP_Tool_Remove_Robots_Rule');
678 - $safe_register('MCP_Tool_Parse_Robots_Txt');
679 - $safe_register('MCP_Tool_Validate_Robots_Txt');
680 - $safe_register('MCP_Tool_Get_News_Sitemap');
681 - $safe_register('MCP_Tool_Get_Video_Sitemap');
682 -
683 - // Plugin Settings Management (4 tools)
684 - $safe_register('MCP_Tool_Get_Plugin_Settings');
685 - $safe_register('MCP_Tool_Update_Plugin_Settings');
686 - $safe_register('MCP_Tool_List_Plugin_Settings_Schema');
687 - $safe_register('MCP_Tool_Get_MCP_Settings');
688 -
689 - // Schema Markup Management (7 tools)
690 - $safe_register('MCP_Tool_Get_Schema_Markup');
691 - $safe_register('MCP_Tool_Update_Schema_Markup');
692 - $safe_register('MCP_Tool_Add_Schema_Type');
693 - $safe_register('MCP_Tool_Remove_Schema_Type');
694 - $safe_register('MCP_Tool_Validate_Schema');
695 - $safe_register('MCP_Tool_Get_Schema_Content');
696 - $safe_register('MCP_Tool_Set_Schema_Content');
697 -
698 - // Google Instant Index (6 tools)
699 - $safe_register('MCP_Tool_Instant_Index_Update');
700 - $safe_register('MCP_Tool_Instant_Index_Delete');
701 - $safe_register('MCP_Tool_Instant_Index_Status');
702 - $safe_register('MCP_Tool_Instant_Index_Bulk_Update');
703 - $safe_register('MCP_Tool_Get_Instant_Index_Settings');
704 - $safe_register('MCP_Tool_Update_Instant_Index_Settings');
705 -
706 - // Custom HTML Pages (6 tools)
707 - $safe_register('MCP_Tool_Create_Custom_Page');
708 - $safe_register('MCP_Tool_Get_Custom_Page');
709 - $safe_register('MCP_Tool_List_Custom_Pages');
710 - $safe_register('MCP_Tool_Update_Custom_Page');
711 - $safe_register('MCP_Tool_Delete_Custom_Page');
712 - $safe_register('MCP_Tool_Import_LPS_Page');
713 -
714 - // HTML to Builder Converter (3 tools)
715 - $safe_register('MCP_Tool_Convert_HTML_To_Builder');
716 - $safe_register('MCP_Tool_Create_Builder_Page_From_HTML');
717 - $safe_register('MCP_Tool_Convert_Custom_Page_To_Builder');
718 -
719 - // Code Snippets (6 tools)
720 - $safe_register('MCP_Tool_Get_Header_Snippet');
721 - $safe_register('MCP_Tool_Update_Header_Snippet');
722 - $safe_register('MCP_Tool_Get_Footer_Snippet');
723 - $safe_register('MCP_Tool_Update_Footer_Snippet');
724 - $safe_register('MCP_Tool_Get_Post_Snippets');
725 - $safe_register('MCP_Tool_Update_Post_Snippets');
726 -
727 - // Categories & Taxonomies (15 tools)
728 - $safe_register('MCP_Tool_List_Categories');
729 - $safe_register('MCP_Tool_Get_Category');
730 - $safe_register('MCP_Tool_Create_Category');
731 - $safe_register('MCP_Tool_Update_Category');
732 - $safe_register('MCP_Tool_Delete_Category');
733 - $safe_register('MCP_Tool_Get_Post_Categories');
734 - $safe_register('MCP_Tool_Set_Post_Categories');
735 -
736 - // Tags (8 tools)
737 - $safe_register('MCP_Tool_List_Tags');
738 - $safe_register('MCP_Tool_Get_Tag');
739 - $safe_register('MCP_Tool_Create_Tag');
740 - $safe_register('MCP_Tool_Update_Tag');
741 - $safe_register('MCP_Tool_Delete_Tag');
742 - $safe_register('MCP_Tool_Get_Post_Tags');
743 - $safe_register('MCP_Tool_Set_Post_Tags');
744 -
745 - // Featured Images & Media (6 tools)
746 - $safe_register('MCP_Tool_Get_Featured_Image');
747 - $safe_register('MCP_Tool_Set_Featured_Image');
748 - $safe_register('MCP_Tool_Upload_Featured_Image');
749 - $safe_register('MCP_Tool_Remove_Featured_Image');
750 - $safe_register('MCP_Tool_List_Media');
751 - $safe_register('MCP_Tool_Get_Media_Details');
752 -
753 - // Post CRUD Operations (1 tool - delete/restore disabled for safety)
754 - $safe_register('MCP_Tool_Create_Post');
755 - // $safe_register('MCP_Tool_Delete_Post'); // DISABLED - safety
756 - // $safe_register('MCP_Tool_Restore_Post'); // DISABLED - safety
757 -
758 - // Bulk Operations (3 tools - bulk delete disabled for safety)
759 - $safe_register('MCP_Tool_Bulk_Update_Meta');
760 - $safe_register('MCP_Tool_Bulk_Set_Categories');
761 - $safe_register('MCP_Tool_Bulk_Change_Status');
762 - // $safe_register('MCP_Tool_Bulk_Delete_Posts'); // DISABLED - safety
763 -
764 - // WordPress Core SEO Settings (10 tools)
765 - $safe_register('MCP_Tool_Get_Site_Info');
766 - $safe_register('MCP_Tool_Update_Site_Info');
767 - $safe_register('MCP_Tool_Get_Permalink_Structure');
768 - $safe_register('MCP_Tool_Update_Permalink_Structure');
769 - $safe_register('MCP_Tool_Get_Reading_Settings');
770 - $safe_register('MCP_Tool_Update_Reading_Settings');
771 - $safe_register('MCP_Tool_Get_Search_Visibility');
772 - $safe_register('MCP_Tool_Update_Search_Visibility');
773 - $safe_register('MCP_Tool_Get_Date_Format');
774 - $safe_register('MCP_Tool_Get_Discussion_Settings');
775 -
776 - // Taxonomy Meta Operations (4 tools - NEW in v2.8.0)
777 - $safe_register('MCP_Tool_Get_Term_Meta');
778 - $safe_register('MCP_Tool_Update_Term_Meta');
779 - $safe_register('MCP_Tool_Bulk_Update_Term_Meta');
780 - $safe_register('MCP_Tool_List_Terms_With_Meta');
781 -
782 - // Bulk Alt Text Operations (4 tools - NEW in v2.8.0)
783 - $safe_register('MCP_Tool_Audit_Alt_Text');
784 - $safe_register('MCP_Tool_Bulk_Update_Alt_Text');
785 - $safe_register('MCP_Tool_Generate_Alt_Text');
786 - $safe_register('MCP_Tool_Validate_Alt_Text');
787 -
788 - // OTTO Persistence Settings (2 tools)
789 - $safe_register('MCP_Tool_Get_Otto_Persistence_Settings');
790 - $safe_register('MCP_Tool_Update_Otto_Persistence_Settings');
791 -
792 - // OTTO Pipeline Tools (3 tools)
793 - $safe_register('MCP_Tool_Trigger_Otto_Optimization');
794 - $safe_register('MCP_Tool_Get_Otto_Status');
795 - $safe_register('MCP_Tool_Verify_SEO_Output');
796 -
797 - // System Diagnostics & Plugin Info (4 tools)
798 - $safe_register('MCP_Tool_System_Diagnostics');
799 - $safe_register('MCP_Tool_List_All_Plugins');
800 - $safe_register('MCP_Tool_Get_Cron_Jobs');
801 - $safe_register('MCP_Tool_Get_WP_Option');
802 -
803 - // SEO Inventory (1 tool — WP-135)
804 - $safe_register('MCP_Tool_List_Posts_SEO_Inventory');
805 -
806 - // Read-Only Database Access (3 tools)
807 - $safe_register('MCP_Tool_DB_Tables');
808 - $safe_register('MCP_Tool_DB_Describe');
809 - $safe_register('MCP_Tool_DB_Select');
810 -
811 - // Breadcrumb Tools (1 tool)
812 - $safe_register('MCP_Tool_Get_Breadcrumb_Path');
813 -
814 - // Cache Purge (2 tools)
815 - $safe_register('MCP_Tool_Cache_Purge_All');
816 - $safe_register('MCP_Tool_Cache_Purge_URL');
817 -
818 - // LLMs.txt Tools (5 tools)
819 - $safe_register('MCP_Tool_Get_LLMs_Txt');
820 - $safe_register('MCP_Tool_Regenerate_LLMs_Txt');
821 - $safe_register('MCP_Tool_Get_LLMs_Txt_Settings');
822 - $safe_register('MCP_Tool_Update_LLMs_Txt_Settings');
823 - $safe_register('MCP_Tool_Get_Post_Markdown');
824 -
825 - // SEO Plugin Audit (4 tools — WP-202)
826 - $safe_register('MCP_Tool_Read_SEO_Plugin_Data');
827 - $safe_register('MCP_Tool_SEO_Plugin_Diff');
828 - $safe_register('MCP_Tool_Sync_To_Active_Plugins');
829 - $safe_register('MCP_Tool_Detect_SEO_Conflicts');
830 -
831 - // Allow other plugins/themes to register tools
832 - do_action('metasync_mcp_register_tools', $metasync_mcp_server);
833 -}
834 -add_action('init', 'metasync_init_mcp_server', 5);
835 -
836 -/**
837 742 * Schedule a daily cron event to auto-purge Sync History records older than 90 days.
838 743 */
839 744 function metasync_schedule_sync_log_cleanup() {
840 745 if (!wp_next_scheduled('metasync_sync_log_daily_cleanup')) {
@@ -861,85 +766,49 @@
861 766 echo '<script>window.__SA_DYO_INITIALIZED__=true;</script>' . "\n";
862 767 }
863 768 add_action('wp_head', 'metasync_output_dyo_init_flag', 1);
864 769
865 -/**
866 - * Initialize GA4 Analytics for Admin Area
867 - * Hooked to admin_init for proper WordPress lifecycle integration
868 - * Only loads after WordPress, plugins, and themes are fully loaded
869 - */
870 -function metasync_init_analytics() {
871 - // Only initialize in admin area (excludes AJAX and REST API requests)
872 - if (is_admin() && !wp_doing_ajax() && !defined('REST_REQUEST')) {
873 - Metasync_GA4::get_instance();
874 - }
875 -}
876 -add_action('admin_init', 'metasync_init_analytics', 10);
770 +// Shared analytics helper is loaded in all request contexts so server-side feature
771 +// events can use the same consent gate as admin analytics.
772 +require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-posthog.php';
773 +require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-analytics-consent-notice.php';
877 774
878 775 /**
879 - * Initialize API Backoff System
880 - * Hooked to init for proper WordPress lifecycle integration
881 - * Monitors API responses and manages exponential backoff for rate limiting
882 - * @since 2.7.1
776 + * Runtime feature initialisers (GA4, API backoff, review notice, JWT accessor,
777 + * debug mode) extracted to keep this entry file lean.
883 778 */
884 -function metasync_init_api_backoff() {
885 - if (metasync_is_non_metasync_admin_ajax()) {
886 - return;
887 - }
888 - // Initialize backoff manager (registers HTTP response filters)
889 - Metasync_API_Backoff_Manager::get_instance();
779 +require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-runtime-init.php';
890 780
891 - // Initialize admin notices (only in admin area)
892 - if (is_admin()) {
893 - Metasync_API_Backoff_Notices::get_instance();
894 - }
895 -}
896 -add_action('init', 'metasync_init_api_backoff', 5);
897 -
898 781 /**
899 - * Initialize Review Notice
900 - * Shows a dismissible notice asking users to rate the plugin after a usage period
901 - * @since 2.8.0
782 + * Append a "Website Studio" post state to LPS-synced / MetaSync custom pages in
783 + * the admin Pages list, so site owners can tell at a glance which pages are
784 + * managed by Website Studio and shouldn't be hand-edited.
785 + *
786 + * Hooks WordPress core's display_post_states filter — the same mechanism that
787 + * renders the grey inline tags like "— Front Page" / "— Draft" — so the label
788 + * is native-styled and only appears next to relevant page titles, with no
789 + * custom admin column.
790 + *
791 + * @param string[] $post_states Existing post-state labels keyed by slug.
792 + * @param WP_Post $post The post being listed.
793 + * @return string[] Possibly-augmented post states.
902 794 */
903 -function metasync_init_review_notice() {
904 - if (metasync_is_non_metasync_admin_ajax()) {
905 - return;
795 +function metasync_add_lps_post_state($post_states, $post) {
796 + // metasync_is_custom_or_lps_page() lives in otto/otto_pixel.php, which is NOT
797 + // loaded on non-MetaSync admin-ajax requests (e.g. Quick Edit's inline-save,
798 + // where this filter still fires), so guard against the undefined function.
799 + if (!function_exists('metasync_is_custom_or_lps_page')) {
800 + return $post_states;
906 801 }
907 - if (is_admin()) {
908 - Metasync_Review_Notice::get_instance();
802 + if (!metasync_is_custom_or_lps_page($post->ID)) {
803 + return $post_states;
909 804 }
805 + $post_states['metasync_website_studio'] = __('Website Studio', 'metasync');
806 + return $post_states;
910 807 }
911 -add_action('init', 'metasync_init_review_notice');
912 808
913 -/**
914 - * Global convenience function to get active JWT token
915 - * Can be called from anywhere in WordPress (themes, other plugins, etc.)
916 - *
917 - * @param bool $force_refresh Force generation of new token even if cached one exists
918 - * @return string|false JWT token on success, false on failure
919 - */
920 -if (!function_exists('metasync_get_jwt_token')) {
921 - function metasync_get_jwt_token($force_refresh = false)
922 - {
923 - return Metasync::get_jwt_token($force_refresh);
924 - }
925 -}
926 809
927 -
928 810 /**
929 - * Initialize Debug Mode Manager
930 - * Hooked to 'init' for proper WordPress lifecycle integration
931 - */
932 -function metasync_init_debug_mode_manager() {
933 - if (metasync_is_non_metasync_admin_ajax()) {
934 - return;
935 - }
936 - // Initialize the Debug Mode Manager singleton
937 - Metasync_Debug_Mode_Manager::get_instance();
938 -}
939 -add_action('init', 'metasync_init_debug_mode_manager', 10);
940 -
941 -/**
942 811 * Oxygen Builder Compatibility
943 812 * Auto re-signs [oxygen] dynamic-data shortcodes when their HMAC signatures
944 813 * are invalid (e.g. after design-set import or site migration).
945 814 * Runs once on admin_init; skips entirely when Oxygen is inactive.
@@ -945,5 +814,6 @@
945 814 * Runs once on admin_init; skips entirely when Oxygen is inactive.
946 815 */
947 816 if (is_admin()) {
948 817 add_action('admin_init', ['Metasync_Oxygen_Compat', 'maybe_resign_shortcodes'], 20);
818 + add_filter('display_post_states', 'metasync_add_lps_post_state', 10, 2);
949 819 }