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 | includes/class-metasync-compatibility-checker.php +196 -51 2.6.11 → 2.7.2 View file →
@@ -38,8 +38,36 @@
38 38
39 39 <div class="dashboard-card">
40 40 <?php $this->render_compatibility_sections(); ?>
41 41 </div>
42 +
43 + <?php
44 + # Plugin-compatibility toggles. Registered under the
45 + # <page_slug>_compatibility settings page only while the relevant
46 + # plugin is active, so the card self-hides when neither plugin is
47 + # present. Saves through options.php with a hidden active_tab so
48 + # the General-tab sanitizer blocks stay inert.
49 + $has_compat_fields = defined('WP_ROCKET_VERSION')
50 + || class_exists('WP_Rocket')
51 + || class_exists('SiteGround_Optimizer\Parser\Parser')
52 + || defined('SiteGround_Optimizer\VERSION')
53 + || class_exists('bookingpress_spam_protection');
54 + if ($has_compat_fields) :
55 + settings_errors();
56 + ?>
57 + <div class="dashboard-card" style="margin-top: 20px;">
58 + <h2 style="margin: 0 0 4px 0;">Plugin Compatibility Settings</h2>
59 + <p style="color: var(--dashboard-text-secondary); margin: 0 0 20px 0;">Control how <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> interacts with detected plugins. Each setting appears only while its plugin is active.</p>
60 + <form method="post" action="options.php">
61 + <input type="hidden" name="active_tab" value="compatibility" />
62 + <?php
63 + settings_fields(Metasync_Admin::option_group);
64 + do_settings_sections(Metasync_Admin::$page_slug . '_compatibility');
65 + submit_button();
66 + ?>
67 + </form>
68 + </div>
69 + <?php endif; ?>
42 70
43 71 <!-- Section: Host Blocking Test -->
44 72 <div id="ms-comp-host-test" class="dashboard-card" style="margin-top: 20px;">
45 73 <details open>
@@ -58,8 +86,9 @@
58 86 <button type="button" id="test-both-requests" class="button button-primary">
59 87 <span class="dashicons dashicons-controls-repeat"></span> Test Both Requests
60 88 </button>
61 89 </div>
90 + <?php $this->render_last_automatic_check(); ?>
62 91 </div>
63 92 <div id="host-test-results" style="display: none; margin-top:16px;">
64 93 <h3 style="color: var(--dashboard-text-primary); margin-bottom: 10px;">Test Results</h3>
65 94 <div id="test-results-content"></div>
@@ -133,9 +162,9 @@
133 162 $.ajax({
134 163 url: ajaxUrl,
135 164 type: 'POST',
136 165 dataType: 'json',
137 - data: { action: action, nonce: '<?php echo wp_create_nonce("metasync_nonce"); ?>' },
166 + data: { action: action, nonce: '<?php echo esc_attr(wp_create_nonce("metasync_nonce")); ?>' },
138 167 timeout: 35000,
139 168 success: function(response, textStatus, xhr) {
140 169 try {
141 170 if (response && response.success && response.data) {
@@ -193,29 +222,33 @@
193 222 function displayResults(results) {
194 223 var html = '';
195 224
196 225 results.forEach(function(result) {
197 - var statusClass = result.status === 'success' ? 'success' : 'error';
198 - var statusIcon = result.status === 'success' ? '✅' : '❌';
199 - var blockedStatus = result.blocked ? 'BLOCKED' : 'ALLOWED';
200 - var blockedClass = result.blocked ? 'blocked' : 'allowed';
226 + // Three states, not two: a request can also be
227 + // inconclusive when the checker service itself is
228 + // unavailable — that must never read as "blocked".
229 + var inconclusive = !!result.checker_unreachable;
230 + var statusClass = result.status === 'success' ? 'success' : (inconclusive ? 'warning' : 'error');
231 + var statusIcon = result.status === 'success' ? '✅' : (inconclusive ? '⚠️' : '❌');
232 + var blockedStatus = inconclusive ? 'INCONCLUSIVE' : (result.blocked ? 'BLOCKED' : 'ALLOWED');
233 + var blockedClass = inconclusive ? 'inconclusive' : (result.blocked ? 'blocked' : 'allowed');
201 234
202 235 html += '<div class="test-result-item ' + statusClass + '">';
203 236 html += '<div class="test-result-header">';
204 - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
237 + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
205 238 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
206 239 html += '</div>';
207 240
208 241 html += '<div class="test-result-details">';
209 - html += '<p><strong>Response Time:</strong> ' + result.response_time + '</p>';
210 - html += '<p><strong>Status:</strong> ' + result.status + '</p>';
242 + html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>';
243 + html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>';
211 244
212 245 if (result.status_code) {
213 - html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + result.status_code + '</span></p>';
246 + html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + escapeHtml(result.status_code) + '</span></p>';
214 247 }
215 248
216 249 if (result.error) {
217 - html += '<p><strong>Error:</strong> <span class="error-message">' + result.error + '</span></p>';
250 + html += '<p><strong>Error:</strong> <span class="error-message">' + escapeHtml(result.error) + '</span></p>';
218 251 }
219 252
220 253 if (result.body) {
221 254 html += '<p><strong>Response Body:</strong></p>';
@@ -225,9 +258,9 @@
225 258 if (result.headers && Object.keys(result.headers).length > 0) {
226 259 html += '<p><strong>Response Headers:</strong></p>';
227 260 html += '<pre class="response-headers">';
228 261 Object.keys(result.headers).forEach(function(key) {
229 - html += key + ': ' + result.headers[key] + '\n';
262 + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
230 263 });
231 264 html += '</pre>';
232 265 }
233 266
@@ -240,9 +273,9 @@
240 273 html += '<p><strong>Parsed Response:</strong></p>';
241 274 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
242 275 }
243 276
244 - html += '<p><strong>Details:</strong> ' + result.details + '</p>';
277 + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
245 278 html += '</div>';
246 279 html += '</div>';
247 280 });
248 281
@@ -257,9 +290,9 @@
257 290 '>': '&gt;',
258 291 '"': '&quot;',
259 292 "'": '&#039;'
260 293 };
261 - return text.replace(/[&<>"']/g, function(m) { return map[m]; });
294 + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
262 295 }
263 296
264 297 setTimeout(function() {
265 298 var btnBoth = $('#test-both-requests');
@@ -289,9 +322,13 @@
289 322
290 323 .test-result-item.error {
291 324 border-left: 4px solid #dc3545;
292 325 }
293 -
326 +
327 + .test-result-item.warning {
328 + border-left: 4px solid #dba617;
329 + }
330 +
294 331 .test-result-header {
295 332 background: var(--dashboard-bg);
296 333 padding: 15px 20px;
297 334 border-bottom: 1px solid #dee2e6;
@@ -322,9 +359,14 @@
322 359 .test-status.blocked {
323 360 background: rgba(248,113,113,0.15);
324 361 color: #f87171;
325 362 }
326 -
363 +
364 + .test-status.inconclusive {
365 + background: rgba(219,166,23,0.15);
366 + color: #dba617;
367 + }
368 +
327 369 .test-result-details {
328 370 padding: 20px;
329 371 }
330 372
@@ -493,8 +535,10 @@
493 535 color: #646970 !important;
494 536 opacity: 0.7;
495 537 }
496 538
539 + /* Equal width so badges of different lengths (Exact, Contains,
540 + etc.) render at the same size for a visually balanced column. */
497 541 .otto-pattern-type-badge {
498 542 display: inline-block;
499 543 padding: 3px 8px;
500 544 border-radius: 3px;
@@ -501,13 +545,19 @@
501 545 font-size: 11px;
502 546 font-weight: 600;
503 547 text-transform: uppercase;
504 548 letter-spacing: 0.5px;
549 + min-width: 90px;
550 + text-align: center;
551 + box-sizing: border-box;
505 552 }
506 553
554 + /* Badge text is 11px uppercase, i.e. normal-size text: the darker
555 + foreground steps keep every pair at or above the 4.5:1 AA bar
556 + on its own pastel background. */
507 557 .otto-pattern-exact {
508 558 background: #e3f2fd;
509 - color: #1976d2;
559 + color: #0d47a1;
510 560 }
511 561
512 562 .otto-pattern-contain {
513 563 background: #f3e5f5;
@@ -515,14 +565,14 @@
515 565 }
516 566
517 567 .otto-pattern-start {
518 568 background: #e8f5e9;
519 - color: #388e3c;
569 + color: #1b5e20;
520 570 }
521 571
522 572 .otto-pattern-end {
523 - background: var(--dashboard-card-bg)3e0;
524 - color: #f57c00;
573 + background: #fff3e0;
574 + color: #8f4400;
525 575 }
526 576
527 577 .otto-pattern-regex {
528 578 background: #fce4ec;
@@ -554,12 +604,35 @@
554 604 }
555 605
556 606 #otto-excluded-urls-table-container .wp-list-table td {
557 607 background: var(--dashboard-card-bg) !important;
558 - color: #2c3338 !important;
608 + color: var(--dashboard-text-primary) !important;
559 609 border-bottom: 1px solid #c3c4c7;
560 610 }
561 611
612 + /* Placeholder text (empty description, no recheck date) still has to
613 + clear AA against the card background in both themes. */
614 + #otto-excluded-urls-table-container .otto-muted {
615 + color: var(--dashboard-text-secondary) !important;
616 + }
617 +
618 + /* Loading / empty-state text shown in place of the table. */
619 + #otto-excluded-urls-table-container .otto-table-message {
620 + text-align: center;
621 + padding: 40px;
622 + color: var(--dashboard-text-secondary);
623 + }
624 +
625 + /* Error text replaces the table on a failed load. No single red
626 + clears AA on both card backgrounds, so each theme gets its own. */
627 + #otto-excluded-urls-table-container .otto-error-message {
628 + color: #f87171 !important;
629 + }
630 +
631 + [data-theme="light"] #otto-excluded-urls-table-container .otto-error-message {
632 + color: #b02a37 !important;
633 + }
634 +
562 635 #otto-excluded-urls-table-container .wp-list-table tbody tr {
563 636 background: var(--dashboard-card-bg);
564 637 }
565 638
@@ -576,11 +649,15 @@
576 649 border: 1px solid #dcdcde;
577 650 }
578 651
579 652 #otto-excluded-urls-table-container .otto-delete-url,
580 - #otto-excluded-urls-table-container .otto-recheck-url {
653 + #otto-excluded-urls-table-container .otto-recheck-url,
654 + #otto-excluded-urls-table-container .otto-recheck-disabled {
581 655 padding: 4px 10px;
582 656 font-size: 12px;
657 + box-sizing: border-box;
658 + height: 26px;
659 + line-height: 1;
583 660 }
584 661
585 662 #otto-excluded-urls-table-container .otto-delete-url {
586 663 background: #dc3545 !important;
@@ -629,8 +706,62 @@
629 706 <?php
630 707 $admin->render_layout_close();
631 708 }
632 709
710 + /**
711 + * Show the result of the last automatic host blocking check.
712 + *
713 + * Reads the stored result only — it never triggers a probe, so opening this page
714 + * costs nothing extra. The automatic check runs ~10 minutes after activation and
715 + * weekly after that; the button above re-runs the same probe on demand.
716 + */
717 + private function render_last_automatic_check(): void
718 + {
719 + if (!class_exists('Metasync_Host_Blocking_Check')) {
720 + return;
721 + }
722 +
723 + $checker = Metasync_Host_Blocking_Check::get_instance();
724 + $result = $checker->get_last_result();
725 +
726 + echo '<p style="margin: 16px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">';
727 +
728 + if ($result === null) {
729 + if (!$checker->site_is_publicly_reachable()) {
730 + echo esc_html__('Automatic checks are skipped on local and private-network sites, because our services cannot call this site back. Use the button above to test manually.', 'metasync');
731 + } else {
732 + echo esc_html__('The automatic check has not run yet. It runs about 10 minutes after activation, then weekly.', 'metasync');
733 + }
734 + echo '</p>';
735 + return;
736 + }
737 +
738 + $blocked = [];
739 + if ($result['get_blocked']) {
740 + $blocked[] = 'GET';
741 + }
742 + if ($result['post_blocked']) {
743 + $blocked[] = 'POST';
744 + }
745 +
746 + if (empty($blocked)) {
747 + printf(
748 + /* translators: %s: date/time of the last automatic check */
749 + esc_html__('Last automatic check: %s — no blocking detected.', 'metasync'),
750 + esc_html($result['checked_at'])
751 + );
752 + } else {
753 + printf(
754 + /* translators: 1: date/time of the last automatic check, 2: blocked HTTP methods */
755 + esc_html__('Last automatic check: %1$s — %2$s requests were blocked. Re-run the test above after changing any firewall or security settings.', 'metasync'),
756 + esc_html($result['checked_at']),
757 + esc_html(implode(' and ', $blocked))
758 + );
759 + }
760 +
761 + echo '</p>';
762 + }
763 +
633 764 public function render_compatibility_sections()
634 765 {
635 766 ?>
636 767 <div class="metasync-settings-accordion">
@@ -864,8 +995,9 @@
864 995 ],
865 996 'bricks' => [
866 997 'name' => 'Bricks Builder',
867 998 'plugin_files' => [ 'bricks/bricks.php' ],
999 + 'theme_name' => 'Bricks',
868 1000 'supported' => true,
869 1001 'version' => '1.11',
870 1002 ],
871 1003 ];
@@ -873,10 +1005,12 @@
873 1005 $result = [];
874 1006
875 1007 foreach ($builders as $key => $builder) {
876 1008 $is_core = isset($builder['is_core']) && $builder['is_core'];
877 - $theme_name = isset($builder['theme_name']) ? $builder['theme_name'] : null;
878 - $plugin_status = $this->get_plugin_status($builder['plugin_files'], $is_core, $theme_name);
1009 + // The array key is the theme directory slug; pass it (not the display name)
1010 + // so wp_get_theme() can resolve the theme on case-sensitive filesystems.
1011 + $theme_slug = isset($builder['theme_name']) ? $key : null;
1012 + $plugin_status = $this->get_plugin_status($builder['plugin_files'], $is_core, $theme_slug);
879 1013
880 1014 if ($builder['supported']) {
881 1015 $status = 'supported';
882 1016 $status_text = 'Supported';
@@ -1002,9 +1136,11 @@
1002 1136 ];
1003 1137
1004 1138 $result = [];
1005 1139 foreach ( $themes as $key => $theme ) {
1006 - $status = $this->get_plugin_status( $theme['plugin_files'], false, $theme['theme_name'] );
1140 + // The array key is the theme directory slug; pass it (not the display name)
1141 + // so wp_get_theme() can resolve the theme on case-sensitive filesystems.
1142 + $status = $this->get_plugin_status( $theme['plugin_files'], false, $key );
1007 1143 $result[] = [
1008 1144 'name' => $theme['name'],
1009 1145 'version' => $theme['version'],
1010 1146 'status' => 'supported',
@@ -1177,12 +1313,14 @@
1177 1313 * Get detailed plugin status (installed and/or active).
1178 1314 *
1179 1315 * @param array $plugin_files Array of plugin file paths to check
1180 1316 * @param bool $is_core Whether this is a WordPress core feature
1181 - * @param string $theme_name Optional theme name to check
1317 + * @param string $theme_slug Optional theme directory slug to check (e.g. 'hello-elementor',
1318 + * 'astra'). Must be the directory slug, not the display name,
1319 + * because wp_get_theme() resolves themes by directory slug.
1182 1320 * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
1183 1321 */
1184 - public function get_plugin_status($plugin_files, $is_core = false, $theme_name = null)
1322 + public function get_plugin_status($plugin_files, $is_core = false, $theme_slug = null)
1185 1323 {
1186 1324 if ($is_core) {
1187 1325 return [
1188 1326 'is_installed' => true,
@@ -1190,14 +1328,16 @@
1190 1328 'active_version' => 'core'
1191 1329 ];
1192 1330 }
1193 1331
1194 - if ($theme_name) {
1332 + if ($theme_slug) {
1195 1333 $current_theme = wp_get_theme();
1196 - $is_theme_active = (strtolower($current_theme->get('Name')) === strtolower($theme_name) ||
1197 - strtolower($current_theme->get_stylesheet()) === strtolower($theme_name));
1334 + $is_theme_active = (strtolower($current_theme->get('Name')) === strtolower($theme_slug) ||
1335 + strtolower($current_theme->get_stylesheet()) === strtolower($theme_slug));
1198 1336
1199 - $theme_exists = wp_get_theme($theme_name)->exists();
1337 + // An active theme is by definition installed. Fall back to a directory
1338 + // lookup for themes that are installed but not currently active.
1339 + $theme_exists = $is_theme_active || wp_get_theme($theme_slug)->exists();
1200 1340
1201 1341 if ($theme_exists) {
1202 1342 return [
1203 1343 'is_installed' => true,
@@ -1246,39 +1386,44 @@
1246 1386 }
1247 1387
1248 1388 public function get_plugin_logo($plugin_key, $type)
1249 1389 {
1390 + // Compatibility icons are bundled locally; loading images from external
1391 + // servers (wp.org, vendor sites, CDNs) counts as offloaded content and
1392 + // is disallowed for wordpress.org releases.
1393 + $assets = plugins_url('assets/images/compat/', dirname(__DIR__) . '/metasync.php');
1394 +
1250 1395 $logos = [
1251 1396 'page_builder' => [
1252 - 'elementor' => 'https://ps.w.org/elementor/assets/icon-256x256.gif',
1253 - 'gutenberg' => 'https://i0.wp.com/wordpress.org/files/2023/02/wmark.png',
1254 - 'divi' => 'https://www.elegantthemes.com/images/logo.svg',
1255 - 'oxygen' => 'https://oxygenbuilder.com/wp-content/uploads/2021/09/oxygen-logo-icon.png',
1256 - 'bricks' => 'https://bricksbuilder.io/wp-content/uploads/2021/05/bricks-icon.svg',
1397 + 'elementor' => $assets . 'elementor.svg',
1398 + 'gutenberg' => $assets . 'gutenberg.svg',
1399 + 'divi' => $assets . 'divi.svg',
1400 + 'oxygen' => $assets . 'oxygen.svg',
1401 + 'bricks' => $assets . 'bricks.svg',
1257 1402 ],
1258 1403 'theme' => [
1259 - 'astra' => 'https://ps.w.org/astra/assets/icon-128x128.png',
1260 - 'generatepress' => 'https://ps.w.org/generatepress/assets/icon-128x128.png',
1261 - 'oceanwp' => 'https://ps.w.org/ocean-extra/assets/icon-128x128.png',
1262 - 'hello-elementor' => 'https://ps.w.org/hello-elementor/assets/icon-128x128.png',
1263 - 'kadence' => 'https://ps.w.org/kadence-blocks/assets/icon-128x128.png',
1404 + 'astra' => $assets . 'astra.svg',
1405 + 'generatepress' => $assets . 'generatepress.svg',
1406 + 'oceanwp' => $assets . 'oceanwp.svg',
1407 + 'hello-elementor' => $assets . 'hello-elementor.svg',
1408 + 'kadence' => $assets . 'kadence.svg',
1264 1409 ],
1265 1410 'seo' => [
1266 - 'yoast' => 'https://ps.w.org/wordpress-seo/assets/icon-128x128.gif',
1267 - 'rankmath' => 'https://ps.w.org/seo-by-rank-math/assets/icon-128x128.png',
1268 - 'aioseo' => 'https://ps.w.org/all-in-one-seo-pack/assets/icon-128x128.png',
1411 + 'yoast' => $assets . 'yoast.svg',
1412 + 'rankmath' => $assets . 'rankmath.svg',
1413 + 'aioseo' => $assets . 'aioseo.svg',
1269 1414 ],
1270 1415 'cache' => [
1271 - 'wp-rocket' => 'https://ps.w.org/rocket-lazy-load/assets/icon-128x128.png',
1272 - 'litespeed-cache' => 'https://ps.w.org/litespeed-cache/assets/icon-128x128.png',
1273 - 'w3-total-cache' => 'https://ps.w.org/w3-total-cache/assets/icon-128x128.png',
1274 - 'sg-cachepress' => 'https://ps.w.org/sg-cachepress/assets/icon-128x128.png',
1275 - 'hummingbird-performance' => 'https://ps.w.org/hummingbird-performance/assets/icon-128x128.png',
1276 - 'autoptimize' => 'https://ps.w.org/autoptimize/assets/icon-128x128.png',
1416 + 'wp-rocket' => $assets . 'wp-rocket.svg',
1417 + 'litespeed-cache' => $assets . 'litespeed-cache.svg',
1418 + 'w3-total-cache' => $assets . 'w3-total-cache.svg',
1419 + 'sg-cachepress' => $assets . 'sg-cachepress.svg',
1420 + 'hummingbird-performance' => $assets . 'hummingbird-performance.svg',
1421 + 'autoptimize' => $assets . 'autoptimize.svg',
1277 1422 ],
1278 1423 'cdn' => [
1279 - 'cloudflare' => 'https://ps.w.org/cloudflare/assets/icon-128x128.png',
1280 - 'fastly' => 'https://ps.w.org/purgely/assets/icon-128x128.png',
1424 + 'cloudflare' => $assets . 'cloudflare.svg',
1425 + 'fastly' => $assets . 'fastly.svg',
1281 1426 ],
1282 1427 ];
1283 1428
1284 1429 return isset($logos[$type][$plugin_key]) ? $logos[$type][$plugin_key] : '';