PluginProbe
Solace Extra / trunk
Solace Extra vtrunk
1.7.1 1.7.0 1.6.2 1.6.1 1.6.0 1.5.3 trunk 1.0.8 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.7 1.1.8 1.1.9 1.2.1 1.2.2 1.3.0 1.3.1 1.3.2 1.3.3 1.5.0 1.5.1 All 26 releases
solace-extra / admin / export-import / class-wp-import.php

class-wp-import.php in Solace Extra trunk, at admin/export-import/class-wp-import.php

1,773 lines 60.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 exit;
4 }
5 /**
6 * WordPress Importer class for managing the import process of a WXR file
7 *
8 * @package WordPress
9 * @subpackage Importer
10 */
11
12 // phpcs:disable WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- WordPress importer uses core/legacy hook names for compatibility.
13
14 if ( ! isset( $wp_filesystem ) || ! $wp_filesystem->method ) {
15 require_once ABSPATH . '/wp-admin/includes/file.php';
16 WP_Filesystem();
17 }
18
19 /**
20 * WordPress importer class.
21 */
22 class Solace_Extra_WP_Import extends WP_Importer {
23 public $max_wxr_version = 1.2; // max. supported WXR version
24
25 public $id; // WXR attachment ID
26
27 // information to import from WXR file
28 public $version;
29 public $authors = array();
30 public $posts = array();
31 public $terms = array();
32 public $categories = array();
33 public $tags = array();
34 public $base_url = '';
35
36 // mappings from old information to new
37 public $processed_authors = array();
38 public $author_mapping = array();
39 public $processed_terms = array();
40 public $processed_posts = array();
41 public $post_orphans = array();
42 public $processed_menu_items = array();
43 public $menu_item_orphans = array();
44 public $missing_menu_items = array();
45
46 public $fetch_attachments = false;
47 public $url_remap = array();
48 public $featured_images = array();
49
50 /**
51 * Cache for Elementor media URL -> attachment ID lookups.
52 *
53 * @var array<string,int>
54 */
55 private $elementor_media_cache = array();
56
57 /**
58 * Registered callback function for the WordPress Importer
59 *
60 * Manages the three separate stages of the WXR import process
61 */
62 public function dispatch() {
63 $this->header();
64
65 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
66 $step = empty( $_GET['step'] ) ? 0 : (int) $_GET['step'];
67 switch ( $step ) {
68 case 0:
69 $this->greet();
70 break;
71 case 1:
72 check_admin_referer( 'import-upload' );
73 if ( $this->handle_upload() ) {
74 $this->import_options();
75 }
76 break;
77 case 2:
78 check_admin_referer( 'import-wordpress' );
79 $this->fetch_attachments = ( ! empty( $_POST['fetch_attachments'] ) && $this->allow_fetch_attachments() );
80 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotValidated
81 $this->id = (int) $_POST['import_id'];
82 $file = get_attached_file( $this->id );
83 // phpcs:disable Squiz.PHP.DiscouragedFunctions.Discouraged
84 set_time_limit( 0 );
85 $this->import( $file );
86 break;
87 }
88
89 $this->footer();
90 }
91
92 /**
93 * The main controller for the actual import stage.
94 *
95 * @param string $file Path to the WXR file for importing
96 */
97 public function import( $file ) {
98 add_filter( 'import_post_meta_key', array( $this, 'is_valid_meta_key' ) );
99 add_filter( 'http_request_timeout', array( &$this, 'bump_request_timeout' ) );
100
101 $this->import_start( $file );
102
103 $this->get_author_mapping();
104
105 wp_suspend_cache_invalidation( true );
106 $this->process_categories();
107 $this->process_tags();
108 $this->process_terms();
109 $this->process_posts();
110 wp_suspend_cache_invalidation( false );
111
112 // update incorrect/missing information in the DB
113 $this->backfill_parents();
114 $this->backfill_attachment_urls();
115 $this->remap_featured_images();
116 $this->remap_elementor_attachment_ids();
117
118 $this->import_end();
119 }
120
121 /**
122 * Parses the WXR file and prepares us for the task of processing parsed data
123 *
124 * @param string $file Path to the WXR file for importing
125 */
126 public function import_start( $file ) {
127 if ( ! is_file( $file ) ) {
128 echo '<p><strong>' . esc_html__( 'Sorry, there has been an error.', 'solace-extra' ) . '</strong><br />';
129 echo esc_html__( 'The file does not exist, please try again.', 'solace-extra' ) . '</p>';
130 $this->footer();
131 die();
132 }
133
134 $import_data = $this->parse( $file );
135
136 if ( is_wp_error( $import_data ) ) {
137 echo '<p><strong>' . esc_html__( 'Sorry, there has been an error.', 'solace-extra' ) . '</strong><br />';
138 echo esc_html( $import_data->get_error_message() ) . '</p>';
139 $this->footer();
140 die();
141 }
142
143 $this->version = $import_data['version'];
144 $this->get_authors_from_import( $import_data );
145 $this->posts = $import_data['posts'];
146 $this->terms = $import_data['terms'];
147 $this->categories = $import_data['categories'];
148 $this->tags = $import_data['tags'];
149 $this->base_url = esc_url( $import_data['base_url'] );
150
151 wp_defer_term_counting( true );
152 wp_defer_comment_counting( true );
153
154 do_action( 'import_start' );
155 }
156
157 /**
158 * Performs post-import cleanup of files and the cache
159 */
160 public function import_end() {
161 wp_import_cleanup( $this->id );
162
163 wp_cache_flush();
164 foreach ( get_taxonomies() as $tax ) {
165 delete_option( "{$tax}_children" );
166 _get_term_hierarchy( $tax );
167 }
168
169 wp_defer_term_counting( false );
170 wp_defer_comment_counting( false );
171
172 // echo '<p>' . __( 'All done.', 'solace-extra' ) . ' <a href="' . admin_url() . '">' . __( 'Have fun!', 'solace-extra' ) . '</a>' . '</p>';
173 // echo '<p>' . __( 'Remember to update the passwords and roles of imported users.', 'solace-extra' ) . '</p>';
174
175 do_action( 'import_end' );
176 }
177
178 /**
179 * Handles the WXR upload and initial parsing of the file to prepare for
180 * displaying author import options
181 *
182 * @return bool False if error uploading or invalid file, true otherwise
183 */
184 public function handle_upload() {
185 $file = wp_import_handle_upload();
186
187 if ( isset( $file['error'] ) ) {
188 echo '<p><strong>' . esc_html__( 'Sorry, there has been an error.', 'solace-extra' ) . '</strong><br />';
189 echo esc_html( $file['error'] ) . '</p>';
190 return false;
191 } elseif ( ! file_exists( $file['file'] ) ) {
192 echo '<p><strong>' . esc_html__( 'Sorry, there has been an error.', 'solace-extra' ) . '</strong><br />';
193 /* translators: %s: The export file could not be found at */
194 printf( esc_html__( 'The export file could not be found at <code>%s</code>. It is likely that this was caused by a permissions problem.', 'solace-extra' ), esc_html( $file['file'] ) );
195 echo '</p>';
196 return false;
197 }
198
199 $this->id = (int) $file['id'];
200 $import_data = $this->parse( $file['file'] );
201 if ( is_wp_error( $import_data ) ) {
202 echo '<p><strong>' . esc_html__( 'Sorry, there has been an error.', 'solace-extra' ) . '</strong><br />';
203 echo esc_html( $import_data->get_error_message() ) . '</p>';
204 return false;
205 }
206
207 $this->version = $import_data['version'];
208 if ( $this->version > $this->max_wxr_version ) {
209 echo '<div class="error"><p><strong>';
210 /* translators: %s: This WXR file */
211 printf( esc_html__( 'This WXR file (version %s) may not be supported by this version of the importer. Please consider updating.', 'solace-extra' ), esc_html( $import_data['version'] ) );
212 echo '</strong></p></div>';
213 }
214
215 $this->get_authors_from_import( $import_data );
216
217 return true;
218 }
219
220 /**
221 * Retrieve authors from parsed WXR data
222 *
223 * Uses the provided author information from WXR 1.1 files
224 * or extracts info from each post for WXR 1.0 files
225 *
226 * @param array $import_data Data returned by a WXR parser
227 */
228 public function get_authors_from_import( $import_data ) {
229 if ( ! empty( $import_data['authors'] ) ) {
230 $this->authors = $import_data['authors'];
231 // no author information, grab it from the posts
232 } else {
233 foreach ( $import_data['posts'] as $post ) {
234 $login = sanitize_user( $post['post_author'], true );
235 if ( empty( $login ) ) {
236 /* translators: %s: Failed to import author */
237 printf( esc_html__( 'Failed to import author %s. Their posts will be attributed to the current user.', 'solace-extra' ), esc_html( $post['post_author'] ) );
238 echo '<br />';
239 continue;
240 }
241
242 if ( ! isset( $this->authors[ $login ] ) ) {
243 $this->authors[ $login ] = array(
244 'author_login' => $login,
245 'author_display_name' => $post['post_author'],
246 );
247 }
248 }
249 }
250 }
251
252 /**
253 * Display pre-import options, author importing/mapping and option to
254 * fetch attachments
255 */
256 public function import_options() {
257 $j = 0;
258 // phpcs:disable Generic.WhiteSpace.ScopeIndent.Incorrect
259 ?>
260 <form action="<?php echo esc_url( admin_url( 'admin.php?import=wordpress&amp;step=2' ) ); ?>" method="post">
261 <?php wp_nonce_field( 'import-wordpress' ); ?>
262 <input type="hidden" name="import_id" value="<?php echo esc_html( $this->id ); ?>" />
263
264 <?php if ( ! empty( $this->authors ) ) : ?>
265 <h3><?php esc_html_e( 'Assign Authors', 'solace-extra' ); ?></h3>
266 <p><?php esc_html_e( 'To make it simpler for you to edit and save the imported content, you may want to reassign the author of the imported item to an existing user of this site, such as your primary administrator account.', 'solace-extra' ); ?></p>
267 <?php if ( $this->allow_create_users() ) : ?>
268 <p>
269 <?php
270 /* translators: %s: default_role */
271 printf( esc_html__( 'If a new user is created by WordPress, a new password will be randomly generated and the new user&#8217;s role will be set as %s. Manually changing the new user&#8217;s details will be necessary.', 'solace-extra' ), esc_html( get_option( 'default_role' ) ) );
272 ?>
273 </p>
274 <?php endif; ?>
275 <ol id="authors">
276 <?php foreach ( $this->authors as $author ) : ?>
277 <li><?php $this->author_select( $j++, $author ); ?></li>
278 <?php endforeach; ?>
279 </ol>
280 <?php endif; ?>
281
282 <?php if ( $this->allow_fetch_attachments() ) : ?>
283 <h3><?php esc_html_e( 'Import Attachments', 'solace-extra' ); ?></h3>
284 <p>
285 <input type="checkbox" value="1" name="fetch_attachments" id="import-attachments" />
286 <label for="import-attachments"><?php esc_html_e( 'Download and import file attachments', 'solace-extra' ); ?></label>
287 </p>
288 <?php endif; ?>
289
290 <p class="submit"><input type="submit" class="button" value="<?php esc_attr_e( 'Submit', 'solace-extra' ); ?>" /></p>
291 </form>
292 <?php
293 // phpcs:enable Generic.WhiteSpace.ScopeIndent.Incorrect
294 }
295
296 /**
297 * Display import options for an individual author. That is, either create
298 * a new user based on import info or map to an existing user
299 *
300 * @param int $n Index for each author in the form
301 * @param array $author Author information, e.g. login, display name, email
302 */
303 public function author_select( $n, $author ) {
304 esc_html_e( 'Import author:', 'solace-extra' );
305 echo ' <strong>' . esc_html( $author['author_display_name'] );
306 if ( '1.0' != $this->version ) {
307 echo ' (' . esc_html( $author['author_login'] ) . ')';
308 }
309 echo '</strong><br />';
310
311 if ( '1.0' != $this->version ) {
312 echo '<div style="margin-left:18px">';
313 }
314
315 $create_users = $this->allow_create_users();
316 if ( $create_users ) {
317 echo '<label for="user_new_' . esc_html( $n ) . '">';
318 if ( '1.0' != $this->version ) {
319 esc_html__( 'or create new user with login name:', 'solace-extra' );
320 $value = '';
321 } else {
322 esc_html__( 'as a new user:', 'solace-extra' );
323 $value = esc_attr( sanitize_user( $author['author_login'], true ) );
324 }
325 echo '</label>';
326
327 echo ' <input type="text" id="user_new_' . esc_html( $n ) . '" name="user_new[' . esc_html( $n ) . ']" value="' . esc_html( $value ) . '" /><br />';
328 }
329
330 echo '<label for="imported_authors_' . esc_html( $n ) . '">';
331 if ( ! $create_users && '1.0' == $this->version ) {
332 esc_html_e( 'assign posts to an existing user:', 'solace-extra' );
333 } else {
334 esc_html_e( 'or assign posts to an existing user:', 'solace-extra' );
335 }
336 echo '</label>';
337
338 echo ' ' . wp_dropdown_users(
339 array(
340 'name' => "user_map[$n]",
341 'id' => 'imported_authors_' . $n,
342 'multi' => true,
343 'show_option_all' => esc_html__( '- Select -', 'solace-extra' ),
344 'show' => 'display_name_with_login',
345 'echo' => 0,
346 )
347 );
348
349 echo '<input type="hidden" name="imported_authors[' . esc_html( $n ) . ']" value="' . esc_attr( $author['author_login'] ) . '" />';
350
351 if ( '1.0' != $this->version ) {
352 echo '</div>';
353 }
354 }
355
356 /**
357 * Map old author logins to local user IDs based on decisions made
358 * in import options form. Can map to an existing user, create a new user
359 * or falls back to the current user in case of error with either of the previous
360 */
361 public function get_author_mapping() {
362 // phpcs:disable WordPress.Security.NonceVerification.Missing
363 if ( ! isset( $_POST['imported_authors'] ) ) {
364 return;
365 }
366
367 $create_users = $this->allow_create_users();
368 // phpcs:disable WordPress.Security.ValidatedSanitizedInput.MissingUnslash, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
369 foreach ( (array) $_POST['imported_authors'] as $i => $old_login ) {
370 // Multisite adds strtolower to sanitize_user. Need to sanitize here to stop breakage in process_posts.
371 $santized_old_login = sanitize_user( $old_login, true );
372 $old_id = isset( $this->authors[ $old_login ]['author_id'] ) ? intval( $this->authors[ $old_login ]['author_id'] ) : false;
373
374 if ( ! empty( $_POST['user_map'][ $i ] ) ) {
375 $user = get_userdata( intval( $_POST['user_map'][ $i ] ) );
376 if ( isset( $user->ID ) ) {
377 if ( $old_id ) {
378 $this->processed_authors[ $old_id ] = $user->ID;
379 }
380 $this->author_mapping[ $santized_old_login ] = $user->ID;
381 }
382 } elseif ( $create_users ) {
383 if ( ! empty( $_POST['user_new'][ $i ] ) ) {
384 $user_id = wp_create_user( $_POST['user_new'][ $i ], wp_generate_password() );
385 } elseif ( '1.0' != $this->version ) {
386 $user_data = array(
387 'user_login' => $old_login,
388 'user_pass' => wp_generate_password(),
389 'user_email' => isset( $this->authors[ $old_login ]['author_email'] ) ? $this->authors[ $old_login ]['author_email'] : '',
390 'display_name' => $this->authors[ $old_login ]['author_display_name'],
391 'first_name' => isset( $this->authors[ $old_login ]['author_first_name'] ) ? $this->authors[ $old_login ]['author_first_name'] : '',
392 'last_name' => isset( $this->authors[ $old_login ]['author_last_name'] ) ? $this->authors[ $old_login ]['author_last_name'] : '',
393 );
394 $user_id = wp_insert_user( $user_data );
395 }
396
397 if ( ! is_wp_error( $user_id ) ) {
398 if ( $old_id ) {
399 $this->processed_authors[ $old_id ] = $user_id;
400 }
401 $this->author_mapping[ $santized_old_login ] = $user_id;
402 } else {
403 /* translators: %s: Failed to create new user */
404 printf( esc_html__( 'Failed to create new user for %s. Their posts will be attributed to the current user.', 'solace-extra' ), esc_html( $this->authors[ $old_login ]['author_display_name'] ) );
405 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
406 echo ' ' . esc_html( $user_id->get_error_message() );
407 }
408 echo '<br />';
409 }
410 }
411
412 // failsafe: if the user_id was invalid, default to the current user
413 if ( ! isset( $this->author_mapping[ $santized_old_login ] ) ) {
414 if ( $old_id ) {
415 $this->processed_authors[ $old_id ] = (int) get_current_user_id();
416 }
417 $this->author_mapping[ $santized_old_login ] = (int) get_current_user_id();
418 }
419 }
420 }
421
422 /**
423 * Create new categories based on import information
424 *
425 * Doesn't create a new category if its slug already exists
426 */
427 public function process_categories() {
428 $this->categories = apply_filters( 'wp_import_categories', $this->categories );
429
430 if ( empty( $this->categories ) ) {
431 return;
432 }
433
434 foreach ( $this->categories as $cat ) {
435 // if the category already exists leave it alone
436 $term_id = term_exists( $cat['category_nicename'], 'category' );
437 if ( $term_id ) {
438 if ( is_array( $term_id ) ) {
439 $term_id = $term_id['term_id'];
440 }
441 if ( isset( $cat['term_id'] ) ) {
442 $this->processed_terms[ intval( $cat['term_id'] ) ] = (int) $term_id;
443 }
444 continue;
445 }
446
447 $parent = empty( $cat['category_parent'] ) ? 0 : category_exists( $cat['category_parent'] );
448 $description = isset( $cat['category_description'] ) ? $cat['category_description'] : '';
449
450 $data = array(
451 'category_nicename' => $cat['category_nicename'],
452 'category_parent' => $parent,
453 'cat_name' => wp_slash( $cat['cat_name'] ),
454 'category_description' => wp_slash( $description ),
455 );
456
457 $id = wp_insert_category( $data, true );
458 if ( ! is_wp_error( $id ) && $id > 0 ) {
459 if ( isset( $cat['term_id'] ) ) {
460 $this->processed_terms[ intval( $cat['term_id'] ) ] = $id;
461 }
462 } else {
463 /* translators: %s: Failed to import category */
464 printf( esc_html__( 'Failed to import category %s', 'solace-extra' ), esc_html( $cat['category_nicename'] ) );
465 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
466 echo ': ' . esc_html( $id->get_error_message() );
467 }
468 echo '<br />';
469 continue;
470 }
471
472 $this->process_termmeta( $cat, $id );
473 }
474
475 unset( $this->categories );
476 }
477
478 /**
479 * Create new post tags based on import information
480 *
481 * Doesn't create a tag if its slug already exists
482 */
483 public function process_tags() {
484 $this->tags = apply_filters( 'wp_import_tags', $this->tags );
485
486 if ( empty( $this->tags ) ) {
487 return;
488 }
489
490 foreach ( $this->tags as $tag ) {
491 // if the tag already exists leave it alone
492 $term_id = term_exists( $tag['tag_slug'], 'post_tag' );
493 if ( $term_id ) {
494 if ( is_array( $term_id ) ) {
495 $term_id = $term_id['term_id'];
496 }
497 if ( isset( $tag['term_id'] ) ) {
498 $this->processed_terms[ intval( $tag['term_id'] ) ] = (int) $term_id;
499 }
500 continue;
501 }
502
503 $description = isset( $tag['tag_description'] ) ? $tag['tag_description'] : '';
504 $args = array(
505 'slug' => $tag['tag_slug'],
506 'description' => wp_slash( $description ),
507 );
508
509 $id = wp_insert_term( wp_slash( $tag['tag_name'] ), 'post_tag', $args );
510 if ( ! is_wp_error( $id ) ) {
511 if ( isset( $tag['term_id'] ) ) {
512 $this->processed_terms[ intval( $tag['term_id'] ) ] = $id['term_id'];
513 }
514 } else {
515 /* translators: %s: Failed to import post tag */
516 printf( esc_html__( 'Failed to import post tag %s', 'solace-extra' ), esc_html( $tag['tag_name'] ) );
517 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
518 echo ': ' . esc_html( $id->get_error_message() );
519 }
520 echo '<br />';
521 continue;
522 }
523
524 $this->process_termmeta( $tag, $id['term_id'] );
525 }
526
527 unset( $this->tags );
528 }
529
530 /**
531 * Create new terms based on import information
532 *
533 * Doesn't create a term its slug already exists
534 */
535 public function process_terms() {
536 $this->terms = apply_filters( 'wp_import_terms', $this->terms );
537
538 if ( empty( $this->terms ) ) {
539 return;
540 }
541
542 foreach ( $this->terms as $term ) {
543 // if the term already exists in the correct taxonomy leave it alone
544 $term_id = term_exists( $term['slug'], $term['term_taxonomy'] );
545 if ( $term_id ) {
546 if ( is_array( $term_id ) ) {
547 $term_id = $term_id['term_id'];
548 }
549 if ( isset( $term['term_id'] ) ) {
550 $this->processed_terms[ intval( $term['term_id'] ) ] = (int) $term_id;
551 }
552 continue;
553 }
554
555 if ( empty( $term['term_parent'] ) ) {
556 $parent = 0;
557 } else {
558 $parent = term_exists( $term['term_parent'], $term['term_taxonomy'] );
559 if ( is_array( $parent ) ) {
560 $parent = $parent['term_id'];
561 }
562 }
563
564 $description = isset( $term['term_description'] ) ? $term['term_description'] : '';
565 $args = array(
566 'slug' => $term['slug'],
567 'description' => wp_slash( $description ),
568 'parent' => (int) $parent,
569 );
570
571 $id = wp_insert_term( wp_slash( $term['term_name'] ), $term['term_taxonomy'], $args );
572 if ( ! is_wp_error( $id ) ) {
573 if ( isset( $term['term_id'] ) ) {
574 $this->processed_terms[ intval( $term['term_id'] ) ] = $id['term_id'];
575 }
576 } else {
577 /* translators: %s: Failed to import */
578 printf( esc_html__( 'Failed to import %1$s %2$s', 'solace-extra' ), esc_html( $term['term_taxonomy'] ), esc_html( $term['term_name'] ) );
579 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
580 echo ': ' . esc_html( $id->get_error_message() );
581 }
582 echo '<br />';
583 continue;
584 }
585
586 $this->process_termmeta( $term, $id['term_id'] );
587 }
588
589 unset( $this->terms );
590 }
591
592 /**
593 * Add metadata to imported term.
594 *
595 * @since 0.6.2
596 *
597 * @param array $term Term data from WXR import.
598 * @param int $term_id ID of the newly created term.
599 */
600 protected function process_termmeta( $term, $term_id ) {
601 if ( ! isset( $term['termmeta'] ) ) {
602 $term['termmeta'] = array();
603 }
604
605 /**
606 * Filters the metadata attached to an imported term.
607 *
608 * @since 0.6.2
609 *
610 * @param array $termmeta Array of term meta.
611 * @param int $term_id ID of the newly created term.
612 * @param array $term Term data from the WXR import.
613 */
614 $term['termmeta'] = apply_filters( 'wp_import_term_meta', $term['termmeta'], $term_id, $term );
615
616 if ( empty( $term['termmeta'] ) ) {
617 return;
618 }
619
620 foreach ( $term['termmeta'] as $meta ) {
621 /**
622 * Filters the meta key for an imported piece of term meta.
623 *
624 * @since 0.6.2
625 *
626 * @param string $meta_key Meta key.
627 * @param int $term_id ID of the newly created term.
628 * @param array $term Term data from the WXR import.
629 */
630 $key = apply_filters( 'import_term_meta_key', $meta['key'], $term_id, $term );
631 if ( ! $key ) {
632 continue;
633 }
634
635 // Export gets meta straight from the DB so could have a serialized string
636 $value = $this->maybe_unserialize( $meta['value'] );
637
638 add_term_meta( $term_id, wp_slash( $key ), solace_extra_wp_slash_strings_only( $value ) );
639
640 /**
641 * Fires after term meta is imported.
642 *
643 * @since 0.6.2
644 *
645 * @param int $term_id ID of the newly created term.
646 * @param string $key Meta key.
647 * @param mixed $value Meta value.
648 */
649 do_action( 'import_term_meta', $term_id, $key, $value );
650 }
651 }
652
653 /**
654 * Create new posts based on import information
655 *
656 * Posts marked as having a parent which doesn't exist will become top level items.
657 * Doesn't create a new post if: the post type doesn't exist, the given post ID
658 * is already noted as imported or a post with the same title and date already exists.
659 * Note that new/updated terms, comments and meta are imported for the last of the above.
660 */
661 public function process_posts() {
662 $this->posts = apply_filters( 'wp_import_posts', $this->posts );
663
664 foreach ( $this->posts as $post ) {
665 $post = apply_filters( 'wp_import_post_data_raw', $post );
666
667 if ( ! post_type_exists( $post['post_type'] ) ) {
668 printf(
669 /* translators: 1: Post title, 2: Invalid post type */
670 esc_html__( 'Failed to import &#8220;%1$s&#8221;: Invalid post type %2$s', 'solace-extra' ),
671 esc_html( $post['post_title'] ),
672 esc_html( $post['post_type'] )
673 );
674 echo '<br />';
675 do_action( 'wp_import_post_exists', $post );
676 continue;
677 }
678
679 if ( isset( $this->processed_posts[ $post['post_id'] ] ) && ! empty( $post['post_id'] ) ) {
680 continue;
681 }
682
683 if ( 'auto-draft' == $post['status'] ) {
684 continue;
685 }
686
687 if ( 'nav_menu_item' == $post['post_type'] ) {
688 $this->process_menu_item( $post );
689 continue;
690 }
691
692 $post_type_object = get_post_type_object( $post['post_type'] );
693
694 $post_exists = post_exists( $post['post_title'], '', $post['post_date'], $post['post_type'] );
695
696 /**
697 * Filter ID of the existing post corresponding to post currently importing.
698 *
699 * Return 0 to force the post to be imported. Filter the ID to be something else
700 * to override which existing post is mapped to the imported post.
701 *
702 * @see post_exists()
703 * @since 0.6.2
704 *
705 * @param int $post_exists Post ID, or 0 if post did not exist.
706 * @param array $post The post array to be inserted.
707 */
708 $post_exists = apply_filters( 'wp_import_existing_post', $post_exists, $post );
709
710 if ( $post_exists && get_post_type( $post_exists ) == $post['post_type'] ) {
711 /* translators: %s: already exists */
712 // printf( __( '%1$s &#8220;%2$s&#8221; already exists.', 'solace-extra' ), $post_type_object->labels->singular_name, esc_html( $post['post_title'] ) );
713 // echo '<br />';
714 // wp_send_json(
715 // array(
716 // 'success' => false,
717 /* translators: %s: already exists */
718 // 'message' => sprintf(
719 // __( '%1$s “%2$s” already exists.', 'solace-extra' ),
720 // $post_type_object->labels->singular_name,
721 // esc_html( $post['post_title'] )
722 // ),
723 // ),
724 // 409
725 // );
726
727 // $comment_post_id = $post_exists;
728 // $post_id = $post_exists;
729 // $this->processed_posts[ intval( $post['post_id'] ) ] = intval( $post_exists );
730
731 $post['post_title'] = $post['post_title'] . esc_html__( ' - Duplicate: ', 'solace-extra' );
732 }
733
734 $post_parent = (int) $post['post_parent'];
735 if ( $post_parent ) {
736 // if we already know the parent, map it to the new local ID
737 if ( isset( $this->processed_posts[ $post_parent ] ) ) {
738 $post_parent = $this->processed_posts[ $post_parent ];
739 // otherwise record the parent for later
740 } else {
741 $this->post_orphans[ intval( $post['post_id'] ) ] = $post_parent;
742 $post_parent = 0;
743 }
744 }
745
746 // map the post author
747 $author = sanitize_user( $post['post_author'], true );
748 if ( isset( $this->author_mapping[ $author ] ) ) {
749 $author = $this->author_mapping[ $author ];
750 } else {
751 $author = (int) get_current_user_id();
752 }
753
754 $postdata = array(
755 'import_id' => $post['post_id'],
756 'post_author' => $author,
757 'post_date' => $post['post_date'],
758 'post_date_gmt' => $post['post_date_gmt'],
759 'post_content' => $post['post_content'],
760 'post_excerpt' => $post['post_excerpt'],
761 'post_title' => $post['post_title'],
762 'post_status' => $post['status'],
763 'post_name' => $post['post_name'],
764 'comment_status' => $post['comment_status'],
765 'ping_status' => $post['ping_status'],
766 'guid' => $post['guid'],
767 'post_parent' => $post_parent,
768 'menu_order' => $post['menu_order'],
769 'post_type' => $post['post_type'],
770 'post_password' => $post['post_password'],
771 );
772
773 $original_post_id = $post['post_id'];
774 $postdata = apply_filters( 'wp_import_post_data_processed', $postdata, $post );
775
776 $postdata = wp_slash( $postdata );
777
778 if ( 'attachment' == $postdata['post_type'] ) {
779 $remote_url = ! empty( $post['attachment_url'] ) ? $post['attachment_url'] : $post['guid'];
780
781 // try to use _wp_attached file for upload folder placement to ensure the same location as the export site
782 // e.g. location is 2003/05/image.jpg but the attachment post_date is 2010/09, see media_handle_upload()
783 $postdata['upload_date'] = $post['post_date'];
784 if ( isset( $post['postmeta'] ) ) {
785 foreach ( $post['postmeta'] as $meta ) {
786 if ( '_wp_attached_file' == $meta['key'] ) {
787 if ( preg_match( '%^[0-9]{4}/[0-9]{2}%', $meta['value'], $matches ) ) {
788 $postdata['upload_date'] = $matches[0];
789 }
790 break;
791 }
792 }
793 }
794
795 $comment_post_id = $this->process_attachment( $postdata, $remote_url );
796 $post_id = $comment_post_id;
797 } else {
798 $comment_post_id = wp_insert_post( $postdata, true );
799 $post_id = $comment_post_id;
800 do_action( 'wp_import_insert_post', $post_id, $original_post_id, $postdata, $post );
801
802 // Add post meta.
803 update_post_meta($post_id, 'solace_wp_importer_site_builder', true);
804
805 // Append post ID to post_title if it contains " - Duplicate:".
806 if ( strpos( $postdata['post_title'], esc_html__( ' - Duplicate:', 'solace-extra' ) ) !== false ) {
807 // Update post_title dengan ID post yang baru dibuat
808 $new_title = $postdata['post_title'] . ' #' . $post_id;
809
810 // Perbarui post dengan judul baru
811 wp_update_post( array(
812 'ID' => $post_id,
813 'post_title' => $new_title,
814 ) );
815 }
816 }
817
818 if ( is_wp_error( $post_id ) ) {
819 printf(
820 /* translators: 1: Post type singular name (e.g. "Post"), 2: Post title */
821 esc_html__( 'Failed to import %1$s &#8220;%2$s&#8221;', 'solace-extra' ),
822 esc_html( $post_type_object->labels->singular_name ),
823 esc_html( $post['post_title'] )
824 );
825 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
826 echo ': ' . esc_html( $post_id->get_error_message() );
827 }
828 echo '<br />';
829 continue;
830 }
831
832 if ( 1 == $post['is_sticky'] ) {
833 stick_post( $post_id );
834 }
835
836 // map pre-import ID to local ID
837 $this->processed_posts[ intval( $post['post_id'] ) ] = (int) $post_id;
838
839 if ( ! isset( $post['terms'] ) ) {
840 $post['terms'] = array();
841 }
842
843 $post['terms'] = apply_filters( 'wp_import_post_terms', $post['terms'], $post_id, $post );
844
845 // add categories, tags and other terms
846 if ( ! empty( $post['terms'] ) ) {
847 $terms_to_set = array();
848 foreach ( $post['terms'] as $term ) {
849 // back compat with WXR 1.0 map 'tag' to 'post_tag'
850 $taxonomy = ( 'tag' == $term['domain'] ) ? 'post_tag' : $term['domain'];
851 $term_exists = term_exists( $term['slug'], $taxonomy );
852 $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists;
853 if ( ! $term_id ) {
854 $t = wp_insert_term( $term['name'], $taxonomy, array( 'slug' => $term['slug'] ) );
855 if ( ! is_wp_error( $t ) ) {
856 $term_id = $t['term_id'];
857 do_action( 'wp_import_insert_term', $t, $term, $post_id, $post );
858 } else {
859 /* translators: %s: Failed to import */
860 printf( esc_html__( 'Failed to import %1$s %2$s', 'solace-extra' ), esc_html( $taxonomy ), esc_html( $term['name'] ) );
861 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
862 echo ': ' . esc_html( $t->get_error_message() );
863 }
864 echo '<br />';
865 do_action( 'wp_import_insert_term_failed', $t, $term, $post_id, $post );
866 continue;
867 }
868 }
869 $terms_to_set[ $taxonomy ][] = intval( $term_id );
870 }
871
872 foreach ( $terms_to_set as $tax => $ids ) {
873 $tt_ids = wp_set_post_terms( $post_id, $ids, $tax );
874 do_action( 'wp_import_set_post_terms', $tt_ids, $ids, $tax, $post_id, $post );
875 }
876 unset( $post['terms'], $terms_to_set );
877 }
878
879 if ( ! isset( $post['comments'] ) ) {
880 $post['comments'] = array();
881 }
882
883 $post['comments'] = apply_filters( 'wp_import_post_comments', $post['comments'], $post_id, $post );
884
885 // add/update comments
886 if ( ! empty( $post['comments'] ) ) {
887 $num_comments = 0;
888 $inserted_comments = array();
889 foreach ( $post['comments'] as $comment ) {
890 $comment_id = $comment['comment_id'];
891 $newcomments[ $comment_id ]['comment_post_ID'] = $comment_post_id;
892 $newcomments[ $comment_id ]['comment_author'] = $comment['comment_author'];
893 $newcomments[ $comment_id ]['comment_author_email'] = $comment['comment_author_email'];
894 $newcomments[ $comment_id ]['comment_author_IP'] = $comment['comment_author_IP'];
895 $newcomments[ $comment_id ]['comment_author_url'] = $comment['comment_author_url'];
896 $newcomments[ $comment_id ]['comment_date'] = $comment['comment_date'];
897 $newcomments[ $comment_id ]['comment_date_gmt'] = $comment['comment_date_gmt'];
898 $newcomments[ $comment_id ]['comment_content'] = $comment['comment_content'];
899 $newcomments[ $comment_id ]['comment_approved'] = $comment['comment_approved'];
900 $newcomments[ $comment_id ]['comment_type'] = $comment['comment_type'];
901 $newcomments[ $comment_id ]['comment_parent'] = $comment['comment_parent'];
902 $newcomments[ $comment_id ]['commentmeta'] = isset( $comment['commentmeta'] ) ? $comment['commentmeta'] : array();
903 if ( isset( $this->processed_authors[ $comment['comment_user_id'] ] ) ) {
904 $newcomments[ $comment_id ]['user_id'] = $this->processed_authors[ $comment['comment_user_id'] ];
905 }
906 }
907 ksort( $newcomments );
908
909 foreach ( $newcomments as $key => $comment ) {
910 // if this is a new post we can skip the comment_exists() check
911 if ( ! $post_exists || ! comment_exists( $comment['comment_author'], $comment['comment_date'] ) ) {
912 if ( isset( $inserted_comments[ $comment['comment_parent'] ] ) ) {
913 $comment['comment_parent'] = $inserted_comments[ $comment['comment_parent'] ];
914 }
915
916 $comment_data = wp_slash( $comment );
917 unset( $comment_data['commentmeta'] ); // Handled separately, wp_insert_comment() also expects `comment_meta`.
918 $comment_data = wp_filter_comment( $comment_data );
919
920 $inserted_comments[ $key ] = wp_insert_comment( $comment_data );
921
922 do_action( 'wp_import_insert_comment', $inserted_comments[ $key ], $comment, $comment_post_id, $post );
923
924 foreach ( $comment['commentmeta'] as $meta ) {
925 $value = $this->maybe_unserialize( $meta['value'] );
926
927 add_comment_meta( $inserted_comments[ $key ], wp_slash( $meta['key'] ), solace_extra_wp_slash_strings_only( $value ) );
928 }
929
930 ++$num_comments;
931 }
932 }
933 unset( $newcomments, $inserted_comments, $post['comments'] );
934 }
935
936 if ( ! isset( $post['postmeta'] ) ) {
937 $post['postmeta'] = array();
938 }
939
940 $post['postmeta'] = apply_filters( 'wp_import_post_meta', $post['postmeta'], $post_id, $post );
941
942 // add/update post meta
943 if ( ! empty( $post['postmeta'] ) ) {
944 foreach ( $post['postmeta'] as $meta ) {
945 $key = apply_filters( 'import_post_meta_key', $meta['key'], $post_id, $post );
946 $value = false;
947
948 if ( '_edit_last' == $key ) {
949 if ( isset( $this->processed_authors[ intval( $meta['value'] ) ] ) ) {
950 $value = $this->processed_authors[ intval( $meta['value'] ) ];
951 } else {
952 $key = false;
953 }
954 }
955
956 if ( $key ) {
957 // export gets meta straight from the DB so could have a serialized string
958 if ( ! $value ) {
959 $value = $this->maybe_unserialize( $meta['value'] );
960 }
961
962 add_post_meta( $post_id, wp_slash( $key ), solace_extra_wp_slash_strings_only( $value ) );
963
964 do_action( 'import_post_meta', $post_id, $key, $value );
965
966 // if the post has a featured image, take note of this in case of remap
967 if ( '_thumbnail_id' == $key ) {
968 $this->featured_images[ $post_id ] = (int) $value;
969 }
970 }
971 }
972 }
973 }
974
975 unset( $this->posts );
976 }
977
978 /**
979 * Attempt to create a new menu item from import data
980 *
981 * Fails for draft, orphaned menu items and those without an associated nav_menu
982 * or an invalid nav_menu term. If the post type or term object which the menu item
983 * represents doesn't exist then the menu item will not be imported (waits until the
984 * end of the import to retry again before discarding).
985 *
986 * @param array $item Menu item details from WXR file
987 */
988 public function process_menu_item( $item ) {
989 // skip draft, orphaned menu items
990 if ( 'draft' == $item['status'] ) {
991 return;
992 }
993
994 $menu_slug = false;
995 if ( isset( $item['terms'] ) ) {
996 // loop through terms, assume first nav_menu term is correct menu
997 foreach ( $item['terms'] as $term ) {
998 if ( 'nav_menu' == $term['domain'] ) {
999 $menu_slug = $term['slug'];
1000 break;
1001 }
1002 }
1003 }
1004
1005 // no nav_menu term associated with this menu item
1006 if ( ! $menu_slug ) {
1007 esc_html_e( 'Menu item skipped due to missing menu slug', 'solace-extra' );
1008 echo '<br />';
1009 return;
1010 }
1011
1012 $menu_id = term_exists( $menu_slug, 'nav_menu' );
1013 if ( ! $menu_id ) {
1014 /* translators: %s: Menu item skipped */
1015 printf( esc_html__( 'Menu item skipped due to invalid menu slug: %s', 'solace-extra' ), esc_html( $menu_slug ) );
1016 echo '<br />';
1017 return;
1018 } else {
1019 $menu_id = is_array( $menu_id ) ? $menu_id['term_id'] : $menu_id;
1020 }
1021
1022 foreach ( $item['postmeta'] as $meta ) {
1023 ${$meta['key']} = $meta['value'];
1024 }
1025
1026 if ( 'taxonomy' == $_menu_item_type && isset( $this->processed_terms[ intval( $_menu_item_object_id ) ] ) ) {
1027 $_menu_item_object_id = $this->processed_terms[ intval( $_menu_item_object_id ) ];
1028 } elseif ( 'post_type' == $_menu_item_type && isset( $this->processed_posts[ intval( $_menu_item_object_id ) ] ) ) {
1029 $_menu_item_object_id = $this->processed_posts[ intval( $_menu_item_object_id ) ];
1030 } elseif ( 'custom' != $_menu_item_type ) {
1031 // associated object is missing or not imported yet, we'll retry later
1032 $this->missing_menu_items[] = $item;
1033 return;
1034 }
1035
1036 if ( isset( $this->processed_menu_items[ intval( $_menu_item_menu_item_parent ) ] ) ) {
1037 $_menu_item_menu_item_parent = $this->processed_menu_items[ intval( $_menu_item_menu_item_parent ) ];
1038 } elseif ( $_menu_item_menu_item_parent ) {
1039 $this->menu_item_orphans[ intval( $item['post_id'] ) ] = (int) $_menu_item_menu_item_parent;
1040 $_menu_item_menu_item_parent = 0;
1041 }
1042
1043 // wp_update_nav_menu_item expects CSS classes as a space separated string
1044 $_menu_item_classes = $this->maybe_unserialize( $_menu_item_classes );
1045 if ( is_array( $_menu_item_classes ) ) {
1046 $_menu_item_classes = implode( ' ', $_menu_item_classes );
1047 }
1048
1049 $args = array(
1050 'menu-item-object-id' => $_menu_item_object_id,
1051 'menu-item-object' => $_menu_item_object,
1052 'menu-item-parent-id' => $_menu_item_menu_item_parent,
1053 'menu-item-position' => intval( $item['menu_order'] ),
1054 'menu-item-type' => $_menu_item_type,
1055 'menu-item-title' => $item['post_title'],
1056 'menu-item-url' => $_menu_item_url,
1057 'menu-item-description' => $item['post_content'],
1058 'menu-item-attr-title' => $item['post_excerpt'],
1059 'menu-item-target' => $_menu_item_target,
1060 'menu-item-classes' => $_menu_item_classes,
1061 'menu-item-xfn' => $_menu_item_xfn,
1062 'menu-item-status' => $item['status'],
1063 );
1064
1065 $id = wp_update_nav_menu_item( $menu_id, 0, $args );
1066 if ( $id && ! is_wp_error( $id ) ) {
1067 $this->processed_menu_items[ intval( $item['post_id'] ) ] = (int) $id;
1068 }
1069 }
1070
1071 /**
1072 * If fetching attachments is enabled then attempt to create a new attachment
1073 *
1074 * @param array $post Attachment post details from WXR
1075 * @param string $url URL to fetch attachment from
1076 * @return int|WP_Error Post ID on success, WP_Error otherwise
1077 */
1078 public function process_attachment( $post, $url ) {
1079 if ( ! $this->fetch_attachments ) {
1080 return new WP_Error(
1081 'attachment_processing_error',
1082 __( 'Fetching attachments is not enabled', 'solace-extra' )
1083 );
1084 }
1085
1086 // if the URL is absolute, but does not contain address, then upload it assuming base_site_url
1087 if ( preg_match( '|^/[\w\W]+$|', $url ) ) {
1088 $url = rtrim( $this->base_url, '/' ) . $url;
1089 }
1090
1091 $upload = $this->fetch_remote_file( $url, $post );
1092 if ( is_wp_error( $upload ) ) {
1093 return $upload;
1094 }
1095
1096 $info = wp_check_filetype( $upload['file'] );
1097 if ( $info ) {
1098 $post['post_mime_type'] = $info['type'];
1099 } else {
1100 return new WP_Error( 'attachment_processing_error', __( 'Invalid file type', 'solace-extra' ) );
1101 }
1102
1103 $post['guid'] = $upload['url'];
1104
1105 // as per wp-admin/includes/upload.php
1106 $post_id = wp_insert_attachment( $post, $upload['file'] );
1107 wp_update_attachment_metadata( $post_id, wp_generate_attachment_metadata( $post_id, $upload['file'] ) );
1108
1109 // remap resized image URLs, works by stripping the extension and remapping the URL stub.
1110 if ( preg_match( '!^image/!', $info['type'] ) ) {
1111 $parts = pathinfo( $url );
1112 $name = basename( $parts['basename'], ".{$parts['extension']}" ); // PATHINFO_FILENAME in PHP 5.2
1113
1114 $parts_new = pathinfo( $upload['url'] );
1115 $name_new = basename( $parts_new['basename'], ".{$parts_new['extension']}" );
1116
1117 $this->url_remap[ $parts['dirname'] . '/' . $name ] = $parts_new['dirname'] . '/' . $name_new;
1118 }
1119
1120 return $post_id;
1121 }
1122
1123 /**
1124 * Attempt to download a remote file attachment
1125 *
1126 * @param string $url URL of item to fetch
1127 * @param array $post Attachment details
1128 * @return array|WP_Error Local file location details on success, WP_Error otherwise
1129 */
1130 public function fetch_remote_file( $url, $post ) {
1131 // Extract the file name from the URL.
1132 $path = wp_parse_url( $url, PHP_URL_PATH );
1133 $file_name = '';
1134 if ( is_string( $path ) ) {
1135 $file_name = basename( $path );
1136 }
1137
1138 if ( ! $file_name ) {
1139 $file_name = md5( $url );
1140 }
1141
1142 $tmp_file_name = wp_tempnam( $file_name );
1143 if ( ! $tmp_file_name ) {
1144 return new WP_Error( 'import_no_file', __( 'Could not create temporary file.', 'solace-extra' ) );
1145 }
1146
1147 // Fetch the remote URL and write it to the placeholder file.
1148 $remote_response = wp_safe_remote_get(
1149 $url,
1150 array(
1151 'timeout' => 300,
1152 'stream' => true,
1153 'filename' => $tmp_file_name,
1154 'headers' => array(
1155 'Accept-Encoding' => 'identity',
1156 ),
1157 )
1158 );
1159
1160 if ( is_wp_error( $remote_response ) ) {
1161 wp_delete_file( $tmp_file_name );
1162 return new WP_Error(
1163 'import_file_error',
1164 sprintf(
1165 /* translators: 1: The WordPress error message. 2: The WordPress error code. */
1166 __( 'Request failed due to an error: %1$s (%2$s)', 'solace-extra' ),
1167 esc_html( $remote_response->get_error_message() ),
1168 esc_html( $remote_response->get_error_code() )
1169 )
1170 );
1171 }
1172
1173 $remote_response_code = (int) wp_remote_retrieve_response_code( $remote_response );
1174
1175 // Make sure the fetch was successful.
1176 if ( 200 !== $remote_response_code ) {
1177 wp_delete_file( $tmp_file_name );
1178 return new WP_Error(
1179 'import_file_error',
1180 sprintf(
1181 /* translators: 1: The HTTP error message. 2: The HTTP error code. */
1182 __( 'Remote server returned the following unexpected result: %1$s (%2$s)', 'solace-extra' ),
1183 get_status_header_desc( $remote_response_code ),
1184 esc_html( $remote_response_code )
1185 )
1186 );
1187 }
1188
1189 $headers = wp_remote_retrieve_headers( $remote_response );
1190
1191 // Request failed.
1192 if ( ! $headers ) {
1193 wp_delete_file( $tmp_file_name );
1194 return new WP_Error( 'import_file_error', __( 'Remote server did not respond', 'solace-extra' ) );
1195 }
1196
1197 $filesize = (int) filesize( $tmp_file_name );
1198
1199 if ( 0 === $filesize ) {
1200 wp_delete_file( $tmp_file_name );
1201 return new WP_Error( 'import_file_error', __( 'Zero size file downloaded', 'solace-extra' ) );
1202 }
1203
1204 if ( ! isset( $headers['content-encoding'] ) && isset( $headers['content-length'] ) && $filesize !== (int) $headers['content-length'] ) {
1205 wp_delete_file( $tmp_file_name );
1206 return new WP_Error( 'import_file_error', __( 'Downloaded file has incorrect size', 'solace-extra' ) );
1207 }
1208
1209 $max_size = (int) $this->max_attachment_size();
1210 if ( ! empty( $max_size ) && $filesize > $max_size ) {
1211 wp_delete_file( $tmp_file_name );
1212 /* translators: %s: import_file_error */
1213 return new WP_Error( 'import_file_error', sprintf( __( 'Remote file is too large, limit is %s', 'solace-extra' ), size_format( $max_size ) ) );
1214 }
1215
1216 // Override file name with Content-Disposition header value.
1217 if ( ! empty( $headers['content-disposition'] ) ) {
1218 $file_name_from_disposition = self::get_filename_from_disposition( (array) $headers['content-disposition'] );
1219 if ( $file_name_from_disposition ) {
1220 $file_name = $file_name_from_disposition;
1221 }
1222 }
1223
1224 // Set file extension if missing.
1225 $file_ext = pathinfo( $file_name, PATHINFO_EXTENSION );
1226 if ( ! $file_ext && ! empty( $headers['content-type'] ) ) {
1227 $extension = self::get_file_extension_by_mime_type( $headers['content-type'] );
1228 if ( $extension ) {
1229 $file_name = "{$file_name}.{$extension}";
1230 }
1231 }
1232
1233 // Handle the upload like _wp_handle_upload() does.
1234 $wp_filetype = wp_check_filetype_and_ext( $tmp_file_name, $file_name );
1235 $ext = empty( $wp_filetype['ext'] ) ? '' : $wp_filetype['ext'];
1236 $type = empty( $wp_filetype['type'] ) ? '' : $wp_filetype['type'];
1237 $proper_filename = empty( $wp_filetype['proper_filename'] ) ? '' : $wp_filetype['proper_filename'];
1238
1239 // Check to see if wp_check_filetype_and_ext() determined the filename was incorrect.
1240 if ( $proper_filename ) {
1241 $file_name = $proper_filename;
1242 }
1243
1244 if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
1245 return new WP_Error( 'import_file_error', __( 'Sorry, this file type is not permitted for security reasons.', 'solace-extra' ) );
1246 }
1247
1248 $uploads = wp_upload_dir( $post['upload_date'] );
1249 if ( ! ( $uploads && false === $uploads['error'] ) ) {
1250 return new WP_Error( 'upload_dir_error', $uploads['error'] );
1251 }
1252
1253 // Move the file to the uploads dir.
1254 $file_name = wp_unique_filename( $uploads['path'], $file_name );
1255 $new_file = $uploads['path'] . "/$file_name";
1256 $move_new_file = copy( $tmp_file_name, $new_file );
1257
1258 if ( ! $move_new_file ) {
1259 wp_delete_file( $tmp_file_name );
1260 return new WP_Error( 'import_file_error', __( 'The uploaded file could not be moved', 'solace-extra' ) );
1261 }
1262
1263 // Set correct file permissions.
1264 $stat = stat( dirname( $new_file ) );
1265 $perms = $stat['mode'] & 0000666;
1266
1267 global $wp_filesystem;
1268 $wp_filesystem->chmod( $new_file, $perms );
1269
1270 $upload = array(
1271 'file' => $new_file,
1272 'url' => $uploads['url'] . "/$file_name",
1273 'type' => $wp_filetype['type'],
1274 'error' => false,
1275 );
1276
1277 // keep track of the old and new urls so we can substitute them later
1278 $this->url_remap[ $url ] = $upload['url'];
1279 $this->url_remap[ $post['guid'] ] = $upload['url']; // r13735, really needed?
1280 // keep track of the destination if the remote url is redirected somewhere else
1281 if ( isset( $headers['x-final-location'] ) && $headers['x-final-location'] != $url ) {
1282 $this->url_remap[ $headers['x-final-location'] ] = $upload['url'];
1283 }
1284
1285 return $upload;
1286 }
1287
1288 /**
1289 * Attempt to associate posts and menu items with previously missing parents
1290 *
1291 * An imported post's parent may not have been imported when it was first created
1292 * so try again. Similarly for child menu items and menu items which were missing
1293 * the object (e.g. post) they represent in the menu
1294 */
1295 public function backfill_parents() {
1296 global $wpdb;
1297
1298 // find parents for post orphans
1299 foreach ( $this->post_orphans as $child_id => $parent_id ) {
1300 $local_child_id = false;
1301 $local_parent_id = false;
1302 if ( isset( $this->processed_posts[ $child_id ] ) ) {
1303 $local_child_id = $this->processed_posts[ $child_id ];
1304 }
1305 if ( isset( $this->processed_posts[ $parent_id ] ) ) {
1306 $local_parent_id = $this->processed_posts[ $parent_id ];
1307 }
1308
1309 if ( $local_child_id && $local_parent_id ) {
1310 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery
1311 $wpdb->update( $wpdb->posts, array( 'post_parent' => $local_parent_id ), array( 'ID' => $local_child_id ), '%d', '%d' );
1312 clean_post_cache( $local_child_id );
1313 }
1314 }
1315
1316 // all other posts/terms are imported, retry menu items with missing associated object
1317 $missing_menu_items = $this->missing_menu_items;
1318 foreach ( $missing_menu_items as $item ) {
1319 $this->process_menu_item( $item );
1320 }
1321
1322 // find parents for menu item orphans
1323 foreach ( $this->menu_item_orphans as $child_id => $parent_id ) {
1324 $local_child_id = 0;
1325 $local_parent_id = 0;
1326 if ( isset( $this->processed_menu_items[ $child_id ] ) ) {
1327 $local_child_id = $this->processed_menu_items[ $child_id ];
1328 }
1329 if ( isset( $this->processed_menu_items[ $parent_id ] ) ) {
1330 $local_parent_id = $this->processed_menu_items[ $parent_id ];
1331 }
1332
1333 if ( $local_child_id && $local_parent_id ) {
1334 update_post_meta( $local_child_id, '_menu_item_menu_item_parent', (int) $local_parent_id );
1335 }
1336 }
1337 }
1338
1339 /**
1340 * Use stored mapping information to update old attachment URLs
1341 */
1342 public function backfill_attachment_urls() {
1343 global $wpdb;
1344 // make sure we do the longest urls first, in case one is a substring of another
1345 uksort( $this->url_remap, array( &$this, 'cmpr_strlen' ) );
1346
1347 foreach ( $this->url_remap as $from_url => $to_url ) {
1348 // remap urls in post_content
1349 // phpcs:disable WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
1350 $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s)", $from_url, $to_url ) );
1351 // remap enclosure urls
1352 $result = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_key='enclosure'", $from_url, $to_url ) );
1353 }
1354 }
1355
1356 /**
1357 * Update _thumbnail_id meta to new, imported attachment IDs
1358 */
1359 public function remap_featured_images() {
1360 // cycle through posts that have a featured image
1361 foreach ( $this->featured_images as $post_id => $value ) {
1362 if ( isset( $this->processed_posts[ $value ] ) ) {
1363 $new_id = $this->processed_posts[ $value ];
1364 // only update if there's a difference
1365 if ( $new_id != $value ) {
1366 update_post_meta( $post_id, '_thumbnail_id', $new_id );
1367 }
1368 }
1369 }
1370 }
1371
1372 /**
1373 * Remap attachment IDs inside _elementor_data for imported solace-sitebuilder posts.
1374 * Fixes icon/widget references (e.g. custom SVG icons) that point to old attachment IDs.
1375 */
1376 public function remap_elementor_attachment_ids() {
1377 $sitebuilder_post_ids = array();
1378 foreach ( $this->processed_posts as $old_id => $new_id ) {
1379 $post_type = get_post_type( $new_id );
1380 if ( 'solace-sitebuilder' === $post_type ) {
1381 $sitebuilder_post_ids[ $new_id ] = true;
1382 }
1383 }
1384
1385 foreach ( array_keys( $sitebuilder_post_ids ) as $post_id ) {
1386 $elementor_data = get_post_meta( $post_id, '_elementor_data', true );
1387 if ( empty( $elementor_data ) || ! is_string( $elementor_data ) ) {
1388 continue;
1389 }
1390
1391 $data = json_decode( $elementor_data, true );
1392 if ( ! is_array( $data ) || json_last_error() !== JSON_ERROR_NONE ) {
1393 continue;
1394 }
1395
1396 $remapped = $this->remap_elementor_data_recursive( $data, $this->processed_posts );
1397 if ( $remapped !== $data ) {
1398 update_post_meta( $post_id, '_elementor_data', wp_slash( wp_json_encode( $remapped ) ) );
1399 }
1400 }
1401 }
1402
1403 /**
1404 * Recursively remap old attachment/post IDs to new IDs in Elementor data.
1405 * Updates "id" and "url" (attachment URL) when the ID was imported.
1406 *
1407 * In addition to direct ID remapping, this also tries to resolve media
1408 * references which only store a URL (common for custom SVG toggle icons)
1409 * by looking up or downloading the missing attachment.
1410 *
1411 * @param array $data Elementor data (array or nested structure).
1412 * @param array $processed_posts Map of old_id => new_id from import.
1413 * @return array Modified data.
1414 */
1415 private function remap_elementor_data_recursive( $data, $processed_posts ) {
1416 if ( ! is_array( $data ) ) {
1417 return $data;
1418 }
1419
1420 foreach ( $data as $k => $v ) {
1421 $data[ $k ] = $this->remap_elementor_data_recursive( $v, $processed_posts );
1422 }
1423
1424 // 1) Primary path: remap numeric attachment IDs that were imported.
1425 if ( isset( $data['id'] ) && is_numeric( $data['id'] ) ) {
1426 $old_id = (int) $data['id'];
1427 if ( isset( $processed_posts[ $old_id ] ) ) {
1428 $data['id'] = $processed_posts[ $old_id ];
1429 if ( isset( $data['url'] ) && ! empty( $data['id'] ) ) {
1430 $new_url = wp_get_attachment_url( (int) $data['id'] );
1431 if ( $new_url ) {
1432 $data['url'] = $new_url;
1433 }
1434 }
1435
1436 // ID successfully remapped, nothing more to do for this node.
1437 return $data;
1438 }
1439 }
1440
1441 // 2) Fallback path: when the ID did not come through the import (common
1442 // for older demo SVGs/icons), try to resolve by URL.
1443 if ( isset( $data['url'] ) && is_string( $data['url'] ) && '' !== $data['url'] ) {
1444 $resolved_id = $this->resolve_elementor_media_by_url( $data['url'] );
1445
1446 // If we managed to resolve or download the asset, update both id & url.
1447 if ( $resolved_id ) {
1448 $data['id'] = $resolved_id;
1449 $new_url = wp_get_attachment_url( $resolved_id );
1450 if ( $new_url ) {
1451 $data['url'] = $new_url;
1452 }
1453 }
1454 }
1455
1456 return $data;
1457 }
1458
1459 /**
1460 * Resolve an Elementor media URL (e.g. toggle_icon_normal SVG) to a local
1461 * attachment ID. If the file is not yet present and attachments are allowed
1462 * to be fetched, this will attempt a one-off download from the original site.
1463 *
1464 * This specifically fixes cases where the WXR did not include the SVG/media
1465 * as an attachment post, but the Elementor JSON still references its URL.
1466 *
1467 * @param string $url Original media URL stored in Elementor settings.
1468 * @return int Attachment ID if resolved or downloaded, 0 otherwise.
1469 */
1470 private function resolve_elementor_media_by_url( $url ) {
1471 $url = trim( $url );
1472 if ( '' === $url ) {
1473 return 0;
1474 }
1475
1476 $parsed = wp_parse_url( $url );
1477 if ( empty( $parsed['scheme'] ) || empty( $parsed['host'] ) ) {
1478 return 0;
1479 }
1480
1481 // Normalise URL by stripping query string for lookup purposes.
1482 $normalized_url = $parsed['scheme'] . '://' . $parsed['host'];
1483 if ( ! empty( $parsed['port'] ) ) {
1484 $normalized_url .= ':' . $parsed['port'];
1485 }
1486 $normalized_url .= isset( $parsed['path'] ) ? $parsed['path'] : '';
1487
1488 // Use cache to avoid repeating work.
1489 if ( isset( $this->elementor_media_cache[ $normalized_url ] ) ) {
1490 return $this->elementor_media_cache[ $normalized_url ];
1491 }
1492
1493 $attachment_id = 0;
1494
1495 // 2.a) If this URL was already remapped by the core importer, use that.
1496 if ( isset( $this->url_remap[ $normalized_url ] ) ) {
1497 $remapped_url = $this->url_remap[ $normalized_url ];
1498 $attachment_id = attachment_url_to_postid( $remapped_url );
1499 if ( $attachment_id ) {
1500 $this->elementor_media_cache[ $normalized_url ] = $attachment_id;
1501 return $attachment_id;
1502 }
1503 }
1504
1505 // 2.b) Try to find an existing attachment that already uses this URL.
1506 $existing_id = attachment_url_to_postid( $normalized_url );
1507 if ( $existing_id ) {
1508 $this->elementor_media_cache[ $normalized_url ] = $existing_id;
1509 return $existing_id;
1510 }
1511
1512 // 2.c) As a last resort, and only if attachment fetching is enabled and
1513 // the URL points to the original export site, attempt to download
1514 // the file and create a local attachment.
1515 if ( ! $this->fetch_attachments ) {
1516 $this->elementor_media_cache[ $normalized_url ] = 0;
1517 return 0;
1518 }
1519
1520 $base = wp_parse_url( $this->base_url );
1521 if ( empty( $base['host'] ) || strcasecmp( $base['host'], $parsed['host'] ) !== 0 ) {
1522 // Different host – don't attempt to sideload arbitrary remote files.
1523 $this->elementor_media_cache[ $normalized_url ] = 0;
1524 return 0;
1525 }
1526
1527 // Prepare a minimal attachment post array for process_attachment().
1528 $filename = isset( $parsed['path'] ) ? basename( $parsed['path'] ) : '';
1529 $post_title = $filename ? preg_replace( '/\.[^.]+$/', '', $filename ) : 'imported-media';
1530
1531 $post = array(
1532 'post_title' => $post_title,
1533 'post_content' => '',
1534 'post_status' => 'inherit',
1535 'post_mime_type' => '',
1536 'post_parent' => 0,
1537 // Use current time so wp_upload_dir() chooses a sensible subdirectory.
1538 'upload_date' => current_time( 'mysql' ),
1539 );
1540
1541 $result = $this->process_attachment( $post, $normalized_url );
1542 if ( is_wp_error( $result ) ) {
1543 $this->elementor_media_cache[ $normalized_url ] = 0;
1544 return 0;
1545 }
1546
1547 $attachment_id = (int) $result;
1548 $this->elementor_media_cache[ $normalized_url ] = $attachment_id;
1549
1550 return $attachment_id;
1551 }
1552
1553 /**
1554 * Parse a WXR file
1555 *
1556 * @param string $file Path to WXR file for parsing
1557 * @return array Information gathered from the WXR file
1558 */
1559 public function parse( $file ) {
1560 $parser = new Solace_Extra_WXR_Parser();
1561 return $parser->parse( $file );
1562 }
1563
1564 // Display import page title
1565 public function header() {
1566 echo '<div class="wrap">';
1567 echo '<h2>' . esc_html__( 'Import WordPress', 'solace-extra' ) . '</h2>';
1568
1569 $updates = get_plugin_updates();
1570 $basename = plugin_basename( __FILE__ );
1571 if ( isset( $updates[ $basename ] ) ) {
1572 $update = $updates[ $basename ];
1573 echo '<div class="error"><p><strong>';
1574 /* translators: %s: A new version */
1575 printf( esc_html__( 'A new version of this importer is available. Please update to version %s to ensure compatibility with newer export files.', 'solace-extra' ), esc_html( $update->update->new_version ) );
1576 echo '</strong></p></div>';
1577 }
1578 }
1579
1580 // Close div.wrap
1581 public function footer() {
1582 echo '</div>';
1583 }
1584
1585 /**
1586 * Display introductory text and file upload form
1587 */
1588 public function greet() {
1589 echo '<div class="narrow">';
1590 echo '<p>' . esc_html__( 'Howdy! Upload your WordPress eXtended RSS (WXR) file and we&#8217;ll import the posts, pages, comments, custom fields, categories, and tags into this site.', 'solace-extra' ) . '</p>';
1591 echo '<p>' . esc_html__( 'Choose a WXR (.xml) file to upload, then click Upload file and import.', 'solace-extra' ) . '</p>';
1592 wp_import_upload_form( 'admin.php?import=wordpress&amp;step=1' );
1593 echo '</div>';
1594 }
1595
1596 /**
1597 * Decide if the given meta key maps to information we will want to import
1598 *
1599 * @param string $key The meta key to check
1600 * @return string|bool The key if we do want to import, false if not
1601 */
1602 public function is_valid_meta_key( $key ) {
1603 // skip attachment metadata since we'll regenerate it from scratch
1604 // skip _edit_lock as not relevant for import
1605 if ( in_array( $key, array( '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock' ), true ) ) {
1606 return false;
1607 }
1608 return $key;
1609 }
1610
1611 /**
1612 * Decide whether or not the importer is allowed to create users.
1613 * Default is true, can be filtered via import_allow_create_users
1614 *
1615 * @return bool True if creating users is allowed
1616 */
1617 public function allow_create_users() {
1618 return apply_filters( 'import_allow_create_users', true );
1619 }
1620
1621 /**
1622 * Decide whether or not the importer should attempt to download attachment files.
1623 * Default is true, can be filtered via import_allow_fetch_attachments. The choice
1624 * made at the import options screen must also be true, false here hides that checkbox.
1625 *
1626 * @return bool True if downloading attachments is allowed
1627 */
1628 public function allow_fetch_attachments() {
1629 return apply_filters( 'import_allow_fetch_attachments', true );
1630 }
1631
1632 /**
1633 * Decide what the maximum file size for downloaded attachments is.
1634 * Default is 0 (unlimited), can be filtered via import_attachment_size_limit
1635 *
1636 * @return int Maximum attachment file size to import
1637 */
1638 public function max_attachment_size() {
1639 return apply_filters( 'import_attachment_size_limit', 0 );
1640 }
1641
1642 /**
1643 * Added to http_request_timeout filter to force timeout at 60 seconds during import
1644 * @return int 60
1645 */
1646 public function bump_request_timeout( $val ) {
1647 return 60;
1648 }
1649
1650 // return the difference in length between two strings
1651 public function cmpr_strlen( $a, $b ) {
1652 return strlen( $b ) - strlen( $a );
1653 }
1654
1655 /**
1656 * Parses filename from a Content-Disposition header value.
1657 *
1658 * As per RFC6266:
1659 *
1660 * content-disposition = "Content-Disposition" ":"
1661 * disposition-type *( ";" disposition-parm )
1662 *
1663 * disposition-type = "inline" | "attachment" | disp-ext-type
1664 * ; case-insensitive
1665 * disp-ext-type = token
1666 *
1667 * disposition-parm = filename-parm | disp-ext-parm
1668 *
1669 * filename-parm = "filename" "=" value
1670 * | "filename*" "=" ext-value
1671 *
1672 * disp-ext-parm = token "=" value
1673 * | ext-token "=" ext-value
1674 * ext-token = <the characters in token, followed by "*">
1675 *
1676 * @since 0.7.0
1677 *
1678 * @see WP_REST_Attachments_Controller::get_filename_from_disposition()
1679 *
1680 * @link http://tools.ietf.org/html/rfc2388
1681 * @link http://tools.ietf.org/html/rfc6266
1682 *
1683 * @param string[] $disposition_header List of Content-Disposition header values.
1684 * @return string|null Filename if available, or null if not found.
1685 */
1686 protected static function get_filename_from_disposition( $disposition_header ) {
1687 // Get the filename.
1688 $filename = null;
1689
1690 foreach ( $disposition_header as $value ) {
1691 $value = trim( $value );
1692
1693 if ( strpos( $value, ';' ) === false ) {
1694 continue;
1695 }
1696
1697 list( $type, $attr_parts ) = explode( ';', $value, 2 );
1698
1699 $attr_parts = explode( ';', $attr_parts );
1700 $attributes = array();
1701
1702 foreach ( $attr_parts as $part ) {
1703 if ( strpos( $part, '=' ) === false ) {
1704 continue;
1705 }
1706
1707 list( $key, $value ) = explode( '=', $part, 2 );
1708
1709 $attributes[ trim( $key ) ] = trim( $value );
1710 }
1711
1712 if ( empty( $attributes['filename'] ) ) {
1713 continue;
1714 }
1715
1716 $filename = trim( $attributes['filename'] );
1717
1718 // Unquote quoted filename, but after trimming.
1719 if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) {
1720 $filename = substr( $filename, 1, -1 );
1721 }
1722 }
1723
1724 return $filename;
1725 }
1726
1727 /**
1728 * Retrieves file extension by mime type.
1729 *
1730 * @since 0.7.0
1731 *
1732 * @param string $mime_type Mime type to search extension for.
1733 * @return string|null File extension if available, or null if not found.
1734 */
1735 protected static function get_file_extension_by_mime_type( $mime_type ) {
1736 static $map = null;
1737
1738 if ( is_array( $map ) ) {
1739 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
1740 }
1741
1742 $mime_types = wp_get_mime_types();
1743 $map = array_flip( $mime_types );
1744
1745 // Some types have multiple extensions, use only the first one.
1746 foreach ( $map as $type => $extensions ) {
1747 $map[ $type ] = strtok( $extensions, '|' );
1748 }
1749
1750 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
1751 }
1752
1753 /**
1754 * Unserializes data only if it was serialized.
1755 *
1756 * @since 0.8.4
1757 *
1758 * @param string $data Data that might be unserialized.
1759 * @return mixed Unserialized data can be any type.
1760 */
1761 protected function maybe_unserialize( $data ) {
1762 // Don't attempt to unserialize data that wasn't serialized going in.
1763 if ( is_serialized( $data ) ) {
1764 // Transform the serialized objects to a stdClass object.
1765 $data = preg_replace( '/O:\d+:"[^"]+":/', 'O:8:"stdClass":', $data );
1766
1767 return maybe_unserialize( $data );
1768 }
1769
1770 return $data;
1771 }
1772 }
1773