Commit 1fcebaa4794 for woocommerce
commit 1fcebaa479487c0d73268a70a0ea1326293f6ca1
Author: SH Sajal Chowdhury <72102985+shsajalchowdhury@users.noreply.github.com>
Date: Fri Aug 7 16:03:33 2026 +0600
Fix: CSV exporter default fopen mode breaks S3 stream wrappers + add error logging (#66295)
* Fix: CSV exporter S3 stream wrapper compatibility and error logging (#63835)
Changed default fopen mode from a+ to a for S3 stream wrapper
compatibility (S3-Uploads and similar don't support + mode variants).
Added error logging when fopen fails, which was previously silent.
* fix: log CSV export write and close failures, not just open failures
Append-mode stream wrappers such as S3-Uploads buffer writes locally and
only transfer the data when the handle is closed, so a successful fopen()
is not enough to know the export data was persisted. Check the fwrite()
byte count and the fclose() return value and log an error when either
indicates the write did not complete, so a truncated or empty export
leaves a trace instead of reporting success silently.
Also send both existing failure log entries to the same wc-csv-exporter
source so all export write failures land in one log file, and correct the
fopen mode filter docblock prose, which still described the old 'a+'
default.
Adds a regression test asserting the filter receives the append-only 'a'
default and that the written data reaches the export file.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01196wKfE9AA32UmzZZPBMTV
* fix: improve CSV exporter fopen failure diagnostics
Address review feedback on the CSV batch exporter logging:
- Log the fopen mode and the last PHP error alongside the path when the
file cannot be opened, so stream-wrapper failures can be told apart
from permission problems and invalid modes.
- Log the raw file path in the permission error instead of running it
through esc_html(), which mangles characters such as "&" in what is a
plain-text log, not HTML output.
- Correct the woocommerce_csv_exporter_fopen_mode docblock: "e" is an
optional flag appended to a mode, not a standalone mode.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011m7Z4ujD9ZzEdb6brNByzp
---------
Co-authored-by: Ján Mikláš <neosinner@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
diff --git a/plugins/woocommerce/changelog/fix-63835-csv-exporter-s3-stream-and-error-logging b/plugins/woocommerce/changelog/fix-63835-csv-exporter-s3-stream-and-error-logging
new file mode 100644
index 00000000000..5f437e03814
--- /dev/null
+++ b/plugins/woocommerce/changelog/fix-63835-csv-exporter-s3-stream-and-error-logging
@@ -0,0 +1,4 @@
+Significance: patch
+Type: fix
+
+Change CSV batch exporter default fopen mode from a+ to a for S3 stream wrapper compatibility and add error logging when opening, writing to, or closing the export file fails.
diff --git a/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php b/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php
index 7c7680333b9..fc9645bd4cd 100644
--- a/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php
+++ b/plugins/woocommerce/includes/export/abstract-wc-csv-batch-exporter.php
@@ -128,33 +128,65 @@ abstract class WC_CSV_Batch_Exporter extends WC_CSV_Exporter {
*/
protected function write_csv_data( $data ) {
+ $log_context = array( 'source' => 'wc-csv-exporter' );
+
if ( ! file_exists( $this->get_file_path() ) || ! is_writeable( $this->get_file_path() ) ) {
wc_get_logger()->error(
sprintf(
/* translators: %s is file path. */
__( 'Unable to create or write to %s during CSV export. Please check file permissions.', 'woocommerce' ),
- esc_html( $this->get_file_path() )
- )
+ $this->get_file_path()
+ ),
+ $log_context
);
return false;
}
/**
* Filters the mode parameter which specifies the type of access you require to the stream (used during file
- * writing for CSV exports). Defaults to 'a+' (which supports both reading and writing, and places the file
- * pointer at the end of the file).
+ * writing for CSV exports). Defaults to 'a' (append, write-only, placing the file pointer at the end of the
+ * file), which is all CSV export needs and is supported by the widest range of stream wrappers.
*
* @see https://www.php.net/manual/en/function.fopen.php
* @since 6.8.0
*
- * @param string $fopen_mode, either (r, r+, w, w+, a, a+, x, x+, c, c+, e)
+ * @param string $fopen_mode One of (r, r+, w, w+, a, a+, x, x+, c, c+), optionally followed by flags such as "b", "t" or "e". Defaults to "a" (append, write-only) for broad stream-wrapper compatibility.
*/
- $fopen_mode = apply_filters( 'woocommerce_csv_exporter_fopen_mode', 'a+' );
+ $fopen_mode = apply_filters( 'woocommerce_csv_exporter_fopen_mode', 'a' );
$fp = fopen( $this->get_file_path(), $fopen_mode );
if ( $fp ) {
- fwrite( $fp, $data );
- fclose( $fp );
+ // WP_Filesystem has no streaming write API, so it would mean rewriting the whole export on every batch.
+ $bytes_written = fwrite( $fp, $data ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite
+
+ // Append-mode stream wrappers may buffer locally and only transfer the data on close, so closing can fail too.
+ $closed = fclose( $fp ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
+
+ if ( false === $bytes_written || strlen( $data ) !== $bytes_written || ! $closed ) {
+ wc_get_logger()->error(
+ sprintf(
+ /* translators: %s: file path */
+ __( 'Failed to write CSV export data to %s. The exported file may be incomplete.', 'woocommerce' ),
+ $this->get_file_path()
+ ),
+ $log_context
+ );
+ }
+ } else {
+ // fopen() raises a warning when it fails, so the last PHP error usually explains why (permissions, an
+ // invalid mode, or a stream wrapper that doesn't support the requested mode).
+ $last_error = error_get_last();
+
+ wc_get_logger()->error(
+ sprintf(
+ /* translators: 1: file path, 2: fopen mode, 3: last PHP error message */
+ __( 'Unable to open file for writing: %1$s (mode: %2$s). Last PHP error: %3$s', 'woocommerce' ),
+ $this->get_file_path(),
+ $fopen_mode,
+ isset( $last_error['message'] ) ? $last_error['message'] : 'n/a'
+ ),
+ $log_context
+ );
}
// Add all columns when finished.
diff --git a/plugins/woocommerce/tests/php/includes/exporter/class-wc-product-csv-exporter-test.php b/plugins/woocommerce/tests/php/includes/exporter/class-wc-product-csv-exporter-test.php
index 57454ccc2b1..4710d45cba0 100644
--- a/plugins/woocommerce/tests/php/includes/exporter/class-wc-product-csv-exporter-test.php
+++ b/plugins/woocommerce/tests/php/includes/exporter/class-wc-product-csv-exporter-test.php
@@ -161,4 +161,46 @@ class WC_Product_CSV_Exporter_Test extends \WC_Unit_Test_Case {
);
}
}
+
+ /**
+ * @testdox CSV data is written with an append-only fopen mode so write-only stream wrappers are supported.
+ */
+ public function test_write_csv_data_uses_append_only_fopen_mode(): void {
+ $exporter = new WC_Product_CSV_Exporter();
+ $exporter->set_filename( 'wc-csv-exporter-fopen-mode-test' );
+
+ // Creates the file so write_csv_data() gets past its writability check.
+ $exporter->get_file();
+
+ $captured_mode = null;
+ $capture_mode = function ( $fopen_mode ) use ( &$captured_mode ) {
+ $captured_mode = $fopen_mode;
+ return $fopen_mode;
+ };
+
+ add_filter( 'woocommerce_csv_exporter_fopen_mode', $capture_mode );
+
+ $reflected_exporter = new ReflectionClass( WC_Product_CSV_Exporter::class );
+ $write_csv_data = $reflected_exporter->getMethod( 'write_csv_data' );
+ $write_csv_data->setAccessible( true );
+ $get_file_path = $reflected_exporter->getMethod( 'get_file_path' );
+ $get_file_path->setAccessible( true );
+ $get_headers_row_file_path = $reflected_exporter->getMethod( 'get_headers_row_file_path' );
+ $get_headers_row_file_path->setAccessible( true );
+
+ try {
+ $write_csv_data->invoke( $exporter, "sku,name\n" );
+
+ $this->assertSame( 'a', $captured_mode, 'The default fopen mode must be append-only; "a+" is rejected by stream wrappers that cannot seek.' );
+ $this->assertStringContainsString( "sku,name\n", $exporter->get_file(), 'Data passed to write_csv_data() should reach the export file.' );
+ } finally {
+ remove_filter( 'woocommerce_csv_exporter_fopen_mode', $capture_mode );
+
+ foreach ( array( $get_file_path->invoke( $exporter ), $get_headers_row_file_path->invoke( $exporter ) ) as $path ) {
+ if ( file_exists( $path ) ) {
+ wp_delete_file( $path );
+ }
+ }
+ }
+ }
}