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
metasync / includes / class-metasync-compatibility-checker.php

class-metasync-compatibility-checker.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.7.2, at includes/class-metasync-compatibility-checker.php

1,433 lines 64.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Compatibility Checker – checks plugin/theme compatibility with MetaSync.
5 *
6 * Extracted from Metasync_Admin to keep the admin class focused on UI scaffolding.
7 *
8 * @package Starter_Plugin
9 */
10
11 if (!defined('ABSPATH')) {
12 exit;
13 }
14
15 class Metasync_Compatibility_Checker
16 {
17 private static $instance = null;
18
19 private function __construct() {}
20
21 public static function instance()
22 {
23 if (null === self::$instance) {
24 self::$instance = new self();
25 }
26 return self::$instance;
27 }
28
29 /**
30 * Compatibility page callback.
31 *
32 * @param object $admin The Metasync_Admin instance (used for render_plugin_header / render_navigation_menu).
33 */
34 public function create_admin_compatibility_page($admin)
35 {
36 $admin->render_layout_open('Compatibility', 'compatibility', 'Check compatibility status with popular plugins, page builders, and caching solutions.');
37 ?>
38
39 <div class="dashboard-card">
40 <?php $this->render_compatibility_sections(); ?>
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; ?>
70
71 <!-- Section: Host Blocking Test -->
72 <div id="ms-comp-host-test" class="dashboard-card" style="margin-top: 20px;">
73 <details open>
74 <summary style="cursor:pointer; list-style:none;">
75 <div style="display:flex; justify-content: space-between; align-items:center;">
76 <div style="flex:1;">
77 <h2 style="margin: 0;">Host Blocking Test</h2>
78 <p style="color: var(--dashboard-text-secondary); margin: 6px 0 0 0;">Verify if this site can reach <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> services.</p>
79 </div>
80 </div>
81 </summary>
82 <div style="margin-top:16px; background: var(--dashboard-bg); border: 1px solid var(--dashboard-border); padding: 20px; border-radius: 8px;">
83 <h3 style="margin-top: 0; color: var(--dashboard-text-primary);">Test Configuration</h3>
84 <p style="margin-bottom: 16px; color: var(--dashboard-text-secondary);">Test connectivity by running both GET and POST requests. Results appear below.</p>
85 <div style="display: flex; gap: 10px; flex-wrap: wrap;">
86 <button type="button" id="test-both-requests" class="button button-primary">
87 <span class="dashicons dashicons-controls-repeat"></span> Test Both Requests
88 </button>
89 </div>
90 <?php $this->render_last_automatic_check(); ?>
91 </div>
92 <div id="host-test-results" style="display: none; margin-top:16px;">
93 <h3 style="color: var(--dashboard-text-primary); margin-bottom: 10px;">Test Results</h3>
94 <div id="test-results-content"></div>
95 </div>
96 </details>
97 </div>
98
99 <?php $this->render_troubleshooting_section(); ?>
100
101 <!-- Host Blocking Test JavaScript -->
102 <script>
103 (function() {
104 function initHostBlockingTest() {
105 if (typeof jQuery === 'undefined') {
106 setTimeout(initHostBlockingTest, 100);
107 return;
108 }
109
110 jQuery(document).ready(function($) {
111 var ajaxUrl = (typeof window.ajaxurl !== 'undefined' && window.ajaxurl)
112 ? window.ajaxurl
113 : '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
114
115 var $btn = $('#test-both-requests');
116
117 $btn.on('click', function(e) {
118 e.preventDefault();
119 e.stopPropagation();
120 runHostTest('BOTH', $);
121 return false;
122 });
123
124 $(document).on('click', '#test-both-requests', function(e) {
125 e.preventDefault();
126 e.stopPropagation();
127 runHostTest('BOTH', $);
128 return false;
129 });
130
131 window.runHostBlockingTest = function() {
132 runHostTest('BOTH', $);
133 };
134
135 function runHostTest(method, $) {
136 method = 'BOTH';
137 var buttonId = 'test-both-requests';
138 var $button = $('#' + buttonId);
139
140 if ($button.length === 0) {
141 alert('Error: Test button not found. Please refresh the page.');
142 return;
143 }
144
145 var originalText = $button.text();
146
147 $button.prop('disabled', true);
148 $button.text('🔄 Testing...');
149
150 var $resultsDiv = $('#host-test-results');
151 var $resultsContent = $('#test-results-content');
152 $resultsDiv.show();
153 $resultsContent.html('<div class="notice notice-info"><p>Running GET and POST test(s)...</p></div>');
154
155 var testsToRun = ['GET', 'POST'];
156 var completedTests = 0;
157 var allResults = [];
158
159 testsToRun.forEach(function(testMethod) {
160 var action = 'metasync_test_host_blocking_' + testMethod.toLowerCase();
161
162 $.ajax({
163 url: ajaxUrl,
164 type: 'POST',
165 dataType: 'json',
166 data: { action: action, nonce: '<?php echo esc_attr(wp_create_nonce("metasync_nonce")); ?>' },
167 timeout: 35000,
168 success: function(response, textStatus, xhr) {
169 try {
170 if (response && response.success && response.data) {
171 allResults.push(response.data);
172 } else {
173 allResults.push({
174 method: testMethod,
175 status: 'error',
176 error: (response && response.data) ? response.data : 'Unexpected response',
177 blocked: true,
178 details: 'Received non-success response from server.'
179 });
180 }
181 } catch (e) {
182 allResults.push({
183 method: testMethod,
184 status: 'error',
185 error: 'Response parse error: ' + (e && e.message ? e.message : e),
186 blocked: true
187 });
188 }
189 finalizeOne();
190 },
191 error: function(xhr, status, error) {
192 var payload = (xhr && xhr.responseText) ? xhr.responseText.substring(0, 500) : '';
193 allResults.push({
194 method: testMethod,
195 status: 'error',
196 error: 'AJAX failed: ' + error + (payload ? ' — ' + payload : ''),
197 blocked: true,
198 details: 'Request did not complete successfully. Status: ' + status
199 });
200 finalizeOne();
201 }
202 });
203 });
204
205 function finalizeOne() {
206 completedTests++;
207 if (completedTests === testsToRun.length) {
208 displayResults(allResults);
209 resetButtons();
210 var $container = $('#host-test-results');
211 if ($container && $container[0] && $container[0].scrollIntoView) {
212 $container[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
213 }
214 }
215 }
216
217 function resetButtons() {
218 $('#test-both-requests').prop('disabled', false);
219 $('#test-both-requests').text('🔄 Test Both Requests');
220 }
221
222 function displayResults(results) {
223 var html = '';
224
225 results.forEach(function(result) {
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' ? '�
232 ' : (inconclusive ? '⚠️' : '❌');
233 var blockedStatus = inconclusive ? 'INCONCLUSIVE' : (result.blocked ? 'BLOCKED' : 'ALLOWED');
234 var blockedClass = inconclusive ? 'inconclusive' : (result.blocked ? 'blocked' : 'allowed');
235
236 html += '<div class="test-result-item ' + statusClass + '">';
237 html += '<div class="test-result-header">';
238 html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
239 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
240 html += '</div>';
241
242 html += '<div class="test-result-details">';
243 html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>';
244 html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>';
245
246 if (result.status_code) {
247 html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + escapeHtml(result.status_code) + '</span></p>';
248 }
249
250 if (result.error) {
251 html += '<p><strong>Error:</strong> <span class="error-message">' + escapeHtml(result.error) + '</span></p>';
252 }
253
254 if (result.body) {
255 html += '<p><strong>Response Body:</strong></p>';
256 html += '<pre class="response-body">' + escapeHtml(result.body) + '</pre>';
257 }
258
259 if (result.headers && Object.keys(result.headers).length > 0) {
260 html += '<p><strong>Response Headers:</strong></p>';
261 html += '<pre class="response-headers">';
262 Object.keys(result.headers).forEach(function(key) {
263 html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
264 });
265 html += '</pre>';
266 }
267
268 if (result.sent_data) {
269 html += '<p><strong>Sent Data:</strong></p>';
270 html += '<pre class="sent-data">' + escapeHtml(JSON.stringify(result.sent_data, null, 2)) + '</pre>';
271 }
272
273 if (result.parsed_response) {
274 html += '<p><strong>Parsed Response:</strong></p>';
275 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
276 }
277
278 html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
279 html += '</div>';
280 html += '</div>';
281 });
282
283 $('#test-results-content').html(html);
284 $('#host-test-results').show();
285 }
286
287 function escapeHtml(text) {
288 var map = {
289 '&': '&amp;',
290 '<': '&lt;',
291 '>': '&gt;',
292 '"': '&quot;',
293 "'": '&#039;'
294 };
295 return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
296 }
297
298 setTimeout(function() {
299 var btnBoth = $('#test-both-requests');
300 }, 500);
301 }
302 });
303 }
304
305 initHostBlockingTest();
306 })();
307 </script>
308
309 <!-- Host Blocking Test CSS -->
310 <style>
311 .test-result-item {
312 background: var(--dashboard-card-bg);
313 border: 1px solid var(--dashboard-border);
314 border-radius: 8px;
315 margin-bottom: 20px;
316 overflow: hidden;
317 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
318 }
319
320 .test-result-item.success {
321 border-left: 4px solid #28a745;
322 }
323
324 .test-result-item.error {
325 border-left: 4px solid #dc3545;
326 }
327
328 .test-result-item.warning {
329 border-left: 4px solid #dba617;
330 }
331
332 .test-result-header {
333 background: var(--dashboard-bg);
334 padding: 15px 20px;
335 border-bottom: 1px solid #dee2e6;
336 display: flex;
337 justify-content: space-between;
338 align-items: center;
339 }
340
341 .test-result-header h4 {
342 margin: 0;
343 color: var(--dashboard-text-primary);
344 font-size: 16px;
345 }
346
347 .test-status {
348 padding: 4px 12px;
349 border-radius: 20px;
350 font-size: 12px;
351 font-weight: bold;
352 text-transform: uppercase;
353 }
354
355 .test-status.allowed {
356 background: rgba(74,222,128,0.15);
357 color: #4ade80;
358 }
359
360 .test-status.blocked {
361 background: rgba(248,113,113,0.15);
362 color: #f87171;
363 }
364
365 .test-status.inconclusive {
366 background: rgba(219,166,23,0.15);
367 color: #dba617;
368 }
369
370 .test-result-details {
371 padding: 20px;
372 }
373
374 .test-result-details p {
375 margin: 8px 0;
376 color: var(--dashboard-text-primary);
377 }
378
379 .test-result-details code {
380 background: var(--dashboard-bg);
381 padding: 2px 6px;
382 border-radius: 3px;
383 font-family: 'Courier New', monospace;
384 color: #e83e8c;
385 }
386
387 .status-code {
388 font-weight: bold;
389 color: #007cba;
390 }
391
392 .error-message {
393 color: #dc3545;
394 font-weight: bold;
395 }
396
397 .response-body, .response-headers, .sent-data, .parsed-response {
398 background: var(--dashboard-bg);
399 border: 1px solid var(--dashboard-border);
400 border-radius: 4px;
401 padding: 12px;
402 margin: 8px 0;
403 font-family: 'Courier New', monospace;
404 font-size: 12px;
405 line-height: 1.4;
406 max-height: 200px;
407 overflow-y: auto;
408 white-space: pre-wrap;
409 word-break: break-all;
410 }
411
412 .response-body {
413 color: var(--dashboard-text-primary);
414 }
415
416 .response-headers {
417 color: var(--dashboard-text-secondary);
418 }
419
420 .sent-data {
421 color: #007cba;
422 }
423
424 .parsed-response {
425 color: #4ade80;
426 background: var(--dashboard-card-bg);
427 border-color: var(--dashboard-border);
428 }
429
430 #host-test-results {
431 margin-top: 20px;
432 }
433
434 #host-test-results h3 {
435 color: var(--dashboard-text-primary);
436 margin-bottom: 15px;
437 padding-bottom: 10px;
438 border-bottom: 2px solid #dee2e6;
439 }
440
441 .button:disabled {
442 opacity: 0.6;
443 cursor: not-allowed;
444 }
445 </style>
446
447 <!-- Section: OTTO Excluded URLs -->
448 <div id="ms-otto-excluded-urls" class="dashboard-card" style="margin-top: 20px;">
449 <details open>
450 <summary style="cursor:pointer; list-style:none;">
451 <div style="display:flex; justify-content: space-between; align-items:center;">
452 <div style="flex:1;">
453 <h2 style="margin: 0;">🚫 <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> Excluded URLs</h2>
454 <p style="color: var(--dashboard-text-secondary); margin: 6px 0 0 0;">Manage URLs where <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> should not run. Add URL patterns to exclude specific pages from <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?> processing.</p>
455 </div>
456 </div>
457 </summary>
458
459 <!-- Add New Excluded URL Form -->
460 <div style="margin-top:16px; background: var(--dashboard-bg); border: 1px solid var(--dashboard-border); padding: 20px; border-radius: 8px;">
461 <h3 style="margin-top: 0; color: var(--dashboard-text-primary);">Add New Excluded URL</h3>
462 <div id="otto-excluded-url-form">
463 <div style="margin-bottom: 15px;">
464 <label for="otto-url-pattern" style="display: block; margin-bottom: 5px; font-weight: 600; color: var(--dashboard-text-primary);">URL Pattern *</label>
465 <input type="text" id="otto-url-pattern" class="regular-text" placeholder="e.g., https://example.com/excluded-page" style="width: 100%; max-width: 500px;" />
466 <p class="description">Enter the URL or pattern you want to exclude from <?php echo esc_html(Metasync::get_whitelabel_otto_name()); ?>.</p>
467 </div>
468
469 <div style="margin-bottom: 15px;">
470 <label for="otto-pattern-type" style="display: block; margin-bottom: 5px; font-weight: 600; color: var(--dashboard-text-primary);">Match Type *</label>
471 <select id="otto-pattern-type" class="regular-text" style="width: 100%; max-width: 300px;">
472 <option value="exact">Exact Match</option>
473 <option value="contain">Contains</option>
474 <!-- <option value="start">Starts With</option>
475 <option value="end">Ends With</option>
476 <option value="regex">Regular Expression</option> -->
477 </select>
478 <p class="description">How the URL should be matched against the pattern.</p>
479 </div>
480
481 <div style="margin-bottom: 15px;">
482 <label for="otto-description" style="display: block; margin-bottom: 5px; font-weight: 600; color: var(--dashboard-text-primary);">Description (Optional)</label>
483 <textarea id="otto-description" rows="3" class="large-text" placeholder="Optional description for this exclusion rule" style="width: 100%; max-width: 500px;"></textarea>
484 </div>
485
486 <div style="display: flex; gap: 10px; align-items: center;">
487 <button type="button" id="otto-add-excluded-url-btn" class="button button-primary">
488 ➕ Add Excluded URL
489 </button>
490 <span id="otto-add-status" style="display: none; padding: 5px 10px; border-radius: 4px;"></span>
491 </div>
492 </div>
493 </div>
494
495 <!-- Excluded URLs List -->
496 <div id="otto-excluded-urls-list" style="margin-top:20px;">
497 <h3 style="color: var(--dashboard-text-primary); margin-bottom: 15px;">Excluded URLs</h3>
498 <div id="otto-excluded-urls-table-container" style="overflow-x: auto;">
499 <div style="text-align: center; padding: 40px; color: var(--dashboard-text-secondary);">
500 <p>Loading excluded URLs...</p>
501 </div>
502 </div>
503 </div>
504 </details>
505 </div>
506
507 <!-- OTTO Excluded URLs CSS -->
508 <style>
509 /* Fix input and select field text colors */
510 #otto-excluded-url-form input[type="text"],
511 #otto-excluded-url-form input.regular-text,
512 #otto-excluded-url-form select,
513 #otto-excluded-url-form select.regular-text,
514 #otto-excluded-url-form textarea,
515 #otto-excluded-url-form textarea.large-text {
516 color: #2c3338 !important;
517 background-color: #fff !important;
518 border: 1px solid #8c8f94 !important;
519 }
520
521 #otto-excluded-url-form select option {
522 color: #2c3338 !important;
523 background-color: #fff !important;
524 }
525
526 #otto-excluded-url-form input[type="text"]:focus,
527 #otto-excluded-url-form select:focus,
528 #otto-excluded-url-form textarea:focus {
529 border-color: #2271b1 !important;
530 box-shadow: 0 0 0 1px #2271b1 !important;
531 outline: 2px solid transparent !important;
532 }
533
534 #otto-excluded-url-form input::placeholder,
535 #otto-excluded-url-form textarea::placeholder {
536 color: #646970 !important;
537 opacity: 0.7;
538 }
539
540 /* Equal width so badges of different lengths (Exact, Contains,
541 etc.) render at the same size for a visually balanced column. */
542 .otto-pattern-type-badge {
543 display: inline-block;
544 padding: 3px 8px;
545 border-radius: 3px;
546 font-size: 11px;
547 font-weight: 600;
548 text-transform: uppercase;
549 letter-spacing: 0.5px;
550 min-width: 90px;
551 text-align: center;
552 box-sizing: border-box;
553 }
554
555 /* Badge text is 11px uppercase, i.e. normal-size text: the darker
556 foreground steps keep every pair at or above the 4.5:1 AA bar
557 on its own pastel background. */
558 .otto-pattern-exact {
559 background: #e3f2fd;
560 color: #0d47a1;
561 }
562
563 .otto-pattern-contain {
564 background: #f3e5f5;
565 color: #7b1fa2;
566 }
567
568 .otto-pattern-start {
569 background: #e8f5e9;
570 color: #1b5e20;
571 }
572
573 .otto-pattern-end {
574 background: #fff3e0;
575 color: #8f4400;
576 }
577
578 .otto-pattern-regex {
579 background: #fce4ec;
580 color: #c2185b;
581 }
582
583 .otto-status-success {
584 background: rgba(74,222,128,0.15);
585 color: #4ade80;
586 border: 1px solid #c3e6cb;
587 }
588
589 .otto-status-error {
590 background: rgba(248,113,113,0.15);
591 color: #f87171;
592 border: 1px solid #f5c6cb;
593 }
594
595 #otto-excluded-urls-table-container .wp-list-table {
596 border: 1px solid #c3c4c7;
597 background: var(--dashboard-card-bg);
598 }
599
600 #otto-excluded-urls-table-container .wp-list-table th {
601 background: #f0f0f1 !important;
602 color: #1d2327 !important;
603 font-weight: 600;
604 border-bottom: 1px solid #c3c4c7;
605 }
606
607 #otto-excluded-urls-table-container .wp-list-table td {
608 background: var(--dashboard-card-bg) !important;
609 color: var(--dashboard-text-primary) !important;
610 border-bottom: 1px solid #c3c4c7;
611 }
612
613 /* Placeholder text (empty description, no recheck date) still has to
614 clear AA against the card background in both themes. */
615 #otto-excluded-urls-table-container .otto-muted {
616 color: var(--dashboard-text-secondary) !important;
617 }
618
619 /* Loading / empty-state text shown in place of the table. */
620 #otto-excluded-urls-table-container .otto-table-message {
621 text-align: center;
622 padding: 40px;
623 color: var(--dashboard-text-secondary);
624 }
625
626 /* Error text replaces the table on a failed load. No single red
627 clears AA on both card backgrounds, so each theme gets its own. */
628 #otto-excluded-urls-table-container .otto-error-message {
629 color: #f87171 !important;
630 }
631
632 [data-theme="light"] #otto-excluded-urls-table-container .otto-error-message {
633 color: #b02a37 !important;
634 }
635
636 #otto-excluded-urls-table-container .wp-list-table tbody tr {
637 background: var(--dashboard-card-bg);
638 }
639
640 #otto-excluded-urls-table-container .wp-list-table tbody tr:hover {
641 background: #f6f7f7;
642 }
643
644 #otto-excluded-urls-table-container .wp-list-table code {
645 background: #f0f0f1 !important;
646 color: #1d2327 !important;
647 padding: 2px 6px;
648 border-radius: 3px;
649 font-size: 12px;
650 border: 1px solid #dcdcde;
651 }
652
653 #otto-excluded-urls-table-container .otto-delete-url,
654 #otto-excluded-urls-table-container .otto-recheck-url,
655 #otto-excluded-urls-table-container .otto-recheck-disabled {
656 padding: 4px 10px;
657 font-size: 12px;
658 box-sizing: border-box;
659 height: 26px;
660 line-height: 1;
661 }
662
663 #otto-excluded-urls-table-container .otto-delete-url {
664 background: #dc3545 !important;
665 color: #fff !important;
666 border-color: #dc3545 !important;
667 }
668
669 #otto-excluded-urls-table-container .otto-delete-url:hover {
670 background: #c82333 !important;
671 border-color: #bd2130 !important;
672 color: #fff !important;
673 }
674
675 .button.disabled {
676 opacity: 0.5;
677 cursor: not-allowed;
678 pointer-events: none;
679 }
680
681 /* Pagination styling */
682 .tablenav {
683 background: #f0f0f1;
684 padding: 10px;
685 border-radius: 4px;
686 }
687
688 .displaying-num {
689 color: #646970 !important;
690 }
691
692 .pagination-links .button {
693 color: #2271b1 !important;
694 background: var(--dashboard-card-bg) !important;
695 border-color: #2271b1 !important;
696 }
697
698 .pagination-links .button:hover {
699 background: #2271b1 !important;
700 color: #fff !important;
701 }
702
703 .tablenav-paging-text {
704 color: #2c3338 !important;
705 }
706 </style>
707 <?php
708 $admin->render_layout_close();
709 }
710
711 /**
712 * Show the result of the last automatic host blocking check.
713 *
714 * Reads the stored result only — it never triggers a probe, so opening this page
715 * costs nothing extra. The automatic check runs ~10 minutes after activation and
716 * weekly after that; the button above re-runs the same probe on demand.
717 */
718 private function render_last_automatic_check(): void
719 {
720 if (!class_exists('Metasync_Host_Blocking_Check')) {
721 return;
722 }
723
724 $checker = Metasync_Host_Blocking_Check::get_instance();
725 $result = $checker->get_last_result();
726
727 echo '<p style="margin: 16px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">';
728
729 if ($result === null) {
730 if (!$checker->site_is_publicly_reachable()) {
731 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');
732 } else {
733 echo esc_html__('The automatic check has not run yet. It runs about 10 minutes after activation, then weekly.', 'metasync');
734 }
735 echo '</p>';
736 return;
737 }
738
739 $blocked = [];
740 if ($result['get_blocked']) {
741 $blocked[] = 'GET';
742 }
743 if ($result['post_blocked']) {
744 $blocked[] = 'POST';
745 }
746
747 if (empty($blocked)) {
748 printf(
749 /* translators: %s: date/time of the last automatic check */
750 esc_html__('Last automatic check: %s — no blocking detected.', 'metasync'),
751 esc_html($result['checked_at'])
752 );
753 } else {
754 printf(
755 /* translators: 1: date/time of the last automatic check, 2: blocked HTTP methods */
756 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'),
757 esc_html($result['checked_at']),
758 esc_html(implode(' and ', $blocked))
759 );
760 }
761
762 echo '</p>';
763 }
764
765 public function render_compatibility_sections()
766 {
767 ?>
768 <div class="metasync-settings-accordion">
769 <?php $this->render_compatibility_section(
770 $this->get_page_builders_compatibility(),
771 'admin-page',
772 'Page Builders',
773 'Elementor, Divi, Gutenberg, Oxygen, Bricks'
774 ); ?>
775 <?php $this->render_compatibility_section(
776 $this->get_themes_compatibility(),
777 'art',
778 'Themes',
779 'Astra, GeneratePress, OceanWP and more'
780 ); ?>
781 <?php $this->render_compatibility_section(
782 $this->get_seo_plugins_compatibility(),
783 'search',
784 'SEO Plugins',
785 'Yoast, Rank Math, AIOSEO and others'
786 ); ?>
787 <?php $this->render_compatibility_section(
788 $this->get_cache_plugins_compatibility(),
789 'performance',
790 'Cache &amp; Performance',
791 'WP Rocket, LiteSpeed, W3 Total Cache and more'
792 ); ?>
793 <?php $this->render_compatibility_section(
794 $this->get_cdn_compatibility(),
795 'networking',
796 'CDN &amp; Hosting',
797 'Cloudflare, Fastly, Bunny CDN, Kinsta, Flywheel'
798 ); ?>
799 </div>
800 <?php
801 }
802
803 /**
804 * Generic section renderer — expandable accordion row.
805 */
806 private function render_compatibility_section( array $items, string $dashicon, string $title, string $description = '' ): void
807 {
808 $section_key = sanitize_title( $title );
809 $section_id = 'msrp-section-' . $section_key;
810 $active_count = count( array_filter( $items, fn( $i ) => $i['is_installed'] && $i['is_active'] ) );
811 $total = count( $items );
812 $subtitle = $active_count > 0
813 ? $active_count . ' of ' . $total . ' active'
814 : ( $description ?: $total . ' checked' );
815 ?>
816 <div class="metasync-accordion-section" data-section="<?php echo esc_attr( $section_key ); ?>">
817 <div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="false" aria-controls="<?php echo esc_attr( $section_id ); ?>">
818 <div class="metasync-accordion-title">
819 <span class="metasync-accordion-icon">
820 <span class="dashicons dashicons-<?php echo esc_attr( $dashicon ); ?>"></span>
821 </span>
822 <div class="metasync-accordion-text">
823 <h3><?php echo wp_kses_post( $title ); ?></h3>
824 <p class="metasync-accordion-description"><?php echo esc_html( $subtitle ); ?></p>
825 </div>
826 </div>
827 <button type="button" class="metasync-accordion-toggle" aria-label="Toggle section">
828 <span class="toggle-icon">▼</span>
829 </button>
830 </div>
831 <div class="metasync-accordion-content msrp-compat-accordion" id="<?php echo esc_attr( $section_id ); ?>" data-state="closed">
832 <div class="msrp-compat-list">
833 <?php foreach ( $items as $item ):
834 $is_active = $item['is_installed'] && $item['is_active'];
835 $is_installed = $item['is_installed'] && ! $item['is_active'];
836 $indicator = $is_active ? 'active' : ( $is_installed ? 'installed' : 'none' );
837 ?>
838 <div class="msrp-compat-row msrp-indicator-<?php echo esc_attr( $indicator ); ?>">
839 <div class="msrp-compat-row-left">
840 <span class="msrp-compat-name"><?php echo esc_html( $item['name'] ); ?></span>
841 <?php if ( ! empty( $item['note'] ) ): ?>
842 <span class="msrp-compat-note"><?php echo esc_html( $item['note'] ); ?></span>
843 <?php endif; ?>
844 </div>
845 <div class="msrp-compat-row-right">
846 <?php if ( $is_active ): ?>
847 <span class="msrp-pill msrp-pill-active">Active</span>
848 <?php elseif ( $is_installed ): ?>
849 <span class="msrp-pill msrp-pill-installed">Installed</span>
850 <?php else: ?>
851 <span class="msrp-pill msrp-pill-none">Not installed</span>
852 <?php endif; ?>
853 <span class="msrp-pill msrp-pill-supported"><?php echo esc_html( $item['status_text'] ); ?></span>
854 </div>
855 </div>
856 <?php endforeach; ?>
857 </div>
858 </div>
859 </div>
860 <?php
861 }
862
863 /**
864 * Troubleshooting section — rendered separately below the host test card.
865 */
866 public function render_troubleshooting_section(): void
867 {
868 $plugin_name = Metasync::get_effective_plugin_name();
869 $otto_name = Metasync::get_whitelabel_otto_name();
870 $issues = [
871 [
872 'title' => 'Title tag disappears when Yoast SEO is active',
873 'cause' => 'Yoast SEO takes ownership of the <title> tag. ' . $plugin_name . ' filters may conflict if both try to modify the document title at the same priority.',
874 'fix' => 'Go to ' . $plugin_name . ' → Settings → Compatibility and make sure "Yoast SEO title handoff" is enabled. This lets Yoast own the title while ' . $plugin_name . ' passes its optimised title through Yoast.',
875 ],
876 [
877 'title' => $otto_name . ' changes not appearing after cache flush',
878 'cause' => 'Some cache plugins (WP Rocket, Kinsta) serve stale HTML even after a purge if cache warm-up races with the write.',
879 'fix' => 'Increase the cache warm-up timeout in ' . $plugin_name . ' → Settings → Advanced, or disable "Immediate Warm-up" and let the cache rebuild organically.',
880 ],
881 [
882 'title' => 'SEO meta not showing for Oxygen Builder pages',
883 'cause' => 'Oxygen replaces the standard WordPress template, bypassing some meta output hooks.',
884 'fix' => $plugin_name . ' automatically handles Oxygen compatibility — no setting required. If meta is still missing, ensure ' . $plugin_name . ' is up to date and try clearing any server-side or CDN cache.',
885 ],
886 [
887 'title' => 'Schema markup injected on every page instead of the target page only',
888 'cause' => 'A known bug in versions prior to 2.5.20 where schema was registered globally.',
889 'fix' => 'Update to the latest version. Go to Settings → Schema Markup and re-save your configuration.',
890 ],
891 [
892 'title' => 'Sitemap returns 404',
893 'cause' => 'Permalink flush needed after activation or after a WordPress update.',
894 'fix' => 'Go to Settings → Permalinks in WordPress admin and click "Save Changes" (no actual change needed — this flushes rewrite rules).',
895 ],
896 [
897 'title' => 'Cloudflare / CDN caching stale SEO meta',
898 'cause' => 'CDN caches the full HTML response including meta tags. After updating SEO meta, the CDN may serve old content.',
899 'fix' => 'Purge the CDN cache for the affected URL via your CDN dashboard, or enable automatic purge integration in ' . $plugin_name . ' → Settings → Cache.',
900 ],
901 ];
902 ?>
903 <div id="ms-comp-troubleshooting" class="dashboard-card" style="margin-top: 20px;">
904 <div style="display:flex; align-items:center; gap:10px; margin-bottom:6px;">
905 <span class="dashicons dashicons-sos" style="font-size:24px;width:24px;height:24px;color:var(--dashboard-accent);"></span>
906 <div>
907 <h2 style="margin:0;">Troubleshooting</h2>
908 <p style="color:var(--dashboard-text-secondary); margin:4px 0 0 0;">Common issues and how to fix them.</p>
909 </div>
910 </div>
911 <div style="margin-top:16px; display:flex; flex-direction:column; gap:8px;">
912 <?php foreach ( $issues as $i => $issue ): ?>
913 <details style="border:1px solid var(--dashboard-border); border-radius:8px; overflow:hidden;">
914 <summary style="cursor:pointer; padding:14px 16px; list-style:none; display:flex; align-items:center; justify-content:space-between; gap:12px; background:var(--dashboard-bg); font-weight:600; font-size:14px; color:var(--dashboard-text-primary);">
915 <span style="display:flex; align-items:center; gap:10px;">
916 <span class="dashicons dashicons-warning" style="font-size:16px;width:16px;height:16px;color:#f59e0b;flex-shrink:0;"></span>
917 <?php echo esc_html( $issue['title'] ); ?>
918 </span>
919 <span class="dashicons dashicons-arrow-down-alt2" style="font-size:14px;width:14px;height:14px;color:var(--dashboard-text-secondary);flex-shrink:0;transition:transform .2s;"></span>
920 </summary>
921 <div style="padding:16px; background:var(--dashboard-card-bg); border-top:1px solid var(--dashboard-border);">
922 <p style="margin:0 0 10px 0; color:var(--dashboard-text-secondary); font-size:13px;">
923 <strong style="color:var(--dashboard-text-primary);">Cause:</strong> <?php echo esc_html( $issue['cause'] ); ?>
924 </p>
925 <p style="margin:0; color:var(--dashboard-text-secondary); font-size:13px;">
926 <strong style="color:var(--dashboard-text-primary);">Fix:</strong> <?php echo esc_html( $issue['fix'] ); ?>
927 </p>
928 </div>
929 </details>
930 <?php endforeach; ?>
931 </div>
932 </div>
933 <script>
934 document.querySelectorAll('#ms-comp-troubleshooting details').forEach(function(el) {
935 el.addEventListener('toggle', function() {
936 var arrow = el.querySelector('.dashicons-arrow-down-alt2');
937 if (arrow) arrow.style.transform = el.open ? 'rotate(180deg)' : '';
938 });
939 });
940 </script>
941 <?php
942 }
943
944 /**
945 * Render Lock Section button for protected tabs.
946 *
947 * @param string $tab The tab identifier (general, whitelabel, advanced)
948 */
949 public function render_lock_button($tab)
950 {
951 ?>
952 <button type="button"
953 class="metasync-lock-btn"
954 data-tab="<?php echo esc_attr($tab); ?>"
955 style="background: #dc3545; color: white; border: none; padding: 10px 20px; border-radius: 6px; font-size: 14px; font-weight: 600; cursor: pointer; display: inline-flex; align-items: center; gap: 8px; transition: all 0.3s ease;"
956 onmouseover="this.style.background='#c82333'"
957 onmouseout="this.style.background='#dc3545'"
958 onfocus="this.style.background='#c82333'"
959 onblur="this.style.background='#dc3545'"
960 aria-label="Lock this section and require password for access">
961 <span class="dashicons dashicons-lock" style="font-size:16px;width:16px;height:16px;" aria-hidden="true"></span>
962 Lock Section
963 </button>
964 <?php
965 }
966
967 public function get_page_builders_compatibility()
968 {
969 $builders = [
970 'elementor' => [
971 'name' => 'Elementor',
972 'plugin_files' => [ 'elementor/elementor.php', 'elementor-pro/elementor-pro.php' ],
973 'supported' => true,
974 'version' => '3.31.5',
975 ],
976 'gutenberg' => [
977 'name' => 'WordPress Block Editor',
978 'plugin_files' => [ 'gutenberg/gutenberg.php' ],
979 'supported' => true,
980 'is_core' => true,
981 'version' => get_bloginfo( 'version' ),
982 ],
983 'divi' => [
984 'name' => 'Divi Builder',
985 'plugin_files' => [ 'divi-builder/divi-builder.php' ],
986 'theme_name' => 'Divi',
987 'supported' => true,
988 'version' => '',
989 ],
990 'oxygen' => [
991 'name' => 'Oxygen Builder',
992 'plugin_files' => [ 'oxygen/functions.php' ],
993 'supported' => true,
994 'version' => '4.9',
995 'note' => 'Enable Oxygen Compatibility in Settings for correct meta output.',
996 ],
997 'bricks' => [
998 'name' => 'Bricks Builder',
999 'plugin_files' => [ 'bricks/bricks.php' ],
1000 'theme_name' => 'Bricks',
1001 'supported' => true,
1002 'version' => '1.11',
1003 ],
1004 ];
1005
1006 $result = [];
1007
1008 foreach ($builders as $key => $builder) {
1009 $is_core = isset($builder['is_core']) && $builder['is_core'];
1010 // The array key is the theme directory slug; pass it (not the display name)
1011 // so wp_get_theme() can resolve the theme on case-sensitive filesystems.
1012 $theme_slug = isset($builder['theme_name']) ? $key : null;
1013 $plugin_status = $this->get_plugin_status($builder['plugin_files'], $is_core, $theme_slug);
1014
1015 if ($builder['supported']) {
1016 $status = 'supported';
1017 $status_text = 'Supported';
1018 } else {
1019 $status = 'coming-soon';
1020 $status_text = 'Coming Soon';
1021 }
1022
1023 $result[] = [
1024 'name' => $builder['name'],
1025 'version' => $builder['version'],
1026 'status' => $status,
1027 'status_text' => $status_text,
1028 'is_installed' => $plugin_status['is_installed'],
1029 'is_active' => $plugin_status['is_active'],
1030 'note' => $builder['note'] ?? '',
1031 'logo' => $this->get_plugin_logo( $key, 'page_builder' ),
1032 ];
1033 }
1034
1035 return $result;
1036 }
1037
1038 public function get_seo_plugins_compatibility()
1039 {
1040 $plugins = [
1041 'yoast' => [
1042 'name' => 'Yoast SEO',
1043 'plugin_files' => [
1044 'wordpress-seo/wp-seo.php',
1045 'wordpress-seo-premium/wp-seo-premium.php'
1046 ],
1047 'supported' => true,
1048 'version' => '25.9.0'
1049 ],
1050 'rankmath' => [
1051 'name' => 'Rank Math',
1052 'plugin_files' => [
1053 'seo-by-rank-math/rank-math.php',
1054 'seo-by-rank-math-pro/rank-math-pro.php'
1055 ],
1056 'supported' => true,
1057 'version' => '1.0.253.0'
1058 ],
1059 'aioseo' => [
1060 'name' => 'All in One SEO',
1061 'plugin_files' => [
1062 'all-in-one-seo-pack/all_in_one_seo_pack.php',
1063 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php'
1064 ],
1065 'supported' => true,
1066 'version' => '4.8.7'
1067 ]
1068 ];
1069
1070 $result = [];
1071
1072 foreach ($plugins as $key => $plugin) {
1073 $plugin_status = $this->get_plugin_status($plugin['plugin_files']);
1074
1075 if ($plugin['supported']) {
1076 $status = 'supported';
1077 $status_text = 'Supported';
1078 } else {
1079 $status = 'coming-soon';
1080 $status_text = 'Coming Soon';
1081 }
1082
1083 $result[] = [
1084 'name' => $plugin['name'],
1085 'version' => $plugin['version'],
1086 'status' => $status,
1087 'status_text' => $status_text,
1088 'is_installed' => $plugin_status['is_installed'],
1089 'is_active' => $plugin_status['is_active'],
1090 'note' => $plugin['note'] ?? '',
1091 'logo' => $this->get_plugin_logo( $key, 'seo' ),
1092 ];
1093 }
1094
1095 return $result;
1096 }
1097
1098 public function get_themes_compatibility()
1099 {
1100 $themes = [
1101 'astra' => [
1102 'name' => 'Astra',
1103 'theme_name' => 'Astra',
1104 'plugin_files' => [],
1105 'supported' => true,
1106 'version' => '4.8',
1107 'note' => 'Full compatibility including title tag and schema support.',
1108 ],
1109 'generatepress' => [
1110 'name' => 'GeneratePress',
1111 'theme_name' => 'GeneratePress',
1112 'plugin_files' => [],
1113 'supported' => true,
1114 'version' => '3.5',
1115 ],
1116 'oceanwp' => [
1117 'name' => 'OceanWP',
1118 'theme_name' => 'OceanWP',
1119 'plugin_files' => [],
1120 'supported' => true,
1121 'version' => '3.5',
1122 ],
1123 'hello-elementor' => [
1124 'name' => 'Hello Elementor',
1125 'theme_name' => 'Hello Elementor',
1126 'plugin_files' => [],
1127 'supported' => true,
1128 'version' => '3.1',
1129 ],
1130 'kadence' => [
1131 'name' => 'Kadence',
1132 'theme_name' => 'Kadence',
1133 'plugin_files' => [],
1134 'supported' => true,
1135 'version' => '1.2',
1136 ],
1137 ];
1138
1139 $result = [];
1140 foreach ( $themes as $key => $theme ) {
1141 // The array key is the theme directory slug; pass it (not the display name)
1142 // so wp_get_theme() can resolve the theme on case-sensitive filesystems.
1143 $status = $this->get_plugin_status( $theme['plugin_files'], false, $key );
1144 $result[] = [
1145 'name' => $theme['name'],
1146 'version' => $theme['version'],
1147 'status' => 'supported',
1148 'status_text' => 'Supported',
1149 'is_installed' => $status['is_installed'],
1150 'is_active' => $status['is_active'],
1151 'note' => $theme['note'] ?? '',
1152 'logo' => $this->get_plugin_logo( $key, 'theme' ),
1153 ];
1154 }
1155 return $result;
1156 }
1157
1158 public function get_cache_plugins_compatibility()
1159 {
1160 $plugins = [
1161 'wp-rocket' => [
1162 'name' => 'WP Rocket',
1163 'plugin_files' => [ 'wp-rocket/wp-rocket.php' ],
1164 'supported' => true,
1165 'version' => '3.16',
1166 'note' => 'Automatic cache purge on OTTO update.',
1167 ],
1168 'litespeed-cache' => [
1169 'name' => 'LiteSpeed Cache',
1170 'plugin_files' => [ 'litespeed-cache/litespeed-cache.php' ],
1171 'supported' => true,
1172 'version' => '7.5.0.1',
1173 ],
1174 'w3-total-cache' => [
1175 'name' => 'W3 Total Cache',
1176 'plugin_files' => [ 'w3-total-cache/w3-total-cache.php' ],
1177 'supported' => true,
1178 'version' => '2.7',
1179 ],
1180 'sg-cachepress' => [
1181 'name' => 'SiteGround Optimizer',
1182 'plugin_files' => [ 'sg-cachepress/sg-cachepress.php' ],
1183 'supported' => true,
1184 'version' => '7.6',
1185 ],
1186 'hummingbird-performance' => [
1187 'name' => 'Hummingbird',
1188 'plugin_files' => [ 'hummingbird-performance/wp-hummingbird.php' ],
1189 'supported' => true,
1190 'version' => '3.9',
1191 ],
1192 'autoptimize' => [
1193 'name' => 'Autoptimize',
1194 'plugin_files' => [ 'autoptimize/autoptimize.php' ],
1195 'supported' => true,
1196 'version' => '3.1',
1197 ],
1198 'comet-cache' => [
1199 'name' => 'Comet Cache',
1200 'plugin_files' => [ 'comet-cache/comet-cache.php' ],
1201 'supported' => true,
1202 'version' => '17.6',
1203 ],
1204 'cache-enabler' => [
1205 'name' => 'Cache Enabler',
1206 'plugin_files' => [ 'cache-enabler/cache-enabler.php' ],
1207 'supported' => true,
1208 'version' => '1.8',
1209 ],
1210 'breeze' => [
1211 'name' => 'Breeze (Cloudways)',
1212 'plugin_files' => [ 'breeze/breeze.php' ],
1213 'supported' => true,
1214 'version' => '2.1',
1215 ],
1216 ];
1217
1218 $result = [];
1219
1220 foreach ($plugins as $key => $plugin) {
1221 $plugin_status = $this->get_plugin_status($plugin['plugin_files']);
1222
1223 if ($plugin['supported']) {
1224 $status = 'supported';
1225 $status_text = 'Supported';
1226 } else {
1227 $status = 'coming-soon';
1228 $status_text = 'Coming Soon';
1229 }
1230
1231 $result[] = [
1232 'name' => $plugin['name'],
1233 'version' => $plugin['version'],
1234 'status' => $status,
1235 'status_text' => $status_text,
1236 'is_installed' => $plugin_status['is_installed'],
1237 'is_active' => $plugin_status['is_active'],
1238 'note' => $plugin['note'] ?? '',
1239 'logo' => $this->get_plugin_logo( $key, 'cache' ),
1240 ];
1241 }
1242
1243 return $result;
1244 }
1245
1246 public function get_cdn_compatibility()
1247 {
1248 $providers = [
1249 'cloudflare' => [
1250 'name' => 'Cloudflare',
1251 'plugin_files' => [ 'cloudflare/cloudflare.php' ],
1252 'supported' => true,
1253 'version' => '',
1254 'note' => 'Cache purge supported. Ensure HTML caching is disabled or bypass rules set for admin.',
1255 ],
1256 'fastly' => [
1257 'name' => 'Fastly',
1258 'plugin_files' => [ 'fastly/fastly.php' ],
1259 'supported' => true,
1260 'version' => '',
1261 'note' => 'Edge cache purge triggered on OTTO update.',
1262 ],
1263 'bunnycdn' => [
1264 'name' => 'Bunny CDN',
1265 'plugin_files' => [ 'bunnycdn/bunnycdn.php', 'bunny-cdn/bunny-cdn.php' ],
1266 'supported' => true,
1267 'version' => '',
1268 ],
1269 'kinsta-mu-plugins' => [
1270 'name' => 'Kinsta (Managed Hosting)',
1271 'plugin_files' => [ 'kinsta-mu-plugins/kinsta-mu-plugins.php' ],
1272 'supported' => true,
1273 'version' => '',
1274 'note' => 'Kinsta Varnish/Nginx cache purged per-URL on OTTO update.',
1275 ],
1276 'flywheel' => [
1277 'name' => 'Flywheel / Local',
1278 'plugin_files' => [ 'flywheel-common/plugin.php' ],
1279 'supported' => true,
1280 'version' => '',
1281 'note' => 'Varnish cache purge supported.',
1282 ],
1283 ];
1284
1285 $result = [];
1286 foreach ( $providers as $key => $provider ) {
1287 $status = $this->get_plugin_status( $provider['plugin_files'] );
1288 $result[] = [
1289 'name' => $provider['name'],
1290 'version' => $provider['version'],
1291 'status' => 'supported',
1292 'status_text' => 'Supported',
1293 'is_installed' => $status['is_installed'],
1294 'is_active' => $status['is_active'],
1295 'note' => $provider['note'] ?? '',
1296 'logo' => $this->get_plugin_logo( $key, 'cdn' ),
1297 ];
1298 }
1299 return $result;
1300 }
1301
1302 /**
1303 * @deprecated Use get_plugin_status() instead
1304 */
1305 public function is_plugin_installed($plugin_file)
1306 {
1307 if (!function_exists('is_plugin_active')) {
1308 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1309 }
1310 return is_plugin_active($plugin_file);
1311 }
1312
1313 /**
1314 * Get detailed plugin status (installed and/or active).
1315 *
1316 * @param array $plugin_files Array of plugin file paths to check
1317 * @param bool $is_core Whether this is a WordPress core feature
1318 * @param string $theme_slug Optional theme directory slug to check (e.g. 'hello-elementor',
1319 * 'astra'). Must be the directory slug, not the display name,
1320 * because wp_get_theme() resolves themes by directory slug.
1321 * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
1322 */
1323 public function get_plugin_status($plugin_files, $is_core = false, $theme_slug = null)
1324 {
1325 if ($is_core) {
1326 return [
1327 'is_installed' => true,
1328 'is_active' => true,
1329 'active_version' => 'core'
1330 ];
1331 }
1332
1333 if ($theme_slug) {
1334 $current_theme = wp_get_theme();
1335 $is_theme_active = (strtolower($current_theme->get('Name')) === strtolower($theme_slug) ||
1336 strtolower($current_theme->get_stylesheet()) === strtolower($theme_slug));
1337
1338 // An active theme is by definition installed. Fall back to a directory
1339 // lookup for themes that are installed but not currently active.
1340 $theme_exists = $is_theme_active || wp_get_theme($theme_slug)->exists();
1341
1342 if ($theme_exists) {
1343 return [
1344 'is_installed' => true,
1345 'is_active' => $is_theme_active,
1346 'active_version' => $is_theme_active ? 'theme' : null
1347 ];
1348 }
1349 }
1350
1351 if (!function_exists('is_plugin_active')) {
1352 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1353 }
1354
1355 if (!function_exists('get_plugins')) {
1356 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1357 }
1358
1359 if (!is_array($plugin_files)) {
1360 $plugin_files = [$plugin_files];
1361 }
1362
1363 $is_installed = false;
1364 $is_active = false;
1365 $active_version = null;
1366
1367 foreach ($plugin_files as $plugin_file) {
1368 $plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
1369 if (file_exists($plugin_path)) {
1370 $is_installed = true;
1371
1372 if (is_plugin_active($plugin_file)) {
1373 $is_active = true;
1374 $active_version = strpos($plugin_file, 'pro') !== false ||
1375 strpos($plugin_file, 'premium') !== false ?
1376 'premium' : 'free';
1377 break;
1378 }
1379 }
1380 }
1381
1382 return [
1383 'is_installed' => $is_installed,
1384 'is_active' => $is_active,
1385 'active_version' => $active_version
1386 ];
1387 }
1388
1389 public function get_plugin_logo($plugin_key, $type)
1390 {
1391 // Compatibility icons are bundled locally; loading images from external
1392 // servers (wp.org, vendor sites, CDNs) counts as offloaded content and
1393 // is disallowed for wordpress.org releases.
1394 $assets = plugins_url('assets/images/compat/', dirname(__DIR__) . '/metasync.php');
1395
1396 $logos = [
1397 'page_builder' => [
1398 'elementor' => $assets . 'elementor.svg',
1399 'gutenberg' => $assets . 'gutenberg.svg',
1400 'divi' => $assets . 'divi.svg',
1401 'oxygen' => $assets . 'oxygen.svg',
1402 'bricks' => $assets . 'bricks.svg',
1403 ],
1404 'theme' => [
1405 'astra' => $assets . 'astra.svg',
1406 'generatepress' => $assets . 'generatepress.svg',
1407 'oceanwp' => $assets . 'oceanwp.svg',
1408 'hello-elementor' => $assets . 'hello-elementor.svg',
1409 'kadence' => $assets . 'kadence.svg',
1410 ],
1411 'seo' => [
1412 'yoast' => $assets . 'yoast.svg',
1413 'rankmath' => $assets . 'rankmath.svg',
1414 'aioseo' => $assets . 'aioseo.svg',
1415 ],
1416 'cache' => [
1417 'wp-rocket' => $assets . 'wp-rocket.svg',
1418 'litespeed-cache' => $assets . 'litespeed-cache.svg',
1419 'w3-total-cache' => $assets . 'w3-total-cache.svg',
1420 'sg-cachepress' => $assets . 'sg-cachepress.svg',
1421 'hummingbird-performance' => $assets . 'hummingbird-performance.svg',
1422 'autoptimize' => $assets . 'autoptimize.svg',
1423 ],
1424 'cdn' => [
1425 'cloudflare' => $assets . 'cloudflare.svg',
1426 'fastly' => $assets . 'fastly.svg',
1427 ],
1428 ];
1429
1430 return isset($logos[$type][$plugin_key]) ? $logos[$type][$plugin_key] : '';
1431 }
1432 }
1433