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 +197 -347 2.6.5 → 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>
@@ -55,11 +83,12 @@
55 83 <h3 style="margin-top: 0; color: var(--dashboard-text-primary);">Test Configuration</h3>
56 84 <p style="margin-bottom: 16px; color: var(--dashboard-text-secondary);">Test connectivity by running both GET and POST requests. Results appear below.</p>
57 85 <div style="display: flex; gap: 10px; flex-wrap: wrap;">
58 86 <button type="button" id="test-both-requests" class="button button-primary">
59 - <span class="dashicons dashicons-controls-repeat" style="margin-top:3px;font-size:15px;width:15px;height:15px;"></span> Test Both Requests
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 },
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
@@ -460,302 +502,8 @@
460 502 </div>
461 503 </details>
462 504 </div>
463 505
464 - <!-- OTTO Excluded URLs JavaScript -->
465 - <script>
466 - (function() {
467 - if (typeof jQuery === 'undefined') {
468 - setTimeout(arguments.callee, 100);
469 - return;
470 - }
471 -
472 - jQuery(document).ready(function($) {
473 - var currentPage = 1;
474 - var perPage = 10;
475 - var ajaxUrl = '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
476 - var nonce = '<?php echo wp_create_nonce('metasync_otto_excluded_urls'); ?>';
477 -
478 - loadExcludedURLs();
479 -
480 - $('#otto-add-excluded-url-btn').on('click', function(e) {
481 - e.preventDefault();
482 - addExcludedURL();
483 - });
484 -
485 - $('#otto-url-pattern').on('keypress', function(e) {
486 - if (e.which === 13) {
487 - e.preventDefault();
488 - addExcludedURL();
489 - }
490 - });
491 -
492 - function addExcludedURL() {
493 - var $button = $('#otto-add-excluded-url-btn');
494 - var $status = $('#otto-add-status');
495 - var urlPattern = $('#otto-url-pattern').val().trim();
496 - var patternType = $('#otto-pattern-type').val();
497 - var description = $('#otto-description').val().trim();
498 -
499 - if (!urlPattern) {
500 - showStatus('error', 'URL pattern is required');
501 - return;
502 - }
503 -
504 - $button.prop('disabled', true).text('Adding...');
505 - $status.hide();
506 -
507 - $.ajax({
508 - url: ajaxUrl,
509 - type: 'POST',
510 - data: {
511 - action: 'metasync_otto_add_excluded_url',
512 - nonce: nonce,
513 - url_pattern: urlPattern,
514 - pattern_type: patternType,
515 - description: description
516 - },
517 - success: function(response) {
518 - if (response.success) {
519 - showStatus('success', response.data.message || 'URL excluded successfully');
520 - $('#otto-url-pattern').val('');
521 - $('#otto-description').val('');
522 - $('#otto-pattern-type').val('exact');
523 - loadExcludedURLs();
524 - } else {
525 - showStatus('error', response.data.message || 'Failed to add excluded URL');
526 - }
527 - },
528 - error: function() {
529 - showStatus('error', 'An error occurred. Please try again.');
530 - },
531 - complete: function() {
532 - $button.prop('disabled', false).text('➕ Add Excluded URL');
533 - }
534 - });
535 - }
536 -
537 - function loadExcludedURLs(page) {
538 - page = page || currentPage;
539 - var $container = $('#otto-excluded-urls-table-container');
540 -
541 - $container.html('<div style="text-align: center; padding: 40px; color: var(--dashboard-text-secondary);"><p>Loading...</p></div>');
542 -
543 - $.ajax({
544 - url: ajaxUrl,
545 - type: 'POST',
546 - data: {
547 - action: 'metasync_otto_get_excluded_urls',
548 - nonce: nonce,
549 - page: page,
550 - per_page: perPage
551 - },
552 - success: function(response) {
553 - if (response.success) {
554 - currentPage = page;
555 - renderExcludedURLsTable(response.data.records, response.data.pagination);
556 - } else {
557 - $container.html('<div style="text-align: center; padding: 40px; color: #dc3545;"><p>Error loading excluded URLs</p></div>');
558 - }
559 - },
560 - error: function() {
561 - $container.html('<div style="text-align: center; padding: 40px; color: #dc3545;"><p>Failed to load excluded URLs</p></div>');
562 - }
563 - });
564 - }
565 -
566 - function renderExcludedURLsTable(records, pagination) {
567 - var $container = $('#otto-excluded-urls-table-container');
568 -
569 - if (!records || records.length === 0) {
570 - $container.html('<div style="text-align: center; padding: 40px; color: var(--dashboard-text-secondary);"><p>No excluded URLs found. Add one above to get started.</p></div>');
571 - return;
572 - }
573 -
574 - var html = '<table class="wp-list-table widefat fixed striped" style="margin-top: 10px;">';
575 - html += '<thead><tr>';
576 - html += '<th style="width: 30%;">URL Pattern</th>';
577 - html += '<th style="width: 12%;">Match Type</th>';
578 - html += '<th style="width: 25%;">Description</th>';
579 - html += '<th style="width: 13%;">Recheck After</th>';
580 - html += '<th style="width: 20%;">Actions</th>';
581 - html += '</tr></thead><tbody>';
582 -
583 - records.forEach(function(record) {
584 - html += '<tr>';
585 - html += '<td><code>' + escapeHtml(record.url_pattern) + '</code></td>';
586 - html += '<td><span class="otto-pattern-type-badge otto-pattern-' + record.pattern_type + '">' + formatPatternType(record.pattern_type) + '</span></td>';
587 - html += '<td>' + (record.description ? escapeHtml(record.description) : '<span style="color: #999;">—</span>') + '</td>';
588 - html += '<td>' + formatRecheckAfter(record) + '</td>';
589 - html += '<td><span class="otto-actions">';
590 - html += '<button type="button" class="button button-small otto-recheck-url" data-id="' + record.id + '" style="margin-right: 5px;">Recheck</button>';
591 - html += '<button type="button" class="button button-small otto-delete-url" data-id="' + record.id + '" style="color: #dc3545;">Delete</button>';
592 - html += '</span></td>';
593 - html += '</tr>';
594 - });
595 -
596 - html += '</tbody></table>';
597 -
598 - if (pagination.total_pages > 1) {
599 - html += '<div class="tablenav" style="margin-top: 15px; display: flex; justify-content: space-between; align-items: center;">';
600 - html += '<div class="tablenav-pages">';
601 - html += '<span class="displaying-num">' + pagination.total_count + ' items</span>';
602 -
603 - html += '<span class="pagination-links">';
604 -
605 - if (pagination.current_page > 1) {
606 - html += '<a class="button otto-page-nav" data-page="1" href="#">«</a> ';
607 - html += '<a class="button otto-page-nav" data-page="' + (pagination.current_page - 1) + '" href="#">‹</a> ';
608 - } else {
609 - html += '<span class="button disabled">«</span> ';
610 - html += '<span class="button disabled">‹</span> ';
611 - }
612 -
613 - html += '<span class="paging-input">';
614 - html += '<span class="tablenav-paging-text">' + pagination.current_page + ' of <span class="total-pages">' + pagination.total_pages + '</span></span>';
615 - html += '</span> ';
616 -
617 - if (pagination.current_page < pagination.total_pages) {
618 - html += '<a class="button otto-page-nav" data-page="' + (pagination.current_page + 1) + '" href="#">›</a> ';
619 - html += '<a class="button otto-page-nav" data-page="' + pagination.total_pages + '" href="#">»</a>';
620 - } else {
621 - html += '<span class="button disabled">›</span> ';
622 - html += '<span class="button disabled">»</span>';
623 - }
624 -
625 - html += '</span></div></div>';
626 - }
627 -
628 - $container.html(html);
629 -
630 - $('.otto-delete-url').on('click', function(e) {
631 - e.preventDefault();
632 - var id = $(this).data('id');
633 - deleteExcludedURL(id);
634 - });
635 -
636 - $('.otto-recheck-url').on('click', function(e) {
637 - e.preventDefault();
638 - var $btn = $(this);
639 - var id = $btn.data('id');
640 - recheckExcludedURL(id, $btn);
641 - });
642 -
643 - $('.otto-page-nav').on('click', function(e) {
644 - e.preventDefault();
645 - var page = $(this).data('page');
646 - loadExcludedURLs(page);
647 - });
648 - }
649 -
650 - function recheckExcludedURL(id, $btn) {
651 - var originalText = $btn.text();
652 - $btn.prop('disabled', true).text('Checking...');
653 -
654 - $.ajax({
655 - url: ajaxUrl,
656 - type: 'POST',
657 - data: {
658 - action: 'metasync_otto_recheck_excluded_url',
659 - nonce: nonce,
660 - id: id
661 - },
662 - success: function(response) {
663 - $btn.prop('disabled', false).text(originalText);
664 - if (response.success) {
665 - if (response.data.available) {
666 - if (confirm('This URL is now available. Do you want to remove it from the excluded list?')) {
667 - deleteExcludedURL(id);
668 - }
669 - } else {
670 - alert('This URL is still returning 404.');
671 - }
672 - } else {
673 - alert('Error: ' + (response.data.message || 'Recheck failed'));
674 - }
675 - },
676 - error: function(xhr, status, err) {
677 - $btn.prop('disabled', false).text(originalText);
678 - alert('An error occurred while rechecking. Please try again.');
679 - }
680 - });
681 - }
682 -
683 - function deleteExcludedURL(id) {
684 - if (!confirm('Are you sure you want to delete this excluded URL?')) {
685 - return;
686 - }
687 -
688 - $.ajax({
689 - url: ajaxUrl,
690 - type: 'POST',
691 - data: {
692 - action: 'metasync_otto_delete_excluded_url',
693 - nonce: nonce,
694 - id: id
695 - },
696 - success: function(response) {
697 - if (response.success) {
698 - loadExcludedURLs();
699 - } else {
700 - alert('Error: ' + (response.data.message || 'Failed to delete'));
701 - }
702 - },
703 - error: function() {
704 - alert('An error occurred. Please try again.');
705 - }
706 - });
707 - }
708 -
709 - function showStatus(type, message) {
710 - var $status = $('#otto-add-status');
711 - $status.removeClass('otto-status-success otto-status-error');
712 - $status.addClass('otto-status-' + type);
713 - $status.text(message).fadeIn();
714 -
715 - setTimeout(function() {
716 - $status.fadeOut();
717 - }, 5000);
718 - }
719 -
720 - function formatPatternType(type) {
721 - var types = {
722 - 'exact': 'Exact',
723 - 'contain': 'Contains',
724 - 'start': 'Starts With',
725 - 'end': 'Ends With',
726 - 'regex': 'Regex'
727 - };
728 - return types[type] || type;
729 - }
730 -
731 - function formatRecheckAfter(record) {
732 - if (!record.auto_excluded || !record.recheck_after) {
733 - return '<span style="color: #999;">NA</span>';
734 - }
735 - var d = new Date(record.recheck_after.replace(' ', 'T'));
736 - if (isNaN(d.getTime())) {
737 - return escapeHtml(record.recheck_after);
738 - }
739 - var months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
740 - return months[d.getMonth()] + ' ' + d.getDate() + ', ' + d.getFullYear();
741 - }
742 -
743 - function escapeHtml(text) {
744 - if (!text) return '';
745 - var map = {
746 - '&': '&amp;',
747 - '<': '&lt;',
748 - '>': '&gt;',
749 - '"': '&quot;',
750 - "'": '&#039;'
751 - };
752 - return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
753 - }
754 - });
755 - })();
756 - </script>
757 -
758 506 <!-- OTTO Excluded URLs CSS -->
759 507 <style>
760 508 /* Fix input and select field text colors */
761 509 #otto-excluded-url-form input[type="text"],
@@ -787,8 +535,10 @@
787 535 color: #646970 !important;
788 536 opacity: 0.7;
789 537 }
790 538
539 + /* Equal width so badges of different lengths (Exact, Contains,
540 + etc.) render at the same size for a visually balanced column. */
791 541 .otto-pattern-type-badge {
792 542 display: inline-block;
793 543 padding: 3px 8px;
794 544 border-radius: 3px;
@@ -795,13 +545,19 @@
795 545 font-size: 11px;
796 546 font-weight: 600;
797 547 text-transform: uppercase;
798 548 letter-spacing: 0.5px;
549 + min-width: 90px;
550 + text-align: center;
551 + box-sizing: border-box;
799 552 }
800 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. */
801 557 .otto-pattern-exact {
802 558 background: #e3f2fd;
803 - color: #1976d2;
559 + color: #0d47a1;
804 560 }
805 561
806 562 .otto-pattern-contain {
807 563 background: #f3e5f5;
@@ -809,14 +565,14 @@
809 565 }
810 566
811 567 .otto-pattern-start {
812 568 background: #e8f5e9;
813 - color: #388e3c;
569 + color: #1b5e20;
814 570 }
815 571
816 572 .otto-pattern-end {
817 - background: var(--dashboard-card-bg)3e0;
818 - color: #f57c00;
573 + background: #fff3e0;
574 + color: #8f4400;
819 575 }
820 576
821 577 .otto-pattern-regex {
822 578 background: #fce4ec;
@@ -848,12 +604,35 @@
848 604 }
849 605
850 606 #otto-excluded-urls-table-container .wp-list-table td {
851 607 background: var(--dashboard-card-bg) !important;
852 - color: #2c3338 !important;
608 + color: var(--dashboard-text-primary) !important;
853 609 border-bottom: 1px solid #c3c4c7;
854 610 }
855 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 +
856 635 #otto-excluded-urls-table-container .wp-list-table tbody tr {
857 636 background: var(--dashboard-card-bg);
858 637 }
859 638
@@ -870,11 +649,15 @@
870 649 border: 1px solid #dcdcde;
871 650 }
872 651
873 652 #otto-excluded-urls-table-container .otto-delete-url,
874 - #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 {
875 655 padding: 4px 10px;
876 656 font-size: 12px;
657 + box-sizing: border-box;
658 + height: 26px;
659 + line-height: 1;
877 660 }
878 661
879 662 #otto-excluded-urls-table-container .otto-delete-url {
880 663 background: #dc3545 !important;
@@ -919,13 +702,66 @@
919 702 .tablenav-paging-text {
920 703 color: #2c3338 !important;
921 704 }
922 705 </style>
923 - </div>
924 706 <?php
925 707 $admin->render_layout_close();
926 708 }
927 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 +
928 764 public function render_compatibility_sections()
929 765 {
930 766 ?>
931 767 <div class="metasync-settings-accordion">
@@ -1159,8 +995,9 @@
1159 995 ],
1160 996 'bricks' => [
1161 997 'name' => 'Bricks Builder',
1162 998 'plugin_files' => [ 'bricks/bricks.php' ],
999 + 'theme_name' => 'Bricks',
1163 1000 'supported' => true,
1164 1001 'version' => '1.11',
1165 1002 ],
1166 1003 ];
@@ -1168,10 +1005,12 @@
1168 1005 $result = [];
1169 1006
1170 1007 foreach ($builders as $key => $builder) {
1171 1008 $is_core = isset($builder['is_core']) && $builder['is_core'];
1172 - $theme_name = isset($builder['theme_name']) ? $builder['theme_name'] : null;
1173 - $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);
1174 1013
1175 1014 if ($builder['supported']) {
1176 1015 $status = 'supported';
1177 1016 $status_text = 'Supported';
@@ -1297,9 +1136,11 @@
1297 1136 ];
1298 1137
1299 1138 $result = [];
1300 1139 foreach ( $themes as $key => $theme ) {
1301 - $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 );
1302 1143 $result[] = [
1303 1144 'name' => $theme['name'],
1304 1145 'version' => $theme['version'],
1305 1146 'status' => 'supported',
@@ -1472,12 +1313,14 @@
1472 1313 * Get detailed plugin status (installed and/or active).
1473 1314 *
1474 1315 * @param array $plugin_files Array of plugin file paths to check
1475 1316 * @param bool $is_core Whether this is a WordPress core feature
1476 - * @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.
1477 1320 * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
1478 1321 */
1479 - 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)
1480 1323 {
1481 1324 if ($is_core) {
1482 1325 return [
1483 1326 'is_installed' => true,
@@ -1485,14 +1328,16 @@
1485 1328 'active_version' => 'core'
1486 1329 ];
1487 1330 }
1488 1331
1489 - if ($theme_name) {
1332 + if ($theme_slug) {
1490 1333 $current_theme = wp_get_theme();
1491 - $is_theme_active = (strtolower($current_theme->get('Name')) === strtolower($theme_name) ||
1492 - 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));
1493 1336
1494 - $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();
1495 1340
1496 1341 if ($theme_exists) {
1497 1342 return [
1498 1343 'is_installed' => true,
@@ -1541,39 +1386,44 @@
1541 1386 }
1542 1387
1543 1388 public function get_plugin_logo($plugin_key, $type)
1544 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 +
1545 1395 $logos = [
1546 1396 'page_builder' => [
1547 - 'elementor' => 'https://ps.w.org/elementor/assets/icon-256x256.gif',
1548 - 'gutenberg' => 'https://i0.wp.com/wordpress.org/files/2023/02/wmark.png',
1549 - 'divi' => 'https://www.elegantthemes.com/images/logo.svg',
1550 - 'oxygen' => 'https://oxygenbuilder.com/wp-content/uploads/2021/09/oxygen-logo-icon.png',
1551 - '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',
1552 1402 ],
1553 1403 'theme' => [
1554 - 'astra' => 'https://ps.w.org/astra/assets/icon-128x128.png',
1555 - 'generatepress' => 'https://ps.w.org/generatepress/assets/icon-128x128.png',
1556 - 'oceanwp' => 'https://ps.w.org/ocean-extra/assets/icon-128x128.png',
1557 - 'hello-elementor' => 'https://ps.w.org/hello-elementor/assets/icon-128x128.png',
1558 - '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',
1559 1409 ],
1560 1410 'seo' => [
1561 - 'yoast' => 'https://ps.w.org/wordpress-seo/assets/icon-128x128.gif',
1562 - 'rankmath' => 'https://ps.w.org/seo-by-rank-math/assets/icon-128x128.png',
1563 - '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',
1564 1414 ],
1565 1415 'cache' => [
1566 - 'wp-rocket' => 'https://ps.w.org/rocket-lazy-load/assets/icon-128x128.png',
1567 - 'litespeed-cache' => 'https://ps.w.org/litespeed-cache/assets/icon-128x128.png',
1568 - 'w3-total-cache' => 'https://ps.w.org/w3-total-cache/assets/icon-128x128.png',
1569 - 'sg-cachepress' => 'https://ps.w.org/sg-cachepress/assets/icon-128x128.png',
1570 - 'hummingbird-performance' => 'https://ps.w.org/hummingbird-performance/assets/icon-128x128.png',
1571 - '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',
1572 1422 ],
1573 1423 'cdn' => [
1574 - 'cloudflare' => 'https://ps.w.org/cloudflare/assets/icon-128x128.png',
1575 - 'fastly' => 'https://ps.w.org/purgely/assets/icon-128x128.png',
1424 + 'cloudflare' => $assets . 'cloudflare.svg',
1425 + 'fastly' => $assets . 'fastly.svg',
1576 1426 ],
1577 1427 ];
1578 1428
1579 1429 return isset($logos[$type][$plugin_key]) ? $logos[$type][$plugin_key] : '';