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

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