key doesn't exist at this depth, we will just create an empty array // to hold the next value, allowing us to create the arrays to hold final // values at the correct depth. Then we'll keep digging into the array. if ( ! isset( $modify[ $key ] ) || ! is_array( $modify[ $key ] ) ) { $modify[ $key ] = []; } $modify = &$modify[ $key ]; } $modify[ array_shift( $keys ) ] = $value; return $array; } /** * Removes items at the given locations from an array. * * This accepts a dotted path with '*' to represent wildcards. * * @param array $array * @param string $dotted_path * * @return array */ public static function array_remove( array $array, string $dotted_path ): array { $paths = explode( '.', $dotted_path ); return self::_array_remove( $array, $paths ); } private static function _array_remove( array $array, array $paths ): array { if ( ! $array ) { return $array; } $path = array_shift( $paths ); if ( '*' === $path ) { foreach ( $array as $k => $v ) { if ( is_array( $v ) ) { $array[ $k ] = self::_array_remove( $v, $paths ); } elseif ( ! $paths ) { // If the last dotted path is a wildcard, // remove all elements. unset( $array[ $k ] ); } } } elseif ( isset( $array[ $path ] ) ) { if ( is_array( $array[ $path ] ) && $paths ) { $array[ $path ] = self::_array_remove( $array[ $path ], $paths ); } else { unset( $array[ $path ] ); } } return $array; } /** * Removes any number of items from a list. * * Values are loosely compared. * * @param array $array * @param ...$values * * @return array */ public static function array_pull( array $array, ...$values ): array { return array_values( array_diff( $array, $values ) ); } /** * Merges two arrays recursively such that only arrays are deeply merged. * * @param array $array1 * @param array $array2 * * @return array */ public static function array_merge_recursive_distinct( array $array1, array $array2 ): array { $merged = $array1; foreach ( $array2 as $key => $value ) { if ( is_array( $value ) && isset( $merged[ $key ] ) && is_array( $merged[ $key ] ) ) { $merged[ $key ] = self::array_merge_recursive_distinct( $merged[ $key ], $value ); } else { $merged[ $key ] = $value; } } return $merged; } public static function print_r( $data, $args = array() ) { require_once( ITSEC_Core::get_core_dir() . '/lib/debug.php' ); ITSEC_Debug::print_r( $data, $args ); } public static function get_print_r( $data, $args = array() ) { require_once( ITSEC_Core::get_core_dir() . '/lib/debug.php' ); return ITSEC_Debug::get_print_r( $data, $args ); } /** * Check if WP Cron appears to be running properly. * * @return bool */ public static function is_cron_working() { $working = ITSEC_Modules::get_setting( 'global', 'cron_status' ); return $working === 1; } /** * Should we be using Cron. * * @return bool */ public static function use_cron() { return ITSEC_Modules::get_setting( 'global', 'use_cron' ); } /** * Schedule a test to see if a user should be suggested to enable the Cron scheduler. */ public static function schedule_cron_test() { if ( defined( 'ITSEC_DISABLE_CRON_TEST' ) && ITSEC_DISABLE_CRON_TEST ) { return; } if ( $crons = _get_cron_array() ) { foreach ( $crons as $timestamp => $cron ) { if ( isset( $cron['itsec_cron_test'] ) ) { return; } } } // Get a random time in the next 6-18 hours on a random minute. $time = ITSEC_Core::get_current_time_gmt() + mt_rand( 6, 18 ) * HOUR_IN_SECONDS + mt_rand( 1, 60 ) * MINUTE_IN_SECONDS; wp_schedule_single_event( $time, 'itsec_cron_test', array( $time ) ); ITSEC_Modules::set_setting( 'global', 'cron_test_time', $time ); } /** * Remove the forward slash. * * @param string $string * * @return string */ public static function unfwdslash( $string ) { return ltrim( $string, '/' ); } /** * Add a forward slash. * * @param string $string * * @return string */ public static function fwdslash( $string ) { return '/' . self::unfwdslash( $string ); } /** * Enqueue the itsec_util script. * * Will only be included once per page. * * @param array $args */ public static function enqueue_util( $args = array() ) { static $enqueued = false; if ( $enqueued ) { return; } $translations = array( 'ajax_invalid' => new WP_Error( 'itsec-settings-page-invalid-ajax-response', __( 'An "invalid format" error prevented the request from completing as expected. The format of data returned could not be recognized. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ), 'ajax_forbidden' => new WP_Error( 'itsec-settings-page-forbidden-ajax-response: %1$s "%2$s"', __( 'A "request forbidden" error prevented the request from completing as expected. The server returned a 403 status code, indicating that the server configuration is prohibiting this request. This could be due to a plugin/theme conflict or a server configuration issue. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings or server configuration that could account for this AJAX request being blocked.', 'better-wp-security' ) ), 'ajax_not_found' => new WP_Error( 'itsec-settings-page-not-found-ajax-response: %1$s "%2$s"', __( 'A "not found" error prevented the request from completing as expected. The server returned a 404 status code, indicating that the server was unable to find the requested admin-ajax.php file. This could be due to a plugin/theme conflict, a server configuration issue, or an incomplete WordPress installation. Please try refreshing the page and trying again. If the request continues to fail, you may have to alter plugin settings, alter server configurations, or reinstall WordPress.', 'better-wp-security' ) ), 'ajax_server_error' => new WP_Error( 'itsec-settings-page-server-error-ajax-response: %1$s "%2$s"', __( 'A "internal server" error prevented the request from completing as expected. The server returned a 500 status code, indicating that the server was unable to complete the request due to a fatal PHP error or a server problem. This could be due to a plugin/theme conflict, a server configuration issue, a temporary hosting issue, or invalid custom PHP modifications. Please check your server\'s error logs for details about the source of the error and contact your hosting company for assistance if required.', 'better-wp-security' ) ), 'ajax_unknown' => new WP_Error( 'itsec-settings-page-ajax-error-unknown: %1$s "%2$s"', __( 'An unknown error prevented the request from completing as expected. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ), 'ajax_timeout' => new WP_Error( 'itsec-settings-page-ajax-error-timeout: %1$s "%2$s"', __( 'A timeout error prevented the request from completing as expected. The site took too long to respond. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ), 'ajax_parsererror' => new WP_Error( 'itsec-settings-page-ajax-error-parsererror: %1$s "%2$s"', __( 'A parser error prevented the request from completing as expected. The site sent a response that jQuery could not process. This could be due to a plugin/theme conflict or a server configuration issue.', 'better-wp-security' ) ), ); foreach ( $translations as $i => $translation ) { $messages = ITSEC_Response::get_error_strings( $translation ); if ( $messages ) { $translations[ $i ] = $messages[0]; } } wp_enqueue_script( 'itsec-util', plugins_url( 'admin-pages/js/util.js', __FILE__ ), array( 'jquery' ), ITSEC_Core::get_plugin_build(), true ); wp_localize_script( 'itsec-util', 'itsec_util', array( 'ajax_action' => isset( $args['action'] ) ? $args['action'] : 'itsec_settings_page', 'ajax_nonce' => wp_create_nonce( isset( $args['nonce'] ) ? $args['nonce'] : 'itsec-settings-nonce' ), 'translations' => $translations, ) ); $enqueued = true; } /** * Replace the prefix of a target string with another prefix. * * If the given target does not start with the current prefix, the string * will be returned unmodified. * * @param string $target String to perform replacement on. * @param string $current The current prefix. * @param string $replacement The new prefix. * * @return string */ public static function replace_prefix( $target, $current, $replacement ) { if ( 0 !== strpos( $target, $current ) ) { return $target; } $stripped = substr( $target, strlen( $current ) ); return $replacement . $stripped; } /** * Convert an iterator to an array. * * @param iterable $iterator * * @return array */ public static function iterator_to_array( $iterator ) { if ( is_array( $iterator ) ) { return $iterator; } // Available since PHP 5.1, but SPL which isn't guaranteed. if ( function_exists( 'iterator_to_array' ) ) { return iterator_to_array( $iterator ); } $array = array(); foreach ( $iterator as $key => $value ) { $array[ $key ] = $value; } return $array; } /** * Inserts a new key/value before the key in the array. * * @param string $key The key to insert before. * @param array $array An array to insert in to. * @param string $new_key The key to insert. * @param mixed $new_value The value to insert. * * @return array */ public static function array_insert_before( $key, $array, $new_key, $new_value ) { if ( array_key_exists( $key, $array ) ) { $new = array(); foreach ( $array as $k => $value ) { if ( $k === $key ) { $new[ $new_key ] = $new_value; } $new[ $k ] = $value; } return $new; } $array[ $new_key ] = $new_value; return $array; } /** * Insert an element after a given key. * * @param string|int $key * @param array $array * @param string|int $new_key * @param mixed $new_value * * @return array */ public static function array_insert_after( $key, $array, $new_key, $new_value ) { if ( array_key_exists( $key, $array ) ) { $new = array(); foreach ( $array as $k => $value ) { $new[ $k ] = $value; if ( $k === $key ) { $new[ $new_key ] = $new_value; } } return $new; } $array[ $new_key ] = $new_value; return $array; } /** * Gets the first key in an array. * * @param array $arr * * @return int|string|null */ public static function array_key_first( array $arr ) { if ( function_exists( 'array_key_first' ) ) { return array_key_first( $arr ); } foreach ( $arr as $key => $value ) { return $key; } return null; } /** * Gets the last ket in an array. * * @param array $arr * * @return int|string|null */ public static function array_key_last( array $arr ) { if ( function_exists( 'array_key_last' ) ) { return array_key_last( $arr ); } end( $arr ); return key( $arr ); } /** * Gets the first item from an array. * * @param array $arr * @param mixed $default * * @return mixed */ public static function first( array $arr, $default = null ) { return $arr[ self::array_key_first( $arr ) ] ?? $default; } /** * Gets the last item from an array. * * @param array $arr * @param mixed $default * * @return mixed */ public static function last( array $arr, $default = null ) { return $arr[ self::array_key_last( $arr ) ] ?? $default; } /** * Plucks a certain field out of each item in the list. * * Similar to {@see wp_list_pluck()} but it supports using methods. * * @param array $list The list of items. * @param string $field The field or method name to use. * @param string $index_key Field from the item to use as keys for the new array. * * @return array */ public static function pluck( array $list, $field, $index_key = '' ) { $output = []; foreach ( $list as $i => $item ) { $key = $index_key ? static::get( $item, $index_key ) : $i; $value = static::get( $item, $field ); if ( null === $key ) { $output[] = $value; } else { $output[ $key ] = $value; } } return $output; } /** * Get's a value from an array or object. * * @param array|object $item The item to retrieve the value from. * @param string $field The field or method name to use. * @param null $default The default value to return if no value is found. * * @return mixed|null */ public static function get( $item, $field, $default = null ) { if ( is_array( $item ) ) { return isset( $item[ $field ] ) ? $item[ $field ] : $default; } if ( is_object( $item ) ) { if ( is_callable( [ $item, $field ] ) ) { return $item->{$field}(); } return isset( $item->{$field} ) ? $item->{$field} : $default; } return $default; } /** * Finds the first item in a list matching the given predicate. * * @param iterable $list * @param callable $predicate * * @return mixed|null */ public static function find_where( iterable $list, callable $predicate ) { foreach ( $list as $item ) { if ( $predicate( $item ) ) { return $item; } } return null; } /** * Array unique implementation that allows for non-scalar values. * * Will compare elements using `serialize()`. * * Keys are preserved. If a numeric array is given, the array will be re-indexed. * * @param array $array * @param bool $stabilize If true, stabilizes the values first according to JSON semantics. * * @return array */ public static function non_scalar_array_unique( $array, $stabilize = false ) { $is_numeric = wp_is_numeric_array( $array ); $hashes = array(); foreach ( $array as $key => $value ) { if ( $stabilize ) { $value = rest_stabilize_value( $value ); } $hash = serialize( $value ); if ( isset( $hashes[ $hash ] ) ) { unset( $array[ $key ] ); } else { $hashes[ $hash ] = 1; } } if ( $is_numeric ) { return array_values( $array ); } return $array; } /** * Parse a complex header that has attributes like quality values. * * @param string $header * * @return array[] * @example Parsing the Accept-Language header. * * "en-US,en;q=0.9,de;q=0.8" transforms to: * * [ * 'en-US' => [], * 'en' => [ 'q' => 0.9 ], * 'de' => [ 'q' => 0.8' ], * ] * */ public static function parse_header_with_attributes( $header ) { $parsed = array(); $list = explode( ',', $header ); foreach ( $list as $value ) { $attrs = array(); $parts = explode( ';', trim( $value ) ); $main = trim( $parts[0], ' <>' ); foreach ( $parts as $part ) { if ( false === strpos( $part, '=' ) ) { continue; } list( $key, $value ) = array_map( 'trim', explode( '=', $part, 2 ) ); $attrs[ $key ] = trim( $value, '" ' ); } $parsed[ $main ] = $attrs; } return $parsed; } /** * Is a particular function allowed to be called. * * Checks disabled functions and the function blacklist. * * @param string $func * * @return bool */ public static function is_func_allowed( $func ) { static $cache = array(); static $disabled; static $suhosin; if ( isset( $cache[ $func ] ) ) { return $cache[ $func ]; } if ( $disabled === null ) { $disabled = preg_split( '/\s*,\s*/', (string) ini_get( 'disable_functions' ) ); } if ( $suhosin === null ) { $suhosin = preg_split( '/\s*,\s*/', (string) ini_get( 'suhosin.executor.func.blacklist' ) ); } if ( ! is_callable( $func ) ) { return $cache[ $func ] = false; } if ( in_array( $func, $disabled, true ) ) { return $cache[ $func ] = false; } if ( in_array( $func, $suhosin, true ) ) { return $cache[ $func ] = false; } return $cache[ $func ] = true; } /** * Get whatever backup plugin is being used on this site. * * @return string */ public static function get_backup_plugin() { $possible = array( 'backupbuddy/backupbuddy.php', 'updraftplus/updraftplus.php', 'backwpup/backwpup.php', 'xcloner-backup-and-restore/xcloner.php', 'duplicator/duplicator.php', 'backup/backup.php', 'wp-db-backup/wp-db-backup.php', 'backupwordpress/backupwordpress.php', 'blogvault-real-time-backup/blogvault.php', 'wp-all-backup/wp-all-backup.php', 'vaultpress/vaultpress.php', ); /** * Filter the list of possible backup plugins. * * @param string[] List of Backup Plugin __FILE__. */ $possible = apply_filters( 'itsec_possible_backup_plugins', $possible ); if ( ! function_exists( 'is_plugin_active' ) ) { require_once( ABSPATH . 'wp-admin/includes/plugin.php' ); } if ( ! function_exists( 'is_plugin_active' ) ) { return ''; } foreach ( $possible as $file ) { if ( is_plugin_active( $file ) ) { return $file; } } return ''; } /** * Generate a random token. * * @return string Hex token. */ public static function generate_token() { $length = 64; try { $random = bin2hex( random_bytes( $length / 2 ) ); } catch ( Exception $e ) { $unpacked = unpack( 'H*', wp_generate_password( $length / 2, true, true ) ); $random = reset( $unpacked ); } return $random; } /** * Generate a hash of the token for storage. * * @param string $token * * @return false|string */ public static function hash_token( $token ) { return hash_hmac( self::get_hash_algo(), $token, wp_salt() ); } /** * Check if the provided token matches the stored hashed token. * * @param string $provided_token * @param string $hashed_token * * @return bool */ public static function verify_token( $provided_token, $hashed_token ) { if ( ! $hashed_token || ! $provided_token ) { return false; } return hash_equals( $hashed_token, self::hash_token( $provided_token ) ); } /** * Get the hash algorithm to use. * * PHP can be compiled without the hash extension and the supported hash algos can be variable. WordPress shims * support for md5 and sha1 hashes with hash_hmac. * * @return string */ public static function get_hash_algo() { if ( ! function_exists( 'hash_algos' ) ) { return 'sha1'; } $algos = hash_algos(); if ( in_array( 'sha256', $algos, true ) ) { return 'sha256'; } return 'sha1'; } public static function get_url_from_file( $file, $auto_ssl = true, $prevent_recursion = false ) { $file = str_replace( '\\', '/', $file ); $url = ''; $upload_dir = ITSEC_Core::get_wp_upload_dir(); $upload_dir['basedir'] = str_replace( '\\', '/', $upload_dir['basedir'] ); if ( is_array( $upload_dir ) && ( false === $upload_dir['error'] ) ) { if ( 0 === strpos( $file, $upload_dir['basedir'] ) ) { $url = str_replace( $upload_dir['basedir'], $upload_dir['baseurl'], $file ); } elseif ( false !== strpos( $file, 'wp-content/uploads' ) ) { $path_pattern = 'wp-content/uploads'; $url_base = $upload_dir['baseurl']; if ( is_multisite() && ! ( is_main_network() && is_main_site() && defined( 'MULTISITE' ) ) ) { if ( defined( 'MULTISITE' ) ) { $mu_path = '/sites/' . get_current_blog_id(); } else { $mu_path = '/' . get_current_blog_id(); } if ( false === strpos( $file, "$path_pattern$mu_path" ) ) { $url_base = substr( $url_base, 0, - strlen( $mu_path ) ); } else { $path_pattern .= $mu_path; } } $url = $url_base . substr( $file, strpos( $file, $path_pattern ) + strlen( $path_pattern ) ); } } if ( empty( $url ) ) { if ( ! isset( $GLOBALS['__itsec_cache_wp_content_dir'] ) ) { $GLOBALS['__itsec_cache_wp_content_dir'] = rtrim( str_replace( '\\', '/', WP_CONTENT_DIR ), '/' ); } if ( ! isset( $GLOBALS['__itsec_cache_abspath'] ) ) { $GLOBALS['__itsec_cache_abspath'] = rtrim( str_replace( '\\', '/', ABSPATH ), '/' ); } if ( 0 === strpos( $file, $GLOBALS['__itsec_cache_wp_content_dir'] ) ) { $url = WP_CONTENT_URL . str_replace( '\\', '/', preg_replace( '/^' . preg_quote( $GLOBALS['__itsec_cache_wp_content_dir'], '/' ) . '/', '', $file ) ); } elseif ( 0 === strpos( $file, $GLOBALS['__itsec_cache_abspath'] ) ) { $url = get_option( 'siteurl' ) . str_replace( '\\', '/', preg_replace( '/^' . preg_quote( $GLOBALS['__itsec_cache_abspath'], '/' ) . '/', '', $file ) ); } } if ( empty( $url ) && ! $prevent_recursion ) { $url = self::get_url_from_file( realpath( $file ), $auto_ssl, true ); } if ( empty( $url ) ) { return ''; } if ( $auto_ssl ) { $url = self::fix_url( $url ); } return $url; } public static function get_file_from_url( $url ) { $url = preg_replace( '/^https/', 'http', $url ); $url = preg_replace( '/\?.*$/', '', $url ); $file = ''; $upload_dir = ITSEC_Core::get_wp_upload_dir(); if ( is_array( $upload_dir ) && ( false === $upload_dir['error'] ) ) { if ( 0 === strpos( $url, $upload_dir['baseurl'] ) ) { $file = str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $url ); } elseif ( false !== strpos( $url, 'wp-content/uploads' ) ) { $path_pattern = 'wp-content/uploads'; $file_base = $upload_dir['basedir']; if ( is_multisite() && ! ( is_main_network() && is_main_site() && defined( 'MULTISITE' ) ) ) { if ( defined( 'MULTISITE' ) ) { $mu_path = '/sites/' . get_current_blog_id(); } else { $mu_path = '/' . get_current_blog_id(); } if ( false === strpos( $url, "$path_pattern$mu_path" ) ) { $file_base = substr( $file_base, 0, - strlen( $mu_path ) ); } else { $path_pattern .= $mu_path; } } $file = $file_base . substr( $url, strpos( $url, $path_pattern ) + strlen( $path_pattern ) ); } } if ( empty( $file ) ) { if ( ! isset( $GLOBALS['__itsec_cache_wp_content_url'] ) ) { $GLOBALS['__itsec_cache_wp_content_url'] = preg_replace( '/^https/', 'http', WP_CONTENT_URL ); } if ( ! isset( $GLOBALS['__itsec_cache_siteurl'] ) ) { $GLOBALS['__itsec_cache_siteurl'] = preg_replace( '/^https/', 'http', get_option( 'siteurl' ) ); } if ( 0 === strpos( $url, $GLOBALS['__itsec_cache_wp_content_url'] ) ) { $file = rtrim( WP_CONTENT_DIR, '\\\/' ) . preg_replace( '/^' . preg_quote( $GLOBALS['__itsec_cache_wp_content_url'], '/' ) . '/', '', $url ); } elseif ( 0 === strpos( $url, $GLOBALS['__itsec_cache_siteurl'] ) ) { $file = rtrim( ABSPATH, '\\\/' ) . preg_replace( '/^' . preg_quote( $GLOBALS['__itsec_cache_siteurl'], '/' ) . '/', '', $url ); } } return $file; } public static function fix_url( $url ) { if ( is_ssl() ) { $url = preg_replace( '|^http://|', 'https://', $url ); } else { $url = preg_replace( '|^https://|', 'http://', $url ); } return $url; } /** * Set a cookie. * * @param string $name * @param string $value * @param array $args */ public static function set_cookie( $name, $value, $args = array() ) { $args = wp_parse_args( array( 'length' => 0, 'http_only' => true, ), $args ); $expires = $args['length'] ? ITSEC_Core::get_current_time_gmt() + $args['length'] : 0; setcookie( $name, $value, $expires, COOKIEPATH, COOKIE_DOMAIN, is_ssl(), $args['http_only'] ); } /** * Clear a cookie. * * @param string $name */ public static function clear_cookie( $name ) { setcookie( $name, ' ', ITSEC_Core::get_current_time_gmt() - YEAR_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN, false, false ); } /** * Is the current request a loopback request. * * @return bool */ public static function is_loopback_request() { return in_array( self::get_ip(), ITSEC_Modules::get_setting( 'global', 'server_ips' ), true ); } /** * Version of {@see wp_slash()} that won't cast numbers to strings. * * @param array|string $value * * @return array|string */ public static function slash( $value ) { if ( is_array( $value ) ) { foreach ( $value as $k => $v ) { if ( is_array( $v ) ) { $value[ $k ] = self::slash( $v ); } elseif ( is_string( $v ) ) { $value[ $k ] = addslashes( $v ); } } } elseif ( is_string( $value ) ) { $value = addslashes( $value ); } return $value; } /** * Format as a ISO 8601 date. * * @param int|string|\DateTimeInterface $date Epoch or strtotime compatible date. * * @return string|false */ public static function to_rest_date( $date = 0 ) { if ( ! $date ) { $date = ITSEC_Core::get_current_time_gmt(); } elseif ( $date instanceof \DateTimeInterface ) { $date = $date->getTimestamp(); } elseif ( ! is_int( $date ) ) { $date = strtotime( $date ); } return gmdate( 'Y-m-d\TH:i:sP', $date ); } /** * Flatten an array. * * @param array $array * * @return array */ public static function flatten( $array ) { if ( ! is_array( $array ) ) { return array( $array ); } $merge = array(); foreach ( $array as $value ) { $merge[] = self::flatten( $value ); } return $merge ? call_user_func_array( 'array_merge', $merge ) : array(); } /** * Preload REST API requests. * * @param array $requests * * @return array */ public static function preload_rest_requests( $requests, string $page = '' ) { if ( $page ) { /** * Filters the list of API requests to preload. * * @param array $requests * @param string $page */ $requests = apply_filters( 'itsec_preload_requests', $requests, 'tools' ); } $preload = array(); foreach ( $requests as $key => $config ) { if ( is_string( $config ) ) { $key = $config; $config = array( 'route' => $config ); } $request = new WP_REST_Request( isset( $config['method'] ) ? $config['method'] : 'GET', $config['route'] ); if ( ! empty( $config['query'] ) ) { $request->set_query_params( $config['query'] ); } $response = rest_do_request( $request ); if ( $response->get_status() >= 200 && $response->get_status() < 300 ) { rest_send_allow_header( $response, rest_get_server(), $request ); if ( is_int( $key ) ) { $key = $config['route']; if ( ! empty( $config['query'] ) ) { $key = add_query_arg( $config['query'], $key ); } if ( ! empty( $config['embed'] ) ) { $key = add_query_arg( '_embed', '1', $key ); } } $preload[ $key ] = array( 'body' => rest_get_server()->response_to_data( $response, ! empty( $config['embed'] ) ), 'headers' => $response->get_headers() ); } } return $preload; } /** * Preloads a REST API request directly into a data store. * * This can be useful when we need the data to be immediately * available when the app renders. Typical preloading still has * a fractional delay, as it goes through an async fetch stack. * * @param string $store The data store handle. * @param string $action The data store action. * @param string $route The REST API route to fetch. * @param array $query Query parameters for the REST API route. * * @return bool */ public static function preload_request_for_data_store( string $store, string $action, string $route, array $query = [] ): bool { $request = new WP_REST_Request( 'GET', $route ); $request->set_query_params( $query ); $response = rest_do_request( $request ); if ( $response->is_error() ) { return false; } $data = rest_get_server()->response_to_data( $response, ! empty( $query['_embed'] ) ); return wp_add_inline_script( 'itsec-packages-data', sprintf( "wp.data.dispatch( '%s' ).%s( %s )", $store, $action, wp_json_encode( $data ) ) ); } /** * Check if the given string starts with the given needle. * * @param string $haystack * @param string $needle * * @return bool */ public static function str_starts_with( $haystack, $needle ) { return 0 === strpos( $haystack, $needle ); } public static function str_ends_with( $haystack, $needle ) { return '' === $needle || substr_compare( $haystack, $needle, - strlen( $needle ) ) === 0; } /** * Load a library class definition. * * @param string $name */ public static function load( $name ) { require_once( dirname( __FILE__ ) . "/lib/class-itsec-lib-{$name}.php" ); } /** * Combine multiple WP_Error instances. * * @param WP_Error|null ...$errors * * @return WP_Error */ public static function combine_wp_error( ...$errors ) { $combined = new WP_Error(); self::add_to_wp_error( $combined, ...$errors ); return $combined; } /** * Add the subsequent WP Error data to the first WP Error instance. * * @param WP_Error $add_to * @param WP_Error|null ...$errors */ public static function add_to_wp_error( WP_Error $add_to, ...$errors ) { foreach ( $errors as $error ) { if ( $error ) { foreach ( $error->get_error_codes() as $code ) { foreach ( $error->get_error_messages( $code ) as $message ) { $add_to->add( $code, $message ); } $data = $error->get_error_data( $code ); if ( null !== $data ) { $add_to->add_data( $data, $code ); } } } } } /** * Render a file with only the given vars in context. * * @param string $file * @param array $context * @param bool $echo * * @return string|void */ public static function render( $file, $context = array(), $echo = true ) { $__echo = $echo; $__file = $file; extract( $context, EXTR_OVERWRITE ); unset( $file, $context, $echo ); if ( ! $__echo ) { ob_start(); } require( $__file ); if ( ! $__echo ) { return ob_get_clean() ?: ''; } } /** * Utility to mark this page as not cacheable. */ public static function no_cache() { nocache_headers(); if ( ! defined( 'DONOTCACHEPAGE' ) ) { define( 'DONOTCACHEPAGE', true ); } } /** * Get the WordPress branch version. * * @return string * @example 5.2.4 => 5.2 * */ public static function get_wp_branch() { $version = get_bloginfo( 'version' ); list( $major, $minor ) = explode( '.', $version ); return $major . '.' . $minor; } /** * Are two lists equal ignoring order. * * @param array $a * @param array $b * @param callable|null $cmp * * @return bool */ public static function equal_sets( array $a, array $b, callable $cmp = null ) { if ( $cmp ) { usort( $a, $cmp ); usort( $b, $cmp ); } else { sort( $a ); sort( $b ); } return $a === $b; } /** * Convert the return val from {@see ITSEC_Modules::set_settings()} to a WP_Error object. * * @param array $updated * * @return WP_Error|null */ public static function updated_settings_to_wp_error( $updated ) { if ( is_wp_error( $updated ) ) { return $updated; } if ( $updated['saved'] ) { return null; } if ( $updated['errors'] ) { $error = self::combine_wp_error( ...$updated['errors'] ); } else { $error = new \WP_Error( 'itsec.settings.set-failed', __( 'Failed to update settings.', 'better-wp-security' ), [ 'status' => \WP_Http::BAD_REQUEST ] ); } return $error; } /** * Sanitize the list of roles. * * @param string[] $roles * * @return array */ public static function sanitize_roles( $roles ) { return array_filter( $roles, static function ( $role ) { return (bool) get_role( $role ); } ); } /** * Get a snapshot of $_SERVER properties. * * @return array */ public static function get_server_snapshot() { $whitelist = [ 'REQUEST_TIME', 'REQUEST_TIME_FLOAT', 'REQUEST_METHOD', 'HTTPS', 'REQUEST_SCHEME', 'SERVER_PROTOCOL', 'SCRIPT_FILENAME', ]; return array_filter( $_SERVER, static function ( $key ) use ( $whitelist ) { if ( $key === 'HTTP_COOKIE' ) { return false; } if ( self::str_starts_with( $key, 'HTTP_' ) ) { return true; } if ( self::str_starts_with( $key, 'CONTENT_' ) ) { return true; } return in_array( $key, $whitelist, true ); }, ARRAY_FILTER_USE_KEY ); } /** * Version of {@see is_super_admin()} that operates on a `WP_User` instance. * * This bypasses an issue where {@see is_super_admin()} cannot be used during the `determine_current_user` filter since * `is_super_admin` has a side effect of querying for the current user, causing an infinite loop. * * @param WP_User $user * * @return bool */ public static function is_super_admin( WP_User $user ) { if ( ! $user->exists() ) { return false; } if ( is_multisite() ) { $super_admins = get_super_admins(); if ( is_array( $super_admins ) && in_array( $user->user_login, $super_admins ) ) { return true; } } else { if ( $user->has_cap( 'delete_users' ) ) { return true; } } return false; } /** * Performs a {@see dbDelta()} but reports any errors encountered. * * @param string $delta * * @return WP_Error */ public static function db_delta_with_error_handling( $delta ) { global $wpdb, $EZSQL_ERROR; require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); $err_count = is_array( $EZSQL_ERROR ) ? count( $EZSQL_ERROR ) : 0; $showed_errors = $wpdb->show_errors( false ); dbDelta( $delta ); if ( $showed_errors ) { $wpdb->show_errors(); } $wp_error = new WP_Error(); if ( is_array( $EZSQL_ERROR ) ) { for ( $i = $err_count, $i_max = count( $EZSQL_ERROR ); $i < $i_max; $i ++ ) { $error = $EZSQL_ERROR[ $i ]; if ( empty( $error['error_str'] ) || empty( $error['query'] ) || 0 === strpos( $error['query'], 'DESCRIBE ' ) ) { continue; } $wp_error->add( 'db_delta_error', $error['error_str'] ); } } return $wp_error; } /** * Get info used to help evaluate requirements according to * {@see ITSEC_Lib::evaluate_requirements()}. * * @return array[] */ public static function get_requirements_info(): array { return [ 'load' => ITSEC_Core::is_loading_early() ? 'early' : 'normal', 'server' => [ 'php' => explode( '-', PHP_VERSION )[0], 'extensions' => [ 'OpenSSL' => self::is_func_allowed( 'openssl_verify' ), ], ], ]; } /** * Evaluate whether this site passes the given requirements. * * @param array $requirements * * @return WP_Error */ public static function evaluate_requirements( array $requirements ) { $schema = [ 'type' => 'object', 'additionalProperties' => false, 'properties' => [ 'version' => [ 'type' => 'object', 'additionalProperties' => false, 'properties' => [ 'pro' => [ 'type' => 'string', 'required' => true, ], 'free' => [ 'type' => 'string', 'required' => true, ], ], ], 'ssl' => [ 'type' => 'boolean', ], 'feature-flags' => [ 'type' => 'array', 'items' => [ 'type' => 'string', ], ], 'multisite' => [ 'type' => 'string', 'enum' => [ 'enabled', 'disabled' ], ], 'server' => [ 'type' => 'object', 'properties' => [ 'php' => [ 'type' => 'string', ], 'extensions' => [ 'type' => 'array', 'items' => [ 'type' => 'string', 'enum' => [ 'OpenSSL' ], ], ], ], ], 'load' => [ 'type' => 'string', 'enum' => [ 'normal', 'early' ], ], 'ip' => [ 'type' => 'boolean', ], ], ]; if ( ITSEC_Core::is_development() ) { $valid_requirements = rest_validate_value_from_schema( $requirements, $schema ); if ( is_wp_error( $valid_requirements ) ) { return $valid_requirements; } } $error = new WP_Error(); foreach ( $requirements as $kind => $requirement ) { switch ( $kind ) { case 'version': $key = ITSEC_Core::is_pro() ? 'pro' : 'free'; $version = $requirement[ $key ]; if ( version_compare( ITSEC_Core::get_plugin_version(), $version, '<' ) ) { $error->add( 'version', sprintf( __( 'You must be running at least version %s of Solid Security.', 'better-wp-security' ), $version ) ); } break; case 'ssl': if ( $requirement !== is_ssl() ) { $error->add( 'ssl', $requirement ? __( 'Your site must support SSL.', 'better-wp-security' ) : __( 'Your site must not support SSL.', 'better-wp-security' ) ); } break; case 'feature-flags': foreach ( $requirement as $flag ) { if ( ! ITSEC_Lib_Feature_Flags::is_enabled( $flag ) ) { $error->add( 'feature-flags', sprintf( __( 'The \'%s\' feature flag must be enabled.', 'better-wp-security' ), ( ITSEC_Lib_Feature_Flags::get_flag_config( $flag )['title'] ?? $flag ) ?: $flag ) ); } } break; case 'multisite': if ( $requirement === 'enabled' && ! is_multisite() ) { $error->add( 'multisite', __( 'Multisite must be enabled.', 'better-wp-security' ) ); } elseif ( $requirement === 'disabled' && is_multisite() ) { $error->add( 'multisite', __( 'Multisite is not supported.', 'better-wp-security' ) ); } break; case 'server': $info = self::get_requirements_info(); if ( isset( $requirement['php'] ) && version_compare( $info['server']['php'], $requirement['php'], '<' ) ) { $error->add( 'server', sprintf( __( 'You must be running PHP version %s or later.', 'better-wp-security' ), $requirement['php'] ) ); } $missing = array_filter( $requirement['extensions'] ?? [], function ( $extension ) use ( $info ) { return empty( $info['server']['extensions'][ $extension ] ); } ); if ( $missing ) { if ( count( $missing ) === 1 ) { $message = sprintf( __( 'The %s PHP extension is required.', 'better-wp-security' ), ITSEC_Lib::first( $missing ) ); } else { $message = wp_sprintf( _n( 'The following PHP extension is required: %l.', 'The following PHP extensions are required: %l.', count( $missing ), 'better-wp-security' ), $missing ); } $error->add( 'server', $message ); } break; case 'load': if ( $requirement === 'normal' && ITSEC_Core::is_loading_early() ) { $error->add( 'load', __( 'Loading Solid Security via an MU-Plugin is not supported.', 'better-wp-security' ) ); } elseif ( $requirement === 'early' && ! ITSEC_Core::is_loading_early() ) { $error->add( 'load', __( 'Loading Solid Security without an MU-Plugin is not supported.', 'better-wp-security' ) ); } break; case 'ip': if ( ! ITSEC_Lib_IP_Detector::is_configured() ) { $error->add( 'ip', __( 'You must select an IP Detection method in Global Settings.', 'better-wp-security' ) ); } break; } } return $error; } /** * Converts a JSON Schema to a WP-CLI synopsis. * * @param array $schema * * @return array */ public static function convert_schema_to_cli_synopsis( array $schema ) { $synopsis = []; $required = isset( $schema['required'] ) ? $schema['required'] : []; if ( isset( $schema['properties'] ) ) { foreach ( $schema['properties'] as $property => $config ) { $param = [ 'name' => $property, ]; if ( 'boolean' === $config['type'] ) { $param['type'] = 'flag'; } else { $param['type'] = 'assoc'; } if ( array_key_exists( 'default', $config ) ) { $param['default'] = $config['default']; } if ( isset( $config['enum'] ) ) { $param['options'] = $config['enum']; } if ( ( ! isset( $config['required'] ) || true !== $config['required'] ) && ! in_array( $property, $required, true ) ) { $param['optional'] = true; } if ( isset( $config['description'] ) ) { $param['description'] = $config['description']; } $synopsis[] = $param; } } if ( ! empty( $schema['additionalProperties'] ) ) { $synopsis[] = [ 'type' => 'generic', ]; } return $synopsis; } /** * Decode a string with URL-safe Base64. * * @param string $input A Base64 encoded string * * @return string A decoded string */ public static function url_safe_b64_decode( $input ) { $remainder = strlen( $input ) % 4; if ( $remainder ) { $padlen = 4 - $remainder; $input .= str_repeat( '=', $padlen ); } return base64_decode( strtr( $input, '-_', '+/' ) ); } /** * Encode a string with URL-safe Base64. * * @param string $input The string you want encoded * * @return string The base64 encode of what you passed in */ public static function url_safe_b64_encode( $input ) { return str_replace( '=', '', strtr( base64_encode( $input ), '+/', '-_' ) ); } /** * Compares the WordPress version with the given version. * * @param string $version The version to compare with. * @param string $operator The operator. * @param bool $allow_dev Whether to treat dev versions as stable. * * @return bool */ public static function wp_version_compare( $version, $operator, $allow_dev = true ) { global $wp_version; if ( $allow_dev ) { list( $wp_version ) = explode( '-', $wp_version ); } return version_compare( $wp_version, $version, $operator ); } /** * Checks if the WordPress version is at least the given version. * * @param string $version The version to check WP for. * @param bool $allow_dev Whether to treat dev versions as stable. * * @return bool */ public static function is_wp_version_at_least( $version, $allow_dev = true ) { return static::wp_version_compare( $version, '>=', $allow_dev ); } /** * Gets the WordPress login URL. * * @param string $action A particular login action to use. * @param string $redirect Where to redirect the user to after login. * @param string $scheme The scheme to use. Accepts `login_post` for form submissions. * * @return string */ public static function get_login_url( $action = '', $redirect = '', $scheme = 'login' ) { if ( 'login_post' === $scheme || ( $action && 'login' !== $action ) ) { $url = 'wp-login.php'; if ( $action ) { $url = add_query_arg( 'action', urlencode( $action ), $url ); } if ( $redirect ) { $url = add_query_arg( 'redirect_to', urlencode( $redirect ), $url ); } $url = site_url( $url, $scheme ); } else { $url = wp_login_url( $redirect ); if ( $action ) { $url = add_query_arg( 'action', urlencode( $action ), $url ); } } if ( function_exists( 'is_wpe' ) && is_wpe() ) { $url = add_query_arg( 'wpe-login', 'true', $url ); } return apply_filters( 'itsec_login_url', $url, $action, $redirect, $scheme ); } /** * Extends a service definition, ignoring if the service has been frozen. * * @param \iThemesSecurity\Strauss\Pimple\Container $c * @param string $id * @param callable $extend * * @return bool */ public static function extend_if_able( \iThemesSecurity\Strauss\Pimple\Container $c, string $id, callable $extend ): bool { try { $c->extend( $id, $extend ); return true; } catch ( \iThemesSecurity\Strauss\Pimple\Exception\FrozenServiceException $e ) { return false; } } /** * Resolve JSON Schema refs. * * @param array $schema * * @return array */ public static function resolve_schema_refs( array $schema ): array { if ( isset( $schema['definitions'] ) ) { array_walk( $schema, [ static::class, 'resolve_ref' ], $schema['definitions'] ); } return $schema; } /** * Resolves $ref entries at any point in the config. * * Currently, only a simplified form of JSON Pointers are supported where `/` is the only * allowed control character. * * Additionally, the `$ref` keyword must start with `#/definitions`. * * @param mixed $value The incoming value. * @param string $key The array key. * @param array $definitions The shared definitions. */ private static function resolve_ref( &$value, $key, $definitions ) { if ( ! is_array( $value ) ) { return; } if ( isset( $value['$ref'] ) ) { $ref = str_replace( '#/definitions/', '', $value['$ref'] ); $value = \ITSEC_Lib::array_get( $definitions, $ref, null, '/' ); return; } array_walk( $value, [ static::class, 'resolve_ref' ], $definitions ); } /** * Generates a v4 UUID using a CSPRNG. * * @return string */ public static function generate_uuid4(): string { return sprintf( '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ), wp_rand( 0, 0x0fff ) | 0x4000, wp_rand( 0, 0x3fff ) | 0x8000, wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ), wp_rand( 0, 0xffff ) ); } /** * Clears the WordPress auth cookies. * * This function is safe to call before plugins have been loaded. * But the request MUST exist after calling it. * * @return void */ public static function clear_auth_cookie() { if ( ! function_exists( 'wp_clear_auth_cookie' ) ) { if ( is_multisite() ) { ms_cookie_constants(); } // Define constants after multisite is loaded. wp_cookie_constants(); require_once ABSPATH . 'wp-includes/pluggable.php'; } wp_clear_auth_cookie(); } public static function recursively_json_serialize( $value ) { if ( $value instanceof JsonSerializable ) { return $value->jsonSerialize(); } if ( is_array( $value ) ) { foreach ( $value as $k => $v ) { $value[ $k ] = self::recursively_json_serialize( $v ); } } return $value; } }