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

449 lines 13.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'.
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 $count = Email_Track::count(
199 $this->slug,
200 $email,
201 strtotime( '-' . $this->configs['cool_off'] . ' hours' ),
202 time()
203 );
204
205 return $count >= $this->configs['threshold'];
206 }
207
208 /**
209 * Gets email subject and body text for a lockout template.
210 *
211 * @param Lockout_Log $model The lockout log model.
212 * @param string $template The email template to use.
213 * @param string $network_site_url The network site URL.
214 *
215 * @return array Email subject and body text.
216 */
217 private function get_email_data( Lockout_Log $model, string $template, string $network_site_url ): array {
218
219 if ( 'login-lockout' === $template ) {
220 return $this->get_login_lockout_email_data( $model, $network_site_url );
221 }
222
223 if ( 'ua-lockout' === $template ) {
224 return $this->get_ua_lockout_email_data( $model, $network_site_url );
225 }
226
227 return $this->get_404_lockout_email_data( $model, $network_site_url );
228 }
229
230 /**
231 * Gets email data for login lockouts.
232 *
233 * @param Lockout_Log $model The lockout log model.
234 * @param string $network_site_url The network site URL.
235 *
236 * @return array Email subject and body text.
237 */
238 private function get_login_lockout_email_data( Lockout_Log $model, string $network_site_url ): array {
239 /* translators: %s: Site URL. */
240 $subject = sprintf( esc_html__( 'Login lockout alert for %s', 'defender-security' ), $network_site_url );
241 // If the log is made from the 2FA module, then we get the settings from it, otherwise from Login_Lockout.
242 $settings = wd_di()->get( Login_Lockout::class );
243 if ( false !== strpos( $model->log, '2fa attempts' ) ) {
244 $component = wd_di()->get( Two_Fa::class );
245 $attempt_limit = $component->get_attempt_limit();
246 $time_limit = $component->get_time_limit() . esc_html__( ' seconds', 'defender-security' );
247 $type = '2fa';
248 } else {
249 $attempt_limit = $settings->attempt;
250 $time_limit = $settings->duration . ' ' . $settings->duration_unit;
251 $type = 'login';
252 }
253 // $text & $string will be escaped at src\view\email\login-lockout.php.
254 /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4. Lockout type, 5: Translation string. */
255 $text = __(
256 'The host %1$s has been locked out of %2$s due to more than %3$s failed %4$s attempts. %5$s',
257 'defender-security'
258 );
259 $text = sprintf(
260 $text,
261 '<strong>' . $model->ip . '</strong>',
262 '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
263 '<strong>' . $attempt_limit . '</strong>',
264 $type,
265 $this->get_lockout_duration_text( $settings->lockout_type, $time_limit )
266 );
267
268 return compact( 'subject', 'text' );
269 }
270
271 /**
272 * Gets email data for user agent lockouts.
273 *
274 * @param Lockout_Log $model The lockout log model.
275 * @param string $network_site_url The network site URL.
276 *
277 * @return array Email subject and body text.
278 */
279 private function get_ua_lockout_email_data( Lockout_Log $model, string $network_site_url ): array {
280 $subject = sprintf(
281 /* translators: %s: Site URL. */
282 esc_html__( 'User Agent lockout alert for %s', 'defender-security' ),
283 $network_site_url
284 );
285 $text = sprintf(
286 /* translators: 1: User agent, 2: Site URL */
287 __( 'The %1$s has been locked out of %2$s.', 'defender-security' ),
288 '<strong>' . $model->user_agent . '</strong>',
289 '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
290 );
291
292 return compact( 'subject', 'text' );
293 }
294
295 /**
296 * Gets email data for 404 lockouts.
297 *
298 * @param Lockout_Log $model The lockout log model.
299 * @param string $network_site_url The network site URL.
300 *
301 * @return array Email subject and body text.
302 */
303 private function get_404_lockout_email_data( Lockout_Log $model, string $network_site_url ): array {
304 /* translators: %s: Site URL. */
305 $subject = sprintf( esc_html__( '404 lockout alert for %s', 'defender-security' ), $network_site_url );
306 $settings = wd_di()->get( Notfound_Lockout::class );
307 /* translators: 1: IP address, 2: Site URL, 3: Total attempt from an IP, 4: Tried, 5. Translation string. */
308 $text = __(
309 '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',
310 'defender-security'
311 );
312 $text = sprintf(
313 $text,
314 '<strong>' . $model->ip . '</strong>',
315 '<a href="' . $network_site_url . '">' . $network_site_url . '</a>',
316 '<strong>' . $settings->attempt . '</strong>',
317 '<strong>' . $model->tried . '</strong>',
318 $this->get_lockout_duration_text( $settings->lockout_type, $settings->duration . ' ' . $settings->duration_unit )
319 );
320
321 return compact( 'subject', 'text' );
322 }
323
324 /**
325 * Gets the lockout duration text used in notification emails.
326 *
327 * @param string $lockout_type The lockout type.
328 * @param string $duration The lockout duration.
329 *
330 * @return string The lockout duration text.
331 */
332 private function get_lockout_duration_text( string $lockout_type, string $duration ): string {
333 if ( 'permanent' === $lockout_type ) {
334 return esc_html__( 'Accordingly, the host has been permanently banned.', 'defender-security' );
335 }
336
337 return sprintf(
338 /* translators: %s: Duration. */
339 esc_html__( 'They have been locked out for %s.', 'defender-security' ),
340 '<strong>' . esc_html( $duration ) . '</strong>'
341 );
342 }
343
344 /**
345 * Renders the complete email content.
346 *
347 * @param string $name The recipient's name.
348 * @param string $text The email body text.
349 * @param string $email The recipient's email address.
350 * @param object $service The notification service object.
351 *
352 * @return string The rendered email content.
353 */
354 private function render_email_content( string $name, string $text, string $email, object $service ): string {
355 $logs_url = network_admin_url( 'admin.php?page=wdf-ip-lockout&view=logs' );
356 // Need for activated Mask Login feature.
357 $logs_url = apply_filters( 'report_email_logs_link', $logs_url, $email );
358 // We don't call the Firewall controller to avoid cyclic dependency. It's a workaround with the simplest controller.
359 $controller = wd_di()->get( Blocklist_Monitor::class );
360 $content_body = $controller->render_partial(
361 'email/login-lockout',
362 array(
363 'name' => $name,
364 // It's escaped value.
365 'text' => $text,
366 'logs_url' => $logs_url,
367 ),
368 false
369 );
370 $unsubscribe_link = $service->create_unsubscribe_url( $this->slug, $email );
371
372 return $controller->render_partial(
373 'email/index',
374 array(
375 'title' => esc_html__( 'Firewall', 'defender-security' ),
376 'content_body' => $content_body,
377 'unsubscribe_link' => $unsubscribe_link,
378 ),
379 false
380 );
381 }
382
383 /**
384 * Define labels.
385 *
386 * @return array The array of settings labels.
387 */
388 public function labels(): array {
389 return array(
390 'notification' => esc_html__( 'Firewall - Alert', 'defender-security' ),
391 'login_lockout_notification' => esc_html__( 'Login Protection Lockout', 'defender-security' ),
392 'ip_lockout_notification' => esc_html__( '404 Detection Lockout', 'defender-security' ),
393 'ua_lockout_notification' => esc_html__( 'User Agent Lockout', 'defender-security' ),
394 'notification_subscribers' => esc_html__( 'Recipients', 'defender-security' ),
395 'cooldown_enabled' => esc_html__( 'Limit email alerts for repeat lockouts', 'defender-security' ),
396 'cooldown_number_lockout' => esc_html__( 'Repeat Lockouts Threshold', 'defender-security' ),
397 'cooldown_period' => esc_html__( 'Repeat Lockouts Period', 'defender-security' ),
398 );
399 }
400
401 /**
402 * Additional converting rules.
403 *
404 * @param array $configs The configuration data.
405 *
406 * @return array The type-casted configuration data.
407 * @since 3.1.0
408 */
409 public function type_casting( array $configs ): array {
410 if ( ! isset( $configs['threshold'] ) ) {
411 $configs['threshold'] = $this->configs['threshold'] ?? 3;
412 }
413 if ( ! isset( $configs['cool_off'] ) ) {
414 $configs['cool_off'] = $this->configs['cool_off'] ?? 24;
415 }
416
417 return $configs;
418 }
419
420 /**
421 * Returns lockout notification flags for the Hub.
422 *
423 * @return array
424 */
425 public function get_hub_data(): array {
426 if ( self::STATUS_ACTIVE !== $this->status ) {
427 return array(
428 'login_lockout' => false,
429 '404_lockout' => false,
430 'ua_lockout' => false,
431 );
432 }
433
434 return array(
435 'login_lockout' => wd_di()->get( Login_Lockout::class )->enabled,
436 '404_lockout' => wd_di()->get( Notfound_Lockout::class )->enabled,
437 'ua_lockout' => wd_di()->get( User_Agent_Lockout::class )->enabled,
438 );
439 }
440
441 /**
442 * Normalizes firewall notification data after loading.
443 */
444 protected function after_load(): void {
445 parent::after_load();
446 $this->title = esc_html__( 'Firewall alert', 'defender-security' );
447 }
448 }
449