PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.8.9
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.8.9
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 3.5.1 All 200 releases
← All changes | includes/Admin/CSVExporter.php +32 -99 4.9.03.8.9 View file →
@@ -1,9 +1,7 @@
1 1 <?php
2 2 namespace WPDeveloper\BetterDocs\Admin;
3 3
4 -use Error;
5 -
6 4 if ( ! defined( 'ABSPATH' ) ) {
7 5 exit; // Exit if accessed directly.
8 6 }
9 7
@@ -17,9 +15,8 @@
17 15 'end_date' => false,
18 16 'status' => false,
19 17 'offset' => 0,
20 18 'limit' => -1,
21 - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query -- exporter accepts user-defined meta filters by design.
22 19 'meta_query' => [], // If specified `meta_key` then will include all post(s) that have this meta_key.
23 20 'query_args' => []
24 21 ];
25 22
@@ -74,11 +71,8 @@
74 71 if ( ! in_array( $this->args['content'], $allowed_post_types ) ) {
75 72 return [];
76 73 }
77 74
78 - // $this->wpdb->posts and $this->wpdb->term_relationships are WP-provided table identifiers.
79 - // Dynamic %d placeholder lists are built to match the corresponding integer arrays.
80 - // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
81 75 $where = $this->wpdb->prepare(
82 76 "{$this->wpdb->posts}.post_type = %s",
83 77 $this->args['content']
84 78 );
@@ -92,9 +86,9 @@
92 86 $where .= " AND {$this->wpdb->posts}.post_status != 'auto-draft'";
93 87 }
94 88
95 89 if ( ! empty( $this->args['post__in'] ) ) {
96 - $post_in = array_map( 'intval', $this->args['post__in'] );
90 + $post_in = $this->args['post__in'];
97 91 $ids_placeholder = implode( ', ', array_fill( 0, count( $post_in ), '%d' ) );
98 92 $where .= $this->wpdb->prepare(
99 93 " AND {$this->wpdb->posts}.ID IN ($ids_placeholder)",
100 94 $post_in
@@ -110,9 +104,9 @@
110 104 // Handle doc categories
111 105 foreach ( $this->args['category_terms'] as $term_slug ) {
112 106 $term = get_term_by( 'slug', $term_slug, 'doc_category' );
113 107 if ( $term ) {
114 - $tax_terms[] = (int) $term->term_taxonomy_id;
108 + $tax_terms[] = $term->term_taxonomy_id;
115 109 }
116 110 }
117 111
118 112 if ( ! empty( $tax_terms ) ) {
@@ -123,24 +117,17 @@
123 117 );
124 118 }
125 119 } elseif ( isset( $this->args['kb_terms'] ) ) {
126 120 $join = "INNER JOIN {$this->wpdb->term_relationships} ON ({$this->wpdb->posts}.ID = {$this->wpdb->term_relationships}.object_id)";
127 - $kb_terms = [];
128 -
129 121 foreach ( $this->args['kb_terms'] as $term_slug ) {
130 122 $term = get_term_by( 'slug', $term_slug, 'knowledge_base' );
131 123 if ( $term ) {
132 - $kb_terms[] = (int) $term->term_taxonomy_id;
124 + $where .= $this->wpdb->prepare(
125 + " AND {$this->wpdb->term_relationships}.term_taxonomy_id = %d",
126 + $term->term_taxonomy_id
127 + );
133 128 }
134 129 }
135 -
136 - if ( ! empty( $kb_terms ) ) {
137 - $term_placeholder = implode( ', ', array_fill( 0, count( $kb_terms ), '%d' ) );
138 - $where .= $this->wpdb->prepare(
139 - " AND {$this->wpdb->term_relationships}.term_taxonomy_id IN ($term_placeholder)",
140 - $kb_terms
141 - );
142 - }
143 130 }
144 131
145 132 if ( $this->args['author'] ) {
146 133 $where .= $this->wpdb->prepare(
@@ -170,10 +157,9 @@
170 157 $join .= ' ' . $query_clauses['join'];
171 158 $where .= ' ' . $query_clauses['where'];
172 159 }
173 160
174 - // $where and $join are composed from prepared fragments above; identifiers are WP-provided.
175 - // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter
161 + // Get post IDs
176 162 $post_ids = $this->wpdb->get_col( "SELECT ID FROM {$this->wpdb->posts} $join WHERE $where" );
177 163
178 164 // Add FAQ post IDs if include_faq is true
179 165 if ( ! empty( $this->args['include_faq'] ) ) {
@@ -181,18 +167,14 @@
181 167 [
182 168 'post_type' => 'betterdocs_faq',
183 169 'posts_per_page' => -1,
184 170 'fields' => 'ids',
185 - 'post_status' => 'publish',
186 - // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.SuppressFilters_suppress_filters -- intentional: export the raw, untranslated FAQ set so multilingual filters don't drop or swap rows during export.
187 - 'suppress_filters' => true,
171 + 'post_status' => 'publish'
188 172 ]
189 173 );
190 174 $post_ids = array_merge( $post_ids, $faq_ids );
191 175 }
192 176
193 - $post_ids = WPMLSupport::expand_with_translations( $post_ids );
194 -
195 177 if ( empty( $post_ids ) ) {
196 178 return [
197 179 'success' => false,
198 180 'message' => 'No posts found matching the criteria.'
@@ -216,12 +198,19 @@
216 198
217 199 // Initialize the combined array with headers
218 200 $csv_data_combined = [ $headers_combined ];
219 201
202 + // Combine posts data
203 + for ( $i = 1; $i < count( $csv_data_posts ); $i++ ) {
204 + $combined_row = array_merge(
205 + $csv_data_posts[ $i ],
206 + array_fill( 0, count( $headers_combined ) - count( $csv_data_posts[ $i ] ), '' )
207 + );
208 + $csv_data_combined[] = $combined_row;
209 + }
210 +
211 + // Combine author data
220 212 $author_start_index = count( $csv_data_posts[0] );
221 - $terms_start_index = $author_start_index + count( $csv_data_author[0] ) - 1;
222 -
223 - // Author rows
224 213 for ( $i = 1; $i < count( $csv_data_author ); $i++ ) {
225 214 $combined_row = array_merge(
226 215 [ $csv_data_author[ $i ][0] ],
227 216 array_fill( 1, $author_start_index - 1, '' ),
@@ -229,21 +218,10 @@
229 218 );
230 219 $csv_data_combined[] = $combined_row;
231 220 }
232 221
233 - // Docs post rows
234 - for ( $i = 1; $i < count( $csv_data_posts ); $i++ ) {
235 - if ( $csv_data_posts[ $i ][0] === 'FAQ' ) {
236 - continue;
237 - }
238 - $combined_row = array_merge(
239 - $csv_data_posts[ $i ],
240 - array_fill( 0, count( $headers_combined ) - count( $csv_data_posts[ $i ] ), '' )
241 - );
242 - $csv_data_combined[] = $combined_row;
243 - }
244 -
245 - // Term rows just before FAQ rows so category IDs can be resolved on import
222 + // Combine terms data
223 + $terms_start_index = $author_start_index + count( $csv_data_author[0] ) - 1;
246 224 for ( $i = 1; $i < count( $csv_data_terms ); $i++ ) {
247 225 $combined_row = array_merge(
248 226 [ $csv_data_terms[ $i ][0] ],
249 227 array_fill( 1, $terms_start_index - 1, '' ),
@@ -251,21 +229,9 @@
251 229 );
252 230 $csv_data_combined[] = $combined_row;
253 231 }
254 232
255 - // FAQ post rows last
256 - for ( $i = 1; $i < count( $csv_data_posts ); $i++ ) {
257 - if ( $csv_data_posts[ $i ][0] !== 'FAQ' ) {
258 - continue;
259 - }
260 - $combined_row = array_merge(
261 - $csv_data_posts[ $i ],
262 - array_fill( 0, count( $headers_combined ) - count( $csv_data_posts[ $i ] ), '' )
263 - );
264 - $csv_data_combined[] = $combined_row;
265 - }
266 -
267 - $filename = 'betterdocs.' . gmdate( 'Y-m-d' ) . '.csv';
233 + $filename = 'betterdocs.' . date( 'Y-m-d' ) . '.csv';
268 234 $csv_content = $this->generate_csv( $csv_data_combined );
269 235
270 236 return [
271 237 'success' => true,
@@ -449,20 +415,12 @@
449 415 array_push( $glossary_term_ids, $term_object->term_id );
450 416 }
451 417 }
452 418 } else {
453 - // $this->wpdb->term_taxonomy is a WP-core table identifier; %s placeholder binds taxonomy name.
454 - // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter
455 - $glossary_term_ids = $this->wpdb->get_col(
456 - $this->wpdb->prepare(
457 - "SELECT term_id FROM {$this->wpdb->term_taxonomy} WHERE taxonomy = %s",
458 - (string) $this->args['content']
459 - )
460 - );
461 - // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter
419 + $glossary_term_ids = $this->wpdb->get_col( "SELECT term_id from {$this->wpdb->term_taxonomy} where taxonomy='{$this->args['content']}';" );
462 420 }
463 421
464 - $filename = 'betterdocs.' . gmdate( 'Y-m-d' ) . '.csv';
422 + $filename = 'betterdocs.' . date( 'Y-m-d' ) . '.csv';
465 423 $csv_data_combined = $this->get_glossaries_csv_data( $glossary_term_ids );
466 424 $csv_content = $this->generate_csv( $csv_data_combined );
467 425
468 426 return [
@@ -593,17 +551,14 @@
593 551 'Doc Categories',
594 552 'Doc Tags',
595 553 'Knowledge Bases',
596 554 'Docs attachement url',
597 - 'Docs attachement ID',
598 - 'Docs language code',
599 - 'Docs translation source slug',
555 + 'Docs attachement ID'
600 556 ];
601 557
602 558 foreach ( $posts as $post ) {
603 559 $attachment_id = get_post_thumbnail_id( $post->ID );
604 560 $attachment_url = get_the_post_thumbnail_url( $post->ID );
605 - $wpml = WPMLSupport::get_post_language_meta( (int) $post->ID );
606 561 // Add CSV row for post
607 562 $csv_data_posts[] = [
608 563 $post->post_type == 'betterdocs_faq' ? 'FAQ' : 'Docs',
609 564 $post->ID,
@@ -621,15 +576,13 @@
621 576 $post->post_parent,
622 577 $post->menu_order,
623 578 $post->post_mime_type,
624 579 $post->comment_count,
625 - $this->get_term_ids( $post->ID, $post->post_type === 'betterdocs_faq' ? [ 'betterdocs_faq_category', 'betterdocs_product_faq_category' ] : 'doc_category' ),
580 + $this->get_term_ids( $post->ID, 'doc_category' ),
626 581 $this->get_term_ids( $post->ID, 'doc_tag' ),
627 582 $this->get_term_ids( $post->ID, 'knowledge_base' ),
628 583 $attachment_url ? $attachment_url : '',
629 - $attachment_id ? $attachment_id : '',
630 - $wpml ? $wpml['language_code'] : '',
631 - $wpml ? $wpml['source_slug'] : '',
584 + $attachment_id ? $attachment_id : ''
632 585 ];
633 586 }
634 587
635 588 return $csv_data_posts;
@@ -635,15 +588,13 @@
635 588 return $csv_data_posts;
636 589 }
637 590
638 591 public function get_term_ids( $post_id, $taxonomy ) {
639 - // Accept one or more taxonomies. FAQ posts can live in either the general
640 - // (betterdocs_faq_category) or the Product FAQ (betterdocs_product_faq_category)
641 - // taxonomy, so both are queried for the FAQ group column.
642 - $term_ids = wp_get_object_terms( $post_id, (array) $taxonomy, [ 'fields' => 'ids' ] );
592 + $terms = get_the_terms( $post_id, $taxonomy );
643 593
644 - if ( $term_ids && ! is_wp_error( $term_ids ) ) {
645 - return implode( ', ', array_map( 'intval', $term_ids ) );
594 + if ( $terms && ! is_wp_error( $terms ) ) {
595 + $term_ids = wp_list_pluck( $terms, 'term_id' );
596 + return implode( ', ', $term_ids );
646 597 }
647 598
648 599 return '';
649 600 }
@@ -652,32 +603,14 @@
652 603 ob_start();
653 604
654 605 $output = fopen( 'php://output', 'w' );
655 606
656 - // Add CSV rows. Neutralize spreadsheet formula injection: a cell that a
657 - // lower-privileged author controls (e.g. a doc/FAQ title or term name) could
658 - // start with =, +, -, @, or a tab/CR and execute when the admin opens the
659 - // export in Excel/LibreOffice. Prefix such cells with a single quote.
607 + // Add CSV rows
660 608 foreach ( $data as $row ) {
661 - fputcsv( $output, array_map( [ $this, 'neutralize_csv_cell' ], (array) $row ) );
609 + fputcsv( $output, $row );
662 610 }
663 611
664 - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- closing php://output stream; WP_Filesystem does not apply.
665 612 fclose( $output );
666 613
667 614 return ob_get_clean();
668 - }
669 -
670 - /**
671 - * Prefix a leading formula trigger (= + - @ tab CR) with a single quote so
672 - * spreadsheet apps treat the cell as text instead of executing it.
673 - */
674 - private function neutralize_csv_cell( $cell ) {
675 - $cell = (string) $cell;
676 -
677 - if ( $cell !== '' && preg_match( '/^[=+\-@\t\r]/', $cell ) ) {
678 - return "'" . $cell;
679 - }
680 -
681 - return $cell;
682 615 }
683 616 }