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

1,288 lines 57.4 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 $theme_name = isset($builder['theme_name']) ? $builder['theme_name'] : null;
879 $plugin_status = $this->get_plugin_status($builder['plugin_files'], $is_core, $theme_name);
880
881 if ($builder['supported']) {
882 $status = 'supported';
883 $status_text = 'Supported';
884 } else {
885 $status = 'coming-soon';
886 $status_text = 'Coming Soon';
887 }
888
889 $result[] = [
890 'name' => $builder['name'],
891 'version' => $builder['version'],
892 'status' => $status,
893 'status_text' => $status_text,
894 'is_installed' => $plugin_status['is_installed'],
895 'is_active' => $plugin_status['is_active'],
896 'note' => $builder['note'] ?? '',
897 'logo' => $this->get_plugin_logo( $key, 'page_builder' ),
898 ];
899 }
900
901 return $result;
902 }
903
904 public function get_seo_plugins_compatibility()
905 {
906 $plugins = [
907 'yoast' => [
908 'name' => 'Yoast SEO',
909 'plugin_files' => [
910 'wordpress-seo/wp-seo.php',
911 'wordpress-seo-premium/wp-seo-premium.php'
912 ],
913 'supported' => true,
914 'version' => '25.9.0'
915 ],
916 'rankmath' => [
917 'name' => 'Rank Math',
918 'plugin_files' => [
919 'seo-by-rank-math/rank-math.php',
920 'seo-by-rank-math-pro/rank-math-pro.php'
921 ],
922 'supported' => true,
923 'version' => '1.0.253.0'
924 ],
925 'aioseo' => [
926 'name' => 'All in One SEO',
927 'plugin_files' => [
928 'all-in-one-seo-pack/all_in_one_seo_pack.php',
929 'all-in-one-seo-pack-pro/all_in_one_seo_pack.php'
930 ],
931 'supported' => true,
932 'version' => '4.8.7'
933 ]
934 ];
935
936 $result = [];
937
938 foreach ($plugins as $key => $plugin) {
939 $plugin_status = $this->get_plugin_status($plugin['plugin_files']);
940
941 if ($plugin['supported']) {
942 $status = 'supported';
943 $status_text = 'Supported';
944 } else {
945 $status = 'coming-soon';
946 $status_text = 'Coming Soon';
947 }
948
949 $result[] = [
950 'name' => $plugin['name'],
951 'version' => $plugin['version'],
952 'status' => $status,
953 'status_text' => $status_text,
954 'is_installed' => $plugin_status['is_installed'],
955 'is_active' => $plugin_status['is_active'],
956 'note' => $plugin['note'] ?? '',
957 'logo' => $this->get_plugin_logo( $key, 'seo' ),
958 ];
959 }
960
961 return $result;
962 }
963
964 public function get_themes_compatibility()
965 {
966 $themes = [
967 'astra' => [
968 'name' => 'Astra',
969 'theme_name' => 'Astra',
970 'plugin_files' => [],
971 'supported' => true,
972 'version' => '4.8',
973 'note' => 'Full compatibility including title tag and schema support.',
974 ],
975 'generatepress' => [
976 'name' => 'GeneratePress',
977 'theme_name' => 'GeneratePress',
978 'plugin_files' => [],
979 'supported' => true,
980 'version' => '3.5',
981 ],
982 'oceanwp' => [
983 'name' => 'OceanWP',
984 'theme_name' => 'OceanWP',
985 'plugin_files' => [],
986 'supported' => true,
987 'version' => '3.5',
988 ],
989 'hello-elementor' => [
990 'name' => 'Hello Elementor',
991 'theme_name' => 'Hello Elementor',
992 'plugin_files' => [],
993 'supported' => true,
994 'version' => '3.1',
995 ],
996 'kadence' => [
997 'name' => 'Kadence',
998 'theme_name' => 'Kadence',
999 'plugin_files' => [],
1000 'supported' => true,
1001 'version' => '1.2',
1002 ],
1003 ];
1004
1005 $result = [];
1006 foreach ( $themes as $key => $theme ) {
1007 $status = $this->get_plugin_status( $theme['plugin_files'], false, $theme['theme_name'] );
1008 $result[] = [
1009 'name' => $theme['name'],
1010 'version' => $theme['version'],
1011 'status' => 'supported',
1012 'status_text' => 'Supported',
1013 'is_installed' => $status['is_installed'],
1014 'is_active' => $status['is_active'],
1015 'note' => $theme['note'] ?? '',
1016 'logo' => $this->get_plugin_logo( $key, 'theme' ),
1017 ];
1018 }
1019 return $result;
1020 }
1021
1022 public function get_cache_plugins_compatibility()
1023 {
1024 $plugins = [
1025 'wp-rocket' => [
1026 'name' => 'WP Rocket',
1027 'plugin_files' => [ 'wp-rocket/wp-rocket.php' ],
1028 'supported' => true,
1029 'version' => '3.16',
1030 'note' => 'Automatic cache purge on OTTO update.',
1031 ],
1032 'litespeed-cache' => [
1033 'name' => 'LiteSpeed Cache',
1034 'plugin_files' => [ 'litespeed-cache/litespeed-cache.php' ],
1035 'supported' => true,
1036 'version' => '7.5.0.1',
1037 ],
1038 'w3-total-cache' => [
1039 'name' => 'W3 Total Cache',
1040 'plugin_files' => [ 'w3-total-cache/w3-total-cache.php' ],
1041 'supported' => true,
1042 'version' => '2.7',
1043 ],
1044 'sg-cachepress' => [
1045 'name' => 'SiteGround Optimizer',
1046 'plugin_files' => [ 'sg-cachepress/sg-cachepress.php' ],
1047 'supported' => true,
1048 'version' => '7.6',
1049 ],
1050 'hummingbird-performance' => [
1051 'name' => 'Hummingbird',
1052 'plugin_files' => [ 'hummingbird-performance/wp-hummingbird.php' ],
1053 'supported' => true,
1054 'version' => '3.9',
1055 ],
1056 'autoptimize' => [
1057 'name' => 'Autoptimize',
1058 'plugin_files' => [ 'autoptimize/autoptimize.php' ],
1059 'supported' => true,
1060 'version' => '3.1',
1061 ],
1062 'comet-cache' => [
1063 'name' => 'Comet Cache',
1064 'plugin_files' => [ 'comet-cache/comet-cache.php' ],
1065 'supported' => true,
1066 'version' => '17.6',
1067 ],
1068 'cache-enabler' => [
1069 'name' => 'Cache Enabler',
1070 'plugin_files' => [ 'cache-enabler/cache-enabler.php' ],
1071 'supported' => true,
1072 'version' => '1.8',
1073 ],
1074 'breeze' => [
1075 'name' => 'Breeze (Cloudways)',
1076 'plugin_files' => [ 'breeze/breeze.php' ],
1077 'supported' => true,
1078 'version' => '2.1',
1079 ],
1080 ];
1081
1082 $result = [];
1083
1084 foreach ($plugins as $key => $plugin) {
1085 $plugin_status = $this->get_plugin_status($plugin['plugin_files']);
1086
1087 if ($plugin['supported']) {
1088 $status = 'supported';
1089 $status_text = 'Supported';
1090 } else {
1091 $status = 'coming-soon';
1092 $status_text = 'Coming Soon';
1093 }
1094
1095 $result[] = [
1096 'name' => $plugin['name'],
1097 'version' => $plugin['version'],
1098 'status' => $status,
1099 'status_text' => $status_text,
1100 'is_installed' => $plugin_status['is_installed'],
1101 'is_active' => $plugin_status['is_active'],
1102 'note' => $plugin['note'] ?? '',
1103 'logo' => $this->get_plugin_logo( $key, 'cache' ),
1104 ];
1105 }
1106
1107 return $result;
1108 }
1109
1110 public function get_cdn_compatibility()
1111 {
1112 $providers = [
1113 'cloudflare' => [
1114 'name' => 'Cloudflare',
1115 'plugin_files' => [ 'cloudflare/cloudflare.php' ],
1116 'supported' => true,
1117 'version' => '',
1118 'note' => 'Cache purge supported. Ensure HTML caching is disabled or bypass rules set for admin.',
1119 ],
1120 'fastly' => [
1121 'name' => 'Fastly',
1122 'plugin_files' => [ 'fastly/fastly.php' ],
1123 'supported' => true,
1124 'version' => '',
1125 'note' => 'Edge cache purge triggered on OTTO update.',
1126 ],
1127 'bunnycdn' => [
1128 'name' => 'Bunny CDN',
1129 'plugin_files' => [ 'bunnycdn/bunnycdn.php', 'bunny-cdn/bunny-cdn.php' ],
1130 'supported' => true,
1131 'version' => '',
1132 ],
1133 'kinsta-mu-plugins' => [
1134 'name' => 'Kinsta (Managed Hosting)',
1135 'plugin_files' => [ 'kinsta-mu-plugins/kinsta-mu-plugins.php' ],
1136 'supported' => true,
1137 'version' => '',
1138 'note' => 'Kinsta Varnish/Nginx cache purged per-URL on OTTO update.',
1139 ],
1140 'flywheel' => [
1141 'name' => 'Flywheel / Local',
1142 'plugin_files' => [ 'flywheel-common/plugin.php' ],
1143 'supported' => true,
1144 'version' => '',
1145 'note' => 'Varnish cache purge supported.',
1146 ],
1147 ];
1148
1149 $result = [];
1150 foreach ( $providers as $key => $provider ) {
1151 $status = $this->get_plugin_status( $provider['plugin_files'] );
1152 $result[] = [
1153 'name' => $provider['name'],
1154 'version' => $provider['version'],
1155 'status' => 'supported',
1156 'status_text' => 'Supported',
1157 'is_installed' => $status['is_installed'],
1158 'is_active' => $status['is_active'],
1159 'note' => $provider['note'] ?? '',
1160 'logo' => $this->get_plugin_logo( $key, 'cdn' ),
1161 ];
1162 }
1163 return $result;
1164 }
1165
1166 /**
1167 * @deprecated Use get_plugin_status() instead
1168 */
1169 public function is_plugin_installed($plugin_file)
1170 {
1171 if (!function_exists('is_plugin_active')) {
1172 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1173 }
1174 return is_plugin_active($plugin_file);
1175 }
1176
1177 /**
1178 * Get detailed plugin status (installed and/or active).
1179 *
1180 * @param array $plugin_files Array of plugin file paths to check
1181 * @param bool $is_core Whether this is a WordPress core feature
1182 * @param string $theme_name Optional theme name to check
1183 * @return array ['is_installed' => bool, 'is_active' => bool, 'active_version' => string|null]
1184 */
1185 public function get_plugin_status($plugin_files, $is_core = false, $theme_name = null)
1186 {
1187 if ($is_core) {
1188 return [
1189 'is_installed' => true,
1190 'is_active' => true,
1191 'active_version' => 'core'
1192 ];
1193 }
1194
1195 if ($theme_name) {
1196 $current_theme = wp_get_theme();
1197 $is_theme_active = (strtolower($current_theme->get('Name')) === strtolower($theme_name) ||
1198 strtolower($current_theme->get_stylesheet()) === strtolower($theme_name));
1199
1200 $theme_exists = wp_get_theme($theme_name)->exists();
1201
1202 if ($theme_exists) {
1203 return [
1204 'is_installed' => true,
1205 'is_active' => $is_theme_active,
1206 'active_version' => $is_theme_active ? 'theme' : null
1207 ];
1208 }
1209 }
1210
1211 if (!function_exists('is_plugin_active')) {
1212 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1213 }
1214
1215 if (!function_exists('get_plugins')) {
1216 include_once(ABSPATH . 'wp-admin/includes/plugin.php');
1217 }
1218
1219 if (!is_array($plugin_files)) {
1220 $plugin_files = [$plugin_files];
1221 }
1222
1223 $is_installed = false;
1224 $is_active = false;
1225 $active_version = null;
1226
1227 foreach ($plugin_files as $plugin_file) {
1228 $plugin_path = WP_PLUGIN_DIR . '/' . $plugin_file;
1229 if (file_exists($plugin_path)) {
1230 $is_installed = true;
1231
1232 if (is_plugin_active($plugin_file)) {
1233 $is_active = true;
1234 $active_version = strpos($plugin_file, 'pro') !== false ||
1235 strpos($plugin_file, 'premium') !== false ?
1236 'premium' : 'free';
1237 break;
1238 }
1239 }
1240 }
1241
1242 return [
1243 'is_installed' => $is_installed,
1244 'is_active' => $is_active,
1245 'active_version' => $active_version
1246 ];
1247 }
1248
1249 public function get_plugin_logo($plugin_key, $type)
1250 {
1251 $logos = [
1252 'page_builder' => [
1253 'elementor' => 'https://ps.w.org/elementor/assets/icon-256x256.gif',
1254 'gutenberg' => 'https://i0.wp.com/wordpress.org/files/2023/02/wmark.png',
1255 'divi' => 'https://www.elegantthemes.com/images/logo.svg',
1256 'oxygen' => 'https://oxygenbuilder.com/wp-content/uploads/2021/09/oxygen-logo-icon.png',
1257 'bricks' => 'https://bricksbuilder.io/wp-content/uploads/2021/05/bricks-icon.svg',
1258 ],
1259 'theme' => [
1260 'astra' => 'https://ps.w.org/astra/assets/icon-128x128.png',
1261 'generatepress' => 'https://ps.w.org/generatepress/assets/icon-128x128.png',
1262 'oceanwp' => 'https://ps.w.org/ocean-extra/assets/icon-128x128.png',
1263 'hello-elementor' => 'https://ps.w.org/hello-elementor/assets/icon-128x128.png',
1264 'kadence' => 'https://ps.w.org/kadence-blocks/assets/icon-128x128.png',
1265 ],
1266 'seo' => [
1267 'yoast' => 'https://ps.w.org/wordpress-seo/assets/icon-128x128.gif',
1268 'rankmath' => 'https://ps.w.org/seo-by-rank-math/assets/icon-128x128.png',
1269 'aioseo' => 'https://ps.w.org/all-in-one-seo-pack/assets/icon-128x128.png',
1270 ],
1271 'cache' => [
1272 'wp-rocket' => 'https://ps.w.org/rocket-lazy-load/assets/icon-128x128.png',
1273 'litespeed-cache' => 'https://ps.w.org/litespeed-cache/assets/icon-128x128.png',
1274 'w3-total-cache' => 'https://ps.w.org/w3-total-cache/assets/icon-128x128.png',
1275 'sg-cachepress' => 'https://ps.w.org/sg-cachepress/assets/icon-128x128.png',
1276 'hummingbird-performance' => 'https://ps.w.org/hummingbird-performance/assets/icon-128x128.png',
1277 'autoptimize' => 'https://ps.w.org/autoptimize/assets/icon-128x128.png',
1278 ],
1279 'cdn' => [
1280 'cloudflare' => 'https://ps.w.org/cloudflare/assets/icon-128x128.png',
1281 'fastly' => 'https://ps.w.org/purgely/assets/icon-128x128.png',
1282 ],
1283 ];
1284
1285 return isset($logos[$type][$plugin_key]) ? $logos[$type][$plugin_key] : '';
1286 }
1287 }
1288