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

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

948 lines 33.0 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 public function run(): array {
66 // Handle glossaries export (as taxonomy)
67 if ('glossaries' === $this->args['content']) {
68 return $this->handle_glossaries_export();
69 }
70
71 // Early return for invalid post types
72 if ($this->args['content'] !== 'docs') {
73 return [];
74 }
75
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();
87 }
88
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'];
94 }
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 }
102
103 // Handle additional filters (author, dates, meta)
104 $where .= $this->build_additional_filters();
105
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");
108
109 // Handle FAQ posts separately
110 $faq_post_ids = [];
111 if (!empty($this->args['include_faq'])) {
112 $faq_post_ids = $this->get_faq_posts();
113 }
114
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 }
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 }
146
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 }
192 }
193
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);
197 }
198 }
199
200 return [
201 'join' => $join,
202 'where' => $where
203 ];
204 }
205
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)";
209
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 }
219 }
220
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);
224 }
225 }
226
227 return [
228 'join' => $join,
229 'where' => $where
230 ];
231 }
232
233 private function build_additional_filters(): string {
234 $where = '';
235
236 if ($this->args['author']) {
237 $where .= $this->wpdb->prepare(" AND {$this->wpdb->posts}.post_author = %d", $this->args['author']);
238 }
239
240 if ($this->args['start_date']) {
241 $where .= $this->wpdb->prepare(" AND {$this->wpdb->posts}.post_date >= %s",
242 gmdate('Y-m-d', strtotime($this->args['start_date'])));
243 }
244
245 if ($this->args['end_date']) {
246 $where .= $this->wpdb->prepare(" AND {$this->wpdb->posts}.post_date < %s",
247 gmdate('Y-m-d', strtotime('+1 month', strtotime($this->args['end_date']))));
248 }
249
250 return $where;
251 }
252
253 private function get_faq_posts(): array {
254 return get_posts([
255 'numberposts' => -1,
256 'post_type' => 'betterdocs_faq',
257 'fields' => 'ids',
258 'post_status' => 'publish'
259 ]);
260 }
261
262 private function get_thumbnail_ids(array $post_ids): array {
263 $thumbnail_ids = [];
264
265 if (!empty($this->args['include_post_featured_image_as_attachment'])) {
266 foreach ($post_ids as $post_id) {
267 $thumbnail_id = get_post_meta($post_id, '_thumbnail_id', true);
268 if ($thumbnail_id && !in_array($thumbnail_id, $post_ids, true)) {
269 $thumbnail_ids[] = $thumbnail_id;
270 }
271 }
272 }
273
274 return $thumbnail_ids;
275 }
276
277 /**
278 * Return tabulation characters, by `$columns`.
279 *
280 * @param int $columns
281 *
282 * @return string
283 */
284 private function indent( int $columns = 1 ): string {
285
286 $output = str_repeat( "\t", $columns );
287
288 return (string) $output;
289 }
290
291 /**
292 * Return wrapped given string in XML CDATA tag.
293 *
294 * @param string $str String to wrap in XML CDATA tag.
295 *
296 * @return string
297 */
298 private function wxr_cdata( $str ): string {
299 $str = (string) $str;
300
301 if ( ! seems_utf8( $str ) ) {
302 $str = utf8_encode( $str );
303 }
304
305 $str = '<![CDATA[' . str_replace( ']]>', ']]]]><![CDATA[>', $str ) . ']]>';
306
307 return $str;
308 }
309
310 /**
311 * Return the URL of the site.
312 *
313 * @return string Site URL.
314 */
315 private function wxr_site_url(): string {
316 if ( is_multisite() ) {
317 // Multisite: the base URL.
318 return network_home_url();
319 } else {
320 // WordPress (single site): the blog URL.
321 return get_bloginfo_rss( 'url' );
322 }
323 }
324
325 /**
326 * Return a cat_name XML tag from a given category object.
327 *
328 * @param WP_Term $category Category Object
329 *
330 * @return string
331 */
332 private function wxr_cat_name( $category ): string {
333 if ( empty( $category->name ) ) {
334 return '';
335 }
336
337 return $this->indent( 3 ) . '<wp:cat_name>' . $this->wxr_cdata( $category->name ) . '</wp:cat_name>' . PHP_EOL;
338 }
339
340 /**
341 * Return a category_description XML tag from a given category object.
342 *
343 * @param WP_Term $category Category Object
344 *
345 * @return string
346 */
347 private function wxr_category_description( $category ): string {
348 if ( empty( $category->description ) ) {
349 return '';
350 }
351
352 return $this->indent( 3 ) . '<wp:category_description>' . $this->wxr_cdata( $category->description ) . "</wp:category_description>\n";
353 }
354
355 /**
356 * Return a tag_name XML tag from a given tag object.
357 *
358 * @param WP_Term $tag Tag Object
359 *
360 * @return string
361 */
362 private function wxr_tag_name( $tag ): string {
363 if ( empty( $tag->name ) ) {
364 return '';
365 }
366
367 return $this->indent( 3 ) . '<wp:tag_name>' . $this->wxr_cdata( $tag->name ) . '</wp:tag_name>' . PHP_EOL;
368 }
369
370 /**
371 * Return a tag_description XML tag from a given tag object.
372 *
373 * @param WP_Term $tag Tag Object
374 *
375 * @return string
376 */
377 private function wxr_tag_description( $tag ): string {
378 if ( empty( $tag->description ) ) {
379 return '';
380 }
381
382 return $this->indent( 3 ) . '<wp:tag_description>' . $this->wxr_cdata( $tag->description ) . '</wp:tag_description>' . PHP_EOL;
383 }
384
385 /**
386 * Return a term_name XML tag from a given term object.
387 *
388 * @param WP_Term $term Term Object
389 *
390 * @return string
391 */
392 private function wxr_term_name( $term ): string {
393 if ( empty( $term->name ) ) {
394 return '';
395 }
396
397 return $this->indent( 3 ) . '<wp:term_name>' . $this->wxr_cdata( $term->name ) . '</wp:term_name>' . PHP_EOL;
398 }
399
400 /**
401 * Return a term_description XML tag from a given term object.
402 *
403 * @param WP_Term $term Term Object
404 *
405 * @return string
406 */
407 private function wxr_term_description( $term ): string {
408 if ( empty( $term->description ) ) {
409 return '';
410 }
411
412 return $this->indent( 3 ) . '<wp:term_description>' . $this->wxr_cdata( $term->description ) . '</wp:term_description>' . PHP_EOL;
413 }
414
415 /**
416 * Return term meta XML tags for a given term object.
417 *
418 * @param WP_Term $term Term object.
419 *
420 * @return string
421 */
422 private function wxr_term_meta( $term ): string {
423 $result = '';
424 $termmeta = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->termmeta} WHERE term_id = %d", $term->term_id ) );// phpcs:ignore
425
426 foreach ( $termmeta as $meta ) {
427 /**
428 * Filters whether to selectively skip term meta used for WXR exports.
429 *
430 * Returning a truthy value from the filter will skip the current meta
431 * object from being exported.
432 *
433 * @param bool $skip Whether to skip the current piece of term meta. Default false.
434 * @param string $meta_key Current meta key.
435 * @param object $meta Current meta object.
436 *
437 * @since 4.6.0
438 *
439 */
440 if ( ! apply_filters( 'wxr_export_skip_termmeta', false, $meta->meta_key, $meta ) ) {
441 $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 ) );
442 }
443 }
444
445 return $result;
446 }
447
448 /**
449 * Return list of authors with posts.
450 *
451 * @param int[] $post_ids Optional. Array of post IDs to filter the query by.
452 *
453 * @return string
454 */
455 private function wxr_authors_list( array $post_ids = null ): string {
456 $result = '';
457
458 if ( ! empty( $post_ids ) ) {
459 $post_ids = array_map( 'absint', $post_ids );
460 $and = 'AND ID IN ( ' . implode( ', ', $post_ids ) . ')';
461 } else {
462 $and = '';
463 }
464
465 $authors = [];
466 $results = $this->wpdb->get_results( "SELECT DISTINCT post_author FROM {$this->wpdb->posts} WHERE post_status != 'auto-draft' $and" );// phpcs:ignore
467 foreach ( (array) $results as $r ) {
468 $authors[] = get_userdata( $r->post_author );
469 }
470
471 $authors = array_filter( $authors );
472
473 foreach ( $authors as $author ) {
474 $result .= $this->indent( 2 ) . '<wp:author>' . PHP_EOL;
475
476 $result .= $this->indent( 3 ) . '<wp:author_id>' . (int) $author->ID . '</wp:author_id>' . PHP_EOL;
477 $result .= $this->indent( 3 ) . '<wp:author_login>' . $this->wxr_cdata( $author->user_login ) . '</wp:author_login>' . PHP_EOL;
478 $result .= $this->indent( 3 ) . '<wp:author_email>' . $this->wxr_cdata( $author->user_email ) . '</wp:author_email>' . PHP_EOL;
479 $result .= $this->indent( 3 ) . '<wp:author_display_name>' . $this->wxr_cdata( $author->display_name ) . '</wp:author_display_name>' . PHP_EOL;
480 $result .= $this->indent( 3 ) . '<wp:author_first_name>' . $this->wxr_cdata( $author->first_name ) . '</wp:author_first_name>' . PHP_EOL;
481 $result .= $this->indent( 3 ) . '<wp:author_last_name>' . $this->wxr_cdata( $author->last_name ) . '</wp:author_last_name>' . PHP_EOL;
482
483 $result .= $this->indent( 2 ) . '</wp:author>' . PHP_EOL;
484 }
485
486 return $result;
487 }
488
489 /**
490 * Return list of categories.
491 *
492 * @param array $cats
493 *
494 * @return string
495 */
496 private function wxr_categories_list( array $cats ): string {
497 $result = '';
498
499 foreach ( $cats as $c ) {
500 $result .= $this->indent( 2 ) . '<wp:category>' . PHP_EOL;
501
502 $result .= $this->indent( 3 ) . '<wp:term_id>' . (int) $c->term_id . '</wp:term_id>' . PHP_EOL;
503 $result .= $this->indent( 3 ) . '<wp:category_nicename>' . $this->wxr_cdata( $c->slug ) . '</wp:category_nicename>' . PHP_EOL;
504 $result .= $this->indent( 3 ) . '<wp:category_parent>' . $this->wxr_cdata( $c->parent ? $cats[ $c->parent ]->slug : '' ) . '</wp:category_parent>' . PHP_EOL;
505 $result .= $this->wxr_cat_name( $c ) . $this->wxr_category_description( $c ) . $this->wxr_term_meta( $c );
506
507 $result .= $this->indent( 2 ) . '</wp:category>' . PHP_EOL;
508 }
509
510 return $result;
511 }
512
513 /**
514 * Return list of tags.
515 *
516 * @param array $tags
517 *
518 * @return string
519 */
520 private function wxr_tags_list( array $tags ): string {
521 $result = '';
522
523 foreach ( $tags as $t ) {
524 $result .= $this->indent( 2 ) . '<wp:tag>' . PHP_EOL;
525
526 $result .= $this->indent( 3 ) . '<wp:term_id>' . (int) $t->term_id . '</wp:term_id>' . PHP_EOL;
527 $result .= $this->indent( 3 ) . '<wp:tag_slug>' . $this->wxr_cdata( $t->slug ) . '</wp:tag_slug>' . PHP_EOL;
528 $result .= $this->wxr_tag_name( $t ) . $this->wxr_tag_description( $t ) . $this->wxr_term_meta( $t );
529
530 $result .= $this->indent( 2 ) . '</wp:tag>' . PHP_EOL;
531 }
532
533 return $result;
534 }
535
536 public function get_parent_terms_slug( $terms, $term_id ) {
537 $key = array_search( $term_id, array_column( $terms, 'term_id' ));
538
539 if ( $key !== false ) {
540 return $terms[$key]->slug;
541 }
542
543 return null;
544 }
545
546 /**
547 * Return list of terms.
548 *
549 * @param array $terms
550 *
551 * @return string
552 */
553 private function wxr_terms_list( array $terms ): string {
554 $result = '';
555 foreach ( $terms as $key => $t ) {
556 $result .= $this->indent( 2 ) . '<wp:term>' . PHP_EOL;
557
558 $result .= $this->indent( 3 ) . '<wp:term_id>' . $this->wxr_cdata( $t->term_id ) . '</wp:term_id>' . PHP_EOL;
559 $result .= $this->indent( 3 ) . '<wp:term_taxonomy>' . $this->wxr_cdata( $t->taxonomy ) . '</wp:term_taxonomy>' . PHP_EOL;
560 $result .= $this->indent( 3 ) . '<wp:term_slug>' . $this->wxr_cdata( $t->slug ) . '</wp:term_slug>' . PHP_EOL;
561 if ( $t->parent ) {
562 $result .= $this->indent( 3 ) . '<wp:term_parent>' . $this->wxr_cdata( $this->get_parent_terms_slug( $terms, $t->parent ) ) . '</wp:term_parent>' . PHP_EOL;
563 }
564 $result .= $this->wxr_term_name( $t ) . $this->wxr_term_description( $t ) . $this->wxr_term_meta( $t );
565
566 $result .= $this->indent( 2 ) . '</wp:term>' . PHP_EOL;
567 }
568
569 return $result;
570 }
571
572 /**
573 * Retrieve terms associated with the specified object IDs and sort them based on term meta.
574 *
575 * @param array $post_ids An array of object IDs.
576 * @return array An array of WP_Term objects sorted based on term meta.
577 */
578 public function get_terms( array $post_ids ) {
579 // Get the object taxonomies
580 $taxonomies = get_object_taxonomies( $this->args['content'] );
581
582 if ( isset( $this->args['selected_docs'] ) && ! empty ( $this->args['selected_docs'] ) && $this->args['selected_docs'][0] == 'all' ) {
583 $terms = get_terms( [
584 'taxonomy' => $taxonomies,
585 'hide_empty' => false,
586 ] );
587 } else if( $this->args['content'] == 'glossaries' ) {
588 $terms = get_terms([
589 'taxonomy' => 'glossaries',
590 'include' => $post_ids,
591 'hide_empty' => false,
592 ]);
593 } else {
594 $terms = wp_get_object_terms( $post_ids, $taxonomies);
595 }
596
597 usort( $terms, array( $this, 'compare_terms_by_meta' ) );
598
599 return $terms;
600 }
601
602 /**
603 * Compare terms based on their associated term meta values.
604 *
605 * @param WP_Term $a The first term object.
606 * @param WP_Term $b The second term object.
607 * @return int Returns a negative value if $a is less than $b,
608 * a positive value if $a is greater than $b, or 0 if they are equal.
609 * Additionally, prioritize sorting terms by taxonomy order,
610 * with 'doc_category' terms appearing before other taxonomy terms.
611 */
612 public function compare_terms_by_meta($a, $b) {
613 // Define the order of taxonomies
614 $taxonomy_order = array(
615 'doc_category' => 0,
616 'knowledge_base' => 1,
617 'doc_tag' => 2,
618 );
619
620 // Get the taxonomy order for terms $a and $b
621 $order_a = isset($taxonomy_order[$a->taxonomy]) ? $taxonomy_order[$a->taxonomy] : PHP_INT_MAX;
622 $order_b = isset($taxonomy_order[$b->taxonomy]) ? $taxonomy_order[$b->taxonomy] : PHP_INT_MAX;
623
624 // If the taxonomies have different order, sort by order
625 if ($order_a !== $order_b) {
626 return $order_a - $order_b;
627 }
628
629 // If the taxonomies have the same order, sort by meta value
630 $taxonomy_order_meta = array(
631 'doc_category' => 'doc_category_order',
632 'knowledge_base' => 'kb_order'
633 );
634
635 if (isset($taxonomy_order_meta[$a->taxonomy]) && isset($taxonomy_order_meta[$b->taxonomy])) {
636 $meta_a = intval(get_term_meta($a->term_id, $taxonomy_order_meta[$a->taxonomy], true));
637 $meta_b = intval(get_term_meta($b->term_id, $taxonomy_order_meta[$b->taxonomy], true));
638
639 return $meta_a - $meta_b;
640 }
641
642 return 0; // Default to no sorting if meta keys are not defined
643 }
644
645 /**
646 * Return list of posts, by requested `$post_ids`.
647 *
648 * @param array $post_ids
649 *
650 * @return string
651 */
652 private function wxr_posts_list( array $post_ids ): string {
653 $result = '';
654
655 if ( $post_ids ) {
656 global $wp_query;
657
658 // Fake being in the loop.
659 $wp_query->in_the_loop = true;
660
661 // Fetch 20 posts at a time rather than loading the entire table into memory.
662 while ( $next_posts = array_splice( $post_ids, 0, 20 ) ) {
663 $where = 'WHERE ID IN (' . implode( ',', $next_posts ) . ')';
664 $posts = $this->wpdb->get_results( "SELECT * FROM {$this->wpdb->posts} $where" );// phpcs:ignore
665
666 // Begin Loop.
667 foreach ( $posts as $post ) {
668 setup_postdata( $post );
669
670 $title = apply_filters( 'the_title_rss', $post->post_title );
671
672 /**
673 * Filters the post content used for WXR exports.
674 *
675 * @param string $post_content Content of the current post.
676 *
677 * @since 2.5.0
678 *
679 */
680 $content = $this->wxr_cdata( apply_filters( 'the_content_export', $post->post_content ) );
681
682 /**
683 * Filters the post excerpt used for WXR exports.
684 *
685 * @param string $post_excerpt Excerpt for the current post.
686 *
687 * @since 2.6.0
688 *
689 */
690 $excerpt = $this->wxr_cdata( apply_filters( 'the_excerpt_export', $post->post_excerpt ) );
691
692 $result .= $this->indent( 2 ) . '<item>' . PHP_EOL;
693
694 $result .= $this->indent( 3 ) . '<title>' . $title . '</title>' . PHP_EOL;
695 $result .= $this->indent( 3 ) . '<link>' . esc_url( get_permalink() ) . '</link>' . PHP_EOL;
696 $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;
697 $result .= $this->indent( 3 ) . '<dc:creator>' . $this->wxr_cdata( get_the_author_meta( 'login' ) ) . '</dc:creator>' . PHP_EOL;
698 $result .= $this->indent( 3 ) . '<guid isPermaLink="false">' . $this->wxr_cdata( get_the_author_meta( 'login' ) ) . '</guid>' . PHP_EOL;
699 $result .= $this->indent( 3 ) . '<description></description>' . PHP_EOL;
700 $result .= $this->indent( 3 ) . '<content:encoded>' . $content . '</content:encoded>' . PHP_EOL;
701 $result .= $this->indent( 3 ) . '<excerpt:encoded>' . $excerpt . '</excerpt:encoded>' . PHP_EOL;
702 $result .= $this->indent( 3 ) . '<wp:post_id>' . (int) $post->ID . '</wp:post_id>' . PHP_EOL;
703 $result .= $this->indent( 3 ) . '<wp:post_date>' . $this->wxr_cdata( $post->post_date ) . '</wp:post_date>' . PHP_EOL;
704 $result .= $this->indent( 3 ) . '<wp:post_date_gmt>' . $this->wxr_cdata( $post->post_date_gmt ) . '</wp:post_date_gmt>' . PHP_EOL;
705 $result .= $this->indent( 3 ) . '<wp:comment_status>' . $this->wxr_cdata( $post->comment_status ) . '</wp:comment_status>' . PHP_EOL;
706 $result .= $this->indent( 3 ) . '<wp:ping_status>' . $this->wxr_cdata( $post->ping_status ) . '</wp:ping_status>' . PHP_EOL;
707 $result .= $this->indent( 3 ) . '<wp:post_name>' . $this->wxr_cdata( $post->post_name ) . '</wp:post_name>' . PHP_EOL;
708 $result .= $this->indent( 3 ) . '<wp:status>' . $this->wxr_cdata( $post->post_status ) . '</wp:status>' . PHP_EOL;
709 $result .= $this->indent( 3 ) . '<wp:post_parent>' . $this->wxr_cdata( $post->post_parent ) . '</wp:post_parent>' . PHP_EOL;
710 $result .= $this->indent( 3 ) . '<wp:menu_order>' . (int) $post->menu_order . '</wp:menu_order>' . PHP_EOL;
711 $result .= $this->indent( 3 ) . '<wp:post_type>' . $this->wxr_cdata( $post->post_type ) . '</wp:post_type>' . PHP_EOL;
712 $result .= $this->indent( 3 ) . '<wp:post_password>' . $this->wxr_cdata( $post->post_password ) . '</wp:post_password>' . PHP_EOL;
713 $result .= $this->indent( 3 ) . '<wp:is_sticky>' . ( is_sticky( $post->ID ) ? 1 : 0 ) . '</wp:is_sticky>' . PHP_EOL;
714
715 if ( 'attachment' === $post->post_type ) {
716 $result .= $this->indent( 3 ) . '<wp:attachment_url>' . $this->wxr_cdata( wp_get_attachment_url( $post->ID ) ) . '</wp:attachment_url>' . PHP_EOL;
717 }
718
719 $result .= $this->wxr_post_taxonomy( $post );
720
721 $postmeta = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->postmeta} WHERE post_id = %d", $post->ID ) );// phpcs:ignore
722 foreach ( $postmeta as $meta ) {
723 /**
724 * Filters whether to selectively skip post meta used for WXR exports.
725 *
726 * Returning a truthy value from the filter will skip the current meta
727 * object from being exported.
728 *
729 * @param bool $skip Whether to skip the current post meta. Default false.
730 * @param string $meta_key Current meta key.
731 * @param object $meta Current meta object.
732 *
733 * @since 3.3.0
734 *
735 */
736 if ( apply_filters( 'wxr_export_skip_postmeta', false, $meta->meta_key, $meta ) ) {
737 continue;
738 }
739
740 $result .= $this->indent( 3 ) . '<wp:postmeta>' . PHP_EOL;
741
742 $result .= $this->indent( 4 ) . '<wp:meta_key>' . $this->wxr_cdata( $meta->meta_key ) . '</wp:meta_key>' . PHP_EOL;
743 $result .= $this->indent( 4 ) . '<wp:meta_value>' . $this->wxr_cdata( $meta->meta_value ) . '</wp:meta_value>' . PHP_EOL;
744
745 $result .= $this->indent( 3 ) . '</wp:postmeta>' . PHP_EOL;
746 }
747
748 $_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
749 $comments = array_map( 'get_comment', $_comments );
750 foreach ( $comments as $c ) {
751
752 $result .= $this->indent( 3 ) . '<wp:comment>' . PHP_EOL;
753
754 $result .= $this->indent( 4 ) . '<wp:comment_id>' . (int) $c->comment_ID . '</wp:comment_id>' . PHP_EOL;
755 $result .= $this->indent( 4 ) . '<wp:comment_author>' . $this->wxr_cdata( $c->comment_author ) . '</wp:comment_author>' . PHP_EOL;
756 $result .= $this->indent( 4 ) . '<wp:comment_author_email>' . $this->wxr_cdata( $c->comment_author_email ) . '</wp:comment_author_email>' . PHP_EOL;
757 $result .= $this->indent( 4 ) . '<wp:comment_author_url>' . $this->wxr_cdata( $c->comment_author_url ) . '</wp:comment_author_url>' . PHP_EOL;
758 $result .= $this->indent( 4 ) . '<wp:comment_author_IP>' . $this->wxr_cdata( $c->comment_author_IP ) . '</wp:comment_author_IP>' . PHP_EOL;
759 $result .= $this->indent( 4 ) . '<wp:comment_date>' . $this->wxr_cdata( $c->comment_date ) . '</wp:comment_date>' . PHP_EOL;
760 $result .= $this->indent( 4 ) . '<wp:comment_date_gmt>' . $this->wxr_cdata( $c->comment_date_gmt ) . '</wp:comment_date_gmt>' . PHP_EOL;
761 $result .= $this->indent( 4 ) . '<wp:comment_content>' . $this->wxr_cdata( $c->comment_content ) . '</wp:comment_content>' . PHP_EOL;
762 $result .= $this->indent( 4 ) . '<wp:comment_approved>' . $this->wxr_cdata( $c->comment_approved ) . '</wp:comment_approved>' . PHP_EOL;
763 $result .= $this->indent( 4 ) . '<wp:comment_type>' . $this->wxr_cdata( $c->comment_type ) . '</wp:comment_type>' . PHP_EOL;
764 $result .= $this->indent( 4 ) . '<wp:comment_parent>' . $this->wxr_cdata( $c->comment_parent ) . '</wp:comment_parent>' . PHP_EOL;
765 $result .= $this->indent( 4 ) . '<wp:comment_user_id>' . (int) $c->user_id . '</wp:comment_user_id>' . PHP_EOL;
766
767 $c_meta = $this->wpdb->get_results( $this->wpdb->prepare( "SELECT * FROM {$this->wpdb->commentmeta} WHERE comment_id = %d", $c->comment_ID ) );// phpcs:ignore
768 foreach ( $c_meta as $meta ) {
769 /**
770 * Filters whether to selectively skip comment meta used for WXR exports.
771 *
772 * Returning a truthy value from the filter will skip the current meta
773 * object from being exported.
774 *
775 * @param bool $skip Whether to skip the current comment meta. Default false.
776 * @param string $meta_key Current meta key.
777 * @param object $meta Current meta object.
778 *
779 * @since 4.0.0
780 *
781 */
782 if ( apply_filters( 'wxr_export_skip_commentmeta', false, $meta->meta_key, $meta ) ) {
783 continue;
784 }
785
786 $result .= $this->indent( 4 ) . '<wp:commentmeta>' . PHP_EOL;
787
788 $result .= $this->indent( 5 ) . '<wp:meta_key>' . $this->wxr_cdata( $meta->meta_key ) . '</wp:meta_key>' . PHP_EOL;
789 $result .= $this->indent( 5 ) . '<wp:meta_value>' . $this->wxr_cdata( $meta->meta_key ) . '</wp:meta_value>' . PHP_EOL;
790
791 $result .= $this->indent( 4 ) . '</wp:commentmeta>' . PHP_EOL;
792 }
793
794 $result .= $this->indent( 3 ) . '</wp:comment>' . PHP_EOL;
795 }
796
797 $result .= $this->indent( 2 ) . '</item>' . PHP_EOL;
798 }
799 }
800 }
801
802 return $result;
803 }
804
805 /**
806 * Return all navigation menu terms
807 *
808 * @return string
809 */
810 private function wxr_nav_menu_terms(): string {
811 $args = [];
812
813 if( ! empty( $this->nav_menu_terms ) ) {
814 $args['include'] = $this->nav_menu_terms;
815 }
816
817 $nav_menus = wp_get_nav_menus( $args );
818 if ( empty( $nav_menus ) || ! is_array( $nav_menus ) ) {
819 return '';
820 }
821
822 $result = '';
823
824 foreach ( $nav_menus as $menu ) {
825 $this->terms[ $menu->term_id ] = $menu;
826
827 $result .= $this->indent( 2 ) . '<wp:term>' . PHP_EOL;
828 $result .= $this->indent( 3 ) . '<wp:term_id>' . (int) $menu->term_id . '</wp:term_id>' . PHP_EOL;
829 $result .= $this->indent( 3 ) . '<wp:term_taxonomy>nav_menu</wp:term_taxonomy>' . PHP_EOL;
830 $result .= $this->indent( 3 ) . '<wp:term_slug>' . $this->wxr_cdata( $menu->slug ) . '</wp:term_slug>' . PHP_EOL;
831 $result .= $this->indent( 3 ) . '<wp:term_name>' . $this->wxr_cdata( $menu->name ) . '</wp:term_name>' . PHP_EOL;
832 $result .= $this->indent( 2 ) . '</wp:term>' . PHP_EOL;
833 }
834
835 return $result;
836 }
837
838 /**
839 * Return list of taxonomy terms, in XML tag format, associated with a post
840 *
841 * @param object $post
842 *
843 * @return string
844 */
845 private function wxr_post_taxonomy( $post ): string {
846 $result = '';
847
848 $taxonomies = get_object_taxonomies( $post->post_type );
849
850 if ( empty( $taxonomies ) ) {
851 return $result;
852 }
853
854 $terms = wp_get_object_terms( $post->ID, $taxonomies );
855
856 foreach ( (array) $terms as $term ) {
857 $result .= $this->indent( 3 ) . "<category domain=\"{$term->taxonomy}\" nicename=\"{$term->slug}\">" . $this->wxr_cdata( $term->name ) . '</category>' . PHP_EOL;
858 }
859
860 return $result;
861 }
862
863 /**
864 * Get the XML export.
865 *
866 * @param array $post_ids
867 *
868 * @return string
869 */
870 private function get_xml_export( array $post_ids ): string {
871 $charset = get_bloginfo( 'charset' );
872 $generator = get_the_generator( 'export' );
873 $wxr_version = self::WXR_VERSION;
874 $wxr_site_url = $this->wxr_site_url();
875 $rss_info_name = get_bloginfo_rss( 'name' );
876 $rss_info_url = get_bloginfo_rss( 'url' );
877 $rss_info_description = get_bloginfo_rss( 'description' );
878 $rss_info_language = get_bloginfo_rss( 'language' );
879 $pub_date = gmdate( 'D, d M Y H:i:s +0000' );
880
881 $dynamic = $this->wxr_authors_list( $post_ids );
882
883 ob_start();
884 /** This action is documented in wp-includes/feed-rss2.php */
885 do_action( 'rss2_head' );
886 $rss2_head = ob_get_clean();
887
888 $dynamic .= $rss2_head;
889
890 if ( 'all' === $this->args['content'] || 'nav_menu_item' === $this->args['content'] ) {
891 $dynamic .= $this->wxr_nav_menu_terms();
892 }
893
894 $dynamic .= $this->wxr_terms_list( $this->get_terms( $post_ids ) );
895 $dynamic .= $this->wxr_posts_list( $post_ids );
896
897 $result = <<<EOT
898 <?xml version="1.0" encoding="$charset" ?>
899 <!-- This is a WordPress eXtended RSS file generated by WordPress as an export of your site. -->
900 <!-- It contains information about your site's posts, pages, comments, categories, and other content. -->
901 <!-- You may use this file to transfer that content from one site to another. -->
902 <!-- This file is not intended to serve as a complete backup of your site. -->
903
904 <!-- To import this information into a WordPress site follow these steps: -->
905 <!-- 1. Log in to that site as an administrator. -->
906 <!-- 2. Go to Tools: Import in the WordPress admin panel. -->
907 <!-- 3. Install the "WordPress" importer from the list. -->
908 <!-- 4. Activate & Run Importer. -->
909 <!-- 5. Upload this file using the form provided on that page. -->
910 <!-- 6. You will first be asked to map the authors in this export file to users -->
911 <!-- on the site. For each author, you may choose to map to an -->
912 <!-- existing user on the site or to create a new user. -->
913 <!-- 7. WordPress will then import each of the posts, pages, comments, categories, etc. -->
914 <!-- contained in this file into your site. -->
915 $generator
916 <rss version="2.0"
917 xmlns:excerpt="http://wordpress.org/export/$wxr_version/excerpt/"
918 xmlns:content="http://purl.org/rss/1.0/modules/content/"
919 xmlns:wfw="http://wellformedweb.org/CommentAPI/"
920 xmlns:dc="http://purl.org/dc/elements/1.1/"
921 xmlns:wp="http://wordpress.org/export/$wxr_version/"
922 >
923 <channel>
924 <title>$rss_info_name</title>
925 <link>$rss_info_url</link>
926 <description>$rss_info_description</description>
927 <pubDate>$pub_date</pubDate>
928 <language>$rss_info_language</language>
929 <wp:wxr_version>$wxr_version</wp:wxr_version>
930 <wp:base_site_url>$wxr_site_url</wp:base_site_url>
931 <wp:base_blog_url>$rss_info_url</wp:base_blog_url>
932 $dynamic
933 </channel>
934 </rss>
935 EOT;
936
937 return $result;
938 }
939
940 public function __construct( array $args = [] ) {
941 global $wpdb;
942
943 $this->args = wp_parse_args( $args, self::$default_args );
944
945 $this->wpdb = $wpdb;
946 }
947 }
948