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; } }