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

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