) ) { $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 ) { $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 ); $preload[ $key ] = array( 'body' => rest_get_server()->response_to_data( $response, ! empty( $config['embed'] ) ), 'headers' => $response->get_headers() ); } } return $preload; } /** * 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.', 'it-l10n-ithemes-security-pro' ), [ '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 [ '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' ], ], ], ], ], ], ]; 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 iThemes Security.', 'it-l10n-ithemes-security-pro' ), $version ) ); } break; case 'ssl': if ( $requirement !== is_ssl() ) { $error->add( 'ssl', $requirement ? __( 'Your site must support SSL.', 'it-l10n-ithemes-security-pro' ) : __( 'Your site must not support SSL.', 'it-l10n-ithemes-security-pro' ) ); } 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.', 'it-l10n-ithemes-security-pro' ), ( 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.', 'it-l10n-ithemes-security-pro' ) ); } elseif ( $requirement === 'disabled' && is_multisite() ) { $error->add( 'multisite', __( 'Multisite is not supported.', 'it-l10n-ithemes-security-pro' ) ); } 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.', 'it-l10n-ithemes-security-pro' ), $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.', 'it-l10n-ithemes-security-pro' ), 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 ), 'it-l10n-ithemes-security-pro' ), $missing ); } $error->add( 'server', $message ); } 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 ) ); } 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; } }