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 | admin/class-metasync-admin.php +167 -201 2.7.0 → 2.7.1 View file →
@@ -212,9 +212,8 @@
212 212 add_action('admin_menu', array($this, 'add_import_external_data_page'));
213 213 add_action('admin_init', array($this, 'settings_page_init'));
214 214 add_filter('all_plugins', array($this,'metasync_plugin_white_label'));
215 215 add_filter( 'plugin_row_meta',array($this,'metasync_view_detials_url'),10,3);
216 - add_filter('site_transient_update_plugins', array($this, 'inject_whitelabel_icon_into_update_transient'));
217 216
218 217 // Display transient error/success messages for redirections
219 218 add_action('admin_notices', array($this, 'display_redirection_messages'));
220 219
@@ -498,9 +497,9 @@
498 497 <?php
499 498 }
500 499
501 500 public function metasync_fouc_prevention_style() {
502 - if ( ! isset( $_GET['page'] ) || strpos( $_GET['page'], self::$page_slug ) !== 0 ) {
501 + if ( ! isset( $_GET['page'] ) || strpos( sanitize_key(wp_unslash($_GET['page'])), self::$page_slug ) !== 0 ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check for an inline style
503 502 return;
504 503 }
505 504 ?>
506 505 <style>
@@ -522,13 +521,13 @@
522 521 <?php
523 522 }
524 523
525 524 public function suppress_notices_on_wizard_page() {
526 - if ( ! isset( $_GET['page'] ) ) {
525 + if ( ! isset( $_GET['page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check to hide notices for this request
527 526 return;
528 527 }
529 528
530 - $page = sanitize_text_field( wp_unslash( $_GET['page'] ) );
529 + $page = sanitize_key(wp_unslash($_GET['page'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only navigation values for a same-site redirect
531 530
532 531 // Setup wizard: strip every notice for a fully focused, distraction-free screen.
533 532 if ( strpos( $page, '-setup-wizard' ) !== false ) {
534 533 remove_all_actions( 'admin_notices' );
@@ -646,9 +645,9 @@
646 645
647 646 #Redirect url to the new slug
648 647 $redirect_url = admin_url('admin.php?page=' . $current_slug);
649 648
650 - wp_redirect($redirect_url);
649 + wp_safe_redirect($redirect_url);
651 650 exit;
652 651 }
653 652 }
654 653
@@ -700,13 +699,13 @@
700 699 * @since 1.0.0
701 700 */
702 701 public function maybe_redirect_to_wizard() {
703 702 // Only redirect if wizard should be shown
704 - if (get_option('metasync_show_wizard') && !isset($_GET['page'])) {
703 + if (get_option('metasync_show_wizard') && !isset($_GET['page'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- isset() routing check for a redirect to a fixed admin URL
705 704 delete_option('metasync_show_wizard');
706 705
707 706 // Don't redirect during AJAX, cron, or bulk activation
708 - if (wp_doing_ajax() || wp_doing_cron() || isset($_GET['activate-multi'])) {
707 + if (wp_doing_ajax() || wp_doing_cron() || isset($_GET['activate-multi'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- isset() routing check for a redirect to a fixed admin URL
709 708 return;
710 709 }
711 710
712 711 // Only redirect if user has access to the plugin
@@ -726,13 +725,13 @@
726 725 * the old URL. Resolve the current white-label slug rather than assuming
727 726 * "searchatlas".
728 727 */
729 728 public function redirect_legacy_instant_indexing_page() {
730 - if (wp_doing_ajax() || wp_doing_cron() || !is_admin() || !isset($_GET['page'])) {
729 + if (wp_doing_ajax() || wp_doing_cron() || !is_admin() || !isset($_GET['page'])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only navigation values for a same-site redirect
731 730 return;
732 731 }
733 732
734 - $page = sanitize_key(wp_unslash($_GET['page']));
733 + $page = sanitize_key(wp_unslash($_GET['page'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check to hide notices for this request
735 734 $legacy_page = self::$page_slug . '-instant-index';
736 735 if ($page !== $legacy_page || !Metasync::current_user_has_plugin_access()) {
737 736 return;
738 737 }
@@ -747,10 +746,10 @@
747 746 if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
748 747 $target = admin_url('admin.php?page=' . self::$page_slug . '-seo-controls');
749 748 $query = [];
750 749 foreach (['tab', 'subtab'] as $key) {
751 - if (isset($_GET[$key])) {
752 - $query[$key] = sanitize_key(wp_unslash($_GET[$key]));
750 + if (isset($_GET[$key])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only navigation values for a same-site redirect
751 + $query[$key] = sanitize_key(wp_unslash($_GET[$key])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only navigation values for a same-site redirect
753 752 }
754 753 }
755 754 if (!empty($query)) {
756 755 $target = add_query_arg($query, $target);
@@ -810,9 +809,9 @@
810 809 ]);
811 810 $_COOKIE['metasync_previous_slug'] = $new_slug;
812 811 // Redirect to the new slug
813 812 $redirect_url = admin_url('admin.php?page=' . $old_slug);
814 - wp_redirect($redirect_url);
813 + wp_safe_redirect($redirect_url);
815 814 exit;
816 815 }else{
817 816 self::$page_slug = Metasync::get_option('general')['white_label_plugin_menu_slug']=="" ? "searchatlas":Metasync::get_option('general')['white_label_plugin_menu_slug'];
818 817 $redirect_url = admin_url('admin.php?page=' . self::$page_slug);
@@ -818,9 +817,9 @@
818 817 $redirect_url = admin_url('admin.php?page=' . self::$page_slug);
819 818
820 819 #add redirection for when the old slug is not defined
821 820 #this fixes the redirect issue #
822 - wp_redirect($redirect_url);
821 + wp_safe_redirect($redirect_url);
823 822 exit;
824 823 }
825 824 }
826 825 public function metasync_view_detials_url( $plugin_meta, $plugin_file, $plugin_data ) {
@@ -831,9 +830,10 @@
831 830 if (strpos($meta, 'open-plugin-details-modal') !== false) {
832 831 $meta = sprintf(
833 832 '<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
834 833 add_query_arg('TB_iframe', 'true', $plugin_uri),
835 - esc_attr(sprintf(esc_html__('More information about %s'), $plugin_data['Name'])),
834 + /* translators: %s: plugin name. */
835 + esc_attr(sprintf(esc_html__('More information about %s', 'metasync'), $plugin_data['Name'])),
836 836 esc_attr($plugin_data['Name']),
837 837 esc_html__('View details', 'metasync')
838 838 );
839 839 break; // Exit loop after replacing the link
@@ -881,53 +881,8 @@
881 881 return $all_plugins;
882 882 }
883 883
884 884 /**
885 - * Inject the whitelabel icon into the update_plugins transient so that
886 - * /wp-admin/update-core.php (Dashboard → Updates) shows the WL icon
887 - * instead of the SearchAtlas icon returned by the update API.
888 - *
889 - * @param object $transient The site transient object.
890 - * @return object
891 - */
892 - public function inject_whitelabel_icon_into_update_transient($transient) {
893 - if (empty($transient) || !is_object($transient)) {
894 - return $transient;
895 - }
896 -
897 - $general = Metasync::get_option('general');
898 - if (!is_array($general)) {
899 - return $transient;
900 - }
901 -
902 - $icon_url = $general['white_label_plugin_menu_icon'] ?? '';
903 - if (empty($icon_url)) {
904 - return $transient;
905 - }
906 -
907 - $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php');
908 -
909 - // Inject into pending updates list
910 - if (!empty($transient->response) && isset($transient->response[$this_plugin])) {
911 - $transient->response[$this_plugin]->icons = [
912 - '1x' => $icon_url,
913 - '2x' => $icon_url,
914 - ];
915 - }
916 -
917 - // Also inject into the "no update needed" list so the icon appears
918 - // on the updates screen even when the plugin is up-to-date
919 - if (!empty($transient->no_update) && isset($transient->no_update[$this_plugin])) {
920 - $transient->no_update[$this_plugin]->icons = [
921 - '1x' => $icon_url,
922 - '2x' => $icon_url,
923 - ];
924 - }
925 -
926 - return $transient;
927 - }
928 -
929 - /**
930 885 * Register the stylesheets for the admin area.
931 886 *
932 887 * @since 1.0.0
933 888 */
@@ -932,9 +887,9 @@
932 887 * @since 1.0.0
933 888 */
934 889 public function enqueue_styles()
935 890 {
936 - $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
891 + $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check to pick stylesheets
937 892
938 893 if (strpos($current_page, self::$page_slug) === 0) {
939 894 wp_enqueue_style(
940 895 $this->plugin_name,
@@ -963,9 +918,9 @@
963 918 );
964 919 }
965 920
966 921 // Enqueue wizard CSS if on wizard page
967 - if (isset($_GET['page']) && strpos($_GET['page'], '-setup-wizard') !== false) {
922 + if (isset($_GET['page']) && strpos(sanitize_key(wp_unslash($_GET['page'])), '-setup-wizard') !== false) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check to pick stylesheets
968 923 wp_enqueue_style(
969 924 $this->plugin_name . '-setup-wizard',
970 925 plugin_dir_url(__FILE__) . 'css/metasync-setup-wizard.css',
971 926 array($this->plugin_name . '-dashboard'),
@@ -974,9 +929,9 @@
974 929 );
975 930 }
976 931
977 932 // Enqueue SEO Health CSS if on the SEO Health page
978 - if (isset($_GET['page']) && strpos($_GET['page'], '-seo-health') !== false) {
933 + if (isset($_GET['page']) && strpos(sanitize_key(wp_unslash($_GET['page'])), '-seo-health') !== false) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check to pick stylesheets
979 934 wp_enqueue_style(
980 935 $this->plugin_name . '-seo-health',
981 936 plugin_dir_url(__FILE__) . 'css/metasync-seo-health.css',
982 937 array($this->plugin_name . '-dashboard'),
@@ -993,9 +948,9 @@
993 948 */
994 949 public function enqueue_scripts()
995 950 {
996 951 // --- Phase 5 (#887): Extracted inline JS files ---
997 - $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : '';
952 + $current_page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check to pick scripts
998 953 $plugin_root_url = plugin_dir_url(dirname(__FILE__));
999 954 $is_metasync_page = ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0);
1000 955
1001 956 if ($is_metasync_page) {
@@ -1056,8 +1011,16 @@
1056 1011 array(),
1057 1012 $this->version,
1058 1013 true
1059 1014 );
1015 + // Selector navigation is a GET, so the per-page nonce rides along
1016 + // as a query parameter and gates the user_meta persist in
1017 + // Metasync_Per_Page_Helper::resolve().
1018 + wp_localize_script(
1019 + $this->plugin_name . '-per-page',
1020 + 'metasyncPerPage',
1021 + array('nonces' => Metasync_Per_Page_Helper::page_nonces())
1022 + );
1060 1023 }
1061 1024
1062 1025 // Dashboard iframe height (only on dashboard page)
1063 1026 if ($current_page === self::$page_slug . '-dashboard' || $current_page === self::$page_slug) {
@@ -1108,12 +1071,12 @@
1108 1071 true
1109 1072 );
1110 1073 // Pass the active tab from server (handles POST redirect)
1111 1074 $sitemap_active_tab = 'general';
1112 - if (isset($_GET['tab']) && in_array($_GET['tab'], ['general', 'news', 'video'], true)) {
1113 - $sitemap_active_tab = sanitize_text_field(wp_unslash($_GET['tab']));
1114 - } elseif (isset($_POST['redirect_tab']) && in_array($_POST['redirect_tab'], ['general', 'news', 'video'], true)) {
1115 - $sitemap_active_tab = sanitize_text_field(wp_unslash($_POST['redirect_tab']));
1075 + if (isset($_GET['tab']) && in_array($_GET['tab'], ['general', 'news', 'video'], true)) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only, allow-listed tab selection for the UI
1076 + $sitemap_active_tab = sanitize_text_field(wp_unslash($_GET['tab'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only, allow-listed tab selection for the UI
1077 + } elseif (isset($_POST['redirect_tab']) && in_array($_POST['redirect_tab'], ['general', 'news', 'video'], true)) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only, allow-listed tab selection for the UI
1078 + $sitemap_active_tab = sanitize_text_field(wp_unslash($_POST['redirect_tab'])); // phpcs:ignore WordPress.Security.NonceVerification.Missing -- read-only, allow-listed tab selection for the UI
1116 1079 }
1117 1080 wp_localize_script($this->plugin_name . '-sitemap-tabs', 'metasyncSitemapTabs', [
1118 1081 'activeTab' => $sitemap_active_tab,
1119 1082 ]);
@@ -1441,8 +1404,10 @@
1441 1404 'nonce' => wp_create_nonce('metasync_nonce'),
1442 1405 'sa_connect_nonce' => $sa_connect_nonce,
1443 1406 'reset_auth_nonce' => wp_create_nonce('metasync_reset_auth_nonce'),
1444 1407 'burst_ping_nonce' => wp_create_nonce('metasync_burst_ping'),
1408 + 'clear_otto_cache_nonce' => wp_create_nonce('metasync_clear_otto_cache'),
1409 + 'lglogin_nonce' => wp_create_nonce('metasync_lglogin'),
1445 1410 'heartbeat_state' => $heartbeat_state,
1446 1411 'dashboard_domain' => self::get_effective_dashboard_domain(),
1447 1412 'support_email' => Metasync::SUPPORT_EMAIL,
1448 1413 'documentation_domain' => Metasync::DOCUMENTATION_DOMAIN,
@@ -1506,9 +1471,9 @@
1506 1471 add_action('admin_notices', array($this, 'display_page_builder_notice'));
1507 1472 // Display update warning banner if plugin update is available
1508 1473 add_action('admin_notices', array($this, 'display_update_warning_banner'));
1509 1474 // Enqueue wizard assets if on wizard page
1510 - if (isset($_GET['page']) && strpos($_GET['page'], '-setup-wizard') !== false) {
1475 + if (isset($_GET['page']) && strpos(sanitize_key(wp_unslash($_GET['page'])), '-setup-wizard') !== false) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check to pick scripts
1511 1476 wp_enqueue_script(
1512 1477 $this->plugin_name . '-setup-wizard',
1513 1478 plugin_dir_url(__FILE__) . 'js/metasync-setup-wizard.js',
1514 1479 array('jquery'),
@@ -1707,13 +1672,8 @@
1707 1672 {
1708 1673 Metasync_Connect_Manager::instance()->test_searchatlas_ajax_endpoint();
1709 1674 }
1710 1675
1711 - public function simple_ajax_test()
1712 - {
1713 - Metasync_Connect_Manager::instance()->simple_ajax_test();
1714 - }
1715 -
1716 1676 /**
1717 1677 * Create Admin Dashboard Iframe Page
1718 1678 * Embeds the Search Atlas dashboard directly in WordPress admin
1719 1679 */
@@ -1800,9 +1760,9 @@
1800 1760 } else {
1801 1761 echo '<p style="color: var(--dashboard-text-secondary);">ℹ️ No cache plugins detected.</p>';
1802 1762 }
1803 1763 } catch (Exception $e) {
1804 - error_log('MetaSync Cache Status Error: ' . $e->getMessage());
1764 + error_log('MetaSync Cache Status Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
1805 1765 echo '<p style="color: var(--dashboard-error);">⚠️ An error occurred while retrieving cache plugin status.</p>';
1806 1766 }
1807 1767 } else {
1808 1768 echo '<p style="color: var(--dashboard-error);">⚠️ Cache Purge class not loaded.</p>';
@@ -1819,12 +1779,12 @@
1819 1779 </form>
1820 1780
1821 1781 <?php
1822 1782 // Display success/error messages
1823 - if (isset($_GET['cache_cleared']) && $_GET['cache_cleared'] == '1') {
1824 - $cleared = isset($_GET['cleared']) ? intval($_GET['cleared']) : 0;
1825 - $failed = isset($_GET['failed']) ? intval($_GET['failed']) : 0;
1826 - $plugins = isset($_GET['plugins']) ? sanitize_text_field(wp_unslash($_GET['plugins'])) : '';
1783 + if (isset($_GET['cache_cleared']) && $_GET['cache_cleared'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1784 + $cleared = isset($_GET['cleared']) ? intval($_GET['cleared']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1785 + $failed = isset($_GET['failed']) ? intval($_GET['failed']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1786 + $plugins = isset($_GET['plugins']) ? sanitize_text_field(wp_unslash($_GET['plugins'])) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1827 1787
1828 1788 if ($cleared > 0) {
1829 1789 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
1830 1790 echo '✅ <strong>Success!</strong> Cleared cache for ' . intval( $cleared ) . ' plugin(s)';
@@ -1844,10 +1804,10 @@
1844 1804 echo '</p></div>';
1845 1805 }
1846 1806 }
1847 1807
1848 - if (isset($_GET['cache_error']) && $_GET['cache_error'] == '1') {
1849 - $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : '';
1808 + if (isset($_GET['cache_error']) && $_GET['cache_error'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1809 + $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1850 1810 if (empty($message)) {
1851 1811 $message = 'An unknown error occurred while clearing cache. Please check error logs for details.';
1852 1812 }
1853 1813 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
@@ -1944,12 +1904,13 @@
1944 1904 </form>
1945 1905
1946 1906 <?php
1947 1907 // Hosting cache result messages
1948 - if (isset($_GET['hosting_cache_cleared']) && $_GET['hosting_cache_cleared'] == '1') {
1949 - $hc_cleared = isset($_GET['hc_cleared']) ? sanitize_text_field(urldecode($_GET['hc_cleared'])) : '';
1950 - $hc_failed = isset($_GET['hc_failed']) ? sanitize_text_field(urldecode($_GET['hc_failed'])) : '';
1951 - $hc_not_detected = isset($_GET['hc_not_detected']) ? sanitize_text_field(urldecode($_GET['hc_not_detected'])) : '';
1908 + if (isset($_GET['hosting_cache_cleared']) && $_GET['hosting_cache_cleared'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1909 + // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- each value is unslashed and sanitized on the following assignments.
1910 + $hc_cleared = isset($_GET['hc_cleared']) ? sanitize_text_field(urldecode(wp_unslash($_GET['hc_cleared']))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1911 + $hc_failed = isset($_GET['hc_failed']) ? sanitize_text_field(urldecode(wp_unslash($_GET['hc_failed']))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1912 + $hc_not_detected = isset($_GET['hc_not_detected']) ? sanitize_text_field(urldecode(wp_unslash($_GET['hc_not_detected']))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only result notice after a nonce-checked purge
1952 1913
1953 1914 if ($hc_cleared) {
1954 1915 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
1955 1916 echo '✅ <strong>Success!</strong> Purged hosting cache on: ' . esc_html($hc_cleared);
@@ -1964,8 +1925,9 @@
1964 1925 echo '<div class="notice notice-info inline" style="margin-top: 10px;"><p>';
1965 1926 echo 'ℹ️ No enabled hosting providers were detected on this server (' . esc_html($hc_not_detected) . ').';
1966 1927 echo '</p></div>';
1967 1928 }
1929 + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1968 1930 }
1969 1931 ?>
1970 1932
1971 1933 <script>
@@ -2130,11 +2092,11 @@
2130 2092 </div>
2131 2093
2132 2094 <?php
2133 2095 // Display success/error messages
2134 - if (isset($_GET['otto_cache_cleared']) && $_GET['otto_cache_cleared'] == '1') {
2135 - $cleared_count = isset($_GET['count']) ? intval($_GET['count']) : 0;
2136 - $url = isset($_GET['url']) ? urldecode(sanitize_text_field(wp_unslash($_GET['url']))) : '';
2096 + if (isset($_GET['otto_cache_cleared']) && $_GET['otto_cache_cleared'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice or prefill, the form submit carries a nonce
2097 + $cleared_count = isset($_GET['count']) ? intval($_GET['count']) : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice or prefill, the form submit carries a nonce
2098 + $url = isset($_GET['url']) ? urldecode(sanitize_text_field(wp_unslash($_GET['url']))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice or prefill, the form submit carries a nonce
2137 2099
2138 2100 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
2139 2101 if (!empty($url)) {
2140 2102 echo '✅ <strong>Success!</strong> Cleared cache for URL: <code>' . esc_html($url) . '</code> (' . intval( $cleared_count ) . ' entries)';
@@ -2143,10 +2105,10 @@
2143 2105 }
2144 2106 echo '</p></div>';
2145 2107 }
2146 2108
2147 - if (isset($_GET['otto_cache_error']) && $_GET['otto_cache_error'] == '1') {
2148 - $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : 'An unknown error occurred.';
2109 + if (isset($_GET['otto_cache_error']) && $_GET['otto_cache_error'] == '1') { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice or prefill, the form submit carries a nonce
2110 + $message = isset($_GET['message']) ? urldecode(sanitize_text_field(wp_unslash($_GET['message']))) : 'An unknown error occurred.'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice or prefill, the form submit carries a nonce
2149 2111 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
2150 2112 echo '❌ <strong>Error:</strong> ' . esc_html($message);
2151 2113 echo '</p></div>';
2152 2114 }
@@ -2185,9 +2147,9 @@
2185 2147 <td>
2186 2148 <input type="url"
2187 2149 id="otto_cache_url"
2188 2150 name="otto_cache_url"
2189 - value="<?php echo isset($_GET['url']) ? esc_attr(urldecode(sanitize_text_field(wp_unslash($_GET['url'])))) : ''; ?>"
2151 + value="<?php echo isset($_GET['url']) ? esc_attr(urldecode(sanitize_text_field(wp_unslash($_GET['url'])))) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only notice or prefill, the form submit carries a nonce ?>"
2190 2152 class="regular-text"
2191 2153 placeholder="https://example.com/page/"
2192 2154 required />
2193 2155 <p class="description" style="color: var(--dashboard-text-secondary);">Enter the full URL of the page whose cache you want to clear.</p>
@@ -2258,9 +2220,10 @@
2258 2220 /**
2259 2221 * AJAX handler for saving hosting cache settings
2260 2222 */
2261 2223 public function ajax_save_hosting_cache_settings() {
2262 - if (!isset($_POST['hosting_cache_nonce']) || !wp_verify_nonce($_POST['hosting_cache_nonce'], 'metasync_hosting_cache_nonce')) {
2224 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce is validated immediately by wp_verify_nonce().
2225 + if (!isset($_POST['hosting_cache_nonce']) || !wp_verify_nonce(wp_unslash($_POST['hosting_cache_nonce']), 'metasync_hosting_cache_nonce')) {
2263 2226 wp_send_json_error(array('message' => 'Security check failed'));
2264 2227 return;
2265 2228 }
2266 2229
@@ -2281,9 +2244,10 @@
2281 2244 /**
2282 2245 * AJAX handler: save object cache behaviour settings
2283 2246 */
2284 2247 public function ajax_save_object_cache_settings() {
2285 - if (!isset($_POST['object_cache_nonce']) || !wp_verify_nonce($_POST['object_cache_nonce'], 'metasync_object_cache_nonce')) {
2248 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce is validated immediately by wp_verify_nonce().
2249 + if (!isset($_POST['object_cache_nonce']) || !wp_verify_nonce(wp_unslash($_POST['object_cache_nonce']), 'metasync_object_cache_nonce')) {
2286 2250 wp_send_json_error(array('message' => 'Security check failed'));
2287 2251 return;
2288 2252 }
2289 2253
@@ -2310,9 +2274,10 @@
2310 2274 /**
2311 2275 * AJAX handler for saving OTTO Cache TTL
2312 2276 */
2313 2277 public function ajax_save_otto_cache_ttl() {
2314 - if (!isset($_POST['otto_cache_ttl_nonce']) || !wp_verify_nonce($_POST['otto_cache_ttl_nonce'], 'metasync_otto_cache_ttl_nonce')) {
2278 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce is validated immediately by wp_verify_nonce().
2279 + if (!isset($_POST['otto_cache_ttl_nonce']) || !wp_verify_nonce(wp_unslash($_POST['otto_cache_ttl_nonce']), 'metasync_otto_cache_ttl_nonce')) {
2315 2280 wp_send_json_error(array('message' => 'Security check failed.'));
2316 2281 return;
2317 2282 }
2318 2283
@@ -2338,9 +2303,10 @@
2338 2303 /**
2339 2304 * admin_post handler: purge WP Engine and Kinsta hosting-level caches
2340 2305 */
2341 2306 public function handle_purge_hosting_cache() {
2342 - if (!isset($_POST['hosting_cache_purge_nonce']) || !wp_verify_nonce($_POST['hosting_cache_purge_nonce'], 'metasync_hosting_cache_purge_nonce')) {
2307 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce is validated immediately by wp_verify_nonce().
2308 + if (!isset($_POST['hosting_cache_purge_nonce']) || !wp_verify_nonce(wp_unslash($_POST['hosting_cache_purge_nonce']), 'metasync_hosting_cache_purge_nonce')) {
2343 2309 wp_die('Security check failed');
2344 2310 }
2345 2311
2346 2312 if (!Metasync::current_user_has_plugin_access()) {
@@ -2360,9 +2326,9 @@
2360 2326 WpeCommon::purge_varnish_cache();
2361 2327 WpeCommon::purge_memcached();
2362 2328 $cleared[] = 'WP Engine';
2363 2329 } catch (Exception $e) {
2364 - error_log('MetaSync: WP Engine hosting cache purge failed - ' . $e->getMessage());
2330 + error_log('MetaSync: WP Engine hosting cache purge failed - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
2365 2331 $failed[] = 'WP Engine';
2366 2332 }
2367 2333 } else {
2368 2334 $not_detected[] = 'WP Engine';
@@ -2375,9 +2341,9 @@
2375 2341 try {
2376 2342 KinstaCache::get_instance()->kinsta_cache_purge_full();
2377 2343 $cleared[] = 'Kinsta';
2378 2344 } catch (Exception $e) {
2379 - error_log('MetaSync: Kinsta hosting cache purge failed - ' . $e->getMessage());
2345 + error_log('MetaSync: Kinsta hosting cache purge failed - ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
2380 2346 $failed[] = 'Kinsta';
2381 2347 }
2382 2348 } else {
2383 2349 $not_detected[] = 'Kinsta';
@@ -2772,9 +2738,9 @@
2772 2738 # Initialize MetaSync API request class and trigger synchronization
2773 2739 (new Metasync_Sync_Requests())->SyncCustomerParams();
2774 2740 } catch (Exception $e) {
2775 2741 # Log any API request errors for debugging
2776 - error_log('Metasync API Error: ' . $e->getMessage());
2742 + error_log('Metasync API Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
2777 2743 }
2778 2744 }
2779 2745
2780 2746 /*
@@ -2789,9 +2755,9 @@
2789 2755 # Initialize MetaSync API request class and trigger synchronization
2790 2756 (new Metasync_Sync_Requests())->SyncCustomerParams();
2791 2757 } catch (Exception $e) {
2792 2758 # Log any API request errors for debugging
2793 - error_log('Metasync API Error: ' . $e->getMessage());
2759 + error_log('Metasync API Error: ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
2794 2760 }
2795 2761 }
2796 2762
2797 2763 /**
@@ -2828,9 +2794,9 @@
2828 2794 // Settings shape the whole site's conversion pipeline — restrict
2829 2795 // the save (and the reset) to site admins, not just anyone who can
2830 2796 // open the page via plugin_access_roles.
2831 2797 if (!current_user_can('manage_options')) {
2832 - wp_die(__('Sorry, you are not allowed to manage media optimization settings.', 'metasync'));
2798 + wp_die(esc_html__('Sorry, you are not allowed to manage media optimization settings.', 'metasync'));
2833 2799 }
2834 2800
2835 2801 // Handle reset to defaults
2836 2802 if (!empty($_POST['metasync_media_reset'])) {
@@ -2837,8 +2803,9 @@
2837 2803 $defaults = Metasync_Media_Settings::get_defaults();
2838 2804 Metasync_Media_Settings::save_settings($defaults);
2839 2805 $save_success = true;
2840 2806 } elseif (isset($_POST['metasync_media'])) {
2807 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated and sanitised by Metasync_Media_Settings::save_settings().
2841 2808 $input = wp_unslash($_POST['metasync_media']);
2842 2809 Metasync_Media_Settings::save_settings($input);
2843 2810 $save_success = true;
2844 2811 }
@@ -2870,8 +2837,15 @@
2870 2837 * Code Minification page callback
2871 2838 */
2872 2839 public function create_admin_code_minification_page()
2873 2840 {
2841 + // Settings shape the whole site's asset pipeline — restrict
2842 + // the save (and the reset) to site admins, not just anyone who can
2843 + // open the page via plugin_access_roles.
2844 + if (!empty($_POST) && !current_user_can('manage_options')) {
2845 + wp_die(esc_html__('Sorry, you are not allowed to manage code minification settings.', 'metasync'));
2846 + }
2847 +
2874 2848 $this->render_layout_open('Code Minification', 'code_minification', 'Minify CSS, JavaScript, and HTML to improve performance.');
2875 2849 // Load settings and compatibility classes
2876 2850 require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-minification-settings.php';
2877 2851 require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-minification-cache.php';
@@ -2888,8 +2862,9 @@
2888 2862 $defaults = Metasync_Minification_Settings::get_defaults();
2889 2863 Metasync_Minification_Settings::save_settings($defaults);
2890 2864 $save_success = true;
2891 2865 } elseif (isset($_POST['metasync_code_min'])) {
2866 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated and sanitised by Metasync_Minification_Settings::save_settings().
2892 2867 $input = (array) wp_unslash($_POST['metasync_code_min']);
2893 2868 Metasync_Minification_Settings::save_settings($input);
2894 2869 $save_success = true;
2895 2870 }
@@ -2944,8 +2919,9 @@
2944 2919 $max_bytes = 10 * 1024 * 1024;
2945 2920 if (filesize($file) > $max_bytes) {
2946 2921 $size_mb = round(filesize($file) / 1024 / 1024, 1);
2947 2922 wp_send_json_error(sprintf(
2923 + /* translators: %s: file size in megabytes. */
2948 2924 __('Optimization skipped: file size (%s MB) exceeds the 10 MB safety limit to prevent memory issues.', 'metasync'),
2949 2925 $size_mb
2950 2926 ));
2951 2927 }
@@ -3154,8 +3130,9 @@
3154 3130 if (!Metasync_Image_Converter::can_revert($id)) {
3155 3131 $skipped++;
3156 3132 $file = get_attached_file($id);
3157 3133 $name = $file ? basename($file) : "ID {$id}";
3134 + /* translators: %s: attachment file name. */
3158 3135 $errors[] = sprintf(__('%s: skipped — original image unavailable.', 'metasync'), $name);
3159 3136 continue;
3160 3137 }
3161 3138
@@ -3164,8 +3141,9 @@
3164 3141 } else {
3165 3142 $failed++;
3166 3143 $file = get_attached_file($id);
3167 3144 $name = $file ? basename($file) : "ID {$id}";
3145 + /* translators: %s: attachment file name. */
3168 3146 $errors[] = sprintf(__('%s: revert failed (original file may not exist).', 'metasync'), $name);
3169 3147 }
3170 3148 }
3171 3149
@@ -3341,8 +3319,17 @@
3341 3319 * XML Sitemap page callback
3342 3320 */
3343 3321 public function create_admin_xml_sitemap_page()
3344 3322 {
3323 + // Every form action below mutates site-wide state — sitemap options,
3324 + // sitemap files on disk, robots.txt, other plugins' sitemap generators
3325 + // — so they are restricted to site admins, not just anyone who can
3326 + // open the page via plugin_access_roles. The nonce is CSRF
3327 + // protection, not authorization.
3328 + if (!empty($_POST) && !current_user_can('manage_options')) {
3329 + wp_die(esc_html__('Sorry, you are not allowed to manage sitemap settings.', 'metasync'));
3330 + }
3331 +
3345 3332 // Load sitemap generator class
3346 3333 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
3347 3334
3348 3335 $sitemap_generator = new Metasync_Sitemap_Generator();
@@ -3373,8 +3360,9 @@
3373 3360 // Regenerate sitemap with new content settings
3374 3361 $result = $sitemap_generator->generate_sitemap();
3375 3362 if (is_wp_error($result)) {
3376 3363 echo '<div class="notice notice-error"><p>' . esc_html(
3364 + /* translators: %s: error message. */
3377 3365 sprintf(__('Settings saved but sitemap generation failed: %s', 'metasync'), $result->get_error_message())
3378 3366 ) . '</p></div>';
3379 3367 } elseif (false === $result) {
3380 3368 echo '<div class="notice notice-error"><p>' . esc_html__('Settings saved but 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>';
@@ -3389,10 +3377,10 @@
3389 3377
3390 3378 // Build generic taxonomy filters
3391 3379 $news_taxonomies = [];
3392 3380 if (!empty($_POST['news_taxonomies']) && is_array($_POST['news_taxonomies'])) {
3393 - foreach ($_POST['news_taxonomies'] as $tax_name => $term_ids) {
3394 - $news_taxonomies[sanitize_key($tax_name)] = array_map('absint', (array) $term_ids);
3381 + foreach (wp_unslash($_POST['news_taxonomies']) as $tax_name => $term_ids) {
3382 + $news_taxonomies[sanitize_key($tax_name)] = array_map('absint', (array) wp_unslash($term_ids));
3395 3383 }
3396 3384 }
3397 3385
3398 3386 $news_settings = [
@@ -3417,9 +3405,9 @@
3417 3405 echo '<div class="notice notice-success"><p>' . esc_html__('News sitemap settings saved and sitemap regenerated!', 'metasync') . '</p></div>';
3418 3406 } else {
3419 3407 // Also remove physical file if it exists
3420 3408 if (file_exists(ABSPATH . 'news-sitemap.xml')) {
3421 - @unlink(ABSPATH . 'news-sitemap.xml');
3409 + @wp_delete_file(ABSPATH . 'news-sitemap.xml');
3422 3410 }
3423 3411 echo '<div class="notice notice-success"><p>' . esc_html__('News sitemap settings saved. Sitemap cache cleared.', 'metasync') . '</p></div>';
3424 3412 }
3425 3413 }
@@ -3430,10 +3418,10 @@
3430 3418
3431 3419 // Build generic taxonomy filters
3432 3420 $video_taxonomies = [];
3433 3421 if (!empty($_POST['video_taxonomies']) && is_array($_POST['video_taxonomies'])) {
3434 - foreach ($_POST['video_taxonomies'] as $tax_name => $term_ids) {
3435 - $video_taxonomies[sanitize_key($tax_name)] = array_map('absint', (array) $term_ids);
3422 + foreach (wp_unslash($_POST['video_taxonomies']) as $tax_name => $term_ids) {
3423 + $video_taxonomies[sanitize_key($tax_name)] = array_map('absint', (array) wp_unslash($term_ids));
3436 3424 }
3437 3425 }
3438 3426
3439 3427 $video_settings = [
@@ -3459,9 +3447,9 @@
3459 3447 echo '<div class="notice notice-success"><p>' . esc_html__('Video sitemap settings saved and sitemap regenerated!', 'metasync') . '</p></div>';
3460 3448 } else {
3461 3449 // Also remove physical file if it exists
3462 3450 if (file_exists(ABSPATH . 'video-sitemap.xml')) {
3463 - @unlink(ABSPATH . 'video-sitemap.xml');
3451 + @wp_delete_file(ABSPATH . 'video-sitemap.xml');
3464 3452 }
3465 3453 echo '<div class="notice notice-success"><p>' . esc_html__('Video sitemap settings saved. Sitemap cache cleared.', 'metasync') . '</p></div>';
3466 3454 }
3467 3455 }
@@ -3529,10 +3517,11 @@
3529 3517 $result = $sitemap_generator->generate_sitemap();
3530 3518
3531 3519 if (is_wp_error($result)) {
3532 3520 $error_msg = $result->get_error_message();
3533 - error_log('[MetaSync] Sitemap generation failed: ' . $error_msg);
3521 + error_log('[MetaSync] Sitemap generation failed: ' . $error_msg); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
3534 3522 echo '<div class="notice notice-error"><p>' . esc_html(
3523 + /* translators: %s: error message. */
3535 3524 sprintf(__('Sitemap generation failed: %s', 'metasync'), $error_msg)
3536 3525 ) . '</p></div>';
3537 3526 } elseif (false === $result) {
3538 3527 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>';
@@ -3539,8 +3528,9 @@
3539 3528 } else {
3540 3529 $message = esc_html__('Sitemap generated successfully!', 'metasync');
3541 3530 if (!empty($extras)) {
3542 3531 $message .= ' ' . sprintf(
3532 + /* translators: %s: list of sitemap types. */
3543 3533 esc_html__('Also generated %s sitemap(s).', 'metasync'),
3544 3534 implode(' & ', $extras)
3545 3535 );
3546 3536 }
@@ -3836,8 +3826,9 @@
3836 3826 if (!get_transient('metasync_llms_conflict')) {
3837 3827 return;
3838 3828 }
3839 3829 echo '<div class="notice notice-info is-dismissible"><p>';
3830 + /* translators: %s: effective plugin name (whitelabel-aware). */
3840 3831 echo esc_html(sprintf(__('Note: Another SEO plugin (Yoast, Rank Math, or AIOSEO) may also be generating /llms.txt. %s\'s version takes priority when enabled.', 'metasync'), Metasync::get_effective_plugin_name()));
3841 3832 echo '</p></div>';
3842 3833 }
3843 3834
@@ -4029,9 +4020,9 @@
4029 4020 return;
4030 4021 }
4031 4022
4032 4023 // Get pagination parameters
4033 - $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1;
4024 + $page = isset($_GET['paged']) ? max(1, intval($_GET['paged'])) : 1; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and pagination values for the sync log
4034 4025 $per_page = Metasync_Per_Page_Helper::resolve('sync_log', 10);
4035 4026 $offset = ($page - 1) * $per_page;
4036 4027
4037 4028 // Get filters
@@ -4036,10 +4027,10 @@
4036 4027
4037 4028 // Get filters
4038 4029 $filters = [
4039 4030 // UI exposes date_range and status only. We compute date_from/date_to based on date_range
4040 - 'date_range' => isset($_GET['date_range']) ? sanitize_text_field(wp_unslash($_GET['date_range'])) : '',
4041 - 'status' => isset($_GET['status']) ? sanitize_text_field(wp_unslash($_GET['status'])) : '',
4031 + 'date_range' => isset($_GET['date_range']) ? sanitize_text_field(wp_unslash($_GET['date_range'])) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and pagination values for the sync log
4032 + 'status' => isset($_GET['status']) ? sanitize_text_field(wp_unslash($_GET['status'])) : '', // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and pagination values for the sync log
4042 4033 ];
4043 4034
4044 4035 // Map date_range to concrete date_from/date_to for DB queries
4045 4036 $date_range = $filters['date_range'];
@@ -4048,28 +4039,28 @@
4048 4039 $date_to = '';
4049 4040
4050 4041 if (!empty($date_range)) {
4051 4042 // End boundary is now by default
4052 - $date_to = date('Y-m-d H:i:s', $wp_now_ts);
4043 + $date_to = gmdate('Y-m-d H:i:s', $wp_now_ts);
4053 4044
4054 4045 if ($date_range === 'today') {
4055 4046 $start_ts = strtotime('today', $wp_now_ts);
4056 - $date_from = date('Y-m-d H:i:s', $start_ts);
4047 + $date_from = gmdate('Y-m-d H:i:s', $start_ts);
4057 4048 } elseif ($date_range === 'yesterday') {
4058 4049 $start_ts = strtotime('yesterday', $wp_now_ts);
4059 4050 $end_ts = strtotime('today', $wp_now_ts) - 1; // end of yesterday
4060 - $date_from = date('Y-m-d H:i:s', $start_ts);
4061 - $date_to = date('Y-m-d H:i:s', $end_ts);
4051 + $date_from = gmdate('Y-m-d H:i:s', $start_ts);
4052 + $date_to = gmdate('Y-m-d H:i:s', $end_ts);
4062 4053 } elseif ($date_range === 'this_week') {
4063 4054 $start_of_week = (int) get_option('start_of_week', 1); // 0=Sun, 1=Mon
4064 - $day_of_week = (int) date('w', $wp_now_ts); // 0=Sun..6=Sat
4055 + $day_of_week = (int) gmdate('w', $wp_now_ts); // 0=Sun..6=Sat
4065 4056 // Convert start_of_week to PHP's 0..6 where 0=Sunday
4066 4057 $delta_days = ($day_of_week - $start_of_week + 7) % 7;
4067 4058 $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
4068 - $date_from = date('Y-m-d H:i:s', $start_ts);
4059 + $date_from = gmdate('Y-m-d H:i:s', $start_ts);
4069 4060 } elseif ($date_range === 'this_month') {
4070 - $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
4071 - $date_from = date('Y-m-d H:i:s', $start_ts);
4061 + $start_ts = strtotime(gmdate('Y-m-01 00:00:00', $wp_now_ts));
4062 + $date_from = gmdate('Y-m-d H:i:s', $start_ts);
4072 4063 } elseif ($date_range === 'all') {
4073 4064 // no bounds
4074 4065 }
4075 4066 }
@@ -4096,12 +4087,24 @@
4096 4087 ?>
4097 4088 <div class="dashboard-card">
4098 4089 <div class="sync-log-header">
4099 4090 <div class="sync-log-title-section">
4100 - <h2>Changes Log</h2>
4101 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">
4102 - Recent content synchronizations from external tools.
4103 - <span style="margin-left:8px; font-size:12px; opacity:.75;">Records are automatically removed after 90 days.</span>
4091 + <?php
4092 + // The page title and description are already rendered by
4093 + // render_layout_open() above. Repeating them here printed
4094 + // "Changes Log" twice on screen, so the card keeps only the
4095 + // retention hint — the one piece of copy the layout header
4096 + // does not carry.
4097 + ?>
4098 + <p style="color: var(--dashboard-text-secondary); margin-top: 0; margin-bottom: 0;">
4099 + <?php
4100 + // No opacity here: this used to be a de-emphasised
4101 + // aside trailing a full-contrast sentence, but it is
4102 + // now the card's only copy. At 12px/.75 it composites
4103 + // to 4.28:1 on the card background, under the 4.5:1
4104 + // AA floor; at full strength it reads 6.52:1.
4105 + ?>
4106 + <span style="font-size:13px;">Records are automatically removed after 90 days.</span>
4104 4107 </p>
4105 4108 </div>
4106 4109
4107 4110 <!-- Filters + Clear Log button - Right aligned -->
@@ -4111,9 +4114,9 @@
4111 4114 data-nonce="<?php echo esc_attr(wp_create_nonce('metasync_clear_sync_log')); ?>">
4112 4115 🗑 Clear Log
4113 4116 </button>
4114 4117 <form method="get" class="sync-filters-form" onchange="this.submit()" style="display:flex;flex-direction:row;align-items:center;gap:12px;flex-wrap:nowrap;">
4115 - <input type="hidden" name="page" value="<?php echo esc_attr($_GET['page']); ?>">
4118 + <input type="hidden" name="page" value="<?php echo esc_attr(sanitize_key(wp_unslash($_GET['page']))); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and pagination values for the sync log ?>">
4116 4119
4117 4120 <select name="date_range" class="sync-filter-select">
4118 4121 <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
4119 4122 <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
@@ -4135,9 +4138,9 @@
4135 4138 <div class="sync-log-list">
4136 4139 <?php if (empty($sync_records)): ?>
4137 4140 <div class="sync-log-empty">
4138 4141 <div class="sync-log-empty-icon"><span class="dashicons dashicons-media-default" style="font-size:48px;width:48px;height:48px;color:var(--dashboard-text-secondary);"></span></div>
4139 - <h3>No sync records found</h3>
4142 + <h2>No sync records found</h2>
4140 4143 <p>Sync records will appear here when content/pages receive new updates.</p>
4141 4144 </div>
4142 4145 <?php else: ?>
4143 4146 <?php foreach ($sync_records as $record): ?>
@@ -4194,24 +4197,25 @@
4194 4197 <!-- Pagination -->
4195 4198 <div class="sync-log-pagination">
4196 4199 <div class="sync-log-pagination-info">
4197 4200 Total records: <?php echo intval( $total_records ); ?><?php if ($total_records > 0): ?> | Showing <?php echo intval( $offset ) + 1; ?>-<?php echo intval( min($offset + $per_page, $total_records) ); ?><?php endif; ?>
4198 - <?php echo Metasync_Per_Page_Helper::render_selector('sync_log', $per_page); ?>
4201 + <?php // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper returns a complete escaped selector element.
4202 + echo Metasync_Per_Page_Helper::render_selector('sync_log', absint($per_page)); ?>
4199 4203 </div>
4200 4204
4201 4205 <?php if ($total_pages > 1): ?>
4202 4206 <div class="sync-log-pagination-controls">
4203 4207 <?php if ($page > 1): ?>
4204 - <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>
4208 + <a href="?page=<?php echo esc_attr(sanitize_key(wp_unslash($_GET['page']))); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and pagination values for the sync log ?>&paged=<?php echo intval( $page ) - 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn">‹</a>
4205 4209 <?php endif; ?>
4206 4210
4207 4211 <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
4208 - <a href="?page=<?php echo esc_attr($_GET['page']); ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>"
4212 + <a href="?page=<?php echo esc_attr(sanitize_key(wp_unslash($_GET['page']))); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and pagination values for the sync log ?>&paged=<?php echo intval( $i ); ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>"
4209 4213 class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a>
4210 4214 <?php endfor; ?>
4211 4215
4212 4216 <?php if ($page < $total_pages): ?>
4213 - <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>
4217 + <a href="?page=<?php echo esc_attr(sanitize_key(wp_unslash($_GET['page']))); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only filter and pagination values for the sync log ?>&paged=<?php echo intval( $page ) + 1; ?><?php echo esc_html( $this->build_filter_query_string($filters) ); ?>" class="sync-pagination-btn">›</a>
4214 4218 <?php endif; ?>
4215 4219 </div>
4216 4220 <?php endif; ?>
4217 4221 </div>
@@ -4300,10 +4304,10 @@
4300 4304
4301 4305 // Preserve the user-selected results-per-page value across page
4302 4306 // navigation so a non-default page size survives clicking a page link.
4303 4307 $per_page_key = Metasync_Per_Page_Helper::request_key('sync_log');
4304 - if (isset($_GET[$per_page_key])) {
4305 - $per_page = (int) $_GET[$per_page_key];
4308 + if (isset($_GET[$per_page_key])) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only, allow-listed per-page value for links
4309 + $per_page = (int) $_GET[$per_page_key]; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only, allow-listed per-page value for links
4306 4310 if (in_array($per_page, Metasync_Per_Page_Helper::allowed_values(), true)) {
4307 4311 $query_parts[] = $per_page_key . '=' . $per_page;
4308 4312 }
4309 4313 }
@@ -4482,18 +4486,8 @@
4482 4486 {
4483 4487 Metasync_Connect_Manager::instance()->handle_session_management_early();
4484 4488 }
4485 4489
4486 - /**
4487 - * @deprecated 2.5.12 Use Metasync_Auth_Manager instead of sessions for authentication
4488 - */
4489 - private function safe_session_start() {
4490 - // This method is deprecated and no longer used
4491 - // Authentication now uses Metasync_Auth_Manager with WordPress transients and user meta
4492 - _deprecated_function(__METHOD__, '2.5.12', 'Metasync_Auth_Manager');
4493 - return Metasync_Session_Helper::safe_start();
4494 - }
4495 -
4496 4490 private function handle_whitelabel_session_logic()
4497 4491 {
4498 4492 Metasync_Connect_Manager::instance()->handle_whitelabel_session_logic();
4499 4493 }
@@ -4589,9 +4583,9 @@
4589 4583 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
4590 4584 Effective Threshold
4591 4585 </label>
4592 4586 <div style="padding: 10px 12px; background: var(--dashboard-input-bg); border: 1px solid var(--dashboard-border); border-radius: 6px; color: var(--dashboard-text-secondary);">
4593 - <strong id="effective_threshold_value"><?php echo round($effective_threshold, 2); ?></strong>
4587 + <strong id="effective_threshold_value"><?php echo esc_html((string) round($effective_threshold, 2)); ?></strong>
4594 4588 </div>
4595 4589 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
4596 4590 Calculated as: cores × per-core threshold
4597 4591 </p>
@@ -4609,13 +4603,13 @@
4609 4603 <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo intval($stats['deferrals']); ?></div>
4610 4604 </div>
4611 4605 <div>
4612 4606 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Max Load Observed</div>
4613 - <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo round($stats['max_load'], 2); ?></div>
4607 + <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo esc_html((string) round($stats['max_load'], 2)); ?></div>
4614 4608 </div>
4615 4609 <div>
4616 4610 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Average Load</div>
4617 - <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo round($stats['avg_load'], 2); ?></div>
4611 + <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo esc_html((string) round($stats['avg_load'], 2)); ?></div>
4618 4612 </div>
4619 4613 </div>
4620 4614 </div>
4621 4615
@@ -5322,9 +5316,9 @@
5322 5316 */
5323 5317 public function display_page_builder_notice() {
5324 5318 // Only relevant on the plugin's own settings page — avoid repeating
5325 5319 // this notice across every admin screen.
5326 - if (!isset($_GET['page']) || $_GET['page'] !== self::$page_slug) {
5320 + if (!isset($_GET['page']) || wp_unslash($_GET['page']) !== self::$page_slug) {
5327 5321 return;
5328 5322 }
5329 5323
5330 5324 $configured = Metasync::get_option('general')['default_page_builder'] ?? '';
@@ -5340,9 +5334,9 @@
5340 5334 return;
5341 5335 }
5342 5336
5343 5337 // Handle dismiss action
5344 - if (isset($_GET['metasync_dismiss_builder_notice']) && wp_verify_nonce($_GET['_wpnonce'] ?? '', 'metasync_dismiss_builder')) {
5338 + if (isset($_GET['metasync_dismiss_builder_notice'], $_GET['_wpnonce']) && wp_verify_nonce(sanitize_text_field(wp_unslash($_GET['_wpnonce'])), 'metasync_dismiss_builder')) {
5345 5339 update_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', '1');
5346 5340 return;
5347 5341 }
5348 5342
@@ -5437,9 +5431,10 @@
5437 5431 * Saves the CPU load threshold and returns statistics
5438 5432 */
5439 5433 public function ajax_save_performance_settings() {
5440 5434 # Check nonce for security and return early if invalid
5441 - if (!isset($_POST['meta_sync_nonce']) || !wp_verify_nonce($_POST['meta_sync_nonce'], 'meta_sync_general_setting_nonce')) {
5435 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce is validated immediately by wp_verify_nonce().
5436 + if (!isset($_POST['meta_sync_nonce']) || !wp_verify_nonce(wp_unslash($_POST['meta_sync_nonce']), 'meta_sync_general_setting_nonce')) {
5442 5437 wp_send_json_error(array('message' => 'Invalid nonce'));
5443 5438 return;
5444 5439 }
5445 5440
@@ -5507,9 +5502,9 @@
5507 5502 if (!wp_next_scheduled('metasync_cleanup_transients')) {
5508 5503 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_cleanup_transients');
5509 5504
5510 5505 if (!$scheduled) {
5511 - error_log('MetaSync: Failed to schedule transient cleanup cron job');
5506 + error_log('MetaSync: Failed to schedule transient cleanup cron job'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
5512 5507 }
5513 5508 }
5514 5509 }
5515 5510
@@ -5520,9 +5515,9 @@
5520 5515 {
5521 5516 $timestamp = wp_next_scheduled('metasync_cleanup_transients');
5522 5517 if ($timestamp) {
5523 5518 wp_unschedule_event($timestamp, 'metasync_cleanup_transients');
5524 - error_log('MetaSync: Transient cleanup cron job unscheduled');
5519 + error_log('MetaSync: Transient cleanup cron job unscheduled'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- unreachable in production
5525 5520 }
5526 5521 }
5527 5522
5528 5523 /**
@@ -5544,12 +5539,10 @@
5544 5539 {
5545 5540 if (!wp_next_scheduled('metasync_hidden_post_check')) {
5546 5541 $scheduled = wp_schedule_event(time(), 'metasync_weekly', 'metasync_hidden_post_check');
5547 5542
5548 - if ($scheduled) {
5549 - error_log('MetaSync: Hidden post manager cron job scheduled successfully (runs every 7 days)');
5550 - } else {
5551 - error_log('MetaSync: Failed to schedule hidden post manager cron job');
5543 + if (!$scheduled) {
5544 + error_log('MetaSync: Failed to schedule hidden post manager cron job'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
5552 5545 }
5553 5546 }
5554 5547 }
5555 5548
@@ -5560,12 +5553,10 @@
5560 5553 public function maybe_schedule_otto_recheck_404_cron()
5561 5554 {
5562 5555 if (!wp_next_scheduled('metasync_otto_recheck_404_exclusions')) {
5563 5556 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_otto_recheck_404_exclusions');
5564 - if ($scheduled) {
5565 - error_log('MetaSync: OTTO 404 recheck cron job scheduled successfully (runs daily)');
5566 - } else {
5567 - error_log('MetaSync: Failed to schedule OTTO 404 recheck cron job');
5557 + if (!$scheduled) {
5558 + error_log('MetaSync: Failed to schedule OTTO 404 recheck cron job'); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
5568 5559 }
5569 5560 }
5570 5561 }
5571 5562
@@ -5638,13 +5629,13 @@
5638 5629
5639 5630 try {
5640 5631 // 1. Post revisions
5641 5632 if (!empty($settings['clean_post_revisions'])) {
5642 - $stats['post_revisions'] = (int) $wpdb->query(
5633 + $stats['post_revisions'] = (int) $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5643 5634 "DELETE FROM {$wpdb->posts} WHERE post_type = 'revision'"
5644 5635 );
5645 5636 // Remove postmeta left behind by deleted revisions
5646 - $wpdb->query(
5637 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5647 5638 "DELETE pm FROM {$wpdb->postmeta} pm
5648 5639 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
5649 5640 WHERE p.ID IS NULL"
5650 5641 );
@@ -5652,20 +5643,19 @@
5652 5643
5653 5644 // 2. Trashed posts + their postmeta
5654 5645 if (!empty($settings['clean_trashed_posts'])) {
5655 5646 // Collect IDs first to cleanly remove postmeta
5656 - $trashed_ids = $wpdb->get_col(
5647 + $trashed_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5657 5648 "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'trash'"
5658 5649 );
5659 5650 if (!empty($trashed_ids)) {
5660 - $placeholders = implode(',', array_fill(0, count($trashed_ids), '%d'));
5661 - $wpdb->query(
5651 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5662 5652 $wpdb->prepare(
5663 - "DELETE FROM {$wpdb->postmeta} WHERE post_id IN ($placeholders)",
5653 + 'DELETE FROM ' . $wpdb->postmeta . ' WHERE post_id IN (' . implode(',', array_fill(0, count($trashed_ids), '%d')) . ')',
5664 5654 $trashed_ids
5665 5655 )
5666 5656 );
5667 - $stats['trashed_posts'] = (int) $wpdb->query(
5657 + $stats['trashed_posts'] = (int) $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5668 5658 "DELETE FROM {$wpdb->posts} WHERE post_status = 'trash'"
5669 5659 );
5670 5660 } else {
5671 5661 $stats['trashed_posts'] = 0;
@@ -5673,9 +5663,9 @@
5673 5663 }
5674 5664
5675 5665 // 3. Trashed comments
5676 5666 if (!empty($settings['clean_trashed_comments'])) {
5677 - $stats['trashed_comments'] = (int) $wpdb->query(
5667 + $stats['trashed_comments'] = (int) $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5678 5668 "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'trash'"
5679 5669 );
5680 5670 }
5681 5671
@@ -5680,9 +5670,9 @@
5680 5670 }
5681 5671
5682 5672 // 4. Spam comments
5683 5673 if (!empty($settings['clean_spam_comments'])) {
5684 - $stats['spam_comments'] = (int) $wpdb->query(
5674 + $stats['spam_comments'] = (int) $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5685 5675 "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'spam'"
5686 5676 );
5687 5677 }
5688 5678
@@ -5688,15 +5678,15 @@
5688 5678
5689 5679 // 5. Expired transients — direct SQL, no cache flush
5690 5680 if (!empty($settings['clean_expired_transients'])) {
5691 5681 // Delete timeout rows that have already expired
5692 - $wpdb->query(
5682 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5693 5683 "DELETE FROM {$wpdb->options}
5694 5684 WHERE option_name LIKE '\_transient\_timeout\_%'
5695 5685 AND option_value + 0 < UNIX_TIMESTAMP()"
5696 5686 );
5697 5687 // Delete value rows whose timeout row no longer exists
5698 - $stats['expired_transients'] = (int) $wpdb->query(
5688 + $stats['expired_transients'] = (int) $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5699 5689 "DELETE o FROM {$wpdb->options} o
5700 5690 LEFT JOIN {$wpdb->options} t
5701 5691 ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 12))
5702 5692 WHERE o.option_name LIKE '\_transient\_%'
@@ -5706,9 +5696,9 @@
5706 5696 }
5707 5697
5708 5698 // 6. Orphaned postmeta (post_id references a post that no longer exists)
5709 5699 if (!empty($settings['clean_orphaned_postmeta'])) {
5710 - $stats['orphaned_postmeta'] = (int) $wpdb->query(
5700 + $stats['orphaned_postmeta'] = (int) $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5711 5701 "DELETE pm FROM {$wpdb->postmeta} pm
5712 5702 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
5713 5703 WHERE p.ID IS NULL"
5714 5704 );
@@ -5720,12 +5710,10 @@
5720 5710 $settings['last_run_at'] = time();
5721 5711 $settings['last_run_stats'] = $stats;
5722 5712 update_option('metasync_db_cleanup_settings', $settings);
5723 5713
5724 - error_log('MetaSync: DB cleanup completed — ' . json_encode($stats));
5725 -
5726 5714 } catch (Exception $e) {
5727 - error_log('MetaSync: DB cleanup failed — ' . $e->getMessage());
5715 + error_log('MetaSync: DB cleanup failed — ' . $e->getMessage()); // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log -- genuine failure path, bounded, no secrets
5728 5716 }
5729 5717 }
5730 5718
5731 5719 /**
@@ -5942,10 +5930,11 @@
5942 5930 /**
5943 5931 * AJAX: Save DB cleanup settings and reschedule cron accordingly.
5944 5932 */
5945 5933 public function ajax_save_db_cleanup_settings() {
5934 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce is validated immediately by wp_verify_nonce().
5946 5935 if (!isset($_POST['db_cleanup_settings_nonce']) ||
5947 - !wp_verify_nonce($_POST['db_cleanup_settings_nonce'], 'metasync_db_cleanup_settings_nonce')) {
5936 + !wp_verify_nonce(wp_unslash($_POST['db_cleanup_settings_nonce']), 'metasync_db_cleanup_settings_nonce')) {
5948 5937 wp_send_json_error(array('message' => 'Invalid security token. Please refresh the page and try again.'));
5949 5938 return;
5950 5939 }
5951 5940
@@ -6002,10 +5991,11 @@
6002 5991 * AJAX: Manually trigger the DB cleanup and return stats for the UI.
6003 5992 * Persists current form state first so unsaved checkbox changes are respected.
6004 5993 */
6005 5994 public function ajax_run_db_cleanup() {
5995 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce is validated immediately by wp_verify_nonce().
6006 5996 if (!isset($_POST['db_cleanup_settings_nonce']) ||
6007 - !wp_verify_nonce($_POST['db_cleanup_settings_nonce'], 'metasync_db_cleanup_settings_nonce')) {
5997 + !wp_verify_nonce(wp_unslash($_POST['db_cleanup_settings_nonce']), 'metasync_db_cleanup_settings_nonce')) {
6008 5998 wp_send_json_error(array('message' => 'Invalid security token.'));
6009 5999 return;
6010 6000 }
6011 6001
@@ -6081,35 +6071,8 @@
6081 6071 ));
6082 6072 }
6083 6073
6084 6074 /**
6085 - * Control plugin auto-updates based on user setting
6086 - *
6087 - * @param bool $update Whether to update
6088 - * @param object $item Update offer
6089 - * @return bool Whether to allow auto-update
6090 - */
6091 - public function control_plugin_auto_updates($update, $item)
6092 - {
6093 - # Check if the item object has the slug property
6094 - if (!isset($item->slug)) {
6095 - return $update;
6096 - }
6097 -
6098 - // Check if this is our plugin
6099 - if ($item->slug === 'metasync') {
6100 - $general_settings = Metasync::get_option('general') ?? [];
6101 - $enable_auto_updates = $general_settings['enable_auto_updates'] ?? false;
6102 -
6103 - // Return the user's preference (true = allow auto-updates, false = prevent)
6104 - return $enable_auto_updates === 'true' || $enable_auto_updates === true;
6105 - }
6106 -
6107 - // For other plugins, don't interfere with their auto-update settings
6108 - return $update;
6109 - }
6110 -
6111 - /**
6112 6075 * AJAX handler to add excluded URL for OTTO
6113 6076 */
6114 6077 public function ajax_otto_add_excluded_url()
6115 6078 {
@@ -6255,8 +6218,9 @@
6255 6218 // If page has raw HTML or custom CSS from conversion, show badge
6256 6219 if ($has_raw_html || !empty($has_custom_css)) {
6257 6220 $label = $this->get_html_source_label();
6258 6221 $tooltip = sprintf(
6222 + /* translators: %s: HTML source label. */
6259 6223 __('This page was created using %s HTML-to-Builder converter', 'metasync'),
6260 6224 $label
6261 6225 );
6262 6226
@@ -6309,8 +6273,9 @@
6309 6273
6310 6274 if ($has_raw_html || !empty($has_custom_css)) {
6311 6275 $label = $this->get_html_source_label();
6312 6276 $message = sprintf(
6277 + /* translators: %s: HTML source label. */
6313 6278 __('This page was created using %s HTML-to-Builder converter. The design is preserved with custom CSS and inline styles.', 'metasync'),
6314 6279 '<strong>' . esc_html($label) . '</strong>'
6315 6280 );
6316 6281
@@ -6324,9 +6289,9 @@
6324 6289 <p class="metasync-editor-message">%s</p>
6325 6290 </div>
6326 6291 </div>',
6327 6292 esc_html($label),
6328 - $message
6293 + wp_kses($message, array('strong' => array()))
6329 6294 );
6330 6295 }
6331 6296 }
6332 6297
@@ -6349,9 +6314,9 @@
6349 6314 ?>
6350 6315 <fieldset class="inline-edit-col-left metasync-quick-edit-source">
6351 6316 <div class="inline-edit-col">
6352 6317 <label>
6353 - <span class="title"><?php _e('Source', 'metasync'); ?></span>
6318 + <span class="title"><?php esc_html_e('Source', 'metasync'); ?></span>
6354 6319 <span class="metasync-quick-edit-badge-container"></span>
6355 6320 </label>
6356 6321 </div>
6357 6322 </fieldset>
@@ -6363,8 +6328,9 @@
6363 6328 */
6364 6329 public function add_html_pages_dashboard_widget()
6365 6330 {
6366 6331 $label = $this->get_html_source_label();
6332 + /* translators: %s: HTML source label. */
6367 6333 $widget_title = sprintf(__('%s Pages', 'metasync'), $label);
6368 6334
6369 6335 wp_add_dashboard_widget(
6370 6336 'metasync_html_pages_widget',