array( 'page', 'post', 'product' ), 'include_home' => 1, 'include_sitemap' => 1, 'follow_links' => 1, 'max_depth' => 2, 'max_urls' => 300, 'batch_size' => 5, 'request_delay_ms' => 300, 'request_timeout' => 15, 'time_budget' => 20, 'max_response_kb' => 2048, 'exclude_paths' => "/wp-admin/*\n/wp-login.php*\n/cart/*\n/checkout/*\n/my-account/*\n/feed/*\n*/feed/", 'strip_params' => 'utm_*, fbclid, gclid, msclkid, replytocom, _ga', 'allow_private_self' => 1, 'check_external_links' => 0, 'max_external_links' => 100, 'thin_content_words' => 300, // Plugin monitor. 'plugin_directory_lookup' => 0, // Integrity. 'integrity_wporg_checksums' => 0, 'integrity_include_uploads' => 1, 'integrity_max_file_kb' => 3072, 'integrity_max_files' => 40000, // Schedules & notifications. 'schedule_site' => 'off', 'schedule_integrity' => 'off', 'schedule_hour' => 3, 'notify_enabled' => 0, 'notify_recipients' => '', 'notify_on_complete' => 1, 'notify_threshold' => 'high', 'weekly_summary' => 0, // Data. 'retention_scans' => 10, 'retention_days' => 90, 'delete_on_uninstall' => 0, // Login address (opt-in). 'login_enabled' => 0, 'login_slug' => '', 'login_block_mode' => '404', 'login_log_enabled' => 1, 'login_log_days' => 90, // Login page design (opt-in). 'login_brand_logo' => 'default', 'login_brand_logo_id' => 0, 'login_brand_logo_width' => 84, 'login_brand_bg_color' => '', 'login_brand_bg_image_id' => 0, // Security hardening (opt-in). 'disable_xmlrpc' => 0, 'disable_theme_editor' => 0, 'disable_plugin_editor' => 0, 'limit_login_enabled' => 0, 'limit_login_attempts' => 5, 'limit_login_minutes' => 15, 'header_nosniff' => 0, 'header_frame' => 0, 'header_referrer' => 0, 'block_user_enum' => 0, 'hide_wp_version' => 0, // Forms & comments (opt-in). 'spam_enabled' => 0, 'disable_comments' => 0, // Speed & SEO (opt-in, front end only). 'speed_emoji' => 0, 'speed_embeds' => 0, 'speed_jquery_migrate' => 0, 'speed_dashicons' => 0, 'speed_defer_js' => 0, 'fix_alt_text' => 0, 'fix_meta_description' => 0, ); } /** * All settings merged with defaults. * * @return array */ public static function all() { if ( null === self::$cache ) { $stored = get_option( self::OPTION, array() ); self::$cache = wp_parse_args( is_array( $stored ) ? $stored : array(), self::defaults() ); } return self::$cache; } /** * Get one setting. * * @param string $key Key. * @return mixed */ public static function get( $key ) { $all = self::all(); return isset( $all[ $key ] ) ? $all[ $key ] : null; } /** * Flush the request cache (after updates). */ public static function flush() { self::$cache = null; } /** * Register with the Settings API. */ public static function register() { register_setting( self::GROUP, self::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( __CLASS__, 'sanitize' ), 'default' => self::defaults(), 'show_in_rest' => false, ) ); add_action( 'update_option_' . self::OPTION, static function ( $old_value, $value ) { self::flush(); Cron::ensure_schedules(); if ( is_array( $old_value ) && ! empty( $old_value['limit_login_enabled'] ) && is_array( $value ) && empty( $value['limit_login_enabled'] ) ) { Login_Limiter::clear_all(); } }, 10, 2 ); } /** * Sanitize the whole settings array. Unknown keys are dropped. * * @param mixed $input Raw input. * @return array */ public static function sanitize( $input ) { $input = is_array( $input ) ? $input : array(); $defaults = self::defaults(); $current = self::all(); $out = array(); $int = static function ( $key, $min, $max ) use ( $input, $current ) { $value = isset( $input[ $key ] ) ? (int) $input[ $key ] : (int) $current[ $key ]; return max( $min, min( $max, $value ) ); }; $bool = static function ( $key ) use ( $input ) { return ! empty( $input[ $key ] ) ? 1 : 0; }; $enum = static function ( $key, $allowed ) use ( $input, $defaults ) { $value = isset( $input[ $key ] ) ? sanitize_key( $input[ $key ] ) : ''; return in_array( $value, $allowed, true ) ? $value : $defaults[ $key ]; }; // Post types: only existing public types. $types = array(); if ( isset( $input['post_types'] ) && is_array( $input['post_types'] ) ) { $public = get_post_types( array( 'public' => true ) ); foreach ( $input['post_types'] as $type ) { $type = sanitize_key( $type ); if ( isset( $public[ $type ] ) && 'attachment' !== $type ) { $types[] = $type; } } } $out['post_types'] = array_values( array_unique( $types ) ); $out['include_home'] = $bool( 'include_home' ); $out['include_sitemap'] = $bool( 'include_sitemap' ); $out['follow_links'] = $bool( 'follow_links' ); $out['max_depth'] = $int( 'max_depth', 0, 5 ); $out['max_urls'] = $int( 'max_urls', 1, 5000 ); $out['batch_size'] = $int( 'batch_size', 1, 25 ); $out['request_delay_ms'] = $int( 'request_delay_ms', 0, 5000 ); $out['request_timeout'] = $int( 'request_timeout', 3, 60 ); $out['time_budget'] = $int( 'time_budget', 5, 50 ); $out['max_response_kb'] = $int( 'max_response_kb', 256, 8192 ); $out['exclude_paths'] = self::sanitize_lines( isset( $input['exclude_paths'] ) ? $input['exclude_paths'] : '', 100 ); $out['strip_params'] = implode( ', ', self::sanitize_param_list( isset( $input['strip_params'] ) ? $input['strip_params'] : '' ) ); $out['allow_private_self'] = $bool( 'allow_private_self' ); $out['check_external_links'] = $bool( 'check_external_links' ); $out['max_external_links'] = $int( 'max_external_links', 1, 1000 ); $out['thin_content_words'] = $int( 'thin_content_words', 0, 5000 ); $out['plugin_directory_lookup'] = $bool( 'plugin_directory_lookup' ); $out['integrity_wporg_checksums'] = $bool( 'integrity_wporg_checksums' ); $out['integrity_include_uploads'] = $bool( 'integrity_include_uploads' ); $out['integrity_max_file_kb'] = $int( 'integrity_max_file_kb', 64, 8192 ); $out['integrity_max_files'] = $int( 'integrity_max_files', 1000, 200000 ); $freq = array( 'off', 'daily', 'weekly', 'monthly' ); $out['schedule_site'] = $enum( 'schedule_site', $freq ); $out['schedule_integrity'] = $enum( 'schedule_integrity', $freq ); $out['schedule_hour'] = $int( 'schedule_hour', 0, 23 ); $out['notify_enabled'] = $bool( 'notify_enabled' ); $out['notify_recipients'] = implode( ', ', self::sanitize_emails( isset( $input['notify_recipients'] ) ? $input['notify_recipients'] : '' ) ); $out['notify_on_complete'] = $bool( 'notify_on_complete' ); $out['notify_threshold'] = $enum( 'notify_threshold', array( 'critical', 'high', 'medium', 'never' ) ); $out['weekly_summary'] = $bool( 'weekly_summary' ); $out['retention_scans'] = $int( 'retention_scans', 1, 100 ); $out['retention_days'] = $int( 'retention_days', 1, 3650 ); $out['delete_on_uninstall'] = $bool( 'delete_on_uninstall' ); $out['login_log_enabled'] = $bool( 'login_log_enabled' ); $out['login_log_days'] = $int( 'login_log_days', 1, 365 ); $out['login_brand_logo'] = $enum( 'login_brand_logo', array( 'default', 'site', 'custom' ) ); $out['login_brand_logo_id'] = Login_Branding::sanitize_image_id( isset( $input['login_brand_logo_id'] ) ? $input['login_brand_logo_id'] : 0 ); $out['login_brand_logo_width'] = $int( 'login_brand_logo_width', 40, 320 ); $out['login_brand_bg_color'] = isset( $input['login_brand_bg_color'] ) ? (string) sanitize_hex_color( trim( (string) wp_unslash( $input['login_brand_bg_color'] ) ) ) : ''; $out['login_brand_bg_image_id'] = Login_Branding::sanitize_image_id( isset( $input['login_brand_bg_image_id'] ) ? $input['login_brand_bg_image_id'] : 0 ); if ( 'custom' === $out['login_brand_logo'] && ! $out['login_brand_logo_id'] && 'login' === ( isset( $input['_tab'] ) ? $input['_tab'] : '' ) ) { $out['login_brand_logo'] = 'default'; if ( function_exists( 'add_settings_error' ) ) { add_settings_error( self::OPTION, 'siteaudithub_login_logo', __( 'Choose an image for the custom login logo. The WordPress logo is shown until you do.', 'siteaudithub' ), 'warning' ); } } $out['disable_xmlrpc'] = $bool( 'disable_xmlrpc' ); $out['disable_theme_editor'] = $bool( 'disable_theme_editor' ); $out['disable_plugin_editor'] = $bool( 'disable_plugin_editor' ); $out['limit_login_enabled'] = $bool( 'limit_login_enabled' ); $out['limit_login_attempts'] = $int( 'limit_login_attempts', 2, 20 ); $out['limit_login_minutes'] = $int( 'limit_login_minutes', 1, 1440 ); $out['header_nosniff'] = $bool( 'header_nosniff' ); $out['header_frame'] = $bool( 'header_frame' ); $out['header_referrer'] = $bool( 'header_referrer' ); $out['block_user_enum'] = $bool( 'block_user_enum' ); $out['hide_wp_version'] = $bool( 'hide_wp_version' ); $out['spam_enabled'] = $bool( 'spam_enabled' ); $out['disable_comments'] = $bool( 'disable_comments' ); foreach ( array( 'speed_emoji', 'speed_embeds', 'speed_jquery_migrate', 'speed_dashicons', 'speed_defer_js', 'fix_alt_text', 'fix_meta_description' ) as $key ) { $out[ $key ] = $bool( $key ); } // Custom login address (validated strictly; never enabled with an unsafe slug). $out['login_block_mode'] = $enum( 'login_block_mode', array( '404', 'home' ) ); $out['login_slug'] = isset( $input['login_slug'] ) ? sanitize_title( strtolower( (string) wp_unslash( $input['login_slug'] ) ) ) : (string) $current['login_slug']; $out['login_enabled'] = $bool( 'login_enabled' ); if ( $out['login_enabled'] ) { $problem = ''; if ( is_multisite() ) { $problem = __( 'The custom login address is not available on multisite networks in this version.', 'siteaudithub' ); } elseif ( Login_Guard::conflicts() ) { /* translators: %s: plugin names */ $problem = sprintf( __( 'Another plugin already changes the login address (%s). Use only one of them.', 'siteaudithub' ), implode( ', ', Login_Guard::conflicts() ) ); } else { $check = Login_Guard::check_slug( $out['login_slug'] ); if ( true !== $check ) { $problem = $check; } } if ( '' !== $problem ) { $out['login_enabled'] = 0; if ( function_exists( 'add_settings_error' ) ) { add_settings_error( self::OPTION, 'siteaudithub_login_slug', $problem . ' ' . __( 'The custom login address was not enabled.', 'siteaudithub' ), 'error' ); } } } // Forms are split into tabs; keep values that belong to other tabs untouched. $tab = isset( $input['_tab'] ) ? sanitize_key( $input['_tab'] ) : ''; $map = self::tab_keys(); if ( isset( $map[ $tab ] ) ) { foreach ( array_keys( $out ) as $key ) { if ( ! in_array( $key, $map[ $tab ], true ) && isset( $current[ $key ] ) ) { $out[ $key ] = $current[ $key ]; } } } self::$cache = null; return $out; } /** * Which keys each settings tab owns. * * @return array */ public static function tab_keys() { return array( 'scanning' => array( 'post_types', 'include_home', 'include_sitemap', 'follow_links', 'max_depth', 'max_urls', 'batch_size', 'request_delay_ms', 'request_timeout', 'time_budget', 'max_response_kb', 'exclude_paths', 'strip_params', 'allow_private_self', 'check_external_links', 'max_external_links', 'thin_content_words' ), 'integrity' => array( 'integrity_wporg_checksums', 'integrity_include_uploads', 'integrity_max_file_kb', 'integrity_max_files', 'plugin_directory_lookup' ), 'notifications' => array( 'schedule_site', 'schedule_integrity', 'schedule_hour', 'notify_enabled', 'notify_recipients', 'notify_on_complete', 'notify_threshold', 'weekly_summary' ), 'data' => array( 'retention_scans', 'retention_days', 'delete_on_uninstall' ), 'login' => array( 'login_enabled', 'login_slug', 'login_block_mode', 'login_log_enabled', 'login_log_days', 'login_brand_logo', 'login_brand_logo_id', 'login_brand_logo_width', 'login_brand_bg_color', 'login_brand_bg_image_id' ), 'forms' => array( 'spam_enabled', 'disable_comments' ), 'speed' => array( 'speed_emoji', 'speed_embeds', 'speed_jquery_migrate', 'speed_dashicons', 'speed_defer_js', 'fix_alt_text', 'fix_meta_description' ), 'security' => array( 'disable_xmlrpc', 'disable_theme_editor', 'disable_plugin_editor', 'limit_login_enabled', 'limit_login_attempts', 'limit_login_minutes', 'header_nosniff', 'header_frame', 'header_referrer', 'block_user_enum', 'hide_wp_version' ), ); } /** * Sanitize a newline list of path patterns. * * @param string $raw Raw. * @param int $limit Max lines. * @return string */ public static function sanitize_lines( $raw, $limit ) { $lines = preg_split( '/[\r\n]+/', (string) $raw ); $clean = array(); foreach ( $lines as $line ) { $line = trim( sanitize_text_field( $line ) ); if ( '' !== $line && strlen( $line ) <= 300 ) { $clean[] = $line; } if ( count( $clean ) >= $limit ) { break; } } return implode( "\n", array_unique( $clean ) ); } /** * Sanitize a comma list of query parameter names (allows * wildcard). * * @param string $raw Raw. * @return array */ public static function sanitize_param_list( $raw ) { $out = array(); foreach ( explode( ',', (string) $raw ) as $param ) { $param = strtolower( trim( $param ) ); if ( '' !== $param && preg_match( '/^[a-z0-9_\-\.\[\]\*]{1,60}$/', $param ) ) { $out[] = $param; } } return array_slice( array_unique( $out ), 0, 50 ); } /** * Sanitize a comma list of emails. * * @param string $raw Raw. * @return array */ public static function sanitize_emails( $raw ) { $out = array(); foreach ( preg_split( '/[\s,;]+/', (string) $raw ) as $email ) { $email = sanitize_email( $email ); if ( $email && is_email( $email ) ) { $out[] = $email; } } return array_slice( array_unique( $out ), 0, 10 ); } /** * Effective notification recipients (falls back to the admin email). * * @return array */ public static function recipients() { $list = self::sanitize_emails( self::get( 'notify_recipients' ) ); if ( empty( $list ) ) { $admin = get_option( 'admin_email' ); if ( is_email( $admin ) ) { $list[] = $admin; } } return $list; } }