PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.25
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.25
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.6.25, at includes/class-metasync-compatibility-checker.php

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