PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.7.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.7.3
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 3.7.3, at includes/Admin/WPExporter.php

882 lines 31.3 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 WP_Post;
6 use WP_Term;
7 use wpdb;
8
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 /*
15 * Originally made by WordPress.
16 *
17 * What changed (by Elementor):
18 * Remove echos.
19 * Fix indents.
20 * Add methods
21 * indent.
22 * wxr_categories_list.
23 * wxr_tags_list.
24 * wxr_terms_list.
25 * wxr_posts_list.
26 *
27 * What changed (by Templately)
28 * Added post__in capabilities for query.
29 * nav_menu_item from terms
30 * Some indent, and data type fixes
31 * query_args added as default args
32 */
33
34 #[\AllowDynamicProperties]
35 class WPExporter {
36 const WXR_VERSION = '1.2';
37
38 private static $default_args = [
39 'content' => 'all',
40 'author' => false,
41 'category' => false,
42 'start_date' => false,
43 'end_date' => false,
44 'status' => false,
45 'offset' => 0,
46 'limit' => -1,
47 'meta_query' => [], // If specified `meta_key` then will include all post(s) that have this meta_key.
48 'query_args' => []
49 ];
50
51 /**
52 * @var array
53 */
54 private $args;
55
56 /**
57 * @var wpdb
58 */
59 private $wpdb;
60
61 private $terms;
62
63 private $nav_menu_terms;
64
65 /**
66 * Run export, by requested args.
67 * Returns XML with exported data.
68 *
69 * @return array
70 */
71 public function run(): array {
72
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 } elseif( 'glossaries' === $this->args['content'] ) { //handles glossaries related xml export
77 if( isset( $this->args['glossary_terms'] ) && ( count( $this->args['glossary_terms'] ) > 0 ) ) {
78 $glossary_term_ids = [];
79 foreach( $this->args['glossary_terms'] as $glossary_slug ) {
80 $term_object = get_term_by('slug', $glossary_slug , 'glossaries');
81 if( isset( $term_object->term_id ) && ! empty( $term_object->term_id ) ){
82 array_push($glossary_term_ids, $term_object->term_id);
83 }
84 }
85 } else {
86 $glossary_term_ids = $this->wpdb->get_col( "SELECT term_id from {$this->wpdb->term_taxonomy} where taxonomy='{$this->args['content']}';" );
87 }
88 $filename = 'betterdocs.' . date( 'Y-m-d' ) . '.xml';
89 return [
90 'success' => true,
91 'data' => [
92 'filename' => $filename,
93 'filetype' => 'text/xml',
94 'download' => $this->get_xml_export($glossary_term_ids),
95 ]
96 ];
97 }else {
98 return [];
99 }
100
101 if( $this->args['include_faq'] ) { //include FAQ if enabled
102 $where .= $this->wpdb->prepare( " OR {$this->wpdb->posts}.post_type = %s", 'betterdocs_faq' );
103 }
104
105 if ( $this->args['status'] && 'docs' === $this->args['content'] ) {
106 $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_status = %s", $this->args['status'] );// phpcs:ignore
107 } else {
108 $where .= " AND {$this->wpdb->posts}.post_status != 'auto-draft'";
109 }
110
111 if ( ! empty( $this->args['post__in'] ) ) {
112 $ids = $this->args['post__in'];
113 if( $this->args['include_faq'] ) { //include FAQ if enabled when specific post id's are selected
114 $faq_posts_ids = get_posts([
115 'numberposts' => -1,
116 'post_type' => 'betterdocs_faq',
117 'fields' => 'ids'
118 ]);
119 $ids = array_merge($ids, $faq_posts_ids);
120 }
121 $ids_placeholder = implode( ', ', array_fill( 0, count( $ids ), '%d' ) );
122 $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.ID IN ($ids_placeholder)", $ids );// phpcs:ignore
123 }
124
125
126
127 $join = '';
128
129 if ( isset( $this->args['category_terms'] ) && 'docs' === $this->args['content'] ) {
130 $join = "INNER JOIN {$this->wpdb->term_relationships} ON ({$this->wpdb->posts}.ID = {$this->wpdb->term_relationships}.object_id)";
131 foreach ( $this->args['category_terms'] as $term_id ) {
132 $term = term_exists( $term_id, 'doc_category' );
133 if ( $term ) {
134 $where .= $this->wpdb->prepare( " AND {$this->wpdb->term_relationships}.term_taxonomy_id = %d", $term['term_taxonomy_id'] );// phpcs:ignore
135 }
136 }
137 } else if ( isset( $this->args['kb_terms'] ) && 'docs' === $this->args['content'] ) {
138 $join = "INNER JOIN {$this->wpdb->term_relationships} ON ({$this->wpdb->posts}.ID = {$this->wpdb->term_relationships}.object_id)";
139 foreach ( $this->args['kb_terms'] as $term_id ) {
140 $term = term_exists( $term_id, 'knowledge_base' );
141 if ( $term ) {
142 $where .= $this->wpdb->prepare( " AND {$this->wpdb->term_relationships}.term_taxonomy_id = %d", $term['term_taxonomy_id'] );// phpcs:ignore
143 }
144 }
145 }
146
147 if ( $this->args['content'] ) {
148 if ( $this->args['author'] ) {
149 $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_author = %d", $this->args['author'] );// phpcs:ignore
150 }
151
152 if ( $this->args['start_date'] ) {
153 $where .= $this->wpdb->prepare( " AND {$this->wpdb->posts}.post_date >= %s", gmdate( 'Y-m-d', strtotime( $this->args['start_date'] ) ) );// phpcs:ignore
154 }
155
156 if ( $this->args['end_date'] ) {
157 $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
158 }
159 }
160
161 $limit = '';
162
163 // if ( - 1 !== (int) $this->args['limit'] ) {
164 // $limit = 'LIMIT ' . (int) $this->args['limit'] . ' OFFSET ' . (int) $this->args['offset'];
165 // }
166
167 if ( ! empty( $this->args['meta_query'] ) ) {
168 if ( $join ) {
169 $join .= ' ';
170 }
171
172 if ( $where ) {
173 $where .= ' ';
174 }
175
176 $meta_query = new \WP_Meta_Query( $this->args['meta_query'] );
177
178 global $wpdb;
179
180 $query_clauses = $meta_query->get_sql( 'docs', $wpdb->posts, 'ID' );
181
182 $join .= $query_clauses['join'];
183 $where .= $query_clauses['where'];
184 }
185
186 // Grab a snapshot of post IDs, just in case it changes during the export.
187 $post_ids = $this->wpdb->get_col( "SELECT ID FROM {$this->wpdb->posts} $join WHERE $where $limit" );// phpcs:ignore
188 $thumbnail_ids = [];
189
190 if ( ! empty( $this->args['include_post_featured_image_as_attachment'] ) ) {
191 foreach ( $post_ids as $post_id ) {
192 $thumbnail_id = get_post_meta( $post_id, '_thumbnail_id', true );
193
194 if ( $thumbnail_id && ! in_array( $thumbnail_id, $post_ids, true ) ) {
195 $thumbnail_ids [] = $thumbnail_id;
196 }
197 }
198 }
199
200 $filename = 'betterdocs.' . date( 'Y-m-d' ) . '.xml';
201 return [
202 'success' => true,
203 'data' => [
204 'filename' => $filename,
205 'filetype' => 'text/xml',
206 'download' => $this->get_xml_export( array_merge( $post_ids, $thumbnail_ids ) ),
207 ]
208 ];
209 }
210
211 /**
212 * Return tabulation characters, by `$columns`.
213 *
214 * @param int $columns
215 *
216 * @return string
217 */
218 private function indent( int $columns = 1 ): string {
219
220 $output = str_repeat( "\t", $columns );
221
222 return (string) $output;
223 }
224
225 /**
226 * Return wrapped given string in XML CDATA tag.
227 *
228 * @param string $str String to wrap in XML CDATA tag.
229 *
230 * @return string
231 */
232 private function wxr_cdata( $str ): string {
233 $str = (string) $str;
234
235 if ( ! seems_utf8( $str ) ) {
236 $str = utf8_encode( $str );
237 }
238
239 $str = '<![CDATA[' . str_replace( ']]>', ']]]]><![CDATA[>', $str ) . ']]>';
240
241 return $str;
242 }
243
244 /**
245 * Return the URL of the site.
246 *
247 * @return string Site URL.
248 */
249 private function wxr_site_url(): string {
250 if ( is_multisite() ) {
251 // Multisite: the base URL.
252 return network_home_url();
253 } else {
254 // WordPress (single site): the blog URL.
255 return get_bloginfo_rss( 'url' );
256 }
257 }
258
259 /**
260 * Return a cat_name XML tag from a given category object.
261 *
262 * @param WP_Term $category Category Object
263 *
264 * @return string
265 */
266 private function wxr_cat_name( $category ): string {
267 if ( empty( $category->name ) ) {
268 return '';
269 }
270
271 return $this->indent( 3 ) . '<wp:cat_name>' . $this->wxr_cdata( $category->name ) . '</wp:cat_name>' . PHP_EOL;
272 }
273
274 /**
275 * Return a category_description XML tag from a given category object.
276 *
277 * @param WP_Term $category Category Object
278 *
279 * @return string
280 */
281 private function wxr_category_description( $category ): string {
282 if ( empty( $category->description ) ) {
283 return '';
284 }
285
286 return $this->indent( 3 ) . '<wp:category_description>' . $this->wxr_cdata( $category->description ) . "</wp:category_description>\n";
287 }
288
289 /**
290 * Return a tag_name XML tag from a given tag object.
291 *
292 * @param WP_Term $tag Tag Object
293 *
294 * @return string
295 */
296 private function wxr_tag_name( $tag ): string {
297 if ( empty( $tag->name ) ) {
298 return '';
299 }
300
301 return $this->indent( 3 ) . '<wp:tag_name>' . $this->wxr_cdata( $tag->name ) . '</wp:tag_name>' . PHP_EOL;
302 }
303
304 /**
305 * Return a tag_description XML tag from a given tag object.
306 *
307 * @param WP_Term $tag Tag Object
308 *
309 * @return string
310 */
311 private function wxr_tag_description( $tag ): string {
312 if ( empty( $tag->description ) ) {
313 return '';
314 }
315
316 return $this->indent( 3 ) . '<wp:tag_description>' . $this->wxr_cdata( $tag->description ) . '</wp:tag_description>' . PHP_EOL;
317 }
318
319 /**
320 * Return a term_name XML tag from a given term object.
321 *
322 * @param WP_Term $term Term Object
323 *
324 * @return string
325 */
326 private function wxr_term_name( $term ): string {
327 if ( empty( $term->name ) ) {
328 return '';
329 }
330
331 return $this->indent( 3 ) . '<wp:term_name>' . $this->wxr_cdata( $term->name ) . '</wp:term_name>' . PHP_EOL;
332 }
333
334 /**
335 * Return a term_description XML tag from a given term object.
336 *
337 * @param WP_Term $term Term Object
338 *
339 * @return string
340 */
341 private function wxr_term_description( $term ): string {
342 if ( empty( $term->description ) ) {
343 return '';
344 }
345
346 return $this->indent( 3 ) . '<wp:term_description>' . $this->wxr_cdata( $term->description ) . '</wp:term_description>' . PHP_EOL;
347 }
348
349 /**
350 * Return term meta XML tags for a given term object.
351 *
352 * @param WP_Term $term Term object.
353 *
354 * @return string
355 */
356 private function wxr_term_meta( $term ): string {
357 $result = '';
358 $termmeta = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->termmeta} WHERE term_id = %d", $term->term_id ) );// phpcs:ignore
359
360 foreach ( $termmeta as $meta ) {
361 /**
362 * Filters whether to selectively skip term meta used for WXR exports.
363 *
364 * Returning a truthy value from the filter will skip the current meta
365 * object from being exported.
366 *
367 * @param bool $skip Whether to skip the current piece of term meta. Default false.
368 * @param string $meta_key Current meta key.
369 * @param object $meta Current meta object.
370 *
371 * @since 4.6.0
372 *
373 */
374 if ( ! apply_filters( 'wxr_export_skip_termmeta', false, $meta->meta_key, $meta ) ) {
375 $result .= sprintf( $this->indent( 3 ) . "<wp:termmeta>\n\t\t\t<wp:meta_key>%s</wp:meta_key>\n\t\t\t<wp:meta_value>%s</wp:meta_value>\n\t\t</wp:termmeta>\n", $this->wxr_cdata( $meta->meta_key ), $this->wxr_cdata( $meta->meta_value ) );
376 }
377 }
378
379 return $result;
380 }
381
382 /**
383 * Return list of authors with posts.
384 *
385 * @param int[] $post_ids Optional. Array of post IDs to filter the query by.
386 *
387 * @return string
388 */
389 private function wxr_authors_list( array $post_ids = null ): string {
390 $result = '';
391
392 if ( ! empty( $post_ids ) ) {
393 $post_ids = array_map( 'absint', $post_ids );
394 $and = 'AND ID IN ( ' . implode( ', ', $post_ids ) . ')';
395 } else {
396 $and = '';
397 }
398
399 $authors = [];
400 $results = $this->wpdb->get_results( "SELECT DISTINCT post_author FROM {$this->wpdb->posts} WHERE post_status != 'auto-draft' $and" );// phpcs:ignore
401 foreach ( (array) $results as $r ) {
402 $authors[] = get_userdata( $r->post_author );
403 }
404
405 $authors = array_filter( $authors );
406
407 foreach ( $authors as $author ) {
408 $result .= $this->indent( 2 ) . '<wp:author>' . PHP_EOL;
409
410 $result .= $this->indent( 3 ) . '<wp:author_id>' . (int) $author->ID . '</wp:author_id>' . PHP_EOL;
411 $result .= $this->indent( 3 ) . '<wp:author_login>' . $this->wxr_cdata( $author->user_login ) . '</wp:author_login>' . PHP_EOL;
412 $result .= $this->indent( 3 ) . '<wp:author_email>' . $this->wxr_cdata( $author->user_email ) . '</wp:author_email>' . PHP_EOL;
413 $result .= $this->indent( 3 ) . '<wp:author_display_name>' . $this->wxr_cdata( $author->display_name ) . '</wp:author_display_name>' . PHP_EOL;
414 $result .= $this->indent( 3 ) . '<wp:author_first_name>' . $this->wxr_cdata( $author->first_name ) . '</wp:author_first_name>' . PHP_EOL;
415 $result .= $this->indent( 3 ) . '<wp:author_last_name>' . $this->wxr_cdata( $author->last_name ) . '</wp:author_last_name>' . PHP_EOL;
416
417 $result .= $this->indent( 2 ) . '</wp:author>' . PHP_EOL;
418 }
419
420 return $result;
421 }
422
423 /**
424 * Return list of categories.
425 *
426 * @param array $cats
427 *
428 * @return string
429 */
430 private function wxr_categories_list( array $cats ): string {
431 $result = '';
432
433 foreach ( $cats as $c ) {
434 $result .= $this->indent( 2 ) . '<wp:category>' . PHP_EOL;
435
436 $result .= $this->indent( 3 ) . '<wp:term_id>' . (int) $c->term_id . '</wp:term_id>' . PHP_EOL;
437 $result .= $this->indent( 3 ) . '<wp:category_nicename>' . $this->wxr_cdata( $c->slug ) . '</wp:category_nicename>' . PHP_EOL;
438 $result .= $this->indent( 3 ) . '<wp:category_parent>' . $this->wxr_cdata( $c->parent ? $cats[ $c->parent ]->slug : '' ) . '</wp:category_parent>' . PHP_EOL;
439 $result .= $this->wxr_cat_name( $c ) . $this->wxr_category_description( $c ) . $this->wxr_term_meta( $c );
440
441 $result .= $this->indent( 2 ) . '</wp:category>' . PHP_EOL;
442 }
443
444 return $result;
445 }
446
447 /**
448 * Return list of tags.
449 *
450 * @param array $tags
451 *
452 * @return string
453 */
454 private function wxr_tags_list( array $tags ): string {
455 $result = '';
456
457 foreach ( $tags as $t ) {
458 $result .= $this->indent( 2 ) . '<wp:tag>' . PHP_EOL;
459
460 $result .= $this->indent( 3 ) . '<wp:term_id>' . (int) $t->term_id . '</wp:term_id>' . PHP_EOL;
461 $result .= $this->indent( 3 ) . '<wp:tag_slug>' . $this->wxr_cdata( $t->slug ) . '</wp:tag_slug>' . PHP_EOL;
462 $result .= $this->wxr_tag_name( $t ) . $this->wxr_tag_description( $t ) . $this->wxr_term_meta( $t );
463
464 $result .= $this->indent( 2 ) . '</wp:tag>' . PHP_EOL;
465 }
466
467 return $result;
468 }
469
470 public function get_parent_terms_slug( $terms, $term_id ) {
471 $key = array_search( $term_id, array_column( $terms, 'term_id' ));
472
473 if ( $key !== false ) {
474 return $terms[$key]->slug;
475 }
476
477 return null;
478 }
479
480 /**
481 * Return list of terms.
482 *
483 * @param array $terms
484 *
485 * @return string
486 */
487 private function wxr_terms_list( array $terms ): string {
488 $result = '';
489 foreach ( $terms as $key => $t ) {
490 $result .= $this->indent( 2 ) . '<wp:term>' . PHP_EOL;
491
492 $result .= $this->indent( 3 ) . '<wp:term_id>' . $this->wxr_cdata( $t->term_id ) . '</wp:term_id>' . PHP_EOL;
493 $result .= $this->indent( 3 ) . '<wp:term_taxonomy>' . $this->wxr_cdata( $t->taxonomy ) . '</wp:term_taxonomy>' . PHP_EOL;
494 $result .= $this->indent( 3 ) . '<wp:term_slug>' . $this->wxr_cdata( $t->slug ) . '</wp:term_slug>' . PHP_EOL;
495 if ( $t->parent ) {
496 $result .= $this->indent( 3 ) . '<wp:term_parent>' . $this->wxr_cdata( $this->get_parent_terms_slug( $terms, $t->parent ) ) . '</wp:term_parent>' . PHP_EOL;
497 }
498 $result .= $this->wxr_term_name( $t ) . $this->wxr_term_description( $t ) . $this->wxr_term_meta( $t );
499
500 $result .= $this->indent( 2 ) . '</wp:term>' . PHP_EOL;
501 }
502
503 return $result;
504 }
505
506 /**
507 * Retrieve terms associated with the specified object IDs and sort them based on term meta.
508 *
509 * @param array $post_ids An array of object IDs.
510 * @return array An array of WP_Term objects sorted based on term meta.
511 */
512 public function get_terms( array $post_ids ) {
513 // Get the object taxonomies
514 $taxonomies = get_object_taxonomies( $this->args['content'] );
515
516 if ( isset( $this->args['selected_docs'] ) && $this->args['selected_docs'][0] == 'all' ) {
517 $terms = get_terms( [
518 'taxonomy' => $taxonomies,
519 'hide_empty' => false,
520 ] );
521 } else if( $this->args['content'] == 'glossaries' ) {
522 $terms = get_terms([
523 'taxonomy' => 'glossaries',
524 'include' => $post_ids,
525 'hide_empty' => false,
526 ]);
527 } else {
528 $terms = wp_get_object_terms( $post_ids, $taxonomies);
529 }
530
531 usort( $terms, array( $this, 'compare_terms_by_meta' ) );
532
533 return $terms;
534 }
535
536 /**
537 * Compare terms based on their associated term meta values.
538 *
539 * @param WP_Term $a The first term object.
540 * @param WP_Term $b The second term object.
541 * @return int Returns a negative value if $a is less than $b,
542 * a positive value if $a is greater than $b, or 0 if they are equal.
543 * Additionally, prioritize sorting terms by taxonomy order,
544 * with 'doc_category' terms appearing before other taxonomy terms.
545 */
546 public function compare_terms_by_meta($a, $b) {
547 // Define the order of taxonomies
548 $taxonomy_order = array(
549 'doc_category' => 0,
550 'knowledge_base' => 1,
551 'doc_tag' => 2,
552 );
553
554 // Get the taxonomy order for terms $a and $b
555 $order_a = isset($taxonomy_order[$a->taxonomy]) ? $taxonomy_order[$a->taxonomy] : PHP_INT_MAX;
556 $order_b = isset($taxonomy_order[$b->taxonomy]) ? $taxonomy_order[$b->taxonomy] : PHP_INT_MAX;
557
558 // If the taxonomies have different order, sort by order
559 if ($order_a !== $order_b) {
560 return $order_a - $order_b;
561 }
562
563 // If the taxonomies have the same order, sort by meta value
564 $taxonomy_order_meta = array(
565 'doc_category' => 'doc_category_order',
566 'knowledge_base' => 'kb_order'
567 );
568
569 if (isset($taxonomy_order_meta[$a->taxonomy]) && isset($taxonomy_order_meta[$b->taxonomy])) {
570 $meta_a = intval(get_term_meta($a->term_id, $taxonomy_order_meta[$a->taxonomy], true));
571 $meta_b = intval(get_term_meta($b->term_id, $taxonomy_order_meta[$b->taxonomy], true));
572
573 return $meta_a - $meta_b;
574 }
575
576 return 0; // Default to no sorting if meta keys are not defined
577 }
578
579 /**
580 * Return list of posts, by requested `$post_ids`.
581 *
582 * @param array $post_ids
583 *
584 * @return string
585 */
586 private function wxr_posts_list( array $post_ids ): string {
587 $result = '';
588
589 if ( $post_ids ) {
590 global $wp_query;
591
592 // Fake being in the loop.
593 $wp_query->in_the_loop = true;
594
595 // Fetch 20 posts at a time rather than loading the entire table into memory.
596 while ( $next_posts = array_splice( $post_ids, 0, 20 ) ) {
597 $where = 'WHERE ID IN (' . implode( ',', $next_posts ) . ')';
598 $posts = $this->wpdb->get_results( "SELECT * FROM {$this->wpdb->posts} $where" );// phpcs:ignore
599
600 // Begin Loop.
601 foreach ( $posts as $post ) {
602 setup_postdata( $post );
603
604 $title = apply_filters( 'the_title_rss', $post->post_title );
605
606 /**
607 * Filters the post content used for WXR exports.
608 *
609 * @param string $post_content Content of the current post.
610 *
611 * @since 2.5.0
612 *
613 */
614 $content = $this->wxr_cdata( apply_filters( 'the_content_export', $post->post_content ) );
615
616 /**
617 * Filters the post excerpt used for WXR exports.
618 *
619 * @param string $post_excerpt Excerpt for the current post.
620 *
621 * @since 2.6.0
622 *
623 */
624 $excerpt = $this->wxr_cdata( apply_filters( 'the_excerpt_export', $post->post_excerpt ) );
625
626 $result .= $this->indent( 2 ) . '<item>' . PHP_EOL;
627
628 $result .= $this->indent( 3 ) . '<title>' . $title . '</title>' . PHP_EOL;
629 $result .= $this->indent( 3 ) . '<link>' . esc_url( get_permalink() ) . '</link>' . PHP_EOL;
630 $result .= $this->indent( 3 ) . '<pubDate>' . mysql2date( 'D, d M Y H:i:s +0000', get_post_time( 'Y-m-d H:i:s', true ), false ) . '</pubDate>' . PHP_EOL;
631 $result .= $this->indent( 3 ) . '<dc:creator>' . $this->wxr_cdata( get_the_author_meta( 'login' ) ) . '</dc:creator>' . PHP_EOL;
632 $result .= $this->indent( 3 ) . '<guid isPermaLink="false">' . $this->wxr_cdata( get_the_author_meta( 'login' ) ) . '</guid>' . PHP_EOL;
633 $result .= $this->indent( 3 ) . '<description></description>' . PHP_EOL;
634 $result .= $this->indent( 3 ) . '<content:encoded>' . $content . '</content:encoded>' . PHP_EOL;
635 $result .= $this->indent( 3 ) . '<excerpt:encoded>' . $excerpt . '</excerpt:encoded>' . PHP_EOL;
636 $result .= $this->indent( 3 ) . '<wp:post_id>' . (int) $post->ID . '</wp:post_id>' . PHP_EOL;
637 $result .= $this->indent( 3 ) . '<wp:post_date>' . $this->wxr_cdata( $post->post_date ) . '</wp:post_date>' . PHP_EOL;
638 $result .= $this->indent( 3 ) . '<wp:post_date_gmt>' . $this->wxr_cdata( $post->post_date_gmt ) . '</wp:post_date_gmt>' . PHP_EOL;
639 $result .= $this->indent( 3 ) . '<wp:comment_status>' . $this->wxr_cdata( $post->comment_status ) . '</wp:comment_status>' . PHP_EOL;
640 $result .= $this->indent( 3 ) . '<wp:ping_status>' . $this->wxr_cdata( $post->ping_status ) . '</wp:ping_status>' . PHP_EOL;
641 $result .= $this->indent( 3 ) . '<wp:post_name>' . $this->wxr_cdata( $post->post_name ) . '</wp:post_name>' . PHP_EOL;
642 $result .= $this->indent( 3 ) . '<wp:status>' . $this->wxr_cdata( $post->post_status ) . '</wp:status>' . PHP_EOL;
643 $result .= $this->indent( 3 ) . '<wp:post_parent>' . $this->wxr_cdata( $post->post_parent ) . '</wp:post_parent>' . PHP_EOL;
644 $result .= $this->indent( 3 ) . '<wp:menu_order>' . (int) $post->menu_order . '</wp:menu_order>' . PHP_EOL;
645 $result .= $this->indent( 3 ) . '<wp:post_type>' . $this->wxr_cdata( $post->post_type ) . '</wp:post_type>' . PHP_EOL;
646 $result .= $this->indent( 3 ) . '<wp:post_password>' . $this->wxr_cdata( $post->post_password ) . '</wp:post_password>' . PHP_EOL;
647 $result .= $this->indent( 3 ) . '<wp:is_sticky>' . ( is_sticky( $post->ID ) ? 1 : 0 ) . '</wp:is_sticky>' . PHP_EOL;
648
649 if ( 'attachment' === $post->post_type ) {
650 $result .= $this->indent( 3 ) . '<wp:attachment_url>' . $this->wxr_cdata( wp_get_attachment_url( $post->ID ) ) . '</wp:attachment_url>' . PHP_EOL;
651 }
652
653 $result .= $this->wxr_post_taxonomy( $post );
654
655 $postmeta = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->postmeta} WHERE post_id = %d", $post->ID ) );// phpcs:ignore
656 foreach ( $postmeta as $meta ) {
657 /**
658 * Filters whether to selectively skip post meta used for WXR exports.
659 *
660 * Returning a truthy value from the filter will skip the current meta
661 * object from being exported.
662 *
663 * @param bool $skip Whether to skip the current post meta. Default false.
664 * @param string $meta_key Current meta key.
665 * @param object $meta Current meta object.
666 *
667 * @since 3.3.0
668 *
669 */
670 if ( apply_filters( 'wxr_export_skip_postmeta', false, $meta->meta_key, $meta ) ) {
671 continue;
672 }
673
674 $result .= $this->indent( 3 ) . '<wp:postmeta>' . PHP_EOL;
675
676 $result .= $this->indent( 4 ) . '<wp:meta_key>' . $this->wxr_cdata( $meta->meta_key ) . '</wp:meta_key>' . PHP_EOL;
677 $result .= $this->indent( 4 ) . '<wp:meta_value>' . $this->wxr_cdata( $meta->meta_value ) . '</wp:meta_value>' . PHP_EOL;
678
679 $result .= $this->indent( 3 ) . '</wp:postmeta>' . PHP_EOL;
680 }
681
682 $_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
683 $comments = array_map( 'get_comment', $_comments );
684 foreach ( $comments as $c ) {
685
686 $result .= $this->indent( 3 ) . '<wp:comment>' . PHP_EOL;
687
688 $result .= $this->indent( 4 ) . '<wp:comment_id>' . (int) $c->comment_ID . '</wp:comment_id>' . PHP_EOL;
689 $result .= $this->indent( 4 ) . '<wp:comment_author>' . $this->wxr_cdata( $c->comment_author ) . '</wp:comment_author>' . PHP_EOL;
690 $result .= $this->indent( 4 ) . '<wp:comment_author_email>' . $this->wxr_cdata( $c->comment_author_email ) . '</wp:comment_author_email>' . PHP_EOL;
691 $result .= $this->indent( 4 ) . '<wp:comment_author_url>' . $this->wxr_cdata( $c->comment_author_url ) . '</wp:comment_author_url>' . PHP_EOL;
692 $result .= $this->indent( 4 ) . '<wp:comment_author_IP>' . $this->wxr_cdata( $c->comment_author_IP ) . '</wp:comment_author_IP>' . PHP_EOL;
693 $result .= $this->indent( 4 ) . '<wp:comment_date>' . $this->wxr_cdata( $c->comment_date ) . '</wp:comment_date>' . PHP_EOL;
694 $result .= $this->indent( 4 ) . '<wp:comment_date_gmt>' . $this->wxr_cdata( $c->comment_date_gmt ) . '</wp:comment_date_gmt>' . PHP_EOL;
695 $result .= $this->indent( 4 ) . '<wp:comment_content>' . $this->wxr_cdata( $c->comment_content ) . '</wp:comment_content>' . PHP_EOL;
696 $result .= $this->indent( 4 ) . '<wp:comment_approved>' . $this->wxr_cdata( $c->comment_approved ) . '</wp:comment_approved>' . PHP_EOL;
697 $result .= $this->indent( 4 ) . '<wp:comment_type>' . $this->wxr_cdata( $c->comment_type ) . '</wp:comment_type>' . PHP_EOL;
698 $result .= $this->indent( 4 ) . '<wp:comment_parent>' . $this->wxr_cdata( $c->comment_parent ) . '</wp:comment_parent>' . PHP_EOL;
699 $result .= $this->indent( 4 ) . '<wp:comment_user_id>' . (int) $c->user_id . '</wp:comment_user_id>' . PHP_EOL;
700
701 $c_meta = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->commentmeta} WHERE comment_id = %d", $c->comment_ID ) );// phpcs:ignore
702 foreach ( $c_meta as $meta ) {
703 /**
704 * Filters whether to selectively skip comment meta used for WXR exports.
705 *
706 * Returning a truthy value from the filter will skip the current meta
707 * object from being exported.
708 *
709 * @param bool $skip Whether to skip the current comment meta. Default false.
710 * @param string $meta_key Current meta key.
711 * @param object $meta Current meta object.
712 *
713 * @since 4.0.0
714 *
715 */
716 if ( apply_filters( 'wxr_export_skip_commentmeta', false, $meta->meta_key, $meta ) ) {
717 continue;
718 }
719
720 $result .= $this->indent( 4 ) . '<wp:commentmeta>' . PHP_EOL;
721
722 $result .= $this->indent( 5 ) . '<wp:meta_key>' . $this->wxr_cdata( $meta->meta_key ) . '</wp:meta_key>' . PHP_EOL;
723 $result .= $this->indent( 5 ) . '<wp:meta_value>' . $this->wxr_cdata( $meta->meta_key ) . '</wp:meta_value>' . PHP_EOL;
724
725 $result .= $this->indent( 4 ) . '</wp:commentmeta>' . PHP_EOL;
726 }
727
728 $result .= $this->indent( 3 ) . '</wp:comment>' . PHP_EOL;
729 }
730
731 $result .= $this->indent( 2 ) . '</item>' . PHP_EOL;
732 }
733 }
734 }
735
736 return $result;
737 }
738
739 /**
740 * Return all navigation menu terms
741 *
742 * @return string
743 */
744 private function wxr_nav_menu_terms(): string {
745 $args = [];
746
747 if( ! empty( $this->nav_menu_terms ) ) {
748 $args['include'] = $this->nav_menu_terms;
749 }
750
751 $nav_menus = wp_get_nav_menus( $args );
752 if ( empty( $nav_menus ) || ! is_array( $nav_menus ) ) {
753 return '';
754 }
755
756 $result = '';
757
758 foreach ( $nav_menus as $menu ) {
759 $this->terms[ $menu->term_id ] = $menu;
760
761 $result .= $this->indent( 2 ) . '<wp:term>' . PHP_EOL;
762 $result .= $this->indent( 3 ) . '<wp:term_id>' . (int) $menu->term_id . '</wp:term_id>' . PHP_EOL;
763 $result .= $this->indent( 3 ) . '<wp:term_taxonomy>nav_menu</wp:term_taxonomy>' . PHP_EOL;
764 $result .= $this->indent( 3 ) . '<wp:term_slug>' . $this->wxr_cdata( $menu->slug ) . '</wp:term_slug>' . PHP_EOL;
765 $result .= $this->indent( 3 ) . '<wp:term_name>' . $this->wxr_cdata( $menu->name ) . '</wp:term_name>' . PHP_EOL;
766 $result .= $this->indent( 2 ) . '</wp:term>' . PHP_EOL;
767 }
768
769 return $result;
770 }
771
772 /**
773 * Return list of taxonomy terms, in XML tag format, associated with a post
774 *
775 * @param object $post
776 *
777 * @return string
778 */
779 private function wxr_post_taxonomy( $post ): string {
780 $result = '';
781
782 $taxonomies = get_object_taxonomies( $post->post_type );
783
784 if ( empty( $taxonomies ) ) {
785 return $result;
786 }
787
788 $terms = wp_get_object_terms( $post->ID, $taxonomies );
789
790 foreach ( (array) $terms as $term ) {
791 $result .= $this->indent( 3 ) . "<category domain=\"{$term->taxonomy}\" nicename=\"{$term->slug}\">" . $this->wxr_cdata( $term->name ) . '</category>' . PHP_EOL;
792 }
793
794 return $result;
795 }
796
797 /**
798 * Get the XML export.
799 *
800 * @param array $post_ids
801 *
802 * @return string
803 */
804 private function get_xml_export( array $post_ids ): string {
805 $charset = get_bloginfo( 'charset' );
806 $generator = get_the_generator( 'export' );
807 $wxr_version = self::WXR_VERSION;
808 $wxr_site_url = $this->wxr_site_url();
809 $rss_info_name = get_bloginfo_rss( 'name' );
810 $rss_info_url = get_bloginfo_rss( 'url' );
811 $rss_info_description = get_bloginfo_rss( 'description' );
812 $rss_info_language = get_bloginfo_rss( 'language' );
813 $pub_date = gmdate( 'D, d M Y H:i:s +0000' );
814
815 $dynamic = $this->wxr_authors_list( $post_ids );
816
817 ob_start();
818 /** This action is documented in wp-includes/feed-rss2.php */
819 do_action( 'rss2_head' );
820 $rss2_head = ob_get_clean();
821
822 $dynamic .= $rss2_head;
823
824 if ( 'all' === $this->args['content'] || 'nav_menu_item' === $this->args['content'] ) {
825 $dynamic .= $this->wxr_nav_menu_terms();
826 }
827
828 $dynamic .= $this->wxr_terms_list( $this->get_terms( $post_ids ) );
829 $dynamic .= $this->wxr_posts_list( $post_ids );
830
831 $result = <<<EOT
832 <?xml version="1.0" encoding="$charset" ?>
833 <!-- This is a WordPress eXtended RSS file generated by WordPress as an export of your site. -->
834 <!-- It contains information about your site's posts, pages, comments, categories, and other content. -->
835 <!-- You may use this file to transfer that content from one site to another. -->
836 <!-- This file is not intended to serve as a complete backup of your site. -->
837
838 <!-- To import this information into a WordPress site follow these steps: -->
839 <!-- 1. Log in to that site as an administrator. -->
840 <!-- 2. Go to Tools: Import in the WordPress admin panel. -->
841 <!-- 3. Install the "WordPress" importer from the list. -->
842 <!-- 4. Activate & Run Importer. -->
843 <!-- 5. Upload this file using the form provided on that page. -->
844 <!-- 6. You will first be asked to map the authors in this export file to users -->
845 <!-- on the site. For each author, you may choose to map to an -->
846 <!-- existing user on the site or to create a new user. -->
847 <!-- 7. WordPress will then import each of the posts, pages, comments, categories, etc. -->
848 <!-- contained in this file into your site. -->
849 $generator
850 <rss version="2.0"
851 xmlns:excerpt="http://wordpress.org/export/$wxr_version/excerpt/"
852 xmlns:content="http://purl.org/rss/1.0/modules/content/"
853 xmlns:wfw="http://wellformedweb.org/CommentAPI/"
854 xmlns:dc="http://purl.org/dc/elements/1.1/"
855 xmlns:wp="http://wordpress.org/export/$wxr_version/"
856 >
857 <channel>
858 <title>$rss_info_name</title>
859 <link>$rss_info_url</link>
860 <description>$rss_info_description</description>
861 <pubDate>$pub_date</pubDate>
862 <language>$rss_info_language</language>
863 <wp:wxr_version>$wxr_version</wp:wxr_version>
864 <wp:base_site_url>$wxr_site_url</wp:base_site_url>
865 <wp:base_blog_url>$rss_info_url</wp:base_blog_url>
866 $dynamic
867 </channel>
868 </rss>
869 EOT;
870
871 return $result;
872 }
873
874 public function __construct( array $args = [] ) {
875 global $wpdb;
876
877 $this->args = wp_parse_args( $args, self::$default_args );
878
879 $this->wpdb = $wpdb;
880 }
881 }
882