Problem/Motivation
Exporting content via /admin/content/content-export produces a CSV file where some rows have the wrong number of columns when opened in Excel or Google Sheets. Content from one field spills into the columns of the next fields (e.g. a body/HTML field's content pushes everything after it several columns to the right).
This happens whenever a field's own value contains a double-quote character (") — for example, raw HTML attributes such as <img alt="..." src="..."> when the "Strip HTML tags for fields" option is left unchecked, or a plain straight/smart quote inside body copy.
Steps to reproduce
- Create a node whose body (or any exported field) contains an HTML attribute with a double quote, e.g.
<img alt="test" src="http://example.com/x.jpg">. - Go to
/admin/content/content-export. - Select a content type/fields that include that field.
- Leave Strip HTML tags for fields unchecked.
- Export the content and open the resulting CSV in Excel or Google Sheets.
- Observe that the row for that node has more columns than the header, with content shifted into the wrong columns starting right after the embedded quote.
Proposed resolution
ContentExport::getNodeData() currently wraps every field value in literal double quotes by hand (e.g. '"' . $value . '"'), and ContentExport::getNodeDataList() joins these pre-quoted strings with implode(',', ...). ContentExportForm::submitForm() then writes the header via implode(',', $fields) and each row via plain fwrite().
This is not real CSV escaping: per RFC 4180, any double quote appearing inside a quoted field must be doubled (" → ""). The manual wrapping never does this, so the first embedded " in a field's value closes the "quoted" field early as far as any RFC 4180-compliant CSV parser is concerned — everything after it in that row, including subsequent commas, is then parsed as extra columns.
The fix is to replace the hand-rolled quoting/joining with PHP's native fputcsv(), which handles quoting, comma-escaping and quote-doubling correctly:
getNodeData()should push raw (HTML-stripped/escaped as before, per the existing option) values without any manually added quotes.getNodeDataList()should return an array of row-arrays instead of pre-joined strings.ContentExportForm::submitForm()should callfputcsv($csvFile, $fieldNames)for the header andfputcsv($csvFile, $csvDataRow)for each row, instead ofimplode()/fwrite().
We have a working patch that makes this change with no functional/UI impact (same export options, fields, and file naming) and verified it against a real export of 617 nodes/36 fields — before the fix, 72/627 rows had the wrong column count; after, 0. Happy to attach the patch here.
Remaining tasks
- Review/attach patch.
- Confirm no regression for rows with commas only, no embedded quotes (already covered by manual testing).
User interface changes
None.
API changes
None. Internal method signatures/return types of ContentExport::getNodeData() and getNodeDataList() change (now return raw arrays instead of pre-joined/quoted strings), but these are internal to the module and not part of a documented public API.
Data model changes
None.
Issue fork content_export_csv-3610989
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments