diff --git a/src/wp-includes/block-supports/typography.php b/src/wp-includes/block-supports/typography.php index 4f8e32571af73..ffa76d31a329c 100644 --- a/src/wp-includes/block-supports/typography.php +++ b/src/wp-includes/block-supports/typography.php @@ -669,7 +669,7 @@ function wp_get_typography_font_size_value( $preset, $settings = array() ) { * For a - b * log2(), lower values of b will make the curve move towards the minimum faster. * The scale factor is constrained between min and max values. */ - $minimum_font_size_factor = min( max( 1 - 0.075 * log( $preferred_font_size_in_px, 2 ), $default_minimum_font_size_factor_min ), $default_minimum_font_size_factor_max ); + $minimum_font_size_factor = clamp( 1 - 0.075 * log( $preferred_font_size_in_px, 2 ), $default_minimum_font_size_factor_min, $default_minimum_font_size_factor_max ); $calculated_minimum_font_size = round( $preferred_size['value'] * $minimum_font_size_factor, 3 ); // Only use calculated min font size if it's > $minimum_font_size_limit value. diff --git a/src/wp-includes/compat.php b/src/wp-includes/compat.php index 3ac1372fdca1e..414f3906c5bca 100644 --- a/src/wp-includes/compat.php +++ b/src/wp-includes/compat.php @@ -535,6 +535,71 @@ function array_last( array $array ) { // phpcs:ignore Universal.NamingConvention } } +/** + * Throws a ValueError (PHP 8.0+) or an InvalidArgumentException (PHP 7.x) with the given message. + * + * Helper for polyfills that need to throw ValueError but must also run on PHP 7.4. + * + * @ignore + * @since 7.1.0 + * @access private + * + * @param string $message The error message. + * @throws ValueError On PHP 8.0 and later. + * @throws InvalidArgumentException On PHP 7.x as a fallback. + */ +function _wp_throw_value_error( $message ) { + if ( ! class_exists( 'ValueError', false ) ) { + throw new InvalidArgumentException( $message ); + } + + throw new ValueError( $message ); +} + +if ( ! function_exists( 'clamp' ) ) { + /** + * Polyfill for `clamp()` function added in PHP 8.6. + * + * Clamps a value to be within the range of a given minimum and maximum. + * + * If the value is within the bounds, the original value is returned. + * If it is not within the bounds, the closest bound is returned. + * + * @since 7.1.0 + * + * @param mixed $value The value to clamp. + * @param mixed $min The minimum bound. Must be less than or equal to `$max`. + * @param mixed $max The maximum bound. Must be greater than or equal to `$min`. + * @return mixed The clamped value. + * + * @throws ValueError On PHP 8.0+: if `$min` is greater than `$max`, or if `$min` or `$max` is NAN. + * @throws InvalidArgumentException On PHP 7.x: if `$min` is greater than `$max`, or if `$min` or `$max` is NAN. + */ + function clamp( $value, $min, $max ) { + if ( is_float( $min ) && is_nan( $min ) ) { + _wp_throw_value_error( 'clamp(): Argument #2 ($min) cannot be NAN' ); + } + + if ( is_float( $max ) && is_nan( $max ) ) { + _wp_throw_value_error( 'clamp(): Argument #3 ($max) cannot be NAN' ); + } + + if ( $max < $min ) { + _wp_throw_value_error( 'clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)' ); + } + + if ( $value < $min ) { + return $min; + } + + if ( $value > $max ) { + return $max; + } + + return $value; + } +} + // IMAGETYPE_AVIF constant is only defined in PHP 8.x or later. if ( ! defined( 'IMAGETYPE_AVIF' ) ) { define( 'IMAGETYPE_AVIF', 19 ); diff --git a/src/wp-includes/embed.php b/src/wp-includes/embed.php index 3fb8968c7c62c..d944ae1f9962f 100644 --- a/src/wp-includes/embed.php +++ b/src/wp-includes/embed.php @@ -594,7 +594,7 @@ function get_oembed_response_data( $post, $width ) { ) ); - $width = min( max( $min_max_width['min'], $width ), $min_max_width['max'] ); + $width = clamp( $width, $min_max_width['min'], $min_max_width['max'] ); $height = max( (int) ceil( $width / 16 * 9 ), 200 ); $data = array( diff --git a/tests/phpunit/tests/compat/clamp.php b/tests/phpunit/tests/compat/clamp.php new file mode 100644 index 0000000000000..16aeaf19ca9ee --- /dev/null +++ b/tests/phpunit/tests/compat/clamp.php @@ -0,0 +1,280 @@ +assertTrue( function_exists( 'clamp' ) ); + } + + /** + * @ticket 65143 + * + * @dataProvider data_clamp + * + * @param mixed $expected The expected clamped value. + * @param mixed $value The value to clamp. + * @param mixed $min The minimum bound. + * @param mixed $max The maximum bound. + */ + public function test_clamp( $expected, $value, $min, $max ): void { + $this->assertSame( $expected, clamp( $value, $min, $max ) ); + } + + /** + * Data provider for clamp(). + * + * @return array[] + */ + public function data_clamp(): array { + return array( + 'integer within range' => array( + 'expected' => 5, + 'value' => 5, + 'min' => 1, + 'max' => 10, + ), + 'integer below min' => array( + 'expected' => 1, + 'value' => -5, + 'min' => 1, + 'max' => 10, + ), + 'integer above max' => array( + 'expected' => 10, + 'value' => 99, + 'min' => 1, + 'max' => 10, + ), + 'integer equals min' => array( + 'expected' => 1, + 'value' => 1, + 'min' => 1, + 'max' => 10, + ), + 'integer equals max' => array( + 'expected' => 10, + 'value' => 10, + 'min' => 1, + 'max' => 10, + ), + 'min equals max, value matches' => array( + 'expected' => 5, + 'value' => 5, + 'min' => 5, + 'max' => 5, + ), + 'min equals max, value below' => array( + 'expected' => 5, + 'value' => 3, + 'min' => 5, + 'max' => 5, + ), + 'min equals max, value above' => array( + 'expected' => 5, + 'value' => 7, + 'min' => 5, + 'max' => 5, + ), + 'float within range' => array( + 'expected' => 0.5, + 'value' => 0.5, + 'min' => 0.0, + 'max' => 1.0, + ), + 'float below min' => array( + 'expected' => 0.0, + 'value' => -0.5, + 'min' => 0.0, + 'max' => 1.0, + ), + 'float above max' => array( + 'expected' => 1.0, + 'value' => 1.5, + 'min' => 0.0, + 'max' => 1.0, + ), + 'negative range, within' => array( + 'expected' => -5, + 'value' => -5, + 'min' => -10, + 'max' => -1, + ), + 'negative range, below min' => array( + 'expected' => -10, + 'value' => -99, + 'min' => -10, + 'max' => -1, + ), + 'negative range, above max' => array( + 'expected' => -1, + 'value' => 0, + 'min' => -10, + 'max' => -1, + ), + 'zero within range' => array( + 'expected' => 0, + 'value' => 0, + 'min' => -1, + 'max' => 1, + ), + 'mixed int/float, within range' => array( + 'expected' => 5, + 'value' => 5, + 'min' => 0.0, + 'max' => 10.0, + ), + 'INF as value' => array( + 'expected' => 100, + 'value' => INF, + 'min' => 0, + 'max' => 100, + ), + '-INF as value' => array( + 'expected' => 0, + 'value' => -INF, + 'min' => 0, + 'max' => 100, + ), + 'INF as max, value within range' => array( + 'expected' => 50, + 'value' => 50, + 'min' => 0, + 'max' => INF, + ), + 'INF as max, value equals INF' => array( + 'expected' => INF, + 'value' => INF, + 'min' => 0, + 'max' => INF, + ), + 'string within range' => array( + 'expected' => 'l', + 'value' => 'l', + 'min' => 'a', + 'max' => 'z', + ), + 'string below min' => array( + 'expected' => 'e', + 'value' => 'a', + 'min' => 'e', + 'max' => 'z', + ), + 'string above max' => array( + 'expected' => 'p', + 'value' => 'z', + 'min' => 'a', + 'max' => 'p', + ), + ); + } + + /** + * @ticket 65143 + * + * @dataProvider data_clamp_datetime + * + * @param DateTimeImmutable $expected The expected clamped value. + * @param DateTimeImmutable $value The value to clamp. + * @param DateTimeImmutable $min The minimum bound. + * @param DateTimeImmutable $max The maximum bound. + */ + public function test_clamp_with_datetime( $expected, $value, $min, $max ): void { + $this->assertEquals( $expected, clamp( $value, $min, $max ) ); + } + + /** + * Data provider for DateTimeImmutable cases. + * + * @return array[] + */ + public function data_clamp_datetime(): array { + return array( + 'within range' => array( + 'expected' => new DateTimeImmutable( '2025-01-15' ), + 'value' => new DateTimeImmutable( '2025-01-15' ), + 'min' => new DateTimeImmutable( '2025-01-01' ), + 'max' => new DateTimeImmutable( '2025-01-31' ), + ), + 'below min' => array( + 'expected' => new DateTimeImmutable( '2025-01-01' ), + 'value' => new DateTimeImmutable( '2024-12-01' ), + 'min' => new DateTimeImmutable( '2025-01-01' ), + 'max' => new DateTimeImmutable( '2025-01-31' ), + ), + 'above max' => array( + 'expected' => new DateTimeImmutable( '2025-01-31' ), + 'value' => new DateTimeImmutable( '2025-03-01' ), + 'min' => new DateTimeImmutable( '2025-01-01' ), + 'max' => new DateTimeImmutable( '2025-01-31' ), + ), + ); + } + + /** + * @ticket 65143 + * + * Test that clamp() throws when $min is NAN. + */ + public function test_clamp_throws_for_nan_min(): void { + $this->expectException( $this->value_error_class() ); + $this->expectExceptionMessage( 'clamp(): Argument #2 ($min) cannot be NAN' ); + + clamp( 5, NAN, 10 ); + } + + /** + * @ticket 65143 + * + * Test that clamp() throws when $max is NAN. + */ + public function test_clamp_throws_for_nan_max(): void { + $this->expectException( $this->value_error_class() ); + $this->expectExceptionMessage( 'clamp(): Argument #3 ($max) cannot be NAN' ); + + clamp( 5, 0, NAN ); + } + + /** + * @ticket 65143 + * + * Test that clamp() throws when $min is greater than $max. + */ + public function test_clamp_throws_when_min_greater_than_max(): void { + $this->expectException( $this->value_error_class() ); + $this->expectExceptionMessage( 'clamp(): Argument #2 ($min) must be smaller than or equal to argument #3 ($max)' ); + + clamp( 5, 10, 1 ); + } + + /** + * @ticket 65143 + * + * Test that clamp() with a NAN value returns NAN (no exception). + */ + public function test_clamp_with_nan_value_returns_nan(): void { + $result = clamp( NAN, 0, 10 ); + $this->assertNan( $result ); + } + + /** + * Returns the expected exception class for ValueError-equivalent errors. + * + * On PHP 8.0+, _wp_throw_value_error() throws ValueError. + * On PHP 7.x, it falls back to InvalidArgumentException. + * + * @return string The fully qualified exception class name. + */ + private function value_error_class(): string { + return PHP_VERSION_ID >= 80000 ? ValueError::class : InvalidArgumentException::class; + } +}