diff --git a/src/wp-admin/includes/class-wp-debug-data.php b/src/wp-admin/includes/class-wp-debug-data.php index ceae392fd2d3b..0c3fa045dca2f 100644 --- a/src/wp-admin/includes/class-wp-debug-data.php +++ b/src/wp-admin/includes/class-wp-debug-data.php @@ -615,7 +615,7 @@ private static function get_wp_media(): array { $post_max_size = ini_get( 'post_max_size' ); $upload_max_filesize = ini_get( 'upload_max_filesize' ); $max_file_uploads = ini_get( 'max_file_uploads' ); - $effective = min( wp_convert_hr_to_bytes( $post_max_size ), wp_convert_hr_to_bytes( $upload_max_filesize ) ); + $effective = wp_ini_lesser_quantity( $post_max_size, $upload_max_filesize ); // Add info in Media section. $fields['file_uploads'] = array( diff --git a/src/wp-admin/includes/class-wp-site-health.php b/src/wp-admin/includes/class-wp-site-health.php index dd537296a8655..3c1909fb144f5 100644 --- a/src/wp-admin/includes/class-wp-site-health.php +++ b/src/wp-admin/includes/class-wp-site-health.php @@ -2303,7 +2303,7 @@ public function get_test_file_uploads() { $post_max_size = ini_get( 'post_max_size' ); $upload_max_filesize = ini_get( 'upload_max_filesize' ); - if ( wp_convert_hr_to_bytes( $post_max_size ) < wp_convert_hr_to_bytes( $upload_max_filesize ) ) { + if ( wp_ini_quantity_cmp( $post_max_size, $upload_max_filesize ) < 0 ) { $result['label'] = sprintf( /* translators: 1: post_max_size, 2: upload_max_filesize */ __( 'The "%1$s" value is smaller than "%2$s"' ), @@ -2312,7 +2312,7 @@ public function get_test_file_uploads() { ); $result['status'] = 'recommended'; - if ( 0 === wp_convert_hr_to_bytes( $post_max_size ) ) { + if ( wp_ini_parse_quantity( $post_max_size ) <= 0 ) { $result['description'] = sprintf( '
%s
', sprintf( diff --git a/src/wp-includes/compat-php.php b/src/wp-includes/compat-php.php new file mode 100644 index 0000000000000..af42f34ec6c53 --- /dev/null +++ b/src/wp-includes/compat-php.php @@ -0,0 +1,218 @@ += 0 ? $a : $b; +} + +/** + * Returns smaller of two php.ini directive quantity values. + * + * Example: + * wp_ini_lesser_quantity( '256m', -1 ) === '256m' + * wp_ini_lesser_quantity( '64K', '64') === '64' + * wp_ini_lesser_quantity( 1000, 2000 ) === 1000 + * + * @since 7.0.0 + * + * @param int|string|false $a Quantity value. + * @param int|string|false $b Quantity value. + * @return int|string|false Smaller quantity value. + */ +function wp_ini_lesser_quantity( $a, $b ) { + return wp_ini_quantity_cmp( $a, $b ) <= 0 ? $a : $b; +} + +/** + * Comparator for php.ini quantity values, can be used + * as the callback for functions such as `usort()`. + * + * Example: + * $a < $b => -1 + * $a === $b => 0 + * $a > $b => 1 + * + * @since 7.0.0 + * + * @param int|string|false $a Quantity being compared. + * @param int|string|false $b Quantity against which $a is compared. + * @return -1|0|1 + */ +function wp_ini_quantity_cmp( $a, $b ): int { + return wp_ini_parse_quantity( $a ) <=> wp_ini_parse_quantity( $b ); +} + +/** + * Fallback function to get interpreted size from ini shorthand syntax for + * systems running versions of PHP up to, but not including, 8.2.0. + * + * @see https://www.php.net/manual/en/function.ini-parse-quantity.php + * @see https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes + * + * @since 7.0.0 + * + * @param string $shorthand Ini shorthand to parse, must be a number followed by an optional + * multiplier. The following multipliers are supported: k/K (1024), + * m/M (1048576), g/G (1073741824). The number can be a decimal, + * hex (prefixed with 0x or 0X), octal (prefixed with 0o, 0O or 0) + * or binary (prefixed with 0b or 0B). + * @return int the interpreted size in bytes as an int. + */ +function ini_parse_quantity_fallback( $shorthand ) { + $end = strlen( $shorthand ); + $at = 0; + $scalar = 0; + + /** Sign of numeric quantity, either positive (1) or negative (-1). */ + $sign = 1; + + /** + * Numeric base of digits determined by string prefix (e.g. "0x" or "0"). + * Must be 8 for octal, 10 for decimal, or 16 for hexadecimal. + */ + $base = 10; + + // Trim leading whitespace from the value. + $at += strspn( $shorthand, " \t\n\r\v\f", $at ); + if ( $at >= $end ) { + return $scalar; + } + + // Handle optional sign indicator. + switch ( $shorthand[ $at ] ) { + case '+': + $at++; + break; + + case '-': + $sign = -1; + $at++; + break; + } + + // Determine base for digit conversion, if not decimal. + $base_a = $shorthand[ $at ] ?? ''; + $base_b = $shorthand[ $at + 1 ] ?? ''; + + if ( '0' === $base_a && ( 'x' === $base_b || 'X' === $base_b ) ) { + $base = 16; + $at += 2; + } else if ( '0' === $base_a && '0' <= $base_b && $base_b <= '9' ) { + $base = 8; + $at += 1; + } + + // Trim leading zeros from the amount. + $at += strspn( $shorthand, '0', $at ); + + // Trap explicitly only the numeric digits for parsing to avoid PHP parsing strings like “1e5.” + $digits = 8 === $base ? '01234567' : ( 10 === $base ? '0123456789' : '0123456789abcdefABCDEF' ); + $digit_length = strspn( $shorthand, $digits, $at ); + $scalar = intval( substr( $shorthand, $at, $digit_length ), $base ); + + /* + * The internal call to `strtoll()` clamps its return value when the + * parsed value would lead to overflow, so recreate that here. + */ + if ( $sign > 0 && $scalar >= PHP_INT_MAX ) { + $scalar = PHP_INT_MAX; + } else if ( $sign < 0 && $scalar <= PHP_INT_MIN ) { + $scalar = PHP_INT_MIN; + } + + /* + * Do not use WP constants here (GB_IN_BYTES, MB_IN_BYTES, KB_IN_BYTES) + * since they are re-definable; PHP shorthand values are hard-coded + * in PHP itself and stay the same regardless of these constants. Also, + * this file loads before these constants are defined. + * + * Note that it’s possible to overflow here, as happens in PHP itself. + * Overflow results will likely not match PHP’s value, but will likely + * break in most cases anyway and so leaving this loose is the best + * that can be done without PHP reporting the internal values. + */ + switch ( $shorthand[ $end - 1 ] ) { + case 'g': + case 'G': + $scalar *= 1073741824; // 1024^3 + break; + + case 'm': + case 'M': + $scalar *= 1048576; // 1024^2 + break; + + case 'k': + case 'K': + $scalar *= 1024; + break; + } + + /** + * Since the overflow behavior is not reproduced here, any negative + * value will report as `-1`, which normalizes negative values for + * more consistent handling inside of plugin code, while large values + * are capped at the max integer value. + * + * These values would be wrong, they are also undefined behavior in + * PHP, so they are also not wrong in any specific way. This function + * only needs to be reliable enough, given that PHP 8.2.0 introduces + * the {@see \ini_parse_quantity()} function natively. + */ + return (int) max( -1, min( $scalar, PHP_INT_MAX ) ); +} diff --git a/src/wp-includes/default-constants.php b/src/wp-includes/default-constants.php index acfc878fb7138..7139312c93d3a 100644 --- a/src/wp-includes/default-constants.php +++ b/src/wp-includes/default-constants.php @@ -39,8 +39,7 @@ function wp_initial_constants() { define( 'WP_START_TIMESTAMP', microtime( true ) ); } - $current_limit = ini_get( 'memory_limit' ); - $current_limit_int = wp_convert_hr_to_bytes( $current_limit ); + $current_limit = ini_get( 'memory_limit' ); // Define memory limits. if ( ! defined( 'WP_MEMORY_LIMIT' ) ) { @@ -56,9 +55,9 @@ function wp_initial_constants() { if ( ! defined( 'WP_MAX_MEMORY_LIMIT' ) ) { if ( false === wp_is_ini_value_changeable( 'memory_limit' ) ) { define( 'WP_MAX_MEMORY_LIMIT', $current_limit ); - } elseif ( -1 === $current_limit_int || $current_limit_int > 256 * MB_IN_BYTES ) { + } elseif ( wp_ini_quantity_cmp( $current_limit, '256M' ) > 0 ) { define( 'WP_MAX_MEMORY_LIMIT', $current_limit ); - } elseif ( wp_convert_hr_to_bytes( WP_MEMORY_LIMIT ) > 256 * MB_IN_BYTES ) { + } elseif ( wp_ini_quantity_cmp( WP_MEMORY_LIMIT, '256M' ) > 0 ) { define( 'WP_MAX_MEMORY_LIMIT', WP_MEMORY_LIMIT ); } else { define( 'WP_MAX_MEMORY_LIMIT', '256M' ); @@ -66,8 +65,7 @@ function wp_initial_constants() { } // Set memory limits. - $wp_limit_int = wp_convert_hr_to_bytes( WP_MEMORY_LIMIT ); - if ( -1 !== $current_limit_int && ( -1 === $wp_limit_int || $wp_limit_int > $current_limit_int ) ) { + if ( wp_ini_quantity_cmp( WP_MEMORY_LIMIT, $current_limit ) > 0 ) { ini_set( 'memory_limit', WP_MEMORY_LIMIT ); } diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index 9cdeef75788f2..62d1f1183a619 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -7845,16 +7845,15 @@ function wp_raise_memory_limit( $context = 'admin' ) { return false; } - $current_limit = ini_get( 'memory_limit' ); - $current_limit_int = wp_convert_hr_to_bytes( $current_limit ); + $current_limit = ini_get( 'memory_limit' ); - if ( -1 === $current_limit_int ) { + // If we're already set to an unlimited value there's no higher limit to set. + if ( wp_ini_parse_quantity( $current_limit ) <= 0 ) { return false; } - $wp_max_limit = WP_MAX_MEMORY_LIMIT; - $wp_max_limit_int = wp_convert_hr_to_bytes( $wp_max_limit ); - $filtered_limit = $wp_max_limit; + $wp_max_limit = WP_MAX_MEMORY_LIMIT; + $filtered_limit = $wp_max_limit; switch ( $context ) { case 'admin': @@ -7929,23 +7928,19 @@ function wp_raise_memory_limit( $context = 'admin' ) { break; } - $filtered_limit_int = wp_convert_hr_to_bytes( $filtered_limit ); + // Set the memory limit to the greatest of all the filtered value, the MAX limit, and the current limit. + $new_limit = wp_ini_greater_quantity( $current_limit, WP_MAX_MEMORY_LIMIT ); + $new_limit = wp_ini_greater_quantity( $filtered_limit, $new_limit ); - if ( -1 === $filtered_limit_int || ( $filtered_limit_int > $wp_max_limit_int && $filtered_limit_int > $current_limit_int ) ) { - if ( false !== ini_set( 'memory_limit', $filtered_limit ) ) { - return $filtered_limit; - } else { - return false; - } - } elseif ( -1 === $wp_max_limit_int || $wp_max_limit_int > $current_limit_int ) { - if ( false !== ini_set( 'memory_limit', $wp_max_limit ) ) { - return $wp_max_limit; - } else { - return false; - } + // If we're already set at the greatest limit we don't need to change it. + if ( 0 === wp_ini_quantity_cmp( $new_limit, $current_limit ) ) { + return false; } - return false; + // Otherwise attempt to set the new limit and return the new value if it succeeded. + return false !== ini_set( 'memory_limit', $new_limit ) + ? $new_limit + : false; } /** diff --git a/src/wp-includes/load.php b/src/wp-includes/load.php index 90318acdddcb4..627d1e01f545b 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -1677,6 +1677,7 @@ function is_ssl() { * * @since 2.3.0 * @since 4.6.0 Moved from media.php to load.php. + * @deprecated 6.1.0 Use wp_ini_parse_quantity() or wp_hr_bytes() instead. * * @link https://www.php.net/manual/en/function.ini-get.php * @link https://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes @@ -1685,6 +1686,19 @@ function is_ssl() { * @return int An integer byte value. */ function wp_convert_hr_to_bytes( $value ) { + _deprecated_function( __FUNCTION__, '6.1.0', 'wp_ini_parse_quantity' ); + return wp_hr_bytes( $value ); +} + +/** + * Parses a "human-readable" byte value into an integer. + * + * @since 6.1.0 + * + * @param string $value Human-readable description of a byte size + * @return int An integer byte value. + */ +function wp_hr_bytes( $value ) { $value = strtolower( trim( $value ) ); $bytes = (int) $value; diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 6933ad69957e2..9ef3dcf65899f 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -4185,8 +4185,9 @@ function wp_expand_dimensions( $example_width, $example_height, $max_width, $max * @return int Allowed upload size. */ function wp_max_upload_size() { - $u_bytes = wp_convert_hr_to_bytes( ini_get( 'upload_max_filesize' ) ); - $p_bytes = wp_convert_hr_to_bytes( ini_get( 'post_max_size' ) ); + $upload_max_filesize = ini_get( 'upload_max_filesize' ); + $post_max_size = ini_get( 'post_max_size' ); + $max_upload = wp_ini_lesser_quantity( $upload_max_filesize, $post_max_size ); /** * Filters the maximum upload size allowed in php.ini. @@ -4197,7 +4198,12 @@ function wp_max_upload_size() { * @param int $u_bytes Maximum upload filesize in bytes. * @param int $p_bytes Maximum size of POST data in bytes. */ - return apply_filters( 'upload_size_limit', min( $u_bytes, $p_bytes ), $u_bytes, $p_bytes ); + return apply_filters( + 'upload_size_limit', + wp_ini_parse_quantity( $max_upload ), + wp_ini_parse_quantity( $upload_max_filesize ), + wp_ini_parse_quantity( $post_max_size ) + ); } /** diff --git a/src/wp-settings.php b/src/wp-settings.php index adaa0b161c3f6..0145a028e2741 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -34,6 +34,7 @@ require ABSPATH . WPINC . '/version.php'; require ABSPATH . WPINC . '/compat-utf8.php'; require ABSPATH . WPINC . '/compat.php'; +require ABSPATH . WPINC . '/compat-php.php'; require ABSPATH . WPINC . '/load.php'; // Check the server requirements. diff --git a/tests/phpunit/tests/load/wpConvertHrToBytes.php b/tests/phpunit/tests/load/wpConvertHrToBytes.php index 62b3fb05e1f8d..6ff51799d0b7e 100644 --- a/tests/phpunit/tests/load/wpConvertHrToBytes.php +++ b/tests/phpunit/tests/load/wpConvertHrToBytes.php @@ -44,7 +44,15 @@ public function data_wp_convert_hr_to_bytes() { array( '128m', 134217728 ), array( '256M', 268435456 ), array( '1g', 1073741824 ), - array( '128m ', 134217728 ), // Leading/trailing whitespace gets trimmed. + + /** + * Leading/trailing whitespace gets trimmed. + * Note that this is not the value that PHP uses internally. + * PHP interprets the value as 128, not 128 MiB. + * + * @see wp_ini_parse_quantity() + */ + array( '128m ', 134217728 ), array( '1024', 1024 ), // No letter will be interpreted as integer value. // Edge cases. diff --git a/tests/phpunit/tests/php-compat/ini_parse_quantity.php b/tests/phpunit/tests/php-compat/ini_parse_quantity.php new file mode 100644 index 0000000000000..eb19c157ede4c --- /dev/null +++ b/tests/phpunit/tests/php-compat/ini_parse_quantity.php @@ -0,0 +1,211 @@ +assertSame( + ini_parse_quantity( $ini_value ), + wp_ini_parse_quantity( $ini_value ), + 'Failed to match PHP’s internal reporting.' + ); + } else { + $this->assertSame( + $expected, + wp_ini_parse_quantity( $ini_value ), + 'Failed to match expected quantity.' + ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_ini_shorthand_values() { + return array( + // Empty, unset, and unlimited values. + array( false, 0 ), + array( '', 0 ), + array( '-1', -1 ), + + // Already-parsed values. + array( 15, 15 ), + array( -1543, -1543 ), + + // Invalid data types. + array( true, 0 ), + array( array( 1, 2, 3 ), 0 ), + array( new stdClass(), 0 ), + + // Non-suffixes clamp. + array( 8 === PHP_INT_SIZE ? '9223372036854775808' : '2147483648', PHP_INT_MAX ), + array( 8 === PHP_INT_SIZE ? '-9223372036854775809' : '-2147483649', -1 ), + + // Suffixes might overflow. + array( 8 === PHP_INT_SIZE ? '9223372036854775808g' : '2147483648g', PHP_INT_MAX ), + array( 8 === PHP_INT_SIZE ? '-9223372036854775809g' : '-2147483649g', -1 ), + + // Decimal integer input. + array( '0', 0 ), + array( '100', 100 ), + array( '-14', -1 ), + + // Octal integer input. + array( '0100', 64 ), + array( '-0654', -1 ), + + // Hex input. + array( '0x14', 20 ), + array( '0X14', 20 ), + array( '-0xAA', -1 ), + + // Size suffixes. + array( '1g', 1073741824 ), + array( '1gb', 0 ), + array( '32k', 32768 ), + array( '64K', 65536 ), + array( '07k', 7168 ), + array( '-0xF3d7m', -65455259648 ), + array( '128m', 134217728 ), + array( '128m ', 128 ), + array( '128mk', 131072 ), + array( '128km', 134217728 ), + array( '1.28 kmg', 1073741824 ), + array( '256M', 268435456 ), + + // Leading characters. + array( ' 68', 68 ), + array( '+1', 1 ), + array( ' -0xdeadbeef', -1 ), + array( ' 00000077', 63 ), + + // Things that don't look valid but are still possible. + array( '', 0 ), + array( '3km', 3145728 ), + array( '1mg', 1073741824 ), + array( 'boat', 0 ), + array( '-14chairsk', -1 ), + array( '0xt', 0 ), + array( '++3', 0 ), + array( '0x5ome 🅰🅱🅲 attack', 5120 ), + ); + } + + /** + * Ensures that INI quantity values compare properly. + * + * @ticket 55635 + * + * @dataProvider data_compared_ini_values + * + * @param string $a First INI shorthand value to compare. + * @param '<'|'='|'>' $cmp Relationship of first to second value. + * @param string $b Second INI shorthand value to compare. + */ + public function test_compares_properly( string $a, string $cmp, string $b ) { + switch ( $cmp ) { + case '<': + $this->assertSame( + -1, + wp_ini_quantity_cmp( $a, $b ), + 'Should have determined that the first value is smaller.' + ); + + $this->assertSame( + $a, + wp_ini_lesser_quantity( $a, $b ), + 'Should have returned the first value as the smaller of the two.' + ); + + $this->assertSame( + $b, + wp_ini_greater_quantity( $a, $b ), + 'Should have returned the second value as the greater of the two.' + ); + + break; + + case '=': + $this->assertSame( + 0, + wp_ini_quantity_cmp( $a, $b ), + 'Should have determined that the values are equal.' + ); + + $this->assertSame( + $a, + wp_ini_lesser_quantity( $a, $b ), + 'Should have returned the first value when they are equal.' + ); + + $this->assertSame( + $a, + wp_ini_greater_quantity( $a, $b ), + 'Should have returned the first value when they are equal.' + ); + + break; + + case '>': + $this->assertSame( + 1, + wp_ini_quantity_cmp( $a, $b ), + 'Should have determined that the second value is greater.' + ); + + $this->assertSame( + $b, + wp_ini_lesser_quantity( $a, $b ), + 'Should have returned the second value as the smaller of the two.' + ); + + $this->assertSame( + $a, + wp_ini_greater_quantity( $a, $b ), + 'Should have returned the first value as the greater of the two.' + ); + + break; + } + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_compared_ini_values() { + return array( + // No limit vs. unlimited. + array( '', '=', '-1' ), + array( '-1', '=', '' ), + + // Unlimited vs. hard limit. + array( -1, '>', 1348 ), + array( -1, '>', '1348g' ), + array( '', '>', 1348 ), + array( '', '>', '1348g' ), + array( 0, '>', 1348 ), + array( 0, '>', '1348g' ), + array( false, '>', 1348 ), + array( false, '>', '1348g' ), + ); + } +}