PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.18
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.18
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 2.6.4 2.6.3 All 139 releases
metasync / includes / class-metasync-admin-pages.php

class-metasync-admin-pages.php in Search Atlas SEO – OTTO AI SEO Automation for WordPress 2.6.18, at includes/class-metasync-admin-pages.php

2,255 lines 116.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if (!defined('ABSPATH')) {
3 exit;
4 }
5
6 /**
7 * Admin page renderer callbacks extracted from Metasync_Admin.
8 *
9 * Each public method renders a specific admin page. The class receives the
10 * Metasync_Admin instance so it can call back into shared helpers such as
11 * render_plugin_header() and render_navigation_menu().
12 *
13 * @package Metasync
14 * @subpackage Metasync/includes
15 */
16 class Metasync_Admin_Pages
17 {
18 /** @var Metasync_Admin */
19 private $admin;
20
21 /** @var self|null */
22 private static $instance = null;
23
24 private function __construct(Metasync_Admin $admin)
25 {
26 $this->admin = $admin;
27 }
28
29 /**
30 * @param Metasync_Admin $admin
31 * @return self
32 */
33 public static function get_instance(Metasync_Admin $admin): self
34 {
35 if (null === self::$instance || self::$instance->admin !== $admin) {
36 self::$instance = new self($admin);
37 }
38 return self::$instance;
39 }
40
41 // ------------------------------------------------------------------
42 // Dashboard iframe (~200 lines)
43 // ------------------------------------------------------------------
44
45 public function create_admin_dashboard_iframe()
46 {
47 $general_options = Metasync::get_option('general');
48 $otto_pixel_uuid = isset($general_options['otto_pixel_uuid']) ? $general_options['otto_pixel_uuid'] : '';
49 $api_key = isset($general_options['searchatlas_api_key']) ? $general_options['searchatlas_api_key'] : '';
50
51 $hide_dashboard = $general_options['hide_dashboard_framework'] ?? false;
52
53 if ($hide_dashboard) {
54 $this->admin->render_layout_open('Dashboard', 'dashboard', 'Overview of your SEO performance and plugin status.');
55 ?>
56 <div class="dashboard-card">
57 <h2>Dashboard Disabled</h2>
58 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">
59 The dashboard is currently disabled.
60 </p>
61 </div>
62 <?php
63 $this->admin->render_layout_close(false);
64 return;
65 }
66
67 if (!$this->admin->is_heartbeat_connected()) {
68 $this->admin->render_layout_open('Dashboard', 'dashboard', 'Overview of your SEO performance and plugin status.');
69 ?>
70 <div class="dashboard-card">
71 <h2>Setup Wizard</h2>
72 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Run the setup wizard to configure your plugin, import from other SEO plugins, and optimize your settings in just a few minutes.</p>
73 <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-setup-wizard')); ?>" class="button button-primary" style="text-decoration: none;">
74 Start Setup Wizard
75 </a>
76 </div>
77
78 <div class="dashboard-card">
79 <h2>Authentication Required</h2>
80 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">
81 You need to authenticate with <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> to access the dashboard.
82 </p>
83 <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug)); ?>" class="button button-primary">
84 Go to Settings &amp; Connect
85 </a>
86 </div>
87 <?php
88 $this->admin->render_layout_close(false);
89 return;
90 }
91
92 $jwt_token = $this->admin->get_fresh_jwt_token();
93
94 if ($jwt_token) {
95 $jwt_parts = explode('.', $jwt_token);
96 $jwt_header_info = count($jwt_parts) >= 2 ? 'Valid format (' . count($jwt_parts) . ' parts)' : 'Invalid format';
97 } else {
98 }
99
100 $public_hash = false;
101 if ($jwt_token && $otto_pixel_uuid) {
102 $public_hash = $this->admin->fetch_public_hash($otto_pixel_uuid, $jwt_token);
103
104 if ($public_hash) {
105 } else {
106 }
107 } else {
108 $missing_params = [];
109 if (empty($jwt_token)) $missing_params[] = 'JWT_TOKEN';
110 if (empty($otto_pixel_uuid)) $missing_params[] = 'OTTO_UUID';
111 }
112
113 $dashboard_domain = Metasync_Admin::get_effective_dashboard_domain();
114
115 if ($public_hash) {
116 $iframe_url = $dashboard_domain . '/seo-automation-v3/public?uuid=' . urlencode($otto_pixel_uuid)
117 . '&category=onpage_optimizations&subGroup=page_title&public_hash=' . urlencode($public_hash);
118 } else {
119 $iframe_url = $dashboard_domain . '/seo-automation-v3/tasks?uuid=' . urlencode($otto_pixel_uuid) . '&category=All&Embed=True';
120 if ($jwt_token) {
121 $iframe_url .= '&jwtToken=' . urlencode($jwt_token) . '&impersonate=1';
122 } else {
123 }
124 }
125
126 $iframe_url .= '&source=wordpress-plugin-iframe';
127
128 $whitelabel_company_name = Metasync::get_whitelabel_company_name();
129 if ($whitelabel_company_name) {
130 $iframe_url .= '&whitelabel=' . urlencode($whitelabel_company_name);
131 }
132
133 $whitelabel_settings = Metasync::get_whitelabel_settings();
134 $is_whitelabel_domain = !empty($whitelabel_settings['domain']);
135
136 $this->admin->render_layout_open('Dashboard', 'dashboard', 'Overview of your SEO performance and plugin status.');
137 ?>
138 <iframe id="metasync-dashboard-iframe"
139 src="<?php echo esc_url($iframe_url); ?>"
140 width="100%"
141 frameborder="0"
142 <?php if (!$is_whitelabel_domain): ?>
143 allow="cookies"
144 referrerpolicy="strict-origin-when-cross-origin"
145 <?php endif; ?>
146 style="border: none; margin: 0; padding: 0; min-height: 800px; border-radius: 12px;"
147 onload="adjustIframeHeight(this)">
148 </iframe>
149
150 <script>
151 function adjustIframeHeight(iframe) {
152 var attempts = 0;
153 var maxAttempts = 20;
154
155 function tryAdjustHeight() {
156 try {
157 attempts++;
158
159 var iframeDocument = iframe.contentDocument || iframe.contentWindow.document;
160 if (iframeDocument) {
161 var body = iframeDocument.body;
162 var hasContent = body && (body.children.length > 1 || body.innerText.trim().length > 100);
163
164 if (!hasContent && attempts < maxAttempts) {
165 setTimeout(tryAdjustHeight, 500);
166 return;
167 }
168
169 var height = Math.max(
170 body ? body.scrollHeight : 0,
171 body ? body.offsetHeight : 0,
172 iframeDocument.documentElement.clientHeight,
173 iframeDocument.documentElement.scrollHeight,
174 iframeDocument.documentElement.offsetHeight
175 );
176
177 if (height > 600) {
178 iframe.style.height = height + 'px';
179 } else if (attempts < maxAttempts) {
180 setTimeout(tryAdjustHeight, 500);
181 return;
182 }
183 } else {
184 if (attempts < maxAttempts) {
185 setTimeout(tryAdjustHeight, 500);
186 return;
187 }
188 }
189 } catch (e) {
190 iframe.style.height = '100vh';
191 }
192 }
193
194 tryAdjustHeight();
195 }
196
197 window.addEventListener('resize', function() {
198 var iframe = document.getElementById('metasync-dashboard-iframe');
199 if (iframe) {
200 adjustIframeHeight(iframe);
201 }
202 });
203
204 setTimeout(function() {
205 var iframe = document.getElementById('metasync-dashboard-iframe');
206 if (iframe) {
207 adjustIframeHeight(iframe);
208 }
209 }, 3000);
210 </script>
211 <?php
212 $this->admin->render_layout_close(false);
213 }
214
215 // ------------------------------------------------------------------
216 // Settings page (~1000 lines)
217 // ------------------------------------------------------------------
218
219 public function create_admin_settings_page()
220 {
221 $active_tab = isset($_GET['tab']) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
222
223 $whitelabel_settings = Metasync::get_whitelabel_settings();
224
225 $user_password = $whitelabel_settings['settings_password'] ?? '';
226 $hide_settings_enabled = !empty($whitelabel_settings['hide_settings']);
227
228 $protected_tabs = [];
229
230 if (!empty($user_password)) {
231 $protected_tabs[] = 'whitelabel';
232 }
233
234 if ($hide_settings_enabled && !empty($user_password)) {
235 $protected_tabs = ['general', 'whitelabel', 'advanced'];
236 }
237
238 $password_protection_enabled = false;
239 $password_validated = false;
240 $password_error = '';
241
242 if (in_array($active_tab, $protected_tabs)) {
243
244 $password_protection_enabled = !empty($user_password);
245
246 $auth = new Metasync_Auth_Manager('whitelabel', 1800);
247 $password_validated = $auth->has_access();
248
249 if (isset($_POST['whitelabel_password_submit']) && !$password_validated) {
250 $password_error = 'Incorrect password. Please try again.';
251 }
252 }
253
254 $is_authenticated = $password_validated;
255
256 $whitelabel_otto_name = Metasync::get_whitelabel_otto_name();
257
258 $page_slug = Metasync_Admin::$page_slug;
259 ?>
260 <?php
261 $settings_current_page = 'general';
262 if ($active_tab === 'advanced') $settings_current_page = 'advanced_settings';
263 elseif ($active_tab === 'whitelabel') $settings_current_page = 'whitelabel';
264 $this->admin->render_layout_open('Settings', $settings_current_page, 'Manage all plugin settings and configuration.');
265 ?>
266 <?php
267 /*
268 # Temporarily commented out: Clear Cache notice and button (can be re-enabled later)
269
270 * <div class="notice notice-success">
271 * <p>
272 * <b>Clear all caches at once</b><br/>
273 * This will slow down your site until caches are rebuilt
274 * <button style="margin-left: 15px;" type ="button" class="button" id="clear_otto_caches" data-toggle="tooltip" data-placement="top" title="Clear all <?php echo $whitelabel_otto_name;?> Caches">Clear <?php echo $whitelabel_otto_name;?> Cache</button>
275 * </p>
276 * </div>
277 */
278 ?>
279
280 <?php /* Navigation now rendered in left sidenav via render_layout_open */ ?>
281
282 <?php
283 if (in_array($active_tab, $protected_tabs)) {
284
285 if ($password_protection_enabled && !$password_validated) {
286 ?>
287 <div class="dashboard-card" style="max-width: 500px; margin: 0 auto;">
288 <h2 style="text-align: center;">Protected Section</h2>
289 <p style="color: var(--dashboard-text-secondary); margin-bottom: 30px; text-align: center;">
290 <?php
291 $plugin_name = Metasync::get_effective_plugin_name('');
292 if ($hide_settings_enabled && !empty($user_password)) {
293 printf('Please enter the password to access the %s Settings section.', esc_html($plugin_name));
294 } else {
295 echo 'Please enter the password to access the Branding section.';
296 }
297 ?>
298 </p>
299
300 <?php if (!empty($password_error)): ?>
301 <div style="background: #f8d7da; color: #721c24; padding: 12px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #f5c6cb;">
302 <strong> Access Denied:</strong> <?php echo esc_html($password_error); ?>
303 </div>
304 <?php endif; ?>
305
306 <form method="post" action="" style="max-width: 400px; margin: 0 auto;">
307 <?php wp_nonce_field('whitelabel_password_nonce', 'whitelabel_nonce'); ?>
308
309 <div style="margin-bottom: 20px;">
310 <label for="whitelabel_password" style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--dashboard-text-primary);">
311 Validate Password
312 </label>
313 <input
314 type="password"
315 id="whitelabel_password"
316 name="whitelabel_password"
317 placeholder="Enter password to access protected settings"
318 style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; box-sizing: border-box;"
319 required
320 autocomplete="off"
321 />
322 </div>
323
324 <div style="text-align: center;">
325 <button
326 type="submit"
327 name="whitelabel_password_submit"
328 value="1"
329 class="button button-primary"
330 style="padding: 12px 24px; font-size: 14px; font-weight: 600;"
331 >
332 Submit Password
333 </button>
334 </div>
335 </form>
336
337 <div style="text-align: center; margin-top: 20px;">
338 <a href="#" id="metasync-forgot-password-link" style="color: #2271b1; text-decoration: none; font-size: 14px;">
339 Forgot Password?
340 </a>
341 <div id="metasync-recovery-message" style="margin-top: 15px; padding: 12px; border-radius: 6px; display: none;"></div>
342 </div>
343
344 </div>
345
346 <script>
347 jQuery(document).ready(function($) {
348 $('#whitelabel_password').focus();
349
350 $('#whitelabel_password').on('keypress', function(e) {
351 if (e.which === 13) {
352 $(this).closest('form').submit();
353 }
354 });
355
356 $('#metasync-forgot-password-link').on('click', function(e) {
357 e.preventDefault();
358
359 var $link = $(this);
360 var $message = $('#metasync-recovery-message');
361
362 $link.css('pointer-events', 'none').css('opacity', '0.6');
363 $message.removeClass('success error').hide();
364 $message.html('⏳ Sending recovery email...').css('background', '#f0f6fc').css('color', '#0c5ba5').css('border', '1px solid #cfe2f3').fadeIn(200);
365
366 $.ajax({
367 url: ajaxurl,
368 type: 'POST',
369 data: {
370 action: 'metasync_recover_password',
371 nonce: '<?php echo wp_create_nonce('metasync_recover_password_nonce'); ?>'
372 },
373 success: function(response) {
374 $link.css('pointer-events', 'auto').css('opacity', '1');
375
376 if (response.success) {
377 $message.addClass('success').html('�
378 ' + response.data.message)
379 .css('background', '#d4edda')
380 .css('color', '#155724')
381 .css('border', '1px solid #c3e6cb');
382 } else {
383 $message.addClass('error').html('' + response.data.message)
384 .css('background', '#f8d7da')
385 .css('color', '#721c24')
386 .css('border', '1px solid #f5c6cb');
387 }
388 },
389 error: function() {
390 $link.css('pointer-events', 'auto').css('opacity', '1');
391 $message.addClass('error').html('❌ An error occurred. Please try again.')
392 .css('background', '#f8d7da')
393 .css('color', '#721c24')
394 .css('border', '1px solid #f5c6cb');
395 }
396 });
397 });
398 });
399 </script>
400 <?php
401 return;
402 } elseif (!$password_protection_enabled) {
403 ?>
404 <div class="notice notice-info" style="margin: 15px 0;">
405 <p>
406 <strong>Security Tip:</strong> You can set a custom password in the White Label Settings to protect this section.
407 </p>
408 </div>
409 <?php
410 }
411 }
412 ?>
413
414 <form method="post" action="options.php?tab=<?php echo esc_attr( $active_tab ); ?>" id="metaSyncGeneralSetting">
415 <?php
416 settings_fields(Metasync_Admin::option_group);
417
418 wp_nonce_field('meta_sync_general_setting_nonce', 'meta_sync_nonce');
419
420 if ($active_tab == 'general') {
421 ?>
422 <div class="dashboard-card">
423 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
424 <div style="flex: 1;">
425 <h2 style="margin: 0;">General Configuration</h2>
426 <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Configure your <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> API, plugin features, caching, and general settings.</p>
427 </div>
428 <?php if ($hide_settings_enabled && !empty($user_password) && $is_authenticated): ?>
429 <div>
430 <?php Metasync_Compatibility_Checker::instance()->render_lock_button('general'); ?>
431 </div>
432 <?php endif; ?>
433 </div>
434
435 <?php if ($hide_settings_enabled && !empty($user_password) && $is_authenticated): ?>
436 <div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 12px; border-radius: 4px; margin-bottom: 20px;">
437 <strong>�
438 Access Granted:</strong> You have successfully authenticated and can now modify settings.
439 </div>
440 <?php endif; ?>
441
442 <?php
443 $this->admin->render_accordion_sections(Metasync_Admin::$page_slug . '_general');
444 ?>
445 </div>
446
447 <div class="dashboard-card">
448 <h2>Content Genius Synchronization</h2>
449 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Sync your categories and user/author data with <?php echo esc_html(Metasync::get_effective_plugin_name()); ?>.</p>
450 <button type="button" class="button button-primary" id="sendAuthToken" data-toggle="tooltip" data-placement="top" title="Sync Categories and User">
451 Sync Now
452 </button>
453 </div>
454
455 <div class="dashboard-card">
456 <h2>Setup Wizard</h2>
457 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Run the setup wizard to configure your plugin, import from other SEO plugins, and optimize your settings in just a few minutes.</p>
458 <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-setup-wizard')); ?>" class="button button-primary" style="text-decoration: none;">
459 Start Setup Wizard
460 </a>
461 </div>
462 <?php
463 } elseif ($active_tab == 'whitelabel') {
464 $whitelabel_settings = Metasync::get_whitelabel_settings();
465 $user_password = $whitelabel_settings['settings_password'] ?? '';
466 $password_protection_enabled = !empty($user_password);
467 ?>
468 <div class="dashboard-card">
469 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
470 <div>
471 <h2>White Label Branding<?php Metasync::render_tooltip_icon('wl_branding_section', 'Replace this plugin\'s name, logo, and links with your own so clients see your brand instead of Search Atlas. Set a password below to stop clients from changing it back.'); ?></h2>
472 <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Customize the plugin appearance with your own branding and logo.</p>
473 </div>
474 <?php if ($password_protection_enabled && $is_authenticated): ?>
475 <div>
476 <?php Metasync_Compatibility_Checker::instance()->render_lock_button('whitelabel'); ?>
477 <?php Metasync::render_tooltip_icon('wl_lock_section', 'Re-lock this section now. You will need the password to open it again.'); ?>
478 </div>
479 <?php endif; ?>
480 </div>
481
482 <?php if ($password_protection_enabled && $is_authenticated): ?>
483 <div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 12px; border-radius: 4px; margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center;">
484 <div>
485 <strong>�
486 Access Granted:</strong> You have successfully authenticated and can now modify white label settings.
487 </div>
488 </div>
489
490 <?php endif; ?>
491
492 <?php
493 do_settings_sections(Metasync_Admin::$page_slug . '_branding');
494 ?>
495 </div>
496
497 <!-- Color Palette Customization section -->
498 <div class="dashboard-card" id="metasync-color-palette-section">
499 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
500 <div>
501 <h2>Color Palette<?php Metasync::render_tooltip_icon('wl_color_palette', 'These colors control this plugin\'s admin dashboard theme only. Changes apply immediately here and never affect what your site\'s visitors see on the frontend.'); ?></h2>
502 <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Customize the plugin color scheme for both dark and light themes. Changes preview instantly.</p>
503 </div>
504 </div>
505
506 <?php
507 $color_palette = isset($whitelabel_settings['color_palette']) && is_array($whitelabel_settings['color_palette'])
508 ? $whitelabel_settings['color_palette']
509 : array();
510 $dark_colors = isset($color_palette['dark']) && is_array($color_palette['dark']) ? $color_palette['dark'] : array();
511 $light_colors = isset($color_palette['light']) && is_array($color_palette['light']) ? $color_palette['light'] : array();
512
513 $palette_fields = array(
514 'dashboard-bg' => array(
515 'label' => 'Background',
516 'hint' => 'Main page background behind all content',
517 'dark' => '#0f1419',
518 'light' => '#f8f9fa',
519 ),
520 'dashboard-card-bg' => array(
521 'label' => 'Card Background',
522 'hint' => 'Background of cards, panels, and content boxes',
523 'dark' => '#1a1f26',
524 'light' => '#ffffff',
525 ),
526 'dashboard-card-hover' => array(
527 'label' => 'Card Hover',
528 'hint' => 'Card background when you hover over it',
529 'dark' => '#222831',
530 'light' => '#f1f3f5',
531 ),
532 'dashboard-text-primary' => array(
533 'label' => 'Primary Text',
534 'hint' => 'Headings, body text, and main content',
535 'dark' => '#ffffff',
536 'light' => '#1a1f26',
537 ),
538 'dashboard-text-secondary' => array(
539 'label' => 'Secondary Text',
540 'hint' => 'Descriptions, hints, and less important text',
541 'dark' => '#9ca3af',
542 'light' => '#6b7280',
543 ),
544 'dashboard-accent' => array(
545 'label' => 'Accent Color',
546 'hint' => 'Buttons, links, and highlighted elements',
547 'dark' => '#3b82f6',
548 'light' => '#3b82f6',
549 ),
550 'dashboard-accent-hover' => array(
551 'label' => 'Accent Hover',
552 'hint' => 'Buttons and links when hovered',
553 'dark' => '#2563eb',
554 'light' => '#2563eb',
555 ),
556 'dashboard-success' => array(
557 'label' => 'Success',
558 'hint' => 'Success messages, connected status, positive indicators',
559 'dark' => '#10b981',
560 'light' => '#10b981',
561 ),
562 'dashboard-warning' => array(
563 'label' => 'Warning',
564 'hint' => 'Warning alerts and caution indicators',
565 'dark' => '#f59e0b',
566 'light' => '#f59e0b',
567 ),
568 'dashboard-error' => array(
569 'label' => 'Error',
570 'hint' => 'Error messages and critical alerts',
571 'dark' => '#ef4444',
572 'light' => '#ef4444',
573 ),
574 'dashboard-border' => array(
575 'label' => 'Border',
576 'hint' => 'Card borders, dividers, and separators',
577 'dark' => '#374151',
578 'light' => '#e5e7eb',
579 ),
580 'dashboard-gradient-primary-from' => array(
581 'label' => 'Button Gradient Start',
582 'hint' => 'Left/top color of buttons, save bars, and active tabs',
583 'dark' => '#667eea',
584 'light' => '#667eea',
585 ),
586 'dashboard-gradient-primary-to' => array(
587 'label' => 'Button Gradient End',
588 'hint' => 'Right/bottom color of buttons, save bars, and active tabs',
589 'dark' => '#764ba2',
590 'light' => '#764ba2',
591 ),
592 'dashboard-gradient-accent-from' => array(
593 'label' => 'Accent Gradient Start',
594 'hint' => 'Left/top color of secondary badges and highlight bars',
595 'dark' => '#f093fb',
596 'light' => '#f093fb',
597 ),
598 'dashboard-gradient-accent-to' => array(
599 'label' => 'Accent Gradient End',
600 'hint' => 'Right/bottom color of secondary badges and highlight bars',
601 'dark' => '#f5576c',
602 'light' => '#f5576c',
603 ),
604 );
605 ?>
606
607 <!-- Theme tabs -->
608 <div style="display: flex; gap: 8px; margin-bottom: 20px;">
609 <button type="button" class="metasync-palette-tab active" data-palette="dark"
610 style="padding: 8px 20px; border-radius: 8px; border: 1px solid var(--dashboard-border); background: var(--dashboard-gradient-primary); color: #fff; cursor: pointer; font-weight: 600; font-size: 13px;">
611 &#9790; Dark Theme
612 </button>
613 <button type="button" class="metasync-palette-tab" data-palette="light"
614 style="padding: 8px 20px; border-radius: 8px; border: 1px solid var(--dashboard-border); background: transparent; color: var(--dashboard-text-secondary); cursor: pointer; font-weight: 600; font-size: 13px;">
615 &#9728; Light Theme
616 </button>
617 </div>
618
619 <!-- Dark theme palette -->
620 <div class="metasync-palette-panel active" id="metasync-palette-dark">
621 <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;">
622 <?php foreach ($palette_fields as $var_name => $field): ?>
623 <div style="background: var(--dashboard-card-hover); border: 1px solid var(--dashboard-border); border-radius: 10px; padding: 14px;">
624 <label style="display: block; font-weight: 600; font-size: 13px; color: var(--dashboard-text-primary); margin-bottom: 2px;">
625 <?php echo esc_html($field['label']); ?>
626 </label>
627 <p style="font-size: 12px; color: var(--dashboard-text-secondary); margin: 0 0 10px 0;"><?php echo esc_html($field['hint']); ?></p>
628 <div style="display: flex; align-items: center; gap: 10px;">
629 <input type="color"
630 class="metasync-color-field"
631 name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][color_palette][dark][<?php echo esc_attr($var_name); ?>]"
632 value="<?php echo esc_attr(!empty($dark_colors[$var_name]) ? $dark_colors[$var_name] : $field['dark']); ?>"
633 data-default-color="<?php echo esc_attr($field['dark']); ?>"
634 data-css-var="<?php echo esc_attr($var_name); ?>"
635 data-color-theme="dark"
636 style="width: 44px; height: 36px; padding: 2px; border: 1px solid var(--dashboard-border); border-radius: 8px; cursor: pointer; background: transparent;"
637 />
638 <code style="font-size: 12px; color: var(--dashboard-text-secondary); background: var(--dashboard-bg); padding: 3px 8px; border-radius: 4px;" class="metasync-color-hex-display"><?php echo esc_html(!empty($dark_colors[$var_name]) ? $dark_colors[$var_name] : $field['dark']); ?></code>
639 </div>
640 </div>
641 <?php endforeach; ?>
642 </div>
643 <div style="margin-top: 16px; text-align: right;">
644 <button type="button" id="metasync-reset-dark-palette" data-reset-theme="dark"
645 style="padding: 8px 18px; border-radius: 6px; border: 1px solid var(--dashboard-border); background: transparent; color: var(--dashboard-text-secondary); cursor: pointer; font-size: 13px;">
646 Reset Dark Theme to Defaults
647 </button>
648 </div>
649 </div>
650
651 <!-- Light theme palette -->
652 <div class="metasync-palette-panel" id="metasync-palette-light">
653 <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;">
654 <?php foreach ($palette_fields as $var_name => $field): ?>
655 <div style="background: var(--dashboard-card-hover); border: 1px solid var(--dashboard-border); border-radius: 10px; padding: 14px;">
656 <label style="display: block; font-weight: 600; font-size: 13px; color: var(--dashboard-text-primary); margin-bottom: 2px;">
657 <?php echo esc_html($field['label']); ?>
658 </label>
659 <p style="font-size: 12px; color: var(--dashboard-text-secondary); margin: 0 0 10px 0;"><?php echo esc_html($field['hint']); ?></p>
660 <div style="display: flex; align-items: center; gap: 10px;">
661 <input type="color"
662 class="metasync-color-field"
663 name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][color_palette][light][<?php echo esc_attr($var_name); ?>]"
664 value="<?php echo esc_attr(!empty($light_colors[$var_name]) ? $light_colors[$var_name] : $field['light']); ?>"
665 data-default-color="<?php echo esc_attr($field['light']); ?>"
666 data-css-var="<?php echo esc_attr($var_name); ?>"
667 data-color-theme="light"
668 style="width: 44px; height: 36px; padding: 2px; border: 1px solid var(--dashboard-border); border-radius: 8px; cursor: pointer; background: transparent;"
669 />
670 <code style="font-size: 12px; color: var(--dashboard-text-secondary); background: var(--dashboard-bg); padding: 3px 8px; border-radius: 4px;" class="metasync-color-hex-display"><?php echo esc_html(!empty($light_colors[$var_name]) ? $light_colors[$var_name] : $field['light']); ?></code>
671 </div>
672 </div>
673 <?php endforeach; ?>
674 </div>
675 <div style="margin-top: 16px; text-align: right;">
676 <button type="button" id="metasync-reset-light-palette" data-reset-theme="light"
677 style="padding: 8px 18px; border-radius: 6px; border: 1px solid var(--dashboard-border); background: transparent; color: var(--dashboard-text-secondary); cursor: pointer; font-size: 13px;">
678 Reset Light Theme to Defaults
679 </button>
680 </div>
681 </div>
682 </div>
683
684 <style>
685 .metasync-palette-tab.active {
686 background: var(--dashboard-gradient-primary) !important;
687 color: #fff !important;
688 }
689 .metasync-palette-tab:not(.active) {
690 background: transparent !important;
691 color: var(--dashboard-text-secondary) !important;
692 }
693 .metasync-palette-panel { display: none !important; }
694 .metasync-palette-panel.active { display: block !important; }
695 </style>
696
697 <script>
698 jQuery(function($) {
699 // Tab switching
700 $(document).on('click', '.metasync-palette-tab', function(e) {
701 e.preventDefault();
702 e.stopPropagation();
703 var target = $(this).data('palette');
704 $('.metasync-palette-tab').removeClass('active');
705 $(this).addClass('active');
706 $('.metasync-palette-panel').removeClass('active');
707 $('#metasync-palette-' + target).addClass('active');
708 });
709
710 // Reset to defaults
711 var paletteDefaults = <?php echo wp_json_encode(array(
712 'dark' => array_combine(
713 array_keys($palette_fields),
714 array_column($palette_fields, 'dark')
715 ),
716 'light' => array_combine(
717 array_keys($palette_fields),
718 array_column($palette_fields, 'light')
719 )
720 )); ?>;
721
722 $(document).on('click', '#metasync-reset-dark-palette, #metasync-reset-light-palette', function(e) {
723 e.preventDefault();
724 e.stopPropagation();
725 var theme = $(this).data('reset-theme');
726 var defs = paletteDefaults[theme];
727 if (!defs) return;
728 $('#metasync-color-palette-section .metasync-color-field[data-color-theme="' + theme + '"]').each(function() {
729 var $input = $(this);
730 var varName = $input.data('css-var');
731 var defVal = defs[varName] || '';
732 if (defVal) {
733 $input.val(defVal);
734 $input.siblings('.metasync-color-hex-display').text(defVal);
735 }
736 });
737 // Trigger live preview if external JS is loaded
738 $input = $('#metasync-color-palette-section .metasync-color-field').first();
739 if ($input.length) $input.trigger('input');
740 });
741 });
742 </script>
743
744 <!-- Export Whitelabel Settings section -->
745 <div class="dashboard-card">
746 <h2>Export Whitelabel Plugin<?php Metasync::render_tooltip_icon('wl_export_plugin', 'Downloads a copy of this plugin with your branding already baked in, ready to upload and install on a client\'s site. All your white-label settings are applied automatically on activation.'); ?></h2>
747 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Export the entire plugin with all whitelabel settings pre-configured. This creates a complete plugin zip file ready for installation on another WordPress site.</p>
748 <button type="button" class="button button-primary" id="metasync-export-whitelabel-btn" style="padding: 10px 20px; font-size: 14px; font-weight: 600;">
749 Export Plugin with Whitelabel Settings
750 </button>
751 <p class="description" style="margin-top: 10px;">This will create a zip file containing the complete plugin with all your whitelabel settings included. Upload and install this zip file on another WordPress site via Plugins Add New Upload Plugin. All whitelabel configurations will be automatically applied upon activation.</p>
752 </div>
753
754 <!-- Advanced Access Control section -->
755 <?php Metasync_Access_Control_UI::render_access_control_table(); ?>
756
757 <!-- Custom Modal for Alerts -->
758 <div id="metasync-custom-modal" style="display: none; position: fixed; z-index: 999999; left: 0; top: 0; width: 100%; height: 100%; background-color: rgba(0,0,0,0.6);">
759 <div style="position: relative; margin: 10% auto; max-width: 500px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 2px; border-radius: 12px; box-shadow: 0 20px 60px rgba(0,0,0,0.3); animation: modalSlideIn 0.3s ease-out;">
760 <div style="background: white; border-radius: 10px; padding: 0; overflow: hidden;">
761 <!-- Header -->
762 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 24px; text-align: center;">
763 <div id="modal-icon" style="font-size: 48px; margin-bottom: 12px; color: #fff;">&#9888;</div>
764 <h2 id="modal-title" style="color: white; margin: 0; font-size: 24px; font-weight: 600;">Password Required</h2>
765 </div>
766
767 <!-- Body -->
768 <div style="padding: 32px 24px;">
769 <p id="modal-message" style="color: #4a5568; font-size: 15px; line-height: 1.6; text-align: center; margin: 0;">
770 You must set a White Label Settings Password before enabling "Hide Settings".
771 </p>
772 </div>
773
774 <!-- Footer -->
775 <div style="padding: 0 24px 24px; text-align: center;">
776 <button type="button" id="modal-close-btn" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 12px 32px; border-radius: 6px; font-size: 15px; font-weight: 600; cursor: pointer; box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); transition: all 0.3s ease;">
777 Got It
778 </button>
779 </div>
780 </div>
781 </div>
782 </div>
783
784 <style>
785 @keyframes modalSlideIn {
786 from {
787 opacity: 0;
788 transform: translateY(-50px);
789 }
790 to {
791 opacity: 1;
792 transform: translateY(0);
793 }
794 }
795
796 #modal-close-btn:hover {
797 transform: translateY(-2px);
798 box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
799 }
800 </style>
801
802 <script>
803 jQuery(document).ready(function($) {
804 var isShowingModal = false;
805
806 function showModal(icon, title, message) {
807 isShowingModal = true;
808 $('#modal-icon').text(icon);
809 $('#modal-title').text(title);
810 $('#modal-message').html(message);
811 $('#metasync-custom-modal').fadeIn(200);
812
813 setTimeout(function() {
814 isShowingModal = false;
815 }, 300);
816 }
817
818 $('#modal-close-btn').on('click', function(e) {
819 e.preventDefault();
820 e.stopPropagation();
821 $('#metasync-custom-modal').fadeOut(200);
822 return false;
823 });
824
825 $('#metasync-custom-modal').on('click', function(e) {
826 if (e.target === this) {
827 $(this).fadeOut(200);
828 }
829 });
830
831 function hasPassword() {
832 var passwordField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]');
833 var passwordValue = passwordField.val();
834 return passwordValue && passwordValue.length > 0;
835 }
836
837 function hasRecoveryEmail() {
838 var recoveryEmailField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][recovery_email]"]');
839 var emailValue = recoveryEmailField.val();
840 return emailValue && emailValue.length > 0 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailValue);
841 }
842
843 var passwordField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]');
844 var recoveryEmailField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][recovery_email]"]');
845
846 passwordField.on('blur', function() {
847 if (hasPassword() && !hasRecoveryEmail()) {
848 showModal(
849 '📧',
850 'Recovery Email Required',
851 'You must set a <strong>Recovery Email</strong> when setting a password.<br><br>Please enter a valid email address in the <strong>Recovery Email</strong> field.'
852 );
853 recoveryEmailField.focus();
854 }
855 });
856
857 passwordField.on('input', function() {
858 if (hasPassword()) {
859 recoveryEmailField.attr('required', true);
860 recoveryEmailField.closest('tr').find('th').addClass('required-field');
861 } else {
862 recoveryEmailField.removeAttr('required');
863 recoveryEmailField.closest('tr').find('th').removeClass('required-field');
864 }
865 });
866
867 passwordField.trigger('input');
868
869 var hideSettingsCheckbox = $('#checkbox_hide_settings');
870 var originalCheckboxState = hideSettingsCheckbox.is(':checked');
871
872 hideSettingsCheckbox.on('click', function(e) {
873 if (!originalCheckboxState && !hasPassword()) {
874 e.preventDefault();
875 e.stopImmediatePropagation();
876
877 showModal(
878 '&#9888;',
879 'Password Required',
880 'You must set a <strong>Settings Password</strong> before enabling "Hide Settings".<br><br>Please scroll up to the <strong>Branding</strong> section and set a password first.'
881 );
882
883 setTimeout(function() {
884 $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]').focus();
885 }, 300);
886
887 return false;
888 }
889
890 setTimeout(function() {
891 originalCheckboxState = hideSettingsCheckbox.is(':checked');
892 }, 0);
893 });
894
895 passwordField.on('keydown', function(e) {
896 var currentValue = $(this).val();
897
898 if (hideSettingsCheckbox.is(':checked') &&
899 (currentValue.length <= 1 || !currentValue) &&
900 (e.keyCode === 8 || e.keyCode === 46)) {
901
902 e.preventDefault();
903 e.stopImmediatePropagation();
904
905 if (!isShowingModal) {
906 showModal(
907 '&#9888;',
908 'Cannot Remove Password',
909 'You cannot remove the <strong>White Label Settings Password</strong> while "Hide Settings" is enabled.<br><br>Please uncheck <strong>"Hide Settings"</strong> first if you want to remove the password.'
910 );
911 }
912
913 return false;
914 }
915 });
916
917 $(document).on('metasync_settings_saved', function() {
918 });
919
920 $(document).on('keydown', function(e) {
921 if (e.key === 'Escape') {
922 $('#metasync-custom-modal').fadeOut(200);
923 }
924 });
925
926 $('#metasync-export-whitelabel-btn').on('click', function(e) {
927 e.preventDefault();
928
929 var $button = $(this);
930 var originalText = $button.html();
931
932 $button.prop('disabled', true).html('⏳ Exporting...');
933
934 var form = $('<form>', {
935 'method': 'POST',
936 'action': '<?php echo esc_js(admin_url('admin-post.php')); ?>',
937 'target': '_blank'
938 });
939
940 form.append($('<input>', {
941 'type': 'hidden',
942 'name': 'action',
943 'value': 'metasync_export_whitelabel_settings'
944 }));
945
946 form.append($('<input>', {
947 'type': 'hidden',
948 'name': '_wpnonce',
949 'value': '<?php echo wp_create_nonce('metasync_export_whitelabel'); ?>'
950 }));
951
952 $('body').append(form);
953 form.submit();
954
955 setTimeout(function() {
956 form.remove();
957 $button.prop('disabled', false).html(originalText);
958 }, 2000);
959 });
960 });
961 </script>
962 <?php
963 } elseif ($active_tab == 'advanced') {
964 if (!Metasync_Access_Control::user_can_access('hide_advanced')) {
965 echo '<div class="dashboard-card" style="background: var(--dashboard-card-bg); padding: 25px; border-radius: 8px; text-align: center;">';
966 echo '<h2 style="color: var(--dashboard-text-primary);">Access Denied</h2>';
967 echo '<p style="color: var(--dashboard-text-secondary);">You do not have permission to access this page.</p>';
968 echo '</div>';
969 } else {
970 ?>
971 <div class="dashboard-card">
972 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
973 <div style="flex: 1;">
974 <h2 style="margin: 0;">🧰 Advanced Settings</h2>
975 <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Technical utilities for troubleshooting and connectivity checks.</p>
976 </div>
977 <?php if ($hide_settings_enabled && !empty($user_password) && $is_authenticated): ?>
978 <div>
979 <?php Metasync_Compatibility_Checker::instance()->render_lock_button('advanced'); ?>
980 </div>
981 <?php endif; ?>
982 </div>
983
984 <?php if ($hide_settings_enabled && !empty($user_password) && $is_authenticated): ?>
985 <div style="background: rgba(212, 237, 218, 0.2); border: 1px solid rgba(195, 230, 203, 0.5); color: var(--dashboard-success, #10b981); padding: 12px; border-radius: 8px; margin-bottom: 20px;">
986 <strong>�
987 Access Granted:</strong> You have successfully authenticated and can now modify settings.
988 </div>
989 <?php endif; ?>
990
991 <?php
992 if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'):
993 $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1';
994 ?>
995 <div class="notice notice-success inline" style="margin-bottom: 20px;">
996 <p>
997 <strong>�
998 Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>.
999 </p>
1000 </div>
1001 <?php endif; ?>
1002
1003 <?php if (isset($_GET['debug_mode_disabled']) && $_GET['debug_mode_disabled'] == '1'): ?>
1004 <div class="notice notice-info inline" style="margin-bottom: 20px;">
1005 <p>
1006 <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled.
1007 </p>
1008 </div>
1009 <?php endif; ?>
1010
1011 <?php if (isset($_GET['debug_mode_extended']) && $_GET['debug_mode_extended'] == '1'): ?>
1012 <div class="notice notice-success inline" style="margin-bottom: 20px;">
1013 <p>
1014 <strong>�
1015 Extended!</strong> Debug mode has been extended for another 24 hours.
1016 </p>
1017 </div>
1018 <?php endif; ?>
1019
1020 <?php if (isset($_GET['debug_error']) && $_GET['debug_error'] == '1'): ?>
1021 <div class="notice notice-error inline" style="margin-bottom: 20px;">
1022 <p>
1023 <strong> Error:</strong> Unable to perform the debug mode operation. Please try again.
1024 </p>
1025 </div>
1026 <?php endif; ?>
1027
1028 <?php
1029 $this->admin->render_advanced_accordion();
1030 ?>
1031 </div>
1032
1033 <?php
1034 }
1035 }
1036 ?>
1037
1038 <!-- Save button removed - using floating notification system instead -->
1039
1040 </form>
1041
1042 <!-- Lock Section Button Handler (runs on all tabs) -->
1043 <script>
1044 jQuery(document).ready(function($) {
1045 var ajaxUrl = (typeof window.ajaxurl !== 'undefined' && window.ajaxurl)
1046 ? window.ajaxurl
1047 : '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
1048 $('.metasync-lock-btn').on('click', function(e) {
1049 e.preventDefault();
1050 e.stopPropagation();
1051
1052 var currentTab = $(this).data('tab');
1053
1054 var logoutForm = $('<form>', {
1055 'method': 'post',
1056 'action': ''
1057 });
1058
1059 logoutForm.append('<?php echo wp_nonce_field("whitelabel_logout_nonce", "whitelabel_logout_nonce", true, false); ?>');
1060
1061 logoutForm.append($('<input>', {
1062 'type': 'hidden',
1063 'name': 'whitelabel_logout',
1064 'value': '1'
1065 }));
1066
1067 $('body').append(logoutForm);
1068 logoutForm.submit();
1069
1070 return false;
1071 });
1072 });
1073 </script>
1074
1075 <!-- Host Blocking-generated JavaScript -->
1076 <script>
1077 jQuery(document).ready(function($) {
1078 var ajaxUrl = (typeof window.ajaxurl !== 'undefined' && window.ajaxurl)
1079 ? window.ajaxurl
1080 : '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
1081
1082 $(document).on('click', '#test-get-request', function(e) {
1083 e.preventDefault();
1084 e.stopPropagation();
1085 runHostTest('GET');
1086 return false;
1087 });
1088
1089 $(document).on('click', '#test-post-request', function(e) {
1090 e.preventDefault();
1091 e.stopPropagation();
1092 runHostTest('POST');
1093 return false;
1094 });
1095
1096 $(document).on('click', '#test-both-requests', function(e) {
1097 e.preventDefault();
1098 e.stopPropagation();
1099 runHostTest('BOTH');
1100 return false;
1101 });
1102
1103 function runHostTest(method) {
1104 var buttonId = (method === 'BOTH' ? 'test-both-requests' : 'test-' + method.toLowerCase() + '-request');
1105 var $button = $('#' + buttonId);
1106
1107 if ($button.length === 0) {
1108 alert('Error: Test button not found. Please refresh the page.');
1109 return;
1110 }
1111
1112 var originalText = $button.text();
1113
1114 $button.prop('disabled', true);
1115 $button.text('Testing...');
1116
1117 var $resultsDiv = $('#host-test-results');
1118 var $resultsContent = $('#test-results-content');
1119 $resultsDiv.show();
1120 $resultsContent.html('<div class="notice notice-info"><p>Running ' + (method === 'BOTH' ? 'GET and POST' : method) + ' test(s)...</p></div>');
1121
1122 var testsToRun = (method === 'BOTH') ? ['GET', 'POST'] : [method];
1123 var completedTests = 0;
1124 var allResults = [];
1125
1126 testsToRun.forEach(function(testMethod) {
1127 var action = 'metasync_test_host_blocking_' + testMethod.toLowerCase();
1128
1129 $.ajax({
1130 url: ajaxUrl,
1131 type: 'POST',
1132 dataType: 'json',
1133 data: { action: action, nonce: '<?php echo wp_create_nonce('metasync_nonce'); ?>' },
1134 timeout: 35000,
1135 success: function(response, textStatus, xhr) {
1136 try {
1137 if (response && response.success && response.data) {
1138 allResults.push(response.data);
1139 } else {
1140 allResults.push({
1141 method: testMethod,
1142 status: 'error',
1143 error: (response && response.data) ? response.data : 'Unexpected response',
1144 blocked: true,
1145 details: 'Received non-success response from server.'
1146 });
1147 }
1148 } catch (e) {
1149 allResults.push({
1150 method: testMethod,
1151 status: 'error',
1152 error: 'Response parse error: ' + (e && e.message ? e.message : e),
1153 blocked: true
1154 });
1155 }
1156 finalizeOne();
1157 },
1158 error: function(xhr, status, error) {
1159 var payload = (xhr && xhr.responseText) ? xhr.responseText.substring(0, 500) : '';
1160 allResults.push({
1161 method: testMethod,
1162 status: 'error',
1163 error: 'AJAX failed: ' + error + (payload ? '' + payload : ''),
1164 blocked: true,
1165 details: 'Request did not complete successfully. Status: ' + status
1166 });
1167 finalizeOne();
1168 }
1169 });
1170 });
1171
1172 function finalizeOne() {
1173 completedTests++;
1174 if (completedTests === testsToRun.length) {
1175 displayResults(allResults);
1176 resetButtons();
1177 var $container = $('#host-test-results');
1178 if ($container && $container[0] && $container[0].scrollIntoView) {
1179 $container[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
1180 }
1181 }
1182 }
1183 }
1184
1185 function resetButtons() {
1186 $('#test-get-request, #test-post-request, #test-both-requests').prop('disabled', false);
1187 $('#test-get-request').text('Test GET Request');
1188 $('#test-post-request').text('Test POST Request');
1189 $('#test-both-requests').text('Test Both Requests');
1190 }
1191
1192 function displayResults(results) {
1193 var html = '';
1194
1195 results.forEach(function(result) {
1196 var statusClass = result.status === 'success' ? 'success' : 'error';
1197 var statusIcon = result.status === 'success' ? '�
1198 ' : '';
1199 var blockedStatus = result.blocked ? 'BLOCKED' : 'ALLOWED';
1200 var blockedClass = result.blocked ? 'blocked' : 'allowed';
1201
1202 html += '<div class="test-result-item ' + statusClass + '">';
1203 html += '<div class="test-result-header">';
1204 html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
1205 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
1206 html += '</div>';
1207
1208 html += '<div class="test-result-details">';
1209 html += '<p><strong>Response Time:</strong> ' + result.response_time + '</p>';
1210 html += '<p><strong>Status:</strong> ' + result.status + '</p>';
1211
1212 if (result.status_code) {
1213 html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + result.status_code + '</span></p>';
1214 }
1215
1216 if (result.error) {
1217 html += '<p><strong>Error:</strong> <span class="error-message">' + result.error + '</span></p>';
1218 }
1219
1220 if (result.body) {
1221 html += '<p><strong>Response Body:</strong></p>';
1222 html += '<pre class="response-body">' + escapeHtml(result.body) + '</pre>';
1223 }
1224
1225 if (result.headers && Object.keys(result.headers).length > 0) {
1226 html += '<p><strong>Response Headers:</strong></p>';
1227 html += '<pre class="response-headers">';
1228 Object.keys(result.headers).forEach(function(key) {
1229 html += key + ': ' + result.headers[key] + '\n';
1230 });
1231 html += '</pre>';
1232 }
1233
1234 if (result.sent_data) {
1235 html += '<p><strong>Sent Data:</strong></p>';
1236 html += '<pre class="sent-data">' + escapeHtml(JSON.stringify(result.sent_data, null, 2)) + '</pre>';
1237 }
1238
1239 if (result.parsed_response) {
1240 html += '<p><strong>Parsed Response:</strong></p>';
1241 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
1242 }
1243
1244 html += '<p><strong>Details:</strong> ' + result.details + '</p>';
1245 html += '</div>';
1246 html += '</div>';
1247 });
1248
1249 $('#test-results-content').html(html);
1250 $('#host-test-results').show();
1251 }
1252
1253 function escapeHtml(text) {
1254 var map = {
1255 '&': '&amp;',
1256 '<': '&lt;',
1257 '>': '&gt;',
1258 '"': '&quot;',
1259 '\'': '&#039;'
1260 };
1261 return text.replace(/[&<>"']/g, function(m) { return map[m]; });
1262 }
1263 });
1264 </script>
1265
1266 <!-- Host Blocking Test CSS -->
1267 <style>
1268 .test-result-item {
1269 background: #fff;
1270 border: 1px solid #ddd;
1271 border-radius: 8px;
1272 margin-bottom: 20px;
1273 overflow: hidden;
1274 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1275 }
1276
1277 .test-result-item.success {
1278 border-left: 4px solid #28a745;
1279 }
1280
1281 .test-result-item.error {
1282 border-left: 4px solid #dc3545;
1283 }
1284
1285 .test-result-header {
1286 background: #f8f9fa;
1287 padding: 15px 20px;
1288 border-bottom: 1px solid #dee2e6;
1289 display: flex;
1290 justify-content: space-between;
1291 align-items: center;
1292 }
1293
1294 .test-result-header h4 {
1295 margin: 0;
1296 color: #495057;
1297 font-size: 16px;
1298 }
1299
1300 .test-status {
1301 padding: 4px 12px;
1302 border-radius: 20px;
1303 font-size: 12px;
1304 font-weight: bold;
1305 text-transform: uppercase;
1306 }
1307
1308 .test-status.allowed {
1309 background: #d4edda;
1310 color: #155724;
1311 }
1312
1313 .test-status.blocked {
1314 background: #f8d7da;
1315 color: #721c24;
1316 }
1317
1318 .test-result-details {
1319 padding: 20px;
1320 }
1321
1322 .test-result-details p {
1323 margin: 8px 0;
1324 color: #495057;
1325 }
1326
1327 .test-result-details code {
1328 background: #f8f9fa;
1329 padding: 2px 6px;
1330 border-radius: 3px;
1331 font-family: 'Courier New', monospace;
1332 color: #e83e8c;
1333 }
1334
1335 .status-code {
1336 font-weight: bold;
1337 color: #007cba;
1338 }
1339
1340 .error-message {
1341 color: #dc3545;
1342 font-weight: bold;
1343 }
1344
1345 .response-body, .response-headers, .sent-data, .parsed-response {
1346 background: #f8f9fa;
1347 border: 1px solid #dee2e6;
1348 border-radius: 4px;
1349 padding: 12px;
1350 margin: 8px 0;
1351 font-family: 'Courier New', monospace;
1352 font-size: 12px;
1353 line-height: 1.4;
1354 max-height: 200px;
1355 overflow-y: auto;
1356 white-space: pre-wrap;
1357 word-break: break-all;
1358 }
1359
1360 .response-body {
1361 color: #495057;
1362 }
1363
1364 .response-headers {
1365 color: #6c757d;
1366 }
1367
1368 .sent-data {
1369 color: #007cba;
1370 }
1371
1372 .parsed-response {
1373 color: #28a745;
1374 background: #f8fff9;
1375 border-color: #c3e6cb;
1376 }
1377
1378 #host-test-results {
1379 margin-top: 20px;
1380 }
1381
1382 #host-test-results h3 {
1383 color: #495057;
1384 margin-bottom: 15px;
1385 padding-bottom: 10px;
1386 border-bottom: 2px solid #dee2e6;
1387 }
1388
1389 .button:disabled {
1390 opacity: 0.6;
1391 cursor: not-allowed;
1392 }
1393 </style>
1394
1395 <?php if ($active_tab === 'advanced'): ?>
1396 <?php if (isset($_GET['settings_cleared']) && $_GET['settings_cleared'] == '1'): ?>
1397 <div class="notice notice-success is-dismissible">
1398 <p><strong>�
1399 Success!</strong> All plugin settings have been cleared successfully and a new Plugin Auth Token has been generated. Please reconfigure the plugin as needed.</p>
1400 </div>
1401 <?php endif; ?>
1402
1403 <?php if (isset($_GET['clear_settings_error']) && $_GET['clear_settings_error'] == '1'): ?>
1404 <div class="notice notice-error is-dismissible">
1405 <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p>
1406 </div>
1407 <?php endif; ?>
1408
1409 <?php if (isset($_GET['log_cleared']) && $_GET['log_cleared'] == '1'): ?>
1410 <div class="notice notice-success is-dismissible">
1411 <p><strong>�
1412 Success!</strong> Error logs have been cleared successfully.</p>
1413 </div>
1414 <?php endif; ?>
1415
1416 <?php if (isset($_GET['clear_error']) && $_GET['clear_error'] == '1'): ?>
1417 <div class="notice notice-error is-dismissible">
1418 <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p>
1419 </div>
1420 <?php endif; ?>
1421
1422 <?php if (isset($_GET['access_roles_saved']) && $_GET['access_roles_saved'] == '1'): ?>
1423 <div class="notice notice-success is-dismissible">
1424 <p><strong>�
1425 Success!</strong> Plugin access roles have been saved successfully.</p>
1426 </div>
1427 <?php endif; ?>
1428
1429 <?php if (isset($_GET['access_roles_error']) && $_GET['access_roles_error'] == '1'): ?>
1430 <div class="notice notice-error is-dismissible">
1431 <p><strong>❌ Error!</strong> Failed to save plugin access roles. <?php echo isset($_GET['message']) ? esc_html(urldecode($_GET['message'])) : 'Please try again.'; ?></p>
1432 </div>
1433 <?php endif; ?>
1434
1435 <?php endif; ?>
1436 <?php $this->admin->render_layout_close(); ?>
1437 <?php
1438 }
1439
1440 // ------------------------------------------------------------------
1441 // Simple page renderers
1442 // ------------------------------------------------------------------
1443
1444 public function create_admin_dashboard_page()
1445 {
1446 ?>
1447 <?php $this->admin->render_layout_open('Dashboard', 'dashboard', ''); ?>
1448
1449 <div class="dashboard-card">
1450 <h2><?php echo esc_html($this->admin->get_effective_menu_title()); ?> Dashboard</h2>
1451 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Access your <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> dashboard to view analytics, manage SEO settings, and monitor your site performance.</p>
1452 <?php
1453 if (!isset(Metasync::get_option('general')['linkgraph_token']) || Metasync::get_option('general')['linkgraph_token'] == '') {
1454 echo '<p style="color: #d54e21; margin-bottom: 15px;">Authentication required: Please authenticate with your ' . esc_html(Metasync::get_effective_plugin_name()) . ' account and save your auth token in general settings.</p>';
1455 echo '<a href="' . admin_url('admin.php?page=' . Metasync_Admin::$page_slug) . '" class="button button-secondary">Go to Settings</a>';
1456 } else {
1457 echo '<a href="' . esc_url($this->admin->get_dashboard_url()) . '" target="_blank" class="button button-primary">Open Dashboard</a>';
1458 }
1459 ?>
1460 </div>
1461 <?php $this->admin->render_layout_close(); ?>
1462 <?php
1463 }
1464
1465 public function create_admin_robots_txt_page()
1466 {
1467 ?>
1468 <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?>
1469
1470 <?php
1471 require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt.php';
1472
1473 $robots_txt = Metasync_Robots_Txt::get_instance();
1474
1475 if (isset($_POST['metasync_robots_txt_nonce'])) {
1476 check_admin_referer('metasync_save_robots_txt', 'metasync_robots_txt_nonce');
1477
1478 if (isset($_POST['robots_content'])) {
1479 $content = wp_unslash($_POST['robots_content']);
1480
1481 $validation = $robots_txt->validate_content($content);
1482
1483 if ($validation['valid']) {
1484 $result = $robots_txt->write_robots_file($content);
1485
1486 if (is_wp_error($result)) {
1487 echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>';
1488 } else {
1489 echo '<div class="notice notice-success"><p>' . esc_html__('robots.txt file saved successfully!', 'metasync') . '</p></div>';
1490
1491 if (!empty($validation['warnings'])) {
1492 foreach ($validation['warnings'] as $warning) {
1493 echo '<div class="notice notice-warning"><p>' . esc_html($warning) . '</p></div>';
1494 }
1495 }
1496 }
1497 } else {
1498 echo '<div class="notice notice-error"><p>' . esc_html__('Validation failed. Please fix the errors below:', 'metasync') . '</p>';
1499 foreach ($validation['errors'] as $error) {
1500 echo '<p>- ' . esc_html($error) . '</p>';
1501 }
1502 echo '</div>';
1503 }
1504 }
1505 }
1506
1507 $current_content = $robots_txt->read_robots_file();
1508 if (is_wp_error($current_content)) {
1509 echo '<div class="notice notice-error"><p>' . esc_html($current_content->get_error_message()) . '</p></div>';
1510 $current_content = '';
1511 }
1512
1513 $backups = $robots_txt->get_backup_history(10);
1514
1515 $file_exists = $robots_txt->file_exists();
1516 $is_writable = $robots_txt->is_writable();
1517
1518 $robots_txt->render($this->admin, $current_content, $backups, $file_exists, $is_writable);
1519 ?>
1520 <?php $this->admin->render_layout_close(); ?>
1521 <?php
1522 }
1523
1524 public function create_admin_report_issue_page()
1525 {
1526 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1527 }
1528
1529 public function create_admin_xml_sitemap_page()
1530 {
1531 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
1532
1533 $sitemap_generator = new Metasync_Sitemap_Generator();
1534
1535 if (isset($_POST['metasync_sitemap_nonce'])) {
1536 check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce');
1537
1538 if (isset($_POST['generate_sitemap'])) {
1539 $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators();
1540
1541 $result = $sitemap_generator->generate_sitemap();
1542
1543 if (is_wp_error($result)) {
1544 error_log('[MetaSync] Sitemap generation failed: ' . $result->get_error_message());
1545 echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed. Please try again or check the error logs.', 'metasync') . '</p></div>';
1546 } else {
1547 $message = esc_html__('Sitemap generated successfully!', 'metasync');
1548 if ($disabled_plugins) {
1549 $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
1550 }
1551
1552 $robots_result = get_transient('metasync_sitemap_robots_updated');
1553 if ($robots_result && $robots_result['success']) {
1554 if ($robots_result['action'] === 'added') {
1555 $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
1556 } elseif ($robots_result['action'] === 'updated') {
1557 $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
1558 } elseif ($robots_result['action'] === 'created') {
1559 $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
1560 }
1561 delete_transient('metasync_sitemap_robots_updated');
1562 }
1563
1564 echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
1565 }
1566 } elseif (isset($_POST['enable_auto_update'])) {
1567 update_option('metasync_sitemap_auto_update', true);
1568 $sitemap_generator->setup_auto_update_hooks();
1569 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
1570 } elseif (isset($_POST['disable_auto_update'])) {
1571 update_option('metasync_sitemap_auto_update', false);
1572 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
1573 } elseif (isset($_POST['delete_sitemap'])) {
1574 $deleted = $sitemap_generator->delete_sitemap();
1575 if ($deleted) {
1576 update_option('metasync_sitemap_auto_update', false);
1577 // Re-enable WP core sitemap so the site isn't left with zero sitemaps
1578 delete_option('metasync_disable_wp_sitemap');
1579 echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap deleted successfully!', 'metasync') . '</p></div>';
1580 } else {
1581 echo '<div class="notice notice-error"><p>' . esc_html__('Failed to delete sitemap. The file may not exist or is not writable.', 'metasync') . '</p></div>';
1582 }
1583 } elseif (isset($_POST['enable_other_sitemaps'])) {
1584 $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
1585 if ($enabled_plugins) {
1586 echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>';
1587 } else {
1588 echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>';
1589 }
1590 }
1591 }
1592
1593 $sitemap_exists = $sitemap_generator->sitemap_exists();
1594 $sitemap_url = $sitemap_generator->get_sitemap_url();
1595 $url_count = $sitemap_generator->count_urls();
1596 $last_generated = $sitemap_generator->get_last_generated_time();
1597 $auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
1598 $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
1599
1600 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
1601 }
1602
1603 public function create_admin_custom_pages_page()
1604 {
1605 ?>
1606 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
1607
1608 <div class="metasync-page-content">
1609 <?php
1610 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-custom-pages-admin.php';
1611 Metasync_Custom_Pages_Admin::render_admin_page();
1612 ?>
1613 </div>
1614
1615 <?php $this->admin->render_layout_close(); ?>
1616 <?php
1617 }
1618
1619 public function create_admin_404_monitor_page()
1620 {
1621 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
1622 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor.php';
1623
1624 $db_404 = new Metasync_Error_Monitor_Database();
1625 $ErrorMonitor = new Metasync_Error_Monitor($db_404);
1626
1627 $this->admin->render_layout_open('404 Monitor', 'monitor_404', 'Track and manage 404 errors detected on your site.');
1628 $ErrorMonitor->create_admin_plugin_interface();
1629 $this->admin->render_layout_close();
1630 }
1631
1632 public function create_admin_search_engine_verification_page()
1633 {
1634 $page_slug = Metasync_Admin::$page_slug . '_searchengines-verification';
1635 ?>
1636 <?php $this->admin->render_layout_open('Site Verification', 'site_verification', 'Verify your site ownership with search engines and other platforms.'); ?>
1637
1638 <form method="post" action="options.php">
1639 <div class="dashboard-card">
1640 <h2>Search Engine Verification</h2>
1641 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Verify your site ownership with major search engines to access their tools and analytics.</p>
1642 <?php
1643 settings_fields(Metasync_Admin::option_group);
1644 do_settings_sections($page_slug);
1645 submit_button();
1646 ?>
1647 </div>
1648 </form>
1649 <?php $this->admin->render_layout_close(); ?>
1650 <?php
1651 }
1652
1653 public function create_admin_breadcrumbs_page()
1654 {
1655 $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs';
1656 ?>
1657 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1658
1659 <form method="post" action="options.php">
1660 <div class="dashboard-card">
1661 <h2>Breadcrumbs Settings</h2>
1662 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1663 <?php
1664 settings_fields(Metasync_Admin::option_group);
1665 do_settings_sections($page_slug);
1666 submit_button();
1667 ?>
1668 </div>
1669 </form>
1670
1671 <div class="dashboard-card" style="margin-top: 20px;">
1672 <h2>📖 How to Display Breadcrumbs</h2>
1673 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Breadcrumbs are not inserted automatically. Use one of the methods below to place them on your site.</p>
1674
1675 <h3 style="margin-top: 0;">Option 1 — Shortcode</h3>
1676 <p style="color: var(--dashboard-text-secondary);">Paste the shortcode inside any page, post, or widget:</p>
1677 <div class="metasync-copy-wrap"><pre class="metasync-copy-snippet" style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text-primary); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">[metasync_breadcrumb]</pre><span class="metasync-copy-badge">Copy</span></div>
1678
1679 <h3>Option 2 PHP Template Function</h3>
1680 <p style="color: var(--dashboard-text-secondary);">Call the helper function directly inside your theme template files (e.g. <code>single.php</code>, <code>page.php</code>, <code>header.php</code>):</p>
1681 <div class="metasync-copy-wrap"><pre class="metasync-copy-snippet" style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text-primary); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">&lt;?php metasync_breadcrumb(); ?&gt;</pre><span class="metasync-copy-badge">Copy</span></div>
1682
1683 <h3>Option 3 PHP Return Value</h3>
1684 <p style="color: var(--dashboard-text-secondary);">Get the breadcrumb HTML as a string to manipulate before output:</p>
1685 <div class="metasync-copy-wrap"><pre class="metasync-copy-snippet" style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text-primary); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">$breadcrumbs = new Metasync_Breadcrumbs();
1686 echo $breadcrumbs-&gt;render_breadcrumb_html();</pre><span class="metasync-copy-badge">Copy</span></div>
1687
1688 <p style="color: var(--dashboard-text-secondary); margin-top: 16px; font-size: 12px;">
1689 <strong>Tip:</strong> You can also pass a <code>post_id</code> argument to generate breadcrumbs for a specific post outside of a template context:<br>
1690 <code>echo $breadcrumbs-&gt;render_breadcrumb_html(['post_id' =&gt; 123]);</code>
1691 </p>
1692 <?php /* click-to-copy for the code snippets above */ ?>
1693 <style>
1694 .metasync-copy-wrap { position: relative; }
1695 .metasync-copy-snippet { cursor: pointer; outline: 1px solid transparent; transition: outline-color .15s ease; }
1696 .metasync-copy-wrap:hover .metasync-copy-snippet { outline-color: var(--dashboard-border, #374151); }
1697 .metasync-copy-badge { position: absolute; top: 8px; right: 10px; font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 4px; pointer-events: none; opacity: 0; transition: opacity .15s ease; background: var(--dashboard-border, #374151); color: var(--dashboard-text-primary, #fff); }
1698 .metasync-copy-wrap:hover .metasync-copy-badge { opacity: .85; }
1699 .metasync-copy-badge.metasync-copy-done { opacity: 1; background: var(--dashboard-gradient-primary, linear-gradient(135deg, #667eea 0%, #764ba2 100%)); color: #fff; }
1700 </style>
1701 <script>
1702 (function(){
1703 if (window.__metasyncCopyInit) return; window.__metasyncCopyInit = true;
1704 function fallback(text){ var ta=document.createElement('textarea'); ta.value=text; ta.style.position='fixed'; ta.style.opacity='0'; document.body.appendChild(ta); ta.focus(); ta.select(); try{document.execCommand('copy');}catch(e){} document.body.removeChild(ta); }
1705 function attach(){
1706 document.querySelectorAll('.metasync-copy-snippet').forEach(function(pre){
1707 if (pre.dataset.copyBound) return; pre.dataset.copyBound='1';
1708 pre.setAttribute('title','Click to copy');
1709 pre.addEventListener('click', function(){
1710 var text = pre.textContent.trim();
1711 var wrap = pre.closest('.metasync-copy-wrap') || pre.parentNode;
1712 var badge = wrap.querySelector('.metasync-copy-badge');
1713 var done = function(){ if(!badge) return; badge.textContent='Copied!'; badge.classList.add('metasync-copy-done'); clearTimeout(badge._t); badge._t=setTimeout(function(){ badge.textContent='Copy'; badge.classList.remove('metasync-copy-done'); },1500); };
1714 if (navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(done).catch(function(){ fallback(text); done(); }); }
1715 else { fallback(text); done(); }
1716 });
1717 });
1718 }
1719 if (document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', attach); } else { attach(); }
1720 })();
1721 </script>
1722 </div>
1723 <?php $this->admin->render_layout_close(); ?>
1724 <?php
1725 }
1726
1727 public function create_admin_schema_markup_page()
1728 {
1729 $page_slug = Metasync_Admin::$page_slug . '_schema-markup';
1730 $this->admin->render_layout_open('Schema Markup', 'schema_markup', 'Configure global schema markup settings for your site. Organization and WebSite schemas are injected site-wide.');
1731 ?>
1732 <form method="post" action="options.php">
1733 <div class="dashboard-card">
1734 <h2>Schema Markup Settings</h2>
1735 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure the site-wide Organization and WebSite structured data injected into every page.</p>
1736 <?php
1737 settings_fields(Metasync_Admin::option_group);
1738 do_settings_sections($page_slug);
1739 submit_button('Save Changes');
1740 ?>
1741 </div>
1742 </form>
1743 <?php
1744 $this->admin->render_layout_close();
1745 }
1746
1747 public function create_admin_local_business_page()
1748 {
1749 $page_slug = Metasync_Admin::$page_slug . '_local-seo';
1750 ?>
1751 <?php $this->admin->render_layout_open('Local Business', 'local_business', 'Configure local business information for better local SEO.'); ?>
1752
1753 <form method="post" action="options.php">
1754 <div class="dashboard-card">
1755 <h2>🏢 Local Business Configuration<?php Metasync::render_tooltip_icon('local_business_section', 'Fill in your business name, address, phone, hours, and category so search engines can show your business in local results and map listings. This generates LocalBusiness schema behind the scenes. Use your exact, consistent NAP (name/address/phone) details.'); ?></h2>
1756 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure your local business information for better local search engine optimization.</p>
1757 <?php
1758 settings_fields(Metasync_Admin::option_group);
1759 do_settings_sections($page_slug);
1760 submit_button();
1761 ?>
1762 </div>
1763 </form>
1764 <?php $this->admin->render_layout_close(); ?>
1765 <?php
1766 }
1767
1768 public function create_admin_code_snippets_page()
1769 {
1770 $page_slug = Metasync_Admin::$page_slug . '_code-snippets';
1771 ?>
1772 <?php $this->admin->render_layout_open('Code Snippets', 'code_snippets', 'Add custom code to your site header and footer.'); ?>
1773
1774 <form method="post" action="options.php">
1775 <div class="dashboard-card">
1776 <h2>Code Snippets Management</h2>
1777 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Add custom code snippets to enhance your site functionality and tracking capabilities.</p>
1778 <?php
1779 settings_fields(Metasync_Admin::option_group);
1780 do_settings_sections($page_slug);
1781 submit_button();
1782 ?>
1783 </div>
1784 </form>
1785 <?php $this->admin->render_layout_close(); ?>
1786 <?php
1787 }
1788
1789 public function create_admin_google_instant_index_page()
1790 {
1791 // Delegates to the admin class which uses the shared credentials view
1792 $this->admin->create_admin_google_instant_index_page();
1793 }
1794
1795 public function create_admin_google_console_page()
1796 {
1797 $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
1798 $google = function_exists('google_index_direct') ? google_index_direct() : null;
1799 if (is_null($google)) {
1800 $service_info = ['error' => 'Module not loaded'];
1801 } else {
1802 $service_info = $google->get_service_account_info();
1803 }
1804 $is_configured = !isset($service_info['error']);
1805 include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php';
1806 $this->admin->render_layout_close();
1807 }
1808
1809 public function create_admin_bing_console_page()
1810 {
1811 ?>
1812 <?php $this->admin->render_layout_open('Bing Console', 'bing_console', ''); ?>
1813
1814 <?php
1815 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
1816 $bing_instant_index = new Metasync_Bing_Instant_Index();
1817 $bing_instant_index->show_bing_instant_indexing_console();
1818 ?>
1819 <?php $this->admin->render_layout_close(); ?>
1820 <?php
1821 }
1822
1823 public function create_admin_optimal_settings_page()
1824 {
1825 ?>
1826 <?php $this->admin->render_layout_open('Optimal Settings', 'optimal_settings', 'Apply recommended SEO settings automatically.'); ?>
1827
1828 <div class="dashboard-card">
1829 <h2>Site Compatibility Status</h2>
1830 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Check your site's compatibility with optimal <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> settings.</p>
1831 <?php
1832 $optimal_settings = new Metasync_Optimal_Settings();
1833 $optimal_settings->site_compatible_status_view();
1834 ?>
1835 </div>
1836
1837 <div class="dashboard-card">
1838 <h2>Optimization Settings</h2>
1839 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure optimization settings for best performance.</p>
1840 <?php
1841 $this->optimization_settings_options();
1842 ?>
1843 </div>
1844 <?php $this->admin->render_layout_close(); ?>
1845 <?php
1846 }
1847
1848 public function create_admin_global_settings_page()
1849 {
1850 $page_slug = Metasync_Admin::$page_slug . '_common-settings';
1851 ?>
1852 <?php $this->admin->render_layout_open('Global Settings', 'general', ''); ?>
1853
1854 <form method="post" action="options.php">
1855 <div class="dashboard-card">
1856 <h2>Global Configuration</h2>
1857 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure global settings that apply across your entire site.</p>
1858 <?php
1859 settings_fields(Metasync_Admin::option_group);
1860 do_settings_sections($page_slug);
1861 ?>
1862 </div>
1863
1864 <!-- Save button removed - using floating notification system instead -->
1865 </form>
1866 <?php $this->admin->render_layout_close(); ?>
1867 <?php
1868 }
1869
1870 public function create_admin_common_meta_settings_page()
1871 {
1872 $page_slug = Metasync_Admin::$page_slug . '_common-meta-settings';
1873 ?>
1874 <?php $this->admin->render_layout_open('Common Meta Settings', 'general', ''); ?>
1875
1876 <form method="post" action="options.php">
1877 <div class="dashboard-card">
1878 <h2>Meta Configuration</h2>
1879 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure common meta tags and SEO settings for your site.</p>
1880 <?php
1881 settings_fields(Metasync_Admin::option_group);
1882 do_settings_sections($page_slug);
1883 ?>
1884 </div>
1885
1886 <!-- Save button removed - using floating notification system instead -->
1887 </form>
1888 <?php $this->admin->render_layout_close(); ?>
1889 <?php
1890 }
1891
1892 public function create_admin_social_meta_page()
1893 {
1894 $page_slug = Metasync_Admin::$page_slug . '_social-meta';
1895 ?>
1896 <?php $this->admin->render_layout_open('Social Meta', 'general', ''); ?>
1897
1898 <form method="post" action="options.php">
1899 <div class="dashboard-card">
1900 <h2>📲 Social Meta Tags</h2>
1901 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how your content appears when shared on social media platforms.</p>
1902 <?php
1903 settings_fields(Metasync_Admin::option_group);
1904 do_settings_sections($page_slug);
1905 ?>
1906 </div>
1907
1908 <!-- Save button removed - using floating notification system instead -->
1909 </form>
1910 <?php $this->admin->render_layout_close(); ?>
1911 <?php
1912 }
1913
1914 public function create_admin_seo_controls_page()
1915 {
1916 $page_slug = Metasync_Admin::$page_slug . '_seo-controls';
1917 ?>
1918 <?php $this->admin->render_layout_open('Indexation Control', 'seo_controls', 'Control how search engines index your content.'); ?>
1919
1920 <!-- Status Messages Container -->
1921 <div id="seo-controls-messages"></div>
1922
1923 <form method="post" action="options.php" id="metaSyncSeoControlsForm">
1924 <div class="dashboard-card">
1925 <div style="display: flex; justify-content: flex-end; margin-bottom: 20px;">
1926 <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-import-external&tab=indexation')); ?>" class="button button-secondary">
1927 <span class="dashicons dashicons-download" style="margin-right:4px; font-size:15px; width:15px; height:15px; line-height:1.6;"></span> Import from SEO Plugins
1928 </a>
1929 </div>
1930 <?php
1931 settings_fields(Metasync_Admin::option_group);
1932 do_settings_sections($page_slug);
1933
1934 wp_nonce_field('meta_sync_seo_controls_nonce', 'meta_sync_seo_controls_nonce');
1935 ?>
1936 </div>
1937
1938 <!-- Save button removed - using floating notification system instead -->
1939 </form>
1940 <?php $this->admin->render_layout_close(); ?>
1941 <?php
1942 }
1943
1944 public function optimization_settings_options()
1945 {
1946 $page_slug = Metasync_Admin::$page_slug . '_optimal-settings';
1947 $site_info_slug = Metasync_Admin::$page_slug . '_site-info-settings';
1948
1949 printf('<form method="post" action="options.php">');
1950 settings_fields(Metasync_Admin::option_group);
1951 do_settings_sections($page_slug);
1952 do_settings_sections($site_info_slug);
1953 submit_button();
1954 printf('</form>');
1955 }
1956
1957 public function create_admin_error_logs_page()
1958 {
1959 ?>
1960 <?php $this->admin->render_layout_open('Error Logs', 'general', ''); ?>
1961
1962 <div class="dashboard-card">
1963 <h2>Error Logs Management</h2>
1964 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">View and manage system error logs to troubleshoot issues and monitor plugin performance.</p>
1965 <?php
1966 $error_logs = new Metasync_Error_Logs();
1967
1968 if ($error_logs->can_show_error_logs()) {
1969 $log_content = $error_logs->get_error_logs(50);
1970
1971 if (!empty(trim($log_content))) {
1972 $error_logs->show_copy_button();
1973 $error_logs->show_logs();
1974 $error_logs->show_info();
1975 } else {
1976 echo '<div class="dashboard-empty-state">';
1977 echo '<p style="color: var(--dashboard-text-secondary); font-style: italic; text-align: center; padding: 40px 20px;">�
1978 Log file is empty - no errors recorded.</p>';
1979 echo '</div>';
1980 }
1981 } else {
1982 $error_message = $error_logs->get_error_message();
1983 if (!empty($error_message)) {
1984 echo '<div class="dashboard-empty-state">';
1985 echo '<p style="color: #d54e21; font-weight: bold; text-align: center; padding: 20px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; margin: 20px 0;">';
1986 echo esc_html($error_message);
1987 echo '</p>';
1988 echo '</div>';
1989 } else {
1990 echo '<div class="dashboard-empty-state">';
1991 echo '<p style="color: var(--dashboard-text-secondary); font-style: italic; text-align: center; padding: 40px 20px;">Unable to access error log file. Please check permissions.</p>';
1992 echo '</div>';
1993 }
1994 }
1995 ?>
1996 </div>
1997 <?php $this->admin->render_layout_close(); ?>
1998 <?php
1999 }
2000
2001 public function create_admin_sync_log_page()
2002 {
2003 $sync_db = new Metasync_Sync_History_Database();
2004
2005 if (wp_doing_ajax()) {
2006 $this->handle_sync_log_ajax();
2007 return;
2008 }
2009
2010 $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
2011 $per_page = 10;
2012 $offset = ($page - 1) * $per_page;
2013
2014 $filters = [
2015 'date_range' => isset($_GET['date_range']) ? sanitize_text_field($_GET['date_range']) : '',
2016 'status' => isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '',
2017 ];
2018
2019 $date_range = $filters['date_range'];
2020 $wp_now_ts = current_time('timestamp');
2021 $date_from = '';
2022 $date_to = '';
2023
2024 if (!empty($date_range)) {
2025 $date_to = date('Y-m-d H:i:s', $wp_now_ts);
2026
2027 if ($date_range === 'today') {
2028 $start_ts = strtotime('today', $wp_now_ts);
2029 $date_from = date('Y-m-d H:i:s', $start_ts);
2030 } elseif ($date_range === 'yesterday') {
2031 $start_ts = strtotime('yesterday', $wp_now_ts);
2032 $end_ts = strtotime('today', $wp_now_ts) - 1;
2033 $date_from = date('Y-m-d H:i:s', $start_ts);
2034 $date_to = date('Y-m-d H:i:s', $end_ts);
2035 } elseif ($date_range === 'this_week') {
2036 $start_of_week = (int) get_option('start_of_week', 1);
2037 $day_of_week = (int) date('w', $wp_now_ts);
2038 $delta_days = ($day_of_week - $start_of_week + 7) % 7;
2039 $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
2040 $date_from = date('Y-m-d H:i:s', $start_ts);
2041 } elseif ($date_range === 'this_month') {
2042 $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
2043 $date_from = date('Y-m-d H:i:s', $start_ts);
2044 } elseif ($date_range === 'all') {
2045 // no bounds
2046 }
2047 }
2048
2049 if (!empty($date_from)) {
2050 $filters['date_from'] = $date_from;
2051 }
2052 if (!empty($date_to)) {
2053 $filters['date_to'] = $date_to;
2054 }
2055
2056 $filters = array_filter($filters);
2057
2058 $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters);
2059 $total_records = $sync_db->get_count($filters);
2060 $total_pages = ceil($total_records / $per_page);
2061
2062 $stats = $sync_db->get_statistics();
2063
2064 ?>
2065 <?php $this->admin->render_layout_open('Changes Log', 'sync_log', 'Track all SEO changes made by the plugin.'); ?>
2066
2067 <div class="dashboard-card">
2068 <div class="sync-log-header">
2069 <div class="sync-log-title-section">
2070 <h2>Changes Log</h2>
2071 <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">Recent content synchronizations from external tools.</p>
2072 </div>
2073
2074 <!-- Filters - Right aligned -->
2075 <div class="sync-log-filters">
2076 <form method="get" class="sync-filters-form" onchange="this.submit()" style="display:flex;flex-direction:row;align-items:center;gap:12px;flex-wrap:nowrap;">
2077 <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>">
2078
2079 <select name="date_range" class="sync-filter-select">
2080 <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
2081 <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
2082 <option value="yesterday" <?php selected($filters['date_range'] ?? '', 'yesterday'); ?>>Yesterday</option>
2083 <option value="this_week" <?php selected($filters['date_range'] ?? '', 'this_week'); ?>>This week</option>
2084 <option value="this_month" <?php selected($filters['date_range'] ?? '', 'this_month'); ?>>This month</option>
2085 </select>
2086
2087 <select name="status" class="sync-filter-select">
2088 <option value="" <?php selected($filters['status'] ?? '', ''); ?>>Status Filter</option>
2089 <option value="published" <?php selected($filters['status'] ?? '', 'published'); ?>>Published</option>
2090 <option value="draft" <?php selected($filters['status'] ?? '', 'draft'); ?>>Draft</option>
2091 </select>
2092 </form>
2093 </div>
2094 </div>
2095
2096 <!-- Sync History List -->
2097 <div class="sync-log-list">
2098 <?php if (empty($sync_records)): ?>
2099 <div class="sync-log-empty">
2100 <div class="sync-log-empty-icon"><span class="dashicons dashicons-list-view" style="font-size:48px;width:48px;height:48px;color:var(--dashboard-text-secondary);"></span></div>
2101 <h3>No sync records found</h3>
2102 <p>Sync records will appear here when content/pages receive new updates.</p>
2103 </div>
2104 <?php else: ?>
2105 <?php foreach ($sync_records as $record): ?>
2106 <div class="sync-log-item">
2107 <div class="sync-log-icon">
2108 <div class="sync-icon-circle">
2109 <span class="dashicons dashicons-media-default sync-icon"></span>
2110 </div>
2111 </div>
2112
2113 <div class="sync-log-content">
2114 <div class="sync-log-title"><?php echo esc_html($record->title); ?>
2115 <?php if (!empty($record->url)): ?>
2116 <a href="<?php echo esc_url($record->url); ?>" target="_blank" rel="noopener" title="Open URL" style="margin-left:8px; text-decoration:none;"><span class="dashicons dashicons-external" style="font-size:14px;width:14px;height:14px;vertical-align:middle;"></span></a>
2117 <?php endif; ?>
2118 </div>
2119 <div class="sync-log-meta">
2120 <?php echo esc_html( $this->time_elapsed_string($record->created_at) ); ?>
2121 </div>
2122 </div>
2123
2124 <div class="sync-log-status">
2125 <?php if ($record->status === 'published' || $record->status === 'publish'): ?>
2126 <span class="sync-status-badge sync-status-published">
2127 <span class="sync-status-icon"></span>
2128 Published
2129 </span>
2130 <?php else: ?>
2131 <span class="sync-status-badge sync-status-draft">
2132 <span class="sync-status-icon">i</span>
2133 Draft
2134 </span>
2135 <?php endif; ?>
2136 </div>
2137 </div>
2138 <?php endforeach; ?>
2139 <?php endif; ?>
2140 </div>
2141
2142 <!-- Pagination -->
2143 <?php if ($total_pages > 1): ?>
2144 <div class="sync-log-pagination">
2145 <div class="sync-log-pagination-info">
2146 Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?>
2147 </div>
2148
2149 <div class="sync-log-pagination-controls">
2150 <?php if ($page > 1): ?>
2151 <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) - 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn"></a>
2152 <?php endif; ?>
2153
2154 <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
2155 <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>"
2156 class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a>
2157 <?php endfor; ?>
2158
2159 <?php if ($page < $total_pages): ?>
2160 <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) + 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn"></a>
2161 <?php endif; ?>
2162 </div>
2163 </div>
2164 <?php endif; ?>
2165 </div>
2166 <?php $this->admin->render_layout_close(); ?>
2167 <?php
2168 }
2169
2170 public function creat_error_Logs_List()
2171 {
2172 // printf('<h1> Error Logs </h1>');
2173 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
2174 // $error_log->create_admin_error_log_list_interface();
2175 }
2176
2177 public function create_admin_heartbeat_error_logs_page()
2178 {
2179 ?>
2180 <?php $this->admin->render_layout_open('Heartbeat Error Logs', 'general', ''); ?>
2181
2182 <div class="dashboard-card">
2183 <h2>💓 HeartBeat Error Logs</h2>
2184 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Monitor WordPress heartbeat errors to identify connectivity issues and system problems.</p>
2185 <?php
2186 $heartbeat_errors = new Metasync_HeartBeat_Error_Monitor($this->admin->db_heartbeat_errors);
2187 $heartbeat_errors->create_admin_heartbeat_errors_interface();
2188 ?>
2189 </div>
2190 <?php $this->admin->render_layout_close(); ?>
2191 <?php
2192 }
2193
2194 public function create_admin_bot_statistics_page()
2195 {
2196 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
2197 }
2198
2199 // ------------------------------------------------------------------
2200 // Private helpers (copied from Metasync_Admin, used only by pages above)
2201 // ------------------------------------------------------------------
2202
2203 private function time_elapsed_string($datetime, $full = false)
2204 {
2205 if(empty($datetime)){
2206 return "";
2207 }
2208 # Stored timestamps use current_time('mysql') which is WP local time
2209 # with no timezone marker. Defaulting DateTime to UTC produces a diff
2210 # equal to the WP timezone offset (QA: "4 hours ago" after a
2211 # 4-minute sync on non-UTC sites).
2212 $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
2213 $now = new DateTime('now', $wp_tz);
2214 $ago = new DateTime($datetime, $wp_tz);
2215
2216 $diff = $now->diff($ago);
2217
2218 $string = [
2219 'y' => 'year',
2220 'm' => 'month',
2221 'w' => 'week',
2222 'd' => 'day',
2223 'h' => 'hour',
2224 'i' => 'minute',
2225 's' => 'second',
2226 ];
2227
2228 foreach ($string as $k => &$v) {
2229 if (isset($diff->$k) && $diff->$k) {
2230 $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
2231 } else {
2232 unset($string[$k]);
2233 }
2234 }
2235 if (!$full) $string = array_slice($string, 0, 1);
2236 return $string ? implode(', ', $string) . ' ago' : 'just now';
2237 }
2238
2239 private function build_filter_query_string($filters)
2240 {
2241 $query_parts = [];
2242 foreach ($filters as $key => $value) {
2243 if (!empty($value)) {
2244 $query_parts[] = $key . '=' . urlencode($value);
2245 }
2246 }
2247 return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
2248 }
2249
2250 private function handle_sync_log_ajax()
2251 {
2252 wp_die();
2253 }
2254 }
2255