PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.6.21
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.6.21
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-admin-pages.php

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

2,220 lines 115.1 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<?php Metasync::render_tooltip_icon('cg_sync_section', sprintf('Syncing sends this site\'s categories and authors to %s, so Content Genius can assign the right category and author when publishing articles to this site.', Metasync::get_effective_plugin_name())); ?></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">
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 // First page of backups; the Backup History panel is paginated so the
1514 // list always reflects the full stored state and its true total.
1515 $backups = $robots_txt->get_backup_history(Metasync_Robots_Txt_Database::BACKUPS_PER_PAGE, 0);
1516
1517 $file_exists = $robots_txt->file_exists();
1518 $is_writable = $robots_txt->is_writable();
1519
1520 $robots_txt->render($this->admin, $current_content, $backups, $file_exists, $is_writable);
1521 ?>
1522 <?php $this->admin->render_layout_close(); ?>
1523 <?php
1524 }
1525
1526 public function create_admin_report_issue_page()
1527 {
1528 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1529 }
1530
1531 public function create_admin_xml_sitemap_page()
1532 {
1533 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
1534
1535 $sitemap_generator = new Metasync_Sitemap_Generator();
1536
1537 if (isset($_POST['metasync_sitemap_nonce'])) {
1538 check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce');
1539
1540 if (isset($_POST['generate_sitemap'])) {
1541 $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators();
1542
1543 $result = $sitemap_generator->generate_sitemap();
1544
1545 if (is_wp_error($result)) {
1546 error_log('[MetaSync] Sitemap generation failed: ' . $result->get_error_message());
1547 echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed. Please try again or check the error logs.', 'metasync') . '</p></div>';
1548 } else {
1549 $message = esc_html__('Sitemap generated successfully!', 'metasync');
1550 if ($disabled_plugins) {
1551 $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
1552 }
1553
1554 $robots_result = get_transient('metasync_sitemap_robots_updated');
1555 if ($robots_result && $robots_result['success']) {
1556 if ($robots_result['action'] === 'added') {
1557 $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
1558 } elseif ($robots_result['action'] === 'updated') {
1559 $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
1560 } elseif ($robots_result['action'] === 'created') {
1561 $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
1562 }
1563 delete_transient('metasync_sitemap_robots_updated');
1564 }
1565
1566 echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
1567 }
1568 } elseif (isset($_POST['enable_auto_update'])) {
1569 update_option('metasync_sitemap_auto_update', true);
1570 $sitemap_generator->setup_auto_update_hooks();
1571 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
1572 } elseif (isset($_POST['disable_auto_update'])) {
1573 update_option('metasync_sitemap_auto_update', false);
1574 echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
1575 } elseif (isset($_POST['delete_sitemap'])) {
1576 $deleted = $sitemap_generator->delete_sitemap();
1577 if ($deleted) {
1578 update_option('metasync_sitemap_auto_update', false);
1579 // Re-enable WP core sitemap so the site isn't left with zero sitemaps
1580 delete_option('metasync_disable_wp_sitemap');
1581 echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap deleted successfully!', 'metasync') . '</p></div>';
1582 } else {
1583 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>';
1584 }
1585 } elseif (isset($_POST['enable_other_sitemaps'])) {
1586 $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
1587 if ($enabled_plugins) {
1588 echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>';
1589 } else {
1590 echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>';
1591 }
1592 }
1593 }
1594
1595 $sitemap_exists = $sitemap_generator->sitemap_exists();
1596 $sitemap_url = $sitemap_generator->get_sitemap_url();
1597 $url_count = $sitemap_generator->count_urls();
1598 $last_generated = $sitemap_generator->get_last_generated_time();
1599 $auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
1600 $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
1601
1602 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
1603 }
1604
1605 public function create_admin_custom_pages_page()
1606 {
1607 ?>
1608 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
1609
1610 <div class="metasync-page-content">
1611 <?php
1612 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-custom-pages-admin.php';
1613 Metasync_Custom_Pages_Admin::render_admin_page();
1614 ?>
1615 </div>
1616
1617 <?php $this->admin->render_layout_close(); ?>
1618 <?php
1619 }
1620
1621 public function create_admin_404_monitor_page()
1622 {
1623 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
1624 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor.php';
1625
1626 $db_404 = new Metasync_Error_Monitor_Database();
1627 $ErrorMonitor = new Metasync_Error_Monitor($db_404);
1628
1629 $this->admin->render_layout_open('404 Monitor', 'monitor_404', 'Track and manage 404 errors detected on your site.');
1630 $ErrorMonitor->create_admin_plugin_interface();
1631 $this->admin->render_layout_close();
1632 }
1633
1634 public function create_admin_search_engine_verification_page()
1635 {
1636 $page_slug = Metasync_Admin::$page_slug . '_searchengines-verification';
1637 ?>
1638 <?php $this->admin->render_layout_open('Site Verification', 'site_verification', 'Verify your site ownership with search engines and other platforms.'); ?>
1639
1640 <form method="post" action="options.php">
1641 <div class="dashboard-card">
1642 <h2>Search Engine Verification</h2>
1643 <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>
1644 <?php
1645 settings_fields(Metasync_Admin::option_group);
1646 do_settings_sections($page_slug);
1647 submit_button();
1648 ?>
1649 </div>
1650 </form>
1651 <?php $this->admin->render_layout_close(); ?>
1652 <?php
1653 }
1654
1655 public function create_admin_breadcrumbs_page()
1656 {
1657 $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs';
1658 ?>
1659 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1660
1661 <form method="post" action="options.php">
1662 <div class="dashboard-card">
1663 <h2>Breadcrumbs Settings</h2>
1664 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1665 <?php
1666 settings_fields(Metasync_Admin::option_group);
1667 do_settings_sections($page_slug);
1668 submit_button();
1669 ?>
1670 </div>
1671 </form>
1672
1673 <div class="dashboard-card" style="margin-top: 20px;">
1674 <h2>📖 How to Display Breadcrumbs</h2>
1675 <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>
1676
1677 <h3 style="margin-top: 0;">Option 1 — Shortcode</h3>
1678 <p style="color: var(--dashboard-text-secondary);">Paste the shortcode inside any page, post, or widget:</p>
1679 <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>
1680
1681 <h3>Option 2 PHP Template Function</h3>
1682 <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>
1683 <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>
1684
1685 <h3>Option 3 PHP Return Value</h3>
1686 <p style="color: var(--dashboard-text-secondary);">Get the breadcrumb HTML as a string to manipulate before output:</p>
1687 <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();
1688 echo $breadcrumbs-&gt;render_breadcrumb_html();</pre><span class="metasync-copy-badge">Copy</span></div>
1689
1690 <p style="color: var(--dashboard-text-secondary); margin-top: 16px; font-size: 12px;">
1691 <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>
1692 <code>echo $breadcrumbs-&gt;render_breadcrumb_html(['post_id' =&gt; 123]);</code>
1693 </p>
1694 <?php /* click-to-copy for the code snippets above */ ?>
1695 <style>
1696 .metasync-copy-wrap { position: relative; }
1697 .metasync-copy-snippet { cursor: pointer; outline: 1px solid transparent; transition: outline-color .15s ease; }
1698 .metasync-copy-wrap:hover .metasync-copy-snippet { outline-color: var(--dashboard-border, #374151); }
1699 .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); }
1700 .metasync-copy-wrap:hover .metasync-copy-badge { opacity: .85; }
1701 .metasync-copy-badge.metasync-copy-done { opacity: 1; background: var(--dashboard-gradient-primary, linear-gradient(135deg, #667eea 0%, #764ba2 100%)); color: #fff; }
1702 </style>
1703 <script>
1704 (function(){
1705 if (window.__metasyncCopyInit) return; window.__metasyncCopyInit = true;
1706 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); }
1707 function attach(){
1708 document.querySelectorAll('.metasync-copy-snippet').forEach(function(pre){
1709 if (pre.dataset.copyBound) return; pre.dataset.copyBound='1';
1710 pre.setAttribute('title','Click to copy');
1711 pre.addEventListener('click', function(){
1712 var text = pre.textContent.trim();
1713 var wrap = pre.closest('.metasync-copy-wrap') || pre.parentNode;
1714 var badge = wrap.querySelector('.metasync-copy-badge');
1715 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); };
1716 if (navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(done).catch(function(){ fallback(text); done(); }); }
1717 else { fallback(text); done(); }
1718 });
1719 });
1720 }
1721 if (document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', attach); } else { attach(); }
1722 })();
1723 </script>
1724 </div>
1725 <?php $this->admin->render_layout_close(); ?>
1726 <?php
1727 }
1728
1729 public function create_admin_schema_markup_page()
1730 {
1731 $page_slug = Metasync_Admin::$page_slug . '_schema-markup';
1732 $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.');
1733 ?>
1734 <form method="post" action="options.php">
1735 <div class="dashboard-card">
1736 <h2>Schema Markup Settings</h2>
1737 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure the site-wide Organization and WebSite structured data injected into every page.</p>
1738 <?php
1739 settings_fields(Metasync_Admin::option_group);
1740 do_settings_sections($page_slug);
1741 submit_button('Save Changes');
1742 ?>
1743 </div>
1744 </form>
1745 <?php
1746 $this->admin->render_layout_close();
1747 }
1748
1749 public function create_admin_local_business_page()
1750 {
1751 $page_slug = Metasync_Admin::$page_slug . '_local-seo';
1752 ?>
1753 <?php $this->admin->render_layout_open('Local Business', 'local_business', 'Configure local business information for better local SEO.'); ?>
1754
1755 <form method="post" action="options.php">
1756 <div class="dashboard-card">
1757 <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>
1758 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure your local business information for better local search engine optimization.</p>
1759 <?php
1760 settings_fields(Metasync_Admin::option_group);
1761 do_settings_sections($page_slug);
1762 submit_button();
1763 ?>
1764 </div>
1765 </form>
1766 <?php $this->admin->render_layout_close(); ?>
1767 <?php
1768 }
1769
1770 public function create_admin_code_snippets_page()
1771 {
1772 $page_slug = Metasync_Admin::$page_slug . '_code-snippets';
1773 ?>
1774 <?php $this->admin->render_layout_open('Code Snippets', 'code_snippets', 'Add custom code to your site header and footer.'); ?>
1775
1776 <form method="post" action="options.php">
1777 <div class="dashboard-card">
1778 <h2>Code Snippets Management</h2>
1779 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Add custom code snippets to enhance your site functionality and tracking capabilities.</p>
1780 <?php
1781 settings_fields(Metasync_Admin::option_group);
1782 do_settings_sections($page_slug);
1783 submit_button();
1784 ?>
1785 </div>
1786 </form>
1787 <?php $this->admin->render_layout_close(); ?>
1788 <?php
1789 }
1790
1791 public function create_admin_google_instant_index_page()
1792 {
1793 // Delegates to the admin class which uses the shared credentials view
1794 $this->admin->create_admin_google_instant_index_page();
1795 }
1796
1797 public function create_admin_google_console_page()
1798 {
1799 $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
1800 $google = function_exists('google_index_direct') ? google_index_direct() : null;
1801 if (is_null($google)) {
1802 $service_info = ['error' => 'Module not loaded'];
1803 } else {
1804 $service_info = $google->get_service_account_info();
1805 }
1806 $is_configured = !isset($service_info['error']);
1807 include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php';
1808 $this->admin->render_layout_close();
1809 }
1810
1811 public function create_admin_bing_console_page()
1812 {
1813 ?>
1814 <?php $this->admin->render_layout_open('Bing Console', 'bing_console', ''); ?>
1815
1816 <?php
1817 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
1818 $bing_instant_index = new Metasync_Bing_Instant_Index();
1819 $bing_instant_index->show_bing_instant_indexing_console();
1820 ?>
1821 <?php $this->admin->render_layout_close(); ?>
1822 <?php
1823 }
1824
1825 public function create_admin_global_settings_page()
1826 {
1827 $page_slug = Metasync_Admin::$page_slug . '_common-settings';
1828 ?>
1829 <?php $this->admin->render_layout_open('Global Settings', 'general', ''); ?>
1830
1831 <form method="post" action="options.php">
1832 <div class="dashboard-card">
1833 <h2>Global Configuration</h2>
1834 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure global settings that apply across your entire site.</p>
1835 <?php
1836 settings_fields(Metasync_Admin::option_group);
1837 do_settings_sections($page_slug);
1838 ?>
1839 </div>
1840
1841 <!-- Save button removed - using floating notification system instead -->
1842 </form>
1843 <?php $this->admin->render_layout_close(); ?>
1844 <?php
1845 }
1846
1847 public function create_admin_common_meta_settings_page()
1848 {
1849 $page_slug = Metasync_Admin::$page_slug . '_common-meta-settings';
1850 ?>
1851 <?php $this->admin->render_layout_open('Common Meta Settings', 'general', ''); ?>
1852
1853 <form method="post" action="options.php">
1854 <div class="dashboard-card">
1855 <h2>Meta Configuration</h2>
1856 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure common meta tags and SEO settings for your site.</p>
1857 <?php
1858 settings_fields(Metasync_Admin::option_group);
1859 do_settings_sections($page_slug);
1860 ?>
1861 </div>
1862
1863 <!-- Save button removed - using floating notification system instead -->
1864 </form>
1865 <?php $this->admin->render_layout_close(); ?>
1866 <?php
1867 }
1868
1869 public function create_admin_social_meta_page()
1870 {
1871 $page_slug = Metasync_Admin::$page_slug . '_social-meta';
1872 ?>
1873 <?php $this->admin->render_layout_open('Social Meta', 'general', ''); ?>
1874
1875 <form method="post" action="options.php">
1876 <div class="dashboard-card">
1877 <h2>📲 Social Meta Tags</h2>
1878 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how your content appears when shared on social media platforms.</p>
1879 <?php
1880 settings_fields(Metasync_Admin::option_group);
1881 do_settings_sections($page_slug);
1882 ?>
1883 </div>
1884
1885 <!-- Save button removed - using floating notification system instead -->
1886 </form>
1887 <?php $this->admin->render_layout_close(); ?>
1888 <?php
1889 }
1890
1891 public function create_admin_seo_controls_page()
1892 {
1893 $page_slug = Metasync_Admin::$page_slug . '_seo-controls';
1894 ?>
1895 <?php $this->admin->render_layout_open('Indexation Control', 'seo_controls', 'Control how search engines index your content.'); ?>
1896
1897 <!-- Status Messages Container -->
1898 <div id="seo-controls-messages"></div>
1899
1900 <form method="post" action="options.php" id="metaSyncSeoControlsForm">
1901 <div class="dashboard-card">
1902 <div style="display: flex; justify-content: flex-end; margin-bottom: 20px;">
1903 <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-import-external&tab=indexation')); ?>" class="button button-secondary">
1904 <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
1905 </a>
1906 </div>
1907 <?php
1908 settings_fields(Metasync_Admin::option_group);
1909 do_settings_sections($page_slug);
1910
1911 wp_nonce_field('meta_sync_seo_controls_nonce', 'meta_sync_seo_controls_nonce');
1912 ?>
1913 </div>
1914
1915 <!-- Save button removed - using floating notification system instead -->
1916 </form>
1917 <?php $this->admin->render_layout_close(); ?>
1918 <?php
1919 }
1920
1921
1922 public function create_admin_error_logs_page()
1923 {
1924 ?>
1925 <?php $this->admin->render_layout_open('Error Logs', 'general', ''); ?>
1926
1927 <div class="dashboard-card">
1928 <h2>Error Logs Management</h2>
1929 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">View and manage system error logs to troubleshoot issues and monitor plugin performance.</p>
1930 <?php
1931 $error_logs = new Metasync_Error_Logs();
1932
1933 if ($error_logs->can_show_error_logs()) {
1934 $log_content = $error_logs->get_error_logs(50);
1935
1936 if (!empty(trim($log_content))) {
1937 $error_logs->show_copy_button();
1938 $error_logs->show_logs();
1939 $error_logs->show_info();
1940 } else {
1941 echo '<div class="dashboard-empty-state">';
1942 echo '<p style="color: var(--dashboard-text-secondary); font-style: italic; text-align: center; padding: 40px 20px;">�
1943 Log file is empty - no errors recorded.</p>';
1944 echo '</div>';
1945 }
1946 } else {
1947 $error_message = $error_logs->get_error_message();
1948 if (!empty($error_message)) {
1949 echo '<div class="dashboard-empty-state">';
1950 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;">';
1951 echo esc_html($error_message);
1952 echo '</p>';
1953 echo '</div>';
1954 } else {
1955 echo '<div class="dashboard-empty-state">';
1956 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>';
1957 echo '</div>';
1958 }
1959 }
1960 ?>
1961 </div>
1962 <?php $this->admin->render_layout_close(); ?>
1963 <?php
1964 }
1965
1966 public function create_admin_sync_log_page()
1967 {
1968 $sync_db = new Metasync_Sync_History_Database();
1969
1970 if (wp_doing_ajax()) {
1971 $this->handle_sync_log_ajax();
1972 return;
1973 }
1974
1975 $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
1976 $per_page = 10;
1977 $offset = ($page - 1) * $per_page;
1978
1979 $filters = [
1980 'date_range' => isset($_GET['date_range']) ? sanitize_text_field($_GET['date_range']) : '',
1981 'status' => isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '',
1982 ];
1983
1984 $date_range = $filters['date_range'];
1985 $wp_now_ts = current_time('timestamp');
1986 $date_from = '';
1987 $date_to = '';
1988
1989 if (!empty($date_range)) {
1990 $date_to = date('Y-m-d H:i:s', $wp_now_ts);
1991
1992 if ($date_range === 'today') {
1993 $start_ts = strtotime('today', $wp_now_ts);
1994 $date_from = date('Y-m-d H:i:s', $start_ts);
1995 } elseif ($date_range === 'yesterday') {
1996 $start_ts = strtotime('yesterday', $wp_now_ts);
1997 $end_ts = strtotime('today', $wp_now_ts) - 1;
1998 $date_from = date('Y-m-d H:i:s', $start_ts);
1999 $date_to = date('Y-m-d H:i:s', $end_ts);
2000 } elseif ($date_range === 'this_week') {
2001 $start_of_week = (int) get_option('start_of_week', 1);
2002 $day_of_week = (int) date('w', $wp_now_ts);
2003 $delta_days = ($day_of_week - $start_of_week + 7) % 7;
2004 $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
2005 $date_from = date('Y-m-d H:i:s', $start_ts);
2006 } elseif ($date_range === 'this_month') {
2007 $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
2008 $date_from = date('Y-m-d H:i:s', $start_ts);
2009 } elseif ($date_range === 'all') {
2010 // no bounds
2011 }
2012 }
2013
2014 if (!empty($date_from)) {
2015 $filters['date_from'] = $date_from;
2016 }
2017 if (!empty($date_to)) {
2018 $filters['date_to'] = $date_to;
2019 }
2020
2021 $filters = array_filter($filters);
2022
2023 $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters);
2024 $total_records = $sync_db->get_count($filters);
2025 $total_pages = ceil($total_records / $per_page);
2026
2027 $stats = $sync_db->get_statistics();
2028
2029 ?>
2030 <?php $this->admin->render_layout_open('Changes Log', 'sync_log', 'Track all SEO changes made by the plugin.'); ?>
2031
2032 <div class="dashboard-card">
2033 <div class="sync-log-header">
2034 <div class="sync-log-title-section">
2035 <h2>Changes Log</h2>
2036 <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">Recent content synchronizations from external tools.</p>
2037 </div>
2038
2039 <!-- Filters - Right aligned -->
2040 <div class="sync-log-filters">
2041 <form method="get" class="sync-filters-form" onchange="this.submit()" style="display:flex;flex-direction:row;align-items:center;gap:12px;flex-wrap:nowrap;">
2042 <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>">
2043
2044 <select name="date_range" class="sync-filter-select">
2045 <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
2046 <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
2047 <option value="yesterday" <?php selected($filters['date_range'] ?? '', 'yesterday'); ?>>Yesterday</option>
2048 <option value="this_week" <?php selected($filters['date_range'] ?? '', 'this_week'); ?>>This week</option>
2049 <option value="this_month" <?php selected($filters['date_range'] ?? '', 'this_month'); ?>>This month</option>
2050 </select>
2051
2052 <select name="status" class="sync-filter-select">
2053 <option value="" <?php selected($filters['status'] ?? '', ''); ?>>Status Filter</option>
2054 <option value="published" <?php selected($filters['status'] ?? '', 'published'); ?>>Published</option>
2055 <option value="draft" <?php selected($filters['status'] ?? '', 'draft'); ?>>Draft</option>
2056 </select>
2057 </form>
2058 </div>
2059 </div>
2060
2061 <!-- Sync History List -->
2062 <div class="sync-log-list">
2063 <?php if (empty($sync_records)): ?>
2064 <div class="sync-log-empty">
2065 <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>
2066 <h3>No sync records found</h3>
2067 <p>Sync records will appear here when content/pages receive new updates.</p>
2068 </div>
2069 <?php else: ?>
2070 <?php foreach ($sync_records as $record): ?>
2071 <div class="sync-log-item">
2072 <div class="sync-log-icon">
2073 <div class="sync-icon-circle">
2074 <span class="dashicons dashicons-media-default sync-icon"></span>
2075 </div>
2076 </div>
2077
2078 <div class="sync-log-content">
2079 <div class="sync-log-title"><?php echo esc_html($record->title); ?>
2080 <?php if (!empty($record->url)): ?>
2081 <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>
2082 <?php endif; ?>
2083 </div>
2084 <div class="sync-log-meta">
2085 <?php echo esc_html( $this->time_elapsed_string($record->created_at) ); ?>
2086 </div>
2087 </div>
2088
2089 <div class="sync-log-status">
2090 <?php if ($record->status === 'published' || $record->status === 'publish'): ?>
2091 <span class="sync-status-badge sync-status-published">
2092 <span class="sync-status-icon">✓</span>
2093 Published
2094 </span>
2095 <?php else: ?>
2096 <span class="sync-status-badge sync-status-draft">
2097 <span class="sync-status-icon">i</span>
2098 Draft
2099 </span>
2100 <?php endif; ?>
2101 </div>
2102 </div>
2103 <?php endforeach; ?>
2104 <?php endif; ?>
2105 </div>
2106
2107 <!-- Pagination -->
2108 <?php if ($total_pages > 1): ?>
2109 <div class="sync-log-pagination">
2110 <div class="sync-log-pagination-info">
2111 Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?>
2112 </div>
2113
2114 <div class="sync-log-pagination-controls">
2115 <?php if ($page > 1): ?>
2116 <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>
2117 <?php endif; ?>
2118
2119 <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
2120 <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>"
2121 class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a>
2122 <?php endfor; ?>
2123
2124 <?php if ($page < $total_pages): ?>
2125 <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>
2126 <?php endif; ?>
2127 </div>
2128 </div>
2129 <?php endif; ?>
2130 </div>
2131 <?php $this->admin->render_layout_close(); ?>
2132 <?php
2133 }
2134
2135 public function creat_error_Logs_List()
2136 {
2137 // printf('<h1> Error Logs </h1>');
2138 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
2139 // $error_log->create_admin_error_log_list_interface();
2140 }
2141
2142 public function create_admin_heartbeat_error_logs_page()
2143 {
2144 ?>
2145 <?php $this->admin->render_layout_open('Heartbeat Error Logs', 'general', ''); ?>
2146
2147 <div class="dashboard-card">
2148 <h2>💓 HeartBeat Error Logs</h2>
2149 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Monitor WordPress heartbeat errors to identify connectivity issues and system problems.</p>
2150 <?php
2151 $heartbeat_errors = new Metasync_HeartBeat_Error_Monitor($this->admin->db_heartbeat_errors);
2152 $heartbeat_errors->create_admin_heartbeat_errors_interface();
2153 ?>
2154 </div>
2155 <?php $this->admin->render_layout_close(); ?>
2156 <?php
2157 }
2158
2159 public function create_admin_bot_statistics_page()
2160 {
2161 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
2162 }
2163
2164 // ------------------------------------------------------------------
2165 // Private helpers (copied from Metasync_Admin, used only by pages above)
2166 // ------------------------------------------------------------------
2167
2168 private function time_elapsed_string($datetime, $full = false)
2169 {
2170 if(empty($datetime)){
2171 return "";
2172 }
2173 # Stored timestamps use current_time('mysql') which is WP local time
2174 # with no timezone marker. Defaulting DateTime to UTC produces a diff
2175 # equal to the WP timezone offset (QA: "4 hours ago" after a
2176 # 4-minute sync on non-UTC sites).
2177 $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
2178 $now = new DateTime('now', $wp_tz);
2179 $ago = new DateTime($datetime, $wp_tz);
2180
2181 $diff = $now->diff($ago);
2182
2183 $string = [
2184 'y' => 'year',
2185 'm' => 'month',
2186 'w' => 'week',
2187 'd' => 'day',
2188 'h' => 'hour',
2189 'i' => 'minute',
2190 's' => 'second',
2191 ];
2192
2193 foreach ($string as $k => &$v) {
2194 if (isset($diff->$k) && $diff->$k) {
2195 $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
2196 } else {
2197 unset($string[$k]);
2198 }
2199 }
2200 if (!$full) $string = array_slice($string, 0, 1);
2201 return $string ? implode(', ', $string) . ' ago' : 'just now';
2202 }
2203
2204 private function build_filter_query_string($filters)
2205 {
2206 $query_parts = [];
2207 foreach ($filters as $key => $value) {
2208 if (!empty($value)) {
2209 $query_parts[] = $key . '=' . urlencode($value);
2210 }
2211 }
2212 return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
2213 }
2214
2215 private function handle_sync_log_ajax()
2216 {
2217 wp_die();
2218 }
2219 }
2220