updated plugin Jetpack Protect
version 2.1.0
This commit is contained in:
@ -5,6 +5,38 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [0.16.0.1] - 2024-04-10
|
||||
### Security
|
||||
- Improves handling of REQUEST_URI [#36833]
|
||||
|
||||
## [0.16.0] - 2024-03-22
|
||||
### Added
|
||||
- Add data to WAF logs and add toggle for users to opt-in to share more data with us if needed. [#36377]
|
||||
|
||||
## [0.15.1] - 2024-03-14
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.15.0] - 2024-03-12
|
||||
### Added
|
||||
- Add JSON parameter support to the Web Application Firewall. [#36169]
|
||||
|
||||
## [0.14.2] - 2024-03-04
|
||||
### Fixed
|
||||
- Fixed base64 transforms to better conform with the modsecurity runtime [#35693]
|
||||
|
||||
## [0.14.1] - 2024-02-27
|
||||
### Changed
|
||||
- Internal updates.
|
||||
|
||||
## [0.14.0] - 2024-02-12
|
||||
### Added
|
||||
- Add standalone mode status to WAF config [#34840]
|
||||
|
||||
## [0.13.0] - 2024-02-05
|
||||
### Added
|
||||
- Run the WAF on JN environments [#35341]
|
||||
|
||||
## [0.12.4] - 2024-01-18
|
||||
### Fixed
|
||||
- Optimize how the web application firewall checks for updates on admin screens. [#34820]
|
||||
@ -257,6 +289,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
### Changed
|
||||
- Core: do not ship .phpcs.dir.xml in production builds.
|
||||
|
||||
[0.16.0.1]: https://github.com/Automattic/jetpack-waf/compare/v0.16.0...v0.16.0.1
|
||||
[0.16.0]: https://github.com/Automattic/jetpack-waf/compare/v0.15.1...v0.16.0
|
||||
[0.15.1]: https://github.com/Automattic/jetpack-waf/compare/v0.15.0...v0.15.1
|
||||
[0.15.0]: https://github.com/Automattic/jetpack-waf/compare/v0.14.2...v0.15.0
|
||||
[0.14.2]: https://github.com/Automattic/jetpack-waf/compare/v0.14.1...v0.14.2
|
||||
[0.14.1]: https://github.com/Automattic/jetpack-waf/compare/v0.14.0...v0.14.1
|
||||
[0.14.0]: https://github.com/Automattic/jetpack-waf/compare/v0.13.0...v0.14.0
|
||||
[0.13.0]: https://github.com/Automattic/jetpack-waf/compare/v0.12.4...v0.13.0
|
||||
[0.12.4]: https://github.com/Automattic/jetpack-waf/compare/v0.12.3...v0.12.4
|
||||
[0.12.3]: https://github.com/Automattic/jetpack-waf/compare/v0.12.2...v0.12.3
|
||||
[0.12.2]: https://github.com/Automattic/jetpack-waf/compare/v0.12.1...v0.12.2
|
||||
|
@ -5,15 +5,15 @@
|
||||
"license": "GPL-2.0-or-later",
|
||||
"require": {
|
||||
"php": ">=7.0",
|
||||
"automattic/jetpack-connection": "^2.2.0",
|
||||
"automattic/jetpack-constants": "^2.0.0",
|
||||
"automattic/jetpack-ip": "^0.2.1",
|
||||
"automattic/jetpack-status": "^2.1.0",
|
||||
"automattic/jetpack-connection": "^2.4.1",
|
||||
"automattic/jetpack-constants": "^2.0.1",
|
||||
"automattic/jetpack-ip": "^0.2.2",
|
||||
"automattic/jetpack-status": "^2.1.2",
|
||||
"wikimedia/aho-corasick": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"yoast/phpunit-polyfills": "1.1.0",
|
||||
"automattic/jetpack-changelogger": "^4.0.5",
|
||||
"automattic/jetpack-changelogger": "^4.1.1",
|
||||
"automattic/wordbless": "@dev"
|
||||
},
|
||||
"suggest": {
|
||||
@ -52,7 +52,7 @@
|
||||
"link-template": "https://github.com/Automattic/jetpack-waf/compare/v${old}...v${new}"
|
||||
},
|
||||
"branch-alias": {
|
||||
"dev-trunk": "0.12.x-dev"
|
||||
"dev-trunk": "0.16.x-dev"
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
@ -121,9 +121,24 @@ class REST_Controller {
|
||||
|
||||
// Share Data
|
||||
if ( isset( $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] ) ) {
|
||||
// If a user disabled the regular share we should disable the debug share data option.
|
||||
if ( false === $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] ) {
|
||||
update_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
|
||||
}
|
||||
|
||||
update_option( Waf_Runner::SHARE_DATA_OPTION_NAME, (bool) $request[ Waf_Runner::SHARE_DATA_OPTION_NAME ] );
|
||||
}
|
||||
|
||||
// Share Debug Data
|
||||
if ( isset( $request[ Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME ] ) ) {
|
||||
// If a user toggles the debug share we should enable the regular share data option.
|
||||
if ( true === $request[ Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME ] ) {
|
||||
update_option( Waf_Runner::SHARE_DATA_OPTION_NAME, true );
|
||||
}
|
||||
|
||||
update_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, (bool) $request[ Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME ] );
|
||||
}
|
||||
|
||||
// Brute Force Protection
|
||||
if ( isset( $request['brute_force_protection'] ) ) {
|
||||
$enable_brute_force = (bool) $request['brute_force_protection'];
|
||||
|
@ -61,9 +61,10 @@ class Waf_Constants {
|
||||
*/
|
||||
public static function define_killswitch() {
|
||||
if ( ! defined( 'DISABLE_JETPACK_WAF' ) ) {
|
||||
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
|
||||
$is_atomic = ( new Host() )->is_atomic_platform();
|
||||
define( 'DISABLE_JETPACK_WAF', $is_wpcom || $is_atomic );
|
||||
$is_wpcom = defined( 'IS_WPCOM' ) && IS_WPCOM;
|
||||
$is_atomic = ( new Host() )->is_atomic_platform();
|
||||
$is_atomic_on_jn = defined( 'IS_ATOMIC_JN' ) ?? IS_ATOMIC_JN;
|
||||
define( 'DISABLE_JETPACK_WAF', $is_wpcom || ( $is_atomic && ! $is_atomic_on_jn ) );
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,9 +87,21 @@ class Waf_Constants {
|
||||
*/
|
||||
public static function define_share_data() {
|
||||
if ( ! defined( 'JETPACK_WAF_SHARE_DATA' ) ) {
|
||||
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
|
||||
$share_data_option = false;
|
||||
if ( function_exists( 'get_option' ) ) {
|
||||
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
|
||||
}
|
||||
|
||||
define( 'JETPACK_WAF_SHARE_DATA', $share_data_option );
|
||||
}
|
||||
if ( ! defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) ) {
|
||||
$share_debug_data_option = false;
|
||||
if ( function_exists( 'get_option' ) ) {
|
||||
$share_debug_data_option = get_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
|
||||
}
|
||||
|
||||
define( 'JETPACK_WAF_SHARE_DEBUG_DATA', $share_debug_data_option );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -279,7 +279,7 @@ class Waf_Operators {
|
||||
return false;
|
||||
}
|
||||
|
||||
return stripos( $test, $input ) !== false
|
||||
return strpos( $test, $input ) !== false
|
||||
? $input
|
||||
: false;
|
||||
}
|
||||
|
@ -146,6 +146,22 @@ class Waf_Request {
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of a specific header that was sent with this request
|
||||
*
|
||||
* @param string $name The name of the header to retrieve.
|
||||
* @return string
|
||||
*/
|
||||
public function get_header( $name ) {
|
||||
$name = $this->normalize_header_name( $name );
|
||||
foreach ( $this->get_headers() as list( $header_name, $header_value ) ) {
|
||||
if ( $header_name === $name ) {
|
||||
return $header_value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Change a header name to all-lowercase and replace spaces and underscores with dashes.
|
||||
*
|
||||
@ -192,7 +208,9 @@ class Waf_Request {
|
||||
$uri = isset( $_SERVER['REQUEST_URI'] ) ? filter_var( wp_unslash( $_SERVER['REQUEST_URI'] ), FILTER_DEFAULT ) : '/';
|
||||
if ( false !== strpos( $uri, '?' ) ) {
|
||||
// remove the query string (we'll pull it from elsewhere later)
|
||||
$uri = substr( $uri, 0, strpos( $uri, '?' ) );
|
||||
$uri = urldecode( substr( $uri, 0, strpos( $uri, '?' ) ) );
|
||||
} else {
|
||||
$uri = urldecode( $uri );
|
||||
}
|
||||
$query_string = isset( $_SERVER['QUERY_STRING'] ) ? '?' . filter_var( wp_unslash( $_SERVER['QUERY_STRING'] ), FILTER_DEFAULT ) : '';
|
||||
if ( 1 === preg_match( '/^https?:\/\//', $uri ) ) {
|
||||
@ -253,6 +271,24 @@ class Waf_Request {
|
||||
return $this->get_url()[1];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the basename part of the request
|
||||
*
|
||||
* @example for 'https://wordpress.com/some/page.php?id=5', return 'page.php'
|
||||
* @return string
|
||||
*/
|
||||
public function get_basename() {
|
||||
// Get the filename part of the request
|
||||
$filename = $this->get_filename();
|
||||
// Normalize slashes
|
||||
$filename = str_replace( '\\', '/', $filename );
|
||||
// Remove trailing slashes
|
||||
$filename = rtrim( $filename, '/' );
|
||||
// Return the basename
|
||||
$offset = strrpos( $filename, '/' );
|
||||
return $offset !== false ? substr( $filename, $offset + 1 ) : $filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the query string. If present, it will be prefixed with '?'. Otherwise, it will be an empty string.
|
||||
*
|
||||
@ -296,6 +332,12 @@ class Waf_Request {
|
||||
* @return array<string, mixed|array>
|
||||
*/
|
||||
public function get_post_vars() {
|
||||
// Attempt to decode JSON requests.
|
||||
if ( strpos( $this->get_header( 'content-type' ), 'application/json' ) !== false ) {
|
||||
$decoded_json = json_decode( $this->get_body(), true ) ?? array();
|
||||
return flatten_array( $decoded_json, 'json', true );
|
||||
}
|
||||
|
||||
return flatten_array( $_POST );
|
||||
}
|
||||
|
||||
|
@ -16,9 +16,10 @@ use Automattic\Jetpack\Waf\Brute_Force_Protection\Brute_Force_Protection;
|
||||
*/
|
||||
class Waf_Runner {
|
||||
|
||||
const WAF_MODULE_NAME = 'waf';
|
||||
const MODE_OPTION_NAME = 'jetpack_waf_mode';
|
||||
const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data';
|
||||
const WAF_MODULE_NAME = 'waf';
|
||||
const MODE_OPTION_NAME = 'jetpack_waf_mode';
|
||||
const SHARE_DATA_OPTION_NAME = 'jetpack_waf_share_data';
|
||||
const SHARE_DEBUG_DATA_OPTION_NAME = 'jetpack_waf_share_debug_data';
|
||||
|
||||
/**
|
||||
* Run the WAF
|
||||
@ -31,6 +32,7 @@ class Waf_Runner {
|
||||
}
|
||||
Waf_Constants::define_mode();
|
||||
Waf_Constants::define_share_data();
|
||||
|
||||
if ( ! self::is_allowed_mode( JETPACK_WAF_MODE ) ) {
|
||||
return;
|
||||
}
|
||||
@ -96,6 +98,10 @@ class Waf_Runner {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( defined( 'IS_ATOMIC_JN' ) && IS_ATOMIC_JN ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do not run in the WPCOM context
|
||||
if ( ( new Host() )->is_wpcom_simple() ) {
|
||||
return false;
|
||||
@ -159,7 +165,9 @@ class Waf_Runner {
|
||||
Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_ALLOW_LIST_OPTION_NAME ),
|
||||
Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME => get_option( Waf_Rules_Manager::IP_BLOCK_LIST_OPTION_NAME ),
|
||||
self::SHARE_DATA_OPTION_NAME => get_option( self::SHARE_DATA_OPTION_NAME ),
|
||||
self::SHARE_DEBUG_DATA_OPTION_NAME => get_option( self::SHARE_DEBUG_DATA_OPTION_NAME ),
|
||||
'bootstrap_path' => self::get_bootstrap_file_path(),
|
||||
'standalone_mode' => self::get_standalone_mode_status(),
|
||||
'automatic_rules_available' => (bool) self::automatic_rules_available(),
|
||||
'brute_force_protection' => (bool) Brute_Force_Protection::is_enabled(),
|
||||
);
|
||||
@ -175,6 +183,15 @@ class Waf_Runner {
|
||||
return $bootstrap->get_bootstrap_file_path();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF standalone mode status
|
||||
*
|
||||
* @return bool|array True if WAF standalone mode is enabled, false otherwise.
|
||||
*/
|
||||
public static function get_standalone_mode_status() {
|
||||
return defined( 'JETPACK_WAF_RUN' ) && JETPACK_WAF_RUN === 'preload';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WAF File Path
|
||||
*
|
||||
|
@ -278,6 +278,20 @@ class Waf_Runtime {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the headers for logging purposes.
|
||||
*/
|
||||
public function get_request_headers() {
|
||||
$all_headers = getallheaders();
|
||||
$exclude_headers = array( 'Authorization', 'Cookie', 'Proxy-Authorization', 'Set-Cookie' );
|
||||
|
||||
foreach ( $exclude_headers as $header ) {
|
||||
unset( $all_headers[ $header ] );
|
||||
}
|
||||
|
||||
return $all_headers;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write block logs. We won't write to the file if it exceeds 100 mb.
|
||||
*
|
||||
@ -285,10 +299,20 @@ class Waf_Runtime {
|
||||
* @param string $reason Block reason.
|
||||
*/
|
||||
public function write_blocklog( $rule_id, $reason ) {
|
||||
$log_data = array();
|
||||
$log_data['rule_id'] = $rule_id;
|
||||
$log_data['reason'] = $reason;
|
||||
$log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
|
||||
$log_data = array();
|
||||
$log_data['rule_id'] = $rule_id;
|
||||
$log_data['reason'] = $reason;
|
||||
$log_data['timestamp'] = gmdate( 'Y-m-d H:i:s' );
|
||||
$log_data['request_uri'] = isset( $_SERVER['REQUEST_URI'] ) ? \stripslashes( $_SERVER['REQUEST_URI'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$log_data['user_agent'] = isset( $_SERVER['HTTP_USER_AGENT'] ) ? \stripslashes( $_SERVER['HTTP_USER_AGENT'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$log_data['referer'] = isset( $_SERVER['HTTP_REFERER'] ) ? \stripslashes( $_SERVER['HTTP_REFERER'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$log_data['content_type'] = isset( $_SERVER['CONTENT_TYPE'] ) ? \stripslashes( $_SERVER['CONTENT_TYPE'] ) : ''; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.MissingUnslash
|
||||
$log_data['get_params'] = json_encode( $_GET );
|
||||
|
||||
if ( defined( 'JETPACK_WAF_SHARE_DEBUG_DATA' ) && JETPACK_WAF_SHARE_DEBUG_DATA ) {
|
||||
$log_data['post_params'] = json_encode( $_POST );
|
||||
$log_data['headers'] = $this->get_request_headers();
|
||||
}
|
||||
|
||||
if ( defined( 'JETPACK_WAF_SHARE_DATA' ) && JETPACK_WAF_SHARE_DATA ) {
|
||||
$file_path = JETPACK_WAF_DIR . '/waf-blocklog';
|
||||
@ -495,7 +519,7 @@ class Waf_Runtime {
|
||||
);
|
||||
break;
|
||||
case 'request_basename':
|
||||
$value = basename( $this->request->get_filename() );
|
||||
$value = $this->request->get_basename();
|
||||
break;
|
||||
case 'request_body':
|
||||
$value = $this->request->get_body();
|
||||
|
@ -140,9 +140,10 @@ class Waf_Standalone_Bootstrap {
|
||||
|
||||
$autoloader_file = $this->locate_autoloader_file();
|
||||
|
||||
$bootstrap_file = $this->get_bootstrap_file_path();
|
||||
$mode_option = get_option( Waf_Runner::MODE_OPTION_NAME, false );
|
||||
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
|
||||
$bootstrap_file = $this->get_bootstrap_file_path();
|
||||
$mode_option = get_option( Waf_Runner::MODE_OPTION_NAME, false );
|
||||
$share_data_option = get_option( Waf_Runner::SHARE_DATA_OPTION_NAME, false );
|
||||
$share_debug_data_option = get_option( Waf_Runner::SHARE_DEBUG_DATA_OPTION_NAME, false );
|
||||
|
||||
// phpcs:disable WordPress.PHP.DevelopmentFunctions
|
||||
$code = "<?php\n"
|
||||
@ -150,6 +151,7 @@ class Waf_Standalone_Bootstrap {
|
||||
. "if ( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF ) return;\n"
|
||||
. sprintf( "define( 'JETPACK_WAF_MODE', %s );\n", var_export( $mode_option ? $mode_option : 'silent', true ) )
|
||||
. sprintf( "define( 'JETPACK_WAF_SHARE_DATA', %s );\n", var_export( $share_data_option, true ) )
|
||||
. sprintf( "define( 'JETPACK_WAF_SHARE_DEBUG_DATA', %s );\n", var_export( $share_debug_data_option, true ) )
|
||||
. sprintf( "define( 'JETPACK_WAF_DIR', %s );\n", var_export( JETPACK_WAF_DIR, true ) )
|
||||
. sprintf( "define( 'JETPACK_WAF_WPCONFIG', %s );\n", var_export( JETPACK_WAF_WPCONFIG, true ) )
|
||||
. 'require_once ' . var_export( $autoloader_file, true ) . ";\n"
|
||||
|
@ -11,14 +11,33 @@ namespace Automattic\Jetpack\Waf;
|
||||
* Waf_Transforms class
|
||||
*/
|
||||
class Waf_Transforms {
|
||||
|
||||
/**
|
||||
* Decode a Base64-encoded string.
|
||||
* Decode a Base64-encoded string. This runs the decode without strict mode, to match Modsecurity's 'base64DecodeExt' transform function.
|
||||
*
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function base64_decode_ext( $value ) {
|
||||
return base64_decode( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Characters to match when trimming a string.
|
||||
* Emulates `std::isspace` used by ModSecurity.
|
||||
*
|
||||
* @see https://en.cppreference.com/w/cpp/string/byte/isspace
|
||||
*/
|
||||
const TRIM_CHARS = " \n\r\t\v\f";
|
||||
|
||||
/**
|
||||
* Decode a Base64-encoded string. This runs the decode with strict mode, to match Modsecurity's 'base64Decode' transform function.
|
||||
*
|
||||
* @param string $value value to be decoded.
|
||||
* @return string
|
||||
*/
|
||||
public function base64_decode( $value ) {
|
||||
return base64_decode( $value );
|
||||
return base64_decode( $value, true );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -307,7 +326,7 @@ class Waf_Transforms {
|
||||
* @return string
|
||||
*/
|
||||
public function trim_left( $value ) {
|
||||
return ltrim( $value );
|
||||
return ltrim( $value, self::TRIM_CHARS );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -317,7 +336,7 @@ class Waf_Transforms {
|
||||
* @return string
|
||||
*/
|
||||
public function trim_right( $value ) {
|
||||
return rtrim( $value );
|
||||
return rtrim( $value, self::TRIM_CHARS );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -327,16 +346,58 @@ class Waf_Transforms {
|
||||
* @return string
|
||||
*/
|
||||
public function trim( $value ) {
|
||||
return trim( $value );
|
||||
return trim( $value, self::TRIM_CHARS );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert utf-8 characters to unicode characters
|
||||
* Convert UTF-8 characters to Unicode characters.
|
||||
*
|
||||
* @param string $value value to be encoded.
|
||||
* @return string
|
||||
* This function iterates through each character of the input string, checks the ASCII value,
|
||||
* and converts it to its corresponding Unicode representation. It handles characters that are
|
||||
* represented with 1 to 4 bytes in UTF-8.
|
||||
*
|
||||
* @param string $str The string value to be encoded from UTF-8 to Unicode.
|
||||
* @return string The converted string with Unicode representation.
|
||||
*/
|
||||
public function utf8_to_unicode( $value ) {
|
||||
return preg_replace( '/\\\u(?=[a-f0-9]{4})/', '%u', substr( json_encode( $value ), 1, -1 ) );
|
||||
public function utf8_to_unicode( $str ) {
|
||||
$unicodeStr = '';
|
||||
$strLen = strlen( $str );
|
||||
$i = 0;
|
||||
|
||||
// Iterate through each character of the input string.
|
||||
while ( $i < $strLen ) {
|
||||
// Get the ASCII value of the current character.
|
||||
$value = ord( $str[ $i ] );
|
||||
|
||||
if ( $value < 128 ) {
|
||||
// If the character is in the ASCII range (0-127), directly add it to the Unicode string.
|
||||
$unicodeStr .= chr( $value );
|
||||
++$i;
|
||||
} else {
|
||||
// For characters outside the ASCII range, determine the number of bytes in the UTF-8 representation.
|
||||
$unicodeValue = '';
|
||||
if ( $value >= 192 && $value <= 223 ) {
|
||||
// For characters represented with 2 bytes in UTF-8.
|
||||
$unicodeValue = ( ord( $str[ $i ] ) & 0x1F ) << 6 | ( ord( $str[ $i + 1 ] ) & 0x3F );
|
||||
$i += 2;
|
||||
} elseif ( $value >= 224 && $value <= 239 ) {
|
||||
// For characters represented with 3 bytes in UTF-8.
|
||||
$unicodeValue = ( ord( $str[ $i ] ) & 0x0F ) << 12 | ( ord( $str[ $i + 1 ] ) & 0x3F ) << 6 | ( ord( $str[ $i + 2 ] ) & 0x3F );
|
||||
$i += 3;
|
||||
} elseif ( $value >= 240 && $value <= 247 ) {
|
||||
// For characters represented with 4 bytes in UTF-8.
|
||||
$unicodeValue = ( ord( $str[ $i ] ) & 0x07 ) << 18 | ( ord( $str[ $i + 1 ] ) & 0x3F ) << 12 | ( ord( $str[ $i + 2 ] ) & 0x3F ) << 6 | ( ord( $str[ $i + 3 ] ) & 0x3F );
|
||||
$i += 4;
|
||||
} else {
|
||||
// If the sequence does not match any known UTF-8 pattern, skip to the next character.
|
||||
++$i;
|
||||
continue;
|
||||
}
|
||||
// Convert the Unicode value to a formatted string and append it to the Unicode string.
|
||||
$unicodeStr .= sprintf( '%%u%04X', $unicodeValue );
|
||||
}
|
||||
}
|
||||
|
||||
return strtolower( $unicodeStr );
|
||||
}
|
||||
}
|
||||
|
@ -47,23 +47,25 @@ function wp_unslash( $value ) {
|
||||
* [ "field2[2]", "f" ],
|
||||
* ]
|
||||
*
|
||||
* @param array $array An array that resembles one of the PHP superglobals like $_GET or $_POST.
|
||||
* @param string $key_prefix String that should be prepended to the keys output by this function.
|
||||
* Usually only used internally as part of recursion when flattening a nested array.
|
||||
* @param array $array An array that resembles one of the PHP superglobals like $_GET or $_POST.
|
||||
* @param string $key_prefix String that should be prepended to the keys output by this function.
|
||||
* Usually only used internally as part of recursion when flattening a nested array.
|
||||
* @param bool|null $dot_notation Whether to use dot notation instead of bracket notation.
|
||||
*
|
||||
* @return array{ 0: string, 1: scalar }[] $key_prefix An array of key/value tuples, one for each distinct value in the input array.
|
||||
*/
|
||||
function flatten_array( $array, $key_prefix = '' ) {
|
||||
function flatten_array( $array, $key_prefix = '', $dot_notation = null ) {
|
||||
$return = array();
|
||||
foreach ( $array as $source_key => $source_value ) {
|
||||
$key = ( '' === $key_prefix )
|
||||
// if this is the first level, the key name isn't enclosed in brackets
|
||||
? $source_key
|
||||
// for every level after the first, enclose the key name in brackets.
|
||||
: $key_prefix . '[' . $source_key . ']';
|
||||
$key = $source_key;
|
||||
if ( ! empty( $key_prefix ) ) {
|
||||
$key = $dot_notation ? "$key_prefix.$source_key" : $key_prefix . "[$source_key]";
|
||||
}
|
||||
|
||||
if ( ! is_array( $source_value ) ) {
|
||||
$return[] = array( $key, $source_value );
|
||||
} else {
|
||||
$return = array_merge( $return, flatten_array( $source_value, $key ) );
|
||||
$return = array_merge( $return, flatten_array( $source_value, $key, $dot_notation ) );
|
||||
}
|
||||
}
|
||||
return $return;
|
||||
|
Reference in New Issue
Block a user