two-factor-extended/includes/class-enforcement.php
2026-03-28 10:06:01 +00:00

415 lines
12 KiB
PHP

<?php
/**
* 2FA requirement enforcement class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Enforcement
*
* Enforces 2FA requirements based on user roles.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Enforcement {
/**
* User meta key for enforcement start date.
*
* @since 0.1.0
* @var string
*/
const META_ENFORCEMENT_START = 'two_factor_extended_enforcement_start';
/**
* User meta key for grace period notified.
*
* @since 0.1.0
* @var string
*/
const META_GRACE_NOTIFIED = 'two_factor_extended_grace_notified';
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
add_filter( 'authenticate', array( $this, 'check_user_requirements' ), 30, 1 );
add_action( 'admin_notices', array( $this, 'display_user_notices' ) );
add_action( 'two_factor_extended_daily_check', array( $this, 'check_all_users_compliance' ) );
}
/**
* Check user's 2FA requirements on login.
*
* @since 0.1.0
*
* @param WP_User|WP_Error|null $user User object or error.
*
* @return WP_User|WP_Error|null User object or error if requirements not met.
*/
public function check_user_requirements( $user ) {
// Skip if not a user object.
if ( ! $user instanceof WP_User ) {
return $user;
}
// Skip if Two Factor not available.
if ( ! class_exists( 'Two_Factor_Core' ) ) {
return $user;
}
// Get required providers for user.
$required_providers = $this->get_required_providers_for_user( $user->ID );
if ( empty( $required_providers ) ) {
return $user;
}
// Check if user is in grace period.
if ( $this->is_in_grace_period( $user->ID ) ) {
$this->set_grace_period_notice( $user->ID );
return $user;
}
// Check if user meets requirements.
if ( ! $this->user_meets_requirements( $user->ID, $required_providers ) ) {
return new WP_Error(
'two_factor_extended_required',
sprintf(
/* translators: %s: Required providers list */
__( 'Your account requires the following 2FA methods to be configured: %s. Please contact an administrator for assistance.', 'two-factor-extended' ),
implode( ', ', $this->get_provider_labels( $required_providers ) )
)
);
}
return $user;
}
/**
* Get required providers for user based on their roles.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return array<int, string> Array of required provider class names.
*/
public function get_required_providers_for_user( int $user_id ): array {
$required = array();
// Check if user is super admin (Multisite).
if ( is_multisite() && Two_Factor_Extended_Role_Manager::is_super_admin( $user_id ) ) {
$network_settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
if ( ! is_array( $network_settings ) ) {
$network_settings = array();
}
if ( isset( $network_settings['super_admin_requirements'] ) && is_array( $network_settings['super_admin_requirements'] ) ) {
foreach ( $network_settings['super_admin_requirements'] as $provider ) {
if ( is_string( $provider ) ) {
$required[] = $provider;
}
}
}
}
// Check network-wide settings (Multisite).
if ( is_multisite() ) {
$network_settings = get_site_option( TWO_FACTOR_EXTENDED_NETWORK_OPTION_SETTINGS, array() );
if ( ! is_array( $network_settings ) ) {
$network_settings = array();
}
if ( ! empty( $network_settings['enforce_network_wide'] ) ) {
// Network enforcement is enabled.
$user_roles = Two_Factor_Extended_Role_Manager::get_user_roles( $user_id );
if ( isset( $network_settings['role_requirements'] ) && is_array( $network_settings['role_requirements'] ) ) {
foreach ( $user_roles as $role ) {
if ( isset( $network_settings['role_requirements'][ $role ] ) && is_array( $network_settings['role_requirements'][ $role ] ) ) {
foreach ( $network_settings['role_requirements'][ $role ] as $provider ) {
if ( is_string( $provider ) ) {
$required[] = $provider;
}
}
}
}
}
// If site override is not allowed, return only network requirements.
if ( empty( $network_settings['allow_site_override'] ) ) {
$network_required = array_values( array_unique( $required ) );
// Intersect with globally-available providers (Two Factor 0.16+).
if ( class_exists( 'Two_Factor_Core' ) ) {
$available = array_keys( Two_Factor_Core::get_providers() );
$network_required = ! empty( $available ) ? array_values( array_intersect( $network_required, $available ) ) : $network_required;
}
return $network_required;
}
}
}
// Get site-level requirements.
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
if ( ! is_array( $settings ) ) {
$settings = array();
}
$user_roles = Two_Factor_Extended_Role_Manager::get_user_roles( $user_id );
if ( ! empty( $user_roles ) && isset( $settings['role_requirements'] ) && is_array( $settings['role_requirements'] ) ) {
foreach ( $user_roles as $role ) {
if ( isset( $settings['role_requirements'][ $role ] ) ) {
$role_requirements = $settings['role_requirements'][ $role ];
if ( is_array( $role_requirements ) ) {
foreach ( $role_requirements as $provider ) {
if ( is_string( $provider ) ) {
$required[] = $provider;
}
}
}
}
}
}
$required = array_values( array_unique( $required ) );
// Intersect with globally-available providers (Two Factor 0.16+).
// If Two-Factor has disabled a provider site-wide, it cannot be enforced.
if ( class_exists( 'Two_Factor_Core' ) ) {
$available = array_keys( Two_Factor_Core::get_providers() );
if ( ! empty( $available ) ) {
$required = array_values( array_intersect( $required, $available ) );
}
}
return $required;
}
/**
* Check if user meets 2FA requirements.
*
* @since 0.1.0
*
* @param int $user_id User ID.
* @param array<int, string> $required_providers Required provider class names.
*
* @return bool True if user meets requirements, false otherwise.
*/
public function user_meets_requirements( int $user_id, array $required_providers ): bool {
if ( empty( $required_providers ) ) {
return true;
}
$enabled_providers = Two_Factor_Extended_Provider_Detector::get_user_enabled_providers( $user_id );
foreach ( $required_providers as $provider_class ) {
if ( ! isset( $enabled_providers[ $provider_class ] ) ) {
return false;
}
}
return true;
}
/**
* Check if user is in grace period.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return bool True if in grace period, false otherwise.
*/
public function is_in_grace_period( int $user_id ): bool {
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
if ( ! is_array( $settings ) ) {
$settings = array();
}
$grace_days = isset( $settings['grace_period_days'] ) && is_numeric( $settings['grace_period_days'] ) ? (int) $settings['grace_period_days'] : 0;
// No grace period configured.
if ( 0 === $grace_days ) {
return false;
}
$start_date = get_user_meta( $user_id, self::META_ENFORCEMENT_START, true );
// No enforcement start date set - set it now.
if ( empty( $start_date ) ) {
update_user_meta( $user_id, self::META_ENFORCEMENT_START, time() );
return true;
}
$days_elapsed = ( time() - ( is_numeric( $start_date ) ? (int) $start_date : 0 ) ) / DAY_IN_SECONDS;
return $days_elapsed < $grace_days;
}
/**
* Get remaining grace period days.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return int Remaining days in grace period.
*/
public function get_grace_period_remaining_days( int $user_id ): int {
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
if ( ! is_array( $settings ) ) {
$settings = array();
}
$grace_days = isset( $settings['grace_period_days'] ) && is_numeric( $settings['grace_period_days'] ) ? (int) $settings['grace_period_days'] : 0;
$start_date = get_user_meta( $user_id, self::META_ENFORCEMENT_START, true );
if ( empty( $start_date ) || 0 === $grace_days ) {
return 0;
}
$days_elapsed = ( time() - ( is_numeric( $start_date ) ? (int) $start_date : 0 ) ) / DAY_IN_SECONDS;
$remaining = $grace_days - $days_elapsed;
return max( 0, (int) ceil( $remaining ) );
}
/**
* Set grace period notice flag.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*/
private function set_grace_period_notice( int $user_id ): void {
update_user_meta( $user_id, self::META_GRACE_NOTIFIED, time() );
}
/**
* Display user notices for 2FA requirements.
*
* @since 0.1.0
*/
public function display_user_notices(): void {
$user_id = get_current_user_id();
if ( ! $user_id ) {
return;
}
$required_providers = $this->get_required_providers_for_user( $user_id );
if ( empty( $required_providers ) ) {
return;
}
// Check if user meets requirements.
if ( $this->user_meets_requirements( $user_id, $required_providers ) ) {
return;
}
// Check if in grace period.
if ( $this->is_in_grace_period( $user_id ) ) {
$remaining_days = $this->get_grace_period_remaining_days( $user_id );
?>
<div class="notice notice-warning">
<p>
<strong><?php esc_html_e( 'Two-Factor Extended:', 'two-factor-extended' ); ?></strong>
<?php
printf(
esc_html(
/* translators: 1: Number of days, 2: Required providers list */
_n(
'You have %1$d day remaining to configure the required 2FA methods: %2$s',
'You have %1$d days remaining to configure the required 2FA methods: %2$s',
$remaining_days,
'two-factor-extended'
)
),
(int) $remaining_days,
esc_html( implode( ', ', $this->get_provider_labels( $required_providers ) ) )
);
?>
</p>
<p>
<a href="<?php echo esc_url( admin_url( 'profile.php#two-factor-options' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Configure 2FA Now', 'two-factor-extended' ); ?>
</a>
</p>
</div>
<?php
} else {
?>
<div class="notice notice-error">
<p>
<strong><?php esc_html_e( 'Two-Factor Extended:', 'two-factor-extended' ); ?></strong>
<?php
printf(
/* translators: %s: Required providers list */
esc_html__( 'Your account requires the following 2FA methods: %s. Your access may be restricted until you configure them.', 'two-factor-extended' ),
esc_html( implode( ', ', $this->get_provider_labels( $required_providers ) ) )
);
?>
</p>
<p>
<a href="<?php echo esc_url( admin_url( 'profile.php#two-factor-options' ) ); ?>" class="button button-primary">
<?php esc_html_e( 'Configure 2FA Now', 'two-factor-extended' ); ?>
</a>
</p>
</div>
<?php
}
}
/**
* Get provider labels from class names.
*
* @since 0.1.0
*
* @param array<int, string> $provider_classes Array of provider class names.
*
* @return array<int, string> Array of provider labels.
*/
private function get_provider_labels( array $provider_classes ): array {
$names = Two_Factor_Extended_Provider_Detector::get_provider_names();
$labels = array();
foreach ( $provider_classes as $class ) {
$labels[] = $names[ $class ] ?? $class;
}
return $labels;
}
/**
* Check compliance for all users (scheduled task).
*
* @since 0.1.0
*/
public function check_all_users_compliance(): void {
// This will be implemented for reporting/audit purposes.
// For now, it's a placeholder for the daily scheduled check.
}
}