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

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