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 +41 -20 2.6.26 → 2.7.1 View file →
@@ -161,9 +161,9 @@
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 168 // The SEO precedence resolver is referenced statically from the render
169 169 // filters, the sync layer and four admin screens. Require it explicitly
@@ -279,16 +279,12 @@
279 279 $this->loader->add_action('wp_ajax_metasync_generate_connect_url', $plugin_admin, 'generate_searchatlas_connect_url');
280 280 $this->loader->add_action('wp_ajax_metasync_check_connect_status', $plugin_admin, 'check_searchatlas_connect_status');
281 281 $this->loader->add_action('wp_ajax_metasync_reset_authentication', $plugin_admin, 'reset_searchatlas_authentication');
282 282
283 - // Auto-update filter
284 - $this->loader->add_filter('auto_update_plugin', $plugin_admin, 'control_plugin_auto_updates', 10, 2);
285 -
286 283 // Search Atlas Connect development/testing endpoints
287 284 $this->loader->add_action('wp_ajax_metasync_test_enhanced_tokens', $plugin_admin, 'test_enhanced_searchatlas_tokens');
288 285 $this->loader->add_action('wp_ajax_metasync_test_whitelabel_domain', $plugin_admin, 'test_whitelabel_domain');
289 286 $this->loader->add_action('wp_ajax_metasync_test_ajax_endpoint', $plugin_admin, 'test_searchatlas_ajax_endpoint');
290 - $this->loader->add_action('wp_ajax_metasync_simple_ajax_test', $plugin_admin, 'simple_ajax_test');
291 287
292 288
293 289 $post_meta_setting = new Metasync_Post_Meta_Settings();
294 290 $this->loader->add_action('admin_init', $post_meta_setting, 'add_post_meta_data', 2);
@@ -304,11 +300,11 @@
304 300
305 301 // SEO Health CSV export: must run on admin_init (before output).
306 302 // Cheap $_GET check avoids loading the class on every admin page.
307 303 if (
308 - isset($_GET['page'], $_GET['export'], $_GET['_wpnonce']) &&
309 - $_GET['export'] === 'csv' &&
310 - 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
311 307 ) {
312 308 $this->loader->add_action('admin_init', Metasync_SEO_Health::get_instance(), 'handle_csv_export', 1);
313 309 }
314 310 $this->loader->add_action('wp', $post_meta_setting, 'show_top_admin_bar', 9);
@@ -381,9 +377,12 @@
381 377
382 378 $plugin_public = new Metasync_Public($this->get_plugin_name(), $this->get_version());
383 379 $rest_api = $plugin_public->get_rest_api();
384 380 $seo_output = $plugin_public->get_seo_output();
385 - $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');
386 385
387 386 // Asset enqueue hooks (Metasync_Public)
388 387 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_styles');
389 388 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_scripts');
@@ -388,8 +387,12 @@
388 387 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_styles');
389 388 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_scripts');
390 389 $this->loader->add_action('wp_enqueue_scripts', $plugin_public, 'enqueue_page_custom_css', 999);
391 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 +
392 395 // Elementor editor CSS injection
393 396 if (class_exists('\Elementor\Plugin')) {
394 397 $this->loader->add_action('elementor/preview/enqueue_styles', $plugin_public, 'enqueue_elementor_editor_css', 999);
395 398 }
@@ -402,8 +405,21 @@
402 405 // Initialize centralized SEO conflict handler (singleton — suppresses
403 406 // third-party SEO plugin descriptions when MetaSync provides its own).
404 407 Metasync_SEO_Conflict_Handler::get_instance();
405 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 +
406 422 // Term-level SEO plugin sync: propagate MetaSync term meta (category/tag
407 423 // archives) into Yoast/Rank Math/AIOSEO term storage on every write.
408 424 $this->loader->add_action('updated_term_meta', $this, 'on_term_meta_updated', 10, 4);
409 425 $this->loader->add_action('added_term_meta', $this, 'on_term_meta_updated', 10, 4);
@@ -421,11 +437,16 @@
421 437 // LocalBusiness / Organization / Person JSON-LD from the Local Business page.
422 438 $this->loader->add_action('wp_head', $seo_output, 'output_local_business_schema', 2, 1);
423 439 $this->loader->add_action('template_redirect', $seo_output, 'inject_archive_seo_controls');
424 440
425 - // Hreflang / language alternates output (wp_head @ priority 2).
426 - $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);
427 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);
428 449
429 450 // Edge Cache: detect Cloudways Varnish and persist for settings UI
430 451 $this->loader->add_action('init', 'Metasync_Edge_Cache_Purge', 'detect_cloudways');
431 452
@@ -623,9 +644,9 @@
623 644 $original_url = $url;
624 645 foreach ($url_replacements as $production => $staging) {
625 646 if (strpos($url, $production) === 0) {
626 647 $url = str_replace($production, $staging, $url);
627 - 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
628 649 break;
629 650 }
630 651 }
631 652
@@ -909,9 +930,9 @@
909 930 }
910 931
911 932 if (!function_exists('openssl_encrypt')) {
912 933 // OpenSSL unavailable — store plaintext rather than lose the key.
913 - 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
914 935 return $plaintext;
915 936 }
916 937
917 938 $key = self::api_key_crypto_key();
@@ -920,9 +941,9 @@
920 941 $ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16);
921 942
922 943 if ($ciphertext === false) {
923 944 // Encryption failed — fall back to plaintext storage, but say so.
924 - 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
925 946 return $plaintext;
926 947 }
927 948
928 949 return self::API_KEY_ENC_PREFIX . base64_encode($iv . $tag . $ciphertext);
@@ -1129,9 +1150,9 @@
1129 1150 }
1130 1151
1131 1152 if (!function_exists('openssl_encrypt')) {
1132 1153 // OpenSSL unavailable — store plaintext rather than lose the secret.
1133 - 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
1134 1155 return $plaintext;
1135 1156 }
1136 1157
1137 1158 $key = self::secret_crypto_key();
@@ -1140,9 +1161,9 @@
1140 1161 $ciphertext = openssl_encrypt($plaintext, 'aes-256-gcm', $key, OPENSSL_RAW_DATA, $iv, $tag, '', 16);
1141 1162
1142 1163 if ($ciphertext === false) {
1143 1164 // Encryption failed — fall back to plaintext storage, but say so.
1144 - 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
1145 1166 return $plaintext;
1146 1167 }
1147 1168
1148 1169 return self::SECRET_ENC_PREFIX . base64_encode($iv . $tag . $ciphertext);
@@ -1567,9 +1588,9 @@
1567 1588 // This could be extended to store in a dedicated log table
1568 1589
1569 1590 } catch (Exception $e) {
1570 1591 // Fallback logging if structured logging fails
1571 - 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
1572 1593 }
1573 1594 }
1574 1595
1575 1596 /**
@@ -1633,9 +1654,9 @@
1633 1654 $global_hits = get_transient($global_rate_key);
1634 1655 if ($global_hits !== false && $global_hits >= 50) {
1635 1656 // More than 50 404s per minute - stop logging to protect database
1636 1657 if ($global_hits === 50) {
1637 - 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
1638 1659 }
1639 1660 set_transient($global_rate_key, $global_hits + 1, 60);
1640 1661 return;
1641 1662 }
@@ -1660,9 +1681,9 @@
1660 1681 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
1661 1682 $db_404 = new Metasync_Error_Monitor_Database();
1662 1683
1663 1684 // Get user agent (sanitized)
1664 - $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'])) : '';
1665 1686
1666 1687 // Log the 404 error
1667 1688 $result = $db_404->update([
1668 1689 'uri' => $current_url,
@@ -1688,9 +1709,9 @@
1688 1709 $host = $_SERVER['SERVER_NAME'];
1689 1710 }
1690 1711 if (empty($host)) {
1691 1712 // Fallback to WordPress site URL if available
1692 - $host = parse_url(home_url(), PHP_URL_HOST);
1713 + $host = wp_parse_url(home_url(), PHP_URL_HOST);
1693 1714 }
1694 1715
1695 1716 // Safely get REQUEST_URI with fallback
1696 1717 $uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/';