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

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