diff --git a/class-two-factor-core.php b/class-two-factor-core.php index 7752abc5..c38ed8d1 100644 --- a/class-two-factor-core.php +++ b/class-two-factor-core.php @@ -85,6 +85,13 @@ class Two_Factor_Core { */ private static $password_auth_tokens = array(); + /** + * Keep track of any errors related to setting updates. + * + * @var array + */ + private static $profile_errors = array(); + /** * Set up filters and actions. * @@ -106,6 +113,7 @@ public static function add_hooks( $compat ) { add_filter( 'manage_users_columns', array( __CLASS__, 'filter_manage_users_columns' ) ); add_filter( 'wpmu_users_columns', array( __CLASS__, 'filter_manage_users_columns' ) ); add_filter( 'manage_users_custom_column', array( __CLASS__, 'manage_users_custom_column' ), 10, 3 ); + add_action( 'user_profile_update_errors', array( __CLASS__, 'action_user_profile_update_errors' ) ); /** * Keep track of all the user sessions for which we need to invalidate the @@ -137,7 +145,7 @@ public static function add_hooks( $compat ) { /** * Delete all plugin data on uninstall. - * + * * @since 0.10.0 * * @return void @@ -205,7 +213,7 @@ public static function uninstall() { /** * Get the registered providers of which some might not be enabled. - * + * * @since 0.11.0 * * @return array List of provider keys and paths to class files. @@ -222,7 +230,7 @@ private static function get_default_providers() { /** * Get the classnames for specific providers. - * + * * @since 0.10.0 * * @param array $providers List of paths to provider class files indexed by class names. @@ -267,7 +275,7 @@ private static function get_providers_classes( $providers ) { * * @since 0.2.0 * - * @return array + * @return Two_Factor_Provider[] */ public static function get_providers() { $providers = self::get_default_providers(); @@ -312,14 +320,15 @@ public static function get_providers() { /** * Get providers available for user which may not be enabled or configured. - * + * * @since 0.13.0 * * @see Two_Factor_Core::get_enabled_providers_for_user() * @see Two_Factor_Core::get_available_providers_for_user() * * @param WP_User|int|null $user User ID. - * @return array List of provider instances indexed by provider key. + * + * @return Two_Factor_Provider[] List of provider instances indexed by provider key. */ public static function get_supported_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); @@ -336,7 +345,7 @@ public static function get_supported_providers_for_user( $user = null ) { /** * Enable the dummy method only during debugging. - * + * * @since 0.5.2 * * @param array $methods List of enabled methods. @@ -372,9 +381,39 @@ public static function add_settings_action_link( $links ) { return $links; } + /** + * Register an error associated with the current request. + * + * @param WP_Error $error Error instance. + + * @return void + */ + private static function add_error( WP_Error $error ) { + self::$profile_errors[ $error->get_error_code() ] = $error; + } + + /** + * Attach Two-Factor profile errors to WordPress core profile update errors. + * + * @since NEXT + * + * @param WP_Error $errors WP_Error object passed by core. + * + * @return void + */ + public static function action_user_profile_update_errors( WP_Error $errors ) { + foreach ( self::$profile_errors as $profile_error ) { + foreach ( $profile_error->get_error_codes() as $code ) { + foreach ( $profile_error->get_error_messages( $code ) as $message ) { + $errors->add( $code, $message ); + } + } + } + } + /** * Check if the debug mode is enabled. - * + * * @since 0.5.2 * * @return boolean @@ -388,7 +427,7 @@ protected static function is_wp_debug() { * * Fetch this from the plugin core after we introduce proper dependency injection * and get away from the singletons at the provider level (should be handled by core). - * + * * @since 0.5.2 * * @param integer $user_id User ID. @@ -410,7 +449,7 @@ protected static function get_user_settings_page_url( $user_id ) { /** * Get the URL for resetting the secret token. - * + * * @since 0.5.2 * * @param integer $user_id User ID. @@ -433,7 +472,7 @@ public static function get_user_update_action_url( $user_id, $action ) { /** * Get the two-factor revalidate URL. - * + * * @since 0.9.0 * * @param bool $interim If the URL should load the interim login iframe modal. @@ -452,7 +491,7 @@ public static function get_user_two_factor_revalidate_url( $interim = false ) { /** * Check if a user action is valid. - * + * * @since 0.5.2 * * @param integer $user_id User ID. @@ -475,7 +514,7 @@ public static function is_valid_user_action( $user_id, $action ) { /** * Get the ID of the user being edited. - * + * * @since 0.5.2 * * @return integer @@ -496,7 +535,7 @@ public static function current_user_being_edited() { /** * Trigger our custom update action if a valid * action request is detected and passes the nonce check. - * + * * @since 0.5.2 * * @return void @@ -520,7 +559,7 @@ public static function trigger_user_settings_action() { /** * Keep track of all the authentication cookies that need to be * invalidated before the second factor authentication. - * + * * @since 0.5.1 * * @param string $cookie Cookie string. @@ -561,14 +600,15 @@ public static function fetch_user( $user = null ) { /** * Get two-factor providers that are enabled for the specified (or current) user * but might not be configured, yet. - * + * * @since 0.2.0 * * @see Two_Factor_Core::get_supported_providers_for_user() * @see Two_Factor_Core::get_available_providers_for_user() * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. - * @return array + * + * @return string[] List of keys of enabled providers for the user. */ public static function get_enabled_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); @@ -595,14 +635,15 @@ public static function get_enabled_providers_for_user( $user = null ) { /** * Get all two-factor providers that are both enabled and configured * for the specified (or current) user. - * + * * @since 0.2.0 * * @see Two_Factor_Core::get_supported_providers_for_user() * @see Two_Factor_Core::get_enabled_providers_for_user() * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. - * @return array List of provider instances. + * + * @return Two_Factor_Provider[] List of provider instances indexed by provider key. */ public static function get_available_providers_for_user( $user = null ) { $user = self::fetch_user( $user ); @@ -625,7 +666,7 @@ public static function get_available_providers_for_user( $user = null ) { /** * Fetch the provider for the request based on the user preferences. - * + * * @since 0.9.0 * * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user. @@ -664,7 +705,7 @@ public static function get_provider_for_user( $user = null, $preferred_provider /** * Get the name of the primary provider selected by the user * and enabled for the user. - * + * * @since 0.12.0 * * @param WP_User|int $user User ID or instance. @@ -770,7 +811,7 @@ public static function wp_login( $user_login, $user ) { * Is there a better way of finding the current session token without * having access to the authentication cookies which are just being set * on the first password-based authentication request. - * + * * @since 0.5.1 * * @param \WP_User $user User object. @@ -789,7 +830,7 @@ public static function destroy_current_session_for_user( $user ) { * Trigget the two-factor workflow only for valid login attempts * with username present. Prevent authentication during API requests * unless explicitly enabled for the user (disabled by default). - * + * * @since 0.4.0 * * @param WP_User|WP_Error $user Valid WP_User only if the previous filters @@ -824,7 +865,7 @@ public static function filter_authenticate( $user, $username, $password ) { * If the user can login via API requests such as XML-RPC and REST. * * Only logins with application passwords are permitted by default. - * + * * @since 0.4.0 * * @param integer $user_id User ID. @@ -848,7 +889,7 @@ public static function is_user_api_login_enabled( $user_id ) { /** * Is the current request an XML-RPC or REST request. - * + * * @since 0.4.0 * * @return boolean @@ -889,7 +930,7 @@ public static function show_two_factor_login( $user ) { /** * Displays a message informing the user that their account has had failed login attempts. - * + * * @since 0.8.0 * * @param WP_User $user WP_User object of the logged-in user. @@ -922,7 +963,7 @@ public static function maybe_show_last_login_failure_notice( $user ) { * * They were also sent an email notification in `send_password_reset_email()`, but email sent from a typical * web server is not reliable enough to trust completely. - * + * * @since 0.8.0 * * @param WP_Error $errors Error object. @@ -967,7 +1008,7 @@ public static function maybe_show_reset_password_notice( $errors ) { /** * Clear the password reset notice after the user resets their password. - * + * * @since 0.8.0 * * @param WP_User $user User object. @@ -1142,7 +1183,7 @@ function() { /** * Generate the two-factor login form URL. - * + * * @since 0.3.0 * * @param array $params List of query argument pairs to add to the URL. @@ -1173,7 +1214,7 @@ public static function login_url( $params = array(), $scheme = 'login' ) { /** * Get the hash of a nonce for storage and comparison. - * + * * @since 0.7.2 * * @param array $nonce Nonce array to be hashed. ⚠️ This must contain user ID and expiration, @@ -1281,7 +1322,7 @@ public static function verify_login_nonce( $user_id, $nonce ) { * * This implements an increasing backoff, requiring an attacker to wait longer * each time to attempt to brute-force the login. - * + * * @since 0.8.0 * * @param WP_User $user The user being operated upon. @@ -1673,7 +1714,7 @@ public static function _login_form_revalidate_2fa( $nonce = '', $provider = '', /** * Process the 2FA provider authentication. - * + * * @since 0.9.0 * * @param object $provider The Two Factor Provider. @@ -1793,7 +1834,7 @@ public static function reset_compromised_password( $user ) { /** * Notify the user and admin that a password was reset for being compromised. - * + * * @since 0.8.0 * * @param WP_User $user The user whose password should be reset. @@ -1817,7 +1858,7 @@ public static function send_password_reset_emails( $user ) { /** * Notify the user that their password has been compromised and reset. - * + * * @since 0.8.0 * * @param WP_User $user The user to notify. @@ -1847,7 +1888,7 @@ public static function notify_user_password_reset( $user ) { /** * Notify the admin that a user's password was compromised and reset. - * + * * @since 0.8.0 * * @param WP_User $user The user whose password was reset. @@ -1879,7 +1920,7 @@ public static function notify_admin_user_password_reset( $user ) { /** * Show the password reset error when on the login screen. - * + * * @since 0.8.0 */ public static function show_password_reset_error() { @@ -1899,7 +1940,7 @@ public static function show_password_reset_error() { /** * Filter the columns on the Users admin screen. - * + * * @since 0.2.0 * * @param array $columns Available columns. @@ -1912,7 +1953,7 @@ public static function filter_manage_users_columns( array $columns ) { /** * Output the 2FA column data on the Users screen. - * + * * @since 0.2.0 * * @param string $output The column output. @@ -1944,8 +1985,6 @@ public static function manage_users_custom_column( $output, $column_name, $user_ * @param WP_User $user WP_User object of the logged-in user. */ public static function user_two_factor_options( $user ) { - $notices = array(); - $providers = self::get_supported_providers_for_user( $user ); wp_enqueue_style( 'user-edit-2fa', plugins_url( 'user-edit.css', __FILE__ ), array(), TWO_FACTOR_VERSION ); @@ -1962,29 +2001,58 @@ public static function user_two_factor_options( $user ) { self::get_user_two_factor_revalidate_url() ); - $notices['warning two-factor-warning-revalidate-session'] = sprintf( - esc_html__( 'To update your Two-Factor options, you must first revalidate your session.', 'two-factor' ) . - ' ' . esc_html__( 'Revalidate now', 'two-factor' ) . '', - esc_url( $url ) + self::add_error( + new WP_Error( + 'two_factor_revalidate_session', + sprintf( + __( 'To update your Two-Factor options, you must first revalidate your session.', 'two-factor' ) . + ' ' . esc_html__( 'Revalidate now', 'two-factor' ) . '', + esc_url( $url ) + ), + array( + 'type' => 'warning', + ) + ) ); } if ( empty( $providers ) ) { - $notices['notice two-factor-notice-no-providers-supported'] = esc_html__( 'No providers are available for your account.', 'two-factor' ); + self::add_error( + new WP_Error( + 'two_factor_no_providers_supported', + __( 'No providers are available for your account.', 'two-factor' ), + array( + 'type' => 'notice', + ) + ) + ); } // Suggest enabling a backup method if only one method is enabled and there are more available. if ( count( $providers ) > 1 && 1 === count( $enabled_providers ) ) { - $notices['warning two-factor-warning-suggest-backup'] = esc_html__( 'To prevent being locked out of your account, consider enabling a backup method like Recovery Codes in case you lose access to your primary authentication method.', 'two-factor' ); + self::add_error( + new WP_Error( + 'two_factor_suggest_backup', + __( 'To prevent being locked out of your account, consider enabling a backup method like Recovery Codes in case you lose access to your primary authentication method.', 'two-factor' ), + array( + 'type' => 'warning', + ) + ) + ); } + + $generic_errors = array_filter( + self::$profile_errors, + static function ( WP_Error $error ) { + $error_data = $error->get_error_data(); + return ! empty( $error_data['provider'] ); // Where the associated provider is not set. + } + ); + ?>

- $notice ) : ?> -
-

-
- +
> has_errors() ) { + wp_admin_notice( + implode( '

', $error->get_error_messages() ), + array( + 'type' => $error->get_error_data()['type'] ?? 'error', + 'additional_classes' => array( 'inline' ), + ) + ); + } + } + } + /** * Render the user settings. - * + * * @since 0.13.0 * * @param WP_User $user User instance. @@ -2042,7 +2129,7 @@ private static function get_recommended_providers( $user ) { */ private static function render_user_providers_form( $user, $providers ) { $primary_provider_key = self::get_primary_provider_key_selected_for_user( $user ); - $enabled_providers = self::get_enabled_providers_for_user( $user ); + $available_providers = self::get_available_providers_for_user( $user ); $recommended_provider_keys = self::get_recommended_providers( $user ); // Move the recommended providers first. @@ -2069,8 +2156,9 @@ private static function render_user_providers_form( $user, $providers ) { get_label() ); ?> +