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 +55 -412 2.6.21 → 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,8 +394,11 @@
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 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>
@@ -919,42 +877,8 @@
919 877 $('#metasync-custom-modal').fadeOut(200);
920 878 }
921 879 });
922 880
923 - $('#metasync-export-whitelabel-btn').on('click', function(e) {
924 - e.preventDefault();
925 -
926 - var $button = $(this);
927 - var originalText = $button.html();
928 -
929 - $button.prop('disabled', true).html('⏳ Exporting...');
930 -
931 - var form = $('<form>', {
932 - 'method': 'POST',
933 - 'action': '<?php echo esc_js(admin_url('admin-post.php')); ?>',
934 - 'target': '_blank'
935 - });
936 -
937 - form.append($('<input>', {
938 - 'type': 'hidden',
939 - 'name': 'action',
940 - 'value': 'metasync_export_whitelabel_settings'
941 - }));
942 -
943 - form.append($('<input>', {
944 - 'type': 'hidden',
945 - 'name': '_wpnonce',
946 - 'value': '<?php echo wp_create_nonce('metasync_export_whitelabel'); ?>'
947 - }));
948 -
949 - $('body').append(form);
950 - form.submit();
951 -
952 - setTimeout(function() {
953 - form.remove();
954 - $button.prop('disabled', false).html(originalText);
955 - }, 2000);
956 - });
957 881 });
958 882 </script>
959 883 <?php
960 884 } elseif ($active_tab == 'advanced') {
@@ -984,10 +908,10 @@
984 908 </div>
985 909 <?php endif; ?>
986 910
987 911 <?php
988 - if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'):
989 - $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
990 914 ?>
991 915 <div class="notice notice-success inline" style="margin-bottom: 20px;">
992 916 <p>
993 917 <strong>✅ Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>.
@@ -994,9 +918,9 @@
994 918 </p>
995 919 </div>
996 920 <?php endif; ?>
997 921
998 - <?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 ?>
999 923 <div class="notice notice-info inline" style="margin-bottom: 20px;">
1000 924 <p>
1001 925 <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled.
1002 926 </p>
@@ -1002,9 +926,9 @@
1002 926 </p>
1003 927 </div>
1004 928 <?php endif; ?>
1005 929
1006 - <?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 ?>
1007 931 <div class="notice notice-success inline" style="margin-bottom: 20px;">
1008 932 <p>
1009 933 <strong>✅ Extended!</strong> Debug mode has been extended for another 24 hours.
1010 934 </p>
@@ -1010,9 +934,9 @@
1010 934 </p>
1011 935 </div>
1012 936 <?php endif; ?>
1013 937
1014 - <?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 ?>
1015 939 <div class="notice notice-error inline" style="margin-bottom: 20px;">
1016 940 <p>
1017 941 <strong>❌ Error:</strong> Unable to perform the debug mode operation. Please try again.
1018 942 </p>
@@ -1193,22 +1117,22 @@
1193 1117 var blockedClass = result.blocked ? 'blocked' : 'allowed';
1194 1118
1195 1119 html += '<div class="test-result-item ' + statusClass + '">';
1196 1120 html += '<div class="test-result-header">';
1197 - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
1121 + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
1198 1122 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
1199 1123 html += '</div>';
1200 1124
1201 1125 html += '<div class="test-result-details">';
1202 - html += '<p><strong>Response Time:</strong> ' + result.response_time + '</p>';
1203 - 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>';
1204 1128
1205 1129 if (result.status_code) {
1206 - 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>';
1207 1131 }
1208 1132
1209 1133 if (result.error) {
1210 - 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>';
1211 1135 }
1212 1136
1213 1137 if (result.body) {
1214 1138 html += '<p><strong>Response Body:</strong></p>';
@@ -1218,9 +1142,9 @@
1218 1142 if (result.headers && Object.keys(result.headers).length > 0) {
1219 1143 html += '<p><strong>Response Headers:</strong></p>';
1220 1144 html += '<pre class="response-headers">';
1221 1145 Object.keys(result.headers).forEach(function(key) {
1222 - html += key + ': ' + result.headers[key] + '\n';
1146 + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
1223 1147 });
1224 1148 html += '</pre>';
1225 1149 }
1226 1150
@@ -1233,9 +1157,9 @@
1233 1157 html += '<p><strong>Parsed Response:</strong></p>';
1234 1158 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
1235 1159 }
1236 1160
1237 - html += '<p><strong>Details:</strong> ' + result.details + '</p>';
1161 + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
1238 1162 html += '</div>';
1239 1163 html += '</div>';
1240 1164 });
1241 1165
@@ -1250,9 +1174,9 @@
1250 1174 '>': '&gt;',
1251 1175 '"': '&quot;',
1252 1176 '\'': '&#039;'
1253 1177 };
1254 - return text.replace(/[&<>"']/g, function(m) { return map[m]; });
1178 + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
1255 1179 }
1256 1180 });
1257 1181 </script>
1258 1182
@@ -1385,41 +1309,41 @@
1385 1309 }
1386 1310 </style>
1387 1311
1388 1312 <?php if ($active_tab === 'advanced'): ?>
1389 - <?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 ?>
1390 1314 <div class="notice notice-success is-dismissible">
1391 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>
1392 1316 </div>
1393 1317 <?php endif; ?>
1394 1318
1395 - <?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 ?>
1396 1320 <div class="notice notice-error is-dismissible">
1397 1321 <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p>
1398 1322 </div>
1399 1323 <?php endif; ?>
1400 1324
1401 - <?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 ?>
1402 1326 <div class="notice notice-success is-dismissible">
1403 1327 <p><strong>✅ Success!</strong> Error logs have been cleared successfully.</p>
1404 1328 </div>
1405 1329 <?php endif; ?>
1406 1330
1407 - <?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 ?>
1408 1332 <div class="notice notice-error is-dismissible">
1409 1333 <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p>
1410 1334 </div>
1411 1335 <?php endif; ?>
1412 1336
1413 - <?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 ?>
1414 1338 <div class="notice notice-success is-dismissible">
1415 1339 <p><strong>✅ Success!</strong> Plugin access roles have been saved successfully.</p>
1416 1340 </div>
1417 1341 <?php endif; ?>
1418 1342
1419 - <?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 ?>
1420 1344 <div class="notice notice-error is-dismissible">
1421 - <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>
1422 1346 </div>
1423 1347 <?php endif; ?>
1424 1348
1425 1349 <?php endif; ?>
@@ -1453,8 +1377,15 @@
1453 1377 }
1454 1378
1455 1379 public function create_admin_robots_txt_page()
1456 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 +
1457 1388 ?>
1458 1389 <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?>
1459 1390
1460 1391 <?php
@@ -1517,82 +1448,8 @@
1517 1448 {
1518 1449 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1519 1450 }
1520 1451
1521 - public function create_admin_xml_sitemap_page()
1522 - {
1523 - require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
1524 -
1525 - $sitemap_generator = new Metasync_Sitemap_Generator();
1526 -
1527 - if (isset($_POST['metasync_sitemap_nonce'])) {
1528 - check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce');
1529 -
1530 - if (isset($_POST['generate_sitemap'])) {
1531 - $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators();
1532 -
1533 - $result = $sitemap_generator->generate_sitemap();
1534 -
1535 - if (is_wp_error($result)) {
1536 - error_log('[MetaSync] Sitemap generation failed: ' . $result->get_error_message());
1537 - echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed. Please try again or check the error logs.', 'metasync') . '</p></div>';
1538 - } else {
1539 - $message = esc_html__('Sitemap generated successfully!', 'metasync');
1540 - if ($disabled_plugins) {
1541 - $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
1542 - }
1543 -
1544 - $robots_result = get_transient('metasync_sitemap_robots_updated');
1545 - if ($robots_result && $robots_result['success']) {
1546 - if ($robots_result['action'] === 'added') {
1547 - $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
1548 - } elseif ($robots_result['action'] === 'updated') {
1549 - $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
1550 - } elseif ($robots_result['action'] === 'created') {
1551 - $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
1552 - }
1553 - delete_transient('metasync_sitemap_robots_updated');
1554 - }
1555 -
1556 - echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
1557 - }
1558 - } elseif (isset($_POST['enable_auto_update'])) {
1559 - update_option('metasync_sitemap_auto_update', true);
1560 - $sitemap_generator->setup_auto_update_hooks();
1561 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
1562 - } elseif (isset($_POST['disable_auto_update'])) {
1563 - update_option('metasync_sitemap_auto_update', false);
1564 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
1565 - } elseif (isset($_POST['delete_sitemap'])) {
1566 - $deleted = $sitemap_generator->delete_sitemap();
1567 - if ($deleted) {
1568 - update_option('metasync_sitemap_auto_update', false);
1569 - // Re-enable WP core sitemap so the site isn't left with zero sitemaps
1570 - delete_option('metasync_disable_wp_sitemap');
1571 - echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap deleted successfully!', 'metasync') . '</p></div>';
1572 - } else {
1573 - 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>';
1574 - }
1575 - } elseif (isset($_POST['enable_other_sitemaps'])) {
1576 - $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
1577 - if ($enabled_plugins) {
1578 - echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>';
1579 - } else {
1580 - echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>';
1581 - }
1582 - }
1583 - }
1584 -
1585 - $sitemap_exists = $sitemap_generator->sitemap_exists();
1586 - $sitemap_url = $sitemap_generator->get_sitemap_url();
1587 - $url_count = $sitemap_generator->count_urls();
1588 - $last_generated = $sitemap_generator->get_last_generated_time();
1589 - $auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
1590 - $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
1591 -
1592 - require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
1593 - }
1594 -
1595 1452 public function create_admin_custom_pages_page()
1596 1453 {
1597 1454 ?>
1598 1455 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
@@ -1643,12 +1500,14 @@
1643 1500 }
1644 1501
1645 1502 public function create_admin_breadcrumbs_page()
1646 1503 {
1647 - $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs';
1648 1504 ?>
1649 1505 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1650 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 +
1651 1510 <form method="post" action="options.php">
1652 1511 <div class="dashboard-card">
1653 1512 <h2>Breadcrumbs Settings</h2>
1654 1513 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
@@ -1653,9 +1512,9 @@
1653 1512 <h2>Breadcrumbs Settings</h2>
1654 1513 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1655 1514 <?php
1656 1515 settings_fields(Metasync_Admin::option_group);
1657 - do_settings_sections($page_slug);
1516 + Metasync_Settings_Fields::instance()->render_breadcrumbs_section();
1658 1517 submit_button();
1659 1518 ?>
1660 1519 </div>
1661 1520 </form>
@@ -1777,14 +1636,8 @@
1777 1636 <?php $this->admin->render_layout_close(); ?>
1778 1637 <?php
1779 1638 }
1780 1639
1781 - public function create_admin_google_instant_index_page()
1782 - {
1783 - // Delegates to the admin class which uses the shared credentials view
1784 - $this->admin->create_admin_google_instant_index_page();
1785 - }
1786 -
1787 1640 public function create_admin_google_console_page()
1788 1641 {
1789 1642 $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
1790 1643 $google = function_exists('google_index_direct') ? google_index_direct() : null;
@@ -1951,177 +1804,15 @@
1951 1804 <?php $this->admin->render_layout_close(); ?>
1952 1805 <?php
1953 1806 }
1954 1807
1955 - public function create_admin_sync_log_page()
1956 - {
1957 - $sync_db = new Metasync_Sync_History_Database();
1958 -
1959 - if (wp_doing_ajax()) {
1960 - $this->handle_sync_log_ajax();
1961 - return;
1962 - }
1963 -
1964 - $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
1965 - $per_page = 10;
1966 - $offset = ($page - 1) * $per_page;
1967 -
1968 - $filters = [
1969 - 'date_range' => isset($_GET['date_range']) ? sanitize_text_field($_GET['date_range']) : '',
1970 - 'status' => isset($_GET['status']) ? sanitize_text_field($_GET['status']) : '',
1971 - ];
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.
1972 1814
1973 - $date_range = $filters['date_range'];
1974 - $wp_now_ts = current_time('timestamp');
1975 - $date_from = '';
1976 - $date_to = '';
1977 -
1978 - if (!empty($date_range)) {
1979 - $date_to = date('Y-m-d H:i:s', $wp_now_ts);
1980 -
1981 - if ($date_range === 'today') {
1982 - $start_ts = strtotime('today', $wp_now_ts);
1983 - $date_from = date('Y-m-d H:i:s', $start_ts);
1984 - } elseif ($date_range === 'yesterday') {
1985 - $start_ts = strtotime('yesterday', $wp_now_ts);
1986 - $end_ts = strtotime('today', $wp_now_ts) - 1;
1987 - $date_from = date('Y-m-d H:i:s', $start_ts);
1988 - $date_to = date('Y-m-d H:i:s', $end_ts);
1989 - } elseif ($date_range === 'this_week') {
1990 - $start_of_week = (int) get_option('start_of_week', 1);
1991 - $day_of_week = (int) date('w', $wp_now_ts);
1992 - $delta_days = ($day_of_week - $start_of_week + 7) % 7;
1993 - $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
1994 - $date_from = date('Y-m-d H:i:s', $start_ts);
1995 - } elseif ($date_range === 'this_month') {
1996 - $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
1997 - $date_from = date('Y-m-d H:i:s', $start_ts);
1998 - } elseif ($date_range === 'all') {
1999 - // no bounds
2000 - }
2001 - }
2002 -
2003 - if (!empty($date_from)) {
2004 - $filters['date_from'] = $date_from;
2005 - }
2006 - if (!empty($date_to)) {
2007 - $filters['date_to'] = $date_to;
2008 - }
2009 -
2010 - $filters = array_filter($filters);
2011 -
2012 - $sync_records = $sync_db->getAllRecords($per_page, $offset, $filters);
2013 - $total_records = $sync_db->get_count($filters);
2014 - $total_pages = ceil($total_records / $per_page);
2015 -
2016 - $stats = $sync_db->get_statistics();
2017 -
2018 - ?>
2019 - <?php $this->admin->render_layout_open('Changes Log', 'sync_log', 'Track all SEO changes made by the plugin.'); ?>
2020 -
2021 - <div class="dashboard-card">
2022 - <div class="sync-log-header">
2023 - <div class="sync-log-title-section">
2024 - <h2>Changes Log</h2>
2025 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">Recent content synchronizations from external tools.</p>
2026 - </div>
2027 -
2028 - <!-- Filters - Right aligned -->
2029 - <div class="sync-log-filters">
2030 - <form method="get" class="sync-filters-form" onchange="this.submit()" style="display:flex;flex-direction:row;align-items:center;gap:12px;flex-wrap:nowrap;">
2031 - <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>">
2032 -
2033 - <select name="date_range" class="sync-filter-select">
2034 - <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
2035 - <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
2036 - <option value="yesterday" <?php selected($filters['date_range'] ?? '', 'yesterday'); ?>>Yesterday</option>
2037 - <option value="this_week" <?php selected($filters['date_range'] ?? '', 'this_week'); ?>>This week</option>
2038 - <option value="this_month" <?php selected($filters['date_range'] ?? '', 'this_month'); ?>>This month</option>
2039 - </select>
2040 -
2041 - <select name="status" class="sync-filter-select">
2042 - <option value="" <?php selected($filters['status'] ?? '', ''); ?>>Status Filter</option>
2043 - <option value="published" <?php selected($filters['status'] ?? '', 'published'); ?>>Published</option>
2044 - <option value="draft" <?php selected($filters['status'] ?? '', 'draft'); ?>>Draft</option>
2045 - </select>
2046 - </form>
2047 - </div>
2048 - </div>
2049 -
2050 - <!-- Sync History List -->
2051 - <div class="sync-log-list">
2052 - <?php if (empty($sync_records)): ?>
2053 - <div class="sync-log-empty">
2054 - <div class="sync-log-empty-icon"><span class="dashicons dashicons-list-view" style="font-size:48px;width:48px;height:48px;color:var(--dashboard-text-secondary);"></span></div>
2055 - <h3>No sync records found</h3>
2056 - <p>Sync records will appear here when content/pages receive new updates.</p>
2057 - </div>
2058 - <?php else: ?>
2059 - <?php foreach ($sync_records as $record): ?>
2060 - <div class="sync-log-item">
2061 - <div class="sync-log-icon">
2062 - <div class="sync-icon-circle">
2063 - <span class="dashicons dashicons-media-default sync-icon"></span>
2064 - </div>
2065 - </div>
2066 -
2067 - <div class="sync-log-content">
2068 - <div class="sync-log-title"><?php echo esc_html($record->title); ?>
2069 - <?php if (!empty($record->url)): ?>
2070 - <a href="<?php echo esc_url($record->url); ?>" target="_blank" rel="noopener" title="Open URL" style="margin-left:8px; text-decoration:none;"><span class="dashicons dashicons-external" style="font-size:14px;width:14px;height:14px;vertical-align:middle;"></span></a>
2071 - <?php endif; ?>
2072 - </div>
2073 - <div class="sync-log-meta">
2074 - <?php echo esc_html( $this->time_elapsed_string($record->created_at) ); ?>
2075 - </div>
2076 - </div>
2077 -
2078 - <div class="sync-log-status">
2079 - <?php if ($record->status === 'published' || $record->status === 'publish'): ?>
2080 - <span class="sync-status-badge sync-status-published">
2081 - <span class="sync-status-icon">✓</span>
2082 - Published
2083 - </span>
2084 - <?php else: ?>
2085 - <span class="sync-status-badge sync-status-draft">
2086 - <span class="sync-status-icon">i</span>
2087 - Draft
2088 - </span>
2089 - <?php endif; ?>
2090 - </div>
2091 - </div>
2092 - <?php endforeach; ?>
2093 - <?php endif; ?>
2094 - </div>
2095 -
2096 - <!-- Pagination -->
2097 - <?php if ($total_pages > 1): ?>
2098 - <div class="sync-log-pagination">
2099 - <div class="sync-log-pagination-info">
2100 - Total records: <?php echo intval( $total_records ); ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?>
2101 - </div>
2102 -
2103 - <div class="sync-log-pagination-controls">
2104 - <?php if ($page > 1): ?>
2105 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) - 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn">‹</a>
2106 - <?php endif; ?>
2107 -
2108 - <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
2109 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>"
2110 - class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a>
2111 - <?php endfor; ?>
2112 -
2113 - <?php if ($page < $total_pages): ?>
2114 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $page ) + 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn">›</a>
2115 - <?php endif; ?>
2116 - </div>
2117 - </div>
2118 - <?php endif; ?>
2119 - </div>
2120 - <?php $this->admin->render_layout_close(); ?>
2121 - <?php
2122 - }
2123 -
2124 1815 public function creat_error_Logs_List()
2125 1816 {
2126 1817 // printf('<h1> Error Logs </h1>');
2127 1818 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
@@ -2150,59 +1841,11 @@
2150 1841 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
2151 1842 }
2152 1843
2153 1844 // ------------------------------------------------------------------
2154 - // Private helpers (copied from Metasync_Admin, used only by pages above)
1845 + // Private helpers
2155 1846 // ------------------------------------------------------------------
2156 -
2157 - private function time_elapsed_string($datetime, $full = false)
2158 - {
2159 - if(empty($datetime)){
2160 - return "";
2161 - }
2162 - # Stored timestamps use current_time('mysql') which is WP local time
2163 - # with no timezone marker. Defaulting DateTime to UTC produces a diff
2164 - # equal to the WP timezone offset (QA: "4 hours ago" after a
2165 - # 4-minute sync on non-UTC sites).
2166 - $wp_tz = function_exists('wp_timezone') ? wp_timezone() : new DateTimeZone('UTC');
2167 - $now = new DateTime('now', $wp_tz);
2168 - $ago = new DateTime($datetime, $wp_tz);
2169 -
2170 - $diff = $now->diff($ago);
2171 -
2172 - $string = [
2173 - 'y' => 'year',
2174 - 'm' => 'month',
2175 - 'w' => 'week',
2176 - 'd' => 'day',
2177 - 'h' => 'hour',
2178 - 'i' => 'minute',
2179 - 's' => 'second',
2180 - ];
2181 -
2182 - foreach ($string as $k => &$v) {
2183 - if (isset($diff->$k) && $diff->$k) {
2184 - $v = $diff->$k . ' ' . $v . ($diff->$k > 1 ? 's' : '');
2185 - } else {
2186 - unset($string[$k]);
2187 - }
2188 - }
2189 - if (!$full) $string = array_slice($string, 0, 1);
2190 - return $string ? implode(', ', $string) . ' ago' : 'just now';
2191 - }
2192 -
2193 - private function build_filter_query_string($filters)
2194 - {
2195 - $query_parts = [];
2196 - foreach ($filters as $key => $value) {
2197 - if (!empty($value)) {
2198 - $query_parts[] = $key . '=' . urlencode($value);
2199 - }
2200 - }
2201 - return !empty($query_parts) ? '&' . implode('&', $query_parts) : '';
2202 - }
2203 -
2204 - private function handle_sync_log_ajax()
2205 - {
2206 - wp_die();
2207 - }
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.
2208 1851 }