PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.0
Defender Security – Malware Scanner, Login Security & Firewall v5.8.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.8.0, at src/controller/class-password-protection.php

423 lines 12.3 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_action( '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|WP_Error
123 */
124 public function handle_reset_check_password( WP_Error $errors, $user ) {
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 $errors;
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 return $errors;
163 }
164
165 /**
166 * Handle password update on new user registration and user profile update.
167 *
168 * @param WP_Error $errors The errors object to add error messages to.
169 * @param bool $update Whether the profile is being updated.
170 * @param stdClass $user The user object being updated.
171 *
172 * @return WP_Error The updated errors object.
173 */
174 public function handle_profile_update_password( WP_Error $errors, bool $update, stdClass $user ) {
175 if ( $errors->get_error_message( 'pass' ) ||
176 is_wp_error( $user ) ||
177 ! isset( $user->user_pass )
178 ) {
179 return;
180 }
181
182 // When updating the profile check if user's role preference is enabled.
183 if ( $update && ! $this->service->is_enabled_by_user_role( $user, $this->model->user_roles ) ) {
184 return;
185 }
186
187 $login_password = $this->service->get_submitted_password();
188 if ( $login_password ) {
189 $is_pwned = $this->service->check_pwned_password( $login_password );
190 if ( is_wp_error( $is_pwned ) ) {
191 return $errors;
192 }
193
194 if ( $is_pwned ) {
195 $message = ! isset( $this->model->pwned_actions['force_change_message'] )
196 || ! is_string( $this->model->pwned_actions['force_change_message'] )
197 || '' === trim( $this->model->pwned_actions['force_change_message'] )
198 ? $this->default_msg
199 : $this->model->pwned_actions['force_change_message'];
200 $errors->add( 'defender_password_protection', $message );
201 }
202 }
203
204 return $errors;
205 }
206
207 /**
208 * Get model.
209 *
210 * @return \WP_Defender\Model\Setting\Password_Protection
211 */
212 private function get_model() {
213 if ( is_object( $this->model ) ) {
214 return $this->model;
215 }
216
217 return new \WP_Defender\Model\Setting\Password_Protection();
218 }
219
220 /**
221 * Provide data to the frontend via localized script.
222 *
223 * @param array $data Data collection is ready to passed.
224 *
225 * @return array Modified data array with added this controller data.
226 */
227 public function script_data( array $data ): array {
228 $data['password_protection'] = $this->data_frontend();
229
230 return $data;
231 }
232
233 /**
234 * Save settings.
235 *
236 * @param Request $request The request object containing new settings data.
237 *
238 * @return Response
239 * @defender_route
240 */
241 public function save_settings( Request $request ): Response {
242 $model_data = $request->get_data_by_model( $this->model );
243 // Additional security layer because 'message' is nested model property of parent 'pwned_actions'.
244 $model_data['pwned_actions']['force_change_message'] = sanitize_textarea_field(
245 $model_data['pwned_actions']['force_change_message']
246 );
247 $this->model->import( $model_data );
248 if ( $this->model->validate() ) {
249 $this->model->save();
250 Config_Hub_Helper::set_clear_active_flag();
251
252 $response = array(
253 'auto_close' => true,
254 );
255 if ( $this->model->enabled && (
256 ! isset( $this->model->user_roles ) || ! is_array( $this->model->user_roles ) || array() === $this->model->user_roles
257 ) ) {
258 // we need to control this message in front.
259 $response['warning'] = sprintf(
260 /* translators: 1. Open tag. 2. Close tag. */
261 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' ),
262 '<b>',
263 '</b>'
264 );
265
266 return new Response( true, array_merge( $response, $this->data_frontend() ) );
267 }
268
269 if ( $this->maybe_track() ) {
270 $prev_data = $this->get_model()->get_old_settings();
271
272 if ( array() !== $prev_data ) {
273 if ( $this->model->enabled && ! $prev_data['enabled'] ) {
274 $need_track = true;
275 $event = 'def_feature_activated';
276 } elseif ( ! $this->model->enabled && $prev_data['enabled'] ) {
277 $need_track = true;
278 $event = 'def_feature_deactivated';
279 } else {
280 $need_track = false;
281 }
282
283 if ( $need_track ) {
284 $data = array(
285 'Feature' => 'Pwned Passwords',
286 'Triggered From' => 'Feature page',
287 );
288 $this->track_feature( $event, $data );
289 }
290 }
291 }
292
293 return new Response( true, array_merge( $response, $this->data_frontend() ) );
294 }
295
296 return new Response(
297 false,
298 array(
299 'message' => $this->model->get_formatted_errors(),
300 )
301 );
302 }
303
304 /**
305 * Removes settings for all submodules.
306 */
307 public function remove_settings() {
308 }
309
310 /**
311 * Delete all the data & the cache.
312 */
313 public function remove_data() {
314 }
315
316 /**
317 * Provides data for the frontend.
318 *
319 * @return array An array of data for the frontend.
320 */
321 public function data_frontend(): array {
322 $model = $this->get_model();
323
324 return array_merge(
325 array(
326 'is_active' => $model->is_active(),
327 'model' => $model->export(),
328 'all_roles' => wp_list_pluck( get_editable_roles(), 'name' ),
329 'default_message' => $this->default_msg,
330 ),
331 $this->dump_routes_and_nonces()
332 );
333 }
334
335 /**
336 * Provides data for the dashboard widget.
337 *
338 * @return array An array of dashboard widget data.
339 */
340 public function dashboard_widget(): array {
341 return array( 'model' => $this->get_model()->export() );
342 }
343
344 /**
345 * Adapt the given data array by adding additional fields if necessary.
346 *
347 * @param array $data The data array to adapt.
348 *
349 * @return array The adapted data array.
350 */
351 private function adapt_data( array $data ): array {
352 $adapted_data = array();
353 if ( isset( $data['custom_message'] ) ) {
354 $adapted_data['force_change_message'] = $data['custom_message'];
355 }
356
357 return array_merge( $data, $adapted_data );
358 }
359
360 /**
361 * Imports data into the model.
362 *
363 * @param array $data Data to be imported into the model.
364 */
365 public function import_data( array $data ) {
366 if ( array() !== $data ) {
367 // Upgrade for old versions.
368 $data = $this->adapt_data( $data );
369 $model = $this->get_model();
370 $model->import( $data );
371 if ( $model->validate() ) {
372 $model->save();
373 }
374 }
375 }
376
377 /**
378 * Converts the current object state to an array.
379 *
380 * @return array The array representation of the object.
381 */
382 public function to_array(): array {
383 return array();
384 }
385
386 /**
387 * Exports strings.
388 *
389 * @return array An array of strings.
390 */
391 public function export_strings(): array {
392 return array(
393 $this->model->is_active() ? esc_html__( 'Active', 'defender-security' ) : esc_html__( 'Inactive', 'defender-security' ),
394 );
395 }
396
397 /**
398 * Generates configuration strings based on the provided configuration and whether the product is a pro version.
399 *
400 * @param mixed $config The configuration data.
401 * @param bool $is_pro Indicates if the product is a pro version.
402 *
403 * @return array Returns an array of configuration strings.
404 */
405 public function config_strings( $config, bool $is_pro ): array {
406 $is_enabled = isset( $config['enabled'] ) ? (bool) $config['enabled'] : false;
407 if (
408 ! $is_enabled ||
409 ! isset( $config['user_roles'] ) ||
410 ! is_array( $config['user_roles'] ) ||
411 array() === $config['user_roles']
412 ) {
413 return array( esc_html__( 'Inactive', 'defender-security' ) );
414 }
415
416 return array(
417 $is_enabled && ( is_array( $config['user_roles'] ) || $config['user_roles'] instanceof Countable ? count( $config['user_roles'] ) : 0 ) > 0
418 ? esc_html__( 'Active', 'defender-security' )
419 : esc_html__( 'Inactive', 'defender-security' ),
420 );
421 }
422 }
423