PluginProbe
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot / 4.2.0
BetterDocs – AI Documentation, Knowledge Base, MCP Server, Docs, Wikis, FAQ & Chatbot v4.2.0
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 4.2.0, at includes/Admin/Importer/WPImport.php

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