PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.9.0
Defender Security – Malware Scanner, Login Security & Firewall v5.9.0
6.3.0 6.2.3 6.2.4 6.2.0 6.2.1 6.2.2 6.1.0 5.3.1 5.4.0 5.4.1 5.5.0 5.5.1 5.6.0 5.6.1 5.6.2 5.7.0 5.7.1 5.7.2 5.8.0 5.8.1 5.9.0 6.0.0 6.0.1 3.0.1 3.1.0 All 141 releases
defender-security / src / controller / class-password-protection.php

class-password-protection.php in Defender Security – Malware Scanner, Login Security & Firewall 5.9.0, at src/controller/class-password-protection.php

419 lines 12.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles password protection.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use WP_User;
11 use WP_Error;
12 use stdClass;
13 use Countable;
14 use WP_Defender\Event;
15 use Calotes\Component\Request;
16 use Calotes\Component\Response;
17 use WP_Defender\Integrations\Woocommerce;
18 use WP_Defender\Component\Config\Config_Hub_Helper;
19
20 /**
21 * Handles password protection.
22 */
23 class Password_Protection extends Event {
24 /**
25 * The model for handling the data.
26 *
27 * @var \WP_Defender\Model\Setting\Password_Protection
28 */
29 public $model;
30
31 /**
32 * Service for handling logic.
33 *
34 * @var \WP_Defender\Component\Password_Protection
35 */
36 public $service;
37
38 /**
39 * Default message.
40 *
41 * @var string
42 */
43 public $default_msg;
44
45 /**
46 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
47 */
48 public function __construct() {
49 $this->model = wd_di()->get( \WP_Defender\Model\Setting\Password_Protection::class );
50 $this->service = wd_di()->get( \WP_Defender\Component\Password_Protection::class );
51 $default_values = $this->model->get_default_values();
52 $this->default_msg = $default_values['message'];
53 add_filter( 'wp_defender_advanced_tools_data', array( $this, 'script_data' ) );
54 $this->register_routes();
55 if ( $this->model->is_active() ) {
56 // Update site url on sub-site when MaskLogin is disabled.
57 if (
58 is_multisite() && ! is_main_site()
59 && ! wd_di()->get( \WP_Defender\Model\Setting\Mask_Login::class )->is_active()
60 ) {
61 add_filter( 'network_site_url', array( $this, 'filter_site_url' ), 100, 2 );
62 }
63 add_filter( 'wp_authenticate_user', array( $this, 'handle_login_password' ), 100, 2 );
64 add_action( 'validate_password_reset', array( $this, 'handle_reset_check_password' ), 100, 2 );
65 add_action( 'user_profile_update_errors', array( $this, 'handle_profile_update_password' ), 1, 3 );
66 if ( wd_di()->get( Woocommerce::class )->is_wc_login_context() ) {
67 add_filter( 'woocommerce_reset_password_message', array( $this->service, 'add_woocommerce_error_message' ) );
68 }
69 }
70 }
71
72 /**
73 * Filters the site URL if the path is not empty and it is a link to reset password.
74 *
75 * @param string $url The original site URL.
76 * @param string $path The URL path.
77 *
78 * @return string The modified site URL if the conditions are met, otherwise the original URL.
79 */
80 public function filter_site_url( string $url, string $path ) {
81 $action = defender_get_data_from_request( 'action', 'g' );
82 if ( $path && is_string( $path )
83 && in_array( $action, array( 'rp', 'resetpass' ), true )
84 && false !== stristr( $url, 'wp-login.php' )
85 ) {
86 return get_option( 'siteurl' ) . '/' . ltrim( $path, '/' );
87 }
88
89 return $url;
90 }
91
92 /**
93 * Handle user login password.
94 * If pwned password found during login then redirect to reset password page to reset password.
95 *
96 * @param WP_User|WP_Error $user WP_User object or WP_Error.
97 * @param string $password Password plain string.
98 *
99 * @return WP_User|WP_Error Return user object or error object.
100 */
101 public function handle_login_password( $user, $password ) {
102 if ( is_wp_error( $user ) || ! $user instanceof WP_User ) {
103 return $user;
104 }
105 if ( ! is_string( $password ) || '' === trim( $password ) ) {
106 return new WP_Error(
107 'defender_invalid_password',
108 esc_html__( 'Invalid user data.', 'defender-security' )
109 );
110 }
111 $this->service->do_weak_reset( $user, $password );
112
113 return $user;
114 }
115
116 /**
117 * Handle password update on password reset.
118 *
119 * @param WP_Error $errors Error object.
120 * @param WP_User|WP_Error $user WP_User object or WP_Error.
121 *
122 * @return void
123 */
124 public function handle_reset_check_password( WP_Error $errors, $user ): void {
125 if ( is_wp_error( $user ) ) {
126 return;
127 }
128
129 if ( ! $this->service->is_enabled_by_user_role( $user, $this->model->user_roles ) ) {
130 return;
131 }
132
133 // Check if display_pwned_password_warning cookie enabled then show warning message on reset password page.
134 if ( ! defender_get_data_from_request( 'wc_reset_password', 'p' ) && isset( $_COOKIE['display_pwned_password_warning'] ) ) {
135 $message = ! isset( $this->model->pwned_actions['force_change_message'] )
136 || ! is_string( $this->model->pwned_actions['force_change_message'] )
137 || '' === trim( $this->model->pwned_actions['force_change_message'] )
138 ? $this->default_msg
139 : $this->model->pwned_actions['force_change_message'];
140 $errors->add( 'defender_password_protection', $message );
141 // Remove the one time cookie notice once it's displayed.
142 $this->service->remove_cookie_notice( 'display_pwned_password_warning' );
143 }
144
145 $login_password = $this->service->get_submitted_password();
146 if ( $login_password ) {
147 $is_pwned = $this->service->check_pwned_password( $login_password );
148 if ( is_wp_error( $is_pwned ) ) {
149 return;
150 }
151
152 if ( $is_pwned ) {
153 $message = ! isset( $this->model->pwned_actions['force_change_message'] )
154 || ! is_string( $this->model->pwned_actions['force_change_message'] )
155 || '' === trim( $this->model->pwned_actions['force_change_message'] )
156 ? $this->default_msg
157 : $this->model->pwned_actions['force_change_message'];
158 $errors->add( 'defender_password_protection', $message );
159 }
160 }
161 }
162
163 /**
164 * Handle password update on new user registration and user profile update.
165 *
166 * @param WP_Error $errors The errors object to add error messages to.
167 * @param bool $update Whether the profile is being updated.
168 * @param stdClass $user The user object being updated.
169 *
170 * @return void
171 */
172 public function handle_profile_update_password( WP_Error $errors, bool $update, stdClass $user ): void {
173 if ( $errors->get_error_message( 'pass' ) ||
174 is_wp_error( $user ) ||
175 ! isset( $user->user_pass )
176 ) {
177 return;
178 }
179
180 // When updating the profile check if user's role preference is enabled.
181 if ( $update && ! $this->service->is_enabled_by_user_role( $user, $this->model->user_roles ) ) {
182 return;
183 }
184
185 $login_password = $this->service->get_submitted_password();
186 if ( $login_password ) {
187 $is_pwned = $this->service->check_pwned_password( $login_password );
188 if ( is_wp_error( $is_pwned ) ) {
189 return;
190 }
191
192 if ( $is_pwned ) {
193 $message = ! isset( $this->model->pwned_actions['force_change_message'] )
194 || ! is_string( $this->model->pwned_actions['force_change_message'] )
195 || '' === trim( $this->model->pwned_actions['force_change_message'] )
196 ? $this->default_msg
197 : $this->model->pwned_actions['force_change_message'];
198 $errors->add( 'defender_password_protection', $message );
199 }
200 }
201 }
202
203 /**
204 * Get model.
205 *
206 * @return \WP_Defender\Model\Setting\Password_Protection
207 */
208 private function get_model() {
209 if ( is_object( $this->model ) ) {
210 return $this->model;
211 }
212
213 return new \WP_Defender\Model\Setting\Password_Protection();
214 }
215
216 /**
217 * Provide data to the frontend via localized script.
218 *
219 * @param array $data Data collection is ready to passed.
220 *
221 * @return array Modified data array with added this controller data.
222 */
223 public function script_data( array $data ): array {
224 $data['password_protection'] = $this->data_frontend();
225
226 return $data;
227 }
228
229 /**
230 * Save settings.
231 *
232 * @param Request $request The request object containing new settings data.
233 *
234 * @return Response
235 * @defender_route
236 */
237 public function save_settings( Request $request ): Response {
238 $model_data = $request->get_data_by_model( $this->model );
239 // Additional security layer because 'message' is nested model property of parent 'pwned_actions'.
240 $model_data['pwned_actions']['force_change_message'] = sanitize_textarea_field(
241 $model_data['pwned_actions']['force_change_message']
242 );
243 $this->model->import( $model_data );
244 if ( $this->model->validate() ) {
245 $this->model->save();
246 Config_Hub_Helper::set_clear_active_flag();
247
248 $response = array(
249 'auto_close' => true,
250 );
251 if ( $this->model->enabled && (
252 ! isset( $this->model->user_roles ) || ! is_array( $this->model->user_roles ) || array() === $this->model->user_roles
253 ) ) {
254 // we need to control this message in front.
255 $response['warning'] = sprintf(
256 /* translators: 1. Open tag. 2. Close tag. */
257 esc_html__( 'You need to check at least one of the %1$sPwned checks preferences below%2$s and save your settings to enable Password Protection.', 'defender-security' ),
258 '<b>',
259 '</b>'
260 );
261
262 return new Response( true, array_merge( $response, $this->data_frontend() ) );
263 }
264
265 if ( $this->maybe_track() ) {
266 $prev_data = $this->get_model()->get_old_settings();
267
268 if ( array() !== $prev_data ) {
269 if ( $this->model->enabled && ! $prev_data['enabled'] ) {
270 $need_track = true;
271 $event = 'def_feature_activated';
272 } elseif ( ! $this->model->enabled && $prev_data['enabled'] ) {
273 $need_track = true;
274 $event = 'def_feature_deactivated';
275 } else {
276 $need_track = false;
277 }
278
279 if ( $need_track ) {
280 $data = array(
281 'Feature' => 'Pwned Passwords',
282 'Triggered From' => 'Feature page',
283 );
284 $this->track_feature( $event, $data );
285 }
286 }
287 }
288
289 return new Response( true, array_merge( $response, $this->data_frontend() ) );
290 }
291
292 return new Response(
293 false,
294 array(
295 'message' => $this->model->get_formatted_errors(),
296 )
297 );
298 }
299
300 /**
301 * Removes settings for all submodules.
302 */
303 public function remove_settings() {
304 }
305
306 /**
307 * Delete all the data & the cache.
308 */
309 public function remove_data() {
310 }
311
312 /**
313 * Provides data for the frontend.
314 *
315 * @return array An array of data for the frontend.
316 */
317 public function data_frontend(): array {
318 $model = $this->get_model();
319
320 return array_merge(
321 array(
322 'is_active' => $model->is_active(),
323 'model' => $model->export(),
324 'all_roles' => wp_list_pluck( get_editable_roles(), 'name' ),
325 'default_message' => $this->default_msg,
326 ),
327 $this->dump_routes_and_nonces()
328 );
329 }
330
331 /**
332 * Provides data for the dashboard widget.
333 *
334 * @return array An array of dashboard widget data.
335 */
336 public function dashboard_widget(): array {
337 return array( 'model' => $this->get_model()->export() );
338 }
339
340 /**
341 * Adapt the given data array by adding additional fields if necessary.
342 *
343 * @param array $data The data array to adapt.
344 *
345 * @return array The adapted data array.
346 */
347 private function adapt_data( array $data ): array {
348 $adapted_data = array();
349 if ( isset( $data['custom_message'] ) ) {
350 $adapted_data['force_change_message'] = $data['custom_message'];
351 }
352
353 return array_merge( $data, $adapted_data );
354 }
355
356 /**
357 * Imports data into the model.
358 *
359 * @param array $data Data to be imported into the model.
360 */
361 public function import_data( array $data ) {
362 if ( array() !== $data ) {
363 // Upgrade for old versions.
364 $data = $this->adapt_data( $data );
365 $model = $this->get_model();
366 $model->import( $data );
367 if ( $model->validate() ) {
368 $model->save();
369 }
370 }
371 }
372
373 /**
374 * Converts the current object state to an array.
375 *
376 * @return array The array representation of the object.
377 */
378 public function to_array(): array {
379 return array();
380 }
381
382 /**
383 * Exports strings.
384 *
385 * @return array An array of strings.
386 */
387 public function export_strings(): array {
388 return array(
389 $this->model->is_active() ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ),
390 );
391 }
392
393 /**
394 * Generates configuration strings based on the provided configuration and whether the product is a pro version.
395 *
396 * @param mixed $config The configuration data.
397 * @param bool $is_pro Indicates if the product is a pro version.
398 *
399 * @return array Returns an array of configuration strings.
400 */
401 public function config_strings( $config, bool $is_pro ): array {
402 $is_enabled = isset( $config['enabled'] ) ? (bool) $config['enabled'] : false;
403 if (
404 ! $is_enabled ||
405 ! isset( $config['user_roles'] ) ||
406 ! is_array( $config['user_roles'] ) ||
407 array() === $config['user_roles']
408 ) {
409 return array( esc_html__( 'Inactive', 'defender-security' ) );
410 }
411
412 return array(
413 $is_enabled && ( is_array( $config['user_roles'] ) || $config['user_roles'] instanceof Countable ? count( $config['user_roles'] ) : 0 ) > 0
414 ? esc_html__( 'Active', 'defender-security' )
415 : esc_html__( 'Inactive', 'defender-security' ),
416 );
417 }
418 }
419