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 | includes/class-metasync.php +81 -20 2.6.22 → 2.7.1 View file →
@@ -161,11 +161,21 @@
161 161 // Procedural init file — not a class, must stay explicit.
162 162 if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
163 163 require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
164 164 } else {
165 - error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php');
165 + error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
166 166 }
167 167
168 + // The SEO precedence resolver is referenced statically from the render
169 + // filters, the sync layer and four admin screens. Require it explicitly
170 + // for the same reason as the admin navigation below: a partial update can
171 + // leave newer PHP files beside an older committed autoload classmap, and
172 + // a missing class on wp_head or the posts list is a fatal, not a
173 + // degradation.
174 + if (!class_exists('Metasync_Seo_Precedence')) {
175 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-precedence.php';
176 + }
177 +
168 178 // Admin navigation is referenced statically from frontend-reachable
169 179 // includes (heartbeat/connect managers). Require it explicitly here so
170 180 // the static call never fatals when wp_head fires before autoload.
171 181 if (!class_exists('Metasync_Admin_Navigation')) {
@@ -171,9 +181,17 @@
171 181 if (!class_exists('Metasync_Admin_Navigation')) {
172 182 require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-admin-navigation.php';
173 183 }
174 184
185 + // Hooked on init at priority 0, before any theme or plugin callback
186 + // runs, so it cannot wait for a lazy autoload. See define_public_hooks.
187 + if (!class_exists('Metasync_BookingPress_Compat')) {
188 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-bookingpress-compat.php';
189 + }
190 +
175 191 $this->loader = new Metasync_Loader();
192 + // Install aggregate-option password protection in every runtime context.
193 + Metasync_Settings_Registration::instance();
176 194 $this->db_heartbeat_errors = new Metasync_HeartBeat_Error_Monitor_Database();
177 195 $this->db_redirection = new Metasync_Redirection_Database();
178 196 }
179 197
@@ -261,16 +279,12 @@
261 279 $this->loader->add_action('wp_ajax_metasync_generate_connect_url', $plugin_admin, 'generate_searchatlas_connect_url');
262 280 $this->loader->add_action('wp_ajax_metasync_check_connect_status', $plugin_admin, 'check_searchatlas_connect_status');
263 281 $this->loader->add_action('wp_ajax_metasync_reset_authentication', $plugin_admin, 'reset_searchatlas_authentication');
264 282
265 - // Auto-update filter
266 - $this->loader->add_filter('auto_update_plugin', $plugin_admin, 'control_plugin_auto_updates', 10, 2);
267 -
268 283 // Search Atlas Connect development/testing endpoints
269 284 $this->loader->add_action('wp_ajax_metasync_test_enhanced_tokens', $plugin_admin, 'test_enhanced_searchatlas_tokens');
270 285 $this->loader->add_action('wp_ajax_metasync_test_whitelabel_domain', $plugin_admin, 'test_whitelabel_domain');
271 286 $this->loader->add_action('wp_ajax_metasync_test_ajax_endpoint', $plugin_admin, 'test_searchatlas_ajax_endpoint');
272 - $this->loader->add_action('wp_ajax_metasync_simple_ajax_test', $plugin_admin, 'simple_ajax_test');
273 287
274 288
275 289 $post_meta_setting = new Metasync_Post_Meta_Settings();
276 290 $this->loader->add_action('admin_init', $post_meta_setting, 'add_post_meta_data', 2);
@@ -286,11 +300,11 @@
286 300
287 301 // SEO Health CSV export: must run on admin_init (before output).
288 302 // Cheap $_GET check avoids loading the class on every admin page.
289 303 if (
290 - isset($_GET['page'], $_GET['export'], $_GET['_wpnonce']) &&
291 - $_GET['export'] === 'csv' &&
292 - strpos($_GET['page'], '-seo-health') !== false
304 + isset($_GET['page'], $_GET['export'], $_GET['_wpnonce']) && // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing check, the handler verifies nonce and capability
305 + $_GET['export'] === 'csv' && // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing check, the handler verifies nonce and capability
306 + strpos($_GET['page'], '-seo-health') !== false // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing check, the handler verifies nonce and capability
293 307 ) {
294 308 $this->loader->add_action('admin_init', Metasync_SEO_Health::get_instance(), 'handle_csv_export', 1);
295 309 }
296 310 $this->loader->add_action('wp', $post_meta_setting, 'show_top_admin_bar', 9);
@@ -363,9 +377,12 @@
363 377
364 378 $plugin_public = new Metasync_Public($this->get_plugin_name(), $this->get_version());
365 379 $rest_api = $plugin_public->get_rest_api();
366 380 $seo_output = $plugin_public->get_seo_output();
367 - $get_plugin_basename = sprintf('%1$s/%1$s.php', $this->plugin_name);
381 + // Derive the basename from the actual file location: the plugin
382 + // folder is not guaranteed to be named after the plugin (renamed
383 + // installs), and a hardcoded name makes the settings links vanish.
384 + $get_plugin_basename = plugin_basename(dirname(__DIR__) . '/metasync.php');
368 385
369 386 // Asset enqueue hooks (Metasync_Public)
370 387 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_styles');
371 388 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_scripts');
@@ -370,8 +387,12 @@
370 387 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_styles');
371 388 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_scripts');
372 389 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_page_custom_css', 999);
373 390
391 + // Registered here rather than from inside enqueue_scripts(), which would
392 + // re-register the same filter on every front-end request.
393 + $this->loader->add_filter('script_loader_tag', $plugin_public, 'add_defer_attribute', 10, 2);
394 +
374 395 // Elementor editor CSS injection
375 396 if (class_exists('\Elementor\Plugin')) {
376 397 $this->loader->add_action('elementor/preview/enqueue_styles', $plugin_public, 'enqueue_elementor_editor_css', 999);
377 398 }
@@ -384,8 +405,21 @@
384 405 // Initialize centralized SEO conflict handler (singleton — suppresses
385 406 // third-party SEO plugin descriptions when MetaSync provides its own).
386 407 Metasync_SEO_Conflict_Handler::get_instance();
387 408
409 + // Headless delivery: register the GraphQL SEO field. This only adds a
410 + // callback on `graphql_register_types`, which never fires unless WPGraphQL
411 + // is active, and the callback returns immediately unless Headless Mode is
412 + // on — so with the mode off nothing about this request changes.
413 + Metasync_Headless_Graphql::init();
414 +
415 + // Headless stale-while-revalidate OTTO refresh: background safety net for
416 + // missed/delayed OTTO deployment webhooks. Every branch is gated on
417 + // Metasync_Headless_Config::is_active(), so with headless mode off this
418 + // schedules no cron event and registers no route beyond an inert REST
419 + // callback that itself checks the flag before doing anything.
420 + Metasync_Headless_Refresh_Job::init();
421 +
388 422 // Term-level SEO plugin sync: propagate MetaSync term meta (category/tag
389 423 // archives) into Yoast/Rank Math/AIOSEO term storage on every write.
390 424 $this->loader->add_action('updated_term_meta', $this, 'on_term_meta_updated', 10, 4);
391 425 $this->loader->add_action('added_term_meta', $this, 'on_term_meta_updated', 10, 4);
@@ -403,15 +437,28 @@
403 437 // LocalBusiness / Organization / Person JSON-LD from the Local Business page.
404 438 $this->loader->add_action('wp_head', $seo_output, 'output_local_business_schema', 2, 1);
405 439 $this->loader->add_action('template_redirect', $seo_output, 'inject_archive_seo_controls');
406 440
407 - // Hreflang / language alternates output (wp_head @ priority 2).
408 - $plugin_hreflang = new Metasync_Hreflang_Output();
441 + // Hreflang / language alternates output (wp_head @ priority 2). The
442 + // emitter shares the SEO output instance for the noindex check.
443 + $plugin_hreflang = new Metasync_Hreflang_Output($seo_output);
409 444 $this->loader->add_action('wp_head', $plugin_hreflang, 'output_hreflang_tags', 2);
445 + // WPML prints its own hreflang set at wp_head priority 1, ahead of
446 + // MetaSync's output at priority 2 — the suppression filter has to be
447 + // in place before wp_head starts, hence template_redirect.
448 + $this->loader->add_action('template_redirect', $plugin_hreflang, 'register_wpml_suppression', 1);
410 449
411 450 // Edge Cache: detect Cloudways Varnish and persist for settings UI
412 451 $this->loader->add_action('init', 'Metasync_Edge_Cache_Purge', 'detect_cloudways');
413 452
453 + // BookingPress starts a PHP session on every front-end request (init
454 + // at priority 1), which makes hosts skip page caching site-wide. Its
455 + // booking flows manage their own sessions inside their AJAX handlers,
456 + // so unhook the global start. Priority 0 keeps this ahead of theirs.
457 + // Registered with add_action directly (like Metasync_Oxygen_Compat)
458 + // because the loader's $component parameter is typed to objects.
459 + add_action('init', ['Metasync_BookingPress_Compat', 'neutralize_bookingpress_session'], 0);
460 +
414 461 // Sitemap exclusions for disabled archive types
415 462 $this->loader->add_filter('wp_sitemaps_taxonomies', $seo_output, 'filter_sitemap_taxonomies');
416 463 $this->loader->add_filter('wp_sitemaps_users_entry', $seo_output, 'filter_sitemap_users', 10, 2);
417 464 $this->loader->add_filter('wp_sitemaps_add_provider', $seo_output, 'filter_sitemap_providers', 10, 2);
@@ -503,8 +550,22 @@
503 550 // on read-only web roots and nginx hosts that 403 direct static .txt access.
504 551 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
505 552 add_action('template_redirect', array('Metasync_Bing_Instant_Index', 'serve_virtual_key_file'), 0);
506 553
554 + // Scheduled IndexNow submissions, deferred from save_post so a slow
555 + // IndexNow round-trip cannot stall the editor or REST write. Registered
556 + // statically because the class is loaded but never instantiated on
557 + // cron loads. The hook name matches the uninstall cron sweep exactly.
558 + add_action('metasync_bing_indexnow_submit_event', array('Metasync_Bing_Instant_Index', 'run_scheduled_submit'), 10, 1);
559 +
560 + // Construct the robots.txt manager on the public load path so its
561 + // `robots_txt` filter is registered before a plain GET /robots.txt is
562 + // served. The filter is attached in the singleton's constructor, and
563 + // previously only admin screens built the instance — so the virtual
564 + // robots.txt rules and sitemap lines never rendered on the frontend.
565 + require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt.php';
566 + Metasync_Robots_Txt::get_instance();
567 +
507 568 // One-time upgrade: regenerate sitemap to remove any Beaver Builder template entries
508 569 if ( ! get_option( 'metasync_sitemap_bb_exclusion_applied' ) ) {
509 570 $this->loader->add_action('init', $this, 'maybe_regenerate_sitemap_after_upgrade');
510 571 }
@@ -583,9 +644,9 @@
583 644 $original_url = $url;
584 645 foreach ($url_replacements as $production => $staging) {
585 646 if (strpos($url, $production) === 0) {
586 647 $url = str_replace($production, $staging, $url);
587 - error_log("MetaSync Endpoint Filter: Replaced {$production} with {$staging} in URL: {$original_url}");
648 + error_log("MetaSync Endpoint Filter: Replaced {$production} with {$staging} in URL: {$original_url}"); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- debug-gated, no secrets
588 649 break;
589 650 }
590 651 }
591 652
@@ -869,9 +930,9 @@
869 930 }
870 931
871 932 if (!function_exists('openssl_encrypt')) {
872 933 // OpenSSL unavailable — store plaintext rather than lose the key.
873 - error_log('MetaSync: OpenSSL is unavailable — the Search Atlas API key was stored WITHOUT encryption at rest.');
934 + error_log('MetaSync: OpenSSL is unavailable — the Search Atlas API key was stored WITHOUT encryption at rest.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
874 935 return $plaintext;
875 936 }
876 937
877 938 $key = self::api_key_crypto_key();
@@ -880,9 +941,9 @@
880 941 $ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16);
881 942
882 943 if ($ciphertext === false) {
883 944 // Encryption failed — fall back to plaintext storage, but say so.
884 - error_log('MetaSync: API key encryption failed — the Search Atlas API key was stored WITHOUT encryption at rest.');
945 + error_log('MetaSync: API key encryption failed — the Search Atlas API key was stored WITHOUT encryption at rest.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
885 946 return $plaintext;
886 947 }
887 948
888 949 return self::API_KEY_ENC_PREFIX . base64_encode($iv . $tag . $ciphertext);
@@ -1089,9 +1150,9 @@
1089 1150 }
1090 1151
1091 1152 if (!function_exists('openssl_encrypt')) {
1092 1153 // OpenSSL unavailable — store plaintext rather than lose the secret.
1093 - error_log('MetaSync: OpenSSL is unavailable — a secret was stored WITHOUT encryption at rest.');
1154 + error_log('MetaSync: OpenSSL is unavailable — a secret was stored WITHOUT encryption at rest.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
1094 1155 return $plaintext;
1095 1156 }
1096 1157
1097 1158 $key = self::secret_crypto_key();
@@ -1100,9 +1161,9 @@
1100 1161 $ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16);
1101 1162
1102 1163 if ($ciphertext === false) {
1103 1164 // Encryption failed — fall back to plaintext storage, but say so.
1104 - error_log('MetaSync: Secret encryption failed — a secret was stored WITHOUT encryption at rest.');
1165 + error_log('MetaSync: Secret encryption failed — a secret was stored WITHOUT encryption at rest.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
1105 1166 return $plaintext;
1106 1167 }
1107 1168
1108 1169 return self::SECRET_ENC_PREFIX . base64_encode($iv . $tag . $ciphertext);
@@ -1527,9 +1588,9 @@
1527 1588 // This could be extended to store in a dedicated log table
1528 1589
1529 1590 } catch (Exception $e) {
1530 1591 // Fallback logging if structured logging fails
1531 - error_log('MetaSync API Key Event Logging Error: ' . $e->getMessage());
1592 + error_log('MetaSync API Key Event Logging Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
1532 1593 }
1533 1594 }
1534 1595
1535 1596 /**
@@ -1593,9 +1654,9 @@
1593 1654 $global_hits = get_transient($global_rate_key);
1594 1655 if ($global_hits !== false && $global_hits >= 50) {
1595 1656 // More than 50 404s per minute - stop logging to protect database
1596 1657 if ($global_hits === 50) {
1597 - error_log('MetaSync 404 Monitor: Rate limit exceeded - 50+ 404s per minute. Pausing logging.');
1658 + error_log('MetaSync 404 Monitor: Rate limit exceeded - 50+ 404s per minute. Pausing logging.'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
1598 1659 }
1599 1660 set_transient($global_rate_key, $global_hits + 1, 60);
1600 1661 return;
1601 1662 }
@@ -1620,9 +1681,9 @@
1620 1681 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
1621 1682 $db_404 = new Metasync_Error_Monitor_Database();
1622 1683
1623 1684 // Get user agent (sanitized)
1624 - $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field($_SERVER['HTTP_USER_AGENT']) : '';
1685 + $user_agent = isset($_SERVER['HTTP_USER_AGENT']) ? sanitize_text_field(wp_unslash($_SERVER['HTTP_USER_AGENT'])) : '';
1625 1686
1626 1687 // Log the 404 error
1627 1688 $result = $db_404->update([
1628 1689 'uri' => $current_url,
@@ -1648,9 +1709,9 @@
1648 1709 $host = $_SERVER['SERVER_NAME'];
1649 1710 }
1650 1711 if (empty($host)) {
1651 1712 // Fallback to WordPress site URL if available
1652 - $host = parse_url(home_url(), PHP_URL_HOST);
1713 + $host = wp_parse_url(home_url(), PHP_URL_HOST);
1653 1714 }
1654 1715
1655 1716 // Safely get REQUEST_URI with fallback
1656 1717 $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';