PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 3.3.4
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v3.3.4
4.9.1 4.9.0 4.8.2 4.8.1 4.8.0 4.7.0 4.6.2 4.6.1 4.6.0 4.5.6 4.5.5 4.5.4 4.5.3 4.5.2 4.5.1 4.5.0 4.4.1 4.4.0 3.3.4 3.4.0 3.4.1 3.4.2 3.5.0 3.5.1 3.5.2 All 199 releases
betterdocs / includes / Admin / Importer / WPImport.php

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

1,633 lines 54.2 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\Importer;
4
5 use WPDeveloper\BetterDocs\Admin\Importer\Parsers\WXR_Parser;
6 use WPDeveloper\BetterDocs\Admin\Importer\Parsers\CSV_Parser;
7 use WP_Error;
8 use WP_Importer;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit; // Exit if accessed directly.
12 }
13
14 /**
15 * Originally made by WordPress part of WordPress/Importer.
16 * https://plugins.trac.wordpress.org/browser/wordpress-importer/trunk/class-wp-import.php
17 *
18 * What was done ( by Elementor):
19 * Reformat of the code.
20 * Changed text domain.
21 * Changed methods visibility.
22 * Changed method from `get_authors_from_import` to `set_authors_from_import`.
23 * Changed method from `get_author_mapping` to `set_author_mapping`.
24 * Removed use of '$_POST' the input 'options' will be passed via constructor args.
25 * Removed echos, UI and print methods, all echos replaced with `$this->output` append.
26 * Removed `die` ( exit(s) ).
27 *
28 * What was done ( by Templately):
29 * Add Action For Every Part Of Import for SSE
30 */
31
32 if ( ! class_exists( 'WP_Importer' ) ) {
33 $class_wp_importer = ABSPATH . 'wp-admin/includes/class-wp-importer.php';
34
35 if ( file_exists( $class_wp_importer ) ) {
36 require $class_wp_importer;
37 }
38 }
39
40 if ( ! function_exists( 'wp_import_cleanup' ) ) {
41 $wp_import = ABSPATH . 'wp-admin/includes/import.php';
42
43 if ( file_exists( $wp_import ) ) {
44 require $wp_import;
45 }
46 }
47
48 use function wp_import_cleanup;
49
50 class WPImport extends WP_Importer {
51 const DEFAULT_BUMP_REQUEST_TIMEOUT = 60;
52 const DEFAULT_ALLOW_CREATE_USERS = true;
53 const DEFAULT_IMPORT_ATTACHMENT_SIZE_LIMIT = 0; // 0 = unlimited.
54
55 /**
56 * @var string
57 */
58 private $requested_file_path;
59
60 /**
61 * @var array
62 */
63 private $args;
64
65 /**
66 * @var array
67 */
68 private $output = [
69 'status' => 'failed',
70 'errors' => [],
71 ];
72
73 /*
74 * WXR attachment ID
75 */
76 private $id;
77
78 // Information to import from WXR file.
79 private $version;
80 private $authors = [];
81 public $posts = [];
82 public $terms = [];
83 private $base_url = '';
84 private $page_on_front;
85 private $base_blog_url = '';
86
87 // Mappings from old information to new.
88 public $file_type;
89 public $processed_taxonomies;
90 public $processed_terms = [];
91 public $processed_posts = [];
92 private $processed_authors = [];
93 private $author_mapping = [];
94 private $processed_menu_items = [];
95 private $post_orphans = [];
96 private $menu_item_orphans = [];
97 private $mapped_terms_slug = [];
98
99 private $fetch_attachments = false;
100 private $url_remap = [];
101 private $featured_images = [];
102
103 /**
104 * @var array[] [meta_key => meta_value] Meta value that should be set for every imported post.
105 */
106 private $posts_meta = [];
107
108 /**
109 * @var array[] [meta_key => meta_value] Meta value that should be set for every imported term.
110 */
111 private $terms_meta = [];
112
113 /**
114 * Parses filename from a Content-Disposition header value.
115 *
116 * As per RFC6266:
117 *
118 * content-disposition = "Content-Disposition" ":"
119 * disposition-type *( ";" disposition-parm )
120 *
121 * disposition-type = "inline" | "attachment" | disp-ext-type
122 * ; case-insensitive
123 * disp-ext-type = token
124 *
125 * disposition-parm = filename-parm | disp-ext-parm
126 *
127 * filename-parm = "filename" "=" value
128 * | "filename*" "=" ext-value
129 *
130 * disp-ext-parm = token "=" value
131 * | ext-token "=" ext-value
132 * ext-token = <the characters in token, followed by "*">
133 *
134 * @param string[] $disposition_header List of Content-Disposition header values.
135 *
136 * @return string|null Filename if available, or null if not found.
137 * @link http://tools.ietf.org/html/rfc2388
138 * @link http://tools.ietf.org/html/rfc6266
139 *
140 * @see WP_REST_Attachments_Controller::get_filename_from_disposition()
141 *
142 */
143 protected static function get_filename_from_disposition( $disposition_header ) {
144 // Get the filename.
145 $filename = null;
146
147 foreach ( $disposition_header as $value ) {
148 $value = trim( $value );
149
150 if ( strpos( $value, ';' ) === false ) {
151 continue;
152 }
153
154 list( $type, $attr_parts ) = explode( ';', $value, 2 );
155
156 $attr_parts = explode( ';', $attr_parts );
157 $attributes = [];
158
159 foreach ( $attr_parts as $part ) {
160 if ( strpos( $part, '=' ) === false ) {
161 continue;
162 }
163
164 list( $key, $value ) = explode( '=', $part, 2 );
165
166 $attributes[ trim( $key ) ] = trim( $value );
167 }
168
169 if ( empty( $attributes['filename'] ) ) {
170 continue;
171 }
172
173 $filename = trim( $attributes['filename'] );
174
175 // Unquote quoted filename, but after trimming.
176 if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, - 1, 1 ) === '"' ) {
177 $filename = substr( $filename, 1, - 1 );
178 }
179 }
180
181 return $filename;
182 }
183
184 /**
185 * Retrieves file extension by mime type.
186 *
187 * @param string $mime_type Mime type to search extension for.
188 *
189 * @return string|null File extension if available, or null if not found.
190 */
191 protected static function get_file_extension_by_mime_type( $mime_type ) {
192 static $map = null;
193
194 if ( is_array( $map ) ) {
195 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
196 }
197
198 $mime_types = wp_get_mime_types();
199 $map = array_flip( $mime_types );
200
201 // Some types have multiple extensions, use only the first one.
202 foreach ( $map as $type => $extensions ) {
203 $map[ $type ] = strtok( $extensions, '|' );
204 }
205
206 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
207 }
208
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 }
228
229 return $post;
230 }
231
232
233 /**
234 * The main controller for the actual import stage.
235 *
236 * @param string $file Path to the WXR file for importing
237 */
238 private function import( $file ) {
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 } );
245
246 if ( ! $this->import_start( $file ) ) {
247 return;
248 }
249
250
251 $this->set_author_mapping();
252
253 wp_suspend_cache_invalidation( true );
254 $imported_summary = [
255 'terms' => $this->process_terms(),
256 'posts' => $this->process_posts(),
257 ];
258 wp_suspend_cache_invalidation( false );
259
260 // Update incorrect/missing information in the DB.
261 $this->backfill_parents();
262 $this->backfill_attachment_urls();
263 $this->remap_featured_images();
264
265 $this->import_end();
266
267 $is_some_succeed = false;
268 foreach ( $imported_summary as $item ) {
269 if ( $item > 0 ) {
270 $is_some_succeed = true;
271 break;
272 }
273 }
274
275 if ( $is_some_succeed ) {
276 $this->output['status'] = 'success';
277 $this->output['summary'] = $imported_summary;
278 }
279 }
280
281 /**
282 * Parses the WXR file and prepares us for the task of processing parsed data.
283 *
284 * @param string $file Path to the WXR file for importing
285 */
286 private function import_start( string $file ): bool {
287 if ( ! is_file( $file ) ) {
288 $this->output['errors'] = [ esc_html__( 'The file does not exist, please try again.', 'betterdocs' ) ];
289
290 return false;
291 }
292
293 $action = $this->args['action'];
294
295 $import_data = $this->parse( $file );
296
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
302 if ( is_wp_error( $import_data ) ) {
303 /**
304 * @var WP_Error $import_data ;
305 */
306 $this->output['errors'] = [ $import_data->get_error_message() ];
307
308 return false;
309 }
310
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);
314
315 if ( ! empty( $action ) ) {
316 $existing_posts = $this->args['existing_slug'];
317 $existing_posts_array = explode(',', $existing_posts);
318
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 });
324
325 // If you want the resulting array to have numeric keys
326 $posts = array_values($filtered_posts);
327
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
331
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 }
340
341 if ( isset($import_data['version']) ) {
342 $this->version = $import_data['version'];
343 }
344
345 $this->set_authors_from_import( $import_data );
346 $this->posts = $posts;
347 $this->terms = $import_data['terms'];
348
349 if ( isset($import_data['base_url']) ) {
350 $this->base_url = esc_url( $import_data['base_url'] );
351 }
352
353 if ( isset($import_data['base_blog_url']) ) {
354 $this->base_blog_url = esc_url( $import_data['base_blog_url'] );
355 }
356
357 if ( isset($import_data['page_on_front']) ) {
358 $this->page_on_front = $import_data['page_on_front'];
359 }
360
361 wp_defer_term_counting( true );
362 wp_defer_comment_counting( true );
363
364 do_action( 'import_start', $this );
365
366 return true;
367 }
368
369 /**
370 * Performs post-import cleanup of files and the cache
371 */
372 private function import_end() {
373 wp_import_cleanup( $this->id );
374
375 wp_cache_flush();
376
377 foreach ( get_taxonomies() as $tax ) {
378 delete_option( "{$tax}_children" );
379 _get_term_hierarchy( $tax );
380 }
381
382 wp_defer_term_counting( false );
383 wp_defer_comment_counting( false );
384
385 do_action( 'import_end' );
386 }
387
388 /**
389 * Retrieve authors from parsed WXR data and set it to `$this->>authors`.
390 *
391 * Uses the provided author information from WXR 1.1 files
392 * or extracts info from each post for WXR 1.0 files
393 *
394 * @param array $import_data Data returned by a WXR parser
395 */
396 private function set_authors_from_import( $import_data ) {
397 if ( ! empty( $import_data['authors'] ) ) {
398 $this->authors = $import_data['authors'];
399 // No author information, grab it from the posts.
400 } else {
401 foreach ( $import_data['posts'] as $post ) {
402 $login = sanitize_user( $post['post_author'], true );
403
404 if ( empty( $login ) ) {
405 /* translators: %s: Post author. */
406 $this->output['errors'][] = sprintf( esc_html__( 'Failed to import author %s. Their posts will be attributed to the current user.', 'betterdocs' ), $post['post_author'] );
407 continue;
408 }
409
410 if ( ! isset( $this->authors[ $login ] ) ) {
411 $this->authors[ $login ] = [
412 'author_login' => $login,
413 'author_display_name' => $post['post_author'],
414 ];
415 }
416 }
417 }
418 }
419
420 /**
421 * Map old author logins to local user IDs based on decisions made
422 * in import options form. Can map to an existing user, create a new user
423 * or falls back to the current user in case of error with either of the previous
424 */
425 private function set_author_mapping() {
426 if ( ! isset( $this->args['imported_authors'] ) ) {
427 return;
428 }
429
430 $create_users = apply_filters( 'import_allow_create_users', self::DEFAULT_ALLOW_CREATE_USERS );
431
432 foreach ( (array) $this->args['imported_authors'] as $i => $old_login ) {
433 // Multisite adds strtolower to sanitize_user. Need to sanitize here to stop breakage in process_posts.
434 $santized_old_login = sanitize_user( $old_login, true );
435 $old_id = isset( $this->authors[ $old_login ]['author_id'] ) ? (int) $this->authors[ $old_login ]['author_id'] : false;
436
437 if ( ! empty( $this->args['user_map'][ $i ] ) ) {
438 $user = get_userdata( (int) $this->args['user_map'][ $i ] );
439 if ( isset( $user->ID ) ) {
440 if ( $old_id ) {
441 $this->processed_authors[ $old_id ] = $user->ID;
442 }
443 $this->author_mapping[ $santized_old_login ] = $user->ID;
444 }
445 } elseif ( $create_users ) {
446 $user_id = 0;
447 if ( ! empty( $this->args['user_new'][ $i ] ) ) {
448 $user_id = wp_create_user( $this->args['user_new'][ $i ], wp_generate_password() );
449 } elseif ( '1.0' !== $this->version ) {
450 $user_data = [
451 'user_login' => $old_login,
452 'user_pass' => wp_generate_password(),
453 'user_email' => isset( $this->authors[ $old_login ]['author_email'] ) ? $this->authors[ $old_login ]['author_email'] : '',
454 'display_name' => $this->authors[ $old_login ]['author_display_name'],
455 'first_name' => isset( $this->authors[ $old_login ]['author_first_name'] ) ? $this->authors[ $old_login ]['author_first_name'] : '',
456 'last_name' => isset( $this->authors[ $old_login ]['author_last_name'] ) ? $this->authors[ $old_login ]['author_last_name'] : '',
457 ];
458 $user_id = wp_insert_user( $user_data );
459 }
460
461 if ( ! is_wp_error( $user_id ) ) {
462 if ( $old_id ) {
463 $this->processed_authors[ $old_id ] = $user_id;
464 }
465 $this->author_mapping[ $santized_old_login ] = $user_id;
466 } else {
467 /* translators: %s: Author display name. */
468 $error = sprintf( esc_html__( 'Failed to create new user for %s. Their posts will be attributed to the current user.', 'betterdocs' ), $this->authors[ $old_login ]['author_display_name'] );
469
470 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
471 $error .= PHP_EOL . $user_id->get_error_message();
472 }
473
474 $this->output['errors'][] = $error;
475 }
476 }
477
478 // Failsafe: if the user_id was invalid, default to the current user.
479 if ( ! isset( $this->author_mapping[ $santized_old_login ] ) ) {
480 if ( $old_id ) {
481 $this->processed_authors[ $old_id ] = (int) get_current_user_id();
482 }
483 $this->author_mapping[ $santized_old_login ] = (int) get_current_user_id();
484 }
485 }
486 }
487
488 /**
489 * Create new terms based on import information
490 *
491 * Doesn't create a term its slug already exists
492 *
493 * @return array|array[] the ids of succeed/failed imported terms.
494 */
495 private function process_terms(): array {
496 $result = [
497 'succeed' => [],
498 'failed' => [],
499 ];
500
501 $this->terms = apply_filters( 'wp_import_terms', $this->terms );
502
503 if ( empty( $this->terms ) ) {
504 return $result;
505 }
506
507 foreach ( $this->terms as $term ) {
508 // if the term already exists in the correct taxonomy leave it alone
509 $term_id = term_exists( $term['slug'], $term['term_taxonomy'] );
510 if ( $term_id ) {
511 if ( is_array( $term_id ) ) {
512 $term_id = $term_id['term_id'];
513 }
514
515 if ( isset( $term['term_id'] ) ) {
516 if ( 'nav_menu' === $term['term_taxonomy'] ) {
517 // BC - support old kits that the menu terms are part of the 'nav_menu_item' post type
518 // and not part of the taxonomies.
519 if ( ! empty( $this->processed_taxonomies[ $term['term_taxonomy'] ] ) ) {
520 foreach ( $this->processed_taxonomies[ $term['term_taxonomy'] ] as $processed_term ) {
521 $old_slug = $processed_term['old_slug'];
522 $new_slug = $processed_term['new_slug'];
523
524 $this->mapped_terms_slug[ $old_slug ] = $new_slug;
525 $result['succeed'][ $old_slug ] = $new_slug;
526 }
527 continue;
528 } else {
529 $term = $this->handle_duplicated_nav_menu_term( $term );
530 }
531 } else {
532 $this->processed_terms[ (int) $term['term_id'] ] = (int) $term_id;
533 $result['succeed'][ (int) $term['term_id'] ] = (int) $term_id;
534 continue;
535 }
536 }
537 }
538
539 if ( empty( $term['term_parent'] ) ) {
540 $parent = 0;
541 } else {
542 $parent = term_exists( $term['term_parent'], $term['term_taxonomy'] );
543 if ( is_array( $parent ) ) {
544 $parent = $parent['term_id'];
545 }
546 }
547
548 $description = $term['term_description'] ?? '';
549 $args = [
550 'slug' => $term['slug'],
551 'description' => wp_slash( $description ),
552 'parent' => (int) $parent,
553 ];
554
555 $id = wp_insert_term( wp_slash( $term['term_name'] ), $term['term_taxonomy'], $args );
556
557 if ( ! is_wp_error( $id ) ) {
558 if ( isset( $term['term_id'] ) ) {
559 $this->processed_terms[ (int) $term['term_id'] ] = $id['term_id'];
560 $result['succeed'][ (int) $term['term_id'] ] = $id['term_id'];
561
562 $this->update_term_meta( $id['term_id'] );
563 }
564 } else {
565 /* translators: 1: Term taxonomy, 2: Term name. */
566 $error = sprintf( esc_html__( 'Failed to import %1$s %2$s', 'betterdocs' ), $term['term_taxonomy'], $term['term_name'] );
567
568 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
569 $error .= PHP_EOL . $id->get_error_message();
570 }
571
572 $result['failed'][] = $id;
573 $this->output['errors'][] = $error;
574 continue;
575 }
576 $this->process_termmeta( $term, $id['term_id'] );
577
578 do_action( 'betterdocs_import.process_term', $term, $this, $result );
579 }
580
581 unset( $this->terms );
582
583 return $result;
584 }
585
586 /**
587 * Add metadata to imported term.
588 *
589 * @param array $term Term data from WXR import.
590 * @param int $term_id ID of the newly created term.
591 */
592 private function process_termmeta( $term, $term_id ) {
593 if ( ! function_exists( 'add_term_meta' ) ) {
594 return;
595 }
596
597 if ( ! isset( $term['termmeta'] ) ) {
598 $term['termmeta'] = [];
599 }
600
601 /**
602 * Filters the metadata attached to an imported term.
603 *
604 * @param array $termmeta Array of term meta.
605 * @param int $term_id ID of the newly created term.
606 * @param array $term Term data from the WXR import.
607 */
608 $term['termmeta'] = apply_filters( 'wp_import_term_meta', $term['termmeta'], $term_id, $term );
609
610 if ( empty( $term['termmeta'] ) ) {
611 return;
612 }
613
614 foreach ( $term['termmeta'] as $meta ) {
615 /**
616 * Filters the meta key for an imported piece of term meta.
617 *
618 * @param string $meta_key Meta key.
619 * @param int $term_id ID of the newly created term.
620 * @param array $term Term data from the WXR import.
621 */
622 $key = apply_filters( 'import_term_meta_key', $meta['key'], $term_id, $term );
623 if ( ! $key ) {
624 continue;
625 }
626
627 // Export gets meta straight from the DB so could have a serialized string
628 $value = maybe_unserialize( $meta['value'] );
629 $insert_meta = add_term_meta( $term_id, wp_slash( $key ), wp_slash_strings_only( $value ) );
630 /**
631 * Fires after term meta is imported.
632 *
633 * @param int $term_id ID of the newly created term.
634 * @param string $key Meta key.
635 * @param mixed $value Meta value.
636 */
637 do_action( 'import_term_meta', $term_id, $key, $value );
638 }
639 }
640
641 public function existing_slug_action ($posts, $action) {
642 $existing_posts = $this->args['existing_slug'];
643 $existing_posts_array = explode(',', $existing_posts);
644
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 });
650
651 // If you want the resulting array to have numeric keys
652 $posts = array_values($filtered_posts);
653
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
657
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 }
665
666 return $posts;
667 }
668
669 private function import_sample_data( $posts, $action ) {
670 $result = [
671 'succeed' => [],
672 'failed' => [],
673 ];
674
675 if ( ! empty( $action ) ) {
676 $posts = $this->existing_slug_action($posts, $action);
677 }
678
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'] : '';
689
690 // Check if the category and knowledge base term exist, if not, create them
691 $default_multiple_kb = betterdocs()->settings->get( 'multiple_kb' );
692
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 }
705
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 }
713
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 }
721
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 ];
733
734 if ( $default_multiple_kb == 1 ) {
735 $post_data['tax_input']['knowledge_base'] = array( $knowledge_base_id );
736 }
737
738 // Insert the post
739 $post_id = wp_insert_post( $post_data );
740
741 // Set the featured image
742 if ( $featured_image_url ) {
743 $this->set_post_thumbnail( $post_id, $featured_image_url );
744 }
745 }
746
747 return $result;
748 }
749
750 public function set_post_thumbnail( $post_id, $attachment_url ) {
751 require_once ABSPATH . 'wp-admin/includes/image.php';
752 require_once ABSPATH . 'wp-admin/includes/media.php';
753 require_once ABSPATH . 'wp-admin/includes/file.php';
754 $post_title = pathinfo( $attachment_url, PATHINFO_FILENAME );
755 $post_name = pathinfo( $attachment_url, PATHINFO_FILENAME );
756
757 $image_id = media_sideload_image( $attachment_url, $post_id, $post_title, $post_name );
758 set_post_thumbnail($post_id, $image_id);
759
760 return $image_id;
761 }
762
763 /**
764 * Create new posts based on import information
765 *
766 * Posts marked as having a parent which doesn't exist will become top level items.
767 * Doesn't create a new post if: the post type doesn't exist, the given post ID
768 * is already noted as imported or a post with the same title and date already exists.
769 * Note that new/updated terms, comments and meta are imported for the last of the above.
770 *
771 * @return array the ids of succeed/failed imported posts.
772 */
773 private function process_posts(): array {
774 $result = [
775 'succeed' => [],
776 'failed' => [],
777 ];
778
779 $this->posts = apply_filters( 'wp_import_posts', $this->posts );
780
781 foreach ( $this->posts as $post ) {
782 $post = apply_filters( 'wp_import_post_data_raw', $post );
783
784 if ( ! post_type_exists( $post['post_type'] ) ) {
785 /* translators: 1: Post title, 2: Post type. */
786 $this->output['errors'][] = sprintf( esc_html__( 'Failed to import %1$s: Invalid post type %2$s', 'betterdocs' ), $post['post_title'], $post['post_type'] );
787 do_action( 'wp_import_post_exists', $post );
788 continue;
789 }
790
791 if ( isset( $this->processed_posts[ $post['post_id'] ] ) && ! empty( $post['post_id'] ) ) {
792 continue;
793 }
794
795 if ( 'auto-draft' === $post['status'] ) {
796 continue;
797 }
798
799 if ( 'nav_menu_item' === $post['post_type'] ) {
800 $result['succeed'] += $this->process_menu_item( $post );
801 continue;
802 }
803
804 if ( 'wp_navigation' === $post['post_type'] ) {
805 $processed = $this->process_navigation( $post );
806 if( ! $processed ) {
807 continue;
808 }
809 }
810
811 $post_type_object = get_post_type_object( $post['post_type'] );
812
813 $post_parent = (int) $post['post_parent'];
814 if ( $post_parent ) {
815 // if we already know the parent, map it to the new local ID.
816 if ( isset( $this->processed_posts[ $post_parent ] ) ) {
817 $post_parent = $this->processed_posts[ $post_parent ];
818 // otherwise record the parent for later.
819 } else {
820 $this->post_orphans[ (int) $post['post_id'] ] = $post_parent;
821 $post_parent = 0;
822 }
823 }
824
825 // Map the post author.
826 $author = sanitize_user( $post['post_author'], true );
827 if ( isset( $this->author_mapping[ $author ] ) ) {
828 $author = $this->author_mapping[ $author ];
829 } else {
830 $author = (int) get_current_user_id();
831 }
832
833 $postdata = [
834 'post_author' => $author,
835 'post_content' => isset($post['post_content']) ? $post['post_content'] : '',
836 'post_excerpt' => isset($post['post_excerpt']) ? $post['post_excerpt'] : '',
837 'post_title' => isset($post['post_title']) ? $post['post_title'] : '',
838 'post_status' => isset($post['status']) ? $post['status'] : '',
839 'post_name' => isset($post['post_name']) ? $post['post_name'] : '',
840 'comment_status' => isset($post['comment_status']) ? $post['comment_status'] : '',
841 'ping_status' => isset($post['ping_status']) ? $post['ping_status'] : '',
842 'guid' => isset($post['guid']) ? $post['guid'] : '',
843 'post_parent' => $post_parent,
844 'menu_order' => isset($post['menu_order']) ? $post['menu_order'] : '',
845 'post_type' => isset($post['post_type']) ? $post['post_type'] : '',
846 'post_password' => isset($post['post_password']) ? $post['post_password'] : '',
847 ];
848
849 $original_post_id = $post['post_id'];
850 $postdata = apply_filters( 'wp_import_post_data_processed', $postdata, $post );
851 $postdata = wp_slash( $postdata );
852
853 if ( 'attachment' === $postdata['post_type'] ) {
854 $remote_url = ! empty( $post['attachment_url'] ) ? $post['attachment_url'] : $post['guid'];
855
856 // try to use _wp_attached file for upload folder placement to ensure the same location as the export site
857 // e.g. location is 2003/05/image.jpg but the attachment post_date is 2010/09, see media_handle_upload()
858 $postdata['upload_date'] = isset($post['post_date']) ? $post['post_date'] : '';
859 if ( isset( $post['postmeta'] ) ) {
860 foreach ( $post['postmeta'] as $meta ) {
861 if ( '_wp_attached_file' === $meta['key'] ) {
862 if ( preg_match( '%^[0-9]{4}/[0-9]{2}%', $meta['value'], $matches ) ) {
863 $postdata['upload_date'] = $matches[0];
864 }
865 break;
866 }
867 }
868 }
869
870 // $post_id = $this->set_post_thumbnail( $postdata['post_parent'], $remote_url );
871 $post_id = $this->process_attachment( $postdata, $remote_url );
872
873 $comment_post_id = $post_id;
874 } else {
875 $post_id = wp_insert_post( $postdata, true );
876
877 $this->update_post_meta( $post_id );
878
879 $comment_post_id = $post_id;
880 do_action( 'wp_import_insert_post', $post_id, $original_post_id, $postdata, $post );
881 }
882
883 if ( is_wp_error( $post_id ) ) {
884 /* translators: 1: Post type singular label, 2: Post title. */
885 $error = sprintf( __( 'Failed to import %1$s %2$s', 'betterdocs' ), $post_type_object->labels->singular_name, $post['post_title'] );
886
887 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
888 $error .= PHP_EOL . $post_id->get_error_message();
889 }
890
891 $result['failed'][] = $original_post_id;
892
893 $this->output['errors'][] = $error;
894
895 continue;
896 }
897
898 $result['succeed'][ $original_post_id ] = $post_id;
899
900 if ( isset( $post['is_sticky'] ) && 1 === $post['is_sticky'] ) {
901 stick_post( $post_id );
902 }
903
904 if ( $this->page_on_front === $original_post_id ) {
905 update_option( 'page_on_front', $post_id );
906 }
907
908 // Map pre-import ID to local ID.
909 $this->processed_posts[ (int) $post['post_id'] ] = (int) $post_id;
910
911 if ( ! isset( $post['terms'] ) ) {
912 $post['terms'] = [];
913 }
914
915 $post['terms'] = apply_filters( 'wp_import_post_terms', $post['terms'], $post_id, $post );
916
917 // add categories, tags and other terms
918 if ( ! empty( $post['terms'] ) ) {
919 $terms_to_set = [];
920 foreach ( $post['terms'] as $term ) {
921 // back compat with WXR 1.0 map 'tag' to 'post_tag'
922 $taxonomy = ( 'tag' === $term['domain'] ) ? 'post_tag' : $term['domain'];
923 $term_exists = term_exists( $term['slug'], $taxonomy );
924 $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists;
925 if ( ! $term_id ) {
926 $t = wp_insert_term( $term['name'], $taxonomy, [ 'slug' => $term['slug'] ] );
927 if ( ! is_wp_error( $t ) ) {
928 $term_id = $t['term_id'];
929
930 $this->update_term_meta( $term_id );
931
932 do_action( 'wp_import_insert_term', $t, $term, $post_id, $post );
933 } else {
934 /* translators: 1: Taxonomy name, 2: Term name. */
935 $error = sprintf( esc_html__( 'Failed to import %1$s %2$s', 'betterdocs' ), $taxonomy, $term['name'] );
936
937 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
938 $error .= PHP_EOL . $t->get_error_message();
939 }
940
941 $this->output['errors'][] = $error;
942
943 do_action( 'wp_import_insert_term_failed', $t, $term, $post_id, $post );
944 continue;
945 }
946 }
947 $terms_to_set[ $taxonomy ][] = (int) $term_id;
948 }
949
950 foreach ( $terms_to_set as $tax => $ids ) {
951 $tt_ids = wp_set_post_terms( $post_id, $ids, $tax );
952 do_action( 'wp_import_set_post_terms', $tt_ids, $ids, $tax, $post_id, $post );
953 }
954 unset( $post['terms'], $terms_to_set );
955 }
956
957 if ( ! isset( $post['comments'] ) ) {
958 $post['comments'] = [];
959 }
960
961 $post['comments'] = apply_filters( 'wp_import_post_comments', $post['comments'], $post_id, $post );
962
963 // Add/update comments.
964 if ( ! empty( $post['comments'] ) ) {
965 $num_comments = 0;
966 $inserted_comments = [];
967 foreach ( $post['comments'] as $comment ) {
968 $comment_id = $comment['comment_id'];
969 $newcomments[ $comment_id ]['comment_post_ID'] = $comment_post_id;
970 $newcomments[ $comment_id ]['comment_author'] = $comment['comment_author'];
971 $newcomments[ $comment_id ]['comment_author_email'] = $comment['comment_author_email'];
972 $newcomments[ $comment_id ]['comment_author_IP'] = $comment['comment_author_IP'];
973 $newcomments[ $comment_id ]['comment_author_url'] = $comment['comment_author_url'];
974 $newcomments[ $comment_id ]['comment_date'] = $comment['comment_date'];
975 $newcomments[ $comment_id ]['comment_date_gmt'] = $comment['comment_date_gmt'];
976 $newcomments[ $comment_id ]['comment_content'] = $comment['comment_content'];
977 $newcomments[ $comment_id ]['comment_approved'] = $comment['comment_approved'];
978 $newcomments[ $comment_id ]['comment_type'] = $comment['comment_type'];
979 $newcomments[ $comment_id ]['comment_parent'] = $comment['comment_parent'];
980 $newcomments[ $comment_id ]['commentmeta'] = isset( $comment['commentmeta'] ) ? $comment['commentmeta'] : [];
981 if ( isset( $this->processed_authors[ $comment['comment_user_id'] ] ) ) {
982 $newcomments[ $comment_id ]['user_id'] = $this->processed_authors[ $comment['comment_user_id'] ];
983 }
984 }
985
986 ksort( $newcomments );
987
988 foreach ( $newcomments as $key => $comment ) {
989 if ( isset( $inserted_comments[ $comment['comment_parent'] ] ) ) {
990 $comment['comment_parent'] = $inserted_comments[ $comment['comment_parent'] ];
991 }
992
993 $comment_data = wp_slash( $comment );
994 unset( $comment_data['commentmeta'] ); // Handled separately, wp_insert_comment() also expects `comment_meta`.
995 $comment_data = wp_filter_comment( $comment_data );
996
997 $inserted_comments[ $key ] = wp_insert_comment( $comment_data );
998
999 do_action( 'wp_import_insert_comment', $inserted_comments[ $key ], $comment, $comment_post_id, $post );
1000
1001 foreach ( $comment['commentmeta'] as $meta ) {
1002 $value = maybe_unserialize( $meta['value'] );
1003
1004 add_comment_meta( $inserted_comments[ $key ], wp_slash( $meta['key'] ), wp_slash_strings_only( $value ) );
1005 }
1006
1007 $num_comments ++;
1008 }
1009 unset( $newcomments, $inserted_comments, $post['comments'] );
1010 }
1011
1012 if ( ! isset( $post['postmeta'] ) ) {
1013 $post['postmeta'] = [];
1014 }
1015
1016 $post['postmeta'] = apply_filters( 'wp_import_post_meta', $post['postmeta'], $post_id, $post );
1017
1018 // Add/update post meta.
1019 if ( ! empty( $post['postmeta'] ) ) {
1020 foreach ( $post['postmeta'] as $meta ) {
1021 $key = apply_filters( 'import_post_meta_key', $meta['key'], $post_id, $post );
1022 $value = false;
1023
1024 if ( '_edit_last' === $key ) {
1025 if ( isset( $this->processed_authors[ (int) $meta['value'] ] ) ) {
1026 $value = $this->processed_authors[ (int) $meta['value'] ];
1027 } else {
1028 $key = false;
1029 }
1030 }
1031
1032 if ( $key ) {
1033 // Export gets meta straight from the DB so could have a serialized string.
1034 if ( ! $value ) {
1035 $value = maybe_unserialize( $meta['value'] );
1036 }
1037
1038 add_post_meta( $post_id, wp_slash( $key ), wp_slash_strings_only( $value ) );
1039
1040 do_action( 'import_post_meta', $post_id, $key, $value );
1041
1042 // If the post has a featured image, take note of this in case of remap.
1043 if ( '_thumbnail_id' === $key ) {
1044 $this->featured_images[ $post_id ] = (int) $value;
1045 }
1046 }
1047 }
1048 }
1049
1050 do_action( 'templately_import.process_post', $post, $this, $result );
1051 }
1052
1053 unset( $this->posts );
1054
1055 return $result;
1056 }
1057
1058 /**
1059 * Attempt to create a new menu item from import data
1060 *
1061 * Fails for draft, orphaned menu items and those without an associated nav_menu
1062 * or an invalid nav_menu term. If the post type or term object which the menu item
1063 * represents doesn't exist then the menu item will not be imported (waits until the
1064 * end of the import to retry again before discarding).
1065 *
1066 * @param array $item Menu item details from WXR file
1067 */
1068 private function process_menu_item( $item ) {
1069 $result = [];
1070
1071 // Skip draft, orphaned menu items.
1072 if ( 'draft' === $item['status'] ) {
1073 return;
1074 }
1075
1076 $menu_slug = false;
1077 if ( isset( $item['terms'] ) ) {
1078 // Loop through terms, assume first nav_menu term is correct menu.
1079 foreach ( $item['terms'] as $term ) {
1080 if ( 'nav_menu' === $term['domain'] ) {
1081 $menu_slug = $term['slug'];
1082 break;
1083 }
1084 }
1085 }
1086
1087 // No nav_menu term associated with this menu item.
1088 if ( ! $menu_slug ) {
1089 $this->output['errors'][] = esc_html__( 'Menu item skipped due to missing menu slug', 'betterdocs' );
1090
1091 return $result;
1092 }
1093
1094 // If menu was already exists, refer the items to the duplicated menu created.
1095 if ( array_key_exists( $menu_slug, $this->mapped_terms_slug ) ) {
1096 $menu_slug = $this->mapped_terms_slug[ $menu_slug ];
1097 }
1098
1099 $menu_id = term_exists( $menu_slug, 'nav_menu' );
1100 if ( ! $menu_id ) {
1101 /* translators: %s: Menu slug. */
1102 $this->output['errors'][] = sprintf( esc_html__( 'Menu item skipped due to invalid menu slug: %s', 'betterdocs' ), $menu_slug );
1103
1104 return $result;
1105 } else {
1106 $menu_id = is_array( $menu_id ) ? $menu_id['term_id'] : $menu_id;
1107 }
1108
1109 $post_meta_key_value = [];
1110 foreach ( $item['postmeta'] as $meta ) {
1111 $post_meta_key_value[ $meta['key'] ] = $meta['value'];
1112 }
1113
1114 $_menu_item_type = $post_meta_key_value['_menu_item_type'];
1115 $_menu_item_url = $post_meta_key_value['_menu_item_url'];
1116
1117 // Skip menu items 'taxonomy' type, when the taxonomy is not exits.
1118 if ( 'taxonomy' === $_menu_item_type && ! taxonomy_exists( $post_meta_key_value['_menu_item_object'] ) ) {
1119 return $result;
1120 }
1121
1122 // Skip menu items 'post_type' type, when the post type is not exits.
1123 if ( 'post_type' === $_menu_item_type && ! post_type_exists( $post_meta_key_value['_menu_item_object'] ) ) {
1124 return $result;
1125 }
1126
1127 $_menu_item_object_id = $post_meta_key_value['_menu_item_object_id'];
1128 if ( 'taxonomy' === $_menu_item_type && isset( $this->processed_terms[ (int) $_menu_item_object_id ] ) ) {
1129 $_menu_item_object_id = $this->processed_terms[ (int) $_menu_item_object_id ];
1130 } elseif ( 'post_type' === $_menu_item_type && isset( $this->processed_posts[ (int) $_menu_item_object_id ] ) ) {
1131 $_menu_item_object_id = $this->processed_posts[ (int) $_menu_item_object_id ];
1132 } elseif ( 'custom' === $_menu_item_type ) {
1133 $_menu_item_url = Url::migrate( $_menu_item_url, $this->base_blog_url );
1134 } else {
1135 return $result;
1136 }
1137
1138 $_menu_item_menu_item_parent = $post_meta_key_value['_menu_item_menu_item_parent'];
1139 if ( isset( $this->processed_menu_items[ (int) $_menu_item_menu_item_parent ] ) ) {
1140 $_menu_item_menu_item_parent = $this->processed_menu_items[ (int) $_menu_item_menu_item_parent ];
1141 } elseif ( $_menu_item_menu_item_parent ) {
1142 $this->menu_item_orphans[ (int) $item['post_id'] ] = (int) $_menu_item_menu_item_parent;
1143 $_menu_item_menu_item_parent = 0;
1144 }
1145
1146 // wp_update_nav_menu_item expects CSS classes as a space separated string
1147 $_menu_item_classes = maybe_unserialize( $post_meta_key_value['_menu_item_classes'] );
1148 if ( is_array( $_menu_item_classes ) ) {
1149 $_menu_item_classes = implode( ' ', $_menu_item_classes );
1150 }
1151
1152 $args = [
1153 'menu-item-object-id' => $_menu_item_object_id,
1154 'menu-item-object' => $post_meta_key_value['_menu_item_object'],
1155 'menu-item-parent-id' => $_menu_item_menu_item_parent,
1156 'menu-item-position' => (int) $item['menu_order'],
1157 'menu-item-type' => $_menu_item_type,
1158 'menu-item-title' => $item['post_title'],
1159 'menu-item-url' => $_menu_item_url,
1160 'menu-item-description' => $item['post_content'],
1161 'menu-item-attr-title' => $item['post_excerpt'],
1162 'menu-item-target' => $post_meta_key_value['_menu_item_target'],
1163 'menu-item-classes' => $_menu_item_classes,
1164 'menu-item-xfn' => $post_meta_key_value['_menu_item_xfn'],
1165 'menu-item-status' => $item['status'],
1166 ];
1167
1168 $id = wp_update_nav_menu_item( $menu_id, 0, $args );
1169 if ( $id && ! is_wp_error( $id ) ) {
1170 $this->processed_menu_items[ (int) $item['post_id'] ] = (int) $id;
1171 $result[ $item['post_id'] ] = $id;
1172
1173 $this->update_post_meta( $id );
1174 }
1175
1176 return $result;
1177 }
1178
1179 private function process_navigation( &$item ): bool {
1180 // Skip draft, orphaned menu items.
1181 if ( 'draft' === $item['status'] ) {
1182 return false;
1183 }
1184
1185 $content = parse_blocks( $item['post_content'] );
1186
1187 $parsed_blocks = [];
1188 foreach ( $content as $block ) {
1189 if ( empty( $block['blockName'] ) ) {
1190 continue;
1191 }
1192
1193 $this->prepare_block( $block );
1194 $parsed_blocks[] = $block;
1195 }
1196
1197 $item['post_content'] = serialize_blocks( $parsed_blocks );
1198
1199 return true;
1200 }
1201
1202 private function prepare_block( &$block ) {
1203 if ( $block['blockName'] == 'core/navigation-link' ) {
1204 $attrs = &$block['attrs'];
1205 switch ( $attrs['kind'] ) {
1206 case 'post-type':
1207 if ( isset( $this->processed_posts[ (int) $attrs['id'] ] ) ) {
1208 $attrs['id'] = $this->processed_posts[ (int) $attrs['id'] ];
1209 $attrs['url'] = get_permalink( $attrs['id'] );
1210 }
1211 break;
1212 case 'taxonomy':
1213 if ( isset( $this->processed_terms[ (int) $attrs['id'] ] ) ) {
1214 $attrs['id'] = $this->processed_terms[ (int) $attrs['id'] ];
1215 $attrs['url'] = get_term_link( $attrs['id'], $attrs['type'] );
1216 }
1217 break;
1218 }
1219 }
1220 }
1221
1222 /**
1223 * If fetching attachments is enabled then attempt to create a new attachment
1224 *
1225 * @param array $post Attachment post details from WXR
1226 * @param string $url URL to fetch attachment from
1227 *
1228 * @return int|WP_Error Post ID on success, WP_Error otherwise
1229 */
1230 private function process_attachment( $post, $url ) {
1231 require_once ABSPATH . 'wp-admin/includes/image.php';
1232
1233 if ( ! $this->fetch_attachments ) {
1234 return new WP_Error( 'attachment_processing_error', esc_html__( 'Fetching attachments is not enabled', 'betterdocs' ) );
1235 }
1236
1237 // if the URL is absolute, but does not contain address, then upload it assuming base_site_url.
1238 if ( preg_match( '|^/[\w\W]+$|', $url ) ) {
1239 $url = rtrim( $this->base_url, '/' ) . $url;
1240 }
1241
1242 $upload = $this->fetch_remote_file( $url, $post );
1243 if ( is_wp_error( $upload ) ) {
1244 return $upload;
1245 }
1246
1247 $info = wp_check_filetype( $upload['file'] );
1248 if ( $info ) {
1249 $post['post_mime_type'] = $info['type'];
1250 } else {
1251 return new WP_Error( 'attachment_processing_error', esc_html__( 'Invalid file type', 'betterdocs' ) );
1252 }
1253
1254 $post['guid'] = $upload['url'];
1255
1256 // As per wp-admin/includes/upload.php.
1257 $post_id = wp_insert_attachment( $post, $upload['file'] );
1258 $this->update_post_meta( $post_id );
1259
1260 wp_update_attachment_metadata( $post_id, wp_generate_attachment_metadata( $post_id, $upload['file'] ) );
1261
1262 // Remap resized image URLs, works by stripping the extension and remapping the URL stub.
1263 if ( preg_match( '!^image/!', $info['type'] ) ) {
1264 $parts = pathinfo( $url );
1265 $name = basename( $parts['basename'], ".{$parts['extension']}" ); // PATHINFO_FILENAME in PHP 5.2
1266
1267 $parts_new = pathinfo( $upload['url'] );
1268 $name_new = basename( $parts_new['basename'], ".{$parts_new['extension']}" );
1269
1270 $this->url_remap[ $parts['dirname'] . '/' . $name ] = $parts_new['dirname'] . '/' . $name_new;
1271 }
1272
1273 return $post_id;
1274 }
1275
1276 /**
1277 * Attempt to download a remote file attachment
1278 *
1279 * @param string $url URL of item to fetch
1280 * @param array $post Attachment details
1281 *
1282 * @return array|WP_Error Local file location details on success, WP_Error otherwise
1283 */
1284 private function fetch_remote_file( $url, $post ) {
1285 include_once ABSPATH . '/wp-admin/includes/file.php';
1286
1287 // Extract the file name from the URL.
1288 $file_name = basename( parse_url( $url, PHP_URL_PATH ) );
1289
1290 if ( ! $file_name ) {
1291 $file_name = md5( $url );
1292 }
1293
1294 $tmp_file_name = wp_tempnam( $file_name );
1295 if ( ! $tmp_file_name ) {
1296 return new WP_Error( 'import_no_file', esc_html__( 'Could not create temporary file.', 'betterdocs' ) );
1297 }
1298
1299 // Fetch the remote URL and write it to the placeholder file.
1300 $remote_response = wp_safe_remote_get( $url, [
1301 'timeout' => 300,
1302 'stream' => true,
1303 'filename' => $tmp_file_name,
1304 'headers' => [
1305 'Accept-Encoding' => 'identity',
1306 ]
1307 ] );
1308
1309 if ( is_wp_error( $remote_response ) ) {
1310 @unlink( $tmp_file_name );
1311
1312 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() ) ) );
1313 }
1314
1315 $remote_response_code = (int) wp_remote_retrieve_response_code( $remote_response );
1316
1317 // Make sure the fetch was successful.
1318 if ( 200 !== $remote_response_code ) {
1319 @unlink( $tmp_file_name );
1320
1321 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 ) ) );
1322 }
1323
1324 $headers = wp_remote_retrieve_headers( $remote_response );
1325
1326 // Request failed.
1327 if ( ! $headers ) {
1328 @unlink( $tmp_file_name );
1329
1330 return new WP_Error( 'import_file_error', esc_html__( 'Remote server did not respond', 'betterdocs' ) );
1331 }
1332
1333 $filesize = (int) filesize( $tmp_file_name );
1334
1335 if ( 0 === $filesize ) {
1336 @unlink( $tmp_file_name );
1337
1338 return new WP_Error( 'import_file_error', esc_html__( 'Zero size file downloaded', 'betterdocs' ) );
1339 }
1340
1341 if ( ! isset( $headers['content-encoding'] ) && isset( $headers['content-length'] ) && $filesize !== (int) $headers['content-length'] ) {
1342 @unlink( $tmp_file_name );
1343
1344 return new WP_Error( 'import_file_error', esc_html__( 'Downloaded file has incorrect size', 'betterdocs' ) );
1345 }
1346
1347 $max_size = (int) apply_filters( 'import_attachment_size_limit', self::DEFAULT_IMPORT_ATTACHMENT_SIZE_LIMIT );
1348 if ( ! empty( $max_size ) && $filesize > $max_size ) {
1349 @unlink( $tmp_file_name );
1350
1351 /* translators: %s: Max file size. */
1352
1353 return new WP_Error( 'import_file_error', sprintf( esc_html__( 'Remote file is too large, limit is %s', 'betterdocs' ), size_format( $max_size ) ) );
1354 }
1355
1356 // Override file name with Content-Disposition header value.
1357 if ( ! empty( $headers['content-disposition'] ) ) {
1358 $file_name_from_disposition = self::get_filename_from_disposition( (array) $headers['content-disposition'] );
1359 if ( $file_name_from_disposition ) {
1360 $file_name = $file_name_from_disposition;
1361 }
1362 }
1363
1364 // Set file extension if missing.
1365 $file_ext = pathinfo( $file_name, PATHINFO_EXTENSION );
1366 if ( ! $file_ext && ! empty( $headers['content-type'] ) ) {
1367 $extension = self::get_file_extension_by_mime_type( $headers['content-type'] );
1368 if ( $extension ) {
1369 $file_name = "{$file_name}.{$extension}";
1370 }
1371 }
1372
1373 // Handle the upload like _wp_handle_upload() does.
1374 $wp_filetype = wp_check_filetype_and_ext( $tmp_file_name, $file_name );
1375 $ext = empty( $wp_filetype['ext'] ) ? '' : $wp_filetype['ext'];
1376 $type = empty( $wp_filetype['type'] ) ? '' : $wp_filetype['type'];
1377 $proper_filename = empty( $wp_filetype['proper_filename'] ) ? '' : $wp_filetype['proper_filename'];
1378
1379 // Check to see if wp_check_filetype_and_ext() determined the filename was incorrect.
1380 if ( $proper_filename ) {
1381 $file_name = $proper_filename;
1382 }
1383
1384 if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
1385 return new WP_Error( 'import_file_error', esc_html__( 'Sorry, this file type is not permitted for security reasons.', 'betterdocs' ) );
1386 }
1387
1388 $uploads = wp_upload_dir( $post['upload_date'] );
1389 if ( ! ( $uploads && false === $uploads['error'] ) ) {
1390 return new WP_Error( 'upload_dir_error', $uploads['error'] );
1391 }
1392
1393 // Move the file to the uploads dir.
1394 $file_name = wp_unique_filename( $uploads['path'], $file_name );
1395 $new_file = $uploads['path'] . "/$file_name";
1396 $move_new_file = copy( $tmp_file_name, $new_file );
1397
1398 if ( ! $move_new_file ) {
1399 @unlink( $tmp_file_name );
1400
1401 return new WP_Error( 'import_file_error', esc_html__( 'The uploaded file could not be moved', 'betterdocs' ) );
1402 }
1403
1404 // Set correct file permissions.
1405 $stat = stat( dirname( $new_file ) );
1406 $perms = $stat['mode'] & 0000666;
1407 chmod( $new_file, $perms );
1408
1409 $upload = [
1410 'file' => $new_file,
1411 'url' => $uploads['url'] . "/$file_name",
1412 'type' => $wp_filetype['type'],
1413 'error' => false,
1414 ];
1415
1416 // Keep track of the old and new urls so we can substitute them later.
1417 $this->url_remap[ $url ] = $upload['url'];
1418 $this->url_remap[ $post['guid'] ] = $upload['url']; // r13735, really needed?
1419 // Keep track of the destination if the remote url is redirected somewhere else.
1420 if ( isset( $headers['x-final-location'] ) && $headers['x-final-location'] !== $url ) {
1421 $this->url_remap[ $headers['x-final-location'] ] = $upload['url'];
1422 }
1423
1424 return $upload;
1425 }
1426
1427 /**
1428 * Attempt to associate posts and menu items with previously missing parents
1429 *
1430 * An imported post's parent may not have been imported when it was first created
1431 * so try again. Similarly for child menu items and menu items which were missing
1432 * the object (e.g. post) they represent in the menu
1433 */
1434 private function backfill_parents() {
1435 global $wpdb;
1436
1437 // Find parents for post orphans.
1438 foreach ( $this->post_orphans as $child_id => $parent_id ) {
1439 $local_child_id = false;
1440 $local_parent_id = false;
1441
1442 if ( isset( $this->processed_posts[ $child_id ] ) ) {
1443 $local_child_id = $this->processed_posts[ $child_id ];
1444 }
1445 if ( isset( $this->processed_posts[ $parent_id ] ) ) {
1446 $local_parent_id = $this->processed_posts[ $parent_id ];
1447 }
1448
1449 if ( $local_child_id && $local_parent_id ) {
1450 $wpdb->update( $wpdb->posts, [ 'post_parent' => $local_parent_id ], [ 'ID' => $local_child_id ], '%d', '%d' );
1451 clean_post_cache( $local_child_id );
1452 }
1453 }
1454
1455 // Find parents for menu item orphans.
1456 foreach ( $this->menu_item_orphans as $child_id => $parent_id ) {
1457 $local_child_id = 0;
1458 $local_parent_id = 0;
1459 if ( isset( $this->processed_menu_items[ $child_id ] ) ) {
1460 $local_child_id = $this->processed_menu_items[ $child_id ];
1461 }
1462 if ( isset( $this->processed_menu_items[ $parent_id ] ) ) {
1463 $local_parent_id = $this->processed_menu_items[ $parent_id ];
1464 }
1465
1466 if ( $local_child_id && $local_parent_id ) {
1467 update_post_meta( $local_child_id, '_menu_item_menu_item_parent', (int) $local_parent_id );
1468 }
1469 }
1470 }
1471
1472 /**
1473 * Use stored mapping information to update old attachment URLs
1474 */
1475 private function backfill_attachment_urls() {
1476 global $wpdb;
1477 // Make sure we do the longest urls first, in case one is a substring of another.
1478 uksort( $this->url_remap, function ( $a, $b ) {
1479 // Return the difference in length between two strings.
1480 return strlen( $b ) - strlen( $a );
1481 } );
1482
1483 foreach ( $this->url_remap as $from_url => $to_url ) {
1484 // Remap urls in post_content.
1485 $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s)", $from_url, $to_url ) );
1486 // Remap enclosure urls.
1487 $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_key='enclosure'", $from_url, $to_url ) );
1488 }
1489 }
1490
1491 /**
1492 * Update _thumbnail_id meta to new, imported attachment IDs
1493 */
1494 private function remap_featured_images() {
1495 // Cycle through posts that have a featured image.
1496 foreach ( $this->featured_images as $post_id => $value ) {
1497 if ( isset( $this->processed_posts[ $value ] ) ) {
1498 $new_id = $this->processed_posts[ $value ];
1499 // Only update if there's a difference.
1500 if ( $new_id !== $value ) {
1501 update_post_meta( $post_id, '_thumbnail_id', $new_id );
1502 }
1503 }
1504 }
1505 }
1506
1507 /**
1508 * Parse a WXR file
1509 *
1510 * @param string $file Path to WXR file for parsing
1511 *
1512 * @return array Information gathered from the WXR file
1513 */
1514 private function parse( $file ): array {
1515 if ($this->file_type == 'text/xml') {
1516 $parser = new WXR_Parser();
1517 } else if ($this->file_type == 'text/csv') {
1518 $parser = new CSV_Parser();
1519 }
1520 return $parser->parse( $file );
1521 }
1522
1523 /**
1524 * Decide if the given meta key maps to information we will want to import
1525 *
1526 * @param string $key The meta key to check
1527 *
1528 * @return string|bool The key if we do want to import, false if not
1529 */
1530 private function is_valid_meta_key( $key ) {
1531 // Skip attachment metadata since we'll regenerate it from scratch.
1532 // Skip _edit_lock as not relevant for import
1533 if ( in_array( $key, [ '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock' ] ) ) {
1534 return false;
1535 }
1536
1537 return $key;
1538 }
1539
1540 /**
1541 * @param $term
1542 *
1543 * @return mixed
1544 */
1545 private function handle_duplicated_nav_menu_term( $term ) {
1546 $duplicate_slug = $term['slug'] . '-duplicate';
1547 $duplicate_name = $term['term_name'] . ' duplicate';
1548
1549 while ( term_exists( $duplicate_slug, 'nav_menu' ) ) {
1550 $duplicate_slug .= '-duplicate';
1551 $duplicate_name .= ' duplicate';
1552 }
1553
1554 $this->mapped_terms_slug[ $term['slug'] ] = $duplicate_slug;
1555
1556 $term['slug'] = $duplicate_slug;
1557 $term['term_name'] = $duplicate_name;
1558
1559 return $term;
1560 }
1561
1562 /**
1563 * Add all term_meta to specified term.
1564 *
1565 * @param $term_id
1566 *
1567 * @return void
1568 */
1569 private function update_term_meta( $term_id ) {
1570 foreach ( $this->terms_meta as $meta_key => $meta_value ) {
1571 update_term_meta( $term_id, $meta_key, $meta_value );
1572 }
1573 }
1574
1575 /**
1576 * Add all post_meta to specified term.
1577 *
1578 * @param $post_id
1579 *
1580 * @return void
1581 */
1582 private function update_post_meta( $post_id ) {
1583 foreach ( $this->posts_meta as $meta_key => $meta_value ) {
1584 update_post_meta( $post_id, $meta_key, $meta_value );
1585 }
1586 }
1587
1588 public function run(): array {
1589 $this->import( $this->requested_file_path );
1590
1591 return $this->output;
1592 }
1593
1594 /**
1595 * @param $file
1596 * @param array $args
1597 */
1598 public function __construct( $file, array $args = [] ) {
1599 parent::__construct();
1600
1601 $this->requested_file_path = $file;
1602 $this->args = $args;
1603
1604 if ( ! empty( $this->args['fetch_attachments'] ) ) {
1605 $this->fetch_attachments = true;
1606 }
1607
1608 if ( isset( $this->args['posts'] ) && is_array( $this->args['posts'] ) ) {
1609 $this->processed_posts = $this->args['posts'];
1610 }
1611
1612 if ( isset( $this->args['terms'] ) && is_array( $this->args['terms'] ) ) {
1613 $this->processed_terms = $this->args['terms'];
1614 }
1615
1616 if ( isset( $this->args['taxonomies'] ) && is_array( $this->args['taxonomies'] ) ) {
1617 $this->processed_taxonomies = $this->args['taxonomies'];
1618 }
1619
1620 if ( ! empty( $this->args['posts_meta'] ) ) {
1621 $this->posts_meta = $this->args['posts_meta'];
1622 }
1623
1624 if ( ! empty( $this->args['terms_meta'] ) ) {
1625 $this->terms_meta = $this->args['terms_meta'];
1626 }
1627
1628 if ( ! empty( $this->args['file_type'] ) ) {
1629 $this->file_type = $this->args['file_type'];
1630 }
1631 }
1632 }
1633