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 +297 -238 2.6.25 → 2.7.1 View file →
@@ -41,9 +41,8 @@
41 41 const SECTION_SEO_CONTROLS = "seo_controls";
42 42 const SECTION_SEO_CONTROLS_ADVANCED = "seo_controls_advanced";
43 43 const SECTION_SEO_CONTROLS_INSTANT_INDEX = "seo_controls_instant_index";
44 44 const SECTION_PLUGIN_VISIBILITY = "plugin_visibility_settings";
45 - const SECTION_BREADCRUMBS = "breadcrumbs_settings";
46 45 const SECTION_LLMS_TXT = "llms_txt_settings";
47 46
48 47 /**
49 48 * The ID of this plugin.
@@ -213,9 +212,8 @@
213 212 add_action('admin_menu', array($this, 'add_import_external_data_page'));
214 213 add_action('admin_init', array($this, 'settings_page_init'));
215 214 add_filter('all_plugins', array($this,'metasync_plugin_white_label'));
216 215 add_filter( 'plugin_row_meta',array($this,'metasync_view_detials_url'),10,3);
217 - add_filter('site_transient_update_plugins', array($this, 'inject_whitelabel_icon_into_update_transient'));
218 216
219 217 // Display transient error/success messages for redirections
220 218 add_action('admin_notices', array($this, 'display_redirection_messages'));
221 219
@@ -264,8 +262,9 @@
264 262 add_action('metasync_api_key_changed', array('Metasync_Admin_Navigation', 'invalidate_admin_bar_status_cache'), 10, 0);
265 263
266 264 add_action('admin_init', array($this, 'initialize_cookie'));
267 265 add_action('admin_init', array($this, 'maybe_redirect_to_wizard'));
266 + add_action('admin_init', array($this, 'redirect_legacy_instant_indexing_page'));
268 267
269 268 // Add admin_post hooks for form submissions (WordPress standard way - no output buffering needed)
270 269 add_action('admin_post_metasync_clear_all_cache_plugins', array($this, 'handle_clear_all_cache_plugins'));
271 270 add_action('admin_post_metasync_clear_otto_cache_all', array($this, 'handle_clear_otto_cache_all'));
@@ -283,8 +282,11 @@
283 282
284 283 // Add AJAX for saving execution settings
285 284 add_action( 'wp_ajax_metasync_save_execution_settings', array($this, 'ajax_save_execution_settings') );
286 285
286 + // Add AJAX for saving the Headless Mode demo controls on the Advanced tab
287 + add_action( 'wp_ajax_metasync_save_headless_settings', array(Metasync_Settings_Registration::instance(), 'ajax_save_headless_settings') );
288 +
287 289 // Add AJAX for saving hosting cache settings
288 290 add_action( 'wp_ajax_metasync_save_hosting_cache_settings', array($this, 'ajax_save_hosting_cache_settings') );
289 291
290 292 // Add AJAX for saving OTTO Cache TTL
@@ -373,8 +375,15 @@
373 375 add_action('wp_ajax_metasync_process_batch_tick', array($this, 'ajax_process_batch_tick'));
374 376 add_action('wp_ajax_metasync_delete_orphaned_image', array($this, 'ajax_delete_orphaned_image'));
375 377 add_action('metasync_media_batch_optimize_cron', array($this, 'handle_media_batch_cron'));
376 378
379 + # SEO Restore (Rollback) AJAX & Cron handlers
380 + add_action('wp_ajax_metasync_seo_restore_start', array($this, 'ajax_seo_restore_start'));
381 + add_action('wp_ajax_metasync_seo_restore_cancel', array($this, 'ajax_seo_restore_cancel'));
382 + add_action('wp_ajax_metasync_seo_restore_progress', array($this, 'ajax_seo_restore_progress'));
383 + add_action('wp_ajax_metasync_seo_restore_process_tick', array($this, 'ajax_seo_restore_process_tick'));
384 + add_action('metasync_seo_restore_cron', array($this, 'handle_seo_restore_cron'));
385 +
377 386 # Add AJAX handlers for Google Instant Indexing
378 387 add_action('wp_ajax_metasync_send_giapi', array($this, 'ajax_send_giapi'));
379 388
380 389 # Add AJAX handlers for Bing Instant Indexing (IndexNow)
@@ -488,9 +497,9 @@
488 497 <?php
489 498 }
490 499
491 500 public function metasync_fouc_prevention_style() {
492 - 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
493 502 return;
494 503 }
495 504 ?>
496 505 <style>
@@ -512,13 +521,13 @@
512 521 <?php
513 522 }
514 523
515 524 public function suppress_notices_on_wizard_page() {
516 - 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
517 526 return;
518 527 }
519 528
520 - $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
521 530
522 531 // Setup wizard: strip every notice for a fully focused, distraction-free screen.
523 532 if ( strpos( $page, '-setup-wizard' ) !== false ) {
524 533 remove_all_actions( 'admin_notices' );
@@ -636,9 +645,9 @@
636 645
637 646 #Redirect url to the new slug
638 647 $redirect_url = admin_url('admin.php?page=' . $current_slug);
639 648
640 - wp_redirect($redirect_url);
649 + wp_safe_redirect($redirect_url);
641 650 exit;
642 651 }
643 652 }
644 653
@@ -690,13 +699,13 @@
690 699 * @since 1.0.0
691 700 */
692 701 public function maybe_redirect_to_wizard() {
693 702 // Only redirect if wizard should be shown
694 - 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
695 704 delete_option('metasync_show_wizard');
696 705
697 706 // Don't redirect during AJAX, cron, or bulk activation
698 - 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
699 708 return;
700 709 }
701 710
702 711 // Only redirect if user has access to the plugin
@@ -708,11 +717,56 @@
708 717 exit;
709 718 }
710 719 }
711 720
721 + /**
722 + * Redirect the removed standalone Instant Indexing page to Indexation Control.
723 + *
724 + * The menu entry is gone, but bookmarks and external links can still target
725 + * the old URL. Resolve the current white-label slug rather than assuming
726 + * "searchatlas".
727 + */
728 + public function redirect_legacy_instant_indexing_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
730 + return;
731 + }
732 +
733 + $page = sanitize_key(wp_unslash($_GET['page'])); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- read-only page check to hide notices for this request
734 + $legacy_page = self::$page_slug . '-instant-index';
735 + if ($page !== $legacy_page || !Metasync::current_user_has_plugin_access()) {
736 + return;
737 + }
738 +
739 + // Indexation Control is registered only when access control keeps it
740 + // visible. Redirecting to it blindly would dead-end on an
741 + // unregistered slug (wp_die "Invalid plugin page") for users whose
742 + // white-label config hides that page — the exact configs where the
743 + // removed standalone page was the feature's only UI. The bare plugin
744 + // slug is the floor: its top-level callback resolves to the first
745 + // reachable page, so it is always a safe target.
746 + if (Metasync_Access_Control::user_can_access('hide_indexation_control')) {
747 + $target = admin_url('admin.php?page=' . self::$page_slug . '-seo-controls');
748 + $query = [];
749 + foreach (['tab', 'subtab'] as $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
752 + }
753 + }
754 + if (!empty($query)) {
755 + $target = add_query_arg($query, $target);
756 + }
757 + } else {
758 + $target = admin_url('admin.php?page=' . self::$page_slug);
759 + }
760 +
761 + wp_safe_redirect($target);
762 + exit;
763 + }
764 +
712 765 public function metasync_display_error_log() {
713 766 Metasync_Debug_Manager::instance()->metasync_display_error_log($this);
714 767 }
768 +
715 769 public function metasync_update_wp_config() {
716 770 Metasync_Debug_Manager::instance()->metasync_update_wp_config();
717 771 }
718 772
@@ -755,9 +809,9 @@
755 809 ]);
756 810 $_COOKIE['metasync_previous_slug'] = $new_slug;
757 811 // Redirect to the new slug
758 812 $redirect_url = admin_url('admin.php?page=' . $old_slug);
759 - wp_redirect($redirect_url);
813 + wp_safe_redirect($redirect_url);
760 814 exit;
761 815 }else{
762 816 self::$page_slug = Metasync::get_option('general')['white_label_plugin_menu_slug']=="" ? "searchatlas":Metasync::get_option('general')['white_label_plugin_menu_slug'];
763 817 $redirect_url = admin_url('admin.php?page=' . self::$page_slug);
@@ -763,9 +817,9 @@
763 817 $redirect_url = admin_url('admin.php?page=' . self::$page_slug);
764 818
765 819 #add redirection for when the old slug is not defined
766 820 #this fixes the redirect issue #
767 - wp_redirect($redirect_url);
821 + wp_safe_redirect($redirect_url);
768 822 exit;
769 823 }
770 824 }
771 825 public function metasync_view_detials_url( $plugin_meta, $plugin_file, $plugin_data ) {
@@ -776,9 +830,10 @@
776 830 if (strpos($meta, 'open-plugin-details-modal') !== false) {
777 831 $meta = sprintf(
778 832 '<a href="%s" class="thickbox open-plugin-details-modal" aria-label="%s" data-title="%s">%s</a>',
779 833 add_query_arg('TB_iframe', 'true', $plugin_uri),
780 - 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'])),
781 836 esc_attr($plugin_data['Name']),
782 837 esc_html__('View details', 'metasync')
783 838 );
784 839 break; // Exit loop after replacing the link
@@ -826,53 +881,8 @@
826 881 return $all_plugins;
827 882 }
828 883
829 884 /**
830 - * Inject the whitelabel icon into the update_plugins transient so that
831 - * /wp-admin/update-core.php (Dashboard → Updates) shows the WL icon
832 - * instead of the SearchAtlas icon returned by the update API.
833 - *
834 - * @param object $transient The site transient object.
835 - * @return object
836 - */
837 - public function inject_whitelabel_icon_into_update_transient($transient) {
838 - if (empty($transient) || !is_object($transient)) {
839 - return $transient;
840 - }
841 -
842 - $general = Metasync::get_option('general');
843 - if (!is_array($general)) {
844 - return $transient;
845 - }
846 -
847 - $icon_url = $general['white_label_plugin_menu_icon'] ?? '';
848 - if (empty($icon_url)) {
849 - return $transient;
850 - }
851 -
852 - $this_plugin = plugin_basename(dirname(__DIR__) . '/metasync.php');
853 -
854 - // Inject into pending updates list
855 - if (!empty($transient->response) && isset($transient->response[$this_plugin])) {
856 - $transient->response[$this_plugin]->icons = [
857 - '1x' => $icon_url,
858 - '2x' => $icon_url,
859 - ];
860 - }
861 -
862 - // Also inject into the "no update needed" list so the icon appears
863 - // on the updates screen even when the plugin is up-to-date
864 - if (!empty($transient->no_update) && isset($transient->no_update[$this_plugin])) {
865 - $transient->no_update[$this_plugin]->icons = [
866 - '1x' => $icon_url,
867 - '2x' => $icon_url,
868 - ];
869 - }
870 -
871 - return $transient;
872 - }
873 -
874 - /**
875 885 * Register the stylesheets for the admin area.
876 886 *
877 887 * @since 1.0.0
878 888 */
@@ -877,9 +887,9 @@
877 887 * @since 1.0.0
878 888 */
879 889 public function enqueue_styles()
880 890 {
881 - $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
882 892
883 893 if (strpos($current_page, self::$page_slug) === 0) {
884 894 wp_enqueue_style(
885 895 $this->plugin_name,
@@ -908,9 +918,9 @@
908 918 );
909 919 }
910 920
911 921 // Enqueue wizard CSS if on wizard page
912 - 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
913 923 wp_enqueue_style(
914 924 $this->plugin_name . '-setup-wizard',
915 925 plugin_dir_url(__FILE__) . 'css/metasync-setup-wizard.css',
916 926 array($this->plugin_name . '-dashboard'),
@@ -919,9 +929,9 @@
919 929 );
920 930 }
921 931
922 932 // Enqueue SEO Health CSS if on the SEO Health page
923 - 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
924 934 wp_enqueue_style(
925 935 $this->plugin_name . '-seo-health',
926 936 plugin_dir_url(__FILE__) . 'css/metasync-seo-health.css',
927 937 array($this->plugin_name . '-dashboard'),
@@ -938,9 +948,9 @@
938 948 */
939 949 public function enqueue_scripts()
940 950 {
941 951 // --- Phase 5 (#887): Extracted inline JS files ---
942 - $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
943 953 $plugin_root_url = plugin_dir_url(dirname(__FILE__));
944 954 $is_metasync_page = ($current_page === self::$page_slug || strpos($current_page, self::$page_slug) === 0);
945 955
946 956 if ($is_metasync_page) {
@@ -1001,8 +1011,16 @@
1001 1011 array(),
1002 1012 $this->version,
1003 1013 true
1004 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 + );
1005 1023 }
1006 1024
1007 1025 // Dashboard iframe height (only on dashboard page)
1008 1026 if ($current_page === self::$page_slug . '-dashboard' || $current_page === self::$page_slug) {
@@ -1053,12 +1071,12 @@
1053 1071 true
1054 1072 );
1055 1073 // Pass the active tab from server (handles POST redirect)
1056 1074 $sitemap_active_tab = 'general';
1057 - if (isset($_GET['tab']) && in_array($_GET['tab'], ['general', 'news', 'video'], true)) {
1058 - $sitemap_active_tab = sanitize_text_field(wp_unslash($_GET['tab']));
1059 - } elseif (isset($_POST['redirect_tab']) && in_array($_POST['redirect_tab'], ['general', 'news', 'video'], true)) {
1060 - $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
1061 1079 }
1062 1080 wp_localize_script($this->plugin_name . '-sitemap-tabs', 'metasyncSitemapTabs', [
1063 1081 'activeTab' => $sitemap_active_tab,
1064 1082 ]);
@@ -1386,8 +1404,10 @@
1386 1404 'nonce' => wp_create_nonce('metasync_nonce'),
1387 1405 'sa_connect_nonce' => $sa_connect_nonce,
1388 1406 'reset_auth_nonce' => wp_create_nonce('metasync_reset_auth_nonce'),
1389 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'),
1390 1410 'heartbeat_state' => $heartbeat_state,
1391 1411 'dashboard_domain' => self::get_effective_dashboard_domain(),
1392 1412 'support_email' => Metasync::SUPPORT_EMAIL,
1393 1413 'documentation_domain' => Metasync::DOCUMENTATION_DOMAIN,
@@ -1451,9 +1471,9 @@
1451 1471 add_action('admin_notices', array($this, 'display_page_builder_notice'));
1452 1472 // Display update warning banner if plugin update is available
1453 1473 add_action('admin_notices', array($this, 'display_update_warning_banner'));
1454 1474 // Enqueue wizard assets if on wizard page
1455 - 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
1456 1476 wp_enqueue_script(
1457 1477 $this->plugin_name . '-setup-wizard',
1458 1478 plugin_dir_url(__FILE__) . 'js/metasync-setup-wizard.js',
1459 1479 array('jquery'),
@@ -1652,13 +1672,8 @@
1652 1672 {
1653 1673 Metasync_Connect_Manager::instance()->test_searchatlas_ajax_endpoint();
1654 1674 }
1655 1675
1656 - public function simple_ajax_test()
1657 - {
1658 - Metasync_Connect_Manager::instance()->simple_ajax_test();
1659 - }
1660 -
1661 1676 /**
1662 1677 * Create Admin Dashboard Iframe Page
1663 1678 * Embeds the Search Atlas dashboard directly in WordPress admin
1664 1679 */
@@ -1745,9 +1760,9 @@
1745 1760 } else {
1746 1761 echo '<p style="color: var(--dashboard-text-secondary);">ℹ️ No cache plugins detected.</p>';
1747 1762 }
1748 1763 } catch (Exception $e) {
1749 - 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
1750 1765 echo '<p style="color: var(--dashboard-error);">⚠️ An error occurred while retrieving cache plugin status.</p>';
1751 1766 }
1752 1767 } else {
1753 1768 echo '<p style="color: var(--dashboard-error);">⚠️ Cache Purge class not loaded.</p>';
@@ -1764,12 +1779,12 @@
1764 1779 </form>
1765 1780
1766 1781 <?php
1767 1782 // Display success/error messages
1768 - if (isset($_GET['cache_cleared']) && $_GET['cache_cleared'] == '1') {
1769 - $cleared = isset($_GET['cleared']) ? intval($_GET['cleared']) : 0;
1770 - $failed = isset($_GET['failed']) ? intval($_GET['failed']) : 0;
1771 - $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
1772 1787
1773 1788 if ($cleared > 0) {
1774 1789 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
1775 1790 echo '✅ <strong>Success!</strong> Cleared cache for ' . intval( $cleared ) . ' plugin(s)';
@@ -1789,10 +1804,10 @@
1789 1804 echo '</p></div>';
1790 1805 }
1791 1806 }
1792 1807
1793 - if (isset($_GET['cache_error']) && $_GET['cache_error'] == '1') {
1794 - $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
1795 1810 if (empty($message)) {
1796 1811 $message = 'An unknown error occurred while clearing cache. Please check error logs for details.';
1797 1812 }
1798 1813 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
@@ -1889,12 +1904,13 @@
1889 1904 </form>
1890 1905
1891 1906 <?php
1892 1907 // Hosting cache result messages
1893 - if (isset($_GET['hosting_cache_cleared']) && $_GET['hosting_cache_cleared'] == '1') {
1894 - $hc_cleared = isset($_GET['hc_cleared']) ? sanitize_text_field(urldecode($_GET['hc_cleared'])) : '';
1895 - $hc_failed = isset($_GET['hc_failed']) ? sanitize_text_field(urldecode($_GET['hc_failed'])) : '';
1896 - $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
1897 1913
1898 1914 if ($hc_cleared) {
1899 1915 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
1900 1916 echo '✅ <strong>Success!</strong> Purged hosting cache on: ' . esc_html($hc_cleared);
@@ -1909,8 +1925,9 @@
1909 1925 echo '<div class="notice notice-info inline" style="margin-top: 10px;"><p>';
1910 1926 echo 'ℹ️ No enabled hosting providers were detected on this server (' . esc_html($hc_not_detected) . ').';
1911 1927 echo '</p></div>';
1912 1928 }
1929 + // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
1913 1930 }
1914 1931 ?>
1915 1932
1916 1933 <script>
@@ -2075,11 +2092,11 @@
2075 2092 </div>
2076 2093
2077 2094 <?php
2078 2095 // Display success/error messages
2079 - if (isset($_GET['otto_cache_cleared']) && $_GET['otto_cache_cleared'] == '1') {
2080 - $cleared_count = isset($_GET['count']) ? intval($_GET['count']) : 0;
2081 - $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
2082 2099
2083 2100 echo '<div class="notice notice-success inline" style="margin-top: 15px;"><p>';
2084 2101 if (!empty($url)) {
2085 2102 echo '✅ <strong>Success!</strong> Cleared cache for URL: <code>' . esc_html($url) . '</code> (' . intval( $cleared_count ) . ' entries)';
@@ -2088,10 +2105,10 @@
2088 2105 }
2089 2106 echo '</p></div>';
2090 2107 }
2091 2108
2092 - if (isset($_GET['otto_cache_error']) && $_GET['otto_cache_error'] == '1') {
2093 - $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
2094 2111 echo '<div class="notice notice-error inline" style="margin-top: 15px;"><p>';
2095 2112 echo '❌ <strong>Error:</strong> ' . esc_html($message);
2096 2113 echo '</p></div>';
2097 2114 }
@@ -2130,9 +2147,9 @@
2130 2147 <td>
2131 2148 <input type="url"
2132 2149 id="otto_cache_url"
2133 2150 name="otto_cache_url"
2134 - 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 ?>"
2135 2152 class="regular-text"
2136 2153 placeholder="https://example.com/page/"
2137 2154 required />
2138 2155 <p class="description" style="color: var(--dashboard-text-secondary);">Enter the full URL of the page whose cache you want to clear.</p>
@@ -2203,9 +2220,10 @@
2203 2220 /**
2204 2221 * AJAX handler for saving hosting cache settings
2205 2222 */
2206 2223 public function ajax_save_hosting_cache_settings() {
2207 - 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')) {
2208 2226 wp_send_json_error(array('message' => 'Security check failed'));
2209 2227 return;
2210 2228 }
2211 2229
@@ -2226,9 +2244,10 @@
2226 2244 /**
2227 2245 * AJAX handler: save object cache behaviour settings
2228 2246 */
2229 2247 public function ajax_save_object_cache_settings() {
2230 - 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')) {
2231 2250 wp_send_json_error(array('message' => 'Security check failed'));
2232 2251 return;
2233 2252 }
2234 2253
@@ -2255,9 +2274,10 @@
2255 2274 /**
2256 2275 * AJAX handler for saving OTTO Cache TTL
2257 2276 */
2258 2277 public function ajax_save_otto_cache_ttl() {
2259 - 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')) {
2260 2280 wp_send_json_error(array('message' => 'Security check failed.'));
2261 2281 return;
2262 2282 }
2263 2283
@@ -2283,9 +2303,10 @@
2283 2303 /**
2284 2304 * admin_post handler: purge WP Engine and Kinsta hosting-level caches
2285 2305 */
2286 2306 public function handle_purge_hosting_cache() {
2287 - 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')) {
2288 2309 wp_die('Security check failed');
2289 2310 }
2290 2311
2291 2312 if (!Metasync::current_user_has_plugin_access()) {
@@ -2305,9 +2326,9 @@
2305 2326 WpeCommon::purge_varnish_cache();
2306 2327 WpeCommon::purge_memcached();
2307 2328 $cleared[] = 'WP Engine';
2308 2329 } catch (Exception $e) {
2309 - 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
2310 2331 $failed[] = 'WP Engine';
2311 2332 }
2312 2333 } else {
2313 2334 $not_detected[] = 'WP Engine';
@@ -2320,9 +2341,9 @@
2320 2341 try {
2321 2342 KinstaCache::get_instance()->kinsta_cache_purge_full();
2322 2343 $cleared[] = 'Kinsta';
2323 2344 } catch (Exception $e) {
2324 - 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
2325 2346 $failed[] = 'Kinsta';
2326 2347 }
2327 2348 } else {
2328 2349 $not_detected[] = 'Kinsta';
@@ -2717,9 +2738,9 @@
2717 2738 # Initialize MetaSync API request class and trigger synchronization
2718 2739 (new Metasync_Sync_Requests())->SyncCustomerParams();
2719 2740 } catch (Exception $e) {
2720 2741 # Log any API request errors for debugging
2721 - 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
2722 2743 }
2723 2744 }
2724 2745
2725 2746 /*
@@ -2734,9 +2755,9 @@
2734 2755 # Initialize MetaSync API request class and trigger synchronization
2735 2756 (new Metasync_Sync_Requests())->SyncCustomerParams();
2736 2757 } catch (Exception $e) {
2737 2758 # Log any API request errors for debugging
2738 - 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
2739 2760 }
2740 2761 }
2741 2762
2742 2763 /**
@@ -2773,9 +2794,9 @@
2773 2794 // Settings shape the whole site's conversion pipeline — restrict
2774 2795 // the save (and the reset) to site admins, not just anyone who can
2775 2796 // open the page via plugin_access_roles.
2776 2797 if (!current_user_can('manage_options')) {
2777 - 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'));
2778 2799 }
2779 2800
2780 2801 // Handle reset to defaults
2781 2802 if (!empty($_POST['metasync_media_reset'])) {
@@ -2782,8 +2803,9 @@
2782 2803 $defaults = Metasync_Media_Settings::get_defaults();
2783 2804 Metasync_Media_Settings::save_settings($defaults);
2784 2805 $save_success = true;
2785 2806 } elseif (isset($_POST['metasync_media'])) {
2807 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated and sanitised by Metasync_Media_Settings::save_settings().
2786 2808 $input = wp_unslash($_POST['metasync_media']);
2787 2809 Metasync_Media_Settings::save_settings($input);
2788 2810 $save_success = true;
2789 2811 }
@@ -2815,8 +2837,15 @@
2815 2837 * Code Minification page callback
2816 2838 */
2817 2839 public function create_admin_code_minification_page()
2818 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 +
2819 2848 $this->render_layout_open('Code Minification', 'code_minification', 'Minify CSS, JavaScript, and HTML to improve performance.');
2820 2849 // Load settings and compatibility classes
2821 2850 require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-minification-settings.php';
2822 2851 require_once plugin_dir_path(dirname(__FILE__)) . 'code-minification/class-minification-cache.php';
@@ -2833,8 +2862,9 @@
2833 2862 $defaults = Metasync_Minification_Settings::get_defaults();
2834 2863 Metasync_Minification_Settings::save_settings($defaults);
2835 2864 $save_success = true;
2836 2865 } elseif (isset($_POST['metasync_code_min'])) {
2866 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- validated and sanitised by Metasync_Minification_Settings::save_settings().
2837 2867 $input = (array) wp_unslash($_POST['metasync_code_min']);
2838 2868 Metasync_Minification_Settings::save_settings($input);
2839 2869 $save_success = true;
2840 2870 }
@@ -2889,8 +2919,9 @@
2889 2919 $max_bytes = 10 * 1024 * 1024;
2890 2920 if (filesize($file) > $max_bytes) {
2891 2921 $size_mb = round(filesize($file) / 1024 / 1024, 1);
2892 2922 wp_send_json_error(sprintf(
2923 + /* translators: %s: file size in megabytes. */
2893 2924 __('Optimization skipped: file size (%s MB) exceeds the 10 MB safety limit to prevent memory issues.', 'metasync'),
2894 2925 $size_mb
2895 2926 ));
2896 2927 }
@@ -3099,8 +3130,9 @@
3099 3130 if (!Metasync_Image_Converter::can_revert($id)) {
3100 3131 $skipped++;
3101 3132 $file = get_attached_file($id);
3102 3133 $name = $file ? basename($file) : "ID {$id}";
3134 + /* translators: %s: attachment file name. */
3103 3135 $errors[] = sprintf(__('%s: skipped — original image unavailable.', 'metasync'), $name);
3104 3136 continue;
3105 3137 }
3106 3138
@@ -3109,8 +3141,9 @@
3109 3141 } else {
3110 3142 $failed++;
3111 3143 $file = get_attached_file($id);
3112 3144 $name = $file ? basename($file) : "ID {$id}";
3145 + /* translators: %s: attachment file name. */
3113 3146 $errors[] = sprintf(__('%s: revert failed (original file may not exist).', 'metasync'), $name);
3114 3147 }
3115 3148 }
3116 3149
@@ -3195,8 +3228,86 @@
3195 3228 Metasync_Media_Batch_Optimizer::process_batch_tick();
3196 3229 }
3197 3230
3198 3231 /**
3232 + * AJAX: Start SEO Restore batch.
3233 + */
3234 + public function ajax_seo_restore_start()
3235 + {
3236 + check_ajax_referer('metasync_seo_restore_nonce', 'nonce');
3237 +
3238 + if (!current_user_can('manage_options')) {
3239 + wp_send_json_error(__('Permission denied.', 'metasync'));
3240 + }
3241 +
3242 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php';
3243 +
3244 + $progress = Metasync_Seo_Restore::start_batch();
3245 + wp_send_json_success($progress);
3246 + }
3247 +
3248 + /**
3249 + * AJAX: Cancel SEO Restore batch.
3250 + */
3251 + public function ajax_seo_restore_cancel()
3252 + {
3253 + check_ajax_referer('metasync_seo_restore_nonce', 'nonce');
3254 +
3255 + if (!current_user_can('manage_options')) {
3256 + wp_send_json_error(__('Permission denied.', 'metasync'));
3257 + }
3258 +
3259 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php';
3260 +
3261 + Metasync_Seo_Restore::cancel_batch();
3262 + wp_send_json_success();
3263 + }
3264 +
3265 + /**
3266 + * AJAX: Get SEO Restore batch progress.
3267 + */
3268 + public function ajax_seo_restore_progress()
3269 + {
3270 + check_ajax_referer('metasync_seo_restore_nonce', 'nonce');
3271 +
3272 + if (!current_user_can('manage_options')) {
3273 + wp_send_json_error(__('Permission denied.', 'metasync'));
3274 + }
3275 +
3276 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php';
3277 +
3278 + $progress = Metasync_Seo_Restore::get_progress();
3279 + wp_send_json_success($progress);
3280 + }
3281 +
3282 + /**
3283 + * AJAX: Process one SEO Restore tick (browser-driven chaining).
3284 + */
3285 + public function ajax_seo_restore_process_tick()
3286 + {
3287 + check_ajax_referer('metasync_seo_restore_nonce', 'nonce');
3288 +
3289 + if (!current_user_can('manage_options')) {
3290 + wp_send_json_error(__('Permission denied.', 'metasync'));
3291 + }
3292 +
3293 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php';
3294 +
3295 + $progress = Metasync_Seo_Restore::process_ajax_tick();
3296 + wp_send_json_success($progress);
3297 + }
3298 +
3299 + /**
3300 + * Cron handler: Process SEO Restore batch tick.
3301 + */
3302 + public function handle_seo_restore_cron()
3303 + {
3304 + require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-metasync-seo-restore.php';
3305 +
3306 + Metasync_Seo_Restore::process_batch_tick();
3307 + }
3308 +
3309 + /**
3199 3310 * Report Issue page callback
3200 3311 */
3201 3312 public function create_admin_report_issue_page()
3202 3313 {
@@ -3208,8 +3319,17 @@
3208 3319 * XML Sitemap page callback
3209 3320 */
3210 3321 public function create_admin_xml_sitemap_page()
3211 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 +
3212 3332 // Load sitemap generator class
3213 3333 require_once plugin_dir_path(dirname(__FILE__)) . 'sitemap/class-metasync-sitemap-generator.php';
3214 3334
3215 3335 $sitemap_generator = new Metasync_Sitemap_Generator();
@@ -3240,8 +3360,9 @@
3240 3360 // Regenerate sitemap with new content settings
3241 3361 $result = $sitemap_generator->generate_sitemap();
3242 3362 if (is_wp_error($result)) {
3243 3363 echo '<div class="notice notice-error"><p>' . esc_html(
3364 + /* translators: %s: error message. */
3244 3365 sprintf(__('Settings saved but sitemap generation failed: %s', 'metasync'), $result->get_error_message())
3245 3366 ) . '</p></div>';
3246 3367 } elseif (false === $result) {
3247 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>';
@@ -3256,10 +3377,10 @@
3256 3377
3257 3378 // Build generic taxonomy filters
3258 3379 $news_taxonomies = [];
3259 3380 if (!empty($_POST['news_taxonomies']) && is_array($_POST['news_taxonomies'])) {
3260 - foreach ($_POST['news_taxonomies'] as $tax_name => $term_ids) {
3261 - $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));
3262 3383 }
3263 3384 }
3264 3385
3265 3386 $news_settings = [
@@ -3284,9 +3405,9 @@
3284 3405 echo '<div class="notice notice-success"><p>' . esc_html__('News sitemap settings saved and sitemap regenerated!', 'metasync') . '</p></div>';
3285 3406 } else {
3286 3407 // Also remove physical file if it exists
3287 3408 if (file_exists(ABSPATH . 'news-sitemap.xml')) {
3288 - @unlink(ABSPATH . 'news-sitemap.xml');
3409 + @wp_delete_file(ABSPATH . 'news-sitemap.xml');
3289 3410 }
3290 3411 echo '<div class="notice notice-success"><p>' . esc_html__('News sitemap settings saved. Sitemap cache cleared.', 'metasync') . '</p></div>';
3291 3412 }
3292 3413 }
@@ -3297,10 +3418,10 @@
3297 3418
3298 3419 // Build generic taxonomy filters
3299 3420 $video_taxonomies = [];
3300 3421 if (!empty($_POST['video_taxonomies']) && is_array($_POST['video_taxonomies'])) {
3301 - foreach ($_POST['video_taxonomies'] as $tax_name => $term_ids) {
3302 - $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));
3303 3424 }
3304 3425 }
3305 3426
3306 3427 $video_settings = [
@@ -3326,9 +3447,9 @@
3326 3447 echo '<div class="notice notice-success"><p>' . esc_html__('Video sitemap settings saved and sitemap regenerated!', 'metasync') . '</p></div>';
3327 3448 } else {
3328 3449 // Also remove physical file if it exists
3329 3450 if (file_exists(ABSPATH . 'video-sitemap.xml')) {
3330 - @unlink(ABSPATH . 'video-sitemap.xml');
3451 + @wp_delete_file(ABSPATH . 'video-sitemap.xml');
3331 3452 }
3332 3453 echo '<div class="notice notice-success"><p>' . esc_html__('Video sitemap settings saved. Sitemap cache cleared.', 'metasync') . '</p></div>';
3333 3454 }
3334 3455 }
@@ -3396,10 +3517,11 @@
3396 3517 $result = $sitemap_generator->generate_sitemap();
3397 3518
3398 3519 if (is_wp_error($result)) {
3399 3520 $error_msg = $result->get_error_message();
3400 - 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
3401 3522 echo '<div class="notice notice-error"><p>' . esc_html(
3523 + /* translators: %s: error message. */
3402 3524 sprintf(__('Sitemap generation failed: %s', 'metasync'), $error_msg)
3403 3525 ) . '</p></div>';
3404 3526 } elseif (false === $result) {
3405 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>';
@@ -3406,8 +3528,9 @@
3406 3528 } else {
3407 3529 $message = esc_html__('Sitemap generated successfully!', 'metasync');
3408 3530 if (!empty($extras)) {
3409 3531 $message .= ' ' . sprintf(
3532 + /* translators: %s: list of sitemap types. */
3410 3533 esc_html__('Also generated %s sitemap(s).', 'metasync'),
3411 3534 implode(' & ', $extras)
3412 3535 );
3413 3536 }
@@ -3596,48 +3719,8 @@
3596 3719 Metasync_Admin_Pages::get_instance($this)->create_admin_breadcrumbs_page();
3597 3720 }
3598 3721
3599 3722 /**
3600 - * Google Instant Index Setting page callback
3601 - */
3602 - public function create_admin_google_instant_index_page()
3603 - {
3604 - $this->render_layout_open('Instant Indexing', 'instant_index', 'Submit URLs to Google for instant indexing via the Indexing API.');
3605 -
3606 - // Render shared Google Index credentials section
3607 - if (!function_exists('google_index_direct')) {
3608 - if (file_exists(plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php')) {
3609 - require_once plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php';
3610 - } else {
3611 - error_log('MetaSync Google Index: google-index-init.php not found at ' . plugin_dir_path(dirname(__FILE__)) . 'google-index/google-index-init.php');
3612 - return;
3613 - }
3614 - }
3615 - $google_index = google_index_direct();
3616 - $service_info = $google_index->get_service_account_info();
3617 - $is_configured = !isset($service_info['error']);
3618 -
3619 - $saved_json_display = $is_configured ? $google_index->get_redacted_config_json() : '';
3620 -
3621 - include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-index-api-settings.php';
3622 -
3623 - // Render post types selection with save form
3624 - $options = get_option('metasync_options_instant_indexing', ['post_types' => []]);
3625 - $post_types_settings = isset($options['post_types']) && is_array($options['post_types']) ? $options['post_types'] : [];
3626 - ?>
3627 - <form method="POST" action="">
3628 - <?php wp_nonce_field('metasync_instant_indexing_settings', 'metasync_instant_indexing_nonce'); ?>
3629 - <?php include plugin_dir_path(dirname(__FILE__)) . 'views/metasync-google-instant-post-types.php'; ?>
3630 - <div class="dashboard-card" style="padding: 20px;">
3631 - <?php submit_button('Save Post Types', 'primary', 'submit', false, array('class' => 'button button-primary')); ?>
3632 - </div>
3633 - </form>
3634 - <?php
3635 -
3636 - $this->render_layout_close();
3637 - }
3638 -
3639 - /**
3640 3723 * Google Console page callback
3641 3724 */
3642 3725 public function create_admin_google_console_page()
3643 3726 {
@@ -3743,8 +3826,9 @@
3743 3826 if (!get_transient('metasync_llms_conflict')) {
3744 3827 return;
3745 3828 }
3746 3829 echo '<div class="notice notice-info is-dismissible"><p>';
3830 + /* translators: %s: effective plugin name (whitelabel-aware). */
3747 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()));
3748 3832 echo '</p></div>';
3749 3833 }
3750 3834
@@ -3936,9 +4020,9 @@
3936 4020 return;
3937 4021 }
3938 4022
3939 4023 // Get pagination parameters
3940 - $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
3941 4025 $per_page = Metasync_Per_Page_Helper::resolve('sync_log', 10);
3942 4026 $offset = ($page - 1) * $per_page;
3943 4027
3944 4028 // Get filters
@@ -3943,10 +4027,10 @@
3943 4027
3944 4028 // Get filters
3945 4029 $filters = [
3946 4030 // UI exposes date_range and status only. We compute date_from/date_to based on date_range
3947 - 'date_range' => isset($_GET['date_range']) ? sanitize_text_field(wp_unslash($_GET['date_range'])) : '',
3948 - '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
3949 4033 ];
3950 4034
3951 4035 // Map date_range to concrete date_from/date_to for DB queries
3952 4036 $date_range = $filters['date_range'];
@@ -3955,28 +4039,28 @@
3955 4039 $date_to = '';
3956 4040
3957 4041 if (!empty($date_range)) {
3958 4042 // End boundary is now by default
3959 - $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);
3960 4044
3961 4045 if ($date_range === 'today') {
3962 4046 $start_ts = strtotime('today', $wp_now_ts);
3963 - $date_from = date('Y-m-d H:i:s', $start_ts);
4047 + $date_from = gmdate('Y-m-d H:i:s', $start_ts);
3964 4048 } elseif ($date_range === 'yesterday') {
3965 4049 $start_ts = strtotime('yesterday', $wp_now_ts);
3966 4050 $end_ts = strtotime('today', $wp_now_ts) - 1; // end of yesterday
3967 - $date_from = date('Y-m-d H:i:s', $start_ts);
3968 - $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);
3969 4053 } elseif ($date_range === 'this_week') {
3970 4054 $start_of_week = (int) get_option('start_of_week', 1); // 0=Sun, 1=Mon
3971 - $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
3972 4056 // Convert start_of_week to PHP's 0..6 where 0=Sunday
3973 4057 $delta_days = ($day_of_week - $start_of_week + 7) % 7;
3974 4058 $start_ts = strtotime('-' . $delta_days . ' days', strtotime('today', $wp_now_ts));
3975 - $date_from = date('Y-m-d H:i:s', $start_ts);
4059 + $date_from = gmdate('Y-m-d H:i:s', $start_ts);
3976 4060 } elseif ($date_range === 'this_month') {
3977 - $start_ts = strtotime(date('Y-m-01 00:00:00', $wp_now_ts));
3978 - $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);
3979 4063 } elseif ($date_range === 'all') {
3980 4064 // no bounds
3981 4065 }
3982 4066 }
@@ -4003,12 +4087,24 @@
4003 4087 ?>
4004 4088 <div class="dashboard-card">
4005 4089 <div class="sync-log-header">
4006 4090 <div class="sync-log-title-section">
4007 - <h2>Changes Log</h2>
4008 - <p style="color: var(--dashboard-text-secondary); margin-bottom: 0;">
4009 - Recent content synchronizations from external tools.
4010 - <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>
4011 4107 </p>
4012 4108 </div>
4013 4109
4014 4110 <!-- Filters + Clear Log button - Right aligned -->
@@ -4018,9 +4114,9 @@
4018 4114 data-nonce="<?php echo esc_attr(wp_create_nonce('metasync_clear_sync_log')); ?>">
4019 4115 🗑 Clear Log
4020 4116 </button>
4021 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;">
4022 - <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 ?>">
4023 4119
4024 4120 <select name="date_range" class="sync-filter-select">
4025 4121 <option value="all" <?php selected($filters['date_range'] ?? 'all', 'all'); ?>> All Time</option>
4026 4122 <option value="today" <?php selected($filters['date_range'] ?? '', 'today'); ?>>Today</option>
@@ -4042,9 +4138,9 @@
4042 4138 <div class="sync-log-list">
4043 4139 <?php if (empty($sync_records)): ?>
4044 4140 <div class="sync-log-empty">
4045 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>
4046 - <h3>No sync records found</h3>
4142 + <h2>No sync records found</h2>
4047 4143 <p>Sync records will appear here when content/pages receive new updates.</p>
4048 4144 </div>
4049 4145 <?php else: ?>
4050 4146 <?php foreach ($sync_records as $record): ?>
@@ -4101,24 +4197,25 @@
4101 4197 <!-- Pagination -->
4102 4198 <div class="sync-log-pagination">
4103 4199 <div class="sync-log-pagination-info">
4104 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; ?>
4105 - <?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)); ?>
4106 4203 </div>
4107 4204
4108 4205 <?php if ($total_pages > 1): ?>
4109 4206 <div class="sync-log-pagination-controls">
4110 4207 <?php if ($page > 1): ?>
4111 - <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>
4112 4209 <?php endif; ?>
4113 4210
4114 4211 <?php for ($i = max(1, $page - 2); $i <= min($total_pages, $page + 2); $i++): ?>
4115 - <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) ); ?>"
4116 4213 class="sync-pagination-btn <?php echo $i === $page ? 'active' : ''; ?>"><?php echo intval( $i ); ?></a>
4117 4214 <?php endfor; ?>
4118 4215
4119 4216 <?php if ($page < $total_pages): ?>
4120 - <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>
4121 4218 <?php endif; ?>
4122 4219 </div>
4123 4220 <?php endif; ?>
4124 4221 </div>
@@ -4207,10 +4304,10 @@
4207 4304
4208 4305 // Preserve the user-selected results-per-page value across page
4209 4306 // navigation so a non-default page size survives clicking a page link.
4210 4307 $per_page_key = Metasync_Per_Page_Helper::request_key('sync_log');
4211 - if (isset($_GET[$per_page_key])) {
4212 - $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
4213 4310 if (in_array($per_page, Metasync_Per_Page_Helper::allowed_values(), true)) {
4214 4311 $query_parts[] = $per_page_key . '=' . $per_page;
4215 4312 }
4216 4313 }
@@ -4389,18 +4486,8 @@
4389 4486 {
4390 4487 Metasync_Connect_Manager::instance()->handle_session_management_early();
4391 4488 }
4392 4489
4393 - /**
4394 - * @deprecated 2.5.12 Use Metasync_Auth_Manager instead of sessions for authentication
4395 - */
4396 - private function safe_session_start() {
4397 - // This method is deprecated and no longer used
4398 - // Authentication now uses Metasync_Auth_Manager with WordPress transients and user meta
4399 - _deprecated_function(__METHOD__, '2.5.12', 'Metasync_Auth_Manager');
4400 - return Metasync_Session_Helper::safe_start();
4401 - }
4402 -
4403 4490 private function handle_whitelabel_session_logic()
4404 4491 {
4405 4492 Metasync_Connect_Manager::instance()->handle_whitelabel_session_logic();
4406 4493 }
@@ -4496,9 +4583,9 @@
4496 4583 <label style="display: block; margin-bottom: 8px; font-weight: 500; color: var(--dashboard-text);">
4497 4584 Effective Threshold
4498 4585 </label>
4499 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);">
4500 - <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>
4501 4588 </div>
4502 4589 <p style="margin: 8px 0 0 0; font-size: 12px; color: var(--dashboard-text-secondary);">
4503 4590 Calculated as: cores × per-core threshold
4504 4591 </p>
@@ -4516,13 +4603,13 @@
4516 4603 <div style="font-size: 18px; font-weight: 600; color: var(--dashboard-text);"><?php echo intval($stats['deferrals']); ?></div>
4517 4604 </div>
4518 4605 <div>
4519 4606 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Max Load Observed</div>
4520 - <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>
4521 4608 </div>
4522 4609 <div>
4523 4610 <div style="font-size: 12px; color: var(--dashboard-text-secondary); margin-bottom: 4px;">Average Load</div>
4524 - <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>
4525 4612 </div>
4526 4613 </div>
4527 4614 </div>
4528 4615
@@ -5229,9 +5316,9 @@
5229 5316 */
5230 5317 public function display_page_builder_notice() {
5231 5318 // Only relevant on the plugin's own settings page — avoid repeating
5232 5319 // this notice across every admin screen.
5233 - if (!isset($_GET['page']) || $_GET['page'] !== self::$page_slug) {
5320 + if (!isset($_GET['page']) || wp_unslash($_GET['page']) !== self::$page_slug) {
5234 5321 return;
5235 5322 }
5236 5323
5237 5324 $configured = Metasync::get_option('general')['default_page_builder'] ?? '';
@@ -5247,9 +5334,9 @@
5247 5334 return;
5248 5335 }
5249 5336
5250 5337 // Handle dismiss action
5251 - 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')) {
5252 5339 update_user_meta(get_current_user_id(), 'metasync_builder_notice_dismissed', '1');
5253 5340 return;
5254 5341 }
5255 5342
@@ -5344,9 +5431,10 @@
5344 5431 * Saves the CPU load threshold and returns statistics
5345 5432 */
5346 5433 public function ajax_save_performance_settings() {
5347 5434 # Check nonce for security and return early if invalid
5348 - 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')) {
5349 5437 wp_send_json_error(array('message' => 'Invalid nonce'));
5350 5438 return;
5351 5439 }
5352 5440
@@ -5414,9 +5502,9 @@
5414 5502 if (!wp_next_scheduled('metasync_cleanup_transients')) {
5415 5503 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_cleanup_transients');
5416 5504
5417 5505 if (!$scheduled) {
5418 - 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
5419 5507 }
5420 5508 }
5421 5509 }
5422 5510
@@ -5427,9 +5515,9 @@
5427 5515 {
5428 5516 $timestamp = wp_next_scheduled('metasync_cleanup_transients');
5429 5517 if ($timestamp) {
5430 5518 wp_unschedule_event($timestamp, 'metasync_cleanup_transients');
5431 - 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
5432 5520 }
5433 5521 }
5434 5522
5435 5523 /**
@@ -5451,12 +5539,10 @@
5451 5539 {
5452 5540 if (!wp_next_scheduled('metasync_hidden_post_check')) {
5453 5541 $scheduled = wp_schedule_event(time(), 'metasync_weekly', 'metasync_hidden_post_check');
5454 5542
5455 - if ($scheduled) {
5456 - error_log('MetaSync: Hidden post manager cron job scheduled successfully (runs every 7 days)');
5457 - } else {
5458 - 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
5459 5545 }
5460 5546 }
5461 5547 }
5462 5548
@@ -5467,12 +5553,10 @@
5467 5553 public function maybe_schedule_otto_recheck_404_cron()
5468 5554 {
5469 5555 if (!wp_next_scheduled('metasync_otto_recheck_404_exclusions')) {
5470 5556 $scheduled = wp_schedule_event(time(), 'metasync_daily_cleanup', 'metasync_otto_recheck_404_exclusions');
5471 - if ($scheduled) {
5472 - error_log('MetaSync: OTTO 404 recheck cron job scheduled successfully (runs daily)');
5473 - } else {
5474 - 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
5475 5559 }
5476 5560 }
5477 5561 }
5478 5562
@@ -5545,13 +5629,13 @@
5545 5629
5546 5630 try {
5547 5631 // 1. Post revisions
5548 5632 if (!empty($settings['clean_post_revisions'])) {
5549 - $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
5550 5634 "DELETE FROM {$wpdb->posts} WHERE post_type = 'revision'"
5551 5635 );
5552 5636 // Remove postmeta left behind by deleted revisions
5553 - $wpdb->query(
5637 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5554 5638 "DELETE pm FROM {$wpdb->postmeta} pm
5555 5639 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
5556 5640 WHERE p.ID IS NULL"
5557 5641 );
@@ -5559,20 +5643,19 @@
5559 5643
5560 5644 // 2. Trashed posts + their postmeta
5561 5645 if (!empty($settings['clean_trashed_posts'])) {
5562 5646 // Collect IDs first to cleanly remove postmeta
5563 - $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
5564 5648 "SELECT ID FROM {$wpdb->posts} WHERE post_status = 'trash'"
5565 5649 );
5566 5650 if (!empty($trashed_ids)) {
5567 - $placeholders = implode(',', array_fill(0, count($trashed_ids), '%d'));
5568 - $wpdb->query(
5651 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5569 5652 $wpdb->prepare(
5570 - "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')) . ')',
5571 5654 $trashed_ids
5572 5655 )
5573 5656 );
5574 - $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
5575 5658 "DELETE FROM {$wpdb->posts} WHERE post_status = 'trash'"
5576 5659 );
5577 5660 } else {
5578 5661 $stats['trashed_posts'] = 0;
@@ -5580,9 +5663,9 @@
5580 5663 }
5581 5664
5582 5665 // 3. Trashed comments
5583 5666 if (!empty($settings['clean_trashed_comments'])) {
5584 - $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
5585 5668 "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'trash'"
5586 5669 );
5587 5670 }
5588 5671
@@ -5587,9 +5670,9 @@
5587 5670 }
5588 5671
5589 5672 // 4. Spam comments
5590 5673 if (!empty($settings['clean_spam_comments'])) {
5591 - $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
5592 5675 "DELETE FROM {$wpdb->comments} WHERE comment_approved = 'spam'"
5593 5676 );
5594 5677 }
5595 5678
@@ -5595,15 +5678,15 @@
5595 5678
5596 5679 // 5. Expired transients — direct SQL, no cache flush
5597 5680 if (!empty($settings['clean_expired_transients'])) {
5598 5681 // Delete timeout rows that have already expired
5599 - $wpdb->query(
5682 + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- bulk site-maintenance operations (revisions/trash/spam/transients/orphans) — no bulk WordPress API exists
5600 5683 "DELETE FROM {$wpdb->options}
5601 5684 WHERE option_name LIKE '\_transient\_timeout\_%'
5602 5685 AND option_value + 0 < UNIX_TIMESTAMP()"
5603 5686 );
5604 5687 // Delete value rows whose timeout row no longer exists
5605 - $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
5606 5689 "DELETE o FROM {$wpdb->options} o
5607 5690 LEFT JOIN {$wpdb->options} t
5608 5691 ON t.option_name = CONCAT('_transient_timeout_', SUBSTRING(o.option_name, 12))
5609 5692 WHERE o.option_name LIKE '\_transient\_%'
@@ -5613,9 +5696,9 @@
5613 5696 }
5614 5697
5615 5698 // 6. Orphaned postmeta (post_id references a post that no longer exists)
5616 5699 if (!empty($settings['clean_orphaned_postmeta'])) {
5617 - $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
5618 5701 "DELETE pm FROM {$wpdb->postmeta} pm
5619 5702 LEFT JOIN {$wpdb->posts} p ON p.ID = pm.post_id
5620 5703 WHERE p.ID IS NULL"
5621 5704 );
@@ -5627,12 +5710,10 @@
5627 5710 $settings['last_run_at'] = time();
5628 5711 $settings['last_run_stats'] = $stats;
5629 5712 update_option('metasync_db_cleanup_settings', $settings);
5630 5713
5631 - error_log('MetaSync: DB cleanup completed — ' . json_encode($stats));
5632 -
5633 5714 } catch (Exception $e) {
5634 - 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
5635 5716 }
5636 5717 }
5637 5718
5638 5719 /**
@@ -5849,10 +5930,11 @@
5849 5930 /**
5850 5931 * AJAX: Save DB cleanup settings and reschedule cron accordingly.
5851 5932 */
5852 5933 public function ajax_save_db_cleanup_settings() {
5934 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce is validated immediately by wp_verify_nonce().
5853 5935 if (!isset($_POST['db_cleanup_settings_nonce']) ||
5854 - !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')) {
5855 5937 wp_send_json_error(array('message' => 'Invalid security token. Please refresh the page and try again.'));
5856 5938 return;
5857 5939 }
5858 5940
@@ -5909,10 +5991,11 @@
5909 5991 * AJAX: Manually trigger the DB cleanup and return stats for the UI.
5910 5992 * Persists current form state first so unsaved checkbox changes are respected.
5911 5993 */
5912 5994 public function ajax_run_db_cleanup() {
5995 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- nonce is validated immediately by wp_verify_nonce().
5913 5996 if (!isset($_POST['db_cleanup_settings_nonce']) ||
5914 - !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')) {
5915 5998 wp_send_json_error(array('message' => 'Invalid security token.'));
5916 5999 return;
5917 6000 }
5918 6001
@@ -5988,35 +6071,8 @@
5988 6071 ));
5989 6072 }
5990 6073
5991 6074 /**
5992 - * Control plugin auto-updates based on user setting
5993 - *
5994 - * @param bool $update Whether to update
5995 - * @param object $item Update offer
5996 - * @return bool Whether to allow auto-update
5997 - */
5998 - public function control_plugin_auto_updates($update, $item)
5999 - {
6000 - # Check if the item object has the slug property
6001 - if (!isset($item->slug)) {
6002 - return $update;
6003 - }
6004 -
6005 - // Check if this is our plugin
6006 - if ($item->slug === 'metasync') {
6007 - $general_settings = Metasync::get_option('general') ?? [];
6008 - $enable_auto_updates = $general_settings['enable_auto_updates'] ?? false;
6009 -
6010 - // Return the user's preference (true = allow auto-updates, false = prevent)
6011 - return $enable_auto_updates === 'true' || $enable_auto_updates === true;
6012 - }
6013 -
6014 - // For other plugins, don't interfere with their auto-update settings
6015 - return $update;
6016 - }
6017 -
6018 - /**
6019 6075 * AJAX handler to add excluded URL for OTTO
6020 6076 */
6021 6077 public function ajax_otto_add_excluded_url()
6022 6078 {
@@ -6162,8 +6218,9 @@
6162 6218 // If page has raw HTML or custom CSS from conversion, show badge
6163 6219 if ($has_raw_html || !empty($has_custom_css)) {
6164 6220 $label = $this->get_html_source_label();
6165 6221 $tooltip = sprintf(
6222 + /* translators: %s: HTML source label. */
6166 6223 __('This page was created using %s HTML-to-Builder converter', 'metasync'),
6167 6224 $label
6168 6225 );
6169 6226
@@ -6216,8 +6273,9 @@
6216 6273
6217 6274 if ($has_raw_html || !empty($has_custom_css)) {
6218 6275 $label = $this->get_html_source_label();
6219 6276 $message = sprintf(
6277 + /* translators: %s: HTML source label. */
6220 6278 __('This page was created using %s HTML-to-Builder converter. The design is preserved with custom CSS and inline styles.', 'metasync'),
6221 6279 '<strong>' . esc_html($label) . '</strong>'
6222 6280 );
6223 6281
@@ -6231,9 +6289,9 @@
6231 6289 <p class="metasync-editor-message">%s</p>
6232 6290 </div>
6233 6291 </div>',
6234 6292 esc_html($label),
6235 - $message
6293 + wp_kses($message, array('strong' => array()))
6236 6294 );
6237 6295 }
6238 6296 }
6239 6297
@@ -6256,9 +6314,9 @@
6256 6314 ?>
6257 6315 <fieldset class="inline-edit-col-left metasync-quick-edit-source">
6258 6316 <div class="inline-edit-col">
6259 6317 <label>
6260 - <span class="title"><?php _e('Source', 'metasync'); ?></span>
6318 + <span class="title"><?php esc_html_e('Source', 'metasync'); ?></span>
6261 6319 <span class="metasync-quick-edit-badge-container"></span>
6262 6320 </label>
6263 6321 </div>
6264 6322 </fieldset>
@@ -6270,8 +6328,9 @@
6270 6328 */
6271 6329 public function add_html_pages_dashboard_widget()
6272 6330 {
6273 6331 $label = $this->get_html_source_label();
6332 + /* translators: %s: HTML source label. */
6274 6333 $widget_title = sprintf(__('%s Pages', 'metasync'), $label);
6275 6334
6276 6335 wp_add_dashboard_widget(
6277 6336 'metasync_html_pages_widget',