Skip to content

Commit ab51db1

Browse files
kasparsdkasparsdmasteradhocgeorgestephanisdknauss
authored
Merge pull request #798 from WordPress/797-ensure-configured-on-save
Unlinked contributors: WordMessie. Co-authored-by: kasparsd <kasparsd@git.wordpress.org> Co-authored-by: masteradhoc <masteradhoc@git.wordpress.org> Co-authored-by: georgestephanis <georgestephanis@git.wordpress.org> Co-authored-by: dknauss <dpknauss@git.wordpress.org> Co-authored-by: crstauf <crstauf@git.wordpress.org> Co-authored-by: simonwheatley <simonwheatley@git.wordpress.org>
2 parents be3bf6f + 3c8c088 commit ab51db1

1 file changed

Lines changed: 154 additions & 24 deletions

File tree

class-two-factor-core.php

Lines changed: 154 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)