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 +66 -13 4.4.1 → 4.9.3 View file →
@@ -32,9 +32,9 @@
32 32 * argument is considered to be respectively less than, equal to, or greater
33 33 * than the second.
34 34 */
35 35 public function csvSort( $a, $b ) {
36 - $order = [ 'Term', 'Author', 'Docs' ];
36 + $order = [ 'Term', 'Author', 'Docs', 'FAQ' ];
37 37
38 38 $keyA = array_search( $a[0], $order );
39 39 $keyB = array_search( $b[0], $order );
40 40
@@ -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
@@ -185,11 +220,14 @@
185 220 'author_display_name' => sanitize_text_field( $author_data['Author display name'] ),
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 - } else if ( $type === 'Docs' ) {
190 - $post_headers = array_slice( $headers, 1, 21 );
191 - $post_row = array_slice( $row, 1, 21 );
224 + } else if ( $type === 'Docs' || $type === 'FAQ' ) {
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
@@ -194,9 +232,9 @@
194 232 $post_data = array_combine( $post_headers, $post_row );
195 233
196 234 $post_args = [
197 235 'post_id' => sanitize_text_field( $post_data['Docs ID'] ) ?? '',
198 - 'post_type' => 'docs',
236 + 'post_type' => $type === 'FAQ' ? 'betterdocs_faq' : 'docs',
199 237 'post_author' => sanitize_text_field( $post_data['Docs author'] ) ?? '',
200 238 'post_content' => sanitize_text_field( $post_data['Docs content'] ) ?? '',
201 239 'post_title' => sanitize_text_field( $post_data['Docs title'] ) ?? '',
202 240 'post_name' => sanitize_text_field( $post_data['Docs slug'] ) ?? '',
@@ -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