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

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

423 lines 14.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;
4
5 use WPDeveloper\BetterDocs\Admin\ExportDefaults;
6 use WP_Term;
7 use wpdb;
8
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 /**
15 * SQL building helpers below interpolate WP-provided $wpdb table identifiers
16 * (`$wpdb->posts`, `$wpdb->term_relationships`, `$wpdb->term_taxonomy`) and use
17 * dynamic %d placeholder lists derived from integer arrays. Suppressing the
18 * related PHPCS notices class-wide rather than per-call.
19 *
20 * phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
21 * phpcs:disable WordPress.DB.PreparedSQLPlaceholders.UnfinishedPrepare
22 * phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
23 * phpcs:disable WordPress.DB.DirectDatabaseQuery.NoCaching
24 */
25 #[\AllowDynamicProperties]
26 class WPExporter {
27 /**
28 * @var array
29 */
30 private $args;
31
32 /**
33 * @var wpdb
34 */
35 private $wpdb;
36
37 public function __construct( array $args = [] ) {
38 global $wpdb;
39
40 $this->args = wp_parse_args($args, ExportDefaults::get_default_args());
41
42 $this->wpdb = $wpdb;
43 }
44
45 public function exporter_ids(): array {
46 // Build the base query
47 $query_parts = $this->build_base_query();
48 $where = $query_parts['where'];
49 $join = $query_parts['join'];
50
51 // Handle post status
52 $where .= $this->build_status_condition();
53
54 // Handle specific posts selection
55 if ( ! empty( $this->args['post__in'] ) ) {
56 $where .= $this->build_post_in_condition();
57 }
58
59 // Handle category terms
60 if ( isset( $this->args['category_terms'] ) ) {
61 $category_query = $this->build_category_query();
62 $join .= $category_query['join'];
63 $where .= $category_query['where'];
64 }
65
66 // Handle knowledge base terms
67 if ( isset( $this->args['kb_terms'] ) ) {
68 $kb_query = $this->build_kb_query();
69 $join .= $kb_query['join'];
70 $where .= $kb_query['where'];
71 }
72
73 // Handle additional filters (author, dates, meta)
74 $where .= $this->build_additional_filters();
75
76 // $join/$where are composed from prepared fragments above; identifiers are WP-provided.
77 // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter
78 $post_ids = $this->wpdb->get_col( "SELECT DISTINCT {$this->wpdb->posts}.ID FROM {$this->wpdb->posts} $join WHERE $where" );
79
80 // Handle FAQ posts separately
81 $faq_post_ids = [];
82 if ( ! empty( $this->args['include_faq'] ) ) {
83 $faq_post_ids = $this->get_faq_posts();
84 }
85
86 // Combine post IDs
87 $all_post_ids = array_merge( $post_ids, $faq_post_ids );
88
89 $all_post_ids = WPMLSupport::expand_with_translations( $all_post_ids );
90
91 // Handle featured images
92 $thumbnail_ids = $this->get_thumbnail_ids( $all_post_ids );
93
94 // Generate final post IDs array
95 return array_unique(array_merge($all_post_ids, $thumbnail_ids));
96
97 }
98
99 // private function handle_glossaries_export(): array {
100 public function get_glossary_term_ids(): array {
101 if ( isset( $this->args['glossary_terms'] ) && ( count( $this->args['glossary_terms'] ) > 0 ) ) {
102 $glossary_term_ids = [];
103 foreach( $this->args['glossary_terms'] as $glossary_slug ) {
104 $term_object = get_term_by('slug', $glossary_slug , 'glossaries');
105 if( isset( $term_object->term_id ) && ! empty( $term_object->term_id ) ){
106 array_push($glossary_term_ids, $term_object->term_id);
107 }
108 }
109 } else {
110 // $this->wpdb->term_taxonomy is a WP-core table identifier; %s placeholder binds taxonomy name.
111 // phpcs:disable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter
112 $glossary_term_ids = $this->wpdb->get_col(
113 $this->wpdb->prepare(
114 "SELECT term_id FROM {$this->wpdb->term_taxonomy} WHERE taxonomy = %s",
115 (string) $this->args['content']
116 )
117 );
118 // phpcs:enable WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.PreparedSQL.InterpolatedNotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching,PluginCheck.Security.DirectDB.UnescapedDBParameter
119 }
120
121 return $glossary_term_ids;
122 }
123
124 // The query-builder methods below interpolate only $wpdb core table names
125 // ({$this->wpdb->posts}, etc.) — never user input — and bind every value via
126 // %s/%d placeholders, so the InterpolatedNotPrepared warnings are spurious.
127 // phpcs:disable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
128 public function build_base_query(): array {
129 $where = $this->wpdb->prepare("{$this->wpdb->posts}.post_type = %s", 'docs');
130 return [
131 'where' => $where,
132 'join' => ''
133 ];
134 }
135
136 public function build_status_condition(): string {
137 if ($this->args['status']) {
138 return $this->wpdb->prepare(" AND {$this->wpdb->posts}.post_status = %s", $this->args['status']);
139 }
140 return " AND {$this->wpdb->posts}.post_status != 'auto-draft'";
141 }
142
143 public function build_post_in_condition(): string {
144 $ids = $this->args['post__in'];
145 $ids_placeholder = implode(', ', array_fill(0, count($ids), '%d'));
146 return $this->wpdb->prepare(" AND {$this->wpdb->posts}.ID IN ($ids_placeholder)", $ids);
147 }
148
149 public function build_category_query(): array {
150 $join = "INNER JOIN {$this->wpdb->term_relationships} tr ON ({$this->wpdb->posts}.ID = tr.object_id)
151 INNER JOIN {$this->wpdb->term_taxonomy} tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)";
152
153 $where = " AND tt.taxonomy = 'doc_category'";
154
155 if ( ! empty( $this->args['category_terms'] ) ) {
156 $term_ids = [];
157 foreach ( $this->args['category_terms'] as $term_id ) {
158 $term = term_exists( $term_id, 'doc_category' );
159 if ( $term ) {
160 $term_ids[] = $term['term_taxonomy_id'];
161 }
162 }
163
164 if ( ! empty( $term_ids ) ) {
165 $term_placeholder = implode( ', ', array_fill( 0, count( $term_ids ), '%d' ) );
166 $where .= $this->wpdb->prepare( " AND tt.term_taxonomy_id IN ($term_placeholder)", $term_ids );
167 }
168 }
169
170 return [
171 'join' => $join,
172 'where' => $where
173 ];
174 }
175
176 public function build_kb_query(): array {
177 $join = "INNER JOIN {$this->wpdb->term_relationships} tr ON ({$this->wpdb->posts}.ID = tr.object_id)
178 INNER JOIN {$this->wpdb->term_taxonomy} tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)";
179
180 $where = " AND tt.taxonomy = 'knowledge_base'";
181
182 if ( ! empty( $this->args['kb_terms'] ) ) {
183 $term_ids = [];
184 foreach ( $this->args['kb_terms'] as $term_id ) {
185 $term = term_exists( $term_id, 'knowledge_base' );
186 if ( $term ) {
187 $term_ids[] = $term['term_taxonomy_id'];
188 }
189 }
190
191 if ( ! empty( $term_ids ) ) {
192 $term_placeholder = implode( ', ', array_fill( 0, count( $term_ids ), '%d' ) );
193 $where .= $this->wpdb->prepare( " AND tt.term_taxonomy_id IN ($term_placeholder)", $term_ids );
194 }
195 }
196
197 return [
198 'join' => $join,
199 'where' => $where
200 ];
201 }
202
203 public function build_additional_filters(): string {
204 $where = '';
205
206 if ( $this->args['author'] ) {
207 $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_author = %d", $this->args['author'] );
208 }
209
210 if ( $this->args['start_date'] ) {
211 $where .= $this->wpdb->prepare(
212 " AND {$this->wpdb->posts}.post_date >= %s",
213 gmdate( 'Y-m-d', strtotime( $this->args['start_date'] ) )
214 );
215 }
216
217 if ( $this->args['end_date'] ) {
218 $where .= $this->wpdb->prepare(
219 " AND {$this->wpdb->posts}.post_date < %s",
220 gmdate( 'Y-m-d', strtotime( '+1 month', strtotime( $this->args['end_date'] ) ) )
221 );
222 }
223
224 return $where;
225 }
226
227 // phpcs:enable WordPress.DB.PreparedSQL.InterpolatedNotPrepared
228 public function get_faq_posts(): array {
229 return get_posts([
230 'numberposts' => -1,
231 'post_type' => 'betterdocs_faq',
232 'fields' => 'ids',
233 'post_status' => 'publish',
234 // 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.
235 'suppress_filters' => true,
236 ]);
237 }
238
239 public function get_thumbnail_ids(array $post_ids): array {
240 $thumbnail_ids = [];
241
242 if ( ! empty( $this->args['include_post_featured_image_as_attachment'] ) ) {
243 foreach ( $post_ids as $post_id ) {
244 $thumbnail_id = get_post_meta( $post_id, '_thumbnail_id', true );
245 if ( $thumbnail_id && ! in_array( $thumbnail_id, $post_ids, true ) ) {
246 $thumbnail_ids[] = $thumbnail_id;
247 }
248 }
249 }
250
251 return $thumbnail_ids;
252 }
253
254 /**
255 * Return tabulation characters, by `$columns`.
256 *
257 * @param int $columns
258 *
259 * @return string
260 */
261 public function indent( int $columns = 1 ): string {
262
263 $output = str_repeat( "\t", $columns );
264
265 return (string) $output;
266 }
267
268 /**
269 * Retrieve terms associated with the specified object IDs and sort them based on term meta.
270 *
271 * @param array $post_ids An array of object IDs.
272 * @return array An array of WP_Term objects sorted based on term meta.
273 */
274 public function get_terms(array $post_ids = []) {
275 // Get the object taxonomies
276 $taxonomies = get_object_taxonomies($this->args['content']);
277
278 $terms = [];
279
280 // Handle glossaries
281 if ($this->args['content'] == 'glossaries') {
282 $terms = get_terms([
283 'taxonomy' => 'glossaries',
284 'include' => $post_ids,
285 'hide_empty' => false,
286 ]);
287
288 if (is_wp_error($terms)) {
289 return [];
290 }
291
292 // Include parent terms for hierarchical glossaries
293 $parent_terms = [];
294 foreach ($terms as $term) {
295 if (!empty($term->parent)) {
296 $parent = get_term($term->parent, 'glossaries');
297 if (!is_wp_error($parent)) {
298 $parent_terms[] = $parent;
299 }
300 }
301 }
302
303 if (!empty($parent_terms)) {
304 $terms = array_merge($terms, $parent_terms);
305 $terms = array_unique($terms, SORT_REGULAR);
306 }
307 } elseif ($this->args['content'] == 'docs') { // Handle docs
308 if (isset($this->args['selected_docs']) && !empty($this->args['selected_docs'])) {
309 if ($this->args['selected_docs'][0] == 'all') {
310 // Fetch all terms
311 $terms = get_terms([
312 'taxonomy' => $taxonomies,
313 'hide_empty' => false,
314 ]);
315 } else {
316 // Fetch terms for specific post IDs
317 $terms = wp_get_object_terms($this->args['selected_docs'], $taxonomies);
318 }
319 }
320 } elseif (in_array($this->args['content'], ['knowledge_base', 'doc_category'])) {
321 // Handle both knowledge_base and doc_category cases
322 $terms_key = $this->args['content'] == 'doc_category' ? 'category_terms' : 'kb_terms';
323
324 if (isset($this->args[$terms_key]) && !empty($this->args[$terms_key])) {
325 $terms_arg = [
326 'taxonomy' => $taxonomies,
327 'hide_empty' => false,
328 ];
329
330 if ($this->args[$terms_key][0] !== 'all') {
331 $terms_arg['slug'] = $this->args[$terms_key];
332 }
333
334 $terms = get_terms($terms_arg);
335 }
336 }
337
338 // Include both parent and child terms for hierarchical structures
339 if (!empty($terms) && is_array($terms)) {
340 $additional_terms = [];
341
342 foreach ($terms as $term) {
343 // Get parent terms
344 if (!empty($term->parent)) {
345 $ancestors = get_ancestors($term->term_id, $term->taxonomy, 'taxonomy');
346 foreach ($ancestors as $ancestor_id) {
347 $parent = get_term($ancestor_id, $term->taxonomy);
348 if (!is_wp_error($parent)) {
349 $additional_terms[] = $parent;
350 }
351 }
352 }
353
354 // Get child terms
355 $children = get_term_children($term->term_id, $term->taxonomy);
356 if (!is_wp_error($children)) {
357 foreach ($children as $child_id) {
358 $child = get_term($child_id, $term->taxonomy);
359 if (!is_wp_error($child)) {
360 $additional_terms[] = $child;
361 }
362 }
363 }
364 }
365
366 if (!empty($additional_terms)) {
367 $terms = array_merge($terms, $additional_terms);
368 $terms = array_unique($terms, SORT_REGULAR);
369 }
370 }
371
372 // Sort terms if not empty
373 if (!empty($terms) && is_array($terms)) {
374 usort($terms, array($this, 'compare_terms_by_meta'));
375 }
376
377 return $terms;
378 }
379
380 /**
381 * Compare terms based on their associated term meta values.
382 *
383 * @param WP_Term $a The first term object.
384 * @param WP_Term $b The second term object.
385 * @return int Returns a negative value if $a is less than $b,
386 * a positive value if $a is greater than $b, or 0 if they are equal.
387 * Additionally, prioritize sorting terms by taxonomy order,
388 * with 'doc_category' terms appearing before other taxonomy terms.
389 */
390 public function compare_terms_by_meta( $a, $b ) {
391 // Define the order of taxonomies
392 $taxonomy_order = array(
393 'doc_category' => 0,
394 'knowledge_base' => 1,
395 'doc_tag' => 2,
396 );
397
398 // Get the taxonomy order for terms $a and $b
399 $order_a = isset( $taxonomy_order[ $a->taxonomy ] ) ? $taxonomy_order[ $a->taxonomy ] : PHP_INT_MAX;
400 $order_b = isset( $taxonomy_order[ $b->taxonomy ] ) ? $taxonomy_order[ $b->taxonomy ] : PHP_INT_MAX;
401
402 // If the taxonomies have different order, sort by order
403 if ( $order_a !== $order_b ) {
404 return $order_a - $order_b;
405 }
406
407 // If the taxonomies have the same order, sort by meta value
408 $taxonomy_order_meta = array(
409 'doc_category' => 'doc_category_order',
410 'knowledge_base' => 'kb_order'
411 );
412
413 if ( isset( $taxonomy_order_meta[ $a->taxonomy ] ) && isset( $taxonomy_order_meta[ $b->taxonomy ] ) ) {
414 $meta_a = intval( get_term_meta( $a->term_id, $taxonomy_order_meta[ $a->taxonomy ], true ) );
415 $meta_b = intval( get_term_meta( $b->term_id, $taxonomy_order_meta[ $b->taxonomy ], true ) );
416
417 return $meta_a - $meta_b;
418 }
419
420 return 0; // Default to no sorting if meta keys are not defined
421 }
422 }
423