Files
2026-04-28 15:13:50 +02:00

294 lines
10 KiB
PHP

<?php
/**
* This file contains the Rsssl_Two_Factor_Admin class.
*
* The Rsssl_Two_Factor_Admin class is responsible for handling the administrative
* aspects of the two-factor authentication feature in the Really Simple SSL plugin.
* It includes two_fa_provider for displaying the two-factor authentication settings in the
* admin area, handling user input, and managing user roles and capabilities related
* to two-factor authentication.
*
* PHP version 7.2
*
* @category Security
* @package Really_Simple_SSL
* @author Really Simple SSL
*/
namespace RSSSL\Security\WordPress\Two_Fa;
use RSSSL\Security\WordPress\Two_Fa\Controllers\Rsssl_Two_Fa_User_Controller;
use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_Data_Parameters;
use RSSSL\Security\WordPress\Two_Fa\Models\Rsssl_Two_FA_user;
use RSSSL\Security\WordPress\Two_Fa\Repositories\Rsssl_Two_Fa_User_Repository;
use RSSSL\Security\WordPress\Two_Fa\Services\Rsssl_Two_Fa_Forced_Role_Service;
use RSSSL\Security\WordPress\Two_Fa\Services\Rsssl_Callback_Queue;
use RSSSL\Pro\Security\WordPress\Passkey\Models\Rsssl_Webauthn;
use WP_User;
/**
* The Rsssl_Two_Factor_Admin class is responsible for handling the administrative
* aspects of the two-factor authentication feature in the Really Simple SSL plugin.
* It includes two_fa_provider for displaying the two-factor authentication settings in the
* admin area, handling user input, and managing user roles and capabilities related
* to two-factor authentication.
*
* @category Security
* @package Really_Simple_SSL
* @subpackage Two_Factor
*/
class Rsssl_Two_Factor_Admin {
/**
* The Rsssl_Two_Factor_Admin instance.
*
* @var Rsssl_Two_Factor_Settings $instance The settings object.
*/
private static $instance;
private Rsssl_Callback_Queue $queue;
/**
* The constructor.
*
* @return void
*/
public function __construct()
{
// if the user is not logged in, it don't need to do anything.
if (!rsssl_admin_logged_in()) {
return;
}
if (isset(self::$instance)) {
wp_die();
}
self::$instance = $this;
add_filter('rsssl_do_action', [$this, 'two_fa_table'], 10, 3);
add_filter('rsssl_after_save_field', [$this, 'change_disabled_users_when_forced'], 20, 3);
add_filter('rsssl_after_save_field', [$this, 'process_added_removed_enabled_roles'], 20, 3);
add_filter('rsssl_after_save_field', [$this, 'set_passkey_table'], 20, 3);
$this->queue = new Rsssl_Callback_Queue();
$this->queue->process_tasks(1);
}
/**
* Sets the passkey table.
*/
public function set_passkey_table(string $field_id, $new_value, $prev_value ): void
{
// checking if the field is the passkey enabled field
if ('enable_passkey_login' === $field_id) {
// if the passkey is enabled, it needs to set the passkey table.
if ($new_value) {
new Rsssl_Webauthn(); // Initialize the Webauthn class. It will install everything needed.
do_action('rsssl_install_tables');
}
//TODO think of what needs to be done when the passkey is disabled.
}
}
/**
* Change the disabled status of users when forced.
*
* @param string $field_id The ID of the field being changed.
* @param mixed $new_value The new value of the field.
* @param array $prev_value The previous value of the field.
* @return void
*/
public function change_disabled_users_when_forced( string $field_id, $new_value, $prev_value = [] ): void {
if ( 'two_fa_forced_roles' !== $field_id
|| empty($new_value)
) {
return;
}
//making sure that the new value is an array as well as the old value
if (!is_array($new_value)) {
$new_value = [];
}
if (!is_array($prev_value)) {
$prev_value = [];
}
$changedRoles = Rsssl_Two_Fa_Forced_Role_Service::getForForcedRolesChange($prev_value, $new_value);
// If no roles have changed, return early.
if(empty($changedRoles)) {
return;
}
// Set up initial batch parameters.
$batch_size = 500;
$offset = 0;
$params = new Rsssl_Two_FA_Data_Parameters([
'filter_column' => 'user_role',
'filter_value' => 'all',
'number' => $batch_size,
'offset' => $offset,
]);
// Add the first processing task to the queue.
$this->queue->add_task([$this, 'process_users_batch'], [ $changedRoles, $params, $batch_size, $offset, 'open' ]);
$this->queue->add_task([$this, 'process_users_batch'], [ $changedRoles, $params, $batch_size, $offset, 'disabled']);
}
/**
* Process a batch of forced two-factor users with disabled status.
*
* @return void
*/
public function process_users_batch(array $changedRoles, Rsssl_Two_FA_Data_Parameters $params, int $batch_size, int $offset, string $status): void {
$collection = (new Rsssl_Two_Fa_Forced_Role_Service($params))->processBatch($changedRoles, $status);
foreach ($collection->getUsers() as $user) {
$statusForUser = $user->getStatus();
if (in_array($statusForUser, ['open', 'disabled','expired'])) {
// Check if the user has a role that has been changed.
$rolesForUser = $user->getRoles();
$matchingRoles = array_intersect($rolesForUser, $changedRoles);
if (!empty($matchingRoles)) {
// Reset the user's status.
$user->resetStatus();
//temp meta key for testing
update_user_meta($user->getId(), 'rsssl_two_fa_status_reset', true);
}
}
}
// Check if there are more users to process.
// The collection contains the total number of records (set in the repository).
$total = $collection->getTotalRecords();
if (($params->offset + $params->number) < $total) {
// Update the offset for the next batch.
$newOffset = $offset + $batch_size;
// Queue the next task with the correct new offset.
$this->queue->add_task([$this, 'process_users_batch'], [$changedRoles, $params, $batch_size, $newOffset, $status]);
}
}
public function process_added_removed_enabled_roles(string $field_id, $new_value, $prev_value = [])
{
if ( 'two_fa_enabled_roles_email' !== $field_id
|| empty($new_value)
) {
return;
}
if ( 'two_fa_enabled_roles_totp' !== $field_id
|| empty($new_value)
) {
return;
}
}
/**
* Checks if the user can use two-factor authentication (2FA).
*
* @return bool Returns true if the user can use 2FA, false otherwise.
*/
public function can_i_use_2fa(): bool
{
return rsssl_get_option('login_protection_enabled');
}
/**
* Creates a captcha notice array.
*
* This method creates and returns an array representing a captcha notice.
*
* @param string $title The title of the notice.
* @param string $msg The message of the notice.
*
* @return array The captcha notice array.
*/
private function create_2fa_notice( string $title, string $msg ): array {
return array(
'callback' => '_true_',
'score' => 1,
'show_with_options' => array( 'login_protection_enabled' ),
'output' => array(
'true' => array(
'title' => $title,
'msg' => $msg,
'icon' => 'warning',
'type' => 'open',
'dismissible' => true,
'admin_notice' => false,
'plusone' => true,
'highlight_field_id' => 'two_fa_enabled_roles',
),
),
);
}
/**
* Reset the two-factor authentication for a user.
*
* @param array $response The response array.
* @param string $action The action being performed.
* @param array $data The data array.
*
* @return array The updated response array.
*/
public static function reset_user_two_fa( array $response, string $action, array $data ): array {
if ( ! rsssl_user_can_manage() ) {
return $response;
}
if ( 'two_fa_table' === $action ) {
// if the user has been disabled, it needs to reset the two-factor authentication.
$user = get_user_by( 'id', $data['user_id'] );
if ( $user ) {
// Delete all 2fa related user meta.
Rsssl_Two_Fa_Status::delete_two_fa_meta( $user->ID );
// Set the 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' ) );
}
}
return $response;
}
/**
* Generates the two-factor authentication table data based on the action and data parameters.
*
* @param array $response The initial response data.
* @param string $action The action to perform.
* @param array $data The data needed for the action.
*
* @return array The updated response data.
*/
public function two_fa_table(array $response, string $action, array $data): array
{
$new_response = $response;
if (rsssl_user_can_manage()) {
switch ($action) {
case 'two_fa_table':
$data_parameters = new Rsssl_Two_FA_Data_Parameters($data);
$userRepository = new Rsssl_Two_Fa_User_Repository();
// Create the controller.
return (new Rsssl_Two_Fa_User_Controller($userRepository))->getUsersForAdminOverview($data_parameters);
case 'two_fa_reset_user':
// if the user has been disabled, it needs to reset the two-factor authentication.
$user = get_user_by('id', $data['id']);
if ($user) {
// 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'));
}
if (!$user) {
$new_response['request_success'] = false;
}
break;
default:
// Default case if no action matches.
break;
}
}
return $new_response;
}
}