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 +89 -465 2.6.16 → 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']) ? 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 }
@@ -918,42 +887,8 @@
918 887 $('#metasync-custom-modal').fadeOut(200);
919 888 }
920 889 });
921 890
922 - $('#metasync-export-whitelabel-btn').on('click', function(e) {
923 - e.preventDefault();
924 -
925 - var $button = $(this);
926 - var originalText = $button.html();
927 -
928 - $button.prop('disabled', true).html('⏳ Exporting...');
929 -
930 - var form = $('<form>', {
931 - 'method': 'POST',
932 - 'action': '<?php echo esc_js(admin_url('admin-post.php')); ?>',
933 - 'target': '_blank'
934 - });
935 -
936 - form.append($('<input>', {
937 - 'type': 'hidden',
938 - 'name': 'action',
939 - 'value': 'metasync_export_whitelabel_settings'
940 - }));
941 -
942 - form.append($('<input>', {
943 - 'type': 'hidden',
944 - 'name': '_wpnonce',
945 - 'value': '<?php echo wp_create_nonce('metasync_export_whitelabel'); ?>'
946 - }));
947 -
948 - $('body').append(form);
949 - form.submit();
950 -
951 - setTimeout(function() {
952 - form.remove();
953 - $button.prop('disabled', false).html(originalText);
954 - }, 2000);
955 - });
956 891 });
957 892 </script>
958 893 <?php
959 894 } elseif ($active_tab == 'advanced') {
@@ -983,10 +918,10 @@
983 918 </div>
984 919 <?php endif; ?>
985 920
986 921 <?php
987 - if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'):
988 - $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
989 924 ?>
990 925 <div class="notice notice-success inline" style="margin-bottom: 20px;">
991 926 <p>
992 927 <strong>✅ Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>.
@@ -993,9 +928,9 @@
993 928 </p>
994 929 </div>
995 930 <?php endif; ?>
996 931
997 - <?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 ?>
998 933 <div class="notice notice-info inline" style="margin-bottom: 20px;">
999 934 <p>
1000 935 <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled.
1001 936 </p>
@@ -1001,9 +936,9 @@
1001 936 </p>
1002 937 </div>
1003 938 <?php endif; ?>
1004 939
1005 - <?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 ?>
1006 941 <div class="notice notice-success inline" style="margin-bottom: 20px;">
1007 942 <p>
1008 943 <strong>✅ Extended!</strong> Debug mode has been extended for another 24 hours.
1009 944 </p>
@@ -1009,9 +944,9 @@
1009 944 </p>
1010 945 </div>
1011 946 <?php endif; ?>
1012 947
1013 - <?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 ?>
1014 949 <div class="notice notice-error inline" style="margin-bottom: 20px;">
1015 950 <p>
1016 951 <strong>❌ Error:</strong> Unable to perform the debug mode operation. Please try again.
1017 952 </p>
@@ -1048,9 +983,9 @@
1048 983 'method': 'post',
1049 984 'action': ''
1050 985 });
1051 986
1052 - 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 ?>');
1053 988
1054 989 logoutForm.append($('<input>', {
1055 990 'type': 'hidden',
1056 991 'name': 'whitelabel_logout',
@@ -1122,9 +1057,9 @@
1122 1057 $.ajax({
1123 1058 url: ajaxUrl,
1124 1059 type: 'POST',
1125 1060 dataType: 'json',
1126 - 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')); ?>' },
1127 1062 timeout: 35000,
1128 1063 success: function(response, textStatus, xhr) {
1129 1064 try {
1130 1065 if (response && response.success && response.data) {
@@ -1192,22 +1127,22 @@
1192 1127 var blockedClass = result.blocked ? 'blocked' : 'allowed';
1193 1128
1194 1129 html += '<div class="test-result-item ' + statusClass + '">';
1195 1130 html += '<div class="test-result-header">';
1196 - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
1131 + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
1197 1132 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
1198 1133 html += '</div>';
1199 1134
1200 1135 html += '<div class="test-result-details">';
1201 - html += '<p><strong>Response Time:</strong> ' + result.response_time + '</p>';
1202 - 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>';
1203 1138
1204 1139 if (result.status_code) {
1205 - 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>';
1206 1141 }
1207 1142
1208 1143 if (result.error) {
1209 - 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>';
1210 1145 }
1211 1146
1212 1147 if (result.body) {
1213 1148 html += '<p><strong>Response Body:</strong></p>';
@@ -1217,9 +1152,9 @@
1217 1152 if (result.headers && Object.keys(result.headers).length > 0) {
1218 1153 html += '<p><strong>Response Headers:</strong></p>';
1219 1154 html += '<pre class="response-headers">';
1220 1155 Object.keys(result.headers).forEach(function(key) {
1221 - html += key + ': ' + result.headers[key] + '\n';
1156 + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
1222 1157 });
1223 1158 html += '</pre>';
1224 1159 }
1225 1160
@@ -1232,9 +1167,9 @@
1232 1167 html += '<p><strong>Parsed Response:</strong></p>';
1233 1168 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
1234 1169 }
1235 1170
1236 - html += '<p><strong>Details:</strong> ' + result.details + '</p>';
1171 + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
1237 1172 html += '</div>';
1238 1173 html += '</div>';
1239 1174 });
1240 1175
@@ -1249,9 +1184,9 @@
1249 1184 '>': '&gt;',
1250 1185 '"': '&quot;',
1251 1186 '\'': '&#039;'
1252 1187 };
1253 - return text.replace(/[&<>"']/g, function(m) { return map[m]; });
1188 + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
1254 1189 }
1255 1190 });
1256 1191 </script>
1257 1192
@@ -1384,41 +1319,41 @@
1384 1319 }
1385 1320 </style>
1386 1321
1387 1322 <?php if ($active_tab === 'advanced'): ?>
1388 - <?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 ?>
1389 1324 <div class="notice notice-success is-dismissible">
1390 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>
1391 1326 </div>
1392 1327 <?php endif; ?>
1393 1328
1394 - <?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 ?>
1395 1330 <div class="notice notice-error is-dismissible">
1396 1331 <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p>
1397 1332 </div>
1398 1333 <?php endif; ?>
1399 1334
1400 - <?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 ?>
1401 1336 <div class="notice notice-success is-dismissible">
1402 1337 <p><strong>✅ Success!</strong> Error logs have been cleared successfully.</p>
1403 1338 </div>
1404 1339 <?php endif; ?>
1405 1340
1406 - <?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 ?>
1407 1342 <div class="notice notice-error is-dismissible">
1408 1343 <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p>
1409 1344 </div>
1410 1345 <?php endif; ?>
1411 1346
1412 - <?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 ?>
1413 1348 <div class="notice notice-success is-dismissible">
1414 1349 <p><strong>✅ Success!</strong> Plugin access roles have been saved successfully.</p>
1415 1350 </div>
1416 1351 <?php endif; ?>
1417 1352
1418 - <?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 ?>
1419 1354 <div class="notice notice-error is-dismissible">
1420 - <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>
1421 1356 </div>
1422 1357 <?php endif; ?>
1423 1358
1424 1359 <?php endif; ?>
@@ -1440,9 +1375,9 @@
1440 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>
1441 1376 <?php
1442 1377 if (!isset(Metasync::get_option('general')['linkgraph_token']) || Metasync::get_option('general')['linkgraph_token'] == '') {
1443 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>';
1444 - 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>';
1445 1380 } else {
1446 1381 echo '<a href="' . esc_url($this->admin->get_dashboard_url()) . '" target="_blank" class="button button-primary">Open Dashboard</a>';
1447 1382 }
1448 1383 ?>
@@ -1452,8 +1387,15 @@
1452 1387 }
1453 1388
1454 1389 public function create_admin_robots_txt_page()
1455 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 +
1456 1398 ?>
1457 1399 <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?>
1458 1400
1459 1401 <?php
@@ -1474,8 +1416,11 @@
1474 1416
1475 1417 if (is_wp_error($result)) {
1476 1418 echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>';
1477 1419 } else {
1420 + Metasync_PostHog::feature('robots_txt_saved', array(
1421 + 'warning_count' => count($validation['warnings']),
1422 + ));
1478 1423 echo '<div class="notice notice-success"><p>' . esc_html__('robots.txt file saved successfully!', 'metasync') . '</p></div>';
1479 1424
1480 1425 if (!empty($validation['warnings'])) {
1481 1426 foreach ($validation['warnings'] as $warning) {
@@ -1498,9 +1443,11 @@
1498 1443 echo '<div class="notice notice-error"><p>' . esc_html($current_content->get_error_message()) . '</p></div>';
1499 1444 $current_content = '';
1500 1445 }
1501 1446
1502 - $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);
1503 1450
1504 1451 $file_exists = $robots_txt->file_exists();
1505 1452 $is_writable = $robots_txt->is_writable();
1506 1453
@@ -1514,82 +1461,8 @@
1514 1461 {
1515 1462 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1516 1463 }
1517 1464
1518 - public function create_admin_xml_sitemap_page()
1519 - {
1520 - require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
1521 -
1522 - $sitemap_generator = new Metasync_Sitemap_Generator();
1523 -
1524 - if (isset($_POST['metasync_sitemap_nonce'])) {
1525 - check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce');
1526 -
1527 - if (isset($_POST['generate_sitemap'])) {
1528 - $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators();
1529 -
1530 - $result = $sitemap_generator->generate_sitemap();
1531 -
1532 - if (is_wp_error($result)) {
1533 - error_log('[MetaSync] Sitemap generation failed: ' . $result->get_error_message());
1534 - echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed. Please try again or check the error logs.', 'metasync') . '</p></div>';
1535 - } else {
1536 - $message = esc_html__('Sitemap generated successfully!', 'metasync');
1537 - if ($disabled_plugins) {
1538 - $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
1539 - }
1540 -
1541 - $robots_result = get_transient('metasync_sitemap_robots_updated');
1542 - if ($robots_result && $robots_result['success']) {
1543 - if ($robots_result['action'] === 'added') {
1544 - $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
1545 - } elseif ($robots_result['action'] === 'updated') {
1546 - $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
1547 - } elseif ($robots_result['action'] === 'created') {
1548 - $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
1549 - }
1550 - delete_transient('metasync_sitemap_robots_updated');
1551 - }
1552 -
1553 - echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
1554 - }
1555 - } elseif (isset($_POST['enable_auto_update'])) {
1556 - update_option('metasync_sitemap_auto_update', true);
1557 - $sitemap_generator->setup_auto_update_hooks();
1558 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
1559 - } elseif (isset($_POST['disable_auto_update'])) {
1560 - update_option('metasync_sitemap_auto_update', false);
1561 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
1562 - } elseif (isset($_POST['delete_sitemap'])) {
1563 - $deleted = $sitemap_generator->delete_sitemap();
1564 - if ($deleted) {
1565 - update_option('metasync_sitemap_auto_update', false);
1566 - // Re-enable WP core sitemap so the site isn't left with zero sitemaps (WP-396)
1567 - delete_option('metasync_disable_wp_sitemap');
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>
@@ -1677,9 +1552,9 @@
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-&gt;render_breadcrumb_html(['post_id' =&gt; 123]);</code>
1680 1555 </p>
1681 - <?php /* WP-492: click-to-copy for the code snippets above */ ?>
1556 + <?php /* click-to-copy for the code snippets above */ ?>
1682 1557 <style>
1683 1558 .metasync-copy-wrap { position: relative; }
1684 1559 .metasync-copy-snippet { cursor: pointer; outline: 1px solid transparent; transition: outline-color .15s ease; }
1685 1560 .metasync-copy-wrap:hover .metasync-copy-snippet { outline-color: var(--dashboard-border, #374151); }
@@ -1719,8 +1594,10 @@
1719 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.');
1720 1595 ?>
1721 1596 <form method="post" action="options.php">
1722 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>
1723 1600 <?php
1724 1601 settings_fields(Metasync_Admin::option_group);
1725 1602 do_settings_sections($page_slug);
1726 1603 submit_button('Save Changes');
@@ -1738,9 +1615,9 @@
1738 1615 <?php $this->admin->render_layout_open('Local Business', 'local_business', 'Configure local business information for better local SEO.'); ?>
1739 1616
1740 1617 <form method="post" action="options.php">
1741 1618 <div class="dashboard-card">
1742 - <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>
1743 1620 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure your local business information for better local search engine optimization.</p>
1744 1621 <?php
1745 1622 settings_fields(Metasync_Admin::option_group);
1746 1623 do_settings_sections($page_slug);
@@ -1772,14 +1649,8 @@
1772 1649 <?php $this->admin->render_layout_close(); ?>
1773 1650 <?php
1774 1651 }
1775 1652
1776 - public function create_admin_google_instant_index_page()
1777 - {
1778 - // Delegates to the admin class which uses the shared credentials view
1779 - $this->admin->create_admin_google_instant_index_page();
1780 - }
1781 -
1782 1653 public function create_admin_google_console_page()
1783 1654 {
1784 1655 $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
1785 1656 $google = function_exists('google_index_direct') ? google_index_direct() : null;
@@ -1806,33 +1677,8 @@
1806 1677 <?php $this->admin->render_layout_close(); ?>
1807 1678 <?php
1808 1679 }
1809 1680
1810 - public function create_admin_optimal_settings_page()
1811 - {
1812 - ?>
1813 - <?php $this->admin->render_layout_open('Optimal Settings', 'optimal_settings', 'Apply recommended SEO settings automatically.'); ?>
1814 -
1815 - <div class="dashboard-card">
1816 - <h2>Site Compatibility Status</h2>
1817 - <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>
1818 - <?php
1819 - $optimal_settings = new Metasync_Optimal_Settings();
1820 - $optimal_settings->site_compatible_status_view();
1821 - ?>
1822 - </div>
1823 -
1824 - <div class="dashboard-card">
1825 - <h2>Optimization Settings</h2>
1826 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure optimization settings for best performance.</p>
1827 - <?php
1828 - $this->optimization_settings_options();
1829 - ?>
1830 - </div>
1831 - <?php $this->admin->render_layout_close(); ?>
1832 - <?php
1833 - }
1834 -
1835 1681 public function create_admin_global_settings_page()
1836 1682 {
1837 1683 $page_slug = Metasync_Admin::$page_slug . '_common-settings';
1838 1684 ?>
@@ -1927,21 +1773,9 @@
1927 1773 <?php $this->admin->render_layout_close(); ?>
1928 1774 <?php
1929 1775 }
1930 1776
1931 - public function optimization_settings_options()
1932 - {
1933 - $page_slug = Metasync_Admin::$page_slug . '_optimal-settings';
1934 - $site_info_slug = Metasync_Admin::$page_slug . '_site-info-settings';
1935 1777
1936 - printf('<form method="post" action="options.php">');
1937 - settings_fields(Metasync_Admin::option_group);
1938 - do_settings_sections($page_slug);
1939 - do_settings_sections($site_info_slug);
1940 - submit_button();
1941 - printf('</form>');
1942 - }
1943 -
1944 1778 public function create_admin_error_logs_page()
1945 1779 {
1946 1780 ?>
1947 1781 <?php $this->admin->render_layout_open('Error Logs', 'general', ''); ?>
@@ -1983,177 +1817,15 @@
1983 1817 <?php $this->admin->render_layout_close(); ?>
1984 1818 <?php
1985 1819 }
1986 1820
1987 - public function create_admin_sync_log_page()
1988 - {
1989 - $sync_db = new Metasync_Sync_History_Database();
1990 -
1991 - if (wp_doing_ajax()) {
1992 - $this->handle_sync_log_ajax();
1993 - return;
1994 - }
1995 -
1996 - $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
1997 - $per_page = 10;
1998 - $offset = ($page - 1) * $per_page;
1999 -
2000 - $filters = [
2001 - 'date_range' => isset($_GET['date_range']) ? sanitize_text_field($_GET['date_range']) : '',
2002 - 'status' => isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '',
2003 - ];
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.
2004 1827
2005 - $date_range = $filters['date_range'];
2006 - $wp_now_ts = current_time('timestamp');
2007 - $date_from = '';
2008 - $date_to = '';
2009 -
2010 - if (!empty($date_range)) {
2011 - $date_to = date('Y-m-d H:i:s', $wp_now_ts);
2012 -
2013 - if ($date_range === 'today') {
2014 - $start_ts = strtotime('today', $wp_now_ts);
2015 - $date_from = date('Y-m-d H:i:s', $start_ts);
2016 - } elseif ($date_range === 'yesterday') {
2017 - $start_ts = strtotime('yesterday', $wp_now_ts);
2018 - $end_ts = strtotime('today', $wp_now_ts) - 1;
2019 - $date_from = date('Y-m-d H:i:s', $start_ts);
2020 - $date_to = date('Y-m-d H:i:s', $end_ts);
2021 - } elseif ($date_range === 'this_week') {
2022 - $start_of_week = (int) get_option('start_of_week', 1);
2023 - $day_of_week = (int) date('w', $wp_now_ts);
2024 - $delta_days = ($day_of_week - $start_of_week + 7) % 7;
2025 - $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
2026 - $date_from = date('Y-m-d H:i:s', $start_ts);
2027 - } elseif ($date_range === 'this_month') {
2028 - $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
2029 - $date_from = date('Y-m-d H:i:s', $start_ts);
2030 - } elseif ($date_range === 'all') {
2031 - // no bounds
2032 - }
2033 - }
2034 -
2035 - if (!empty($date_from)) {
2036 - $filters['date_from'] = $date_from;
2037 - }
2038 - if (!empty($date_to)) {
2039 - $filters['date_to'] = $date_to;
2040 - }
2041 -
2042 - $filters = array_filter($filters);
2043 -
2044 - $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters);
2045 - $total_records = $sync_db->get_count($filters);
2046 - $total_pages = ceil($total_records / $per_page);
2047 -
2048 - $stats = $sync_db->get_statistics();
2049 -
2050 - ?>
2051 - <?php $this->admin->render_layout_open('Changes Log', 'sync_log', 'Track all SEO changes made by the plugin.'); ?>
2052 -
2053 - <div class="dashboard-card">
2054 - <div class="sync-log-header">
2055 - <div class="sync-log-title-section">
2056 - <h2>Changes Log</h2>
2057 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">Recent content synchronizations from external tools.</p>
2058 - </div>
2059 -
2060 - <!-- Filters - Right aligned -->
2061 - <div class="sync-log-filters">
2062 - <form method="get" class="sync-filters-form" onchange="this.submit()" style="display:flex;flex-direction:row;align-items:center;gap:12px;flex-wrap:nowrap;">
2063 - <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>">
2064 -
2065 - <select name="date_range" class="sync-filter-select">
2066 - <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
2067 - <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
2068 - <option value="yesterday" <?php selected($filters['date_range'] ?? '', 'yesterday'); ?>>Yesterday</option>
2069 - <option value="this_week" <?php selected($filters['date_range'] ?? '', 'this_week'); ?>>This week</option>
2070 - <option value="this_month" <?php selected($filters['date_range'] ?? '', 'this_month'); ?>>This month</option>
2071 - </select>
2072 -
2073 - <select name="status" class="sync-filter-select">
2074 - <option value="" <?php selected($filters['status'] ?? '', ''); ?>>Status Filter</option>
2075 - <option value="published" <?php selected($filters['status'] ?? '', 'published'); ?>>Published</option>
2076 - <option value="draft" <?php selected($filters['status'] ?? '', 'draft'); ?>>Draft</option>
2077 - </select>
2078 - </form>
2079 - </div>
2080 - </div>
2081 -
2082 - <!-- Sync History List -->
2083 - <div class="sync-log-list">
2084 - <?php if (empty($sync_records)): ?>
2085 - <div class="sync-log-empty">
2086 - <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>
2087 - <h3>No sync records found</h3>
2088 - <p>Sync records will appear here when content/pages receive new updates.</p>
2089 - </div>
2090 - <?php else: ?>
2091 - <?php foreach ($sync_records as $record): ?>
2092 - <div class="sync-log-item">
2093 - <div class="sync-log-icon">
2094 - <div class="sync-icon-circle">
2095 - <span class="dashicons dashicons-media-default sync-icon"></span>
2096 - </div>
2097 - </div>
2098 -
2099 - <div class="sync-log-content">
2100 - <div class="sync-log-title"><?php echo esc_html($record->title); ?>
2101 - <?php if (!empty($record->url)): ?>
2102 - <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>
2103 - <?php endif; ?>
2104 - </div>
2105 - <div class="sync-log-meta">
2106 - <?php echo esc_html( $this->time_elapsed_string($record->created_at) ); ?>
2107 - </div>
2108 - </div>
2109 -
2110 - <div class="sync-log-status">
2111 - <?php if ($record->status === 'published' || $record->status === 'publish'): ?>
2112 - <span class="sync-status-badge sync-status-published">
2113 - <span class="sync-status-icon">✓</span>
2114 - Published
2115 - </span>
2116 - <?php else: ?>
2117 - <span class="sync-status-badge sync-status-draft">
2118 - <span class="sync-status-icon">i</span>
2119 - Draft
2120 - </span>
2121 - <?php endif; ?>
2122 - </div>
2123 - </div>
2124 - <?php endforeach; ?>
2125 - <?php endif; ?>
2126 - </div>
2127 -
2128 - <!-- Pagination -->
2129 - <?php if ($total_pages > 1): ?>
2130 - <div class="sync-log-pagination">
2131 - <div class="sync-log-pagination-info">
2132 - Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?>
2133 - </div>
2134 -
2135 - <div class="sync-log-pagination-controls">
2136 - <?php if ($page > 1): ?>
2137 - <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>
2138 - <?php endif; ?>
2139 -
2140 - <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
2141 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>"
2142 - class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a>
2143 - <?php endfor; ?>
2144 -
2145 - <?php if ($page < $total_pages): ?>
2146 - <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>
2147 - <?php endif; ?>
2148 - </div>
2149 - </div>
2150 - <?php endif; ?>
2151 - </div>
2152 - <?php $this->admin->render_layout_close(); ?>
2153 - <?php
2154 - }
2155 -
2156 1828 public function creat_error_Logs_List()
2157 1829 {
2158 1830 // printf('<h1> Error Logs </h1>');
2159 1831 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
@@ -2182,59 +1854,11 @@
2182 1854 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
2183 1855 }
2184 1856
2185 1857 // ------------------------------------------------------------------
2186 - // Private helpers (copied from Metasync_Admin, used only by pages above)
1858 + // Private helpers
2187 1859 // ------------------------------------------------------------------
2188 -
2189 - private function time_elapsed_string($datetime, $full = false)
2190 - {
2191 - if(empty($datetime)){
2192 - return "";
2193 - }
2194 - # Stored timestamps use current_time('mysql') which is WP local time
2195 - # with no timezone marker. Defaulting DateTime to UTC produces a diff
2196 - # equal to the WP timezone offset (WP-350 QA: "4 hours ago" after a
2197 - # 4-minute sync on non-UTC sites).
2198 - $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
2199 - $now = new DateTime('now', $wp_tz);
2200 - $ago = new DateTime($datetime, $wp_tz);
2201 -
2202 - $diff = $now->diff($ago);
2203 -
2204 - $string = [
2205 - 'y' => 'year',
2206 - 'm' => 'month',
2207 - 'w' => 'week',
2208 - 'd' => 'day',
2209 - 'h' => 'hour',
2210 - 'i' => 'minute',
2211 - 's' => 'second',
2212 - ];
2213 -
2214 - foreach ($string as $k => &$v) {
2215 - if (isset($diff->$k) && $diff->$k) {
2216 - $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
2217 - } else {
2218 - unset($string[$k]);
2219 - }
2220 - }
2221 - if (!$full) $string = array_slice($string, 0, 1);
2222 - return $string ? implode(', ', $string) . ' ago' : 'just now';
2223 - }
2224 -
2225 - private function build_filter_query_string($filters)
2226 - {
2227 - $query_parts = [];
2228 - foreach ($filters as $key => $value) {
2229 - if (!empty($value)) {
2230 - $query_parts[] = $key . '=' . urlencode($value);
2231 - }
2232 - }
2233 - return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
2234 - }
2235 -
2236 - private function handle_sync_log_ajax()
2237 - {
2238 - wp_die();
2239 - }
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.
2240 1864 }