| @@ -217,10 +217,15 @@ | ||
| 217 | 217 | // ------------------------------------------------------------------ |
| 218 | 218 | |
| 219 | 219 | public function create_admin_settings_page() |
| 220 | 220 | { |
| 221 | - $active_tab = isset($_GET['tab']) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; | |
| 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 | + } | |
| 222 | 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 | + | |
| 223 | 228 | $whitelabel_settings = Metasync::get_whitelabel_settings(); |
| 224 | 229 | |
| 225 | 230 | $user_password = $whitelabel_settings['settings_password'] ?? ''; |
| 226 | 231 | $hide_settings_enabled = !empty($whitelabel_settings['hide_settings']); |
| @@ -245,9 +250,9 @@ | ||
| 245 | 250 | |
| 246 | 251 | $auth = new Metasync_Auth_Manager('whitelabel', 1800); |
| 247 | 252 | $password_validated = $auth->has_access(); |
| 248 | 253 | |
| 249 | - if (isset($_POST['whitelabel_password_submit']) && !$password_validated) { | |
| 254 | + if (isset($_POST['whitelabel_password_submit']) && !$password_validated) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only access check for a display message | |
| 250 | 255 | $password_error = 'Incorrect password. Please try again.'; |
| 251 | 256 | } |
| 252 | 257 | } |
| 253 | 258 | |
| @@ -264,8 +269,18 @@ | ||
| 264 | 269 | $this->admin->render_layout_open('Settings', $settings_current_page, 'Manage all plugin settings and configuration.'); |
| 265 | 270 | ?> |
| 266 | 271 | <?php |
| 267 | 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 | + /* | |
| 268 | 283 | # Temporarily commented out: Clear Cache notice and button (can be re-enabled later) |
| 269 | 284 | |
| 270 | 285 | * <div class="notice notice-success"> |
| 271 | 286 | * <p> |
| @@ -333,8 +348,9 @@ | ||
| 333 | 348 | </button> |
| 334 | 349 | </div> |
| 335 | 350 | </form> |
| 336 | 351 | |
| 352 | + <?php if (current_user_can('manage_options')): ?> | |
| 337 | 353 | <div style="text-align: center; margin-top: 20px;"> |
| 338 | 354 | <a href="#" id="metasync-forgot-password-link" style="color: #2271b1; text-decoration: none; font-size: 14px;"> |
| 339 | 355 | Forgot Password? |
| 340 | 356 | </a> |
| @@ -339,64 +355,12 @@ | ||
| 339 | 355 | Forgot Password? |
| 340 | 356 | </a> |
| 341 | 357 | <div id="metasync-recovery-message" style="margin-top: 15px; padding: 12px; border-radius: 6px; display: none;"></div> |
| 342 | 358 | </div> |
| 359 | + <?php endif; ?> | |
| 343 | 360 | |
| 344 | 361 | </div> |
| 345 | 362 | |
| 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('✅ ' + response.data.message) | |
| 378 | - .css('background', '#d4edda') | |
| 379 | - .css('color', '#155724') | |
| 380 | - .css('border', '1px solid #c3e6cb'); | |
| 381 | - } else { | |
| 382 | - $message.addClass('error').html('❌ ' + response.data.message) | |
| 383 | - .css('background', '#f8d7da') | |
| 384 | - .css('color', '#721c24') | |
| 385 | - .css('border', '1px solid #f5c6cb'); | |
| 386 | - } | |
| 387 | - }, | |
| 388 | - error: function() { | |
| 389 | - $link.css('pointer-events', 'auto').css('opacity', '1'); | |
| 390 | - $message.addClass('error').html('❌ An error occurred. Please try again.') | |
| 391 | - .css('background', '#f8d7da') | |
| 392 | - .css('color', '#721c24') | |
| 393 | - .css('border', '1px solid #f5c6cb'); | |
| 394 | - } | |
| 395 | - }); | |
| 396 | - }); | |
| 397 | - }); | |
| 398 | - </script> | |
| 399 | 363 | <?php |
| 400 | 364 | return; |
| 401 | 365 | } elseif (!$password_protection_enabled) { |
| 402 | 366 | ?> |
| @@ -410,8 +374,9 @@ | ||
| 410 | 374 | } |
| 411 | 375 | ?> |
| 412 | 376 | |
| 413 | 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)); ?>" /> | |
| 414 | 379 | <?php |
| 415 | 380 | settings_fields(Metasync_Admin::option_group); |
| 416 | 381 | |
| 417 | 382 | wp_nonce_field('meta_sync_general_setting_nonce', 'meta_sync_nonce'); |
| @@ -439,14 +404,17 @@ | ||
| 439 | 404 | |
| 440 | 405 | <?php |
| 441 | 406 | $this->admin->render_accordion_sections(Metasync_Admin::$page_slug . '_general'); |
| 442 | 407 | ?> |
| 408 | + <p style="margin: 20px 0 0 0; font-size: 13px; color: var(--dashboard-text-secondary);"> | |
| 409 | + 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>. | |
| 410 | + </p> | |
| 443 | 411 | </div> |
| 444 | 412 | |
| 445 | 413 | <div class="dashboard-card"> |
| 446 | - <h2>Content Genius Synchronization</h2> | |
| 414 | + <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> | |
| 447 | 415 | <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> |
| 448 | - <button type="button" class="button button-primary" id="sendAuthToken" data-toggle="tooltip" data-placement="top" title="Sync Categories and User"> | |
| 416 | + <button type="button" class="button button-primary" id="sendAuthToken"> | |
| 449 | 417 | Sync Now |
| 450 | 418 | </button> |
| 451 | 419 | </div> |
| 452 | 420 | |
| @@ -465,14 +433,15 @@ | ||
| 465 | 433 | ?> |
| 466 | 434 | <div class="dashboard-card"> |
| 467 | 435 | <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> |
| 468 | 436 | <div> |
| 469 | - <h2>White Label Branding</h2> | |
| 437 | + <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> | |
| 470 | 438 | <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Customize the plugin appearance with your own branding and logo.</p> |
| 471 | 439 | </div> |
| 472 | 440 | <?php if ($password_protection_enabled && $is_authenticated): ?> |
| 473 | 441 | <div> |
| 474 | 442 | <?php Metasync_Compatibility_Checker::instance()->render_lock_button('whitelabel'); ?> |
| 443 | + <?php Metasync::render_tooltip_icon('wl_lock_section', 'Re-lock this section now. You will need the password to open it again.'); ?> | |
| 475 | 444 | </div> |
| 476 | 445 | <?php endif; ?> |
| 477 | 446 | </div> |
| 478 | 447 | |
| @@ -493,9 +462,9 @@ | ||
| 493 | 462 | <!-- Color Palette Customization section --> |
| 494 | 463 | <div class="dashboard-card" id="metasync-color-palette-section"> |
| 495 | 464 | <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;"> |
| 496 | 465 | <div> |
| 497 | - <h2>Color Palette</h2> | |
| 466 | + <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> | |
| 498 | 467 | <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> |
| 499 | 468 | </div> |
| 500 | 469 | </div> |
| 501 | 470 | |
| @@ -738,9 +707,9 @@ | ||
| 738 | 707 | </script> |
| 739 | 708 | |
| 740 | 709 | <!-- Export Whitelabel Settings section --> |
| 741 | 710 | <div class="dashboard-card"> |
| 742 | - <h2>Export Whitelabel Plugin</h2> | |
| 711 | + <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> | |
| 743 | 712 | <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> |
| 744 | 713 | <button type="button" class="button button-primary" id="metasync-export-whitelabel-btn" style="padding: 10px 20px; font-size: 14px; font-weight: 600;"> |
| 745 | 714 | Export Plugin with Whitelabel Settings |
| 746 | 715 | </button> |
| @@ -824,21 +793,21 @@ | ||
| 824 | 793 | } |
| 825 | 794 | }); |
| 826 | 795 | |
| 827 | 796 | function hasPassword() { |
| 828 | - var passwordField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]'); | |
| 797 | + var passwordField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][settings_password]"]'); | |
| 829 | 798 | var passwordValue = passwordField.val(); |
| 830 | 799 | return passwordValue && passwordValue.length > 0; |
| 831 | 800 | } |
| 832 | 801 | |
| 833 | 802 | function hasRecoveryEmail() { |
| 834 | - var recoveryEmailField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][recovery_email]"]'); | |
| 803 | + var recoveryEmailField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][recovery_email]"]'); | |
| 835 | 804 | var emailValue = recoveryEmailField.val(); |
| 836 | 805 | return emailValue && emailValue.length > 0 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailValue); |
| 837 | 806 | } |
| 838 | 807 | |
| 839 | - var passwordField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]'); | |
| 840 | - var recoveryEmailField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][recovery_email]"]'); | |
| 808 | + var passwordField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][settings_password]"]'); | |
| 809 | + var recoveryEmailField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][recovery_email]"]'); | |
| 841 | 810 | |
| 842 | 811 | passwordField.on('blur', function() { |
| 843 | 812 | if (hasPassword() && !hasRecoveryEmail()) { |
| 844 | 813 | showModal( |
| @@ -876,9 +845,9 @@ | ||
| 876 | 845 | '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.' |
| 877 | 846 | ); |
| 878 | 847 | |
| 879 | 848 | setTimeout(function() { |
| 880 | - $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]').focus(); | |
| 849 | + $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][settings_password]"]').focus(); | |
| 881 | 850 | }, 300); |
| 882 | 851 | |
| 883 | 852 | return false; |
| 884 | 853 | } |
| @@ -887,10 +856,8 @@ | ||
| 887 | 856 | originalCheckboxState = hideSettingsCheckbox.is(':checked'); |
| 888 | 857 | }, 0); |
| 889 | 858 | }); |
| 890 | 859 | |
| 891 | - var storedPassword = '<?php echo esc_js($whitelabel_settings['settings_password'] ?? ''); ?>'; | |
| 892 | - | |
| 893 | 860 | passwordField.on('keydown', function(e) { |
| 894 | 861 | var currentValue = $(this).val(); |
| 895 | 862 | |
| 896 | 863 | if (hideSettingsCheckbox.is(':checked') && |
| @@ -920,42 +887,8 @@ | ||
| 920 | 887 | $('#metasync-custom-modal').fadeOut(200); |
| 921 | 888 | } |
| 922 | 889 | }); |
| 923 | 890 | |
| 924 | - $('#metasync-export-whitelabel-btn').on('click', function(e) { | |
| 925 | - e.preventDefault(); | |
| 926 | - | |
| 927 | - var $button = $(this); | |
| 928 | - var originalText = $button.html(); | |
| 929 | - | |
| 930 | - $button.prop('disabled', true).html('⏳ Exporting...'); | |
| 931 | - | |
| 932 | - var form = $('<form>', { | |
| 933 | - 'method': 'POST', | |
| 934 | - 'action': '<?php echo esc_js(admin_url('admin-post.php')); ?>', | |
| 935 | - 'target': '_blank' | |
| 936 | - }); | |
| 937 | - | |
| 938 | - form.append($('<input>', { | |
| 939 | - 'type': 'hidden', | |
| 940 | - 'name': 'action', | |
| 941 | - 'value': 'metasync_export_whitelabel_settings' | |
| 942 | - })); | |
| 943 | - | |
| 944 | - form.append($('<input>', { | |
| 945 | - 'type': 'hidden', | |
| 946 | - 'name': '_wpnonce', | |
| 947 | - 'value': '<?php echo wp_create_nonce('metasync_export_whitelabel'); ?>' | |
| 948 | - })); | |
| 949 | - | |
| 950 | - $('body').append(form); | |
| 951 | - form.submit(); | |
| 952 | - | |
| 953 | - setTimeout(function() { | |
| 954 | - form.remove(); | |
| 955 | - $button.prop('disabled', false).html(originalText); | |
| 956 | - }, 2000); | |
| 957 | - }); | |
| 958 | 891 | }); |
| 959 | 892 | </script> |
| 960 | 893 | <?php |
| 961 | 894 | } elseif ($active_tab == 'advanced') { |
| @@ -985,10 +918,10 @@ | ||
| 985 | 918 | </div> |
| 986 | 919 | <?php endif; ?> |
| 987 | 920 | |
| 988 | 921 | <?php |
| 989 | - if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'): | |
| 990 | - $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1'; | |
| 922 | + 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 | |
| 923 | + $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice | |
| 991 | 924 | ?> |
| 992 | 925 | <div class="notice notice-success inline" style="margin-bottom: 20px;"> |
| 993 | 926 | <p> |
| 994 | 927 | <strong>✅ Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>. |
| @@ -995,9 +928,9 @@ | ||
| 995 | 928 | </p> |
| 996 | 929 | </div> |
| 997 | 930 | <?php endif; ?> |
| 998 | 931 | |
| 999 | - <?php if (isset($_GET['debug_mode_disabled']) && $_GET['debug_mode_disabled'] == '1'): ?> | |
| 932 | + <?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 ?> | |
| 1000 | 933 | <div class="notice notice-info inline" style="margin-bottom: 20px;"> |
| 1001 | 934 | <p> |
| 1002 | 935 | <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled. |
| 1003 | 936 | </p> |
| @@ -1003,9 +936,9 @@ | ||
| 1003 | 936 | </p> |
| 1004 | 937 | </div> |
| 1005 | 938 | <?php endif; ?> |
| 1006 | 939 | |
| 1007 | - <?php if (isset($_GET['debug_mode_extended']) && $_GET['debug_mode_extended'] == '1'): ?> | |
| 940 | + <?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 ?> | |
| 1008 | 941 | <div class="notice notice-success inline" style="margin-bottom: 20px;"> |
| 1009 | 942 | <p> |
| 1010 | 943 | <strong>✅ Extended!</strong> Debug mode has been extended for another 24 hours. |
| 1011 | 944 | </p> |
| @@ -1011,9 +944,9 @@ | ||
| 1011 | 944 | </p> |
| 1012 | 945 | </div> |
| 1013 | 946 | <?php endif; ?> |
| 1014 | 947 | |
| 1015 | - <?php if (isset($_GET['debug_error']) && $_GET['debug_error'] == '1'): ?> | |
| 948 | + <?php if (isset($_GET['debug_error']) && $_GET['debug_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?> | |
| 1016 | 949 | <div class="notice notice-error inline" style="margin-bottom: 20px;"> |
| 1017 | 950 | <p> |
| 1018 | 951 | <strong>❌ Error:</strong> Unable to perform the debug mode operation. Please try again. |
| 1019 | 952 | </p> |
| @@ -1050,9 +983,9 @@ | ||
| 1050 | 983 | 'method': 'post', |
| 1051 | 984 | 'action': '' |
| 1052 | 985 | }); |
| 1053 | 986 | |
| 1054 | - logoutForm.append('<?php echo wp_nonce_field("whitelabel_logout_nonce", "whitelabel_logout_nonce", true, false); ?>'); | |
| 987 | + 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 ?>'); | |
| 1055 | 988 | |
| 1056 | 989 | logoutForm.append($('<input>', { |
| 1057 | 990 | 'type': 'hidden', |
| 1058 | 991 | 'name': 'whitelabel_logout', |
| @@ -1124,9 +1057,9 @@ | ||
| 1124 | 1057 | $.ajax({ |
| 1125 | 1058 | url: ajaxUrl, |
| 1126 | 1059 | type: 'POST', |
| 1127 | 1060 | dataType: 'json', |
| 1128 | - data: { action: action, nonce: '<?php echo wp_create_nonce('metasync_nonce'); ?>' }, | |
| 1061 | + data: { action: action, nonce: '<?php echo esc_attr(wp_create_nonce('metasync_nonce')); ?>' }, | |
| 1129 | 1062 | timeout: 35000, |
| 1130 | 1063 | success: function(response, textStatus, xhr) { |
| 1131 | 1064 | try { |
| 1132 | 1065 | if (response && response.success && response.data) { |
| @@ -1194,22 +1127,22 @@ | ||
| 1194 | 1127 | var blockedClass = result.blocked ? 'blocked' : 'allowed'; |
| 1195 | 1128 | |
| 1196 | 1129 | html += '<div class="test-result-item ' + statusClass + '">'; |
| 1197 | 1130 | html += '<div class="test-result-header">'; |
| 1198 | - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>'; | |
| 1131 | + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>'; | |
| 1199 | 1132 | html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>'; |
| 1200 | 1133 | html += '</div>'; |
| 1201 | 1134 | |
| 1202 | 1135 | html += '<div class="test-result-details">'; |
| 1203 | - html += '<p><strong>Response Time:</strong> ' + result.response_time + '</p>'; | |
| 1204 | - html += '<p><strong>Status:</strong> ' + result.status + '</p>'; | |
| 1136 | + html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>'; | |
| 1137 | + html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>'; | |
| 1205 | 1138 | |
| 1206 | 1139 | if (result.status_code) { |
| 1207 | - html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + result.status_code + '</span></p>'; | |
| 1140 | + html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + escapeHtml(result.status_code) + '</span></p>'; | |
| 1208 | 1141 | } |
| 1209 | 1142 | |
| 1210 | 1143 | if (result.error) { |
| 1211 | - html += '<p><strong>Error:</strong> <span class="error-message">' + result.error + '</span></p>'; | |
| 1144 | + html += '<p><strong>Error:</strong> <span class="error-message">' + escapeHtml(result.error) + '</span></p>'; | |
| 1212 | 1145 | } |
| 1213 | 1146 | |
| 1214 | 1147 | if (result.body) { |
| 1215 | 1148 | html += '<p><strong>Response Body:</strong></p>'; |
| @@ -1219,9 +1152,9 @@ | ||
| 1219 | 1152 | if (result.headers && Object.keys(result.headers).length > 0) { |
| 1220 | 1153 | html += '<p><strong>Response Headers:</strong></p>'; |
| 1221 | 1154 | html += '<pre class="response-headers">'; |
| 1222 | 1155 | Object.keys(result.headers).forEach(function(key) { |
| 1223 | - html += key + ': ' + result.headers[key] + '\n'; | |
| 1156 | + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n'; | |
| 1224 | 1157 | }); |
| 1225 | 1158 | html += '</pre>'; |
| 1226 | 1159 | } |
| 1227 | 1160 | |
| @@ -1234,9 +1167,9 @@ | ||
| 1234 | 1167 | html += '<p><strong>Parsed Response:</strong></p>'; |
| 1235 | 1168 | html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>'; |
| 1236 | 1169 | } |
| 1237 | 1170 | |
| 1238 | - html += '<p><strong>Details:</strong> ' + result.details + '</p>'; | |
| 1171 | + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>'; | |
| 1239 | 1172 | html += '</div>'; |
| 1240 | 1173 | html += '</div>'; |
| 1241 | 1174 | }); |
| 1242 | 1175 | |
| @@ -1251,9 +1184,9 @@ | ||
| 1251 | 1184 | '>': '>', |
| 1252 | 1185 | '"': '"', |
| 1253 | 1186 | '\'': ''' |
| 1254 | 1187 | }; |
| 1255 | - return text.replace(/[&<>"']/g, function(m) { return map[m]; }); | |
| 1188 | + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; }); | |
| 1256 | 1189 | } |
| 1257 | 1190 | }); |
| 1258 | 1191 | </script> |
| 1259 | 1192 | |
| @@ -1386,41 +1319,41 @@ | ||
| 1386 | 1319 | } |
| 1387 | 1320 | </style> |
| 1388 | 1321 | |
| 1389 | 1322 | <?php if ($active_tab === 'advanced'): ?> |
| 1390 | - <?php if (isset($_GET['settings_cleared']) && $_GET['settings_cleared'] == '1'): ?> | |
| 1323 | + <?php if (isset($_GET['settings_cleared']) && $_GET['settings_cleared'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?> | |
| 1391 | 1324 | <div class="notice notice-success is-dismissible"> |
| 1392 | 1325 | <p><strong>✅ 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> |
| 1393 | 1326 | </div> |
| 1394 | 1327 | <?php endif; ?> |
| 1395 | 1328 | |
| 1396 | - <?php if (isset($_GET['clear_settings_error']) && $_GET['clear_settings_error'] == '1'): ?> | |
| 1329 | + <?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 ?> | |
| 1397 | 1330 | <div class="notice notice-error is-dismissible"> |
| 1398 | 1331 | <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p> |
| 1399 | 1332 | </div> |
| 1400 | 1333 | <?php endif; ?> |
| 1401 | 1334 | |
| 1402 | - <?php if (isset($_GET['log_cleared']) && $_GET['log_cleared'] == '1'): ?> | |
| 1335 | + <?php if (isset($_GET['log_cleared']) && $_GET['log_cleared'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?> | |
| 1403 | 1336 | <div class="notice notice-success is-dismissible"> |
| 1404 | 1337 | <p><strong>✅ Success!</strong> Error logs have been cleared successfully.</p> |
| 1405 | 1338 | </div> |
| 1406 | 1339 | <?php endif; ?> |
| 1407 | 1340 | |
| 1408 | - <?php if (isset($_GET['clear_error']) && $_GET['clear_error'] == '1'): ?> | |
| 1341 | + <?php if (isset($_GET['clear_error']) && $_GET['clear_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?> | |
| 1409 | 1342 | <div class="notice notice-error is-dismissible"> |
| 1410 | 1343 | <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p> |
| 1411 | 1344 | </div> |
| 1412 | 1345 | <?php endif; ?> |
| 1413 | 1346 | |
| 1414 | - <?php if (isset($_GET['access_roles_saved']) && $_GET['access_roles_saved'] == '1'): ?> | |
| 1347 | + <?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 ?> | |
| 1415 | 1348 | <div class="notice notice-success is-dismissible"> |
| 1416 | 1349 | <p><strong>✅ Success!</strong> Plugin access roles have been saved successfully.</p> |
| 1417 | 1350 | </div> |
| 1418 | 1351 | <?php endif; ?> |
| 1419 | 1352 | |
| 1420 | - <?php if (isset($_GET['access_roles_error']) && $_GET['access_roles_error'] == '1'): ?> | |
| 1353 | + <?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 ?> | |
| 1421 | 1354 | <div class="notice notice-error is-dismissible"> |
| 1422 | - <p><strong>❌ Error!</strong> Failed to save plugin access roles. <?php echo isset($_GET['message']) ? esc_html(urldecode($_GET['message'])) : 'Please try again.'; ?></p> | |
| 1355 | + <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> | |
| 1423 | 1356 | </div> |
| 1424 | 1357 | <?php endif; ?> |
| 1425 | 1358 | |
| 1426 | 1359 | <?php endif; ?> |
| @@ -1442,9 +1375,9 @@ | ||
| 1442 | 1375 | <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> |
| 1443 | 1376 | <?php |
| 1444 | 1377 | if (!isset(Metasync::get_option('general')['linkgraph_token']) || Metasync::get_option('general')['linkgraph_token'] == '') { |
| 1445 | 1378 | 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>'; |
| 1446 | - echo '<a href="' . admin_url('admin.php?page=' . Metasync_Admin::$page_slug) . '" class="button button-secondary">Go to Settings</a>'; | |
| 1379 | + echo '<a href="' . esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug)) . '" class="button button-secondary">Go to Settings</a>'; | |
| 1447 | 1380 | } else { |
| 1448 | 1381 | echo '<a href="' . esc_url($this->admin->get_dashboard_url()) . '" target="_blank" class="button button-primary">Open Dashboard</a>'; |
| 1449 | 1382 | } |
| 1450 | 1383 | ?> |
| @@ -1454,8 +1387,15 @@ | ||
| 1454 | 1387 | } |
| 1455 | 1388 | |
| 1456 | 1389 | public function create_admin_robots_txt_page() |
| 1457 | 1390 | { |
| 1391 | + // The form below rewrites the site's physical robots.txt — restrict | |
| 1392 | + // that to site admins, not just anyone who can open the page via | |
| 1393 | + // plugin_access_roles. The nonce is CSRF protection, not authorization. | |
| 1394 | + if (!empty($_POST) && !current_user_can('manage_options')) { | |
| 1395 | + wp_die(esc_html__('Sorry, you are not allowed to manage robots.txt settings.', 'metasync')); | |
| 1396 | + } | |
| 1397 | + | |
| 1458 | 1398 | ?> |
| 1459 | 1399 | <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?> |
| 1460 | 1400 | |
| 1461 | 1401 | <?php |
| @@ -1476,8 +1416,11 @@ | ||
| 1476 | 1416 | |
| 1477 | 1417 | if (is_wp_error($result)) { |
| 1478 | 1418 | echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>'; |
| 1479 | 1419 | } else { |
| 1420 | + Metasync_PostHog::feature('robots_txt_saved', array( | |
| 1421 | + 'warning_count' => count($validation['warnings']), | |
| 1422 | + )); | |
| 1480 | 1423 | echo '<div class="notice notice-success"><p>' . esc_html__('robots.txt file saved successfully!', 'metasync') . '</p></div>'; |
| 1481 | 1424 | |
| 1482 | 1425 | if (!empty($validation['warnings'])) { |
| 1483 | 1426 | foreach ($validation['warnings'] as $warning) { |
| @@ -1500,9 +1443,11 @@ | ||
| 1500 | 1443 | echo '<div class="notice notice-error"><p>' . esc_html($current_content->get_error_message()) . '</p></div>'; |
| 1501 | 1444 | $current_content = ''; |
| 1502 | 1445 | } |
| 1503 | 1446 | |
| 1504 | - $backups = $robots_txt->get_backup_history(10); | |
| 1447 | + // First page of backups; the Backup History panel is paginated so the | |
| 1448 | + // list always reflects the full stored state and its true total. | |
| 1449 | + $backups = $robots_txt->get_backup_history(Metasync_Robots_Txt_Database::BACKUPS_PER_PAGE, 0); | |
| 1505 | 1450 | |
| 1506 | 1451 | $file_exists = $robots_txt->file_exists(); |
| 1507 | 1452 | $is_writable = $robots_txt->is_writable(); |
| 1508 | 1453 | |
| @@ -1516,80 +1461,8 @@ | ||
| 1516 | 1461 | { |
| 1517 | 1462 | require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php'; |
| 1518 | 1463 | } |
| 1519 | 1464 | |
| 1520 | - public function create_admin_xml_sitemap_page() | |
| 1521 | - { | |
| 1522 | - require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php'; | |
| 1523 | - | |
| 1524 | - $sitemap_generator = new Metasync_Sitemap_Generator(); | |
| 1525 | - | |
| 1526 | - if (isset($_POST['metasync_sitemap_nonce'])) { | |
| 1527 | - check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce'); | |
| 1528 | - | |
| 1529 | - if (isset($_POST['generate_sitemap'])) { | |
| 1530 | - $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators(); | |
| 1531 | - | |
| 1532 | - $result = $sitemap_generator->generate_sitemap(); | |
| 1533 | - | |
| 1534 | - if (is_wp_error($result)) { | |
| 1535 | - error_log('[MetaSync] Sitemap generation failed: ' . $result->get_error_message()); | |
| 1536 | - echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed. Please try again or check the error logs.', 'metasync') . '</p></div>'; | |
| 1537 | - } else { | |
| 1538 | - $message = esc_html__('Sitemap generated successfully!', 'metasync'); | |
| 1539 | - if ($disabled_plugins) { | |
| 1540 | - $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync'); | |
| 1541 | - } | |
| 1542 | - | |
| 1543 | - $robots_result = get_transient('metasync_sitemap_robots_updated'); | |
| 1544 | - if ($robots_result && $robots_result['success']) { | |
| 1545 | - if ($robots_result['action'] === 'added') { | |
| 1546 | - $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync'); | |
| 1547 | - } elseif ($robots_result['action'] === 'updated') { | |
| 1548 | - $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync'); | |
| 1549 | - } elseif ($robots_result['action'] === 'created') { | |
| 1550 | - $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync'); | |
| 1551 | - } | |
| 1552 | - delete_transient('metasync_sitemap_robots_updated'); | |
| 1553 | - } | |
| 1554 | - | |
| 1555 | - echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>'; | |
| 1556 | - } | |
| 1557 | - } elseif (isset($_POST['enable_auto_update'])) { | |
| 1558 | - update_option('metasync_sitemap_auto_update', true); | |
| 1559 | - $sitemap_generator->setup_auto_update_hooks(); | |
| 1560 | - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>'; | |
| 1561 | - } elseif (isset($_POST['disable_auto_update'])) { | |
| 1562 | - update_option('metasync_sitemap_auto_update', false); | |
| 1563 | - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>'; | |
| 1564 | - } elseif (isset($_POST['delete_sitemap'])) { | |
| 1565 | - $deleted = $sitemap_generator->delete_sitemap(); | |
| 1566 | - if ($deleted) { | |
| 1567 | - update_option('metasync_sitemap_auto_update', false); | |
| 1568 | - echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap deleted successfully!', 'metasync') . '</p></div>'; | |
| 1569 | - } else { | |
| 1570 | - 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>'; | |
| 1571 | - } | |
| 1572 | - } elseif (isset($_POST['enable_other_sitemaps'])) { | |
| 1573 | - $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators(); | |
| 1574 | - if ($enabled_plugins) { | |
| 1575 | - echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>'; | |
| 1576 | - } else { | |
| 1577 | - echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>'; | |
| 1578 | - } | |
| 1579 | - } | |
| 1580 | - } | |
| 1581 | - | |
| 1582 | - $sitemap_exists = $sitemap_generator->sitemap_exists(); | |
| 1583 | - $sitemap_url = $sitemap_generator->get_sitemap_url(); | |
| 1584 | - $url_count = $sitemap_generator->count_urls(); | |
| 1585 | - $last_generated = $sitemap_generator->get_last_generated_time(); | |
| 1586 | - $auto_update_enabled = get_option('metasync_sitemap_auto_update', false); | |
| 1587 | - $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins(); | |
| 1588 | - | |
| 1589 | - require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php'; | |
| 1590 | - } | |
| 1591 | - | |
| 1592 | 1465 | public function create_admin_custom_pages_page() |
| 1593 | 1466 | { |
| 1594 | 1467 | ?> |
| 1595 | 1468 | <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?> |
| @@ -1640,12 +1513,14 @@ | ||
| 1640 | 1513 | } |
| 1641 | 1514 | |
| 1642 | 1515 | public function create_admin_breadcrumbs_page() |
| 1643 | 1516 | { |
| 1644 | - $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs'; | |
| 1645 | 1517 | ?> |
| 1646 | 1518 | <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?> |
| 1647 | 1519 | |
| 1520 | + <?php /* options.php redirects back here with ?settings-updated=true; without this call the save confirmation never prints */ ?> | |
| 1521 | + <?php settings_errors(); ?> | |
| 1522 | + | |
| 1648 | 1523 | <form method="post" action="options.php"> |
| 1649 | 1524 | <div class="dashboard-card"> |
| 1650 | 1525 | <h2>Breadcrumbs Settings</h2> |
| 1651 | 1526 | <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p> |
| @@ -1650,9 +1525,9 @@ | ||
| 1650 | 1525 | <h2>Breadcrumbs Settings</h2> |
| 1651 | 1526 | <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p> |
| 1652 | 1527 | <?php |
| 1653 | 1528 | settings_fields(Metasync_Admin::option_group); |
| 1654 | - do_settings_sections($page_slug); | |
| 1529 | + Metasync_Settings_Fields::instance()->render_breadcrumbs_section(); | |
| 1655 | 1530 | submit_button(); |
| 1656 | 1531 | ?> |
| 1657 | 1532 | </div> |
| 1658 | 1533 | </form> |
| @@ -1662,23 +1537,53 @@ | ||
| 1662 | 1537 | <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> |
| 1663 | 1538 | |
| 1664 | 1539 | <h3 style="margin-top: 0;">Option 1 — Shortcode</h3> |
| 1665 | 1540 | <p style="color: var(--dashboard-text-secondary);">Paste the shortcode inside any page, post, or widget:</p> |
| 1666 | - <pre style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text, #e0e0e0); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">[metasync_breadcrumb]</pre> | |
| 1541 | + <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> | |
| 1667 | 1542 | |
| 1668 | 1543 | <h3>Option 2 — PHP Template Function</h3> |
| 1669 | 1544 | <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> |
| 1670 | - <pre style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text, #e0e0e0); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;"><?php metasync_breadcrumb(); ?></pre> | |
| 1545 | + <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;"><?php metasync_breadcrumb(); ?></pre><span class="metasync-copy-badge">Copy</span></div> | |
| 1671 | 1546 | |
| 1672 | 1547 | <h3>Option 3 — PHP Return Value</h3> |
| 1673 | 1548 | <p style="color: var(--dashboard-text-secondary);">Get the breadcrumb HTML as a string to manipulate before output:</p> |
| 1674 | - <pre style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text, #e0e0e0); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">$breadcrumbs = new Metasync_Breadcrumbs(); | |
| 1675 | -echo $breadcrumbs->render_breadcrumb_html();</pre> | |
| 1549 | + <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(); | |
| 1550 | +echo $breadcrumbs->render_breadcrumb_html();</pre><span class="metasync-copy-badge">Copy</span></div> | |
| 1676 | 1551 | |
| 1677 | 1552 | <p style="color: var(--dashboard-text-secondary); margin-top: 16px; font-size: 12px;"> |
| 1678 | 1553 | <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> |
| 1679 | 1554 | <code>echo $breadcrumbs->render_breadcrumb_html(['post_id' => 123]);</code> |
| 1680 | 1555 | </p> |
| 1556 | + <?php /* click-to-copy for the code snippets above */ ?> | |
| 1557 | + <style> | |
| 1558 | + .metasync-copy-wrap { position: relative; } | |
| 1559 | + .metasync-copy-snippet { cursor: pointer; outline: 1px solid transparent; transition: outline-color .15s ease; } | |
| 1560 | + .metasync-copy-wrap:hover .metasync-copy-snippet { outline-color: var(--dashboard-border, #374151); } | |
| 1561 | + .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); } | |
| 1562 | + .metasync-copy-wrap:hover .metasync-copy-badge { opacity: .85; } | |
| 1563 | + .metasync-copy-badge.metasync-copy-done { opacity: 1; background: var(--dashboard-gradient-primary, linear-gradient(135deg, #667eea 0%, #764ba2 100%)); color: #fff; } | |
| 1564 | + </style> | |
| 1565 | + <script> | |
| 1566 | + (function(){ | |
| 1567 | + if (window.__metasyncCopyInit) return; window.__metasyncCopyInit = true; | |
| 1568 | + 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); } | |
| 1569 | + function attach(){ | |
| 1570 | + document.querySelectorAll('.metasync-copy-snippet').forEach(function(pre){ | |
| 1571 | + if (pre.dataset.copyBound) return; pre.dataset.copyBound='1'; | |
| 1572 | + pre.setAttribute('title','Click to copy'); | |
| 1573 | + pre.addEventListener('click', function(){ | |
| 1574 | + var text = pre.textContent.trim(); | |
| 1575 | + var wrap = pre.closest('.metasync-copy-wrap') || pre.parentNode; | |
| 1576 | + var badge = wrap.querySelector('.metasync-copy-badge'); | |
| 1577 | + 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); }; | |
| 1578 | + if (navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(done).catch(function(){ fallback(text); done(); }); } | |
| 1579 | + else { fallback(text); done(); } | |
| 1580 | + }); | |
| 1581 | + }); | |
| 1582 | + } | |
| 1583 | + if (document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', attach); } else { attach(); } | |
| 1584 | + })(); | |
| 1585 | + </script> | |
| 1681 | 1586 | </div> |
| 1682 | 1587 | <?php $this->admin->render_layout_close(); ?> |
| 1683 | 1588 | <?php |
| 1684 | 1589 | } |
| @@ -1689,8 +1594,10 @@ | ||
| 1689 | 1594 | $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.'); |
| 1690 | 1595 | ?> |
| 1691 | 1596 | <form method="post" action="options.php"> |
| 1692 | 1597 | <div class="dashboard-card"> |
| 1598 | + <h2>Schema Markup Settings</h2> | |
| 1599 | + <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure the site-wide Organization and WebSite structured data injected into every page.</p> | |
| 1693 | 1600 | <?php |
| 1694 | 1601 | settings_fields(Metasync_Admin::option_group); |
| 1695 | 1602 | do_settings_sections($page_slug); |
| 1696 | 1603 | submit_button('Save Changes'); |
| @@ -1708,9 +1615,9 @@ | ||
| 1708 | 1615 | <?php $this->admin->render_layout_open('Local Business', 'local_business', 'Configure local business information for better local SEO.'); ?> |
| 1709 | 1616 | |
| 1710 | 1617 | <form method="post" action="options.php"> |
| 1711 | 1618 | <div class="dashboard-card"> |
| 1712 | - <h2>🏢 Local Business Configuration</h2> | |
| 1619 | + <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> | |
| 1713 | 1620 | <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure your local business information for better local search engine optimization.</p> |
| 1714 | 1621 | <?php |
| 1715 | 1622 | settings_fields(Metasync_Admin::option_group); |
| 1716 | 1623 | do_settings_sections($page_slug); |
| @@ -1742,24 +1649,20 @@ | ||
| 1742 | 1649 | <?php $this->admin->render_layout_close(); ?> |
| 1743 | 1650 | <?php |
| 1744 | 1651 | } |
| 1745 | 1652 | |
| 1746 | - public function create_admin_google_instant_index_page() | |
| 1747 | - { | |
| 1748 | - // Delegates to the admin class which uses the shared credentials view | |
| 1749 | - $this->admin->create_admin_google_instant_index_page(); | |
| 1750 | - } | |
| 1751 | - | |
| 1752 | 1653 | public function create_admin_google_console_page() |
| 1753 | 1654 | { |
| 1754 | - ?> | |
| 1755 | - <?php $this->admin->render_layout_open('Google Console', 'google_console', ''); ?> | |
| 1756 | - | |
| 1757 | - <?php | |
| 1758 | - google_index_direct()->show_google_instant_indexing_console(); | |
| 1759 | - ?> | |
| 1760 | - <?php $this->admin->render_layout_close(); ?> | |
| 1761 | - <?php | |
| 1655 | + $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.'); | |
| 1656 | + $google = function_exists('google_index_direct') ? google_index_direct() : null; | |
| 1657 | + if (is_null($google)) { | |
| 1658 | + $service_info = ['error' => 'Module not loaded']; | |
| 1659 | + } else { | |
| 1660 | + $service_info = $google->get_service_account_info(); | |
| 1661 | + } | |
| 1662 | + $is_configured = !isset($service_info['error']); | |
| 1663 | + include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php'; | |
| 1664 | + $this->admin->render_layout_close(); | |
| 1762 | 1665 | } |
| 1763 | 1666 | |
| 1764 | 1667 | public function create_admin_bing_console_page() |
| 1765 | 1668 | { |
| @@ -1774,33 +1677,8 @@ | ||
| 1774 | 1677 | <?php $this->admin->render_layout_close(); ?> |
| 1775 | 1678 | <?php |
| 1776 | 1679 | } |
| 1777 | 1680 | |
| 1778 | - public function create_admin_optimal_settings_page() | |
| 1779 | - { | |
| 1780 | - ?> | |
| 1781 | - <?php $this->admin->render_layout_open('Optimal Settings', 'optimal_settings', 'Apply recommended SEO settings automatically.'); ?> | |
| 1782 | - | |
| 1783 | - <div class="dashboard-card"> | |
| 1784 | - <h2>Site Compatibility Status</h2> | |
| 1785 | - <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Check your site's compatibility with optimal <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> settings.</p> | |
| 1786 | - <?php | |
| 1787 | - $optimal_settings = new Metasync_Optimal_Settings(); | |
| 1788 | - $optimal_settings->site_compatible_status_view(); | |
| 1789 | - ?> | |
| 1790 | - </div> | |
| 1791 | - | |
| 1792 | - <div class="dashboard-card"> | |
| 1793 | - <h2>Optimization Settings</h2> | |
| 1794 | - <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure optimization settings for best performance.</p> | |
| 1795 | - <?php | |
| 1796 | - $this->optimization_settings_options(); | |
| 1797 | - ?> | |
| 1798 | - </div> | |
| 1799 | - <?php $this->admin->render_layout_close(); ?> | |
| 1800 | - <?php | |
| 1801 | - } | |
| 1802 | - | |
| 1803 | 1681 | public function create_admin_global_settings_page() |
| 1804 | 1682 | { |
| 1805 | 1683 | $page_slug = Metasync_Admin::$page_slug . '_common-settings'; |
| 1806 | 1684 | ?> |
| @@ -1895,21 +1773,9 @@ | ||
| 1895 | 1773 | <?php $this->admin->render_layout_close(); ?> |
| 1896 | 1774 | <?php |
| 1897 | 1775 | } |
| 1898 | 1776 | |
| 1899 | - public function optimization_settings_options() | |
| 1900 | - { | |
| 1901 | - $page_slug = Metasync_Admin::$page_slug . '_optimal-settings'; | |
| 1902 | - $site_info_slug = Metasync_Admin::$page_slug . '_site-info-settings'; | |
| 1903 | 1777 | |
| 1904 | - printf('<form method="post" action="options.php">'); | |
| 1905 | - settings_fields(Metasync_Admin::option_group); | |
| 1906 | - do_settings_sections($page_slug); | |
| 1907 | - do_settings_sections($site_info_slug); | |
| 1908 | - submit_button(); | |
| 1909 | - printf('</form>'); | |
| 1910 | - } | |
| 1911 | - | |
| 1912 | 1778 | public function create_admin_error_logs_page() |
| 1913 | 1779 | { |
| 1914 | 1780 | ?> |
| 1915 | 1781 | <?php $this->admin->render_layout_open('Error Logs', 'general', ''); ?> |
| @@ -1951,177 +1817,15 @@ | ||
| 1951 | 1817 | <?php $this->admin->render_layout_close(); ?> |
| 1952 | 1818 | <?php |
| 1953 | 1819 | } |
| 1954 | 1820 | |
| 1955 | - public function create_admin_sync_log_page() | |
| 1956 | - { | |
| 1957 | - $sync_db = new Metasync_Sync_History_Database(); | |
| 1958 | - | |
| 1959 | - if (wp_doing_ajax()) { | |
| 1960 | - $this->handle_sync_log_ajax(); | |
| 1961 | - return; | |
| 1962 | - } | |
| 1963 | - | |
| 1964 | - $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; | |
| 1965 | - $per_page = 10; | |
| 1966 | - $offset = ($page - 1) * $per_page; | |
| 1967 | - | |
| 1968 | - $filters = [ | |
| 1969 | - 'date_range' => isset($_GET['date_range']) ? sanitize_text_field($_GET['date_range']) : '', | |
| 1970 | - 'status' => isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '', | |
| 1971 | - ]; | |
| 1821 | + // NOTE: The Changes Log (sync log) page is rendered by | |
| 1822 | + // Metasync_Admin::create_admin_sync_log_page() in | |
| 1823 | + // admin/class-metasync-admin.php, which is the callback registered in | |
| 1824 | + // class-metasync-admin-navigation.php. A former duplicate of that method | |
| 1825 | + // lived here but was never wired to a menu and has been removed to avoid | |
| 1826 | + // drift. | |
| 1972 | 1827 | |
| 1973 | - $date_range = $filters['date_range']; | |
| 1974 | - $wp_now_ts = current_time('timestamp'); | |
| 1975 | - $date_from = ''; | |
| 1976 | - $date_to = ''; | |
| 1977 | - | |
| 1978 | - if (!empty($date_range)) { | |
| 1979 | - $date_to = date('Y-m-d H:i:s', $wp_now_ts); | |
| 1980 | - | |
| 1981 | - if ($date_range === 'today') { | |
| 1982 | - $start_ts = strtotime('today', $wp_now_ts); | |
| 1983 | - $date_from = date('Y-m-d H:i:s', $start_ts); | |
| 1984 | - } elseif ($date_range === 'yesterday') { | |
| 1985 | - $start_ts = strtotime('yesterday', $wp_now_ts); | |
| 1986 | - $end_ts = strtotime('today', $wp_now_ts) - 1; | |
| 1987 | - $date_from = date('Y-m-d H:i:s', $start_ts); | |
| 1988 | - $date_to = date('Y-m-d H:i:s', $end_ts); | |
| 1989 | - } elseif ($date_range === 'this_week') { | |
| 1990 | - $start_of_week = (int) get_option('start_of_week', 1); | |
| 1991 | - $day_of_week = (int) date('w', $wp_now_ts); | |
| 1992 | - $delta_days = ($day_of_week - $start_of_week + 7) % 7; | |
| 1993 | - $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts)); | |
| 1994 | - $date_from = date('Y-m-d H:i:s', $start_ts); | |
| 1995 | - } elseif ($date_range === 'this_month') { | |
| 1996 | - $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts)); | |
| 1997 | - $date_from = date('Y-m-d H:i:s', $start_ts); | |
| 1998 | - } elseif ($date_range === 'all') { | |
| 1999 | - // no bounds | |
| 2000 | - } | |
| 2001 | - } | |
| 2002 | - | |
| 2003 | - if (!empty($date_from)) { | |
| 2004 | - $filters['date_from'] = $date_from; | |
| 2005 | - } | |
| 2006 | - if (!empty($date_to)) { | |
| 2007 | - $filters['date_to'] = $date_to; | |
| 2008 | - } | |
| 2009 | - | |
| 2010 | - $filters = array_filter($filters); | |
| 2011 | - | |
| 2012 | - $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters); | |
| 2013 | - $total_records = $sync_db->get_count($filters); | |
| 2014 | - $total_pages = ceil($total_records / $per_page); | |
| 2015 | - | |
| 2016 | - $stats = $sync_db->get_statistics(); | |
| 2017 | - | |
| 2018 | - ?> | |
| 2019 | - <?php $this->admin->render_layout_open('Changes Log', 'sync_log', 'Track all SEO changes made by the plugin.'); ?> | |
| 2020 | - | |
| 2021 | - <div class="dashboard-card"> | |
| 2022 | - <div class="sync-log-header"> | |
| 2023 | - <div class="sync-log-title-section"> | |
| 2024 | - <h2>Changes Log</h2> | |
| 2025 | - <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">Recent content synchronizations from external tools.</p> | |
| 2026 | - </div> | |
| 2027 | - | |
| 2028 | - <!-- Filters - Right aligned --> | |
| 2029 | - <div class="sync-log-filters"> | |
| 2030 | - <form method="get" class="sync-filters-form" onchange="this.submit()"> | |
| 2031 | - <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>"> | |
| 2032 | - | |
| 2033 | - <select name="date_range" class="sync-filter-select"> | |
| 2034 | - <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option> | |
| 2035 | - <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option> | |
| 2036 | - <option value="yesterday" <?php selected($filters['date_range'] ?? '', 'yesterday'); ?>>Yesterday</option> | |
| 2037 | - <option value="this_week" <?php selected($filters['date_range'] ?? '', 'this_week'); ?>>This week</option> | |
| 2038 | - <option value="this_month" <?php selected($filters['date_range'] ?? '', 'this_month'); ?>>This month</option> | |
| 2039 | - </select> | |
| 2040 | - | |
| 2041 | - <select name="status" class="sync-filter-select"> | |
| 2042 | - <option value="" <?php selected($filters['status'] ?? '', ''); ?>>Status Filter</option> | |
| 2043 | - <option value="published" <?php selected($filters['status'] ?? '', 'published'); ?>>Published</option> | |
| 2044 | - <option value="draft" <?php selected($filters['status'] ?? '', 'draft'); ?>>Draft</option> | |
| 2045 | - </select> | |
| 2046 | - </form> | |
| 2047 | - </div> | |
| 2048 | - </div> | |
| 2049 | - | |
| 2050 | - <!-- Sync History List --> | |
| 2051 | - <div class="sync-log-list"> | |
| 2052 | - <?php if (empty($sync_records)): ?> | |
| 2053 | - <div class="sync-log-empty"> | |
| 2054 | - <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> | |
| 2055 | - <h3>No sync records found</h3> | |
| 2056 | - <p>Sync records will appear here when content/pages receive new updates.</p> | |
| 2057 | - </div> | |
| 2058 | - <?php else: ?> | |
| 2059 | - <?php foreach ($sync_records as $record): ?> | |
| 2060 | - <div class="sync-log-item"> | |
| 2061 | - <div class="sync-log-icon"> | |
| 2062 | - <div class="sync-icon-circle"> | |
| 2063 | - <span class="dashicons dashicons-media-default sync-icon"></span> | |
| 2064 | - </div> | |
| 2065 | - </div> | |
| 2066 | - | |
| 2067 | - <div class="sync-log-content"> | |
| 2068 | - <div class="sync-log-title"><?php echo esc_html($record->title); ?> | |
| 2069 | - <?php if (!empty($record->url)): ?> | |
| 2070 | - <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> | |
| 2071 | - <?php endif; ?> | |
| 2072 | - </div> | |
| 2073 | - <div class="sync-log-meta"> | |
| 2074 | - <?php echo esc_html( $this->time_elapsed_string($record->created_at) ); ?> | |
| 2075 | - </div> | |
| 2076 | - </div> | |
| 2077 | - | |
| 2078 | - <div class="sync-log-status"> | |
| 2079 | - <?php if ($record->status === 'published' || $record->status === 'publish'): ?> | |
| 2080 | - <span class="sync-status-badge sync-status-published"> | |
| 2081 | - <span class="sync-status-icon">✓</span> | |
| 2082 | - Published | |
| 2083 | - </span> | |
| 2084 | - <?php else: ?> | |
| 2085 | - <span class="sync-status-badge sync-status-draft"> | |
| 2086 | - <span class="sync-status-icon">i</span> | |
| 2087 | - Draft | |
| 2088 | - </span> | |
| 2089 | - <?php endif; ?> | |
| 2090 | - </div> | |
| 2091 | - </div> | |
| 2092 | - <?php endforeach; ?> | |
| 2093 | - <?php endif; ?> | |
| 2094 | - </div> | |
| 2095 | - | |
| 2096 | - <!-- Pagination --> | |
| 2097 | - <?php if ($total_pages > 1): ?> | |
| 2098 | - <div class="sync-log-pagination"> | |
| 2099 | - <div class="sync-log-pagination-info"> | |
| 2100 | - Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?> | |
| 2101 | - </div> | |
| 2102 | - | |
| 2103 | - <div class="sync-log-pagination-controls"> | |
| 2104 | - <?php if ($page > 1): ?> | |
| 2105 | - <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> | |
| 2106 | - <?php endif; ?> | |
| 2107 | - | |
| 2108 | - <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?> | |
| 2109 | - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" | |
| 2110 | - class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a> | |
| 2111 | - <?php endfor; ?> | |
| 2112 | - | |
| 2113 | - <?php if ($page < $total_pages): ?> | |
| 2114 | - <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> | |
| 2115 | - <?php endif; ?> | |
| 2116 | - </div> | |
| 2117 | - </div> | |
| 2118 | - <?php endif; ?> | |
| 2119 | - </div> | |
| 2120 | - <?php $this->admin->render_layout_close(); ?> | |
| 2121 | - <?php | |
| 2122 | - } | |
| 2123 | - | |
| 2124 | 1828 | public function creat_error_Logs_List() |
| 2125 | 1829 | { |
| 2126 | 1830 | // printf('<h1> Error Logs </h1>'); |
| 2127 | 1831 | // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list); |
| @@ -2150,59 +1854,11 @@ | ||
| 2150 | 1854 | require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php'; |
| 2151 | 1855 | } |
| 2152 | 1856 | |
| 2153 | 1857 | // ------------------------------------------------------------------ |
| 2154 | - // Private helpers (copied from Metasync_Admin, used only by pages above) | |
| 1858 | + // Private helpers | |
| 2155 | 1859 | // ------------------------------------------------------------------ |
| 2156 | - | |
| 2157 | - private function time_elapsed_string($datetime, $full = false) | |
| 2158 | - { | |
| 2159 | - if(empty($datetime)){ | |
| 2160 | - return ""; | |
| 2161 | - } | |
| 2162 | - # Stored timestamps use current_time('mysql') which is WP local time | |
| 2163 | - # with no timezone marker. Defaulting DateTime to UTC produces a diff | |
| 2164 | - # equal to the WP timezone offset (WP-350 QA: "4 hours ago" after a | |
| 2165 | - # 4-minute sync on non-UTC sites). | |
| 2166 | - $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC'); | |
| 2167 | - $now = new DateTime('now', $wp_tz); | |
| 2168 | - $ago = new DateTime($datetime, $wp_tz); | |
| 2169 | - | |
| 2170 | - $diff = $now->diff($ago); | |
| 2171 | - | |
| 2172 | - $string = [ | |
| 2173 | - 'y' => 'year', | |
| 2174 | - 'm' => 'month', | |
| 2175 | - 'w' => 'week', | |
| 2176 | - 'd' => 'day', | |
| 2177 | - 'h' => 'hour', | |
| 2178 | - 'i' => 'minute', | |
| 2179 | - 's' => 'second', | |
| 2180 | - ]; | |
| 2181 | - | |
| 2182 | - foreach ($string as $k => &$v) { | |
| 2183 | - if (isset($diff->$k) && $diff->$k) { | |
| 2184 | - $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : ''); | |
| 2185 | - } else { | |
| 2186 | - unset($string[$k]); | |
| 2187 | - } | |
| 2188 | - } | |
| 2189 | - if (!$full) $string = array_slice($string, 0, 1); | |
| 2190 | - return $string ? implode(', ', $string) . ' ago' : 'just now'; | |
| 2191 | - } | |
| 2192 | - | |
| 2193 | - private function build_filter_query_string($filters) | |
| 2194 | - { | |
| 2195 | - $query_parts = []; | |
| 2196 | - foreach ($filters as $key => $value) { | |
| 2197 | - if (!empty($value)) { | |
| 2198 | - $query_parts[] = $key . '=' . urlencode($value); | |
| 2199 | - } | |
| 2200 | - } | |
| 2201 | - return !empty($query_parts) ? '&' . implode('&', $query_parts) : ''; | |
| 2202 | - } | |
| 2203 | - | |
| 2204 | - private function handle_sync_log_ajax() | |
| 2205 | - { | |
| 2206 | - wp_die(); | |
| 2207 | - } | |
| 1860 | + // The former time_elapsed_string / build_filter_query_string / | |
| 1861 | + // handle_sync_log_ajax helpers here were only used by the dead | |
| 1862 | + // create_admin_sync_log_page() duplicate that has since been removed. The | |
| 1863 | + // live equivalents live on Metasync_Admin. | |
| 2208 | 1864 | } |