@@ -85,6 +85,13 @@ class Two_Factor_Core {
8585 */
8686 private static $ password_auth_tokens = array ();
8787
88+ /**
89+ * Keep track of any errors related to setting updates.
90+ *
91+ * @var array
92+ */
93+ private static $ profile_errors = array ();
94+
8895 /**
8996 * Set up filters and actions.
9097 *
@@ -114,6 +121,7 @@ public static function add_hooks( $compat ) {
114121
115122 // 2. Render two-factor UI after WP core has validated username/password during `wp_signon()`.
116123 add_action ( 'wp_login ' , array ( __CLASS__ , 'wp_login ' ), PHP_INT_MAX , 2 );
124+ add_action ( 'user_profile_update_errors ' , array ( __CLASS__ , 'action_user_profile_update_errors ' ) );
117125
118126 /**
119127 * Keep track of all the user sessions for which we need to invalidate the
@@ -296,7 +304,7 @@ private static function get_providers_classes( $providers ) {
296304 *
297305 * @since 0.2.0
298306 *
299- * @return array
307+ * @return Two_Factor_Provider[]
300308 */
301309 public static function get_providers () {
302310 $ providers = self ::get_default_providers ();
@@ -338,7 +346,8 @@ public static function get_providers() {
338346 * @see Two_Factor_Core::get_available_providers_for_user()
339347 *
340348 * @param WP_User|int|null $user User ID.
341- * @return array List of provider instances indexed by provider key.
349+ *
350+ * @return Two_Factor_Provider[] List of provider instances indexed by provider key.
342351 */
343352 public static function get_supported_providers_for_user ( $ user = null ) {
344353 $ user = self ::fetch_user ( $ user );
@@ -403,6 +412,36 @@ public static function add_settings_action_link( $links ) {
403412 return $ links ;
404413 }
405414
415+ /**
416+ * Register an error associated with the current request.
417+ *
418+ * @param WP_Error $error Error instance.
419+
420+ * @return void
421+ */
422+ private static function add_error ( WP_Error $ error ) {
423+ self ::$ profile_errors [ $ error ->get_error_code () ] = $ error ;
424+ }
425+
426+ /**
427+ * Attach Two-Factor profile errors to WordPress core profile update errors.
428+ *
429+ * @since NEXT
430+ *
431+ * @param WP_Error $errors WP_Error object passed by core.
432+ *
433+ * @return void
434+ */
435+ public static function action_user_profile_update_errors ( WP_Error $ errors ) {
436+ foreach ( self ::$ profile_errors as $ profile_error ) {
437+ foreach ( $ profile_error ->get_error_codes () as $ code ) {
438+ foreach ( $ profile_error ->get_error_messages ( $ code ) as $ message ) {
439+ $ errors ->add ( $ code , $ message );
440+ }
441+ }
442+ }
443+ }
444+
406445 /**
407446 * Check if the debug mode is enabled.
408447 *
@@ -599,7 +638,8 @@ public static function fetch_user( $user = null ) {
599638 * @see Two_Factor_Core::get_available_providers_for_user()
600639 *
601640 * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user.
602- * @return array
641+ *
642+ * @return string[] List of keys of enabled providers for the user.
603643 */
604644 public static function get_enabled_providers_for_user ( $ user = null ) {
605645 $ user = self ::fetch_user ( $ user );
@@ -635,7 +675,7 @@ public static function get_enabled_providers_for_user( $user = null ) {
635675 * @see Two_Factor_Core::get_enabled_providers_for_user()
636676 *
637677 * @param int|WP_User $user Optional. User ID, or WP_User object of the the user. Defaults to current user.
638- * @return array |WP_Error List of provider instances, or a WP_Error if all configured providers are unavailable.
678+ * @return Two_Factor_Provider[] |WP_Error List of provider instances, or a WP_Error if all configured providers are unavailable.
639679 */
640680 public static function get_available_providers_for_user ( $ user = null ) {
641681 $ user = self ::fetch_user ( $ user );
@@ -2044,8 +2084,6 @@ public static function manage_users_custom_column( $output, $column_name, $user_
20442084 * @param WP_User $user WP_User object of the logged-in user.
20452085 */
20462086 public static function user_two_factor_options ( $ user ) {
2047- $ notices = array ();
2048-
20492087 $ providers = self ::get_supported_providers_for_user ( $ user );
20502088
20512089 wp_enqueue_style ( 'user-edit-2fa ' , plugins_url ( 'user-edit.css ' , __FILE__ ), array (), TWO_FACTOR_VERSION );
@@ -2062,29 +2100,58 @@ public static function user_two_factor_options( $user ) {
20622100 self ::get_user_two_factor_revalidate_url ()
20632101 );
20642102
2065- $ notices ['warning two-factor-warning-revalidate-session ' ] = sprintf (
2066- /* translators: %s: URL to revalidate the session */
2067- __ ( 'To update your Two-Factor options, you must first revalidate your session. <a class="button" href="%s">Revalidate now</a> ' , 'two-factor ' ),
2068- esc_url ( $ url )
2103+ self ::add_error (
2104+ new WP_Error (
2105+ 'two_factor_revalidate_session ' ,
2106+ sprintf (
2107+ __ ( 'To update your Two-Factor options, you must first revalidate your session. ' , 'two-factor ' ) .
2108+ ' <a class="button" href="%s"> ' . esc_html__ ( 'Revalidate now ' , 'two-factor ' ) . '</a> ' ,
2109+ esc_url ( $ url )
2110+ ),
2111+ array (
2112+ 'type ' => 'warning ' ,
2113+ )
2114+ )
20692115 );
20702116 }
20712117
20722118 if ( empty ( $ providers ) ) {
2073- $ notices ['notice two-factor-notice-no-providers-supported ' ] = esc_html__ ( 'No providers are available for your account. ' , 'two-factor ' );
2119+ self ::add_error (
2120+ new WP_Error (
2121+ 'two_factor_no_providers_supported ' ,
2122+ __ ( 'No providers are available for your account. ' , 'two-factor ' ),
2123+ array (
2124+ 'type ' => 'notice ' ,
2125+ )
2126+ )
2127+ );
20742128 }
20752129
20762130 // Suggest enabling a backup method if only one method is enabled and there are more available.
20772131 if ( count ( $ providers ) > 1 && 1 === count ( $ enabled_providers ) ) {
2078- $ 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 ' );
2132+ self ::add_error (
2133+ new WP_Error (
2134+ 'two_factor_suggest_backup ' ,
2135+ __ ( '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 ' ),
2136+ array (
2137+ 'type ' => 'warning ' ,
2138+ )
2139+ )
2140+ );
20792141 }
2142+
2143+ $ generic_errors = array_filter (
2144+ self ::$ profile_errors ,
2145+ static function ( WP_Error $ error ) {
2146+ $ error_data = $ error ->get_error_data ();
2147+ return empty ( $ error_data ['provider ' ] ); // Where the associated provider is not set.
2148+ }
2149+ );
2150+
20802151 ?>
20812152 <h2><?php esc_html_e ( 'Two-Factor Options ' , 'two-factor ' ); ?> </h2>
20822153
2083- <?php foreach ( $ notices as $ notice_type => $ notice ) : ?>
2084- <div class="<?php echo esc_attr ( $ notice_type ? 'notice inline notice- ' . $ notice_type : '' ); ?> ">
2085- <p><?php echo wp_kses_post ( $ notice ); ?> </p>
2086- </div>
2087- <?php endforeach ; ?>
2154+ <?php self ::render_errors ( $ generic_errors ); ?>
20882155
20892156 <fieldset id="two-factor-options" <?php echo $ show_2fa_options ? '' : 'disabled="disabled" ' ; ?> >
20902157 <legend class="screen-reader-text"><?php esc_html_e ( 'Two-Factor Options ' , 'two-factor ' ); ?> </legend>
@@ -2135,6 +2202,27 @@ private static function get_recommended_providers( $user ) {
21352202 return (array ) apply_filters ( 'two_factor_recommended_providers ' , $ providers , $ user );
21362203 }
21372204
2205+ /**
2206+ * Render WP errors.
2207+ *
2208+ * @param WP_Error[] $errors List of errors to render.
2209+ */
2210+ private static function render_errors ( array $ errors ) {
2211+ foreach ( $ errors as $ error ) {
2212+ if ( $ error ->has_errors () ) {
2213+ $ error_type = $ error ->get_error_data ()['type ' ] ?? null ;
2214+
2215+ wp_admin_notice (
2216+ implode ( '</p><p> ' , $ error ->get_error_messages () ),
2217+ array (
2218+ 'type ' => is_string ( $ error_type ) ? $ error_type : 'error ' ,
2219+ 'additional_classes ' => array ( 'inline ' ),
2220+ )
2221+ );
2222+ }
2223+ }
2224+ }
2225+
21382226 /**
21392227 * Render the user settings.
21402228 *
@@ -2145,7 +2233,7 @@ private static function get_recommended_providers( $user ) {
21452233 */
21462234 private static function render_user_providers_form ( $ user , $ providers ) {
21472235 $ primary_provider_key = self ::get_primary_provider_key_selected_for_user ( $ user );
2148- $ enabled_providers = self ::get_enabled_providers_for_user ( $ user );
2236+ $ available_providers = self ::get_available_providers_for_user ( $ user );
21492237 $ recommended_provider_keys = self ::get_recommended_providers ( $ user );
21502238
21512239 // Move the recommended providers first.
@@ -2172,8 +2260,9 @@ private static function render_user_providers_form( $user, $providers ) {
21722260 <tr>
21732261 <th><?php echo esc_html ( $ object ->get_label () ); ?> </th>
21742262 <td>
2263+ <?php self ::render_errors ( self ::get_provider_errors ( $ provider_key ) ); ?>
21752264 <label class="two-factor-method-label">
2176- <input id="enabled-<?php echo esc_attr ( $ provider_key ); ?> " type="checkbox" name="<?php echo esc_attr ( self ::ENABLED_PROVIDERS_USER_META_KEY ); ?> []" value="<?php echo esc_attr ( $ provider_key ); ?> " <?php checked ( in_array ( $ provider_key , $ enabled_providers , true ) ); ?> />
2265+ <input id="enabled-<?php echo esc_attr ( $ provider_key ); ?> " type="checkbox" name="<?php echo esc_attr ( self ::ENABLED_PROVIDERS_USER_META_KEY ); ?> []" value="<?php echo esc_attr ( $ provider_key ); ?> " <?php checked ( isset ( $ available_providers [ $ provider_key ] ) ); ?> />
21772266 <?php /* translators: %s: authentication method name. */ ?>
21782267 <strong><?php echo esc_html ( sprintf ( __ ( 'Enable %s ' , 'two-factor ' ), $ object ->get_label () ) ); ?> </strong>
21792268 <?php if ( in_array ( $ provider_key , $ recommended_provider_keys , true ) ) : ?>
@@ -2207,7 +2296,7 @@ private static function render_user_providers_form( $user, $providers ) {
22072296 <select id="two-factor-primary-provider" name="<?php echo esc_attr ( self ::PROVIDER_USER_META_KEY ); ?> ">
22082297 <option value=""><?php echo esc_html ( __ ( 'Default ' , 'two-factor ' ) ); ?> </option>
22092298 <?php foreach ( $ providers as $ provider_key => $ object ) : ?>
2210- <option value="<?php echo esc_attr ( $ provider_key ); ?> " <?php selected ( $ provider_key , $ primary_provider_key ); ?> <?php disabled ( ! in_array ( $ provider_key , $ enabled_providers , true ) ); ?> >
2299+ <option value="<?php echo esc_attr ( $ provider_key ); ?> " <?php selected ( $ provider_key , $ primary_provider_key ); ?> <?php disabled ( ! isset ( $ available_providers [ $ provider_key ] ) ); ?> >
22112300 <?php echo esc_html ( $ object ->get_label () ); ?>
22122301 </option>
22132302 <?php endforeach ; ?>
@@ -2220,6 +2309,24 @@ private static function render_user_providers_form( $user, $providers ) {
22202309 <?php
22212310 }
22222311
2312+ /**
2313+ * Get the errors marked for a specific provider.
2314+ *
2315+ * @param string $provider_key The provider key to get errors for.
2316+ *
2317+ * @return WP_Error[] List of errors for the provider.
2318+ */
2319+ private static function get_provider_errors ( string $ provider_key ): array {
2320+ return array_filter (
2321+ self ::$ profile_errors ,
2322+ static function ( WP_Error $ error ) use ( $ provider_key ) {
2323+ $ error_data = $ error ->get_error_data (); // Return the data for the first error.
2324+
2325+ return isset ( $ error_data ['provider ' ] ) && $ error_data ['provider ' ] === $ provider_key ;
2326+ }
2327+ );
2328+ }
2329+
22232330 /**
22242331 * Enable a provider for a user.
22252332 *
@@ -2311,17 +2418,40 @@ public static function user_two_factor_options_update( $user_id ) {
23112418 return ;
23122419 }
23132420
2421+ $ user = self ::fetch_user ( $ user_id );
23142422 $ providers = self ::get_supported_providers_for_user ( $ user_id );
23152423 $ enabled_providers = $ _POST [ self ::ENABLED_PROVIDERS_USER_META_KEY ];
23162424 $ existing_providers = self ::get_enabled_providers_for_user ( $ user_id );
23172425
23182426 // Enable only the available providers.
2319- $ enabled_providers = array_intersect ( $ enabled_providers , array_keys ( $ providers ) );
2320- update_user_meta ( $ user_id , self ::ENABLED_PROVIDERS_USER_META_KEY , $ enabled_providers );
2427+ $ enabled_providers = array_intersect_key ( $ providers , array_flip ( $ enabled_providers ) );
2428+
2429+ // Ensure the enabled providers are configured and can be enabled.
2430+ foreach ( $ enabled_providers as $ provider_key => $ provider ) {
2431+ if ( ! $ provider ->is_available_for_user ( $ user ) ) {
2432+ unset( $ enabled_providers [ $ provider_key ] );
2433+
2434+ self ::add_error (
2435+ new WP_Error (
2436+ 'two_factor_provider_not_configured_ ' . $ provider_key ,
2437+ sprintf (
2438+ /* translators: %s: provider label. */
2439+ __ ( 'The %s method must be configured before it can be enabled. ' , 'two-factor ' ),
2440+ esc_html ( $ provider ->get_label () )
2441+ ),
2442+ array (
2443+ 'provider ' => $ provider_key ,
2444+ )
2445+ )
2446+ );
2447+ }
2448+ }
2449+
2450+ update_user_meta ( $ user_id , self ::ENABLED_PROVIDERS_USER_META_KEY , array_keys ( $ enabled_providers ) );
23212451
23222452 // Primary provider must be enabled.
23232453 $ new_provider = isset ( $ _POST [ self ::PROVIDER_USER_META_KEY ] ) ? $ _POST [ self ::PROVIDER_USER_META_KEY ] : '' ;
2324- if ( ! empty ( $ new_provider ) && in_array ( $ new_provider , $ enabled_providers , true ) ) {
2454+ if ( ! empty ( $ new_provider ) && isset ( $ enabled_providers [ $ new_provider ] ) ) {
23252455 update_user_meta ( $ user_id , self ::PROVIDER_USER_META_KEY , $ new_provider );
23262456 } else {
23272457 delete_user_meta ( $ user_id , self ::PROVIDER_USER_META_KEY );
@@ -2354,7 +2484,7 @@ public static function user_two_factor_options_update( $user_id ) {
23542484 // No providers, enabling one (or more)
23552485 ( ! $ existing_providers && $ enabled_providers ) ||
23562486 // Has providers, and is disabling one (or more), but remaining with 2FA.
2357- ( $ existing_providers && $ enabled_providers && array_diff ( $ existing_providers , $ enabled_providers ) )
2487+ ( $ existing_providers && $ enabled_providers && array_diff ( $ existing_providers , array_keys ( $ enabled_providers ) ) )
23582488 ) {
23592489 if ( $ user_id === get_current_user_id () ) {
23602490 // Keep the current session, destroy others sessions for this user.
0 commit comments