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 +67 -456 2.6.17 → 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']) ? 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
@@ -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 ?>
@@ -410,8 +364,9 @@
410 364 }
411 365 ?>
412 366
413 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>
@@ -918,42 +877,8 @@
918 877 $('#metasync-custom-modal').fadeOut(200);
919 878 }
920 879 });
921 880
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 881 });
957 882 </script>
958 883 <?php
959 884 } elseif ($active_tab == 'advanced') {
@@ -983,10 +908,10 @@
983 908 </div>
984 909 <?php endif; ?>
985 910
986 911 <?php
987 - if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'):
988 - $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
989 914 ?>
990 915 <div class="notice notice-success inline" style="margin-bottom: 20px;">
991 916 <p>
992 917 <strong>✅ Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>.
@@ -993,9 +918,9 @@
993 918 </p>
994 919 </div>
995 920 <?php endif; ?>
996 921
997 - <?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 ?>
998 923 <div class="notice notice-info inline" style="margin-bottom: 20px;">
999 924 <p>
1000 925 <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled.
1001 926 </p>
@@ -1001,9 +926,9 @@
1001 926 </p>
1002 927 </div>
1003 928 <?php endif; ?>
1004 929
1005 - <?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 ?>
1006 931 <div class="notice notice-success inline" style="margin-bottom: 20px;">
1007 932 <p>
1008 933 <strong>✅ Extended!</strong> Debug mode has been extended for another 24 hours.
1009 934 </p>
@@ -1009,9 +934,9 @@
1009 934 </p>
1010 935 </div>
1011 936 <?php endif; ?>
1012 937
1013 - <?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 ?>
1014 939 <div class="notice notice-error inline" style="margin-bottom: 20px;">
1015 940 <p>
1016 941 <strong>❌ Error:</strong> Unable to perform the debug mode operation. Please try again.
1017 942 </p>
@@ -1192,22 +1117,22 @@
1192 1117 var blockedClass = result.blocked ? 'blocked' : 'allowed';
1193 1118
1194 1119 html += '<div class="test-result-item ' + statusClass + '">';
1195 1120 html += '<div class="test-result-header">';
1196 - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
1121 + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
1197 1122 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
1198 1123 html += '</div>';
1199 1124
1200 1125 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>';
1126 + html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>';
1127 + html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>';
1203 1128
1204 1129 if (result.status_code) {
1205 - 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>';
1206 1131 }
1207 1132
1208 1133 if (result.error) {
1209 - 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>';
1210 1135 }
1211 1136
1212 1137 if (result.body) {
1213 1138 html += '<p><strong>Response Body:</strong></p>';
@@ -1217,9 +1142,9 @@
1217 1142 if (result.headers && Object.keys(result.headers).length > 0) {
1218 1143 html += '<p><strong>Response Headers:</strong></p>';
1219 1144 html += '<pre class="response-headers">';
1220 1145 Object.keys(result.headers).forEach(function(key) {
1221 - html += key + ': ' + result.headers[key] + '\n';
1146 + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
1222 1147 });
1223 1148 html += '</pre>';
1224 1149 }
1225 1150
@@ -1232,9 +1157,9 @@
1232 1157 html += '<p><strong>Parsed Response:</strong></p>';
1233 1158 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
1234 1159 }
1235 1160
1236 - html += '<p><strong>Details:</strong> ' + result.details + '</p>';
1161 + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
1237 1162 html += '</div>';
1238 1163 html += '</div>';
1239 1164 });
1240 1165
@@ -1249,9 +1174,9 @@
1249 1174 '>': '&gt;',
1250 1175 '"': '&quot;',
1251 1176 '\'': '&#039;'
1252 1177 };
1253 - return text.replace(/[&<>"']/g, function(m) { return map[m]; });
1178 + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
1254 1179 }
1255 1180 });
1256 1181 </script>
1257 1182
@@ -1384,41 +1309,41 @@
1384 1309 }
1385 1310 </style>
1386 1311
1387 1312 <?php if ($active_tab === 'advanced'): ?>
1388 - <?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 ?>
1389 1314 <div class="notice notice-success is-dismissible">
1390 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>
1391 1316 </div>
1392 1317 <?php endif; ?>
1393 1318
1394 - <?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 ?>
1395 1320 <div class="notice notice-error is-dismissible">
1396 1321 <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p>
1397 1322 </div>
1398 1323 <?php endif; ?>
1399 1324
1400 - <?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 ?>
1401 1326 <div class="notice notice-success is-dismissible">
1402 1327 <p><strong>✅ Success!</strong> Error logs have been cleared successfully.</p>
1403 1328 </div>
1404 1329 <?php endif; ?>
1405 1330
1406 - <?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 ?>
1407 1332 <div class="notice notice-error is-dismissible">
1408 1333 <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p>
1409 1334 </div>
1410 1335 <?php endif; ?>
1411 1336
1412 - <?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 ?>
1413 1338 <div class="notice notice-success is-dismissible">
1414 1339 <p><strong>✅ Success!</strong> Plugin access roles have been saved successfully.</p>
1415 1340 </div>
1416 1341 <?php endif; ?>
1417 1342
1418 - <?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 ?>
1419 1344 <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>
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>
1421 1346 </div>
1422 1347 <?php endif; ?>
1423 1348
1424 1349 <?php endif; ?>
@@ -1452,8 +1377,15 @@
1452 1377 }
1453 1378
1454 1379 public function create_admin_robots_txt_page()
1455 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 +
1456 1388 ?>
1457 1389 <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?>
1458 1390
1459 1391 <?php
@@ -1498,9 +1430,11 @@
1498 1430 echo '<div class="notice notice-error"><p>' . esc_html($current_content->get_error_message()) . '</p></div>';
1499 1431 $current_content = '';
1500 1432 }
1501 1433
1502 - $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);
1503 1437
1504 1438 $file_exists = $robots_txt->file_exists();
1505 1439 $is_writable = $robots_txt->is_writable();
1506 1440
@@ -1514,82 +1448,8 @@
1514 1448 {
1515 1449 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1516 1450 }
1517 1451
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
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 1452 public function create_admin_custom_pages_page()
1593 1453 {
1594 1454 ?>
1595 1455 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
@@ -1640,12 +1500,14 @@
1640 1500 }
1641 1501
1642 1502 public function create_admin_breadcrumbs_page()
1643 1503 {
1644 - $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs';
1645 1504 ?>
1646 1505 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1647 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 +
1648 1510 <form method="post" action="options.php">
1649 1511 <div class="dashboard-card">
1650 1512 <h2>Breadcrumbs Settings</h2>
1651 1513 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
@@ -1650,9 +1512,9 @@
1650 1512 <h2>Breadcrumbs Settings</h2>
1651 1513 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1652 1514 <?php
1653 1515 settings_fields(Metasync_Admin::option_group);
1654 - do_settings_sections($page_slug);
1516 + Metasync_Settings_Fields::instance()->render_breadcrumbs_section();
1655 1517 submit_button();
1656 1518 ?>
1657 1519 </div>
1658 1520 </form>
@@ -1719,8 +1581,10 @@
1719 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.');
1720 1582 ?>
1721 1583 <form method="post" action="options.php">
1722 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>
1723 1587 <?php
1724 1588 settings_fields(Metasync_Admin::option_group);
1725 1589 do_settings_sections($page_slug);
1726 1590 submit_button('Save Changes');
@@ -1738,9 +1602,9 @@
1738 1602 <?php $this->admin->render_layout_open('Local Business', 'local_business', 'Configure local business information for better local SEO.'); ?>
1739 1603
1740 1604 <form method="post" action="options.php">
1741 1605 <div class="dashboard-card">
1742 - <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>
1743 1607 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure your local business information for better local search engine optimization.</p>
1744 1608 <?php
1745 1609 settings_fields(Metasync_Admin::option_group);
1746 1610 do_settings_sections($page_slug);
@@ -1772,14 +1636,8 @@
1772 1636 <?php $this->admin->render_layout_close(); ?>
1773 1637 <?php
1774 1638 }
1775 1639
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 1640 public function create_admin_google_console_page()
1783 1641 {
1784 1642 $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
1785 1643 $google = function_exists('google_index_direct') ? google_index_direct() : null;
@@ -1806,33 +1664,8 @@
1806 1664 <?php $this->admin->render_layout_close(); ?>
1807 1665 <?php
1808 1666 }
1809 1667
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 1668 public function create_admin_global_settings_page()
1836 1669 {
1837 1670 $page_slug = Metasync_Admin::$page_slug . '_common-settings';
1838 1671 ?>
@@ -1927,21 +1760,9 @@
1927 1760 <?php $this->admin->render_layout_close(); ?>
1928 1761 <?php
1929 1762 }
1930 1763
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 1764
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 1765 public function create_admin_error_logs_page()
1945 1766 {
1946 1767 ?>
1947 1768 <?php $this->admin->render_layout_open('Error Logs', 'general', ''); ?>
@@ -1983,177 +1804,15 @@
1983 1804 <?php $this->admin->render_layout_close(); ?>
1984 1805 <?php
1985 1806 }
1986 1807
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 - ];
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.
2004 1814
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 1815 public function creat_error_Logs_List()
2157 1816 {
2158 1817 // printf('<h1> Error Logs </h1>');
2159 1818 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
@@ -2182,59 +1841,11 @@
2182 1841 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
2183 1842 }
2184 1843
2185 1844 // ------------------------------------------------------------------
2186 - // Private helpers (copied from Metasync_Admin, used only by pages above)
1845 + // Private helpers
2187 1846 // ------------------------------------------------------------------
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 (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 - }
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.
2240 1851 }