Files
krolewskie-miody.pl/wp-content/plugins/really-simple-ssl/security/wordpress/two-fa/class-rsssl-two-factor-profile-settings.php
2026-04-28 15:13:50 +02:00

518 lines
21 KiB
PHP

<?php
/**
* Holds the logic for the profile page.
*
* @package REALLY_SIMPLE_SSL
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use Exception;
use RSSSL\Pro\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Totp;
use RSSSL\Pro\Security\WordPress\Two_Fa\Rsssl_Two_Factor_Backup_Codes;
use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Provider_Loader;
use RSSSL\Security\WordPress\Two_Fa\Providers\Rsssl_Two_Factor_Email;
use RSSSL\Security\WordPress\Two_Fa\Traits\Rsssl_Two_Fa_Helper;
use WP_User;
if (!class_exists('Rsssl_Two_Factor_Profile_Settings')) {
/**
* Class Rsssl_Two_Factor_Profile_Settings
*
* This class is responsible for handling the Two-Factor Authentication settings on the user profile page.
*
* @package REALLY_SIMPLE_SSL
*/
class Rsssl_Two_Factor_Profile_Settings
{
use Rsssl_Two_Fa_Helper;
/**
* Instance of this class.
*
* @var Rsssl_Two_Factor_Profile_Settings
*/
private static $instance = null;
/**
* The available providers.
*
* @var array $available_providers An array to store the available providers.
*/
private $available_providers = array();
/**
* The forced Two-Factor Authentication roles.
*
* @var array $forced_two_fa An array to store the forced Two-Factor Authentication roles.
*/
private array $forced_two_fa = array();
/**
* Get instance of this class.
*
* @return Rsssl_Two_Factor_Profile_Settings
*/
public static function get_instance() {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Constructor for the class.
*
* If the user is logged in, retrieve the user object and check if two-factor authentication is turned on for the user.
* If two-factor authentication is enabled, add the necessary hooks.
*
* @return void
*/
private function __construct() {
if ( is_user_logged_in() ) {
$user_id = get_current_user_id();
$user = get_user_by( 'ID', $user_id );
global $pagenow;
$relevant_ajax_actions = [ 'change_method_to_email', 'resend_email_code_profile' ];
if (
'profile.php' === $pagenow ||
( 'user-edit.php' === $pagenow && isset( $_GET['user_id'] ) ) ||
( defined( 'DOING_AJAX' ) && DOING_AJAX && isset( $_REQUEST['action'] ) && in_array( $_REQUEST['action'], $relevant_ajax_actions, true ) )
) {
if ( $this->validate_two_turned_on_for_user( $user ) ) {
add_action( 'admin_init', array( $this, 'add_hooks' ) );
}
}
}
}
/**
* Add hooks for user profile page.
*
* This method adds hooks to display the Two-Factor Authentication settings on user profile pages.
*
* @return void
*/
public function add_hooks(): void
{
if (is_user_logged_in()) {
$errors = Rsssl_Parameter_Validation::get_cached_errors(get_current_user_id());
if (!empty($errors)) {
// We display the errors.
foreach ($errors as $error) {
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
$error['message'],
$error['type']
);
}
}
}
add_action('show_user_profile', array($this, 'show_user_profile'));
add_action('edit_user_profile', array($this, 'show_user_profile'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
add_action('admin_enqueue_scripts', array($this, 'enqueue_styles'));
add_action('personal_options_update', array($this, 'save_user_profile'));
add_action('edit_user_profile_update', array($this, 'save_user_profile'));
add_action( 'wp_ajax_resend_email_code_profile', [$this, 'resend_email_code_profile_callback'] );
add_action( 'wp_ajax_change_method_to_email', [$this, 'start_email_validation_callback'] );
if (isset($_GET['profile'], $_GET['_wpnonce']) && rest_sanitize_boolean(wp_unslash($_GET['profile']))) {
self::set_active_provider(get_current_user_id(), 'email');
}
}
/**
* Resend the email code for the user.
*
* @return void
*/
public function resend_email_code_profile_callback(): void
{
// Check for nonce (make sure your nonce name and action match what you output to the page)
if ( ! isset( $_POST['login_nonce'] )
|| !wp_verify_nonce( sanitize_text_field( wp_unslash( $_POST['login_nonce'] ) ), 'update_user_two_fa_settings' ) ) {
wp_send_json_error( array( 'message' => __( 'Invalid nonce.', 'really-simple-ssl' ) ), 403 );
}
// Ensure the user is logged in.
if ( ! is_user_logged_in() ) {
wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 );
}
// Get the user ID.
$user_id = get_current_user_id();
$user = get_user_by( 'ID', $user_id );
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true);
wp_send_json_success( array( 'message' => __('Verification code re-sent', 'really-simple-ssl') ), 200 );
}
/**
* Starts the process of email validation for a user.
*
*/
public function start_email_validation_callback(): void
{
if(!is_user_logged_in()) {
wp_send_json_error( array( 'message' => __( 'User not logged in.', 'really-simple-ssl' ) ), 401 );
}
$user = get_user_by('id', get_current_user_id());
// Sending the email with the code.
Rsssl_Two_Factor_Email::get_instance()->generate_and_email_token($user, true);
$token = get_user_meta( $user->ID, Rsssl_Two_Factor_Email::RSSSL_TOKEN_META_KEY, true );
wp_send_json_success( array( 'message' => __('Verification code sent', 'really-simple-ssl'), 'token' => $token ), 200 );
}
/**
* Save the Two-Factor Authentication settings for the user.
*
* @param int $user_id The user ID.
*
* @noinspection UnusedFunctionResultInspection
* @return void
*/
public function save_user_profile(int $user_id): void
{
// We check if the user owns the profile.
if (!current_user_can('edit_user', $user_id)) {
return;
}
// Handle reset action
if (isset($_POST['change_2fa_config_field'])) {
if (
isset($_POST['reset_two_fa_nonce']) &&
wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['reset_two_fa_nonce'])), 'reset_two_fa_settings')
) {
$reset_input = filter_var($_POST['change_2fa_config_field'], FILTER_VALIDATE_BOOLEAN);
$this->maybe_the_user_resets_config($user_id, $reset_input);
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-reset',
__('Two-Factor Authentication settings have been reset.', 'really-simple-ssl'),
'updated'
);
// Redirect to avoid form resubmission
wp_redirect(add_query_arg('settings-updated', 'true'));
exit;
}
return;
}
if (isset($_POST['rsssl_two_fa_nonce']) && !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['rsssl_two_fa_nonce'])), 'update_user_two_fa_settings')) {
return;
}
if (isset($_POST['change_2fa_config_field'])) {
// We sanitize the input needs to be a boolean.
$reset_input = filter_var($_POST['change_2fa_config_field'], FILTER_VALIDATE_BOOLEAN);
$this->maybe_the_user_resets_config($user_id, $reset_input);
return;
}
$params = new Rsssl_Parameter_Validation();
$params::validate_user_id($user_id);
$user = get_user_by('ID', $user_id);
$params::validate_user($user);
if (!isset($_POST['two-factor-authentication'])) {
// reset the user's 2fa settings.
// Delete all 2fa related user meta.
Rsssl_Two_Fa_Status::delete_two_fa_meta($user->ID);
// Set the rsssl_two_fa_last_login to now, so the user will be forced to use 2fa.
update_user_meta($user->ID, 'rsssl_two_fa_last_login', gmdate('Y-m-d H:i:s'));
// also make sure no lingering errpr messages are shown.
Rsssl_Parameter_Validation::delete_cached_errors($user_id);
return;
}
if (!isset($_POST['preferred_method'])) {
return;
}
// now we check witch provider is selected from the $_POST.
$params::validate_selected_provider($this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method']))));
$selected_provider = $this->sanitize_method(sanitize_text_field(wp_unslash($_POST['preferred_method'])));
// if the selected provider is not then return.
if (!$selected_provider) {
return;
}
switch ($selected_provider) {
case 'totp':
$current_status = Rsssl_Two_Factor_Settings::get_user_status('totp', $user_id);
// if ('active' === $current_status) {
// return;
// }
if ((empty($_POST['two-factor-totp-authcode']))
|| !isset($_POST['two-factor-totp-key'])
) {
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('Two-Factor Authentication for TOTP failed. No Authentication code provided, please try again.', 'really-simple-ssl'),
);
$params::cache_errors($user_id);
return;
}
$params::validate_auth_code(sanitize_text_field(wp_unslash($_POST['two-factor-totp-authcode'])));
$params::validate_key(sanitize_text_field(wp_unslash($_POST['two-factor-totp-key'])));
$auth_code = sanitize_text_field(wp_unslash($_POST['two-factor-totp-authcode']));
$key = sanitize_text_field(wp_unslash($_POST['two-factor-totp-key']));
if (Rsssl_Two_Factor_Totp::setup_totp($user, $key, $auth_code)) {
self::set_active_provider($user_id, 'totp');
// We generate the backup codes.
Rsssl_Two_Factor_Backup_Codes::generate_codes(
$user,
array(
'cached' => true,
)
);
} else {
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The Two-Factor Authentication setup for TOTP failed. Please try again.', 'really-simple-ssl'),
);
}
// We cache the errors.
$params::cache_errors($user_id);
break;
case 'email':
$current_status = Rsssl_Two_Factor_Settings::get_user_status('email', $user_id);
if ('active' === $current_status) {
return;
}
$user = get_user_by('ID', $user_id);
// fetch current status of the user for the email method.
$status = Rsssl_Two_Factor_Settings::get_user_status('email', $user->ID);
if ('active' === $status) {
return;
}
if (Rsssl_Two_Factor_Email::get_instance()->validate_authentication($user)) {
self::set_active_provider($user->ID, 'email');
} else {
add_settings_error(
'two-factor-authentication',
'rsssl-two-factor-authentication-error',
__('The Two-Factor Authentication setup for email failed. Please try again.', 'really-simple-ssl'),
);
}
break;
case 'none':
// We disable the Two-Factor Authentication.
Rsssl_Two_Fa_Status::delete_two_fa_meta($user->ID);
break;
default:
break;
}
$params::cache_errors($user_id);
}
/**
* Sanitize the input method.
*
* @param string $method The input method.
*
* @return string The sanitized input method. Defaults to 'email' if not found in the allowed methods.
*/
private function sanitize_method(string $method): string
{
$methods = array('totp', 'email', 'passkey', 'none');
return in_array($method, $methods, true) ? sanitize_text_field($method) : 'email';
}
/**
* Display the user profile with Two-Factor Authentication settings.
*
* @param WP_User $user The user object.
*
* @noinspection UnusedFunctionResultInspection
* @return void
* @throws Exception Throws an exception if the template file is not found.
*/
public function show_user_profile(WP_User $user): void
{
// Check if the current user is viewing their own profile
if ($user->ID !== get_current_user_id()) {
return;
}
settings_errors('two-factor-authentication');
settings_errors('rsssl-two-factor-authentication-error');
$loader = Rsssl_Provider_Loader::get_loader();
$available_providers = $loader::get_enabled_providers_for_user($user);
$forced = !empty(array_intersect($user->roles, $this->forced_two_fa));
$one_enabled = 'onboarding' !== Rsssl_Two_Factor_Settings::get_login_action($user->ID);
$selected_provider = '';
if ($one_enabled) {
$selected_provider = strtolower(Rsssl_Two_Factor_Settings::get_configured_provider($user->ID));
}
$backup_codes = '';
$key = '';
$totp_url = '';
/*
* Added this as a temporary fix to prevent errors when TOTP is not available.
* TODO: Make a better solution to handle the case when TOTP is not available.
*/
if (isset($available_providers['totp'])) {
$backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes( $user->ID );
$key = Rsssl_Two_Factor_Totp::generate_key();
$totp_url = Rsssl_Two_Factor_Totp::generate_qr_code_url( $user, $key );
}
wp_nonce_field('update_user_two_fa_settings', 'rsssl_two_fa_nonce');
// Pass user_id instead of user object to prevent object corruption during template rendering
$user_id = $user->ID;
$data = array(
'key' => $key,
'totp_url' => $totp_url,
'backup_codes' => $backup_codes,
'selected_provider' => $selected_provider,
'one_enabled' => $one_enabled,
'forced' => $forced,
'available_providers' => $available_providers,
'user_id' => $user_id,
'login_nonce' => wp_create_nonce('rsssl_login_nonce'),
);
$data = self::removeCircularReferences($data);
$data_js = 'rsssl_profile.totp_data = ' . json_encode($data, JSON_THROW_ON_ERROR) . ';';
$passkeys_enabled = rsssl_get_option('enable_passkey_login' );
wp_add_inline_script('rsssl-profile-settings', $data_js);
// We load the needed template for the Two-Factor Authentication settings.
rsssl_load_template(
'profile-settings.php',
compact(
'user_id',
'available_providers',
'forced',
'one_enabled',
'selected_provider',
'backup_codes',
'totp_url',
'key',
'passkeys_enabled'
),
rsssl_path . 'assets/templates/two_fa/'
);
}
/**
* Validates if the Two-Factor Authentication is turned on for the user.
*
* @param WP_User $user The user object.
*
* @return bool Returns true if Two-Factor Authentication is turned on for the user, false otherwise.
*/
private function validate_two_turned_on_for_user(WP_User $user): bool
{
// Get the setting for the system to check if it is turned on.
$enabled_two_fa = rsssl_get_option('login_protection_enabled');
$providers = Rsssl_Provider_Loader::get_loader()::get_user_enabled_providers($user);
$option = rsssl_get_option('two_fa_forced_roles');
$this->forced_two_fa = $option !== false ? $option : array();
return $enabled_two_fa && !empty($providers);
}
/**
* Enqueues the RSSSL profile settings script.
*
* @return void
*/
public function enqueue_scripts(): void
{
$path = trailingslashit(rsssl_url) . 'assets/features/two-fa/assets.min.js';
$file_path = trailingslashit(rsssl_path) . 'assets/features/two-fa/assets.min.js';
$backup_codes = Rsssl_Two_Factor_Settings::get_backup_codes(get_current_user_id());
$user = get_user_by('ID', get_current_user_id());
// We check if the backup codes are available.
wp_register_script('rsssl-profile-settings', $path, array(), filemtime($file_path), true);
wp_enqueue_script('rsssl-profile-settings');
wp_localize_script('rsssl-profile-settings', 'rsssl_profile', array(
'ajax_url' => admin_url( 'admin-ajax.php' ),
'backup_codes' => $backup_codes,
'root' => esc_url_raw(rest_url(Rsssl_Two_Factor::REST_NAMESPACE)),
'user_id' => get_current_user_id(),
'origin' => 'profile',
'redirect_to' => 'rsssl_no_redirect', //added this for comparison in the json output.
'login_nonce' => Rsssl_Two_Fa_Authentication::create_login_nonce(get_current_user_id())['rsssl_key'],
'user_name' => $user->display_name,
'display_name' => $user->user_nicename . ' (' . $user->user_email . ')',
'translatables' => apply_filters('rsssl_two_factor_translatables', []),
));
}
/**
* Enqueues the RSSSL profile settings stylesheet.
*
* @return void
*/
public function enqueue_styles(): void
{
$path = trailingslashit(rsssl_url) . 'assets/features/two-fa/styles.css';
$file_path = trailingslashit(rsssl_path) . 'assets/features/two-fa/styles.css';
wp_enqueue_style('rsssl-profile-style', $path, array(), filemtime($file_path));
}
/**
* Checks if the user resets the configuration and actually reset everything.
*
* @param int $user_id The ID of the user.
* @param $reset_input
*
* @return bool
*/
private function maybe_the_user_resets_config(int $user_id, $reset_input): bool
{
// If the reset is true, we do the reset.
if ($reset_input && $user_id) {
// We reset the user's Two-Factor Authentication settings.
Rsssl_Two_Fa_Status::delete_two_fa_meta($user_id);
}
return $reset_input;
}
/**
* Remove circular references from the data.
*
* @param $data
* @param array $seen
* @return mixed|null
*/
public static function removeCircularReferences(&$data, array &$seen = [])
{
if (is_array($data)
|| is_object($data)
) {
if (in_array($data, $seen, true)) {
return null; // Circular reference detected, return null or handle appropriately
}
$seen[] = $data;
foreach ($data as &$value) {
$value = self::removeCircularReferences($value, $seen);
}
}
return $data;
}
}
}