PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.7.2
Defender Security – Malware Scanner, Login Security & Firewall v5.7.2
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 / controller / class-blacklist.php

class-blacklist.php in Defender Security – Malware Scanner, Login Security & Firewall 5.7.2, at src/controller/class-blacklist.php

721 lines 19.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Handles IP and country blacklisting functionalities.
4 *
5 * @package WP_Defender\Controller
6 */
7
8 namespace WP_Defender\Controller;
9
10 use PharData;
11 use Exception;
12 use Countable;
13 use WP_Defender\Traits\IP;
14 use WP_Defender\Controller;
15 use Calotes\Component\Request;
16 use Calotes\Component\Response;
17 use WP_Defender\Traits\Country;
18 use WP_Defender\Behavior\WPMUDEV;
19 use WP_Defender\Model\Lockout_Ip;
20 use WP_Defender\Traits\Continent;
21 use WP_Defender\Component\Blacklist_Lockout;
22 use MaxMind\Db\Reader\InvalidDatabaseException;
23 use WP_Defender\Component\Network_Cron_Manager;
24 use WP_Defender\Integrations\MaxMind_Geolocation;
25 use WP_Defender\Component\Config\Config_Hub_Helper;
26 use WP_Defender\Model\Setting\Blacklist_Lockout as Model_Blacklist_Lockout;
27
28 /**
29 * Handles IP and country blacklisting functionalities.
30 */
31 class Blacklist extends Controller {
32
33 use IP;
34 use Country;
35 use Continent;
36
37 /**
38 * The slug identifier for this controller.
39 *
40 * @var string
41 */
42 protected $slug = 'wdf-ip-lockout';
43
44 /**
45 * The model for handling the data.
46 *
47 * @var Model_Blacklist_Lockout
48 */
49 protected $model;
50
51 /**
52 * Service for handling logic.
53 *
54 * @var Blacklist_Lockout
55 */
56 protected $service;
57
58 /**
59 * Initializes the model and service, registers routes, and sets up scheduled events if the model is active.
60 */
61 public function __construct() {
62 $this->register_routes();
63 add_action( 'defender_enqueue_assets', array( $this, 'enqueue_assets' ) );
64 $this->model = wd_di()->get( Model_Blacklist_Lockout::class );
65 $this->service = wd_di()->get( Blacklist_Lockout::class );
66 add_action( 'wd_blacklist_this_ip', array( $this, 'blacklist_an_ip' ) );
67 // Update MaxMind's DB.
68 if ( ! empty( $this->model->maxmind_license_key ) ) {
69 // @since 2.8.0 Allows update or remove the database of MaxMind automatic and periodically (MaxMind's TOS).
70 $bind_updater = (bool) apply_filters( 'wd_update_maxmind_database', true );
71
72 if ( $bind_updater ) {
73 /**
74 * Network Cron Manager
75 *
76 * @var Network_Cron_Manager $network_cron_manager
77 */
78 $network_cron_manager = wd_di()->get( Network_Cron_Manager::class );
79 $network_cron_manager->register_callback(
80 'wpdef_update_geoip',
81 array( $this, 'update_database' ),
82 WEEK_IN_SECONDS,
83 'next Thursday'
84 );
85 }
86 }
87 }
88
89 /**
90 * Add an IP into blacklist.
91 *
92 * @param string $ip IP address to be blacklisted.
93 *
94 * @return void
95 */
96 public function blacklist_an_ip( string $ip ): void {
97 $this->model->add_to_list( $ip, 'blocklist' );
98 if ( defender_is_wp_org_version() ) {
99 \WP_Defender\Component\Rate::run_counter_of_ip_lockouts();
100 }
101 }
102
103 /**
104 * Enqueues scripts and styles for this page.
105 * Only enqueues assets if the page is active.
106 *
107 * @throws InvalidDatabaseException|Exception When unexpected data is found in the database.
108 */
109 public function enqueue_assets() {
110 if ( ! $this->is_page_active() ) {
111 return;
112 }
113 wp_localize_script( 'def-iplockout', 'blacklist', $this->data_frontend() );
114 }
115
116 /**
117 * Provides data for the frontend.
118 *
119 * @return array An array of data for the frontend.
120 * @throws InvalidDatabaseException|Exception When unexpected data is found in the database.
121 */
122 public function data_frontend(): array {
123 $user_ip = $this->get_user_ip();
124 $arr_model = $this->model->export();
125 $exist_geodb = $this->service->is_geodb_downloaded();
126 // If MaxMind GeoIP DB is downloaded then display the required data.
127 if ( $exist_geodb ) {
128 $country_list = $this->countries_list();
129 $blacklist_countries = array_merge(
130 array( 'all' => esc_html__( 'Block all', 'defender-security' ) ),
131 $country_list
132 );
133 $whitelist_countries = array_merge(
134 array( 'all' => esc_html__( 'Allow all', 'defender-security' ) ),
135 $country_list
136 );
137 $countries_with_continents_list = $this->get_countries_with_continents();
138 } else {
139 $blacklist_countries = array();
140 $whitelist_countries = array();
141 $countries_with_continents_list = array();
142 }
143
144 $current_country = array();
145 foreach ( $user_ip as $ip ) {
146 $current_country[] = $this->get_current_country( $ip );
147 }
148
149 $misc = array(
150 'user_ip' => implode( ',', $user_ip ),
151 'is_geodb_downloaded' => $exist_geodb,
152 'blacklist_countries' => $blacklist_countries,
153 'whitelist_countries' => $whitelist_countries,
154 'current_country' => $current_country,
155 'no_ips' => '' === $arr_model['ip_blacklist'] && '' === $arr_model['ip_whitelist'],
156 'countries_with_continents_list' => $countries_with_continents_list,
157 'geodb_license_key' => $this->mask_license_key( $this->model->maxmind_license_key ),
158 'module_name' => Model_Blacklist_Lockout::get_module_name(),
159 );
160
161 return array_merge(
162 array(
163 'model' => $arr_model,
164 'misc' => $misc,
165 ),
166 $this->dump_routes_and_nonces()
167 );
168 }
169
170 /**
171 * Masks a license key with asterisks.
172 *
173 * @param mixed $maxmind_license_key The license key to be masked.
174 *
175 * @return string The masked license key.
176 */
177 private function mask_license_key( $maxmind_license_key ): string {
178 if ( ! is_string( $maxmind_license_key ) || empty( $maxmind_license_key ) ) {
179 return $maxmind_license_key;
180 }
181 // Get the length of the license key.
182 $key_length = strlen( $maxmind_license_key );
183 // Decide how many characters to reveal. Revealing at least 4 characters or 25% of the key, whichever is greater.
184 $reveal_chars = max( 4, intval( $key_length / 5 ) );
185 // Calculate the number of asterisks to replace the hidden characters.
186 $num_asterisks = $key_length - $reveal_chars;
187 // Generate masked key.
188 return substr( $maxmind_license_key, 0, $reveal_chars ) . str_repeat( '*', $num_asterisks );
189 }
190
191 /**
192 * Save settings.
193 *
194 * @param Request $request The request object containing new settings data.
195 *
196 * @return Response
197 * @defender_route
198 */
199 public function save_settings( Request $request ) {
200 $curr_blacklist = $this->model->get_list( 'blocklist' );
201 $curr_allowlist = $this->model->get_list( 'allowlist' );
202 $data = $request->get_data(
203 array(
204 'country_blacklist' => array(
205 'type' => 'array',
206 ),
207 'country_whitelist' => array(
208 'type' => 'array',
209 ),
210 'ip_blacklist' => array(
211 'type' => 'string',
212 'sanitize' => 'sanitize_textarea_field',
213 ),
214 'ip_whitelist' => array(
215 'type' => 'string',
216 'sanitize' => 'sanitize_textarea_field',
217 ),
218 'ip_lockout_message' => array(
219 'type' => 'string',
220 'sanitize' => 'sanitize_textarea_field',
221 ),
222 'http_ip_header' => array(
223 'type' => 'string',
224 'sanitize' => 'sanitize_text_field',
225 ),
226 'trusted_proxies_ip' => array(
227 'type' => 'string',
228 'sanitize' => 'sanitize_textarea_field',
229 ),
230 )
231 );
232 $this->model->import( $data );
233 if ( $this->model->validate() ) {
234 $this->model->save();
235 Config_Hub_Helper::set_clear_active_flag();
236
237 return new Response(
238 true,
239 array_merge(
240 array(
241 'message' => esc_html__( 'Your settings have been updated.', 'defender-security' ),
242 'auto_close' => true,
243 ),
244 $this->data_frontend()
245 )
246 );
247 }
248
249 $after_validate_blacklist = $this->model->get_list( 'blocklist' );
250 $after_validate_allowlist = $this->model->get_list( 'allowlist' );
251 if (
252 ! defender_are_arrays_equal( $curr_blacklist, $after_validate_blacklist ) ||
253 ! defender_are_arrays_equal( $curr_allowlist, $after_validate_allowlist )
254 ) {
255 $this->model->save();
256 Config_Hub_Helper::set_clear_active_flag();
257 }
258
259 $this->model->import( $data );
260
261 return new Response(
262 false,
263 array_merge(
264 array( 'message' => $this->model->get_formatted_errors() ),
265 $this->data_frontend()
266 )
267 );
268 }
269
270 /**
271 * Download the GEODB IP from Maxmind.
272 *
273 * @param Request $request The request object containing the license key.
274 *
275 * @return Response
276 * @defender_route
277 * @throws InvalidDatabaseException|Exception When unexpected data is found in the database.
278 */
279 public function download_geodb( Request $request ) {
280 $data = $request->get_data(
281 array(
282 'license_key' => array(
283 'type' => 'string',
284 'sanitize' => 'sanitize_text_field',
285 ),
286 )
287 );
288 $license_key = $data['license_key'];
289 $service_geo = wd_di()->get( MaxMind_Geolocation::class );
290 $tmp = $service_geo->get_downloaded_url( $license_key );
291 if ( ! is_wp_error( $tmp ) ) {
292 $phar = new PharData( $tmp );
293 $path = $service_geo->get_db_base_path();
294 if ( ! is_dir( $path ) ) {
295 wp_mkdir_p( $path );
296 }
297 $phar->extractTo( $path, null, true );
298 // Todo: move logic for the path to MaxMind_Geolocation class.
299 $this->model->geodb_path = $path . DIRECTORY_SEPARATOR . $phar->current()->getFileName() . DIRECTORY_SEPARATOR . $service_geo->get_db_full_name();
300 // Save because we'll check for a saved path.
301 $this->model->save();
302
303 if ( file_exists( $tmp ) ) {
304 wp_delete_file( $tmp );
305 }
306
307 foreach ( $this->get_user_ip() as $ip ) {
308 $country = $this->get_current_country( $ip );
309 if ( ! empty( $country ) && ! empty( $country['iso'] ) ) {
310 $this->model = $this->service->add_default_whitelisted_country( $this->model, $country['iso'] );
311 }
312 }
313 $this->model->maxmind_license_key = $license_key;
314 $this->model->save();
315
316 return new Response(
317 true,
318 array(
319 'message' => esc_html__(
320 'You have successfully downloaded Geo IP Database. You can now use this feature to ban any countries to access any area of your website.',
321 'defender-security'
322 ),
323 'is_geodb_downloaded' => $this->service->is_geodb_downloaded(),
324 )
325 );
326 } else {
327 $this->log( 'Error from MaxMind: ' . $tmp->get_error_message(), Firewall::FIREWALL_LOG );
328 $string = sprintf(
329 /* translators: 1. License key with link. */
330 esc_html__(
331 'You have entered an invalid %1$s. If you just created the key, please wait 5 minutes before trying to activate it.',
332 'defender-security'
333 ),
334 '<a target="_blank" href="https://www.maxmind.com/en/accounts/current/license-key">' . esc_html__( 'license key', 'defender-security' ) . '</a>'
335 );
336
337 if ( ( new WPMUDEV() )->show_support_links() ) {
338 $string .= defender_support_ticket_text();
339 }
340
341 return new Response( false, array( 'invalid_text' => $string ) );
342 }
343 }
344
345 /**
346 * Delete the Maxmind License key from the settings.
347 *
348 * @return Response
349 * @defender_route
350 * @throws InvalidDatabaseException When unexpected data is found in the database.
351 */
352 public function delete_geodb(): Response {
353 $this->model->maxmind_license_key = '';
354 $this->model->geodb_path = '';
355 $this->model->save();
356
357 return new Response(
358 true,
359 array(
360 'message' => esc_html__(
361 'Maxmind GeoLite2 database license successfully disconnected.',
362 'defender-security'
363 ),
364 'is_geodb_deleted' => $this->service->is_geodb_downloaded(),
365 )
366 );
367 }
368
369 /**
370 * Export IPs
371 * This method exports the IP addresses from the blocklist and allowlist
372 * and generates a CSV file for download.
373 *
374 * @defender_route
375 */
376 public function export_ips(): void {
377 $data = array();
378
379 foreach ( $this->model->get_list( 'blocklist' ) as $ip ) {
380 $data[] = array(
381 'ip' => $ip,
382 'type' => 'blocklist',
383 );
384 }
385 foreach ( $this->model->get_list( 'allowlist' ) as $ip ) {
386 $data[] = array(
387 'ip' => $ip,
388 'type' => 'allowlist',
389 );
390 }
391 // WP_Filesystem class doesn’t directly provide a function for opening a stream to php://memory with the 'w' mode.
392 $fp = fopen( 'php://memory', 'w' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen
393 foreach ( $data as $fields ) {
394 fputcsv( $fp, $fields, ',', '"', '\\' );
395 }
396 $filename = 'wdf-ips-export-' . wp_date( 'ymdHis' ) . '.csv';
397 fseek( $fp, 0 );
398 header( 'Content-Type: text/csv' );
399 header( 'Content-Disposition: attachment; filename="' . $filename . '";' );
400 // Make php send the generated csv lines to the browser.
401 fpassthru( $fp );
402 exit();
403 }
404
405 /**
406 * Perform IP blocking or unblocking actions.
407 *
408 * @param Request $request Request object.
409 *
410 * @return void
411 * @throws Exception If table is not defined.
412 * @defender_route
413 */
414 public function ip_action( Request $request ): void {
415 $data = $request->get_data(
416 array(
417 'ip' => array(
418 'type' => 'string',
419 'sanitize' => 'sanitize_text_field',
420 ),
421 'behavior' => array(
422 'type' => 'string',
423 'sanitize' => 'sanitize_text_field',
424 ),
425 )
426 );
427
428 $ip = $data['ip'];
429 $action = $data['behavior'];
430 $models = Lockout_Ip::get( $ip, $action, true );
431
432 foreach ( $models as $model ) {
433 if ( 'unban' === $action ) {
434 $model->status = Lockout_Ip::STATUS_NORMAL;
435 $model->save();
436 } elseif ( 'ban' === $action ) {
437 $model->status = Lockout_Ip::STATUS_BLOCKED;
438 $model->save();
439 }
440 }
441
442 $this->query_locked_ips( $request );
443 }
444
445 /**
446 * Bulk ban or unban IPs.
447 *
448 * @param Request $request Request object.
449 *
450 * @return Response
451 * @throws Exception If table is not defined.
452 * @defender_route
453 */
454 public function bulk_ip_action( Request $request ) {
455 $data = $request->get_data(
456 array(
457 'behavior' => array(
458 'type' => 'string',
459 'sanitize' => 'sanitize_text_field',
460 ),
461 'ips' => array(
462 'type' => 'string',
463 'sanitize' => 'sanitize_text_field',
464 ),
465 )
466 );
467
468 $status = 'unban' === $data['behavior'] ? Lockout_Ip::STATUS_BLOCKED : Lockout_Ip::STATUS_NORMAL;
469 $ips = null;
470 $bulk_ips = null;
471 $limit = 50;
472
473 if ( ! empty( $data['ips'] ) ) {
474 $ips = json_decode( $data['ips'] );
475 $first_nth_ips = array_slice( $ips, 0, $limit );
476 $bulk_ips = wp_list_pluck( $first_nth_ips, 'ip' );
477 }
478
479 try {
480 $models = Lockout_Ip::get_bulk( $status, $bulk_ips, $limit );
481 foreach ( $models as $model ) {
482 $model->status = ( 'unban' === $data['behavior'] ) ? Lockout_Ip::STATUS_NORMAL : Lockout_Ip::STATUS_BLOCKED;
483 $model->save();
484 }
485 // While bulk banning the IPs, needs to slice the IPs array for next iteration.
486 if ( 'ban' === $data['behavior'] ) {
487 $ips = array_slice( $ips, $limit );
488 }
489 // If the queried models are less than the limit it means we are on the last set of IPs.
490 if ( ( is_array( $models ) || $models instanceof Countable ? count( $models ) : 0 ) < $limit ) {
491 return new Response(
492 true,
493 array(
494 'status' => 'done',
495 )
496 );
497 }
498 } catch ( Exception $e ) {
499 return new Response(
500 true,
501 array(
502 'status' => 'error',
503 )
504 );
505 }
506
507 return new Response(
508 true,
509 array(
510 'status' => 'continue',
511 'ips' => $ips,
512 )
513 );
514 }
515
516 /**
517 * Query locked IPs and return the results as a Response object.
518 *
519 * @return Response
520 * @defender_route
521 */
522 public function query_locked_ips() {
523 $results = Lockout_Ip::query_locked_ip();
524 $locked_ips = array();
525 if ( ! empty( $results ) ) {
526 foreach ( $results as $key => $locked_ip ) {
527 $locked_ips[] = array(
528 'id' => $locked_ip['id'],
529 'ip' => $locked_ip['ip'],
530 'status' => $locked_ip['status'],
531 );
532 }
533 }
534
535 return new Response(
536 true,
537 array(
538 'ips' => $locked_ips,
539 )
540 );
541 }
542
543 /**
544 * Get Listed IPs.
545 *
546 * @return Response
547 * @defender_route
548 * @throws Exception If table is not defined.
549 */
550 public function get_listed_ips(): Response {
551 return new Response( true, $this->model->export() );
552 }
553
554 /**
555 * Converts the current state of the object to an array.
556 *
557 * @return array Returns an associative array of object properties.
558 */
559 public function to_array(): array {
560 return array();
561 }
562
563 /**
564 * Adapt the given data array by adding additional fields if necessary.
565 *
566 * @param array $data The data array to adapt.
567 *
568 * @return array The adapted data array.
569 */
570 private function adapt_data( array $data ): array {
571 $adapted_data = array(
572 'ip_blacklist' => $data['ip_blacklist'],
573 'ip_whitelist' => $data['ip_whitelist'],
574 'ip_lockout_message' => $data['ip_lockout_message'],
575 );
576 if ( isset( $data['geoIP_db'] ) && file_exists( $data['geoIP_db'] ) ) {
577 $adapted_data['geodb_path'] = $data['geoIP_db'];
578 if ( isset( $data['country_blacklist'] ) ) {
579 $adapted_data['country_blacklist'] = $data['country_blacklist'];
580 }
581 if ( isset( $data['country_whitelist'] ) ) {
582 $adapted_data['country_whitelist'] = $data['country_whitelist'];
583 }
584 }
585
586 return array_merge( $data, $adapted_data );
587 }
588
589 /**
590 * Imports data into the model.
591 *
592 * @param array $data Data to be imported into the model.
593 *
594 * @throws Exception If table is not defined.
595 */
596 public function import_data( array $data ) {
597 if ( ! empty( $data ) ) {
598 // Upgrade for old versions.
599 $data = $this->adapt_data( $data );
600 $model = $this->model;
601 $model->import( $data );
602 if ( $model->validate() ) {
603 $model->save();
604 }
605 }
606 }
607
608 /**
609 * Removes settings for all submodules.
610 */
611 public function remove_settings() {
612 }
613
614 /**
615 * Delete all the data & the cache.
616 */
617 public function remove_data() {
618 }
619
620 /**
621 * Exports strings.
622 *
623 * @return array An array of strings.
624 */
625 public function export_strings() {
626 return array();
627 }
628
629 /**
630 * Importing IPs from exporter.
631 *
632 * @param Request $request The request object containing the data.
633 *
634 * @defender_route
635 * @return Response
636 */
637 public function import_ips( Request $request ) {
638 $data = $request->get_data(
639 array(
640 'id' => array(
641 'type' => 'int',
642 ),
643 )
644 );
645 $attached_id = $data['id'];
646 if ( ! is_object( get_post( $attached_id ) ) ) {
647 return new Response(
648 false,
649 array(
650 'message' => esc_html__( 'Your file is invalid!', 'defender-security' ),
651 )
652 );
653 }
654
655 $file = get_attached_file( $attached_id );
656 if ( ! is_file( $file ) ) {
657 return new Response(
658 false,
659 array(
660 'message' => esc_html__( 'Your file is invalid!', 'defender-security' ),
661 )
662 );
663 }
664
665 $data = $this->service->verify_import_file( $file );
666 if ( ! $data ) {
667 return new Response(
668 false,
669 array(
670 'message' => esc_html__( 'Your file content is invalid!', 'defender-security' ),
671 )
672 );
673 }
674
675 // All good, start to import.
676 foreach ( $data as $line ) {
677 $this->model->add_to_list( $line[0], $line[1] );
678 }
679
680 return new Response(
681 true,
682 array(
683 'message' => esc_html__( 'Your allowlist/blocklist has been successfully imported.', 'defender-security' ),
684 'interval' => 1,
685 )
686 );
687 }
688
689 /**
690 * Update the geolocation database.
691 *
692 * @return void
693 * @throws Exception If table is not defined.
694 * @since 2.8.0
695 */
696 public function update_database() {
697 if ( empty( $this->model->maxmind_license_key ) ) {
698 return;
699 }
700
701 $service_geo = wd_di()->get( MaxMind_Geolocation::class );
702 $service_geo->delete_database();
703
704 $tmp = $service_geo->get_downloaded_url( $this->model->maxmind_license_key );
705 if ( is_wp_error( $tmp ) ) {
706 $this->log( 'CRON error downloading from MaxMind: ' . $tmp->get_error_message(), Firewall::FIREWALL_LOG );
707
708 return;
709 }
710
711 $geodb_path = $service_geo->extract_db_archive( $tmp );
712 if ( is_wp_error( $geodb_path ) ) {
713 $this->log( 'CRON error extracting MaxMind archive: ' . $geodb_path->get_error_message(), Firewall::FIREWALL_LOG );
714
715 return;
716 }
717 $this->model->geodb_path = $geodb_path;
718 $this->model->save();
719 }
720 }
721