Skip to content

Commit 39e403e

Browse files
committed
Media: Report failed image writes to streams.
When saving an image to a stream, `WP_Image_Editor::make_image()` currently ignores the return values of `fwrite()` and `fclose()`. A save that failed to write the image still reported success. `WP_Image_Editor_GD` uses this path for all stream destinations. The buffered image is now written with `file_put_contents()`, and `make_image()` returns `false` when that fails. This happens when the stream cannot be opened or the image is only partially written. As of PHP 8.6, it also happens when flushing or closing the stream fails. For stream wrappers that only send data on flush or close, a failed upload was previously reported as a successful save. Streams are now written the same way as in `WP_Image_Editor_Imagick::write_image()`, so both editors report the same failures. Developed in #15477. Props adamsilverstein. Fixes #66486. git-svn-id: https://develop.svn.wordpress.org/trunk@65294 602fd350-edb4-49c9-b593-d223f7449a82
1 parent 830f997 commit 39e403e

2 files changed

Lines changed: 110 additions & 8 deletions

File tree

‎src/wp-includes/class-wp-image-editor.php‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,7 @@ public function maybe_exif_rotate() {
559559
* Either calls editor's save function or handles file as a stream.
560560
*
561561
* @since 3.5.0
562+
* @since 7.2.0 Returns false when the image cannot be fully written to a stream.
562563
*
563564
* @param string $filename
564565
* @param callable $callback
@@ -579,15 +580,17 @@ protected function make_image( $filename, $callback, $arguments ) {
579580
if ( $result && $stream ) {
580581
$contents = ob_get_contents();
581582

582-
$fp = fopen( $filename, 'w' );
583-
584-
if ( ! $fp ) {
585-
ob_end_clean();
586-
return false;
583+
/*
584+
* file_put_contents() returns false if the stream cannot be opened, or if fewer
585+
* bytes are written than the image contains.
586+
*
587+
* As of PHP 8.6, false is also returned when flushing or closing the stream fails.
588+
* For stream wrappers that only send data on flush or close, such a failure means
589+
* the image was never saved.
590+
*/
591+
if ( false === file_put_contents( $filename, $contents ) ) {
592+
$result = false;
587593
}
588-
589-
fwrite( $fp, $contents );
590-
fclose( $fp );
591594
}
592595

593596
if ( $stream ) {

‎tests/phpunit/tests/image/editorGd.php‎

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @group wp-image-editor-gd
99
*/
1010
require_once __DIR__ . '/base.php';
11+
require_once DIR_TESTROOT . '/includes/class-wp-test-stream.php';
1112

1213
class Tests_Image_Editor_GD extends WP_Image_UnitTestCase {
1314

@@ -674,4 +675,102 @@ public function test_image_non_existent_extension() {
674675

675676
$this->assertTrue( $loaded );
676677
}
678+
679+
/**
680+
* Tests that images can be saved to a stream.
681+
*
682+
* @covers WP_Image_Editor::make_image
683+
*/
684+
public function test_save_to_stream() {
685+
$saved = $this->save_to_stream( 'wptestgd', 'WP_Test_Stream' );
686+
687+
$this->assertNotWPError( $saved );
688+
$this->assertSame( 'wptestgd://Tests_Image_Editor_GD/write.jpg', $saved['path'] );
689+
$this->assertNotEmpty( WP_Test_Stream::$data['Tests_Image_Editor_GD']['/write.jpg'], 'The image should have been written to the stream.' );
690+
}
691+
692+
/**
693+
* Tests that saving fails when the image cannot be fully written to the stream.
694+
*
695+
* @covers WP_Image_Editor::make_image
696+
*/
697+
public function test_save_to_stream_fails_when_write_fails() {
698+
// file_put_contents() warns about the incomplete write. $this->expectWarning() is deprecated in PHPUnit 10.
699+
$warnings = array();
700+
set_error_handler(
701+
static function ( int $errno, string $errstr ) use ( &$warnings ) {
702+
$warnings[] = $errstr;
703+
return true;
704+
},
705+
E_WARNING
706+
);
707+
708+
try {
709+
$saved = $this->save_to_stream( 'wptestgdwritefail', 'WP_Test_Stream_GD_Write_Failure' );
710+
} finally {
711+
restore_error_handler();
712+
}
713+
714+
$this->assertCount( 1, $warnings, 'Expected one warning about the incomplete write.' );
715+
$this->assertStringContainsString( 'bytes written', $warnings[0] );
716+
$this->assertWPError( $saved );
717+
$this->assertSame( 'image_save_error', $saved->get_error_code() );
718+
}
719+
720+
/**
721+
* Tests that saving fails when the stream cannot be flushed.
722+
*
723+
* PHP 8.6 is the first version where fclose() reports flush and close failures.
724+
*
725+
* @requires PHP >= 8.6
726+
*
727+
* @covers WP_Image_Editor::make_image
728+
*/
729+
public function test_save_to_stream_fails_when_flush_fails() {
730+
$saved = $this->save_to_stream( 'wptestgdflushfail', 'WP_Test_Stream_GD_Flush_Failure' );
731+
732+
$this->assertWPError( $saved );
733+
$this->assertSame( 'image_save_error', $saved->get_error_code() );
734+
}
735+
736+
/**
737+
* Loads a JPEG image with the GD editor and saves it to a stream.
738+
*
739+
* @param string $protocol The stream protocol to register.
740+
* @param string $wrapper_class The stream wrapper class to register for the protocol.
741+
* @return array|WP_Error The result of WP_Image_Editor_GD::save().
742+
*/
743+
private function save_to_stream( $protocol, $wrapper_class ) {
744+
stream_wrapper_register( $protocol, $wrapper_class );
745+
WP_Test_Stream::$data = array();
746+
747+
$gd_image_editor = new WP_Image_Editor_GD( DIR_TESTDATA . '/images/waffles.jpg' );
748+
$loaded = $gd_image_editor->load();
749+
750+
$saved = $gd_image_editor->save( $protocol . '://Tests_Image_Editor_GD/write.jpg', 'image/jpeg' );
751+
752+
stream_wrapper_unregister( $protocol );
753+
754+
$this->assertTrue( $loaded );
755+
756+
return $saved;
757+
}
758+
}
759+
760+
/**
761+
* A stream wrapper that fails to write any data.
762+
*/
763+
class WP_Test_Stream_GD_Write_Failure extends WP_Test_Stream {
764+
public function stream_write( $data ) {
765+
return 0;
766+
}
767+
}
768+
769+
/**
770+
* A stream wrapper that fails to flush, like a remote storage wrapper whose upload fails.
771+
*/
772+
class WP_Test_Stream_GD_Flush_Failure extends WP_Test_Stream {
773+
public function stream_flush() {
774+
return false;
775+
}
677776
}

0 commit comments

Comments
 (0)