PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.5.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.5.4
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 / CSVExporter.php

CSVExporter.php in BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot 4.5.4, at includes/Admin/CSVExporter.php

650 lines 17.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace WPDeveloper\BetterDocs\Admin;
3
4 use Error;
5
6 if ( ! defined( 'ABSPATH' ) ) {
7 exit; // Exit if accessed directly.
8 }
9
10 #[\AllowDynamicProperties]
11 class CSVExporter {
12 private static $default_args = [
13 'content' => 'docs',
14 'author' => false,
15 'category' => false,
16 'start_date' => false,
17 'end_date' => false,
18 'status' => false,
19 'offset' => 0,
20 'limit' => -1,
21 'meta_query' => [], // If specified `meta_key` then will include all post(s) that have this meta_key.
22 'query_args' => []
23 ];
24
25 /**
26 * @var array
27 */
28 private $args;
29
30 /**
31 * @var wpdb
32 */
33 private $wpdb;
34
35 public function __construct( array $args = [] ) {
36 global $wpdb;
37
38 $this->args = wp_parse_args( $args, self::$default_args );
39
40 $this->wpdb = $wpdb;
41 }
42
43 public function combine_csv_data( $csv_data_array ) {
44 // Combine headers
45 $headers_combined = $csv_data_array[0][0];
46
47 foreach ( $csv_data_array as $csv_data ) {
48 $headers_combined = array_merge( $headers_combined, array_slice( $csv_data[0], 1 ) );
49 }
50
51 $csv_data_combined = [ $headers_combined ];
52
53 // Combine data
54 for ( $i = 1; $i < count( $csv_data_array[0] ); $i++ ) {
55 $combined_row = [];
56 foreach ( $csv_data_array as $csv_data ) {
57 $combined_row = array_merge( $combined_row, array_fill( 0, count( $csv_data_combined[0] ) - count( $combined_row ) ), [ $csv_data[ $i ][0] ], array_slice( $csv_data[ $i ], 1 ) );
58 }
59 $csv_data_combined[] = $combined_row;
60 }
61
62 return $csv_data_combined;
63 }
64
65
66 public function run(): array {
67 $allowed_post_types = [ 'docs', 'betterdocs_faq' ];
68
69 if ( $this->args['content'] === 'glossaries' ) {
70 return $this->handle_glossaries_export();
71 }
72
73 if ( ! in_array( $this->args['content'], $allowed_post_types ) ) {
74 return [];
75 }
76
77 $where = $this->wpdb->prepare(
78 "{$this->wpdb->posts}.post_type = %s",
79 $this->args['content']
80 );
81
82 if ( $this->args['status'] ) {
83 $where .= $this->wpdb->prepare(
84 " AND {$this->wpdb->posts}.post_status = %s",
85 $this->args['status']
86 );
87 } else {
88 $where .= " AND {$this->wpdb->posts}.post_status != 'auto-draft'";
89 }
90
91 if ( ! empty( $this->args['post__in'] ) ) {
92 $post_in = $this->args['post__in'];
93 $ids_placeholder = implode( ', ', array_fill( 0, count( $post_in ), '%d' ) );
94 $where .= $this->wpdb->prepare(
95 " AND {$this->wpdb->posts}.ID IN ($ids_placeholder)",
96 $post_in
97 );
98 }
99
100 $join = '';
101
102 if ( isset( $this->args['category_terms'] ) ) {
103 $join = "INNER JOIN {$this->wpdb->term_relationships} ON ({$this->wpdb->posts}.ID = {$this->wpdb->term_relationships}.object_id)";
104 $tax_terms = [];
105
106 // Handle doc categories
107 foreach ( $this->args['category_terms'] as $term_slug ) {
108 $term = get_term_by( 'slug', $term_slug, 'doc_category' );
109 if ( $term ) {
110 $tax_terms[] = $term->term_taxonomy_id;
111 }
112 }
113
114 if ( ! empty( $tax_terms ) ) {
115 $tax_placeholder = implode( ', ', array_fill( 0, count( $tax_terms ), '%d' ) );
116 $where .= $this->wpdb->prepare(
117 " AND {$this->wpdb->term_relationships}.term_taxonomy_id IN ($tax_placeholder)",
118 $tax_terms
119 );
120 }
121 } elseif ( isset( $this->args['kb_terms'] ) ) {
122 $join = "INNER JOIN {$this->wpdb->term_relationships} ON ({$this->wpdb->posts}.ID = {$this->wpdb->term_relationships}.object_id)";
123 $kb_terms = [];
124
125 foreach ( $this->args['kb_terms'] as $term_slug ) {
126 $term = get_term_by( 'slug', $term_slug, 'knowledge_base' );
127 if ( $term ) {
128 $kb_terms[] = $term->term_taxonomy_id;
129 }
130 }
131
132 if ( ! empty( $kb_terms ) ) {
133 $term_placeholder = implode( ', ', array_fill( 0, count( $kb_terms ), '%d' ) );
134 $where .= $this->wpdb->prepare(
135 " AND {$this->wpdb->term_relationships}.term_taxonomy_id IN ($term_placeholder)",
136 $kb_terms
137 );
138 }
139 }
140
141 if ( $this->args['author'] ) {
142 $where .= $this->wpdb->prepare(
143 " AND {$this->wpdb->posts}.post_author = %d",
144 $this->args['author']
145 );
146 }
147
148 if ( $this->args['start_date'] ) {
149 $where .= $this->wpdb->prepare(
150 " AND {$this->wpdb->posts}.post_date >= %s",
151 gmdate( 'Y-m-d', strtotime( $this->args['start_date'] ) )
152 );
153 }
154
155 if ( $this->args['end_date'] ) {
156 $where .= $this->wpdb->prepare(
157 " AND {$this->wpdb->posts}.post_date < %s",
158 gmdate( 'Y-m-d', strtotime( '+1 month', strtotime( $this->args['end_date'] ) ) )
159 );
160 }
161
162 if ( ! empty( $this->args['meta_query'] ) ) {
163 $meta_query = new \WP_Meta_Query( $this->args['meta_query'] );
164 $query_clauses = $meta_query->get_sql( 'post', $this->wpdb->posts, 'ID' );
165
166 $join .= ' ' . $query_clauses['join'];
167 $where .= ' ' . $query_clauses['where'];
168 }
169
170 // Get post IDs
171 $post_ids = $this->wpdb->get_col( "SELECT ID FROM {$this->wpdb->posts} $join WHERE $where" );
172
173 // Add FAQ post IDs if include_faq is true
174 if ( ! empty( $this->args['include_faq'] ) ) {
175 $faq_ids = get_posts(
176 [
177 'post_type' => 'betterdocs_faq',
178 'posts_per_page' => -1,
179 'fields' => 'ids',
180 'post_status' => 'publish',
181 'suppress_filters' => true,
182 ]
183 );
184 $post_ids = array_merge( $post_ids, $faq_ids );
185 }
186
187 $post_ids = WPMLSupport::expand_with_translations( $post_ids );
188
189 if ( empty( $post_ids ) ) {
190 return [
191 'success' => false,
192 'message' => 'No posts found matching the criteria.'
193 ];
194 }
195
196 // Get posts data
197 $posts = array_map( 'get_post', $post_ids );
198
199 // Prepare CSV data
200 $csv_data_terms = $this->get_terms_csv_data( $post_ids );
201 $csv_data_author = $this->authors_list( $post_ids );
202 $csv_data_posts = $this->get_posts_csv_data( $posts );
203
204 // Combine headers
205 $headers_combined = array_merge(
206 $csv_data_posts[0],
207 array_slice( $csv_data_author[0], 1 ),
208 array_slice( $csv_data_terms[0], 1 )
209 );
210
211 // Initialize the combined array with headers
212 $csv_data_combined = [ $headers_combined ];
213
214 $author_start_index = count( $csv_data_posts[0] );
215 $terms_start_index = $author_start_index + count( $csv_data_author[0] ) - 1;
216
217 // Author rows
218 for ( $i = 1; $i < count( $csv_data_author ); $i++ ) {
219 $combined_row = array_merge(
220 [ $csv_data_author[ $i ][0] ],
221 array_fill( 1, $author_start_index - 1, '' ),
222 array_slice( $csv_data_author[ $i ], 1 )
223 );
224 $csv_data_combined[] = $combined_row;
225 }
226
227 // Docs post rows
228 for ( $i = 1; $i < count( $csv_data_posts ); $i++ ) {
229 if ( $csv_data_posts[ $i ][0] === 'FAQ' ) {
230 continue;
231 }
232 $combined_row = array_merge(
233 $csv_data_posts[ $i ],
234 array_fill( 0, count( $headers_combined ) - count( $csv_data_posts[ $i ] ), '' )
235 );
236 $csv_data_combined[] = $combined_row;
237 }
238
239 // Term rows just before FAQ rows so category IDs can be resolved on import
240 for ( $i = 1; $i < count( $csv_data_terms ); $i++ ) {
241 $combined_row = array_merge(
242 [ $csv_data_terms[ $i ][0] ],
243 array_fill( 1, $terms_start_index - 1, '' ),
244 array_slice( $csv_data_terms[ $i ], 1 )
245 );
246 $csv_data_combined[] = $combined_row;
247 }
248
249 // FAQ post rows last
250 for ( $i = 1; $i < count( $csv_data_posts ); $i++ ) {
251 if ( $csv_data_posts[ $i ][0] !== 'FAQ' ) {
252 continue;
253 }
254 $combined_row = array_merge(
255 $csv_data_posts[ $i ],
256 array_fill( 0, count( $headers_combined ) - count( $csv_data_posts[ $i ] ), '' )
257 );
258 $csv_data_combined[] = $combined_row;
259 }
260
261 $filename = 'betterdocs.' . date( 'Y-m-d' ) . '.csv';
262 $csv_content = $this->generate_csv( $csv_data_combined );
263
264 return [
265 'success' => true,
266 'data' => [
267 'filename' => $filename,
268 'filetype' => 'text/csv',
269 'download' => $csv_content,
270 ]
271 ];
272 }
273
274 /**
275 * Retrieve terms associated with the specified object IDs and sort them based on term meta.
276 *
277 * @param array $post_ids An array of object IDs.
278 * @return array An array of WP_Term objects sorted based on term meta.
279 */
280 private function get_terms( array $post_ids, $include_faq = false ) {
281 $post_types = [
282 'docs'
283 ];
284
285 if ( $include_faq ) {
286 array_push( $post_types, 'betterdocs_faq' );
287 }
288
289 // Get the object taxonomies
290 $taxonomies = get_object_taxonomies( $post_types );
291
292 // Get the object terms with parent terms coming before their child terms
293 $terms = wp_get_object_terms( $post_ids, $taxonomies );
294
295 usort( $terms, array( $this, 'compare_terms_by_meta' ) );
296
297 return $terms;
298 }
299
300 /**
301 * Compare terms based on their associated term meta values.
302 *
303 * @param WP_Term $a The first term object.
304 * @param WP_Term $b The second term object.
305 * @return int Returns a negative value if $a is less than $b,
306 * a positive value if $a is greater than $b, or 0 if they are equal.
307 * Additionally, prioritize sorting terms by taxonomy order,
308 * with 'doc_category' terms appearing before other taxonomy terms.
309 */
310 public function compare_terms_by_meta( $a, $b ) {
311 // Define the order of taxonomies
312 $taxonomy_order = array(
313 'doc_category' => 0,
314 'knowledge_base' => 1,
315 'doc_tag' => 2,
316 );
317
318 // Get the taxonomy order for terms $a and $b
319 $order_a = isset( $taxonomy_order[ $a->taxonomy ] ) ? $taxonomy_order[ $a->taxonomy ] : PHP_INT_MAX;
320 $order_b = isset( $taxonomy_order[ $b->taxonomy ] ) ? $taxonomy_order[ $b->taxonomy ] : PHP_INT_MAX;
321
322 // If the taxonomies have different order, sort by order
323 if ( $order_a !== $order_b ) {
324 return $order_a - $order_b;
325 }
326
327 // If the taxonomies have the same order, sort by meta value
328 $taxonomy_order_meta = array(
329 'doc_category' => 'doc_category_order',
330 'knowledge_base' => 'kb_order'
331 );
332
333 if ( isset( $taxonomy_order_meta[ $a->taxonomy ] ) && isset( $taxonomy_order_meta[ $b->taxonomy ] ) ) {
334 $meta_a = intval( get_term_meta( $a->term_id, $taxonomy_order_meta[ $a->taxonomy ], true ) );
335 $meta_b = intval( get_term_meta( $b->term_id, $taxonomy_order_meta[ $b->taxonomy ], true ) );
336
337 return $meta_a - $meta_b;
338 }
339
340 return 0; // Default to no sorting if meta keys are not defined
341 }
342
343 /**
344 * Sorting function for sorting term data.
345 *
346 * @param array $a The first array to compare.
347 * @param array $b The second array to compare.
348 *
349 * @return int Returns an integer less than, equal to, or greater than zero if the first array is considered
350 * to be respectively less than, equal to, or greater than the second.
351 */
352 public function sort_terms( $a, $b ) {
353 // Compare parent values
354 $parentComparison = strcmp( $a[7], $b[7] );
355
356 // If parent values are equal, compare Term ID values
357 return ( $parentComparison === 0 ) ? strcmp( $a[2], $b[2] ) : $parentComparison;
358 }
359
360 private function get_terms_csv_data( $post_ids ) {
361 $csv_data_terms = [];
362
363 // Add CSV headers for terms
364 $csv_data_terms[] = [
365 'Type',
366 'Taxonomy',
367 'Term ID',
368 'Term name',
369 'Term slug',
370 'Term group',
371 'Term description',
372 'Term parent',
373 'Assigned Docs',
374 'Assigned KBs',
375 'Doc Category order',
376 'KB order', // Add additional term meta headers here
377 ];
378
379 if ( $this->args['content'] == 'glossaries' ) {
380 $terms = get_terms(
381 [
382 'taxonomy' => 'glossaries',
383 'hide_empty' => false,
384 ]
385 );
386 } else {
387 $terms = $this->get_terms( $post_ids, $this->args['include_faq'] );
388 }
389 foreach ( $terms as $term ) {
390 $term_meta = '';
391
392 // Add term meta based on taxonomy
393 switch ( $term->taxonomy ) {
394 case 'doc_category':
395 $doc_category_knowledge_base = maybe_unserialize( get_term_meta( $term->term_id, 'doc_category_knowledge_base', true ) );
396 if ( is_array( $doc_category_knowledge_base ) && $doc_category_knowledge_base !== false ) {
397 $doc_category_knowledge_base = implode( ', ', array_filter( $doc_category_knowledge_base ) );
398 } else {
399 $doc_category_knowledge_base = '';
400 }
401
402 $term_meta = [
403 '_docs_order' => get_term_meta( $term->term_id, '_docs_order', true ),
404 'doc_category_knowledge_base' => $doc_category_knowledge_base,
405 'doc_category_order' => get_term_meta( $term->term_id, 'doc_category_order', true ),
406 ];
407 break;
408
409 case 'knowledge_base':
410 $term_meta = [
411 'kb_order' => get_term_meta( $term->term_id, 'kb_order', true ),
412 ];
413 break;
414 }
415
416 $parent = $term->parent ? get_term_by( 'id', $term->parent, $term->taxonomy ) : '';
417 // Add CSV row for term
418 $csv_data_terms[] = [
419 'Term',
420 $term->taxonomy,
421 $term->term_id,
422 $term->name,
423 $term->slug,
424 $term->term_group,
425 $term->description,
426 $parent ? $parent->slug : '',
427 isset( $term_meta['_docs_order'] ) ? $term_meta['_docs_order'] : '',
428 isset( $term_meta['doc_category_knowledge_base'] ) ? $term_meta['doc_category_knowledge_base'] : '',
429 isset( $term_meta['doc_category_order'] ) ? $term_meta['doc_category_order'] : '',
430 isset( $term_meta['kb_order'] ) ? $term_meta['kb_order'] : '',
431 ];
432 }
433
434 return $csv_data_terms;
435 }
436
437 private function handle_glossaries_export(): array {
438 if ( isset( $this->args['glossary_terms'] ) && ( count( $this->args['glossary_terms'] ) > 0 ) ) {
439 $glossary_term_ids = [];
440 foreach ( $this->args['glossary_terms'] as $glossary_slug ) {
441 $term_object = get_term_by( 'slug', $glossary_slug, 'glossaries' );
442 if ( isset( $term_object->term_id ) && ! empty( $term_object->term_id ) ) {
443 array_push( $glossary_term_ids, $term_object->term_id );
444 }
445 }
446 } else {
447 $glossary_term_ids = $this->wpdb->get_col( "SELECT term_id from {$this->wpdb->term_taxonomy} where taxonomy='{$this->args['content']}';" );
448 }
449
450 $filename = 'betterdocs.' . date( 'Y-m-d' ) . '.csv';
451 $csv_data_combined = $this->get_glossaries_csv_data( $glossary_term_ids );
452 $csv_content = $this->generate_csv( $csv_data_combined );
453
454 return [
455 'success' => true,
456 'data' => [
457 'filename' => $filename,
458 'filetype' => 'text/csv',
459 'download' => $csv_content,
460 ],
461 ];
462 }
463
464 private function get_glossaries_csv_data( $post_ids ) {
465 $csv_data_terms = [];
466
467 if ( $this->args['content'] == 'glossaries' ) {
468 $terms = get_terms(
469 [
470 'taxonomy' => 'glossaries',
471 'hide_empty' => false,
472 ]
473 );
474 } else {
475 $terms = $this->get_terms( $post_ids, $this->args['include_faq'] );
476 }
477
478 if ( is_wp_error( $terms ) ) {
479 return $csv_data_terms;
480 }
481
482 // Add CSV headers for terms
483 $csv_data_terms[] = [
484 'Taxonomy',
485 'Term ID',
486 'Term name',
487 'Term slug',
488 'Term group',
489 'Term description'
490 ];
491
492 foreach ( $terms as $term ) {
493 // Add CSV row for term
494 $csv_data_terms[] = [
495 $term->taxonomy,
496 $term->term_id,
497 $term->name,
498 $term->slug,
499 $term->term_group,
500 get_term_meta( $term->term_id, 'glossary_term_description', true )
501 ];
502 }
503
504 return $csv_data_terms;
505 }
506
507 /**
508 * Return list of authors with posts.
509 *
510 * @param int[] $post_ids Optional. Array of post IDs to filter the query by.
511 *
512 * @return string
513 */
514 private function authors_list( $post_ids ) {
515 $authors = [];
516
517 // Add CSV headers for terms
518 $authors[] = [
519 'Type',
520 'Author id',
521 'Author login',
522 'Author email',
523 'Author display name',
524 'Author first name',
525 'Author last name'
526 ];
527
528 if ( ! empty( $post_ids ) ) {
529 $post_ids = array_map( 'absint', $post_ids );
530 $and = 'AND ID IN ( ' . implode( ', ', $post_ids ) . ')';
531 } else {
532 $and = '';
533 }
534 $authors_data = [];
535 $results = $this->wpdb->get_results( "SELECT DISTINCT post_author FROM {$this->wpdb->posts} WHERE post_status != 'auto-draft' $and" );// phpcs:ignore
536 foreach ( (array) $results as $r ) {
537 $authors_data[] = get_userdata( $r->post_author );
538 }
539
540 $authors_data = array_filter( $authors_data );
541
542 foreach ( $authors_data as $author ) {
543 $authors[] = [
544 'Author',
545 $author->ID,
546 $author->user_login,
547 $author->user_email,
548 $author->display_name,
549 $author->first_name,
550 $author->last_name
551 ];
552 }
553
554 return $authors;
555 }
556
557 private function get_posts_csv_data( $posts ) {
558 $csv_data_posts = [];
559
560 // Add CSV headers for posts
561 $csv_data_posts[] = [
562 'Type',
563 'Docs ID',
564 'Docs author',
565 'Docs date',
566 'Docs date gmt',
567 'Docs title',
568 'Docs content',
569 'Docs excerpt',
570 'Docs status',
571 'Docs password',
572 'Docs slug',
573 'Docs modified date',
574 'Docs modified date gmt',
575 'Docs parent',
576 'Docs menu order',
577 'Docs mime type',
578 'Comment count',
579 'Doc Categories',
580 'Doc Tags',
581 'Knowledge Bases',
582 'Docs attachement url',
583 'Docs attachement ID',
584 'Docs language code',
585 'Docs translation source slug',
586 ];
587
588 foreach ( $posts as $post ) {
589 $attachment_id = get_post_thumbnail_id( $post->ID );
590 $attachment_url = get_the_post_thumbnail_url( $post->ID );
591 $wpml = WPMLSupport::get_post_language_meta( (int) $post->ID );
592 // Add CSV row for post
593 $csv_data_posts[] = [
594 $post->post_type == 'betterdocs_faq' ? 'FAQ' : 'Docs',
595 $post->ID,
596 $post->post_author,
597 $post->post_date,
598 $post->post_date_gmt,
599 $post->post_title,
600 $post->post_content,
601 $post->post_excerpt,
602 $post->post_status,
603 $post->post_password,
604 $post->post_name,
605 $post->post_modified,
606 $post->post_modified_gmt,
607 $post->post_parent,
608 $post->menu_order,
609 $post->post_mime_type,
610 $post->comment_count,
611 $this->get_term_ids( $post->ID, $post->post_type === 'betterdocs_faq' ? 'betterdocs_faq_category' : 'doc_category' ),
612 $this->get_term_ids( $post->ID, 'doc_tag' ),
613 $this->get_term_ids( $post->ID, 'knowledge_base' ),
614 $attachment_url ? $attachment_url : '',
615 $attachment_id ? $attachment_id : '',
616 $wpml ? $wpml['language_code'] : '',
617 $wpml ? $wpml['source_slug'] : '',
618 ];
619 }
620
621 return $csv_data_posts;
622 }
623
624 public function get_term_ids( $post_id, $taxonomy ) {
625 $terms = get_the_terms( $post_id, $taxonomy );
626
627 if ( $terms && ! is_wp_error( $terms ) ) {
628 $term_ids = wp_list_pluck( $terms, 'term_id' );
629 return implode( ', ', $term_ids );
630 }
631
632 return '';
633 }
634
635 private function generate_csv( array $data ): string {
636 ob_start();
637
638 $output = fopen( 'php://output', 'w' );
639
640 // Add CSV rows
641 foreach ( $data as $row ) {
642 fputcsv( $output, $row );
643 }
644
645 fclose( $output );
646
647 return ob_get_clean();
648 }
649 }
650