file = defender_wp_config_path(); $this->get_options(); $this->cron_schedule(); } /** * Check whether the issue has been resolved or not. * * @return bool|void */ public function check() { if ( $this->is_reverted ) { return false; } if ( ! $this->is_salts_exist() ) { return false; } if ( $this->last_modified ) { $reminder_date = strtotime( '+' . $this->reminder_duration, $this->last_modified ); return $reminder_date > defender_get_current_time(); } } /** * Get options. * * @return void */ private function get_options(): void { $options = get_site_option( 'defender_security_tweaks_' . $this->slug ); $this->reminder_date = $options['reminder_date'] ?? 0; $this->reminder_duration = isset( $options['reminder_duration'] ) && '' !== $options['reminder_duration'] ? $options['reminder_duration'] : $this->default_days; $this->is_reverted = isset( $options['is_reverted'] ) && true === (bool) $options['is_reverted']; $last_modified = $this->get_wp_config_last_modified_time(); if ( false === $last_modified ) { $last_modified = $options['last_modified'] ?? null; } elseif ( isset( $options['last_modified'] ) && is_int( $options['last_modified'] ) && 0 < $options['last_modified'] && $options['last_modified'] < $last_modified ) { $last_modified = $options['last_modified']; } $this->last_modified = $last_modified; $this->is_autogenerate_keys = isset( $options['is_autogenerate_keys'] ); } /** * Here is the code for processing. If the return is true or Response, we add it to resolve list. WP_Error if any error. * * @return bool|WP_Error|Response */ public function process() { $is_done = $this->update_keys(); if ( is_wp_error( $is_done ) ) { return $is_done; } if ( $is_done ) { $mask_login = wd_di()->get( Mask_Login::class ); $url = $mask_login->is_active() ? $mask_login->get_new_login_url() : wp_login_url( network_admin_url( 'admin.php?page=wdf-hardener' ) ); $interval = 3; return new Response( true, array( 'message' => sprintf( /* translators: 1: login link, 2: line break, 3: timer. */ esc_html__( 'All key salts have been regenerated. You will now need to %1$s. %2$s This will auto reload after %3$s seconds.', 'defender-security' ), '' . esc_html__( 're-login', 'defender-security' ) . '', '
', '' . $interval . '' ), 'redirect' => $url, 'interval' => $interval, ) ); } return $is_done; } /** * This is for un-do stuff that has be done in @process. * * @return bool */ public function revert(): bool { $values = get_site_option( 'defender_security_tweaks_' . $this->slug, array() ); $values['is_reverted'] = true; update_site_option( 'defender_security_tweaks_' . $this->slug, $values ); $this->is_reverted = true; return true; } /** * Shield up. * * @return bool */ public function shield_up(): bool { return true; } /** * Parse and validate salt definitions string. * * @param string $body API response body. * @param array $constants Expected constant keys. * * @return array|false */ private function parse_and_validate_salts( string $body, array $constants ): array|false { $raw_salts = explode( "\n", $body ); $salts = array(); $values = array(); $index = 0; foreach ( $raw_salts as $salt ) { $salt = trim( $salt ); if ( '' === $salt ) { continue; } $salt = stripslashes( $salt ); $expected_const = $constants[ $index ] ?? null; if ( null === $expected_const || 0 !== strpos( $salt, 'define(' ) || 1 !== preg_match( '/^define\(\s*[\'"]' . preg_quote( $expected_const, '/' ) . '[\'"]\s*,\s*[\'"](.*?)[\'"]\s*\)\s*;$/s', $salt, $matches ) ) { return false; } $val = $matches[1]; if ( 'put your unique phrase here' === $val || strlen( $val ) < 64 || isset( $values[ $val ] ) ) { return false; } $values[ $val ] = true; $salts[] = $salt; ++$index; } if ( count( $salts ) !== count( $constants ) ) { return false; } return $salts; } /** * Get salts to be placed in wp-config.php. * * @return array|WP_Error */ private function get_salts(): WP_Error|array { $constants = $this->get_constants(); $max_attempts = 3; $attempt = 0; while ( $attempt < $max_attempts ) { ++$attempt; $response = wp_safe_remote_get( 'https://api.wordpress.org/secret-key/1.1/salt/' ); if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) { continue; } $salts = $this->parse_and_validate_salts( wp_remote_retrieve_body( $response ), $constants ); if ( false !== $salts ) { return $salts; } } return new WP_Error( 'defender_salts_not_found', esc_html__( 'Unable to generate salts. Please try again.', 'defender-security' ) ); } /** * Get how long the wp-config file is last updated. * * @return int|string */ private function get_last_modified_days(): int|string { $current_time = defender_get_current_time(); $days_ago = ( $current_time - $this->last_modified ) / DAY_IN_SECONDS; return $days_ago ? (int) round( $days_ago ) : 'unknown'; } /** * Get all the constants. * * @return array */ private function get_constants(): array { return array( 'AUTH_KEY', 'SECURE_AUTH_KEY', 'LOGGED_IN_KEY', 'NONCE_KEY', 'AUTH_SALT', 'SECURE_AUTH_SALT', 'LOGGED_IN_SALT', 'NONCE_SALT', ); } /** * Retrieve the tweak's label. * * @return string */ public function get_label(): string { return esc_html__( 'Update security keys', 'defender-security' ); } /** * Get the error reason. * * @return string */ public function get_error_reason(): string { if ( ! $this->is_salts_exist() ) { return wp_kses( __( 'Your security keys are outdated
and one or more security salts are missing in wp-config.php.', 'defender-security' ), array( 'strong' => array(), 'br' => array(), ) ); } $days = $this->get_last_modified_days(); if ( 'unknown' === $days ) { return wp_kses( __( 'Your security keys are outdated
and we can\'t tell how old they are.', 'defender-security' ), array( 'strong' => array(), 'br' => array(), ) ); } return wp_kses( sprintf( /* translators: %1$s: opening strong tag, %2$s: closing strong tag */ __( '%1$sYour security keys are outdated.%2$s', 'defender-security' ), '', '' ), array( 'strong' => array(), ) ); } /** * Return a summary data of this tweak. * * @return array */ public function to_array(): array { $auto_regenerate = $this->get_is_autogenerate_keys(); $auto_status = $auto_regenerate ? __( 'Auto-regenerate is turned on.', 'defender-security' ) : __( 'Auto-regenerate is turned off.', 'defender-security' ); $saved_duration = $this->get_option( 'reminder_duration' ); $reminder_duration = ( is_string( $saved_duration ) && '' !== $saved_duration ) ? $saved_duration : $this->default_days; return array( 'slug' => $this->slug, 'title' => $this->get_label(), 'errorReason' => $this->get_error_reason(), 'successReason' => wp_strip_all_tags( sprintf( /* translators: %1$s: reminder duration, %2$s: auto-regenerate status */ __( 'Security keys regenerate every %1$s. %2$s', 'defender-security' ), $reminder_duration, $auto_status ) ), 'misc' => array( 'show_revert_button' => true, 'reminder' => $reminder_duration, 'show_action_button' => true, ), 'bulk_description' => wp_strip_all_tags( __( 'WordPress security keys help protect user sessions and cookies. Update security keys with strong, unique values to improve encryption and reduce the risk of unauthorized access.', 'defender-security' ) ), 'bulk_title' => wp_strip_all_tags( __( 'Security Keys', 'defender-security' ) ), ); } /** * Getter method of is_autogenerate_keys. * * @return bool. Return true or false which is used to trigger auto generation of security salt/key. */ public function get_is_autogenerate_keys(): bool { $is_autogenerate_keys = $this->get_option( 'is_autogenerate_keys' ); $this->is_autogenerate_keys = is_null( $is_autogenerate_keys ) || (bool) $is_autogenerate_keys; return $this->is_autogenerate_keys; } /** * Setter method of is_autogenerate_keys. * * @param bool $value Boolean flag of the is_autogenerate_keys. * * @return bool Return true if value updated, otherwise false. */ public function set_is_autogenrate_keys( bool $value ): bool { $this->is_autogenerate_keys = $value; return $this->update_option( 'is_autogenerate_keys', $this->is_autogenerate_keys ); } /** * Cron schedule. * * @return void */ public function cron_schedule(): void { if ( true === $this->get_is_autogenerate_keys() ) { $display_name = $this->get_option( 'reminder_duration' ); if ( ! is_string( $display_name ) || '' === $display_name ) { $display_name = $this->default_days; } /** * Network Cron Manager instance. * * @var Network_Cron_Manager $network_cron_manager */ $network_cron_manager = wd_di()->get( Network_Cron_Manager::class ); $interval_seconds = strtotime( $display_name, 0 ); $start_time = defender_get_current_time() + $interval_seconds; $network_cron_manager->register_callback( 'wpdef_sec_key_gen', array( $this, 'cron_process' ), $interval_seconds, $start_time, ); } } /** * Cron unscheduled. * * @return void */ public function cron_unschedule(): void { /** * Network Cron Manager instance. * * @var Network_Cron_Manager $network_cron_manager */ $network_cron_manager = wd_di()->get( Network_Cron_Manager::class ); $network_cron_manager->remove_callback( 'wpdef_sec_key_gen' ); } /** * Cron schedule security key/salt generation process. * * @return void */ public function cron_process(): void { try { $security_tweak_model = new Security_Tweaks(); if ( ! $security_tweak_model->is_tweak_ignore( $this->slug ) && true === $this->get_is_autogenerate_keys() ) { $this->update_keys(); } } catch ( Throwable $th ) { $this->log( 'Security Key. Cron Throwable: ' . get_class( $th ) . ': ' . $th->getMessage(), Security_Tweak::LOG_FILE_NAME ); } } /** * Check all salts are exists. * * @return bool Return true if all constants have salt else false. */ private function is_salts_exist(): bool { foreach ( $this->get_constants() as $constants ) { if ( ! defined( $constants ) ) { return false; } } return true; } /** * Append salt to wp-config.php file content string. * * @param bool|string $contents Text content of wp-config.php. * @param string $new_salts Salts need to be appended. * * @return string Text content after salts appended. */ private function append_salts( bool|string $contents, string $new_salts ): string { $pattern = '/(define\(\s*\'AUTH_KEY\'.*?\);)\s*' . '(define\(\s*\'SECURE_AUTH_KEY\'.*?\);)\s*' . '(define\(\s*\'LOGGED_IN_KEY\'.*?\);)\s*' . '(define\(\s*\'NONCE_KEY\'.*?\);)\s*' . '(define\(\s*\'AUTH_SALT\'.*?\);)\s*' . '(define\(\s*\'SECURE_AUTH_SALT\'.*?\);)\s*' . '(define\(\s*\'LOGGED_IN_SALT\'.*?\);)\s*' . '(define\(\s*\'NONCE_SALT\'.*?\);)/s'; if ( preg_match( $pattern, $contents ) ) { return preg_replace( $pattern, $new_salts, $contents ); } else { return preg_replace( '/(define\(\s*\'DB_NAME\'\s*,.*?\);)/s', $new_salts . PHP_EOL . '$1', $contents ); } } /** * Return collection of reminder frequencies. * * @return array */ public function reminder_frequencies(): array { return array( '30 days', '60 days', '90 days', '6 months', '1 year', ); } /** * Get the last modified time of wp-config file. * * @return int|false * @since 3.10.0 */ private function get_wp_config_last_modified_time(): bool|int { $file = $this->file; if ( ! file_exists( $file ) ) { $file = ABSPATH . WPINC . '/general-template.php'; } return filemtime( $file ); } /** * Update keys. * * @return WP_Error|bool|array True if success. WP_Error if any error. */ private function update_keys(): WP_Error|bool|array { $wp_filesystem = $this->get_wp_filesystem(); if ( ! $wp_filesystem->is_writable( $this->file ) ) { return new WP_Error( 'defender_file_not_writable', /* translators: %s: file path */ sprintf( esc_html__( 'The file %s is not writable', 'defender-security' ), $this->file ) ); } $constants = $this->get_constants(); $salts = $this->get_salts(); if ( is_wp_error( $salts ) ) { return $salts; } $contents = $wp_filesystem->get_contents( $this->file ); $new_salts = ''; foreach ( $constants as $key => $const ) { if ( defined( $const ) ) { $pattern = "/^define\(\s*['|\"]{$const}['|\"],(.*)\)\s*;/m"; $replacement = $salts[ $key ]; $contents = preg_replace_callback( $pattern, function () use ( $replacement ) { return $replacement; }, $contents ); } else { $new_salts .= $salts[ $key ] . PHP_EOL; } } if ( '' !== $new_salts ) { $new_salts = PHP_EOL . '/* DEFENDER GENERATED SALTS */' . PHP_EOL . $new_salts . PHP_EOL; $contents = $this->append_salts( $contents, $new_salts ); } $is_done = $wp_filesystem->put_contents( $this->file, $contents, FS_CHMOD_FILE ); if ( ! $is_done ) { return false; } $values = get_site_option( 'defender_security_tweaks_' . $this->slug, array() ); $this->last_modified = defender_get_current_time(); $values['last_modified'] = defender_get_current_time(); unset( $values['is_reverted'] ); $this->is_reverted = false; update_site_option( 'defender_security_tweaks_' . $this->slug, $values ); $this->log( 'Security keys are updated.', Security_Tweak::LOG_FILE_NAME ); if ( is_multisite() && ! headers_sent() ) { // Delete cookies forced. wp_clear_auth_cookie(); } return true; } }