Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
57.97% covered (warning)
57.97%
40 / 69
42.11% covered (danger)
42.11%
8 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
Account_Protection
57.97% covered (warning)
57.97%
40 / 69
42.11% covered (danger)
42.11%
8 / 19
119.82
0.00% covered (danger)
0.00%
0 / 1
 instance
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 initialize
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
3.04
 __construct
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_password_detection
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_password_manager
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_password_strength_meter
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 register_hooks
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 register_runtime_hooks
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 register_password_detection_hooks
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 register_password_manager_hooks
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 register_password_strength_meter_hooks
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 register_strong_passwords_hooks
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 is_enabled
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 enable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 disable
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 is_supported_environment
60.00% covered (warning)
60.00%
3 / 5
0.00% covered (danger)
0.00%
0 / 1
5.02
 has_unsupported_jetpack_version
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
 remove_module_on_unsupported_environments
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 remove_standalone_module_on_unsupported_environments
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2/**
3 * Class used to define Account Protection.
4 *
5 * @package automattic/jetpack-account-protection
6 */
7
8namespace Automattic\Jetpack\Account_Protection;
9
10use Automattic\Jetpack\Constants;
11use Automattic\Jetpack\Modules;
12use Automattic\Jetpack\Status\Host;
13
14/**
15 * Class Account_Protection
16 */
17class Account_Protection {
18    const PACKAGE_VERSION                = '0.3.7';
19    const ACCOUNT_PROTECTION_MODULE_NAME = 'account-protection';
20
21    /**
22     * Account_Protection instance
23     *
24     * @var Account_Protection
25     */
26    private static $instance = null;
27
28    /**
29     * Flag to track if hooks have been registered.
30     *
31     * @var bool
32     */
33    private static $hooks_registered = false;
34
35    /**
36     * Modules instance.
37     *
38     * @var Modules
39     */
40    private $modules;
41
42    /**
43     * Password detection instance.
44     *
45     * @var Password_Detection|null
46     */
47    private $password_detection;
48
49    /**
50     * Password manager instance
51     *
52     * @var Password_Manager|null
53     */
54    private $password_manager;
55
56    /**
57     * Password strength meter instance
58     *
59     * @var Password_Strength_Meter|null
60     */
61    private $password_strength_meter;
62
63    /**
64     * Initialize the Account_Protection instance
65     *
66     * @return Account_Protection
67     */
68    public static function instance(): Account_Protection {
69        if ( self::$instance === null ) {
70            self::$instance = new Account_Protection();
71        }
72
73        return self::$instance;
74    }
75
76    /**
77     * Initializes the configurations needed for the account protection module.
78     *
79     * @return void
80     */
81    public function initialize(): void {
82        if ( self::$hooks_registered ) {
83            return;
84        }
85
86        $this->register_hooks();
87
88        if ( $this->is_enabled() ) {
89            $this->register_runtime_hooks();
90        }
91
92        self::$hooks_registered = true;
93    }
94
95    /**
96     * Account_Protection constructor.
97     *
98     * @param ?Modules                 $modules            Modules instance.
99     * @param ?Password_Detection      $password_detection Password detection instance.
100     * @param ?Password_Manager        $password_manager Password manager instance.
101     * @param ?Password_Strength_Meter $password_strength_meter Password strength meter instance.
102     */
103    public function __construct( ?Modules $modules = null, ?Password_Detection $password_detection = null, ?Password_Manager $password_manager = null, ?Password_Strength_Meter $password_strength_meter = null ) {
104        $this->modules = $modules ?? new Modules();
105
106        /*
107         * The password collaborators are only used by the runtime hooks, which
108         * only register when the module is active. Keep any injected instances
109         * (used by tests) but otherwise leave them unset and create them lazily,
110         * so their classes are not autoloaded on requests where the module is
111         * disabled.
112         */
113        $this->password_detection      = $password_detection;
114        $this->password_manager        = $password_manager;
115        $this->password_strength_meter = $password_strength_meter;
116    }
117
118    /**
119     * Lazily get the password detection instance.
120     *
121     * @return Password_Detection
122     */
123    private function get_password_detection(): Password_Detection {
124        if ( null === $this->password_detection ) {
125            $this->password_detection = new Password_Detection();
126        }
127        return $this->password_detection;
128    }
129
130    /**
131     * Lazily get the password manager instance.
132     *
133     * @return Password_Manager
134     */
135    private function get_password_manager(): Password_Manager {
136        if ( null === $this->password_manager ) {
137            $this->password_manager = new Password_Manager();
138        }
139        return $this->password_manager;
140    }
141
142    /**
143     * Lazily get the password strength meter instance.
144     *
145     * @return Password_Strength_Meter
146     */
147    private function get_password_strength_meter(): Password_Strength_Meter {
148        if ( null === $this->password_strength_meter ) {
149            $this->password_strength_meter = new Password_Strength_Meter();
150        }
151        return $this->password_strength_meter;
152    }
153
154    /**
155     * Register hooks for module activation and environment validation.
156     *
157     * @return void
158     */
159    protected function register_hooks(): void {
160        // Do not run in unsupported environments
161        add_filter( 'jetpack_get_available_modules', array( $this, 'remove_module_on_unsupported_environments' ) );
162        add_filter( 'jetpack_get_available_standalone_modules', array( $this, 'remove_standalone_module_on_unsupported_environments' ) );
163    }
164
165    /**
166     * Register hooks for runtime operations.
167     *
168     * @return void
169     */
170    protected function register_runtime_hooks(): void {
171        $this->register_password_detection_hooks();
172        $this->register_strong_passwords_hooks();
173    }
174
175    /**
176     * Register hooks for password detection.
177     *
178     * @return void
179     */
180    public function register_password_detection_hooks(): void {
181        $password_detection = $this->get_password_detection();
182        add_action( 'wp_authenticate_user', array( $password_detection, 'login_form_password_detection' ), 10, 2 );
183        add_action( 'login_form_password-detection', array( $password_detection, 'render_page' ), 10, 2 );
184        add_action( 'wp_enqueue_scripts', array( $password_detection, 'enqueue_styles' ) );
185    }
186
187    /**
188     * Register hooks for password manager.
189     *
190     * @return void
191     */
192    public function register_password_manager_hooks(): void {
193        $password_manager = $this->get_password_manager();
194        add_action( 'user_profile_update_errors', array( $password_manager, 'validate_profile_update' ), 10, 3 );
195        add_action( 'validate_password_reset', array( $password_manager, 'validate_password_reset' ), 10, 2 );
196        add_action( 'profile_update', array( $password_manager, 'on_profile_update' ), 10, 2 );
197        add_action( 'after_password_reset', array( $password_manager, 'on_password_reset' ), 10, 1 );
198    }
199
200    /**
201     * Register hooks for password strength meter.
202     *
203     * @return void
204     */
205    public function register_password_strength_meter_hooks(): void {
206        $password_strength_meter = $this->get_password_strength_meter();
207        add_action( 'admin_enqueue_scripts', array( $password_strength_meter, 'enqueue_jetpack_password_strength_meter_profile_script' ) );
208        add_action( 'login_enqueue_scripts', array( $password_strength_meter, 'enqueue_jetpack_password_strength_meter_reset_script' ) );
209        add_action( 'wp_ajax_validate_password_ajax', array( $password_strength_meter, 'validate_password_ajax' ) );
210        add_action( 'wp_ajax_nopriv_validate_password_ajax', array( $password_strength_meter, 'validate_password_ajax' ) );
211    }
212
213    /**
214     * Register hooks for strong passwords.
215     *
216     * @return void
217     */
218    public function register_strong_passwords_hooks(): void {
219        $this->register_password_manager_hooks();
220        $this->register_password_strength_meter_hooks();
221    }
222
223    /**
224     * Determines if the account protection module is enabled on the site.
225     *
226     * @return bool
227     */
228    public function is_enabled(): bool {
229        return $this->modules->is_active( self::ACCOUNT_PROTECTION_MODULE_NAME );
230    }
231
232    /**
233     * Enables the account protection module.
234     *
235     * @return bool
236     */
237    public function enable(): bool {
238        // Return true if already enabled.
239        if ( $this->is_enabled() ) {
240            return true;
241        }
242        return $this->modules->activate( self::ACCOUNT_PROTECTION_MODULE_NAME, false, false );
243    }
244
245    /**
246     * Disables the account protection module.
247     *
248     * @return bool
249     */
250    public function disable(): bool {
251        // Return true if already disabled.
252        if ( ! $this->is_enabled() ) {
253            return true;
254        }
255        return $this->modules->deactivate( self::ACCOUNT_PROTECTION_MODULE_NAME );
256    }
257
258    /**
259     * Determines if Account Protection is supported in the current environment.
260     *
261     * @return bool
262     */
263    public function is_supported_environment(): bool {
264        // Do not run when killswitch is enabled
265        if ( defined( 'DISABLE_JETPACK_ACCOUNT_PROTECTION' ) && DISABLE_JETPACK_ACCOUNT_PROTECTION ) {
266            return false;
267        }
268
269        // Do not run for WordPress.com Simple sites
270        if ( ( new Host() )->is_wpcom_simple() ) {
271            return false;
272        }
273
274        return true;
275    }
276
277    /**
278     * Determines if the current Jetpack version is supported.
279     *
280     * @return bool
281     */
282    public function has_unsupported_jetpack_version(): bool {
283        // Do not run when Jetpack version is less than 14.5
284        $jetpack_version = Constants::get_constant( 'JETPACK__VERSION' );
285        if ( $jetpack_version && version_compare( $jetpack_version, '14.5', '<' ) ) {
286            return true;
287        }
288
289        return false;
290    }
291
292    /**
293     * Disables the Account Protection module when on an unsupported platform in Jetpack.
294     *
295     * @param array $modules Filterable value for `jetpack_get_available_modules`.
296     *
297     * @return array Array of module slugs.
298     */
299    public function remove_module_on_unsupported_environments( array $modules ): array {
300        if ( ! $this->is_supported_environment() ) {
301            // Account protection should never be available on unsupported platforms.
302            unset( $modules[ self::ACCOUNT_PROTECTION_MODULE_NAME ] );
303        }
304
305        return $modules;
306    }
307
308    /**
309     * Disables the Account Protection module when on an unsupported platform in a standalone plugin.
310     *
311     * @param array $modules Filterable value for `jetpack_get_available_standalone_modules`.
312     *
313     * @return array Array of module slugs.
314     */
315    public function remove_standalone_module_on_unsupported_environments( array $modules ): array {
316        if ( ! $this->is_supported_environment() ) {
317            // Account Protection should never be available on unsupported platforms.
318            $modules = array_filter(
319                $modules,
320                function ( $module ) {
321                    return $module !== self::ACCOUNT_PROTECTION_MODULE_NAME;
322                }
323            );
324
325        }
326
327        return $modules;
328    }
329}