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 +76 -420 2.6.21 → 2.7.2 View file →
@@ -217,10 +217,15 @@
217 217 // ------------------------------------------------------------------
218 218
219 219 public function create_admin_settings_page()
220 220 {
221 - $active_tab = isset($_GET['tab']) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general';
221 + if (isset($_REQUEST['metasync_password_reset'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- routing check, the reset form verifies its own nonce
222 + Metasync_Admin_Ajax::instance()->render_password_reset_page();
223 + return;
224 + }
222 225
226 + $active_tab = isset($_GET['tab']) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'general'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only tab selection
227 +
223 228 $whitelabel_settings = Metasync::get_whitelabel_settings();
224 229
225 230 $user_password = $whitelabel_settings['settings_password'] ?? '';
226 231 $hide_settings_enabled = !empty($whitelabel_settings['hide_settings']);
@@ -245,9 +250,9 @@
245 250
246 251 $auth = new Metasync_Auth_Manager('whitelabel', 1800);
247 252 $password_validated = $auth->has_access();
248 253
249 - if (isset($_POST['whitelabel_password_submit']) && !$password_validated) {
254 + if (isset($_POST['whitelabel_password_submit']) && !$password_validated) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only access check for a display message
250 255 $password_error = 'Incorrect password. Please try again.';
251 256 }
252 257 }
253 258
@@ -264,8 +269,18 @@
264 269 $this->admin->render_layout_open('Settings', $settings_current_page, 'Manage all plugin settings and configuration.');
265 270 ?>
266 271 <?php
267 272 /*
273 + * options.php redirects back here after the native (no-JavaScript)
274 + * settings submit. Scoped to metasync_messages so field-level
275 + * rejections (e.g. invalid white-label branding) print without the
276 + * generic "Settings saved." notice that would otherwise accompany
277 + * a rejected save.
278 + */
279 + settings_errors('metasync_messages');
280 + ?>
281 + <?php
282 + /*
268 283 # Temporarily commented out: Clear Cache notice and button (can be re-enabled later)
269 284
270 285 * <div class="notice notice-success">
271 286 * <p>
@@ -333,8 +348,9 @@
333 348 </button>
334 349 </div>
335 350 </form>
336 351
352 + <?php if (current_user_can('manage_options')): ?>
337 353 <div style="text-align: center; margin-top: 20px;">
338 354 <a href="#" id="metasync-forgot-password-link" style="color: #2271b1; text-decoration: none; font-size: 14px;">
339 355 Forgot Password?
340 356 </a>
@@ -339,64 +355,12 @@
339 355 Forgot Password?
340 356 </a>
341 357 <div id="metasync-recovery-message" style="margin-top: 15px; padding: 12px; border-radius: 6px; display: none;"></div>
342 358 </div>
359 + <?php endif; ?>
343 360
344 361 </div>
345 362
346 - <script>
347 - jQuery(document).ready(function($) {
348 - $('#whitelabel_password').focus();
349 -
350 - $('#whitelabel_password').on('keypress', function(e) {
351 - if (e.which === 13) {
352 - $(this).closest('form').submit();
353 - }
354 - });
355 -
356 - $('#metasync-forgot-password-link').on('click', function(e) {
357 - e.preventDefault();
358 -
359 - var $link = $(this);
360 - var $message = $('#metasync-recovery-message');
361 -
362 - $link.css('pointer-events', 'none').css('opacity', '0.6');
363 - $message.removeClass('success error').hide();
364 - $message.html('⏳ Sending recovery email...').css('background', '#f0f6fc').css('color', '#0c5ba5').css('border', '1px solid #cfe2f3').fadeIn(200);
365 -
366 - $.ajax({
367 - url: ajaxurl,
368 - type: 'POST',
369 - data: {
370 - action: 'metasync_recover_password',
371 - nonce: '<?php echo wp_create_nonce('metasync_recover_password_nonce'); ?>'
372 - },
373 - success: function(response) {
374 - $link.css('pointer-events', 'auto').css('opacity', '1');
375 -
376 - if (response.success) {
377 - $message.addClass('success').html('✅ ' + response.data.message)
378 - .css('background', '#d4edda')
379 - .css('color', '#155724')
380 - .css('border', '1px solid #c3e6cb');
381 - } else {
382 - $message.addClass('error').html('❌ ' + response.data.message)
383 - .css('background', '#f8d7da')
384 - .css('color', '#721c24')
385 - .css('border', '1px solid #f5c6cb');
386 - }
387 - },
388 - error: function() {
389 - $link.css('pointer-events', 'auto').css('opacity', '1');
390 - $message.addClass('error').html('❌ An error occurred. Please try again.')
391 - .css('background', '#f8d7da')
392 - .css('color', '#721c24')
393 - .css('border', '1px solid #f5c6cb');
394 - }
395 - });
396 - });
397 - });
398 - </script>
399 363 <?php
400 364 return;
401 365 } elseif (!$password_protection_enabled) {
402 366 ?>
@@ -410,8 +374,9 @@
410 374 }
411 375 ?>
412 376
413 377 <form method="post" action="options.php?tab=<?php echo esc_attr( $active_tab ); ?>" id="metaSyncGeneralSetting">
378 + <input type="hidden" name="metasync_pw_reset_generation" value="<?php echo esc_attr((string) (int) get_option('metasync_pw_reset_generation', 0)); ?>" />
414 379 <?php
415 380 settings_fields(Metasync_Admin::option_group);
416 381
417 382 wp_nonce_field('meta_sync_general_setting_nonce', 'meta_sync_nonce');
@@ -439,8 +404,11 @@
439 404
440 405 <?php
441 406 $this->admin->render_accordion_sections(Metasync_Admin::$page_slug . '_general');
442 407 ?>
408 + <p style="margin: 20px 0 0 0; font-size: 13px; color: var(--dashboard-text-secondary);">
409 + Breadcrumb settings now live on their own page: <a href="<?php echo esc_url(admin_url('admin.php?page=' . Metasync_Admin::$page_slug . '-breadcrumbs')); ?>">Breadcrumbs</a>.
410 + </p>
443 411 </div>
444 412
445 413 <div class="dashboard-card">
446 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>
@@ -825,21 +793,21 @@
825 793 }
826 794 });
827 795
828 796 function hasPassword() {
829 - 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]"]');
830 798 var passwordValue = passwordField.val();
831 799 return passwordValue && passwordValue.length > 0;
832 800 }
833 801
834 802 function hasRecoveryEmail() {
835 - 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]"]');
836 804 var emailValue = recoveryEmailField.val();
837 805 return emailValue && emailValue.length > 0 && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailValue);
838 806 }
839 807
840 - var passwordField = $('input[name="<?php echo Metasync_Admin::option_key; ?>[whitelabel][settings_password]"]');
841 - 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]"]');
842 810
843 811 passwordField.on('blur', function() {
844 812 if (hasPassword() && !hasRecoveryEmail()) {
845 813 showModal(
@@ -877,9 +845,9 @@
877 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.'
878 846 );
879 847
880 848 setTimeout(function() {
881 - $('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();
882 850 }, 300);
883 851
884 852 return false;
885 853 }
@@ -919,42 +887,8 @@
919 887 $('#metasync-custom-modal').fadeOut(200);
920 888 }
921 889 });
922 890
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 891 });
958 892 </script>
959 893 <?php
960 894 } elseif ($active_tab == 'advanced') {
@@ -984,10 +918,10 @@
984 918 </div>
985 919 <?php endif; ?>
986 920
987 921 <?php
988 - if (isset($_GET['debug_mode_enabled']) && $_GET['debug_mode_enabled'] == '1'):
989 - $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
990 924 ?>
991 925 <div class="notice notice-success inline" style="margin-bottom: 20px;">
992 926 <p>
993 927 <strong>✅ Success!</strong> Debug mode has been enabled<?php echo $indefinite ? ' indefinitely' : ' for 24 hours'; ?>.
@@ -994,9 +928,9 @@
994 928 </p>
995 929 </div>
996 930 <?php endif; ?>
997 931
998 - <?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 ?>
999 933 <div class="notice notice-info inline" style="margin-bottom: 20px;">
1000 934 <p>
1001 935 <strong>ℹ️ Debug Mode Disabled:</strong> Debug mode has been successfully disabled.
1002 936 </p>
@@ -1002,9 +936,9 @@
1002 936 </p>
1003 937 </div>
1004 938 <?php endif; ?>
1005 939
1006 - <?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 ?>
1007 941 <div class="notice notice-success inline" style="margin-bottom: 20px;">
1008 942 <p>
1009 943 <strong>✅ Extended!</strong> Debug mode has been extended for another 24 hours.
1010 944 </p>
@@ -1010,9 +944,9 @@
1010 944 </p>
1011 945 </div>
1012 946 <?php endif; ?>
1013 947
1014 - <?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 ?>
1015 949 <div class="notice notice-error inline" style="margin-bottom: 20px;">
1016 950 <p>
1017 951 <strong>❌ Error:</strong> Unable to perform the debug mode operation. Please try again.
1018 952 </p>
@@ -1049,9 +983,9 @@
1049 983 'method': 'post',
1050 984 'action': ''
1051 985 });
1052 986
1053 - 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 ?>');
1054 988
1055 989 logoutForm.append($('<input>', {
1056 990 'type': 'hidden',
1057 991 'name': 'whitelabel_logout',
@@ -1123,9 +1057,9 @@
1123 1057 $.ajax({
1124 1058 url: ajaxUrl,
1125 1059 type: 'POST',
1126 1060 dataType: 'json',
1127 - 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')); ?>' },
1128 1062 timeout: 35000,
1129 1063 success: function(response, textStatus, xhr) {
1130 1064 try {
1131 1065 if (response && response.success && response.data) {
@@ -1193,22 +1127,22 @@
1193 1127 var blockedClass = result.blocked ? 'blocked' : 'allowed';
1194 1128
1195 1129 html += '<div class="test-result-item ' + statusClass + '">';
1196 1130 html += '<div class="test-result-header">';
1197 - html += '<h4>' + statusIcon + ' ' + result.method + ' Request - ' + blockedStatus + '</h4>';
1131 + html += '<h4>' + statusIcon + ' ' + escapeHtml(result.method) + ' Request - ' + blockedStatus + '</h4>';
1198 1132 html += '<span class="test-status ' + blockedClass + '">' + blockedStatus + '</span>';
1199 1133 html += '</div>';
1200 1134
1201 1135 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>';
1136 + html += '<p><strong>Response Time:</strong> ' + escapeHtml(result.response_time) + '</p>';
1137 + html += '<p><strong>Status:</strong> ' + escapeHtml(result.status) + '</p>';
1204 1138
1205 1139 if (result.status_code) {
1206 - 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>';
1207 1141 }
1208 1142
1209 1143 if (result.error) {
1210 - 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>';
1211 1145 }
1212 1146
1213 1147 if (result.body) {
1214 1148 html += '<p><strong>Response Body:</strong></p>';
@@ -1218,9 +1152,9 @@
1218 1152 if (result.headers && Object.keys(result.headers).length > 0) {
1219 1153 html += '<p><strong>Response Headers:</strong></p>';
1220 1154 html += '<pre class="response-headers">';
1221 1155 Object.keys(result.headers).forEach(function(key) {
1222 - html += key + ': ' + result.headers[key] + '\n';
1156 + html += escapeHtml(key) + ': ' + escapeHtml(result.headers[key]) + '\n';
1223 1157 });
1224 1158 html += '</pre>';
1225 1159 }
1226 1160
@@ -1233,9 +1167,9 @@
1233 1167 html += '<p><strong>Parsed Response:</strong></p>';
1234 1168 html += '<pre class="parsed-response">' + escapeHtml(JSON.stringify(result.parsed_response, null, 2)) + '</pre>';
1235 1169 }
1236 1170
1237 - html += '<p><strong>Details:</strong> ' + result.details + '</p>';
1171 + html += '<p><strong>Details:</strong> ' + escapeHtml(result.details) + '</p>';
1238 1172 html += '</div>';
1239 1173 html += '</div>';
1240 1174 });
1241 1175
@@ -1250,9 +1184,9 @@
1250 1184 '>': '&gt;',
1251 1185 '"': '&quot;',
1252 1186 '\'': '&#039;'
1253 1187 };
1254 - return text.replace(/[&<>"']/g, function(m) { return map[m]; });
1188 + return String(text).replace(/[&<>"']/g, function(m) { return map[m]; });
1255 1189 }
1256 1190 });
1257 1191 </script>
1258 1192
@@ -1385,41 +1319,41 @@
1385 1319 }
1386 1320 </style>
1387 1321
1388 1322 <?php if ($active_tab === 'advanced'): ?>
1389 - <?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 ?>
1390 1324 <div class="notice notice-success is-dismissible">
1391 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>
1392 1326 </div>
1393 1327 <?php endif; ?>
1394 1328
1395 - <?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 ?>
1396 1330 <div class="notice notice-error is-dismissible">
1397 1331 <p><strong>❌ Error!</strong> Failed to clear settings due to a security check failure. Please try again.</p>
1398 1332 </div>
1399 1333 <?php endif; ?>
1400 1334
1401 - <?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 ?>
1402 1336 <div class="notice notice-success is-dismissible">
1403 1337 <p><strong>✅ Success!</strong> Error logs have been cleared successfully.</p>
1404 1338 </div>
1405 1339 <?php endif; ?>
1406 1340
1407 - <?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 ?>
1408 1342 <div class="notice notice-error is-dismissible">
1409 1343 <p><strong>❌ Error!</strong> Failed to clear error logs due to a security check failure. Please try again.</p>
1410 1344 </div>
1411 1345 <?php endif; ?>
1412 1346
1413 - <?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 ?>
1414 1348 <div class="notice notice-success is-dismissible">
1415 1349 <p><strong>✅ Success!</strong> Plugin access roles have been saved successfully.</p>
1416 1350 </div>
1417 1351 <?php endif; ?>
1418 1352
1419 - <?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 ?>
1420 1354 <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>
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>
1422 1356 </div>
1423 1357 <?php endif; ?>
1424 1358
1425 1359 <?php endif; ?>
@@ -1441,9 +1375,9 @@
1441 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>
1442 1376 <?php
1443 1377 if (!isset(Metasync::get_option('general')['linkgraph_token']) || Metasync::get_option('general')['linkgraph_token'] == '') {
1444 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>';
1445 - 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>';
1446 1380 } else {
1447 1381 echo '<a href="' . esc_url($this->admin->get_dashboard_url()) . '" target="_blank" class="button button-primary">Open Dashboard</a>';
1448 1382 }
1449 1383 ?>
@@ -1453,8 +1387,15 @@
1453 1387 }
1454 1388
1455 1389 public function create_admin_robots_txt_page()
1456 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 +
1457 1398 ?>
1458 1399 <?php $this->admin->render_layout_open('Robots.txt', 'robots_txt', 'Control which pages search engines can crawl.'); ?>
1459 1400
1460 1401 <?php
@@ -1475,8 +1416,11 @@
1475 1416
1476 1417 if (is_wp_error($result)) {
1477 1418 echo '<div class="notice notice-error"><p>' . esc_html($result->get_error_message()) . '</p></div>';
1478 1419 } else {
1420 + Metasync_PostHog::feature('robots_txt_saved', array(
1421 + 'warning_count' => count($validation['warnings']),
1422 + ));
1479 1423 echo '<div class="notice notice-success"><p>' . esc_html__('robots.txt file saved successfully!', 'metasync') . '</p></div>';
1480 1424
1481 1425 if (!empty($validation['warnings'])) {
1482 1426 foreach ($validation['warnings'] as $warning) {
@@ -1517,82 +1461,8 @@
1517 1461 {
1518 1462 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-report-issue.php';
1519 1463 }
1520 1464
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 1465 public function create_admin_custom_pages_page()
1596 1466 {
1597 1467 ?>
1598 1468 <?php $this->admin->render_layout_open('Custom Pages', 'custom_pages', 'Create blank canvas pages for Elementor, Divi, or Gutenberg.'); ?>
@@ -1643,12 +1513,14 @@
1643 1513 }
1644 1514
1645 1515 public function create_admin_breadcrumbs_page()
1646 1516 {
1647 - $page_slug = Metasync_Admin::$page_slug . '_breadcrumbs';
1648 1517 ?>
1649 1518 <?php $this->admin->render_layout_open('Breadcrumbs', 'breadcrumbs', 'Configure breadcrumb trail settings and placement.'); ?>
1650 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 +
1651 1523 <form method="post" action="options.php">
1652 1524 <div class="dashboard-card">
1653 1525 <h2>Breadcrumbs Settings</h2>
1654 1526 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
@@ -1653,9 +1525,9 @@
1653 1525 <h2>Breadcrumbs Settings</h2>
1654 1526 <p style="color: var(--dashboard-text-secondary); margin-bottom: 20px;">Configure how breadcrumb trails are displayed across your site.</p>
1655 1527 <?php
1656 1528 settings_fields(Metasync_Admin::option_group);
1657 - do_settings_sections($page_slug);
1529 + Metasync_Settings_Fields::instance()->render_breadcrumbs_section();
1658 1530 submit_button();
1659 1531 ?>
1660 1532 </div>
1661 1533 </form>
@@ -1777,14 +1649,8 @@
1777 1649 <?php $this->admin->render_layout_close(); ?>
1778 1650 <?php
1779 1651 }
1780 1652
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 1653 public function create_admin_google_console_page()
1788 1654 {
1789 1655 $this->admin->render_layout_open('Google Console', 'google_console', 'View Google Search Console data and manage indexing requests.');
1790 1656 $google = function_exists('google_index_direct') ? google_index_direct() : null;
@@ -1951,177 +1817,15 @@
1951 1817 <?php $this->admin->render_layout_close(); ?>
1952 1818 <?php
1953 1819 }
1954 1820
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 - ];
1821 + // NOTE: The Changes Log (sync log) page is rendered by
1822 + // Metasync_Admin::create_admin_sync_log_page() in
1823 + // admin/class-metasync-admin.php, which is the callback registered in
1824 + // class-metasync-admin-navigation.php. A former duplicate of that method
1825 + // lived here but was never wired to a menu and has been removed to avoid
1826 + // drift.
1972 1827
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 1828 public function creat_error_Logs_List()
2125 1829 {
2126 1830 // printf('<h1> Error Logs </h1>');
2127 1831 // $error_log = new Metasync_Error_Logs_Table($this->data_error_log_list);
@@ -2150,59 +1854,11 @@
2150 1854 require_once plugin_dir_path(dirname(__FILE__)) . 'views/metasync-otto-bot-statistics.php';
2151 1855 }
2152 1856
2153 1857 // ------------------------------------------------------------------
2154 - // Private helpers (copied from Metasync_Admin, used only by pages above)
1858 + // Private helpers
2155 1859 // ------------------------------------------------------------------
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 - }
1860 + // The former time_elapsed_string / build_filter_query_string /
1861 + // handle_sync_log_ajax helpers here were only used by the dead
1862 + // create_admin_sync_log_page() duplicate that has since been removed. The
1863 + // live equivalents live on Metasync_Admin.
2208 1864 }