|
@@ -55,6 +55,14 @@ class Studiou_WC_Product_Manage_Product_Import {
|
|
|
return array('error' => __('Could not open file', 'studiou-wc-product-cat-manage'));
|
|
return array('error' => __('Could not open file', 'studiou-wc-product-cat-manage'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // M3 — strip UTF-8 BOM if present so the first header cell isn't
|
|
|
|
|
+ // "\xEF\xBB\xBFID", which would make every row miss the ID key and
|
|
|
|
|
+ // be treated as new (and then likely skipped by SKU-uniqueness).
|
|
|
|
|
+ $bom = fread($handle, 3);
|
|
|
|
|
+ if ($bom !== "\xEF\xBB\xBF") {
|
|
|
|
|
+ rewind($handle);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Read header row
|
|
// Read header row
|
|
|
$header = fgetcsv($handle, 0, ',', '"');
|
|
$header = fgetcsv($handle, 0, ',', '"');
|
|
|
if (!$header) {
|
|
if (!$header) {
|
|
@@ -62,8 +70,11 @@ class Studiou_WC_Product_Manage_Product_Import {
|
|
|
return array('error' => __('Invalid CSV format', 'studiou-wc-product-cat-manage'));
|
|
return array('error' => __('Invalid CSV format', 'studiou-wc-product-cat-manage'));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ $header_count = count($header);
|
|
|
|
|
+
|
|
|
// Read all data rows
|
|
// Read all data rows
|
|
|
$rows = array();
|
|
$rows = array();
|
|
|
|
|
+ $skipped_rows = array();
|
|
|
$row_number = 1;
|
|
$row_number = 1;
|
|
|
while (($row = fgetcsv($handle, 0, ',', '"')) !== false) {
|
|
while (($row = fgetcsv($handle, 0, ',', '"')) !== false) {
|
|
|
$row_number++;
|
|
$row_number++;
|
|
@@ -73,6 +84,32 @@ class Studiou_WC_Product_Manage_Product_Import {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // H4 — guard array_combine: on PHP 8+ it throws ValueError when
|
|
|
|
|
+ // the row width doesn't match the header. ValueError extends
|
|
|
|
|
+ // Error (not Exception), so the surrounding catches miss it and
|
|
|
|
|
+ // the whole import dies with a bare 500. Pad short rows (likely
|
|
|
|
|
+ // trailing empty cells from Excel); skip and report over-long
|
|
|
|
|
+ // rows (an unescaped comma we can't safely recover).
|
|
|
|
|
+ $row_count = count($row);
|
|
|
|
|
+ if ($row_count < $header_count) {
|
|
|
|
|
+ $row = array_pad($row, $header_count, '');
|
|
|
|
|
+ } elseif ($row_count > $header_count) {
|
|
|
|
|
+ $padded_for_failed = array_pad($row, $header_count, '');
|
|
|
|
|
+ $padded_for_failed = array_slice($padded_for_failed, 0, $header_count);
|
|
|
|
|
+ $skipped_rows[] = array(
|
|
|
|
|
+ 'row_number' => $row_number,
|
|
|
|
|
+ 'data' => array_combine($header, $padded_for_failed),
|
|
|
|
|
+ 'reason' => sprintf(
|
|
|
|
|
+ /* translators: 1: row number, 2: actual column count, 3: expected column count */
|
|
|
|
|
+ __('Row %1$d: column count mismatch (%2$d found, %3$d expected) — likely an unescaped comma. Row skipped.', 'studiou-wc-product-cat-manage'),
|
|
|
|
|
+ $row_number,
|
|
|
|
|
+ $row_count,
|
|
|
|
|
+ $header_count
|
|
|
|
|
+ ),
|
|
|
|
|
+ );
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// Combine header with row data
|
|
// Combine header with row data
|
|
|
$data = array_combine($header, $row);
|
|
$data = array_combine($header, $row);
|
|
|
$rows[] = array(
|
|
$rows[] = array(
|
|
@@ -83,13 +120,32 @@ class Studiou_WC_Product_Manage_Product_Import {
|
|
|
|
|
|
|
|
fclose($handle);
|
|
fclose($handle);
|
|
|
|
|
|
|
|
- // Store data in transient (expires in 1 hour)
|
|
|
|
|
|
|
+ // M4 (guard) — set_transient returns false when the serialized
|
|
|
|
|
+ // payload exceeds max_allowed_packet or storage fails for any
|
|
|
|
|
+ // other reason. The downstream batch handler then reports
|
|
|
|
|
+ // "Import data not found or expired", which is misleading — the
|
|
|
|
|
+ // upload was fine, the store just couldn't take it. Surface a
|
|
|
|
|
+ // clear, actionable message instead. The disk-staging rework that
|
|
|
|
|
+ // actually removes this ceiling lands in Phase D.
|
|
|
$transient_key = 'studiou_wcpcm_import_' . uniqid();
|
|
$transient_key = 'studiou_wcpcm_import_' . uniqid();
|
|
|
- set_transient($transient_key, $rows, 3600);
|
|
|
|
|
|
|
+ if (!set_transient($transient_key, $rows, 3600)) {
|
|
|
|
|
+ return array('error' => sprintf(
|
|
|
|
|
+ /* translators: %d: number of rows */
|
|
|
|
|
+ __('Import too large to stage (%d rows) — the parsed CSV would not fit in the WordPress transient store. Split the file into smaller batches, or wait for the streaming importer in a future release.', 'studiou-wc-product-cat-manage'),
|
|
|
|
|
+ count($rows)
|
|
|
|
|
+ ));
|
|
|
|
|
+ }
|
|
|
|
|
+ // Skipped (column-count-mismatch) rows live in a sibling transient and
|
|
|
|
|
+ // get folded into the final batch's failed_items + messages by
|
|
|
|
|
+ // process_batch(), so they appear in the failed-CSV download without
|
|
|
|
|
+ // changing the JS contract.
|
|
|
|
|
+ if (!empty($skipped_rows)) {
|
|
|
|
|
+ set_transient($transient_key . '_skipped', $skipped_rows, 3600);
|
|
|
|
|
+ }
|
|
|
|
|
|
|
|
return array(
|
|
return array(
|
|
|
'transient_key' => $transient_key,
|
|
'transient_key' => $transient_key,
|
|
|
- 'total' => count($rows)
|
|
|
|
|
|
|
+ 'total' => count($rows),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -121,6 +177,22 @@ class Studiou_WC_Product_Manage_Product_Import {
|
|
|
// Get batch of rows to process
|
|
// Get batch of rows to process
|
|
|
$batch = array_slice($rows, $offset, $batch_size);
|
|
$batch = array_slice($rows, $offset, $batch_size);
|
|
|
|
|
|
|
|
|
|
+ // On the final batch, fold in any ragged-row skips from the parse
|
|
|
|
|
+ // step (set by parse_csv_for_batch when row width != header width)
|
|
|
|
|
+ // so they appear in the failed-CSV download and the messages list.
|
|
|
|
|
+ $is_final_batch = ($offset + count($batch)) >= count($rows);
|
|
|
|
|
+ if ($is_final_batch) {
|
|
|
|
|
+ $parse_skipped = get_transient($transient_key . '_skipped');
|
|
|
|
|
+ if (is_array($parse_skipped) && !empty($parse_skipped)) {
|
|
|
|
|
+ foreach ($parse_skipped as $skip) {
|
|
|
|
|
+ $result['skipped']++;
|
|
|
|
|
+ $result['messages'][] = $skip['reason'];
|
|
|
|
|
+ $result['failed_items'][] = $skip['data'];
|
|
|
|
|
+ }
|
|
|
|
|
+ delete_transient($transient_key . '_skipped');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
foreach ($batch as $item) {
|
|
foreach ($batch as $item) {
|
|
|
$row_number = $item['row_number'];
|
|
$row_number = $item['row_number'];
|
|
|
$data = $item['data'];
|
|
$data = $item['data'];
|