Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion src/wp-includes/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -1460,14 +1460,44 @@ function is_multisite() {
/**
* Converts a value to non-negative integer.
*
* Values outside of the range of an integer are clamped to `PHP_INT_MAX`.
*
* @since 2.5.0
* @since 7.1.0 Out of range values are clamped to `PHP_INT_MAX` instead of
* being cast to an undefined value.
*
* @param mixed $maybeint Data you wish to have converted to a non-negative integer.
* @return int A non-negative integer.
* @phpstan-return non-negative-int
*/
function absint( $maybeint ): int {
return abs( (int) $maybeint );
/*

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about something like this to short-circuit before even trying to go further?

Suggested change
/*
if ( ! is_float( $maybeint ) && ! is_int( $maybeint ) && ! is_numeric( $maybeint ) && ! is_bool( $maybeint ) ) {
return 0;
}
/*

Including bool here since true passed to abs() is 1, and false is 0.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be a BC break, because that would return 0 for a string that PHP would have parsed into an int.

For example, send 999 stuff through absint() - https://3v4l.org/3M0Zf

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right you are. OK, in that case, what about just short-circuiting if it isn't a scalar?

Suggested change
/*
if ( ! is_scalar( $maybeint ) ) {
return 0;
}
/*

* Casting an out of range float to an integer is undefined, and raises a
* warning as of PHP 8.5. Numeric strings do not need this treatment, as
* casting those already clamps to PHP_INT_MAX or PHP_INT_MIN.
*/
if ( is_float( $maybeint ) ) {
if ( is_nan( $maybeint ) ) {
return 0;
}

if ( abs( $maybeint ) >= (float) PHP_INT_MAX ) {
return PHP_INT_MAX;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this trigger a warning as is done in PHP 8.5+ when attempting to cast a float larger than PHP_INT_MAX to an int?

}
}

$intval = (int) $maybeint;

/*
* PHP_INT_MIN is the only integer whose absolute value is larger than
* PHP_INT_MAX, which would make abs() return a float. Clamping it to
* -PHP_INT_MAX first keeps the result within the integer range.
*/
if ( PHP_INT_MIN === $intval ) {
return PHP_INT_MAX;
}

return abs( $intval );
}

/**
Expand Down
127 changes: 115 additions & 12 deletions tests/phpunit/tests/functions/absint.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Tests_Functions_Absint extends WP_UnitTestCase {

/**
* @ticket 60101
* @ticket 65826
*
* @dataProvider data_absint
*/
Expand All @@ -28,54 +29,156 @@ public function test_absint( $test_value, $expected_value ) {
*/
public function data_absint() {
return array(
'1 int' => array(
'zero' => array(
'test_value' => 0,
'expected_value' => 0,
),
'1 int' => array(
'test_value' => 1,
'expected_value' => 1,
),
'1 string' => array(
'1 string' => array(
'test_value' => '1',
'expected_value' => 1,
),
'-1 int' => array(
'-1 int' => array(
'test_value' => -1,
'expected_value' => 1,
),
'-1 string' => array(
'-1 string' => array(
'test_value' => '-1',
'expected_value' => 1,
),
'9.1 float' => array(
'9.1 float' => array(
'test_value' => 9.1,
'expected_value' => 9,
),
'9.9 float' => array(
'9.9 float' => array(
'test_value' => 9.9,
'expected_value' => 9,
),
'string' => array(
'string' => array(
'test_value' => 'string',
'expected_value' => 0,
),
'string_1' => array(
'string_1' => array(
'test_value' => 'string_1',
'expected_value' => 0,
),
'999_string' => array(
'999_string' => array(
'test_value' => '999_string',
'expected_value' => 999,
),
'99 string with spaces' => array(
'99 string with spaces' => array(
'test_value' => '99 string with spaces',
'expected_value' => 99,
),
'99 array' => array(
'99 array' => array(
'test_value' => array( 99 ),
'expected_value' => 1,
),
'99 string array' => array(
'99 string array' => array(
'test_value' => array( '99' ),
'expected_value' => 1,
),
'PHP_INT_MAX int' => array(
'test_value' => PHP_INT_MAX,
'expected_value' => PHP_INT_MAX,
),
'PHP_INT_MIN int' => array(
'test_value' => PHP_INT_MIN,
'expected_value' => PHP_INT_MAX,
),
// The adjacent value is representable, so it does not need clamping.
'PHP_INT_MIN + 1 int' => array(
'test_value' => PHP_INT_MIN + 1,
'expected_value' => PHP_INT_MAX,
),
'PHP_INT_MAX string' => array(
'test_value' => '9223372036854775807',
'expected_value' => PHP_INT_MAX,
),
'PHP_INT_MIN string' => array(
'test_value' => '-9223372036854775808',
'expected_value' => PHP_INT_MAX,
),
/*
* Casting a numeric string beyond the integer range clamps without
* overflowing, so this needs no special handling. PHP 8.5.0 and 8.5.1
* warned here, which was reverted in PHP 8.5.2.
*/
'PHP_INT_MAX * 1000 string' => array(
'test_value' => '9223372036854775807000',
'expected_value' => PHP_INT_MAX,
),
'out of range negative string' => array(
'test_value' => '-99999999999999999999',
'expected_value' => PHP_INT_MAX,
),
'out of range positive string' => array(
'test_value' => '99999999999999999999',
'expected_value' => PHP_INT_MAX,
),
'out of range float string' => array(
'test_value' => '1.0e30',
'expected_value' => PHP_INT_MAX,
),
'out of range negative float' => array(
'test_value' => -1.0e30,
'expected_value' => PHP_INT_MAX,
),
'out of range positive float' => array(
'test_value' => 1.0e30,
'expected_value' => PHP_INT_MAX,
),
'PHP_INT_MAX as a float' => array(
'test_value' => (float) PHP_INT_MAX,
'expected_value' => PHP_INT_MAX,
),
'PHP_INT_MIN as a float' => array(
'test_value' => (float) PHP_INT_MIN,
'expected_value' => PHP_INT_MAX,
),
/*
* Integer arithmetic silently overflows to a float, which is the most
* likely way an out of range value reaches this function.
*/
'PHP_INT_MAX + 1' => array(
'test_value' => PHP_INT_MAX + 1,
'expected_value' => PHP_INT_MAX,
),
'PHP_INT_MIN - 1' => array(
'test_value' => PHP_INT_MIN - 1,
'expected_value' => PHP_INT_MAX,
),
/*
* Overflowing to exactly 2**64 wraps to 0 when cast, rather than to an
* arbitrary value, so it is easily mistaken for a legitimate result.
*/
'PHP_INT_MAX * 2' => array(
'test_value' => PHP_INT_MAX * 2,
'expected_value' => PHP_INT_MAX,
),
'PHP_INT_MIN * 2' => array(
'test_value' => PHP_INT_MIN * 2,
'expected_value' => PHP_INT_MAX,
),
'in range float' => array(
'test_value' => 2.0 ** 63 - 2048.0,
'expected_value' => 9223372036854773760,
),
'INF' => array(
'test_value' => INF,
'expected_value' => PHP_INT_MAX,
),
'-INF' => array(
'test_value' => -INF,
'expected_value' => PHP_INT_MAX,
),
'NAN' => array(
'test_value' => NAN,
'expected_value' => 0,
),
);
}
}
Loading