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
← All changes | includes/class-metasync-admin-pages.php +133 -471 2.6.5 → 2.7.2 View file →
@@ -217,10 +217,15 @@
217 217 // ------------------------------------------------------------------
218 218
219 219 public function create_admin_settings_page()
220 220 {
221 - $active_tab = isset($_GET['tab']) ? $_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 ?>
@@ -409,9 +373,10 @@
409 373 }
410 374 }
411 375 ?>
412 376
413 - <form method="post" action="options.php?tab=<?php echo $active_tab?>" id="metaSyncGeneralSetting">
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 },
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 '>': '&gt;',
1252 1185 '"': '&quot;',
1253 1186 '\'': '&#039;'
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,79 +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 - echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>';
1536 - } else {
1537 - $message = esc_html__('Sitemap generated successfully!', 'metasync');
1538 - if ($disabled_plugins) {
1539 - $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
1540 - }
1541 -
1542 - $robots_result = get_transient('metasync_sitemap_robots_updated');
1543 - if ($robots_result && $robots_result['success']) {
1544 - if ($robots_result['action'] === 'added') {
1545 - $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
1546 - } elseif ($robots_result['action'] === 'updated') {
1547 - $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
1548 - } elseif ($robots_result['action'] === 'created') {
1549 - $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
1550 - }
1551 - delete_transient('metasync_sitemap_robots_updated');
1552 - }
1553 -
1554 - echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
1555 - }
1556 - } elseif (isset($_POST['enable_auto_update'])) {
1557 - update_option('metasync_sitemap_auto_update', true);
1558 - $sitemap_generator->setup_auto_update_hooks();
1559 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
1560 - } elseif (isset($_POST['disable_auto_update'])) {
1561 - update_option('metasync_sitemap_auto_update', false);
1562 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
1563 - } elseif (isset($_POST['delete_sitemap'])) {
1564 - $deleted = $sitemap_generator->delete_sitemap();
1565 - if ($deleted) {
1566 - update_option('metasync_sitemap_auto_update', false);
1567 - echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap deleted successfully!', 'metasync') . '</p></div>';
1568 - } else {
1569 - 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>';
1570 - }
1571 - } elseif (isset($_POST['enable_other_sitemaps'])) {
1572 - $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
1573 - if ($enabled_plugins) {
1574 - echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>';
1575 - } else {
1576 - echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>';
1577 - }
1578 - }
1579 - }
1580 -
1581 - $sitemap_exists = $sitemap_generator->sitemap_exists();
1582 - $sitemap_url = $sitemap_generator->get_sitemap_url();
1583 - $url_count = $sitemap_generator->count_urls();
1584 - $last_generated = $sitemap_generator->get_last_generated_time();
1585 - $auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
1586 - $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
1587 -
1588 - require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
1589 - }
1590 -
1591 1465 public function create_admin_custom_pages_page()
1592 1466 {
1593 1467 ?>
1594 1468 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
@@ -1639,12 +1513,14 @@
1639 1513 }
1640 1514
1641 1515 public function create_admin_breadcrumbs_page()
1642 1516 {
1643 - $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs';
1644 1517 ?>
1645 1518 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1646 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 +
1647 1523 <form method="post" action="options.php">
1648 1524 <div class="dashboard-card">
1649 1525 <h2>Breadcrumbs Settings</h2>
1650 1526 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
@@ -1649,9 +1525,9 @@
1649 1525 <h2>Breadcrumbs Settings</h2>
1650 1526 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1651 1527 <?php
1652 1528 settings_fields(Metasync_Admin::option_group);
1653 - do_settings_sections($page_slug);
1529 + Metasync_Settings_Fields::instance()->render_breadcrumbs_section();
1654 1530 submit_button();
1655 1531 ?>
1656 1532 </div>
1657 1533 </form>
@@ -1661,23 +1537,53 @@
1661 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>
1662 1538
1663 1539 <h3 style="margin-top: 0;">Option 1 — Shortcode</h3>
1664 1540 <p style="color: var(--dashboard-text-secondary);">Paste the shortcode inside any page, post, or widget:</p>
1665 - <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>
1666 1542
1667 1543 <h3>Option 2 — PHP Template Function</h3>
1668 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>
1669 - <pre style="background: var(--dashboard-bg, #1e1e1e); color: var(--dashboard-text, #e0e0e0); padding: 12px 16px; border-radius: 6px; font-size: 13px; overflow-x: auto;">&lt;?php metasync_breadcrumb(); ?&gt;</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;">&lt;?php metasync_breadcrumb(); ?&gt;</pre><span class="metasync-copy-badge">Copy</span></div>
1670 1546
1671 1547 <h3>Option 3 — PHP Return Value</h3>
1672 1548 <p style="color: var(--dashboard-text-secondary);">Get the breadcrumb HTML as a string to manipulate before output:</p>
1673 - <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();
1674 -echo $breadcrumbs-&gt;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-&gt;render_breadcrumb_html();</pre><span class="metasync-copy-badge">Copy</span></div>
1675 1551
1676 1552 <p style="color: var(--dashboard-text-secondary); margin-top: 16px; font-size: 12px;">
1677 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>
1678 1554 <code>echo $breadcrumbs-&gt;render_breadcrumb_html(['post_id' =&gt; 123]);</code>
1679 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>
1680 1586 </div>
1681 1587 <?php $this->admin->render_layout_close(); ?>
1682 1588 <?php
1683 1589 }
@@ -1688,8 +1594,10 @@
1688 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.');
1689 1595 ?>
1690 1596 <form method="post" action="options.php">
1691 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>
1692 1600 <?php
1693 1601 settings_fields(Metasync_Admin::option_group);
1694 1602 do_settings_sections($page_slug);
1695 1603 submit_button('Save Changes');
@@ -1707,9 +1615,9 @@
1707 1615 <?php $this->admin->render_layout_open('Local Business', 'local_business', 'Configure local business information for better local SEO.'); ?>
1708 1616
1709 1617 <form method="post" action="options.php">
1710 1618 <div class="dashboard-card">
1711 - <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>
1712 1620 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure your local business information for better local search engine optimization.</p>
1713 1621 <?php
1714 1622 settings_fields(Metasync_Admin::option_group);
1715 1623 do_settings_sections($page_slug);
@@ -1741,24 +1649,20 @@
1741 1649 <?php $this->admin->render_layout_close(); ?>
1742 1650 <?php
1743 1651 }
1744 1652
1745 - public function create_admin_google_instant_index_page()
1746 - {
1747 - // Delegates to the admin class which uses the shared credentials view
1748 - $this->admin->create_admin_google_instant_index_page();
1749 - }
1750 -
1751 1653 public function create_admin_google_console_page()
1752 1654 {
1753 - ?>
1754 - <?php $this->admin->render_layout_open('Google Console', 'google_console', ''); ?>
1755 -
1756 - <?php
1757 - google_index_direct()->show_google_instant_indexing_console();
1758 - ?>
1759 - <?php $this->admin->render_layout_close(); ?>
1760 - <?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();
1761 1665 }
1762 1666
1763 1667 public function create_admin_bing_console_page()
1764 1668 {
@@ -1773,33 +1677,8 @@
1773 1677 <?php $this->admin->render_layout_close(); ?>
1774 1678 <?php
1775 1679 }
1776 1680
1777 - public function create_admin_optimal_settings_page()
1778 - {
1779 - ?>
1780 - <?php $this->admin->render_layout_open('Optimal Settings', 'optimal_settings', 'Apply recommended SEO settings automatically.'); ?>
1781 -
1782 - <div class="dashboard-card">
1783 - <h2>Site Compatibility Status</h2>
1784 - <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>
1785 - <?php
1786 - $optimal_settings = new Metasync_Optimal_Settings();
1787 - $optimal_settings->site_compatible_status_view();
1788 - ?>
1789 - </div>
1790 -
1791 - <div class="dashboard-card">
1792 - <h2>Optimization Settings</h2>
1793 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure optimization settings for best performance.</p>
1794 - <?php
1795 - $this->optimization_settings_options();
1796 - ?>
1797 - </div>
1798 - <?php $this->admin->render_layout_close(); ?>
1799 - <?php
1800 - }
1801 -
1802 1681 public function create_admin_global_settings_page()
1803 1682 {
1804 1683 $page_slug = Metasync_Admin::$page_slug . '_common-settings';
1805 1684 ?>
@@ -1894,21 +1773,9 @@
1894 1773 <?php $this->admin->render_layout_close(); ?>
1895 1774 <?php
1896 1775 }
1897 1776
1898 - public function optimization_settings_options()
1899 - {
1900 - $page_slug = Metasync_Admin::$page_slug . '_optimal-settings';
1901 - $site_info_slug = Metasync_Admin::$page_slug . '_site-info-settings';
1902 1777
1903 - printf('<form method="post" action="options.php">');
1904 - settings_fields(Metasync_Admin::option_group);
1905 - do_settings_sections($page_slug);
1906 - do_settings_sections($site_info_slug);
1907 - submit_button();
1908 - printf('</form>');
1909 - }
1910 -
1911 1778 public function create_admin_error_logs_page()
1912 1779 {
1913 1780 ?>
1914 1781 <?php $this->admin->render_layout_open('Error Logs', 'general', ''); ?>
@@ -1950,177 +1817,15 @@
1950 1817 <?php $this->admin->render_layout_close(); ?>
1951 1818 <?php
1952 1819 }
1953 1820
1954 - public function create_admin_sync_log_page()
1955 - {
1956 - $sync_db = new Metasync_Sync_History_Database();
1957 -
1958 - if (wp_doing_ajax()) {
1959 - $this->handle_sync_log_ajax();
1960 - return;
1961 - }
1962 -
1963 - $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
1964 - $per_page = 10;
1965 - $offset = ($page - 1) * $per_page;
1966 -
1967 - $filters = [
1968 - 'date_range' => isset($_GET['date_range']) ? sanitize_text_field($_GET['date_range']) : '',
1969 - 'status' => isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '',
1970 - ];
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.
1971 1827
1972 - $date_range = $filters['date_range'];
1973 - $wp_now_ts = current_time('timestamp');
1974 - $date_from = '';
1975 - $date_to = '';
1976 -
1977 - if (!empty($date_range)) {
1978 - $date_to = date('Y-m-d H:i:s', $wp_now_ts);
1979 -
1980 - if ($date_range === 'today') {
1981 - $start_ts = strtotime('today', $wp_now_ts);
1982 - $date_from = date('Y-m-d H:i:s', $start_ts);
1983 - } elseif ($date_range === 'yesterday') {
1984 - $start_ts = strtotime('yesterday', $wp_now_ts);
1985 - $end_ts = strtotime('today', $wp_now_ts) - 1;
1986 - $date_from = date('Y-m-d H:i:s', $start_ts);
1987 - $date_to = date('Y-m-d H:i:s', $end_ts);
1988 - } elseif ($date_range === 'this_week') {
1989 - $start_of_week = (int) get_option('start_of_week', 1);
1990 - $day_of_week = (int) date('w', $wp_now_ts);
1991 - $delta_days = ($day_of_week - $start_of_week + 7) % 7;
1992 - $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
1993 - $date_from = date('Y-m-d H:i:s', $start_ts);
1994 - } elseif ($date_range === 'this_month') {
1995 - $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
1996 - $date_from = date('Y-m-d H:i:s', $start_ts);
1997 - } elseif ($date_range === 'all') {
1998 - // no bounds
1999 - }
2000 - }
2001 -
2002 - if (!empty($date_from)) {
2003 - $filters['date_from'] = $date_from;
2004 - }
2005 - if (!empty($date_to)) {
2006 - $filters['date_to'] = $date_to;
2007 - }
2008 -
2009 - $filters = array_filter($filters);
2010 -
2011 - $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters);
2012 - $total_records = $sync_db->get_count($filters);
2013 - $total_pages = ceil($total_records / $per_page);
2014 -
2015 - $stats = $sync_db->get_statistics();
2016 -
2017 - ?>
2018 - <?php $this->admin->render_layout_open('Changes Log', 'sync_log', 'Track all SEO changes made by the plugin.'); ?>
2019 -
2020 - <div class="dashboard-card">
2021 - <div class="sync-log-header">
2022 - <div class="sync-log-title-section">
2023 - <h2>Changes Log</h2>
2024 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">Recent content synchronizations from external tools.</p>
2025 - </div>
2026 -
2027 - <!-- Filters - Right aligned -->
2028 - <div class="sync-log-filters">
2029 - <form method="get" class="sync-filters-form" onchange="this.submit()">
2030 - <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>">
2031 -
2032 - <select name="date_range" class="sync-filter-select">
2033 - <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
2034 - <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
2035 - <option value="yesterday" <?php selected($filters['date_range'] ?? '', 'yesterday'); ?>>Yesterday</option>
2036 - <option value="this_week" <?php selected($filters['date_range'] ?? '', 'this_week'); ?>>This week</option>
2037 - <option value="this_month" <?php selected($filters['date_range'] ?? '', 'this_month'); ?>>This month</option>
2038 - </select>
2039 -
2040 - <select name="status" class="sync-filter-select">
2041 - <option value="" <?php selected($filters['status'] ?? '', ''); ?>>Status Filter</option>
2042 - <option value="published" <?php selected($filters['status'] ?? '', 'published'); ?>>Published</option>
2043 - <option value="draft" <?php selected($filters['status'] ?? '', 'draft'); ?>>Draft</option>
2044 - </select>
2045 - </form>
2046 - </div>
2047 - </div>
2048 -
2049 - <!-- Sync History List -->
2050 - <div class="sync-log-list">
2051 - <?php if (empty($sync_records)): ?>
2052 - <div class="sync-log-empty">
2053 - <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>
2054 - <h3>No sync records found</h3>
2055 - <p>Sync records will appear here when content/pages receive new updates.</p>
2056 - </div>
2057 - <?php else: ?>
2058 - <?php foreach ($sync_records as $record): ?>
2059 - <div class="sync-log-item">
2060 - <div class="sync-log-icon">
2061 - <div class="sync-icon-circle">
2062 - <span class="dashicons dashicons-media-default sync-icon"></span>
2063 - </div>
2064 - </div>
2065 -
2066 - <div class="sync-log-content">
2067 - <div class="sync-log-title"><?php echo esc_html($record->title); ?>
2068 - <?php if (!empty($record->url)): ?>
2069 - <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>
2070 - <?php endif; ?>
2071 - </div>
2072 - <div class="sync-log-meta">
2073 - <?php echo $this->time_elapsed_string($record->created_at); ?>
2074 - </div>
2075 - </div>
2076 -
2077 - <div class="sync-log-status">
2078 - <?php if ($record->status === 'published' || $record->status === 'publish'): ?>
2079 - <span class="sync-status-badge sync-status-published">
2080 - <span class="sync-status-icon">✓</span>
2081 - Published
2082 - </span>
2083 - <?php else: ?>
2084 - <span class="sync-status-badge sync-status-draft">
2085 - <span class="sync-status-icon">i</span>
2086 - Draft
2087 - </span>
2088 - <?php endif; ?>
2089 - </div>
2090 - </div>
2091 - <?php endforeach; ?>
2092 - <?php endif; ?>
2093 - </div>
2094 -
2095 - <!-- Pagination -->
2096 - <?php if ($total_pages > 1): ?>
2097 - <div class="sync-log-pagination">
2098 - <div class="sync-log-pagination-info">
2099 - Total records: <?php echo $total_records; ?> | Showing <?php echo $offset + 1; ?>-<?php echo min($offset + $per_page, $total_records); ?>
2100 - </div>
2101 -
2102 - <div class="sync-log-pagination-controls">
2103 - <?php if ($page > 1): ?>
2104 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo $page - 1; ?><?php echo $this->build_filter_query_string($filters); ?>" class="sync-pagination-btn">‹</a>
2105 - <?php endif; ?>
2106 -
2107 - <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
2108 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo $i; ?><?php echo $this->build_filter_query_string($filters); ?>"
2109 - class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo $i; ?></a>
2110 - <?php endfor; ?>
2111 -
2112 - <?php if ($page < $total_pages): ?>
2113 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo $page + 1; ?><?php echo $this->build_filter_query_string($filters); ?>" class="sync-pagination-btn">›</a>
2114 - <?php endif; ?>
2115 - </div>
2116 - </div>
2117 - <?php endif; ?>
2118 - </div>
2119 - <?php $this->admin->render_layout_close(); ?>
2120 - <?php
2121 - }
2122 -
2123 1828 public function creat_error_Logs_List()
2124 1829 {
2125 1830 // printf('<h1> Error Logs </h1>');
2126 1831 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
@@ -2149,54 +1854,11 @@
2149 1854 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
2150 1855 }
2151 1856
2152 1857 // ------------------------------------------------------------------
2153 - // Private helpers (copied from Metasync_Admin, used only by pages above)
1858 + // Private helpers
2154 1859 // ------------------------------------------------------------------
2155 -
2156 - private function time_elapsed_string($datetime, $full = false)
2157 - {
2158 - if(empty($datetime)){
2159 - return "";
2160 - }
2161 - $now = new DateTime;
2162 - $ago = new DateTime($datetime);
2163 -
2164 - $diff = $now->diff($ago);
2165 -
2166 - $string = [
2167 - 'y' => 'year',
2168 - 'm' => 'month',
2169 - 'w' => 'week',
2170 - 'd' => 'day',
2171 - 'h' => 'hour',
2172 - 'i' => 'minute',
2173 - 's' => 'second',
2174 - ];
2175 -
2176 - foreach ($string as $k => &$v) {
2177 - if (isset($diff->$k) && $diff->$k) {
2178 - $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
2179 - } else {
2180 - unset($string[$k]);
2181 - }
2182 - }
2183 - if (!$full) $string = array_slice($string, 0, 1);
2184 - return $string ? implode(', ', $string) . ' ago' : 'just now';
2185 - }
2186 -
2187 - private function build_filter_query_string($filters)
2188 - {
2189 - $query_parts = [];
2190 - foreach ($filters as $key => $value) {
2191 - if (!empty($value)) {
2192 - $query_parts[] = $key . '=' . urlencode($value);
2193 - }
2194 - }
2195 - return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
2196 - }
2197 -
2198 - private function handle_sync_log_ajax()
2199 - {
2200 - wp_die();
2201 - }
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.
2202 1864 }