PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
metasync / includes / class-metasync-admin-pages.php

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

1,875 lines 98.9 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 if (isset($_REQUEST['metasync_password_reset'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing check, the reset form verifies its own nonce
222 Metasync_Admin_Ajax::instance()->render_password_reset_page();
223 return;
224 }
225
226 $active_tab = isset($_GET['tab']) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
227
228 $whitelabel_settings = Metasync::get_whitelabel_settings();
229
230 $user_password = $whitelabel_settings['settings_password'] ?? '';
231 $hide_settings_enabled = !empty($whitelabel_settings['hide_settings']);
232
233 $protected_tabs = [];
234
235 if (!empty($user_password)) {
236 $protected_tabs[] = 'whitelabel';
237 }
238
239 if ($hide_settings_enabled && !empty($user_password)) {
240 $protected_tabs = ['general', 'whitelabel', 'advanced'];
241 }
242
243 $password_protection_enabled = false;
244 $password_validated = false;
245 $password_error = '';
246
247 if (in_array($active_tab, $protected_tabs)) {
248
249 $password_protection_enabled = !empty($user_password);
250
251 $auth = new Metasync_Auth_Manager('whitelabel', 1800);
252 $password_validated = $auth->has_access();
253
254 if (isset($_POST['whitelabel_password_submit']) && !$password_validated) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only access check for a display message
255 $password_error = 'Incorrect password. Please try again.';
256 }
257 }
258
259 $is_authenticated = $password_validated;
260
261 $whitelabel_otto_name = Metasync::get_whitelabel_otto_name();
262
263 $page_slug = Metasync_Admin::$page_slug;
264 ?>
265 <?php
266 $settings_current_page = 'general';
267 if ($active_tab === 'advanced') $settings_current_page = 'advanced_settings';
268 elseif ($active_tab === 'whitelabel') $settings_current_page = 'whitelabel';
269 $this->admin->render_layout_open('Settings', $settings_current_page, 'Manage all plugin settings and configuration.');
270 ?>
271 <?php
272 /*
273 * options.php redirects back here after the native (no-JavaScript)
274 * settings submit. Scoped to metasync_messages so field-level
275 * rejections (e.g. invalid white-label branding) print without the
276 * generic "Settings saved." notice that would otherwise accompany
277 * a rejected save.
278 */
279 settings_errors('metasync_messages');
280 ?>
281 <?php
282 /*
283 # Temporarily commented out: Clear Cache notice and button (can be re-enabled later)
284
285 * <div class="notice notice-success">
286 * <p>
287 * <b>Clear all caches at once</b><br/>
288 * This will slow down your site until caches are rebuilt
289 * <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>
290 * </p>
291 * </div>
292 */
293 ?>
294
295 <?php /* Navigation now rendered in left sidenav via render_layout_open */ ?>
296
297 <?php
298 if (in_array($active_tab, $protected_tabs)) {
299
300 if ($password_protection_enabled && !$password_validated) {
301 ?>
302 <div class="dashboard-card" style="max-width: 500px; margin: 0 auto;">
303 <h2 style="text-align: center;">Protected Section</h2>
304 <p style="color: var(--dashboard-text-secondary); margin-bottom: 30px; text-align: center;">
305 <?php
306 $plugin_name = Metasync::get_effective_plugin_name('');
307 if ($hide_settings_enabled && !empty($user_password)) {
308 printf('Please enter the password to access the %s Settings section.', esc_html($plugin_name));
309 } else {
310 echo 'Please enter the password to access the Branding section.';
311 }
312 ?>
313 </p>
314
315 <?php if (!empty($password_error)): ?>
316 <div style="background: #f8d7da; color: #721c24; padding: 12px; border-radius: 6px; margin-bottom: 20px; border: 1px solid #f5c6cb;">
317 <strong>❌ Access Denied:</strong> <?php echo esc_html($password_error); ?>
318 </div>
319 <?php endif; ?>
320
321 <form method="post" action="" style="max-width: 400px; margin: 0 auto;">
322 <?php wp_nonce_field('whitelabel_password_nonce', 'whitelabel_nonce'); ?>
323
324 <div style="margin-bottom: 20px;">
325 <label for="whitelabel_password" style="display: block; font-weight: 600; margin-bottom: 8px; color: var(--dashboard-text-primary);">
326 Validate Password
327 </label>
328 <input
329 type="password"
330 id="whitelabel_password"
331 name="whitelabel_password"
332 placeholder="Enter password to access protected settings"
333 style="width: 100%; padding: 12px; border: 2px solid #ddd; border-radius: 6px; font-size: 14px; box-sizing: border-box;"
334 required
335 autocomplete="off"
336 />
337 </div>
338
339 <div style="text-align: center;">
340 <button
341 type="submit"
342 name="whitelabel_password_submit"
343 value="1"
344 class="button button-primary"
345 style="padding: 12px 24px; font-size: 14px; font-weight: 600;"
346 >
347 Submit Password
348 </button>
349 </div>
350 </form>
351
352 <?php if (current_user_can('manage_options')): ?>
353 <div style="text-align: center; margin-top: 20px;">
354 <a href="#" id="metasync-forgot-password-link" style="color: #2271b1; text-decoration: none; font-size: 14px;">
355 Forgot Password?
356 </a>
357 <div id="metasync-recovery-message" style="margin-top: 15px; padding: 12px; border-radius: 6px; display: none;"></div>
358 </div>
359 <?php endif; ?>
360
361 </div>
362
363 <?php
364 return;
365 } elseif (!$password_protection_enabled) {
366 ?>
367 <div class="notice notice-info" style="margin: 15px 0;">
368 <p>
369 <strong>Security Tip:</strong> You can set a custom password in the White Label Settings to protect this section.
370 </p>
371 </div>
372 <?php
373 }
374 }
375 ?>
376
377 <form method="post" action="options.php?tab=<?php echo esc_attr( $active_tab ); ?>" id="metaSyncGeneralSetting">
378 <input type="hidden" name="metasync_pw_reset_generation" value="<?php echo esc_attr((string) (int) get_option('metasync_pw_reset_generation', 0)); ?>" />
379 <?php
380 settings_fields(Metasync_Admin::option_group);
381
382 wp_nonce_field('meta_sync_general_setting_nonce', 'meta_sync_nonce');
383
384 if ($active_tab == 'general') {
385 ?>
386 <div class="dashboard-card">
387 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
388 <div style="flex: 1;">
389 <h2 style="margin: 0;">General Configuration</h2>
390 <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>
391 </div>
392 <?php if ($hide_settings_enabled && !empty($user_password) && $is_authenticated): ?>
393 <div>
394 <?php Metasync_Compatibility_Checker::instance()->render_lock_button('general'); ?>
395 </div>
396 <?php endif; ?>
397 </div>
398
399 <?php if ($hide_settings_enabled && !empty($user_password) && $is_authenticated): ?>
400 <div style="background: #d4edda; border: 1px solid #c3e6cb; color: #155724; padding: 12px; border-radius: 4px; margin-bottom: 20px;">
401 <strong>�
402 Access Granted:</strong> You have successfully authenticated and can now modify settings.
403 </div>
404 <?php endif; ?>
405
406 <?php
407 $this->admin->render_accordion_sections(Metasync_Admin::$page_slug . '_general');
408 ?>
409 <p style="margin: 20px 0 0 0; font-size: 13px; color: var(--dashboard-text-secondary);">
410 Breadcrumb settings now live on their own page: <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-breadcrumbs')); ?>">Breadcrumbs</a>.
411 </p>
412 </div>
413
414 <div class="dashboard-card">
415 <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>
416 <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>
417 <button type="button" class="button button-primary" id="sendAuthToken">
418 Sync Now
419 </button>
420 </div>
421
422 <div class="dashboard-card">
423 <h2>Setup Wizard</h2>
424 <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>
425 <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;">
426 Start Setup Wizard
427 </a>
428 </div>
429 <?php
430 } elseif ($active_tab == 'whitelabel') {
431 $whitelabel_settings = Metasync::get_whitelabel_settings();
432 $user_password = $whitelabel_settings['settings_password'] ?? '';
433 $password_protection_enabled = !empty($user_password);
434 ?>
435 <div class="dashboard-card">
436 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
437 <div>
438 <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>
439 <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Customize the plugin appearance with your own branding and logo.</p>
440 </div>
441 <?php if ($password_protection_enabled && $is_authenticated): ?>
442 <div>
443 <?php Metasync_Compatibility_Checker::instance()->render_lock_button('whitelabel'); ?>
444 <?php Metasync::render_tooltip_icon('wl_lock_section', 'Re-lock this section now. You will need the password to open it again.'); ?>
445 </div>
446 <?php endif; ?>
447 </div>
448
449 <?php if ($password_protection_enabled && $is_authenticated): ?>
450 <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;">
451 <div>
452 <strong>�
453 Access Granted:</strong> You have successfully authenticated and can now modify white label settings.
454 </div>
455 </div>
456
457 <?php endif; ?>
458
459 <?php
460 do_settings_sections(Metasync_Admin::$page_slug . '_branding');
461 ?>
462 </div>
463
464 <!-- Color Palette Customization section -->
465 <div class="dashboard-card" id="metasync-color-palette-section">
466 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
467 <div>
468 <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>
469 <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>
470 </div>
471 </div>
472
473 <?php
474 $color_palette = isset($whitelabel_settings['color_palette']) && is_array($whitelabel_settings['color_palette'])
475 ? $whitelabel_settings['color_palette']
476 : array();
477 $dark_colors = isset($color_palette['dark']) && is_array($color_palette['dark']) ? $color_palette['dark'] : array();
478 $light_colors = isset($color_palette['light']) && is_array($color_palette['light']) ? $color_palette['light'] : array();
479
480 $palette_fields = array(
481 'dashboard-bg' => array(
482 'label' => 'Background',
483 'hint' => 'Main page background behind all content',
484 'dark' => '#0f1419',
485 'light' => '#f8f9fa',
486 ),
487 'dashboard-card-bg' => array(
488 'label' => 'Card Background',
489 'hint' => 'Background of cards, panels, and content boxes',
490 'dark' => '#1a1f26',
491 'light' => '#ffffff',
492 ),
493 'dashboard-card-hover' => array(
494 'label' => 'Card Hover',
495 'hint' => 'Card background when you hover over it',
496 'dark' => '#222831',
497 'light' => '#f1f3f5',
498 ),
499 'dashboard-text-primary' => array(
500 'label' => 'Primary Text',
501 'hint' => 'Headings, body text, and main content',
502 'dark' => '#ffffff',
503 'light' => '#1a1f26',
504 ),
505 'dashboard-text-secondary' => array(
506 'label' => 'Secondary Text',
507 'hint' => 'Descriptions, hints, and less important text',
508 'dark' => '#9ca3af',
509 'light' => '#6b7280',
510 ),
511 'dashboard-accent' => array(
512 'label' => 'Accent Color',
513 'hint' => 'Buttons, links, and highlighted elements',
514 'dark' => '#3b82f6',
515 'light' => '#3b82f6',
516 ),
517 'dashboard-accent-hover' => array(
518 'label' => 'Accent Hover',
519 'hint' => 'Buttons and links when hovered',
520 'dark' => '#2563eb',
521 'light' => '#2563eb',
522 ),
523 'dashboard-success' => array(
524 'label' => 'Success',
525 'hint' => 'Success messages, connected status, positive indicators',
526 'dark' => '#10b981',
527 'light' => '#10b981',
528 ),
529 'dashboard-warning' => array(
530 'label' => 'Warning',
531 'hint' => 'Warning alerts and caution indicators',
532 'dark' => '#f59e0b',
533 'light' => '#f59e0b',
534 ),
535 'dashboard-error' => array(
536 'label' => 'Error',
537 'hint' => 'Error messages and critical alerts',
538 'dark' => '#ef4444',
539 'light' => '#ef4444',
540 ),
541 'dashboard-border' => array(
542 'label' => 'Border',
543 'hint' => 'Card borders, dividers, and separators',
544 'dark' => '#374151',
545 'light' => '#e5e7eb',
546 ),
547 'dashboard-gradient-primary-from' => array(
548 'label' => 'Button Gradient Start',
549 'hint' => 'Left/top color of buttons, save bars, and active tabs',
550 'dark' => '#667eea',
551 'light' => '#667eea',
552 ),
553 'dashboard-gradient-primary-to' => array(
554 'label' => 'Button Gradient End',
555 'hint' => 'Right/bottom color of buttons, save bars, and active tabs',
556 'dark' => '#764ba2',
557 'light' => '#764ba2',
558 ),
559 'dashboard-gradient-accent-from' => array(
560 'label' => 'Accent Gradient Start',
561 'hint' => 'Left/top color of secondary badges and highlight bars',
562 'dark' => '#f093fb',
563 'light' => '#f093fb',
564 ),
565 'dashboard-gradient-accent-to' => array(
566 'label' => 'Accent Gradient End',
567 'hint' => 'Right/bottom color of secondary badges and highlight bars',
568 'dark' => '#f5576c',
569 'light' => '#f5576c',
570 ),
571 );
572 ?>
573
574 <!-- Theme tabs -->
575 <div style="display: flex; gap: 8px; margin-bottom: 20px;">
576 <button type="button" class="metasync-palette-tab active" data-palette="dark"
577 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;">
578 &#9790; Dark Theme
579 </button>
580 <button type="button" class="metasync-palette-tab" data-palette="light"
581 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;">
582 &#9728; Light Theme
583 </button>
584 </div>
585
586 <!-- Dark theme palette -->
587 <div class="metasync-palette-panel active" id="metasync-palette-dark">
588 <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;">
589 <?php foreach ($palette_fields as $var_name => $field): ?>
590 <div style="background: var(--dashboard-card-hover); border: 1px solid var(--dashboard-border); border-radius: 10px; padding: 14px;">
591 <label style="display: block; font-weight: 600; font-size: 13px; color: var(--dashboard-text-primary); margin-bottom: 2px;">
592 <?php echo esc_html($field['label']); ?>
593 </label>
594 <p style="font-size: 12px; color: var(--dashboard-text-secondary); margin: 0 0 10px 0;"><?php echo esc_html($field['hint']); ?></p>
595 <div style="display: flex; align-items: center; gap: 10px;">
596 <input type="color"
597 class="metasync-color-field"
598 name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][color_palette][dark][<?php echo esc_attr($var_name); ?>]"
599 value="<?php echo esc_attr(!empty($dark_colors[$var_name]) ? $dark_colors[$var_name] : $field['dark']); ?>"
600 data-default-color="<?php echo esc_attr($field['dark']); ?>"
601 data-css-var="<?php echo esc_attr($var_name); ?>"
602 data-color-theme="dark"
603 style="width: 44px; height: 36px; padding: 2px; border: 1px solid var(--dashboard-border); border-radius: 8px; cursor: pointer; background: transparent;"
604 />
605 <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>
606 </div>
607 </div>
608 <?php endforeach; ?>
609 </div>
610 <div style="margin-top: 16px; text-align: right;">
611 <button type="button" id="metasync-reset-dark-palette" data-reset-theme="dark"
612 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;">
613 Reset Dark Theme to Defaults
614 </button>
615 </div>
616 </div>
617
618 <!-- Light theme palette -->
619 <div class="metasync-palette-panel" id="metasync-palette-light">
620 <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 16px;">
621 <?php foreach ($palette_fields as $var_name => $field): ?>
622 <div style="background: var(--dashboard-card-hover); border: 1px solid var(--dashboard-border); border-radius: 10px; padding: 14px;">
623 <label style="display: block; font-weight: 600; font-size: 13px; color: var(--dashboard-text-primary); margin-bottom: 2px;">
624 <?php echo esc_html($field['label']); ?>
625 </label>
626 <p style="font-size: 12px; color: var(--dashboard-text-secondary); margin: 0 0 10px 0;"><?php echo esc_html($field['hint']); ?></p>
627 <div style="display: flex; align-items: center; gap: 10px;">
628 <input type="color"
629 class="metasync-color-field"
630 name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][color_palette][light][<?php echo esc_attr($var_name); ?>]"
631 value="<?php echo esc_attr(!empty($light_colors[$var_name]) ? $light_colors[$var_name] : $field['light']); ?>"
632 data-default-color="<?php echo esc_attr($field['light']); ?>"
633 data-css-var="<?php echo esc_attr($var_name); ?>"
634 data-color-theme="light"
635 style="width: 44px; height: 36px; padding: 2px; border: 1px solid var(--dashboard-border); border-radius: 8px; cursor: pointer; background: transparent;"
636 />
637 <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>
638 </div>
639 </div>
640 <?php endforeach; ?>
641 </div>
642 <div style="margin-top: 16px; text-align: right;">
643 <button type="button" id="metasync-reset-light-palette" data-reset-theme="light"
644 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;">
645 Reset Light Theme to Defaults
646 </button>
647 </div>
648 </div>
649 </div>
650
651 <style>
652 .metasync-palette-tab.active {
653 background: var(--dashboard-gradient-primary) !important;
654 color: #fff !important;
655 }
656 .metasync-palette-tab:not(.active) {
657 background: transparent !important;
658 color: var(--dashboard-text-secondary) !important;
659 }
660 .metasync-palette-panel { display: none !important; }
661 .metasync-palette-panel.active { display: block !important; }
662 </style>
663
664 <script>
665 jQuery(function($) {
666 // Tab switching
667 $(document).on('click', '.metasync-palette-tab', function(e) {
668 e.preventDefault();
669 e.stopPropagation();
670 var target = $(this).data('palette');
671 $('.metasync-palette-tab').removeClass('active');
672 $(this).addClass('active');
673 $('.metasync-palette-panel').removeClass('active');
674 $('#metasync-palette-' + target).addClass('active');
675 });
676
677 // Reset to defaults
678 var paletteDefaults = <?php echo wp_json_encode(array(
679 'dark' => array_combine(
680 array_keys($palette_fields),
681 array_column($palette_fields, 'dark')
682 ),
683 'light' => array_combine(
684 array_keys($palette_fields),
685 array_column($palette_fields, 'light')
686 )
687 )); ?>;
688
689 $(document).on('click', '#metasync-reset-dark-palette, #metasync-reset-light-palette', function(e) {
690 e.preventDefault();
691 e.stopPropagation();
692 var theme = $(this).data('reset-theme');
693 var defs = paletteDefaults[theme];
694 if (!defs) return;
695 $('#metasync-color-palette-section .metasync-color-field[data-color-theme="' + theme + '"]').each(function() {
696 var $input = $(this);
697 var varName = $input.data('css-var');
698 var defVal = defs[varName] || '';
699 if (defVal) {
700 $input.val(defVal);
701 $input.siblings('.metasync-color-hex-display').text(defVal);
702 }
703 });
704 // Trigger live preview if external JS is loaded
705 $input = $('#metasync-color-palette-section .metasync-color-field').first();
706 if ($input.length) $input.trigger('input');
707 });
708 });
709 </script>
710
711 <!-- Export Whitelabel Settings section -->
712 <div class="dashboard-card">
713 <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>
714 <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>
715 <button type="button" class="button button-primary" id="metasync-export-whitelabel-btn" style="padding: 10px 20px; font-size: 14px; font-weight: 600;">
716 Export Plugin with Whitelabel Settings
717 </button>
718 <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>
719 </div>
720
721 <!-- Advanced Access Control section -->
722 <?php Metasync_Access_Control_UI::render_access_control_table(); ?>
723
724 <!-- Custom Modal for Alerts -->
725 <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);">
726 <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;">
727 <div style="background: white; border-radius: 10px; padding: 0; overflow: hidden;">
728 <!-- Header -->
729 <div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 24px; text-align: center;">
730 <div id="modal-icon" style="font-size: 48px; margin-bottom: 12px; color: #fff;">&#9888;</div>
731 <h2 id="modal-title" style="color: white; margin: 0; font-size: 24px; font-weight: 600;">Password Required</h2>
732 </div>
733
734 <!-- Body -->
735 <div style="padding: 32px 24px;">
736 <p id="modal-message" style="color: #4a5568; font-size: 15px; line-height: 1.6; text-align: center; margin: 0;">
737 You must set a White Label Settings Password before enabling "Hide Settings".
738 </p>
739 </div>
740
741 <!-- Footer -->
742 <div style="padding: 0 24px 24px; text-align: center;">
743 <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;">
744 Got It
745 </button>
746 </div>
747 </div>
748 </div>
749 </div>
750
751 <style>
752 @keyframes modalSlideIn {
753 from {
754 opacity: 0;
755 transform: translateY(-50px);
756 }
757 to {
758 opacity: 1;
759 transform: translateY(0);
760 }
761 }
762
763 #modal-close-btn:hover {
764 transform: translateY(-2px);
765 box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5);
766 }
767 </style>
768
769 <script>
770 jQuery(document).ready(function($) {
771 var isShowingModal = false;
772
773 function showModal(icon, title, message) {
774 isShowingModal = true;
775 $('#modal-icon').text(icon);
776 $('#modal-title').text(title);
777 $('#modal-message').html(message);
778 $('#metasync-custom-modal').fadeIn(200);
779
780 setTimeout(function() {
781 isShowingModal = false;
782 }, 300);
783 }
784
785 $('#modal-close-btn').on('click', function(e) {
786 e.preventDefault();
787 e.stopPropagation();
788 $('#metasync-custom-modal').fadeOut(200);
789 return false;
790 });
791
792 $('#metasync-custom-modal').on('click', function(e) {
793 if (e.target === this) {
794 $(this).fadeOut(200);
795 }
796 });
797
798 function hasPassword() {
799 var passwordField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][settings_password]"]');
800 var passwordValue = passwordField.val();
801 return passwordValue && passwordValue.length > 0;
802 }
803
804 function hasRecoveryEmail() {
805 var recoveryEmailField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][recovery_email]"]');
806 var emailValue = recoveryEmailField.val();
807 return emailValue && emailValue.length > 0 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailValue);
808 }
809
810 var passwordField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][settings_password]"]');
811 var recoveryEmailField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][recovery_email]"]');
812
813 passwordField.on('blur', function() {
814 if (hasPassword() && !hasRecoveryEmail()) {
815 showModal(
816 '📧',
817 'Recovery Email Required',
818 '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.'
819 );
820 recoveryEmailField.focus();
821 }
822 });
823
824 passwordField.on('input', function() {
825 if (hasPassword()) {
826 recoveryEmailField.attr('required', true);
827 recoveryEmailField.closest('tr').find('th').addClass('required-field');
828 } else {
829 recoveryEmailField.removeAttr('required');
830 recoveryEmailField.closest('tr').find('th').removeClass('required-field');
831 }
832 });
833
834 passwordField.trigger('input');
835
836 var hideSettingsCheckbox = $('#checkbox_hide_settings');
837 var originalCheckboxState = hideSettingsCheckbox.is(':checked');
838
839 hideSettingsCheckbox.on('click', function(e) {
840 if (!originalCheckboxState && !hasPassword()) {
841 e.preventDefault();
842 e.stopImmediatePropagation();
843
844 showModal(
845 '&#9888;',
846 'Password Required',
847 '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.'
848 );
849
850 setTimeout(function() {
851 $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][settings_password]"]').focus();
852 }, 300);
853
854 return false;
855 }
856
857 setTimeout(function() {
858 originalCheckboxState = hideSettingsCheckbox.is(':checked');
859 }, 0);
860 });
861
862 passwordField.on('keydown', function(e) {
863 var currentValue = $(this).val();
864
865 if (hideSettingsCheckbox.is(':checked') &&
866 (currentValue.length <= 1 || !currentValue) &&
867 (e.keyCode === 8 || e.keyCode === 46)) {
868
869 e.preventDefault();
870 e.stopImmediatePropagation();
871
872 if (!isShowingModal) {
873 showModal(
874 '&#9888;',
875 'Cannot Remove Password',
876 '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.'
877 );
878 }
879
880 return false;
881 }
882 });
883
884 $(document).on('metasync_settings_saved', function() {
885 });
886
887 $(document).on('keydown', function(e) {
888 if (e.key === 'Escape') {
889 $('#metasync-custom-modal').fadeOut(200);
890 }
891 });
892
893 });
894 </script>
895 <?php
896 } elseif ($active_tab == 'advanced') {
897 if (!Metasync_Access_Control::user_can_access('hide_advanced')) {
898 echo '<div class="dashboard-card" style="background: var(--dashboard-card-bg); padding: 25px; border-radius: 8px; text-align: center;">';
899 echo '<h2 style="color: var(--dashboard-text-primary);">Access Denied</h2>';
900 echo '<p style="color: var(--dashboard-text-secondary);">You do not have permission to access this page.</p>';
901 echo '</div>';
902 } else {
903 ?>
904 <div class="dashboard-card">
905 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
906 <div style="flex: 1;">
907 <h2 style="margin: 0;">🧰 Advanced Settings</h2>
908 <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Technical utilities for troubleshooting and connectivity checks.</p>
909 </div>
910 <?php if ($hide_settings_enabled && !empty($user_password) && $is_authenticated): ?>
911 <div>
912 <?php Metasync_Compatibility_Checker::instance()->render_lock_button('advanced'); ?>
913 </div>
914 <?php endif; ?>
915 </div>
916
917 <?php if ($hide_settings_enabled && !empty($user_password) && $is_authenticated): ?>
918 <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;">
919 <strong>�
920 Access Granted:</strong> You have successfully authenticated and can now modify settings.
921 </div>
922 <?php endif; ?>
923
924 <?php
925 if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice
926 $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice
927 ?>
928 <div class="notice notice-success inline" style="margin-bottom: 20px;">
929 <p>
930 <strong>�
931 Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>.
932 </p>
933 </div>
934 <?php endif; ?>
935
936 <?php if (isset($_GET['debug_mode_disabled']) && $_GET['debug_mode_disabled'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
937 <div class="notice notice-info inline" style="margin-bottom: 20px;">
938 <p>
939 <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled.
940 </p>
941 </div>
942 <?php endif; ?>
943
944 <?php if (isset($_GET['debug_mode_extended']) && $_GET['debug_mode_extended'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
945 <div class="notice notice-success inline" style="margin-bottom: 20px;">
946 <p>
947 <strong>�
948 Extended!</strong> Debug mode has been extended for another 24 hours.
949 </p>
950 </div>
951 <?php endif; ?>
952
953 <?php if (isset($_GET['debug_error']) && $_GET['debug_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
954 <div class="notice notice-error inline" style="margin-bottom: 20px;">
955 <p>
956 <strong>❌ Error:</strong> Unable to perform the debug mode operation. Please try again.
957 </p>
958 </div>
959 <?php endif; ?>
960
961 <?php
962 $this->admin->render_advanced_accordion();
963 ?>
964 </div>
965
966 <?php
967 }
968 }
969 ?>
970
971 <!-- Save button removed - using floating notification system instead -->
972
973 </form>
974
975 <!-- Lock Section Button Handler (runs on all tabs) -->
976 <script>
977 jQuery(document).ready(function($) {
978 var ajaxUrl = (typeof window.ajaxurl !== 'undefined' && window.ajaxurl)
979 ? window.ajaxurl
980 : '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
981 $('.metasync-lock-btn').on('click', function(e) {
982 e.preventDefault();
983 e.stopPropagation();
984
985 var currentTab = $(this).data('tab');
986
987 var logoutForm = $('<form>', {
988 'method': 'post',
989 'action': ''
990 });
991
992 logoutForm.append('<?php echo wp_nonce_field("whitelabel_logout_nonce", "whitelabel_logout_nonce", true, false); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- nonce markup from wp_nonce_field() is safe by construction; kses-based escaping strips the <input> and breaks the logout nonce ?>');
993
994 logoutForm.append($('<input>', {
995 'type': 'hidden',
996 'name': 'whitelabel_logout',
997 'value': '1'
998 }));
999
1000 $('body').append(logoutForm);
1001 logoutForm.submit();
1002
1003 return false;
1004 });
1005 });
1006 </script>
1007
1008 <!-- Host Blocking-generated JavaScript -->
1009 <script>
1010 jQuery(document).ready(function($) {
1011 var ajaxUrl = (typeof window.ajaxurl !== 'undefined' && window.ajaxurl)
1012 ? window.ajaxurl
1013 : '<?php echo esc_js(admin_url('admin-ajax.php')); ?>';
1014
1015 $(document).on('click', '#test-get-request', function(e) {
1016 e.preventDefault();
1017 e.stopPropagation();
1018 runHostTest('GET');
1019 return false;
1020 });
1021
1022 $(document).on('click', '#test-post-request', function(e) {
1023 e.preventDefault();
1024 e.stopPropagation();
1025 runHostTest('POST');
1026 return false;
1027 });
1028
1029 $(document).on('click', '#test-both-requests', function(e) {
1030 e.preventDefault();
1031 e.stopPropagation();
1032 runHostTest('BOTH');
1033 return false;
1034 });
1035
1036 function runHostTest(method) {
1037 var buttonId = (method === 'BOTH' ? 'test-both-requests' : 'test-' + method.toLowerCase() + '-request');
1038 var $button = $('#' + buttonId);
1039
1040 if ($button.length === 0) {
1041 alert('Error: Test button not found. Please refresh the page.');
1042 return;
1043 }
1044
1045 var originalText = $button.text();
1046
1047 $button.prop('disabled', true);
1048 $button.text('Testing...');
1049
1050 var $resultsDiv = $('#host-test-results');
1051 var $resultsContent = $('#test-results-content');
1052 $resultsDiv.show();
1053 $resultsContent.html('<div class="notice notice-info"><p>Running ' + (method === 'BOTH' ? 'GET and POST' : method) + ' test(s)...</p></div>');
1054
1055 var testsToRun = (method === 'BOTH') ? ['GET', 'POST'] : [method];
1056 var completedTests = 0;
1057 var allResults = [];
1058
1059 testsToRun.forEach(function(testMethod) {
1060 var action = 'metasync_test_host_blocking_' + testMethod.toLowerCase();
1061
1062 $.ajax({
1063 url: ajaxUrl,
1064 type: 'POST',
1065 dataType: 'json',
1066 data: { action: action, nonce: '<?php echo esc_attr(wp_create_nonce('metasync_nonce')); ?>' },
1067 timeout: 35000,
1068 success: function(response, textStatus, xhr) {
1069 try {
1070 if (response && response.success && response.data) {
1071 allResults.push(response.data);
1072 } else {
1073 allResults.push({
1074 method: testMethod,
1075 status: 'error',
1076 error: (response && response.data) ? response.data : 'Unexpected response',
1077 blocked: true,
1078 details: 'Received non-success response from server.'
1079 });
1080 }
1081 } catch (e) {
1082 allResults.push({
1083 method: testMethod,
1084 status: 'error',
1085 error: 'Response parse error: ' + (e && e.message ? e.message : e),
1086 blocked: true
1087 });
1088 }
1089 finalizeOne();
1090 },
1091 error: function(xhr, status, error) {
1092 var payload = (xhr && xhr.responseText) ? xhr.responseText.substring(0, 500) : '';
1093 allResults.push({
1094 method: testMethod,
1095 status: 'error',
1096 error: 'AJAX failed: ' + error + (payload ? ' — ' + payload : ''),
1097 blocked: true,
1098 details: 'Request did not complete successfully. Status: ' + status
1099 });
1100 finalizeOne();
1101 }
1102 });
1103 });
1104
1105 function finalizeOne() {
1106 completedTests++;
1107 if (completedTests === testsToRun.length) {
1108 displayResults(allResults);
1109 resetButtons();
1110 var $container = $('#host-test-results');
1111 if ($container && $container[0] && $container[0].scrollIntoView) {
1112 $container[0].scrollIntoView({ behavior: 'smooth', block: 'start' });
1113 }
1114 }
1115 }
1116 }
1117
1118 function resetButtons() {
1119 $('#test-get-request, #test-post-request, #test-both-requests').prop('disabled', false);
1120 $('#test-get-request').text('Test GET Request');
1121 $('#test-post-request').text('Test POST Request');
1122 $('#test-both-requests').text('Test Both Requests');
1123 }
1124
1125 function displayResults(results) {
1126 var html = '';
1127
1128 results.forEach(function(result) {
1129 var statusClass = result.status === 'success' ? 'success' : 'error';
1130 var statusIcon = result.status === 'success' ? '�
1131 ' : '❌';
1132 var blockedStatus = result.blocked ? 'BLOCKED' : 'ALLOWED';
1133 var blockedClass = result.blocked ? 'blocked' : 'allowed';
1134
1135 html += '<div class="test-result-item ' + statusClass + '">';
1136 html += '<div class="test-result-header">';
1137 html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
1138 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
1139 html += '</div>';
1140
1141 html += '<div class="test-result-details">';
1142 html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>';
1143 html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>';
1144
1145 if (result.status_code) {
1146 html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + escapeHtml(result.status_code) + '</span></p>';
1147 }
1148
1149 if (result.error) {
1150 html += '<p><strong>Error:</strong> <span class="error-message">' + escapeHtml(result.error) + '</span></p>';
1151 }
1152
1153 if (result.body) {
1154 html += '<p><strong>Response Body:</strong></p>';
1155 html += '<pre class="response-body">' + escapeHtml(result.body) + '</pre>';
1156 }
1157
1158 if (result.headers && Object.keys(result.headers).length > 0) {
1159 html += '<p><strong>Response Headers:</strong></p>';
1160 html += '<pre class="response-headers">';
1161 Object.keys(result.headers).forEach(function(key) {
1162 html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
1163 });
1164 html += '</pre>';
1165 }
1166
1167 if (result.sent_data) {
1168 html += '<p><strong>Sent Data:</strong></p>';
1169 html += '<pre class="sent-data">' + escapeHtml(JSON.stringify(result.sent_data, null, 2)) + '</pre>';
1170 }
1171
1172 if (result.parsed_response) {
1173 html += '<p><strong>Parsed Response:</strong></p>';
1174 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
1175 }
1176
1177 html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
1178 html += '</div>';
1179 html += '</div>';
1180 });
1181
1182 $('#test-results-content').html(html);
1183 $('#host-test-results').show();
1184 }
1185
1186 function escapeHtml(text) {
1187 var map = {
1188 '&': '&amp;',
1189 '<': '&lt;',
1190 '>': '&gt;',
1191 '"': '&quot;',
1192 '\'': '&#039;'
1193 };
1194 return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
1195 }
1196 });
1197 </script>
1198
1199 <!-- Host Blocking Test CSS -->
1200 <style>
1201 .test-result-item {
1202 background: #fff;
1203 border: 1px solid #ddd;
1204 border-radius: 8px;
1205 margin-bottom: 20px;
1206 overflow: hidden;
1207 box-shadow: 0 2px 4px rgba(0,0,0,0.1);
1208 }
1209
1210 .test-result-item.success {
1211 border-left: 4px solid #28a745;
1212 }
1213
1214 .test-result-item.error {
1215 border-left: 4px solid #dc3545;
1216 }
1217
1218 .test-result-header {
1219 background: #f8f9fa;
1220 padding: 15px 20px;
1221 border-bottom: 1px solid #dee2e6;
1222 display: flex;
1223 justify-content: space-between;
1224 align-items: center;
1225 }
1226
1227 .test-result-header h4 {
1228 margin: 0;
1229 color: #495057;
1230 font-size: 16px;
1231 }
1232
1233 .test-status {
1234 padding: 4px 12px;
1235 border-radius: 20px;
1236 font-size: 12px;
1237 font-weight: bold;
1238 text-transform: uppercase;
1239 }
1240
1241 .test-status.allowed {
1242 background: #d4edda;
1243 color: #155724;
1244 }
1245
1246 .test-status.blocked {
1247 background: #f8d7da;
1248 color: #721c24;
1249 }
1250
1251 .test-result-details {
1252 padding: 20px;
1253 }
1254
1255 .test-result-details p {
1256 margin: 8px 0;
1257 color: #495057;
1258 }
1259
1260 .test-result-details code {
1261 background: #f8f9fa;
1262 padding: 2px 6px;
1263 border-radius: 3px;
1264 font-family: 'Courier New', monospace;
1265 color: #e83e8c;
1266 }
1267
1268 .status-code {
1269 font-weight: bold;
1270 color: #007cba;
1271 }
1272
1273 .error-message {
1274 color: #dc3545;
1275 font-weight: bold;
1276 }
1277
1278 .response-body, .response-headers, .sent-data, .parsed-response {
1279 background: #f8f9fa;
1280 border: 1px solid #dee2e6;
1281 border-radius: 4px;
1282 padding: 12px;
1283 margin: 8px 0;
1284 font-family: 'Courier New', monospace;
1285 font-size: 12px;
1286 line-height: 1.4;
1287 max-height: 200px;
1288 overflow-y: auto;
1289 white-space: pre-wrap;
1290 word-break: break-all;
1291 }
1292
1293 .response-body {
1294 color: #495057;
1295 }
1296
1297 .response-headers {
1298 color: #6c757d;
1299 }
1300
1301 .sent-data {
1302 color: #007cba;
1303 }
1304
1305 .parsed-response {
1306 color: #28a745;
1307 background: #f8fff9;
1308 border-color: #c3e6cb;
1309 }
1310
1311 #host-test-results {
1312 margin-top: 20px;
1313 }
1314
1315 #host-test-results h3 {
1316 color: #495057;
1317 margin-bottom: 15px;
1318 padding-bottom: 10px;
1319 border-bottom: 2px solid #dee2e6;
1320 }
1321
1322 .button:disabled {
1323 opacity: 0.6;
1324 cursor: not-allowed;
1325 }
1326 </style>
1327
1328 <?php if ($active_tab === 'advanced'): ?>
1329 <?php if (isset($_GET['settings_cleared']) && $_GET['settings_cleared'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1330 <div class="notice notice-success is-dismissible">
1331 <p><strong>�
1332 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>
1333 </div>
1334 <?php endif; ?>
1335
1336 <?php if (isset($_GET['clear_settings_error']) && $_GET['clear_settings_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1337 <div class="notice notice-error is-dismissible">
1338 <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p>
1339 </div>
1340 <?php endif; ?>
1341
1342 <?php if (isset($_GET['log_cleared']) && $_GET['log_cleared'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1343 <div class="notice notice-success is-dismissible">
1344 <p><strong>�
1345 Success!</strong> Error logs have been cleared successfully.</p>
1346 </div>
1347 <?php endif; ?>
1348
1349 <?php if (isset($_GET['clear_error']) && $_GET['clear_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1350 <div class="notice notice-error is-dismissible">
1351 <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p>
1352 </div>
1353 <?php endif; ?>
1354
1355 <?php if (isset($_GET['access_roles_saved']) && $_GET['access_roles_saved'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1356 <div class="notice notice-success is-dismissible">
1357 <p><strong>�
1358 Success!</strong> Plugin access roles have been saved successfully.</p>
1359 </div>
1360 <?php endif; ?>
1361
1362 <?php if (isset($_GET['access_roles_error']) && $_GET['access_roles_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1363 <div class="notice notice-error is-dismissible">
1364 <p><strong>❌ Error!</strong> Failed to save plugin access roles. <?php echo isset($_GET['message']) ? esc_html(urldecode($_GET['message'])) : 'Please try again.'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only error text, escaped on output ?></p>
1365 </div>
1366 <?php endif; ?>
1367
1368 <?php endif; ?>
1369 <?php $this->admin->render_layout_close(); ?>
1370 <?php
1371 }
1372
1373 // ------------------------------------------------------------------
1374 // Simple page renderers
1375 // ------------------------------------------------------------------
1376
1377 public function create_admin_dashboard_page()
1378 {
1379 ?>
1380 <?php $this->admin->render_layout_open('Dashboard', 'dashboard', ''); ?>
1381
1382 <div class="dashboard-card">
1383 <h2><?php echo esc_html($this->admin->get_effective_menu_title()); ?> Dashboard</h2>
1384 <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>
1385 <?php
1386 if (!isset(Metasync::get_option('general')['linkgraph_token']) || Metasync::get_option('general')['linkgraph_token'] == '') {
1387 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>';
1388 echo '<a href="' . esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug)) . '" class="button button-secondary">Go to Settings</a>';
1389 } else {
1390 echo '<a href="' . esc_url($this->admin->get_dashboard_url()) . '" target="_blank" class="button button-primary">Open Dashboard</a>';
1391 }
1392 ?>
1393 </div>
1394 <?php $this->admin->render_layout_close(); ?>
1395 <?php
1396 }
1397
1398 public function create_admin_robots_txt_page()
1399 {
1400 // The form below rewrites the site's physical robots.txt — restrict
1401 // that to site admins, not just anyone who can open the page via
1402 // plugin_access_roles. The nonce is CSRF protection, not authorization.
1403 if (!empty($_POST) && !current_user_can('manage_options')) {
1404 wp_die(esc_html__('Sorry, you are not allowed to manage robots.txt settings.', 'metasync'));
1405 }
1406
1407 ?>
1408 <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?>
1409
1410 <?php
1411 require_once plugin_dir_path(dirname(__FILE__)) . 'robots-txt/class-metasync-robots-txt.php';
1412
1413 $robots_txt = Metasync_Robots_Txt::get_instance();
1414
1415 if (isset($_POST['metasync_robots_txt_nonce'])) {
1416 check_admin_referer('metasync_save_robots_txt', 'metasync_robots_txt_nonce');
1417
1418 if (isset($_POST['robots_content'])) {
1419 $content = wp_unslash($_POST['robots_content']);
1420
1421 $validation = $robots_txt->validate_content($content);
1422
1423 if ($validation['valid']) {
1424 $result = $robots_txt->write_robots_file($content);
1425
1426 if (is_wp_error($result)) {
1427 echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>';
1428 } else {
1429 Metasync_PostHog::feature('robots_txt_saved', array(
1430 'warning_count' => count($validation['warnings']),
1431 ));
1432 echo '<div class="notice notice-success"><p>' . esc_html__('robots.txt file saved successfully!', 'metasync') . '</p></div>';
1433
1434 if (!empty($validation['warnings'])) {
1435 foreach ($validation['warnings'] as $warning) {
1436 echo '<div class="notice notice-warning"><p>' . esc_html($warning) . '</p></div>';
1437 }
1438 }
1439 }
1440 } else {
1441 echo '<div class="notice notice-error"><p>' . esc_html__('Validation failed. Please fix the errors below:', 'metasync') . '</p>';
1442 foreach ($validation['errors'] as $error) {
1443 echo '<p>- ' . esc_html($error) . '</p>';
1444 }
1445 echo '</div>';
1446 }
1447 }
1448 }
1449
1450 $current_content = $robots_txt->read_robots_file();
1451 if (is_wp_error($current_content)) {
1452 echo '<div class="notice notice-error"><p>' . esc_html($current_content->get_error_message()) . '</p></div>';
1453 $current_content = '';
1454 }
1455
1456 // First page of backups; the Backup History panel is paginated so the
1457 // list always reflects the full stored state and its true total.
1458 $backups = $robots_txt->get_backup_history(Metasync_Robots_Txt_Database::BACKUPS_PER_PAGE, 0);
1459
1460 $file_exists = $robots_txt->file_exists();
1461 $is_writable = $robots_txt->is_writable();
1462
1463 $robots_txt->render($this->admin, $current_content, $backups, $file_exists, $is_writable);
1464 ?>
1465 <?php $this->admin->render_layout_close(); ?>
1466 <?php
1467 }
1468
1469 public function create_admin_report_issue_page()
1470 {
1471 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1472 }
1473
1474 public function create_admin_custom_pages_page()
1475 {
1476 ?>
1477 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
1478
1479 <div class="metasync-page-content">
1480 <?php
1481 require_once plugin_dir_path(dirname(__FILE__)) . 'custom-pages/class-metasync-custom-pages-admin.php';
1482 Metasync_Custom_Pages_Admin::render_admin_page();
1483 ?>
1484 </div>
1485
1486 <?php $this->admin->render_layout_close(); ?>
1487 <?php
1488 }
1489
1490 public function create_admin_404_monitor_page()
1491 {
1492 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor-database.php';
1493 require_once plugin_dir_path(dirname(__FILE__)) . '404-monitor/class-metasync-404-monitor.php';
1494
1495 $db_404 = new Metasync_Error_Monitor_Database();
1496 $ErrorMonitor = new Metasync_Error_Monitor($db_404);
1497
1498 $this->admin->render_layout_open('404 Monitor', 'monitor_404', 'Track and manage 404 errors detected on your site.');
1499 $ErrorMonitor->create_admin_plugin_interface();
1500 $this->admin->render_layout_close();
1501 }
1502
1503 public function create_admin_search_engine_verification_page()
1504 {
1505 $page_slug = Metasync_Admin::$page_slug . '_searchengines-verification';
1506 ?>
1507 <?php $this->admin->render_layout_open('Site Verification', 'site_verification', 'Verify your site ownership with search engines and other platforms.'); ?>
1508
1509 <form method="post" action="options.php">
1510 <div class="dashboard-card">
1511 <h2>Search Engine Verification</h2>
1512 <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>
1513 <?php
1514 settings_fields(Metasync_Admin::option_group);
1515 do_settings_sections($page_slug);
1516 submit_button();
1517 ?>
1518 </div>
1519 </form>
1520 <?php $this->admin->render_layout_close(); ?>
1521 <?php
1522 }
1523
1524 public function create_admin_breadcrumbs_page()
1525 {
1526 ?>
1527 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1528
1529 <?php /* options.php redirects back here with ?settings-updated=true; without this call the save confirmation never prints */ ?>
1530 <?php settings_errors(); ?>
1531
1532 <form method="post" action="options.php">
1533 <div class="dashboard-card">
1534 <h2>Breadcrumbs Settings</h2>
1535 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1536 <?php
1537 settings_fields(Metasync_Admin::option_group);
1538 Metasync_Settings_Fields::instance()->render_breadcrumbs_section();
1539 submit_button();
1540 ?>
1541 </div>
1542 </form>
1543
1544 <div class="dashboard-card" style="margin-top: 20px;">
1545 <h2>📖 How to Display Breadcrumbs</h2>
1546 <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>
1547
1548 <h3 style="margin-top: 0;">Option 1 — Shortcode</h3>
1549 <p style="color: var(--dashboard-text-secondary);">Paste the shortcode inside any page, post, or widget:</p>
1550 <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>
1551
1552 <h3>Option 2 — PHP Template Function</h3>
1553 <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>
1554 <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>
1555
1556 <h3>Option 3 — PHP Return Value</h3>
1557 <p style="color: var(--dashboard-text-secondary);">Get the breadcrumb HTML as a string to manipulate before output:</p>
1558 <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();
1559 echo $breadcrumbs-&gt;render_breadcrumb_html();</pre><span class="metasync-copy-badge">Copy</span></div>
1560
1561 <p style="color: var(--dashboard-text-secondary); margin-top: 16px; font-size: 12px;">
1562 <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>
1563 <code>echo $breadcrumbs-&gt;render_breadcrumb_html(['post_id' =&gt; 123]);</code>
1564 </p>
1565 <?php /* click-to-copy for the code snippets above */ ?>
1566 <style>
1567 .metasync-copy-wrap { position: relative; }
1568 .metasync-copy-snippet { cursor: pointer; outline: 1px solid transparent; transition: outline-color .15s ease; }
1569 .metasync-copy-wrap:hover .metasync-copy-snippet { outline-color: var(--dashboard-border, #374151); }
1570 .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); }
1571 .metasync-copy-wrap:hover .metasync-copy-badge { opacity: .85; }
1572 .metasync-copy-badge.metasync-copy-done { opacity: 1; background: var(--dashboard-gradient-primary, linear-gradient(135deg, #667eea 0%, #764ba2 100%)); color: #fff; }
1573 </style>
1574 <script>
1575 (function(){
1576 if (window.__metasyncCopyInit) return; window.__metasyncCopyInit = true;
1577 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); }
1578 function attach(){
1579 document.querySelectorAll('.metasync-copy-snippet').forEach(function(pre){
1580 if (pre.dataset.copyBound) return; pre.dataset.copyBound='1';
1581 pre.setAttribute('title','Click to copy');
1582 pre.addEventListener('click', function(){
1583 var text = pre.textContent.trim();
1584 var wrap = pre.closest('.metasync-copy-wrap') || pre.parentNode;
1585 var badge = wrap.querySelector('.metasync-copy-badge');
1586 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); };
1587 if (navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(done).catch(function(){ fallback(text); done(); }); }
1588 else { fallback(text); done(); }
1589 });
1590 });
1591 }
1592 if (document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', attach); } else { attach(); }
1593 })();
1594 </script>
1595 </div>
1596 <?php $this->admin->render_layout_close(); ?>
1597 <?php
1598 }
1599
1600 public function create_admin_schema_markup_page()
1601 {
1602 $page_slug = Metasync_Admin::$page_slug . '_schema-markup';
1603 $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.');
1604 ?>
1605 <form method="post" action="options.php">
1606 <div class="dashboard-card">
1607 <h2>Schema Markup Settings</h2>
1608 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure the site-wide Organization and WebSite structured data injected into every page.</p>
1609 <?php
1610 settings_fields(Metasync_Admin::option_group);
1611 do_settings_sections($page_slug);
1612 submit_button('Save Changes');
1613 ?>
1614 </div>
1615 </form>
1616 <?php
1617 $this->admin->render_layout_close();
1618 }
1619
1620 public function create_admin_local_business_page()
1621 {
1622 $page_slug = Metasync_Admin::$page_slug . '_local-seo';
1623 ?>
1624 <?php $this->admin->render_layout_open('Local Business', 'local_business', 'Configure local business information for better local SEO.'); ?>
1625
1626 <form method="post" action="options.php">
1627 <div class="dashboard-card">
1628 <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>
1629 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure your local business information for better local search engine optimization.</p>
1630 <?php
1631 settings_fields(Metasync_Admin::option_group);
1632 do_settings_sections($page_slug);
1633 submit_button();
1634 ?>
1635 </div>
1636 </form>
1637 <?php $this->admin->render_layout_close(); ?>
1638 <?php
1639 }
1640
1641 public function create_admin_code_snippets_page()
1642 {
1643 $page_slug = Metasync_Admin::$page_slug . '_code-snippets';
1644 ?>
1645 <?php $this->admin->render_layout_open('Code Snippets', 'code_snippets', 'Add custom code to your site header and footer.'); ?>
1646
1647 <form method="post" action="options.php">
1648 <div class="dashboard-card">
1649 <h2>Code Snippets Management</h2>
1650 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Add custom code snippets to enhance your site functionality and tracking capabilities.</p>
1651 <?php
1652 settings_fields(Metasync_Admin::option_group);
1653 do_settings_sections($page_slug);
1654 submit_button();
1655 ?>
1656 </div>
1657 </form>
1658 <?php $this->admin->render_layout_close(); ?>
1659 <?php
1660 }
1661
1662 public function create_admin_google_console_page()
1663 {
1664 $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
1665 $google = function_exists('google_index_direct') ? google_index_direct() : null;
1666 if (is_null($google)) {
1667 $service_info = ['error' => 'Module not loaded'];
1668 } else {
1669 $service_info = $google->get_service_account_info();
1670 }
1671 $is_configured = !isset($service_info['error']);
1672 include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php';
1673 $this->admin->render_layout_close();
1674 }
1675
1676 public function create_admin_bing_console_page()
1677 {
1678 ?>
1679 <?php $this->admin->render_layout_open('Bing Console', 'bing_console', ''); ?>
1680
1681 <?php
1682 require_once plugin_dir_path(dirname(__FILE__)) . 'bing-index/class-metasync-bing-instant-index.php';
1683 $bing_instant_index = new Metasync_Bing_Instant_Index();
1684 $bing_instant_index->show_bing_instant_indexing_console();
1685 ?>
1686 <?php $this->admin->render_layout_close(); ?>
1687 <?php
1688 }
1689
1690 public function create_admin_global_settings_page()
1691 {
1692 $page_slug = Metasync_Admin::$page_slug . '_common-settings';
1693 ?>
1694 <?php $this->admin->render_layout_open('Global Settings', 'general', ''); ?>
1695
1696 <form method="post" action="options.php">
1697 <div class="dashboard-card">
1698 <h2>Global Configuration</h2>
1699 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure global settings that apply across your entire site.</p>
1700 <?php
1701 settings_fields(Metasync_Admin::option_group);
1702 do_settings_sections($page_slug);
1703 ?>
1704 </div>
1705
1706 <!-- Save button removed - using floating notification system instead -->
1707 </form>
1708 <?php $this->admin->render_layout_close(); ?>
1709 <?php
1710 }
1711
1712 public function create_admin_common_meta_settings_page()
1713 {
1714 $page_slug = Metasync_Admin::$page_slug . '_common-meta-settings';
1715 ?>
1716 <?php $this->admin->render_layout_open('Common Meta Settings', 'general', ''); ?>
1717
1718 <form method="post" action="options.php">
1719 <div class="dashboard-card">
1720 <h2>Meta Configuration</h2>
1721 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure common meta tags and SEO settings for your site.</p>
1722 <?php
1723 settings_fields(Metasync_Admin::option_group);
1724 do_settings_sections($page_slug);
1725 ?>
1726 </div>
1727
1728 <!-- Save button removed - using floating notification system instead -->
1729 </form>
1730 <?php $this->admin->render_layout_close(); ?>
1731 <?php
1732 }
1733
1734 public function create_admin_social_meta_page()
1735 {
1736 $page_slug = Metasync_Admin::$page_slug . '_social-meta';
1737 ?>
1738 <?php $this->admin->render_layout_open('Social Meta', 'general', ''); ?>
1739
1740 <form method="post" action="options.php">
1741 <div class="dashboard-card">
1742 <h2>📲 Social Meta Tags</h2>
1743 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how your content appears when shared on social media platforms.</p>
1744 <?php
1745 settings_fields(Metasync_Admin::option_group);
1746 do_settings_sections($page_slug);
1747 ?>
1748 </div>
1749
1750 <!-- Save button removed - using floating notification system instead -->
1751 </form>
1752 <?php $this->admin->render_layout_close(); ?>
1753 <?php
1754 }
1755
1756 public function create_admin_seo_controls_page()
1757 {
1758 $page_slug = Metasync_Admin::$page_slug . '_seo-controls';
1759 ?>
1760 <?php $this->admin->render_layout_open('Indexation Control', 'seo_controls', 'Control how search engines index your content.'); ?>
1761
1762 <!-- Status Messages Container -->
1763 <div id="seo-controls-messages"></div>
1764
1765 <form method="post" action="options.php" id="metaSyncSeoControlsForm">
1766 <div class="dashboard-card">
1767 <div style="display: flex; justify-content: flex-end; margin-bottom: 20px;">
1768 <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-import-external&tab=indexation')); ?>" class="button button-secondary">
1769 <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
1770 </a>
1771 </div>
1772 <?php
1773 settings_fields(Metasync_Admin::option_group);
1774 do_settings_sections($page_slug);
1775
1776 wp_nonce_field('meta_sync_seo_controls_nonce', 'meta_sync_seo_controls_nonce');
1777 ?>
1778 </div>
1779
1780 <!-- Save button removed - using floating notification system instead -->
1781 </form>
1782 <?php $this->admin->render_layout_close(); ?>
1783 <?php
1784 }
1785
1786
1787 public function create_admin_error_logs_page()
1788 {
1789 ?>
1790 <?php $this->admin->render_layout_open('Error Logs', 'general', ''); ?>
1791
1792 <div class="dashboard-card">
1793 <h2>Error Logs Management</h2>
1794 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">View and manage system error logs to troubleshoot issues and monitor plugin performance.</p>
1795 <?php
1796 $error_logs = new Metasync_Error_Logs();
1797
1798 if ($error_logs->can_show_error_logs()) {
1799 $log_content = $error_logs->get_error_logs(50);
1800
1801 if (!empty(trim($log_content))) {
1802 $error_logs->show_copy_button();
1803 $error_logs->show_logs();
1804 $error_logs->show_info();
1805 } else {
1806 echo '<div class="dashboard-empty-state">';
1807 echo '<p style="color: var(--dashboard-text-secondary); font-style: italic; text-align: center; padding: 40px 20px;">�
1808 Log file is empty - no errors recorded.</p>';
1809 echo '</div>';
1810 }
1811 } else {
1812 $error_message = $error_logs->get_error_message();
1813 if (!empty($error_message)) {
1814 echo '<div class="dashboard-empty-state">';
1815 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;">';
1816 echo esc_html($error_message);
1817 echo '</p>';
1818 echo '</div>';
1819 } else {
1820 echo '<div class="dashboard-empty-state">';
1821 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>';
1822 echo '</div>';
1823 }
1824 }
1825 ?>
1826 </div>
1827 <?php $this->admin->render_layout_close(); ?>
1828 <?php
1829 }
1830
1831 // NOTE: The Changes Log (sync log) page is rendered by
1832 // Metasync_Admin::create_admin_sync_log_page() in
1833 // admin/class-metasync-admin.php, which is the callback registered in
1834 // class-metasync-admin-navigation.php. A former duplicate of that method
1835 // lived here but was never wired to a menu and has been removed to avoid
1836 // drift.
1837
1838 public function creat_error_Logs_List()
1839 {
1840 // printf('<h1> Error Logs </h1>');
1841 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
1842 // $error_log->create_admin_error_log_list_interface();
1843 }
1844
1845 public function create_admin_heartbeat_error_logs_page()
1846 {
1847 ?>
1848 <?php $this->admin->render_layout_open('Heartbeat Error Logs', 'general', ''); ?>
1849
1850 <div class="dashboard-card">
1851 <h2>💓 HeartBeat Error Logs</h2>
1852 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Monitor WordPress heartbeat errors to identify connectivity issues and system problems.</p>
1853 <?php
1854 $heartbeat_errors = new Metasync_HeartBeat_Error_Monitor($this->admin->db_heartbeat_errors);
1855 $heartbeat_errors->create_admin_heartbeat_errors_interface();
1856 ?>
1857 </div>
1858 <?php $this->admin->render_layout_close(); ?>
1859 <?php
1860 }
1861
1862 public function create_admin_bot_statistics_page()
1863 {
1864 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
1865 }
1866
1867 // ------------------------------------------------------------------
1868 // Private helpers
1869 // ------------------------------------------------------------------
1870 // The former time_elapsed_string / build_filter_query_string /
1871 // handle_sync_log_ajax helpers here were only used by the dead
1872 // create_admin_sync_log_page() duplicate that has since been removed. The
1873 // live equivalents live on Metasync_Admin.
1874 }
1875