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 +147 -25 2.6.23 → 2.7.1 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.23
18 + * Version: 2.7.1
19 19 * Requires PHP: 8.1
20 20 * Author: Search Atlas
21 21 * Author URI: https://searchatlas.com
22 22 * License: GPL v3
@@ -35,14 +35,31 @@
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.23';
61 +$metasync_version = '2.7.1';
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
@@ -119,13 +136,19 @@
119 136 static $result = null;
120 137 if ($result !== null) {
121 138 return $result;
122 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 + }
123 146 if (!defined('DOING_AJAX') || !DOING_AJAX) {
124 147 return ($result = false);
125 148 }
126 - $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action']))
127 - : (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
128 151 if ($action === '') {
129 152 return ($result = true);
130 153 }
131 154 if (strpos($action, 'metasync_') === 0 || strpos($action, 'meta_sync_') === 0 || $action === 'sample-permalink') {
@@ -165,8 +188,9 @@
165 188
166 189 // Phase 2 — these files have file-level side effects (add_action outside class body)
167 190 // and must remain as explicit require_once until refactored.
168 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';
169 193 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-error-logger.php';
170 194
171 195 // Shared helpers (custom/LPS page detection + query exclusion) — loaded in all
172 196 // request contexts (admin, REST, MCP, AJAX) so every SEO surface uses one rule.
@@ -345,19 +369,90 @@
345 369 if (empty($candidates)) {
346 370 return;
347 371 }
348 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 + }
349 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 +
350 437 $migrated_files = [];
351 438
352 - foreach ($candidates as $file) {
439 + foreach ($ours as $file) {
353 440 $bn = basename($file);
354 441 $content = @file_get_contents($file);
355 442 if (false !== $content) {
356 443 $tkey = 'metasync_vsm_' . md5($bn);
357 - set_transient($tkey, $content, 30 * DAY_IN_SECONDS);
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);
358 451 if (false !== get_transient($tkey)) {
359 - @unlink($file);
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);
360 455 $virtual_index[$bn] = $tkey;
361 456 if ($bn !== 'sitemap_index.xml' && $bn !== 'news-sitemap.xml' && $bn !== 'video-sitemap.xml') {
362 457 $migrated_files[] = [
363 458 'filename' => $bn,
@@ -388,10 +483,17 @@
388 483
389 484 $current_version = get_option('metasync_version', '0.0.0');
390 485 $plugin_version = METASYNC_VERSION;
391 486
392 - // If versions don't match, run migration
393 - 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)) {
394 496 // Import whitelabel settings only if the JSON file is new or changed
395 497 // (prevents overwriting admin UI changes on every version check)
396 498 Metasync_Activator::check_whitelabel_settings_update();
397 499
@@ -444,8 +546,23 @@
444 546 MetaSync_DBMigration::cleanup_corrupted_canonicals();
445 547 update_option('metasync_canonical_cleanup_done', 'done', false);
446 548 }
447 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 +
448 565 // Migrate physical sitemap files on version update.
449 566 metasync_migrate_physical_sitemaps();
450 567
451 568 // Run full migration to ensure all tables are up to date
@@ -546,9 +663,9 @@
546 663 }
547 664 }
548 665
549 666 if ($should_import) {
550 - Metasync_Activator::check_whitelabel_settings_update();
667 + Metasync_Activator::check_whitelabel_settings_update(true);
551 668 }
552 669 }
553 670
554 671 // Hook into WordPress upgrader to detect plugin updates
@@ -557,25 +674,30 @@
557 674 /**
558 675 * Fallback: Check for whitelabel file changes on admin pages
559 676 * This handles edge cases where upgrader_process_complete doesn't fire
560 677 * (e.g., FTP uploads, manual file replacements)
561 - * Only checks once per admin session to minimize performance impact
678 + * Only checks once per admin request to minimize performance impact
562 679 */
563 -// function metasync_check_whitelabel_on_admin()
564 -// {
565 -// // Only check once per admin session to avoid overhead
566 -// static $checked = false;
567 -// if ($checked) {
568 -// return;
569 -// }
570 -// $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;
571 687
572 -// require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
573 -// Metasync_Activator::check_whitelabel_settings_update();
574 -// }
688 + require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
575 689
576 -// Check for whitelabel changes on admin pages (fallback for edge cases)
577 -// 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'));
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);
578 700
579 701 // Include Media Optimization Module
580 702 if (!metasync_is_non_metasync_admin_ajax()) {
581 703 require_once plugin_dir_path(__FILE__) . 'media-optimization/media-optimization-loader.php';