PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.5.3
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.5.3
4.9.5 4.9.4 4.9.3 4.9.2 4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 All 203 releases
← All changes | includes/Admin/Importer/WPImport.php +469 -552 4.9.2 → 3.5.3 View file →
@@ -1,14 +1,12 @@
1 1 <?php
2 -// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedVariableFound -- view template receives variables via extract(); prefixing is impractical.
3 -// phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- bulk-import write paths from upstream WP Importer; caching unwanted.
4 -// phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- inherited WP Importer hook contract (import_*, wp_import_*) preserved for compat with external listeners.
2 +
5 3 namespace WPDeveloper\BetterDocs\Admin\Importer;
6 4
5 +use WPDeveloper\BetterDocs\Admin\Importer\Parsers\WXR_Parser;
6 +use WPDeveloper\BetterDocs\Admin\Importer\Parsers\CSV_Parser;
7 7 use WP_Error;
8 8 use WP_Importer;
9 -use WPDeveloper\BetterDocs\Admin\Importer\Parsers\CSV_Parser;
10 -use WPDeveloper\BetterDocs\Admin\WPMLSupport;
11 9
12 10 if ( ! defined( 'ABSPATH' ) ) {
13 11 exit; // Exit if accessed directly.
14 12 }
@@ -46,9 +44,9 @@
46 44 require $wp_import;
47 45 }
48 46 }
49 47
50 -use WPDeveloper\BetterDocs\Admin\Importer\Parsers\WXR_Parser;
48 +use function wp_import_cleanup;
51 49
52 50 class WPImport extends WP_Importer {
53 51 const DEFAULT_BUMP_REQUEST_TIMEOUT = 60;
54 52 const DEFAULT_ALLOW_CREATE_USERS = true;
@@ -68,9 +66,9 @@
68 66 * @var array
69 67 */
70 68 private $output = [
71 69 'status' => 'failed',
72 - 'errors' => []
70 + 'errors' => [],
73 71 ];
74 72
75 73 /*
76 74 * WXR attachment ID
@@ -78,20 +76,20 @@
78 76 private $id;
79 77
80 78 // Information to import from WXR file.
81 79 private $version;
82 - private $authors = [];
83 - public $posts = [];
84 - public $terms = [];
85 - private $base_url = '';
80 + private $authors = [];
81 + public $posts = [];
82 + public $terms = [];
83 + private $base_url = '';
86 84 private $page_on_front;
87 85 private $base_blog_url = '';
88 86
89 87 // Mappings from old information to new.
90 - public $file_type;
91 - public $processed_taxonomies;
92 - public $processed_terms = [];
93 - public $processed_posts = [];
88 + public $file_type;
89 + public $processed_taxonomies;
90 + public $processed_terms = [];
91 + public $processed_posts = [];
94 92 private $processed_authors = [];
95 93 private $author_mapping = [];
96 94 private $processed_menu_items = [];
97 95 private $post_orphans = [];
@@ -174,10 +172,10 @@
174 172
175 173 $filename = trim( $attributes['filename'] );
176 174
177 175 // Unquote quoted filename, but after trimming.
178 - if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) {
179 - $filename = substr( $filename, 1, -1 );
176 + if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, - 1, 1 ) === '"' ) {
177 + $filename = substr( $filename, 1, - 1 );
180 178 }
181 179 }
182 180
183 181 return $filename;
@@ -207,31 +205,32 @@
207 205
208 206 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
209 207 }
210 208
211 - /**
212 - * Modify WordPress Post Type to 'docs'
213 - *
214 - * This function is designed to modify the 'post_type' value of a WordPress post array.
215 - * If the existing 'post_type' is not equal to 'docs', it will be replaced with 'docs'.
216 - * Additionally, any 'terms' associated with the post will be removed.
217 - *
218 - * @param array $post An array representing a WordPress post.
219 - *
220 - * @return array The modified WordPress post array with 'post_type' set to 'docs'.
221 - */
222 - private function modify_post_type( $post ) {
223 - // Check if [post_type] is not equal to 'docs'
224 - if ( isset( $post['post_type'] ) && $post['post_type'] !== 'docs' && $post['post_type'] !== 'attachment' && $post['post_type'] != 'betterdocs_faq' ) {
225 - // Remove [terms] if [post_type] is not equal to 'docs'
226 - unset( $post['terms'] );
227 - // Replace [post_type] with 'docs'
228 - $post['post_type'] = 'docs';
229 - }
209 + /**
210 + * Modify WordPress Post Type to 'docs'
211 + *
212 + * This function is designed to modify the 'post_type' value of a WordPress post array.
213 + * If the existing 'post_type' is not equal to 'docs', it will be replaced with 'docs'.
214 + * Additionally, any 'terms' associated with the post will be removed.
215 + *
216 + * @param array $post An array representing a WordPress post.
217 + *
218 + * @return array The modified WordPress post array with 'post_type' set to 'docs'.
219 + */
220 + private function modify_post_type($post) {
221 + // Check if [post_type] is not equal to 'docs'
222 + if (isset($post['post_type']) && $post['post_type'] !== 'docs' && $post['post_type'] !== 'attachment') {
223 + // Remove [terms] if [post_type] is not equal to 'docs'
224 + unset($post['terms']);
225 + // Replace [post_type] with 'docs'
226 + $post['post_type'] = 'docs';
227 + }
230 228
231 - return $post;
232 - }
229 + return $post;
230 + }
233 231
232 +
234 233 /**
235 234 * The main controller for the actual import stage.
236 235 *
237 236 * @param string $file Path to the WXR file for importing
@@ -236,31 +235,26 @@
236 235 *
237 236 * @param string $file Path to the WXR file for importing
238 237 */
239 238 private function import( $file ) {
240 - add_filter(
241 - 'import_post_meta_key',
242 - function ( $key ) {
243 - return $this->is_valid_meta_key( $key );
244 - }
245 - );
246 - add_filter(
247 - 'http_request_timeout',
248 - function () {
249 - return self::DEFAULT_BUMP_REQUEST_TIMEOUT;
250 - }
251 - );
239 + add_filter( 'import_post_meta_key', function ( $key ) {
240 + return $this->is_valid_meta_key( $key );
241 + } );
242 + add_filter( 'http_request_timeout', function () {
243 + return self::DEFAULT_BUMP_REQUEST_TIMEOUT;
244 + } );
252 245
253 246 if ( ! $this->import_start( $file ) ) {
254 247 return;
255 248 }
256 249
250 +
257 251 $this->set_author_mapping();
258 252
259 253 wp_suspend_cache_invalidation( true );
260 254 $imported_summary = [
261 255 'terms' => $this->process_terms(),
262 - 'posts' => $this->process_posts()
256 + 'posts' => $this->process_posts(),
263 257 ];
264 258 wp_suspend_cache_invalidation( false );
265 259
266 260 // Update incorrect/missing information in the DB.
@@ -295,16 +289,17 @@
295 289
296 290 return false;
297 291 }
298 292
299 - $action = $this->args['action'];
293 + $action = $this->args['action'];
300 294
301 295 $import_data = $this->parse( $file );
302 296
303 - // Check for a parser error first: on an invalid/unsupported file the
304 - // parser returns a WP_Error, and reading $import_data['type'] on it
305 - // would fatal ("Cannot use object of type WP_Error as array"). Report
306 - // it cleanly instead.
297 + if ( isset( $import_data['type'] ) && $import_data['type'] === 'sample/csv' ) {
298 + $this->import_sample_data( $import_data['posts'], $action );
299 + return true;
300 + }
301 +
307 302 if ( is_wp_error( $import_data ) ) {
308 303 /**
309 304 * @var WP_Error $import_data ;
310 305 */
@@ -312,76 +307,61 @@
312 307
313 308 return false;
314 309 }
315 310
316 - if ( isset( $import_data['type'] ) && $import_data['type'] === 'sample/csv' ) {
317 - $this->import_sample_data( $import_data['posts'], $action );
318 - return true;
319 - }
311 + $posts = $import_data['posts'];
312 + // Use array_map to apply the callback function to each item in the array
313 + $posts = array_map(array($this, 'modify_post_type'), $posts);
320 314
321 - $posts = $import_data['posts'];
322 - // Use array_map to apply the callback function to each item in the array
323 - $posts = array_map( [ $this, 'modify_post_type' ], $posts );
315 + if ( ! empty( $action ) ) {
316 + $existing_posts = $this->args['existing_slug'];
317 + $existing_posts_array = explode(',', $existing_posts);
324 318
325 - if ( ! empty( $action ) ) {
326 - $existing_posts = $this->args['existing_slug'];
327 - $existing_posts_array = is_array( $existing_posts ) ? $existing_posts : explode( ',', (string) $existing_posts );
319 + if ( $existing_posts && $action == 'ignore' ) {
320 + // Filter out posts with slugs in $existing_slugs_array
321 + $filtered_posts = array_filter($posts, function ($post) use ($existing_posts_array) {
322 + return !in_array($post['post_name'], $existing_posts_array);
323 + });
328 324
329 - if ( $existing_posts && $action == 'ignore' ) {
330 - // Filter out posts with slugs in $existing_slugs_array
331 - $filtered_posts = array_filter(
332 - $posts,
333 - function ( $post ) use ( $existing_posts_array ) {
334 - return ! in_array( $post['post_name'], $existing_posts_array );
335 - }
336 - );
325 + // If you want the resulting array to have numeric keys
326 + $posts = array_values($filtered_posts);
337 327
338 - // If you want the resulting array to have numeric keys
339 - $posts = array_values( $filtered_posts );
340 - }if ( $existing_posts_array && $action == 'replace' ) {
341 - foreach ( $existing_posts_array as $slug ) {
342 - $post = get_page_by_path( $slug, OBJECT, 'docs' ); // Replace 'docs' with your custom post type if needed
328 + } if ( $existing_posts_array && $action == 'replace' ) {
329 + foreach ($existing_posts_array as $slug) {
330 + $post = get_page_by_path($slug, OBJECT, 'docs'); // Replace 'docs' with your custom post type if needed
343 331
344 - if ( $post ) {
345 - $post_id = $post->ID;
346 - // Permanently delete the post (including from trash)
347 - wp_delete_post( $post_id, true );
348 - }
349 - }
350 - }
351 - }
332 + if ($post) {
333 + $post_id = $post->ID;
334 + // Permanently delete the post (including from trash)
335 + wp_delete_post($post_id, true);
336 + }
337 + }
338 + }
339 + }
352 340
353 - if ( isset( $import_data['version'] ) ) {
354 - $this->version = $import_data['version'];
355 - }
341 + if ( isset($import_data['version']) ) {
342 + $this->version = $import_data['version'];
343 + }
356 344
357 345 $this->set_authors_from_import( $import_data );
358 - $this->posts = $posts;
359 - $this->terms = $import_data['terms'];
346 + $this->posts = $posts;
347 + $this->terms = $import_data['terms'];
360 348
361 - if ( isset( $import_data['base_url'] ) ) {
362 - $this->base_url = esc_url( $import_data['base_url'] );
363 - }
349 + if ( isset($import_data['base_url']) ) {
350 + $this->base_url = esc_url( $import_data['base_url'] );
351 + }
364 352
365 - if ( isset( $import_data['base_blog_url'] ) ) {
366 - $this->base_blog_url = esc_url( $import_data['base_blog_url'] );
367 - }
353 + if ( isset($import_data['base_blog_url']) ) {
354 + $this->base_blog_url = esc_url( $import_data['base_blog_url'] );
355 + }
368 356
369 - if ( isset( $import_data['page_on_front'] ) ) {
370 - $this->page_on_front = $import_data['page_on_front'];
371 - }
357 + if ( isset($import_data['page_on_front']) ) {
358 + $this->page_on_front = $import_data['page_on_front'];
359 + }
372 360
373 361 wp_defer_term_counting( true );
374 362 wp_defer_comment_counting( true );
375 363
376 - // Safety net for an aborted run: a PHP timeout, memory limit, fatal, or a
377 - // cut-off request can end the import before import_end() runs, which would
378 - // strand wp_defer_term_counting(true) and a stale {taxonomy}_children cache
379 - // and silently drop nested-category counts site-wide. A shutdown handler
380 - // guarantees the restore runs on the way out; it is idempotent, so the normal
381 - // import_end() path (which also calls it) makes this a no-op. (#167)
382 - register_shutdown_function( array( $this, 'restore_counting_and_hierarchy' ) );
383 -
384 364 do_action( 'import_start', $this );
385 365
386 366 return true;
387 367 }
@@ -393,29 +373,8 @@
393 373 wp_import_cleanup( $this->id );
394 374
395 375 wp_cache_flush();
396 376
397 - $this->restore_counting_and_hierarchy();
398 -
399 - do_action( 'import_end' );
400 - }
401 -
402 - /**
403 - * Turn term/comment counting back on and rebuild the term hierarchy.
404 - *
405 - * import_start() registers this as a shutdown handler so an aborted run (timeout,
406 - * memory, fatal, cut-off request) that never reaches import_end() cannot strand
407 - * wp_defer_term_counting(true) or a stale {taxonomy}_children cache. Idempotent,
408 - * so it is safe to call from both import_end() and the shutdown handler. (#167)
409 - */
410 - public function restore_counting_and_hierarchy() {
411 - static $restored = false;
412 -
413 - if ( $restored ) {
414 - return;
415 - }
416 - $restored = true;
417 -
418 377 foreach ( get_taxonomies() as $tax ) {
419 378 delete_option( "{$tax}_children" );
420 379 _get_term_hierarchy( $tax );
421 380 }
@@ -421,8 +380,10 @@
421 380 }
422 381
423 382 wp_defer_term_counting( false );
424 383 wp_defer_comment_counting( false );
384 +
385 + do_action( 'import_end' );
425 386 }
426 387
427 388 /**
428 389 * Retrieve authors from parsed WXR data and set it to `$this->>authors`.
@@ -448,9 +409,9 @@
448 409
449 410 if ( ! isset( $this->authors[ $login ] ) ) {
450 411 $this->authors[ $login ] = [
451 412 'author_login' => $login,
452 - 'author_display_name' => $post['post_author']
413 + 'author_display_name' => $post['post_author'],
453 414 ];
454 415 }
455 416 }
456 417 }
@@ -491,9 +452,9 @@
491 452 'user_pass' => wp_generate_password(),
492 453 'user_email' => isset( $this->authors[ $old_login ]['author_email'] ) ? $this->authors[ $old_login ]['author_email'] : '',
493 454 'display_name' => $this->authors[ $old_login ]['author_display_name'],
494 455 'first_name' => isset( $this->authors[ $old_login ]['author_first_name'] ) ? $this->authors[ $old_login ]['author_first_name'] : '',
495 - 'last_name' => isset( $this->authors[ $old_login ]['author_last_name'] ) ? $this->authors[ $old_login ]['author_last_name'] : ''
456 + 'last_name' => isset( $this->authors[ $old_login ]['author_last_name'] ) ? $this->authors[ $old_login ]['author_last_name'] : '',
496 457 ];
497 458 $user_id = wp_insert_user( $user_data );
498 459 }
499 460
@@ -533,9 +494,9 @@
533 494 */
534 495 private function process_terms(): array {
535 496 $result = [
536 497 'succeed' => [],
537 - 'failed' => []
498 + 'failed' => [],
538 499 ];
539 500
540 501 $this->terms = apply_filters( 'wp_import_terms', $this->terms );
541 502
@@ -587,9 +548,9 @@
587 548 $description = $term['term_description'] ?? '';
588 549 $args = [
589 550 'slug' => $term['slug'],
590 551 'description' => wp_slash( $description ),
591 - 'parent' => (int) $parent
552 + 'parent' => (int) $parent,
592 553 ];
593 554
594 555 $id = wp_insert_term( wp_slash( $term['term_name'] ), $term['term_taxonomy'], $args );
595 556
@@ -663,9 +624,9 @@
663 624 continue;
664 625 }
665 626
666 627 // Export gets meta straight from the DB so could have a serialized string
667 - $value = maybe_unserialize( $meta['value'] );
628 + $value = maybe_unserialize( $meta['value'] );
668 629 $insert_meta = add_term_meta( $term_id, wp_slash( $key ), wp_slash_strings_only( $value ) );
669 630 /**
670 631 * Fires after term meta is imported.
671 632 *
@@ -676,332 +637,180 @@
676 637 do_action( 'import_term_meta', $term_id, $key, $value );
677 638 }
678 639 }
679 640
680 - public function existing_slug_action( $posts, $action ) {
681 - $existing_posts = $this->args['existing_slug'];
682 - $existing_posts_array = is_array( $existing_posts ) ? $existing_posts : explode( ',', (string) $existing_posts );
641 + public function existing_slug_action ($posts, $action) {
642 + $existing_posts = $this->args['existing_slug'];
643 + $existing_posts_array = explode(',', $existing_posts);
683 644
684 - if ( $existing_posts && $action == 'ignore' ) {
685 - // Filter out posts with slugs in $existing_slugs_array
686 - $filtered_posts = array_filter(
687 - $posts,
688 - function ( $post ) use ( $existing_posts_array ) {
689 - return ! in_array( $post['post_name'], $existing_posts_array );
690 - }
691 - );
645 + if ( $existing_posts && $action == 'ignore' ) {
646 + // Filter out posts with slugs in $existing_slugs_array
647 + $filtered_posts = array_filter($posts, function ($post) use ($existing_posts_array) {
648 + return !in_array($post['post_name'], $existing_posts_array);
649 + });
692 650
693 - // If you want the resulting array to have numeric keys
694 - $posts = array_values( $filtered_posts );
695 - }if ( $existing_posts_array && $action == 'replace' ) {
696 - foreach ( $existing_posts_array as $slug ) {
697 - $post = get_page_by_path( $slug, OBJECT, 'docs' ); // Replace 'docs' with your custom post type if needed
651 + // If you want the resulting array to have numeric keys
652 + $posts = array_values($filtered_posts);
698 653
699 - if ( $post ) {
700 - $post_id = $post->ID;
701 - // Permanently delete the post (including from trash)
702 - wp_delete_post( $post_id, true );
703 - }
704 - }
705 - }
654 + } if ( $existing_posts_array && $action == 'replace' ) {
655 + foreach ($existing_posts_array as $slug) {
656 + $post = get_page_by_path($slug, OBJECT, 'docs'); // Replace 'docs' with your custom post type if needed
706 657
707 - return $posts;
708 - }
658 + if ($post) {
659 + $post_id = $post->ID;
660 + // Permanently delete the post (including from trash)
661 + wp_delete_post($post_id, true);
662 + }
663 + }
664 + }
709 665
710 - /**
711 - * Fix encoding issues in CSV data
712 - *
713 - * @param string $text
714 - * @return string
715 - */
716 - private function fix_encoding_issues( $text ) {
717 - if ( empty( $text ) ) {
718 - return $text;
719 - }
666 + return $posts;
667 + }
720 668
721 - // Convert to UTF-8 if not already
722 - if ( ! mb_check_encoding( $text, 'UTF-8' ) ) {
723 - $detected_encoding = mb_detect_encoding( $text, ['UTF-8', 'ISO-8859-1', 'Windows-1252', 'ASCII'], true );
724 - if ( $detected_encoding ) {
725 - $text = mb_convert_encoding( $text, 'UTF-8', $detected_encoding );
726 - } else {
727 - // Fallback: assume ISO-8859-1 if detection fails
728 - $text = mb_convert_encoding( $text, 'UTF-8', 'ISO-8859-1' );
729 - }
730 - }
731 -
732 - // Fix common encoding issues
733 - $replacements = [
734 - // Smart quotes (using Unicode escape sequences)
735 - "\u{2019}" => "'", // Right single quotation mark
736 - "\u{2018}" => "'", // Left single quotation mark
737 - "\u{201C}" => '"', // Left double quotation mark
738 - "\u{201D}" => '"', // Right double quotation mark
739 - "\u{2013}" => '-', // En dash
740 - "\u{2014}" => '--', // Em dash
741 - "\u{2026}" => '...', // Horizontal ellipsis
742 - // HTML entities that might be double-encoded
743 - '&amp;' => '&',
744 - '&lt;' => '<',
745 - '&gt;' => '>',
746 - '&quot;' => '"',
747 - // Remove replacement characters
748 - "\u{FFFD}" => '', // Replacement character
749 - ];
750 -
751 - $text = str_replace( array_keys( $replacements ), array_values( $replacements ), $text );
752 -
753 - // Remove any remaining non-printable characters except newlines and tabs
754 - $text = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $text );
755 -
756 - return $text;
757 - }
758 -
759 - private function import_sample_data( $posts, $action ) {
760 - $result = [
669 + private function import_sample_data( $posts, $action ) {
670 + $result = [
761 671 'succeed' => [],
762 - 'failed' => []
672 + 'failed' => [],
763 673 ];
764 674
765 - if ( ! empty( $action ) ) {
766 - $posts = $this->existing_slug_action( $posts, $action );
767 - }
675 + if ( ! empty( $action ) ) {
676 + $posts = $this->existing_slug_action($posts, $action);
677 + }
768 678
769 - foreach ( $posts as $post ) {
770 - $post_title = isset( $post['Docs Title'] ) ? $post['Docs Title'] : '';
771 - $post_name = isset( $post['post_name'] ) ? $post['post_name'] : '';
772 - $post_content = isset( $post['Docs Content'] ) ? $post['Docs Content'] : '';
773 - $featured_image_url = isset( $post['Featured Image'] ) ? $post['Featured Image'] : '';
774 - $category_name = isset( $post['Category Name'] ) ? $post['Category Name'] : '';
775 - $category_slug = isset( $post['Category Slug'] ) ? $post['Category Slug'] : $category_name;
776 - $knowledge_base_name = isset( $post['Knowledge Base'] ) ? $post['Knowledge Base'] : '';
777 - $knowledge_base_slug = isset( $post['Knowledge Base Slug'] ) ? $post['Knowledge Base Slug'] : $knowledge_base_name;
778 - $post_status = isset( $post['Status'] ) ? $post['Status'] : '';
679 + foreach ( $posts as $post ) {
680 + $post_title = isset($post['Docs Title']) ? $post['Docs Title'] : '';
681 + $post_name = isset($post['post_name']) ? $post['post_name'] : '';
682 + $post_content = isset($post['Docs Content']) ? $post['Docs Content'] : '';
683 + $featured_image_url = isset($post['Featured Image']) ? $post['Featured Image'] : '';
684 + $category_name = isset($post['Category Name']) ? $post['Category Name'] : '';
685 + $category_slug = isset($post['Category Slug']) ? $post['Category Slug'] : '';
686 + $knowledge_base_name = isset($post['Knowledge Base']) ? $post['Knowledge Base'] : '';
687 + $knowledge_base_slug = isset($post['Knowledge Base Slug']) ? $post['Knowledge Base Slug'] : '';
688 + $post_status = isset($post['Status']) ? $post['Status'] : '';
779 689
780 - // Check if the category and knowledge base term exist, if not, create them
781 - $default_multiple_kb = betterdocs()->settings->get( 'multiple_kb' );
690 + // Check if the category and knowledge base term exist, if not, create them
691 + $default_multiple_kb = betterdocs()->settings->get( 'multiple_kb' );
782 692
783 - $knowledge_base_id = 0;
784 - if ( $default_multiple_kb == 1 && $knowledge_base_slug ) {
785 - $existing_kb = term_exists( $knowledge_base_name, 'knowledge_base' );
786 - if ( ! $existing_kb && $knowledge_base_name ) {
787 - $kb_term = wp_insert_term( $knowledge_base_name, 'knowledge_base', [ 'slug' => $knowledge_base_slug ] );
788 - if ( is_wp_error( $kb_term ) ) {
789 - $knowledge_base_id = 0;
790 - } else {
791 - $knowledge_base_id = $kb_term['term_id'];
792 - }
793 - } elseif ( $existing_kb ) {
794 - $knowledge_base_id = $existing_kb['term_id'];
795 - }
796 - }
693 + if ( $default_multiple_kb == 1 ) {
694 + $knowledge_base_id = term_exists($knowledge_base_name, 'knowledge_base');
695 + if (!$knowledge_base_id && $knowledge_base_name) {
696 + $knowledge_base_id = wp_insert_term($knowledge_base_name, 'knowledge_base', array('slug' => $knowledge_base_slug));
697 + $knowledge_base_id = $knowledge_base_id['term_id'];
698 + } elseif ( ! $knowledge_base_name ) {
699 + // If no knowledge base term provided, set it to 0
700 + $knowledge_base_id = 0;
701 + } else {
702 + $knowledge_base_id = $knowledge_base_id['term_id'];
703 + }
704 + }
797 705
798 - $category_id = 0;
799 - if ( $category_slug ) {
800 - $existing_category = term_exists( $category_name, 'doc_category' );
801 - if ( ! $existing_category ) {
802 - $category_term = wp_insert_term( $category_name, 'doc_category', [ 'slug' => $category_slug ] );
803 - if ( is_wp_error( $category_term ) ) {
804 - $category_id = 0;
805 - } else {
806 - $category_id = $category_term['term_id'];
807 - }
808 - } else {
809 - $category_id = $existing_category['term_id'];
810 - }
706 + $category_id = term_exists( $category_name, 'doc_category' );
707 + if ( ! $category_id ) {
708 + $category_id = wp_insert_term($category_name, 'doc_category', array('slug' => $category_slug));
709 + $category_id = $category_id['term_id'];
710 + } else {
711 + $category_id = $category_id['term_id'];
712 + }
811 713
812 - if ( $default_multiple_kb == 1 && $knowledge_base_id && $category_id ) {
813 - $kb_slug = get_term_field( 'slug', $knowledge_base_id, 'knowledge_base' );
814 - if ( ! is_wp_error( $kb_slug ) ) {
815 - $doc_category_kb = rest_sanitize_array( [ $kb_slug ] );
816 - update_term_meta( $category_id, 'doc_category_knowledge_base', $doc_category_kb );
817 - }
818 - }
819 - }
714 + if ( $default_multiple_kb == 1 && $knowledge_base_id && $category_id ) {
715 + $kb_slug = get_term_field('slug', $knowledge_base_id, 'knowledge_base');
716 + if ( ! is_wp_error( $kb_slug ) ) {
717 + $doc_category_kb = rest_sanitize_array( [$kb_slug] );
718 + update_term_meta( $category_id, "doc_category_knowledge_base", $doc_category_kb );
719 + }
720 + }
820 721
821 - // Validate and sanitize post data with encoding fixes
822 - $post_title = $this->fix_encoding_issues( trim( $post_title ) );
823 - $post_content = $this->fix_encoding_issues( trim( $post_content ) );
824 - $post_name = $this->fix_encoding_issues( trim( $post_name ) );
825 - $post_status = !empty( $post_status ) ? $post_status : 'publish';
722 + // Set up the post data
723 + $post_data = [
724 + 'post_title' => $post_title,
725 + 'post_name' => $post_name,
726 + 'post_content' => $post_content,
727 + 'post_status' => $post_status,
728 + 'post_type' => 'docs',
729 + 'tax_input' => array(
730 + 'doc_category' => array( $category_id )
731 + ),
732 + ];
826 733
827 - // Skip posts with empty titles
828 - if ( empty( $post_title ) ) {
829 - $result['failed'][] = [
830 - 'title' => 'Empty Title',
831 - 'error' => 'Post title is required'
832 - ];
833 - continue;
834 - }
734 + if ( $default_multiple_kb == 1 ) {
735 + $post_data['tax_input']['knowledge_base'] = array( $knowledge_base_id );
736 + }
835 737
836 - // Additional WordPress validation checks
837 - $validation_errors = [];
738 + // Insert the post
739 + $post_id = wp_insert_post( $post_data );
838 740
839 - // Check title length (WordPress has a 200 character limit for post_title)
840 - if ( strlen( $post_title ) > 200 ) {
841 - $validation_errors[] = 'Title too long (max 200 characters)';
842 - }
741 + // Set the featured image
742 + if ( $featured_image_url ) {
743 + $this->set_post_thumbnail( $post_id, $featured_image_url );
744 + }
745 + }
843 746
844 - // Check for valid post status
845 - $valid_statuses = get_post_stati();
846 - if ( !in_array( $post_status, array_keys( $valid_statuses ) ) ) {
847 - $post_status = 'publish';
848 - }
747 + return $result;
748 + }
849 749
850 - // Check if post with same title already exists
851 - $existing_posts = get_posts([
852 - 'post_type' => 'docs',
853 - 'title' => $post_title,
854 - 'post_status' => 'any',
855 - 'numberposts' => 1
856 - ]);
857 - if ( !empty( $existing_posts ) ) {
858 - $validation_errors[] = 'Post with same title already exists (ID: ' . $existing_posts[0]->ID . ')';
859 - }
860 -
861 - // Check if slug already exists (if provided)
862 - if ( !empty( $post_name ) ) {
863 - $existing_by_slug = get_posts([
864 - 'post_type' => 'docs',
865 - 'name' => $post_name,
866 - 'post_status' => 'any',
867 - 'numberposts' => 1
868 - ]);
869 - }
870 -
871 - if ( !empty( $validation_errors ) ) {
872 - $result['failed'][] = [
873 - 'title' => $post_title,
874 - 'error' => implode(', ', $validation_errors)
875 - ];
876 - continue;
877 - }
878 -
879 - // Set up the post data
880 - $post_data = [
881 - 'post_title' => $post_title,
882 - 'post_name' => $post_name,
883 - 'post_content' => $post_content,
884 - 'post_status' => $post_status,
885 - 'post_type' => 'docs'
886 - ];
887 -
888 - if ( $category_name && $category_id ) {
889 - $post_data['tax_input']['doc_category'] = [ $category_id ];
890 - }
891 -
892 - if ( $default_multiple_kb == 1 && $knowledge_base_id ) {
893 - $post_data['tax_input']['knowledge_base'] = [ $knowledge_base_id ];
894 - }
895 -
896 - // Insert the post (without knowledge_base taxonomy to avoid capability issues)
897 - $post_data_without_kb = $post_data;
898 - if (isset($post_data_without_kb['tax_input']['knowledge_base'])) {
899 - $kb_terms = $post_data_without_kb['tax_input']['knowledge_base'];
900 - unset($post_data_without_kb['tax_input']['knowledge_base']);
901 - }
902 -
903 - $post_id = wp_insert_post( $post_data_without_kb );
904 -
905 - // Manually assign knowledge_base terms after post creation to bypass capability check
906 - if ( !is_wp_error($post_id) && $post_id > 0 && isset($kb_terms) && !empty($kb_terms) ) {
907 - // Convert term IDs to integers and ensure they exist
908 - $term_ids = array_map('intval', $kb_terms);
909 -
910 - $result_kb = wp_set_object_terms( $post_id, $term_ids, 'knowledge_base' );
911 - if ( !is_wp_error($result_kb) ) {
912 - $assigned_terms = wp_get_object_terms($post_id, 'knowledge_base');
913 - if (!empty($assigned_terms)) {
914 - $term_names = array_map(function($term) { return $term->name; }, $assigned_terms);
915 - }
916 - }
917 - }
918 -
919 - // Enhanced debugging for failed insertions
920 - if ( is_wp_error( $post_id ) ) {
921 - $result['failed'][] = [
922 - 'title' => $post_title,
923 - 'error' => $post_id->get_error_message()
924 - ];
925 - continue;
926 - } elseif ( $post_id === 0 ) {
927 - $result['failed'][] = [
928 - 'title' => $post_title,
929 - 'error' => 'Post insertion returned 0 - validation failed'
930 - ];
931 - continue;
932 - } else {
933 - $result['succeed'][] = [
934 - 'title' => $post_title,
935 - 'id' => $post_id
936 - ];
937 - }
938 - // Set the featured image
939 - if ( $featured_image_url ) {
940 - $this->set_post_thumbnail( $post_id, $featured_image_url );
941 - }
942 - }
943 -
944 - return $result;
945 - }
946 -
947 - public function import_helpscout_data( $posts ) {
948 - $result = [
750 + public function import_helpscout_data( $posts ) {
751 + $result = [
949 752 'succeed' => [],
950 - 'failed' => []
753 + 'failed' => [],
951 754 ];
952 755
953 - foreach ( $posts as $post ) {
954 - $post_title = isset( $post['name'] ) ? $post['name'] : '';
955 - $post_slug = isset( $post['slug'] ) ? $post['slug'] : '';
956 - $post_content = isset( $post['text'] ) ? $post['text'] : '';
957 - $categories = isset( $post['categories'] ) ? $post['categories'] : '';
958 - $post_status = isset( $post['status'] ) ? $post['status'] : '';
756 + foreach ( $posts as $post ) {
757 + error_log(print_r($post , 1));
758 + $post_title = isset($post['name']) ? $post['name'] : '';
759 + $post_slug = isset($post['slug']) ? $post['slug'] : '';
760 + $post_content = isset($post['text']) ? $post['text'] : '';
761 + $categories = isset($post['categories']) ? $post['categories'] : '';
762 + $post_status = isset($post['status']) ? $post['status'] : '';
959 763
960 - // Check if the category and knowledge base term exist, if not, create them
961 - $category_ids = [];
962 - foreach ( $categories as $category ) {
963 - $category_id = term_exists( $category['slug'], 'doc_category' );
964 - if ( ! $category_id ) {
965 - $category_id = wp_insert_term( $category['name'], 'doc_category', [ 'slug' => $category['slug'] ] );
966 - $category_ids[] = $category_id['term_id'];
967 - } else {
968 - $category_ids[] = $category_id['term_id'];
969 - }
970 - }
764 + // Check if the category and knowledge base term exist, if not, create them
765 + $category_ids = [];
766 + foreach ( $categories as $category ) {
767 + $category_id = term_exists($category['name'], 'doc_category');
768 + if (!$category_id) {
769 + $category_id = wp_insert_term($category['name'], 'doc_category', array('slug' => $category['slug']));
770 + $category_ids[] = $category_id['term_id'];
771 + } else {
772 + $category_ids[] = $category_id['term_id'];
773 + }
774 + }
971 775
972 - // Set up the post data
973 - $post_data = [
974 - 'post_title' => $post_title,
975 - 'post_name' => $post_slug,
976 - 'post_content' => $post_content,
977 - 'post_status' => 'publish',
978 - 'post_type' => 'docs',
979 - 'tax_input' => [
980 - 'doc_category' => $category_ids
981 - ]
982 - ];
776 + // Set up the post data
777 + $post_data = array(
778 + 'post_title' => $post_title,
779 + 'post_name' => $post_slug,
780 + 'post_content' => $post_content,
781 + 'post_status' => 'publish',
782 + 'post_type' => 'docs',
783 + 'tax_input' => array(
784 + 'doc_category' => $category_ids,
785 + ),
786 + );
983 787
984 - // Insert the post
985 - wp_insert_post( $post_data );
986 - }
788 + // Insert the post
789 + $post_id = wp_insert_post($post_data);
987 790
988 - return $result;
989 - }
791 + // Set the featured image
792 + // if ($featured_image_url) {
793 + // $this->set_post_thumbnail( $post_id, $featured_image_url );
794 + // }
795 + }
990 796
991 - public function set_post_thumbnail( $post_id, $attachment_url ) {
992 - require_once ABSPATH . 'wp-admin/includes/image.php';
993 - require_once ABSPATH . 'wp-admin/includes/media.php';
994 - require_once ABSPATH . 'wp-admin/includes/file.php';
995 - $post_title = pathinfo( $attachment_url, PATHINFO_FILENAME );
996 - $post_name = pathinfo( $attachment_url, PATHINFO_FILENAME );
797 + return $result;
798 + }
997 799
998 - $image_id = media_sideload_image( $attachment_url, $post_id, $post_title, $post_name );
999 - set_post_thumbnail( $post_id, $image_id );
800 + public function set_post_thumbnail( $post_id, $attachment_url ) {
801 + require_once ABSPATH . 'wp-admin/includes/image.php';
802 + require_once ABSPATH . 'wp-admin/includes/media.php';
803 + require_once ABSPATH . 'wp-admin/includes/file.php';
804 + $post_title = pathinfo( $attachment_url, PATHINFO_FILENAME );
805 + $post_name = pathinfo( $attachment_url, PATHINFO_FILENAME );
1000 806
1001 - return $image_id;
1002 - }
807 + $image_id = media_sideload_image( $attachment_url, $post_id, $post_title, $post_name );
808 + set_post_thumbnail($post_id, $image_id);
1003 809
810 + return $image_id;
811 + }
812 +
1004 813 /**
1005 814 * Create new posts based on import information
1006 815 *
1007 816 * Posts marked as having a parent which doesn't exist will become top level items.
@@ -1013,9 +822,9 @@
1013 822 */
1014 823 private function process_posts(): array {
1015 824 $result = [
1016 825 'succeed' => [],
1017 - 'failed' => []
826 + 'failed' => [],
1018 827 ];
1019 828
1020 829 $this->posts = apply_filters( 'wp_import_posts', $this->posts );
1021 830
@@ -1036,8 +845,20 @@
1036 845 if ( 'auto-draft' === $post['status'] ) {
1037 846 continue;
1038 847 }
1039 848
849 + if ( 'nav_menu_item' === $post['post_type'] ) {
850 + $result['succeed'] += $this->process_menu_item( $post );
851 + continue;
852 + }
853 +
854 + if ( 'wp_navigation' === $post['post_type'] ) {
855 + $processed = $this->process_navigation( $post );
856 + if( ! $processed ) {
857 + continue;
858 + }
859 + }
860 +
1040 861 $post_type_object = get_post_type_object( $post['post_type'] );
1041 862
1042 863 $post_parent = (int) $post['post_parent'];
1043 864 if ( $post_parent ) {
@@ -1060,25 +881,25 @@
1060 881 }
1061 882
1062 883 $postdata = [
1063 884 'post_author' => $author,
1064 - 'post_content' => isset( $post['post_content'] ) ? $this->fix_encoding_issues( $post['post_content'] ) : '',
1065 - 'post_excerpt' => isset( $post['post_excerpt'] ) ? $this->fix_encoding_issues( $post['post_excerpt'] ) : '',
1066 - 'post_title' => isset( $post['post_title'] ) ? $this->fix_encoding_issues( $post['post_title'] ) : '',
1067 - 'post_status' => isset( $post['status'] ) ? $post['status'] : '',
1068 - 'post_name' => isset( $post['post_name'] ) ? $this->fix_encoding_issues( $post['post_name'] ) : '',
1069 - 'comment_status' => isset( $post['comment_status'] ) ? $post['comment_status'] : '',
1070 - 'ping_status' => isset( $post['ping_status'] ) ? $post['ping_status'] : '',
1071 - 'guid' => isset( $post['guid'] ) ? $this->fix_encoding_issues( $post['guid'] ) : '',
885 + 'post_content' => isset($post['post_content']) ? $post['post_content'] : '',
886 + 'post_excerpt' => isset($post['post_excerpt']) ? $post['post_excerpt'] : '',
887 + 'post_title' => isset($post['post_title']) ? $post['post_title'] : '',
888 + 'post_status' => isset($post['status']) ? $post['status'] : '',
889 + 'post_name' => isset($post['post_name']) ? $post['post_name'] : '',
890 + 'comment_status' => isset($post['comment_status']) ? $post['comment_status'] : '',
891 + 'ping_status' => isset($post['ping_status']) ? $post['ping_status'] : '',
892 + 'guid' => isset($post['guid']) ? $post['guid'] : '',
1072 893 'post_parent' => $post_parent,
1073 - 'menu_order' => isset( $post['menu_order'] ) ? $post['menu_order'] : '',
1074 - 'post_type' => isset( $post['post_type'] ) ? $post['post_type'] : '',
1075 - 'post_password' => isset( $post['post_password'] ) ? $this->fix_encoding_issues( $post['post_password'] ) : ''
894 + 'menu_order' => isset($post['menu_order']) ? $post['menu_order'] : '',
895 + 'post_type' => isset($post['post_type']) ? $post['post_type'] : '',
896 + 'post_password' => isset($post['post_password']) ? $post['post_password'] : '',
1076 897 ];
1077 898
1078 899 $original_post_id = $post['post_id'];
1079 900 $postdata = apply_filters( 'wp_import_post_data_processed', $postdata, $post );
1080 - $postdata = wp_slash( $postdata );
901 + $postdata = wp_slash( $postdata );
1081 902
1082 903 if ( 'attachment' === $postdata['post_type'] ) {
1083 904 $remote_url = ! empty( $post['attachment_url'] ) ? $post['attachment_url'] : $post['guid'];
1084 905
@@ -1083,9 +904,9 @@
1083 904 $remote_url = ! empty( $post['attachment_url'] ) ? $post['attachment_url'] : $post['guid'];
1084 905
1085 906 // try to use _wp_attached file for upload folder placement to ensure the same location as the export site
1086 907 // e.g. location is 2003/05/image.jpg but the attachment post_date is 2010/09, see media_handle_upload()
1087 - $postdata['upload_date'] = isset( $post['post_date'] ) ? $post['post_date'] : '';
908 + $postdata['upload_date'] = isset($post['post_date']) ? $post['post_date'] : '';
1088 909 if ( isset( $post['postmeta'] ) ) {
1089 910 foreach ( $post['postmeta'] as $meta ) {
1090 911 if ( '_wp_attached_file' === $meta['key'] ) {
1091 912 if ( preg_match( '%^[0-9]{4}/[0-9]{2}%', $meta['value'], $matches ) ) {
@@ -1095,10 +916,10 @@
1095 916 }
1096 917 }
1097 918 }
1098 919
1099 - // $post_id = $this->set_post_thumbnail( $postdata['post_parent'], $remote_url );
1100 - $post_id = $this->process_attachment( $postdata, $remote_url );
920 + // $post_id = $this->set_post_thumbnail( $postdata['post_parent'], $remote_url );
921 + $post_id = $this->process_attachment( $postdata, $remote_url );
1101 922
1102 923 $comment_post_id = $post_id;
1103 924 } else {
1104 925 $post_id = wp_insert_post( $postdata, true );
@@ -1148,17 +969,12 @@
1148 969 $terms_to_set = [];
1149 970 foreach ( $post['terms'] as $term ) {
1150 971 // back compat with WXR 1.0 map 'tag' to 'post_tag'
1151 972 $taxonomy = ( 'tag' === $term['domain'] ) ? 'post_tag' : $term['domain'];
1152 -
1153 - // Apply encoding fixes to term data
1154 - $term_name = $this->fix_encoding_issues( $term['name'] );
1155 - $term_slug = $this->fix_encoding_issues( $term['slug'] );
1156 -
1157 - $term_exists = term_exists( $term_slug, $taxonomy );
973 + $term_exists = term_exists( $term['slug'], $taxonomy );
1158 974 $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists;
1159 975 if ( ! $term_id ) {
1160 - $t = wp_insert_term( $term_name, $taxonomy, [ 'slug' => $term_slug ] );
976 + $t = wp_insert_term( $term['name'], $taxonomy, [ 'slug' => $term['slug'] ] );
1161 977 if ( ! is_wp_error( $t ) ) {
1162 978 $term_id = $t['term_id'];
1163 979
1164 980 $this->update_term_meta( $term_id );
@@ -1181,14 +997,9 @@
1181 997 $terms_to_set[ $taxonomy ][] = (int) $term_id;
1182 998 }
1183 999
1184 1000 foreach ( $terms_to_set as $tax => $ids ) {
1185 - // Handle knowledge_base taxonomy capability issues
1186 - if ( $tax === 'knowledge_base' ) {
1187 - $tt_ids = wp_set_object_terms( $post_id, $ids, $tax );
1188 - } else {
1189 - $tt_ids = wp_set_post_terms( $post_id, $ids, $tax );
1190 - }
1001 + $tt_ids = wp_set_post_terms( $post_id, $ids, $tax );
1191 1002 do_action( 'wp_import_set_post_terms', $tt_ids, $ids, $tax, $post_id, $post );
1192 1003 }
1193 1004 unset( $post['terms'], $terms_to_set );
1194 1005 }
@@ -1203,11 +1014,11 @@
1203 1014 if ( ! empty( $post['comments'] ) ) {
1204 1015 $num_comments = 0;
1205 1016 $inserted_comments = [];
1206 1017 foreach ( $post['comments'] as $comment ) {
1207 - $comment_id = $comment['comment_id'];
1208 - $newcomments[ $comment_id ]['comment_post_ID'] = $comment_post_id;
1209 - $newcomments[ $comment_id ]['comment_author'] = $comment['comment_author'];
1018 + $comment_id = $comment['comment_id'];
1019 + $newcomments[ $comment_id ]['comment_post_ID'] = $comment_post_id;
1020 + $newcomments[ $comment_id ]['comment_author'] = $comment['comment_author'];
1210 1021 $newcomments[ $comment_id ]['comment_author_email'] = $comment['comment_author_email'];
1211 1022 $newcomments[ $comment_id ]['comment_author_IP'] = $comment['comment_author_IP'];
1212 1023 $newcomments[ $comment_id ]['comment_author_url'] = $comment['comment_author_url'];
1213 1024 $newcomments[ $comment_id ]['comment_date'] = $comment['comment_date'];
@@ -1242,9 +1053,9 @@
1242 1053
1243 1054 add_comment_meta( $inserted_comments[ $key ], wp_slash( $meta['key'] ), wp_slash_strings_only( $value ) );
1244 1055 }
1245 1056
1246 - ++$num_comments;
1057 + $num_comments ++;
1247 1058 }
1248 1059 unset( $newcomments, $inserted_comments, $post['comments'] );
1249 1060 }
1250 1061
@@ -1253,28 +1064,11 @@
1253 1064 }
1254 1065
1255 1066 $post['postmeta'] = apply_filters( 'wp_import_post_meta', $post['postmeta'], $post_id, $post );
1256 1067
1257 - // WPML language metadata transported via synthetic postmeta — captured
1258 - // here so the keys never persist on the post (WPML's icl_translations
1259 - // is the source of truth on the target site).
1260 - $wpml_lang = '';
1261 - $wpml_source_slug = '';
1262 -
1263 1068 // Add/update post meta.
1264 1069 if ( ! empty( $post['postmeta'] ) ) {
1265 - $imported_meta_keys = []; // Track imported meta keys to prevent duplicates
1266 -
1267 1070 foreach ( $post['postmeta'] as $meta ) {
1268 - if ( WPMLSupport::META_LANG === $meta['key'] ) {
1269 - $wpml_lang = (string) $meta['value'];
1270 - continue;
1271 - }
1272 - if ( WPMLSupport::META_SOURCE_SLUG === $meta['key'] ) {
1273 - $wpml_source_slug = (string) $meta['value'];
1274 - continue;
1275 - }
1276 -
1277 1071 $key = apply_filters( 'import_post_meta_key', $meta['key'], $post_id, $post );
1278 1072 $value = false;
1279 1073
1280 1074 if ( '_edit_last' === $key ) {
@@ -1285,16 +1079,8 @@
1285 1079 }
1286 1080 }
1287 1081
1288 1082 if ( $key ) {
1289 - // Skip if this meta key has already been imported for this post
1290 - if ( isset( $imported_meta_keys[ $key ] ) ) {
1291 - continue;
1292 - }
1293 -
1294 - // Mark this meta key as imported
1295 - $imported_meta_keys[ $key ] = true;
1296 -
1297 1083 // Export gets meta straight from the DB so could have a serialized string.
1298 1084 if ( ! $value ) {
1299 1085 $value = maybe_unserialize( $meta['value'] );
1300 1086 }
@@ -1310,20 +1096,180 @@
1310 1096 }
1311 1097 }
1312 1098 }
1313 1099
1314 - if ( $wpml_lang !== '' ) {
1315 - WPMLSupport::assign_post_language( (int) $post_id, $wpml_lang, $wpml_source_slug );
1100 + do_action( 'templately_import.process_post', $post, $this, $result );
1101 + }
1102 +
1103 + unset( $this->posts );
1104 +
1105 + return $result;
1106 + }
1107 +
1108 + /**
1109 + * Attempt to create a new menu item from import data
1110 + *
1111 + * Fails for draft, orphaned menu items and those without an associated nav_menu
1112 + * or an invalid nav_menu term. If the post type or term object which the menu item
1113 + * represents doesn't exist then the menu item will not be imported (waits until the
1114 + * end of the import to retry again before discarding).
1115 + *
1116 + * @param array $item Menu item details from WXR file
1117 + */
1118 + private function process_menu_item( $item ) {
1119 + $result = [];
1120 +
1121 + // Skip draft, orphaned menu items.
1122 + if ( 'draft' === $item['status'] ) {
1123 + return;
1124 + }
1125 +
1126 + $menu_slug = false;
1127 + if ( isset( $item['terms'] ) ) {
1128 + // Loop through terms, assume first nav_menu term is correct menu.
1129 + foreach ( $item['terms'] as $term ) {
1130 + if ( 'nav_menu' === $term['domain'] ) {
1131 + $menu_slug = $term['slug'];
1132 + break;
1133 + }
1316 1134 }
1135 + }
1317 1136
1318 - do_action( 'templately_import.process_post', $post, $this, $result );
1137 + // No nav_menu term associated with this menu item.
1138 + if ( ! $menu_slug ) {
1139 + $this->output['errors'][] = esc_html__( 'Menu item skipped due to missing menu slug', 'betterdocs' );
1140 +
1141 + return $result;
1319 1142 }
1320 1143
1321 - unset( $this->posts );
1144 + // If menu was already exists, refer the items to the duplicated menu created.
1145 + if ( array_key_exists( $menu_slug, $this->mapped_terms_slug ) ) {
1146 + $menu_slug = $this->mapped_terms_slug[ $menu_slug ];
1147 + }
1322 1148
1149 + $menu_id = term_exists( $menu_slug, 'nav_menu' );
1150 + if ( ! $menu_id ) {
1151 + /* translators: %s: Menu slug. */
1152 + $this->output['errors'][] = sprintf( esc_html__( 'Menu item skipped due to invalid menu slug: %s', 'betterdocs' ), $menu_slug );
1153 +
1154 + return $result;
1155 + } else {
1156 + $menu_id = is_array( $menu_id ) ? $menu_id['term_id'] : $menu_id;
1157 + }
1158 +
1159 + $post_meta_key_value = [];
1160 + foreach ( $item['postmeta'] as $meta ) {
1161 + $post_meta_key_value[ $meta['key'] ] = $meta['value'];
1162 + }
1163 +
1164 + $_menu_item_type = $post_meta_key_value['_menu_item_type'];
1165 + $_menu_item_url = $post_meta_key_value['_menu_item_url'];
1166 +
1167 + // Skip menu items 'taxonomy' type, when the taxonomy is not exits.
1168 + if ( 'taxonomy' === $_menu_item_type && ! taxonomy_exists( $post_meta_key_value['_menu_item_object'] ) ) {
1169 + return $result;
1170 + }
1171 +
1172 + // Skip menu items 'post_type' type, when the post type is not exits.
1173 + if ( 'post_type' === $_menu_item_type && ! post_type_exists( $post_meta_key_value['_menu_item_object'] ) ) {
1174 + return $result;
1175 + }
1176 +
1177 + $_menu_item_object_id = $post_meta_key_value['_menu_item_object_id'];
1178 + if ( 'taxonomy' === $_menu_item_type && isset( $this->processed_terms[ (int) $_menu_item_object_id ] ) ) {
1179 + $_menu_item_object_id = $this->processed_terms[ (int) $_menu_item_object_id ];
1180 + } elseif ( 'post_type' === $_menu_item_type && isset( $this->processed_posts[ (int) $_menu_item_object_id ] ) ) {
1181 + $_menu_item_object_id = $this->processed_posts[ (int) $_menu_item_object_id ];
1182 + } elseif ( 'custom' === $_menu_item_type ) {
1183 + $_menu_item_url = Url::migrate( $_menu_item_url, $this->base_blog_url );
1184 + } else {
1185 + return $result;
1186 + }
1187 +
1188 + $_menu_item_menu_item_parent = $post_meta_key_value['_menu_item_menu_item_parent'];
1189 + if ( isset( $this->processed_menu_items[ (int) $_menu_item_menu_item_parent ] ) ) {
1190 + $_menu_item_menu_item_parent = $this->processed_menu_items[ (int) $_menu_item_menu_item_parent ];
1191 + } elseif ( $_menu_item_menu_item_parent ) {
1192 + $this->menu_item_orphans[ (int) $item['post_id'] ] = (int) $_menu_item_menu_item_parent;
1193 + $_menu_item_menu_item_parent = 0;
1194 + }
1195 +
1196 + // wp_update_nav_menu_item expects CSS classes as a space separated string
1197 + $_menu_item_classes = maybe_unserialize( $post_meta_key_value['_menu_item_classes'] );
1198 + if ( is_array( $_menu_item_classes ) ) {
1199 + $_menu_item_classes = implode( ' ', $_menu_item_classes );
1200 + }
1201 +
1202 + $args = [
1203 + 'menu-item-object-id' => $_menu_item_object_id,
1204 + 'menu-item-object' => $post_meta_key_value['_menu_item_object'],
1205 + 'menu-item-parent-id' => $_menu_item_menu_item_parent,
1206 + 'menu-item-position' => (int) $item['menu_order'],
1207 + 'menu-item-type' => $_menu_item_type,
1208 + 'menu-item-title' => $item['post_title'],
1209 + 'menu-item-url' => $_menu_item_url,
1210 + 'menu-item-description' => $item['post_content'],
1211 + 'menu-item-attr-title' => $item['post_excerpt'],
1212 + 'menu-item-target' => $post_meta_key_value['_menu_item_target'],
1213 + 'menu-item-classes' => $_menu_item_classes,
1214 + 'menu-item-xfn' => $post_meta_key_value['_menu_item_xfn'],
1215 + 'menu-item-status' => $item['status'],
1216 + ];
1217 +
1218 + $id = wp_update_nav_menu_item( $menu_id, 0, $args );
1219 + if ( $id && ! is_wp_error( $id ) ) {
1220 + $this->processed_menu_items[ (int) $item['post_id'] ] = (int) $id;
1221 + $result[ $item['post_id'] ] = $id;
1222 +
1223 + $this->update_post_meta( $id );
1224 + }
1225 +
1323 1226 return $result;
1324 1227 }
1325 1228
1229 + private function process_navigation( &$item ): bool {
1230 + // Skip draft, orphaned menu items.
1231 + if ( 'draft' === $item['status'] ) {
1232 + return false;
1233 + }
1234 +
1235 + $content = parse_blocks( $item['post_content'] );
1236 +
1237 + $parsed_blocks = [];
1238 + foreach ( $content as $block ) {
1239 + if ( empty( $block['blockName'] ) ) {
1240 + continue;
1241 + }
1242 +
1243 + $this->prepare_block( $block );
1244 + $parsed_blocks[] = $block;
1245 + }
1246 +
1247 + $item['post_content'] = serialize_blocks( $parsed_blocks );
1248 +
1249 + return true;
1250 + }
1251 +
1252 + private function prepare_block( &$block ) {
1253 + if ( $block['blockName'] == 'core/navigation-link' ) {
1254 + $attrs = &$block['attrs'];
1255 + switch ( $attrs['kind'] ) {
1256 + case 'post-type':
1257 + if ( isset( $this->processed_posts[ (int) $attrs['id'] ] ) ) {
1258 + $attrs['id'] = $this->processed_posts[ (int) $attrs['id'] ];
1259 + $attrs['url'] = get_permalink( $attrs['id'] );
1260 + }
1261 + break;
1262 + case 'taxonomy':
1263 + if ( isset( $this->processed_terms[ (int) $attrs['id'] ] ) ) {
1264 + $attrs['id'] = $this->processed_terms[ (int) $attrs['id'] ];
1265 + $attrs['url'] = get_term_link( $attrs['id'], $attrs['type'] );
1266 + }
1267 + break;
1268 + }
1269 + }
1270 + }
1271 +
1326 1272 /**
1327 1273 * If fetching attachments is enabled then attempt to create a new attachment
1328 1274 *
1329 1275 * @param array $post Attachment post details from WXR
@@ -1331,9 +1277,9 @@
1331 1277 *
1332 1278 * @return int|WP_Error Post ID on success, WP_Error otherwise
1333 1279 */
1334 1280 private function process_attachment( $post, $url ) {
1335 - require_once ABSPATH . 'wp-admin/includes/image.php';
1281 + require_once ABSPATH . 'wp-admin/includes/image.php';
1336 1282
1337 1283 if ( ! $this->fetch_attachments ) {
1338 1284 return new WP_Error( 'attachment_processing_error', esc_html__( 'Fetching attachments is not enabled', 'betterdocs' ) );
1339 1285 }
@@ -1385,12 +1331,12 @@
1385 1331 *
1386 1332 * @return array|WP_Error Local file location details on success, WP_Error otherwise
1387 1333 */
1388 1334 private function fetch_remote_file( $url, $post ) {
1389 - include_once ABSPATH . '/wp-admin/includes/file.php';
1335 + include_once ABSPATH . '/wp-admin/includes/file.php';
1390 1336
1391 1337 // Extract the file name from the URL.
1392 - $file_name = basename( (string) wp_parse_url( $url, PHP_URL_PATH ) );
1338 + $file_name = basename( parse_url( $url, PHP_URL_PATH ) );
1393 1339
1394 1340 if ( ! $file_name ) {
1395 1341 $file_name = md5( $url );
1396 1342 }
@@ -1400,24 +1346,21 @@
1400 1346 return new WP_Error( 'import_no_file', esc_html__( 'Could not create temporary file.', 'betterdocs' ) );
1401 1347 }
1402 1348
1403 1349 // Fetch the remote URL and write it to the placeholder file.
1404 - $remote_response = wp_safe_remote_get(
1405 - $url,
1406 - [
1407 - 'timeout' => 300,
1408 - 'stream' => true,
1409 - 'filename' => $tmp_file_name,
1410 - 'headers' => [
1411 - 'Accept-Encoding' => 'identity'
1412 - ]
1350 + $remote_response = wp_safe_remote_get( $url, [
1351 + 'timeout' => 300,
1352 + 'stream' => true,
1353 + 'filename' => $tmp_file_name,
1354 + 'headers' => [
1355 + 'Accept-Encoding' => 'identity',
1413 1356 ]
1414 - );
1357 + ] );
1415 1358
1416 1359 if ( is_wp_error( $remote_response ) ) {
1417 - wp_delete_file( $tmp_file_name );
1360 + @unlink( $tmp_file_name );
1418 1361
1419 - return new WP_Error( 'import_file_error', sprintf( /* translators: 1: WordPress error message, 2: WordPress error code. */esc_html__( 'Request failed due to an error: %1$s (%2$s)', 'betterdocs' ), esc_html( $remote_response->get_error_message() ), esc_html( $remote_response->get_error_code() ) ) );
1362 + return new WP_Error( 'import_file_error', sprintf( /* translators: 1: WordPress error message, 2: WordPress error code. */ esc_html__( 'Request failed due to an error: %1$s (%2$s)', 'betterdocs' ), esc_html( $remote_response->get_error_message() ), esc_html( $remote_response->get_error_code() ) ) );
1420 1363 }
1421 1364
1422 1365 $remote_response_code = (int) wp_remote_retrieve_response_code( $remote_response );
1423 1366
@@ -1422,11 +1365,11 @@
1422 1365 $remote_response_code = (int) wp_remote_retrieve_response_code( $remote_response );
1423 1366
1424 1367 // Make sure the fetch was successful.
1425 1368 if ( 200 !== $remote_response_code ) {
1426 - wp_delete_file( $tmp_file_name );
1369 + @unlink( $tmp_file_name );
1427 1370
1428 - return new WP_Error( 'import_file_error', sprintf( /* translators: 1: HTTP error message, 2: HTTP error code. */esc_html__( 'Remote server returned the following unexpected result: %1$s (%2$s)', 'betterdocs' ), get_status_header_desc( $remote_response_code ), esc_html( $remote_response_code ) ) );
1371 + return new WP_Error( 'import_file_error', sprintf( /* translators: 1: HTTP error message, 2: HTTP error code. */ esc_html__( 'Remote server returned the following unexpected result: %1$s (%2$s)', 'betterdocs' ), get_status_header_desc( $remote_response_code ), esc_html( $remote_response_code ) ) );
1429 1372 }
1430 1373
1431 1374 $headers = wp_remote_retrieve_headers( $remote_response );
1432 1375
@@ -1431,9 +1374,9 @@
1431 1374 $headers = wp_remote_retrieve_headers( $remote_response );
1432 1375
1433 1376 // Request failed.
1434 1377 if ( ! $headers ) {
1435 - wp_delete_file( $tmp_file_name );
1378 + @unlink( $tmp_file_name );
1436 1379
1437 1380 return new WP_Error( 'import_file_error', esc_html__( 'Remote server did not respond', 'betterdocs' ) );
1438 1381 }
1439 1382
@@ -1439,15 +1382,15 @@
1439 1382
1440 1383 $filesize = (int) filesize( $tmp_file_name );
1441 1384
1442 1385 if ( 0 === $filesize ) {
1443 - wp_delete_file( $tmp_file_name );
1386 + @unlink( $tmp_file_name );
1444 1387
1445 1388 return new WP_Error( 'import_file_error', esc_html__( 'Zero size file downloaded', 'betterdocs' ) );
1446 1389 }
1447 1390
1448 1391 if ( ! isset( $headers['content-encoding'] ) && isset( $headers['content-length'] ) && $filesize !== (int) $headers['content-length'] ) {
1449 - wp_delete_file( $tmp_file_name );
1392 + @unlink( $tmp_file_name );
1450 1393
1451 1394 return new WP_Error( 'import_file_error', esc_html__( 'Downloaded file has incorrect size', 'betterdocs' ) );
1452 1395 }
1453 1396
@@ -1452,9 +1395,9 @@
1452 1395 }
1453 1396
1454 1397 $max_size = (int) apply_filters( 'import_attachment_size_limit', self::DEFAULT_IMPORT_ATTACHMENT_SIZE_LIMIT );
1455 1398 if ( ! empty( $max_size ) && $filesize > $max_size ) {
1456 - wp_delete_file( $tmp_file_name );
1399 + @unlink( $tmp_file_name );
1457 1400
1458 1401 /* translators: %s: Max file size. */
1459 1402
1460 1403 return new WP_Error( 'import_file_error', sprintf( esc_html__( 'Remote file is too large, limit is %s', 'betterdocs' ), size_format( $max_size ) ) );
@@ -1502,17 +1445,16 @@
1502 1445 $new_file = $uploads['path'] . "/$file_name";
1503 1446 $move_new_file = copy( $tmp_file_name, $new_file );
1504 1447
1505 1448 if ( ! $move_new_file ) {
1506 - wp_delete_file( $tmp_file_name );
1449 + @unlink( $tmp_file_name );
1507 1450
1508 1451 return new WP_Error( 'import_file_error', esc_html__( 'The uploaded file could not be moved', 'betterdocs' ) );
1509 1452 }
1510 1453
1511 - // Set correct file permissions to match parent directory.
1454 + // Set correct file permissions.
1512 1455 $stat = stat( dirname( $new_file ) );
1513 1456 $perms = $stat['mode'] & 0000666;
1514 - // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_chmod -- WP_Filesystem chmod requires init; mirrors WP core media handler.
1515 1457 chmod( $new_file, $perms );
1516 1458
1517 1459 $upload = [
1518 1460 'file' => $new_file,
@@ -1517,9 +1459,9 @@
1517 1459 $upload = [
1518 1460 'file' => $new_file,
1519 1461 'url' => $uploads['url'] . "/$file_name",
1520 1462 'type' => $wp_filetype['type'],
1521 - 'error' => false
1463 + 'error' => false,
1522 1464 ];
1523 1465
1524 1466 // Keep track of the old and new urls so we can substitute them later.
1525 1467 $this->url_remap[ $url ] = $upload['url'];
@@ -1582,15 +1524,12 @@
1582 1524 */
1583 1525 private function backfill_attachment_urls() {
1584 1526 global $wpdb;
1585 1527 // Make sure we do the longest urls first, in case one is a substring of another.
1586 - uksort(
1587 - $this->url_remap,
1588 - function ( $a, $b ) {
1589 - // Return the difference in length between two strings.
1590 - return strlen( $b ) - strlen( $a );
1591 - }
1592 - );
1528 + uksort( $this->url_remap, function ( $a, $b ) {
1529 + // Return the difference in length between two strings.
1530 + return strlen( $b ) - strlen( $a );
1531 + } );
1593 1532
1594 1533 foreach ( $this->url_remap as $from_url => $to_url ) {
1595 1534 // Remap urls in post_content.
1596 1535 $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s)", $from_url, $to_url ) );
@@ -1621,29 +1560,14 @@
1621 1560 * @param string $file Path to WXR file for parsing
1622 1561 *
1623 1562 * @return array Information gathered from the WXR file
1624 1563 */
1625 - private function parse( $file ) {
1626 - $type = strtolower( (string) $this->file_type );
1627 -
1628 - // The uploaded temp file has no extension, and browsers report a .xml
1629 - // upload inconsistently (text/xml, application/xml, text/plain, or
1630 - // nothing) — so prefer the original file name's extension when it was
1631 - // passed through, fall back to the MIME, and default anything that is
1632 - // not clearly CSV to the WordPress export parser. WXR_Parser validates
1633 - // the file and returns a WP_Error for a non-WXR file, which
1634 - // import_start() already reports cleanly; previously an unrecognised
1635 - // MIME left $parser null and fataled with "Call to a member function
1636 - // parse() on null".
1637 - $name = isset( $this->args['file_name'] ) ? (string) $this->args['file_name'] : (string) $this->requested_file_path;
1638 - $ext = strtolower( pathinfo( $name, PATHINFO_EXTENSION ) );
1639 -
1640 - if ( 'csv' === $ext || ( '' === $ext && strpos( $type, 'csv' ) !== false ) ) {
1641 - $parser = new CSV_Parser();
1642 - } else {
1643 - $parser = new WXR_Parser();
1644 - }
1645 -
1564 + private function parse( $file ): array {
1565 + if ($this->file_type == 'text/xml') {
1566 + $parser = new WXR_Parser();
1567 + } else if ($this->file_type == 'text/csv') {
1568 + $parser = new CSV_Parser();
1569 + }
1646 1570 return $parser->parse( $file );
1647 1571 }
1648 1572
1649 1573 /**
@@ -1721,16 +1645,9 @@
1721 1645 * @param $file
1722 1646 * @param array $args
1723 1647 */
1724 1648 public function __construct( $file, array $args = [] ) {
1725 - // WP core's WP_Importer has no constructor, and on PHP 8 calling
1726 - // parent::__construct() on a constructor-less parent throws
1727 - // "Error: Cannot call constructor" — which aborted every import. Only
1728 - // call the parent when it actually defines one (future-proof if WP adds
1729 - // it back).
1730 - if ( method_exists( get_parent_class( $this ), '__construct' ) ) {
1731 - parent::__construct();
1732 - }
1649 + parent::__construct();
1733 1650
1734 1651 $this->requested_file_path = $file;
1735 1652 $this->args = $args;
1736 1653
@@ -1757,9 +1674,9 @@
1757 1674 if ( ! empty( $this->args['terms_meta'] ) ) {
1758 1675 $this->terms_meta = $this->args['terms_meta'];
1759 1676 }
1760 1677
1761 - if ( ! empty( $this->args['file_type'] ) ) {
1678 + if ( ! empty( $this->args['file_type'] ) ) {
1762 1679 $this->file_type = $this->args['file_type'];
1763 1680 }
1764 1681 }
1765 1682 }