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 +123 -12 2.6.26 → 2.7.2 View file →
@@ -11,12 +11,12 @@
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.26
18 + * Version: 2.7.2
19 19 * Requires PHP: 8.1
20 20 * Author: Search Atlas
21 21 * Author URI: https://searchatlas.com
22 22 * License: GPL v3
@@ -41,14 +41,25 @@
41 41 // which can run before the classmap has resolved anything else.
42 42 require_once __DIR__ . '/includes/class-metasync-feature-flags.php';
43 43 Metasync_Feature_Flags::register_invalidation();
44 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 +
45 56 /**
46 57 * Currently plugin version.
47 58 * Start at version 1.0.0 and use SemVer - https://semver.org
48 59 * Rename this for your plugin and update it as you release new versions.
49 60 */
50 -$metasync_version = '2.6.26';
61 +$metasync_version = '2.7.2';
51 62 define('METASYNC_VERSION', preg_match('/^\d+\.\d+/', $metasync_version) ? $metasync_version : '9.9.9');
52 63 /**
53 64 * Define the current required php version
54 65 * This will be used to validate whether the user can install the plugin or not
@@ -84,8 +95,18 @@
84 95 define('METASYNC_GA4_MEASUREMENT_ID', 'G-SBLWW1EMTJ');
85 96 define('METASYNC_GA4_API_SECRET', 'nMGs22mxQ3qVUy-aInqfZA');
86 97
87 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 +/**
88 109 * Define whether to show the plugin status in WordPress admin top navigation bar
89 110 * Set to false to hide the status indicator
90 111 */
91 112 define('METASYNC_SHOW_ADMIN_BAR_STATUS', true);
@@ -134,10 +155,10 @@
134 155 }
135 156 if (!defined('DOING_AJAX') || !DOING_AJAX) {
136 157 return ($result = false);
137 158 }
138 - $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action']))
139 - : (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
140 161 if ($action === '') {
141 162 return ($result = true);
142 163 }
143 164 if (strpos($action, 'metasync_') === 0 || strpos($action, 'meta_sync_') === 0 || $action === 'sample-permalink') {
@@ -177,8 +198,9 @@
177 198
178 199 // Phase 2 — these files have file-level side effects (add_action outside class body)
179 200 // and must remain as explicit require_once until refactored.
180 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';
181 203 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-error-logger.php';
182 204
183 205 // Shared helpers (custom/LPS page detection + query exclusion) — loaded in all
184 206 // request contexts (admin, REST, MCP, AJAX) so every SEO surface uses one rule.
@@ -241,9 +263,9 @@
241 263 #craft the message
242 264 sprintf(
243 265 '%s requires WordPress version %s or later. You are currently using version %s. Please update WordPress to activate this plugin.',
244 266 esc_html($plugin_name),
245 - METASYNC_MIN_WP,
267 + esc_html(METASYNC_MIN_WP),
246 268 esc_html($wp_version)
247 269 ),
248 270
249 271 #the plugin title as page title
@@ -263,9 +285,9 @@
263 285 #craft the message
264 286 sprintf(
265 287 '%s requires PHP version %s or later. You are currently using version %s. Please update PHP to activate this plugin.',
266 288 esc_html($plugin_name),
267 - METASYNC_MIN_PHP,
289 + esc_html(METASYNC_MIN_PHP),
268 290 esc_html($php_version)
269 291 ),
270 292
271 293 #the plugin title as page title
@@ -291,8 +313,12 @@
291 313 Metasync_Cache_Purge::purge_all('plugin_activation');
292 314
293 315 // Migrate physical sitemap files on activation.
294 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());
295 321 }
296 322
297 323 // Log-sync removed - error monitoring now handled by Sentry
298 324 // require_once plugin_dir_path(__FILE__) . 'log-sync/log-sync.php';
@@ -357,12 +383,75 @@
357 383 if (empty($candidates)) {
358 384 return;
359 385 }
360 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 + }
361 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 +
362 451 $migrated_files = [];
363 452
364 - foreach ($candidates as $file) {
453 + foreach ($ours as $file) {
365 454 $bn = basename($file);
366 455 $content = @file_get_contents($file);
367 456 if (false !== $content) {
368 457 $tkey = 'metasync_vsm_' . md5($bn);
@@ -373,9 +462,11 @@
373 462 // it with a fresh date_query when this expires.
374 463 $ttl = ('news-sitemap.xml' === $bn) ? (int) (DAY_IN_SECONDS / 4) : 30 * DAY_IN_SECONDS;
375 464 set_transient($tkey, $content, $ttl);
376 465 if (false !== get_transient($tkey)) {
377 - @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);
378 469 $virtual_index[$bn] = $tkey;
379 470 if ($bn !== 'sitemap_index.xml' && $bn !== 'news-sitemap.xml' && $bn !== 'video-sitemap.xml') {
380 471 $migrated_files[] = [
381 472 'filename' => $bn,
@@ -406,10 +497,17 @@
406 497
407 498 $current_version = get_option('metasync_version', '0.0.0');
408 499 $plugin_version = METASYNC_VERSION;
409 500
410 - // If versions don't match, run migration
411 - 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)) {
412 510 // Import whitelabel settings only if the JSON file is new or changed
413 511 // (prevents overwriting admin UI changes on every version check)
414 512 Metasync_Activator::check_whitelabel_settings_update();
415 513
@@ -415,8 +513,13 @@
415 513
416 514 // Run version-specific migrations first
417 515 MetaSync_DBMigration::run_version_migrations($current_version, $plugin_version);
418 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 +
419 522 // Migration for v2.7.0+: Remove AI Agent, switch to plugin auth token, make MCP always-on
420 523 if (version_compare($current_version, '2.7.0', '<')) {
421 524 // Remove old MCP API key option
422 525 delete_option('metasync_mcp_api_key');
@@ -663,9 +766,17 @@
663 766 echo '<script>window.__SA_DYO_INITIALIZED__=true;</script>' . "\n";
664 767 }
665 768 add_action('wp_head', 'metasync_output_dyo_init_flag', 1);
666 769
667 -// 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 + */
668 779 require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-runtime-init.php';
669 780
670 781 /**
671 782 * Append a "Website Studio" post state to LPS-synced / MetaSync custom pages in