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;
}
}