Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
78.38% covered (warning)
78.38%
29 / 37
50.00% covered (danger)
50.00%
3 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
Password_Manager
78.38% covered (warning)
78.38%
29 / 37
50.00% covered (danger)
50.00%
3 / 6
29.82
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validate_profile_update
66.67% covered (warning)
66.67%
6 / 9
0.00% covered (danger)
0.00%
0 / 1
5.93
 validate_password_reset
75.00% covered (warning)
75.00%
9 / 12
0.00% covered (danger)
0.00%
0 / 1
6.56
 on_profile_update
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
5
 on_password_reset
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
4
 save_recent_password_hash
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
3.14
1<?php
2/**
3 * Class used to define Password Manager.
4 *
5 * @package automattic/jetpack-account-protection
6 */
7
8namespace Automattic\Jetpack\Account_Protection;
9
10/**
11 * Class Password_Manager
12 */
13class Password_Manager {
14    /**
15     * Validaton service instance
16     *
17     * @var Validation_Service
18     */
19    private $validation_service;
20
21    /**
22     * Validation_Service constructor.
23     *
24     * @param ?Validation_Service $validation_service Password manager instance.
25     */
26    public function __construct( ?Validation_Service $validation_service = null ) {
27        $this->validation_service = $validation_service ?? new Validation_Service();
28    }
29
30    /**
31     * Validate the profile update.
32     *
33     * @param \WP_Error $errors The error object.
34     * @param bool      $update Whether the user is being updated.
35     * @param \stdClass $user A copy of the new user object.
36     *
37     * @return void
38     */
39    public function validate_profile_update( \WP_Error $errors, bool $update, \stdClass $user ): void {
40        if ( empty( $user->user_pass ) ) {
41            return;
42        }
43
44        // If bypass is enabled, do not validate the password
45        // phpcs:ignore WordPress.Security.NonceVerification
46        if ( isset( $_POST['pw_weak'] ) && 'on' === $_POST['pw_weak'] ) {
47            return;
48        }
49
50        $core_validation_errors    = $errors->get_error_messages( 'pass' );
51        $jetpack_validation_errors = $this->validation_service->get_validation_errors( $user->user_pass, true, $user );
52        $validation_errors         = array_diff( $jetpack_validation_errors, $core_validation_errors );
53
54        foreach ( $validation_errors as $validation_error ) {
55            $errors->add( 'pass', $validation_error, array( 'form-field' => 'pass1' ) );
56        }
57    }
58
59    /**
60     * Validate the password reset.
61     *
62     * @param \WP_Error          $errors The error object.
63     * @param \WP_User|\WP_Error $user The user object.
64     *
65     * @return void
66     */
67    public function validate_password_reset( \WP_Error $errors, $user ): void {
68        if ( is_wp_error( $user ) ) {
69            return;
70        }
71
72        // phpcs:ignore WordPress.Security.NonceVerification
73        if ( empty( $_POST['pass1'] ) ) {
74            return;
75        }
76
77        // If bypass is enabled, do not validate the password
78        // phpcs:ignore WordPress.Security.NonceVerification
79        if ( isset( $_POST['pw_weak'] ) && 'on' === $_POST['pw_weak'] ) {
80            return;
81        }
82
83        // phpcs:ignore WordPress.Security.NonceVerification, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
84        $password = wp_unslash( $_POST['pass1'] );
85
86        $core_validation_errors    = $errors->get_error_messages( 'pass' );
87        $jetpack_validation_errors = $this->validation_service->get_validation_errors( $password );
88        $validation_errors         = array_diff( $jetpack_validation_errors, $core_validation_errors );
89
90        foreach ( $validation_errors as $validation_error ) {
91            $errors->add( 'pass', $validation_error, array( 'form-field' => 'pass1' ) );
92        }
93    }
94
95    /**
96     * Handle the profile update.
97     *
98     * @param int                     $user_id The user ID.
99     * @param \WP_User|\stdClass|null $old_user_data Object containing user data prior to update.
100     *
101     * @return void
102     */
103    public function on_profile_update( int $user_id, $old_user_data ): void {
104        if ( ! is_object( $old_user_data ) || empty( $old_user_data->user_pass ) ) {
105            return;
106        }
107
108        // phpcs:ignore WordPress.Security.NonceVerification
109        if ( isset( $_POST['action'] ) && $_POST['action'] === 'update' ) {
110            $this->save_recent_password_hash( $user_id, $old_user_data->user_pass );
111        }
112    }
113
114    /**
115     * Handle the password reset.
116     *
117     * @param \WP_User|\stdClass|null $user The user object.
118     *
119     * @return void
120     */
121    public function on_password_reset( $user ): void {
122        if ( ! is_object( $user ) || ! isset( $user->ID ) || empty( $user->user_pass ) ) {
123            return;
124        }
125
126        $this->save_recent_password_hash( $user->ID, $user->user_pass );
127    }
128
129    /**
130     * Save the new password hash to the user's recent passwords list.
131     *
132     * @param int    $user_id  The user ID.
133     * @param string $password_hash The password hash to store.
134     *
135     * @return void
136     */
137    public function save_recent_password_hash( int $user_id, string $password_hash ): void {
138        $recent_passwords = get_user_meta( $user_id, Config::RECENT_PASSWORD_HASHES_USER_META_KEY, true );
139
140        if ( ! is_array( $recent_passwords ) ) {
141            $recent_passwords = array();
142        }
143
144        if ( in_array( $password_hash, $recent_passwords, true ) ) {
145            return;
146        }
147
148        // Add the new hashed password and keep only the last 10
149        array_unshift( $recent_passwords, $password_hash );
150        $recent_passwords = array_slice( $recent_passwords, 0, Config::PASSWORD_MANAGER_RECENT_PASSWORDS_LIMIT );
151
152        update_user_meta( $user_id, Config::RECENT_PASSWORD_HASHES_USER_META_KEY, $recent_passwords );
153    }
154}