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 / functions.php

functions.php in Defender Security – Malware Scanner, Login Security & Firewall 5.9.0, at src/functions.php

834 lines 22.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * This file contains all functions used in the plugin.
4 *
5 * @package WP_Defender
6 */
7
8 use WP_Defender\Central;
9 use WP_Defender\Component\Crypt;
10 use WP_Defender\Component\Two_Fa;
11 use WP_Defender\Behavior\WPMUDEV;
12 use WP_Defender\Component\User_Agent;
13 use WP_Defender\Controller\Two_Factor;
14 use WP_Defender\Model\Setting\Main_Setting;
15 use WP_Defender\Helper\Request;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 die;
19 }
20
21 /**
22 * Generates the URL for a given asset path in the Defender plugin.
23 *
24 * @param string $path The path of the asset.
25 * @param bool $print_url Whether to print the URL or return it.
26 *
27 * @return string|void The URL of the asset.
28 */
29 function defender_asset_url( string $path, bool $print_url = false ) {
30 $url = untrailingslashit( WP_DEFENDER_BASE_URL ) . $path;
31
32 if ( ! $print_url ) {
33 return $url;
34 }
35
36 echo esc_url_raw( $url );
37 }
38
39 /**
40 * Generates the absolute path for a given path relative to the Defender plugin directory.
41 *
42 * @param string $path The relative path within the Defender plugin directory.
43 *
44 * @return string The absolute path.
45 */
46 function defender_path( string $path ): string {
47 $base_path = plugin_dir_path( __DIR__ );
48
49 return $base_path . $path;
50 }
51
52 /**
53 * Sanitize submitted data.
54 *
55 * @param array $data The data to sanitize.
56 *
57 * @return array
58 */
59 function defender_sanitize_data( $data ) {
60 foreach ( $data as $key => &$value ) {
61 if ( is_array( $value ) ) {
62 $value = defender_sanitize_data( $value );
63 } else {
64 $value = sanitize_textarea_field( $value );
65 }
66 }
67
68 return $data;
69 }
70
71 /**
72 * Retrieve wp-config.php file path.
73 *
74 * @return string
75 */
76 function defender_wp_config_path(): string {
77 if ( file_exists( ABSPATH . 'wp-config.php' ) ) {
78 return ABSPATH . 'wp-config.php';
79 }
80
81 if (
82 file_exists( dirname( ABSPATH ) . '/wp-config.php' )
83 && ! file_exists( dirname( ABSPATH ) . '/wp-settings.php' )
84 ) {
85 return dirname( ABSPATH ) . '/wp-config.php';
86 }
87
88 return ( defined( 'WD_TEST' ) && WD_TEST ) ? '/tmp/wordpress-tests-lib/wp-tests-config.php' : '';
89 }
90
91 /**
92 * Check whether we're on Windows platform or not.
93 *
94 * @return bool
95 */
96 function defender_is_windows(): bool {
97 return '\\' === DIRECTORY_SEPARATOR;
98 }
99
100 /**
101 * Returns the global DI container for the WP Defender plugin.
102 *
103 * @return \WPMU_DEV\Defender\Vendor\DI\Container The global DI container.
104 */
105 function wd_di() {
106 global $wp_defender_di;
107
108 return $wp_defender_di;
109 }
110
111 /**
112 * Returns the global Central object for the WP Defender plugin.
113 *
114 * @return Central
115 */
116 function wd_central() {
117 global $wp_defender_central;
118
119 return $wp_defender_central;
120 }
121
122 /**
123 * Get base action.
124 *
125 * @return string
126 * @since 2.8.0
127 */
128 function defender_base_action(): string {
129 return 'wp_defender/v1/hub/';
130 }
131
132 /**
133 * Get backward compatibility. Forminator uses this method.
134 *
135 * @return array
136 */
137 function defender_backward_compatibility() {
138 $wpmu_dev = new WPMUDEV();
139 $two_fa_settings = new \WP_Defender\Model\Setting\Two_Fa();
140 $controller = wd_di()->get( Two_Factor::class );
141 $collection = $controller->dump_routes_and_nonces();
142 $lost_url = add_query_arg(
143 array(
144 'action' => defender_base_action(),
145 '_def_nonce' => $collection['nonces']['send_backup_code'],
146 // Add a dummy values to avoid displaying errors, e.g. for the case with null.
147 'route' => $controller->check_route( $collection['routes']['send_backup_code'] ?? 'test' ),
148 ),
149 admin_url( 'admin-ajax.php' )
150 );
151
152 return array(
153 'is_free' => ! $wpmu_dev->is_pro(),
154 'plugin_url' => defender_asset_url( '' ),
155 'two_fa_settings' => $two_fa_settings,
156 'two_fa_component' => Two_Fa::class,
157 'lost_url' => $lost_url,
158 );
159 }
160
161 /**
162 * Polyfill functions for supporting WordPress 5.3.
163 *
164 * @since 2.4.2
165 */
166 if ( ! function_exists( 'wp_timezone_string' ) ) {
167 /**
168 * Retrieves the timezone from site settings as a string.
169 * Uses the `timezone_string` option to get a proper timezone if available, otherwise falls back to an offset.
170 *
171 * @return string PHP timezone string or a ±HH:MM offset.
172 * @since 5.3.0
173 */
174 function wp_timezone_string() {
175 $timezone_string = get_option( 'timezone_string' );
176
177 if ( '' !== $timezone_string ) {
178 return $timezone_string;
179 }
180
181 $offset = (float) get_option( 'gmt_offset' );
182 $hours = (int) $offset;
183 $minutes = ( $offset - $hours );
184
185 $sign = ( $offset < 0 ) ? '-' : '+';
186 $abs_hour = abs( $hours );
187 $abs_mins = abs( $minutes * 60 );
188 $tz_offset = sprintf( '%s%02d:%02d', $sign, $abs_hour, $abs_mins );
189
190 return $tz_offset;
191 }
192 }
193
194 if ( ! function_exists( 'wp_timezone' ) ) {
195 /**
196 * Retrieves the timezone from site settings as a `DateTimeZone` object.
197 * Timezone can be based on a PHP timezone string or a ±HH:MM offset.
198 *
199 * @return DateTimeZone Timezone object.
200 * @since 5.3.0
201 */
202 function wp_timezone() {
203 return new DateTimeZone( wp_timezone_string() );
204 }
205 }
206
207 /**
208 * Get hostname.
209 *
210 * @return string
211 */
212 function defender_get_hostname(): string {
213 $host = wp_parse_url( get_site_url(), PHP_URL_HOST );
214 $host = str_replace( 'www.', '', $host );
215 $host = explode( '.', $host );
216 $host = array_shift( $host );
217
218 return $host;
219 }
220
221 if ( ! function_exists( 'sanitize_mask_url' ) ) {
222 /**
223 * Sanitizes the mask login URL allowing uppercase letters,
224 * Replacing whitespace and a few other characters with dashes and
225 * Limits the output to alphanumeric characters, underscore (_) and dash (-).
226 * Whitespace becomes a dash.
227 *
228 * @param string $title The title to be sanitized.
229 *
230 * @return string The sanitized title.
231 */
232 function sanitize_mask_url( $title ) {
233 $title = wp_strip_all_tags( $title );
234 // Preserve escaped octets.
235 $title = preg_replace( '|%([a-fA-F0-9][a-fA-F0-9])|', '---$1---', $title );
236 // Remove percent signs that are not part of an octet.
237 $title = str_replace( '%', '', $title );
238 // Restore octets.
239 $title = preg_replace( '|---([a-fA-F0-9][a-fA-F0-9])---|', '%$1', $title );
240 // Support for WordPress 6.9+ and older versions.
241 if ( function_exists( 'wp_is_valid_utf8' ) ) {
242 // New function since WP 6.9.0.
243 if ( wp_is_valid_utf8( $title ) ) {
244 $title = utf8_uri_encode( $title, 200 );
245 }
246 } elseif ( function_exists( 'seems_utf8' ) ) {
247 // Old method (before WP 6.9.0).
248 if ( seems_utf8( $title ) ) { // phpcs:ignore WordPress.WP.DeprecatedFunctions.seems_utf8Found
249 $title = utf8_uri_encode( $title, 200 );
250 }
251 }
252
253 // Kill entities.
254 $title = preg_replace( '/&.+?;/', '', $title );
255 $title = str_replace( '.', '-', $title );
256 $title = preg_replace( '/[^%a-zA-Z0-9 _-]/', '', $title );
257 $title = preg_replace( '/\s+/', '-', $title );
258
259 return preg_replace( '|-+|', '-', $title );
260 }
261 }
262
263 /**
264 * Return the noreply email.
265 * A utility function which will return common noreply from address.
266 *
267 * @param string $filter_tag Tag name of the filter to override email address.
268 *
269 * @return string Noreply email.
270 */
271 function defender_noreply_email( string $filter_tag = '' ) {
272 $host = wp_parse_url( get_site_url(), PHP_URL_HOST );
273
274 if ( 'www.' === substr( $host, 0, 4 ) ) {
275 $host = substr( $host, 4 );
276 }
277
278 $no_reply_email = 'noreply@' . $host;
279
280 if ( strlen( $filter_tag ) > 0 ) {
281 $no_reply_email = apply_filters( $filter_tag, $no_reply_email );
282 }
283
284 return $no_reply_email;
285 }
286
287 /**
288 * Get data of the whitelabel feature from WPMUDEV Dashboard:
289 * hide_branding, hide_doc_link, footer_text, hero_image, change_footer.
290 *
291 * @return array
292 * @since 2.5.5
293 */
294 function defender_white_label_status() {
295 /* translators: %s: heart icon */
296 $footer_text = sprintf( esc_html__( 'Made with %s by WPMU DEV', 'defender-security' ), '<i class="sui-icon-heart"></i>' );
297 $custom_image = apply_filters( 'wpmudev_branding_hero_image', '' );
298 $custom_image = is_string( $custom_image ) ? $custom_image : (string) $custom_image;
299 $custom_image = trim( $custom_image );
300 $whitelabled = apply_filters( 'wpmudev_branding_hide_branding', false );
301 $whitelabled = is_bool( $whitelabled ) ? $whitelabled : (bool) $whitelabled;
302
303 return array(
304 'hide_branding' => apply_filters( 'wpmudev_branding_hide_branding', false ),
305 'hide_doc_link' => apply_filters( 'wpmudev_branding_hide_doc_link', false ),
306 'footer_text' => apply_filters( 'wpmudev_branding_footer_text', $footer_text ),
307 'hero_image' => $custom_image,
308 'change_footer' => apply_filters( 'wpmudev_branding_change_footer', false ),
309 'is_unbranded' => '' === $custom_image && $whitelabled,
310 'is_rebranded' => '' !== $custom_image && $whitelabled,
311 );
312 }
313
314 /**
315 * Indicate this is not fresh setup.
316 *
317 * @since 2.5.5
318 */
319 function defender_no_fresh_install() {
320 if ( false === get_site_option( 'wd_nofresh_install', false ) ) {
321 update_site_option( 'wd_nofresh_install', true );
322 }
323 }
324
325 /**
326 * Polyfill for PHP version < 7.3.
327 */
328 if ( ! function_exists( 'array_key_first' ) ) {
329 /**
330 * Returns the first key of an array.
331 *
332 * @param array $arr The input array.
333 *
334 * @return mixed|null The first key of the array, or null if the array is empty.
335 */
336 function array_key_first( array $arr ) {
337 $arr_keys = array_keys( $arr );
338
339 return $arr_keys[0] ?? null;
340 }
341 }
342
343 /**
344 * Fetch request url.
345 *
346 * @return string
347 */
348 function defender_get_request_url(): string {
349 return home_url( esc_url( filter_input( INPUT_SERVER, 'REQUEST_URI' ) ) );
350 }
351
352 /**
353 * What is the current WP page?
354 *
355 * @return string
356 */
357 function defender_get_current_page(): string {
358 return defender_get_data_from_request( 'page', 'g' );
359 }
360
361 /**
362 * Check that current page is from Defender.
363 *
364 * @return bool
365 */
366 function is_defender_page(): bool {
367 $pages = array(
368 'wp-defender',
369 'wdf-hardener',
370 'wdf-scan',
371 'wdf-logging',
372 'wdf-ip-lockout',
373 'wdf-waf',
374 'wdf-2fa',
375 'wdf-advanced-tools',
376 'wdf-notification',
377 'wdf-setting',
378 'wdf-expert-services',
379 );
380
381 return in_array( defender_get_current_page(), $pages, true );
382 }
383
384 /**
385 * Return the high contrast css class if it is.
386 *
387 * @return bool
388 * @since 2.7.0
389 */
390 function defender_high_contrast() {
391 $model = new Main_Setting();
392
393 return $model->high_contrast_mode;
394 }
395
396 /**
397 * Add more cron schedules for plugin modules. E.g. schedules:
398 * cleaning completed Scan logs,
399 * cleaning temporary firewall IPs,
400 * send reports,
401 * update MaxMind DB.
402 *
403 * @param array $schedules The schedules.
404 *
405 * @return array
406 * @since 2.7.1
407 */
408 function defender_cron_schedules( $schedules ) {
409 if ( ! isset( $schedules['thirty_minutes'] ) ) {
410 $schedules['thirty_minutes'] = array(
411 'interval' => 30 * MINUTE_IN_SECONDS,
412 'display' => esc_html__( 'Every Half Hour', 'defender-security' ),
413 );
414 }
415 if ( ! isset( $schedules['weekly'] ) ) {
416 $schedules['weekly'] = array(
417 'interval' => WEEK_IN_SECONDS,
418 'display' => esc_html__( 'Weekly', 'defender-security' ),
419 );
420 }
421 if ( ! isset( $schedules['monthly'] ) ) {
422 $schedules['monthly'] = array(
423 'interval' => MONTH_IN_SECONDS,
424 'display' => esc_html__( 'Once Monthly', 'defender-security' ),
425 );
426 }
427 // Todo: find the right solution because 'monthly' (from Firewall)='thirty_days' (from Security_Key tweak).
428 // For regeneration of security keys/salts. Schedules: 30, 60, 90 days, 6 months and 1 year.
429 if ( ! isset( $schedules['thirty_days'] ) ) {
430 $schedules['thirty_days'] = array(
431 'interval' => 2592000,
432 'display' => esc_html__( '30 days', 'defender-security' ),
433 );
434 }
435 if ( ! isset( $schedules['sixty_days'] ) ) {
436 $schedules['sixty_days'] = array(
437 'interval' => 5184000,
438 'display' => esc_html__( '60 days', 'defender-security' ),
439 );
440 }
441 if ( ! isset( $schedules['ninety_days'] ) ) {
442 $schedules['ninety_days'] = array(
443 'interval' => 7776000,
444 'display' => esc_html__( '90 days', 'defender-security' ),
445 );
446 }
447 if ( ! isset( $schedules['six_months'] ) ) {
448 $schedules['six_months'] = array(
449 'interval' => 15780000,
450 'display' => esc_html__( '6 months', 'defender-security' ),
451 );
452 }
453 if ( ! isset( $schedules['one_year'] ) ) {
454 $schedules['one_year'] = array(
455 'interval' => 31536000,
456 'display' => esc_html__( '1 year', 'defender-security' ),
457 );
458 }
459
460 return $schedules;
461 }
462
463 /**
464 * Generate random string.
465 *
466 * @param int $length Length of random string.
467 * @param string $strings Characters to include in a random string.
468 *
469 * @return string
470 * @since 3.0.0
471 */
472 function defender_generate_random_string( int $length = 16, string $strings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567' ) {
473 if ( defined( 'DEFENDER_2FA_SECRET' ) ) {
474 // Only use in test.
475 return constant( 'DEFENDER_2FA_SECRET' );
476 }
477
478 $secret = array();
479 for ( $i = 0; $i < $length; $i++ ) {
480 $secret[] = $strings[ Crypt::random_int( 0, strlen( $strings ) - 1 ) ];
481 }
482
483 return implode( '', $secret );
484 }
485
486 /**
487 * Either return array or echo json.
488 *
489 * @param mixed $data A Data to be returned or echoed.
490 * @param bool $success Is it a success or failure.
491 * @param bool $is_return True if data needs to be returned.
492 *
493 * @return array|void
494 * @since 3.0.0
495 */
496 function defender_maybe_echo_json( $data, $success, $is_return ) {
497 if ( true === $is_return ) {
498 return array(
499 'success' => $success,
500 'data' => $data,
501 );
502 } else {
503 $success ? wp_send_json_success( $data ) : wp_send_json_error( $data );
504 }
505 }
506
507 /**
508 * Get translations for the 'defender-security' text domain.
509 *
510 * @return array List of words/phrases and their translations.
511 * @global Mo[] $l10n An array of all currently loaded text domains.
512 */
513 function defender_gettext_translations(): array {
514 global $l10n;
515
516 // Check if the 'defender-security' text domain is loaded.
517 if ( ! isset( $l10n['defender-security'] ) ) {
518 return array();
519 }
520
521 $items = array();
522
523 /**
524 * Go through all the translation entries in the 'defender-security' text domain.
525 *
526 * @var Translation_Entry $value
527 */
528 foreach ( $l10n['defender-security']->entries as $value ) {
529 // If there's a translation, use it; otherwise, use the original word/phrase.
530 $items[ $value->key() ] = isset( $value->translations[0] ) ? $value->translations[0] : $value->key();
531 }
532
533 // Return the list of words/phrases and their translations.
534 return $items;
535 }
536
537 /**
538 * Get string replacement regardless of the operating system.
539 *
540 * @param string $path The path to be replaced.
541 *
542 * @return string
543 * @since 3.3.0
544 */
545 function defender_replace_line( $path ): string {
546 return str_replace( array( '/', '\\' ), DIRECTORY_SEPARATOR, $path );
547 }
548
549 /**
550 * Generates the support ticket text for the Defender plugin.
551 *
552 * @return string The support ticket text, including the formatted link.
553 */
554 function defender_support_ticket_text(): string {
555 return sprintf(
556 /* translators: 1. Support link. */
557 esc_html__( 'Still, having trouble? %1$s.', 'defender-security' ),
558 '<a target="_blank" href="' . WP_DEFENDER_SUPPORT_LINK . '">' . esc_html__( 'Open a support ticket', 'defender-security' ) . '</a>'
559 );
560 }
561
562 /**
563 * The message is shown on the inappropriate access of Safe Repair feature.
564 *
565 * @return string
566 */
567 function defender_quarantine_pro_only(): string {
568 return esc_html__( 'Safe Repair feature is only for Pro', 'defender-security' );
569 }
570
571 /**
572 * Retrieves the user agent from the $_SERVER super global or returns a default string.
573 *
574 * @param string $default_string The default string to return if the user agent is empty. Default is an empty string.
575 *
576 * @return string The cleaned user agent or the default string.
577 */
578 function defender_get_user_agent( $default_string = '' ) {
579 $user_agent = defender_get_data_from_request( 'HTTP_USER_AGENT', 's' );
580
581 return '' !== $user_agent ? User_Agent::fast_cleaning( $user_agent ) : $default_string;
582 }
583
584 /**
585 * Check if it is a CLI request.
586 *
587 * @return bool
588 * @since 4.2.0
589 */
590 function defender_is_wp_cli() {
591 return defined( 'WP_CLI' ) && WP_CLI;
592 }
593
594 /**
595 * Check if the current request is a REST API request.
596 * This function checks if the current request URI contains the REST API prefix,
597 * indicating that it's a request to the WordPress REST API.
598 *
599 * @return bool Whether the current request is a REST API request.
600 * @since 4.2.0
601 */
602 function defender_is_rest_api_request(): bool {
603 $request_uri = Request::get_request_uri();
604 if ( '' === $request_uri ) {
605 return false;
606 }
607
608 $rest_prefix = trailingslashit( rest_get_url_prefix() );
609
610 return false !== strpos( $request_uri, $rest_prefix );
611 }
612
613 /**
614 * Handle deprecated functions by logging or triggering actions.
615 * This function is a wrapper for WordPress's _deprecated_function() function.
616 * It is used to handle deprecated functions by either logging a deprecation
617 * message or triggering an action. It checks if the current request is an AJAX
618 * request or a REST API request and acts accordingly.
619 *
620 * @param string $function_name The function that was called.
621 * @param string $version The version number that deprecated the function.
622 * @param string $replacement (Optional) The function that should be used instead.
623 *
624 * @return void
625 * @since 4.2.0
626 */
627 function defender_deprecated_function( string $function_name, string $version, string $replacement = '' ): void {
628 /**
629 * Filters whether to trigger an error for deprecated functions.
630 *
631 * @param bool $trigger Whether to trigger the error for deprecated functions. Default false.
632 *
633 * @since 4.2.1
634 */
635 if ( WP_DEBUG && apply_filters( 'defender_deprecated_function_trigger_error', false ) ) {
636 if ( wp_doing_ajax() || defender_is_rest_api_request() ) {
637 do_action( 'deprecated_function_run', $function_name, $replacement, $version );
638
639 $log_string = "Function {$function_name} is deprecated since version {$version}!";
640 $log_string .= '' !== $replacement ? " Use {$replacement} instead." : '';
641 wp_die( esc_html( $log_string ) );
642 } else {
643 _deprecated_function( esc_html( $function_name ), esc_html( $version ), esc_html( $replacement ) );
644 }
645 }
646 }
647
648 if ( ! function_exists( 'defender_get_data_from_request' ) ) {
649 /**
650 * Retrieves the value of a specific server data key after sanitizing it.
651 *
652 * @param string|null $key The key of the data to retrieve from the request. If empty, the entire $_REQUEST or $_SERVER array will be returned.
653 * @param string $source The source of the data. Default is 'r' for $_REQUEST. Other options are 's' for $_SERVER, 'c' for $_COOKIE and 'f' for $_FILES.
654 * @param string $nonce_key The nonce key for verification.
655 * @param string $nonce_action The nonce action for verification.
656 *
657 * @return array|string|null The sanitized value of the server data key.
658 */
659 function defender_get_data_from_request(
660 ?string $key,
661 string $source,
662 string $nonce_key = '',
663 string $nonce_action = ''
664 ) {
665 if ( in_array( $source, array( 'r', 'p', 'g' ), true ) ) {
666 if (
667 '' !== $nonce_key && ! wp_verify_nonce(
668 sanitize_text_field( wp_unslash( $_REQUEST[ $nonce_key ] ?? '' ) ),
669 $nonce_action
670 )
671 ) {
672 return null;
673 }
674 }
675 switch ( $source ) {
676 case 'r':
677 $data = $_REQUEST;
678 break;
679 case 'p':
680 $data = $_POST;
681 break;
682 case 'g':
683 $data = $_GET;
684 break;
685 case 's':
686 $data = $_SERVER;
687 break;
688 case 'c':
689 $data = $_COOKIE;
690 break;
691 case 'f':
692 $data = $_FILES;
693 break;
694 default:
695 $data = array();
696 break;
697 }
698
699 if ( null === $key || '' === $key ) {
700 return $data;
701 } elseif ( 's' === $source ) {
702 return sanitize_text_field( wp_unslash( $data[ $key ] ?? '' ) );
703 } elseif ( 'data' === $key ) {
704 return json_decode( sanitize_text_field( wp_unslash( $data[ $key ] ?? '' ) ), true );
705 } elseif ( 'f' === $source && 'file' === $key && isset( $data[ $key ] ) ) {
706 if ( isset( $data[ $key ]['name'] ) && '' !== $data[ $key ]['name'] ) {
707 $data[ $key ]['name'] = sanitize_file_name( $data[ $key ]['name'] );
708 }
709
710 return $data[ $key ];
711 }
712
713 return sanitize_text_field( $data[ $key ] ?? '' );
714 }
715 }
716
717 /**
718 * Check if arrays are same.
719 *
720 * @param array $array1 The first array to compare.
721 * @param array $array2 The second array to compare.
722 *
723 * @return bool True if arrays are equal, false otherwise.
724 */
725 function defender_are_arrays_equal( $array1, $array2 ): bool {
726 if ( count( $array1 ) !== count( $array2 ) ) {
727 return false;
728 }
729
730 // Sort both arrays.
731 sort( $array1 );
732 sort( $array2 );
733
734 return $array1 === $array2;
735 }
736
737 /**
738 * Get a feature state on WPMU DEV hosting.
739 *
740 * @param string $feature_key The feature key, e.g. xmlrpc_block, globaliplist or waf.
741 *
742 * @return bool|string True or false if the feature is enabled or disabled, or an empty string if the feature is not found.
743 */
744 function defender_get_hosting_feature_state( string $feature_key ) {
745 if ( function_exists( 'wpmudev_hosting_features' ) ) {
746 $states = wpmudev_hosting_features();
747
748 return isset( $states[ $feature_key ] ) ? $states[ $feature_key ] : '';
749 }
750
751 return '';
752 }
753
754 /**
755 * Get an internal log file name.
756 *
757 * @return string
758 */
759 function wd_internal_log(): string {
760 return Central::INTERNAL_LOG;
761 }
762
763 /**
764 * Retrieves the current time, allowing overrides for testing.
765 *
766 * @return int The current timestamp.
767 */
768 function defender_get_current_time(): int {
769 /**
770 * Filter the current time.
771 *
772 * @since 5.2.0
773 *
774 * @param int $time The current timestamp.
775 *
776 * @return int The current/filtered timestamp.
777 */
778 $current_time = apply_filters( 'wpdef_current_time', time() );
779 return ! is_int( $current_time ) ? (int) $current_time : $current_time;
780 }
781
782 /**
783 * Get the current site domain.
784 *
785 * @return string The determined domain name.
786 */
787 function defender_get_domain() {
788 static $domain = '';
789
790 if ( '' === $domain ) {
791 $domain = is_multisite()
792 ? ( get_network()->domain ?? '' )
793 : wp_parse_url( get_site_url(), PHP_URL_HOST );
794 }
795
796 /**
797 * Filter the current site domain.
798 *
799 * @param string $domain The determined domain name.
800 *
801 * @return string The filtered domain name.
802 *
803 * @since 5.2.0
804 */
805 $domain = apply_filters( 'wpdef_current_site_domain', $domain );
806 $domain = is_string( $domain ) ? $domain : (string) $domain;
807
808 return esc_html( $domain );
809 }
810
811 /**
812 * WP_DEFENDER_PRO sometimes doesn't match WPMUDEV::is_pro().
813 *
814 * @return bool
815 */
816 function defender_is_wp_org_version(): bool {
817 return ! wd_di()->get( WPMUDEV::class )->is_pro()
818 && ( defined( 'WP_DEFENDER_PRO' ) && ! WP_DEFENDER_PRO );
819 }
820
821 /**
822 * Get the user agent of the plugin.
823 *
824 * @return string
825 */
826 function defender_get_own_user_agent(): string {
827 return defender_is_wp_org_version()
828 ? sprintf( 'Mozilla/5.0 (compatible; Defender/%1$s)', DEFENDER_VERSION )
829 : sprintf(
830 'Mozilla/5.0 (compatible; WPMU DEV Defender/%1$s; +https://wpmudev.com)',
831 DEFENDER_VERSION
832 );
833 }
834