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

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

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