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-compatibility-checker.php +159 -346 2.6.5 → 2.7.1 View file →
@@ -55,11 +55,12 @@
55 55 <h3 style="margin-top: 0; color: var(--dashboard-text-primary);">Test Configuration</h3>
56 56 <p style="margin-bottom: 16px; color: var(--dashboard-text-secondary);">Test connectivity by running both GET and POST requests. Results appear below.</p>
57 57 <div style="display: flex; gap: 10px; flex-wrap: wrap;">
58 58 <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
59 + <span class="dashicons dashicons-controls-repeat"></span> Test Both Requests
60 60 </button>
61 61 </div>
62 + <?php $this->render_last_automatic_check(); ?>
62 63 </div>
63 64 <div id="host-test-results" style="display: none; margin-top:16px;">
64 65 <h3 style="color: var(--dashboard-text-primary); margin-bottom: 10px;">Test Results</h3>
65 66 <div id="test-results-content"></div>
@@ -133,9 +134,9 @@
133 134 $.ajax({
134 135 url: ajaxUrl,
135 136 type: 'POST',
136 137 dataType: 'json',
137 - data: { action: action },
138 + data: { action: action, nonce: '<?php echo wp_create_nonce("metasync_nonce"); ?>' },
138 139 timeout: 35000,
139 140 success: function(response, textStatus, xhr) {
140 141 try {
141 142 if (response && response.success && response.data) {
@@ -193,29 +194,33 @@
193 194 function displayResults(results) {
194 195 var html = '';
195 196
196 197 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';
198 + // Three states, not two: a request can also be
199 + // inconclusive when the checker service itself is
200 + // unavailable — that must never read as "blocked".
201 + var inconclusive = !!result.checker_unreachable;
202 + var statusClass = result.status === 'success' ? 'success' : (inconclusive ? 'warning' : 'error');
203 + var statusIcon = result.status === 'success' ? '✅' : (inconclusive ? '⚠️' : '❌');
204 + var blockedStatus = inconclusive ? 'INCONCLUSIVE' : (result.blocked ? 'BLOCKED' : 'ALLOWED');
205 + var blockedClass = inconclusive ? 'inconclusive' : (result.blocked ? 'blocked' : 'allowed');
201 206
202 207 html += '<div class="test-result-item ' + statusClass + '">';
203 208 html += '<div class="test-result-header">';
204 - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
209 + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
205 210 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
206 211 html += '</div>';
207 212
208 213 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>';
214 + html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>';
215 + html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>';
211 216
212 217 if (result.status_code) {
213 - html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + result.status_code + '</span></p>';
218 + html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + escapeHtml(result.status_code) + '</span></p>';
214 219 }
215 220
216 221 if (result.error) {
217 - html += '<p><strong>Error:</strong> <span class="error-message">' + result.error + '</span></p>';
222 + html += '<p><strong>Error:</strong> <span class="error-message">' + escapeHtml(result.error) + '</span></p>';
218 223 }
219 224
220 225 if (result.body) {
221 226 html += '<p><strong>Response Body:</strong></p>';
@@ -225,9 +230,9 @@
225 230 if (result.headers && Object.keys(result.headers).length > 0) {
226 231 html += '<p><strong>Response Headers:</strong></p>';
227 232 html += '<pre class="response-headers">';
228 233 Object.keys(result.headers).forEach(function(key) {
229 - html += key + ': ' + result.headers[key] + '\n';
234 + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
230 235 });
231 236 html += '</pre>';
232 237 }
233 238
@@ -240,9 +245,9 @@
240 245 html += '<p><strong>Parsed Response:</strong></p>';
241 246 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
242 247 }
243 248
244 - html += '<p><strong>Details:</strong> ' + result.details + '</p>';
249 + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
245 250 html += '</div>';
246 251 html += '</div>';
247 252 });
248 253
@@ -257,9 +262,9 @@
257 262 '>': '&gt;',
258 263 '"': '&quot;',
259 264 "'": '&#039;'
260 265 };
261 - return text.replace(/[&<>"']/g, function(m) { return map[m]; });
266 + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
262 267 }
263 268
264 269 setTimeout(function() {
265 270 var btnBoth = $('#test-both-requests');
@@ -289,9 +294,13 @@
289 294
290 295 .test-result-item.error {
291 296 border-left: 4px solid #dc3545;
292 297 }
293 -
298 +
299 + .test-result-item.warning {
300 + border-left: 4px solid #dba617;
301 + }
302 +
294 303 .test-result-header {
295 304 background: var(--dashboard-bg);
296 305 padding: 15px 20px;
297 306 border-bottom: 1px solid #dee2e6;
@@ -322,9 +331,14 @@
322 331 .test-status.blocked {
323 332 background: rgba(248,113,113,0.15);
324 333 color: #f87171;
325 334 }
326 -
335 +
336 + .test-status.inconclusive {
337 + background: rgba(219,166,23,0.15);
338 + color: #dba617;
339 + }
340 +
327 341 .test-result-details {
328 342 padding: 20px;
329 343 }
330 344
@@ -460,302 +474,8 @@
460 474 </div>
461 475 </details>
462 476 </div>
463 477
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 478 <!-- OTTO Excluded URLs CSS -->
759 479 <style>
760 480 /* Fix input and select field text colors */
761 481 #otto-excluded-url-form input[type="text"],
@@ -797,11 +517,14 @@
797 517 text-transform: uppercase;
798 518 letter-spacing: 0.5px;
799 519 }
800 520
521 + /* Badge text is 11px uppercase, i.e. normal-size text: the darker
522 + foreground steps keep every pair at or above the 4.5:1 AA bar
523 + on its own pastel background. */
801 524 .otto-pattern-exact {
802 525 background: #e3f2fd;
803 - color: #1976d2;
526 + color: #0d47a1;
804 527 }
805 528
806 529 .otto-pattern-contain {
807 530 background: #f3e5f5;
@@ -809,14 +532,14 @@
809 532 }
810 533
811 534 .otto-pattern-start {
812 535 background: #e8f5e9;
813 - color: #388e3c;
536 + color: #1b5e20;
814 537 }
815 538
816 539 .otto-pattern-end {
817 - background: var(--dashboard-card-bg)3e0;
818 - color: #f57c00;
540 + background: #fff3e0;
541 + color: #8f4400;
819 542 }
820 543
821 544 .otto-pattern-regex {
822 545 background: #fce4ec;
@@ -848,12 +571,35 @@
848 571 }
849 572
850 573 #otto-excluded-urls-table-container .wp-list-table td {
851 574 background: var(--dashboard-card-bg) !important;
852 - color: #2c3338 !important;
575 + color: var(--dashboard-text-primary) !important;
853 576 border-bottom: 1px solid #c3c4c7;
854 577 }
855 578
579 + /* Placeholder text (empty description, no recheck date) still has to
580 + clear AA against the card background in both themes. */
581 + #otto-excluded-urls-table-container .otto-muted {
582 + color: var(--dashboard-text-secondary) !important;
583 + }
584 +
585 + /* Loading / empty-state text shown in place of the table. */
586 + #otto-excluded-urls-table-container .otto-table-message {
587 + text-align: center;
588 + padding: 40px;
589 + color: var(--dashboard-text-secondary);
590 + }
591 +
592 + /* Error text replaces the table on a failed load. No single red
593 + clears AA on both card backgrounds, so each theme gets its own. */
594 + #otto-excluded-urls-table-container .otto-error-message {
595 + color: #f87171 !important;
596 + }
597 +
598 + [data-theme="light"] #otto-excluded-urls-table-container .otto-error-message {
599 + color: #b02a37 !important;
600 + }
601 +
856 602 #otto-excluded-urls-table-container .wp-list-table tbody tr {
857 603 background: var(--dashboard-card-bg);
858 604 }
859 605
@@ -919,13 +665,66 @@
919 665 .tablenav-paging-text {
920 666 color: #2c3338 !important;
921 667 }
922 668 </style>
923 - </div>
924 669 <?php
925 670 $admin->render_layout_close();
926 671 }
927 672
673 + /**
674 + * Show the result of the last automatic host blocking check.
675 + *
676 + * Reads the stored result only — it never triggers a probe, so opening this page
677 + * costs nothing extra. The automatic check runs ~10 minutes after activation and
678 + * weekly after that; the button above re-runs the same probe on demand.
679 + */
680 + private function render_last_automatic_check(): void
681 + {
682 + if (!class_exists('Metasync_Host_Blocking_Check')) {
683 + return;
684 + }
685 +
686 + $checker = Metasync_Host_Blocking_Check::get_instance();
687 + $result = $checker->get_last_result();
688 +
689 + echo '<p style="margin: 16px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">';
690 +
691 + if ($result === null) {
692 + if (!$checker->site_is_publicly_reachable()) {
693 + 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');
694 + } else {
695 + echo esc_html__('The automatic check has not run yet. It runs about 10 minutes after activation, then weekly.', 'metasync');
696 + }
697 + echo '</p>';
698 + return;
699 + }
700 +
701 + $blocked = [];
702 + if ($result['get_blocked']) {
703 + $blocked[] = 'GET';
704 + }
705 + if ($result['post_blocked']) {
706 + $blocked[] = 'POST';
707 + }
708 +
709 + if (empty($blocked)) {
710 + printf(
711 + /* translators: %s: date/time of the last automatic check */
712 + esc_html__('Last automatic check: %s — no blocking detected.', 'metasync'),
713 + esc_html($result['checked_at'])
714 + );
715 + } else {
716 + printf(
717 + /* translators: 1: date/time of the last automatic check, 2: blocked HTTP methods */
718 + 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'),
719 + esc_html($result['checked_at']),
720 + esc_html(implode(' and ', $blocked))
721 + );
722 + }
723 +
724 + echo '</p>';
725 + }
726 +
928 727 public function render_compatibility_sections()
929 728 {
930 729 ?>
931 730 <div class="metasync-settings-accordion">
@@ -1159,8 +958,9 @@
1159 958 ],
1160 959 'bricks' => [
1161 960 'name' => 'Bricks Builder',
1162 961 'plugin_files' => [ 'bricks/bricks.php' ],
962 + 'theme_name' => 'Bricks',
1163 963 'supported' => true,
1164 964 'version' => '1.11',
1165 965 ],
1166 966 ];
@@ -1168,10 +968,12 @@
1168 968 $result = [];
1169 969
1170 970 foreach ($builders as $key => $builder) {
1171 971 $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);
972 + // The array key is the theme directory slug; pass it (not the display name)
973 + // so wp_get_theme() can resolve the theme on case-sensitive filesystems.
974 + $theme_slug = isset($builder['theme_name']) ? $key : null;
975 + $plugin_status = $this->get_plugin_status($builder['plugin_files'], $is_core, $theme_slug);
1174 976
1175 977 if ($builder['supported']) {
1176 978 $status = 'supported';
1177 979 $status_text = 'Supported';
@@ -1297,9 +1099,11 @@
1297 1099 ];
1298 1100
1299 1101 $result = [];
1300 1102 foreach ( $themes as $key => $theme ) {
1301 - $status = $this->get_plugin_status( $theme['plugin_files'], false, $theme['theme_name'] );
1103 + // The array key is the theme directory slug; pass it (not the display name)
1104 + // so wp_get_theme() can resolve the theme on case-sensitive filesystems.
1105 + $status = $this->get_plugin_status( $theme['plugin_files'], false, $key );
1302 1106 $result[] = [
1303 1107 'name' => $theme['name'],
1304 1108 'version' => $theme['version'],
1305 1109 'status' => 'supported',
@@ -1472,12 +1276,14 @@
1472 1276 * Get detailed plugin status (installed and/or active).
1473 1277 *
1474 1278 * @param array $plugin_files Array of plugin file paths to check
1475 1279 * @param bool $is_core Whether this is a WordPress core feature
1476 - * @param string $theme_name Optional theme name to check
1280 + * @param string $theme_slug Optional theme directory slug to check (e.g. 'hello-elementor',
1281 + * 'astra'). Must be the directory slug, not the display name,
1282 + * because wp_get_theme() resolves themes by directory slug.
1477 1283 * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
1478 1284 */
1479 - public function get_plugin_status($plugin_files, $is_core = false, $theme_name = null)
1285 + public function get_plugin_status($plugin_files, $is_core = false, $theme_slug = null)
1480 1286 {
1481 1287 if ($is_core) {
1482 1288 return [
1483 1289 'is_installed' => true,
@@ -1485,14 +1291,16 @@
1485 1291 'active_version' => 'core'
1486 1292 ];
1487 1293 }
1488 1294
1489 - if ($theme_name) {
1295 + if ($theme_slug) {
1490 1296 $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));
1297 + $is_theme_active = (strtolower($current_theme->get('Name')) === strtolower($theme_slug) ||
1298 + strtolower($current_theme->get_stylesheet()) === strtolower($theme_slug));
1493 1299
1494 - $theme_exists = wp_get_theme($theme_name)->exists();
1300 + // An active theme is by definition installed. Fall back to a directory
1301 + // lookup for themes that are installed but not currently active.
1302 + $theme_exists = $is_theme_active || wp_get_theme($theme_slug)->exists();
1495 1303
1496 1304 if ($theme_exists) {
1497 1305 return [
1498 1306 'is_installed' => true,
@@ -1541,39 +1349,44 @@
1541 1349 }
1542 1350
1543 1351 public function get_plugin_logo($plugin_key, $type)
1544 1352 {
1353 + // Compatibility icons are bundled locally; loading images from external
1354 + // servers (wp.org, vendor sites, CDNs) counts as offloaded content and
1355 + // is disallowed for wordpress.org releases.
1356 + $assets = plugins_url('assets/images/compat/', dirname(__DIR__) . '/metasync.php');
1357 +
1545 1358 $logos = [
1546 1359 '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',
1360 + 'elementor' => $assets . 'elementor.svg',
1361 + 'gutenberg' => $assets . 'gutenberg.svg',
1362 + 'divi' => $assets . 'divi.svg',
1363 + 'oxygen' => $assets . 'oxygen.svg',
1364 + 'bricks' => $assets . 'bricks.svg',
1552 1365 ],
1553 1366 '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',
1367 + 'astra' => $assets . 'astra.svg',
1368 + 'generatepress' => $assets . 'generatepress.svg',
1369 + 'oceanwp' => $assets . 'oceanwp.svg',
1370 + 'hello-elementor' => $assets . 'hello-elementor.svg',
1371 + 'kadence' => $assets . 'kadence.svg',
1559 1372 ],
1560 1373 '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',
1374 + 'yoast' => $assets . 'yoast.svg',
1375 + 'rankmath' => $assets . 'rankmath.svg',
1376 + 'aioseo' => $assets . 'aioseo.svg',
1564 1377 ],
1565 1378 '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',
1379 + 'wp-rocket' => $assets . 'wp-rocket.svg',
1380 + 'litespeed-cache' => $assets . 'litespeed-cache.svg',
1381 + 'w3-total-cache' => $assets . 'w3-total-cache.svg',
1382 + 'sg-cachepress' => $assets . 'sg-cachepress.svg',
1383 + 'hummingbird-performance' => $assets . 'hummingbird-performance.svg',
1384 + 'autoptimize' => $assets . 'autoptimize.svg',
1572 1385 ],
1573 1386 'cdn' => [
1574 - 'cloudflare' => 'https://ps.w.org/cloudflare/assets/icon-128x128.png',
1575 - 'fastly' => 'https://ps.w.org/purgely/assets/icon-128x128.png',
1387 + 'cloudflare' => $assets . 'cloudflare.svg',
1388 + 'fastly' => $assets . 'fastly.svg',
1576 1389 ],
1577 1390 ];
1578 1391
1579 1392 return isset($logos[$type][$plugin_key]) ? $logos[$type][$plugin_key] : '';