PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.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
← All changes | src/model/notification/class-firewall-notification.php +291 -138 5.6.0 → 6.3.0 View file →
@@ -15,8 +15,9 @@
15 15 use WP_Defender\Component\Notification;
16 16 use WP_Defender\Model\Setting\Login_Lockout;
17 17 use WP_Defender\Controller\Blocklist_Monitor;
18 18 use WP_Defender\Model\Setting\Notfound_Lockout;
19 +use WP_Defender\Model\Setting\User_Agent_Lockout;
19 20
20 21 /**
21 22 * Handles the scheduling and sending of firewall-related notifications.
22 23 */
@@ -43,9 +44,9 @@
43 44 * Sets default values for the class.
44 45 */
45 46 protected function before_load(): void {
46 47 $default = array(
47 - 'title' => esc_html__( 'Firewall - Notification', 'defender-security' ),
48 + 'title' => esc_html__( 'Firewall - Alert', 'defender-security' ),
48 49 'slug' => self::SLUG,
49 50 'status' => self::STATUS_DISABLED,
50 51 'description' => esc_html__(
51 52 'Get email when a user or IP is locked out for trying to access your login area.',
@@ -51,19 +52,17 @@
51 52 'Get email when a user or IP is locked out for trying to access your login area.',
52 53 'defender-security'
53 54 ),
54 55 // @since 3.0.0 Fix 'Guest'-line.
55 - 'in_house_recipients' => is_user_logged_in() ? array( $this->get_default_user() ) : array(),
56 + 'in_house_recipients' => $this->get_default_user(),
56 57 'out_house_recipients' => array(),
57 58 'type' => 'notification',
58 59 'dry_run' => false,
59 60 'configs' => array(
60 - 'login_lockout' => false,
61 - 'nf_lockout' => false,
62 - 'ua_lockout' => false,
63 - 'limit' => false,
64 61 'threshold' => 3,
65 62 'cool_off' => 24,
63 + 'login_lockout' => true,
64 + 'nf_lockout' => true,
66 65 ),
67 66 );
68 67 $this->import( $default );
69 68 }
@@ -79,18 +78,27 @@
79 78 if ( self::STATUS_ACTIVE !== $this->status ) {
80 79 return false;
81 80 }
82 81 // Check 'Login Protection Lockout'.
83 - if ( Lockout_Log::AUTH_LOCK === $model->type && true === $this->configs['login_lockout'] ) {
84 - return true;
82 + if ( Lockout_Log::AUTH_LOCK === $model->type ) {
83 + if ( true === wd_di()->get( Login_Lockout::class )->enabled ) {
84 + return true;
85 + }
86 + return false;
85 87 }
86 88 // Check '404 Protection Lockout'.
87 - if ( Lockout_Log::LOCKOUT_404 === $model->type && true === $this->configs['nf_lockout'] ) {
88 - return true;
89 + if ( in_array( $model->type, Lockout_Log::get_404_lockout_types(), true ) ) {
90 + if ( true === wd_di()->get( Notfound_Lockout::class )->enabled ) {
91 + return true;
92 + }
93 + return false;
89 94 }
90 - // Check 'User Agent Lockout'.
91 - if ( Lockout_Log::LOCKOUT_UA === $model->type && true === $this->configs['ua_lockout'] ) {
92 - return true;
95 + // Check 'User Agent Lockout' (includes Fake Bot and Malicious Bot lockouts).
96 + if ( in_array( $model->type, Lockout_Log::get_ua_lockout_types(), true ) ) {
97 + if ( true === wd_di()->get( User_Agent_Lockout::class )->enabled ) {
98 + return true;
99 + }
100 + return false;
93 101 }
94 102
95 103 return false;
96 104 }
@@ -95,8 +103,24 @@
95 103 return false;
96 104 }
97 105
98 106 /**
107 + * Sends the notification for a lockout when it is enabled for that type.
108 + *
109 + * Firewall-time detectors call this directly because the 'defender_notify' hook
110 + * is not registered yet at that stage, keeping the eligibility rules in this class.
111 + *
112 + * @param Lockout_Log $model The lockout log model.
113 + *
114 + * @return void
115 + */
116 + public function notify_lockout( Lockout_Log $model ): void {
117 + if ( $this->check_options( $model ) ) {
118 + $this->send( $model );
119 + }
120 + }
121 +
122 + /**
99 123 * Constructs and sends the firewall notification email based on the lockout log.
100 124 *
101 125 * @param Lockout_Log $model The lockout log model.
102 126 *
@@ -102,28 +126,25 @@
102 126 *
103 127 * @return void
104 128 */
105 129 public function send( Lockout_Log $model ): void {
106 - if (
107 - true === filter_var( $this->configs['login_lockout'], FILTER_VALIDATE_BOOLEAN )
108 - && Lockout_Log::AUTH_LOCK === $model->type
109 - ) {
110 - $template = 'login-lockout';
111 - } elseif (
112 - true === filter_var( $this->configs['ua_lockout'], FILTER_VALIDATE_BOOLEAN )
113 - && Lockout_Log::LOCKOUT_UA === $model->type
114 - ) {
115 - $template = 'ua-lockout';
116 - } else {
117 - $template = 'lockout-404';
130 + if ( ! $this->check_options( $model ) ) {
131 + return;
118 132 }
119 133
120 - $service = wd_di()->get( Notification::class );
134 + $template = $this->get_template( $model );
135 + $service = wd_di()->get( Notification::class );
136 + $network_site_url = network_site_url();
137 + $email_data = $this->get_email_data( $model, $template, $network_site_url );
138 + $headers = wd_di()->get( Mail::class )->get_headers(
139 + defender_noreply_email( 'wd_lockout_noreply_email' ),
140 + self::SLUG
141 + );
121 142 foreach ( $this->in_house_recipients as $user ) {
122 143 if ( self::USER_SUBSCRIBED !== $user['status'] ) {
123 144 continue;
124 145 }
125 - $this->send_to_user( $user['email'], $user['name'], $model, $template, $service );
146 + $this->send_to_user( $user['email'], $user['name'], $email_data, $headers, $service );
126 147 }
127 148
128 149 foreach ( $this->out_house_recipients as $user ) {
129 150 if ( self::USER_SUBSCRIBED !== $user['status'] ) {
@@ -128,20 +149,39 @@
128 149 foreach ( $this->out_house_recipients as $user ) {
129 150 if ( self::USER_SUBSCRIBED !== $user['status'] ) {
130 151 continue;
131 152 }
132 - $this->send_to_user( $user['email'], $user['name'], $model, $template, $service );
153 + $this->send_to_user( $user['email'], $user['name'], $email_data, $headers, $service );
133 154 }
134 155 }
135 156
136 157 /**
158 + * Gets the email template for the lockout log.
159 + *
160 + * @param Lockout_Log $model The lockout log model.
161 + *
162 + * @return string The email template key.
163 + */
164 + private function get_template( Lockout_Log $model ): string {
165 + if ( Lockout_Log::AUTH_LOCK === $model->type ) {
166 + return 'login-lockout';
167 + }
168 +
169 + if ( in_array( $model->type, Lockout_Log::get_ua_lockout_types(), true ) ) {
170 + return 'ua-lockout';
171 + }
172 +
173 + return 'lockout-404';
174 + }
175 +
176 + /**
137 177 * Constructs and sends the email to the specified recipient.
138 178 *
139 - * @param string $email The recipient's email address.
140 - * @param string $name The recipient's name.
141 - * @param Lockout_Log $model The lockout log model.
142 - * @param string $template The email template to use.
143 - * @param object $service The notification service object.
179 + * @param string $email The recipient's email address.
180 + * @param string $name The recipient's name.
181 + * @param array $email_data Pre-computed email subject and body text.
182 + * @param array $headers Pre-computed mail headers.
183 + * @param object $service The notification service object.
144 184 *
145 185 * @return void
146 186 */
147 187 private function send_to_user(
@@ -146,104 +186,195 @@
146 186 */
147 187 private function send_to_user(
148 188 string $email,
149 189 string $name,
150 - Lockout_Log $model,
151 - string $template,
190 + array $email_data,
191 + array $headers,
152 192 object $service
153 193 ): void {
154 - // Check if this meet the threshold.
155 - if ( true === $this->configs['limit'] ) {
156 - $count = Email_Track::count(
157 - $this->slug,
158 - $email,
159 - strtotime( '-' . $this->configs['cool_off'] . ' hours' ),
160 - time()
161 - );
162 - if ( $count >= $this->configs['threshold'] ) {
163 - // No send.
164 - return;
165 - }
194 + if ( $this->is_email_limit_reached( $email ) ) {
195 + return;
166 196 }
167 - $network_site_url = network_site_url();
197 +
198 + $content = $this->render_email_content( $name, $email_data['text'], $email, $service );
199 +
200 + $ret = wp_mail( $email, $email_data['subject'], $content, $headers );
201 + if ( $ret ) {
202 + $this->save_log( $email );
203 + }
204 + }
205 +
206 + /**
207 + * Checks whether the recipient has reached the email notification limit.
208 + *
209 + * @param string $email The recipient's email address.
210 + *
211 + * @return bool True when the email limit has been reached.
212 + */
213 + private function is_email_limit_reached( string $email ): bool {
214 + // Handle cool_off period in minutes and hours.
215 + // The minutes values are in fractional hours, e.g., 0.25 = 15 minutes, 0.5 = 30 minutes.
216 + $cool_off_seconds = (int) round( (float) $this->configs['cool_off'] * HOUR_IN_SECONDS );
217 + $count = Email_Track::count(
218 + $this->slug,
219 + $email,
220 + time() - $cool_off_seconds,
221 + time()
222 + );
223 +
224 + return $count >= $this->configs['threshold'];
225 + }
226 +
227 + /**
228 + * Gets email subject and body text for a lockout template.
229 + *
230 + * @param Lockout_Log $model The lockout log model.
231 + * @param string $template The email template to use.
232 + * @param string $network_site_url The network site URL.
233 + *
234 + * @return array Email subject and body text.
235 + */
236 + private function get_email_data( Lockout_Log $model, string $template, string $network_site_url ): array {
237 +
168 238 if ( 'login-lockout' === $template ) {
239 + return $this->get_login_lockout_email_data( $model, $network_site_url );
240 + }
241 +
242 + if ( 'ua-lockout' === $template ) {
243 + return $this->get_ua_lockout_email_data( $model, $network_site_url );
244 + }
245 +
246 + return $this->get_404_lockout_email_data( $model, $network_site_url );
247 + }
248 +
249 + /**
250 + * Gets email data for login lockouts.
251 + *
252 + * @param Lockout_Log $model The lockout log model.
253 + * @param string $network_site_url The network site URL.
254 + *
255 + * @return array Email subject and body text.
256 + */
257 + private function get_login_lockout_email_data( Lockout_Log $model, string $network_site_url ): array {
258 + /* translators: %s: Site URL. */
259 + $subject = sprintf( __( 'Login lockout alert for %s', 'defender-security' ), $network_site_url );
260 + $subject = wp_specialchars_decode( $subject, ENT_QUOTES );
261 + // If the log is made from the 2FA module, then we get the settings from it, otherwise from Login_Lockout.
262 + $settings = wd_di()->get( Login_Lockout::class );
263 + if ( false !== strpos( $model->log, '2fa attempts' ) ) {
264 + $component = wd_di()->get( Two_Fa::class );
265 + $attempt_limit = $component->get_attempt_limit();
266 + $time_limit = $component->get_time_limit() . esc_html__( ' seconds', 'defender-security' );
267 + $type = '2fa';
268 + } else {
269 + $attempt_limit = $settings->attempt;
270 + $time_limit = $settings->duration . ' ' . $settings->duration_unit;
271 + $type = 'login';
272 + }
273 + // $text & $string will be escaped at src\view\email\login-lockout.php.
274 + /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4. Lockout type, 5: Translation string. */
275 + $text = __(
276 + 'The host %1$s has been locked out of %2$s due to more than %3$s failed %4$s attempts. %5$s',
277 + 'defender-security'
278 + );
279 + $text = sprintf(
280 + $text,
281 + '<strong>' . esc_html( $model->ip ) . '</strong>',
282 + '<a href="' . esc_url( $network_site_url ) . '">' . esc_html( $network_site_url ) . '</a>',
283 + '<strong>' . $attempt_limit . '</strong>',
284 + $type,
285 + $this->get_lockout_duration_text( $settings->lockout_type, $time_limit )
286 + );
287 +
288 + return compact( 'subject', 'text' );
289 + }
290 +
291 + /**
292 + * Gets email data for user agent lockouts.
293 + *
294 + * @param Lockout_Log $model The lockout log model.
295 + * @param string $network_site_url The network site URL.
296 + *
297 + * @return array Email subject and body text.
298 + */
299 + private function get_ua_lockout_email_data( Lockout_Log $model, string $network_site_url ): array {
300 + $subject = sprintf(
169 301 /* translators: %s: Site URL. */
170 - $subject = sprintf( esc_html__( 'Login lockout alert for %s', 'defender-security' ), $network_site_url );
171 - // If the log is made from the 2FA module, then we get the settings from it, otherwise from Login_Lockout.
172 - $settings = wd_di()->get( Login_Lockout::class );
173 - if ( false !== strpos( $model->log, '2fa attempts' ) ) {
174 - $component = wd_di()->get( Two_Fa::class );
175 - $attempt_limit = $component->get_attempt_limit();
176 - $time_limit = $component->get_time_limit() . esc_html__( ' seconds', 'defender-security' );
177 - $type = '2fa';
178 - } else {
179 - $attempt_limit = $settings->attempt;
180 - $time_limit = $settings->duration . ' ' . $settings->duration_unit;
181 - $type = 'login';
182 - }
183 - // $text & $string will be escaped at src\view\email\login-lockout.php.
184 - /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4. Lockout type, 5: Translation string. */
185 - $text = __(
186 - 'The host %1$s has been locked out of %2$s due to more than %3$s failed %4$s attempts. %5$s',
187 - 'defender-security'
188 - );
189 - if ( 'permanent' === $settings->lockout_type ) {
190 - $string = esc_html__( 'Accordingly, the host has been permanently banned.', 'defender-security' );
191 - } else {
192 - $string = sprintf(
193 - /* translators: %s: Duration. */
194 - esc_html__( 'They have been locked out for %s.', 'defender-security' ),
195 - '<strong>' . esc_html( $time_limit ) . '</strong>'
196 - );
197 - }
198 - $text = sprintf(
199 - $text,
200 - '<strong>' . $model->ip . '</strong>',
201 - '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
202 - '<strong>' . $attempt_limit . '</strong>',
203 - $type,
204 - $string
205 - );
206 - } elseif ( 'ua-lockout' === $template ) {
207 - $subject = sprintf(
208 - /* translators: %s: Site URL. */
209 - esc_html__( 'User Agent lockout alert for %s', 'defender-security' ),
210 - $network_site_url
211 - );
212 - $text = sprintf(
302 + __( 'User Agent lockout alert for %s', 'defender-security' ),
303 + $network_site_url
304 + );
305 + $subject = wp_specialchars_decode( $subject, ENT_QUOTES );
306 + $text = sprintf(
213 307 /* translators: 1: User agent, 2: Site URL */
214 - __( 'The %1$s has been locked out of %2$s.', 'defender-security' ),
215 - '<strong>' . $model->user_agent . '</strong>',
216 - '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
217 - );
218 - } else {
219 - /* translators: %s: Site URL. */
220 - $subject = sprintf( esc_html__( '404 lockout alert for %s', 'defender-security' ), $network_site_url );
221 - $settings = wd_di()->get( Notfound_Lockout::class );
222 - /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4: Tried, 5. Translation string. */
223 - $text = __(
224 - 'The host %1$s has been locked out of %2$s due to more than %3$s 404 requests for the file %4$s. %5$s',
225 - 'defender-security'
226 - );
227 - if ( 'permanent' === $settings->lockout_type ) {
228 - $string = esc_html__( 'Accordingly, the host has been permanently banned.', 'defender-security' );
229 - } else {
230 - $string = sprintf(
231 - /* translators: %s: Duration. */
232 - __( 'They have been locked out for %s.', 'defender-security' ),
233 - '<strong>' . esc_html( $settings->duration . ' ' . $settings->duration_unit ) . '</strong>'
234 - );
235 - }
236 - $text = sprintf(
237 - $text,
238 - '<strong>' . $model->ip . '</strong>',
239 - '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
240 - '<strong>' . $settings->attempt . '</strong>',
241 - '<strong>' . $model->tried . '</strong>',
242 - $string
243 - );
308 + __( 'The %1$s has been locked out of %2$s.', 'defender-security' ),
309 + '<strong>' . esc_html( $model->user_agent ) . '</strong>',
310 + '<a href="' . esc_url( $network_site_url ) . '">' . esc_html( $network_site_url ) . '</a>'
311 + );
312 +
313 + return compact( 'subject', 'text' );
314 + }
315 +
316 + /**
317 + * Gets email data for 404 lockouts.
318 + *
319 + * @param Lockout_Log $model The lockout log model.
320 + * @param string $network_site_url The network site URL.
321 + *
322 + * @return array Email subject and body text.
323 + */
324 + private function get_404_lockout_email_data( Lockout_Log $model, string $network_site_url ): array {
325 + /* translators: %s: Site URL. */
326 + $subject = sprintf( __( '404 lockout alert for %s', 'defender-security' ), $network_site_url );
327 + $subject = wp_specialchars_decode( $subject, ENT_QUOTES );
328 + $settings = wd_di()->get( Notfound_Lockout::class );
329 + /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4: Tried, 5. Translation string. */
330 + $text = __(
331 + 'The host %1$s has been locked out of %2$s due to more than %3$s 404 requests for the file %4$s. %5$s',
332 + 'defender-security'
333 + );
334 + $text = sprintf(
335 + $text,
336 + '<strong>' . esc_html( $model->ip ) . '</strong>',
337 + '<a href="' . esc_url( $network_site_url ) . '">' . esc_html( $network_site_url ) . '</a>',
338 + '<strong>' . $settings->attempt . '</strong>',
339 + '<strong>' . esc_html( $model->tried ) . '</strong>',
340 + $this->get_lockout_duration_text( $settings->lockout_type, $settings->duration . ' ' . $settings->duration_unit )
341 + );
342 +
343 + return compact( 'subject', 'text' );
344 + }
345 +
346 + /**
347 + * Gets the lockout duration text used in notification emails.
348 + *
349 + * @param string $lockout_type The lockout type.
350 + * @param string $duration The lockout duration.
351 + *
352 + * @return string The lockout duration text.
353 + */
354 + private function get_lockout_duration_text( string $lockout_type, string $duration ): string {
355 + if ( 'permanent' === $lockout_type ) {
356 + return esc_html__( 'Accordingly, the host has been permanently banned.', 'defender-security' );
244 357 }
245 358
359 + return sprintf(
360 + /* translators: %s: Duration. */
361 + esc_html__( 'They have been locked out for %s.', 'defender-security' ),
362 + '<strong>' . esc_html( $duration ) . '</strong>'
363 + );
364 + }
365 +
366 + /**
367 + * Renders the complete email content.
368 + *
369 + * @param string $name The recipient's name.
370 + * @param string $text The email body text.
371 + * @param string $email The recipient's email address.
372 + * @param object $service The notification service object.
373 + *
374 + * @return string The rendered email content.
375 + */
376 + private function render_email_content( string $name, string $text, string $email, object $service ): string {
246 377 $logs_url = network_admin_url( 'admin.php?page=wdf-ip-lockout&view=logs' );
247 378 // Need for activated Mask Login feature.
248 379 $logs_url = apply_filters( 'report_email_logs_link', $logs_url, $email );
249 380 // We don't call the Firewall controller to avoid cyclic dependency. It's a workaround with the simplest controller.
@@ -258,9 +389,10 @@
258 389 ),
259 390 false
260 391 );
261 392 $unsubscribe_link = $service->create_unsubscribe_url( $this->slug, $email );
262 - $content = $controller->render_partial(
393 +
394 + return $controller->render_partial(
263 395 'email/index',
264 396 array(
265 397 'title' => esc_html__( 'Firewall', 'defender-security' ),
266 398 'content_body' => $content_body,
@@ -267,18 +399,8 @@
267 399 'unsubscribe_link' => $unsubscribe_link,
268 400 ),
269 401 false
270 402 );
271 -
272 - $headers = wd_di()->get( Mail::class )->get_headers(
273 - defender_noreply_email( 'wd_lockout_noreply_email' ),
274 - self::SLUG
275 - );
276 -
277 - $ret = wp_mail( $email, $subject, $content, $headers );
278 - if ( $ret ) {
279 - $this->save_log( $email );
280 - }
281 403 }
282 404
283 405 /**
284 406 * Define labels.
@@ -286,14 +408,14 @@
286 408 * @return array The array of settings labels.
287 409 */
288 410 public function labels(): array {
289 411 return array(
290 - 'notification' => esc_html__( 'Firewall - Notification', 'defender-security' ),
412 + 'notification' => esc_html__( 'Firewall - Alert', 'defender-security' ),
291 413 'login_lockout_notification' => esc_html__( 'Login Protection Lockout', 'defender-security' ),
292 414 'ip_lockout_notification' => esc_html__( '404 Detection Lockout', 'defender-security' ),
293 415 'ua_lockout_notification' => esc_html__( 'User Agent Lockout', 'defender-security' ),
294 416 'notification_subscribers' => esc_html__( 'Recipients', 'defender-security' ),
295 - 'cooldown_enabled' => esc_html__( 'Limit email notifications for repeat lockouts', 'defender-security' ),
417 + 'cooldown_enabled' => esc_html__( 'Limit email alerts for repeat lockouts', 'defender-security' ),
296 418 'cooldown_number_lockout' => esc_html__( 'Repeat Lockouts Threshold', 'defender-security' ),
297 419 'cooldown_period' => esc_html__( 'Repeat Lockouts Period', 'defender-security' ),
298 420 );
299 421 }
@@ -306,12 +428,43 @@
306 428 * @return array The type-casted configuration data.
307 429 * @since 3.1.0
308 430 */
309 431 public function type_casting( array $configs ): array {
310 - $configs['login_lockout'] = (bool) $configs['login_lockout'];
311 - $configs['nf_lockout'] = (bool) $configs['nf_lockout'];
312 - $configs['ua_lockout'] = isset( $configs['ua_lockout'] ) ? (bool) $configs['ua_lockout'] : false;
313 - $configs['limit'] = (bool) $configs['limit'];
432 + if ( ! isset( $configs['threshold'] ) ) {
433 + $configs['threshold'] = $this->configs['threshold'] ?? 3;
434 + }
435 + if ( ! isset( $configs['cool_off'] ) ) {
436 + $configs['cool_off'] = $this->configs['cool_off'] ?? 24;
437 + }
314 438
315 439 return $configs;
440 + }
441 +
442 + /**
443 + * Returns lockout notification flags for the Hub.
444 + *
445 + * @return array
446 + */
447 + public function get_hub_data(): array {
448 + if ( self::STATUS_ACTIVE !== $this->status ) {
449 + return array(
450 + 'login_lockout' => false,
451 + '404_lockout' => false,
452 + 'ua_lockout' => false,
453 + );
454 + }
455 +
456 + return array(
457 + 'login_lockout' => wd_di()->get( Login_Lockout::class )->enabled,
458 + '404_lockout' => wd_di()->get( Notfound_Lockout::class )->enabled,
459 + 'ua_lockout' => wd_di()->get( User_Agent_Lockout::class )->enabled,
460 + );
461 + }
462 +
463 + /**
464 + * Normalizes firewall notification data after loading.
465 + */
466 + protected function after_load(): void {
467 + parent::after_load();
468 + $this->title = esc_html__( 'Firewall alert', 'defender-security' );
316 469 }
317 470 }