PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.22
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.22
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 2.6.4 2.6.3 2.5.23 All 138 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.22, at includes/class-metasync-compatibility-checker.php

1,296 lines 58.1 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 </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, nonce: '<?php echo wp_create_nonce("metasync_nonce"); ?>' },
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 CSS -->
466 <style>
467 /* Fix input and select field text colors */
468 #otto-excluded-url-form input[type="text"],
469 #otto-excluded-url-form input.regular-text,
470 #otto-excluded-url-form select,
471 #otto-excluded-url-form select.regular-text,
472 #otto-excluded-url-form textarea,
473 #otto-excluded-url-form textarea.large-text {
474 color: #2c3338 !important;
475 background-color: #fff !important;
476 border: 1px solid #8c8f94 !important;
477 }
478
479 #otto-excluded-url-form select option {
480 color: #2c3338 !important;
481 background-color: #fff !important;
482 }
483
484 #otto-excluded-url-form input[type="text"]:focus,
485 #otto-excluded-url-form select:focus,
486 #otto-excluded-url-form textarea:focus {
487 border-color: #2271b1 !important;
488 box-shadow: 0 0 0 1px #2271b1 !important;
489 outline: 2px solid transparent !important;
490 }
491
492 #otto-excluded-url-form input::placeholder,
493 #otto-excluded-url-form textarea::placeholder {
494 color: #646970 !important;
495 opacity: 0.7;
496 }
497
498 .otto-pattern-type-badge {
499 display: inline-block;
500 padding: 3px 8px;
501 border-radius: 3px;
502 font-size: 11px;
503 font-weight: 600;
504 text-transform: uppercase;
505 letter-spacing: 0.5px;
506 }
507
508 .otto-pattern-exact {
509 background: #e3f2fd;
510 color: #1976d2;
511 }
512
513 .otto-pattern-contain {
514 background: #f3e5f5;
515 color: #7b1fa2;
516 }
517
518 .otto-pattern-start {
519 background: #e8f5e9;
520 color: #388e3c;
521 }
522
523 .otto-pattern-end {
524 background: var(--dashboard-card-bg)3e0;
525 color: #f57c00;
526 }
527
528 .otto-pattern-regex {
529 background: #fce4ec;
530 color: #c2185b;
531 }
532
533 .otto-status-success {
534 background: rgba(74,222,128,0.15);
535 color: #4ade80;
536 border: 1px solid #c3e6cb;
537 }
538
539 .otto-status-error {
540 background: rgba(248,113,113,0.15);
541 color: #f87171;
542 border: 1px solid #f5c6cb;
543 }
544
545 #otto-excluded-urls-table-container .wp-list-table {
546 border: 1px solid #c3c4c7;
547 background: var(--dashboard-card-bg);
548 }
549
550 #otto-excluded-urls-table-container .wp-list-table th {
551 background: #f0f0f1 !important;
552 color: #1d2327 !important;
553 font-weight: 600;
554 border-bottom: 1px solid #c3c4c7;
555 }
556
557 #otto-excluded-urls-table-container .wp-list-table td {
558 background: var(--dashboard-card-bg) !important;
559 color: #2c3338 !important;
560 border-bottom: 1px solid #c3c4c7;
561 }
562
563 #otto-excluded-urls-table-container .wp-list-table tbody tr {
564 background: var(--dashboard-card-bg);
565 }
566
567 #otto-excluded-urls-table-container .wp-list-table tbody tr:hover {
568 background: #f6f7f7;
569 }
570
571 #otto-excluded-urls-table-container .wp-list-table code {
572 background: #f0f0f1 !important;
573 color: #1d2327 !important;
574 padding: 2px 6px;
575 border-radius: 3px;
576 font-size: 12px;
577 border: 1px solid #dcdcde;
578 }
579
580 #otto-excluded-urls-table-container .otto-delete-url,
581 #otto-excluded-urls-table-container .otto-recheck-url {
582 padding: 4px 10px;
583 font-size: 12px;
584 }
585
586 #otto-excluded-urls-table-container .otto-delete-url {
587 background: #dc3545 !important;
588 color: #fff !important;
589 border-color: #dc3545 !important;
590 }
591
592 #otto-excluded-urls-table-container .otto-delete-url:hover {
593 background: #c82333 !important;
594 border-color: #bd2130 !important;
595 color: #fff !important;
596 }
597
598 .button.disabled {
599 opacity: 0.5;
600 cursor: not-allowed;
601 pointer-events: none;
602 }
603
604 /* Pagination styling */
605 .tablenav {
606 background: #f0f0f1;
607 padding: 10px;
608 border-radius: 4px;
609 }
610
611 .displaying-num {
612 color: #646970 !important;
613 }
614
615 .pagination-links .button {
616 color: #2271b1 !important;
617 background: var(--dashboard-card-bg) !important;
618 border-color: #2271b1 !important;
619 }
620
621 .pagination-links .button:hover {
622 background: #2271b1 !important;
623 color: #fff !important;
624 }
625
626 .tablenav-paging-text {
627 color: #2c3338 !important;
628 }
629 </style>
630 <?php
631 $admin->render_layout_close();
632 }
633
634 public function render_compatibility_sections()
635 {
636 ?>
637 <div class="metasync-settings-accordion">
638 <?php $this->render_compatibility_section(
639 $this->get_page_builders_compatibility(),
640 'admin-page',
641 'Page Builders',
642 'Elementor, Divi, Gutenberg, Oxygen, Bricks'
643 ); ?>
644 <?php $this->render_compatibility_section(
645 $this->get_themes_compatibility(),
646 'art',
647 'Themes',
648 'Astra, GeneratePress, OceanWP and more'
649 ); ?>
650 <?php $this->render_compatibility_section(
651 $this->get_seo_plugins_compatibility(),
652 'search',
653 'SEO Plugins',
654 'Yoast, Rank Math, AIOSEO and others'
655 ); ?>
656 <?php $this->render_compatibility_section(
657 $this->get_cache_plugins_compatibility(),
658 'performance',
659 'Cache &amp; Performance',
660 'WP Rocket, LiteSpeed, W3 Total Cache and more'
661 ); ?>
662 <?php $this->render_compatibility_section(
663 $this->get_cdn_compatibility(),
664 'networking',
665 'CDN &amp; Hosting',
666 'Cloudflare, Fastly, Bunny CDN, Kinsta, Flywheel'
667 ); ?>
668 </div>
669 <?php
670 }
671
672 /**
673 * Generic section renderer — expandable accordion row.
674 */
675 private function render_compatibility_section( array $items, string $dashicon, string $title, string $description = '' ): void
676 {
677 $section_key = sanitize_title( $title );
678 $section_id = 'msrp-section-' . $section_key;
679 $active_count = count( array_filter( $items, fn( $i ) => $i['is_installed'] && $i['is_active'] ) );
680 $total = count( $items );
681 $subtitle = $active_count > 0
682 ? $active_count . ' of ' . $total . ' active'
683 : ( $description ?: $total . ' checked' );
684 ?>
685 <div class="metasync-accordion-section" data-section="<?php echo esc_attr( $section_key ); ?>">
686 <div class="metasync-accordion-header" role="button" tabindex="0" aria-expanded="false" aria-controls="<?php echo esc_attr( $section_id ); ?>">
687 <div class="metasync-accordion-title">
688 <span class="metasync-accordion-icon">
689 <span class="dashicons dashicons-<?php echo esc_attr( $dashicon ); ?>"></span>
690 </span>
691 <div class="metasync-accordion-text">
692 <h3><?php echo wp_kses_post( $title ); ?></h3>
693 <p class="metasync-accordion-description"><?php echo esc_html( $subtitle ); ?></p>
694 </div>
695 </div>
696 <button type="button" class="metasync-accordion-toggle" aria-label="Toggle section">
697 <span class="toggle-icon"></span>
698 </button>
699 </div>
700 <div class="metasync-accordion-content msrp-compat-accordion" id="<?php echo esc_attr( $section_id ); ?>" data-state="closed">
701 <div class="msrp-compat-list">
702 <?php foreach ( $items as $item ):
703 $is_active = $item['is_installed'] && $item['is_active'];
704 $is_installed = $item['is_installed'] && ! $item['is_active'];
705 $indicator = $is_active ? 'active' : ( $is_installed ? 'installed' : 'none' );
706 ?>
707 <div class="msrp-compat-row msrp-indicator-<?php echo esc_attr( $indicator ); ?>">
708 <div class="msrp-compat-row-left">
709 <span class="msrp-compat-name"><?php echo esc_html( $item['name'] ); ?></span>
710 <?php if ( ! empty( $item['note'] ) ): ?>
711 <span class="msrp-compat-note"><?php echo esc_html( $item['note'] ); ?></span>
712 <?php endif; ?>
713 </div>
714 <div class="msrp-compat-row-right">
715 <?php if ( $is_active ): ?>
716 <span class="msrp-pill msrp-pill-active">Active</span>
717 <?php elseif ( $is_installed ): ?>
718 <span class="msrp-pill msrp-pill-installed">Installed</span>
719 <?php else: ?>
720 <span class="msrp-pill msrp-pill-none">Not installed</span>
721 <?php endif; ?>
722 <span class="msrp-pill msrp-pill-supported"><?php echo esc_html( $item['status_text'] ); ?></span>
723 </div>
724 </div>
725 <?php endforeach; ?>
726 </div>
727 </div>
728 </div>
729 <?php
730 }
731
732 /**
733 * Troubleshooting section — rendered separately below the host test card.
734 */
735 public function render_troubleshooting_section(): void
736 {
737 $plugin_name = Metasync::get_effective_plugin_name();
738 $otto_name = Metasync::get_whitelabel_otto_name();
739 $issues = [
740 [
741 'title' => 'Title tag disappears when Yoast SEO is active',
742 '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.',
743 '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.',
744 ],
745 [
746 'title' => $otto_name . ' changes not appearing after cache flush',
747 'cause' => 'Some cache plugins (WP Rocket, Kinsta) serve stale HTML even after a purge if cache warm-up races with the write.',
748 'fix' => 'Increase the cache warm-up timeout in ' . $plugin_name . ' → Settings → Advanced, or disable "Immediate Warm-up" and let the cache rebuild organically.',
749 ],
750 [
751 'title' => 'SEO meta not showing for Oxygen Builder pages',
752 'cause' => 'Oxygen replaces the standard WordPress template, bypassing some meta output hooks.',
753 '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.',
754 ],
755 [
756 'title' => 'Schema markup injected on every page instead of the target page only',
757 'cause' => 'A known bug in versions prior to 2.5.20 where schema was registered globally.',
758 'fix' => 'Update to the latest version. Go to Settings → Schema Markup and re-save your configuration.',
759 ],
760 [
761 'title' => 'Sitemap returns 404',
762 'cause' => 'Permalink flush needed after activation or after a WordPress update.',
763 'fix' => 'Go to Settings → Permalinks in WordPress admin and click "Save Changes" (no actual change needed — this flushes rewrite rules).',
764 ],
765 [
766 'title' => 'Cloudflare / CDN caching stale SEO meta',
767 'cause' => 'CDN caches the full HTML response including meta tags. After updating SEO meta, the CDN may serve old content.',
768 'fix' => 'Purge the CDN cache for the affected URL via your CDN dashboard, or enable automatic purge integration in ' . $plugin_name . ' → Settings → Cache.',
769 ],
770 ];
771 ?>
772 <div id="ms-comp-troubleshooting" class="dashboard-card" style="margin-top: 20px;">
773 <div style="display:flex; align-items:center; gap:10px; margin-bottom:6px;">
774 <span class="dashicons dashicons-sos" style="font-size:24px;width:24px;height:24px;color:var(--dashboard-accent);"></span>
775 <div>
776 <h2 style="margin:0;">Troubleshooting</h2>
777 <p style="color:var(--dashboard-text-secondary); margin:4px 0 0 0;">Common issues and how to fix them.</p>
778 </div>
779 </div>
780 <div style="margin-top:16px; display:flex; flex-direction:column; gap:8px;">
781 <?php foreach ( $issues as $i => $issue ): ?>
782 <details style="border:1px solid var(--dashboard-border); border-radius:8px; overflow:hidden;">
783 <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);">
784 <span style="display:flex; align-items:center; gap:10px;">
785 <span class="dashicons dashicons-warning" style="font-size:16px;width:16px;height:16px;color:#f59e0b;flex-shrink:0;"></span>
786 <?php echo esc_html( $issue['title'] ); ?>
787 </span>
788 <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>
789 </summary>
790 <div style="padding:16px; background:var(--dashboard-card-bg); border-top:1px solid var(--dashboard-border);">
791 <p style="margin:0 0 10px 0; color:var(--dashboard-text-secondary); font-size:13px;">
792 <strong style="color:var(--dashboard-text-primary);">Cause:</strong> <?php echo esc_html( $issue['cause'] ); ?>
793 </p>
794 <p style="margin:0; color:var(--dashboard-text-secondary); font-size:13px;">
795 <strong style="color:var(--dashboard-text-primary);">Fix:</strong> <?php echo esc_html( $issue['fix'] ); ?>
796 </p>
797 </div>
798 </details>
799 <?php endforeach; ?>
800 </div>
801 </div>
802 <script>
803 document.querySelectorAll('#ms-comp-troubleshooting details').forEach(function(el) {
804 el.addEventListener('toggle', function() {
805 var arrow = el.querySelector('.dashicons-arrow-down-alt2');
806 if (arrow) arrow.style.transform = el.open ? 'rotate(180deg)' : '';
807 });
808 });
809 </script>
810 <?php
811 }
812
813 /**
814 * Render Lock Section button for protected tabs.
815 *
816 * @param string $tab The tab identifier (general, whitelabel, advanced)
817 */
818 public function render_lock_button($tab)
819 {
820 ?>
821 <button type="button"
822 class="metasync-lock-btn"
823 data-tab="<?php echo esc_attr($tab); ?>"
824 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;"
825 onmouseover="this.style.background='#c82333'"
826 onmouseout="this.style.background='#dc3545'"
827 onfocus="this.style.background='#c82333'"
828 onblur="this.style.background='#dc3545'"
829 aria-label="Lock this section and require password for access">
830 <span class="dashicons dashicons-lock" style="font-size:16px;width:16px;height:16px;" aria-hidden="true"></span>
831 Lock Section
832 </button>
833 <?php
834 }
835
836 public function get_page_builders_compatibility()
837 {
838 $builders = [
839 'elementor' => [
840 'name' => 'Elementor',
841 'plugin_files' => [ 'elementor/elementor.php', 'elementor-pro/elementor-pro.php' ],
842 'supported' => true,
843 'version' => '3.31.5',
844 ],
845 'gutenberg' => [
846 'name' => 'WordPress Block Editor',
847 'plugin_files' => [ 'gutenberg/gutenberg.php' ],
848 'supported' => true,
849 'is_core' => true,
850 'version' => get_bloginfo( 'version' ),
851 ],
852 'divi' => [
853 'name' => 'Divi Builder',
854 'plugin_files' => [ 'divi-builder/divi-builder.php' ],
855 'theme_name' => 'Divi',
856 'supported' => true,
857 'version' => '',
858 ],
859 'oxygen' => [
860 'name' => 'Oxygen Builder',
861 'plugin_files' => [ 'oxygen/functions.php' ],
862 'supported' => true,
863 'version' => '4.9',
864 'note' => 'Enable Oxygen Compatibility in Settings for correct meta output.',
865 ],
866 'bricks' => [
867 'name' => 'Bricks Builder',
868 'plugin_files' => [ 'bricks/bricks.php' ],
869 'supported' => true,
870 'version' => '1.11',
871 ],
872 ];
873
874 $result = [];
875
876 foreach ($builders as $key => $builder) {
877 $is_core = isset($builder['is_core']) && $builder['is_core'];
878 // The array key is the theme directory slug; pass it (not the display name)
879 // so wp_get_theme() can resolve the theme on case-sensitive filesystems.
880 $theme_slug = isset($builder['theme_name']) ? $key : null;
881 $plugin_status = $this->get_plugin_status($builder['plugin_files'], $is_core, $theme_slug);
882
883 if ($builder['supported']) {
884 $status = 'supported';
885 $status_text = 'Supported';
886 } else {
887 $status = 'coming-soon';
888 $status_text = 'Coming Soon';
889 }
890
891 $result[] = [
892 'name' => $builder['name'],
893 'version' => $builder['version'],
894 'status' => $status,
895 'status_text' => $status_text,
896 'is_installed' => $plugin_status['is_installed'],
897 'is_active' => $plugin_status['is_active'],
898 'note' => $builder['note'] ?? '',
899 'logo' => $this->get_plugin_logo( $key, 'page_builder' ),
900 ];
901 }
902
903 return $result;
904 }
905
906 public function get_seo_plugins_compatibility()
907 {
908 $plugins = [
909 'yoast' => [
910 'name' => 'Yoast SEO',
911 'plugin_files' => [
912 'wordpress-seo/wp-seo.php',
913 'wordpress-seo-premium/wp-seo-premium.php'
914 ],
915 'supported' => true,
916 'version' => '25.9.0'
917 ],
918 'rankmath' => [
919 'name' => 'Rank Math',
920 'plugin_files' => [
921 'seo-by-rank-math/rank-math.php',
922 'seo-by-rank-math-pro/rank-math-pro.php'
923 ],
924 'supported' => true,
925 'version' => '1.0.253.0'
926 ],
927 'aioseo' => [
928 'name' => 'All in One SEO',
929 'plugin_files' => [
930 'all-in-one-seo-pack/all_in_one_seo_pack.php',
931 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php'
932 ],
933 'supported' => true,
934 'version' => '4.8.7'
935 ]
936 ];
937
938 $result = [];
939
940 foreach ($plugins as $key => $plugin) {
941 $plugin_status = $this->get_plugin_status($plugin['plugin_files']);
942
943 if ($plugin['supported']) {
944 $status = 'supported';
945 $status_text = 'Supported';
946 } else {
947 $status = 'coming-soon';
948 $status_text = 'Coming Soon';
949 }
950
951 $result[] = [
952 'name' => $plugin['name'],
953 'version' => $plugin['version'],
954 'status' => $status,
955 'status_text' => $status_text,
956 'is_installed' => $plugin_status['is_installed'],
957 'is_active' => $plugin_status['is_active'],
958 'note' => $plugin['note'] ?? '',
959 'logo' => $this->get_plugin_logo( $key, 'seo' ),
960 ];
961 }
962
963 return $result;
964 }
965
966 public function get_themes_compatibility()
967 {
968 $themes = [
969 'astra' => [
970 'name' => 'Astra',
971 'theme_name' => 'Astra',
972 'plugin_files' => [],
973 'supported' => true,
974 'version' => '4.8',
975 'note' => 'Full compatibility including title tag and schema support.',
976 ],
977 'generatepress' => [
978 'name' => 'GeneratePress',
979 'theme_name' => 'GeneratePress',
980 'plugin_files' => [],
981 'supported' => true,
982 'version' => '3.5',
983 ],
984 'oceanwp' => [
985 'name' => 'OceanWP',
986 'theme_name' => 'OceanWP',
987 'plugin_files' => [],
988 'supported' => true,
989 'version' => '3.5',
990 ],
991 'hello-elementor' => [
992 'name' => 'Hello Elementor',
993 'theme_name' => 'Hello Elementor',
994 'plugin_files' => [],
995 'supported' => true,
996 'version' => '3.1',
997 ],
998 'kadence' => [
999 'name' => 'Kadence',
1000 'theme_name' => 'Kadence',
1001 'plugin_files' => [],
1002 'supported' => true,
1003 'version' => '1.2',
1004 ],
1005 ];
1006
1007 $result = [];
1008 foreach ( $themes as $key => $theme ) {
1009 // The array key is the theme directory slug; pass it (not the display name)
1010 // so wp_get_theme() can resolve the theme on case-sensitive filesystems.
1011 $status = $this->get_plugin_status( $theme['plugin_files'], false, $key );
1012 $result[] = [
1013 'name' => $theme['name'],
1014 'version' => $theme['version'],
1015 'status' => 'supported',
1016 'status_text' => 'Supported',
1017 'is_installed' => $status['is_installed'],
1018 'is_active' => $status['is_active'],
1019 'note' => $theme['note'] ?? '',
1020 'logo' => $this->get_plugin_logo( $key, 'theme' ),
1021 ];
1022 }
1023 return $result;
1024 }
1025
1026 public function get_cache_plugins_compatibility()
1027 {
1028 $plugins = [
1029 'wp-rocket' => [
1030 'name' => 'WP Rocket',
1031 'plugin_files' => [ 'wp-rocket/wp-rocket.php' ],
1032 'supported' => true,
1033 'version' => '3.16',
1034 'note' => 'Automatic cache purge on OTTO update.',
1035 ],
1036 'litespeed-cache' => [
1037 'name' => 'LiteSpeed Cache',
1038 'plugin_files' => [ 'litespeed-cache/litespeed-cache.php' ],
1039 'supported' => true,
1040 'version' => '7.5.0.1',
1041 ],
1042 'w3-total-cache' => [
1043 'name' => 'W3 Total Cache',
1044 'plugin_files' => [ 'w3-total-cache/w3-total-cache.php' ],
1045 'supported' => true,
1046 'version' => '2.7',
1047 ],
1048 'sg-cachepress' => [
1049 'name' => 'SiteGround Optimizer',
1050 'plugin_files' => [ 'sg-cachepress/sg-cachepress.php' ],
1051 'supported' => true,
1052 'version' => '7.6',
1053 ],
1054 'hummingbird-performance' => [
1055 'name' => 'Hummingbird',
1056 'plugin_files' => [ 'hummingbird-performance/wp-hummingbird.php' ],
1057 'supported' => true,
1058 'version' => '3.9',
1059 ],
1060 'autoptimize' => [
1061 'name' => 'Autoptimize',
1062 'plugin_files' => [ 'autoptimize/autoptimize.php' ],
1063 'supported' => true,
1064 'version' => '3.1',
1065 ],
1066 'comet-cache' => [
1067 'name' => 'Comet Cache',
1068 'plugin_files' => [ 'comet-cache/comet-cache.php' ],
1069 'supported' => true,
1070 'version' => '17.6',
1071 ],
1072 'cache-enabler' => [
1073 'name' => 'Cache Enabler',
1074 'plugin_files' => [ 'cache-enabler/cache-enabler.php' ],
1075 'supported' => true,
1076 'version' => '1.8',
1077 ],
1078 'breeze' => [
1079 'name' => 'Breeze (Cloudways)',
1080 'plugin_files' => [ 'breeze/breeze.php' ],
1081 'supported' => true,
1082 'version' => '2.1',
1083 ],
1084 ];
1085
1086 $result = [];
1087
1088 foreach ($plugins as $key => $plugin) {
1089 $plugin_status = $this->get_plugin_status($plugin['plugin_files']);
1090
1091 if ($plugin['supported']) {
1092 $status = 'supported';
1093 $status_text = 'Supported';
1094 } else {
1095 $status = 'coming-soon';
1096 $status_text = 'Coming Soon';
1097 }
1098
1099 $result[] = [
1100 'name' => $plugin['name'],
1101 'version' => $plugin['version'],
1102 'status' => $status,
1103 'status_text' => $status_text,
1104 'is_installed' => $plugin_status['is_installed'],
1105 'is_active' => $plugin_status['is_active'],
1106 'note' => $plugin['note'] ?? '',
1107 'logo' => $this->get_plugin_logo( $key, 'cache' ),
1108 ];
1109 }
1110
1111 return $result;
1112 }
1113
1114 public function get_cdn_compatibility()
1115 {
1116 $providers = [
1117 'cloudflare' => [
1118 'name' => 'Cloudflare',
1119 'plugin_files' => [ 'cloudflare/cloudflare.php' ],
1120 'supported' => true,
1121 'version' => '',
1122 'note' => 'Cache purge supported. Ensure HTML caching is disabled or bypass rules set for admin.',
1123 ],
1124 'fastly' => [
1125 'name' => 'Fastly',
1126 'plugin_files' => [ 'fastly/fastly.php' ],
1127 'supported' => true,
1128 'version' => '',
1129 'note' => 'Edge cache purge triggered on OTTO update.',
1130 ],
1131 'bunnycdn' => [
1132 'name' => 'Bunny CDN',
1133 'plugin_files' => [ 'bunnycdn/bunnycdn.php', 'bunny-cdn/bunny-cdn.php' ],
1134 'supported' => true,
1135 'version' => '',
1136 ],
1137 'kinsta-mu-plugins' => [
1138 'name' => 'Kinsta (Managed Hosting)',
1139 'plugin_files' => [ 'kinsta-mu-plugins/kinsta-mu-plugins.php' ],
1140 'supported' => true,
1141 'version' => '',
1142 'note' => 'Kinsta Varnish/Nginx cache purged per-URL on OTTO update.',
1143 ],
1144 'flywheel' => [
1145 'name' => 'Flywheel / Local',
1146 'plugin_files' => [ 'flywheel-common/plugin.php' ],
1147 'supported' => true,
1148 'version' => '',
1149 'note' => 'Varnish cache purge supported.',
1150 ],
1151 ];
1152
1153 $result = [];
1154 foreach ( $providers as $key => $provider ) {
1155 $status = $this->get_plugin_status( $provider['plugin_files'] );
1156 $result[] = [
1157 'name' => $provider['name'],
1158 'version' => $provider['version'],
1159 'status' => 'supported',
1160 'status_text' => 'Supported',
1161 'is_installed' => $status['is_installed'],
1162 'is_active' => $status['is_active'],
1163 'note' => $provider['note'] ?? '',
1164 'logo' => $this->get_plugin_logo( $key, 'cdn' ),
1165 ];
1166 }
1167 return $result;
1168 }
1169
1170 /**
1171 * @deprecated Use get_plugin_status() instead
1172 */
1173 public function is_plugin_installed($plugin_file)
1174 {
1175 if (!function_exists('is_plugin_active')) {
1176 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1177 }
1178 return is_plugin_active($plugin_file);
1179 }
1180
1181 /**
1182 * Get detailed plugin status (installed and/or active).
1183 *
1184 * @param array $plugin_files Array of plugin file paths to check
1185 * @param bool $is_core Whether this is a WordPress core feature
1186 * @param string $theme_slug Optional theme directory slug to check (e.g. 'hello-elementor',
1187 * 'astra'). Must be the directory slug, not the display name,
1188 * because wp_get_theme() resolves themes by directory slug.
1189 * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
1190 */
1191 public function get_plugin_status($plugin_files, $is_core = false, $theme_slug = null)
1192 {
1193 if ($is_core) {
1194 return [
1195 'is_installed' => true,
1196 'is_active' => true,
1197 'active_version' => 'core'
1198 ];
1199 }
1200
1201 if ($theme_slug) {
1202 $current_theme = wp_get_theme();
1203 $is_theme_active = (strtolower($current_theme->get('Name')) === strtolower($theme_slug) ||
1204 strtolower($current_theme->get_stylesheet()) === strtolower($theme_slug));
1205
1206 // An active theme is by definition installed. Fall back to a directory
1207 // lookup for themes that are installed but not currently active.
1208 $theme_exists = $is_theme_active || wp_get_theme($theme_slug)->exists();
1209
1210 if ($theme_exists) {
1211 return [
1212 'is_installed' => true,
1213 'is_active' => $is_theme_active,
1214 'active_version' => $is_theme_active ? 'theme' : null
1215 ];
1216 }
1217 }
1218
1219 if (!function_exists('is_plugin_active')) {
1220 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1221 }
1222
1223 if (!function_exists('get_plugins')) {
1224 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1225 }
1226
1227 if (!is_array($plugin_files)) {
1228 $plugin_files = [$plugin_files];
1229 }
1230
1231 $is_installed = false;
1232 $is_active = false;
1233 $active_version = null;
1234
1235 foreach ($plugin_files as $plugin_file) {
1236 $plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
1237 if (file_exists($plugin_path)) {
1238 $is_installed = true;
1239
1240 if (is_plugin_active($plugin_file)) {
1241 $is_active = true;
1242 $active_version = strpos($plugin_file, 'pro') !== false ||
1243 strpos($plugin_file, 'premium') !== false ?
1244 'premium' : 'free';
1245 break;
1246 }
1247 }
1248 }
1249
1250 return [
1251 'is_installed' => $is_installed,
1252 'is_active' => $is_active,
1253 'active_version' => $active_version
1254 ];
1255 }
1256
1257 public function get_plugin_logo($plugin_key, $type)
1258 {
1259 $logos = [
1260 'page_builder' => [
1261 'elementor' => 'https://ps.w.org/elementor/assets/icon-256x256.gif',
1262 'gutenberg' => 'https://i0.wp.com/wordpress.org/files/2023/02/wmark.png',
1263 'divi' => 'https://www.elegantthemes.com/images/logo.svg',
1264 'oxygen' => 'https://oxygenbuilder.com/wp-content/uploads/2021/09/oxygen-logo-icon.png',
1265 'bricks' => 'https://bricksbuilder.io/wp-content/uploads/2021/05/bricks-icon.svg',
1266 ],
1267 'theme' => [
1268 'astra' => 'https://ps.w.org/astra/assets/icon-128x128.png',
1269 'generatepress' => 'https://ps.w.org/generatepress/assets/icon-128x128.png',
1270 'oceanwp' => 'https://ps.w.org/ocean-extra/assets/icon-128x128.png',
1271 'hello-elementor' => 'https://ps.w.org/hello-elementor/assets/icon-128x128.png',
1272 'kadence' => 'https://ps.w.org/kadence-blocks/assets/icon-128x128.png',
1273 ],
1274 'seo' => [
1275 'yoast' => 'https://ps.w.org/wordpress-seo/assets/icon-128x128.gif',
1276 'rankmath' => 'https://ps.w.org/seo-by-rank-math/assets/icon-128x128.png',
1277 'aioseo' => 'https://ps.w.org/all-in-one-seo-pack/assets/icon-128x128.png',
1278 ],
1279 'cache' => [
1280 'wp-rocket' => 'https://ps.w.org/rocket-lazy-load/assets/icon-128x128.png',
1281 'litespeed-cache' => 'https://ps.w.org/litespeed-cache/assets/icon-128x128.png',
1282 'w3-total-cache' => 'https://ps.w.org/w3-total-cache/assets/icon-128x128.png',
1283 'sg-cachepress' => 'https://ps.w.org/sg-cachepress/assets/icon-128x128.png',
1284 'hummingbird-performance' => 'https://ps.w.org/hummingbird-performance/assets/icon-128x128.png',
1285 'autoptimize' => 'https://ps.w.org/autoptimize/assets/icon-128x128.png',
1286 ],
1287 'cdn' => [
1288 'cloudflare' => 'https://ps.w.org/cloudflare/assets/icon-128x128.png',
1289 'fastly' => 'https://ps.w.org/purgely/assets/icon-128x128.png',
1290 ],
1291 ];
1292
1293 return isset($logos[$type][$plugin_key]) ? $logos[$type][$plugin_key] : '';
1294 }
1295 }
1296