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 +184 -38 2.6.16 → 2.7.1 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.16
19 - * Requires PHP: 8.2
18 + * Version: 2.7.1
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
@@ -31,20 +31,41 @@
31 31
32 32 // Composer classmap autoloader — loads all plugin classes on demand.
33 33 require_once __DIR__ . '/vendor/autoload.php';
34 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 +
35 56 /**
36 57 * Currently plugin version.
37 58 * Start at version 1.0.0 and use SemVer - https://semver.org
38 59 * Rename this for your plugin and update it as you release new versions.
39 60 */
40 -$metasync_version = '2.6.16';
61 +$metasync_version = '2.7.1';
41 62 define('METASYNC_VERSION', preg_match('/^\d+\.\d+/', $metasync_version) ? $metasync_version : '9.9.9');
42 63 /**
43 64 * Define the current required php version
44 65 * This will be used to validate whether the user can install the plugin or not
45 66 */
46 -define('METASYNC_MIN_PHP', '8.2');
67 +define('METASYNC_MIN_PHP', '8.1');
47 68
48 69 /**
49 70 * Define the current required php version
50 71 * This will be used to validate whether the user can install the plugin or not
@@ -108,9 +129,9 @@
108 129 return $sanitized;
109 130 }
110 131 }
111 132
112 -// 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).
113 134 if (!function_exists('metasync_is_non_metasync_admin_ajax')) {
114 135 function metasync_is_non_metasync_admin_ajax() {
115 136 static $result = null;
116 137 if ($result !== null) {
@@ -115,13 +136,19 @@
115 136 static $result = null;
116 137 if ($result !== null) {
117 138 return $result;
118 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 + }
119 146 if (!defined('DOING_AJAX') || !DOING_AJAX) {
120 147 return ($result = false);
121 148 }
122 - $action = isset($_POST['action']) ? sanitize_text_field(wp_unslash($_POST['action']))
123 - : (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
124 151 if ($action === '') {
125 152 return ($result = true);
126 153 }
127 154 if (strpos($action, 'metasync_') === 0 || strpos($action, 'meta_sync_') === 0 || $action === 'sample-permalink') {
@@ -133,9 +160,9 @@
133 160
134 161 // Lazy-load guard: only initialise the MCP server when the request is actually targeting
135 162 // the MCP REST route (or its sibling SEO-inventory route, which depends on $metasync_mcp_server
136 163 // for its permission callback). Saves ~2-5 MB / 10-50 ms on the 99.9% of requests that never
137 -// touch MCP. See WP-255.
164 +// touch MCP.
138 165 if (!function_exists('metasync_is_mcp_rest_request')) {
139 166 function metasync_is_mcp_rest_request(): bool {
140 167 $uri = isset($_SERVER['REQUEST_URI']) ? (string) $_SERVER['REQUEST_URI'] : '';
141 168 if ($uri === '') {
@@ -161,8 +188,9 @@
161 188
162 189 // Phase 2 — these files have file-level side effects (add_action outside class body)
163 190 // and must remain as explicit require_once until refactored.
164 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';
165 193 require_once plugin_dir_path( __FILE__ ) . 'includes/class-metasync-error-logger.php';
166 194
167 195 // Shared helpers (custom/LPS page detection + query exclusion) — loaded in all
168 196 // request contexts (admin, REST, MCP, AJAX) so every SEO surface uses one rule.
@@ -167,8 +195,13 @@
167 195 // Shared helpers (custom/LPS page detection + query exclusion) — loaded in all
168 196 // request contexts (admin, REST, MCP, AJAX) so every SEO surface uses one rule.
169 197 require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-helpers.php';
170 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 +
171 204 /**
172 205 * Include the Otto Pixel Php Code
173 206 */
174 207 if (!metasync_is_non_metasync_admin_ajax()) {
@@ -261,9 +294,9 @@
261 294
262 295 // Set initial version
263 296 update_option('metasync_version', METASYNC_VERSION);
264 297
265 - // WP-315: Record activation timestamp so Divi CSS fix transients
298 + // Record activation timestamp so Divi CSS fix transients
266 299 // auto-invalidate after plugin deactivate/reactivate cycles.
267 300 update_option('metasync_activated_at', (string) time());
268 301
269 302 // Clear all cache plugins to ensure fresh start
@@ -268,9 +301,9 @@
268 301
269 302 // Clear all cache plugins to ensure fresh start
270 303 Metasync_Cache_Purge::purge_all('plugin_activation');
271 304
272 - // WP-332: Migrate physical sitemap files on activation.
305 + // Migrate physical sitemap files on activation.
273 306 metasync_migrate_physical_sitemaps();
274 307 }
275 308
276 309 // Log-sync removed - error monitoring now handled by Sentry
@@ -305,9 +338,9 @@
305 338 register_activation_hook(__FILE__, 'activate_metasync');
306 339 register_deactivation_hook(__FILE__, 'deactivate_metasync');
307 340
308 341 /**
309 - * WP-332: Migrate physical sitemap .xml files into transients and delete them.
342 + * Migrate physical sitemap .xml files into transients and delete them.
310 343 *
311 344 * Physical files in ABSPATH cause nginx/Plesk to 403 before WordPress can
312 345 * serve them. Called from both the activation hook and the version-gate
313 346 * migration so it covers fresh activations and auto-updates.
@@ -336,19 +369,90 @@
336 369 if (empty($candidates)) {
337 370 return;
338 371 }
339 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 + }
340 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 +
341 437 $migrated_files = [];
342 438
343 - foreach ($candidates as $file) {
439 + foreach ($ours as $file) {
344 440 $bn = basename($file);
345 441 $content = @file_get_contents($file);
346 442 if (false !== $content) {
347 443 $tkey = 'metasync_vsm_' . md5($bn);
348 - 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);
349 451 if (false !== get_transient($tkey)) {
350 - @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);
351 455 $virtual_index[$bn] = $tkey;
352 456 if ($bn !== 'sitemap_index.xml' && $bn !== 'news-sitemap.xml' && $bn !== 'video-sitemap.xml') {
353 457 $migrated_files[] = [
354 458 'filename' => $bn,
@@ -379,10 +483,17 @@
379 483
380 484 $current_version = get_option('metasync_version', '0.0.0');
381 485 $plugin_version = METASYNC_VERSION;
382 486
383 - // If versions don't match, run migration
384 - 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)) {
385 496 // Import whitelabel settings only if the JSON file is new or changed
386 497 // (prevents overwriting admin UI changes on every version check)
387 498 Metasync_Activator::check_whitelabel_settings_update();
388 499
@@ -409,9 +520,9 @@
409 520 update_option('metasync_options', $options);
410 521 }
411 522 }
412 523
413 - // WP-299: One-time purge of stale OTTO SEO cron backlog.
524 + // One-time purge of stale OTTO SEO cron backlog.
414 525 // Prior versions could accumulate thousands of metasync_process_seo_job and
415 526 // metasync_process_otto_crawl_url_job events due to unbounded rescheduling.
416 527 // Clear the backlog once on update; the new code prevents re-accumulation.
417 528 if (!get_option('metasync_wp299_cron_cleanup_done')) {
@@ -420,9 +531,39 @@
420 531 wp_unschedule_hook('metasync_process_otto_batch_cache_job');
421 532 update_option('metasync_wp299_cron_cleanup_done', true, false);
422 533 }
423 534
424 - // WP-332: Migrate physical sitemap files on version update.
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.
425 566 metasync_migrate_physical_sitemaps();
426 567
427 568 // Run full migration to ensure all tables are up to date
428 569 MetaSync_DBMigration::run_migrations();
@@ -454,9 +595,9 @@
454 595 if (!isset($hook_extra['type']) || $hook_extra['type'] !== 'plugin') {
455 596 return;
456 597 }
457 598
458 - // WP-427: By the time upgrader_process_complete fires, the upgrader may have
599 + // By the time upgrader_process_complete fires, the upgrader may have
459 600 // deleted the directory this (old, still-in-memory) copy of the plugin was
460 601 // loaded from — e.g. when the installed dir name differs from the package's
461 602 // root dir ('metasync-develop' vs 'metasync'). The Composer classmap then
462 603 // points at files that no longer exist, so autoloading Metasync_Activator
@@ -522,9 +663,9 @@
522 663 }
523 664 }
524 665
525 666 if ($should_import) {
526 - Metasync_Activator::check_whitelabel_settings_update();
667 + Metasync_Activator::check_whitelabel_settings_update(true);
527 668 }
528 669 }
529 670
530 671 // Hook into WordPress upgrader to detect plugin updates
@@ -533,26 +674,31 @@
533 674 /**
534 675 * Fallback: Check for whitelabel file changes on admin pages
535 676 * This handles edge cases where upgrader_process_complete doesn't fire
536 677 * (e.g., FTP uploads, manual file replacements)
537 - * Only checks once per admin session to minimize performance impact
678 + * Only checks once per admin request to minimize performance impact
538 679 */
539 -// function metasync_check_whitelabel_on_admin()
540 -// {
541 -// // Only check once per admin session to avoid overhead
542 -// static $checked = false;
543 -// if ($checked) {
544 -// return;
545 -// }
546 -// $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;
547 687
548 -// require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
549 -// Metasync_Activator::check_whitelabel_settings_update();
550 -// }
688 + require_once plugin_dir_path(__FILE__) . 'includes/class-metasync-activator.php';
551 689
552 -// Check for whitelabel changes on admin pages (fallback for edge cases)
553 -// 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'));
554 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 +
555 701 // Include Media Optimization Module
556 702 if (!metasync_is_non_metasync_admin_ajax()) {
557 703 require_once plugin_dir_path(__FILE__) . 'media-optimization/media-optimization-loader.php';
558 704 }
@@ -569,9 +715,9 @@
569 715 $plugin->run();
570 716 }
571 717 run_metasync();
572 718
573 -// WP-530: MCP server bootstrap (server + tool registration) extracted to keep this entry file lean.
719 +// MCP server bootstrap (server + tool registration) extracted to keep this entry file lean.
574 720 require_once plugin_dir_path( __FILE__ ) . 'includes/mcp-server-bootstrap.php';
575 721
576 722 /**
577 723 * Schedule a daily cron event to auto-purge Sync History records older than 90 days.
@@ -601,15 +747,15 @@
601 747 echo '<script>window.__SA_DYO_INITIALIZED__=true;</script>' . "\n";
602 748 }
603 749 add_action('wp_head', 'metasync_output_dyo_init_flag', 1);
604 750
605 -// WP-530: Runtime feature initialisers (GA4, API backoff, review notice, JWT accessor, debug mode) extracted to keep this entry file lean.
751 +// Runtime feature initialisers (GA4, API backoff, review notice, JWT accessor, debug mode) extracted to keep this entry file lean.
606 752 require_once plugin_dir_path( __FILE__ ) . 'includes/metasync-runtime-init.php';
607 753
608 754 /**
609 755 * Append a "Website Studio" post state to LPS-synced / MetaSync custom pages in
610 756 * the admin Pages list, so site owners can tell at a glance which pages are
611 - * managed by Website Studio and shouldn't be hand-edited (WP-491).
757 + * managed by Website Studio and shouldn't be hand-edited.
612 758 *
613 759 * Hooks WordPress core's display_post_states filter — the same mechanism that
614 760 * renders the grey inline tags like "— Front Page" / "— Draft" — so the label
615 761 * is native-styled and only appears next to relevant page titles, with no