PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 5.4.0
Defender Security – Malware Scanner, Login Security & Firewall v5.4.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.4.0, at src/controller/class-blacklist.php

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