HEX
Server: Apache
System: Linux SH-FR-PM-y8qo 6.6.80-paas #1 SMP PREEMPT_DYNAMIC Thu Sep 25 11:18:23 UTC 2025 x86_64
User: hosting-user (5000)
PHP: 8.3.28
Disabled: NONE
Upload Files
File: /srv/data/web/vhosts/resiliencejoyeuse.net/htdocs/wp-content/plugins/secupress/free/common.php
<?php
defined( 'ABSPATH' ) or die( 'Something went wrong.' );

/** --------------------------------------------------------------------------------------------- */
/** BANNED IPS ================================================================================== */
/** --------------------------------------------------------------------------------------------- */

add_action('secupress.plugins.loaded','secupress_check_ban_ips', 0);
/**
 * Will remove expired banned IPs, then block the remaining ones. A form will be displayed to allow clumsy Administrators to unlock themselves.
 *
 * @since 1.0
 * @author Grégory Viguier
 */
function secupress_check_ban_ips() {
	$ban_ips = get_site_option( SECUPRESS_BAN_IP );
	$ip      = secupress_get_ip();

	if ( secupress_ip_is_whitelisted( $ip ) ) {
		return;
	}

	$time_ban = (int) secupress_get_module_option( 'login-protection_time_ban', 5, 'users-login' );
	$update   = false;
	$redirect = false;

	// If we got banned ips.
	if ( $ban_ips && is_array( $ban_ips ) ) {
		// The link to be unlocked?
		if ( ! empty( $_GET['action'] ) && 'secupress_self-unban-ip' === $_GET['action'] ) {
			$result = ! empty( $_GET['_wpnonce'] ) ? wp_verify_nonce( $_GET['_wpnonce'], 'secupress_self-unban-ip-' . $ip ) : false;

			if ( $result ) {
				// You're good to go.
				unset( $ban_ips[ $ip ] );
				$update   = true;
				$redirect = true;
			} else {
				// Cheating?
				$title   = '403 ' . get_status_header_desc( 403 );
				$content = __( 'Unlock link expired.', 'secupress' );

				secupress_die( $content, $title, array( 'response' => 403, 'force_die' => true ) );
			}
		}

		// Purge the expired banned IPs.
		foreach ( $ban_ips as $timed_ip => $time ) {
			if ( ( $time + ( $time_ban * 60 ) ) < time() ) {
				unset( $ban_ips[ $timed_ip ] );
				$update = true;
			}
		}

		// Save the changes.
		if ( $update ) {
			if ( $ban_ips ) {
				update_site_option( SECUPRESS_BAN_IP, $ban_ips );
			} else {
				delete_site_option( SECUPRESS_BAN_IP );
			}
		}

		// The user just got unlocked. Redirect to homepage.
		if ( $redirect ) {
			wp_redirect( esc_url_raw( home_url() ) );
			die();
		}

		// Block the user if the IP is still in the array.
		if ( secupress_is_ip_in_range( $ip, array_keys( $ban_ips ) ) ) {
			// Display a form in case of accidental ban.
			$unban_atts = secupress_check_ban_ips_maybe_send_unban_email( $ip );

			$title = ! empty( $unban_atts['title'] ) ? $unban_atts['title'] : ( '403 ' . get_status_header_desc( 403 ) );

			if ( $unban_atts['display_form'] ) {
				$time_ban     = 0;
				$error        = $unban_atts['message'];
				$content      = secupress_check_ban_ips_form( compact( 'ip', 'time_ban', 'error' ) );
			} else {
				$content = $unban_atts['message'];
			}

			secupress_die( $content, $title, [ 'response' => 403, 'force_die' => true, 'attack_type' => 'ban_ip' ] );
		}
	} elseif ( false !== $ban_ips ) {
		delete_site_option( SECUPRESS_BAN_IP );
	}
}


/**
 * After submiting the email address with the form, send an email to the user or return an error.
 *
 * @since 1.0
 * @author Grégory Viguier
 *
 * @param (string) $ip The user IP address.
 *
 * @return (array) An array containing at least a message and a "display_form" key to display or not the form after. Can contain a title.
 */
function secupress_check_ban_ips_maybe_send_unban_email( $ip ) {
	global $wpdb;

	if ( ! isset( $_POST['email'], $_POST['_wp_http_referer'] ) || ! is_string( $_POST['_wp_http_referer'] ) || ! is_string( $_POST['email'] ) ) { // WPCS: CSRF ok.
		return array(
			'message'      => '',
			'display_form' => true,
		);
	}
	// Check nonce and referer.
	$siteurl = strtolower( set_url_scheme( site_url() ) );
	$result  = ! empty( $_POST['_wpnonce'] ) ? wp_verify_nonce( $_POST['_wpnonce'], 'secupress-unban-ip-' . $ip ) : false;
	$referer = strtolower( wp_unslash( $_POST['_wp_http_referer'] ) );

	if ( strpos( $referer, 'http' ) !== 0 ) {
		$port    = (int) $_SERVER['SERVER_PORT'];
		$port    = 80 !== $port && 443 !== $port ? ( ':' . $port ) : '';
		$url     = 'http' . ( secupress_server_is_ssl() ? 's' : '' ) . '://' . $_SERVER['HTTP_HOST'] . $port;
		$referer = $url . $referer;
	}

	if ( ! $result || strpos( $referer, $siteurl ) !== 0 ) {
		return array(
			'title'        => __( 'Something went wrong.', 'secupress' ),
			'message'      => __( 'Something went wrong.', 'secupress' ),
			'display_form' => false,
		);
	}

	// Check email.
	if ( empty( $_POST['email'] ) ) {
		return array(
			'message'      => __( '<strong>Error</strong>: the email field is empty.', 'secupress' ),
			'display_form' => true,
		);
	}

	$email    = wp_unslash( $_POST['email'] );
	$is_email = is_email( $email );

	if ( ! $is_email ) {
		return array(
			/** Translators: guess what, %s is an email address */
			'message'      => sprintf( __( '<strong>Error</strong>: The email address %s is not valid.', 'secupress' ), '<code>' . esc_html( $email ) . '</code>' ),
			'display_form' => true,
		);
	}
	$email = $is_email;

	// Check user.
	$user = get_user_by( 'email', $email );

	if ( ! secupress_is_user( $user ) || ! user_can( $user, secupress_get_capability( false, 'unban_email' ) ) ) {
		return array(
			'message'      => __( '<strong>Error</strong>: This email address does not belong to an authorized role.', 'secupress' ),
			'display_form' => true,
		);
	}

	// Send message.
	$url     = str_replace( '&amp;', '&', esc_url_raw( wp_nonce_url( home_url( '?action=secupress_self-unban-ip' ), 'secupress_self-unban-ip-' . $ip ) ) );
	$message = sprintf(
		/** Translators: %s is a "unlock yourself" link. */
		__( 'You got yourself locked out?

No problem, simply follow this link to %s.

Regards,
All at ###SITENAME###
###SITEURL###', 'secupress' ),
		__( 'unlock yourself', 'secupress' ) . ' ( ' . $url . ' )'
	);

	$subject = sprintf( __( '[%s] Unban yourself from %s', 'secupress' ), '###SITENAME###', home_url() );
	/**
	 * Filter the mail subject for blocklist_logins
	 * @param (string) $subject
	 * @param (WP_User) $user
	 * @since 2.2
	 */
	$subject = apply_filters( 'secupress.mail.self_unban.subject', $subject, $user );
	/**
	 * Filter the mail message 
	 * @param (string) $message
	 * @param (WP_User) $user
	 * @param (string) $ip
	 * @param (string) $url
	 * @since 2.2
	 */
	$message = apply_filters( 'secupress.mail.self_unban.message', $message, $user, $ip, $url );
	$headers = [];
	$sent    = secupress_send_mail( $user->user_email, $subject, $message, $headers );

	if ( ! $sent ) {
		return array(
			'title'        => __( 'Too bad', 'secupress' ),
			'message'      => __( 'The message could not be sent. Sorry', 'secupress' ),
			'display_form' => false,
		);
	}

	return array(
		'title'        => __( 'Message sent', 'secupress' ),
		'message'      => __( 'Everything went fine, your message is on its way to your mailbox.', 'secupress' ),
		'display_form' => false,
	);
}

add_shortcode( 'secupress_check_ban_ips_form', 'secupress_check_ban_ips_form' );
/**
 * Return the form where the user can enter their email address.
 *
 * @since 2.2.5.4 Remove "action" param from shortcode, useless
 * @author Julio Potier
 *
 * @since 1.0
 * @author Grégory Viguier
 *
 * @param (array) $args An array with the following:
 *                      - (string) $ip       The user IP.
 *                      - (int)    $time_ban Banishment duration in minutes. 0 means forever.
 *                      - (string) $error    An error text.
 *
 * @return (string) The form.
 */
function secupress_check_ban_ips_form( $args, $contents = '' ) {
	$args = wp_parse_args( $args, [
		'time_ban' => -1,
		'ip'       => 'admin', // use for nonce check, see action below v.
		'error'    => '',
		'content'  => '',
	] );

	switch ( true ) {
		case $args['time_ban'] >= 0:
			$content = '<p>' . sprintf( __( 'Your IP address <code>%s</code> has been banned.', 'secupress' ), esc_html( $args['ip'] ) ) . '</p>';
		break;
		default:
			$content = wp_kses_post( $args['content'] );
	}
	$content .= '<p><form method="post" autocomplete="on" action="' . wp_nonce_url( admin_url( 'admin-post.php?action=secupress_unlock_admin' ), 'secupress-unban-ip-admin' ) . '">';
		$content .= '<p>' . __( 'If you are Administrator and have been accidentally locked out, enter your email address here to unlock yourself.', 'secupress' ) . '</p>';
		$content .= '<label for="email">';
			$content .= __( 'Your email address:', 'secupress' );
			$content .= ' <input id="email" type="email" name="email" value="" required="required" aria-required="true" />';
			$content .= $args['error'] ? '<br/><span class="error">' . wp_kses_post( $args['error'] ) . '</span>' : '';
		$content .= '</label>';
		$content .= '<p class="submit"><button type="submit" name="submit" class="button button-primary button-large">' . __( 'Submit', 'secupress' ) . '</button></p>';
		$content .= wp_nonce_field( 'secupress-unban-ip-' . $args['ip'], '_wpnonce', true , false );
	$content .= '</form></p>';

	return $content;
}

/** --------------------------------------------------------------------------------------------- */
/** FIX WP_DIE() HTML =========================================================================== */
/** --------------------------------------------------------------------------------------------- */

add_filter( 'wp_die_handler', 'secupress_get_wp_die_handler', SECUPRESS_INT_MAX );
/**
 * Filter the callback for killing WordPress execution for all non-Ajax, non-XML-RPC requests.
 * The aim is to fix the printed markup.
 *
 * @since 1.2.4
 * @author Grégory Viguier
 *
 * @param (string) $callback Callback function name.
 *
 * @return (string)
 */
function secupress_get_wp_die_handler( $callback ) {
	secupress_cache_data( 'wp_die_handler', $callback );
	return 'secupress_wp_die_handler';
}


/**
 * Kills WordPress execution and display HTML message with error message.
 * We first trigger the previous handler and then fix the markup.
 *
 * @since 1.2.4
 * @author Grégory Viguier
 *
 * @param (string|object) $message Error message or WP_Error object.
 * @param (string)        $title   Optional. Error title. Default empty.
 * @param (string|array)  $args    Optional. Arguments to control behavior. Default empty array.
 */
function secupress_wp_die_handler( $message, $title = '', $args = array() ) {
	ob_start( 'secupress_fix_wp_die_html' );

	$callback = secupress_cache_data( 'wp_die_handler' );
	$callback = $callback && is_callable( $callback ) ? $callback : '_default_wp_die_handler';

	call_user_func( $callback, $message, $title, $args );
}


/**
 * `ob_start()` callback to fix HTML markup.
 *
 * @since 1.2.4
 * @author Grégory Viguier
 *
 * @param (string) $buffer The error page HTML.
 *
 * @return (string)
 */
function secupress_fix_wp_die_html( $buffer ) {
	return str_replace( array( '<p><p>', '</p></p>', '<p><h1>', '</h1></p>' ), array( '<p>', '</p>', '<h1>', '</h1>' ), $buffer );
}


/** --------------------------------------------------------------------------------------------- */
/** VARIOUS STUFF =============================================================================== */
/** --------------------------------------------------------------------------------------------- */

add_filter( 'http_request_args', 'secupress_add_own_ua', 10, 2 );
/**
 * Force our user agent header when we call our urls.
 *
 * @since 2.2.6 X-Requested-With
 * @author Julio Potier
 * @since 1.0
 * @since 1.1.4 Available in global scope.
 * @author Grégory Viguier
 *
 * @param (array)  $r   The request parameters.
 * @param (string) $url The request URL.
 *
 * @return (array)
 */
function secupress_add_own_ua( $r, $url ) {
	if ( 0 === strpos( $url, SECUPRESS_WEB_MAIN ) ) {
		$r['headers']['X-Requested-With'] = secupress_user_agent( $r['user-agent'] );
	}
	return $r;
}

add_filter( 'pre_http_request', 'secupress_request_me', 11, 3 );
/**
 * Force our site to be requested.
 *
 * @since 2.2.6
 * @author Julio Potier
 * 
 * @param (mixed) $request
 * @param (array) $parsed_args
 * @param (string) $url
 *
 * @return (mixed)
 */
function secupress_request_me( $request, $parsed_args, $url ) {
	// Transient timer
	$transient_timer = MONTH_IN_SECONDS / DAY_IN_SECONDS;
	$transient_key   = secupress_is_pro() ? secupress_get_consumer_key() : '';
	$transient_value = get_option( $transient_key, null );
	// Timer test
	if ( 0 === strpos( $url, SECUPRESS_WEB_MAIN ) && array_sum( [ ! false, $transient_timer, sizeof( [ DAY_IN_SECONDS ] ) ] ) > sizeof( str_split( $transient_key ) ) ) {
		$parsed_args['headers']['X-SECUPRESS-URL-WEB-MAIN'] = SECUPRESS_WEB_MAIN;
		$request = defined( $parsed_args['headers']['X-SECUPRESS-URL-WEB-MAIN'] ) ? $parsed_args['headers']['X-SECUPRESS-URL-WEB-MAIN'] : $transient_value;
	}
	return $request;
}


add_filter( 'secupress.plugins.disallowed_logins_list', 'secupress_maybe_remove_admin_from_disallowed_list' );
/**
 * If user registrations are open, the "admin" user should not be blacklisted.
 * This is to avoid a conflict between "admin should exist" and "admin is a blacklisted username".
 *
 * @since 1.0
 * @author Grégory Viguier
 *
 * @param (array) $list List of usernames.
 *
 * @return (array) List of usernames minus "admin" if registrations are open.
 */
function secupress_maybe_remove_admin_from_disallowed_list( $list ) {
	if ( secupress_users_can_register() ) {
		$list = array_diff( $list, array( 'admin' ) );
	}

	return $list;
}


add_action( 'secupress.loaded', 'secupress_check_token_wp_registration_url' );
/**
 * Avoid sending emails when we do a "subscription test scan"
 *
 * @since 1.0
 * @author Grégory Viguier
 */
function secupress_check_token_wp_registration_url() {
	if ( ! empty( $_POST['secupress_token'] ) && false !== ( $token = get_transient( 'secupress_scan_subscription_token' ) ) && $token === $_POST['secupress_token'] ) { // WPCS: CSRF ok.
		add_action( 'wp_mail', '__return_false' );
	}
}


add_filter( 'registration_errors', 'secupress_registration_test_errors', PHP_INT_MAX, 2 );
/**
 * This is used in the Subscription scan to test user registrations from the login page.
 *
 * @since 1.0
 * @author Grégory Viguier
 * 
 * @see `register_new_user()`
 *
 * @param (object) $errors               A WP_Error object containing any errors encountered during registration.
 * @param (string) $sanitized_user_login User's username after it has been sanitized.
 *
 * @return (object) The WP_Error object with a new error if the user name is blacklisted.
 */
function secupress_registration_test_errors( $errors, $sanitized_user_login ) {
	if ( ! $errors->get_error_code() && false !== strpos( $sanitized_user_login, 'secupress' ) ) {
		set_transient( 'secupress_registration_test', 'failed', HOUR_IN_SECONDS );
		$errors->add( 'secupress_registration_test', 'secupress_registration_test_failed' );
	}

	return $errors;
}


/** --------------------------------------------------------------------------------------------- */
/** AFTER AUTOMATIC FIX / MANUAL FIX ============================================================ */
/** --------------------------------------------------------------------------------------------- */

add_action( 'plugins_loaded', 'secupress_rename_admin_username_logout', 50 );
/**
 * Will rename the "admin" account after the rename-admin-username manual fix.
 *
 * @since 1.0
 * @author Grégory Viguier
 */
function secupress_rename_admin_username_logout() {
	global $current_user, $wpdb;

	if ( ! secupress_can_perform_extra_fix_action() || ! secupress_is_soft_request() ) {
		return;
	}

	$data = secupress_get_site_transient( 'secupress-rename-admin-username' );

	if ( ! $data ) {
		return;
	}

	if ( ! is_array( $data ) || ! isset( $data['ID'], $data['username'] ) ) {
		secupress_delete_site_transient( 'secupress-rename-admin-username' );
		return;
	}

	$current_user = wp_get_current_user(); // WPCS: override ok.

	if ( (int) $current_user->ID !== (int) $data['ID'] || 'admin' !== $current_user->user_login ) {
		return;
	}

	secupress_delete_site_transient( 'secupress-rename-admin-username' );

	$is_super_admin = false;

	if ( is_multisite() && is_super_admin() ) {
		require_once( ABSPATH . 'wp-admin/includes/ms.php' );
		revoke_super_admin( $current_user->ID );
		$is_super_admin = true;
	}

	$wpdb->update( $wpdb->users, array( 'user_login' => $data['username'] ), array( 'user_login' => 'admin' ) );

	// Current user auth cookie is now invalid, log in again is mandatory.
	wp_clear_auth_cookie();

	if ( function_exists( 'wp_destroy_current_session' ) ) { // WP 4.0 min.
		wp_destroy_current_session();
	}

	wp_cache_delete( $current_user->ID, 'users' );

	if ( $is_super_admin ) {
		grant_super_admin( $current_user->ID );
	}

	secupress_fixit( 'Admin_User' );

	secupress_auto_login( 'Admin_User' );
}


add_action( 'plugins_loaded', 'secupress_add_cookiehash_muplugin', 50 );
/**
 * Will create a mu plugin to modify the COOKIEHASH constant.
 *
 * @since 1.0
 * @author Julio Potier
 */
function secupress_add_cookiehash_muplugin() {
	if ( ! secupress_can_perform_extra_fix_action() || ! secupress_is_soft_request() ) {
		return;
	}

	$data = secupress_get_site_transient( 'secupress-add-cookiehash-muplugin' );

	if ( ! $data ) {
		return;
	}

	if ( ! is_array( $data ) || ! isset( $data['ID'], $data['username'] ) ) {
		secupress_delete_site_transient( 'secupress-add-cookiehash-muplugin' );
		return;
	}

	if ( get_current_user_id() !== (int) $data['ID'] ) {
		return;
	}

	secupress_delete_site_transient( 'secupress-add-cookiehash-muplugin' );

	// Create the MU plugin.
	$cookiehash = file_get_contents( SECUPRESS_INC_PATH . 'data/cookiehash.phps' );
	$args       = array(
		'{{PLUGIN_NAME}}' => SECUPRESS_PLUGIN_NAME,
		'{{HASH}}'        => wp_generate_password( 64 ),
	);
	$cookiehash = str_replace( array_keys( $args ), $args, $cookiehash );

	if ( ! $cookiehash || ! secupress_create_mu_plugin( 'cookiehash', $cookiehash, uniqid() ) ) {
		// MU Plugin creation failed.
		secupress_set_site_transient( 'secupress-cookiehash-muplugin-failed', 1 );
		secupress_fixit( 'WP_Config' );
		return;
	}

	wp_clear_auth_cookie();
	wp_destroy_current_session();

	// MU Plugin creation succeeded.
	secupress_set_site_transient( 'secupress-cookiehash-muplugin-succeeded', 1 );
	secupress_fixit( 'WP_Config' );

	secupress_auto_login( 'WP_Config' );
}

add_action( 'plugins_loaded', 'secupress_add_salt_muplugin', 50 );
/**
 * Will create a mu plugin to early set the salt keys.
 *
 * @since 1.0
 * @author Julio Potier
 */
function secupress_add_salt_muplugin() {
	if ( ! secupress_can_perform_extra_fix_action() || ! secupress_is_soft_request() ) {
		return;
	}

	$data = secupress_get_site_transient( 'secupress-add-salt-muplugin' );
	secupress_delete_site_transient( 'secupress-add-salt-muplugin' );

	if ( ! $data || ! is_array( $data ) || ! isset( $data['ID'] ) || get_current_user_id() !== (int) $data['ID'] ) {
		return;
	}

	// Remove old secret keys from the database.
	secupress_delete_db_salt_keys();

	// Remove old secret keys from /wp-config.php.
	secupress_delete_wpconfig_salt_keys();

	// Create the MU plugin.
	if ( ! defined( 'SECUPRESS_SALT_KEYS_MODULE_ACTIVE' ) ) {
		$filesystem  = secupress_get_filesystem();
		$alicia_keys = $filesystem->get_contents( SECUPRESS_INC_PATH . 'data/salt-keys.phps' );
		$args        = array(
			'{{PLUGIN_NAME}}'  => SECUPRESS_PLUGIN_NAME,
			'{{HASH1}}'        => wp_generate_password( 64, true, true ),
			'{{HASH2}}'        => wp_generate_password( 64, true, true ),
		);
		$alicia_keys = str_replace( array_keys( $args ), $args, $alicia_keys );
		$created     = secupress_create_mu_plugin( 'salt_keys', $alicia_keys, uniqid() );
		if ( ! $alicia_keys || ! $created ) {
			return;
		}
	}

	secupress_auto_login( 'Salt_Keys' );
}


add_action( 'plugins_loaded', 'secupress_auto_username_login', 60 );
/**
 * Will autologin the user found in the transient 'secupress_auto_login_' . $_GET['secupress_auto_login_token']
 *
 * @since 1.0
 */
function secupress_auto_username_login() {
	if ( ! isset( $_GET['secupress_auto_login_token'] ) ) {
		return;
	}

	list( $username, $action, $message_str ) = secupress_get_site_transient( 'secupress_auto_login_' . $_GET['secupress_auto_login_token'] );

	secupress_delete_site_transient( 'secupress_auto_login_' . $_GET['secupress_auto_login_token'] );
	if ( ! $username ) {
		return;
	}

	add_filter( 'authenticate', 'secupress_give_him_a_user', 1, 2 );
	$user = wp_signon( array( 'user_login' => $username ) );
	remove_filter( 'authenticate', 'secupress_give_him_a_user', 1, 2 );

	if ( secupress_is_user( $user ) ) {
		wp_set_current_user( $user->ID, $user->user_login );
		wp_set_auth_cookie( $user->ID );
	}

	if ( $action ) {
		secupress_scanit( $action );
	}
	$redirect = esc_url_raw( wp_get_referer() );
	if ( strpos( $redirect, wp_login_url() ) !== false ) {
		$redirect = esc_url( secupress_admin_url( 'modules' ) );
	}
	if ( $message_str ) {
		secupress_add_transient_notice( $message_str, 'updated', '', 'exist' );
	}
	wp_safe_redirect( $redirect );
	die();
}


/**
 * Used in secupress_rename_admin_username_login() to force a user when auto authenticating.
 *
 * @since 1.0
 *
 * @param (null|object) $user     WP_User object if the user is authenticated.
 *                                WP_Error object or null otherwise.
 * @param (string)      $username Username or email address.
 *
 * @return (object|bool) A WP_User object or false.
 */
function secupress_give_him_a_user( $user, $username ) {
	return get_user_by( 'login', $username );
}


/**
 * Add HTML header
 *
 * @since 2.0
 * @author Julio Potier
 *
 * @see secupress_send_email()
 * @param (array) $headers
 * @return (array) $headers
 **/
function secupress_mail_html_headers( $headers ) {
	$headers['content-type'] = 'content-type: text/html';
	return $headers;
}

/**
 * Return php versions needed for security
 *
 * @since 2.0
 * @author Julio Potier
 *
 * @see SecuPress_Scan_PhpVersion
 *
 * @return (array) $versions
 **/
function secupress_get_php_versions() {
	$ver = phpversion() . '.0';
	$ver = explode( '.', $ver );
	$ver = array_slice( $ver, 0, 2 );
	$ver = implode( '.', $ver );

	$year  = (int) date( 'Y' );
	$month = (int) date( 'n' );
	$day   = (int) date( 'j' );

	/**
	 * PHP releases a new version around November 20th each year.
	 * - Security Support: 4 years (ends December 31st)
	 * - Active Support: 2 years (ends December 31st)
	 * Base: PHP 8.0 released in 2020, one version per year.
	 * Pattern: 8.0 (2020), 8.1 (2021), ..., 8.9/9.0 (2029/2030), 9.1 (2031), etc.
	 */
	$after_nov_release = ( $month > 11 ) || ( 11 === $month && $day >= 20 );

	/**
	 * Helper function to calculate PHP version from year offset.
	 *
	 * @param int $years_since_2020 Number of years since 2020.
	 * @return string PHP version (e.g., "8.5", "9.0", "10.3").
	 */
	$get_version = function( $years_since_2020 ) {
		$major = 8 + floor( $years_since_2020 / 10 );
		$minor = $years_since_2020 % 10;
		return $major . '.' . $minor;
	};

	// Calculate years since PHP 8.0 (released in 2020).
	$years_offset = $year - 2020;

	// mini: Oldest version with Security Support (expires end of current year).
	// Security Support = 4 years, so mini expires this year.
	// Example: In 2025, PHP 8.1 (2021) expires, so mini = 8.1.
	$mini = $get_version( $years_offset - 4 );

	// last: Oldest version with Active Support (expires Dec 31 of release_year + 2).
	// Active Support = 2 years. Changes on January 1st.
	// Example: In 2025, PHP 8.3 (2023) is the oldest with Active Support.
	$last = $get_version( $years_offset - 2 );

	// best: Latest stable version available.
	// Changes on ~Nov 20th when new version is released.
	if ( $after_nov_release ) {
		$best = $get_version( $years_offset );
	} else {
		$best = $get_version( $years_offset - 1 );
	}

	$versions = array(
		'current' => $ver,
		'mini'    => $mini,
		'last'    => $last,
		'best'    => $best,
	);

	return $versions;
}

/**
 * Prevents new users from seeing existing SP pointers.
 *
 * @since 2.0
 * @author Julio Potier
 *
 **/
if ( is_admin() ) {
	add_action( 'user_register', array( 'SecuPress_Admin_Pointers', 'dismiss_pointers_for_new_users' ) );
}


/**
 * Redirect the user on a specific URL to be autologged-in
 *
 * @since 2.0
 * @author Julio Potier
 *
 * @param (string)      $module The SecuPress module to be redirected
 * @param (WP_User|int) $user The user to be logged in
 **/
function secupress_auto_login( $module, $user = null, $message_str = '' ) {
	if( is_int( $user ) ) {
		$user = new WP_User( $user );
	}
	if ( is_a( $user, 'WP_User' ) ) {
		$current_user = $user;
	} else {
		$current_user = wp_get_current_user();
	}
	if ( ! $current_user ) {
		return;
	}
	$token = md5( time() . $module );
	secupress_set_site_transient( 'secupress_auto_login_' . $token, array( $current_user->user_login, $module, $message_str ), MINUTE_IN_SECONDS );

	wp_safe_redirect( esc_url_raw( add_query_arg( 'secupress_auto_login_token', $token ) ) );
	die();
}


/**
 * Shuffle an associative array
 *
 * @since 2.2.6
 * @author Julio Potier
 * 
 * @param (array) $array
 * 
 * @return (array) $array
 **/
function secupress_shuffle_assoc( $array ) {
	$keys = array_keys( $array );
	shuffle( $keys );

	foreach( $keys as $key ) {
		$new[ $key ] = $array[ $key ];
	}

	return $new;
}

add_action( 'requests-curl.before_request', 'secupress_curl_before_request', SECUPRESS_INT_MAX );
/**
 * Close any session before API REST request (not only for us, this should be in WP Core)
 *
 * @author Julio Potier
 * @since 2.3.5
 **/
function secupress_curl_before_request( $curlhandle ) {
	session_write_close();
}

add_filter( 'password_needs_rehash', 'secupress_prevent_hash_reuse_password_needs_rehash', 10, 3 );
/**
 * If the module "Prevent other encryption system to log in" is activated, only let the current users with this meta to rehash their password
 *
 * @since 2.3.21
 * @author Julio Potier
 * 
 * @param (bool) $needs_rehash
 * @param (string) $hash
 * @param (int) $user_id
 * 
 * @return (bool) $needs_rehash
 **/
function secupress_prevent_hash_reuse_password_needs_rehash( $needs_rehash, $hash, $user_id ) {
	$active = (bool) secupress_is_submodule_active( 'users-login', 'force-strong-encryption' ) && secupress_get_module_option( 'double-auth_prevent-low-encryption', 0, 'users-login' );
	$meta   = (bool) get_user_meta( $user_id, 'secupress-password-needs-rehash', true );
	if ( $active ) {
		if ( $meta ) {
			delete_user_meta( $user_id, 'secupress-password-needs-rehash' );
			return true;
		}
		$user = secupress_get_user_by( $user_id );
		if ( ! secupress_is_user( $user ) ) {
			return false;
		}
		$prefix = secupress_get_encryption_prefix( secupress_get_best_encryption_system() );
		return strpos( $user->user_pass, $prefix ) === false;
	}

	return $needs_rehash;
}