PluginProbe
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… / 3.0.3
Vigilant – 100% Free Security Suite: Firewall, 2FA, Login, Headers, Scanner… v3.0.3
3.0.5 3.0.4 3.0.3 3.0.2 3.0.1 3.0.0 2.11.12 2.11.11 2.11.10 2.11.9 2.11.7 2.11.8 2.11.6 2.11.5 2.11.4 2.11.3 2.11.1 2.11.2 2.11.0 2.10.5 2.10.4 2.10.3 2.10.2 2.10.1 2.10.0 All 93 releases
← All changes | includes/class-user-security.php +23 -162 3.0.4 → 3.0.3 View file →
@@ -173,11 +173,8 @@
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' ) );
180 177 add_filter( 'authenticate', array( $this, 'block_expired_password_xmlrpc' ), 30, 1 );
181 178 add_action( 'profile_update', array( $this, 'update_password_change_date' ), 10, 2 );
182 179 add_action( 'user_register', array( $this, 'set_initial_password_date' ) );
183 180 add_action( 'user_profile_update_errors', array( $this, 'check_password_history' ), 10, 3 );
@@ -2322,13 +2319,11 @@
2322 2319 }
2323 2320 return;
2324 2321 }
2325 2322
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 ) ) {
2323 + // Check if must change password
2324 + $must_change = get_user_meta( $user_id, 'vigilante_must_change_password', true );
2325 + if ( $must_change ) {
2331 2326 global $pagenow;
2332 2327 $on_profile = ( 'profile.php' === $pagenow );
2333 2328 ?>
2334 2329 <div class="notice notice-error">
@@ -2335,9 +2330,8 @@
2335 2330 <p>
2336 2331 <strong><?php esc_html_e( 'Password change required', 'vigilante' ); ?></strong>
2337 2332 <?php if ( $on_profile ) : ?>
2338 2333 <?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>
2340 2334 <?php else : ?>
2341 2335 <?php
2342 2336 printf(
2343 2337 /* translators: %s: Link to profile */
@@ -2356,40 +2350,28 @@
2356 2350 <?php
2357 2351 return;
2358 2352 }
2359 2353
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.
2354 + // Show warning if expiring soon
2365 2355 $days_left = $this->get_days_until_expiration( $user_id );
2366 2356 $warning_days = absint( $settings['warning_days'] ?? 14 );
2367 2357
2368 - if ( $warning_days > 0 && $days_left >= 0 && $days_left <= $warning_days ) {
2358 + if ( $days_left > 0 && $days_left <= $warning_days ) {
2369 2359 ?>
2370 2360 <div class="notice notice-warning is-dismissible">
2371 2361 <p>
2372 2362 <?php
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 - }
2363 + printf(
2364 + /* translators: 1: Number of days, 2: Link to profile */
2365 + esc_html( _n(
2366 + 'Your password will expire in %1$d day. Please %2$s.',
2367 + 'Your password will expire in %1$d days. Please %2$s.',
2368 + $days_left,
2369 + 'vigilante'
2370 + ) ),
2371 + absint( $days_left ),
2372 + '<a href="' . esc_url( admin_url( 'profile.php' ) ) . '">' . esc_html__( 'change it now', 'vigilante' ) . '</a>'
2373 + );
2392 2374 ?>
2393 2375 </p>
2394 2376 </div>
2395 2377 <?php
@@ -2440,63 +2422,8 @@
2440 2422 return $this->is_password_expired( $user_id );
2441 2423 }
2442 2424
2443 2425 /**
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 - /**
2499 2426 * Force a user with an expired password to change it, on wp-admin and AJAX
2500 2427 *
2501 2428 * Until 2.11.9 this only redirected wp-admin pages and skipped AJAX, so an
2502 2429 * expired-password session kept working through admin-ajax, and the REST API
@@ -2504,11 +2431,9 @@
2504 2431 * 2.11.8 flagged it: setting a flag on login is not enforcement if the flag
2505 2432 * is only read by one redirect. It is now enforced on every entry point,
2506 2433 * here for wp-admin and AJAX and in the three methods below for REST, the
2507 2434 * front end and XML-RPC. The only thing an affected user can still do is
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().
2435 + * change the password on profile.php or log out.
2511 2436 */
2512 2437 public function force_password_change_redirect() {
2513 2438 if ( ! is_user_logged_in() || ! $this->must_change_password( get_current_user_id() ) ) {
2514 2439 return;
@@ -2523,46 +2448,15 @@
2523 2448 403
2524 2449 );
2525 2450 }
2526 2451
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() ) {
2452 + // profile.php is where the change happens; do not redirect it onto itself.
2453 + global $pagenow;
2454 + if ( 'profile.php' === $pagenow ) {
2530 2455 return;
2531 2456 }
2532 2457
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 );
2458 + wp_safe_redirect( admin_url( 'profile.php#password' ) );
2565 2459 exit;
2566 2460 }
2567 2461
2568 2462 /**
@@ -2600,43 +2494,10 @@
2600 2494 if ( is_admin() || ! is_user_logged_in() || ! $this->must_change_password( get_current_user_id() ) ) {
2601 2495 return;
2602 2496 }
2603 2497
2604 - $this->redirect_to_profile();
2605 - }
2606 -
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;
2498 + wp_safe_redirect( admin_url( 'profile.php#password' ) );
2499 + exit;
2639 2500 }
2640 2501
2641 2502 /**
2642 2503 * Refuse XML-RPC calls from a user whose password has expired