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/WPExporter.php +265 -77 3.4.2 → 3.8.9 View file →
@@ -61,119 +61,224 @@
61 61 private $terms;
62 62
63 63 private $nav_menu_terms;
64 64
65 - /**
66 - * Run export, by requested args.
67 - * Returns XML with exported data.
68 - *
69 - * @return array
70 - */
71 65 public function run(): array {
66 + // Handle glossaries export (as taxonomy)
67 + if ( 'glossaries' === $this->args['content'] ) {
68 + return $this->handle_glossaries_export();
69 + }
72 70
73 - $ptype = get_post_type_object( $this->args['content'] );
74 - if ( 'docs' === $this->args['content'] ) {
75 - $where = $this->wpdb->prepare( "{$this->wpdb->posts}.post_type = %s", $this->args['content'] );// phpcs:ignore
76 - } else {
71 + // Early return for invalid post types
72 + if ( $this->args['content'] !== 'docs' ) {
77 73 return [];
78 74 }
79 75
80 - if ( $this->args['status'] && 'docs' === $this->args['content'] ) {
81 - $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_status = %s", $this->args['status'] );// phpcs:ignore
82 - } else {
83 - $where .= " AND {$this->wpdb->posts}.post_status != 'auto-draft'";
76 + // Build the base query
77 + $query_parts = $this->build_base_query();
78 + $where = $query_parts['where'];
79 + $join = $query_parts['join'];
80 +
81 + // Handle post status
82 + $where .= $this->build_status_condition();
83 +
84 + // Handle specific posts selection
85 + if ( ! empty( $this->args['post__in'] ) ) {
86 + $where .= $this->build_post_in_condition();
84 87 }
85 88
86 - if ( ! empty( $this->args['post__in'] ) ) {
87 - $post_in = [implode(', ', $this->args['post__in'])];
88 - $ids_placeholder = implode( ', ', array_fill( 0, count( $post_in ), '%d' ) );
89 - $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.ID IN ($ids_placeholder)", $post_in );// phpcs:ignore
89 + // Handle category terms
90 + if ( isset( $this->args['category_terms'] ) ) {
91 + $category_query = $this->build_category_query();
92 + $join .= $category_query['join'];
93 + $where .= $category_query['where'];
90 94 }
91 95
96 + // Handle knowledge base terms
97 + if ( isset( $this->args['kb_terms'] ) ) {
98 + $kb_query = $this->build_kb_query();
99 + $join .= $kb_query['join'];
100 + $where .= $kb_query['where'];
101 + }
92 102
103 + // Handle additional filters (author, dates, meta)
104 + $where .= $this->build_additional_filters();
93 105
94 - $join = '';
106 + // Get the main doc post IDs
107 + $post_ids = $this->wpdb->get_col( "SELECT DISTINCT {$this->wpdb->posts}.ID FROM {$this->wpdb->posts} $join WHERE $where" );
95 108
96 - if ( isset( $this->args['category_terms'] ) && 'docs' === $this->args['content'] ) {
97 - $join = "INNER JOIN {$this->wpdb->term_relationships} ON ({$this->wpdb->posts}.ID = {$this->wpdb->term_relationships}.object_id)";
98 - foreach ( $this->args['category_terms'] as $term_id ) {
99 - $term = term_exists( $term_id, 'doc_category' );
100 - if ( $term ) {
101 - $where .= $this->wpdb->prepare( " AND {$this->wpdb->term_relationships}.term_taxonomy_id = %d", $term['term_taxonomy_id'] );// phpcs:ignore
102 - }
103 - }
104 - } else if ( isset( $this->args['kb_terms'] ) && 'docs' === $this->args['content'] ) {
105 - $join = "INNER JOIN {$this->wpdb->term_relationships} ON ({$this->wpdb->posts}.ID = {$this->wpdb->term_relationships}.object_id)";
106 - foreach ( $this->args['kb_terms'] as $term_id ) {
107 - $term = term_exists( $term_id, 'knowledge_base' );
108 - if ( $term ) {
109 - $where .= $this->wpdb->prepare( " AND {$this->wpdb->term_relationships}.term_taxonomy_id = %d", $term['term_taxonomy_id'] );// phpcs:ignore
110 - }
111 - }
109 + // Handle FAQ posts separately
110 + $faq_post_ids = [];
111 + if ( ! empty( $this->args['include_faq'] ) ) {
112 + $faq_post_ids = $this->get_faq_posts();
112 113 }
113 114
114 - if ( $this->args['content'] ) {
115 - if ( $this->args['author'] ) {
116 - $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_author = %d", $this->args['author'] );// phpcs:ignore
115 + // Combine post IDs
116 + $all_post_ids = array_merge( $post_ids, $faq_post_ids );
117 +
118 + // Handle featured images
119 + $thumbnail_ids = $this->get_thumbnail_ids( $all_post_ids );
120 +
121 + // Generate final post IDs array
122 + $final_post_ids = array_unique( array_merge( $all_post_ids, $thumbnail_ids ) );
123 +
124 + return [
125 + 'success' => true,
126 + 'data' => [
127 + 'filename' => 'betterdocs.' . date( 'Y-m-d' ) . '.xml',
128 + 'filetype' => 'text/xml',
129 + 'download' => $this->get_xml_export( $final_post_ids ),
130 + ]
131 + ];
132 + }
133 +
134 + private function handle_glossaries_export(): array {
135 + if ( isset( $this->args['glossary_terms'] ) && ( count( $this->args['glossary_terms'] ) > 0 ) ) {
136 + $glossary_term_ids = [];
137 + foreach ( $this->args['glossary_terms'] as $glossary_slug ) {
138 + $term_object = get_term_by( 'slug', $glossary_slug, 'glossaries' );
139 + if ( isset( $term_object->term_id ) && ! empty( $term_object->term_id ) ) {
140 + array_push( $glossary_term_ids, $term_object->term_id );
141 + }
117 142 }
143 + } else {
144 + $glossary_term_ids = $this->wpdb->get_col( "SELECT term_id from {$this->wpdb->term_taxonomy} where taxonomy='{$this->args['content']}';" );
145 + }
118 146
119 - if ( $this->args['start_date'] ) {
120 - $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_date >= %s", gmdate( 'Y-m-d', strtotime( $this->args['start_date'] ) ) );// phpcs:ignore
147 + $filename = 'betterdocs.' . date( 'Y-m-d' ) . '.xml';
148 + return [
149 + 'success' => true,
150 + 'data' => [
151 + 'filename' => $filename,
152 + 'filetype' => 'text/xml',
153 + 'download' => $this->get_xml_export( $glossary_term_ids ),
154 + ]
155 + ];
156 + }
157 +
158 + private function build_base_query(): array {
159 + $where = $this->wpdb->prepare( "{$this->wpdb->posts}.post_type = %s", 'docs' );
160 + return [
161 + 'where' => $where,
162 + 'join' => ''
163 + ];
164 + }
165 +
166 + private function build_status_condition(): string {
167 + if ( $this->args['status'] ) {
168 + return $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_status = %s", $this->args['status'] );
169 + }
170 + return " AND {$this->wpdb->posts}.post_status != 'auto-draft'";
171 + }
172 +
173 + private function build_post_in_condition(): string {
174 + $ids = $this->args['post__in'];
175 + $ids_placeholder = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
176 + return $this->wpdb->prepare( " AND {$this->wpdb->posts}.ID IN ($ids_placeholder)", $ids );
177 + }
178 +
179 + private function build_category_query(): array {
180 + $join = "INNER JOIN {$this->wpdb->term_relationships} tr ON ({$this->wpdb->posts}.ID = tr.object_id)
181 + INNER JOIN {$this->wpdb->term_taxonomy} tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)";
182 +
183 + $where = " AND tt.taxonomy = 'doc_category'";
184 +
185 + if ( ! empty( $this->args['category_terms'] ) ) {
186 + $term_ids = [];
187 + foreach ( $this->args['category_terms'] as $term_id ) {
188 + $term = term_exists( $term_id, 'doc_category' );
189 + if ( $term ) {
190 + $term_ids[] = $term['term_taxonomy_id'];
191 + }
121 192 }
122 193
123 - if ( $this->args['end_date'] ) {
124 - $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_date < %s", gmdate( 'Y-m-d', strtotime( '+1 month', strtotime( $this->args['end_date'] ) ) ) );// phpcs:ignore
194 + if ( ! empty( $term_ids ) ) {
195 + $term_placeholder = implode( ', ', array_fill( 0, count( $term_ids ), '%d' ) );
196 + $where .= $this->wpdb->prepare( " AND tt.term_taxonomy_id IN ($term_placeholder)", $term_ids );
125 197 }
126 198 }
127 199
128 - $limit = '';
200 + return [
201 + 'join' => $join,
202 + 'where' => $where
203 + ];
204 + }
129 205
130 - // if ( - 1 !== (int) $this->args['limit'] ) {
131 - // $limit = 'LIMIT ' . (int) $this->args['limit'] . ' OFFSET ' . (int) $this->args['offset'];
132 - // }
206 + private function build_kb_query(): array {
207 + $join = "INNER JOIN {$this->wpdb->term_relationships} tr ON ({$this->wpdb->posts}.ID = tr.object_id)
208 + INNER JOIN {$this->wpdb->term_taxonomy} tt ON (tr.term_taxonomy_id = tt.term_taxonomy_id)";
133 209
134 - if ( ! empty( $this->args['meta_query'] ) ) {
135 - if ( $join ) {
136 - $join .= ' ';
210 + $where = " AND tt.taxonomy = 'knowledge_base'";
211 +
212 + if ( ! empty( $this->args['kb_terms'] ) ) {
213 + $term_ids = [];
214 + foreach ( $this->args['kb_terms'] as $term_id ) {
215 + $term = term_exists( $term_id, 'knowledge_base' );
216 + if ( $term ) {
217 + $term_ids[] = $term['term_taxonomy_id'];
218 + }
137 219 }
138 220
139 - if ( $where ) {
140 - $where .= ' ';
221 + if ( ! empty( $term_ids ) ) {
222 + $term_placeholder = implode( ', ', array_fill( 0, count( $term_ids ), '%d' ) );
223 + $where .= $this->wpdb->prepare( " AND tt.term_taxonomy_id IN ($term_placeholder)", $term_ids );
141 224 }
225 + }
142 226
143 - $meta_query = new \WP_Meta_Query( $this->args['meta_query'] );
227 + return [
228 + 'join' => $join,
229 + 'where' => $where
230 + ];
231 + }
144 232
145 - global $wpdb;
233 + private function build_additional_filters(): string {
234 + $where = '';
146 235
147 - $query_clauses = $meta_query->get_sql( 'docs', $wpdb->posts, 'ID' );
236 + if ( $this->args['author'] ) {
237 + $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_author = %d", $this->args['author'] );
238 + }
148 239
149 - $join .= $query_clauses['join'];
150 - $where .= $query_clauses['where'];
240 + if ( $this->args['start_date'] ) {
241 + $where .= $this->wpdb->prepare(
242 + " AND {$this->wpdb->posts}.post_date >= %s",
243 + gmdate( 'Y-m-d', strtotime( $this->args['start_date'] ) )
244 + );
151 245 }
152 246
153 - // Grab a snapshot of post IDs, just in case it changes during the export.
154 - $post_ids = $this->wpdb->get_col( "SELECT ID FROM {$this->wpdb->posts} $join WHERE $where $limit" );// phpcs:ignore
247 + if ( $this->args['end_date'] ) {
248 + $where .= $this->wpdb->prepare(
249 + " AND {$this->wpdb->posts}.post_date < %s",
250 + gmdate( 'Y-m-d', strtotime( '+1 month', strtotime( $this->args['end_date'] ) ) )
251 + );
252 + }
253 +
254 + return $where;
255 + }
256 +
257 + private function get_faq_posts(): array {
258 + return get_posts(
259 + [
260 + 'numberposts' => -1,
261 + 'post_type' => 'betterdocs_faq',
262 + 'fields' => 'ids',
263 + 'post_status' => 'publish'
264 + ]
265 + );
266 + }
267 +
268 + private function get_thumbnail_ids( array $post_ids ): array {
155 269 $thumbnail_ids = [];
156 270
157 271 if ( ! empty( $this->args['include_post_featured_image_as_attachment'] ) ) {
158 272 foreach ( $post_ids as $post_id ) {
159 273 $thumbnail_id = get_post_meta( $post_id, '_thumbnail_id', true );
160 -
161 274 if ( $thumbnail_id && ! in_array( $thumbnail_id, $post_ids, true ) ) {
162 - $thumbnail_ids [] = $thumbnail_id;
275 + $thumbnail_ids[] = $thumbnail_id;
163 276 }
164 277 }
165 278 }
166 279
167 - $filename = 'betterdocs.' . date( 'Y-m-d' ) . '.xml';
168 - return [
169 - 'success' => true,
170 - 'data' => [
171 - 'filename' => $filename,
172 - 'filetype' => 'text/xml',
173 - 'download' => $this->get_xml_export( array_merge( $post_ids, $thumbnail_ids ) ),
174 - ]
175 - ];
280 + return $thumbnail_ids;
176 281 }
177 282
178 283 /**
179 284 * Return tabulation characters, by `$columns`.
@@ -199,9 +304,9 @@
199 304 private function wxr_cdata( $str ): string {
200 305 $str = (string) $str;
201 306
202 307 if ( ! seems_utf8( $str ) ) {
203 - $str = utf8_encode( $str );
308 + $str = mb_convert_encoding( $str, 'UTF-8', 'ISO-8859-1' );
204 309 }
205 310
206 311 $str = '<![CDATA[' . str_replace( ']]>', ']]]]><![CDATA[>', $str ) . ']]>';
207 312
@@ -433,8 +538,18 @@
433 538
434 539 return $result;
435 540 }
436 541
542 + public function get_parent_terms_slug( $terms, $term_id ) {
543 + $key = array_search( $term_id, array_column( $terms, 'term_id' ) );
544 +
545 + if ( $key !== false ) {
546 + return $terms[ $key ]->slug;
547 + }
548 +
549 + return null;
550 + }
551 +
437 552 /**
438 553 * Return list of terms.
439 554 *
440 555 * @param array $terms
@@ -442,16 +557,17 @@
442 557 * @return string
443 558 */
444 559 private function wxr_terms_list( array $terms ): string {
445 560 $result = '';
446 -
447 - foreach ( $terms as $t ) {
561 + foreach ( $terms as $key => $t ) {
448 562 $result .= $this->indent( 2 ) . '<wp:term>' . PHP_EOL;
449 563
450 564 $result .= $this->indent( 3 ) . '<wp:term_id>' . $this->wxr_cdata( $t->term_id ) . '</wp:term_id>' . PHP_EOL;
451 565 $result .= $this->indent( 3 ) . '<wp:term_taxonomy>' . $this->wxr_cdata( $t->taxonomy ) . '</wp:term_taxonomy>' . PHP_EOL;
452 566 $result .= $this->indent( 3 ) . '<wp:term_slug>' . $this->wxr_cdata( $t->slug ) . '</wp:term_slug>' . PHP_EOL;
453 - $result .= $this->indent( 3 ) . '<wp:term_parent>' . $this->wxr_cdata( $t->parent ? $terms[ $t->parent ]->slug : '' ) . '</wp:term_parent>' . PHP_EOL;
567 + if ( $t->parent ) {
568 + $result .= $this->indent( 3 ) . '<wp:term_parent>' . $this->wxr_cdata( $this->get_parent_terms_slug( $terms, $t->parent ) ) . '</wp:term_parent>' . PHP_EOL;
569 + }
454 570 $result .= $this->wxr_term_name( $t ) . $this->wxr_term_description( $t ) . $this->wxr_term_meta( $t );
455 571
456 572 $result .= $this->indent( 2 ) . '</wp:term>' . PHP_EOL;
457 573 }
@@ -458,13 +574,86 @@
458 574
459 575 return $result;
460 576 }
461 577
462 - private function get_terms( array $post_ids ) {
463 - return wp_get_object_terms( $post_ids, get_object_taxonomies( $this->args['content'] ) );
578 + /**
579 + * Retrieve terms associated with the specified object IDs and sort them based on term meta.
580 + *
581 + * @param array $post_ids An array of object IDs.
582 + * @return array An array of WP_Term objects sorted based on term meta.
583 + */
584 + public function get_terms( array $post_ids ) {
585 + // Get the object taxonomies
586 + $taxonomies = get_object_taxonomies( $this->args['content'] );
587 +
588 + if ( isset( $this->args['selected_docs'] ) && ! empty( $this->args['selected_docs'] ) && $this->args['selected_docs'][0] == 'all' ) {
589 + $terms = get_terms(
590 + [
591 + 'taxonomy' => $taxonomies,
592 + 'hide_empty' => false,
593 + ]
594 + );
595 + } elseif ( $this->args['content'] == 'glossaries' ) {
596 + $terms = get_terms(
597 + [
598 + 'taxonomy' => 'glossaries',
599 + 'include' => $post_ids,
600 + 'hide_empty' => false,
601 + ]
602 + );
603 + } else {
604 + $terms = wp_get_object_terms( $post_ids, $taxonomies );
605 + }
606 +
607 + usort( $terms, array( $this, 'compare_terms_by_meta' ) );
608 +
609 + return $terms;
464 610 }
465 611
466 612 /**
613 + * Compare terms based on their associated term meta values.
614 + *
615 + * @param WP_Term $a The first term object.
616 + * @param WP_Term $b The second term object.
617 + * @return int Returns a negative value if $a is less than $b,
618 + * a positive value if $a is greater than $b, or 0 if they are equal.
619 + * Additionally, prioritize sorting terms by taxonomy order,
620 + * with 'doc_category' terms appearing before other taxonomy terms.
621 + */
622 + public function compare_terms_by_meta( $a, $b ) {
623 + // Define the order of taxonomies
624 + $taxonomy_order = array(
625 + 'doc_category' => 0,
626 + 'knowledge_base' => 1,
627 + 'doc_tag' => 2,
628 + );
629 +
630 + // Get the taxonomy order for terms $a and $b
631 + $order_a = isset( $taxonomy_order[ $a->taxonomy ] ) ? $taxonomy_order[ $a->taxonomy ] : PHP_INT_MAX;
632 + $order_b = isset( $taxonomy_order[ $b->taxonomy ] ) ? $taxonomy_order[ $b->taxonomy ] : PHP_INT_MAX;
633 +
634 + // If the taxonomies have different order, sort by order
635 + if ( $order_a !== $order_b ) {
636 + return $order_a - $order_b;
637 + }
638 +
639 + // If the taxonomies have the same order, sort by meta value
640 + $taxonomy_order_meta = array(
641 + 'doc_category' => 'doc_category_order',
642 + 'knowledge_base' => 'kb_order'
643 + );
644 +
645 + if ( isset( $taxonomy_order_meta[ $a->taxonomy ] ) && isset( $taxonomy_order_meta[ $b->taxonomy ] ) ) {
646 + $meta_a = intval( get_term_meta( $a->term_id, $taxonomy_order_meta[ $a->taxonomy ], true ) );
647 + $meta_b = intval( get_term_meta( $b->term_id, $taxonomy_order_meta[ $b->taxonomy ], true ) );
648 +
649 + return $meta_a - $meta_b;
650 + }
651 +
652 + return 0; // Default to no sorting if meta keys are not defined
653 + }
654 +
655 + /**
467 656 * Return list of posts, by requested `$post_ids`.
468 657 *
469 658 * @param array $post_ids
470 659 *
@@ -568,9 +757,8 @@
568 757
569 758 $_comments = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->comments} WHERE comment_post_ID = %d AND comment_approved <> 'spam'", $post->ID ) );// phpcs:ignore
570 759 $comments = array_map( 'get_comment', $_comments );
571 760 foreach ( $comments as $c ) {
572 -
573 761 $result .= $this->indent( 3 ) . '<wp:comment>' . PHP_EOL;
574 762
575 763 $result .= $this->indent( 4 ) . '<wp:comment_id>' . (int) $c->comment_ID . '</wp:comment_id>' . PHP_EOL;
576 764 $result .= $this->indent( 4 ) . '<wp:comment_author>' . $this->wxr_cdata( $c->comment_author ) . '</wp:comment_author>' . PHP_EOL;
@@ -630,9 +818,9 @@
630 818 */
631 819 private function wxr_nav_menu_terms(): string {
632 820 $args = [];
633 821
634 - if( ! empty( $this->nav_menu_terms ) ) {
822 + if ( ! empty( $this->nav_menu_terms ) ) {
635 823 $args['include'] = $this->nav_menu_terms;
636 824 }
637 825
638 826 $nav_menus = wp_get_nav_menus( $args );