diff --git a/src/wp-includes/functions.php b/src/wp-includes/functions.php index ac4781d1f817b..a1b8dc934f791 100644 --- a/src/wp-includes/functions.php +++ b/src/wp-includes/functions.php @@ -3639,24 +3639,29 @@ function wp_get_ext_types() { * Wrapper for PHP filesize with filters and casting the result as an integer. * * @since 6.0.0 + * @since 7.1.0 The return value is now always zero or greater. * * @link https://www.php.net/manual/en/function.filesize.php * * @param string $path Path to the file. * @return int The size of the file in bytes, or 0 in the event of an error. + * @phpstan-return non-negative-int */ -function wp_filesize( $path ) { +function wp_filesize( $path ): int { /** * Filters the result of wp_filesize() before the file_exists() PHP function is run. * * @since 6.0.0 + * @since 7.1.0 Negative values are now ignored, being treated the same as null. Numeric values are cast to integers. * - * @param null|int $size The unfiltered value. Returning an int from the callback bypasses the filesize call. + * @param null|int $size The unfiltered value. Returning a non-negative number from the callback bypasses the filesize call. * @param string $path Path to the file. */ $size = apply_filters( 'pre_wp_filesize', null, $path ); - - if ( is_int( $size ) ) { + if ( is_numeric( $size ) ) { + $size = (int) $size; + } + if ( is_int( $size ) && $size >= 0 ) { return $size; } @@ -3666,11 +3671,18 @@ function wp_filesize( $path ) { * Filters the size of the file. * * @since 6.0.0 + * @since 7.1.0 The return value is now always zero or greater. Numeric values are cast to integers. * * @param int $size The result of PHP filesize on the file. * @param string $path Path to the file. */ - return (int) apply_filters( 'wp_filesize', $size, $path ); + $size = apply_filters( 'wp_filesize', $size, $path ); + if ( is_numeric( $size ) ) { + $size = (int) $size; + } else { + $size = 0; + } + return max( 0, $size ); } /** diff --git a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php index 4737ee747a52b..c7a1cfc88bd2c 100644 --- a/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php +++ b/src/wp-includes/rest-api/endpoints/class-wp-rest-attachments-controller.php @@ -1707,7 +1707,7 @@ public function get_item_schema() { $schema['properties']['filesize'] = array( 'description' => __( 'Attachment file size in bytes.' ), - 'type' => 'integer', + 'type' => array( 'integer', 'null' ), 'context' => array( 'view', 'edit' ), 'readonly' => true, ); @@ -2359,12 +2359,13 @@ protected function get_attachment_filename( int $attachment_id ): ?string { * * @param int $attachment_id Attachment ID. * @return int|null Attachment file size in bytes, or null if not available. + * @phpstan-return non-negative-int|null */ protected function get_attachment_filesize( int $attachment_id ): ?int { $meta = wp_get_attachment_metadata( $attachment_id ); - if ( isset( $meta['filesize'] ) ) { - return $meta['filesize']; + if ( isset( $meta['filesize'] ) && is_numeric( $meta['filesize'] ) && $meta['filesize'] > 0 ) { + return (int) $meta['filesize']; } $original_path = wp_get_original_image_path( $attachment_id ); diff --git a/tests/phpunit/tests/functions/wpFilesize.php b/tests/phpunit/tests/functions/wpFilesize.php index 4ed6de5676bd2..07a62ac2004aa 100644 --- a/tests/phpunit/tests/functions/wpFilesize.php +++ b/tests/phpunit/tests/functions/wpFilesize.php @@ -9,46 +9,100 @@ */ class Tests_Functions_wpFilesize extends WP_UnitTestCase { + const TEST_FILE = DIR_TESTDATA . '/images/test-image-upside-down.jpg'; + /** * @ticket 49412 */ - public function test_wp_filesize() { - $file = DIR_TESTDATA . '/images/test-image-upside-down.jpg'; - - $this->assertSame( filesize( $file ), wp_filesize( $file ) ); + public function test_wp_filesize(): void { + $this->assertSame( filesize( self::TEST_FILE ), wp_filesize( self::TEST_FILE ) ); } /** * @ticket 49412 + * @ticket 65670 */ - public function test_wp_filesize_filters() { - $file = DIR_TESTDATA . '/images/test-image-upside-down.jpg'; + public function test_wp_filesize_filters(): void { + add_filter( 'wp_filesize', static fn () => 999 ); + $this->assertSame( 999, wp_filesize( self::TEST_FILE ) ); - add_filter( - 'wp_filesize', - static function () { - return 999; - } - ); + add_filter( 'wp_filesize', static fn () => '9991', 100 ); + $this->assertSame( 9991, wp_filesize( self::TEST_FILE ) ); - $this->assertSame( 999, wp_filesize( $file ) ); + add_filter( 'pre_wp_filesize', static fn () => 111 ); + $this->assertSame( 111, wp_filesize( self::TEST_FILE ) ); - add_filter( - 'pre_wp_filesize', - static function () { - return 111; - } - ); + add_filter( 'pre_wp_filesize', static fn () => '2222', 100 ); + $this->assertSame( 2222, wp_filesize( self::TEST_FILE ) ); - $this->assertSame( 111, wp_filesize( $file ) ); + add_filter( 'pre_wp_filesize', static fn () => -100, 200 ); + $this->assertSame( 9991, wp_filesize( self::TEST_FILE ) ); } /** * @ticket 49412 */ - public function test_wp_filesize_with_nonexistent_file() { - $file = 'nonexistent/file.jpg'; + public function test_wp_filesize_with_nonexistent_file(): void { + $this->assertSame( 0, wp_filesize( 'nonexistent/file.jpg' ) ); + } + + /** + * @ticket 65670 + */ + public function test_wp_filesize_pre_wp_filesize_filter_null(): void { + add_filter( 'pre_wp_filesize', '__return_null' ); + + $this->assertSame( filesize( self::TEST_FILE ), wp_filesize( self::TEST_FILE ) ); + } - $this->assertSame( 0, wp_filesize( $file ) ); + /** + * @ticket 65670 + * + * @dataProvider data_wp_filesize_pre_wp_filesize_filter_negative + * + * @param float|int|string $value Negative value returned by the filter. + */ + public function test_wp_filesize_pre_wp_filesize_filter_negative( $value ): void { + add_filter( 'pre_wp_filesize', static fn () => $value ); + + $this->assertSame( filesize( self::TEST_FILE ), wp_filesize( self::TEST_FILE ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_filesize_pre_wp_filesize_filter_negative(): array { + return array( + 'negative int' => array( -1 ), + 'negative numeric string' => array( '-1' ), + 'negative float' => array( -1.5 ), + ); + } + + /** + * @ticket 65670 + * + * @dataProvider data_wp_filesize_filter_invalid_value + * + * @param mixed $value + */ + public function test_wp_filesize_wp_filesize_filter_invalid_value( $value ): void { + add_filter( 'wp_filesize', static fn () => $value ); + $this->assertSame( 0, wp_filesize( self::TEST_FILE ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_filesize_filter_invalid_value(): array { + return array( + 'negative' => array( -1 ), + 'null' => array( null ), + 'array' => array( array( 'bad', 'array' ) ), + ); } } diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 911676614754d..2b277c11d96a7 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -1005,6 +1005,111 @@ public function test_get_item() { $this->assertSame( 'image/jpeg', $data['mime_type'] ); } + /** + * Ensures int-castable `filesize` values in attachment metadata are normalized + * to an integer in the response. + * + * Attachment metadata is untyped, so plugins that populate `filesize` from + * a remote storage API may store it as a string. + * + * @ticket 65670 + * + * @dataProvider data_valid_filesize_meta + * + * @param mixed $stored_filesize Valid `filesize` metadata value. + * @param int $expected_filesize Expected `filesize` value in the REST response after normalization. + */ + public function test_get_item_normalizes_int_castable_filesize_meta( $stored_filesize, int $expected_filesize ) { + $attachment_id = self::factory()->attachment->create_object( + array( + 'file' => self::$test_file, + 'post_mime_type' => 'image/jpeg', + ) + ); + $this->assertIsInt( $attachment_id ); + + $meta = wp_get_attachment_metadata( $attachment_id ); + $meta = is_array( $meta ) ? $meta : array(); + $meta['filesize'] = $stored_filesize; + $this->assertNotFalse( wp_update_attachment_metadata( $attachment_id, $meta ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertIsArray( $data ); + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( $expected_filesize, $data['filesize'] ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_valid_filesize_meta(): array { + return array( + 'integer string' => array( '123456', 123456 ), + 'float string' => array( '123.4', 123 ), + 'scientific notation' => array( '1e3', 1000 ), + 'float' => array( 123.0, 123 ), + ); + } + + /** + * Ensures a `filesize` metadata value that is not a positive integer does + * not cause a fatal TypeError or a silently truncated size, falling back to + * the actual file size instead. + * + * @ticket 65670 + * + * @dataProvider data_invalid_filesize_meta + * + * @param mixed $filesize Invalid `filesize` metadata value. + */ + public function test_get_item_recovers_from_invalid_filesize_meta( $filesize ) { + $attachment_id = self::factory()->attachment->create_object( + array( + 'file' => self::$test_file, + 'post_mime_type' => 'image/jpeg', + ) + ); + $this->assertIsInt( $attachment_id ); + + $meta = wp_get_attachment_metadata( $attachment_id ); + $meta = is_array( $meta ) ? $meta : array(); + $meta['filesize'] = $filesize; + $this->assertNotFalse( wp_update_attachment_metadata( $attachment_id, $meta ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertIsArray( $data ); + $this->assertSame( 200, $response->get_status() ); + $this->assertIsInt( $data['filesize'] ); + $attached_file = wp_get_original_image_path( $attachment_id ); + $attached_file = $attached_file ? $attached_file : get_attached_file( $attachment_id ); + $this->assertIsString( $attached_file ); + $this->assertSame( filesize( $attached_file ), $data['filesize'] ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_invalid_filesize_meta(): array { + return array( + 'non-numeric string' => array( 'corrupt' ), + 'boolean' => array( true ), + 'zero' => array( 0 ), + 'zero string' => array( '0' ), + 'negative integer' => array( -5 ), + 'negative integer string' => array( '-5' ), + ); + } + /** * @requires function imagejpeg */