PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.9.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.9.3
4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 All 201 releases
← All changes | includes/Admin/Importer/Parsers/CSV_Parser.php +63 -10 4.5.0 → 4.9.3 View file →
@@ -57,21 +57,32 @@
57 57 }
58 58
59 59 $fileContent = str_replace( [ "\r\n", "\r" ], "\n", $fileContent );
60 60
61 + // Parse through a stream so fgetcsv() correctly assembles records whose
62 + // quoted fields span multiple lines (e.g. multi-line doc content). A plain
63 + // explode( "\n" ) + str_getcsv() per line splits such records apart, producing
64 + // rows whose column count no longer matches the headers — which makes the
65 + // array_combine() calls below fatal (500 error on Sample Docs import).
66 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fopen -- in-memory stream, no filesystem access; required for multi-line CSV records.
61 67 $handle = fopen( 'php://temp', 'r+' );
62 - fwrite( $handle, $fileContent );
63 - rewind( $handle );
64 -
65 68 if ( $handle !== false ) {
66 - while ( ( $row = fgetcsv( $handle, 1000, ',' ) ) !== false ) {
69 + fwrite( $handle, $fileContent );
70 + rewind( $handle );
71 + while ( ( $row = fgetcsv( $handle, 0, ',' ) ) !== false ) {
67 72 $csv_data[] = $row;
68 73 }
74 + // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closing in-memory stream; WP_Filesystem does not apply.
69 75 fclose( $handle );
70 76 }
71 77
72 78 $headers = array_shift( $csv_data );
73 79
80 + // Bail out cleanly on an empty/malformed file instead of fataling below.
81 + if ( ! is_array( $headers ) || empty( $headers ) ) {
82 + return $data;
83 + }
84 +
74 85 // Process specific headers for 'Docs Title'
75 86 if ( $headers[0] == 'Docs Title' ) {
76 87 $replacementMap = [ 'Docs Slug' => 'post_name' ];
77 88
@@ -93,8 +104,32 @@
93 104 }
94 105
95 106 usort( $csv_data, [ $this, 'csvSort' ] );
96 107
108 + // The combined CSV packs three blocks side by side: Docs (from index 1),
109 + // then Author, then Term. CSVs exported after the WPML language columns
110 + // were added widen the Docs block by 2 columns (Docs language code +
111 + // translation source slug), shifting the Author/Term offsets. Locate
112 + // each block by its header name rather than a fixed position, so the
113 + // layout is detected wherever the columns land (and adding a column to
114 + // one block can't silently corrupt the others).
115 + $has_wpml_columns = in_array( 'Docs language code', $headers, true );
116 +
117 + $author_offset = array_search( 'Author id', $headers, true );
118 + $term_offset = array_search( 'Taxonomy', $headers, true );
119 +
120 + // Fall back to the historical fixed offsets if a block header is missing
121 + // (malformed file) so such files still parse exactly as they did before.
122 + if ( $author_offset === false ) {
123 + $author_offset = $has_wpml_columns ? 24 : 22;
124 + }
125 + if ( $term_offset === false ) {
126 + $term_offset = $has_wpml_columns ? 30 : 28;
127 + }
128 +
129 + // The Docs block runs from index 1 up to the start of the Author block.
130 + $post_block_len = $author_offset - 1;
131 +
97 132 foreach ( $csv_data as $row ) {
98 133 $type = $row[0];
99 134
100 135 if ( $type === 'glossaries' ) {
@@ -122,10 +157,10 @@
122 157 ];
123 158
124 159 $data['terms'][] = $term_args;
125 160 } elseif ( $type === 'Term' ) {
126 - $term_headers = array_slice( $headers, 28, 11 );
127 - $term_row = array_slice( $row, 28, 11 );
161 + $term_headers = array_slice( $headers, $term_offset, 11 );
162 + $term_row = array_slice( $row, $term_offset, 11 );
128 163 $term_row = array_pad( $term_row, count( $term_headers ), '' );
129 164
130 165 $term_data = array_combine( $term_headers, $term_row );
131 166
@@ -171,10 +206,10 @@
171 206 }
172 207
173 208 $data['terms'][] = $term_args;
174 209 } elseif ( $type === 'Author' ) {
175 - $author_headers = array_slice( $headers, 22, 6 );
176 - $author_row = array_slice( $row, 22, 6 );
210 + $author_headers = array_slice( $headers, $author_offset, 6 );
211 + $author_row = array_slice( $row, $author_offset, 6 );
177 212 $author_row = array_pad( $author_row, count( $author_headers ), '' );
178 213
179 214 $author_data = array_combine( $author_headers, $author_row );
180 215
@@ -186,10 +221,13 @@
186 221 'author_first_name' => sanitize_text_field( $author_data['Author first name'] ),
187 222 'author_last_name' => sanitize_text_field( $author_data['Author last name'] )
188 223 ];
189 224 } else if ( $type === 'Docs' || $type === 'FAQ' ) {
190 - $post_headers = array_slice( $headers, 1, 21 );
191 - $post_row = array_slice( $row, 1, 21 );
225 + // Keep FAQ import (HEAD) and use the dynamic post-block length
226 + // from the WPML branch so the variable WPML language columns are
227 + // handled instead of a hardcoded count.
228 + $post_headers = array_slice( $headers, 1, $post_block_len );
229 + $post_row = array_slice( $row, 1, $post_block_len );
192 230 $post_row = array_pad( $post_row, count( $post_headers ), '' );
193 231
194 232 $post_data = array_combine( $post_headers, $post_row );
195 233
@@ -218,8 +256,23 @@
218 256 $this->searchTermsByIds( $data['terms'], sanitize_text_field( $post_data['Doc Categories'] ) ),
219 257 $this->searchTermsByIds( $data['terms'], sanitize_text_field( $post_data['Doc Tags'] ) ),
220 258 $this->searchTermsByIds( $data['terms'], sanitize_text_field( $post_data['Knowledge Bases'] ) )
221 259 );
260 + }
261 +
262 + if ( $has_wpml_columns ) {
263 + if ( ! empty( $post_data['Docs language code'] ) ) {
264 + $post_args['postmeta'][] = [
265 + 'key' => '_betterdocs_wpml_lang',
266 + 'value' => sanitize_text_field( $post_data['Docs language code'] ),
267 + ];
268 + }
269 + if ( ! empty( $post_data['Docs translation source slug'] ) ) {
270 + $post_args['postmeta'][] = [
271 + 'key' => '_betterdocs_wpml_source_slug',
272 + 'value' => sanitize_text_field( $post_data['Docs translation source slug'] ),
273 + ];
274 + }
222 275 }
223 276
224 277 $data['posts'][] = $post_args;
225 278