PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.2.1
Defender Security – Malware Scanner, Login Security & Firewall v6.2.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 / component / ip / class-antibot-global-firewall.php

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

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