| @@ -173,8 +173,11 @@ | ||
| 173 | 173 | // Enforcement beyond wp-admin: REST and the front end, so an expired |
| 174 | 174 | // password cannot keep operating outside the redirect (2.11.9). |
| 175 | 175 | add_filter( 'rest_authentication_errors', array( $this, 'block_expired_password_rest' ), 20 ); |
| 176 | 176 | add_action( 'template_redirect', array( $this, 'force_password_change_frontend' ) ); |
| 177 | + // The profile is the only screen where the change can be made, and | |
| 178 | + // WooCommerce keeps accounts with no back-end role out of wp-admin. | |
| 179 | + add_filter( 'woocommerce_prevent_admin_access', array( $this, 'allow_profile_for_password_change' ) ); | |
| 177 | 180 | add_filter( 'authenticate', array( $this, 'block_expired_password_xmlrpc' ), 30, 1 ); |
| 178 | 181 | add_action( 'profile_update', array( $this, 'update_password_change_date' ), 10, 2 ); |
| 179 | 182 | add_action( 'user_register', array( $this, 'set_initial_password_date' ) ); |
| 180 | 183 | add_action( 'user_profile_update_errors', array( $this, 'check_password_history' ), 10, 3 ); |
| @@ -284,9 +287,11 @@ | ||
| 284 | 287 | * @return bool |
| 285 | 288 | */ |
| 286 | 289 | private function is_insecure_username( $username ) { |
| 287 | 290 | $username = strtolower( trim( $username ) ); |
| 288 | - $insecure_usernames = $this->options['insecure_usernames'] ?? array(); | |
| 291 | + // Vigilante_Settings::list_or_shipped(): an empty list of a setting with | |
| 292 | + // no field on screen is a leftover, not a choice. See its docblock. | |
| 293 | + $insecure_usernames = $this->get_insecure_usernames(); | |
| 289 | 294 | |
| 290 | 295 | return in_array( $username, array_map( 'strtolower', $insecure_usernames ), true ); |
| 291 | 296 | } |
| 292 | 297 | |
| @@ -988,9 +993,14 @@ | ||
| 988 | 993 | * |
| 989 | 994 | * @return array |
| 990 | 995 | */ |
| 991 | 996 | public function get_insecure_usernames() { |
| 992 | - return $this->options['insecure_usernames'] ?? array(); | |
| 997 | + // The single reader of this list, so what blocks and what the screen | |
| 998 | + // reports can never disagree. | |
| 999 | + return Vigilante_Settings::list_or_shipped( | |
| 1000 | + $this->options['insecure_usernames'] ?? array(), | |
| 1001 | + array( 'user_security', 'insecure_usernames' ) | |
| 1002 | + ); | |
| 993 | 1003 | } |
| 994 | 1004 | |
| 995 | 1005 | /** |
| 996 | 1006 | * Check for existing insecure admin users |
| @@ -1249,22 +1259,27 @@ | ||
| 1249 | 1259 | * @param string $password Password. |
| 1250 | 1260 | * @return WP_User|WP_Error|null |
| 1251 | 1261 | */ |
| 1252 | 1262 | public function check_force_reset_on_login( $user, $username, $password ) { |
| 1253 | - // Resolve the target user. The flag must be evaluated whether the | |
| 1254 | - // credentials matched (WP_User) or not (WP_Error). | |
| 1255 | - if ( $user instanceof WP_User ) { | |
| 1256 | - $login_user = $user; | |
| 1257 | - } else { | |
| 1258 | - $login_user = get_user_by( 'login', $username ); | |
| 1259 | - if ( ! $login_user ) { | |
| 1260 | - $login_user = get_user_by( 'email', $username ); | |
| 1261 | - } | |
| 1263 | + /* | |
| 1264 | + * Only a login that would otherwise have succeeded is turned into this | |
| 1265 | + * rejection. Wrong credentials are left exactly as WordPress reported | |
| 1266 | + * them, and they are counted like any other failed login. | |
| 1267 | + * | |
| 1268 | + * Until 2.11.12 this ran for a WP_Error too, resolving the account from | |
| 1269 | + * the username, and replaced an incorrect_password with the rejection | |
| 1270 | + * below. A rejection of Vigilant's own is not counted towards the brute | |
| 1271 | + * force lockout, so any account with a pending forced reset could be | |
| 1272 | + * guessed at without limit: measured on 17 Sep 2026 against 2.11.11 and | |
| 1273 | + * against the first build of 2.11.12, six wrong passwords in a row, none | |
| 1274 | + * of them counted and no lockout at the end. The message this function | |
| 1275 | + * exists to show belongs to whoever typed the right password. | |
| 1276 | + */ | |
| 1277 | + if ( ! ( $user instanceof WP_User ) ) { | |
| 1278 | + return $user; | |
| 1262 | 1279 | } |
| 1263 | 1280 | |
| 1264 | - if ( ! $login_user ) { | |
| 1265 | - return $user; | |
| 1266 | - } | |
| 1281 | + $login_user = $user; | |
| 1267 | 1282 | |
| 1268 | 1283 | // Check if this user has a pending forced reset. |
| 1269 | 1284 | $force_reset = get_user_meta( $login_user->ID, 'vigilante_force_reset_pending', true ); |
| 1270 | 1285 | if ( ! $force_reset ) { |
| @@ -1270,17 +1285,11 @@ | ||
| 1270 | 1285 | if ( ! $force_reset ) { |
| 1271 | 1286 | return $user; |
| 1272 | 1287 | } |
| 1273 | 1288 | |
| 1274 | - // If credentials were wrong with an error other than incorrect_password | |
| 1275 | - // (e.g. a Vigilant lockout, pending approval), don't shadow it. | |
| 1276 | - if ( is_wp_error( $user ) && ! in_array( 'incorrect_password', $user->get_error_codes(), true ) ) { | |
| 1277 | - return $user; | |
| 1278 | - } | |
| 1289 | + // Not counted towards the brute force lockout: the rejection is | |
| 1290 | + // recognised by its error code (Vigilante_Login_Security::CONTROLLED_REJECTIONS). | |
| 1279 | 1291 | |
| 1280 | - // Skip brute force counter for this controlled rejection. | |
| 1281 | - add_filter( 'vigilante_skip_failed_login_count', '__return_true' ); | |
| 1282 | - | |
| 1283 | 1292 | // Surface the controlled rejection in the activity log so the admin |
| 1284 | 1293 | // can tell apart "user fails login because they typed wrong password" |
| 1285 | 1294 | // from "user fails login because we are forcing a reset". |
| 1286 | 1295 | if ( $this->activity_log ) { |
| @@ -1342,9 +1351,14 @@ | ||
| 1342 | 1351 | return; |
| 1343 | 1352 | } |
| 1344 | 1353 | |
| 1345 | 1354 | $settings = $this->options['registration_approval'] ?? array(); |
| 1346 | - $affected_roles = $settings['affected_roles'] ?? array( 'subscriber' ); | |
| 1355 | + // Vigilante_Settings::list_or_shipped(): an empty list of a setting with | |
| 1356 | + // no field on screen is a leftover, not a choice. See its docblock. | |
| 1357 | + $affected_roles = Vigilante_Settings::list_or_shipped( | |
| 1358 | + $settings['affected_roles'] ?? array(), | |
| 1359 | + array( 'user_security', 'registration_approval', 'affected_roles' ) | |
| 1360 | + ); | |
| 1347 | 1361 | |
| 1348 | 1362 | // Check if user role requires approval |
| 1349 | 1363 | $user_roles = $user->roles; |
| 1350 | 1364 | $needs_approval = array_intersect( $user_roles, $affected_roles ); |
| @@ -1390,11 +1404,8 @@ | ||
| 1390 | 1404 | return $user; |
| 1391 | 1405 | } |
| 1392 | 1406 | |
| 1393 | 1407 | if ( self::is_pending_anywhere( $user->ID ) ) { |
| 1394 | - // Mark this as a controlled rejection (not a brute force attempt) | |
| 1395 | - add_filter( 'vigilante_skip_failed_login_count', '__return_true' ); | |
| 1396 | - | |
| 1397 | 1408 | return new WP_Error( |
| 1398 | 1409 | 'pending_approval', |
| 1399 | 1410 | __( '<strong>Account pending:</strong> Your account is awaiting administrator approval. You will receive an email once approved.', 'vigilante' ) |
| 1400 | 1411 | ); |
| @@ -2143,11 +2154,8 @@ | ||
| 2143 | 2154 | 'warning' |
| 2144 | 2155 | ); |
| 2145 | 2156 | } |
| 2146 | 2157 | |
| 2147 | - // Mark this as a controlled rejection (not a brute force attempt) | |
| 2148 | - add_filter( 'vigilante_skip_failed_login_count', '__return_true' ); | |
| 2149 | - | |
| 2150 | 2158 | return new WP_Error( |
| 2151 | 2159 | 'session_limit_exceeded', |
| 2152 | 2160 | sprintf( |
| 2153 | 2161 | /* translators: %d: Maximum sessions allowed */ |
| @@ -2314,11 +2322,13 @@ | ||
| 2314 | 2322 | } |
| 2315 | 2323 | return; |
| 2316 | 2324 | } |
| 2317 | 2325 | |
| 2318 | - // Check if must change password | |
| 2319 | - $must_change = get_user_meta( $user_id, 'vigilante_must_change_password', true ); | |
| 2320 | - if ( $must_change ) { | |
| 2326 | + // Ask what the redirect asks. From 2.11.9 to 3.0.3 this read only the | |
| 2327 | + // flag, which is written at login, while force_password_change_redirect() | |
| 2328 | + // also works the expiry out on the spot: a session still open when the | |
| 2329 | + // password expired was sent to the profile with no notice saying why. | |
| 2330 | + if ( $this->must_change_password( $user_id ) ) { | |
| 2321 | 2331 | global $pagenow; |
| 2322 | 2332 | $on_profile = ( 'profile.php' === $pagenow ); |
| 2323 | 2333 | ?> |
| 2324 | 2334 | <div class="notice notice-error"> |
| @@ -2325,8 +2335,9 @@ | ||
| 2325 | 2335 | <p> |
| 2326 | 2336 | <strong><?php esc_html_e( 'Password change required', 'vigilante' ); ?></strong> |
| 2327 | 2337 | <?php if ( $on_profile ) : ?> |
| 2328 | 2338 | <?php esc_html_e( 'Your password has expired. Set a new password in the section below and save your profile to continue.', 'vigilante' ); ?> |
| 2339 | + <a href="#password"><?php esc_html_e( 'Go to the password field', 'vigilante' ); ?></a> | |
| 2329 | 2340 | <?php else : ?> |
| 2330 | 2341 | <?php |
| 2331 | 2342 | printf( |
| 2332 | 2343 | /* translators: %s: Link to profile */ |
| @@ -2345,28 +2356,40 @@ | ||
| 2345 | 2356 | <?php |
| 2346 | 2357 | return; |
| 2347 | 2358 | } |
| 2348 | 2359 | |
| 2349 | - // Show warning if expiring soon | |
| 2360 | + // Show warning if expiring soon. get_days_until_expiration() rounds down, | |
| 2361 | + // so 0 here is less than a day left: an expired password never gets this | |
| 2362 | + // far, must_change_password() has answered for it above. Until 3.0.3 the | |
| 2363 | + // check asked for more than 0, so the last day showed nothing and the | |
| 2364 | + // next thing the user saw was the forced change. | |
| 2350 | 2365 | $days_left = $this->get_days_until_expiration( $user_id ); |
| 2351 | 2366 | $warning_days = absint( $settings['warning_days'] ?? 14 ); |
| 2352 | 2367 | |
| 2353 | - if ( $days_left > 0 && $days_left <= $warning_days ) { | |
| 2368 | + if ( $warning_days > 0 && $days_left >= 0 && $days_left <= $warning_days ) { | |
| 2354 | 2369 | ?> |
| 2355 | 2370 | <div class="notice notice-warning is-dismissible"> |
| 2356 | 2371 | <p> |
| 2357 | 2372 | <?php |
| 2358 | - printf( | |
| 2359 | - /* translators: 1: Number of days, 2: Link to profile */ | |
| 2360 | - esc_html( _n( | |
| 2361 | - 'Your password will expire in %1$d day. Please %2$s.', | |
| 2362 | - 'Your password will expire in %1$d days. Please %2$s.', | |
| 2363 | - $days_left, | |
| 2364 | - 'vigilante' | |
| 2365 | - ) ), | |
| 2366 | - absint( $days_left ), | |
| 2367 | - '<a href="' . esc_url( admin_url( 'profile.php' ) ) . '">' . esc_html__( 'change it now', 'vigilante' ) . '</a>' | |
| 2368 | - ); | |
| 2373 | + if ( $days_left < 1 ) { | |
| 2374 | + printf( | |
| 2375 | + /* translators: %s: Link to profile */ | |
| 2376 | + esc_html__( 'Your password expires in less than a day. Please %s.', 'vigilante' ), | |
| 2377 | + '<a href="' . esc_url( admin_url( 'profile.php' ) ) . '">' . esc_html__( 'change it now', 'vigilante' ) . '</a>' | |
| 2378 | + ); | |
| 2379 | + } else { | |
| 2380 | + printf( | |
| 2381 | + /* translators: 1: Number of days, 2: Link to profile */ | |
| 2382 | + esc_html( _n( | |
| 2383 | + 'Your password will expire in %1$d day. Please %2$s.', | |
| 2384 | + 'Your password will expire in %1$d days. Please %2$s.', | |
| 2385 | + $days_left, | |
| 2386 | + 'vigilante' | |
| 2387 | + ) ), | |
| 2388 | + absint( $days_left ), | |
| 2389 | + '<a href="' . esc_url( admin_url( 'profile.php' ) ) . '">' . esc_html__( 'change it now', 'vigilante' ) . '</a>' | |
| 2390 | + ); | |
| 2391 | + } | |
| 2369 | 2392 | ?> |
| 2370 | 2393 | </p> |
| 2371 | 2394 | </div> |
| 2372 | 2395 | <?php |
| @@ -2417,8 +2440,63 @@ | ||
| 2417 | 2440 | return $this->is_password_expired( $user_id ); |
| 2418 | 2441 | } |
| 2419 | 2442 | |
| 2420 | 2443 | /** |
| 2444 | + * Whether this request is the user's own profile form and nothing else | |
| 2445 | + * | |
| 2446 | + * A user who must change an expired password is let through to the profile, | |
| 2447 | + * because that is where the change is made. Until 3.0.3 "the profile" meant | |
| 2448 | + * the name of the script, and profile.php can be asked for other things: the | |
| 2449 | + * admin bootstrap runs a plugin page or an importer named in the address | |
| 2450 | + * whatever the script is, and fires the hook of any action it is sent | |
| 2451 | + * (wp-admin/admin.php), and the profile script edits whichever user the | |
| 2452 | + * request names (wp-admin/user-edit.php). The pending change could be put | |
| 2453 | + * off while part of the admin area was still in use. | |
| 2454 | + * | |
| 2455 | + * Two requests are the profile form: the one that shows it, which posts | |
| 2456 | + * nothing, and the one that saves it carrying the nonce WordPress printed | |
| 2457 | + * on this user's own form. Parameters in the address that WordPress does | |
| 2458 | + * nothing with are left alone, so a proxy or a browser extension that adds | |
| 2459 | + * one to every address does not send the user round in circles. | |
| 2460 | + * | |
| 2461 | + * Meant for admin_init. wp-admin/profile.php defines IS_PROFILE_PAGE before | |
| 2462 | + * it loads the admin bootstrap, so the constant is there when that script is | |
| 2463 | + * the one running. The network admin and user dashboard copies load the | |
| 2464 | + * bootstrap first: from them the user is taken to the site profile, where | |
| 2465 | + * the notice is shown. | |
| 2466 | + * | |
| 2467 | + * @return bool | |
| 2468 | + */ | |
| 2469 | + private function is_own_profile_request() { | |
| 2470 | + global $pagenow, $plugin_page; | |
| 2471 | + | |
| 2472 | + if ( 'profile.php' !== $pagenow || ! defined( 'IS_PROFILE_PAGE' ) || ! IS_PROFILE_PAGE ) { | |
| 2473 | + return false; | |
| 2474 | + } | |
| 2475 | + | |
| 2476 | + // A plugin page or an importer named in the address. | |
| 2477 | + if ( isset( $plugin_page ) || isset( $_GET['import'] ) ) { | |
| 2478 | + return false; | |
| 2479 | + } | |
| 2480 | + | |
| 2481 | + // Somebody else's profile. | |
| 2482 | + $own_id = (string) get_current_user_id(); | |
| 2483 | + if ( ! empty( $_REQUEST['user_id'] ) && $own_id !== $_REQUEST['user_id'] ) { | |
| 2484 | + return false; | |
| 2485 | + } | |
| 2486 | + | |
| 2487 | + // No action: the form being shown, which posts nothing. | |
| 2488 | + if ( empty( $_REQUEST['action'] ) ) { | |
| 2489 | + return empty( $_POST ); | |
| 2490 | + } | |
| 2491 | + | |
| 2492 | + // An action: only the form being saved. | |
| 2493 | + return 'update' === $_REQUEST['action'] | |
| 2494 | + && isset( $_REQUEST['_wpnonce'] ) | |
| 2495 | + && false !== wp_verify_nonce( sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ), 'update-user_' . $own_id ); | |
| 2496 | + } | |
| 2497 | + | |
| 2498 | + /** | |
| 2421 | 2499 | * Force a user with an expired password to change it, on wp-admin and AJAX |
| 2422 | 2500 | * |
| 2423 | 2501 | * Until 2.11.9 this only redirected wp-admin pages and skipped AJAX, so an |
| 2424 | 2502 | * expired-password session kept working through admin-ajax, and the REST API |
| @@ -2426,9 +2504,11 @@ | ||
| 2426 | 2504 | * 2.11.8 flagged it: setting a flag on login is not enforcement if the flag |
| 2427 | 2505 | * is only read by one redirect. It is now enforced on every entry point, |
| 2428 | 2506 | * here for wp-admin and AJAX and in the three methods below for REST, the |
| 2429 | 2507 | * front end and XML-RPC. The only thing an affected user can still do is |
| 2430 | - * change the password on profile.php or log out. | |
| 2508 | + * change the password on profile.php or log out. From 2.11.9 to 3.0.3 that | |
| 2509 | + * was not quite true, because any request to profile.php was let through: | |
| 2510 | + * see is_own_profile_request(). | |
| 2431 | 2511 | */ |
| 2432 | 2512 | public function force_password_change_redirect() { |
| 2433 | 2513 | if ( ! is_user_logged_in() || ! $this->must_change_password( get_current_user_id() ) ) { |
| 2434 | 2514 | return; |
| @@ -2443,15 +2523,46 @@ | ||
| 2443 | 2523 | 403 |
| 2444 | 2524 | ); |
| 2445 | 2525 | } |
| 2446 | 2526 | |
| 2447 | - // profile.php is where the change happens; do not redirect it onto itself. | |
| 2448 | - global $pagenow; | |
| 2449 | - if ( 'profile.php' === $pagenow ) { | |
| 2527 | + // The profile form is where the change happens; do not redirect it onto | |
| 2528 | + // itself. Anything else profile.php is asked for goes back to the form. | |
| 2529 | + if ( $this->is_own_profile_request() ) { | |
| 2450 | 2530 | return; |
| 2451 | 2531 | } |
| 2452 | 2532 | |
| 2453 | - wp_safe_redirect( admin_url( 'profile.php#password' ) ); | |
| 2533 | + $this->redirect_to_profile(); | |
| 2534 | + } | |
| 2535 | + | |
| 2536 | + /** | |
| 2537 | + * Send a user who must change an expired password to their profile | |
| 2538 | + * | |
| 2539 | + * To the top of it, with no "#password" fragment (it had one from 1.0.0 to | |
| 2540 | + * 3.0.3): the browser jumped straight to the password row and left the | |
| 2541 | + * notice that says why, printed at the top of the page, out of sight. The | |
| 2542 | + * notice carries the link to the field instead. | |
| 2543 | + * | |
| 2544 | + * wp_safe_redirect() answers an address it does not take for the site's | |
| 2545 | + * own with a redirect to admin_url(), the dashboard, and the dashboard sent | |
| 2546 | + * the user here again, and so on until the browser gave up. It only knows | |
| 2547 | + * the home host, so that happened on a site whose WordPress lives on a host | |
| 2548 | + * of its own, and also where the host has non-ASCII letters stored as | |
| 2549 | + * written or is an IPv6 literal. The address sent is built here from the | |
| 2550 | + * site's own settings and carries nothing from the request, so the fallback | |
| 2551 | + * for this one call is that same address: what admin_url() says is as good | |
| 2552 | + * for the profile as it is for the dashboard WordPress falls back to. | |
| 2553 | + */ | |
| 2554 | + private function redirect_to_profile() { | |
| 2555 | + $profile = admin_url( 'profile.php' ); | |
| 2556 | + | |
| 2557 | + add_filter( | |
| 2558 | + 'wp_safe_redirect_fallback', | |
| 2559 | + static function () use ( $profile ) { | |
| 2560 | + return $profile; | |
| 2561 | + } | |
| 2562 | + ); | |
| 2563 | + | |
| 2564 | + wp_safe_redirect( $profile ); | |
| 2454 | 2565 | exit; |
| 2455 | 2566 | } |
| 2456 | 2567 | |
| 2457 | 2568 | /** |
| @@ -2489,13 +2600,46 @@ | ||
| 2489 | 2600 | if ( is_admin() || ! is_user_logged_in() || ! $this->must_change_password( get_current_user_id() ) ) { |
| 2490 | 2601 | return; |
| 2491 | 2602 | } |
| 2492 | 2603 | |
| 2493 | - wp_safe_redirect( admin_url( 'profile.php#password' ) ); | |
| 2494 | - exit; | |
| 2604 | + $this->redirect_to_profile(); | |
| 2495 | 2605 | } |
| 2496 | 2606 | |
| 2497 | 2607 | /** |
| 2608 | + * Let a user who must change an expired password reach the profile in a WooCommerce store | |
| 2609 | + * | |
| 2610 | + * WooCommerce sends accounts with no back-end role (a customer, a subscriber) | |
| 2611 | + * from wp-admin to My Account, and force_password_change_frontend() sends | |
| 2612 | + * them from there back to the profile. With one of those roles in Affected | |
| 2613 | + * Roles the two redirects chased each other until the browser gave up, from | |
| 2614 | + * 2.11.9 to 3.0.3, and the account could neither use the store nor change | |
| 2615 | + * the password. | |
| 2616 | + * | |
| 2617 | + * WooCommerce's redirect is lifted on the profile and nowhere else, and only | |
| 2618 | + * while the change is pending: from any other wp-admin screen the account | |
| 2619 | + * still ends up on the profile, and once the new password is saved | |
| 2620 | + * WooCommerce sends it back to My Account as it always does. | |
| 2621 | + * | |
| 2622 | + * What this depends on is not the caller's to choose: the user comes from | |
| 2623 | + * the session, whether the change is pending from must_change_password(), | |
| 2624 | + * and the request has to be the profile form and nothing else, which is | |
| 2625 | + * is_own_profile_request(), the same test the redirect uses. What it gives | |
| 2626 | + * is the user's own profile, which WordPress opens to any account that can | |
| 2627 | + * read (wp-admin/menu.php); WooCommerce's redirect is the store's | |
| 2628 | + * preference, not a permission. | |
| 2629 | + * | |
| 2630 | + * @param bool $prevent_access Whether WooCommerce is about to send this request out of wp-admin. | |
| 2631 | + * @return bool | |
| 2632 | + */ | |
| 2633 | + public function allow_profile_for_password_change( $prevent_access ) { | |
| 2634 | + if ( $prevent_access && is_user_logged_in() && $this->is_own_profile_request() && $this->must_change_password( get_current_user_id() ) ) { | |
| 2635 | + return false; | |
| 2636 | + } | |
| 2637 | + | |
| 2638 | + return $prevent_access; | |
| 2639 | + } | |
| 2640 | + | |
| 2641 | + /** | |
| 2498 | 2642 | * Refuse XML-RPC calls from a user whose password has expired |
| 2499 | 2643 | * |
| 2500 | 2644 | * The last of the four non-wp-admin entry points. XML-RPC authenticates on |
| 2501 | 2645 | * every call with the account credentials (a password or an application |
| @@ -3239,9 +3383,9 @@ | ||
| 3239 | 3383 | |
| 3240 | 3384 | // Check if this user's role requires approval |
| 3241 | 3385 | $needs_approval = false; |
| 3242 | 3386 | if ( ! empty( $registration_approval['enabled'] ) ) { |
| 3243 | - $affected_roles = $registration_approval['affected_roles'] ?? array( 'subscriber' ); | |
| 3387 | + $affected_roles = Vigilante_Settings::list_or_shipped( $registration_approval['affected_roles'] ?? array(), array( 'user_security', 'registration_approval', 'affected_roles' ) ); | |
| 3244 | 3388 | $needs_approval = ! empty( array_intersect( $user->roles, $affected_roles ) ); |
| 3245 | 3389 | } |
| 3246 | 3390 | |
| 3247 | 3391 | // Check if email verification is enabled |
| @@ -3353,9 +3497,9 @@ | ||
| 3353 | 3497 | if ( empty( $registration_approval['enabled'] ) ) { |
| 3354 | 3498 | return false; |
| 3355 | 3499 | } |
| 3356 | 3500 | |
| 3357 | - $affected_roles = $registration_approval['affected_roles'] ?? array( 'subscriber' ); | |
| 3501 | + $affected_roles = Vigilante_Settings::list_or_shipped( $registration_approval['affected_roles'] ?? array(), array( 'user_security', 'registration_approval', 'affected_roles' ) ); | |
| 3358 | 3502 | return ! empty( array_intersect( $user->roles, $affected_roles ) ); |
| 3359 | 3503 | } |
| 3360 | 3504 | |
| 3361 | 3505 | /** |