*/ private const TWEAK_CATEGORIES = array( 'disable-xml-rpc' => 'network', 'wp-version' => 'updates', 'hide-error' => 'error', 'php-version' => 'updates', 'replace-admin-username' => 'auth', 'security-key' => 'updates', 'login-duration' => 'auth', 'disable-trackback' => 'network', 'prevent-enum-users' => 'error', 'disable-file-editor' => 'access', 'protect-information' => 'error', 'prevent-php-executed' => 'access', ); /** * Initializes the model and service, registers routes, and sets up scheduled events if the model is active. */ public function __construct() { $this->register_page( esc_html__( 'Hardening', 'defender-security' ), $this->slug, array( $this, 'main_view' ), $this->parent_slug ); $this->model = wd_di()->get( Model_Security_Tweaks::class ); $this->register_routes(); // Init all the tweaks, should happen one time. $this->component_instances = $this->init_tweaks(); $this->security_key = $this->component_instances['security-key']; $this->prevent_enum_users = $this->component_instances['prevent-enum-users']; // Now shield up. $this->boot(); // Add addition hooks. add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) ); add_action( 'wp_loaded', array( $this, 'should_output_error' ) ); } /** * Dummy function for testing a check. */ public function should_output_error() { $test_err_reporting = defender_get_data_from_request( 'defender_test_error_reporting', 'g' ); if ( ! is_string( $test_err_reporting ) || '' === trim( $test_err_reporting ) ) { return; } // It should be triggered only by admin. if ( ! $this->check_permission() ) { return; } // This should output a warning. Ignored by PHPCS as it's a test. // @phpstan-ignore-next-line. echo $undefined_variable; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Intentionally outputs an undefined variable to test warning handling. exit(); } /** * Process. * * @param Request $request Request object. * * @return Response * @defender_route */ public function process( Request $request ) { $data = $request->get_data( array( 'slug' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), 'current_server' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), 'username' => array( 'type' => 'string', 'sanitize' => 'sanitize_user', ), ) ); $slug = $data['slug'] ?? false; $tweak = $this->get_tweak( $slug ); if ( ! is_object( $tweak ) ) { return new Response( false, array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) ) ); } if ( 'prevent-enum-users' === $slug ) { $pe_data = $request->get_data(); $enabled_enums = $pe_data['enabled_user_enums'] ?? null; $all_enums = array( 'enum-rest', 'enum-oembed', 'enum-sitemap' ); $this->prevent_enum_users->set_enabled_user_enums( is_array( $enabled_enums ) ? $enabled_enums : $all_enums ); } if ( 'security-key' === $slug ) { $sk_data = $request->get_data(); $remind_date = sanitize_text_field( $sk_data['remind_date'] ?? '' ); if ( $remind_date ) { $is_autogen = filter_var( $sk_data['is_autogenerate_keys'] ?? false, FILTER_VALIDATE_BOOLEAN ); $values = get_site_option( 'defender_security_tweaks_' . $this->security_key->slug, array() ); $values['reminder_duration'] = $remind_date; $values['reminder_date'] = strtotime( '+' . $remind_date, time() ); $values['is_autogenerate_keys'] = $is_autogen; update_site_option( 'defender_security_tweaks_' . $this->security_key->slug, $values ); if ( true === $is_autogen ) { $this->security_key->cron_schedule(); } else { $this->security_key->cron_unschedule(); } } } if ( 'replace-admin-username' === $slug && isset( $data['username'] ) && '' !== trim( $data['username'] ) ) { $_POST['username'] = $data['username']; } if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) { $current_server = $data['current_server'] ?? false; if ( ! $current_server ) { return new Response( false, array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) ) ); } $ret = $tweak->process( $current_server ); } else { $ret = $tweak->process(); } if ( true === $ret ) { Config_Hub_Helper::set_clear_active_flag(); $this->model->mark( self::STATUS_RESOLVE, $slug ); // Track. $this->track_tweak( $tweak->get_label(), 'Actioned' ); // Response. $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ) ); } if ( $ret instanceof Response && true === $ret->status ) { Config_Hub_Helper::set_clear_active_flag(); $this->model->mark( self::STATUS_RESOLVE, $slug ); $this->track_tweak( $tweak->get_label(), 'Actioned' ); return $ret; } if ( is_wp_error( $ret ) ) { $this->ajax_response( $ret->get_error_message(), false ); } return new Response( false, array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) ) ); } /** * Revert. * * @param Request $request Request object. * * @return Response * @defender_route */ public function revert( Request $request ) { $data = $request->get_data( array( 'slug' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), 'current_server' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), ) ); $slug = $data['slug'] ?? false; $tweak = $this->get_tweak( $slug ); $invalid = array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) ); if ( ! is_object( $tweak ) ) { return new Response( false, $invalid ); } if ( in_array( $slug, array( 'prevent-php-executed', 'protect-information' ), true ) ) { $current_server = $data['current_server'] ?? false; if ( ! $current_server ) { return new Response( false, $invalid ); } $ret = $tweak->revert( $current_server ); } else { $ret = $tweak->revert(); } if ( is_wp_error( $ret ) ) { $this->ajax_response( $ret->get_error_message(), false ); } if ( true === $ret ) { Config_Hub_Helper::set_clear_active_flag(); $this->model->mark( self::STATUS_ISSUES, $slug ); // Track. $this->track_tweak( $tweak->get_label(), 'Reverted' ); // Response. $this->ajax_response( esc_html__( 'Security recommendation successfully reverted.', 'defender-security' ) ); } return new Response( false, $invalid ); } /** * Ignore. * * @param Request $request Request object. * * @return Response|void * @defender_route */ public function ignore( Request $request ) { $data = $request->get_data( array( 'slug' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), ) ); $slug = $data['slug'] ?? false; $tweak = $this->get_tweak( $slug ); if ( ! is_object( $tweak ) ) { return new Response( false, array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) ) ); } $this->model->mark( self::STATUS_IGNORE, $slug ); // Track. $this->track_tweak( $tweak->get_label(), 'Ignored' ); if ( 'security-key' === $slug ) { $this->security_key->cron_unschedule(); } $this->ajax_response( esc_html__( 'Security recommendation successfully ignored.', 'defender-security' ) ); } /** * Restore. * * @param Request $request Request object. * * @return Response|void * @defender_route */ public function restore( Request $request ) { $data = $request->get_data( array( 'slug' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), ) ); $slug = $data['slug'] ?? false; $tweak = $this->get_tweak( $slug ); if ( ! is_object( $tweak ) ) { return new Response( false, array( 'message' => esc_html__( 'Invalid request', 'defender-security' ) ) ); } $this->model->mark( self::STATUS_RESTORE, $slug ); // Track. $this->track_tweak( $tweak->get_label(), 'Restored' ); if ( $this->security_key->get_is_autogenerate_keys() ) { $this->security_key->cron_schedule(); } $this->ajax_response( esc_html__( 'Security recommendation successfully restored.', 'defender-security' ) ); } /** * Recheck. * * @param Request $request Request object. * * @return Response * @defender_route */ public function recheck( Request $request ): Response { $data = $request->get_data( array( 'slug' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), ) ); $slug = $data['slug'] ?? false; $tweak = $this->get_tweak( $slug ); if ( ! is_object( $tweak ) ) { return new Response( false, array( 'message' => esc_html__( 'The status cannot be verified as the request contains an invalid slug.', 'defender-security' ), ) ); } $ret = $tweak->check(); if ( true === $ret ) { $this->ajax_response( esc_html__( 'Security recommendation successfully resolved.', 'defender-security' ), true, 1 ); } if ( is_wp_error( $ret ) ) { return new Response( false, array( 'message' => $ret->get_error_message() ) ); } $msg = sprintf( /* translators: %s: Tweak title. */ esc_html__( '%s is not actioned. Please ensure that all the instructions are followed.', 'defender-security' ), $tweak->get_label() ?? '' ); return new Response( false, array( 'message' => $msg ) ); } /** * Update security reminder. * * @param Request $request Request object. * * @return Response * @defender_route */ public function update_security_reminder( Request $request ): Response { $data = $request->get_data(); $remind_date = $data['remind_date'] ?? false; $is_autogen_flag = isset( $data['is_autogenerate_keys'] ) ? filter_var( $data['is_autogenerate_keys'], FILTER_VALIDATE_BOOLEAN ) : false; if ( ! $remind_date ) { return new Response( false, array( 'message' => esc_html__( 'Invalid Reminder frequency', 'defender-security' ) ) ); } $values = array( 'reminder_duration' => $remind_date, 'reminder_date' => strtotime( '+' . $remind_date, time() ), 'is_autogenerate_keys' => $is_autogen_flag, ); if ( update_site_option( 'defender_security_tweaks_' . $this->security_key->slug, $values ) ) { if ( true === $is_autogen_flag ) { $this->security_key->cron_schedule(); } else { $this->security_key->cron_unschedule(); } return new Response( true, array( 'message' => esc_html__( 'Security recommendation successfully updated.', 'defender-security' ) ) ); } else { return new Response( false, array( 'message' => esc_html__( 'Error while updating.', 'defender-security' ) ) ); } } /** * Returns tweaks for a given status formatted for the React frontend. * * Adds `category` and remaps the backend status string to the value the * frontend HARDENING_STATUS constants expect ('issue', 'resolved', 'ignored'). * * @param string $status One of STATUS_ISSUES, STATUS_RESOLVE, STATUS_IGNORE. * * @return array */ private function format_tweaks_for_frontend( string $status ): array { $status_map = array( self::STATUS_ISSUES => 'issue', self::STATUS_RESOLVE => 'resolved', self::STATUS_IGNORE => 'ignored', ); $tweaks = $this->init_tweaks( $status, 'array' ); $frontend_status = $status_map[ $status ] ?? $status; foreach ( $tweaks as &$tweak ) { $tweak['status'] = $frontend_status; $tweak['category'] = self::TWEAK_CATEGORIES[ $tweak['slug'] ] ?? 'access'; } unset( $tweak ); return $tweaks; } /** * AJAX Response handler. * * @param string $message The message to be displayed in the response. * @param bool $is_success Whether the response should have a success status. * @param bool|int $interval The cron interval for the security key autogeneration. Default is false. * * @return Response The AJAX response. */ private function ajax_response( $message, $is_success = true, $interval = false ): Response { global $wp_version; // Creating a new Model_Security_Tweaks() here risks reading stale data from the // DB when the WP options cache hasn't flushed the write yet. $data = array( 'message' => $message, 'summary' => array( 'issues_count' => count( $this->model->issues ), 'fixed_count' => count( $this->model->fixed ), 'ignore_count' => count( $this->model->ignore ), 'php_version' => PHP_VERSION, 'wp_version' => $wp_version, ), 'issues' => $this->build_tweaks_response( $this->model->issues, 'issue' ), 'fixed' => $this->build_tweaks_response( $this->model->fixed, 'resolved' ), 'ignored' => $this->build_tweaks_response( $this->model->ignore, 'ignored' ), 'issues_slugs' => $this->model->issues, ); if ( $interval ) { $data['interval'] = $interval; } return new Response( $is_success, $data ); } /** * Builds the frontend-shaped tweak array from a list of slugs. * * Uses the in-memory tweak object cache rather than re-querying the model, * so the result is always consistent with the state after mark() was called. * * @param array $slugs Slugs to include. * @param string $frontend_status Frontend status string ('issue', 'resolved', 'ignored'). * * @return array */ private function build_tweaks_response( array $slugs, string $frontend_status ): array { $tweaks = Array_Cache::get( 'tweaks', 'tweaks' ); $result = array(); foreach ( $slugs as $slug ) { if ( ! isset( $tweaks[ $slug ] ) ) { continue; } $arr = $tweaks[ $slug ]->to_array(); $arr['status'] = $frontend_status; $arr['category'] = self::TWEAK_CATEGORIES[ $slug ] ?? 'access'; $result[ $slug ] = $arr; } return $result; } /** * Enqueues scripts and styles for this page. * Only enqueues assets if the page is active. */ public function enqueue_assets() { if ( ! $this->is_page_active() ) { return; } $handle = 'defender-ui-hardening'; wp_enqueue_script( $handle, WP_DEFENDER_BASE_URL . 'assets/js/hardening-ui.js', array( 'def-vue', 'def-manifest', 'def-core-ui', 'defender', 'wp-i18n' ), DEFENDER_VERSION, true ); wp_set_script_translations( $handle, 'wpdef' ); wp_localize_script( $handle, 'defenderUIData', array_merge( $this->get_shared_data(), $this->data_frontend() ) ); wp_enqueue_style( $handle, WP_DEFENDER_BASE_URL . 'assets/css/core-ui.css', array(), DEFENDER_VERSION ); $this->enqueue_main_assets(); } /** * Provides data for the dashboard widget. * * @return array */ public function dashboard_widget(): array { $tweak_arr = $this->model->get_tweak_types(); $data = array(); foreach ( $this->init_tweaks( self::STATUS_ISSUES ) as $slug => $tweak ) { $data[] = array( 'title' => $tweak->get_label(), 'slug' => $slug, ); } return array( 'summary' => array( 'fixed_count' => $tweak_arr['count_fixed'], 'ignore_count' => $tweak_arr['count_ignored'], 'issues_count' => $tweak_arr['count_issues'], ), 'issues' => $data, ); } /** * Provides data for the frontend. * * @return array An array of data for the frontend. */ public function data_frontend(): array { $this->refresh_tweaks_status(); global $wp_version; $not_allowed_bulk = array( 'php-version', 'replace-admin-username', ); if ( 'nginx' === Server::get_current_server() ) { $not_allowed_bulk[] = 'protect-information'; $not_allowed_bulk[] = 'prevent-php-executed'; } $tweak_arr = $this->model->get_tweak_types(); $hardening_data = array( 'summary' => array( 'fixed_count' => $tweak_arr['count_fixed'], 'ignore_count' => $tweak_arr['count_ignored'], 'issues_count' => $tweak_arr['count_issues'], 'php_version' => PHP_VERSION, 'wp_version' => $wp_version, ), 'issues' => $this->format_tweaks_for_frontend( self::STATUS_ISSUES ), 'fixed' => $this->format_tweaks_for_frontend( self::STATUS_RESOLVE ), 'ignored' => $this->format_tweaks_for_frontend( self::STATUS_IGNORE ), 'not_allowed_bulk' => $not_allowed_bulk, 'is_autogenerate_keys' => $this->security_key->get_is_autogenerate_keys(), 'reminder_frequencies' => $this->security_key->reminder_frequencies(), 'enabled_user_enums' => $this->prevent_enum_users->get_enabled_user_enums(), ); return array( 'hardening' => array_merge( $hardening_data, $this->dump_routes_and_nonces() ), ); } /** * Renders the main view for this page. */ public function main_view() { $this->render( 'main' ); } /** * Bulk action for security tweaks. * Handles bulk resolving, ignoring, or reverting security tweaks. * * @param Request $request The request object. * * @return Response * @defender_route */ public function bulk_action( Request $request ) { $data = $request->get_data( array( 'slugs' => array( 'type' => 'array', 'sanitize' => 'sanitize_text_field', ), 'intention' => array( 'type' => 'string', 'sanitize' => 'sanitize_text_field', ), ) ); $slugs = $data['slugs'] ?? array(); $intention = $data['intention'] ?? false; // Get processed and unprocessed tweaks. [ $processed, $unprocessed, $error_message ] = $this->security_tweaks_auto_action( $slugs, $intention ); if ( null !== $error_message ) { return $this->ajax_response( $error_message, false ); } $message = sprintf( /* translators: 1: Either ignored or resolved, 2: Count security recommendations */ esc_html__( 'You have bulk %1$s %2$s security recommendations.', 'defender-security' ), 'ignore' === $intention ? 'ignored' : 'resolved', $processed ); if ( isset( $unprocessed ) && $unprocessed > 0 ) { // If we have this case this mean the intention is resolved. $message = sprintf( /* translators: %d: Count security tweaks */ esc_html__( 'You have bulk actioned %d security recommendations. You still have a few unresolved security recommendations, which cannot be bulk actioned automatically, so please address them below.', 'defender-security' ), $processed ); Config_Hub_Helper::set_clear_active_flag(); } return $this->ajax_response( $message ); } /** * Mass processing. * * @param array $slugs Tweak slugs. * @param string $intention Status. * * @return array */ public function security_tweaks_auto_action( $slugs, $intention ): array { $processed = 0; $unprocessed = 0; $error_message = null; foreach ( $slugs as $slug ) { $tweak = $this->get_tweak( $slug ); if ( 'ignore' === $intention ) { $this->model->mark( self::STATUS_IGNORE, $slug ); // Track. $this->track_tweak( $tweak->get_label(), 'Ignored' ); } elseif ( 'resolve' === $intention ) { $wont_do = array( 'replace-admin-username', 'prevent-php-executed', 'wp-version', 'php-version', 'protect-information', ); if ( in_array( $slug, $wont_do, true ) ) { ++$unprocessed; continue; } if ( $tweak->has_method( 'bulk_process' ) ) { $ret = $tweak->bulk_process(); } else { $ret = $tweak->process(); } if ( is_wp_error( $ret ) ) { $error_message = sprintf( /* translators: 1: Security tweak title, 2: Error message */ esc_html__( 'There is an error while processing recommendation %1$s, error message: %2$s', 'defender-security' ), $tweak->get_label(), $ret->get_error_message() ); ++$unprocessed; // Don't mark a failed tweak as resolved; stop on first error (same as original behavior). break; } $this->model->mark( self::STATUS_RESOLVE, $slug ); // Track. $this->track_tweak( $tweak->get_label(), 'Actioned' ); } elseif ( 'restore' === $intention ) { // New bulk action since V6. $this->model->mark( self::STATUS_ISSUES, $slug ); // Track. $this->track_tweak( $tweak->get_label(), 'Restored' ); } ++$processed; } return array( $processed, $unprocessed, $error_message ); } /** * Refresh the tweak status and save their state. * * @return void */ public function refresh_tweaks_status() { $tweaks = $this->init_tweaks(); $settings = new Model_Security_Tweaks(); $fixed = array(); $issues = array(); foreach ( $tweaks as $slug => $class ) { if ( $settings->is_tweak_ignore( $slug ) ) { continue; } $is_resolved = $class->check(); if ( $is_resolved ) { $fixed[] = $slug; } else { $issues[] = $slug; } } $settings->fixed = $fixed; $settings->issues = $issues; $settings->save(); } /** * This function for shield every active tweaks up, we will use the cached result. * No check function trigger in this init runtime. */ private function boot() { $tweaks = $this->init_tweaks( self::STATUS_RESOLVE ); foreach ( $tweaks as $tweak ) { $tweak->shield_up(); } } /** * Instance all the tweaks, happen one time in init runtime. * * @param string $type Type of tweaks. * @param string $format Object for internal use, array for frontend use. * * @return array */ public function init_tweaks( $type = '', $format = 'object' ): array { $classes = array( Disable_XML_RPC::class, WP_Version::class, Hide_Error::class, PHP_Version::class, Change_Admin::class, Security_Key::class, Login_Duration::class, Disable_Trackback::class, Prevent_Enum_Users::class, Disable_File_Editor::class, ); if ( ! defender_is_wp_cli() ) { // We don't load this in cli, as clearly no server is running. $classes = array_merge( $classes, array( Protect_Information::class, Prevent_PHP::class, ) ); } $tweaks = Array_Cache::get( 'tweaks', 'tweaks' ); if ( ! is_array( $tweaks ) ) { foreach ( $classes as $class ) { $obj = new $class(); $tweaks[ $obj->slug ] = $obj; } Array_Cache::set( 'tweaks', $tweaks, 'tweaks' ); } $tmp = array(); if ( ! is_string( $type ) || '' === trim( $type ) ) { $tmp = $tweaks; } else { $settings = new Model_Security_Tweaks(); try { $compare = $settings->get_collection( $type ); } catch ( \InvalidArgumentException $e ) { $compare = array(); } foreach ( $compare as $slug ) { if ( isset( $tweaks[ $slug ] ) ) { $tmp[ $slug ] = $tweaks[ $slug ]; } } } if ( 'array' === $format ) { // We need to parse this as array. foreach ( $tmp as $slug => $obj ) { $arr = $obj->to_array(); $arr['status'] = $type; $tmp[ $slug ] = $arr; } } return $tmp; } /** * Get tweak object from cache by slug. * * @param string $slug Tweak slug. * * @return mixed Tweak object if exists, else null. */ private function get_tweak( $slug ) { $tweaks = Array_Cache::get( 'tweaks', 'tweaks' ); return $tweaks[ $slug ] ?? null; } /** * Converts the current object state to an array. * * @return array The array representation of the object. */ public function to_array(): array { $this->refresh_tweaks_status(); $settings = new Model_Security_Tweaks(); return array( 'count' => array( 'issues' => count( $settings->issues ), 'resolved' => count( $settings->fixed ), 'total' => count( $this->init_tweaks() ), ), ); } /** * Removes settings for all submodules. */ public function remove_settings() { // Revert it first. $tweaks = $this->init_tweaks( self::STATUS_RESOLVE ); // Assign this so internal can use the current server. $_POST['current_server'] = Server::get_current_server(); foreach ( $tweaks as $tweak ) { $tweak->revert(); if ( method_exists( $tweak, 'delete_all_option' ) ) { $tweak->delete_all_option(); } } ( new Model_Security_Tweaks() )->delete(); delete_site_transient( Server::CACHE_CURRENT_SERVER ); delete_site_transient( \WP_Defender\Component\Security_Tweaks\Servers\Apache::CACHE_APACHE_VERSION ); $this->security_key->cron_unschedule(); } /** * Delete all the data & the cache. */ public function remove_data() { // Remove cached data. Array_Cache::remove( 'tweaks', 'tweaks' ); delete_site_option( Security_Key::REGENERATE_SALT_NEXT_RUN_OPTION ); } /** * Automate the process of resolving, reverting, and ignoring security tweaks based on the provided data. * * @param array $data The data containing information about fixed, issues, and ignored security tweaks. * @param string $request_reason The reason for the request, e.g., 'hub'. * * @return mixed Whether a reauthentication is needed after processing the tweaks. * @since 2.8.1 Add $request_reason param. If there's a request from Hub the plugin doesn't send the error message. */ public function automate( $data, $request_reason ) { $this->refresh_tweaks_status(); $need_reauth = false; // Resolve tweaks. if ( isset( $data['fixed'] ) && is_array( $data['fixed'] ) && array() !== $data['fixed'] ) { // There are some tweak that need manual apply, as files based, or change admin. $manual_done = array( 'replace-admin-username', 'prevent-php-executed', 'wp-version', 'php-version', 'protect-information', ); if ( 'hub' === $request_reason ) { $manual_done[] = 'security-key'; } $diff_keys = array_diff( $data['fixed'], $this->model->fixed, $manual_done ); if ( array() !== $diff_keys ) { foreach ( $diff_keys as $slug ) { $tweak = $this->get_tweak( $slug ); if ( $tweak->has_method( 'bulk_process' ) ) { $ret = $tweak->bulk_process(); } else { $ret = $tweak->process(); } if ( is_wp_error( $ret ) ) { if ( 'hub' === $request_reason ) { continue; } return sprintf( /* translators: 1: Security tweak title, 2: Error message */ esc_html__( 'There is an error while processing recommendation %1$s, error message: %2$s', 'defender-security' ), $tweak->get_label(), $ret->get_error_message() ); } $this->model->mark( self::STATUS_RESOLVE, $slug ); } if ( in_array( 'security-key', $diff_keys, true ) ) { $need_reauth = true; } } } // Revert tweaks. if ( isset( $data['issues'] ) && is_array( $data['issues'] ) && array() !== $data['issues'] ) { $diff_keys = array_diff( $data['issues'], $this->model->issues ); if ( array() !== $diff_keys ) { // Issues. foreach ( $diff_keys as $slug ) { $tweak = $this->get_tweak( $slug ); $ret = $tweak->revert(); if ( is_wp_error( $ret ) ) { if ( 'hub' === $request_reason ) { continue; } return sprintf( /* translators: 1: Security tweak title, 2: Error message */ esc_html__( 'There is an error while processing recommendation %1$s, error message: %2$s', 'defender-security' ), $tweak->get_label(), $ret->get_error_message() ); } $this->model->mark( self::STATUS_ISSUES, $slug ); } } } // Ignore tweaks. if ( isset( $data['ignore'] ) && is_array( $data['ignore'] ) && array() !== $data['ignore'] ) { $diff_keys = array_diff( $data['ignore'], $this->model->ignore ); if ( array() !== $diff_keys ) { foreach ( $diff_keys as $slug ) { $this->model->mark( self::STATUS_IGNORE, $slug ); } } } return $need_reauth; } /** * Imports data into the model. * * @param array $data Data to be imported into the model. */ public function import_data( array $data ) { $enabled_user_enums = array(); if ( isset( $data['enabled_user_enums'] ) ) { $enabled_user_enums = (array) $data['enabled_user_enums']; unset( $data['enabled_user_enums'] ); } $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums ); if ( isset( $data['security_key'] ) && is_array( $data['security_key'] ) && array() !== $data['security_key'] ) { $this->security_key->update_all_option( $data['security_key'] ); } $model = new Model_Security_Tweaks(); $model->import( $data ); if ( $model->validate() ) { $model->save(); } } /** * Exports strings. * * @return array An array of strings. */ public function export_strings(): array { $this->refresh_tweaks_status(); $settings = new Model_Security_Tweaks(); $strings = array(); $count_all = count( $settings->fixed ) + count( $settings->issues ) + count( $settings->ignore ); if ( ! is_array( $settings->issues ) || array() === $settings->issues ) { $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' ); } else { $strings[] = sprintf( /* translators: 1: Total security tweaks activated, 2: Total security tweaks */ esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ), count( $settings->fixed ), $count_all ); } return $strings; } /** * Config strings. * * @param array $config Settings. * * @return array Strings to be displayed in the settings page. */ public function config_strings( $config ): array { $strings = array(); if ( ! isset( $config['issues'] ) || ! ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable ) || 0 === count( $config['issues'] ) ) { $strings[] = esc_html__( 'All available recommendations activated', 'defender-security' ); } else { $strings[] = sprintf( /* translators: 1: Total security tweaks activated, 2: Total security tweaks */ esc_html__( '%1$d/%2$d recommendations activated', 'defender-security' ), is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0, ( is_array( $config['fixed'] ) || $config['fixed'] instanceof Countable ? count( $config['fixed'] ) : 0 ) + ( is_array( $config['issues'] ) || $config['issues'] instanceof Countable ? count( $config['issues'] ) : 0 ) + ( is_array( $config['ignore'] ) || $config['ignore'] instanceof Countable ? count( $config['ignore'] ) : 0 ) ); } if ( 'enabled' === $config['notification'] ) { $strings[] = esc_html__( 'Email notifications active', 'defender-security' ); } return $strings; } /** * Update auto generate flag. * * @param Request $request Request object. * * @defender_route */ public function update_autogenerate_flag( Request $request ): Response { $data = $request->get_data(); $is_autogen_flag = isset( $data['is_autogenerate_keys'] ) ? filter_var( $data['is_autogenerate_keys'], FILTER_VALIDATE_BOOLEAN ) : false; $is_success = false; $message = esc_html__( 'An error occurred, try again.', 'defender-security' ); if ( $this->security_key->set_is_autogenrate_keys( $is_autogen_flag ) ) { $is_success = true; if ( $is_autogen_flag ) { $this->security_key->cron_schedule(); $message = esc_html__( 'Security key/salt autogenerate enabled.', 'defender-security' ); } else { $this->security_key->cron_unschedule(); $message = esc_html__( 'Security key/salt autogenerate disabled.', 'defender-security' ); } } return new Response( $is_success, array( 'message' => $message ) ); } /** * Get component security key instance. * * @return Security_Key */ public function get_security_key() { return $this->security_key; } /** * Update enabled user enums list. * * @param Request $request Request object. * * @return Response * @defender_route */ public function update_enabled_user_enums( Request $request ): Response { $data = $request->get_data(); $data = ! is_array( $data ) ? (array) $data : $data; $enabled_user_enums = $data['enabled_user_enums']; $is_success = false; $message = esc_html__( 'An error occurred, try again.', 'defender-security' ); if ( $this->prevent_enum_users->set_enabled_user_enums( $enabled_user_enums ) ) { $is_success = true; $message = esc_html__( 'User enumeration option(s) updated successfully.', 'defender-security' ); } return new Response( $is_success, array( 'message' => $message ) ); } /** * Check XML-RPC status. * * @return Response * @defender_route */ public function check_xml_rpc() { $tweak_xml_rpc = wd_di()->get( Disable_XML_RPC::class ); // If this tweak is ON, block XML-RPC. if ( $tweak_xml_rpc->recheck() ) { $tweak_xml_rpc->add_hooks(); } return new Response( true, array() ); } /** * Tracks a tweak recommendation. * * @param string $title The title of the tweak recommendation. * @param string $status The status of the tweak recommendation. * * @return void */ private function track_tweak( string $title, string $status ) { if ( ! defender_is_wp_cli() ) { $this->track_feature( 'def_recommendation_applied', array( 'Recommendation Name' => $title, 'Status' => $status, ) ); } } }