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

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