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