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 / model / notification / class-firewall-notification.php

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

452 lines 14.1 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'.
96 if ( Lockout_Log::LOCKOUT_UA === $model->type ) {
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 * Constructs and sends the firewall notification email based on the lockout log.
108 *
109 * @param Lockout_Log $model The lockout log model.
110 *
111 * @return void
112 */
113 public function send( Lockout_Log $model ): void {
114 if ( ! $this->check_options( $model ) ) {
115 return;
116 }
117
118 $template = $this->get_template( $model );
119 $service = wd_di()->get( Notification::class );
120 $network_site_url = network_site_url();
121 $email_data = $this->get_email_data( $model, $template, $network_site_url );
122 $headers = wd_di()->get( Mail::class )->get_headers(
123 defender_noreply_email( 'wd_lockout_noreply_email' ),
124 self::SLUG
125 );
126 foreach ( $this->in_house_recipients as $user ) {
127 if ( self::USER_SUBSCRIBED !== $user['status'] ) {
128 continue;
129 }
130 $this->send_to_user( $user['email'], $user['name'], $email_data, $headers, $service );
131 }
132
133 foreach ( $this->out_house_recipients as $user ) {
134 if ( self::USER_SUBSCRIBED !== $user['status'] ) {
135 continue;
136 }
137 $this->send_to_user( $user['email'], $user['name'], $email_data, $headers, $service );
138 }
139 }
140
141 /**
142 * Gets the email template for the lockout log.
143 *
144 * @param Lockout_Log $model The lockout log model.
145 *
146 * @return string The email template key.
147 */
148 private function get_template( Lockout_Log $model ): string {
149 if ( Lockout_Log::AUTH_LOCK === $model->type ) {
150 return 'login-lockout';
151 }
152
153 if ( Lockout_Log::LOCKOUT_UA === $model->type ) {
154 return 'ua-lockout';
155 }
156
157 return 'lockout-404';
158 }
159
160 /**
161 * Constructs and sends the email to the specified recipient.
162 *
163 * @param string $email The recipient's email address.
164 * @param string $name The recipient's name.
165 * @param array $email_data Pre-computed email subject and body text.
166 * @param array $headers Pre-computed mail headers.
167 * @param object $service The notification service object.
168 *
169 * @return void
170 */
171 private function send_to_user(
172 string $email,
173 string $name,
174 array $email_data,
175 array $headers,
176 object $service
177 ): void {
178 if ( $this->is_email_limit_reached( $email ) ) {
179 return;
180 }
181
182 $content = $this->render_email_content( $name, $email_data['text'], $email, $service );
183
184 $ret = wp_mail( $email, $email_data['subject'], $content, $headers );
185 if ( $ret ) {
186 $this->save_log( $email );
187 }
188 }
189
190 /**
191 * Checks whether the recipient has reached the email notification limit.
192 *
193 * @param string $email The recipient's email address.
194 *
195 * @return bool True when the email limit has been reached.
196 */
197 private function is_email_limit_reached( string $email ): bool {
198 // Handle cool_off period in minutes and hours.
199 // The minutes values are in fractional hours, e.g., 0.25 = 15 minutes, 0.5 = 30 minutes.
200 $cool_off_seconds = (int) round( (float) $this->configs['cool_off'] * HOUR_IN_SECONDS );
201 $count = Email_Track::count(
202 $this->slug,
203 $email,
204 time() - $cool_off_seconds,
205 time()
206 );
207
208 return $count >= $this->configs['threshold'];
209 }
210
211 /**
212 * Gets email subject and body text for a lockout template.
213 *
214 * @param Lockout_Log $model The lockout log model.
215 * @param string $template The email template to use.
216 * @param string $network_site_url The network site URL.
217 *
218 * @return array Email subject and body text.
219 */
220 private function get_email_data( Lockout_Log $model, string $template, string $network_site_url ): array {
221
222 if ( 'login-lockout' === $template ) {
223 return $this->get_login_lockout_email_data( $model, $network_site_url );
224 }
225
226 if ( 'ua-lockout' === $template ) {
227 return $this->get_ua_lockout_email_data( $model, $network_site_url );
228 }
229
230 return $this->get_404_lockout_email_data( $model, $network_site_url );
231 }
232
233 /**
234 * Gets email data for login lockouts.
235 *
236 * @param Lockout_Log $model The lockout log model.
237 * @param string $network_site_url The network site URL.
238 *
239 * @return array Email subject and body text.
240 */
241 private function get_login_lockout_email_data( Lockout_Log $model, string $network_site_url ): array {
242 /* translators: %s: Site URL. */
243 $subject = sprintf( esc_html__( 'Login lockout alert for %s', 'defender-security' ), $network_site_url );
244 // If the log is made from the 2FA module, then we get the settings from it, otherwise from Login_Lockout.
245 $settings = wd_di()->get( Login_Lockout::class );
246 if ( false !== strpos( $model->log, '2fa attempts' ) ) {
247 $component = wd_di()->get( Two_Fa::class );
248 $attempt_limit = $component->get_attempt_limit();
249 $time_limit = $component->get_time_limit() . esc_html__( ' seconds', 'defender-security' );
250 $type = '2fa';
251 } else {
252 $attempt_limit = $settings->attempt;
253 $time_limit = $settings->duration . ' ' . $settings->duration_unit;
254 $type = 'login';
255 }
256 // $text & $string will be escaped at src\view\email\login-lockout.php.
257 /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4. Lockout type, 5: Translation string. */
258 $text = __(
259 'The host %1$s has been locked out of %2$s due to more than %3$s failed %4$s attempts. %5$s',
260 'defender-security'
261 );
262 $text = sprintf(
263 $text,
264 '<strong>' . $model->ip . '</strong>',
265 '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
266 '<strong>' . $attempt_limit . '</strong>',
267 $type,
268 $this->get_lockout_duration_text( $settings->lockout_type, $time_limit )
269 );
270
271 return compact( 'subject', 'text' );
272 }
273
274 /**
275 * Gets email data for user agent lockouts.
276 *
277 * @param Lockout_Log $model The lockout log model.
278 * @param string $network_site_url The network site URL.
279 *
280 * @return array Email subject and body text.
281 */
282 private function get_ua_lockout_email_data( Lockout_Log $model, string $network_site_url ): array {
283 $subject = sprintf(
284 /* translators: %s: Site URL. */
285 esc_html__( 'User Agent lockout alert for %s', 'defender-security' ),
286 $network_site_url
287 );
288 $text = sprintf(
289 /* translators: 1: User agent, 2: Site URL */
290 __( 'The %1$s has been locked out of %2$s.', 'defender-security' ),
291 '<strong>' . $model->user_agent . '</strong>',
292 '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
293 );
294
295 return compact( 'subject', 'text' );
296 }
297
298 /**
299 * Gets email data for 404 lockouts.
300 *
301 * @param Lockout_Log $model The lockout log model.
302 * @param string $network_site_url The network site URL.
303 *
304 * @return array Email subject and body text.
305 */
306 private function get_404_lockout_email_data( Lockout_Log $model, string $network_site_url ): array {
307 /* translators: %s: Site URL. */
308 $subject = sprintf( esc_html__( '404 lockout alert for %s', 'defender-security' ), $network_site_url );
309 $settings = wd_di()->get( Notfound_Lockout::class );
310 /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4: Tried, 5. Translation string. */
311 $text = __(
312 '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',
313 'defender-security'
314 );
315 $text = sprintf(
316 $text,
317 '<strong>' . $model->ip . '</strong>',
318 '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
319 '<strong>' . $settings->attempt . '</strong>',
320 '<strong>' . $model->tried . '</strong>',
321 $this->get_lockout_duration_text( $settings->lockout_type, $settings->duration . ' ' . $settings->duration_unit )
322 );
323
324 return compact( 'subject', 'text' );
325 }
326
327 /**
328 * Gets the lockout duration text used in notification emails.
329 *
330 * @param string $lockout_type The lockout type.
331 * @param string $duration The lockout duration.
332 *
333 * @return string The lockout duration text.
334 */
335 private function get_lockout_duration_text( string $lockout_type, string $duration ): string {
336 if ( 'permanent' === $lockout_type ) {
337 return esc_html__( 'Accordingly, the host has been permanently banned.', 'defender-security' );
338 }
339
340 return sprintf(
341 /* translators: %s: Duration. */
342 esc_html__( 'They have been locked out for %s.', 'defender-security' ),
343 '<strong>' . esc_html( $duration ) . '</strong>'
344 );
345 }
346
347 /**
348 * Renders the complete email content.
349 *
350 * @param string $name The recipient's name.
351 * @param string $text The email body text.
352 * @param string $email The recipient's email address.
353 * @param object $service The notification service object.
354 *
355 * @return string The rendered email content.
356 */
357 private function render_email_content( string $name, string $text, string $email, object $service ): string {
358 $logs_url = network_admin_url( 'admin.php?page=wdf-ip-lockout&view=logs' );
359 // Need for activated Mask Login feature.
360 $logs_url = apply_filters( 'report_email_logs_link', $logs_url, $email );
361 // We don't call the Firewall controller to avoid cyclic dependency. It's a workaround with the simplest controller.
362 $controller = wd_di()->get( Blocklist_Monitor::class );
363 $content_body = $controller->render_partial(
364 'email/login-lockout',
365 array(
366 'name' => $name,
367 // It's escaped value.
368 'text' => $text,
369 'logs_url' => $logs_url,
370 ),
371 false
372 );
373 $unsubscribe_link = $service->create_unsubscribe_url( $this->slug, $email );
374
375 return $controller->render_partial(
376 'email/index',
377 array(
378 'title' => esc_html__( 'Firewall', 'defender-security' ),
379 'content_body' => $content_body,
380 'unsubscribe_link' => $unsubscribe_link,
381 ),
382 false
383 );
384 }
385
386 /**
387 * Define labels.
388 *
389 * @return array The array of settings labels.
390 */
391 public function labels(): array {
392 return array(
393 'notification' => esc_html__( 'Firewall - Alert', 'defender-security' ),
394 'login_lockout_notification' => esc_html__( 'Login Protection Lockout', 'defender-security' ),
395 'ip_lockout_notification' => esc_html__( '404 Detection Lockout', 'defender-security' ),
396 'ua_lockout_notification' => esc_html__( 'User Agent Lockout', 'defender-security' ),
397 'notification_subscribers' => esc_html__( 'Recipients', 'defender-security' ),
398 'cooldown_enabled' => esc_html__( 'Limit email alerts for repeat lockouts', 'defender-security' ),
399 'cooldown_number_lockout' => esc_html__( 'Repeat Lockouts Threshold', 'defender-security' ),
400 'cooldown_period' => esc_html__( 'Repeat Lockouts Period', 'defender-security' ),
401 );
402 }
403
404 /**
405 * Additional converting rules.
406 *
407 * @param array $configs The configuration data.
408 *
409 * @return array The type-casted configuration data.
410 * @since 3.1.0
411 */
412 public function type_casting( array $configs ): array {
413 if ( ! isset( $configs['threshold'] ) ) {
414 $configs['threshold'] = $this->configs['threshold'] ?? 3;
415 }
416 if ( ! isset( $configs['cool_off'] ) ) {
417 $configs['cool_off'] = $this->configs['cool_off'] ?? 24;
418 }
419
420 return $configs;
421 }
422
423 /**
424 * Returns lockout notification flags for the Hub.
425 *
426 * @return array
427 */
428 public function get_hub_data(): array {
429 if ( self::STATUS_ACTIVE !== $this->status ) {
430 return array(
431 'login_lockout' => false,
432 '404_lockout' => false,
433 'ua_lockout' => false,
434 );
435 }
436
437 return array(
438 'login_lockout' => wd_di()->get( Login_Lockout::class )->enabled,
439 '404_lockout' => wd_di()->get( Notfound_Lockout::class )->enabled,
440 'ua_lockout' => wd_di()->get( User_Agent_Lockout::class )->enabled,
441 );
442 }
443
444 /**
445 * Normalizes firewall notification data after loading.
446 */
447 protected function after_load(): void {
448 parent::after_load();
449 $this->title = esc_html__( 'Firewall alert', 'defender-security' );
450 }
451 }
452