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
24 changes: 18 additions & 6 deletions src/wp-admin/includes/image.php
Original file line number Diff line number Diff line change
Expand Up @@ -1170,22 +1170,34 @@
* Validates that file is suitable for displaying within a web page.
*
* @since 2.5.0
* @since 7.2.0 Uses exif_imagetype() when available.
*
* @param string $path File path to test.
* @return bool True if suitable, false if not suitable.
*/
function file_is_displayable_image( $path ) {
$displayable_image_types = array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP, IMAGETYPE_ICO, IMAGETYPE_WEBP, IMAGETYPE_AVIF );

$info = wp_getimagesize( $path );
if ( empty( $info ) ) {
$result = false;
} elseif ( ! in_array( $info[2], $displayable_image_types, true ) ) {
$result = false;
/*
* Use exif_imagetype() to determine the image type when available. It reads
* the same header bytes as getimagesize() but does not need to parse the
* image dimensions, and it works with non-seekable streams.
*/
if ( is_callable( 'exif_imagetype' ) ) {

Check warning on line 1186 in src/wp-admin/includes/image.php

View workflow job for this annotation

GitHub Actions / PHP static analysis / Run PHP static analysis

PHPStan: function.alreadyNarrowedType

Call to function is_callable() with 'exif_imagetype' will always evaluate to true.
// Don't silence errors when in debug mode, unless running unit tests.
if ( defined( 'WP_DEBUG' ) && WP_DEBUG && ! defined( 'WP_RUN_CORE_TESTS' ) ) {
$image_type = exif_imagetype( $path );
} else {
// phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Errors are checked via the return value.
$image_type = @exif_imagetype( $path );
}
} else {
$result = true;
$info = wp_getimagesize( $path );
$image_type = ! empty( $info ) ? $info[2] : false;
}

$result = $image_type && in_array( $image_type, $displayable_image_types, true );

/**
* Filters whether the current image is displayable in the browser.
*
Expand Down
111 changes: 111 additions & 0 deletions tests/phpunit/tests/image/functions.php
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,117 @@ public function data_file_is_displayable_image_negative() {
return $this->text_array_to_dataprovider( $files );
}

/**
* Tests that the result matches the getimagesize() derived answer.
*
* @ticket 28077
*
* @covers ::file_is_displayable_image
*/
public function test_file_is_displayable_image_uses_exif_imagetype() {
$displayable_image_types = array( IMAGETYPE_GIF, IMAGETYPE_JPEG, IMAGETYPE_PNG, IMAGETYPE_BMP, IMAGETYPE_ICO, IMAGETYPE_WEBP, IMAGETYPE_AVIF );

$files = array(
'test-image.png' => true,
'test-image.tga' => false,
);

foreach ( $files as $file => $expected ) {
$path = DIR_TESTDATA . '/images/' . $file;
$info = wp_getimagesize( $path );

$getimagesize_result = ! empty( $info ) && in_array( $info[2], $displayable_image_types, true );

$this->assertSame( $expected, $getimagesize_result, "The getimagesize() derived answer for '$file' is unexpected." );
$this->assertSame(
$getimagesize_result,
file_is_displayable_image( $path ),
"file_is_displayable_image( '$file' ) should match the getimagesize() derived answer."
);
}
}

/**
* Tests that detection is based on the file signature only.
*
* exif_imagetype() identifies an image from its signature and does not
* verify that the rest of the header can be parsed. A file whose signature
* is valid is therefore treated as displayable even when it cannot be
* measured, which is the behaviour change this makes on purpose.
*
* @ticket 28077
*
* @covers ::file_is_displayable_image
*/
public function test_file_is_displayable_image_accepts_a_valid_signature() {
$file = wp_tempnam( 'signature-only.png' );

// A PNG signature followed by data that is not a valid IHDR chunk.
file_put_contents( $file, "\x89PNG\r\n\x1a\n" . str_repeat( "\0", 8 ) );

$this->assertSame(
IMAGETYPE_PNG,
exif_imagetype( $file ),
'The signature should be recognized as a PNG.'
);

$this->assertTrue(
file_is_displayable_image( $file ),
'Should be displayable because the signature is valid.'
);

unlink( $file );
}

/**
* Tests that the result passed to the filter is always a strict boolean.
*
* @ticket 28077
*
* @covers ::file_is_displayable_image
*/
public function test_file_is_displayable_image_returns_bool() {
$this->assertIsBool( file_is_displayable_image( DIR_TESTDATA . '/images/test-image.png' ) );
$this->assertIsBool( file_is_displayable_image( DIR_TESTDATA . '/images/this-file-does-not-exist.png' ) );
}

/**
* Tests that a path which does not exist is not displayable.
*
* @ticket 28077
*
* @covers ::file_is_displayable_image
*/
public function test_file_is_displayable_image_nonexistent_path() {
$this->assertFalse( file_is_displayable_image( DIR_TESTDATA . '/images/this-file-does-not-exist.png' ) );
}

/**
* Tests that the filter receives the result and that its return value is passed through.
*
* @ticket 28077
*
* @covers ::file_is_displayable_image
*/
public function test_file_is_displayable_image_filter() {
$received = null;

$filter = static function ( $result ) use ( &$received ) {
$received = $result;

return true;
};

add_filter( 'file_is_displayable_image', $filter );

$result = file_is_displayable_image( DIR_TESTDATA . '/images/test-image.tga' );

remove_filter( 'file_is_displayable_image', $filter );

$this->assertSame( false, $received, 'The filter should receive false for a non-displayable image.' );
$this->assertTrue( $result, 'The return value of the filter should be passed through.' );
}

/**
* @ticket 50833
* @requires extension gd
Expand Down
Loading