PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.8.1
Defender Security – Malware Scanner, Login Security & Firewall v5.8.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-global-ip.php

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

606 lines 16.2 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 return new WP_Error( 'defender_hub_api_invalid_returned', $e->getMessage() );
295 }
296
297 if ( is_array( $data ) ) {
298 $this->set_global_ip_list( $data );
299 }
300 } else {
301 $updated_time = $this->fetch_global_ip_list_updated_time();
302
303 if (
304 is_wp_error( $updated_time ) ||
305 ! isset( $updated_time['last_update_time_utc'] ) ||
306 ! is_string( $updated_time['last_update_time_utc'] ) ||
307 '' === $updated_time['last_update_time_utc'] ||
308 ! isset( $data['last_update_time_utc'] ) ||
309 ! is_string( $data['last_update_time_utc'] ) ||
310 '' === $data['last_update_time_utc'] ||
311 strtotime( $updated_time['last_update_time_utc'] ) > strtotime( $data['last_update_time_utc'] )
312 ) {
313 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
314
315 try {
316 $data = $this->make_wpmu_free_request( WPMUDEV::API_GLOBAL_IP_LIST );
317 } catch ( Exception $e ) {
318 return new WP_Error( 'defender_hub_api_invalid_returned', $e->getMessage() );
319 }
320
321 if ( is_array( $data ) ) {
322 $this->set_global_ip_list( $data );
323 }
324 } else {
325 self::set_last_synced();
326 }
327 }
328
329 if ( ! is_array( $data ) ) {
330 $this->log( 'Global IP API Error: Fetch Global IP list', Firewall::FIREWALL_LOG );
331 $this->log( $data, Firewall::FIREWALL_LOG );
332
333 return new WP_Error(
334 'defender_hub_api_invalid_returned',
335 esc_html__( 'API returned invalid data format.', 'defender-security' )
336 );
337 }
338
339 return $data;
340 }
341
342 /**
343 * Fetch Global IP list updated time from HUB.
344 *
345 * @return array|WP_Error
346 */
347 public function fetch_global_ip_list_updated_time() {
348 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
349
350 return $this->make_wpmu_request(
351 WPMUDEV::API_GLOBAL_IP_LIST,
352 array(
353 '_fields' => 'last_update_time_utc',
354 )
355 );
356 }
357
358 /**
359 * Add one or more IPs to the allow_list and/or block_list.
360 *
361 * @param array $params The IPs to add.
362 *
363 * @since 4.12.0 Send a request using API key from the Dashboard plugin or HCM.
364 * @return array|WP_Error
365 */
366 public function add_to_global_ip_list( array $params ) {
367 $allow_list = isset( $params['allow_list'] ) ? (array) $params['allow_list'] : array();
368 $block_list = isset( $params['block_list'] ) ? (array) $params['block_list'] : array();
369 $is_allow_list = array() !== $allow_list;
370 $is_block_list = array() !== $block_list;
371 if ( ! $is_allow_list && ! $is_block_list ) {
372 return new WP_Error(
373 'global_ip_invalid_params',
374 esc_html__( 'Invalid Global IP parameter(s) provided.', 'defender-security' )
375 );
376 }
377
378 $data = array();
379 if ( $is_allow_list ) {
380 $data['allow_list'] = $allow_list;
381 }
382 if ( $is_block_list ) {
383 $data['block_list'] = $block_list;
384 }
385
386 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
387 $ret = $this->make_wpmu_request(
388 WPMUDEV::API_GLOBAL_IP_LIST,
389 $data,
390 array(
391 'method' => 'POST',
392 )
393 );
394
395 if ( is_array( $ret ) ) {
396 $this->set_global_ip_list( $ret );
397 } else {
398 $this->log( 'Global IP API Error: Add IP(s) to allow_list and/or block_list', Firewall::FIREWALL_LOG );
399 $this->log( $ret, Firewall::FIREWALL_LOG );
400 }
401
402 return $ret;
403 }
404
405 /**
406 * Get welcome modal closed timestamp.
407 *
408 * @return false|int
409 */
410 public function get_dashboard_notice_reminder() {
411 return get_site_option( self::DASHBOARD_NOTICE_REMINDER, false );
412 }
413
414 /**
415 * Delete welcome modal closed timestamp.
416 *
417 * @return void
418 */
419 public function delete_dashboard_notice_reminder(): void {
420 delete_site_option( self::DASHBOARD_NOTICE_REMINDER );
421 }
422
423 /**
424 * Check if notice on Defender dashboard can be displayed.
425 *
426 * @return bool
427 */
428 public function is_show_dashboard_notice(): bool {
429 $reminder = $this->get_dashboard_notice_reminder();
430 if ( $this->is_global_ip_enabled() || ! is_int( $reminder ) || time() < $reminder ) {
431 return false;
432 }
433
434 $is_pro = $this->wpmudev->is_pro();
435 $user_roles = is_user_logged_in() ? $this->get_roles( wp_get_current_user() ) : array();
436
437 $is_show_to_user = false;
438 if ( ! $is_pro && array() !== array_intersect(
439 $user_roles,
440 array(
441 $this->super_admin_slug,
442 'administrator',
443 )
444 ) ) {
445 $is_show_to_user = true;
446 } elseif ( $is_pro && $this->is_wpmu_dev_admin() ) {
447 $is_show_to_user = true;
448 }
449
450 return $is_show_to_user;
451 }
452
453 /**
454 * Format a number to a human-readable format.
455 *
456 * @param int|float|null $number The number to format.
457 *
458 * @return string The formatted number.
459 */
460 public function format_number( $number ) {
461 if ( null === $number || ! is_numeric( $number ) ) {
462 return '';
463 }
464
465 if ( $number >= 1000000000 ) {
466 return $number / 1000000000 . 'b';
467 } elseif ( $number >= 1000000 ) {
468 return $number / 1000000 . 'm';
469 } elseif ( $number >= 1000 ) {
470 return $number / 1000 . 'k';
471 } else {
472 return $number;
473 }
474 }
475
476 /**
477 * Remove one or more IPs from the block_list.
478 *
479 * @param array $ips The IPs to remove.
480 *
481 * @return array|WP_Error
482 */
483 public function remove_from_blocklist( array $ips ) {
484 if ( array() === $ips ) {
485 return new WP_Error( 'defender_hub_api_invalid_ips', esc_html__( 'No IP(s) provided.', 'defender-security' ) );
486 }
487
488 $this->attach_behavior( WPMUDEV::class, WPMUDEV::class );
489
490 try {
491 $data = $this->make_wpmu_free_request( WPMUDEV::API_GLOBAL_IP_LIST );
492 } catch ( Exception $e ) {
493 return new WP_Error( 'defender_hub_api_invalid_returned', $e->getMessage() );
494 }
495
496 if ( ! isset( $data['block_list'] ) ) {
497 return new WP_Error(
498 'defender_hub_api_missing_blocklist',
499 esc_html__( 'Missing blocklist data.', 'defender-security' )
500 );
501 }
502
503 $data['block_list'] = (array) $data['block_list'];
504 foreach ( $ips as $ip ) {
505 $key = array_search( $ip, $data['block_list'], true );
506 if ( false !== $key ) {
507 unset( $data['block_list'][ $key ] );
508 }
509 }
510 // Reindex the array.
511 $data['block_list'] = array_values( $data['block_list'] );
512
513 $ret = $this->make_wpmu_request(
514 WPMUDEV::API_GLOBAL_IP_LIST,
515 array(
516 'block_list' => $data['block_list'],
517 ),
518 array(
519 'method' => 'PUT',
520 )
521 );
522
523 if ( is_wp_error( $ret ) ) {
524 return new WP_Error( 'defender_hub_api_invalid_returned', $ret->get_error_message() );
525 } else {
526 $this->set_global_ip_list( $ret );
527 }
528
529 return $ret;
530 }
531
532 /**
533 * Set the last sync time.
534 *
535 * @return void
536 */
537 public static function set_last_synced(): void {
538 update_site_option( self::LIST_LAST_SYNCED_KEY, time() );
539 }
540
541 /**
542 * Get the last sync time.
543 *
544 * @return int
545 */
546 public static function get_last_synced(): int {
547 return get_site_option( self::LIST_LAST_SYNCED_KEY, 0 );
548 }
549
550 /**
551 * Check if the Global IP feature is enabled and the site is connected to the HUB.
552 *
553 * @return bool True if the Global IP feature is active, false otherwise.
554 */
555 public function is_active(): bool {
556 return $this->is_global_ip_enabled() && $this->is_site_connected_to_hub_via_hcm_or_dash();
557 }
558
559 /**
560 * Log the event into db.
561 *
562 * @param string $ip The IP address involved in the event.
563 */
564 public function log_event( $ip ): void {
565 $model = wd_di()->get( Lockout_Log::class );
566 $model->ip = $ip;
567 if ( $model->has_recent_ip_log() ) {
568 $this->log( 'Custom IP Blocklist: IP already logged', Firewall::FIREWALL_LOG );
569 return;
570 }
571 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
572 $model->user_agent = isset( $user_agent )
573 ? \WP_Defender\Component\User_Agent::fast_cleaning( $user_agent )
574 : null;
575 $model->date = time();
576 $model->blog_id = get_current_blog_id();
577
578 $ip_to_country = $this->ip_to_country( $ip );
579
580 if ( isset( $ip_to_country['iso'] ) ) {
581 $model->country_iso_code = $ip_to_country['iso'];
582 }
583 /* translators: %s is the IP address */
584 $model->log = sprintf( esc_html__( '%s locked out - found in Custom IP Blocklist', 'defender-security' ), $ip );
585
586 $model->type = Lockout_Log::LOCKOUT_IP_CUSTOM;
587 $model->tried = Lockout_Log::LOCKOUT_IP_CUSTOM;
588 $model->save();
589 // We don’t use defender_notify hook for LOCKOUT_IP_CUSTOM.
590 }
591
592 /**
593 * Handle expired membership by automatically disabling the Central IP module.
594 * Logs the action when the feature is disabled due to expired membership.
595 *
596 * @return void
597 */
598 public function handle_expired_membership(): void {
599 if ( $this->is_expired_membership_type() && $this->model->enabled ) {
600 $this->model->enabled = false;
601 $this->model->save();
602 $this->log( 'Central IP automatically disabled due to expired membership.', 'central_ip.log' );
603 }
604 }
605 }
606