PluginProbe ʕ •ᴥ•ʔ
WordPress Importer / 0.9.3
WordPress Importer v0.9.3
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 8 months ago php-toolkit 8 months ago class-wp-import.php 8 months ago compat.php 3 years ago parsers.php 8 months ago readme.txt 8 months ago wordpress-importer.php 8 months ago
class-wp-import.php
1664 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 // if the category already exists leave it alone
485 $term_id = term_exists( $cat['category_nicename'], 'category' );
486 if ( $term_id ) {
487 if ( is_array( $term_id ) ) {
488 $term_id = $term_id['term_id'];
489 }
490 if ( isset( $cat['term_id'] ) ) {
491 $this->processed_terms[ intval( $cat['term_id'] ) ] = (int) $term_id;
492 }
493 continue;
494 }
495
496 $parent = empty( $cat['category_parent'] ) ? 0 : category_exists( $cat['category_parent'] );
497 $description = isset( $cat['category_description'] ) ? $cat['category_description'] : '';
498
499 $data = array(
500 'category_nicename' => $cat['category_nicename'],
501 'category_parent' => $parent,
502 'cat_name' => wp_slash( $cat['cat_name'] ),
503 'category_description' => wp_slash( $description ),
504 );
505
506 $id = wp_insert_category( $data, true );
507 if ( ! is_wp_error( $id ) && $id > 0 ) {
508 if ( isset( $cat['term_id'] ) ) {
509 $this->processed_terms[ intval( $cat['term_id'] ) ] = $id;
510 }
511 } else {
512 printf( __( 'Failed to import category %s', 'wordpress-importer' ), esc_html( $cat['category_nicename'] ) );
513 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
514 echo ': ' . $id->get_error_message();
515 }
516 echo '<br />';
517 continue;
518 }
519
520 $this->process_termmeta( $cat, $id );
521 }
522
523 unset( $this->categories );
524 }
525
526 /**
527 * Create new post tags based on import information
528 *
529 * Doesn't create a tag if its slug already exists
530 */
531 public function process_tags() {
532 $this->tags = apply_filters( 'wp_import_tags', $this->tags );
533
534 if ( empty( $this->tags ) ) {
535 return;
536 }
537
538 foreach ( $this->tags as $tag ) {
539 // if the tag already exists leave it alone
540 $term_id = term_exists( $tag['tag_slug'], 'post_tag' );
541 if ( $term_id ) {
542 if ( is_array( $term_id ) ) {
543 $term_id = $term_id['term_id'];
544 }
545 if ( isset( $tag['term_id'] ) ) {
546 $this->processed_terms[ intval( $tag['term_id'] ) ] = (int) $term_id;
547 }
548 continue;
549 }
550
551 $description = isset( $tag['tag_description'] ) ? $tag['tag_description'] : '';
552 $args = array(
553 'slug' => $tag['tag_slug'],
554 'description' => wp_slash( $description ),
555 );
556
557 $id = wp_insert_term( wp_slash( $tag['tag_name'] ), 'post_tag', $args );
558 if ( ! is_wp_error( $id ) ) {
559 if ( isset( $tag['term_id'] ) ) {
560 $this->processed_terms[ intval( $tag['term_id'] ) ] = $id['term_id'];
561 }
562 } else {
563 printf( __( 'Failed to import post tag %s', 'wordpress-importer' ), esc_html( $tag['tag_name'] ) );
564 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
565 echo ': ' . $id->get_error_message();
566 }
567 echo '<br />';
568 continue;
569 }
570
571 $this->process_termmeta( $tag, $id['term_id'] );
572 }
573
574 unset( $this->tags );
575 }
576
577 /**
578 * Create new terms based on import information
579 *
580 * Doesn't create a term its slug already exists
581 */
582 public function process_terms() {
583 $this->terms = apply_filters( 'wp_import_terms', $this->terms );
584
585 if ( empty( $this->terms ) ) {
586 return;
587 }
588
589 foreach ( $this->terms as $term ) {
590 // if the term already exists in the correct taxonomy leave it alone
591 $term_id = term_exists( $term['slug'], $term['term_taxonomy'] );
592 if ( $term_id ) {
593 if ( is_array( $term_id ) ) {
594 $term_id = $term_id['term_id'];
595 }
596 if ( isset( $term['term_id'] ) ) {
597 $this->processed_terms[ intval( $term['term_id'] ) ] = (int) $term_id;
598 }
599 continue;
600 }
601
602 if ( empty( $term['term_parent'] ) ) {
603 $parent = 0;
604 } else {
605 $parent = term_exists( $term['term_parent'], $term['term_taxonomy'] );
606 if ( is_array( $parent ) ) {
607 $parent = $parent['term_id'];
608 }
609 }
610
611 $description = isset( $term['term_description'] ) ? $term['term_description'] : '';
612 $args = array(
613 'slug' => $term['slug'],
614 'description' => wp_slash( $description ),
615 'parent' => (int) $parent,
616 );
617
618 $id = wp_insert_term( wp_slash( $term['term_name'] ), $term['term_taxonomy'], $args );
619 if ( ! is_wp_error( $id ) ) {
620 if ( isset( $term['term_id'] ) ) {
621 $this->processed_terms[ intval( $term['term_id'] ) ] = $id['term_id'];
622 }
623 } else {
624 printf( __( 'Failed to import %1$s %2$s', 'wordpress-importer' ), esc_html( $term['term_taxonomy'] ), esc_html( $term['term_name'] ) );
625 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
626 echo ': ' . $id->get_error_message();
627 }
628 echo '<br />';
629 continue;
630 }
631
632 $this->process_termmeta( $term, $id['term_id'] );
633 }
634
635 unset( $this->terms );
636 }
637
638 /**
639 * Add metadata to imported term.
640 *
641 * @since 0.6.2
642 *
643 * @param array $term Term data from WXR import.
644 * @param int $term_id ID of the newly created term.
645 */
646 protected function process_termmeta( $term, $term_id ) {
647 if ( ! isset( $term['termmeta'] ) ) {
648 $term['termmeta'] = array();
649 }
650
651 /**
652 * Filters the metadata attached to an imported term.
653 *
654 * @since 0.6.2
655 *
656 * @param array $termmeta Array of term meta.
657 * @param int $term_id ID of the newly created term.
658 * @param array $term Term data from the WXR import.
659 */
660 $term['termmeta'] = apply_filters( 'wp_import_term_meta', $term['termmeta'], $term_id, $term );
661
662 if ( empty( $term['termmeta'] ) ) {
663 return;
664 }
665
666 foreach ( $term['termmeta'] as $meta ) {
667 /**
668 * Filters the meta key for an imported piece of term meta.
669 *
670 * @since 0.6.2
671 *
672 * @param string $meta_key Meta key.
673 * @param int $term_id ID of the newly created term.
674 * @param array $term Term data from the WXR import.
675 */
676 $key = apply_filters( 'import_term_meta_key', $meta['key'], $term_id, $term );
677 if ( ! $key ) {
678 continue;
679 }
680
681 // Export gets meta straight from the DB so could have a serialized string
682 $value = $this->maybe_unserialize( $meta['value'] );
683
684 add_term_meta( $term_id, wp_slash( $key ), wp_slash_strings_only( $value ) );
685
686 /**
687 * Fires after term meta is imported.
688 *
689 * @since 0.6.2
690 *
691 * @param int $term_id ID of the newly created term.
692 * @param string $key Meta key.
693 * @param mixed $value Meta value.
694 */
695 do_action( 'import_term_meta', $term_id, $key, $value );
696 }
697 }
698
699 /**
700 * Create new posts based on import information
701 *
702 * Posts marked as having a parent which doesn't exist will become top level items.
703 * Doesn't create a new post if: the post type doesn't exist, the given post ID
704 * is already noted as imported or a post with the same title and date already exists.
705 * Note that new/updated terms, comments and meta are imported for the last of the above.
706 */
707 public function process_posts() {
708 $this->posts = apply_filters( 'wp_import_posts', $this->posts );
709
710 foreach ( $this->posts as $post ) {
711 $post = apply_filters( 'wp_import_post_data_raw', $post );
712
713 if ( ! post_type_exists( $post['post_type'] ) ) {
714 printf(
715 __( 'Failed to import &#8220;%1$s&#8221;: Invalid post type %2$s', 'wordpress-importer' ),
716 esc_html( $post['post_title'] ),
717 esc_html( $post['post_type'] )
718 );
719 echo '<br />';
720 do_action( 'wp_import_post_exists', $post );
721 continue;
722 }
723
724 if ( isset( $this->processed_posts[ $post['post_id'] ] ) && ! empty( $post['post_id'] ) ) {
725 continue;
726 }
727
728 if ( 'auto-draft' == $post['status'] ) {
729 continue;
730 }
731
732 if ( 'nav_menu_item' == $post['post_type'] ) {
733 $this->process_menu_item( $post );
734 continue;
735 }
736
737 $post_type_object = get_post_type_object( $post['post_type'] );
738
739 $post_exists = post_exists( $post['post_title'], '', $post['post_date'], $post['post_type'] );
740
741 /**
742 * Filter ID of the existing post corresponding to post currently importing.
743 *
744 * Return 0 to force the post to be imported. Filter the ID to be something else
745 * to override which existing post is mapped to the imported post.
746 *
747 * @see post_exists()
748 * @since 0.6.2
749 *
750 * @param int $post_exists Post ID, or 0 if post did not exist.
751 * @param array $post The post array to be inserted.
752 */
753 $post_exists = apply_filters( 'wp_import_existing_post', $post_exists, $post );
754
755 if ( $post_exists && get_post_type( $post_exists ) == $post['post_type'] ) {
756 printf( __( '%1$s &#8220;%2$s&#8221; already exists.', 'wordpress-importer' ), $post_type_object->labels->singular_name, esc_html( $post['post_title'] ) );
757 echo '<br />';
758 $comment_post_id = $post_exists;
759 $post_id = $post_exists;
760 $this->processed_posts[ intval( $post['post_id'] ) ] = intval( $post_exists );
761 } else {
762 $post_parent = (int) $post['post_parent'];
763 if ( $post_parent ) {
764 // if we already know the parent, map it to the new local ID
765 if ( isset( $this->processed_posts[ $post_parent ] ) ) {
766 $post_parent = $this->processed_posts[ $post_parent ];
767 // otherwise record the parent for later
768 } else {
769 $this->post_orphans[ intval( $post['post_id'] ) ] = $post_parent;
770 $post_parent = 0;
771 }
772 }
773
774 // map the post author
775 $author = sanitize_user( $post['post_author'], true );
776 if ( isset( $this->author_mapping[ $author ] ) ) {
777 $author = $this->author_mapping[ $author ];
778 } else {
779 $author = (int) get_current_user_id();
780 }
781
782 $postdata = array(
783 'import_id' => $post['post_id'],
784 'post_author' => $author,
785 'post_date' => $post['post_date'],
786 'post_date_gmt' => $post['post_date_gmt'],
787 'post_content' => $post['post_content'],
788 'post_excerpt' => $post['post_excerpt'],
789 'post_title' => $post['post_title'],
790 'post_status' => $post['status'],
791 'post_name' => $post['post_name'],
792 'comment_status' => $post['comment_status'],
793 'ping_status' => $post['ping_status'],
794 'guid' => $post['guid'],
795 'post_parent' => $post_parent,
796 'menu_order' => $post['menu_order'],
797 'post_type' => $post['post_type'],
798 'post_password' => $post['post_password'],
799 );
800
801 if ( $this->options['rewrite_urls'] ) {
802 $url_mapping = array(
803 $this->base_url_parsed->toString() => $this->site_url_parsed,
804 );
805 $postdata['post_content'] = wp_rewrite_urls(
806 array(
807 'block_markup' => $postdata['post_content'],
808 'url-mapping' => $url_mapping,
809 )
810 );
811 $postdata['post_excerpt'] = wp_rewrite_urls(
812 array(
813 'block_markup' => $postdata['post_excerpt'],
814 'url-mapping' => $url_mapping,
815 )
816 );
817 }
818
819 $original_post_id = $post['post_id'];
820 $postdata = apply_filters( 'wp_import_post_data_processed', $postdata, $post );
821
822 $postdata = wp_slash( $postdata );
823
824 if ( 'attachment' == $postdata['post_type'] ) {
825 $remote_url = ! empty( $post['attachment_url'] ) ? $post['attachment_url'] : $post['guid'];
826
827 // try to use _wp_attached file for upload folder placement to ensure the same location as the export site
828 // e.g. location is 2003/05/image.jpg but the attachment post_date is 2010/09, see media_handle_upload()
829 $postdata['upload_date'] = $post['post_date'];
830 if ( isset( $post['postmeta'] ) ) {
831 foreach ( $post['postmeta'] as $meta ) {
832 if ( '_wp_attached_file' == $meta['key'] ) {
833 if ( preg_match( '%^[0-9]{4}/[0-9]{2}%', $meta['value'], $matches ) ) {
834 $postdata['upload_date'] = $matches[0];
835 }
836 break;
837 }
838 }
839 }
840
841 $comment_post_id = $this->process_attachment( $postdata, $remote_url );
842 $post_id = $comment_post_id;
843 } else {
844 $comment_post_id = wp_insert_post( $postdata, true );
845 $post_id = $comment_post_id;
846 do_action( 'wp_import_insert_post', $post_id, $original_post_id, $postdata, $post );
847 }
848
849 if ( is_wp_error( $post_id ) ) {
850 printf(
851 __( 'Failed to import %1$s &#8220;%2$s&#8221;', 'wordpress-importer' ),
852 $post_type_object->labels->singular_name,
853 esc_html( $post['post_title'] )
854 );
855 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
856 echo ': ' . $post_id->get_error_message();
857 }
858 echo '<br />';
859 continue;
860 }
861
862 if ( 1 == $post['is_sticky'] ) {
863 stick_post( $post_id );
864 }
865 }
866
867 // map pre-import ID to local ID
868 $this->processed_posts[ intval( $post['post_id'] ) ] = (int) $post_id;
869
870 if ( ! isset( $post['terms'] ) ) {
871 $post['terms'] = array();
872 }
873
874 $post['terms'] = apply_filters( 'wp_import_post_terms', $post['terms'], $post_id, $post );
875
876 // add categories, tags and other terms
877 if ( ! empty( $post['terms'] ) ) {
878 $terms_to_set = array();
879 foreach ( $post['terms'] as $term ) {
880 // back compat with WXR 1.0 map 'tag' to 'post_tag'
881 $taxonomy = ( 'tag' == $term['domain'] ) ? 'post_tag' : $term['domain'];
882 $term_exists = term_exists( $term['slug'], $taxonomy );
883 $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists;
884 if ( ! $term_id ) {
885 $t = wp_insert_term( $term['name'], $taxonomy, array( 'slug' => $term['slug'] ) );
886 if ( ! is_wp_error( $t ) ) {
887 $term_id = $t['term_id'];
888 do_action( 'wp_import_insert_term', $t, $term, $post_id, $post );
889 } else {
890 printf( __( 'Failed to import %1$s %2$s', 'wordpress-importer' ), esc_html( $taxonomy ), esc_html( $term['name'] ) );
891 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
892 echo ': ' . $t->get_error_message();
893 }
894 echo '<br />';
895 do_action( 'wp_import_insert_term_failed', $t, $term, $post_id, $post );
896 continue;
897 }
898 }
899 $terms_to_set[ $taxonomy ][] = intval( $term_id );
900 }
901
902 foreach ( $terms_to_set as $tax => $ids ) {
903 $tt_ids = wp_set_post_terms( $post_id, $ids, $tax );
904 do_action( 'wp_import_set_post_terms', $tt_ids, $ids, $tax, $post_id, $post );
905 }
906 unset( $post['terms'], $terms_to_set );
907 }
908
909 if ( ! isset( $post['comments'] ) ) {
910 $post['comments'] = array();
911 }
912
913 $post['comments'] = apply_filters( 'wp_import_post_comments', $post['comments'], $post_id, $post );
914
915 // add/update comments
916 if ( ! empty( $post['comments'] ) ) {
917 $num_comments = 0;
918 $inserted_comments = array();
919 foreach ( $post['comments'] as $comment ) {
920 $comment_id = $comment['comment_id'];
921 $newcomments[ $comment_id ]['comment_post_ID'] = $comment_post_id;
922 $newcomments[ $comment_id ]['comment_author'] = $comment['comment_author'];
923 $newcomments[ $comment_id ]['comment_author_email'] = $comment['comment_author_email'];
924 $newcomments[ $comment_id ]['comment_author_IP'] = $comment['comment_author_IP'];
925 $newcomments[ $comment_id ]['comment_author_url'] = $comment['comment_author_url'];
926 $newcomments[ $comment_id ]['comment_date'] = $comment['comment_date'];
927 $newcomments[ $comment_id ]['comment_date_gmt'] = $comment['comment_date_gmt'];
928 $newcomments[ $comment_id ]['comment_content'] = $comment['comment_content'];
929 $newcomments[ $comment_id ]['comment_approved'] = $comment['comment_approved'];
930 $newcomments[ $comment_id ]['comment_type'] = $comment['comment_type'];
931 $newcomments[ $comment_id ]['comment_parent'] = $comment['comment_parent'];
932 $newcomments[ $comment_id ]['commentmeta'] = isset( $comment['commentmeta'] ) ? $comment['commentmeta'] : array();
933 if ( isset( $this->processed_authors[ $comment['comment_user_id'] ] ) ) {
934 $newcomments[ $comment_id ]['user_id'] = $this->processed_authors[ $comment['comment_user_id'] ];
935 }
936 }
937 ksort( $newcomments );
938
939 foreach ( $newcomments as $key => $comment ) {
940 // if this is a new post we can skip the comment_exists() check
941 if ( ! $post_exists || ! comment_exists( $comment['comment_author'], $comment['comment_date'] ) ) {
942 if ( isset( $inserted_comments[ $comment['comment_parent'] ] ) ) {
943 $comment['comment_parent'] = $inserted_comments[ $comment['comment_parent'] ];
944 }
945
946 $comment_data = wp_slash( $comment );
947 unset( $comment_data['commentmeta'] ); // Handled separately, wp_insert_comment() also expects `comment_meta`.
948 $comment_data = wp_filter_comment( $comment_data );
949
950 $inserted_comments[ $key ] = wp_insert_comment( $comment_data );
951
952 do_action( 'wp_import_insert_comment', $inserted_comments[ $key ], $comment, $comment_post_id, $post );
953
954 foreach ( $comment['commentmeta'] as $meta ) {
955 $value = $this->maybe_unserialize( $meta['value'] );
956
957 add_comment_meta( $inserted_comments[ $key ], wp_slash( $meta['key'] ), wp_slash_strings_only( $value ) );
958 }
959
960 ++$num_comments;
961 }
962 }
963 unset( $newcomments, $inserted_comments, $post['comments'] );
964 }
965
966 if ( ! isset( $post['postmeta'] ) ) {
967 $post['postmeta'] = array();
968 }
969
970 $post['postmeta'] = apply_filters( 'wp_import_post_meta', $post['postmeta'], $post_id, $post );
971
972 // add/update post meta
973 if ( ! empty( $post['postmeta'] ) ) {
974 foreach ( $post['postmeta'] as $meta ) {
975 $key = apply_filters( 'import_post_meta_key', $meta['key'], $post_id, $post );
976 $value = false;
977
978 if ( '_edit_last' == $key ) {
979 if ( isset( $this->processed_authors[ intval( $meta['value'] ) ] ) ) {
980 $value = $this->processed_authors[ intval( $meta['value'] ) ];
981 } else {
982 $key = false;
983 }
984 }
985
986 if ( $key ) {
987 // export gets meta straight from the DB so could have a serialized string
988 if ( ! $value ) {
989 $value = $this->maybe_unserialize( $meta['value'] );
990 }
991
992 add_post_meta( $post_id, wp_slash( $key ), wp_slash_strings_only( $value ) );
993
994 do_action( 'import_post_meta', $post_id, $key, $value );
995
996 // if the post has a featured image, take note of this in case of remap
997 if ( '_thumbnail_id' == $key ) {
998 $this->featured_images[ $post_id ] = (int) $value;
999 }
1000 }
1001 }
1002 }
1003 }
1004
1005 unset( $this->posts );
1006 }
1007
1008 /**
1009 * Attempt to create a new menu item from import data
1010 *
1011 * Fails for draft, orphaned menu items and those without an associated nav_menu
1012 * or an invalid nav_menu term. If the post type or term object which the menu item
1013 * represents doesn't exist then the menu item will not be imported (waits until the
1014 * end of the import to retry again before discarding).
1015 *
1016 * @param array $item Menu item details from WXR file
1017 */
1018 public function process_menu_item( $item ) {
1019 // skip draft, orphaned menu items
1020 if ( 'draft' == $item['status'] ) {
1021 return;
1022 }
1023
1024 $menu_slug = false;
1025 if ( isset( $item['terms'] ) ) {
1026 // loop through terms, assume first nav_menu term is correct menu
1027 foreach ( $item['terms'] as $term ) {
1028 if ( 'nav_menu' == $term['domain'] ) {
1029 $menu_slug = $term['slug'];
1030 break;
1031 }
1032 }
1033 }
1034
1035 // no nav_menu term associated with this menu item
1036 if ( ! $menu_slug ) {
1037 _e( 'Menu item skipped due to missing menu slug', 'wordpress-importer' );
1038 echo '<br />';
1039 return;
1040 }
1041
1042 $menu_id = term_exists( $menu_slug, 'nav_menu' );
1043 if ( ! $menu_id ) {
1044 printf( __( 'Menu item skipped due to invalid menu slug: %s', 'wordpress-importer' ), esc_html( $menu_slug ) );
1045 echo '<br />';
1046 return;
1047 } else {
1048 $menu_id = is_array( $menu_id ) ? $menu_id['term_id'] : $menu_id;
1049 }
1050
1051 foreach ( $item['postmeta'] as $meta ) {
1052 ${$meta['key']} = $meta['value'];
1053 }
1054
1055 if ( 'taxonomy' == $_menu_item_type && isset( $this->processed_terms[ intval( $_menu_item_object_id ) ] ) ) {
1056 $_menu_item_object_id = $this->processed_terms[ intval( $_menu_item_object_id ) ];
1057 } elseif ( 'post_type' == $_menu_item_type && isset( $this->processed_posts[ intval( $_menu_item_object_id ) ] ) ) {
1058 $_menu_item_object_id = $this->processed_posts[ intval( $_menu_item_object_id ) ];
1059 } elseif ( 'custom' != $_menu_item_type ) {
1060 // associated object is missing or not imported yet, we'll retry later
1061 $this->missing_menu_items[] = $item;
1062 return;
1063 }
1064
1065 if ( isset( $this->processed_menu_items[ intval( $_menu_item_menu_item_parent ) ] ) ) {
1066 $_menu_item_menu_item_parent = $this->processed_menu_items[ intval( $_menu_item_menu_item_parent ) ];
1067 } elseif ( $_menu_item_menu_item_parent ) {
1068 $this->menu_item_orphans[ intval( $item['post_id'] ) ] = (int) $_menu_item_menu_item_parent;
1069 $_menu_item_menu_item_parent = 0;
1070 }
1071
1072 // wp_update_nav_menu_item expects CSS classes as a space separated string
1073 $_menu_item_classes = $this->maybe_unserialize( $_menu_item_classes );
1074 if ( is_array( $_menu_item_classes ) ) {
1075 $_menu_item_classes = implode( ' ', $_menu_item_classes );
1076 }
1077
1078 $args = array(
1079 'menu-item-object-id' => $_menu_item_object_id,
1080 'menu-item-object' => $_menu_item_object,
1081 'menu-item-parent-id' => $_menu_item_menu_item_parent,
1082 'menu-item-position' => intval( $item['menu_order'] ),
1083 'menu-item-type' => $_menu_item_type,
1084 'menu-item-title' => $item['post_title'],
1085 'menu-item-url' => $_menu_item_url,
1086 'menu-item-description' => $item['post_content'],
1087 'menu-item-attr-title' => $item['post_excerpt'],
1088 'menu-item-target' => $_menu_item_target,
1089 'menu-item-classes' => $_menu_item_classes,
1090 'menu-item-xfn' => $_menu_item_xfn,
1091 'menu-item-status' => $item['status'],
1092 );
1093
1094 $id = wp_update_nav_menu_item( $menu_id, 0, $args );
1095 if ( $id && ! is_wp_error( $id ) ) {
1096 $this->processed_menu_items[ intval( $item['post_id'] ) ] = (int) $id;
1097 }
1098 }
1099
1100 /**
1101 * If fetching attachments is enabled then attempt to create a new attachment
1102 *
1103 * @param array $post Attachment post details from WXR
1104 * @param string $url URL to fetch attachment from
1105 * @return int|WP_Error Post ID on success, WP_Error otherwise
1106 */
1107 public function process_attachment( $post, $url ) {
1108 if ( ! $this->fetch_attachments ) {
1109 return new WP_Error(
1110 'attachment_processing_error',
1111 __( 'Fetching attachments is not enabled', 'wordpress-importer' )
1112 );
1113 }
1114
1115 // if the URL is absolute, but does not contain address, then upload it assuming base_site_url
1116 if ( preg_match( '|^/[\w\W]+$|', $url ) ) {
1117 $url = rtrim( $this->base_url, '/' ) . $url;
1118 }
1119
1120 $upload = $this->fetch_remote_file( $url, $post );
1121 if ( is_wp_error( $upload ) ) {
1122 return $upload;
1123 }
1124
1125 $info = wp_check_filetype( $upload['file'] );
1126 if ( $info ) {
1127 $post['post_mime_type'] = $info['type'];
1128 } else {
1129 return new WP_Error( 'attachment_processing_error', __( 'Invalid file type', 'wordpress-importer' ) );
1130 }
1131
1132 $post['guid'] = $upload['url'];
1133
1134 // as per wp-admin/includes/upload.php
1135 $post_id = wp_insert_attachment( $post, $upload['file'] );
1136 wp_update_attachment_metadata( $post_id, wp_generate_attachment_metadata( $post_id, $upload['file'] ) );
1137
1138 // remap resized image URLs, works by stripping the extension and remapping the URL stub.
1139 if ( preg_match( '!^image/!', $info['type'] ) ) {
1140 $parts = pathinfo( $url );
1141 $name = basename( $parts['basename'], ".{$parts['extension']}" ); // PATHINFO_FILENAME in PHP 5.2
1142
1143 $parts_new = pathinfo( $upload['url'] );
1144 $name_new = basename( $parts_new['basename'], ".{$parts_new['extension']}" );
1145
1146 $this->url_remap[ $parts['dirname'] . '/' . $name ] = $parts_new['dirname'] . '/' . $name_new;
1147 }
1148
1149 return $post_id;
1150 }
1151
1152 /**
1153 * Attempt to download a remote file attachment
1154 *
1155 * @param string $url URL of item to fetch
1156 * @param array $post Attachment details
1157 * @return array|WP_Error Local file location details on success, WP_Error otherwise
1158 */
1159 public function fetch_remote_file( $url, $post ) {
1160 // Extract the file name from the URL.
1161 $path = parse_url( $url, PHP_URL_PATH );
1162 $file_name = '';
1163 if ( is_string( $path ) ) {
1164 $file_name = basename( $path );
1165 }
1166
1167 if ( ! $file_name ) {
1168 $file_name = md5( $url );
1169 }
1170
1171 $tmp_file_name = wp_tempnam( $file_name );
1172 if ( ! $tmp_file_name ) {
1173 return new WP_Error( 'import_no_file', __( 'Could not create temporary file.', 'wordpress-importer' ) );
1174 }
1175
1176 // Fetch the remote URL and write it to the placeholder file.
1177 $remote_response = wp_safe_remote_get(
1178 $url,
1179 array(
1180 'timeout' => 300,
1181 'stream' => true,
1182 'filename' => $tmp_file_name,
1183 'headers' => array(
1184 'Accept-Encoding' => 'identity',
1185 ),
1186 )
1187 );
1188
1189 if ( is_wp_error( $remote_response ) ) {
1190 @unlink( $tmp_file_name );
1191 return new WP_Error(
1192 'import_file_error',
1193 sprintf(
1194 /* translators: 1: The WordPress error message. 2: The WordPress error code. */
1195 __( 'Request failed due to an error: %1$s (%2$s)', 'wordpress-importer' ),
1196 esc_html( $remote_response->get_error_message() ),
1197 esc_html( $remote_response->get_error_code() )
1198 )
1199 );
1200 }
1201
1202 $remote_response_code = (int) wp_remote_retrieve_response_code( $remote_response );
1203
1204 // Make sure the fetch was successful.
1205 if ( 200 !== $remote_response_code ) {
1206 @unlink( $tmp_file_name );
1207 return new WP_Error(
1208 'import_file_error',
1209 sprintf(
1210 /* translators: 1: The HTTP error message. 2: The HTTP error code. */
1211 __( 'Remote server returned the following unexpected result: %1$s (%2$s)', 'wordpress-importer' ),
1212 get_status_header_desc( $remote_response_code ),
1213 esc_html( $remote_response_code )
1214 )
1215 );
1216 }
1217
1218 $headers = wp_remote_retrieve_headers( $remote_response );
1219
1220 // Request failed.
1221 if ( ! $headers ) {
1222 @unlink( $tmp_file_name );
1223 return new WP_Error( 'import_file_error', __( 'Remote server did not respond', 'wordpress-importer' ) );
1224 }
1225
1226 $filesize = (int) filesize( $tmp_file_name );
1227
1228 if ( 0 === $filesize ) {
1229 @unlink( $tmp_file_name );
1230 return new WP_Error( 'import_file_error', __( 'Zero size file downloaded', 'wordpress-importer' ) );
1231 }
1232
1233 if ( ! isset( $headers['content-encoding'] ) && isset( $headers['content-length'] ) && $filesize !== (int) $headers['content-length'] ) {
1234 @unlink( $tmp_file_name );
1235 return new WP_Error( 'import_file_error', __( 'Downloaded file has incorrect size', 'wordpress-importer' ) );
1236 }
1237
1238 $max_size = (int) $this->max_attachment_size();
1239 if ( ! empty( $max_size ) && $filesize > $max_size ) {
1240 @unlink( $tmp_file_name );
1241 return new WP_Error( 'import_file_error', sprintf( __( 'Remote file is too large, limit is %s', 'wordpress-importer' ), size_format( $max_size ) ) );
1242 }
1243
1244 // Override file name with Content-Disposition header value.
1245 if ( ! empty( $headers['content-disposition'] ) ) {
1246 $file_name_from_disposition = self::get_filename_from_disposition( (array) $headers['content-disposition'] );
1247 if ( $file_name_from_disposition ) {
1248 $file_name = $file_name_from_disposition;
1249 }
1250 }
1251
1252 // Set file extension if missing.
1253 $file_ext = pathinfo( $file_name, PATHINFO_EXTENSION );
1254 if ( ! $file_ext && ! empty( $headers['content-type'] ) ) {
1255 $extension = self::get_file_extension_by_mime_type( $headers['content-type'] );
1256 if ( $extension ) {
1257 $file_name = "{$file_name}.{$extension}";
1258 }
1259 }
1260
1261 // Handle the upload like _wp_handle_upload() does.
1262 $wp_filetype = wp_check_filetype_and_ext( $tmp_file_name, $file_name );
1263 $ext = empty( $wp_filetype['ext'] ) ? '' : $wp_filetype['ext'];
1264 $type = empty( $wp_filetype['type'] ) ? '' : $wp_filetype['type'];
1265 $proper_filename = empty( $wp_filetype['proper_filename'] ) ? '' : $wp_filetype['proper_filename'];
1266
1267 // Check to see if wp_check_filetype_and_ext() determined the filename was incorrect.
1268 if ( $proper_filename ) {
1269 $file_name = $proper_filename;
1270 }
1271
1272 if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
1273 return new WP_Error( 'import_file_error', __( 'Sorry, this file type is not permitted for security reasons.', 'wordpress-importer' ) );
1274 }
1275
1276 $uploads = wp_upload_dir( $post['upload_date'] );
1277 if ( ! ( $uploads && false === $uploads['error'] ) ) {
1278 return new WP_Error( 'upload_dir_error', $uploads['error'] );
1279 }
1280
1281 // Move the file to the uploads dir.
1282 $file_name = wp_unique_filename( $uploads['path'], $file_name );
1283 $new_file = $uploads['path'] . "/$file_name";
1284 $move_new_file = copy( $tmp_file_name, $new_file );
1285
1286 if ( ! $move_new_file ) {
1287 @unlink( $tmp_file_name );
1288 return new WP_Error( 'import_file_error', __( 'The uploaded file could not be moved', 'wordpress-importer' ) );
1289 }
1290
1291 // Set correct file permissions.
1292 $stat = stat( dirname( $new_file ) );
1293 $perms = $stat['mode'] & 0000666;
1294 chmod( $new_file, $perms );
1295
1296 $upload = array(
1297 'file' => $new_file,
1298 'url' => $uploads['url'] . "/$file_name",
1299 'type' => $wp_filetype['type'],
1300 'error' => false,
1301 );
1302
1303 /**
1304 * When URL rewriting is enabled, posts such as this one:
1305 *
1306 * <img src="https://example.com/subpath/wp-content/uploads/2008/06/canola2.jpg" />
1307 *
1308 * Are already stored as:
1309 *
1310 * <img src="https://example.org/wp-content/uploads/2008/06/canola2.jpg" />
1311 *
1312 * Therefore, we can't just remap the old URL to the new URL here. This substring
1313 * is no longer present in the post:
1314 *
1315 * https://example.com/subpath/wp-content/uploads/2008/06/canola2.jpg
1316 *
1317 * We need to replace the base URL in the media file URL the same way as we did
1318 * in the post content:
1319 *
1320 * https://example.org/wp-content/uploads/2008/06/canola2.jpg
1321 *
1322 * Only from there we can remap that URL to the new media files URL:
1323 *
1324 * https://example.org/wp-content/uploads/canola2.jpg"
1325 * ^ there may be no 2008/06 on the target site.
1326 */
1327 if ( $this->options['rewrite_urls'] ) {
1328 $url = WPURL::replace_base_url(
1329 array(
1330 'url' => $url,
1331 'old_base_url' => $this->base_url_parsed,
1332 'new_base_url' => $this->site_url_parsed,
1333 )
1334 );
1335 $post['guid'] = WPURL::replace_base_url(
1336 array(
1337 'url' => $post['guid'],
1338 'old_base_url' => $this->base_url_parsed,
1339 'new_base_url' => $this->site_url_parsed,
1340 )
1341 );
1342 if ( isset( $headers['x-final-location'] ) ) {
1343 $headers['x-final-location'] = WPURL::replace_base_url(
1344 array(
1345 'url' => $headers['x-final-location'],
1346 'old_base_url' => $this->base_url_parsed,
1347 'new_base_url' => $this->site_url_parsed,
1348 )
1349 );
1350 }
1351 }
1352
1353 $this->url_remap[ $url ] = $upload['url'];
1354 $this->url_remap[ $post['guid'] ] = $upload['url']; // r13735, really needed?
1355 // keep track of the destination if the remote url is redirected somewhere else
1356 if ( isset( $headers['x-final-location'] ) && $headers['x-final-location'] != $url ) {
1357 $this->url_remap[ $headers['x-final-location'] ] = $upload['url'];
1358 }
1359
1360 return $upload;
1361 }
1362
1363 /**
1364 * Attempt to associate posts and menu items with previously missing parents
1365 *
1366 * An imported post's parent may not have been imported when it was first created
1367 * so try again. Similarly for child menu items and menu items which were missing
1368 * the object (e.g. post) they represent in the menu
1369 */
1370 public function backfill_parents() {
1371 global $wpdb;
1372
1373 // find parents for post orphans
1374 foreach ( $this->post_orphans as $child_id => $parent_id ) {
1375 $local_child_id = false;
1376 $local_parent_id = false;
1377 if ( isset( $this->processed_posts[ $child_id ] ) ) {
1378 $local_child_id = $this->processed_posts[ $child_id ];
1379 }
1380 if ( isset( $this->processed_posts[ $parent_id ] ) ) {
1381 $local_parent_id = $this->processed_posts[ $parent_id ];
1382 }
1383
1384 if ( $local_child_id && $local_parent_id ) {
1385 $wpdb->update( $wpdb->posts, array( 'post_parent' => $local_parent_id ), array( 'ID' => $local_child_id ), '%d', '%d' );
1386 clean_post_cache( $local_child_id );
1387 }
1388 }
1389
1390 // all other posts/terms are imported, retry menu items with missing associated object
1391 $missing_menu_items = $this->missing_menu_items;
1392 foreach ( $missing_menu_items as $item ) {
1393 $this->process_menu_item( $item );
1394 }
1395
1396 // find parents for menu item orphans
1397 foreach ( $this->menu_item_orphans as $child_id => $parent_id ) {
1398 $local_child_id = 0;
1399 $local_parent_id = 0;
1400 if ( isset( $this->processed_menu_items[ $child_id ] ) ) {
1401 $local_child_id = $this->processed_menu_items[ $child_id ];
1402 }
1403 if ( isset( $this->processed_menu_items[ $parent_id ] ) ) {
1404 $local_parent_id = $this->processed_menu_items[ $parent_id ];
1405 }
1406
1407 if ( $local_child_id && $local_parent_id ) {
1408 update_post_meta( $local_child_id, '_menu_item_menu_item_parent', (int) $local_parent_id );
1409 }
1410 }
1411 }
1412
1413 /**
1414 * Use stored mapping information to update old attachment URLs
1415 */
1416 public function backfill_attachment_urls() {
1417 global $wpdb;
1418 // make sure we do the longest urls first, in case one is a substring of another
1419 uksort( $this->url_remap, array( &$this, 'cmpr_strlen' ) );
1420
1421 foreach ( $this->url_remap as $from_url => $to_url ) {
1422 // remap urls in post_content
1423 $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s)", $from_url, $to_url ) );
1424 // remap enclosure urls
1425 $result = $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_key='enclosure'", $from_url, $to_url ) );
1426 }
1427 }
1428
1429 /**
1430 * Update _thumbnail_id meta to new, imported attachment IDs
1431 */
1432 public function remap_featured_images() {
1433 // cycle through posts that have a featured image
1434 foreach ( $this->featured_images as $post_id => $value ) {
1435 if ( isset( $this->processed_posts[ $value ] ) ) {
1436 $new_id = $this->processed_posts[ $value ];
1437 // only update if there's a difference
1438 if ( $new_id != $value ) {
1439 update_post_meta( $post_id, '_thumbnail_id', $new_id );
1440 }
1441 }
1442 }
1443 }
1444
1445 /**
1446 * Parse a WXR file
1447 *
1448 * @param string $file Path to WXR file for parsing
1449 * @return array Information gathered from the WXR file
1450 */
1451 public function parse( $file ) {
1452 $parser = new WXR_Parser();
1453 return $parser->parse( $file );
1454 }
1455
1456 // Display import page title
1457 public function header() {
1458 echo '<div class="wrap">';
1459 echo '<h2>' . __( 'Import WordPress', 'wordpress-importer' ) . '</h2>';
1460
1461 $updates = get_plugin_updates();
1462 $basename = plugin_basename( __FILE__ );
1463 if ( isset( $updates[ $basename ] ) ) {
1464 $update = $updates[ $basename ];
1465 echo '<div class="error"><p><strong>';
1466 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 );
1467 echo '</strong></p></div>';
1468 }
1469 }
1470
1471 // Close div.wrap
1472 public function footer() {
1473 echo '</div>';
1474 }
1475
1476 /**
1477 * Display introductory text and file upload form
1478 */
1479 public function greet() {
1480 echo '<div class="narrow">';
1481 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>';
1482 echo '<p>' . __( 'Choose a WXR (.xml) file to upload, then click Upload file and import.', 'wordpress-importer' ) . '</p>';
1483 wp_import_upload_form( 'admin.php?import=wordpress&amp;step=1' );
1484 echo '</div>';
1485 }
1486
1487 /**
1488 * Decide if the given meta key maps to information we will want to import
1489 *
1490 * @param string $key The meta key to check
1491 * @return string|bool The key if we do want to import, false if not
1492 */
1493 public function is_valid_meta_key( $key ) {
1494 // skip attachment metadata since we'll regenerate it from scratch
1495 // skip _edit_lock as not relevant for import
1496 if ( in_array( $key, array( '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock' ), true ) ) {
1497 return false;
1498 }
1499 return $key;
1500 }
1501
1502 /**
1503 * Decide whether or not the importer is allowed to create users.
1504 * Default is true, can be filtered via import_allow_create_users
1505 *
1506 * @return bool True if creating users is allowed
1507 */
1508 public function allow_create_users() {
1509 return apply_filters( 'import_allow_create_users', true );
1510 }
1511
1512 /**
1513 * Decide whether or not the importer should attempt to download attachment files.
1514 * Default is true, can be filtered via import_allow_fetch_attachments. The choice
1515 * made at the import options screen must also be true, false here hides that checkbox.
1516 *
1517 * @return bool True if downloading attachments is allowed
1518 */
1519 public function allow_fetch_attachments() {
1520 return apply_filters( 'import_allow_fetch_attachments', true );
1521 }
1522
1523 /**
1524 * Decide what the maximum file size for downloaded attachments is.
1525 * Default is 0 (unlimited), can be filtered via import_attachment_size_limit
1526 *
1527 * @return int Maximum attachment file size to import
1528 */
1529 public function max_attachment_size() {
1530 return apply_filters( 'import_attachment_size_limit', 0 );
1531 }
1532
1533 /**
1534 * Added to http_request_timeout filter to force timeout at 60 seconds during import
1535 * @return int 60
1536 */
1537 public function bump_request_timeout( $val ) {
1538 return 60;
1539 }
1540
1541 // return the difference in length between two strings
1542 public function cmpr_strlen( $a, $b ) {
1543 return strlen( $b ) - strlen( $a );
1544 }
1545
1546 /**
1547 * Parses filename from a Content-Disposition header value.
1548 *
1549 * As per RFC6266:
1550 *
1551 * content-disposition = "Content-Disposition" ":"
1552 * disposition-type *( ";" disposition-parm )
1553 *
1554 * disposition-type = "inline" | "attachment" | disp-ext-type
1555 * ; case-insensitive
1556 * disp-ext-type = token
1557 *
1558 * disposition-parm = filename-parm | disp-ext-parm
1559 *
1560 * filename-parm = "filename" "=" value
1561 * | "filename*" "=" ext-value
1562 *
1563 * disp-ext-parm = token "=" value
1564 * | ext-token "=" ext-value
1565 * ext-token = <the characters in token, followed by "*">
1566 *
1567 * @since 0.7.0
1568 *
1569 * @see WP_REST_Attachments_Controller::get_filename_from_disposition()
1570 *
1571 * @link http://tools.ietf.org/html/rfc2388
1572 * @link http://tools.ietf.org/html/rfc6266
1573 *
1574 * @param string[] $disposition_header List of Content-Disposition header values.
1575 * @return string|null Filename if available, or null if not found.
1576 */
1577 protected static function get_filename_from_disposition( $disposition_header ) {
1578 // Get the filename.
1579 $filename = null;
1580
1581 foreach ( $disposition_header as $value ) {
1582 $value = trim( $value );
1583
1584 if ( strpos( $value, ';' ) === false ) {
1585 continue;
1586 }
1587
1588 list( $type, $attr_parts ) = explode( ';', $value, 2 );
1589
1590 $attr_parts = explode( ';', $attr_parts );
1591 $attributes = array();
1592
1593 foreach ( $attr_parts as $part ) {
1594 if ( strpos( $part, '=' ) === false ) {
1595 continue;
1596 }
1597
1598 list( $key, $value ) = explode( '=', $part, 2 );
1599
1600 $attributes[ trim( $key ) ] = trim( $value );
1601 }
1602
1603 if ( empty( $attributes['filename'] ) ) {
1604 continue;
1605 }
1606
1607 $filename = trim( $attributes['filename'] );
1608
1609 // Unquote quoted filename, but after trimming.
1610 if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) {
1611 $filename = substr( $filename, 1, -1 );
1612 }
1613 }
1614
1615 return $filename;
1616 }
1617
1618 /**
1619 * Retrieves file extension by mime type.
1620 *
1621 * @since 0.7.0
1622 *
1623 * @param string $mime_type Mime type to search extension for.
1624 * @return string|null File extension if available, or null if not found.
1625 */
1626 protected static function get_file_extension_by_mime_type( $mime_type ) {
1627 static $map = null;
1628
1629 if ( is_array( $map ) ) {
1630 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
1631 }
1632
1633 $mime_types = wp_get_mime_types();
1634 $map = array_flip( $mime_types );
1635
1636 // Some types have multiple extensions, use only the first one.
1637 foreach ( $map as $type => $extensions ) {
1638 $map[ $type ] = strtok( $extensions, '|' );
1639 }
1640
1641 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
1642 }
1643
1644 /**
1645 * Unserializes data only if it was serialized.
1646 *
1647 * @since 0.8.4
1648 *
1649 * @param string $data Data that might be unserialized.
1650 * @return mixed Unserialized data can be any type.
1651 */
1652 protected function maybe_unserialize( $data ) {
1653 // Don't attempt to unserialize data that wasn't serialized going in.
1654 if ( is_serialized( $data ) ) {
1655 // Transform the serialized objects to a stdClass object.
1656 $data = preg_replace( '/O:\d+:"[^"]+":/', 'O:8:"stdClass":', $data );
1657
1658 return maybe_unserialize( $data );
1659 }
1660
1661 return $data;
1662 }
1663 }
1664