PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.3.0
Defender Security – Malware Scanner, Login Security & Firewall v6.3.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-global-ip.php

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

638 lines 17.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles global IP functionalities including allow and block lists.
4 *
5 * @package WP_Defender\Component\IP
6 */
7
8 namespace WP_Defender\Component\IP;
9
10 use WP_Error;
11 use Exception;
12 use WP_Defender\Component;
13 use WP_Defender\Behavior\WPMUDEV;
14 use WP_Defender\Controller\Firewall;
15 use WP_Defender\Model\Lockout_Log;
16 use WP_Defender\Model\Setting\Global_Ip_Lockout;
17 use WP_Defender\Traits\Defender_Dashboard_Client;
18 use WP_Defender\Traits\Country;
19
20 /**
21 * Handles global IP functionalities including allow and block lists.
22 */
23 class Global_IP extends Component {
24
25 use Defender_Dashboard_Client;
26 use Country;
27
28 public const REASON_SLUG = 'global_ip';
29 /**
30 * Global IP list key.
31 */
32 public const LIST_KEY = 'wpdef_global_ip_list';
33 public const LIST_LAST_SYNCED_KEY = 'wpdef_global_ip_list_last_synced';
34 public const DASHBOARD_NOTICE_REMINDER = 'wpdef_global_ip_dashoard_notice_reminder';
35
36 /**
37 * The list of allowed IPs.
38 *
39 * @var array
40 */
41 private $allow_list = array();
42
43 /**
44 * The list of blocked IPs.
45 *
46 * @var array
47 */
48 private $block_list = array();
49
50 /**
51 * Fetches data from HUB API service method.
52 *
53 * @var array
54 */
55 private $global_list = array();
56
57 /**
58 * Instance of WPMUDEV class.
59 *
60 * @var WPMUDEV
61 */
62 private $wpmudev;
63
64 /**
65 * The model for global IP lockout settings.
66 *
67 * @var Global_Ip_Lockout
68 */
69 protected $model;
70
71 /**
72 * Initializes the WPMUDEV and Global_Ip_Lockout instances and sets up the initial state.
73 */
74 public function __construct() {
75 $this->wpmudev = wd_di()->get( WPMUDEV::class );
76 $this->model = wd_di()->get( Global_Ip_Lockout::class );
77 $this->init();
78 }
79
80 /**
81 * Initialize
82 *
83 * @return void
84 */
85 public function init(): void {
86 $global_ip_list = $this->get_global_ip_list();
87
88 $this->global_list = is_array( $global_ip_list ) ? $global_ip_list : array();
89 $this->allow_list = $global_ip_list['allow_list'] ?? array();
90 $this->block_list = $global_ip_list['block_list'] ?? array();
91 }
92
93 /**
94 * Return global ip allow list from HUB.
95 */
96 public function allow_list(): array {
97 return $this->allow_list;
98 }
99
100 /**
101 * Return global ip block list from HUB.
102 */
103 public function block_list(): array {
104 return $this->block_list;
105 }
106
107 /**
108 * Verify is given ip exists in global IP allow list.
109 *
110 * @param string $ip ip address.
111 *
112 * @return bool if exists return true else false.
113 */
114 public function is_ip_allowed( string $ip ): bool {
115 return $this->is_ip_in_format( $ip, $this->allow_list() );
116 }
117
118 /**
119 * Verify is given ip exists in global IP block list.
120 *
121 * @param string $ip ip address.
122 *
123 * @return bool if exists return true else false.
124 */
125 public function is_ip_blocked( string $ip ): bool {
126 return $this->is_ip_in_format( $ip, $this->block_list() );
127 }
128
129 /**
130 * Check if Global IP is enabled.
131 *
132 * @return bool True for enabled or false for disabled.
133 */
134 public function is_global_ip_enabled(): bool {
135 return $this->model->enabled;
136 }
137
138 /**
139 * Check if Custom IP lists can be synced automatically.
140 *
141 * @return bool True for enabled or false for disabled.
142 */
143 public function is_blocklist_autosync_enabled(): bool {
144 return $this->model->blocklist_autosync;
145 }
146
147 /**
148 * Check if permanently blocked IPs can be synced with HUB.
149 *
150 * @return bool
151 */
152 public function can_blocklist_autosync(): bool {
153 return $this->wpmudev->is_dash_activated() &&
154 $this->wpmudev->is_site_connected_to_hub() &&
155 $this->is_global_ip_enabled() &&
156 $this->is_blocklist_autosync_enabled();
157 }
158
159 /**
160 * Check if blocked or allowlisted IPs can be synced with the Custom IP lists on HUB.
161 *
162 * @return bool
163 */
164 public function can_central_ip_autosync(): bool {
165 return $this->wpmudev->hub_connector_connected() &&
166 $this->is_global_ip_enabled() &&
167 $this->is_blocklist_autosync_enabled();
168 }
169
170 /**
171 * Get Global IP list from DB.
172 *
173 * @return mixed
174 */
175 public function get_global_ip_list() {
176 return get_site_transient( self::LIST_KEY );
177 }
178
179 /**
180 * Set Global IP list.
181 *
182 * @param array $data The data containing the allow list, block list, last update time, and last update time UTC.
183 *
184 * @return bool|WP_Error
185 */
186 public function set_global_ip_list( array $data ) {
187 if (
188 ! isset( $data['allow_list'] ) ||
189 ! isset( $data['block_list'] ) ||
190 ! isset( $data['last_update_time'] ) ||
191 ! isset( $data['last_update_time_utc'] )
192 ) {
193 return new WP_Error( 'defender_hub_api_missing_params', esc_html__( 'Missing parameter(s)', 'defender-security' ) );
194 }
195
196 $allow_list = array();
197 $block_list = array();
198 $errors = array(
199 'allow_list' => array(),
200 'block_list' => array(),
201 );
202
203 if ( is_array( $data['allow_list'] ) ) {
204 foreach ( $data['allow_list'] as $key => $ip ) {
205 $error = $this->display_validation_message( $ip );
206
207 if ( array() !== $error ) {
208 $errors['allow_list'] = array_merge( $error, $errors['allow_list'] );
209 unset( $data['allow_list'][ $key ] );
210 }
211 }
212 $allow_list = $data['allow_list'];
213 }
214
215 if ( is_array( $data['block_list'] ) ) {
216 foreach ( $data['block_list'] as $key => $ip ) {
217 $error = $this->display_validation_message( $ip );
218
219 if ( array() !== $error ) {
220 $errors['block_list'] = array_merge( $error, $errors['block_list'] );
221 unset( $data['block_list'][ $key ] );
222 }
223 }
224 $block_list = $data['block_list'];
225 }
226
227 $value = array(
228 'allow_list' => $allow_list,
229 'block_list' => $block_list,
230 'last_update_time' => $data['last_update_time'] ?? '',
231 'last_update_time_utc' => $data['last_update_time_utc'] ?? '',
232 );
233 $ret = set_site_transient( self::LIST_KEY, $value );
234 $this->global_list = $value;
235 $this->allow_list = $value['allow_list'];
236 $this->block_list = $value['block_list'];
237
238 self::set_last_synced();
239
240 if (
241 ( isset( $errors['allow_list'] ) && array() !== $errors['allow_list'] )
242 || ( isset( $errors['block_list'] ) && array() !== $errors['block_list'] )
243 ) {
244 return new WP_Error( 'defender_hub_api_invalid_ips', esc_html__( 'Invalid IP(s)', 'defender-security' ), $errors );
245 } else {
246 return $ret;
247 }
248 }
249
250 /**
251 * Format Global IP list for frontend.
252 *
253 * @return array
254 */
255 public function get_formated_global_ip_list(): array {
256 $data = $this->global_list;
257 $last_synced = self::get_last_synced();
258
259 return array(
260 'allow_list' => isset( $data['allow_list'] ) && is_array( $data['allow_list'] ) && array() !== $data['allow_list'] ?
261 implode( '<br>', $data['allow_list'] ) :
262 '',
263 'block_list' => isset( $data['block_list'] ) && is_array( $data['block_list'] ) && array() !== $data['block_list'] ?
264 implode( '<br>', $data['block_list'] ) :
265 '',
266 'last_synced' => 0 !== $last_synced ?
267 $this->format_date_time( $last_synced ) :
268 esc_html__( 'Never', 'defender-security' ),
269 'block_list_count' => isset( $data['block_list'] ) && is_array( $data['block_list'] ) && array() !== $data['block_list'] ? $this->format_number( count( $data['block_list'] ) ) : 0,
270 'last_update_time_utc' => isset( $data['last_update_time_utc'] ) && false !== $data['last_update_time_utc'] && '' !== $data['last_update_time_utc'] ?
271 $this->format_date_time( $data['last_update_time_utc'] ) :
272 esc_html__( 'Never', 'defender-security' ),
273 'last_update_time' => isset( $data['last_update_time'] ) && false !== $data['last_update_time'] && '' !== $data['last_update_time'] ?
274 $this->format_date_time( $data['last_update_time'] ) :
275 esc_html__( 'Never', 'defender-security' ),
276 'is_synced_before' => array() !== $data,
277 );
278 }
279
280 /**
281 * Fetch Global IP list from HUB if DB has old data.
282 *
283 * @return array|WP_Error On success return global ip list or on failure return WP_Error object.
284 */
285 public function fetch_global_ip_list() {
286 $data = $this->get_global_ip_list();
287
288 if ( ! is_array( $data ) ) {
289 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
290
291 try {
292 $data = $this->make_wpmu_free_request( WPMUDEV::API_GLOBAL_IP_LIST );
293 } catch ( Exception $e ) {
294 $this->log( 'Global IP API Error: ' . $e->getMessage(), Firewall::FIREWALL_LOG );
295
296 return new WP_Error( 'defender_hub_api_invalid_returned', $e->getMessage() );
297 }
298
299 if ( is_array( $data ) ) {
300 $this->set_global_ip_list( $data );
301 }
302 } else {
303 $updated_time = $this->fetch_global_ip_list_updated_time();
304
305 if (
306 is_wp_error( $updated_time ) ||
307 ! isset( $updated_time['last_update_time_utc'] ) ||
308 ! is_string( $updated_time['last_update_time_utc'] ) ||
309 '' === $updated_time['last_update_time_utc'] ||
310 ! isset( $data['last_update_time_utc'] ) ||
311 ! is_string( $data['last_update_time_utc'] ) ||
312 '' === $data['last_update_time_utc'] ||
313 strtotime( $updated_time['last_update_time_utc'] ) > strtotime( $data['last_update_time_utc'] )
314 ) {
315 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
316
317 try {
318 $data = $this->make_wpmu_free_request( WPMUDEV::API_GLOBAL_IP_LIST );
319 } catch ( Exception $e ) {
320 $this->log( 'Global IP API Error: ' . $e->getMessage(), Firewall::FIREWALL_LOG );
321
322 return new WP_Error( 'defender_hub_api_invalid_returned', $e->getMessage() );
323 }
324
325 if ( is_array( $data ) ) {
326 $this->set_global_ip_list( $data );
327 }
328 } else {
329 self::set_last_synced();
330 }
331 }
332
333 if ( ! is_array( $data ) ) {
334 $this->log( 'Global IP API Error: Fetch Global IP list', Firewall::FIREWALL_LOG );
335 $this->log( $data, Firewall::FIREWALL_LOG );
336
337 return new WP_Error(
338 'defender_hub_api_invalid_returned',
339 esc_html__( 'API returned invalid data format.', 'defender-security' )
340 );
341 }
342
343 return $data;
344 }
345
346 /**
347 * Fetch Global IP list updated time from HUB.
348 *
349 * @return array|WP_Error
350 */
351 public function fetch_global_ip_list_updated_time() {
352 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
353
354 return $this->make_wpmu_request(
355 WPMUDEV::API_GLOBAL_IP_LIST,
356 array(
357 '_fields' => 'last_update_time_utc',
358 )
359 );
360 }
361
362 /**
363 * Add one or more IPs to the allow_list and/or block_list.
364 *
365 * @param array $params The IPs to add.
366 *
367 * @since 4.12.0 Send a request using API key from the Dashboard plugin or HCM.
368 * @return array|WP_Error
369 */
370 public function add_to_global_ip_list( array $params ) {
371 $allow_list = isset( $params['allow_list'] ) ? (array) $params['allow_list'] : array();
372 $block_list = isset( $params['block_list'] ) ? (array) $params['block_list'] : array();
373 $is_allow_list = array() !== $allow_list;
374 $is_block_list = array() !== $block_list;
375 if ( ! $is_allow_list && ! $is_block_list ) {
376 return new WP_Error(
377 'global_ip_invalid_params',
378 esc_html__( 'Invalid Global IP parameter(s) provided.', 'defender-security' )
379 );
380 }
381
382 $data = array();
383 if ( $is_allow_list ) {
384 $data['allow_list'] = $allow_list;
385 }
386 if ( $is_block_list ) {
387 $data['block_list'] = $block_list;
388 }
389
390 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
391 $ret = $this->make_wpmu_request(
392 WPMUDEV::API_GLOBAL_IP_LIST,
393 $data,
394 array(
395 'method' => 'POST',
396 )
397 );
398
399 if ( is_array( $ret ) ) {
400 $this->set_global_ip_list( $ret );
401 } else {
402 $this->log( 'Global IP API Error: Add IP(s) to allow_list and/or block_list', Firewall::FIREWALL_LOG );
403 $this->log( $ret, Firewall::FIREWALL_LOG );
404 }
405
406 return $ret;
407 }
408
409 /**
410 * Get welcome modal closed timestamp.
411 *
412 * @return false|int
413 */
414 public function get_dashboard_notice_reminder() {
415 return get_site_option( self::DASHBOARD_NOTICE_REMINDER, false );
416 }
417
418 /**
419 * Delete welcome modal closed timestamp.
420 *
421 * @return void
422 */
423 public function delete_dashboard_notice_reminder(): void {
424 delete_site_option( self::DASHBOARD_NOTICE_REMINDER );
425 }
426
427 /**
428 * Check if notice on Defender dashboard can be displayed.
429 *
430 * @return bool
431 */
432 public function is_show_dashboard_notice(): bool {
433 $reminder = $this->get_dashboard_notice_reminder();
434 if ( $this->is_global_ip_enabled() || ! is_int( $reminder ) || time() < $reminder ) {
435 return false;
436 }
437
438 $is_pro = $this->wpmudev->is_pro();
439 $user_roles = is_user_logged_in() ? $this->get_roles( wp_get_current_user() ) : array();
440
441 $is_show_to_user = false;
442 if ( ! $is_pro && array() !== array_intersect(
443 $user_roles,
444 array(
445 $this->super_admin_slug,
446 'administrator',
447 )
448 ) ) {
449 $is_show_to_user = true;
450 } elseif ( $is_pro && $this->is_wpmu_dev_admin() ) {
451 $is_show_to_user = true;
452 }
453
454 return $is_show_to_user;
455 }
456
457 /**
458 * Format a number to a human-readable format.
459 *
460 * @param int|float|null $number The number to format.
461 *
462 * @return string The formatted number.
463 */
464 public function format_number( $number ) {
465 if ( null === $number || ! is_numeric( $number ) ) {
466 return '';
467 }
468
469 if ( $number >= 1000000000 ) {
470 return $number / 1000000000 . 'b';
471 } elseif ( $number >= 1000000 ) {
472 return $number / 1000000 . 'm';
473 } elseif ( $number >= 1000 ) {
474 return $number / 1000 . 'k';
475 } else {
476 return $number;
477 }
478 }
479
480 /**
481 * Remove one or more IPs from the block_list.
482 *
483 * @param array $ips The IPs to remove.
484 *
485 * @return array|WP_Error
486 */
487 public function remove_from_blocklist( array $ips ) {
488 if ( array() === $ips ) {
489 return new WP_Error( 'defender_hub_api_invalid_ips', esc_html__( 'No IP(s) provided.', 'defender-security' ) );
490 }
491
492 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
493
494 try {
495 $data = $this->make_wpmu_free_request( WPMUDEV::API_GLOBAL_IP_LIST );
496 } catch ( Exception $e ) {
497 return new WP_Error( 'defender_hub_api_invalid_returned', $e->getMessage() );
498 }
499
500 if ( ! isset( $data['block_list'] ) ) {
501 return new WP_Error(
502 'defender_hub_api_missing_blocklist',
503 esc_html__( 'Missing blocklist data.', 'defender-security' )
504 );
505 }
506
507 $data['block_list'] = (array) $data['block_list'];
508 foreach ( $ips as $ip ) {
509 $key = array_search( $ip, $data['block_list'], true );
510 if ( false !== $key ) {
511 unset( $data['block_list'][ $key ] );
512 }
513 }
514 // Reindex the array.
515 $data['block_list'] = array_values( $data['block_list'] );
516
517 $ret = $this->make_wpmu_request(
518 WPMUDEV::API_GLOBAL_IP_LIST,
519 array(
520 'block_list' => $data['block_list'],
521 ),
522 array(
523 'method' => 'PUT',
524 )
525 );
526
527 if ( is_wp_error( $ret ) ) {
528 return new WP_Error( 'defender_hub_api_invalid_returned', $ret->get_error_message() );
529 } else {
530 $this->set_global_ip_list( $ret );
531 }
532
533 return $ret;
534 }
535
536 /**
537 * Set the last sync time.
538 *
539 * @return void
540 */
541 public static function set_last_synced(): void {
542 update_site_option( self::LIST_LAST_SYNCED_KEY, time() );
543 }
544
545 /**
546 * Get the last sync time.
547 *
548 * @return int
549 */
550 public static function get_last_synced(): int {
551 return get_site_option( self::LIST_LAST_SYNCED_KEY, 0 );
552 }
553
554 /**
555 * Check if the Global IP feature is enabled and the site is connected to the HUB.
556 *
557 * @return bool True if the Global IP feature is active, false otherwise.
558 */
559 public function is_active(): bool {
560 return $this->is_global_ip_enabled() && $this->is_site_connected_to_hub_via_hcm_or_dash();
561 }
562
563 /**
564 * Log the event into db.
565 *
566 * @param string $ip The IP address involved in the event.
567 */
568 public function log_event( $ip ): void {
569 $model = wd_di()->get( Lockout_Log::class );
570 $model->ip = $ip;
571 if ( $model->has_recent_ip_log() ) {
572 $this->log( 'Custom IP Blocklist: IP already logged', Firewall::FIREWALL_LOG );
573 return;
574 }
575 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
576 $model->user_agent = isset( $user_agent )
577 ? \WP_Defender\Component\User_Agent::fast_cleaning( $user_agent )
578 : null;
579 $model->date = time();
580 $model->blog_id = get_current_blog_id();
581
582 $ip_to_country = $this->ip_to_country( $ip );
583
584 if ( isset( $ip_to_country['iso'] ) ) {
585 $model->country_iso_code = $ip_to_country['iso'];
586 }
587 /* translators: %s is the IP address */
588 $model->log = sprintf( esc_html__( '%s locked out - found in Custom IP Blocklist', 'defender-security' ), $ip );
589
590 $model->type = Lockout_Log::LOCKOUT_IP_CUSTOM;
591 $model->tried = Lockout_Log::LOCKOUT_IP_CUSTOM;
592 $model->save();
593 // We don’t use defender_notify hook for LOCKOUT_IP_CUSTOM.
594 }
595
596 /**
597 * Handle expired membership by automatically disabling the Central IP module.
598 * Logs the action when the feature is disabled due to expired membership.
599 *
600 * @return void
601 */
602 public function handle_expired_membership(): void {
603 if ( $this->model->enabled && $this->is_expired_membership_type() ) {
604 $this->model->enabled = false;
605 $this->model->save();
606 $this->log( 'Central IP automatically disabled due to expired membership.', 'central_ip.log' );
607 }
608 }
609
610 /**
611 * Toggle the Custom IP List feature on hosting.
612 *
613 * @param bool $enable True to enable, false to disable.
614 *
615 * @return bool|WP_Error True on success, false if not on WPMU hosting, WP_Error on failure.
616 */
617 public function toggle_on_hosting( bool $enable ) {
618 if ( ! $this->wpmudev->is_wpmu_hosting() ) {
619 return false;
620 }
621
622 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
623
624 $data = $this->make_wpmu_request(
625 WPMUDEV::API_GLOBAL_IP_LIST_HOSTING,
626 array( 'is_active' => $enable ),
627 array( 'method' => 'PUT' )
628 );
629
630 if ( is_wp_error( $data ) ) {
631 $this->log( 'Global IP API Error: ' . $data->get_error_message(), Firewall::FIREWALL_LOG );
632 return $data;
633 }
634
635 return true;
636 }
637 }
638