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

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

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