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 / component / ip / class-antibot-global-firewall.php

class-antibot-global-firewall.php in Defender Security – Malware Scanner, Login Security & Firewall 6.1.0, at src/component/ip/class-antibot-global-firewall.php

987 lines 28.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Responsible for handling AntiBot Global Firewall.
4 *
5 * @package WP_Defender\Component\IP
6 */
7
8 namespace WP_Defender\Component\IP;
9
10 use Exception;
11 use Generator;
12 use WP_Defender\Behavior\WPMUDEV;
13 use WP_Defender\Component;
14 use WP_Defender\Controller\Firewall;
15 use WP_Defender\Integrations\Antibot_Global_Firewall_Client;
16 use WP_Defender\Model\Antibot_Global_Firewall as Antibot_Global_Firewall_Model;
17 use WP_Defender\Model\Setting\Antibot_Global_Firewall_Setting;
18 use WP_Defender\Traits\Defender_Dashboard_Client;
19 use WP_Error;
20
21 /**
22 * Provides methods for downloading IPs from AntiBot Global Firewall, storing, deleting and checking IPs in DB table.
23 */
24 class Antibot_Global_Firewall extends Component {
25 use Defender_Dashboard_Client;
26
27 public const REASON_SLUG = 'antibot_global_firewall';
28
29 public const LOG_FILE_NAME = 'antibot-global-firewall.log';
30
31 public const DOWNLOAD_SYNC_SCHEDULE = 'twicedaily';
32
33 public const DOWNLOAD_SYNC_NEXT_RUN_OPTION = 'wpdef_antibot_global_firewall_download_sync_next_run';
34
35 public const LAST_SYNC_OPTION = 'wpdef_antibot_global_firewall_last_sync';
36
37 public const NOTICE_SLUG = 'wpdef_show_antibot_global_firewall_notice';
38
39 public const BLOCKLIST_STATS_KEY = 'wpdef_antibot_global_firewall_stats';
40
41 public const BLOCKLIST_STATS_FULL_KEY = 'wpdef_antibot_global_firewall_stats_full';
42
43 public const BLOCKLIST_STATS_FAILURE_KEY = 'wpdef_antibot_global_firewall_stats_failure';
44
45 public const FAILURE_CACHE_TTL = HOUR_IN_SECONDS;
46
47 public const MAX_BACKOFF_TIME = HOUR_IN_SECONDS;
48
49 public const IS_SWITCHING_TO_PLUGIN_IN_PROGRESS = 'wpdef_antibot_global_firewall_switching_to_plugin_in_progress';
50
51 public const GLOBAL_NOTICE_TIME_OPTION = 'wpdef_antibot_global_firewall_global_notice_time';
52
53 public const GLOBAL_NOTICE_DELAY_DAYS_OPTION = 60;
54
55 /**
56 * The AntiBot Global Firewall model for storing IPs.
57 *
58 * @var Antibot_Global_Firewall_Model
59 */
60 private $model;
61
62 /**
63 * The AntiBot Global Firewall setting model.
64 *
65 * @var Antibot_Global_Firewall_Setting
66 */
67 private $model_setting;
68
69 /**
70 * The WPMUDEV object.
71 *
72 * @var WPMUDEV
73 */
74 private $wpmudev;
75
76 /**
77 * The client object for AntiBot Global Firewall.
78 *
79 * @var Antibot_Global_Firewall_Client
80 */
81 private $antibot_client;
82
83 /**
84 * Lock file name for firewall updates.
85 *
86 * @var string
87 */
88 protected string $lock_filename = 'antibot_global_firewall.lock';
89
90 /**
91 * Initializes the class with the Antibot_Global_Firewall_Model, Antibot_Global_Firewall_Setting, WPMUDEV and Antibot_Global_Firewall_Client instances.
92 *
93 * @param Antibot_Global_Firewall_Model $model The AntiBot Global Firewall model.
94 * @param Antibot_Global_Firewall_Setting $model_setting The AntiBot Global FIrewall setting model.
95 * @param WPMUDEV $wpmudev The WPMUDEV object.
96 * @param Antibot_Global_Firewall_Client $antibot_client The client object for AntiBot Global Firewall API.
97 */
98 public function __construct( Antibot_Global_Firewall_Model $model, Antibot_Global_Firewall_Setting $model_setting, WPMUDEV $wpmudev, Antibot_Global_Firewall_Client $antibot_client ) {
99 $this->model = $model;
100 $this->model_setting = $model_setting;
101 $this->wpmudev = $wpmudev;
102 $this->antibot_client = $antibot_client;
103
104 add_action( 'wpdef_confirm_antibot_toggle_on_hosting', array( $this, 'confirm_toggle_on_hosting' ) );
105 add_action( 'wp_loaded', array( $this, 'clear_antibot_on_disconnection' ) );
106 }
107
108 /**
109 * Check if AntiBot Global Firewall feature is enabled.
110 *
111 * @return bool True for enabled or false for disabled.
112 */
113 public function is_enabled(): bool {
114 /**
115 * Filter to enable or disable the AntiBot Global Firewall.
116 *
117 * @param bool $is_enabled True for enabled or false for disabled.
118 */
119 $is_enabled = apply_filters( 'wpdef_antibot_enabled', $this->model_setting->enabled );
120
121 return is_bool( $is_enabled ) ? $is_enabled : (bool) $is_enabled;
122 }
123
124 /**
125 * Check if the AntiBot Global Firewall is enabled on WPMU DEV.
126 *
127 * @return bool True if the AntiBot Global Firewall is enabled, false otherwise.
128 */
129 public function hosting_is_enabled(): bool {
130 return true === defender_get_hosting_feature_state( 'antibot' );
131 }
132
133 /**
134 * Is managed by Plugin side?
135 *
136 * @return bool
137 */
138 protected function is_managed_by_plugin(): bool {
139 return 'plugin' === $this->get_managed_by();
140 }
141
142 /**
143 * Check if the AntiBot Global Firewall is enabled based on managed by.
144 *
145 * @return bool True if the AntiBot Global Firewall is enabled, false otherwise.
146 */
147 public function frontend_is_enabled(): bool {
148 return $this->is_managed_by_plugin()
149 ? ! $this->is_expired_membership_type() && $this->is_enabled()
150 : $this->hosting_is_enabled();
151 }
152
153 /**
154 * Check if the AntiBot Global Firewall is enabled and the site is connected to the HUB.
155 *
156 * @return bool True if the AntiBot Global Firewall is active, false otherwise.
157 */
158 public function is_active(): bool {
159 return $this->frontend_is_enabled() && $this->is_site_connected_to_hub_via_hcm_or_dash();
160 }
161
162 /**
163 * Check if the AntiBot Global Firewall is active via plugin.
164 *
165 * @since 5.1.1
166 * @return bool True if the AntiBot Global Firewall is active via plugin, false otherwise.
167 */
168 public function is_active_via_plugin(): bool {
169 return $this->is_managed_by_plugin() && $this->is_enabled() && $this->is_site_connected_to_hub_via_hcm_or_dash();
170 }
171
172 /**
173 * Check if the AntiBot Global Firewall is active via hosting.
174 *
175 * @since 5.6.0
176 * @return bool True if the AntiBot Global Firewall is active via hosting, false otherwise.
177 */
178 public function is_active_via_hosting(): bool {
179 return 'hosting' === $this->get_managed_by() && $this->hosting_is_enabled();
180 }
181
182 /**
183 * Check if the IP is blocked.
184 *
185 * @param string $ip The IP address.
186 *
187 * @return bool True if the IP is blocked, false otherwise.
188 */
189 public function is_ip_blocked( string $ip ): bool {
190 $result = $this->model->get_by_ip( $ip );
191
192 return isset( $result->unlocked ) && 1 !== $result->unlocked;
193 }
194
195 /**
196 * Delete complete blocklist.
197 */
198 public function delete_blocklist() {
199 $this->model->truncate();
200
201 delete_site_option( self::LAST_SYNC_OPTION );
202 }
203
204 /**
205 * Check if the Unlock Me Captcha should be displayed.
206 *
207 * @param array $ips The IP addresses.
208 *
209 * @return bool True if the IP is blocked by AntiBot Global Firewall, false otherwise.
210 */
211 public function is_displayed( array $ips ): bool {
212 if ( ! $this->is_enabled() ) {
213 return false;
214 }
215
216 foreach ( $ips as $ip ) {
217 if ( $this->is_ip_blocked( $ip ) ) {
218 return true;
219 }
220 }
221
222 return false;
223 }
224
225 /**
226 * Get the Unlock Me Captcha button text.
227 *
228 * @return string The button text.
229 */
230 public static function get_button_text(): string {
231 return esc_html__( 'AntiBot Unlock Me', 'defender-security' );
232 }
233
234 /**
235 * Handle the download and store blocklist.
236 *
237 * @since 4.8.0
238 * @return void
239 */
240 public function download_and_store_blocklist(): void {
241 if ( ! $this->is_site_connected_to_hub_via_hcm_or_dash() ) {
242 return;
243 }
244
245 if ( $this->has_lock( $this->lock_filename ) ) {
246 $this->log( 'Fallback as already a process is running', Firewall::FIREWALL_LOG );
247 return;
248 }
249
250 $this->create_lock( $this->lock_filename );
251 $file_path = $this->download_blocklist();
252
253 if ( is_string( $file_path ) && '' !== $file_path ) {
254 $this->store_blocklist( $file_path );
255 }
256 $this->remove_lock( $this->lock_filename );
257 }
258
259 /**
260 * Download blocklist.
261 *
262 * @since 4.8.0
263 * @return string|void
264 */
265 private function download_blocklist() {
266 $response = $this->antibot_client->get_blocklist_download( $this->model_setting->mode );
267
268 if ( is_wp_error( $response ) ) {
269 $this->log( sprintf( 'AntiBot Global Firewall Error: %s', $response->get_error_message() ), Firewall::FIREWALL_LOG );
270 return;
271 } elseif ( isset( $response['status'] ) && 'error' === $response['status'] ) {
272 $this->log( sprintf( 'AntiBot Global Firewall Error: %s', $response['message'] ), Firewall::FIREWALL_LOG );
273 return;
274 } elseif (
275 ! isset( $response['data']['download_url'] )
276 || ! is_string( $response['data']['download_url'] )
277 || '' === $response['data']['download_url']
278 || ! isset( $response['data']['hashes']['sha256'] )
279 || ! is_string( $response['data']['hashes']['sha256'] )
280 || '' === $response['data']['hashes']['sha256']
281 ) {
282 $this->log( 'AntiBot Global Firewall Error: Download link not found in the response.', Firewall::FIREWALL_LOG );
283 return;
284 }
285
286 $file_url = $response['data']['download_url'];
287
288 if ( ! function_exists( 'download_url' ) ) {
289 require_once ABSPATH . 'wp-admin/includes/file.php';
290 }
291
292 $tmp_file = download_url( $file_url );
293
294 if ( is_wp_error( $tmp_file ) ) {
295 $this->log( 'AntiBot Global Firewall Error: Unable to download file', Firewall::FIREWALL_LOG );
296 return;
297 }
298
299 $expected_file_hash = '';
300 $response = wp_remote_get( $response['data']['hashes']['sha256'] );
301 if ( wp_remote_retrieve_response_code( $response ) === 200 ) {
302 $expected_file_hash = trim( wp_remote_retrieve_body( $response ) );
303 }
304
305 $file_hash = hash_file( 'sha256', $tmp_file );
306 if ( $file_hash !== $expected_file_hash ) {
307 $this->log( 'AntiBot Global Firewall Error: File is not legit. Hash does not match.', Firewall::FIREWALL_LOG );
308 return;
309 }
310
311 $this->log( 'AntiBot Global Firewall: Downloaded ' . $this->model_setting->get_mode_label() . ' blocklist successfully.', Firewall::FIREWALL_LOG );
312
313 return $tmp_file;
314 }
315
316 /**
317 * Store blocklist.
318 *
319 * @param string $file_path File path.
320 *
321 * @since 4.8.0
322 * @return void
323 */
324 private function store_blocklist( string $file_path ): void {
325 try {
326 $generator = $this->read_file_generator( $file_path );
327
328 $this->delete_blocklist();
329 $this->model->bulk_insert( $generator );
330
331 self::set_last_sync();
332
333 $this->log( 'AntiBot Global Firewall: IPs stored in the DB table.', Firewall::FIREWALL_LOG );
334 } catch ( Exception $e ) {
335 $this->log( 'AntiBot Global Firewall Error: ' . $e->getMessage(), Firewall::FIREWALL_LOG );
336 }
337
338 // Remove temporary file.
339 wp_delete_file( $file_path );
340 }
341
342 /**
343 * Read file generator.
344 *
345 * @param string $file_path File path.
346 *
347 * @return Generator
348 * @throws Exception If file not found or could not open the file.
349 */
350 private function read_file_generator( $file_path ) {
351 if ( ! file_exists( $file_path ) ) {
352 throw new Exception( 'File not found.' );
353 }
354
355 global $wp_filesystem;
356 $lines = $wp_filesystem->get_contents_array( $file_path );
357
358 if ( ! is_array( $lines ) || array() === $lines ) {
359 throw new Exception( 'Could not retrieve the file contents!' );
360 }
361
362 try {
363 foreach ( $lines as $line ) {
364 yield trim( $line );
365 }
366 } catch ( Exception $exception ) {
367 throw $exception;
368 }
369 }
370
371 /**
372 * Get the blocklisted IP count.
373 *
374 * @return string The blocklisted IP count.
375 */
376 public function get_blocklisted_ip_count(): string {
377 static $count = null;
378
379 if ( null !== $count ) {
380 return $count;
381 }
382
383 // Check if the feature is enabled.
384 if ( ! $this->frontend_is_enabled() ) {
385 $count = '0';
386 } else {
387 // Since from v5.0.2 one method is used for counting.
388 $count = number_format( $this->get_cached_blocklisted_ips() );
389 }
390
391 return $count;
392 }
393
394 /**
395 * Get the last sync timestamp.
396 *
397 * @return false|int The last sync timestamp.
398 */
399 public static function get_last_sync() {
400 return get_site_option( self::LAST_SYNC_OPTION, false );
401 }
402
403 /**
404 * Set the last sync timestamp.
405 */
406 public static function set_last_sync(): void {
407 update_site_option( self::LAST_SYNC_OPTION, time() );
408 }
409
410 /**
411 * Check if the IPs should be downloaded from Antibot service.
412 *
413 * @return bool True if the IPs should be downloaded, false otherwise.
414 */
415 public function maybe_download(): bool {
416 if ( $this->wpmudev->is_wpmu_hosting() && 'plugin' !== $this->get_managed_by() ) {
417 return false;
418 }
419
420 $last_sync = self::get_last_sync();
421
422 if ( false === $last_sync ) {
423 return true;
424 }
425
426 return time() - $last_sync >= 12 * HOUR_IN_SECONDS;
427 }
428
429 /**
430 * Get the default managed by.
431 *
432 * @return string The default managed by.
433 */
434 public function get_default_managed_by(): string {
435 return $this->wpmudev->is_wpmu_hosting() ? 'hosting' : 'plugin';
436 }
437
438 /**
439 * Get the AntiBot managed by.
440 *
441 * @return string The AntiBot managed by.
442 */
443 public function get_managed_by(): string {
444 $managed_by = $this->model_setting->managed_by;
445
446 return '' !== $managed_by ? $managed_by : $this->get_default_managed_by();
447 }
448
449 /**
450 * Get the AntiBot managed by label.
451 *
452 * @return string The AntiBot managed by label.
453 */
454 public function get_managed_by_label(): string {
455 return $this->is_managed_by_plugin() ?
456 esc_html__( 'Defender Plugin', 'defender-security' ) :
457 esc_html__( 'WPMU DEV Hosting', 'defender-security' );
458 }
459
460 /**
461 * Set the AntiBot managed by.
462 *
463 * @param string $managed_by The managed by value.
464 *
465 * @return bool True if the managed by is set, false otherwise.
466 */
467 public function set_managed_by( string $managed_by ): bool {
468 if ( in_array( $managed_by, Antibot_Global_Firewall_Setting::MANAGED_BY_ALLOWED, true ) ) {
469 $this->model_setting->managed_by = $managed_by;
470 $this->model_setting->save();
471
472 return true;
473 }
474
475 return false;
476 }
477
478 /**
479 * Switch the AntiBot managed by.
480 *
481 * @return string|false The managed by value if it's switched, false otherwise.
482 */
483 public function switch_managed_by() {
484 $managed_by = $this->is_managed_by_plugin() ? 'hosting' : 'plugin';
485
486 if ( $this->set_managed_by( $managed_by ) ) {
487 if ( 'plugin' === $managed_by ) {
488 return false === $this->managed_by_plugin_action() ? false : $managed_by;
489 } else {
490 return false === $this->managed_by_hosting_action() ? false : $managed_by;
491 }
492 }
493
494 return false;
495 }
496
497 /**
498 * Action when the AntiBot Global Firewall is enabled/disabled by plugin.
499 *
500 * @param bool $enabled True to enable, false to disable.
501 *
502 * @return bool True if the enabled/disabled action is successful, false otherwise.
503 */
504 public function managed_by_plugin_action( bool $enabled = true ): bool {
505 if ( $enabled ) {
506 set_site_transient( self::IS_SWITCHING_TO_PLUGIN_IN_PROGRESS, true, 30 );
507
508 // Disable AntiBot on hosting if it's currently enabled.
509 $this->toggle_on_hosting( false, 0.1 );
510
511 // Schedule a single event to confirm AntiBot is disabled on hosting after 15 seconds.
512 if ( ! wp_next_scheduled( 'wpdef_confirm_antibot_toggle_on_hosting' ) ) {
513 wp_schedule_single_event( time() + 15, 'wpdef_confirm_antibot_toggle_on_hosting', array( false ) );
514 }
515
516 // Enable AntiBot on plugin side.
517 $this->model_setting->enabled = true;
518 $this->model_setting->save();
519
520 // Download IPs.
521 $this->download_and_store_blocklist();
522 } else {
523 // Disable AntiBot on plugin side.
524 $this->model_setting->enabled = false;
525 $this->model_setting->save();
526
527 // Remove IPs.
528 $this->delete_blocklist();
529 }
530
531 return true;
532 }
533
534 /**
535 * Action when the AntiBot Global Firewall is enabled/disabled by hosting.
536 *
537 * @param bool $enabled True to enable, false to disable.
538 * @return bool True if the enabled/disabled action is successful, false otherwise.
539 */
540 public function managed_by_hosting_action( bool $enabled = true ): bool {
541 if ( $enabled ) {
542 wp_clear_scheduled_hook( 'wpdef_confirm_antibot_toggle_on_hosting' );
543 delete_site_transient( self::IS_SWITCHING_TO_PLUGIN_IN_PROGRESS );
544
545 // Disable AntiBot on plugin side.
546 $this->model_setting->enabled = false;
547 $this->model_setting->save();
548
549 // Remove IPs from DB table.
550 $this->delete_blocklist();
551
552 // Enable AntiBot on hosting if it's currently disabled.
553 $result = $this->toggle_on_hosting( true );
554 if ( is_wp_error( $result ) ) {
555 return false;
556 }
557 } else {
558 $result = $this->toggle_on_hosting( false );
559 if ( is_wp_error( $result ) ) {
560 return false;
561 }
562 }
563
564 return true;
565 }
566
567 /**
568 * Toggle the AntiBot Global Firewall on hosting.
569 *
570 * @param bool $enable True to enable, false to disable.
571 * @param float $timeout The timeout for the request.
572 *
573 * @return bool|WP_Error True if the AntiBot Global Firewall is toggled, false if not, WP_Error otherwise.
574 */
575 public function toggle_on_hosting( bool $enable, float $timeout = 30 ) {
576 if ( ! $this->wpmudev->is_wpmu_hosting() ) {
577 return false;
578 }
579
580 $hosting_enable = defender_get_hosting_feature_state( 'antibot' );
581
582 if (
583 ( $enable && ! $hosting_enable ) ||
584 ( ! $enable && $hosting_enable )
585 ) {
586 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
587
588 $body = array( 'is_active' => $enable );
589 if ( $enable ) {
590 $body['mode'] = $this->get_hosting_mode();
591 }
592
593 $data = $this->make_wpmu_request(
594 WPMUDEV::API_ANTIBOT_GLOBAL_FIREWALL,
595 $body,
596 array(
597 'method' => 'PUT',
598 'timeout' => $timeout,
599 )
600 );
601
602 if ( $timeout > 1 && is_wp_error( $data ) ) {
603 $this->log( 'AntiBot Global Firewall Error: ' . $data->get_error_message(), Firewall::FIREWALL_LOG );
604 return $data;
605 }
606 }
607
608 return true;
609 }
610
611 /**
612 * Fetches the number of blocklisted IPs from cache or from the Blocklist API.
613 *
614 * If the value is not cached, it will fetch the number of blocklisted IPs from the Blocklist API and cache it.
615 * Implements failure caching and exponential backoff to prevent API hammering on errors.
616 *
617 * @return int The number of blocklisted IPs.
618 */
619 public function get_cached_blocklisted_ips(): int {
620 // If not connected skip the remote call entirely and return zero.
621 if ( ! $this->is_site_connected_to_hub_via_hcm_or_dash() ) {
622 return 0;
623 }
624
625 $mode = $this->frontend_mode();
626 $stats_key = self::BLOCKLIST_STATS_KEY . '_' . $mode;
627 $cached_data = get_site_transient( $stats_key );
628 $cached_full = get_site_transient( self::BLOCKLIST_STATS_FULL_KEY );
629 if ( false !== $cached_data && is_array( $cached_full ) ) {
630 return (int) $cached_data;
631 }
632 $cached_count = false !== $cached_data ? (int) $cached_data : 0;
633
634 // Check if we're in a backoff period due to previous failures.
635 $failure_key = self::BLOCKLIST_STATS_FAILURE_KEY;
636 $failure_data = get_site_transient( $failure_key );
637 if ( is_array( $failure_data ) && array() !== $failure_data ) {
638 $fail_count = isset( $failure_data['count'] ) ? (int) $failure_data['count'] : 0;
639 $fail_time = isset( $failure_data['time'] ) ? (int) $failure_data['time'] : 0;
640
641 if ( $fail_count > 0 && $fail_time > 0 ) {
642 // Exponential backoff: 5, 25, 125, 625, 3125, max 1 hour.
643 $backoff = min( pow( 5, $fail_count ), self::MAX_BACKOFF_TIME );
644 if ( $fail_time > ( defender_get_current_time() - $backoff ) ) {
645 $this->log( 'AntiBot Global Firewall: Skipping API call due to backoff (failures: ' . $fail_count . ')', Firewall::FIREWALL_LOG );
646 return $cached_count;
647 }
648 }
649 }
650
651 $blocklist_stats = $this->antibot_client->get_blocklist_stats();
652
653 if ( is_wp_error( $blocklist_stats ) ) {
654 $this->log( 'AntiBot Global Firewall Error: ' . $blocklist_stats->get_error_message(), Firewall::FIREWALL_LOG );
655 $this->record_stats_failure( $failure_key, $failure_data );
656 return $cached_count;
657 }
658
659 $blocklisted_ips_key = Antibot_Global_Firewall_Setting::MODE_BASIC === $mode ? 'blocked_ips' : 'strict_blocked_ips';
660 if ( ! isset( $blocklist_stats[ $blocklisted_ips_key ] ) || ! is_int( $blocklist_stats[ $blocklisted_ips_key ] ) || 0 >= $blocklist_stats[ $blocklisted_ips_key ] ) {
661 $this->log( 'AntiBot Global Firewall Error: Stats missing for mode: ' . $mode, Firewall::FIREWALL_LOG );
662 $this->record_stats_failure( $failure_key, $failure_data );
663 return $cached_count;
664 }
665
666 // Success - clear any failure tracking.
667 delete_site_transient( $failure_key );
668
669 set_site_transient( self::BLOCKLIST_STATS_FULL_KEY, $blocklist_stats, 12 * HOUR_IN_SECONDS );
670
671 $blocklisted_ips = $blocklist_stats[ $blocklisted_ips_key ];
672 set_site_transient( $stats_key, $blocklisted_ips, 12 * HOUR_IN_SECONDS );
673
674 return $blocklisted_ips;
675 }
676
677 /**
678 * Cached full GET /stats `data` (BLOCKLIST_STATS_FULL_KEY).
679 *
680 * @return array<string, mixed>
681 */
682 public function get_cached_blocklist_stats_data(): array {
683 if ( ! $this->is_site_connected_to_hub_via_hcm_or_dash() ) {
684 return array();
685 }
686 $full = get_site_transient( self::BLOCKLIST_STATS_FULL_KEY );
687
688 return is_array( $full ) ? $full : array();
689 }
690
691 /**
692 * Records a stats API failure for backoff tracking.
693 *
694 * @param string $failure_key The transient key for failure tracking.
695 * @param array|false $failure_data Existing failure data or false.
696 */
697 private function record_stats_failure( string $failure_key, $failure_data ): void {
698 $fail_count = 0;
699 if ( false !== $failure_data && is_array( $failure_data ) && isset( $failure_data['count'] ) ) {
700 $fail_count = (int) $failure_data['count'];
701 }
702
703 set_site_transient(
704 $failure_key,
705 array(
706 'count' => $fail_count + 1,
707 'time' => defender_get_current_time(),
708 ),
709 self::FAILURE_CACHE_TTL
710 );
711 }
712
713 /**
714 * Confirm the toggle on hosting.
715 *
716 * @param bool $enable True to enable, false to disable.
717 *
718 * @return void
719 */
720 public function confirm_toggle_on_hosting( bool $enable ): void {
721 $this->toggle_on_hosting( $enable );
722 delete_site_transient( self::IS_SWITCHING_TO_PLUGIN_IN_PROGRESS );
723 }
724
725 /**
726 * Logs IP-related messages if logging is enabled via filter.
727 *
728 * @param string $message The message to be logged.
729 */
730 public function log_ip_message( string $message ): void {
731 /**
732 * Filters whether IP logging is enabled.
733 *
734 * This filter allows developers to enable or disable IP logging globally.
735 * Returning false will prevent the log message from being written.
736 *
737 * @param bool $is_enabled Whether IP logging is enabled. Default true.
738 * @since 5.1.0
739 */
740 $is_logging_enabled = apply_filters( 'wpdef_antibot_global_firewall_ip_log', true );
741 $is_logging_enabled = is_bool( $is_logging_enabled ) ? $is_logging_enabled : (bool) $is_logging_enabled;
742 if ( ! $is_logging_enabled ) {
743 return;
744 }
745
746 $this->log( $message, self::LOG_FILE_NAME );
747 }
748
749 /**
750 * Get the AntiBot mode status in DB.
751 *
752 * @return string The AntiBot mode.
753 */
754 public function get_mode(): string {
755 return $this->model_setting->mode;
756 }
757
758 /**
759 * Get the AntiBot mode on WPMU DEV.
760 *
761 * @return string The AntiBot mode label.
762 */
763 public function get_hosting_mode(): string {
764 $mode = defender_get_hosting_feature_state( 'antibot_mode' );
765
766 return '' !== $mode ? $mode : Antibot_Global_Firewall_Setting::MODE_BASIC;
767 }
768
769 /**
770 * Get AntiBot mode based on managed by.
771 *
772 * @return string The AntiBot mode.
773 */
774 public function frontend_mode(): string {
775 return $this->is_managed_by_plugin() ? $this->get_mode() : $this->get_hosting_mode();
776 }
777
778 /**
779 * Switch the AntiBot mode.
780 *
781 * @return string|false|WP_Error The AntiBot mode value if it's switched, false otherwise.
782 */
783 public function switch_mode() {
784 if ( $this->is_managed_by_plugin() ) {
785 $mode = $this->get_mode();
786
787 $this->model_setting->mode = Antibot_Global_Firewall_Setting::MODE_STRICT === $mode
788 ? Antibot_Global_Firewall_Setting::MODE_BASIC
789 : Antibot_Global_Firewall_Setting::MODE_STRICT;
790 $this->model_setting->save();
791
792 $this->download_and_store_blocklist();
793
794 delete_site_transient( self::BLOCKLIST_STATS_KEY . '_' . $mode );
795 delete_site_transient( self::BLOCKLIST_STATS_FULL_KEY );
796 } else {
797 if ( ! $this->wpmudev->is_wpmu_hosting() ) {
798 return false;
799 }
800
801 $mode = $this->get_hosting_mode();
802
803 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
804 $data = $this->make_wpmu_request(
805 WPMUDEV::API_ANTIBOT_GLOBAL_FIREWALL,
806 array(
807 'is_active' => $this->hosting_is_enabled(),
808 'mode' => Antibot_Global_Firewall_Setting::MODE_STRICT === $mode
809 ? Antibot_Global_Firewall_Setting::MODE_BASIC
810 : Antibot_Global_Firewall_Setting::MODE_STRICT,
811 ),
812 array( 'method' => 'PUT' )
813 );
814
815 if ( is_wp_error( $data ) ) {
816 $this->log( 'AntiBot Global Firewall Error: ' . $data->get_error_message(), Firewall::FIREWALL_LOG );
817 return $data;
818 }
819
820 delete_site_transient( self::BLOCKLIST_STATS_KEY . '_' . $mode );
821 delete_site_transient( self::BLOCKLIST_STATS_FULL_KEY );
822 }
823
824 return $this->frontend_mode();
825 }
826
827 /**
828 * Clear antibot table when site is disconnected from HUB.
829 *
830 * @return void
831 */
832 public function clear_antibot_on_disconnection(): void {
833 if ( $this->is_site_connected_to_hub_via_hcm_or_dash() ) {
834 return;
835 }
836 // Skip if there is nothing to clean up. No local DB records and no cached stats transients.
837 $has_db_records = $this->model->has_records();
838 $has_stats_cached = false !== get_site_transient( self::BLOCKLIST_STATS_KEY . '_' . $this->get_mode() )
839 || false !== get_site_transient( self::BLOCKLIST_STATS_KEY . '_' . $this->get_hosting_mode() )
840 || false !== get_site_transient( self::BLOCKLIST_STATS_FULL_KEY );
841 if ( ! $has_db_records && ! $has_stats_cached ) {
842 return;
843 }
844
845 $this->delete_blocklist();
846 delete_site_transient( self::BLOCKLIST_STATS_KEY . '_' . $this->get_mode() );
847 delete_site_transient( self::BLOCKLIST_STATS_KEY . '_' . $this->get_hosting_mode() );
848 delete_site_transient( self::BLOCKLIST_STATS_FULL_KEY );
849 $this->log( 'Antibot table cleared due to site disconnection.', self::LOG_FILE_NAME );
850 }
851
852 /**
853 * Get the timestamp for when the notice should start showing.
854 *
855 * @return false|int The timestamp or false if not set.
856 */
857 public function get_global_notice_time() {
858 return (int) get_site_option( self::GLOBAL_NOTICE_TIME_OPTION, 0 );
859 }
860
861 /**
862 * Set a timestamp for when the notice should start showing.
863 *
864 * @return void
865 */
866 public function maybe_set_notice_time(): void {
867 if ( 0 === $this->get_global_notice_time() ) {
868 $future = time() + ( DAY_IN_SECONDS * self::GLOBAL_NOTICE_DELAY_DAYS_OPTION );
869 update_site_option( self::GLOBAL_NOTICE_TIME_OPTION, $future );
870 }
871 }
872
873 /**
874 * Disable the global notice.
875 *
876 * @return void
877 */
878 public function dismiss_global_notice(): void {
879 update_site_option( self::GLOBAL_NOTICE_TIME_OPTION, -1 );
880 }
881
882 /**
883 * Determine if the global notice should be shown.
884 *
885 * @return bool True if the notice should be shown, false otherwise.
886 */
887 public function should_show_global_notice(): bool {
888 // Is this from Unlimited hosting?
889 if ( defender_is_unlimited_hosting() ) {
890 return false;
891 }
892 // Is FREE or PRO plugin?
893 if ( $this->wpmudev->is_pro() ) {
894 return false;
895 }
896
897 // Is AntiBot enabled?
898 if ( $this->frontend_is_enabled() ) {
899 return false;
900 }
901
902 // Has 60 days passed since installation?
903 $notice_time = $this->get_global_notice_time();
904
905 // No notice time or marked as dismissed (-1) or not yet reached the time.
906 if ( false === $notice_time || -1 === $notice_time || time() < $notice_time ) {
907 return false;
908 }
909
910 return true;
911 }
912
913 /**
914 * Get the Unlimited Hosting site ID.
915 *
916 * @return string|bool
917 */
918 private function get_uh_site_id() {
919 return defined( 'WPMUDEV_HOSTING_SITE_ID' ) ? WPMUDEV_HOSTING_SITE_ID : gethostname();
920 }
921
922 /**
923 * Get the link to the site tools page on the Unlimited Hosting dashboard.
924 *
925 * @return string|bool
926 */
927 public function get_uh_site_tools_link() {
928 if ( $this->get_uh_site_id() ) {
929 return 'https://wpmudev.com/hub2/unlimited-hosting/' . $this->get_uh_site_id() . '/tools';
930 }
931
932 return false;
933 }
934
935 /**
936 * Show some Antibot parts depending on Antibot settings on Unlimited hosting (UH) server:
937 * 1) when Antibot is enabled on UH, on the plugin side we need to:
938 * -remove the Switch & Deactivate buttons,
939 * -only show the Active Mode & hide the other mode.
940 * 2) when Antibot is disabled on UH, on the plugin side we need to:
941 * -remove only the Switch button.
942 * 2.1) if Whitelabel is enabled, the activation button is disabled.
943 * 2.2) if Whitelabel is disabled, the activation button is redirected to the Hub.
944 *
945 * If the Antibot is managed by Dedicated DEV hosting, we can show the Switch button.
946 *
947 * Without changes for other hostings.
948 */
949 public function get_states_of_antibot_options_for_different_hosting_types(): array {
950 $show_antibot_options = array(
951 'is_unlimited_hosting' => false,
952 'switch_button' => true,
953 'deactivate_button' => true,
954 'available_modes' => Antibot_Global_Firewall_Setting::get_valid_modes(),
955 'uh_activate_btn_text' => __( 'Activate', 'defender-security' ),
956 'uh_activate_btn_link' => '',
957 );
958
959 if ( defender_is_unlimited_hosting() ) {
960 $show_antibot_options['switch_button'] = false;
961 if ( $this->is_active_via_hosting() ) {
962 $show_antibot_options['deactivate_button'] = false;
963 // Rewrite the array().
964 $show_antibot_options['available_modes'] = array( $this->frontend_mode() );
965 }
966 // Get activation button text if the Antibot is disabled.
967 if ( ! $this->frontend_is_enabled() ) {
968 $show_antibot_options['is_unlimited_hosting'] = true;
969 if ( $this->wpmudev->is_whitelabel_enabled() ) {
970 $show_antibot_options['uh_activate_btn_text'] = __( 'DISABLED', 'defender-security' );
971 } else {
972 $uh_site_tools_link = $this->get_uh_site_tools_link();
973 if ( $uh_site_tools_link ) {
974 $show_antibot_options['uh_activate_btn_link'] = $uh_site_tools_link;
975 $show_antibot_options['uh_activate_btn_text'] = __( 'MANAGE', 'defender-security' );
976
977 }
978 }
979 }
980 } else {
981 $show_antibot_options['switch_button'] = $this->wpmudev->is_wpmu_hosting();
982 }
983
984 return $show_antibot_options;
985 }
986 }
987