PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.8.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.8.0
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 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Admin / Importer / Parsers / CSV_Parser.php

CSV_Parser.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.8.0, at includes/Admin/Importer/Parsers/CSV_Parser.php

333 lines 13.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace WPDeveloper\BetterDocs\Admin\Importer\Parsers;
4
5 if ( ! defined( 'ABSPATH' ) ) {
6 exit; // Exit if accessed directly.
7 }
8
9 /**
10 * WordPress extended RSS file parser implementations
11 * Originally made by WordPress part of WordPress/Importer.
12 * https://plugins.trac.wordpress.org/browser/wordpress-importer/trunk/parsers/class-wxr-parser-regex.php
13 *
14 * What was done (by Elementor):
15 * Reformat of the code.
16 * Changed text domain.
17 * Changed methods visibility.
18 */
19
20 /**
21 * WXR Parser that uses regular expressions. Fallback for installs without an XML parser.
22 */
23 class CSV_Parser {
24
25 /**
26 * Sort function for sorting CSV file data to keep Term and Author on top.
27 *
28 * @param array $a The first element to compare.
29 * @param array $b The second element to compare.
30 *
31 * @return int Returns an integer less than, equal to, or greater than zero if the first
32 * argument is considered to be respectively less than, equal to, or greater
33 * than the second.
34 */
35 public function csvSort( $a, $b ) {
36 $order = [ 'Term', 'Author', 'Docs', 'FAQ' ];
37
38 $keyA = array_search( $a[0], $order );
39 $keyB = array_search( $b[0], $order );
40
41 return $keyA - $keyB;
42 }
43
44 public function parse( $file ) {
45 $data = [
46 'terms' => [],
47 'posts' => [],
48 'authors' => []
49 ];
50
51 $csv_data = [];
52
53 // Read and normalize file content
54 $fileContent = file_get_contents( $file );
55 if ( $fileContent === false ) {
56 return $data; // Return empty data if file reading fails
57 }
58
59 $fileContent = str_replace( [ "\r\n", "\r" ], "\n", $fileContent );
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.
67 $handle = fopen( 'php://temp', 'r+' );
68 if ( $handle !== false ) {
69 fwrite( $handle, $fileContent );
70 rewind( $handle );
71 while ( ( $row = fgetcsv( $handle, 0, ',' ) ) !== false ) {
72 $csv_data[] = $row;
73 }
74 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closing in-memory stream; WP_Filesystem does not apply.
75 fclose( $handle );
76 }
77
78 $headers = array_shift( $csv_data );
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
85 // Process specific headers for 'Docs Title'
86 if ( $headers[0] == 'Docs Title' ) {
87 $replacementMap = [ 'Docs Slug' => 'post_name' ];
88
89 $headers = array_map(
90 function ( $item ) use ( $replacementMap ) {
91 return $replacementMap[ $item ] ?? $item;
92 },
93 $headers
94 );
95
96 $data['type'] = 'sample/csv';
97
98 foreach ( $csv_data as $row ) {
99 $row = array_pad( $row, count( $headers ), '' );
100 $data['posts'][] = array_combine( $headers, $row );
101 }
102
103 return $data;
104 }
105
106 usort( $csv_data, [ $this, 'csvSort' ] );
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
132 foreach ( $csv_data as $row ) {
133 $type = $row[0];
134
135 if ( $type === 'glossaries' ) {
136 $term_headers = array_slice( $headers, 0, 7 );
137 $term_row = array_slice( $row, 0, 7 );
138 $term_row = array_pad( $term_row, count( $term_headers ), '' );
139
140 if ( count( $term_headers ) !== count( $term_row ) ) {
141 return $data;
142 }
143
144 $term_data = array_combine( $term_headers, $term_row );
145
146 $term_args = [
147 'term_taxonomy' => $term_data['Taxonomy'],
148 'term_id' => $term_data['Term ID'],
149 'term_name' => $term_data['Term name'],
150 'slug' => $term_data['Term slug'],
151 'term_group' => $term_data['Term group']
152 ];
153
154 $term_args['termmeta'][] = [
155 'key' => 'glossary_term_description',
156 'value' => $term_data['Term description']
157 ];
158
159 $data['terms'][] = $term_args;
160 } elseif ( $type === 'Term' ) {
161 $term_headers = array_slice( $headers, $term_offset, 11 );
162 $term_row = array_slice( $row, $term_offset, 11 );
163 $term_row = array_pad( $term_row, count( $term_headers ), '' );
164
165 $term_data = array_combine( $term_headers, $term_row );
166
167 $taxonomy = $term_data['Taxonomy'];
168 $term_args = [
169 'term_id' => sanitize_text_field( $term_data['Term ID'] ),
170 'term_taxonomy' => $taxonomy,
171 'slug' => sanitize_text_field( $term_data['Term slug'] ),
172 'term_parent' => sanitize_text_field( $term_data['Term parent'] ),
173 'term_name' => sanitize_text_field( $term_data['Term name'] ),
174 'description' => sanitize_text_field( $term_data['Term description'] ),
175 'term_group' => sanitize_text_field( $term_data['Term group'] ),
176 'termmeta' => []
177 ];
178
179 if ( $taxonomy === 'doc_category' ) {
180 if ( ! empty( $term_data['Assigned Docs'] ) ) {
181 $term_args['termmeta'][] = [
182 'key' => '_docs_order',
183 'value' => sanitize_text_field( $term_data['Assigned Docs'] )
184 ];
185 }
186
187 if ( ! empty( $term_data['Assigned KBs'] ) ) {
188 $doc_category_knowledge_base = explode( ",", sanitize_text_field( $term_data['Assigned KBs'] ) );
189 $term_args['termmeta'][] = [
190 'key' => 'doc_category_knowledge_base',
191 'value' => rest_sanitize_array( $doc_category_knowledge_base )
192 ];
193 }
194
195 if ( ! empty( $term_data['Doc Category order'] ) ) {
196 $term_args['termmeta'][] = [
197 'key' => 'doc_category_order',
198 'value' => sanitize_text_field( $term_data['Doc Category order'] )
199 ];
200 }
201 } else if ( $taxonomy === 'knowledge_base' && ! empty( $term_data['KB order'] ) ) {
202 $term_args['termmeta'][] = [
203 'key' => 'kb_order',
204 'value' => $term_data['KB order']
205 ];
206 }
207
208 $data['terms'][] = $term_args;
209 } elseif ( $type === 'Author' ) {
210 $author_headers = array_slice( $headers, $author_offset, 6 );
211 $author_row = array_slice( $row, $author_offset, 6 );
212 $author_row = array_pad( $author_row, count( $author_headers ), '' );
213
214 $author_data = array_combine( $author_headers, $author_row );
215
216 $data['authors'][$author_data['Author login']] = [
217 'author_id' => sanitize_text_field( $author_data['Author id'] ),
218 'author_login' => sanitize_text_field( $author_data['Author login'] ),
219 'author_email' => sanitize_text_field( $author_data['Author email'] ),
220 'author_display_name' => sanitize_text_field( $author_data['Author display name'] ),
221 'author_first_name' => sanitize_text_field( $author_data['Author first name'] ),
222 'author_last_name' => sanitize_text_field( $author_data['Author last name'] )
223 ];
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 );
230 $post_row = array_pad( $post_row, count( $post_headers ), '' );
231
232 $post_data = array_combine( $post_headers, $post_row );
233
234 $post_args = [
235 'post_id' => sanitize_text_field( $post_data['Docs ID'] ) ?? '',
236 'post_type' => $type === 'FAQ' ? 'betterdocs_faq' : 'docs',
237 'post_author' => sanitize_text_field( $post_data['Docs author'] ) ?? '',
238 'post_content' => sanitize_text_field( $post_data['Docs content'] ) ?? '',
239 'post_title' => sanitize_text_field( $post_data['Docs title'] ) ?? '',
240 'post_name' => sanitize_text_field( $post_data['Docs slug'] ) ?? '',
241 'post_excerpt' => sanitize_text_field( $post_data['Docs excerpt'] ) ?? '',
242 'status' => sanitize_text_field( $post_data['Docs status'] ) ?? 'publish',
243 'post_password' => sanitize_text_field( $post_data['Docs password'] ) ?? '',
244 'post_parent' => sanitize_text_field( $post_data['Docs parent'] ) ?? '',
245 'menu_order' => sanitize_text_field( $post_data['Docs menu order'] ) ?? '',
246 'post_date' => sanitize_text_field( $post_data['Docs date'] ) ?? '',
247 'post_date_gmt' => sanitize_text_field( $post_data['Docs date gmt'] ) ?? '',
248 'post_modified' => sanitize_text_field( $post_data['Docs modified date'] ) ?? '',
249 'post_modified_gmt' => sanitize_text_field( $post_data['Docs modified date gmt'] ) ?? '',
250 'terms' => [],
251 'postmeta' => []
252 ];
253
254 if ( isset( $post_data['Doc Categories'] ) && $data['terms'] ) {
255 $post_args['terms'] = array_merge(
256 $this->searchTermsByIds( $data['terms'], sanitize_text_field( $post_data['Doc Categories'] ) ),
257 $this->searchTermsByIds( $data['terms'], sanitize_text_field( $post_data['Doc Tags'] ) ),
258 $this->searchTermsByIds( $data['terms'], sanitize_text_field( $post_data['Knowledge Bases'] ) )
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 }
275 }
276
277 $data['posts'][] = $post_args;
278
279 if ( ! empty( $post_data['Docs attachement url'] ) ) {
280 $attachment_args = [
281 'post_type' => 'attachment',
282 'post_author' => sanitize_text_field( $post_data['Docs author'] ) ?? '',
283 'post_id' => sanitize_text_field( $post_data['Docs attachement ID'] ) ?? '',
284 'status' => 'inherit',
285 'post_content' => '',
286 'post_excerpt' => '',
287 'guid' => '',
288 'post_title' => pathinfo( sanitize_text_field( $post_data['Docs attachement url'] ), PATHINFO_FILENAME ),
289 'post_name' => pathinfo( sanitize_text_field( $post_data['Docs attachement url'] ), PATHINFO_FILENAME ),
290 'post_parent' => sanitize_text_field( $post_data['Docs ID'] ) ?? '',
291 'attachment_url' => sanitize_text_field( $post_data['Docs attachement url'] )
292 ];
293
294 $data['posts'][] = $attachment_args;
295 }
296 }
297 }
298
299 return $data;
300 }
301
302 public function searchTermsByIds( $terms, $termIds ) {
303 // Convert the comma-separated term IDs to an array
304 $termIdsArray = explode( ',', $termIds );
305
306 // Initialize the result array
307 $result = [];
308
309 // Iterate through each term_id in the array
310 foreach ( $termIdsArray as $termId ) {
311 // Find the corresponding term in the terms array
312 $foundTerm = array_filter(
313 $terms,
314 function ( $term ) use ( $termId ) {
315 return $term['term_id'] == $termId;
316 }
317 );
318
319 // If the term is found, add it to the result array
320 if ( ! empty( $foundTerm ) ) {
321 $foundTerm = reset( $foundTerm );
322 $result[] = [
323 'name' => $foundTerm['term_name'],
324 'slug' => $foundTerm['slug'],
325 'domain' => $foundTerm['term_taxonomy']
326 ];
327 }
328 }
329
330 return $result;
331 }
332 }
333