two-factor-extended/includes/class-provider-filter.php
2026-03-28 09:59:53 +00:00

310 lines
8 KiB
PHP

<?php
/**
* Provider filter class
*
* @package TwoFactorExtended
* @since 0.1.0
*/
// Prevent direct access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Two_Factor_Extended_Provider_Filter
*
* Filters visible 2FA providers based on user roles.
*
* @since 0.1.0
*/
class Two_Factor_Extended_Provider_Filter {
/**
* Constructor.
*
* @since 0.1.0
*/
public function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @since 0.1.0
*/
private function init_hooks(): void {
// Filter available providers for user.
add_filter( 'two_factor_providers', array( $this, 'filter_providers_by_role' ), 10, 1 );
// Add explanation text in user profile.
add_action( 'show_user_profile', array( $this, 'render_visibility_explanation' ), 5 );
add_action( 'edit_user_profile', array( $this, 'render_visibility_explanation' ), 5 );
}
/**
* Filter providers based on user role.
*
* @since 0.1.0
*
* @param array<string, object> $providers Available providers.
*
* @return array<string, object> Filtered providers.
*/
public function filter_providers_by_role( array $providers ): array {
// IMPORTANT: Only filter providers on user profile pages, NOT on admin settings pages.
// Check if we're on a user profile/edit page.
global $pagenow;
$is_profile_page = ( 'profile.php' === $pagenow || 'user-edit.php' === $pagenow );
// Don't filter on our settings page or other admin pages.
if ( ! $is_profile_page ) {
return $providers;
}
// Get current user being edited.
$user_id = $this->get_profile_user_id();
if ( ! $user_id ) {
return $providers;
}
// Get visible providers for user.
$visible_providers = $this->get_visible_providers_for_user( $user_id );
// If no visibility rules, show all providers.
if ( null === $visible_providers ) {
return $providers;
}
// Filter providers.
$filtered = array();
foreach ( $providers as $class_name => $provider ) {
// Always show required providers.
if ( $this->is_required_provider( $user_id, $class_name ) ) {
$filtered[ $class_name ] = $provider;
continue;
}
// Show if visible for user's roles.
if ( in_array( $class_name, $visible_providers, true ) ) {
$filtered[ $class_name ] = $provider;
}
}
return $filtered;
}
/**
* Get visible providers for user based on their roles.
*
* @since 0.1.0
*
* @param int $user_id User ID.
*
* @return array<int, string>|null Array of visible provider class names, or null if no rules.
*/
public function get_visible_providers_for_user( int $user_id ): ?array {
$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['provider_visibility'] ) || ! is_array( $settings['provider_visibility'] ) ) {
return null;
}
$visible = array();
// Collect visible providers from all user roles.
foreach ( $user_roles as $role ) {
if ( isset( $settings['provider_visibility'][ $role ] ) ) {
$role_visible = $settings['provider_visibility'][ $role ];
if ( is_array( $role_visible ) ) {
foreach ( $role_visible as $provider ) {
if ( is_string( $provider ) ) {
$visible[] = $provider;
}
}
}
}
}
// If no visibility rules for any role, return null (show all).
if ( empty( $visible ) ) {
return null;
}
return array_values( array_unique( $visible ) );
}
/**
* Check if provider is required for user.
*
* @since 0.1.0
*
* @param int $user_id User ID.
* @param string $provider_class Provider class name.
*
* @return bool True if provider is required.
*/
private function is_required_provider( int $user_id, string $provider_class ): bool {
$enforcement = two_factor_extended()->get_enforcement();
if ( ! $enforcement ) {
return false;
}
$required_providers = $enforcement->get_required_providers_for_user( $user_id );
return in_array( $provider_class, $required_providers, true );
}
/**
* Get user ID from profile page context.
*
* Uses filter_input() for safe GET parameter access when determining
* which user's profile is being viewed. This is a read-only operation
* used for displaying the correct 2FA provider options.
*
* @since 0.1.0
*
* @return int User ID.
*/
private function get_profile_user_id(): int {
// Check if editing another user's profile (e.g., wp-admin/user-edit.php?user_id=123).
// Use filter_input() for safe access to GET parameter.
$user_id = filter_input( INPUT_GET, 'user_id', FILTER_VALIDATE_INT );
if ( $user_id && $user_id > 0 ) {
return $user_id;
}
// Check if editing own profile.
return get_current_user_id();
}
/**
* Render visibility explanation in user profile.
*
* @since 0.1.0
*
* @param WP_User $user User object.
*/
public function render_visibility_explanation( WP_User $user ): void {
$visible_providers = $this->get_visible_providers_for_user( $user->ID );
// No visibility rules set.
if ( null === $visible_providers ) {
return;
}
$all_providers = Two_Factor_Extended_Provider_Detector::get_all_providers();
$enforcement = two_factor_extended()->get_enforcement();
$provider_names = Two_Factor_Extended_Provider_Detector::get_provider_names();
$required_providers = null !== $enforcement
? $enforcement->get_required_providers_for_user( $user->ID )
: array();
// Calculate hidden providers.
$hidden_providers = array();
foreach ( array_keys( $all_providers ) as $class ) {
// Skip required providers (always visible).
if ( in_array( $class, $required_providers, true ) ) {
continue;
}
// Not in visible list = hidden.
if ( ! in_array( $class, $visible_providers, true ) ) {
$hidden_providers[] = $provider_names[ $class ] ?? $class;
}
}
if ( empty( $hidden_providers ) ) {
return;
}
?>
<div class="notice notice-info inline">
<p>
<strong><?php esc_html_e( 'Two-Factor Extended:', 'two-factor-extended' ); ?></strong>
<?php
printf(
/* translators: %s: List of hidden providers */
esc_html__( 'Some 2FA methods are hidden based on your role: %s', 'two-factor-extended' ),
'<strong>' . esc_html( implode( ', ', $hidden_providers ) ) . '</strong>'
);
?>
</p>
</div>
<?php
}
/**
* Get provider visibility inheritance for multiple roles.
*
* Implements union logic: user sees providers visible to ANY of their roles.
*
* @since 0.1.0
*
* @param array<string> $roles Array of role slugs.
*
* @return array<string> Array of visible provider class names.
*/
public function get_inherited_visibility( array $roles ): array {
$settings = get_option( TWO_FACTOR_EXTENDED_OPTION_SETTINGS, array() );
$visible = array();
if ( ! is_array( $settings ) || ! isset( $settings['provider_visibility'] ) || ! is_array( $settings['provider_visibility'] ) ) {
return array();
}
foreach ( $roles as $role ) {
if ( isset( $settings['provider_visibility'][ $role ] ) ) {
$role_visible = $settings['provider_visibility'][ $role ];
if ( is_array( $role_visible ) ) {
foreach ( $role_visible as $provider ) {
if ( is_string( $provider ) ) {
$visible[] = $provider;
}
}
}
}
}
return array_values( array_unique( $visible ) );
}
/**
* Check if provider is visible for user.
*
* @since 0.1.0
*
* @param int $user_id User ID.
* @param string $provider_class Provider class name.
*
* @return bool True if provider is visible, false otherwise.
*/
public function is_provider_visible( int $user_id, string $provider_class ): bool {
// Required providers are always visible.
if ( $this->is_required_provider( $user_id, $provider_class ) ) {
return true;
}
$visible_providers = $this->get_visible_providers_for_user( $user_id );
// No visibility rules = all visible.
if ( null === $visible_providers ) {
return true;
}
return in_array( $provider_class, $visible_providers, true );
}
}