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' => '
', 'after_section' => '
', ], 'fields' => [ [ 'type' => 'callback', 'id' => '2fa-compulsory-roles', 'title' => __( 'Make Two-factor Authentication compulsory for these roles', 'admin-optimizer' ), 'callback' => [ $this, 'render_disabled_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_disabled_trusted_devices_settings' ], ], ], ]; } $this->setup(); } /** * Docs URL for this module, or ''. * * @return string */ private static function get_docs_url(): string { $module = Module_Registry::get_module( Two_Factor_Authentication::MODULE_ID ); return is_array( $module ) && ! empty( $module['website'] ) ? (string) $module['website'] : ''; } /** * One copy snippet: the text in a 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' ), ]; ?>

    • $purpose ) : ?>

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 ); } } }