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/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..ab6c301bf1936 100644 --- a/src/wp-includes/load.php +++ b/src/wp-includes/load.php @@ -5,6 +5,8 @@ * @package WordPress */ +require_once __DIR__ . '/php-compat.php'; + /** * Returns the HTTP protocol sent by the server. * @@ -1677,6 +1679,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 +1688,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-includes/php-compat.php b/src/wp-includes/php-compat.php new file mode 100644 index 0000000000000..7e4652003aa15 --- /dev/null +++ b/src/wp-includes/php-compat.php @@ -0,0 +1,290 @@ += 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 + * + * @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 + * + * @param int|string|false $a Quantity being compared. + * @param int|string|false $b Quantity against which $a is compared. + * @return int + */ +function wp_ini_quantity_cmp( $a, $b ) { + $a_scalar = wp_ini_parse_quantity( $a ); + $b_scalar = wp_ini_parse_quantity( $b ); + + if ( $a_scalar === $b_scalar ) { + return 0; + } + + // No limit on $a means it's at least as large as any $b value. + if ( $a_scalar <= 0 ) { + return 1; + } + + // No limit on $b means it's at least as large as any $a value. + if ( $b_scalar <= 0 ) { + return -1; + } + + return $a_scalar > $b_scalar ? 1 : -1; +} + +/** + * 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 ); + + /** + * Numeric values for scanned digits. + * + * These are used to determine the decimal value the digit + * represents and whether it's an allowed character in + * the given base. It's allowed if its value is less + * than the base: e.g. '7' is allowed in octal (base 8) + * but '8' and '9' aren't because they are greater than 8. + * + * @var array $digits + */ + $digits = array( + '0' => 0, + '1' => 1, + '2' => 2, + '3' => 3, + '4' => 4, + '5' => 5, + '6' => 6, + '7' => 7, + '8' => 8, + '9' => 9, + 'A' => 10, + 'a' => 10, + 'B' => 11, + 'b' => 11, + 'C' => 12, + 'c' => 12, + 'D' => 13, + 'd' => 13, + 'E' => 14, + 'e' => 14, + 'F' => 15, + 'f' => 15, + ); + + /* + * Build the scalar value by consuming the next sequence of contiguous digits. + * It looks like this could be replaced with native functions to parse digits, + * but the behavior mimics peculiarities internal to PHP when parsing overflowed + * digits, inasmuch as is possible from user-space PHP code. + */ + for ( ; $at < $end; $at++ ) { + $c = $shorthand[ $at ]; + + /* + * Only digits recognized in this base system can be used. + * Once any unrecognized digits are found, abort and move + * on to the next step in parsing the size suffix. + */ + if ( ! isset( $digits[ $c ] ) || $digits[ $c ] >= $base ) { + break; + } + + /* + * This is the step that computes the integer as new digits arrive. + * + * Example: + * 4 = (0 * 10) + 4 + * 45 = ((0 * 10 + 4) * 10) + 5 + * 458 = ((0 * 10 + 4) * 10 + 5) * 10 + 8 + */ + $scalar = $scalar * $base + $digits[ $c ]; + + /* + * There’s nothing to do one having reached the maximum + * storable size, so bail. This comparison works because + * the int value is cast into a float once having crossed + * beyond the maximum integer. + */ + if ( + ( $sign > 0 && $scalar > PHP_INT_MAX ) || + ( $sign < 0 && $scalar > -PHP_INT_MIN ) + ) { + break; + } + } + + // Clamp the parsed digits to an integer value as PHP does internally. + if ( $sign > 0 && $scalar >= PHP_INT_MAX ) { + $scalar = PHP_INT_MAX; + } else if ( $sign < 0 && $scalar >= -PHP_INT_MIN ) { + $scalar = PHP_INT_MIN; + } else if ( $sign < 0 ) { + $scalar = -$scalar; + } + + /* + * 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; + } + + return (int) $scalar; +} 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..5f6233615c466 --- /dev/null +++ b/tests/phpunit/tests/php-compat/ini_parse_quantity.php @@ -0,0 +1,133 @@ +assertEquals( 0, wp_ini_parse_quantity( false ) ); + } + + public function test_absent_limit_is_no_limit() { + $this->assertEquals( 0, wp_ini_parse_quantity( '' ) ); + } + + public function test_unlimited_is_unlimited() { + $this->assertEquals( -1, wp_ini_parse_quantity( '-1' ) ); + } + + public function test_unlimited_is_same_as_missing_limit() { + $this->assertEqual( '', wp_ini_greater_quantity( '', '-1' ) ); + $this->assertEqual( '-1', wp_ini_greater_quantity( '-1', '' ) ); + $this->assertEqual( '', wp_ini_lesser_quantity( '', '-1' ) ); + $this->assertEqual( '-1', wp_ini_lesser_quantity( '-1', '' ) ); + } + + public function test_unlimited_is_greater_than_hard_limit() { + $this->assertEqual( 1, wp_ini_quantity_cmp( -1, 1348 ) ); + $this->assertEqual( 1, wp_ini_quantity_cmp( -1, '1348g' ) ); + $this->assertEqual( 1, wp_ini_quantity_cmp( '', 1348 ) ); + $this->assertEqual( 1, wp_ini_quantity_cmp( '', '1348g' ) ); + $this->assertEqual( 1, wp_ini_quantity_cmp( 0, 1348 ) ); + $this->assertEqual( 1, wp_ini_quantity_cmp( 0, '1348g' ) ); + $this->assertEqual( 1, wp_ini_quantity_cmp( false, 1348 ) ); + $this->assertEqual( 1, wp_ini_quantity_cmp( false, '1348g' ) ); + } + + public function test_invalid_data_is_no_limit() { + $this->assertEquals( 0, wp_ini_parse_quantity( true ) ); + $this->assertEquals( 0, wp_ini_parse_quantity( false ) ); + $this->assertEquals( 0, wp_ini_parse_quantity( array( 1, 2, 3 ) ) ); + $this->assertEquals( 0, wp_ini_parse_quantity( new stdClass ) ); + } + + public function test_returns_already_parsed_values() { + $this->assertEquals( 15, wp_ini_parse_quantity( 15 ) ); + $this->assertEquals( -1543, wp_ini_parse_quantity( -1543 ) ); + } + + public function test_clamped_to_max_int_before_suffix() { + if ( IS_32_BIT_SYSTEM ) { + $this->assertEquals( PHP_INT_MAX, wp_ini_parse_quantity( '2147483648' ) ); + $this->assertEquals( PHP_INT_MIN, wp_ini_parse_quantity( '-2147483649' ) ); + } else { + $this->assertEquals( PHP_INT_MAX, wp_ini_parse_quantity( '9223372036854775808' ) ); + $this->assertEquals( PHP_INT_MIN, wp_ini_parse_quantity( '-9223372036854775809' ) ); + } + } + + public function test_suffix_math_may_overflow() { + if ( IS_32_BIT_SYSTEM ) { + $this->assertNotEquals( PHP_INT_MAX, wp_ini_parse_quantity( '2147483648g' ) ); + $this->assertNotEquals( PHP_INT_MIN, wp_ini_parse_quantity( '-2147483648g' ) ); + } else { + $this->assertNotEquals( PHP_INT_MAX, wp_ini_parse_quantity( '9223372036854775807g' ) ); + $this->assertNotEquals( PHP_INT_MIN, wp_ini_parse_quantity( '-9223372036854775807g' ) ); + } + } + + /** + * Tests converting numeric php.ini directive strings into their scalar equivalents. + * + * @ticket 55635 + * + * @dataProvider data_php_numeric_strings + * + * @param $value + * @param $expected + */ + public function test_parse_matches_php_internal_value( $value, $expected ) { + $this->assertEquals( $expected, wp_ini_parse_quantity( $value ) ); + } + + public function data_php_numeric_strings() { + return array( + // Decimal integer input. + array( '0', 0 ), + array( '100', 100 ), + array( '-14', -14 ), + + // Octal integer input. + array( '0100', 64 ), + array( '-0654', -428 ), + + // Hex input. + array( '0x14', 20 ), + array( '0X14', 20 ), + array( '-0xAA', -170 ), + + // 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', -3735928559 ), + 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', -14336 ), + array( '0xt', 0 ), + array( '++3', 0 ), + array( '0x5ome 🅰🅱🅲 attack', 5120 ), + ); + } +}