Server IP : 104.21.14.48 / Your IP : 3.145.204.107 [ Web Server : Apache System : Linux b70eb322-3aee-0c53-7c82-0db91281f2c6.secureserver.net 6.1.90-1.el9.elrepo.x86_64 #1 SMP PREEMPT_DYNAMIC Thu May 2 12:09:22 EDT 2024 x86_64 User : root ( 0) PHP Version : 8.0.30.2 Disable Function : NONE Domains : 0 Domains MySQL : ON | cURL : ON | WGET : ON | Perl : OFF | Python : OFF | Sudo : OFF | Pkexec : OFF Directory : /var/www/wp-content/plugins/defender-security/src/controller/ |
Upload File : |
<?php /** * Handles the main settings. * * @package WP_Defender\Controller */ namespace WP_Defender\Controller; use Countable; use WP_Defender\Event; use Calotes\Component\Request; use Calotes\Component\Response; use WP_Defender\Behavior\WPMUDEV; use WP_Defender\Component\Backup_Settings; use WP_Defender\Component\Config\Config_Adapter; use WP_Defender\Component\Config\Config_Hub_Helper; use WP_Defender\Model\Setting\Main_Setting as Model_Main_Setting; /** * Methods for handling main settings. */ class Main_Setting extends Event { /** * The slug identifier for this controller. * * @var string */ public $slug = 'wdf-setting'; /** * The model for handling the data. * * @var Model_Main_Setting */ public $model; /** * Service for handling logic. * * @var Backup_Settings */ protected $service; /** * The intention/nonce action of the current request. * * @since 4.0.0 * @var string */ protected $intention = ''; /** * 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__( 'Settings', 'defender-security' ), $this->slug, array( &$this, 'main_view', ), $this->parent_slug ); // Internal cache. $this->model = new Model_Main_Setting(); $this->service = wd_di()->get( Backup_Settings::class ); add_action( 'defender_enqueue_assets', array( &$this, 'enqueue_assets' ) ); $this->register_routes(); // 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' ) ); add_action( 'wd_settings_update', array( $this, 'intercept_settings_update' ), 10, 2 ); } /** * Check actual config data. * * @return Response * @defender_route */ public function check_configs(): Response { Config_Hub_Helper::clear_config_transient(); return new Response( true, array() ); } /** * Safe way to get cached model. * * @return Model_Main_Setting */ private function get_model() { if ( is_object( $this->model ) ) { return $this->model; } return wd_di()->get( Model_Main_Setting::class ); } /** * 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; } wp_localize_script( 'def-settings', 'settings', $this->data_frontend() ); wp_enqueue_script( 'def-settings' ); $this->enqueue_main_assets(); } /** * Render the root element for frontend. * * @return void */ public function main_view(): void { $this->render( 'main' ); } /** * Save settings. * * @param Request $request The request object containing new settings data. * * @return Response * @defender_route */ public function save_settings( Request $request ) { $model = $this->get_model(); $data = $request->get_data(); $model->import( $data ); if ( $model->validate() ) { $this->set_intention( 'Settings' ); $model->save(); Config_Hub_Helper::set_clear_active_flag(); return new Response( true, array( 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ), 'auto_close' => true, ) ); } return new Response( false, array( 'message' => $model->get_formatted_errors(), ) ); } /** * Reset settings. * * @return Response * @defender_route */ public function reset_settings(): Response { wd_di()->get( Advanced_Tools::class )->remove_settings(); wd_di()->get( Audit_Logging::class )->remove_settings(); wd_di()->get( Dashboard::class )->remove_settings(); wd_di()->get( Security_Tweaks::class )->remove_settings(); wd_di()->get( Scan::class )->remove_settings(); // Parent and submodules. wd_di()->get( Firewall::class )->remove_settings(); wd_di()->get( Mask_Login::class )->remove_settings(); wd_di()->get( Notification::class )->remove_settings(); wd_di()->get( Tutorial::class )->remove_settings(); wd_di()->get( Two_Factor::class )->remove_settings(); wd_di()->get( Blocklist_Monitor::class )->remove_settings(); $this->set_intention( 'Data Reset' ); // Track first until settings are removed. $this->track_opt( false ); $this->remove_settings(); // Indicate that it is not a new installation. defender_no_fresh_install(); return new Response( true, array( 'message' => esc_html__( 'Your settings have been reset.', 'defender-security' ), 'redirect' => network_admin_url( 'admin.php?page=wp-defender' ), 'interval' => 1, ) ); } /** * Tracks the settings toggle. * * @param bool $active The status of the toggle. * * @return void */ public function track_opt( $active ) { $model = $this->get_model(); // Track only if the Data tracking option was enabled before changes. if ( $model->usage_tracking ) { $from = $this->get_triggered_location(); $this->track_opt_toggle( $active, $from ); } } /** * Removes settings for all submodules. */ public function remove_settings(): void { wd_di()->get( Model_Main_Setting::class )->delete(); } /** * Delete all the data & the cache. */ public function remove_data() { } /** * Provides data for the frontend. * * @return array An array of data for the frontend. */ public function data_frontend(): array { $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 WPMUDEV() )->is_member() ? 'https://wpmudev.com/translate/projects/wpdef/' : 'https://translate.wordpress.org/projects/wp-plugins/defender-security/'; $allowed_user = (array) get_option( 'wdp_un_general', array( 'limit_to_user' => array() ) )['limit_to_user']; /** * Ignore WordPress.PHP.StrictInArray.MissingTrueStrict * * We're not using strict comparison. Here's why: * Initially, when the dashboard plugin is installed, the user ID is an integer. * However, after adding a new user, all user IDs become strings. * This can cause errors if users aren't updated in the permissions tab. */ return array_merge( array( 'general' => array( 'translate' => $model->translate, 'show_usage_tracking' => in_array( get_current_user_id(), $allowed_user ), // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict 'usage_tracking' => $model->usage_tracking, 'translation_link' => $link, ), 'data_settings' => array( 'uninstall_settings' => $model->uninstall_settings, 'uninstall_data' => $model->uninstall_data, 'uninstall_quarantine' => $model->uninstall_quarantine, ), 'accessibility' => array( 'high_contrast_mode' => $model->high_contrast_mode, ), 'misc' => array( 'setting_url' => network_admin_url( is_multisite() ? 'settings.php' : 'options-general.php' ), 'privacy_link' => Model_Main_Setting::PRIVACY_LINK, ), 'configs' => $configs, ), $this->dump_routes_and_nonces() ); } /** * Converts the current object state to an array. * * @return array The array representation of the object. */ public function to_array(): array { return array(); } /** * Imports data into the model. * * @param array $data Data to be imported into the model. */ public function import_data( array $data ) { $model = $this->get_model(); $model->import( $data ); if ( $model->validate() ) { $model->save(); } } /** * Validates the importer data. * This function checks if the importer data is valid by verifying its configuration data and comparing it with the * sample data. * * @param array $importer The importer data to be validated. * * @return bool Returns true if the importer data is valid, false otherwise. */ private function validate_importer( $importer ): bool { 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. * * @return Response * @defender_route */ public function import_config(): Response { global $wp_filesystem; // Initialize the WP filesystem, no more using 'file-put-contents' function. if ( empty( $wp_filesystem ) ) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } $file = defender_get_data_from_request( 'file', 'f' ); $tmp = $file['tmp_name']; $content = $wp_filesystem->get_contents( $tmp ); $importer = json_decode( $content, true ); if ( ! is_array( $importer ) ) { return new Response( false, array( 'message' => esc_html__( 'The file is corrupted.', 'defender-security' ), ) ); } // 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( Config_Adapter::class ); $importer['configs'] = $adapter->upgrade( $importer['configs'] ); } if ( ! $this->validate_importer( $importer ) ) { return new Response( false, array( 'message' => esc_html__( 'An error occurred while importing the file. Please check your file or upload another file.', 'defender-security' ), ) ); } // Do not use wp_strip_all_tags() to prevent XSS attack. $name = sanitize_text_field( $importer['name'] ); $configs = array( 'name' => $name, 'immortal' => false, 'is_removable' => true, ); $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. */ esc_html__( '%s config has been uploaded successfully – you can now apply it to this site.', 'defender-security' ), '<strong>' . $name . '</strong>' ), 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ), ) ); } /** * Create config. * * @param Request $request The request object containing new config data. * * @return Response * @defender_route */ public function new_config( Request $request ): Response { $data = $request->get_data(); $name = trim( $data['name'] ); if ( empty( $name ) ) { return new Response( false, array( 'message' => esc_html__( 'Invalid config name', 'defender-security' ), ) ); } $name = sanitize_text_field( $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, 'is_removable' => true, ), $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. */ esc_html__( '%s config saved successfully.', 'defender-security' ), '<strong>' . $name . '</strong>' ), 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ), ) ); } else { return new Response( false, array( 'message' => esc_html__( 'An error occurred while saving your config. Please try it again.', 'defender-security' ), ) ); } } /** * Download config * * @return Response|void * @defender_route */ public function download_config() { $key = defender_get_data_from_request( 'key', 'g' ); if ( empty( $key ) ) { return new Response( false, array( 'message' => esc_html__( 'Invalid config', 'defender-security' ), ) ); } $config = get_site_option( $key ); if ( false === $config ) { return new Response( false, array( 'message' => esc_html__( 'Invalid config', 'defender-security' ), ) ); } $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; } } } $filename = 'wp-defender-config-' . sanitize_file_name( $config['name'] ) . '.json'; header( 'Content-disposition: attachment; filename=' . $filename ); echo wp_json_encode( $config, JSON_PRETTY_PRINT ); exit(); } /** * Apply config. * * @param Request $request The request object containing new config data. * * @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' => esc_html__( 'Invalid config', 'defender-security' ), ) ); } $config = get_site_option( $key ); if ( false === $config ) { return new Response( false, array( 'message' => esc_html__( 'Invalid config', 'defender-security' ), ) ); } // 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 ); // Track. $this->track_feature( 'def_config_applied', array( // The check is based on the fact that the Default config cannot be deleted. 'Config Type' => isset( $config['is_removable'] ) && false === $config['is_removable'] ? 'Default' : 'Custom', ) ); $message = sprintf( /* translators: %s: Config name. */ esc_html__( '%s config has been applied successfully.', 'defender-security' ), '<strong>' . $config['name'] . '</strong>' ); $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 .= '<br/>' . sprintf( /* translators: %s: Login link. */ esc_html__( 'Due to currently applied security recommendations, you will now need to %s.', 'defender-security' ), '<a href="' . $login_url . '"><strong>' . esc_html__( 're-login', 'defender-security' ) . '</strong></a>' ); $message .= '<br/>'; $message .= esc_html__( 'This will auto reload now.', 'defender-security' ); $return['reload'] = 3; $redirect = rawurlencode( network_admin_url( 'admin.php?page=wdf-setting&view=configs' ) ); if ( isset( $data['screen'] ) && 'dashboard' === $data['screen'] ) { $redirect = rawurlencode( 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['auto_close'] = true; $return['configs'] = Config_Hub_Helper::get_fresh_frontend_configs( $this->service ); return new Response( true, $return ); } /** * Update config. * * @param Request $request The request object containing new config data. * * @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' => esc_html__( 'Invalid config', 'defender-security' ), ) ); } $config = get_site_option( $key ); if ( false === $config ) { return new Response( false, array( 'message' => esc_html__( 'Invalid config', 'defender-security' ), ) ); } $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. */ esc_html__( '%s config saved successfully.', 'defender-security' ), '<strong>' . $name . '</strong>' ), 'auto_close' => true, 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ), ) ); } else { return new Response( false, array( 'message' => esc_html__( 'An error occurred while saving your config. Please try it again.', 'defender-security' ), ) ); } } /** * Delete config. * * @param Request $request The request object containing config key. * * @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' => esc_html__( 'Invalid config', 'defender-security' ), ) ); } $config = get_site_option( $key ); if ( isset( $config['is_removable'] ) && ! $config['is_removable'] ) { return new Response( false, array( 'message' => esc_html__( 'Config can\'t be removed', 'defender-security' ), ) ); } // Remove from HUB. if ( isset( $config['hub_id'] ) ) { Config_Hub_Helper::delete_configs_from_hub( (int) $config['hub_id'] ); } if ( 0 === strpos( $key, 'wp_defender_config' ) ) { delete_site_option( $key ); $this->service->clear_keys(); return new Response( true, array( 'message' => esc_html__( 'Config removed successfully.', 'defender-security' ), 'auto_close' => true, 'configs' => Config_Hub_Helper::get_fresh_frontend_configs( $this->service ), ) ); } return new Response( false, array( 'message' => esc_html__( 'Invalid config', 'defender-security' ), ) ); } /** * Exports strings. * * @return array An array of strings. */ public function export_strings(): array { 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; } /** * Generates an error message for when there is an issue applying some tweaks from the Recommendations tab. * * @return Response The response object containing the error message and fresh frontend configurations. */ private function apply_config_recommendations_error_message(): Response { $message = sprintf( /* translators: 1: Recommendations tab, 2: wp-config.php file, 3: Documentation. */ esc_html__( 'There was an issue with applying some of the tweaks from the %1$s tab because we cannot make changes to your %2$s file. Please see our %3$s to apply the changes manually.', 'defender-security' ), '<strong>' . esc_html__( 'Recommendations', 'defender-security' ) . '</strong>', '<strong>' . esc_html__( 'wp-config.php', 'defender-security' ) . '</strong>', '<a href="https://wpmudev.com/docs/wpmu-dev-plugins/defender/#manually-applying-recommendations" target="_blank">' . esc_html__( 'documentation', 'defender-security' ) . '</a>' ); 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. * * @return void */ public function check_cron_schedule(): void { 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. * * @return void */ public function clear_logs(): void { // 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; // Variable within condition is for comparison. while ( $blogs = $wpdb->get_results( $wpdb->prepare( "SELECT blog_id FROM {$wpdb->blogs} LIMIT %d, %d", $offset, $limit ), ARRAY_A ) ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery, Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition 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 log files older than the specified time. * * @param int $time_limit The time limit in seconds. * * @return void * @since 2.7.0 */ public function clear_logs_from_files( int $time_limit = MONTH_IN_SECONDS ) { global $wp_filesystem; // Initialize the WP filesystem, no more using 'file-put-contents' function. if ( empty( $wp_filesystem ) ) { require_once ABSPATH . '/wp-admin/includes/file.php'; WP_Filesystem(); } $now = wp_date( 'c' ); $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 specific time, 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 ); $wp_filesystem->put_contents( $file_path, $content ); } } /** * Track the data if there are settings changes. * * @param array $old_settings Old settings. * @param array $new_settings New settings. * * @return void * @since 4.2.0 */ public function intercept_settings_update( $old_settings, $new_settings ) { $from = $this->get_triggered_location(); if ( '' !== $from && isset( $new_settings['usage_tracking'], $old_settings['usage_tracking'] ) && $new_settings['usage_tracking'] !== $old_settings['usage_tracking'] ) { $this->track_opt_toggle( ! empty( $new_settings['usage_tracking'] ), $from ); } } }