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 +184 -30 2.6.21 → 2.7.2 View file →
@@ -11,13 +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.21
19 - * Requires PHP: 8.2
18 + * Version: 2.7.2
19 + * Requires PHP: 8.1
20 20 * Author: Search Atlas
21 21 * Author URI: https://searchatlas.com
22 22 * License: GPL v3
23 23 * License URI: https://www.gnu.org/licenses/gpl-3.0.txt
@@ -35,20 +35,37 @@
35 35 // Canonical sanitizer — required explicitly (not via the committed classmap)
36 36 // so it is guaranteed loadable everywhere canonicals are read or written.
37 37 require_once __DIR__ . '/includes/class-metasync-canonical-sanitizer.php';
38 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 +
39 56 /**
40 57 * Currently plugin version.
41 58 * Start at version 1.0.0 and use SemVer - https://semver.org
42 59 * Rename this for your plugin and update it as you release new versions.
43 60 */
44 -$metasync_version = '2.6.21';
61 +$metasync_version = '2.7.2';
45 62 define('METASYNC_VERSION', preg_match('/^\d+\.\d+/', $metasync_version) ? $metasync_version : '9.9.9');
46 63 /**
47 64 * Define the current required php version
48 65 * This will be used to validate whether the user can install the plugin or not
49 66 */
50 -define('METASYNC_MIN_PHP', '8.2');
67 +define('METASYNC_MIN_PHP', '8.1');
51 68
52 69 /**
53 70 * Define the current required php version
54 71 * This will be used to validate whether the user can install the plugin or not
@@ -78,8 +95,18 @@
78 95 define('METASYNC_GA4_MEASUREMENT_ID', 'G-SBLWW1EMTJ');
79 96 define('METASYNC_GA4_API_SECRET', 'nMGs22mxQ3qVUy-aInqfZA');
80 97
81 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 +/**
82 109 * Define whether to show the plugin status in WordPress admin top navigation bar
83 110 * Set to false to hide the status indicator
84 111 */
85 112 define('METASYNC_SHOW_ADMIN_BAR_STATUS', true);
@@ -119,13 +146,19 @@
119 146 static $result = null;
120 147 if ($result !== null) {
121 148 return $result;
122 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 + }
123 156 if (!defined('DOING_AJAX') || !DOING_AJAX) {
124 157 return ($result = false);
125 158 }
126 - $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action']))
127 - : (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
128 161 if ($action === '') {
129 162 return ($result = true);
130 163 }
131 164 if (strpos($action, 'metasync_') === 0 || strpos($action, 'meta_sync_') === 0 || $action === 'sample-permalink') {
@@ -165,8 +198,9 @@
165 198
166 199 // Phase 2 — these files have file-level side effects (add_action outside class body)
167 200 // and must remain as explicit require_once until refactored.
168 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';
169 203 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-error-logger.php';
170 204
171 205 // Shared helpers (custom/LPS page detection + query exclusion) — loaded in all
172 206 // request contexts (admin, REST, MCP, AJAX) so every SEO surface uses one rule.
@@ -171,8 +205,13 @@
171 205 // Shared helpers (custom/LPS page detection + query exclusion) — loaded in all
172 206 // request contexts (admin, REST, MCP, AJAX) so every SEO surface uses one rule.
173 207 require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-helpers.php';
174 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 +
175 214 /**
176 215 * Include the Otto Pixel Php Code
177 216 */
178 217 if (!metasync_is_non_metasync_admin_ajax()) {
@@ -224,9 +263,9 @@
224 263 #craft the message
225 264 sprintf(
226 265 '%s requires WordPress version %s or later. You are currently using version %s. Please update WordPress to activate this plugin.',
227 266 esc_html($plugin_name),
228 - METASYNC_MIN_WP,
267 + esc_html(METASYNC_MIN_WP),
229 268 esc_html($wp_version)
230 269 ),
231 270
232 271 #the plugin title as page title
@@ -246,9 +285,9 @@
246 285 #craft the message
247 286 sprintf(
248 287 '%s requires PHP version %s or later. You are currently using version %s. Please update PHP to activate this plugin.',
249 288 esc_html($plugin_name),
250 - METASYNC_MIN_PHP,
289 + esc_html(METASYNC_MIN_PHP),
251 290 esc_html($php_version)
252 291 ),
253 292
254 293 #the plugin title as page title
@@ -274,8 +313,12 @@
274 313 Metasync_Cache_Purge::purge_all('plugin_activation');
275 314
276 315 // Migrate physical sitemap files on activation.
277 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());
278 321 }
279 322
280 323 // Log-sync removed - error monitoring now handled by Sentry
281 324 // require_once plugin_dir_path(__FILE__) . 'log-sync/log-sync.php';
@@ -340,19 +383,90 @@
340 383 if (empty($candidates)) {
341 384 return;
342 385 }
343 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 + }
344 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 +
345 451 $migrated_files = [];
346 452
347 - foreach ($candidates as $file) {
453 + foreach ($ours as $file) {
348 454 $bn = basename($file);
349 455 $content = @file_get_contents($file);
350 456 if (false !== $content) {
351 457 $tkey = 'metasync_vsm_' . md5($bn);
352 - 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);
353 465 if (false !== get_transient($tkey)) {
354 - @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);
355 469 $virtual_index[$bn] = $tkey;
356 470 if ($bn !== 'sitemap_index.xml' && $bn !== 'news-sitemap.xml' && $bn !== 'video-sitemap.xml') {
357 471 $migrated_files[] = [
358 472 'filename' => $bn,
@@ -383,10 +497,17 @@
383 497
384 498 $current_version = get_option('metasync_version', '0.0.0');
385 499 $plugin_version = METASYNC_VERSION;
386 500
387 - // If versions don't match, run migration
388 - 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)) {
389 510 // Import whitelabel settings only if the JSON file is new or changed
390 511 // (prevents overwriting admin UI changes on every version check)
391 512 Metasync_Activator::check_whitelabel_settings_update();
392 513
@@ -392,8 +513,13 @@
392 513
393 514 // Run version-specific migrations first
394 515 MetaSync_DBMigration::run_version_migrations($current_version, $plugin_version);
395 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 +
396 522 // Migration for v2.7.0+: Remove AI Agent, switch to plugin auth token, make MCP always-on
397 523 if (version_compare($current_version, '2.7.0', '<')) {
398 524 // Remove old MCP API key option
399 525 delete_option('metasync_mcp_api_key');
@@ -439,8 +565,23 @@
439 565 MetaSync_DBMigration::cleanup_corrupted_canonicals();
440 566 update_option('metasync_canonical_cleanup_done', 'done', false);
441 567 }
442 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 +
443 584 // Migrate physical sitemap files on version update.
444 585 metasync_migrate_physical_sitemaps();
445 586
446 587 // Run full migration to ensure all tables are up to date
@@ -541,9 +682,9 @@
541 682 }
542 683 }
543 684
544 685 if ($should_import) {
545 - Metasync_Activator::check_whitelabel_settings_update();
686 + Metasync_Activator::check_whitelabel_settings_update(true);
546 687 }
547 688 }
548 689
549 690 // Hook into WordPress upgrader to detect plugin updates
@@ -552,26 +693,31 @@
552 693 /**
553 694 * Fallback: Check for whitelabel file changes on admin pages
554 695 * This handles edge cases where upgrader_process_complete doesn't fire
555 696 * (e.g., FTP uploads, manual file replacements)
556 - * Only checks once per admin session to minimize performance impact
697 + * Only checks once per admin request to minimize performance impact
557 698 */
558 -// function metasync_check_whitelabel_on_admin()
559 -// {
560 -// // Only check once per admin session to avoid overhead
561 -// static $checked = false;
562 -// if ($checked) {
563 -// return;
564 -// }
565 -// $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;
566 706
567 -// require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
568 -// Metasync_Activator::check_whitelabel_settings_update();
569 -// }
707 + require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
570 708
571 -// Check for whitelabel changes on admin pages (fallback for edge cases)
572 -// 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'));
573 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 +
574 720 // Include Media Optimization Module
575 721 if (!metasync_is_non_metasync_admin_ajax()) {
576 722 require_once plugin_dir_path(__FILE__) . 'media-optimization/media-optimization-loader.php';
577 723 }
@@ -620,9 +766,17 @@
620 766 echo '<script>window.__SA_DYO_INITIALIZED__=true;</script>' . "\n";
621 767 }
622 768 add_action('wp_head', 'metasync_output_dyo_init_flag', 1);
623 769
624 -// Runtime feature initialisers (GA4, API backoff, review notice, JWT accessor, debug mode) extracted to keep this entry file lean.
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';
774 +
775 +/**
776 + * Runtime feature initialisers (GA4, API backoff, review notice, JWT accessor,
777 + * debug mode) extracted to keep this entry file lean.
778 + */
625 779 require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-runtime-init.php';
626 780
627 781 /**
628 782 * Append a "Website Studio" post state to LPS-synced / MetaSync custom pages in