PluginProbe
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder / 2.9.1
aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder v2.9.1
2.13.0 2.13.1 2.12.0 2.11.1 2.11.0 2.10.0 2.9.0 2.7.4 2.7.5 2.7.6 2.7.7 2.8.0 2.8.1 2.9.1 trunk 1.0 1.0-beta1 1.0-beta2 1.0-beta3 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 All 80 releases
ablocks / includes / import / wp-import.php

wp-import.php in aBlocks – Gutenberg Blocks, User Dashboard Builder, Popup Builder, Form Builder & Animation Builder 2.9.1, at includes/import/wp-import.php

1,330 lines 41.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace ABlocks\import;
4
5 use ABlocks\Helper;
6 use ABlocks\import\XmlParsers\WXR_Parser;
7 use WP_Error;
8
9 if ( ! defined( 'ABSPATH' ) ) {
10 exit;
11 }
12
13 /**
14 * WordPress importer class.
15 */
16 class WP_Import extends \WP_Importer {
17
18 public $id;
19
20 public $version;
21 public $authors = array();
22 public $posts = array();
23 public $terms = array();
24 public $categories = array();
25 public $tags = array();
26 public $base_url = '';
27 public $show_on_front = 'posts';
28 public $page_on_front = 0;
29 public $page_for_posts = 0;
30
31 public $processed_authors = array();
32 public $author_mapping = array();
33 public $processed_terms = array();
34 public $processed_posts = array();
35 public $post_orphans = array();
36 public $processed_menu_items = array();
37 public $menu_item_orphans = array();
38 public $missing_menu_items = array();
39
40 public $fetch_attachments = true;
41 public $url_remap = array();
42 public $featured_images = array();
43
44 /**
45 * The main controller for the actual import stage.
46 *
47 * @param string $file Path to the WXR file for importing.
48 * @param bool $with_attachments with attachments ?.
49 */
50 public function import( string $file, bool $with_attachments = true ) {
51 add_filter( 'import_post_meta_key', array( $this, 'is_valid_meta_key' ) );
52 add_filter( 'http_request_timeout', array( &$this, 'bump_request_timeout' ) );
53
54 $this->fetch_attachments = $with_attachments;
55
56 $result = $this->import_start( $file );
57 if ( is_wp_error( $result ) ) {
58 return $result;
59 }
60
61 wp_suspend_cache_invalidation();
62 $this->process_categories();
63 $this->process_tags();
64 $this->process_terms();
65 $this->process_posts();
66 wp_suspend_cache_invalidation( false );
67
68 // update incorrect/missing information in the DB
69 $this->backfill_parents();
70 $this->backfill_attachment_urls();
71 $this->remap_featured_images();
72
73 $this->import_end();
74
75 return true;
76 }
77
78 /**
79 * Parses the WXR file and prepares us for the task of processing parsed data
80 *
81 * @param string $file Path to the WXR file for importing.
82 */
83 public function import_start( string $file ) {
84 if ( ! is_file( $file ) ) {
85 return new WP_Error( 'file_not_found', __( 'File not found', 'ablocks' ) );
86 }
87
88 Helper::emit_sse_message( [
89 'action' => 'log',
90 'message' => __( 'Import process started...', 'ablocks' )
91 ] );
92
93 $import_data = $this->parse( $file );
94
95 if ( is_wp_error( $import_data ) ) {
96 return $import_data;
97 }
98
99 $this->version = $import_data['version'];
100 $this->get_authors_from_import( $import_data );
101 $this->posts = $import_data['posts'];
102 $this->terms = $import_data['terms'];
103 $this->categories = $import_data['categories'];
104 $this->tags = $import_data['tags'];
105 $this->base_url = esc_url( $import_data['base_url'] );
106 $this->show_on_front = $import_data['show_on_front'];
107 $this->page_on_front = $import_data['page_on_front'];
108 $this->page_for_posts = $import_data['page_for_posts'];
109
110 wp_defer_term_counting( true );
111 wp_defer_comment_counting( true );
112
113 do_action( 'ablocks/template_import_start' );
114
115 return true;
116 }
117
118 /**
119 * Performs post-import cleanup of files and the cache
120 */
121 public function import_end() {
122 wp_import_cleanup( $this->id );
123
124 wp_cache_flush();
125 foreach ( get_taxonomies() as $tax ) {
126 delete_option( "{$tax}_children" );
127 _get_term_hierarchy( $tax );
128 }
129
130 wp_defer_term_counting( false );
131 wp_defer_comment_counting( false );
132
133 do_action( 'ablocks/template_import_end' );
134
135 Helper::emit_sse_message( [
136 'action' => 'log',
137 'level' => 'info',
138 'message' => __( 'Import process Finished.', 'ablocks' ),
139 ] );
140 }
141
142 /**
143 * Handles the WXR upload and initial parsing of the file to prepare for
144 * displaying author import options
145 *
146 * @return bool False if error uploading or invalid file, true otherwise
147 */
148 public function handle_upload(): bool {
149 $file = wp_import_handle_upload();
150
151 if ( isset( $file['error'] ) || ! file_exists( $file['file'] ) ) {
152 return false;
153 }
154
155 $this->id = (int) $file['id'];
156 $import_data = $this->parse( $file['file'] );
157 if ( is_wp_error( $import_data ) ) {
158 return false;
159 }
160
161 $this->version = $import_data['version'];
162 $this->get_authors_from_import( $import_data );
163
164 return true;
165 }
166
167 /**
168 * Retrieve authors from parsed WXR data
169 *
170 * Uses the provided author information from WXR 1.1 files
171 * or extracts info from each post for WXR 1.0 files
172 *
173 * @param array $import_data Data returned by a WXR parser.
174 */
175 public function get_authors_from_import( $import_data ) {
176 if ( ! empty( $import_data['authors'] ) ) {
177 $this->authors = $import_data['authors'];
178 // no author information, grab it from the posts
179 } else {
180 foreach ( $import_data['posts'] as $post ) {
181 $login = sanitize_user( $post['post_author'], true );
182 if ( empty( $login ) ) {
183 continue;
184 }
185
186 if ( ! isset( $this->authors[ $login ] ) ) {
187 $this->authors[ $login ] = array(
188 'author_login' => $login,
189 'author_display_name' => $post['post_author'],
190 );
191 }
192 }
193 }
194 }
195
196 /**
197 * Create new categories based on import information
198 *
199 * Doesn't create a new category if its slug already exists
200 */
201 public function process_categories() {
202 $this->categories = apply_filters( 'ablocks/template_import_categories', $this->categories );
203
204 if ( empty( $this->categories ) ) {
205 return;
206 }
207
208 foreach ( $this->categories as $cat ) {
209 // if the category already exists leave it alone
210 $term_id = term_exists( $cat['category_nicename'], 'category' );
211 if ( $term_id ) {
212 if ( is_array( $term_id ) ) {
213 $term_id = $term_id['term_id'];
214 }
215 if ( isset( $cat['term_id'] ) ) {
216 $this->processed_terms[ intval( $cat['term_id'] ) ] = (int) $term_id;
217 }
218 continue;
219 }
220
221 $parent = empty( $cat['category_parent'] ) ? 0 : category_exists( $cat['category_parent'] );
222 $description = isset( $cat['category_description'] ) ? $cat['category_description'] : '';
223
224 $data = array(
225 'category_nicename' => $cat['category_nicename'],
226 'category_parent' => $parent,
227 'cat_name' => wp_slash( $cat['cat_name'] ),
228 'category_description' => wp_slash( $description ),
229 );
230
231 $id = wp_insert_category( $data, true );
232 if ( ! is_wp_error( $id ) && $id > 0 ) {
233 if ( isset( $cat['term_id'] ) ) {
234 $this->processed_terms[ intval( $cat['term_id'] ) ] = $id;
235 }
236 } else {
237 continue;
238 }
239
240 $this->process_termmeta( $cat, $id );
241 }//end foreach
242
243 Helper::emit_sse_message( [
244 'action' => 'log',
245 'level' => 'info',
246 'message' => __( 'Categories imported', 'ablocks' ),
247 ] );
248
249 unset( $this->categories );
250 }
251
252 /**
253 * Create new post tags based on import information
254 *
255 * Doesn't create a tag if its slug already exists
256 */
257 public function process_tags() {
258 $this->tags = apply_filters( 'ablocks/template_import_tags', $this->tags );
259
260 if ( empty( $this->tags ) ) {
261 return;
262 }
263
264 foreach ( $this->tags as $tag ) {
265 // if the tag already exists leave it alone
266 $term_id = term_exists( $tag['tag_slug'], 'post_tag' );
267 if ( $term_id ) {
268 if ( is_array( $term_id ) ) {
269 $term_id = $term_id['term_id'];
270 }
271 if ( isset( $tag['term_id'] ) ) {
272 $this->processed_terms[ intval( $tag['term_id'] ) ] = (int) $term_id;
273 }
274 continue;
275 }
276
277 $description = isset( $tag['tag_description'] ) ? $tag['tag_description'] : '';
278 $args = array(
279 'slug' => $tag['tag_slug'],
280 'description' => wp_slash( $description ),
281 );
282
283 $id = wp_insert_term( wp_slash( $tag['tag_name'] ), 'post_tag', $args );
284 if ( ! is_wp_error( $id ) ) {
285 if ( isset( $tag['term_id'] ) ) {
286 $this->processed_terms[ intval( $tag['term_id'] ) ] = $id['term_id'];
287 }
288 } else {
289 continue;
290 }
291
292 $this->process_termmeta( $tag, $id['term_id'] );
293 }//end foreach
294
295 Helper::emit_sse_message( [
296 'action' => 'log',
297 'level' => 'info',
298 'message' => __( 'Tags imported', 'ablocks' ),
299 ] );
300
301 unset( $this->tags );
302 }
303
304 /**
305 * Create new terms based on import information
306 *
307 * Doesn't create a term its slug already exists
308 */
309 public function process_terms() {
310 $this->terms = apply_filters( 'ablocks/template_import_terms', $this->terms );
311
312 if ( empty( $this->terms ) ) {
313 return;
314 }
315
316 foreach ( $this->terms as $term ) {
317 // if the term already exists in the correct taxonomy leave it alone
318 $term_id = term_exists( $term['slug'], $term['term_taxonomy'] );
319 if ( $term_id ) {
320 if ( is_array( $term_id ) ) {
321 $term_id = $term_id['term_id'];
322 }
323 if ( isset( $term['term_id'] ) ) {
324 $this->processed_terms[ intval( $term['term_id'] ) ] = (int) $term_id;
325 }
326 continue;
327 }
328
329 if ( empty( $term['term_parent'] ) ) {
330 $parent = 0;
331 } else {
332 $parent = term_exists( $term['term_parent'], $term['term_taxonomy'] );
333 if ( is_array( $parent ) ) {
334 $parent = $parent['term_id'];
335 }
336 }
337
338 $description = isset( $term['term_description'] ) ? $term['term_description'] : '';
339 $args = array(
340 'slug' => $term['slug'],
341 'description' => wp_slash( $description ),
342 'parent' => (int) $parent,
343 );
344
345 $id = wp_insert_term( wp_slash( $term['term_name'] ), $term['term_taxonomy'], $args );
346 if ( ! is_wp_error( $id ) ) {
347 if ( isset( $term['term_id'] ) ) {
348 $this->processed_terms[ intval( $term['term_id'] ) ] = $id['term_id'];
349 }
350 } else {
351 continue;
352 }
353
354 $this->process_termmeta( $term, $id['term_id'] );
355 }//end foreach
356
357 Helper::emit_sse_message( [
358 'action' => 'log',
359 'level' => 'info',
360 'message' => __( 'Terms imported', 'ablocks' ),
361 ] );
362
363 unset( $this->terms );
364 }
365
366 /**
367 * Add metadata to imported term.
368 *
369 * @param array $term Term data from WXR import.
370 * @param int $term_id ID of the newly created term.
371 *
372 * @since 0.6.2
373 */
374 protected function process_termmeta( $term, $term_id ) {
375 if ( ! isset( $term['termmeta'] ) ) {
376 $term['termmeta'] = array();
377 }
378
379 /**
380 * Filters the metadata attached to an imported term.
381 *
382 * @param array $termmeta Array of term meta.
383 * @param int $term_id ID of the newly created term.
384 * @param array $term Term data from the WXR import.
385 *
386 * @since 0.6.2
387 */
388 $term['termmeta'] = apply_filters( 'ablocks/template_import_term_meta', $term['termmeta'], $term_id, $term );
389
390 if ( empty( $term['termmeta'] ) ) {
391 return;
392 }
393
394 foreach ( $term['termmeta'] as $meta ) {
395 /**
396 * Filters the meta key for an imported piece of term meta.
397 *
398 * @param string $meta_key Meta key.
399 * @param int $term_id ID of the newly created term.
400 * @param array $term Term data from the WXR import.
401 *
402 * @since 0.6.2
403 */
404 $key = apply_filters( 'import_term_meta_key', $meta['key'], $term_id, $term );
405 if ( ! $key ) {
406 continue;
407 }
408
409 // Export gets meta straight from the DB so could have a serialized string
410 $value = maybe_unserialize( $meta['value'] );
411
412 add_term_meta( $term_id, wp_slash( $key ), wp_slash_strings_only( $value ) );
413
414 /**
415 * Fires after term meta is imported.
416 *
417 * @param int $term_id ID of the newly created term.
418 * @param string $key Meta key.
419 * @param mixed $value Meta value.
420 *
421 * @since 0.6.2
422 */
423 do_action( 'import_term_meta', $term_id, $key, $value );
424 }//end foreach
425 }
426
427 /**
428 * Create new posts based on import information
429 *
430 * Posts marked as having a parent which doesn't exist will become top level items.
431 * Doesn't create a new post if: the post type doesn't exist, the given post ID
432 * is already noted as imported or a post with the same title and date already exists.
433 * Note that new/updated terms, comments and meta are imported for the last of the above.
434 */
435 public function process_posts() {
436 $this->posts = apply_filters( 'ablocks/template_import_posts', $this->posts );
437
438 foreach ( $this->posts as $post ) {
439 $post = apply_filters( 'ablocks/template_import_post_data_raw', $post );
440
441 if ( ! post_type_exists( $post['post_type'] ) ) {
442 do_action( 'ablocks/template_import_post_exists', $post );
443 continue;
444 }
445
446 if ( isset( $this->processed_posts[ $post['post_id'] ] ) && ! empty( $post['post_id'] ) ) {
447 continue;
448 }
449
450 if ( 'auto-draft' === $post['status'] ) {
451 continue;
452 }
453
454 if ( 'nav_menu_item' === $post['post_type'] ) {
455 $this->process_menu_item( $post );
456 continue;
457 }
458
459 $post_type_object = get_post_type_object( $post['post_type'] );
460
461 $post_exists = post_exists( $post['post_title'], '', $post['post_date'], $post['post_type'] );
462
463 /**
464 * Filter ID of the existing post corresponding to post currently importing.
465 *
466 * Return 0 to force the post to be imported. Filter the ID to be something else
467 * to override which existing post is mapped to the imported post.
468 *
469 * @param int $post_exists Post ID, or 0 if post did not exist.
470 * @param array $post The post array to be inserted.
471 *
472 * @see post_exists()
473 * @since 0.6.2
474 */
475 $post_exists = apply_filters( 'ablocks/template_import_existing_post', $post_exists, $post );
476
477 if ( $post_exists && get_post_type( $post_exists ) === $post['post_type'] ) {
478 $comment_post_id = $post_exists;
479 $post_id = $post_exists;
480 $this->processed_posts[ intval( $post['post_id'] ) ] = intval( $post_exists );
481 } else {
482 $post_parent = (int) $post['post_parent'];
483 if ( $post_parent ) {
484 // if we already know the parent, map it to the new local ID
485 if ( isset( $this->processed_posts[ $post_parent ] ) ) {
486 $post_parent = $this->processed_posts[ $post_parent ];
487 // otherwise record the parent for later
488 } else {
489 $this->post_orphans[ intval( $post['post_id'] ) ] = $post_parent;
490 $post_parent = 0;
491 }
492 }
493
494 // map the post author
495 $author = sanitize_user( $post['post_author'], true );
496 if ( isset( $this->author_mapping[ $author ] ) ) {
497 $author = $this->author_mapping[ $author ];
498 } else {
499 $author = (int) get_current_user_id();
500 }
501
502 $postdata = array(
503 'import_id' => $post['post_id'],
504 'post_author' => $author,
505 'post_date' => $post['post_date'],
506 'post_date_gmt' => $post['post_date_gmt'],
507 'post_content' => $post['post_content'],
508 'post_excerpt' => $post['post_excerpt'],
509 'post_title' => $post['post_title'],
510 'post_status' => $post['status'],
511 'post_name' => $post['post_name'],
512 'comment_status' => $post['comment_status'],
513 'ping_status' => $post['ping_status'],
514 'guid' => $post['guid'],
515 'post_parent' => $post_parent,
516 'menu_order' => $post['menu_order'],
517 'post_type' => $post['post_type'],
518 'post_password' => $post['post_password'],
519 );
520
521 // remove existence global styles.
522 if ( 'wp_global_styles' === $post['post_type'] ) {
523 $this->remove_gutenberg_styles();
524 }
525
526 $original_post_id = $post['post_id'];
527 $postdata = apply_filters( 'ablocks/template_import_post_data_processed', $postdata, $post );
528
529 $postdata = wp_slash( $postdata );
530
531 if ( 'attachment' === $postdata['post_type'] ) {
532 if ( ! $this->fetch_attachments ) {
533 continue;
534 }
535 $remote_url = ! empty( $post['attachment_url'] ) ? $post['attachment_url'] : $post['guid'];
536
537 // try to use _wp_attached file for upload folder placement to ensure the same location as the export site
538 // e.g. location is 2003/05/image.jpg but the attachment post_date is 2010/09, see media_handle_upload()
539 $postdata['upload_date'] = $post['post_date'];
540 if ( isset( $post['postmeta'] ) ) {
541 foreach ( $post['postmeta'] as $meta ) {
542 if ( '_wp_attached_file' === $meta['key'] ) {
543 if ( preg_match( '%^[0-9]{4}/[0-9]{2}%', $meta['value'], $matches ) ) {
544 $postdata['upload_date'] = $matches[0];
545 }
546 break;
547 }
548 }
549 }
550
551 $comment_post_id = $this->process_attachment( $postdata, $remote_url );
552 $post_id = $comment_post_id;
553 } else {
554 $comment_post_id = wp_insert_post( $postdata, true );
555 $post_id = $comment_post_id;
556 do_action( 'ablocks/template_import_insert_post', $post_id, $original_post_id, $postdata, $post );
557 }//end if
558
559 if ( is_wp_error( $post_id ) ) {
560 Helper::emit_sse_message( [
561 'action' => 'log',
562 'level' => 'warning',
563 'message' => "Failed to import: {$post['post_type']} - {$post['post_title']}",
564 ] );
565 continue;
566 }
567
568 if ( 1 === (int) $post['is_sticky'] ) {
569 stick_post( $post_id );
570 }
571
572 Helper::emit_sse_message( [
573 'action' => 'log',
574 'level' => 'info',
575 'message' => "{$post['post_type']} - {$post['post_title']} imported.",
576 ] );
577 }//end if
578
579 // update reading settings.
580 if ( 'page' === $this->show_on_front && 'page' === $post['post_type'] ) {
581 update_option( 'show_on_front', 'page' );
582 if ( $this->page_on_front === $post['post_id'] ) {
583 update_option( 'page_on_front', $post_id );
584 Helper::emit_sse_message( [
585 'action' => 'log',
586 'level' => 'info',
587 'message' => __( 'Front page setting\'s updated.', 'ablocks' ),
588 ] );
589 } elseif ( $post['post_id'] === $this->page_for_posts ) {
590 update_option( 'page_for_posts', $post_id );
591 Helper::emit_sse_message( [
592 'action' => 'log',
593 'level' => 'info',
594 'message' => __( 'Posts page setting\'s updated.', 'ablocks' ),
595 ] );
596 }
597 }//end if
598
599 // map pre-import ID to local ID
600 $this->processed_posts[ intval( $post['post_id'] ) ] = (int) $post_id;
601
602 if ( ! isset( $post['terms'] ) ) {
603 $post['terms'] = array();
604 }
605
606 $post['terms'] = apply_filters( 'ablocks/template_import_post_terms', $post['terms'], $post_id, $post );
607
608 // add categories, tags and other terms
609 if ( ! empty( $post['terms'] ) ) {
610 $terms_to_set = array();
611 foreach ( $post['terms'] as $term ) {
612 // back compat with WXR 1.0 map 'tag' to 'post_tag'
613 $taxonomy = ( 'tag' === $term['domain'] ) ? 'post_tag' : $term['domain'];
614 if ( 'wp_theme' === $taxonomy ) {
615 $term['name'] = wp_get_theme()->get_stylesheet();
616 $term['slug'] = wp_get_theme()->get_stylesheet();
617 }
618 $term_exists = term_exists( $term['slug'], $taxonomy );
619 $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists;
620
621 if ( ! $term_id ) {
622 $t = wp_insert_term( $term['name'], $taxonomy, array( 'slug' => $term['slug'] ) );
623 if ( ! is_wp_error( $t ) ) {
624 $term_id = $t['term_id'];
625 do_action( 'ablocks/template_import_insert_term', $t, $term, $post_id, $post );
626 } else {
627 do_action( 'ablocks/template_import_insert_term_failed', $t, $term, $post_id, $post );
628 continue;
629 }
630 }
631 $terms_to_set[ $taxonomy ][] = intval( $term_id );
632 }//end foreach
633
634 foreach ( $terms_to_set as $tax => $ids ) {
635 $tt_ids = wp_set_post_terms( $post_id, $ids, $tax );
636 do_action( 'ablocks/template_import_set_post_terms', $tt_ids, $ids, $tax, $post_id, $post );
637 }
638 unset( $post['terms'], $terms_to_set );
639 }//end if
640
641 if ( ! isset( $post['comments'] ) ) {
642 $post['comments'] = array();
643 }
644
645 $post['comments'] = apply_filters( 'ablocks/template_import_post_comments', $post['comments'], $post_id, $post );
646
647 // add/update comments
648 if ( ! empty( $post['comments'] ) ) {
649 $num_comments = 0;
650 $inserted_comments = array();
651 foreach ( $post['comments'] as $comment ) {
652 $comment_id = $comment['comment_id'];
653 $newcomments[ $comment_id ]['comment_post_ID'] = $comment_post_id;
654 $newcomments[ $comment_id ]['comment_author'] = $comment['comment_author'];
655 $newcomments[ $comment_id ]['comment_author_email'] = $comment['comment_author_email'];
656 $newcomments[ $comment_id ]['comment_author_IP'] = $comment['comment_author_IP'];
657 $newcomments[ $comment_id ]['comment_author_url'] = $comment['comment_author_url'];
658 $newcomments[ $comment_id ]['comment_date'] = $comment['comment_date'];
659 $newcomments[ $comment_id ]['comment_date_gmt'] = $comment['comment_date_gmt'];
660 $newcomments[ $comment_id ]['comment_content'] = $comment['comment_content'];
661 $newcomments[ $comment_id ]['comment_approved'] = $comment['comment_approved'];
662 $newcomments[ $comment_id ]['comment_type'] = $comment['comment_type'];
663 $newcomments[ $comment_id ]['comment_parent'] = $comment['comment_parent'];
664 $newcomments[ $comment_id ]['commentmeta'] = isset( $comment['commentmeta'] ) ? $comment['commentmeta'] : array();
665 if ( isset( $this->processed_authors[ $comment['comment_user_id'] ] ) ) {
666 $newcomments[ $comment_id ]['user_id'] = $this->processed_authors[ $comment['comment_user_id'] ];
667 }
668 }
669 ksort( $newcomments );
670
671 foreach ( $newcomments as $key => $comment ) {
672 // if this is a new post we can skip the comment_exists() check
673 if ( ! $post_exists || ! comment_exists( $comment['comment_author'], $comment['comment_date'] ) ) {
674 if ( isset( $inserted_comments[ $comment['comment_parent'] ] ) ) {
675 $comment['comment_parent'] = $inserted_comments[ $comment['comment_parent'] ];
676 }
677
678 $comment_data = wp_slash( $comment );
679 unset( $comment_data['commentmeta'] ); // Handled separately, wp_insert_comment() also expects `comment_meta`.
680 $comment_data = wp_filter_comment( $comment_data );
681
682 $inserted_comments[ $key ] = wp_insert_comment( $comment_data );
683
684 do_action( 'ablocks/template_import_insert_comment', $inserted_comments[ $key ], $comment, $comment_post_id, $post );
685
686 foreach ( $comment['commentmeta'] as $meta ) {
687 $value = maybe_unserialize( $meta['value'] );
688
689 add_comment_meta( $inserted_comments[ $key ], wp_slash( $meta['key'] ), wp_slash_strings_only( $value ) );
690 }
691
692 ++ $num_comments;
693 }//end if
694 }//end foreach
695 unset( $newcomments, $inserted_comments, $post['comments'] );
696 }//end if
697
698 if ( ! isset( $post['postmeta'] ) ) {
699 $post['postmeta'] = array();
700 }
701
702 $post['postmeta'] = apply_filters( 'ablocks/template_import_post_meta', $post['postmeta'], $post_id, $post );
703
704 // add/update post meta
705 if ( ! empty( $post['postmeta'] ) ) {
706 foreach ( $post['postmeta'] as $meta ) {
707 $key = apply_filters( 'import_post_meta_key', $meta['key'], $post_id, $post );
708 $value = false;
709
710 if ( '_edit_last' === $key ) {
711 if ( isset( $this->processed_authors[ intval( $meta['value'] ) ] ) ) {
712 $value = $this->processed_authors[ intval( $meta['value'] ) ];
713 } else {
714 $key = false;
715 }
716 }
717
718 if ( $key ) {
719 // export gets meta straight from the DB so could have a serialized string
720 if ( ! $value ) {
721 $value = maybe_unserialize( $meta['value'] );
722 }
723
724 add_post_meta( $post_id, wp_slash( $key ), wp_slash_strings_only( $value ) );
725
726 do_action( 'import_post_meta', $post_id, $key, $value );
727
728 // if the post has a featured image, take note of this in case of remap
729 if ( '_thumbnail_id' === $key ) {
730 $this->featured_images[ $post_id ] = (int) $value;
731 }
732 }
733 }//end foreach
734 }//end if
735 }//end foreach
736
737 unset( $this->posts );
738 }
739
740 private function remove_gutenberg_styles() {
741 $args = array(
742 'post_type' => 'wp_global_styles',
743 'post_status' => 'any',
744 'posts_per_page' => 10,
745 'fields' => 'ids',
746 );
747 $styles = get_posts( $args );
748 foreach ( $styles as $style ) {
749 wp_delete_post( $style );
750 }
751 Helper::emit_sse_message( [
752 'action' => 'log',
753 'level' => 'info',
754 'message' => __( 'Existence gutenberg styles removed.', 'ablocks' ),
755 ] );
756 }
757
758 /**
759 * Attempt to create a new menu item from import data
760 *
761 * Fails for draft, orphaned menu items and those without an associated nav_menu
762 * or an invalid nav_menu term. If the post type or term object which the menu item
763 * represents doesn't exist then the menu item will not be imported (waits until the
764 * end of the import to retry again before discarding).
765 *
766 * @param array $item Menu item details from WXR file.
767 */
768 public function process_menu_item( $item ) {
769 // skip draft, orphaned menu items
770 if ( 'draft' === $item['status'] ) {
771 return;
772 }
773
774 $menu_slug = false;
775 if ( isset( $item['terms'] ) ) {
776 // loop through terms, assume first nav_menu term is correct menu
777 foreach ( $item['terms'] as $term ) {
778 if ( 'nav_menu' === $term['domain'] ) {
779 $menu_slug = $term['slug'];
780 break;
781 }
782 }
783 }
784
785 // no nav_menu term associated with this menu item
786 if ( ! $menu_slug ) {
787 return;
788 }
789
790 $menu_id = term_exists( $menu_slug, 'nav_menu' );
791 if ( ! $menu_id ) {
792 return;
793 } else {
794 $menu_id = is_array( $menu_id ) ? $menu_id['term_id'] : $menu_id;
795 }
796
797 foreach ( $item['postmeta'] as $meta ) {
798 ${$meta['key']} = $meta['value'];
799 }
800
801 if ( 'taxonomy' === $_menu_item_type && isset( $this->processed_terms[ intval( $_menu_item_object_id ) ] ) ) {
802 $_menu_item_object_id = $this->processed_terms[ intval( $_menu_item_object_id ) ];
803 } elseif ( 'post_type' === $_menu_item_type && isset( $this->processed_posts[ intval( $_menu_item_object_id ) ] ) ) {
804 $_menu_item_object_id = $this->processed_posts[ intval( $_menu_item_object_id ) ];
805 } elseif ( 'custom' !== $_menu_item_type ) {
806 // associated object is missing or not imported yet, we'll retry later
807 $this->missing_menu_items[] = $item;
808
809 return;
810 }
811
812 if ( isset( $this->processed_menu_items[ intval( $_menu_item_menu_item_parent ) ] ) ) {
813 $_menu_item_menu_item_parent = $this->processed_menu_items[ intval( $_menu_item_menu_item_parent ) ];
814 } elseif ( $_menu_item_menu_item_parent ) {
815 $this->menu_item_orphans[ intval( $item['post_id'] ) ] = (int) $_menu_item_menu_item_parent;
816 $_menu_item_menu_item_parent = 0;
817 }
818
819 // wp_update_nav_menu_item expects CSS classes as a space separated string
820 $_menu_item_classes = maybe_unserialize( $_menu_item_classes );
821 if ( is_array( $_menu_item_classes ) ) {
822 $_menu_item_classes = implode( ' ', $_menu_item_classes );
823 }
824
825 $args = array(
826 'menu-item-object-id' => $_menu_item_object_id,
827 'menu-item-object' => $_menu_item_object,
828 'menu-item-parent-id' => $_menu_item_menu_item_parent,
829 'menu-item-position' => intval( $item['menu_order'] ),
830 'menu-item-type' => $_menu_item_type,
831 'menu-item-title' => $item['post_title'],
832 'menu-item-url' => $_menu_item_url,
833 'menu-item-description' => $item['post_content'],
834 'menu-item-attr-title' => $item['post_excerpt'],
835 'menu-item-target' => $_menu_item_target,
836 'menu-item-classes' => $_menu_item_classes,
837 'menu-item-xfn' => $_menu_item_xfn,
838 'menu-item-status' => $item['status'],
839 );
840
841 $id = wp_update_nav_menu_item( $menu_id, 0, $args );
842 if ( $id && ! is_wp_error( $id ) ) {
843 $this->processed_menu_items[ intval( $item['post_id'] ) ] = (int) $id;
844 }
845 }
846
847 /**
848 * If fetching attachments is enabled then attempt to create a new attachment
849 *
850 * @param array $post Attachment post details from WXR.
851 * @param string $url URL to fetch attachment from.
852 *
853 * @return int|WP_Error Post ID on success, WP_Error otherwise
854 */
855 public function process_attachment( $post, $url ) {
856 if ( ! $this->fetch_attachments ) {
857 return;
858 }
859
860 // if the URL is absolute, but does not contain address, then upload it assuming base_site_url
861 if ( preg_match( '|^/[\w\W]+$|', $url ) ) {
862 $url = rtrim( $this->base_url, '/' ) . $url;
863 }
864
865 $upload = $this->fetch_remote_file( $url, $post );
866 if ( is_wp_error( $upload ) ) {
867 return $upload;
868 }
869
870 $info = wp_check_filetype( $upload['file'] );
871 if ( $info ) {
872 $post['post_mime_type'] = $info['type'];
873 } else {
874 return new WP_Error( 'attachment_processing_error', __( 'Invalid file type', 'ablocks' ) );
875 }
876
877 $post['guid'] = $upload['url'];
878
879 // as per wp-admin/includes/upload.php
880 $post_id = wp_insert_attachment( $post, $upload['file'] );
881 wp_update_attachment_metadata( $post_id, wp_generate_attachment_metadata( $post_id, $upload['file'] ) );
882
883 // remap resized image URLs, works by stripping the extension and remapping the URL stub.
884 if ( preg_match( '!^image/!', $info['type'] ) ) {
885 $parts = pathinfo( $url );
886 $name = basename( $parts['basename'], ".{$parts['extension']}" ); // PATHINFO_FILENAME in PHP 5.2
887
888 $parts_new = pathinfo( $upload['url'] );
889 $name_new = basename( $parts_new['basename'], ".{$parts_new['extension']}" );
890
891 $this->url_remap[ $parts['dirname'] . '/' . $name ] = $parts_new['dirname'] . '/' . $name_new;
892 }
893
894 return $post_id;
895 }
896
897 /**
898 * Attempt to download a remote file attachment
899 *
900 * @param string $url URL of item to fetch.
901 * @param array $post Attachment details.
902 *
903 * @return array|WP_Error Local file location details on success, WP_Error otherwise
904 */
905 public function fetch_remote_file( $url, $post ) {
906 // Extract the file name from the URL.
907 $path = wp_parse_url( $url, PHP_URL_PATH );
908 $file_name = '';
909 if ( is_string( $path ) ) {
910 $file_name = basename( $path );
911 }
912
913 if ( ! $file_name ) {
914 $file_name = md5( $url );
915 }
916
917 $tmp_file_name = wp_tempnam( $file_name );
918 if ( ! $tmp_file_name ) {
919 return new WP_Error( 'import_no_file', __( 'Could not create temporary file.', 'ablocks' ) );
920 }
921
922 // Fetch the remote URL and write it to the placeholder file.
923 $remote_response = wp_safe_remote_get(
924 $url,
925 array(
926 'timeout' => 300,
927 'stream' => true,
928 'filename' => $tmp_file_name,
929 'headers' => array(
930 'Accept-Encoding' => 'identity',
931 ),
932 )
933 );
934
935 if ( is_wp_error( $remote_response ) ) {
936 // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
937 @unlink( $tmp_file_name );
938
939 return new WP_Error(
940 'import_file_error',
941 sprintf(
942 /* translators: 1: The WordPress error message. 2: The WordPress error code. */
943 __( 'Request failed due to an error: %1$s (%2$s)', 'ablocks' ),
944 esc_html( $remote_response->get_error_message() ),
945 esc_html( $remote_response->get_error_code() )
946 )
947 );
948 }
949
950 $remote_response_code = (int) wp_remote_retrieve_response_code( $remote_response );
951
952 // Make sure the fetch was successful.
953 if ( 200 !== $remote_response_code ) {
954 if ( file_exists( $tmp_file_name ) ) {
955 unlink( $tmp_file_name );
956 }
957
958 return new WP_Error(
959 'import_file_error',
960 sprintf(
961 /* translators: 1: The HTTP error message. 2: The HTTP error code. */
962 __( 'Remote server returned the following unexpected result: %1$s (%2$s)', 'ablocks' ),
963 get_status_header_desc( $remote_response_code ),
964 esc_html( $remote_response_code )
965 )
966 );
967 }
968
969 $headers = wp_remote_retrieve_headers( $remote_response );
970
971 // Request failed.
972 if ( ! $headers ) {
973 if ( file_exists( $tmp_file_name ) ) {
974 unlink( $tmp_file_name );
975 }
976
977 return new WP_Error( 'import_file_error', __( 'Remote server did not respond', 'ablocks' ) );
978 }
979
980 $filesize = (int) filesize( $tmp_file_name );
981
982 if ( 0 === $filesize ) {
983 if ( file_exists( $tmp_file_name ) ) {
984 unlink( $tmp_file_name );
985 }
986
987 return new WP_Error( 'import_file_error', __( 'Zero size file downloaded', 'ablocks' ) );
988 }
989
990 if ( ! isset( $headers['content-encoding'] ) && isset( $headers['content-length'] ) && $filesize !== (int) $headers['content-length'] ) {
991 if ( file_exists( $tmp_file_name ) ) {
992 unlink( $tmp_file_name );
993 }
994
995 return new WP_Error( 'import_file_error', __( 'Downloaded file has incorrect size', 'ablocks' ) );
996 }
997
998 $max_size = (int) $this->max_attachment_size();
999 if ( ! empty( $max_size ) && $filesize > $max_size ) {
1000 if ( file_exists( $tmp_file_name ) ) {
1001 unlink( $tmp_file_name );
1002 }
1003
1004 // Translators: %s is the file size limit for the remote file import
1005 return new WP_Error( 'import_file_error', sprintf( __( 'Remote file is too large, limit is %s', 'ablocks' ), size_format( $max_size ) ) );
1006 }
1007
1008 // Override file name with Content-Disposition header value.
1009 if ( ! empty( $headers['content-disposition'] ) ) {
1010 $file_name_from_disposition = self::get_filename_from_disposition( (array) $headers['content-disposition'] );
1011 if ( $file_name_from_disposition ) {
1012 $file_name = $file_name_from_disposition;
1013 }
1014 }
1015
1016 // Set file extension if missing.
1017 $file_ext = pathinfo( $file_name, PATHINFO_EXTENSION );
1018 if ( ! $file_ext && ! empty( $headers['content-type'] ) ) {
1019 $extension = self::get_file_extension_by_mime_type( $headers['content-type'] );
1020 if ( $extension ) {
1021 $file_name = "{$file_name}.{$extension}";
1022 }
1023 }
1024
1025 // Handle the upload like _wp_handle_upload() does.
1026 $wp_filetype = wp_check_filetype_and_ext( $tmp_file_name, $file_name );
1027 $ext = empty( $wp_filetype['ext'] ) ? '' : $wp_filetype['ext'];
1028 $type = empty( $wp_filetype['type'] ) ? '' : $wp_filetype['type'];
1029 $proper_filename = empty( $wp_filetype['proper_filename'] ) ? '' : $wp_filetype['proper_filename'];
1030
1031 // Check to see if wp_check_filetype_and_ext() determined the filename was incorrect.
1032 if ( $proper_filename ) {
1033 $file_name = $proper_filename;
1034 }
1035
1036 if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
1037 return new WP_Error( 'import_file_error', __( 'Sorry, this file type is not permitted for security reasons.', 'ablocks' ) );
1038 }
1039
1040 $uploads = wp_upload_dir( $post['upload_date'] );
1041 if ( ! ( $uploads && false === $uploads['error'] ) ) {
1042 return new WP_Error( 'upload_dir_error', $uploads['error'] );
1043 }
1044
1045 // Move the file to the uploads dir.
1046 $file_name = wp_unique_filename( $uploads['path'], $file_name );
1047 $new_file = $uploads['path'] . "/$file_name";
1048 $move_new_file = copy( $tmp_file_name, $new_file );
1049
1050 if ( ! $move_new_file ) {
1051 if ( file_exists( $tmp_file_name ) ) {
1052 unlink( $tmp_file_name );
1053 }
1054
1055 return new WP_Error( 'import_file_error', __( 'The uploaded file could not be moved', 'ablocks' ) );
1056 }
1057
1058 // Set correct file permissions.
1059 $stat = stat( dirname( $new_file ) );
1060 $perms = $stat['mode'] & 0000666;
1061 chmod( $new_file, $perms );
1062
1063 $upload = array(
1064 'file' => $new_file,
1065 'url' => $uploads['url'] . "/$file_name",
1066 'type' => $wp_filetype['type'],
1067 'error' => false,
1068 );
1069
1070 // keep track of the old and new urls so we can substitute them later
1071 $this->url_remap[ $url ] = $upload['url'];
1072 $this->url_remap[ $post['guid'] ] = $upload['url']; // r13735, really needed?
1073 // keep track of the destination if the remote url is redirected somewhere else
1074 if ( isset( $headers['x-final-location'] ) && $headers['x-final-location'] !== $url ) {
1075 $this->url_remap[ $headers['x-final-location'] ] = $upload['url'];
1076 }
1077
1078 return $upload;
1079 }
1080
1081 /**
1082 * Attempt to associate posts and menu items with previously missing parents
1083 *
1084 * An imported post's parent may not have been imported when it was first created
1085 * so try again. Similarly, for child menu items and menu items which were missing
1086 * the object (e.g. post) they represent in the menu
1087 */
1088 public function backfill_parents() {
1089 global $wpdb;
1090
1091 // find parents for post orphans
1092 foreach ( $this->post_orphans as $child_id => $parent_id ) {
1093 $local_child_id = false;
1094 $local_parent_id = false;
1095 if ( isset( $this->processed_posts[ $child_id ] ) ) {
1096 $local_child_id = $this->processed_posts[ $child_id ];
1097 }
1098 if ( isset( $this->processed_posts[ $parent_id ] ) ) {
1099 $local_parent_id = $this->processed_posts[ $parent_id ];
1100 }
1101
1102 if ( $local_child_id && $local_parent_id ) {
1103 $wpdb->update( $wpdb->posts, array( 'post_parent' => $local_parent_id ), array( 'ID' => $local_child_id ), '%d', '%d' );
1104 clean_post_cache( $local_child_id );
1105 }
1106 }
1107
1108 // all other posts/terms are imported, retry menu items with missing associated object
1109 $missing_menu_items = $this->missing_menu_items;
1110 foreach ( $missing_menu_items as $item ) {
1111 $this->process_menu_item( $item );
1112 }
1113
1114 // find parents for menu item orphans
1115 foreach ( $this->menu_item_orphans as $child_id => $parent_id ) {
1116 $local_child_id = 0;
1117 $local_parent_id = 0;
1118 if ( isset( $this->processed_menu_items[ $child_id ] ) ) {
1119 $local_child_id = $this->processed_menu_items[ $child_id ];
1120 }
1121 if ( isset( $this->processed_menu_items[ $parent_id ] ) ) {
1122 $local_parent_id = $this->processed_menu_items[ $parent_id ];
1123 }
1124
1125 if ( $local_child_id && $local_parent_id ) {
1126 update_post_meta( $local_child_id, '_menu_item_menu_item_parent', (int) $local_parent_id );
1127 }
1128 }
1129 }
1130
1131 /**
1132 * Use stored mapping information to update old attachment URLs
1133 */
1134 public function backfill_attachment_urls() {
1135 global $wpdb;
1136 // make sure we do the longest urls first, in case one is a substring of another
1137 uksort( $this->url_remap, array( &$this, 'cmpr_strlen' ) );
1138
1139 foreach ( $this->url_remap as $from_url => $to_url ) {
1140 // remap urls in post_content
1141 $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s)", $from_url, $to_url ) );
1142 // remap enclosure urls
1143 $wpdb->query( $wpdb->prepare( "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_key='enclosure'", $from_url, $to_url ) );
1144 }
1145
1146 if ( ! empty( $this->url_remap ) ) {
1147 Helper::emit_sse_message( [
1148 'action' => 'log',
1149 'level' => 'info',
1150 'message' => __( 'Old attachment URLs have been updated.', 'ablocks' ),
1151 ] );
1152 }
1153 }
1154
1155 /**
1156 * Update _thumbnail_id meta to new, imported attachment IDs
1157 */
1158 public function remap_featured_images() {
1159 // cycle through posts that have a featured image
1160 foreach ( $this->featured_images as $post_id => $value ) {
1161 if ( isset( $this->processed_posts[ $value ] ) ) {
1162 $new_id = $this->processed_posts[ $value ];
1163 // only update if there's a difference
1164 if ( $new_id !== $value ) {
1165 update_post_meta( $post_id, '_thumbnail_id', $new_id );
1166 }
1167 }
1168 }
1169 Helper::emit_sse_message( [
1170 'action' => 'log',
1171 'level' => 'info',
1172 'message' => __( 'Imported posts `_thumbnail_id` updated.', 'ablocks' ),
1173 ] );
1174 }
1175
1176 /**
1177 * Parse a WXR file
1178 *
1179 * @param string $file Path to WXR file for parsing.
1180 *
1181 * @return array|WP_Error Information gathered from the WXR file.
1182 */
1183 public function parse( string $file ) {
1184 $parser = new WXR_Parser();
1185
1186 return $parser->parse( $file );
1187 }
1188
1189 /**
1190 * Decide if the given meta key maps to information we will want to import
1191 *
1192 * @param string $key The meta key to check.
1193 *
1194 * @return string|bool The key if we do want to import, false if not
1195 */
1196 public function is_valid_meta_key( $key ) {
1197 // skip attachment metadata since we'll regenerate it from scratch
1198 // skip _edit_lock as not relevant for import
1199 if ( in_array( $key, array( '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock' ), true ) ) {
1200 return false;
1201 }
1202
1203 return $key;
1204 }
1205
1206 /**
1207 * Decide what the maximum file size for downloaded attachments is.
1208 * Default is 0 (unlimited), can be filtered via import_attachment_size_limit
1209 *
1210 * @return int Maximum attachment file size to import
1211 */
1212 public function max_attachment_size(): int {
1213 return apply_filters( 'import_attachment_size_limit', 0 );
1214 }
1215
1216 /**
1217 * Added to http_request_timeout filter to force timeout at 60 seconds during import
1218 *
1219 * @param mixed $val Value.
1220 *
1221 * @return int 60
1222 */
1223 public function bump_request_timeout( $val ): int {
1224 return 60;
1225 }
1226
1227 public function cmpr_strlen( $a, $b ): int {
1228 // return the difference in length between two strings.
1229 return strlen( $b ) - strlen( $a );
1230 }
1231
1232 /**
1233 * Parses filename from a Content-Disposition header value.
1234 *
1235 * As per RFC6266:
1236 *
1237 * content-disposition = "Content-Disposition" ":"
1238 * disposition-type *( ";" disposition-parm )
1239 *
1240 * disposition-type = "inline" | "attachment" | disp-ext-type
1241 * ; case-insensitive
1242 * disp-ext-type = token
1243 *
1244 * disposition-parm = filename-parm | disp-ext-parm
1245 *
1246 * filename-parm = "filename" "=" value
1247 * | "filename*" "=" ext-value
1248 *
1249 * disp-ext-parm = token "=" value
1250 * | ext-token "=" ext-value
1251 * ext-token = <the characters in token, followed by "*">
1252 *
1253 * @param string[] $disposition_header List of Content-Disposition header values.
1254 *
1255 * @return string|null Filename if available, or null if not found.
1256 * @link http://tools.ietf.org/html/rfc2388
1257 * @link http://tools.ietf.org/html/rfc6266
1258 *
1259 * @since 0.7.0
1260 *
1261 * @see WP_REST_Attachments_Controller::get_filename_from_disposition()
1262 */
1263 protected static function get_filename_from_disposition( $disposition_header ) {
1264 // Get the filename.
1265 $filename = null;
1266
1267 foreach ( $disposition_header as $value ) {
1268 $value = trim( $value );
1269
1270 if ( strpos( $value, ';' ) === false ) {
1271 continue;
1272 }
1273
1274 list( $type, $attr_parts ) = explode( ';', $value, 2 );
1275
1276 $attr_parts = explode( ';', $attr_parts );
1277 $attributes = array();
1278
1279 foreach ( $attr_parts as $part ) {
1280 if ( strpos( $part, '=' ) === false ) {
1281 continue;
1282 }
1283
1284 list( $key, $value ) = explode( '=', $part, 2 );
1285
1286 $attributes[ trim( $key ) ] = trim( $value );
1287 }
1288
1289 if ( empty( $attributes['filename'] ) ) {
1290 continue;
1291 }
1292
1293 $filename = trim( $attributes['filename'] );
1294
1295 // Unquote quoted filename, but after trimming.
1296 if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, - 1, 1 ) === '"' ) {
1297 $filename = substr( $filename, 1, - 1 );
1298 }
1299 }//end foreach
1300
1301 return $filename;
1302 }
1303
1304 /**
1305 * Retrieves file extension by mime type.
1306 *
1307 * @param string $mime_type Mime type to search extension for.
1308 *
1309 * @return string|null File extension if available, or null if not found.
1310 * @since 0.7.0
1311 */
1312 protected static function get_file_extension_by_mime_type( $mime_type ) {
1313 static $map = null;
1314
1315 if ( is_array( $map ) ) {
1316 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
1317 }
1318
1319 $mime_types = wp_get_mime_types();
1320 $map = array_flip( $mime_types );
1321
1322 // Some types have multiple extensions, use only the first one.
1323 foreach ( $map as $type => $extensions ) {
1324 $map[ $type ] = strtok( $extensions, '|' );
1325 }
1326
1327 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
1328 }
1329 }
1330