register_page( esc_html__( 'Settings', 'wpdef' ), $this->slug, array( &$this, 'main_view', ), $this->parent_slug ); // Internal cache. $this->model = new \WP_Defender\Model\Setting\Main_Setting(); $this->service = wd_di()->get( \WP_Defender\Component\Backup_Settings::class ); add_action( 'defender_enqueue_assets', array( &$this, 'enqueue_assets' ) ); $this->register_routes(); Config_Hub_Helper::clear_config_transient(); // Add cron schedule to clean out outdated logs. add_action( 'wp_defender_clear_logs', array( $this, 'clear_logs' ) ); add_action( 'admin_init', array( $this, 'check_cron_schedule' ) ); } /** * Safe way to get cached model. * * @return \WP_Defender\Model\Setting\Main_Setting */ private function get_model() { if ( is_object( $this->model ) ) { return $this->model; } return wd_di()->get( \WP_Defender\Model\Setting\Main_Setting::class ); } public function enqueue_assets() { if ( ! $this->is_page_active() ) { return; } wp_localize_script( 'def-settings', 'settings', $this->data_frontend() ); wp_enqueue_script( 'def-settings' ); $this->enqueue_main_assets(); } /** * Render the root element for frontend. */ public function main_view() { $this->render( 'main' ); } /** * Store settings into db. * @param Request $request * * @return Response * * @defender_route */ public function save_settings( Request $request ) { $model = $this->get_model(); $data = $request->get_data(); $model->import( $data ); if ( $model->validate() ) { $model->save(); return new Response( true, array( 'message' => __( 'Your settings have been updated.', 'wpdef' ), ) ); } return new Response( false, array( 'message' => $model->get_formatted_errors(), ) ); } /** * Reset settings. * @param Request $request * * @return Response * @defender_route */ public function reset_settings( Request $request ) { wd_di()->get( \WP_Defender\Controller\Advanced_Tools::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Audit_Logging::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Dashboard::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Security_Tweaks::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Scan::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Firewall::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Firewall_Logs::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Login_Lockout::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Nf_Lockout::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Mask_Login::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Notification::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Tutorial::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Two_Factor::class )->remove_settings(); wd_di()->get( \WP_Defender\Controller\Blocklist_Monitor::class )->remove_settings(); $this->remove_settings(); // Indicate that it is not a new installation. defender_no_fresh_install(); return new Response( true, array( 'message' => __( 'Your settings have been reset.', 'wpdef' ), 'redirect' => network_admin_url( 'admin.php?page=wp-defender' ), 'interval' => 1, ) ); } public function remove_settings() { wd_di()->get( \WP_Defender\Model\Setting\Main_Setting::class )->delete(); } public function remove_data() {} public function data_frontend() { $model = $this->get_model(); $this->service->maybe_create_default_config(); $configs = $this->get_configs_and_update_status(); foreach ( $configs as &$config ) { // Unset the data as we don't need it. if ( isset( $config['configs'] ) ) { unset( $config['configs'] ); } } $link = ( new \WP_Defender\Behavior\WPMUDEV() )->is_member() ? 'https://wpmudev.com/translate/projects/wpdef/' : 'https://translate.wordpress.org/projects/wp-plugins/defender-security/'; return array_merge( array( 'general' => array( 'translate' => $model->translate, 'usage_tracking' => $model->usage_tracking, 'translation_link' => $link, ), 'data_settings' => array( 'uninstall_settings' => $model->uninstall_settings, 'uninstall_data' => $model->uninstall_data, ), 'accessibility' => array( 'high_contrast_mode' => $model->high_contrast_mode, ), 'misc' => array( 'setting_url' => network_admin_url( is_multisite() ? 'settings.php' : 'options-general.php' ), 'clear_transient_url' => network_admin_url( 'admin.php?page=wdf-setting&view=configs&transient=clear' ), ), 'configs' => $configs, ), $this->dump_routes_and_nonces() ); } public function to_array() {} public function import_data( $data ) { $model = $this->get_model(); $model->import( $data ); if ( $model->validate() ) { $model->save(); } } private function validate_importer( $importer ) { if ( $this->service->verify_config_data( $importer ) ){ // Validate content. This is the current data, we use this for verify the schema. $sample = $this->service->gather_data(); foreach ( $importer['configs'] as $slug => $module ) { // This is not in the sample, file is invalid. if ( ! isset( $sample[ $slug ] ) ) { return false; } $keys = array_keys( $sample[ $slug ] ); $import_keys = array_keys( $module ); $diff = array_diff( $import_keys, $keys ); if ( count( $diff ) ) { return false; } return true; } } return false; } /** * Import config. * @param Request $request * * @return Response * @defender_route */ public function import_config( Request $request ) { $data = $request->get_data(); $file = $_FILES['file']; $tmp = $file['tmp_name']; $content = file_get_contents( $tmp ); $importer = json_decode( $content, true ); if ( ! is_array( $importer ) ) { return new Response( false, array( 'message' => __( 'The file is corrupted.', 'wpdef' ), ) ); } // If it's old config structure then we upgrade configs to new format. if ( ! empty( $importer['configs'] ) && ! $this->service->check_for_new_structure( $importer['configs'] ) ) { $adapter = wd_di()->get( \WP_Defender\Component\Config\Config_Adapter::class ); $importer['configs'] = $adapter->upgrade( $importer['configs'] ); } if ( ! $this->validate_importer( $importer ) ) { return new Response( false, array( 'message' => __( 'An error occurred while importing the file. Please check your file or upload another file.', 'wpdef' ), ) ); } // Sanitize the files a bit. $name = strip_tags( $importer['name'] ); $configs = array( 'name' => $name, 'immortal' => false, ); $configs['configs'] = $importer['configs']; $configs['description'] = isset( $importer['description'] ) && ! empty( $importer['description'] ) ? sanitize_textarea_field( $importer['description'] ) : ''; $configs['strings'] = $this->service->import_module_strings( $importer ); $key = 'wp_defender_config_import_' . time(); update_site_option( $key, $configs ); $this->service->index_key( $key ); return new Response( true, array( 'message' => sprintf( /* translators: %s: Config name. */ __( '%s config has been uploaded successfully – you can now apply it to this site.', 'wpdef' ), $name ), 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ), ) ); } /** * Create config. * @param Request $request * * @return Response * @defender_route */ public function new_config( Request $request ) { $data = $request->get_data(); $name = trim( $data['name'] ); if ( empty( $name ) ) { return new Response( false, array( 'message' => __( 'Invalid config name', 'wpdef' ), ) ); } $name = strip_tags( $name ); $desc = isset( $data['desc'] ) && ! empty( $data['desc'] ) ? wp_kses_post( $data['desc'] ) : ''; $key = 'wp_defender_config_' . time(); $settings = $this->service->parse_data_for_import(); $data = array_merge( array( 'name' => $name, 'immortal' => false, 'description' => $desc, ), $settings ); // Add config to HUB. $hub_id = Config_Hub_Helper::add_configs_to_hub( $data ); if ( $hub_id ) { $data['hub_id'] = $hub_id; } unset( $data['labels'] ); if ( update_site_option( $key, $data ) ) { $this->service->index_key( $key ); return new Response( true, array( 'message' => sprintf( /* translators: %s: Config name. */ __( '%s config saved successfully.', 'wpdef' ), $name ), 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ), ) ); } else { return new Response( false, array( 'message' => __( 'An error occurred while saving your config. Please try it again.', 'wpdef' ), ) ); } } /** * Download config * * @defender_route */ public function download_config() { $key = isset( $_GET['key'] ) ? sanitize_text_field( $_GET['key'] ) : false; if ( empty( $key ) ) { return new Response( false, array( 'message' => __( 'Invalid config', 'wpdef' ), ) ); } $config = get_site_option( $key ); if ( false === $config ) { return new Response( false, array( 'message' => __( 'Invalid config', 'wpdef' ), ) ); } $sample = $this->service->gather_data(); foreach ( $sample as $slug => $data ) { foreach ( $data as $key => $val ) { if ( ! isset( $config['configs'][ $slug ][ $key ] ) ) { $config['configs'][ $slug ][ $key ] = null; } } } $json = json_encode( $config ); $filename = 'wp-defender-config-' . sanitize_file_name( $config['name'] ) . '.json'; header( 'Content-disposition: attachment; filename=' . $filename ); header( 'Content-type: application/json' ); echo $json; exit; } /** * Apply config. * @param Request $request * * @return Response * @defender_route */ public function apply_config( Request $request ) { $data = $request->get_data(); $key = trim( $data['key'] ); if ( empty( $key ) ) { return new Response( false, array( 'message' => __( 'Invalid config', 'wpdef' ), ) ); } $config = get_site_option( $key ); if ( false === $config ) { return new Response( false, array( 'message' => __( 'Invalid config', 'wpdef' ), ) ); } // Return error message or bool value for auth action. $restore_result = $this->service->restore_data( $config['configs'], 'plugin' ); if ( is_string( $restore_result ) ) { return $this->apply_config_recommendations_error_message(); } $this->service->make_config_active( $key ); $message = sprintf( /* translators: %s: Config name. */ __( '%s config has been applied successfully.', 'wpdef' ), $config['name'] ); $return = array(); if ( $restore_result ) { $login_url = wp_login_url(); $settings_mask_login = new \WP_Defender\Model\Setting\Mask_Login(); if ( $settings_mask_login->is_active() ) { $login_url = $settings_mask_login->get_new_login_url(); } $message .= '
' . sprintf( /* translators: %s: Login link. */ __( 'Because of some security recommendations get applied, You will now need to re-login.
This will auto reload now', 'wpdef' ), $login_url ); $return['reload'] = 3; $redirect = urlencode( network_admin_url( 'admin.php?page=wdf-setting&view=configs' ) ); if ( isset( $data['screen'] ) && 'dashboard' === $data['screen'] ) { $redirect = urlencode( network_admin_url( 'admin.php?page=wp-defender' ) ); } $return['redirect'] = add_query_arg( 'redirect_to', $redirect, $login_url ); $return['interval'] = 2; } $return['message'] = $message; $return['configs'] = Config_Hub_Helper::get_fresh_frontend_configs( $this->service ); return new Response( true, $return ); } /** * Update config. * @param Request $request * * @return Response * @defender_route */ public function update_config( Request $request ) { $data = $request->get_data(); $key = trim( $data['key'] ); $name = trim( $data['name'] ); $description = trim( $data['description'] ); if ( empty( $name ) || empty( $key ) ) { return new Response( false, array( 'message' => __( 'Invalid config', 'wpdef' ), ) ); } $config = get_site_option( $key ); if ( false === $config ) { return new Response( false, array( 'message' => __( 'Invalid config', 'wpdef' ), ) ); } $old_config = $config; $config['name'] = sanitize_text_field( $name ); $config['description'] = sanitize_textarea_field( $description ); // Check data has been changed or not. if ( $old_config['name'] === $config['name'] && $old_config['description'] === $config['description'] ) { // Data is not changed, so not need to run update query. $option_updated = true; } else { $option_updated = update_site_option( $key, $config ); Config_Hub_Helper::update_on_hub( $config ); } if ( $option_updated ) { return new Response( true, array( 'message' => sprintf( /* translators: %s: Config name. */ __( '%s config saved successfully.', 'wpdef' ), $name ), 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ), ) ); } else { return new Response( false, array( 'message' => __( 'An error occurred while saving your config. Please try it again.', 'wpdef' ), ) ); } } /** * Delete config. * @param Request $request * * @return Response * @defender_route */ public function delete_config( Request $request ) { $data = $request->get_data(); $key = trim( $data['key'] ); if ( empty( $key ) ) { return new Response( false, array( 'message' => __( 'Invalid config', 'wpdef' ), ) ); } // Remove from HUB. $config = get_site_option( $key ); if ( isset( $config['hub_id'] ) ) { Config_Hub_Helper::delete_configs_from_hub( $config['hub_id'] ); } if ( 0 === strpos( $key, 'wp_defender_config' ) ) { delete_site_option( $key ); return new Response( true, array( 'message' => __( 'Config removed successfully.', 'wpdef' ), 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ), ) ); } return new Response( false, array( 'message' => __( 'Invalid config', 'wpdef' ), ) ); } /** * @return array */ public function export_strings() { return array(); } /** * Update config status and return them. * * @return array */ private function get_configs_and_update_status() { $configs = Config_Hub_Helper::get_configs( $this->service ); $is_remove = Config_Hub_Helper::check_remove_active_flag(); // Loop to update strings of configs. foreach ( $configs as $key => &$config ) { if ( ! is_array( $config ) ) { continue; } $config['strings'] = $this->service->import_module_strings( $config ); if ( $is_remove ) { $config['is_active'] = false; } // Update config data. update_site_option( $key, $config ); } return $configs; } /** * Response error message along with configs. * * @return Response * * @throws \Exception */ private function apply_config_recommendations_error_message() { $message = sprintf( __( 'There was an issue with applying some of the tweaks from the Recommendations tab because we cannot make changes to your wp-config.php file. Please see our %s to apply the changes manually.', 'wpdef' ), 'documentation' ); return new Response( false, array( 'message' => $message, 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ), ) ); } /** * Check if the logger cron is scheduled to run. */ public function check_cron_schedule() { if ( ! wp_next_scheduled( 'wp_defender_clear_logs' ) ) { wp_schedule_event( time() + HOUR_IN_SECONDS, 'daily', 'wp_defender_clear_logs' ); } } /** * Clear out lines that are older than 30 days. */ public function clear_logs() { // @since 2.7.0 $time_limit = apply_filters( 'wpdef_clear_logs_time_limit', MONTH_IN_SECONDS ); if ( is_multisite() ) { global $wpdb; $offset = 0; $limit = 100; while ( $blogs = $wpdb->get_results( "SELECT blog_id FROM {$wpdb->blogs} LIMIT {$offset}, {$limit}", ARRAY_A ) ) { if ( ! empty( $blogs ) && is_array( $blogs ) ) { foreach ( $blogs as $blog ) { switch_to_blog( $blog['blog_id'] ); $this->clear_logs_from_files( $time_limit ); restore_current_blog(); } } $offset += $limit; } } else { $this->clear_logs_from_files( $time_limit ); } } /** * Clear logs from file that are 30 days old. * * @param int $time_limit * * @since 2.7.0 */ public function clear_logs_from_files( $time_limit = MONTH_IN_SECONDS ) { $now = date( 'c' ); // Todo: why only defender.log? $files = array( 'defender.log' ); foreach ( $files as $file_name ) { $file_path = $this->get_log_path( $file_name ); if ( ! file_exists( $file_path ) ) { return; } $content = file( $file_path ); $size_of_content = is_array($content) || $content instanceof \Countable ? count( $content ) : 0; foreach ( $content as $index => $line ) { // If the line does not start with '[' (it's probably not a new entry). $first_char = substr( $line, 0, 1 ); if ( '[' !== $first_char ) { // Delete. unset( $content[ $index ] ); } /** * Get the date from entry. Items can be an array it two cases - if there's a valid date, or if the line * contained something like [header] in the start. Cannot make assumptions just on the fact it's an array. */ preg_match( '/\[(.*)\]/', $line, $items ); // If, for some reason, can't get the date, or it's not the size of an ISO 8601 date. if ( ! isset( $items[1] ) || 25 !== strlen( $items[1] ) ) { // Delete. unset( $content[ $index ] ); } else { // It looks like it's a valid date string, compare with today. $time_diff = strtotime( $now ) - strtotime( $items[1] ); // We don't need to continue on, because if this entry is not older than 30 days, the next one will not be as well. if ( $time_diff < $time_limit ) { break; } unset( $content[ $index ] ); } } // Nothing changed - do nothing. if ( (is_array($content) || $content instanceof \Countable ? count( $content ) : 0) === $size_of_content ) { return; } // Glue back together and write back to file. $content = implode( '', $content ); file_put_contents( $file_path, $content ); } } }