PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.4
Defender Security – Malware Scanner, Login Security & Firewall v6.2.4
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 / model / notification / class-firewall-notification.php

class-firewall-notification.php in Defender Security – Malware Scanner, Login Security & Firewall 6.2.4, at src/model/notification/class-firewall-notification.php

471 lines 14.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles the scheduling and sending of firewall-related notifications.
4 *
5 * @package WP_Defender\Model\Notification
6 */
7
8 namespace WP_Defender\Model\Notification;
9
10 use WP_Defender\Traits\IO;
11 use WP_Defender\Component\Mail;
12 use WP_Defender\Component\Two_Fa;
13 use WP_Defender\Model\Lockout_Log;
14 use WP_Defender\Model\Email_Track;
15 use WP_Defender\Component\Notification;
16 use WP_Defender\Model\Setting\Login_Lockout;
17 use WP_Defender\Controller\Blocklist_Monitor;
18 use WP_Defender\Model\Setting\Notfound_Lockout;
19 use WP_Defender\Model\Setting\User_Agent_Lockout;
20
21 /**
22 * Handles the scheduling and sending of firewall-related notifications.
23 */
24 class Firewall_Notification extends \WP_Defender\Model\Notification {
25
26 use IO;
27
28 /**
29 * Table name.
30 *
31 * @var string
32 */
33 protected $table = 'wd_malware_firewall_notification';
34
35 /**
36 * Slug identifier for the firewall notification.
37 *
38 * @var string
39 */
40 public const SLUG = 'firewall-notification';
41
42 /**
43 * Constructor method.
44 * Sets default values for the class.
45 */
46 protected function before_load(): void {
47 $default = array(
48 'title' => esc_html__( 'Firewall - Alert', 'defender-security' ),
49 'slug' => self::SLUG,
50 'status' => self::STATUS_DISABLED,
51 'description' => esc_html__(
52 'Get email when a user or IP is locked out for trying to access your login area.',
53 'defender-security'
54 ),
55 // @since 3.0.0 Fix 'Guest'-line.
56 'in_house_recipients' => $this->get_default_user(),
57 'out_house_recipients' => array(),
58 'type' => 'notification',
59 'dry_run' => false,
60 'configs' => array(
61 'threshold' => 3,
62 'cool_off' => 24,
63 'login_lockout' => true,
64 'nf_lockout' => true,
65 ),
66 );
67 $this->import( $default );
68 }
69
70 /**
71 * Checks whether the notification options are enabled for the specified lockout log.
72 *
73 * @param Lockout_Log $model The lockout log model.
74 *
75 * @return bool True if notification options are enabled; otherwise, false.
76 */
77 public function check_options( Lockout_Log $model ): bool {
78 if ( self::STATUS_ACTIVE !== $this->status ) {
79 return false;
80 }
81 // Check 'Login Protection Lockout'.
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;
87 }
88 // Check '404 Protection Lockout'.
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;
94 }
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;
101 }
102
103 return false;
104 }
105
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 /**
123 * Constructs and sends the firewall notification email based on the lockout log.
124 *
125 * @param Lockout_Log $model The lockout log model.
126 *
127 * @return void
128 */
129 public function send( Lockout_Log $model ): void {
130 if ( ! $this->check_options( $model ) ) {
131 return;
132 }
133
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 );
142 foreach ( $this->in_house_recipients as $user ) {
143 if ( self::USER_SUBSCRIBED !== $user['status'] ) {
144 continue;
145 }
146 $this->send_to_user( $user['email'], $user['name'], $email_data, $headers, $service );
147 }
148
149 foreach ( $this->out_house_recipients as $user ) {
150 if ( self::USER_SUBSCRIBED !== $user['status'] ) {
151 continue;
152 }
153 $this->send_to_user( $user['email'], $user['name'], $email_data, $headers, $service );
154 }
155 }
156
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 /**
177 * Constructs and sends the email to the specified recipient.
178 *
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.
184 *
185 * @return void
186 */
187 private function send_to_user(
188 string $email,
189 string $name,
190 array $email_data,
191 array $headers,
192 object $service
193 ): void {
194 if ( $this->is_email_limit_reached( $email ) ) {
195 return;
196 }
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
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>' . $model->ip . '</strong>',
282 '<a href="' . $network_site_url . '">' . $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(
301 /* translators: %s: Site URL. */
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(
307 /* translators: 1: User agent, 2: Site URL */
308 __( 'The %1$s has been locked out of %2$s.', 'defender-security' ),
309 '<strong>' . $model->user_agent . '</strong>',
310 '<a href="' . $network_site_url . '">' . $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>' . $model->ip . '</strong>',
337 '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
338 '<strong>' . $settings->attempt . '</strong>',
339 '<strong>' . $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' );
357 }
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 {
377 $logs_url = network_admin_url( 'admin.php?page=wdf-ip-lockout&view=logs' );
378 // Need for activated Mask Login feature.
379 $logs_url = apply_filters( 'report_email_logs_link', $logs_url, $email );
380 // We don't call the Firewall controller to avoid cyclic dependency. It's a workaround with the simplest controller.
381 $controller = wd_di()->get( Blocklist_Monitor::class );
382 $content_body = $controller->render_partial(
383 'email/login-lockout',
384 array(
385 'name' => $name,
386 // It's escaped value.
387 'text' => $text,
388 'logs_url' => $logs_url,
389 ),
390 false
391 );
392 $unsubscribe_link = $service->create_unsubscribe_url( $this->slug, $email );
393
394 return $controller->render_partial(
395 'email/index',
396 array(
397 'title' => esc_html__( 'Firewall', 'defender-security' ),
398 'content_body' => $content_body,
399 'unsubscribe_link' => $unsubscribe_link,
400 ),
401 false
402 );
403 }
404
405 /**
406 * Define labels.
407 *
408 * @return array The array of settings labels.
409 */
410 public function labels(): array {
411 return array(
412 'notification' => esc_html__( 'Firewall - Alert', 'defender-security' ),
413 'login_lockout_notification' => esc_html__( 'Login Protection Lockout', 'defender-security' ),
414 'ip_lockout_notification' => esc_html__( '404 Detection Lockout', 'defender-security' ),
415 'ua_lockout_notification' => esc_html__( 'User Agent Lockout', 'defender-security' ),
416 'notification_subscribers' => esc_html__( 'Recipients', 'defender-security' ),
417 'cooldown_enabled' => esc_html__( 'Limit email alerts for repeat lockouts', 'defender-security' ),
418 'cooldown_number_lockout' => esc_html__( 'Repeat Lockouts Threshold', 'defender-security' ),
419 'cooldown_period' => esc_html__( 'Repeat Lockouts Period', 'defender-security' ),
420 );
421 }
422
423 /**
424 * Additional converting rules.
425 *
426 * @param array $configs The configuration data.
427 *
428 * @return array The type-casted configuration data.
429 * @since 3.1.0
430 */
431 public function type_casting( array $configs ): array {
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 }
438
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' );
469 }
470 }
471