PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.1
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.1
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 +113 -464 2.6.6 → 2.7.1 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
@@ -333,8 +338,9 @@
333 338 </button>
334 339 </div>
335 340 </form>
336 341
342 + <?php if (current_user_can('manage_options')): ?>
337 343 <div style="text-align: center; margin-top: 20px;">
338 344 <a href="#" id="metasync-forgot-password-link" style="color: #2271b1; text-decoration: none; font-size: 14px;">
339 345 Forgot Password?
340 346 </a>
@@ -339,64 +345,12 @@
339 345 Forgot Password?
340 346 </a>
341 347 <div id="metasync-recovery-message" style="margin-top: 15px; padding: 12px; border-radius: 6px; display: none;"></div>
342 348 </div>
349 + <?php endif; ?>
343 350
344 351 </div>
345 352
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 353 <?php
400 354 return;
401 355 } elseif (!$password_protection_enabled) {
402 356 ?>
@@ -409,9 +363,10 @@
409 363 }
410 364 }
411 365 ?>
412 366
413 - <form method="post" action="options.php?tab=<?php echo $active_tab?>" id="metaSyncGeneralSetting">
367 + <form method="post" action="options.php?tab=<?php echo esc_attr( $active_tab ); ?>" id="metaSyncGeneralSetting">
368 + <input type="hidden" name="metasync_pw_reset_generation" value="<?php echo esc_attr((string) (int) get_option('metasync_pw_reset_generation', 0)); ?>" />
414 369 <?php
415 370 settings_fields(Metasync_Admin::option_group);
416 371
417 372 wp_nonce_field('meta_sync_general_setting_nonce', 'meta_sync_nonce');
@@ -439,14 +394,17 @@
439 394
440 395 <?php
441 396 $this->admin->render_accordion_sections(Metasync_Admin::$page_slug . '_general');
442 397 ?>
398 + <p style="margin: 20px 0 0 0; font-size: 13px; color: var(--dashboard-text-secondary);">
399 + 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>.
400 + </p>
443 401 </div>
444 402
445 403 <div class="dashboard-card">
446 - <h2>Content Genius Synchronization</h2>
404 + <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 405 <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">
406 + <button type="button" class="button button-primary" id="sendAuthToken">
449 407 Sync Now
450 408 </button>
451 409 </div>
452 410
@@ -465,14 +423,15 @@
465 423 ?>
466 424 <div class="dashboard-card">
467 425 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
468 426 <div>
469 - <h2>White Label Branding</h2>
427 + <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 428 <p style="color: var(--dashboard-text-secondary); margin: 5px 0 0 0;">Customize the plugin appearance with your own branding and logo.</p>
471 429 </div>
472 430 <?php if ($password_protection_enabled && $is_authenticated): ?>
473 431 <div>
474 432 <?php Metasync_Compatibility_Checker::instance()->render_lock_button('whitelabel'); ?>
433 + <?php Metasync::render_tooltip_icon('wl_lock_section', 'Re-lock this section now. You will need the password to open it again.'); ?>
475 434 </div>
476 435 <?php endif; ?>
477 436 </div>
478 437
@@ -493,9 +452,9 @@
493 452 <!-- Color Palette Customization section -->
494 453 <div class="dashboard-card" id="metasync-color-palette-section">
495 454 <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
496 455 <div>
497 - <h2>Color Palette</h2>
456 + <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 457 <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 458 </div>
500 459 </div>
501 460
@@ -738,9 +697,9 @@
738 697 </script>
739 698
740 699 <!-- Export Whitelabel Settings section -->
741 700 <div class="dashboard-card">
742 - <h2>Export Whitelabel Plugin</h2>
701 + <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 702 <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 703 <button type="button" class="button button-primary" id="metasync-export-whitelabel-btn" style="padding: 10px 20px; font-size: 14px; font-weight: 600;">
745 704 Export Plugin with Whitelabel Settings
746 705 </button>
@@ -887,10 +846,8 @@
887 846 originalCheckboxState = hideSettingsCheckbox.is(':checked');
888 847 }, 0);
889 848 });
890 849
891 - var storedPassword = '<?php echo esc_js($whitelabel_settings['settings_password'] ?? ''); ?>';
892 -
893 850 passwordField.on('keydown', function(e) {
894 851 var currentValue = $(this).val();
895 852
896 853 if (hideSettingsCheckbox.is(':checked') &&
@@ -920,42 +877,8 @@
920 877 $('#metasync-custom-modal').fadeOut(200);
921 878 }
922 879 });
923 880
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 881 });
959 882 </script>
960 883 <?php
961 884 } elseif ($active_tab == 'advanced') {
@@ -985,10 +908,10 @@
985 908 </div>
986 909 <?php endif; ?>
987 910
988 911 <?php
989 - if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'):
990 - $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1';
912 + 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
913 + $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice
991 914 ?>
992 915 <div class="notice notice-success inline" style="margin-bottom: 20px;">
993 916 <p>
994 917 <strong>✅ Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>.
@@ -995,9 +918,9 @@
995 918 </p>
996 919 </div>
997 920 <?php endif; ?>
998 921
999 - <?php if (isset($_GET['debug_mode_disabled']) && $_GET['debug_mode_disabled'] == '1'): ?>
922 + <?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 923 <div class="notice notice-info inline" style="margin-bottom: 20px;">
1001 924 <p>
1002 925 <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled.
1003 926 </p>
@@ -1003,9 +926,9 @@
1003 926 </p>
1004 927 </div>
1005 928 <?php endif; ?>
1006 929
1007 - <?php if (isset($_GET['debug_mode_extended']) && $_GET['debug_mode_extended'] == '1'): ?>
930 + <?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 931 <div class="notice notice-success inline" style="margin-bottom: 20px;">
1009 932 <p>
1010 933 <strong>✅ Extended!</strong> Debug mode has been extended for another 24 hours.
1011 934 </p>
@@ -1011,9 +934,9 @@
1011 934 </p>
1012 935 </div>
1013 936 <?php endif; ?>
1014 937
1015 - <?php if (isset($_GET['debug_error']) && $_GET['debug_error'] == '1'): ?>
938 + <?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 939 <div class="notice notice-error inline" style="margin-bottom: 20px;">
1017 940 <p>
1018 941 <strong>❌ Error:</strong> Unable to perform the debug mode operation. Please try again.
1019 942 </p>
@@ -1124,9 +1047,9 @@
1124 1047 $.ajax({
1125 1048 url: ajaxUrl,
1126 1049 type: 'POST',
1127 1050 dataType: 'json',
1128 - data: { action: action },
1051 + data: { action: action, nonce: '<?php echo wp_create_nonce('metasync_nonce'); ?>' },
1129 1052 timeout: 35000,
1130 1053 success: function(response, textStatus, xhr) {
1131 1054 try {
1132 1055 if (response && response.success && response.data) {
@@ -1194,22 +1117,22 @@
1194 1117 var blockedClass = result.blocked ? 'blocked' : 'allowed';
1195 1118
1196 1119 html += '<div class="test-result-item ' + statusClass + '">';
1197 1120 html += '<div class="test-result-header">';
1198 - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
1121 + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
1199 1122 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
1200 1123 html += '</div>';
1201 1124
1202 1125 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>';
1126 + html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>';
1127 + html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>';
1205 1128
1206 1129 if (result.status_code) {
1207 - html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + result.status_code + '</span></p>';
1130 + html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + escapeHtml(result.status_code) + '</span></p>';
1208 1131 }
1209 1132
1210 1133 if (result.error) {
1211 - html += '<p><strong>Error:</strong> <span class="error-message">' + result.error + '</span></p>';
1134 + html += '<p><strong>Error:</strong> <span class="error-message">' + escapeHtml(result.error) + '</span></p>';
1212 1135 }
1213 1136
1214 1137 if (result.body) {
1215 1138 html += '<p><strong>Response Body:</strong></p>';
@@ -1219,9 +1142,9 @@
1219 1142 if (result.headers && Object.keys(result.headers).length > 0) {
1220 1143 html += '<p><strong>Response Headers:</strong></p>';
1221 1144 html += '<pre class="response-headers">';
1222 1145 Object.keys(result.headers).forEach(function(key) {
1223 - html += key + ': ' + result.headers[key] + '\n';
1146 + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
1224 1147 });
1225 1148 html += '</pre>';
1226 1149 }
1227 1150
@@ -1234,9 +1157,9 @@
1234 1157 html += '<p><strong>Parsed Response:</strong></p>';
1235 1158 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
1236 1159 }
1237 1160
1238 - html += '<p><strong>Details:</strong> ' + result.details + '</p>';
1161 + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
1239 1162 html += '</div>';
1240 1163 html += '</div>';
1241 1164 });
1242 1165
@@ -1251,9 +1174,9 @@
1251 1174 '>': '&gt;',
1252 1175 '"': '&quot;',
1253 1176 '\'': '&#039;'
1254 1177 };
1255 - return text.replace(/[&<>"']/g, function(m) { return map[m]; });
1178 + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
1256 1179 }
1257 1180 });
1258 1181 </script>
1259 1182
@@ -1386,41 +1309,41 @@
1386 1309 }
1387 1310 </style>
1388 1311
1389 1312 <?php if ($active_tab === 'advanced'): ?>
1390 - <?php if (isset($_GET['settings_cleared']) && $_GET['settings_cleared'] == '1'): ?>
1313 + <?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 1314 <div class="notice notice-success is-dismissible">
1392 1315 <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 1316 </div>
1394 1317 <?php endif; ?>
1395 1318
1396 - <?php if (isset($_GET['clear_settings_error']) && $_GET['clear_settings_error'] == '1'): ?>
1319 + <?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 1320 <div class="notice notice-error is-dismissible">
1398 1321 <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p>
1399 1322 </div>
1400 1323 <?php endif; ?>
1401 1324
1402 - <?php if (isset($_GET['log_cleared']) && $_GET['log_cleared'] == '1'): ?>
1325 + <?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 1326 <div class="notice notice-success is-dismissible">
1404 1327 <p><strong>✅ Success!</strong> Error logs have been cleared successfully.</p>
1405 1328 </div>
1406 1329 <?php endif; ?>
1407 1330
1408 - <?php if (isset($_GET['clear_error']) && $_GET['clear_error'] == '1'): ?>
1331 + <?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 1332 <div class="notice notice-error is-dismissible">
1410 1333 <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p>
1411 1334 </div>
1412 1335 <?php endif; ?>
1413 1336
1414 - <?php if (isset($_GET['access_roles_saved']) && $_GET['access_roles_saved'] == '1'): ?>
1337 + <?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 1338 <div class="notice notice-success is-dismissible">
1416 1339 <p><strong>✅ Success!</strong> Plugin access roles have been saved successfully.</p>
1417 1340 </div>
1418 1341 <?php endif; ?>
1419 1342
1420 - <?php if (isset($_GET['access_roles_error']) && $_GET['access_roles_error'] == '1'): ?>
1343 + <?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 1344 <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>
1345 + <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 1346 </div>
1424 1347 <?php endif; ?>
1425 1348
1426 1349 <?php endif; ?>
@@ -1454,8 +1377,15 @@
1454 1377 }
1455 1378
1456 1379 public function create_admin_robots_txt_page()
1457 1380 {
1381 + // The form below rewrites the site's physical robots.txt — restrict
1382 + // that to site admins, not just anyone who can open the page via
1383 + // plugin_access_roles. The nonce is CSRF protection, not authorization.
1384 + if (!empty($_POST) && !current_user_can('manage_options')) {
1385 + wp_die(esc_html__('Sorry, you are not allowed to manage robots.txt settings.', 'metasync'));
1386 + }
1387 +
1458 1388 ?>
1459 1389 <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?>
1460 1390
1461 1391 <?php
@@ -1500,9 +1430,11 @@
1500 1430 echo '<div class="notice notice-error"><p>' . esc_html($current_content->get_error_message()) . '</p></div>';
1501 1431 $current_content = '';
1502 1432 }
1503 1433
1504 - $backups = $robots_txt->get_backup_history(10);
1434 + // First page of backups; the Backup History panel is paginated so the
1435 + // list always reflects the full stored state and its true total.
1436 + $backups = $robots_txt->get_backup_history(Metasync_Robots_Txt_Database::BACKUPS_PER_PAGE, 0);
1505 1437
1506 1438 $file_exists = $robots_txt->file_exists();
1507 1439 $is_writable = $robots_txt->is_writable();
1508 1440
@@ -1516,79 +1448,8 @@
1516 1448 {
1517 1449 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1518 1450 }
1519 1451
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 1452 public function create_admin_custom_pages_page()
1592 1453 {
1593 1454 ?>
1594 1455 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
@@ -1639,12 +1500,14 @@
1639 1500 }
1640 1501
1641 1502 public function create_admin_breadcrumbs_page()
1642 1503 {
1643 - $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs';
1644 1504 ?>
1645 1505 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1646 1506
1507 + <?php /* options.php redirects back here with ?settings-updated=true; without this call the save confirmation never prints */ ?>
1508 + <?php settings_errors(); ?>
1509 +
1647 1510 <form method="post" action="options.php">
1648 1511 <div class="dashboard-card">
1649 1512 <h2>Breadcrumbs Settings</h2>
1650 1513 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
@@ -1649,9 +1512,9 @@
1649 1512 <h2>Breadcrumbs Settings</h2>
1650 1513 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1651 1514 <?php
1652 1515 settings_fields(Metasync_Admin::option_group);
1653 - do_settings_sections($page_slug);
1516 + Metasync_Settings_Fields::instance()->render_breadcrumbs_section();
1654 1517 submit_button();
1655 1518 ?>
1656 1519 </div>
1657 1520 </form>
@@ -1661,23 +1524,53 @@
1661 1524 <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 1525
1663 1526 <h3 style="margin-top: 0;">Option 1 — Shortcode</h3>
1664 1527 <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>
1528 + <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 1529
1667 1530 <h3>Option 2 — PHP Template Function</h3>
1668 1531 <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>
1532 + <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 1533
1671 1534 <h3>Option 3 — PHP Return Value</h3>
1672 1535 <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>
1536 + <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();
1537 +echo $breadcrumbs-&gt;render_breadcrumb_html();</pre><span class="metasync-copy-badge">Copy</span></div>
1675 1538
1676 1539 <p style="color: var(--dashboard-text-secondary); margin-top: 16px; font-size: 12px;">
1677 1540 <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 1541 <code>echo $breadcrumbs-&gt;render_breadcrumb_html(['post_id' =&gt; 123]);</code>
1679 1542 </p>
1543 + <?php /* click-to-copy for the code snippets above */ ?>
1544 + <style>
1545 + .metasync-copy-wrap { position: relative; }
1546 + .metasync-copy-snippet { cursor: pointer; outline: 1px solid transparent; transition: outline-color .15s ease; }
1547 + .metasync-copy-wrap:hover .metasync-copy-snippet { outline-color: var(--dashboard-border, #374151); }
1548 + .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); }
1549 + .metasync-copy-wrap:hover .metasync-copy-badge { opacity: .85; }
1550 + .metasync-copy-badge.metasync-copy-done { opacity: 1; background: var(--dashboard-gradient-primary, linear-gradient(135deg, #667eea 0%, #764ba2 100%)); color: #fff; }
1551 + </style>
1552 + <script>
1553 + (function(){
1554 + if (window.__metasyncCopyInit) return; window.__metasyncCopyInit = true;
1555 + 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); }
1556 + function attach(){
1557 + document.querySelectorAll('.metasync-copy-snippet').forEach(function(pre){
1558 + if (pre.dataset.copyBound) return; pre.dataset.copyBound='1';
1559 + pre.setAttribute('title','Click to copy');
1560 + pre.addEventListener('click', function(){
1561 + var text = pre.textContent.trim();
1562 + var wrap = pre.closest('.metasync-copy-wrap') || pre.parentNode;
1563 + var badge = wrap.querySelector('.metasync-copy-badge');
1564 + 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); };
1565 + if (navigator.clipboard && navigator.clipboard.writeText){ navigator.clipboard.writeText(text).then(done).catch(function(){ fallback(text); done(); }); }
1566 + else { fallback(text); done(); }
1567 + });
1568 + });
1569 + }
1570 + if (document.readyState==='loading'){ document.addEventListener('DOMContentLoaded', attach); } else { attach(); }
1571 + })();
1572 + </script>
1680 1573 </div>
1681 1574 <?php $this->admin->render_layout_close(); ?>
1682 1575 <?php
1683 1576 }
@@ -1688,8 +1581,10 @@
1688 1581 $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 1582 ?>
1690 1583 <form method="post" action="options.php">
1691 1584 <div class="dashboard-card">
1585 + <h2>Schema Markup Settings</h2>
1586 + <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 1587 <?php
1693 1588 settings_fields(Metasync_Admin::option_group);
1694 1589 do_settings_sections($page_slug);
1695 1590 submit_button('Save Changes');
@@ -1707,9 +1602,9 @@
1707 1602 <?php $this->admin->render_layout_open('Local Business', 'local_business', 'Configure local business information for better local SEO.'); ?>
1708 1603
1709 1604 <form method="post" action="options.php">
1710 1605 <div class="dashboard-card">
1711 - <h2>🏢 Local Business Configuration</h2>
1606 + <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 1607 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure your local business information for better local search engine optimization.</p>
1713 1608 <?php
1714 1609 settings_fields(Metasync_Admin::option_group);
1715 1610 do_settings_sections($page_slug);
@@ -1741,24 +1636,20 @@
1741 1636 <?php $this->admin->render_layout_close(); ?>
1742 1637 <?php
1743 1638 }
1744 1639
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 1640 public function create_admin_google_console_page()
1752 1641 {
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
1642 + $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
1643 + $google = function_exists('google_index_direct') ? google_index_direct() : null;
1644 + if (is_null($google)) {
1645 + $service_info = ['error' => 'Module not loaded'];
1646 + } else {
1647 + $service_info = $google->get_service_account_info();
1648 + }
1649 + $is_configured = !isset($service_info['error']);
1650 + include_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-console.php';
1651 + $this->admin->render_layout_close();
1761 1652 }
1762 1653
1763 1654 public function create_admin_bing_console_page()
1764 1655 {
@@ -1773,33 +1664,8 @@
1773 1664 <?php $this->admin->render_layout_close(); ?>
1774 1665 <?php
1775 1666 }
1776 1667
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 1668 public function create_admin_global_settings_page()
1803 1669 {
1804 1670 $page_slug = Metasync_Admin::$page_slug . '_common-settings';
1805 1671 ?>
@@ -1894,21 +1760,9 @@
1894 1760 <?php $this->admin->render_layout_close(); ?>
1895 1761 <?php
1896 1762 }
1897 1763
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 1764
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 1765 public function create_admin_error_logs_page()
1912 1766 {
1913 1767 ?>
1914 1768 <?php $this->admin->render_layout_open('Error Logs', 'general', ''); ?>
@@ -1950,177 +1804,15 @@
1950 1804 <?php $this->admin->render_layout_close(); ?>
1951 1805 <?php
1952 1806 }
1953 1807
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 - ];
1808 + // NOTE: The Changes Log (sync log) page is rendered by
1809 + // Metasync_Admin::create_admin_sync_log_page() in
1810 + // admin/class-metasync-admin.php, which is the callback registered in
1811 + // class-metasync-admin-navigation.php. A former duplicate of that method
1812 + // lived here but was never wired to a menu and has been removed to avoid
1813 + // drift.
1971 1814
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 1815 public function creat_error_Logs_List()
2124 1816 {
2125 1817 // printf('<h1> Error Logs </h1>');
2126 1818 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
@@ -2149,54 +1841,11 @@
2149 1841 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
2150 1842 }
2151 1843
2152 1844 // ------------------------------------------------------------------
2153 - // Private helpers (copied from Metasync_Admin, used only by pages above)
1845 + // Private helpers
2154 1846 // ------------------------------------------------------------------
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 - }
1847 + // The former time_elapsed_string / build_filter_query_string /
1848 + // handle_sync_log_ajax helpers here were only used by the dead
1849 + // create_admin_sync_log_page() duplicate that has since been removed. The
1850 + // live equivalents live on Metasync_Admin.
2202 1851 }