PluginProbe
Defender Security – Malware Scanner, Login Security & Firewall / 6.1.0
Defender Security – Malware Scanner, Login Security & Firewall v6.1.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 6.1.0, at src/functions.php

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