options = $options;
add_action( 'admin_init', [ $this, 'init' ] );
add_action( 'wp_ajax_adminoptim_2fa_user_settings', [ $this, 'ajax_mange_user_2fa' ] );
}
/**
* Option keys that only the Pro settings UI writes.
*
* @return string[]
*/
protected function get_pro_only_option_keys(): array {
return [ 'compulsory_roles', 'block_users_login', 'block_users_days', 'show_admin_notice', 'trusted_device_roles', 'trusted_time' ];
}
/**
* Start the engine running
*
* @return void
*/
public function init() {
$this->settings_options = [
[
'option_group' => Two_Factor_Authentication::MENU_SLUG,
'option_name' => Two_Factor_Authentication::OPTION_NAME,
'args' => [ 'sanitize_callback' => [ $this, 'sanitize_settings' ] ],
],
];
$this->settings_sections = [
'free' => [
'id' => 'adminoptimizer-2fa-admin-settings',
'title' => '',
'description' => esc_html__( 'By default, every user can enable two-factor authentication on their User profile page. You can change this behavior by enabling the following settings.', 'admin-optimizer' )
. '
' . esc_html__( 'Login forms:', 'admin-optimizer' ) . ' '
. esc_html__( 'Users with 2FA enabled must sign in on the WordPress login page or on a login form that reloads the page, such as WooCommerce My Account. Login forms that sign in without reloading the page (AJAX login forms) will not work for them.', 'admin-optimizer' ),
'menu_slug' => Two_Factor_Authentication::MENU_SLUG,
'option_name' => Two_Factor_Authentication::OPTION_NAME,
'fields' => [
[
'type' => 'checkbox',
'id' => 'recovery-code',
'title' => __( 'Recovery Code', 'admin-optimizer' ),
'name' => 'enable_recovery_code',
'label' => __( 'Enable backup recovery code', 'admin-optimizer' ),
],
[
'type' => 'callback',
'id' => '2fa-email-otp',
'title' => __( 'Email One-Time Code', 'admin-optimizer' ),
'callback' => [ $this, 'render_email_otp_setting' ],
],
[
'type' => 'checkbox',
'id' => 'user-2fa-column',
'title' => __( 'User 2FA Column', 'admin-optimizer' ),
'name' => 'add_user_2fa_column',
'label' => __( 'Add a "2FA Enabled" column in All Users page to get an overview of who has enabled/not enabled 2FA.', 'admin-optimizer' ),
],
],
],
];
if ( $this->is_pro_active() ) {
$this->settings_sections = $this->inject_pro_fields(
$this->settings_sections,
'adminoptimizer-2fa-admin-settings',
[
[
'type' => 'callback',
'id' => '2fa-compulsory-roles',
'title' => __( 'Make Two-factor Authentication compulsory for these roles', 'admin-optimizer' ),
'callback' => [ $this, 'render_compulsory_roles_checkboxes' ],
],
[
'type' => 'callback',
'id' => '2fa-trusted-devices',
'title' => __( 'Allow these roles to set up trusted device', 'admin-optimizer' ),
'desc' => __( '2FA will be temporary disabled on a trusted device. This feature requires browser cookies and an https (i.e. SSL) connection to the website to work.', 'admin-optimizer' ),
'callback' => [ $this, 'render_trusted_devices_settings' ],
],
]
);
} else {
$this->settings_sections['pro'] = [
'id' => 'adminoptimizer-2fa-admin-settings-pro',
'title' => __( 'Pro Options', 'admin-optimizer' ),
/* translators: %1$s is the anchor link to the Pro version. %2$s is the closing anchor tag */
'description' => sprintf( __( 'Upgrade to the %1$sPro version%2$s to access these features', 'admin-optimizer' ), '', '' ),
'menu_slug' => Two_Factor_Authentication::MENU_SLUG . '_Pro',
'option_name' => Two_Factor_Authentication::OPTION_NAME,
'args' => [
'before_section' => '
and its Copy button, joined in a single bordered
* unit. `assets/js/src/recovery.js` reads data-ao-copy.
*
* @param string $text The text shown and copied.
* @param string $label Accessible name of the button.
*
* @return void
*/
private static function render_snippet( string $text, string $label ): void {
?>
, closed by
* default, outside the settings form. Its buttons are copy buttons handled by recovery.js.
*
* @return void
*/
public function render_recovery_section() {
$until = gmdate( 'Y-m-d', time() + 2 * DAY_IN_SECONDS );
$consts = [
[ "define( 'ADMINOPTIM_2FA_BYPASS', 'username' );", __( 'Lets that user sign in with a password only. Use their login, email, or ID; separate several users with commas.', 'admin-optimizer' ) ],
[ "define( 'ADMINOPTIM_2FA_BYPASS_UNTIL', '" . $until . "' );", __( 'Optional. Ends the bypass at the start of that date (UTC), which must be in the future. Add a time for an exact end, such as 2026-11-01 18:00. Use a date, not "+2 hours".', 'admin-optimizer' ) ],
];
$cli = [
'wp ao 2fa status' => __( 'Shows how many users have 2FA, and whether a bypass is set.', 'admin-optimizer' ),
'wp ao 2fa status username' => __( 'Shows one user: enrolled or not, and whether their secret can be read.', 'admin-optimizer' ),
'wp ao 2fa reset username' => __( 'Removes that user\'s 2FA. They sign in with their password and set it up again.', 'admin-optimizer' ),
'wp ao 2fa reset-all --yes' => __( 'Does the same for every user. For a total lockout.', 'admin-optimizer' ),
];
?>
options );
?>
options['compulsory_roles'] ) && is_array( $this->options['compulsory_roles'] ) ) ? $this->options['compulsory_roles'] : [];
$roles = get_editable_roles();
if ( ! empty( $field['desc'] ) ) {
echo '' . esc_html( $field['desc'] ) . '
';
}
foreach ( $roles as $role => $role_details ) {
$name = translate_user_role( $role_details['name'] );
?>
options['block_users_days'] ) ? absint( $this->options['block_users_days'] ) : 10;
$block_users_login = ! empty( $this->options['block_users_login'] ) ? 1 : 0;
$show_admin_notice = ! empty( $this->options['show_admin_notice'] ) ? 1 : 0;
?>
>
options['trusted_device_roles'] ) && is_array( $this->options['trusted_device_roles'] ) ) ? $this->options['trusted_device_roles'] : [];
$roles = get_editable_roles();
if ( ! empty( $field['desc'] ) ) {
echo '' . esc_html( $field['desc'] ) . '
';
}
foreach ( $roles as $role => $role_details ) {
$name = translate_user_role( $role_details['name'] );
?>
options['trusted_time'] ) ? absint( $this->options['trusted_time'] ) : 30;
?>
' );
?>
' . esc_html( $field['desc'] ) . '
';
}
$disabled = 'disabled="disabled"';
foreach ( $roles as $role => $role_details ) {
$name = translate_user_role( $role_details['name'] );
?>
>
' . esc_html( $field['desc'] ) . '
';
}
$disabled = 'disabled="disabled"';
foreach ( $roles as $role => $role_details ) {
$name = translate_user_role( $role_details['name'] );
?>
' );
?>
render_manage_users_settings(); ?>
render_settings_on_page( Two_Factor_Authentication::MENU_SLUG ); ?>
is_pro_active() ) : ?>
render_settings_on_page( Two_Factor_Authentication::MENU_SLUG . '_Pro', [ 'remove_submit_button' => true ] ); ?>
render_recovery_section(); ?>
'adminoptim_2fa_user',
'id' => 'adminoptim-2fa-user',
'show_option_all' => 'All Users',
'show' => 'display_name_with_login',
]
);
?>
is_pro_active() ) {
$editable_roles = array_keys( get_editable_roles() );
if ( isset( $options['compulsory_roles'] ) ) {
$submitted_roles = is_array( $options['compulsory_roles'] ) ? array_map( 'sanitize_text_field', $options['compulsory_roles'] ) : [];
$sanitized_options['compulsory_roles'] = array_values( array_intersect( $submitted_roles, $editable_roles ) );
}
if ( isset( $options['block_users_login'] ) ) {
$sanitized_options['block_users_login'] = 1;
}
if ( isset( $options['block_users_days'] ) ) {
$sanitized_options['block_users_days'] = absint( $options['block_users_days'] );
}
if ( isset( $options['show_admin_notice'] ) ) {
$sanitized_options['show_admin_notice'] = 1;
}
if ( isset( $options['trusted_device_roles'] ) ) {
$submitted_roles = is_array( $options['trusted_device_roles'] ) ? array_map( 'sanitize_text_field', $options['trusted_device_roles'] ) : [];
$sanitized_options['trusted_device_roles'] = array_values( array_intersect( $submitted_roles, $editable_roles ) );
}
if ( isset( $options['trusted_time'] ) ) {
$sanitized_options['trusted_time'] = absint( $options['trusted_time'] );
}
}
}
return $this->carry_forward_inactive_pro_keys( $sanitized_options );
}
/**
* Enqueue scripts on the Settings page
*
* @param string $hook_suffix The hook suffix to check if we are on the right page.
*
* @return void
*/
public function enqueue_settings_assets( string $hook_suffix ): void {
wp_enqueue_style( 'adminoptim-modules-pro-settings' );
if ( ! isset( $_GET['tab'] ) || 'user' !== sanitize_text_field( wp_unslash( $_GET['tab'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
wp_enqueue_style( 'adminoptim-2fa-settings', Two_Factor_Authentication::MODULE_URL . 'assets/css/settings.min.css', [], ADMINOPTIMIZER_VERSION );
wp_enqueue_script( 'adminoptim-2fa-recovery', Two_Factor_Authentication::MODULE_URL . 'assets/js/recovery.min.js', [], ADMINOPTIMIZER_VERSION, true );
}
if ( isset( $_GET['tab'] ) && 'user' === sanitize_text_field( wp_unslash( $_GET['tab'] ) ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
wp_enqueue_script( 'adminoptim-select2' );
wp_enqueue_style( 'adminoptim-select2-style' );
wp_enqueue_script( 'adminoptim-2fa-settings', Two_Factor_Authentication::MODULE_URL . 'assets/js/settings.min.js', [ 'jquery', 'adminoptim-select2' ], ADMINOPTIMIZER_VERSION, true );
}
}
/**
* Ajax action for admin to manage user's 2FA settings.
*
* @return void
*/
public function ajax_mange_user_2fa() {
if ( ! current_user_can( 'edit_users' ) ) {
wp_send_json_error(
new \WP_Error(
'adminoptim-unauthorized-user',
ao_get_admin_notice(
__( 'You have no permission to edit users.', 'admin-optimizer' ),
[
'type' => 'error',
]
)
)
);
}
check_ajax_referer( 'adminoptim_2fa_user_action', 'nonce' );
$response = [];
$action = ! empty( $_REQUEST['user_action'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['user_action'] ) ) : '';
if ( ! in_array( $action, [ 'deactivate', 'unblock' ], true ) ) {
$response = new \WP_Error(
'adminoptim-no-2fa-action-found',
ao_get_admin_notice(
__( 'No appropriate action is found', 'admin-optimizer' ),
[
'type' => 'error',
]
)
);
wp_send_json_error( $response );
}
$user_id = ! empty( $_REQUEST['user_id'] ) ? absint( $_REQUEST['user_id'] ) : 0;
if ( $user_id > 0 ) {
$user_2fa = get_user_meta( $user_id, Two_Factor_Authentication::OPTION_NAME, true );
if ( ! empty( $user_2fa ) && is_array( $user_2fa ) ) {
if ( 'deactivate' === $action && isset( $user_2fa['enabled'] ) ) {
unset( $user_2fa['enabled'] );
unset( $user_2fa['secret'] );
Two_Factor_Authentication::delete_secret( $user_id );
update_user_meta( $user_id, Two_Factor_Authentication::OPTION_NAME, $user_2fa );
delete_user_meta( $user_id, Two_Factor_Authentication::TRUSTED_TOKEN_NAME );
$response['message'] = ao_get_admin_notice(
__( 'Two-factor authentication disabled for user.', 'admin-optimizer' ),
[
'type' => 'success',
]
);
wp_send_json_success( $response );
} elseif ( 'unblock' === $action && ! empty( $user_2fa['block_login_time'] ) ) {
if ( time() > $user_2fa['block_login_time'] ) {
$user_2fa['block_login_time'] = time() + ( 3 * DAY_IN_SECONDS );
update_user_meta( $user_id, Two_Factor_Authentication::OPTION_NAME, $user_2fa );
}
$response['message'] = ao_get_admin_notice(
__( 'Login access is now temporarily granted for the user.', 'admin-optimizer' ),
[
'type' => 'success',
]
);
wp_send_json_success( $response );
} else {
$message = 'deactivate' === $action
? __( 'Cannot deactivate 2FA. This user did not have a valid 2FA setup.', 'admin-optimizer' )
: __( 'Cannot unblock login. This user is not being blocked at the moment.', 'admin-optimizer' );
$response = new \WP_Error(
'adminoptim-2fa-user-action-noop',
ao_get_admin_notice(
$message,
[
'type' => 'error',
]
)
);
wp_send_json_error( $response );
}
} else {
$response = new \WP_Error(
'adminoptim-invalid-user-2fa',
ao_get_admin_notice(
__( 'Cannot deactivate 2FA. This user did not have a valid 2FA setup.', 'admin-optimizer' ),
[
'type' => 'error',
]
)
);
wp_send_json_error( $response );
}
} else {
$response = new \WP_Error(
'adminoptim-invalid-user',
ao_get_admin_notice(
__( 'Invalid user', 'admin-optimizer' ),
[
'type' => 'error',
]
)
);
wp_send_json_error( $response );
}
}
}