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