PluginProbe
Search Atlas SEO – OTTO AI SEO Automation for WordPress / 2.7.2
Search Atlas SEO – OTTO AI SEO Automation for WordPress v2.7.2
2.7.1 2.7.2 2.7.0 2.6.26 2.6.25 2.6.24 2.6.23 2.6.22 2.6.21 2.6.20 2.6.19 2.6.18 2.6.17 2.6.16 2.6.15 2.6.14 2.6.13 2.6.12 2.6.11 2.6.10 2.6.9 2.6.8 2.6.7 2.6.6 2.6.5 All 141 releases
← All changes | includes/class-metasync-admin-pages.php +63 -169 2.6.24 → 2.7.2 View file →
@@ -217,10 +217,15 @@
217 217 // ------------------------------------------------------------------
218 218
219 219 public function create_admin_settings_page()
220 220 {
221 - $active_tab = isset($_GET['tab']) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
221 + if (isset($_REQUEST['metasync_password_reset'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing check, the reset form verifies its own nonce
222 + Metasync_Admin_Ajax::instance()->render_password_reset_page();
223 + return;
224 + }
222 225
226 + $active_tab = isset($_GET['tab']) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
227 +
223 228 $whitelabel_settings = Metasync::get_whitelabel_settings();
224 229
225 230 $user_password = $whitelabel_settings['settings_password'] ?? '';
226 231 $hide_settings_enabled = !empty($whitelabel_settings['hide_settings']);
@@ -245,9 +250,9 @@
245 250
246 251 $auth = new Metasync_Auth_Manager('whitelabel', 1800);
247 252 $password_validated = $auth->has_access();
248 253
249 - if (isset($_POST['whitelabel_password_submit']) && !$password_validated) {
254 + if (isset($_POST['whitelabel_password_submit']) && !$password_validated) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only access check for a display message
250 255 $password_error = 'Incorrect password. Please try again.';
251 256 }
252 257 }
253 258
@@ -264,8 +269,18 @@
264 269 $this->admin->render_layout_open('Settings', $settings_current_page, 'Manage all plugin settings and configuration.');
265 270 ?>
266 271 <?php
267 272 /*
273 + * options.php redirects back here after the native (no-JavaScript)
274 + * settings submit. Scoped to metasync_messages so field-level
275 + * rejections (e.g. invalid white-label branding) print without the
276 + * generic "Settings saved." notice that would otherwise accompany
277 + * a rejected save.
278 + */
279 + settings_errors('metasync_messages');
280 + ?>
281 + <?php
282 + /*
268 283 # Temporarily commented out: Clear Cache notice and button (can be re-enabled later)
269 284
270 285 * <div class="notice notice-success">
271 286 * <p>
@@ -344,63 +359,8 @@
344 359 <?php endif; ?>
345 360
346 361 </div>
347 362
348 - <script>
349 - jQuery(document).ready(function($) {
350 - $('#whitelabel_password').focus();
351 -
352 - $('#whitelabel_password').on('keypress', function(e) {
353 - if (e.which === 13) {
354 - $(this).closest('form').submit();
355 - }
356 - });
357 -
358 - <?php if (current_user_can('manage_options')): ?>
359 - $('#metasync-forgot-password-link').on('click', function(e) {
360 - e.preventDefault();
361 -
362 - var $link = $(this);
363 - var $message = $('#metasync-recovery-message');
364 -
365 - $link.css('pointer-events', 'none').css('opacity', '0.6');
366 - $message.removeClass('success error').hide();
367 - $message.html('⏳ Sending recovery email...').css('background', '#f0f6fc').css('color', '#0c5ba5').css('border', '1px solid #cfe2f3').fadeIn(200);
368 -
369 - $.ajax({
370 - url: ajaxurl,
371 - type: 'POST',
372 - data: {
373 - action: 'metasync_recover_password',
374 - nonce: '<?php echo wp_create_nonce('metasync_recover_password_nonce'); ?>'
375 - },
376 - success: function(response) {
377 - $link.css('pointer-events', 'auto').css('opacity', '1');
378 -
379 - if (response.success) {
380 - $message.addClass('success').html('✅ ' + response.data.message)
381 - .css('background', '#d4edda')
382 - .css('color', '#155724')
383 - .css('border', '1px solid #c3e6cb');
384 - } else {
385 - $message.addClass('error').html('❌ ' + response.data.message)
386 - .css('background', '#f8d7da')
387 - .css('color', '#721c24')
388 - .css('border', '1px solid #f5c6cb');
389 - }
390 - },
391 - error: function() {
392 - $link.css('pointer-events', 'auto').css('opacity', '1');
393 - $message.addClass('error').html('❌ An error occurred. Please try again.')
394 - .css('background', '#f8d7da')
395 - .css('color', '#721c24')
396 - .css('border', '1px solid #f5c6cb');
397 - }
398 - });
399 - });
400 - <?php endif; ?>
401 - });
402 - </script>
403 363 <?php
404 364 return;
405 365 } elseif (!$password_protection_enabled) {
406 366 ?>
@@ -414,8 +374,9 @@
414 374 }
415 375 ?>
416 376
417 377 <form method="post" action="options.php?tab=<?php echo esc_attr( $active_tab ); ?>" id="metaSyncGeneralSetting">
378 + <input type="hidden" name="metasync_pw_reset_generation" value="<?php echo esc_attr((string) (int) get_option('metasync_pw_reset_generation', 0)); ?>" />
418 379 <?php
419 380 settings_fields(Metasync_Admin::option_group);
420 381
421 382 wp_nonce_field('meta_sync_general_setting_nonce', 'meta_sync_nonce');
@@ -443,8 +404,11 @@
443 404
444 405 <?php
445 406 $this->admin->render_accordion_sections(Metasync_Admin::$page_slug . '_general');
446 407 ?>
408 + <p style="margin: 20px 0 0 0; font-size: 13px; color: var(--dashboard-text-secondary);">
409 + Breadcrumb settings now live on their own page: <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-breadcrumbs')); ?>">Breadcrumbs</a>.
410 + </p>
447 411 </div>
448 412
449 413 <div class="dashboard-card">
450 414 <h2>Content Genius Synchronization<?php Metasync::render_tooltip_icon('cg_sync_section', sprintf('Syncing sends this site\'s categories and authors to %s, so Content Genius can assign the right category and author when publishing articles to this site.', Metasync::get_effective_plugin_name())); ?></h2>
@@ -829,21 +793,21 @@
829 793 }
830 794 });
831 795
832 796 function hasPassword() {
833 - var passwordField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]');
797 + var passwordField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][settings_password]"]');
834 798 var passwordValue = passwordField.val();
835 799 return passwordValue && passwordValue.length > 0;
836 800 }
837 801
838 802 function hasRecoveryEmail() {
839 - var recoveryEmailField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][recovery_email]"]');
803 + var recoveryEmailField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][recovery_email]"]');
840 804 var emailValue = recoveryEmailField.val();
841 805 return emailValue && emailValue.length > 0 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailValue);
842 806 }
843 807
844 - var passwordField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]');
845 - var recoveryEmailField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][recovery_email]"]');
808 + var passwordField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][settings_password]"]');
809 + var recoveryEmailField = $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][recovery_email]"]');
846 810
847 811 passwordField.on('blur', function() {
848 812 if (hasPassword() && !hasRecoveryEmail()) {
849 813 showModal(
@@ -881,9 +845,9 @@
881 845 'You must set a <strong>Settings Password</strong> before enabling "Hide Settings".<br><br>Please scroll up to the <strong>Branding</strong> section and set a password first.'
882 846 );
883 847
884 848 setTimeout(function() {
885 - $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]').focus();
849 + $('input[name="<?php echo esc_attr(Metasync_Admin::option_key); ?>[whitelabel][settings_password]"]').focus();
886 850 }, 300);
887 851
888 852 return false;
889 853 }
@@ -954,10 +918,10 @@
954 918 </div>
955 919 <?php endif; ?>
956 920
957 921 <?php
958 - if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'):
959 - $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1';
922 + if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice
923 + $indefinite = isset($_GET['indefinite']) && $_GET['indefinite'] == '1'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice
960 924 ?>
961 925 <div class="notice notice-success inline" style="margin-bottom: 20px;">
962 926 <p>
963 927 <strong>✅ Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>.
@@ -964,9 +928,9 @@
964 928 </p>
965 929 </div>
966 930 <?php endif; ?>
967 931
968 - <?php if (isset($_GET['debug_mode_disabled']) && $_GET['debug_mode_disabled'] == '1'): ?>
932 + <?php if (isset($_GET['debug_mode_disabled']) && $_GET['debug_mode_disabled'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
969 933 <div class="notice notice-info inline" style="margin-bottom: 20px;">
970 934 <p>
971 935 <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled.
972 936 </p>
@@ -972,9 +936,9 @@
972 936 </p>
973 937 </div>
974 938 <?php endif; ?>
975 939
976 - <?php if (isset($_GET['debug_mode_extended']) && $_GET['debug_mode_extended'] == '1'): ?>
940 + <?php if (isset($_GET['debug_mode_extended']) && $_GET['debug_mode_extended'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
977 941 <div class="notice notice-success inline" style="margin-bottom: 20px;">
978 942 <p>
979 943 <strong>✅ Extended!</strong> Debug mode has been extended for another 24 hours.
980 944 </p>
@@ -980,9 +944,9 @@
980 944 </p>
981 945 </div>
982 946 <?php endif; ?>
983 947
984 - <?php if (isset($_GET['debug_error']) && $_GET['debug_error'] == '1'): ?>
948 + <?php if (isset($_GET['debug_error']) && $_GET['debug_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
985 949 <div class="notice notice-error inline" style="margin-bottom: 20px;">
986 950 <p>
987 951 <strong>❌ Error:</strong> Unable to perform the debug mode operation. Please try again.
988 952 </p>
@@ -1019,9 +983,9 @@
1019 983 'method': 'post',
1020 984 'action': ''
1021 985 });
1022 986
1023 - logoutForm.append('<?php echo wp_nonce_field("whitelabel_logout_nonce", "whitelabel_logout_nonce", true, false); ?>');
987 + logoutForm.append('<?php echo wp_nonce_field("whitelabel_logout_nonce", "whitelabel_logout_nonce", true, false); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- nonce markup from wp_nonce_field() is safe by construction; kses-based escaping strips the <input> and breaks the logout nonce ?>');
1024 988
1025 989 logoutForm.append($('<input>', {
1026 990 'type': 'hidden',
1027 991 'name': 'whitelabel_logout',
@@ -1093,9 +1057,9 @@
1093 1057 $.ajax({
1094 1058 url: ajaxUrl,
1095 1059 type: 'POST',
1096 1060 dataType: 'json',
1097 - data: { action: action, nonce: '<?php echo wp_create_nonce('metasync_nonce'); ?>' },
1061 + data: { action: action, nonce: '<?php echo esc_attr(wp_create_nonce('metasync_nonce')); ?>' },
1098 1062 timeout: 35000,
1099 1063 success: function(response, textStatus, xhr) {
1100 1064 try {
1101 1065 if (response && response.success && response.data) {
@@ -1163,22 +1127,22 @@
1163 1127 var blockedClass = result.blocked ? 'blocked' : 'allowed';
1164 1128
1165 1129 html += '<div class="test-result-item ' + statusClass + '">';
1166 1130 html += '<div class="test-result-header">';
1167 - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
1131 + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
1168 1132 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
1169 1133 html += '</div>';
1170 1134
1171 1135 html += '<div class="test-result-details">';
1172 - html += '<p><strong>Response Time:</strong> ' + result.response_time + '</p>';
1173 - html += '<p><strong>Status:</strong> ' + result.status + '</p>';
1136 + html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>';
1137 + html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>';
1174 1138
1175 1139 if (result.status_code) {
1176 - html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + result.status_code + '</span></p>';
1140 + html += '<p><strong>HTTP Status Code:</strong> <span class="status-code">' + escapeHtml(result.status_code) + '</span></p>';
1177 1141 }
1178 1142
1179 1143 if (result.error) {
1180 - html += '<p><strong>Error:</strong> <span class="error-message">' + result.error + '</span></p>';
1144 + html += '<p><strong>Error:</strong> <span class="error-message">' + escapeHtml(result.error) + '</span></p>';
1181 1145 }
1182 1146
1183 1147 if (result.body) {
1184 1148 html += '<p><strong>Response Body:</strong></p>';
@@ -1188,9 +1152,9 @@
1188 1152 if (result.headers && Object.keys(result.headers).length > 0) {
1189 1153 html += '<p><strong>Response Headers:</strong></p>';
1190 1154 html += '<pre class="response-headers">';
1191 1155 Object.keys(result.headers).forEach(function(key) {
1192 - html += key + ': ' + result.headers[key] + '\n';
1156 + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
1193 1157 });
1194 1158 html += '</pre>';
1195 1159 }
1196 1160
@@ -1203,9 +1167,9 @@
1203 1167 html += '<p><strong>Parsed Response:</strong></p>';
1204 1168 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
1205 1169 }
1206 1170
1207 - html += '<p><strong>Details:</strong> ' + result.details + '</p>';
1171 + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
1208 1172 html += '</div>';
1209 1173 html += '</div>';
1210 1174 });
1211 1175
@@ -1220,9 +1184,9 @@
1220 1184 '>': '&gt;',
1221 1185 '"': '&quot;',
1222 1186 '\'': '&#039;'
1223 1187 };
1224 - return text.replace(/[&<>"']/g, function(m) { return map[m]; });
1188 + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
1225 1189 }
1226 1190 });
1227 1191 </script>
1228 1192
@@ -1355,41 +1319,41 @@
1355 1319 }
1356 1320 </style>
1357 1321
1358 1322 <?php if ($active_tab === 'advanced'): ?>
1359 - <?php if (isset($_GET['settings_cleared']) && $_GET['settings_cleared'] == '1'): ?>
1323 + <?php if (isset($_GET['settings_cleared']) && $_GET['settings_cleared'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1360 1324 <div class="notice notice-success is-dismissible">
1361 1325 <p><strong>✅ Success!</strong> All plugin settings have been cleared successfully and a new Plugin Auth Token has been generated. Please reconfigure the plugin as needed.</p>
1362 1326 </div>
1363 1327 <?php endif; ?>
1364 1328
1365 - <?php if (isset($_GET['clear_settings_error']) && $_GET['clear_settings_error'] == '1'): ?>
1329 + <?php if (isset($_GET['clear_settings_error']) && $_GET['clear_settings_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1366 1330 <div class="notice notice-error is-dismissible">
1367 1331 <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p>
1368 1332 </div>
1369 1333 <?php endif; ?>
1370 1334
1371 - <?php if (isset($_GET['log_cleared']) && $_GET['log_cleared'] == '1'): ?>
1335 + <?php if (isset($_GET['log_cleared']) && $_GET['log_cleared'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1372 1336 <div class="notice notice-success is-dismissible">
1373 1337 <p><strong>✅ Success!</strong> Error logs have been cleared successfully.</p>
1374 1338 </div>
1375 1339 <?php endif; ?>
1376 1340
1377 - <?php if (isset($_GET['clear_error']) && $_GET['clear_error'] == '1'): ?>
1341 + <?php if (isset($_GET['clear_error']) && $_GET['clear_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1378 1342 <div class="notice notice-error is-dismissible">
1379 1343 <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p>
1380 1344 </div>
1381 1345 <?php endif; ?>
1382 1346
1383 - <?php if (isset($_GET['access_roles_saved']) && $_GET['access_roles_saved'] == '1'): ?>
1347 + <?php if (isset($_GET['access_roles_saved']) && $_GET['access_roles_saved'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1384 1348 <div class="notice notice-success is-dismissible">
1385 1349 <p><strong>✅ Success!</strong> Plugin access roles have been saved successfully.</p>
1386 1350 </div>
1387 1351 <?php endif; ?>
1388 1352
1389 - <?php if (isset($_GET['access_roles_error']) && $_GET['access_roles_error'] == '1'): ?>
1353 + <?php if (isset($_GET['access_roles_error']) && $_GET['access_roles_error'] == '1'): // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result flag for a static notice ?>
1390 1354 <div class="notice notice-error is-dismissible">
1391 - <p><strong>❌ Error!</strong> Failed to save plugin access roles. <?php echo isset($_GET['message']) ? esc_html(urldecode($_GET['message'])) : 'Please try again.'; ?></p>
1355 + <p><strong>❌ Error!</strong> Failed to save plugin access roles. <?php echo isset($_GET['message']) ? esc_html(urldecode($_GET['message'])) : 'Please try again.'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only error text, escaped on output ?></p>
1392 1356 </div>
1393 1357 <?php endif; ?>
1394 1358
1395 1359 <?php endif; ?>
@@ -1411,9 +1375,9 @@
1411 1375 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Access your <?php echo esc_html(Metasync::get_effective_plugin_name()); ?> dashboard to view analytics, manage SEO settings, and monitor your site performance.</p>
1412 1376 <?php
1413 1377 if (!isset(Metasync::get_option('general')['linkgraph_token']) || Metasync::get_option('general')['linkgraph_token'] == '') {
1414 1378 echo '<p style="color: #d54e21; margin-bottom: 15px;">Authentication required: Please authenticate with your ' . esc_html(Metasync::get_effective_plugin_name()) . ' account and save your auth token in general settings.</p>';
1415 - echo '<a href="' . admin_url('admin.php?page=' . Metasync_Admin::$page_slug) . '" class="button button-secondary">Go to Settings</a>';
1379 + echo '<a href="' . esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug)) . '" class="button button-secondary">Go to Settings</a>';
1416 1380 } else {
1417 1381 echo '<a href="' . esc_url($this->admin->get_dashboard_url()) . '" target="_blank" class="button button-primary">Open Dashboard</a>';
1418 1382 }
1419 1383 ?>
@@ -1423,8 +1387,15 @@
1423 1387 }
1424 1388
1425 1389 public function create_admin_robots_txt_page()
1426 1390 {
1391 + // The form below rewrites the site's physical robots.txt — restrict
1392 + // that to site admins, not just anyone who can open the page via
1393 + // plugin_access_roles. The nonce is CSRF protection, not authorization.
1394 + if (!empty($_POST) && !current_user_can('manage_options')) {
1395 + wp_die(esc_html__('Sorry, you are not allowed to manage robots.txt settings.', 'metasync'));
1396 + }
1397 +
1427 1398 ?>
1428 1399 <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?>
1429 1400
1430 1401 <?php
@@ -1445,8 +1416,11 @@
1445 1416
1446 1417 if (is_wp_error($result)) {
1447 1418 echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>';
1448 1419 } else {
1420 + Metasync_PostHog::feature('robots_txt_saved', array(
1421 + 'warning_count' => count($validation['warnings']),
1422 + ));
1449 1423 echo '<div class="notice notice-success"><p>' . esc_html__('robots.txt file saved successfully!', 'metasync') . '</p></div>';
1450 1424
1451 1425 if (!empty($validation['warnings'])) {
1452 1426 foreach ($validation['warnings'] as $warning) {
@@ -1487,84 +1461,8 @@
1487 1461 {
1488 1462 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1489 1463 }
1490 1464
1491 - public function create_admin_xml_sitemap_page()
1492 - {
1493 - require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
1494 -
1495 - $sitemap_generator = new Metasync_Sitemap_Generator();
1496 -
1497 - if (isset($_POST['metasync_sitemap_nonce'])) {
1498 - check_admin_referer('metasync_sitemap_action', 'metasync_sitemap_nonce');
1499 -
1500 - if (isset($_POST['generate_sitemap'])) {
1501 - $disabled_plugins = $sitemap_generator->disable_other_sitemap_generators();
1502 -
1503 - $result = $sitemap_generator->generate_sitemap();
1504 -
1505 - if (is_wp_error($result)) {
1506 - error_log('[MetaSync] Sitemap generation failed: ' . $result->get_error_message());
1507 - echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed. Please try again or check the error logs.', 'metasync') . '</p></div>';
1508 - } elseif (false === $result) {
1509 - echo '<div class="notice notice-error"><p>' . esc_html__('Sitemap generation failed: the sitemap data could not be stored, so no sitemap is being served. Check your object cache and database write settings, then try again.', 'metasync') . '</p></div>';
1510 - } else {
1511 - $message = esc_html__('Sitemap generated successfully!', 'metasync');
1512 - if ($disabled_plugins) {
1513 - $message .= ' ' . esc_html__('Conflicting sitemap generators have been automatically disabled.', 'metasync');
1514 - }
1515 -
1516 - $robots_result = get_transient('metasync_sitemap_robots_updated');
1517 - if ($robots_result && $robots_result['success']) {
1518 - if ($robots_result['action'] === 'added') {
1519 - $message .= ' ' . esc_html__('Sitemap URL has been added to robots.txt.', 'metasync');
1520 - } elseif ($robots_result['action'] === 'updated') {
1521 - $message .= ' ' . esc_html__('Sitemap URL has been updated in robots.txt.', 'metasync');
1522 - } elseif ($robots_result['action'] === 'created') {
1523 - $message .= ' ' . esc_html__('robots.txt file has been created with sitemap URL.', 'metasync');
1524 - }
1525 - delete_transient('metasync_sitemap_robots_updated');
1526 - }
1527 -
1528 - echo '<div class="notice notice-success"><p>' . esc_html($message) . '</p></div>';
1529 - }
1530 - } elseif (isset($_POST['enable_auto_update'])) {
1531 - update_option('metasync_sitemap_auto_update', true);
1532 - $sitemap_generator->setup_auto_update_hooks();
1533 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update enabled!', 'metasync') . '</p></div>';
1534 - } elseif (isset($_POST['disable_auto_update'])) {
1535 - update_option('metasync_sitemap_auto_update', false);
1536 - echo '<div class="notice notice-success"><p>' . esc_html__('Auto-update disabled!', 'metasync') . '</p></div>';
1537 - } elseif (isset($_POST['delete_sitemap'])) {
1538 - $deleted = $sitemap_generator->delete_sitemap();
1539 - if ($deleted) {
1540 - update_option('metasync_sitemap_auto_update', false);
1541 - // Re-enable WP core sitemap so the site isn't left with zero sitemaps
1542 - delete_option('metasync_disable_wp_sitemap');
1543 - echo '<div class="notice notice-success"><p>' . esc_html__('Sitemap deleted successfully!', 'metasync') . '</p></div>';
1544 - } else {
1545 - 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>';
1546 - }
1547 - } elseif (isset($_POST['enable_other_sitemaps'])) {
1548 - $enabled_plugins = $sitemap_generator->enable_other_sitemap_generators();
1549 - if ($enabled_plugins) {
1550 - echo '<div class="notice notice-success"><p>' . esc_html__('Other sitemap plugins have been re-enabled successfully!', 'metasync') . '</p></div>';
1551 - } else {
1552 - echo '<div class="notice notice-info"><p>' . esc_html__('No sitemap plugins were found to re-enable.', 'metasync') . '</p></div>';
1553 - }
1554 - }
1555 - }
1556 -
1557 - $sitemap_exists = $sitemap_generator->sitemap_exists();
1558 - $sitemap_url = $sitemap_generator->get_sitemap_url();
1559 - $url_count = $sitemap_generator->count_urls();
1560 - $last_generated = $sitemap_generator->get_last_generated_time();
1561 - $auto_update_enabled = get_option('metasync_sitemap_auto_update', false);
1562 - $active_sitemap_plugins = $sitemap_generator->check_active_sitemap_plugins();
1563 -
1564 - require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-xml-sitemap.php';
1565 - }
1566 -
1567 1465 public function create_admin_custom_pages_page()
1568 1466 {
1569 1467 ?>
1570 1468 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
@@ -1615,12 +1513,14 @@
1615 1513 }
1616 1514
1617 1515 public function create_admin_breadcrumbs_page()
1618 1516 {
1619 - $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs';
1620 1517 ?>
1621 1518 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1622 1519
1520 + <?php /* options.php redirects back here with ?settings-updated=true; without this call the save confirmation never prints */ ?>
1521 + <?php settings_errors(); ?>
1522 +
1623 1523 <form method="post" action="options.php">
1624 1524 <div class="dashboard-card">
1625 1525 <h2>Breadcrumbs Settings</h2>
1626 1526 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
@@ -1625,9 +1525,9 @@
1625 1525 <h2>Breadcrumbs Settings</h2>
1626 1526 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1627 1527 <?php
1628 1528 settings_fields(Metasync_Admin::option_group);
1629 - do_settings_sections($page_slug);
1529 + Metasync_Settings_Fields::instance()->render_breadcrumbs_section();
1630 1530 submit_button();
1631 1531 ?>
1632 1532 </div>
1633 1533 </form>
@@ -1747,14 +1647,8 @@
1747 1647 </div>
1748 1648 </form>
1749 1649 <?php $this->admin->render_layout_close(); ?>
1750 1650 <?php
1751 - }
1752 -
1753 - public function create_admin_google_instant_index_page()
1754 - {
1755 - // Delegates to the admin class which uses the shared credentials view
1756 - $this->admin->create_admin_google_instant_index_page();
1757 1651 }
1758 1652
1759 1653 public function create_admin_google_console_page()
1760 1654 {