PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / full-site-import / WPImport.php

WPImport.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.8.0, at modules/full-site-import/WPImport.php

2,016 lines 66.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Templately\Modules\FullSiteImport;
4
5 use Templately\Modules\FullSiteImport\Parsers\WXR_Parser;
6 use Templately\Modules\FullSiteImport\Runners\Loop;
7 use Templately\Modules\FullSiteImport\Utils\URL;
8 use Templately\Modules\FullSiteImport\Utils\Utils;
9 use Templately\Utils\Helper;
10 use WP_Error;
11 use WP_Importer;
12 use function wp_import_cleanup;
13 use Templately\Modules\FullSiteImport\Utils\AttachmentPrefetcher;
14
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit; // Exit if accessed directly.
17 }
18
19 /**
20 * Originally made by WordPress part of WordPress/Importer.
21 * https://plugins.trac.wordpress.org/browser/wordpress-importer/trunk/class-wp-import.php
22 *
23 * What was done ( by Elementor):
24 * Reformat of the code.
25 * Changed text domain.
26 * Changed methods visibility.
27 * Changed method from `get_authors_from_import` to `set_authors_from_import`.
28 * Changed method from `get_author_mapping` to `set_author_mapping`.
29 * Removed use of '$_POST' the input 'options' will be passed via constructor args.
30 * Removed echos, UI and print methods, all echos replaced with `$this->output` append.
31 * Removed `die` ( exit(s) ).
32 *
33 * What was done ( by Templately):
34 * Add Action For Every Part Of Import for SSE
35 */
36
37 if ( ! class_exists( 'WP_Importer' ) ) {
38 $class_wp_importer = ABSPATH . 'wp-admin/includes/class-wp-importer.php';
39
40 if ( file_exists( $class_wp_importer ) ) {
41 require $class_wp_importer;
42 }
43 }
44
45 if ( ! function_exists( 'wp_import_cleanup' ) ) {
46 $wp_import = ABSPATH . 'wp-admin/includes/import.php';
47
48 if ( file_exists( $wp_import ) ) {
49 require $wp_import;
50 }
51 }
52
53 class WPImport extends WP_Importer {
54 use Concerns\LogHelper;
55 use Loop;
56
57 const DEFAULT_BUMP_REQUEST_TIMEOUT = 60;
58 const DEFAULT_ALLOW_CREATE_USERS = true;
59 const DEFAULT_IMPORT_ATTACHMENT_SIZE_LIMIT = 0; // 0 = unlimited.
60
61 /**
62 * @var string
63 */
64 private $requested_file_path;
65 /**
66 * @var string
67 */
68 private $import_data_key;
69
70 /**
71 * @var array
72 */
73 private $args;
74
75 /**
76 * @var FullSiteImport
77 */
78 private $origin;
79
80 /**
81 * @var array
82 */
83 private $output = [
84 'status' => 'failed',
85 'errors' => [],
86 ];
87
88 /*
89 * WXR attachment ID
90 */
91 private $id;
92
93 /**
94 * @var mixed
95 */
96 private $json;
97
98 /**
99 * @var string
100 */
101 private $session_id;
102
103 // Information to import from WXR file.
104 private $version;
105 private $authors = [];
106 public $posts = [];
107 public $terms = [];
108 private $base_url = '';
109 private $page_on_front;
110 private $base_blog_url = '';
111
112 // Mappings from old information to new.
113 public $processed_taxonomies;
114 public $processed_terms = [];
115 public $processed_posts = [];
116 public $url_remap = [];
117 private $processed_authors = [];
118 private $author_mapping = [];
119 private $processed_menu_items = [];
120 private $post_orphans = [];
121 private $menu_item_orphans = [];
122 private $mapped_terms_slug = [];
123
124 private $fetch_attachments = false;
125 private $attachment_timeout = 300;
126 private $attachment_retry_count = 3;
127 private $featured_images = [];
128
129 /**
130 * @var array[] [meta_key => meta_value] Meta value that should be set for every imported post.
131 */
132 private $posts_meta = [];
133
134 /**
135 * @var array[] [meta_key => meta_value] Meta value that should be set for every imported term.
136 */
137 private $terms_meta = [];
138
139 public static $_replace_image_ids = [];
140
141
142 public $backup_attributes = [
143 'output',
144 'url_remap',
145 '_replace_image_ids',
146 'menu_item_orphans',
147 'processed_menu_items',
148 'post_orphans',
149 'processed_posts',
150 'featured_images',
151 'mapped_terms_slug',
152 'processed_terms',
153 'processed_authors',
154 'author_mapping',
155 ];
156
157
158 /**
159 * Parses filename from a Content-Disposition header value.
160 *
161 * As per RFC6266:
162 *
163 * content-disposition = "Content-Disposition" ":"
164 * disposition-type *( ";" disposition-parm )
165 *
166 * disposition-type = "inline" | "attachment" | disp-ext-type
167 * ; case-insensitive
168 * disp-ext-type = token
169 *
170 * disposition-parm = filename-parm | disp-ext-parm
171 *
172 * filename-parm = "filename" "=" value
173 * | "filename*" "=" ext-value
174 *
175 * disp-ext-parm = token "=" value
176 * | ext-token "=" ext-value
177 * ext-token = <the characters in token, followed by "*">
178 *
179 * @param string[] $disposition_header List of Content-Disposition header values.
180 *
181 * @return string|null Filename if available, or null if not found.
182 * @link http://tools.ietf.org/html/rfc2388
183 * @link http://tools.ietf.org/html/rfc6266
184 *
185 * @see WP_REST_Attachments_Controller::get_filename_from_disposition()
186 *
187 */
188 protected static function get_filename_from_disposition( $disposition_header ) {
189 // Get the filename.
190 $filename = null;
191
192 foreach ( $disposition_header as $value ) {
193 $value = trim( $value );
194
195 if ( strpos( $value, ';' ) === false ) {
196 continue;
197 }
198
199 list( $type, $attr_parts ) = explode( ';', $value, 2 );
200
201 $attr_parts = explode( ';', $attr_parts );
202 $attributes = [];
203
204 foreach ( $attr_parts as $part ) {
205 if ( strpos( $part, '=' ) === false ) {
206 continue;
207 }
208
209 list( $key, $value ) = explode( '=', $part, 2 );
210
211 $attributes[ trim( $key ) ] = trim( $value );
212 }
213
214 if ( empty( $attributes['filename'] ) ) {
215 continue;
216 }
217
218 $filename = trim( $attributes['filename'] );
219
220 // Unquote quoted filename, but after trimming.
221 if ( substr( $filename, 0, 1 ) === '"' && substr( $filename, -1, 1 ) === '"' ) {
222 $filename = substr( $filename, 1, -1 );
223 }
224 }
225
226 return $filename;
227 }
228
229 /**
230 * Retrieves file extension by mime type.
231 *
232 * @param string $mime_type Mime type to search extension for.
233 *
234 * @return string|null File extension if available, or null if not found.
235 */
236 protected static function get_file_extension_by_mime_type( $mime_type ) {
237 static $map = null;
238
239 if ( is_array( $map ) ) {
240 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
241 }
242
243 $mime_types = wp_get_mime_types();
244 $map = array_flip( $mime_types );
245
246 // Some types have multiple extensions, use only the first one.
247 foreach ( $map as $type => $extensions ) {
248 $map[ $type ] = strtok( $extensions, '|' );
249 }
250
251 return isset( $map[ $mime_type ] ) ? $map[ $mime_type ] : null;
252 }
253
254 /**
255 * The main controller for the actual import stage.
256 *
257 * @param string $file Path to the WXR file for importing
258 */
259 private function import( $file ) {
260 add_filter( 'import_post_meta_key', function ( $key ) {
261 return $this->is_valid_meta_key( $key );
262 } );
263 add_filter( 'http_request_timeout', function () {
264 return self::DEFAULT_BUMP_REQUEST_TIMEOUT;
265 } );
266
267 if ( ! $this->import_start( $file ) ) {
268 return;
269 }
270
271
272 $this->set_author_mapping();
273
274 wp_suspend_cache_invalidation( true );
275 $imported_summary = [
276 'terms' => $this->process_terms(),
277 'posts' => $this->process_posts(),
278 ];
279 wp_suspend_cache_invalidation( false );
280
281 // Update incorrect/missing information in the DB.
282 $this->backfill_parents();
283 $this->backfill_attachment_urls();
284 $this->remap_featured_images();
285
286 $this->import_end();
287
288 $is_some_succeed = false;
289 foreach ( $imported_summary as $item ) {
290 if ( $item > 0 ) {
291 $is_some_succeed = true;
292 break;
293 }
294 }
295
296 if ( $is_some_succeed ) {
297 $this->output['status'] = 'success';
298 $this->output['summary'] = $imported_summary;
299 }
300 }
301
302 /**
303 * Parses the WXR file and prepares us for the task of processing parsed data.
304 *
305 * @param string $file Path to the WXR file for importing
306 */
307 private function import_start( string $file ): bool {
308 if ( ! is_file( $file ) ) {
309 $this->output['errors'] = [ esc_html__( 'The file does not exist, please try again.', 'elementor' ) ];
310
311 return false;
312 }
313
314 $import_data = $this->parse( $file );
315
316 if ( is_wp_error( $import_data ) ) {
317 /**
318 * @var WP_Error $import_data ;
319 */
320 $this->output['errors'] = [ $import_data->get_error_message() ];
321
322 return false;
323 }
324 $this->version = $import_data['version'];
325 $this->set_authors_from_import( $import_data );
326 $this->posts = $import_data['posts'];
327 $this->terms = $import_data['terms'];
328 $this->base_url = esc_url( $import_data['base_url'] );
329 $this->base_blog_url = esc_url( $import_data['base_blog_url'] );
330 $this->page_on_front = $import_data['page_on_front'];
331
332 wp_defer_term_counting( true );
333 wp_defer_comment_counting( true );
334
335 do_action( 'import_start' );
336 do_action( 'templately_import_start', $this );
337
338 return true;
339 }
340
341 /**
342 * Performs post-import cleanup of files and the cache
343 */
344 private function import_end() {
345 wp_import_cleanup( $this->id );
346
347 wp_cache_flush();
348
349 foreach ( get_taxonomies() as $tax ) {
350 delete_option( "{$tax}_children" );
351 _get_term_hierarchy( $tax );
352 }
353
354 wp_defer_term_counting( false );
355 wp_defer_comment_counting( false );
356
357 do_action( 'import_end' );
358 }
359
360 /**
361 * Retrieve authors from parsed WXR data and set it to `$this->>authors`.
362 *
363 * Uses the provided author information from WXR 1.1 files
364 * or extracts info from each post for WXR 1.0 files
365 *
366 * @param array $import_data Data returned by a WXR parser
367 */
368 private function set_authors_from_import( $import_data ) {
369 if ( ! empty( $import_data['authors'] ) ) {
370 $this->authors = $import_data['authors'];
371 // No author information, grab it from the posts.
372 } else {
373 foreach ( $import_data['posts'] as $post ) {
374 $login = sanitize_user( $post['post_author'], true );
375
376 if ( empty( $login ) ) {
377 /* translators: %s: Post author. */
378 $this->output['errors'][] = sprintf( esc_html__( 'Failed to import author %s. Their posts will be attributed to the current user.', 'elementor' ), $post['post_author'] );
379 continue;
380 }
381
382 if ( ! isset( $this->authors[ $login ] ) ) {
383 $this->authors[ $login ] = [
384 'author_login' => $login,
385 'author_display_name' => $post['post_author'],
386 ];
387 }
388 }
389 }
390 }
391
392 /**
393 * Map old author logins to local user IDs based on decisions made
394 * in import options form. Can map to an existing user, create a new user
395 * or falls back to the current user in case of error with either of the previous
396 */
397 private function set_author_mapping() {
398 if ( ! isset( $this->args['imported_authors'] ) ) {
399 return;
400 }
401
402
403 $processed_templates = $this->get_loop_result([], $this->import_data_key);
404 if (!empty($processed_templates)) {
405 return;
406 }
407
408 $create_users = apply_filters( 'import_allow_create_users', self::DEFAULT_ALLOW_CREATE_USERS );
409
410 foreach ( (array) $this->args['imported_authors'] as $i => $old_login ) {
411 // Multisite adds strtolower to sanitize_user. Need to sanitize here to stop breakage in process_posts.
412 $santized_old_login = sanitize_user( $old_login, true );
413 $old_id = isset( $this->authors[ $old_login ]['author_id'] ) ? (int) $this->authors[ $old_login ]['author_id'] : false;
414
415 if ( ! empty( $this->args['user_map'][ $i ] ) ) {
416 $user = get_userdata( (int) $this->args['user_map'][ $i ] );
417 if ( isset( $user->ID ) ) {
418 if ( $old_id ) {
419 $this->processed_authors[ $old_id ] = $user->ID;
420 }
421 $this->author_mapping[ $santized_old_login ] = $user->ID;
422 }
423 } elseif ( $create_users ) {
424 $user_id = 0;
425 if ( ! empty( $this->args['user_new'][ $i ] ) ) {
426 $user_id = wp_create_user( $this->args['user_new'][ $i ], wp_generate_password() );
427 } elseif ( '1.0' !== $this->version ) {
428 $user_data = [
429 'user_login' => $old_login,
430 'user_pass' => wp_generate_password(),
431 'user_email' => isset( $this->authors[ $old_login ]['author_email'] ) ? $this->authors[ $old_login ]['author_email'] : '',
432 'display_name' => $this->authors[ $old_login ]['author_display_name'],
433 'first_name' => isset( $this->authors[ $old_login ]['author_first_name'] ) ? $this->authors[ $old_login ]['author_first_name'] : '',
434 'last_name' => isset( $this->authors[ $old_login ]['author_last_name'] ) ? $this->authors[ $old_login ]['author_last_name'] : '',
435 ];
436 $user_id = wp_insert_user( $user_data );
437 }
438
439 if ( ! is_wp_error( $user_id ) ) {
440 if ( $old_id ) {
441 $this->processed_authors[ $old_id ] = $user_id;
442 }
443 $this->author_mapping[ $santized_old_login ] = $user_id;
444 } else {
445 /* translators: %s: Author display name. */
446 $error = sprintf( esc_html__( 'Failed to create new user for %s. Their posts will be attributed to the current user.', 'elementor' ), $this->authors[ $old_login ]['author_display_name'] );
447
448 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
449 $error .= PHP_EOL . $user_id->get_error_message();
450 }
451
452 $this->output['errors'][] = $error;
453 }
454 }
455
456 // Failsafe: if the user_id was invalid, default to the current user.
457 if ( ! isset( $this->author_mapping[ $santized_old_login ] ) ) {
458 if ( $old_id ) {
459 $this->processed_authors[ $old_id ] = (int) get_current_user_id();
460 }
461 $this->author_mapping[ $santized_old_login ] = (int) get_current_user_id();
462 }
463 }
464
465 $this->set_loop_result( true, $this->import_data_key );
466 }
467
468 /**
469 * Create new terms based on import information
470 *
471 * Doesn't create a term its slug already exists
472 *
473 * @return array|array[] the ids of succeed/failed imported terms.
474 */
475 private function process_terms(): array {
476 $result = [
477 'succeed' => [],
478 'failed' => [],
479 ];
480
481 $processed_templates = $this->get_loop_result([], "wp_import_terms_" . $this->import_data_key);
482 if (!empty($processed_templates)) {
483 $result = $this->get_loop_result([], "wp_import_terms_" . $this->import_data_key);
484 return $result;
485 }
486
487 $this->terms = apply_filters( 'wp_import_terms', $this->terms );
488 if ( empty( $this->terms ) ) {
489 return $result;
490 }
491 foreach ( $this->terms as $term ) {
492
493 // if the term already exists in the correct taxonomy leave it alone
494 $term_id = term_exists( $term['slug'], $term['term_taxonomy'] );
495 if ( $term_id ) {
496 if ( is_array( $term_id ) ) {
497 $term_id = $term_id['term_id'];
498 }
499
500 if ( isset( $term['term_id'] ) ) {
501 if ( 'nav_menu' === $term['term_taxonomy'] ) {
502 // BC - support old kits that the menu terms are part of the 'nav_menu_item' post type
503 // and not part of the taxonomies.
504 if ( ! empty( $this->processed_taxonomies[ $term['term_taxonomy'] ] ) ) {
505 foreach ( $this->processed_taxonomies[ $term['term_taxonomy'] ] as $processed_term ) {
506 $old_slug = $processed_term['old_slug'];
507 $new_slug = $processed_term['new_slug'];
508
509 $this->mapped_terms_slug[ $old_slug ] = $new_slug;
510 $result['succeed'][ $old_slug ] = $new_slug;
511 }
512 continue;
513 } else {
514 $term = $this->handle_duplicated_nav_menu_term( $term );
515 }
516 } else {
517 $this->processed_terms[ (int) $term['term_id'] ] = (int) $term_id;
518 $result['succeed'][ (int) $term['term_id'] ] = (int) $term_id;
519 continue;
520 }
521 }
522 }
523
524 if ( empty( $term['term_parent'] ) ) {
525 $parent = 0;
526 } else {
527 $parent = term_exists( $term['term_parent'], $term['term_taxonomy'] );
528 if ( is_array( $parent ) ) {
529 $parent = $parent['term_id'];
530 }
531 }
532
533 $description = $term['term_description'] ?? '';
534 $args = [
535 'slug' => $term['slug'],
536 'description' => wp_slash( $description ),
537 'parent' => (int) $parent,
538 ];
539
540 $id = wp_insert_term( wp_slash( $term['term_name'] ), $term['term_taxonomy'], $args );
541 if ( ! is_wp_error( $id ) ) {
542 if ( isset( $term['term_id'] ) ) {
543 $this->processed_terms[ (int) $term['term_id'] ] = $id['term_id'];
544 $result['succeed'][ (int) $term['term_id'] ] = $id['term_id'];
545
546 $this->update_term_meta( $id['term_id'] );
547 }
548 } else {
549 /* translators: 1: Term taxonomy, 2: Term name. */
550 $error = sprintf( esc_html__( 'Failed to import %1$s %2$s', 'elementor' ), $term['term_taxonomy'], $term['term_name'] );
551
552 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
553 $error .= PHP_EOL . $id->get_error_message();
554 }
555
556 $result['failed'][] = $id;
557 $this->output['errors'][] = $error;
558 continue;
559 }
560
561 $this->process_termmeta( $term, $id['term_id'] );
562
563 do_action( 'templately_import.process_term', $term, $result, $this );
564 }
565
566 unset( $this->terms );
567
568 // Add the template to the processed templates and update the session data
569 $this->set_loop_result( $result, "wp_import_terms_" . $this->import_data_key);
570 return $result;
571 }
572
573 /**
574 * Add metadata to imported term.
575 *
576 * @param array $term Term data from WXR import.
577 * @param int $term_id ID of the newly created term.
578 */
579 private function process_termmeta( $term, $term_id ) {
580 if ( ! function_exists( 'add_term_meta' ) ) {
581 return;
582 }
583
584 if ( ! isset( $term['termmeta'] ) ) {
585 $term['termmeta'] = [];
586 }
587
588 /**
589 * Filters the metadata attached to an imported term.
590 *
591 * @param array $termmeta Array of term meta.
592 * @param int $term_id ID of the newly created term.
593 * @param array $term Term data from the WXR import.
594 */
595 $term['termmeta'] = apply_filters( 'wp_import_term_meta', $term['termmeta'], $term_id, $term );
596
597 if ( empty( $term['termmeta'] ) ) {
598 return;
599 }
600
601 foreach ( $term['termmeta'] as $meta ) {
602 /**
603 * Filters the meta key for an imported piece of term meta.
604 *
605 * @param string $meta_key Meta key.
606 * @param int $term_id ID of the newly created term.
607 * @param array $term Term data from the WXR import.
608 */
609 $key = apply_filters( 'import_term_meta_key', $meta['key'], $term_id, $term );
610 if ( ! $key ) {
611 continue;
612 }
613
614 // Export gets meta straight from the DB so could have a serialized string
615 $value = maybe_unserialize( $meta['value'] );
616
617 add_term_meta( $term_id, wp_slash( $key ), wp_slash_strings_only( $value ) );
618
619 /**
620 * Fires after term meta is imported.
621 *
622 * @param int $term_id ID of the newly created term.
623 * @param string $key Meta key.
624 * @param mixed $value Meta value.
625 */
626 do_action( 'import_term_meta', $term_id, $key, $value );
627 }
628 }
629
630 /**
631 * Create new posts based on import information
632 *
633 * Posts marked as having a parent which doesn't exist will become top level items.
634 * Doesn't create a new post if: the post type doesn't exist, the given post ID
635 * is already noted as imported or a post with the same title and date already exists.
636 * Note that new/updated terms, comments and meta are imported for the last of the above.
637 *
638 * @return array the ids of succeed/failed imported posts.
639 */
640 /**
641 * The attachment URLs this file is about to download, in import order.
642 *
643 * Only what would genuinely reach `fetch_remote_file()`: an attachment already on the
644 * site is matched by hash and never downloaded, and one shipped inside the pack is copied
645 * from disk. Queueing either would spend a parallel slot on a file nobody will ask for.
646 *
647 * Best-effort by design. A URL that a later filter rewrites (AI image replacement swaps
648 * `attachment_url` at import time) simply misses the cache and is fetched the old way.
649 *
650 * @return string[]
651 */
652 private function upcoming_remote_attachments() {
653 if ( ! $this->fetch_attachments || ! AttachmentPrefetcher::is_available() ) {
654 return [];
655 }
656
657 $urls = [];
658 foreach ( (array) $this->posts as $post ) {
659 if ( ! is_array( $post ) || 'attachment' !== ( $post['post_type'] ?? '' ) ) {
660 continue;
661 }
662 if ( ! empty( $post['post_id'] ) && isset( $this->processed_posts[ $post['post_id'] ] ) ) {
663 continue;
664 }
665
666 $url = ! empty( $post['attachment_url'] ) ? $post['attachment_url'] : ( $post['guid'] ?? '' );
667 if ( ! is_string( $url ) || '' === $url ) {
668 continue;
669 }
670 if ( preg_match( '|^/[\w\W]+$|', $url ) ) {
671 $url = rtrim( $this->base_url, '/' ) . $url;
672 }
673
674 $file_name = sanitize_file_name( basename( (string) wp_parse_url( $url, PHP_URL_PATH ) ) );
675 if ( apply_filters( 'templately_import_has_local_attachment', false, $post['post_id'] ?? null, $file_name ) ) {
676 continue;
677 }
678 if ( $this->get_saved_image( $url ) ) {
679 continue;
680 }
681
682 $urls[] = $url;
683 }
684
685 return $urls;
686 }
687
688 /**
689 * Announce attachment URLs that are about to be passed to process_attachment().
690 *
691 * For callers that import images found INSIDE content (block markup) rather than
692 * attachment posts in the XML. Purely an optimisation hint: it never imports anything,
693 * and skipping the call changes nothing but speed.
694 *
695 * @param string[] $urls In the order they will be imported.
696 * @return void
697 */
698 public function prefetch_attachments( array $urls ) {
699 if ( ! $this->fetch_attachments || ! AttachmentPrefetcher::is_available() ) {
700 return;
701 }
702
703 $upcoming = [];
704 foreach ( $urls as $url ) {
705 if ( ! is_string( $url ) || '' === $url ) {
706 continue;
707 }
708 if ( preg_match( '|^/[\w\W]+$|', $url ) ) {
709 $url = rtrim( $this->base_url, '/' ) . $url;
710 }
711 // Already on the site: matched by hash, never downloaded.
712 if ( false !== strpos( $url, 'wp-includes/images' ) || $this->get_saved_image( $url ) ) {
713 continue;
714 }
715 $upcoming[] = $url;
716 }
717
718 AttachmentPrefetcher::enqueue( $upcoming );
719 }
720
721 private function process_posts(): array {
722 $backup_key = "wp_import_post_" . $this->import_data_key;
723
724 $this->posts = apply_filters( 'wp_import_posts', $this->posts );
725
726 AttachmentPrefetcher::enqueue( $this->upcoming_remote_attachments() );
727
728 $results = $this->loop( $this->posts, function($key, $post, $result ) {
729
730 $result = !empty($result) ? $result : [
731 'succeed' => [],
732 'failed' => [],
733 ];
734
735 $original_post_id = $post['post_id'];
736 $post = apply_filters( 'wp_import_post_data_raw', $post, $this );
737
738 if(empty($post)){
739 return $result;
740 }
741
742 if(!is_array($post) && is_numeric($post)){
743 $result['succeed'][ $original_post_id ] = $post;
744 return $result;
745 }
746
747 if ( ! post_type_exists( $post['post_type'] ) ) {
748 /* translators: 1: Post title, 2: Post type. */
749 $this->output['errors'][] = sprintf( esc_html__( 'Failed to import %1$s: Invalid post type %2$s', 'elementor' ), $post['post_title'], $post['post_type'] );
750 do_action( 'wp_import_post_exists', $post );
751 return $result;
752 }
753
754 if ( isset( $this->processed_posts[ $post['post_id'] ] ) && ! empty( $post['post_id'] ) ) {
755 return $result;
756 }
757
758 if ( 'auto-draft' === $post['status'] ) {
759 return $result;
760 }
761
762 if(!empty($post['post_content']) && !empty($post['post_id'])){
763 $post['post_content'] = Utils::import_and_replace_attachments($post['post_content'], $post['post_id']);
764 }
765
766 if ( 'nav_menu_item' === $post['post_type'] ) {
767 $result['succeed'] += $this->process_menu_item( $post );
768 return $result;
769 }
770
771 if ( 'wp_navigation' === $post['post_type'] ) {
772 $processed = $this->process_navigation( $post );
773 if ( ! $processed ) {
774 return $result;
775 }
776 }
777
778 $post_type_object = get_post_type_object( $post['post_type'] );
779
780 $post_parent = (int) $post['post_parent'];
781 if ( $post_parent ) {
782 // if we already know the parent, map it to the new local ID.
783 if ( isset( $this->processed_posts[ $post_parent ] ) ) {
784 $post_parent = $this->processed_posts[ $post_parent ];
785 // otherwise record the parent for later.
786 } else {
787 $this->post_orphans[ (int) $post['post_id'] ] = $post_parent;
788 $post_parent = 0;
789 }
790 }
791
792 // Map the post author.
793 $author = sanitize_user( $post['post_author'], true );
794 if ( isset( $this->author_mapping[ $author ] ) ) {
795 $author = $this->author_mapping[ $author ];
796 } else {
797 $author = (int) get_current_user_id();
798 }
799
800 $postdata = [
801 'post_author' => $author,
802 'post_content' => $post['post_content'],
803 'post_excerpt' => $post['post_excerpt'],
804 'post_title' => $post['post_title'],
805 'post_status' => $post['status'],
806 'post_name' => $post['post_name'],
807 'comment_status' => $post['comment_status'],
808 'ping_status' => $post['ping_status'],
809 'guid' => $post['guid'],
810 'post_parent' => $post_parent,
811 'menu_order' => $post['menu_order'],
812 'post_type' => $post['post_type'],
813 'post_password' => $post['post_password'],
814 ];
815
816 if(isset($post['original_attachment_url'])){
817 $postdata['original_attachment_url'] = $post['original_attachment_url'];
818 }
819
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 $attachment_sizes = [];
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 else if ( '_wp_attachment_metadata' === $meta['key'] ) {
839 $attachment_metadata = maybe_unserialize( $meta['value'] );
840 $attachment_sizes = $attachment_metadata['sizes'] ?? [];
841 // break;
842 }
843 }
844 }
845
846 $post_id = $this->process_attachment( $postdata, $remote_url, $attachment_sizes, $original_post_id );
847 $comment_post_id = $post_id;
848 } else {
849 $post_id = wp_insert_post( $postdata, true );
850
851 $this->update_post_meta( $post_id );
852
853 $comment_post_id = $post_id;
854 do_action( 'wp_import_insert_post', $post_id, $original_post_id, $postdata, $post );
855 }
856
857 if ( is_wp_error( $post_id ) ) {
858 /* translators: 1: Post type singular label, 2: Post title. */
859 $error = sprintf( __( 'Failed to import %1$s %2$s', 'elementor' ), $post_type_object->labels->singular_name, $post['post_title'] );
860
861 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
862 $error .= PHP_EOL . $post_id->get_error_message();
863 }
864
865 $result['failed'][] = $original_post_id;
866
867 $this->output['errors'][] = $error;
868
869 if ( 'attachment' === $postdata['post_type'] ) {
870 do_action( 'templately_import.process_post', $post, $result, $this );
871 }
872
873 return $result;
874 }
875
876 $result['succeed'][ $original_post_id ] = $post_id;
877
878 if ( 1 === $post['is_sticky'] ) {
879 stick_post( $post_id );
880 }
881
882 if ( $this->page_on_front === $original_post_id ) {
883 Utils::update_option( 'page_on_front', $post_id );
884 }
885
886 // Map pre-import ID to local ID.
887 $this->processed_posts[ (int) $post['post_id'] ] = (int) $post_id;
888
889 if ( ! isset( $post['terms'] ) ) {
890 $post['terms'] = [];
891 }
892
893 $post['terms'] = apply_filters( 'wp_import_post_terms', $post['terms'], $post_id, $post );
894
895 // add categories, tags and other terms
896 if ( ! empty( $post['terms'] ) ) {
897 $terms_to_set = [];
898 foreach ( $post['terms'] as $term ) {
899 // back compat with WXR 1.0 map 'tag' to 'post_tag'
900 $taxonomy = ( 'tag' === $term['domain'] ) ? 'post_tag' : $term['domain'];
901 $term_exists = term_exists( $term['slug'], $taxonomy );
902 $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists;
903 if ( ! $term_id ) {
904 $t = wp_insert_term( $term['name'], $taxonomy, [ 'slug' => $term['slug'] ] );
905 if ( ! is_wp_error( $t ) ) {
906 $term_id = $t['term_id'];
907
908 $this->update_term_meta( $term_id );
909
910 do_action( 'wp_import_insert_term', $t, $term, $post_id, $post );
911 } else {
912 /* translators: 1: Taxonomy name, 2: Term name. */
913 $error = sprintf( esc_html__( 'Failed to import %1$s %2$s', 'elementor' ), $taxonomy, $term['name'] );
914
915 if ( defined( 'IMPORT_DEBUG' ) && IMPORT_DEBUG ) {
916 $error .= PHP_EOL . $t->get_error_message();
917 }
918
919 $this->output['errors'][] = $error;
920
921 do_action( 'wp_import_insert_term_failed', $t, $term, $post_id, $post );
922 continue;
923 }
924 }
925 $terms_to_set[ $taxonomy ][] = (int) $term_id;
926 }
927
928 foreach ( $terms_to_set as $tax => $ids ) {
929 $tt_ids = wp_set_post_terms( $post_id, $ids, $tax );
930 do_action( 'wp_import_set_post_terms', $tt_ids, $ids, $tax, $post_id, $post );
931 }
932 unset( $post['terms'], $terms_to_set );
933 }
934
935 if ( ! isset( $post['comments'] ) ) {
936 $post['comments'] = [];
937 }
938
939 $post['comments'] = apply_filters( 'wp_import_post_comments', $post['comments'], $post_id, $post );
940
941 // Add/update comments.
942 if ( ! empty( $post['comments'] ) ) {
943 $num_comments = 0;
944 $inserted_comments = [];
945 foreach ( $post['comments'] as $comment ) {
946 $comment_id = $comment['comment_id'];
947 $newcomments[ $comment_id ]['comment_post_ID'] = $comment_post_id;
948 $newcomments[ $comment_id ]['comment_author'] = $comment['comment_author'];
949 $newcomments[ $comment_id ]['comment_author_email'] = $comment['comment_author_email'];
950 $newcomments[ $comment_id ]['comment_author_IP'] = $comment['comment_author_IP'];
951 $newcomments[ $comment_id ]['comment_author_url'] = $comment['comment_author_url'];
952 $newcomments[ $comment_id ]['comment_date'] = $comment['comment_date'];
953 $newcomments[ $comment_id ]['comment_date_gmt'] = $comment['comment_date_gmt'];
954 $newcomments[ $comment_id ]['comment_content'] = $comment['comment_content'];
955 $newcomments[ $comment_id ]['comment_approved'] = $comment['comment_approved'];
956 $newcomments[ $comment_id ]['comment_type'] = $comment['comment_type'];
957 $newcomments[ $comment_id ]['comment_parent'] = $comment['comment_parent'];
958 $newcomments[ $comment_id ]['commentmeta'] = isset( $comment['commentmeta'] ) ? $comment['commentmeta'] : [];
959 if ( isset( $this->processed_authors[ $comment['comment_user_id'] ] ) ) {
960 $newcomments[ $comment_id ]['user_id'] = $this->processed_authors[ $comment['comment_user_id'] ];
961 }
962 }
963
964 ksort( $newcomments );
965
966 foreach ( $newcomments as $key => $comment ) {
967 if ( isset( $inserted_comments[ $comment['comment_parent'] ] ) ) {
968 $comment['comment_parent'] = $inserted_comments[ $comment['comment_parent'] ];
969 }
970
971 $comment_data = wp_slash( $comment );
972 unset( $comment_data['commentmeta'] ); // Handled separately, wp_insert_comment() also expects `comment_meta`.
973 $comment_data = wp_filter_comment( $comment_data );
974
975 $inserted_comments[ $key ] = wp_insert_comment( $comment_data );
976
977 do_action( 'wp_import_insert_comment', $inserted_comments[ $key ], $comment, $comment_post_id, $post );
978
979 foreach ( $comment['commentmeta'] as $meta ) {
980 $value = maybe_unserialize( $meta['value'] );
981
982 add_comment_meta( $inserted_comments[ $key ], wp_slash( $meta['key'] ), wp_slash_strings_only( $value ) );
983 }
984
985 $num_comments++;
986 }
987 unset( $newcomments, $inserted_comments, $post['comments'] );
988 }
989
990 if ( ! isset( $post['postmeta'] ) ) {
991 $post['postmeta'] = [];
992 }
993
994 $post['postmeta'] = apply_filters( 'wp_import_post_meta', $post['postmeta'], $post_id, $post );
995
996 // Add/update post meta.
997 if ( ! empty( $post['postmeta'] ) ) {
998 foreach ( $post['postmeta'] as $meta ) {
999 $key = apply_filters( 'import_post_meta_key', $meta['key'], $post_id, $post );
1000 $value = false;
1001
1002 if ( '_edit_last' === $key ) {
1003 if ( isset( $this->processed_authors[ (int) $meta['value'] ] ) ) {
1004 $value = $this->processed_authors[ (int) $meta['value'] ];
1005 } else {
1006 $key = false;
1007 }
1008 }
1009
1010 if ( $key ) {
1011 // Export gets meta straight from the DB so could have a serialized string.
1012 if ( ! $value ) {
1013 $value = maybe_unserialize( $meta['value'] );
1014 }
1015
1016 add_post_meta( $post_id, wp_slash( $key ), wp_slash_strings_only( $value ) );
1017
1018 do_action( 'import_post_meta', $post_id, $key, $value );
1019
1020 // If the post has a featured image, take note of this in case of remap.
1021 if ( '_thumbnail_id' === $key ) {
1022 $this->featured_images[ $post_id ] = (int) $value;
1023 }
1024 }
1025 }
1026 }
1027
1028 do_action( 'templately_import.process_post', $post, $result, $this );
1029
1030 return $result;
1031 }, $backup_key); //, true
1032
1033 unset( $this->posts );
1034
1035 return $results;
1036 }
1037
1038 /**
1039 * Attempt to create a new menu item from import data
1040 *
1041 * Fails for draft, orphaned menu items and those without an associated nav_menu
1042 * or an invalid nav_menu term. If the post type or term object which the menu item
1043 * represents doesn't exist then the menu item will not be imported (waits until the
1044 * end of the import to retry again before discarding).
1045 *
1046 * @param array $item Menu item details from WXR file
1047 */
1048 private function process_menu_item( $item ) {
1049 $result = [];
1050
1051 // Skip draft, orphaned menu items.
1052 if ( 'draft' === $item['status'] ) {
1053 return;
1054 }
1055
1056 $menu_slug = false;
1057 if ( isset( $item['terms'] ) ) {
1058 // Loop through terms, assume first nav_menu term is correct menu.
1059 foreach ( $item['terms'] as $term ) {
1060 if ( 'nav_menu' === $term['domain'] ) {
1061 $menu_slug = $term['slug'];
1062 break;
1063 }
1064 }
1065 }
1066
1067 // No nav_menu term associated with this menu item.
1068 if ( ! $menu_slug ) {
1069 $this->output['errors'][] = esc_html__( 'Menu item skipped due to missing menu slug', 'elementor' );
1070
1071 return $result;
1072 }
1073
1074 // If menu was already exists, refer the items to the duplicated menu created.
1075 if ( array_key_exists( $menu_slug, $this->mapped_terms_slug ) ) {
1076 $menu_slug = $this->mapped_terms_slug[ $menu_slug ];
1077 }
1078
1079 $menu_id = term_exists( $menu_slug, 'nav_menu' );
1080 if ( ! $menu_id ) {
1081 /* translators: %s: Menu slug. */
1082 $this->output['errors'][] = sprintf( esc_html__( 'Menu item skipped due to invalid menu slug: %s', 'elementor' ), $menu_slug );
1083
1084 return $result;
1085 } else {
1086 $menu_id = is_array( $menu_id ) ? $menu_id['term_id'] : $menu_id;
1087 }
1088
1089 $post_meta_key_value = [];
1090 foreach ( $item['postmeta'] as $meta ) {
1091 $post_meta_key_value[ $meta['key'] ] = $meta['value'];
1092 }
1093
1094 $_menu_item_type = $post_meta_key_value['_menu_item_type'];
1095 $_menu_item_url = $post_meta_key_value['_menu_item_url'];
1096
1097 // Skip menu items 'taxonomy' type, when the taxonomy is not exits.
1098 if ( 'taxonomy' === $_menu_item_type && ! taxonomy_exists( $post_meta_key_value['_menu_item_object'] ) ) {
1099 return $result;
1100 }
1101
1102 // Skip menu items 'post_type' type, when the post type is not exits.
1103 if ( 'post_type' === $_menu_item_type && ! post_type_exists( $post_meta_key_value['_menu_item_object'] ) ) {
1104 return $result;
1105 }
1106
1107 $_menu_item_object_id = $post_meta_key_value['_menu_item_object_id'];
1108 if ( 'taxonomy' === $_menu_item_type && isset( $this->processed_terms[ (int) $_menu_item_object_id ] ) ) {
1109 $_menu_item_object_id = $this->processed_terms[ (int) $_menu_item_object_id ];
1110 } elseif ( 'post_type' === $_menu_item_type && isset( $this->processed_posts[ (int) $_menu_item_object_id ] ) ) {
1111 $_menu_item_object_id = $this->processed_posts[ (int) $_menu_item_object_id ];
1112 } elseif ( 'custom' === $_menu_item_type ) {
1113 // FIXME: Later on you need to check if there is custom menu link related fixes any.
1114 $_menu_item_url = URL::migrate( $_menu_item_url, $this->base_blog_url );
1115 // `substr()`, not `str_starts_with()` (PHP 8.0+, core-polyfilled only from WP 5.9):
1116 // the plugin advertises WordPress 5.0 / PHP 7.2.
1117 if ( substr( $_menu_item_url, 0, strlen( $this->base_blog_url ) ) === $this->base_blog_url ) {
1118 $_menu_item_url = '#';
1119 }
1120 } else {
1121 return $result;
1122 }
1123
1124 $_menu_item_menu_item_parent = $post_meta_key_value['_menu_item_menu_item_parent'];
1125 if ( isset( $this->processed_menu_items[ (int) $_menu_item_menu_item_parent ] ) ) {
1126 $_menu_item_menu_item_parent = $this->processed_menu_items[ (int) $_menu_item_menu_item_parent ];
1127 } elseif ( $_menu_item_menu_item_parent ) {
1128 $this->menu_item_orphans[ (int) $item['post_id'] ] = (int) $_menu_item_menu_item_parent;
1129 $_menu_item_menu_item_parent = 0;
1130 }
1131
1132 // wp_update_nav_menu_item expects CSS classes as a space separated string
1133 $_menu_item_classes = maybe_unserialize( $post_meta_key_value['_menu_item_classes'] );
1134 if ( is_array( $_menu_item_classes ) ) {
1135 $_menu_item_classes = implode( ' ', $_menu_item_classes );
1136 }
1137
1138 $args = [
1139 'menu-item-object-id' => $_menu_item_object_id,
1140 'menu-item-object' => $post_meta_key_value['_menu_item_object'],
1141 'menu-item-parent-id' => $_menu_item_menu_item_parent,
1142 'menu-item-position' => (int) $item['menu_order'],
1143 'menu-item-type' => $_menu_item_type,
1144 'menu-item-title' => $item['post_title'],
1145 'menu-item-url' => $_menu_item_url,
1146 'menu-item-description' => $item['post_content'],
1147 'menu-item-attr-title' => $item['post_excerpt'],
1148 'menu-item-target' => $post_meta_key_value['_menu_item_target'],
1149 'menu-item-classes' => $_menu_item_classes,
1150 'menu-item-xfn' => $post_meta_key_value['_menu_item_xfn'],
1151 'menu-item-status' => $item['status'],
1152 ];
1153
1154 $id = wp_update_nav_menu_item( $menu_id, 0, $args );
1155 if ( $id && ! is_wp_error( $id ) ) {
1156 $this->processed_menu_items[ (int) $item['post_id'] ] = (int) $id;
1157 $result[ $item['post_id'] ] = $id;
1158
1159 $this->update_post_meta( $id );
1160 }
1161
1162 return $result;
1163 }
1164
1165 private function process_navigation( &$item ): bool {
1166 if ( 'draft' === $item['status'] ) {
1167 return false;
1168 }
1169
1170 $content = parse_blocks( $item['post_content'] );
1171
1172 $parsed_blocks = [];
1173 foreach ( $content as $block ) {
1174 if ( empty( $block['blockName'] ) ) {
1175 continue;
1176 }
1177
1178 $this->prepare_block( $block );
1179 $parsed_blocks[] = $block;
1180 }
1181
1182 $item['post_content'] = serialize_blocks( $parsed_blocks );
1183
1184 return true;
1185 }
1186
1187 private function prepare_block( &$block ) {
1188 if ( $block['blockName'] == 'core/navigation-link' || $block['blockName'] == 'core/navigation-submenu' ) {
1189 $attrs = &$block['attrs'];
1190 switch ( $attrs['kind'] ) {
1191 case 'post-type':
1192 if ( isset( $this->processed_posts[ (int) $attrs['id'] ] ) ) {
1193 $attrs['id'] = $this->processed_posts[ (int) $attrs['id'] ];
1194 $attrs['url'] = get_permalink( $attrs['id'] );
1195 }
1196 break;
1197 case 'taxonomy':
1198 if ( isset( $this->processed_terms[ (int) $attrs['id'] ] ) ) {
1199 $attrs['id'] = $this->processed_terms[ (int) $attrs['id'] ];
1200 $attrs['url'] = get_term_link( $attrs['id'], $attrs['type'] );
1201 }
1202 break;
1203 }
1204 if ( ! empty( $block['innerBlocks'] ) ) {
1205 foreach ( $block['innerBlocks'] as &$b ) {
1206 $this->prepare_block( $b );
1207 }
1208 }
1209 }
1210 }
1211
1212 /**
1213 * If fetching attachments is enabled then attempt to create a new attachment
1214 *
1215 * @param array $post Attachment post details from WXR
1216 * @param string $url URL to fetch attachment from
1217 *
1218 * @return int|WP_Error Post ID on success, WP_Error otherwise
1219 */
1220 public function process_attachment( $post, $url, $sizes = [], $original_post_id = null ) {
1221 if ( ! $this->fetch_attachments ) {
1222 return new WP_Error( 'attachment_processing_error', esc_html__( 'Fetching attachments is not enabled', 'elementor' ) );
1223 }
1224 if ( ! function_exists( 'wp_crop_image' ) ) {
1225 include( ABSPATH . 'wp-admin/includes/image.php' );
1226 }
1227 // if the URL is absolute, but does not contain address, then upload it assuming base_site_url.
1228 if ( preg_match( '|^/[\w\W]+$|', $url ) ) {
1229 $url = rtrim( $this->base_url, '/' ) . $url;
1230 }
1231
1232 if($saved_image = $this->get_saved_image($url)){
1233 // $this->url_remap[ $url ] = wp_get_attachment_url( $saved_image );
1234 // $this->url_remap[ $this->remove_extension($url) ] = $this->remove_extension(wp_get_attachment_url( $saved_image ));
1235 $upload_url = set_url_scheme( wp_get_attachment_url( $saved_image ) );
1236 $this->set_url_map($url, $upload_url, true);
1237
1238 // AI image replacement: the imported content still carries the ORIGINAL
1239 // demo URL, so it must be remapped onto the replacement image here too.
1240 // The fresh-upload branch below does this, but when the replacement
1241 // image was already imported (a re-import, or the same stock photo
1242 // reused) we land here instead — without the map the demo URL survives
1243 // finalize and the pack's original images are silently kept.
1244 if(!empty($post['original_attachment_url'])){
1245 $this->set_url_map($post['original_attachment_url'], $upload_url, true);
1246
1247 $original_hash = sha1( $post['original_attachment_url'] );
1248
1249 // A stale hash from a previous import can still point the demo URL at
1250 // the old, unreplaced attachment — re-point it at the replacement.
1251 if( (int) $this->get_saved_image($post['original_attachment_url']) !== (int) $saved_image ){
1252 $hash_meta_id = add_post_meta( $saved_image, '_elementor_source_image_hash', $original_hash );
1253 add_post_meta( $saved_image, '_templately_image_hash_meta_id', $hash_meta_id );
1254 }
1255
1256 self::$_replace_image_ids[ $original_hash ] = (int) $saved_image;
1257 }
1258
1259 $full_size_path = get_attached_file($saved_image);
1260 $metadata = wp_get_attachment_metadata($saved_image);
1261 $updated_metadata = $this->import_sizes($sizes, $metadata, $full_size_path, $saved_image);
1262 if ($updated_metadata !== false) {
1263 wp_update_attachment_metadata( $saved_image, $updated_metadata );
1264 }
1265 return $saved_image;
1266 }
1267
1268 // Check if the URL is from the wp-includes/images directory
1269 if (strpos($url, 'wp-includes/images') !== false) {
1270 // Get the URL for 'wp-includes/images' directory of the current site
1271 $current_site_url = get_site_url(null, 'wp-includes/images');
1272
1273 // Use regex to replace the old URL base with the new URL base
1274 $updated_url = preg_replace('#https?://[^/]+/(wp-includes/images)#', $current_site_url, $url);
1275
1276 return $updated_url;
1277 }
1278
1279 $upload_dir = wp_upload_dir( $post['upload_date'] );
1280 if ( ! ( $upload_dir && false === $upload_dir['error'] ) ) {
1281 return new WP_Error( 'upload_dir_error', $upload_dir['error'] );
1282 }
1283
1284 // Move the file to the uploads dir.
1285 //
1286 // The attachment url is attacker-reachable — a cloud template can point an
1287 // image anywhere — so the url-derived name is sanitized, and an extension
1288 // WordPress does not recognise as an uploadable type is dropped rather than
1289 // carried into the destination path. fetch_remote_file() re-derives the
1290 // extension from the response and validates it before writing, so dropping
1291 // an unusable one here costs nothing.
1292 $file_name = sanitize_file_name( basename( parse_url( $url, PHP_URL_PATH ) ) );
1293 if ( pathinfo( $file_name, PATHINFO_EXTENSION ) ) {
1294 $url_filetype = wp_check_filetype( $file_name );
1295 if ( empty( $url_filetype['type'] ) ) {
1296 $file_name = pathinfo( $file_name, PATHINFO_FILENAME );
1297 }
1298 }
1299 $file_name = wp_unique_filename( $upload_dir['path'], $file_name );
1300 $dest_file = $upload_dir['path'] . "/$file_name";
1301
1302 $upload = apply_filters( 'templately_import_copy_attachment', null, $original_post_id, $dest_file, $upload_dir );
1303 if ( null === $upload ) {
1304 $upload = $this->fetch_remote_file( $url, $dest_file, $upload_dir );
1305 }
1306
1307 // Normalize URL scheme to match the current site scheme (fixes HTTP URLs on HTTPS multisites).
1308 if ( ! is_wp_error( $upload ) && ! empty( $upload['url'] ) ) {
1309 $upload['url'] = set_url_scheme( $upload['url'] );
1310 }
1311
1312 if ( is_wp_error( $upload ) ) {
1313 return $upload;
1314 }
1315
1316 $info = wp_check_filetype( $upload['file'] );
1317 if ( $info ) {
1318 $post['post_mime_type'] = $info['type'];
1319 } else {
1320 return new WP_Error( 'attachment_processing_error', esc_html__( 'Invalid file type', 'elementor' ) );
1321 }
1322
1323 // $this->url_remap[ $post['guid'] ] = $upload['url']; // r13735, really needed?
1324 // $this->set_url_map($post['guid'], $upload['url']);
1325 $post['guid'] = $upload['url'];
1326
1327 // As per wp-admin/includes/upload.php.
1328 $post_id = wp_insert_attachment( $post, $upload['file'] );
1329
1330 if(is_wp_error($post_id)){
1331 return $post_id;
1332 }
1333
1334 $this->update_post_meta( $post_id );
1335
1336 // Generate attachment metadata
1337 $metadata = wp_generate_attachment_metadata( $post_id, $upload['file'] );
1338
1339 // For gutenberg pages
1340 $updated_metadata = $this->import_sizes($sizes, $metadata, $upload['file'], $post_id);
1341
1342 if ($updated_metadata !== false) {
1343 wp_update_attachment_metadata( $post_id, $updated_metadata );
1344 } else {
1345 wp_update_attachment_metadata( $post_id, $metadata );
1346 }
1347
1348 // @todo: add missing image sizes
1349 if(defined('TEMPLATELY_DEV') && TEMPLATELY_DEV){
1350 update_post_meta( $post_id, '_templately_original_id', $original_post_id );
1351 update_post_meta( $post_id, '_templately_original_url', $url );
1352
1353 if(!empty($post['original_attachment_url'])){
1354 update_post_meta( $post_id, '_templately_demo_url', $post['original_attachment_url'] );
1355 }
1356 }
1357
1358 update_post_meta( $post_id, '_elementor_source_image_hash', sha1( $url ) );
1359 self::$_replace_image_ids[ sha1( $url ) ] = $post_id;
1360
1361 // add a second hash for original demo url
1362 // if user is replacing image.
1363 // so we can also match original demo url to new image
1364 if(!empty($post['original_attachment_url'])){
1365 $hash_meta_id = add_post_meta( $post_id, '_elementor_source_image_hash', sha1( $post['original_attachment_url'] ) );
1366 add_post_meta( $post_id, '_templately_image_hash_meta_id', $hash_meta_id );
1367 self::$_replace_image_ids[ sha1( $post['original_attachment_url'] ) ] = $post_id;
1368 }
1369
1370 // Remap resized image URLs, works by stripping the extension and remapping the URL stub.
1371 if ( preg_match( '!^image/!', $info['type'] ) ) {
1372 // $this->url_remap[ $this->remove_extension($url) ] = $this->remove_extension($upload['url']);
1373 $this->set_url_map($url, $upload['url'], true);
1374 if(!empty($post['original_attachment_url'])){
1375 $this->set_url_map($post['original_attachment_url'], $upload['url'], true);
1376 }
1377 }
1378
1379 return $post_id;
1380 }
1381
1382 private function remove_extension($url) {
1383 $parts = pathinfo($url);
1384 // pathinfo() omits 'extension' for a dotless name, and 'dirname' for an
1385 // empty one. Remote attachment urls are not obliged to carry an extension
1386 // (`.../media/1234`), and the url of an attachment that has since been
1387 // deleted resolves to ''.
1388 $dirname = isset( $parts['dirname'] ) ? $parts['dirname'] : '';
1389 $basename = isset( $parts['basename'] ) ? $parts['basename'] : '';
1390 $extension = isset( $parts['extension'] ) ? $parts['extension'] : '';
1391 $name = '' === $extension ? $basename : basename($basename, ".{$extension}"); // PATHINFO_FILENAME in PHP 5.2
1392
1393 return $dirname . '/' . $name;
1394 }
1395
1396 public function set_url_map($original_url, $new_url, $remove_extension = false){
1397 $this->url_remap[ $original_url ] = $new_url;
1398 if($remove_extension){
1399 $original_url = $this->remove_extension($original_url);
1400 $new_url = $this->remove_extension($new_url);
1401 $this->url_remap[ $original_url ] = $new_url;
1402 }
1403 }
1404
1405 /**
1406 * Attempt to download a remote file attachment
1407 *
1408 * @param string $url URL of item to fetch
1409 * @param array $post Attachment details
1410 *
1411 * @return array|WP_Error Local file location details on success, WP_Error otherwise
1412 */
1413 private function fetch_remote_file( $url, $new_file, $uploads ) {
1414 // Extract the file name from the new_file.
1415 $file_name = basename( $new_file );
1416
1417 // Include the file for the download_url function
1418 if(!function_exists('wp_tempnam')) {
1419 require_once ABSPATH . 'wp-admin/includes/file.php';
1420 }
1421
1422 $tmp_file_name = wp_tempnam( $file_name );
1423 if ( ! $tmp_file_name ) {
1424 return new WP_Error( 'import_no_file', esc_html__( 'Could not create temporary file.', 'elementor' ) );
1425 }
1426
1427 // A parallel window may already have this file (see AttachmentPrefetcher). A hit is
1428 // handed to the SAME validation below as a fresh download — it is a faster way to
1429 // reach this point, not a way around the checks. Anything short of a usable file
1430 // falls through to the request this method has always made.
1431 $remote_response = null;
1432 $prefetched = AttachmentPrefetcher::take( $url );
1433 if ( null !== $prefetched ) {
1434 // rename() can fail across filesystems (sys temp dir vs uploads); copy is the fallback.
1435 $moved = @rename( $prefetched['file'], $tmp_file_name ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1436 if ( ! $moved ) {
1437 $moved = @copy( $prefetched['file'], $tmp_file_name ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1438 @unlink( $prefetched['file'] ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
1439 }
1440 if ( $moved ) {
1441 $remote_response = [
1442 'headers' => $prefetched['headers'],
1443 'response' => [ 'code' => $prefetched['code'], 'message' => 'OK' ],
1444 'body' => '',
1445 ];
1446 }
1447 }
1448
1449 // Fetch the remote URL and write it to the placeholder file.
1450 $attempt = 0;
1451 $retry_count = $this->attachment_retry_count;
1452 while ( null === $remote_response || ( is_wp_error( $remote_response ) && $attempt < $retry_count ) ) {
1453 $remote_response = wp_safe_remote_get( $url, [
1454 'timeout' => $this->attachment_timeout,
1455 'stream' => true,
1456 'filename' => $tmp_file_name,
1457 'headers' => [
1458 'Accept-Encoding' => 'identity',
1459 ]
1460 ] );
1461 $attempt++;
1462 }
1463
1464
1465 if ( is_wp_error( $remote_response ) ) {
1466 @unlink( $tmp_file_name );
1467
1468 return new WP_Error( 'import_file_error', sprintf( /* translators: 1: WordPress error message, 2: WordPress error code. */ esc_html__( 'Request failed due to an error: %1$s (%2$s)', 'elementor' ), esc_html( $remote_response->get_error_message() ), esc_html( $remote_response->get_error_code() ) ) );
1469 }
1470
1471 $remote_response_code = (int) wp_remote_retrieve_response_code( $remote_response );
1472
1473 // Make sure the fetch was successful.
1474 if ( 200 !== $remote_response_code ) {
1475 @unlink( $tmp_file_name );
1476 return new WP_Error( 'import_file_error', sprintf( /* translators: 1: HTTP error message, 2: HTTP error code. */ esc_html__( 'Remote server returned the following unexpected result: %1$s (%2$s)', 'elementor' ), get_status_header_desc( $remote_response_code ), esc_html( $remote_response_code ) ) );
1477 }
1478
1479 $headers = wp_remote_retrieve_headers( $remote_response );
1480
1481 // Request failed.
1482 if ( ! $headers ) {
1483 @unlink( $tmp_file_name );
1484
1485 return new WP_Error( 'import_file_error', esc_html__( 'Remote server did not respond', 'elementor' ) );
1486 }
1487
1488 $filesize = (int) filesize( $tmp_file_name );
1489
1490 if ( 0 === $filesize ) {
1491 @unlink( $tmp_file_name );
1492
1493 return new WP_Error( 'import_file_error', esc_html__( 'Zero size file downloaded', 'elementor' ) );
1494 }
1495
1496 if ( ! isset( $headers['content-encoding'] ) && isset( $headers['content-length'] ) && $filesize !== (int) $headers['content-length'] ) {
1497 @unlink( $tmp_file_name );
1498
1499 return new WP_Error( 'import_file_error', esc_html__( 'Downloaded file has incorrect size', 'elementor' ) );
1500 }
1501
1502 $max_size = (int) apply_filters( 'import_attachment_size_limit', self::DEFAULT_IMPORT_ATTACHMENT_SIZE_LIMIT );
1503 if ( ! empty( $max_size ) && $filesize > $max_size ) {
1504 @unlink( $tmp_file_name );
1505
1506 /* translators: %s: Max file size. */
1507
1508 return new WP_Error( 'import_file_error', sprintf( esc_html__( 'Remote file is too large, limit is %s', 'elementor' ), size_format( $max_size ) ) );
1509 }
1510
1511 // Override file name with Content-Disposition header value.
1512 // The header is whatever the remote chose to send, so it is sanitized before
1513 // it is allowed anywhere near a path: sanitize_file_name() strips directory
1514 // separators and neutralizes the inner extension of a double-extension name.
1515 if ( ! empty( $headers['content-disposition'] ) ) {
1516 $file_name_from_disposition = self::get_filename_from_disposition( (array) $headers['content-disposition'] );
1517 if ( $file_name_from_disposition ) {
1518 $file_name_from_disposition = sanitize_file_name( $file_name_from_disposition );
1519 }
1520 if ( $file_name_from_disposition ) {
1521 $file_name = $file_name_from_disposition;
1522 }
1523 }
1524
1525 // Set file extension if missing.
1526 $file_ext = pathinfo( $file_name, PATHINFO_EXTENSION );
1527 if ( ! $file_ext && ! empty( $headers['content-type'] ) ) {
1528 $extension = self::get_file_extension_by_mime_type( $headers['content-type'] );
1529 if ( $extension ) {
1530 $file_name = "{$file_name}.{$extension}";
1531 }
1532 }
1533
1534 // Handle the upload like _wp_handle_upload() does.
1535 $wp_filetype = wp_check_filetype_and_ext( $tmp_file_name, $file_name );
1536 $ext = empty( $wp_filetype['ext'] ) ? '' : $wp_filetype['ext'];
1537 $type = empty( $wp_filetype['type'] ) ? '' : $wp_filetype['type'];
1538 $proper_filename = empty( $wp_filetype['proper_filename'] ) ? '' : $wp_filetype['proper_filename'];
1539
1540 // Check to see if wp_check_filetype_and_ext() determined the filename was incorrect.
1541 if ( $proper_filename ) {
1542 $file_name = $proper_filename;
1543 }
1544
1545 if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
1546 @unlink( $tmp_file_name );
1547
1548 return new WP_Error( 'import_file_error', esc_html__( 'Sorry, this file type is not permitted for security reasons.', 'elementor' ) );
1549 }
1550
1551 // The type check above ran against $file_name (Content-Disposition), but the
1552 // write lands on $new_file, whose name came from the request url path. When
1553 // those two extensions disagree the check guarantees nothing about what is
1554 // actually written — an image/gif verdict on `ok.gif` would let the bytes go
1555 // to a `.php` destination. Re-derive the destination from the validated name
1556 // so the two can never diverge. (CVE-2026-18438)
1557 if ( $ext && strtolower( pathinfo( $new_file, PATHINFO_EXTENSION ) ) !== strtolower( $ext ) ) {
1558 $file_name = wp_unique_filename( $uploads['path'], $file_name );
1559 $new_file = $uploads['path'] . "/$file_name";
1560 }
1561
1562 // Whatever the branch above decided, the reported name must describe the file
1563 // that is actually on disk — the remapped url in the imported content is built
1564 // from it.
1565 $file_name = wp_basename( $new_file );
1566
1567 $move_new_file = copy( $tmp_file_name, $new_file );
1568
1569 if ( ! $move_new_file ) {
1570 @unlink( $tmp_file_name );
1571
1572 return new WP_Error( 'import_file_error', esc_html__( 'The uploaded file could not be moved', 'elementor' ) );
1573 }
1574
1575 // Set correct file permissions.
1576 $stat = stat( dirname( $new_file ) );
1577 $perms = $stat['mode'] & 0000666;
1578 chmod( $new_file, $perms );
1579
1580 $upload = [
1581 'file' => $new_file,
1582 'url' => $uploads['url'] . "/$file_name",
1583 'type' => $wp_filetype['type'],
1584 'error' => false,
1585 ];
1586
1587 // Keep track of the old and new urls so we can substitute them later.
1588 $this->set_url_map($url, $upload['url']);
1589 // Keep track of the destination if the remote url is redirected somewhere else.
1590 if ( isset( $headers['x-final-location'] ) && $headers['x-final-location'] !== $url ) {
1591 $this->set_url_map($headers['x-final-location'], $upload['url']);
1592 }
1593
1594 return $upload;
1595 }
1596
1597 /**
1598 * Get saved image.
1599 *
1600 * Retrieve new image ID, if the image has a new ID after the import.
1601 *
1602 * @since 2.0.0
1603 * @access private
1604 *
1605 * @param string $url The image URL.
1606 *
1607 * @return false|int New image ID or false.
1608 */
1609 private function get_saved_image( $url ) {
1610 global $wpdb;
1611
1612 $hash = sha1( $url );
1613
1614 if ( isset( self::$_replace_image_ids[ $hash ] ) ) {
1615 return self::$_replace_image_ids[ $hash ];
1616 }
1617
1618 // Newest row wins. A URL can be claimed by more than one attachment: an
1619 // AI/customizer image replacement registers the ORIGINAL demo URL against
1620 // the REPLACEMENT attachment (see process_attachment), while a previous
1621 // plain import of the same pack already claimed that URL for the original
1622 // image — and that older row is never cleaned up (clear_old_el_cache only
1623 // tracks replacement hashes). Without the ordering the stale original wins
1624 // and the replacement is silently ignored on every re-import.
1625 //
1626 // The static cache above short-circuits this, but the import is split
1627 // across many requests (each SSE `continue` is a fresh PHP process), so
1628 // the DB is the real source of truth for anything set in an earlier step.
1629 $post_id = $wpdb->get_var(
1630 $wpdb->prepare(
1631 'SELECT `post_id` FROM `' . $wpdb->postmeta . '`
1632 WHERE `meta_key` = \'_elementor_source_image_hash\'
1633 AND `meta_value` = %s
1634 ORDER BY `meta_id` DESC
1635 LIMIT 1
1636 ;',
1637 $hash
1638 )
1639 );
1640
1641 if ( $post_id ) {
1642 self::$_replace_image_ids[ $hash ] = $post_id;
1643 return (int) $post_id;
1644 }
1645
1646 return false;
1647 }
1648
1649
1650 public function import_sizes($sizes, $metadata, $full_size_file, $post_id) {
1651 $metadata_modified = false;
1652
1653 if (!empty($sizes)) {
1654 do_action('templately_import.finalize_gutenberg_attachment', $post_id);
1655
1656 foreach ($sizes as $size_name => $size) {
1657 $size_dimension = $size['width'] . 'x' . $size['height'];
1658 $size_name = !is_string($size_name) ? $size_dimension : $size_name;
1659 $unique_destination_file = $this->create_unique_destination_file($full_size_file, $size);
1660 // check non cropped sizes. with dynamic height
1661 if (!$this->size_exists_in_metadata(basename($unique_destination_file), $metadata)) {
1662 if ($unique_destination_file) {
1663 $missing_size = $this->generate_missing_size_from_full($full_size_file, $unique_destination_file, $size);
1664 if ($missing_size && !is_wp_error($missing_size)) {
1665 unset($missing_size['path']);
1666 $metadata['sizes'][$size_name] = $missing_size;
1667 $metadata_modified = true;
1668 do_action('templately_import.finalize_gutenberg_attachment', $post_id, $size_dimension);
1669 }
1670 }
1671 }
1672 }
1673 }
1674
1675 // Only return metadata if new sizes were actually added
1676 return $metadata_modified ? $metadata : false;
1677 }
1678
1679 public function generate_missing_size_from_full($full_size_file, $destination_file, $size) {
1680 // Generate the missing size from the full-size image
1681 $editor = wp_get_image_editor($full_size_file);
1682 if (is_wp_error($editor)) {
1683 return $editor;
1684 }
1685
1686 $resized = $editor->resize($size['width'], $size['height'], true);
1687 if (is_wp_error($resized)) {
1688 return $resized;
1689 }
1690
1691 $saved = $editor->save($destination_file);
1692 if (is_wp_error($saved)) {
1693 return $saved;
1694 }
1695
1696 return $saved;
1697 }
1698
1699 public function size_exists_in_metadata($size_file, $metadata) {
1700 if (isset($metadata['sizes']) && is_array($metadata['sizes'])) {
1701 foreach ($metadata['sizes'] as $size_info) {
1702 if ($size_info['file'] == $size_file) {
1703 return true;
1704 }
1705 }
1706 }
1707 return false;
1708 }
1709
1710 public function create_unique_destination_file($full_size_file, $size) {
1711 $pathinfo = pathinfo($full_size_file);
1712 $directory = $pathinfo['dirname'];
1713 $filename = $pathinfo['filename'];
1714 $extension = $pathinfo['extension'];
1715
1716 $destination_file = $directory . '/' . $filename . '-' . $size['width'] . 'x' . $size['height'] . '.' . $extension;
1717
1718 // Skip if the file already exists
1719 if (file_exists($destination_file)) {
1720 // return false;
1721 }
1722
1723 return $destination_file;
1724 }
1725
1726 public function create_size_array($destination_file, $size_dimension) {
1727 list($width, $height) = explode('x', $size_dimension);
1728 return array(
1729 'file' => basename($destination_file),
1730 'width' => $width,
1731 'height' => $height,
1732 'mime-type' => wp_check_filetype($destination_file)['type'],
1733 'filesize' => filesize($destination_file),
1734 'resized' => false,
1735 );
1736 }
1737
1738
1739 /**
1740 * Attempt to associate posts and menu items with previously missing parents
1741 *
1742 * An imported post's parent may not have been imported when it was first created
1743 * so try again. Similarly for child menu items and menu items which were missing
1744 * the object (e.g. post) they represent in the menu
1745 */
1746 private function backfill_parents() {
1747 global $wpdb;
1748
1749 // Find parents for post orphans.
1750 foreach ( $this->post_orphans as $child_id => $parent_id ) {
1751 $local_child_id = false;
1752 $local_parent_id = false;
1753
1754 if ( isset( $this->processed_posts[ $child_id ] ) ) {
1755 $local_child_id = $this->processed_posts[ $child_id ];
1756 }
1757 if ( isset( $this->processed_posts[ $parent_id ] ) ) {
1758 $local_parent_id = $this->processed_posts[ $parent_id ];
1759 }
1760
1761 if ( $local_child_id && $local_parent_id ) {
1762 $wpdb->update( $wpdb->posts, [ 'post_parent' => $local_parent_id ], [ 'ID' => $local_child_id ], '%d', '%d' );
1763 clean_post_cache( $local_child_id );
1764 }
1765 }
1766
1767 // Find parents for menu item orphans.
1768 foreach ( $this->menu_item_orphans as $child_id => $parent_id ) {
1769 $local_child_id = 0;
1770 $local_parent_id = 0;
1771 if ( isset( $this->processed_menu_items[ $child_id ] ) ) {
1772 $local_child_id = $this->processed_menu_items[ $child_id ];
1773 }
1774 if ( isset( $this->processed_menu_items[ $parent_id ] ) ) {
1775 $local_parent_id = $this->processed_menu_items[ $parent_id ];
1776 }
1777
1778 if ( $local_child_id && $local_parent_id ) {
1779 update_post_meta( $local_child_id, '_menu_item_menu_item_parent', (int) $local_parent_id );
1780 }
1781 }
1782 }
1783
1784 /**
1785 * Use stored mapping information to update old attachment URLs
1786 */
1787 private function backfill_attachment_urls() {
1788 global $wpdb;
1789 // Make sure we do the longest urls first, in case one is a substring of another.
1790 uksort( $this->url_remap, function ( $a, $b ) {
1791 // Return the difference in length between two strings.
1792 return strlen( $b ) - strlen( $a );
1793 } );
1794
1795 // Reindex to a positional (0..n) array. processed_posts is keyed by
1796 // old_id => new_id, and when restored from a saved session its keys can
1797 // be non-sequential/string keys. Spreading such an array via `...` would
1798 // pass them as NAMED arguments and trigger a PHP 8.1+ fatal:
1799 // "Cannot use positional argument after named argument during unpacking".
1800 // The IN clause only needs the new post IDs as positional %d args.
1801 $processed_post_ids = array_values( $this->processed_posts );
1802
1803 foreach ( $this->url_remap as $from_url => $to_url ) {
1804 // Remap urls in post_content.
1805 $processed_posts_placeholders = implode(',', array_fill(0, count($processed_post_ids), '%d'));
1806
1807 // Prepare the query for posts
1808 $query_posts = $wpdb->prepare(
1809 "UPDATE {$wpdb->posts} SET post_content = REPLACE(post_content, %s, %s) WHERE ID IN ($processed_posts_placeholders)",
1810 $from_url,
1811 $to_url,
1812 ...$processed_post_ids
1813 );
1814 $wpdb->query($query_posts);
1815
1816 // Prepare the query for postmeta
1817 $query_postmeta = $wpdb->prepare(
1818 "UPDATE {$wpdb->postmeta} SET meta_value = REPLACE(meta_value, %s, %s) WHERE meta_key='enclosure' AND post_id IN ($processed_posts_placeholders)",
1819 $from_url,
1820 $to_url,
1821 ...$processed_post_ids
1822 );
1823 $wpdb->query($query_postmeta);
1824 }
1825 }
1826
1827 /**
1828 * Update _thumbnail_id meta to new, imported attachment IDs
1829 */
1830 private function remap_featured_images() {
1831 // Cycle through posts that have a featured image.
1832 foreach ( $this->featured_images as $post_id => $value ) {
1833 if ( isset( $this->processed_posts[ $value ] ) ) {
1834 $new_id = $this->processed_posts[ $value ];
1835 // Only update if there's a difference.
1836 if ( $new_id !== $value ) {
1837 update_post_meta( $post_id, '_thumbnail_id', $new_id );
1838 }
1839 }
1840 }
1841 }
1842
1843 /**
1844 * Parse a WXR file
1845 *
1846 * @param string $file Path to WXR file for parsing
1847 *
1848 * @return array Information gathered from the WXR file
1849 */
1850 private function parse( $file ): array {
1851 $parser = new WXR_Parser();
1852
1853 return $parser->parse( $file );
1854 }
1855
1856 /**
1857 * Decide if the given meta key maps to information we will want to import
1858 *
1859 * @param string $key The meta key to check
1860 *
1861 * @return string|bool The key if we do want to import, false if not
1862 */
1863 private function is_valid_meta_key( $key ) {
1864 // Skip attachment metadata since we'll regenerate it from scratch.
1865 // Skip _edit_lock as not relevant for import
1866 if ( in_array( $key, [ '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock', '_elementor_source_image_hash', 'sm_cloud' ] ) ) {
1867 return false;
1868 }
1869
1870 return $key;
1871 }
1872
1873 /**
1874 * @param $term
1875 *
1876 * @return mixed
1877 */
1878 private function handle_duplicated_nav_menu_term( $term ) {
1879 $duplicate_slug = $term['slug'] . '-duplicate';
1880 $duplicate_name = $term['term_name'] . ' duplicate';
1881
1882 while ( term_exists( $duplicate_slug, 'nav_menu' ) ) {
1883 $duplicate_slug .= '-duplicate';
1884 $duplicate_name .= ' duplicate';
1885 }
1886
1887 $this->mapped_terms_slug[ $term['slug'] ] = $duplicate_slug;
1888
1889 $term['slug'] = $duplicate_slug;
1890 $term['term_name'] = $duplicate_name;
1891
1892 return $term;
1893 }
1894
1895 /**
1896 * Add all term_meta to specified term.
1897 *
1898 * @param $term_id
1899 *
1900 * @return void
1901 */
1902 private function update_term_meta( $term_id ) {
1903 foreach ( $this->terms_meta as $meta_key => $meta_value ) {
1904 update_term_meta( $term_id, $meta_key, $meta_value );
1905 }
1906 }
1907
1908 /**
1909 * Add all post_meta to specified term.
1910 *
1911 * @param $post_id
1912 *
1913 * @return void
1914 */
1915 public function update_post_meta( $post_id ) {
1916 foreach ( $this->posts_meta as $meta_key => $meta_value ) {
1917 update_post_meta( $post_id, $meta_key, $meta_value );
1918 }
1919 }
1920
1921 public function run(): array {
1922 $this->import( $this->requested_file_path );
1923
1924 return $this->output;
1925 }
1926
1927 /**
1928 * Register Templately with Jetpack Sync's known importers.
1929 *
1930 * This filter callback adds Templately to Jetpack's list of known importers,
1931 * ensuring that 'templately' is sent to WordPress.com instead of the full class name.
1932 * This provides cleaner analytics data and better tracking.
1933 *
1934 * @param array $known_importers Array of known importers with class names as keys and friendly names as values.
1935 * @return array Modified array of known importers.
1936 */
1937 public function register_jetpack_importer( $known_importers ) {
1938 $known_importers[ __CLASS__ ] = 'templately';
1939 return $known_importers;
1940 }
1941
1942 /**
1943 * There is deliberately NO `parent::__construct()` call here.
1944 *
1945 * `WP_Importer` declared an EMPTY constructor up to WordPress 7.0 and
1946 * removed it in 7.1. Calling a parent constructor the parent does not
1947 * declare throws `Error: Cannot call constructor`, so the call fataled every
1948 * Full Site Import on 7.1 — and it had never done anything on the versions
1949 * where it resolved, because the body was empty.
1950 *
1951 * Nothing contained it: `Error` is not an `Exception` in PHP 7+ (siblings
1952 * under `Throwable`), and both outer import handlers — `Import.php` and
1953 * `Concerns\RunsImport` — catch `Exception`. The import streams over SSE, so
1954 * the fatal landed mid-stream with no `error` event: the connection just
1955 * stopped.
1956 *
1957 * Do not "restore" it for symmetry. `tests/unit/test-WPImportConstruction.php`
1958 * pins this, and only discriminates on the `forward` (WP 7.1) matrix cell.
1959 *
1960 * @param $file
1961 * @param array $args
1962 */
1963 public function __construct( $file, array $args = [] ) {
1964 $this->args = $args;
1965 $this->session_id = $args['session_id'];
1966
1967 if ( ! empty( $args['json'] ) ) {
1968 $this->json = $args['json'];
1969 }
1970
1971 if ( ! empty( $args['origin'] ) ) {
1972 $this->origin = $args['origin'];
1973 }
1974
1975 if ( ! empty( $file ) ) {
1976 $this->requested_file_path = $file;
1977 $this->import_data_key = 'wp_importer_attributes_' . md5($this->requested_file_path);
1978 }
1979
1980 if ( ! empty( $this->args['fetch_attachments'] ) ) {
1981 $this->fetch_attachments = true;
1982 }
1983
1984 if ( isset( $this->args['attachment_timeout'] ) && is_numeric( $this->args['attachment_timeout'] ) ) {
1985 $this->attachment_timeout = max( 1, (int) $this->args['attachment_timeout'] );
1986 }
1987
1988 if ( isset( $this->args['attachment_retries'] ) && is_numeric( $this->args['attachment_retries'] ) ) {
1989 $this->attachment_retry_count = max( 1, (int) $this->args['attachment_retries'] + 1 );
1990 }
1991
1992 if ( isset( $this->args['posts'] ) && is_array( $this->args['posts'] ) ) {
1993 $this->processed_posts = $this->args['posts'];
1994 }
1995
1996 if ( isset( $this->args['terms'] ) && is_array( $this->args['terms'] ) ) {
1997 $this->processed_terms = $this->args['terms'];
1998 }
1999
2000 if ( isset( $this->args['taxonomies'] ) && is_array( $this->args['taxonomies'] ) ) {
2001 $this->processed_taxonomies = $this->args['taxonomies'];
2002 }
2003
2004 if ( ! empty( $this->args['posts_meta'] ) ) {
2005 $this->posts_meta = $this->args['posts_meta'];
2006 }
2007
2008 if ( ! empty( $this->args['terms_meta'] ) ) {
2009 $this->terms_meta = $this->args['terms_meta'];
2010 }
2011
2012 // Register with Jetpack Sync for better analytics tracking.
2013 add_filter( 'jetpack_sync_known_importers', [ $this, 'register_jetpack_importer' ] );
2014 }
2015 }
2016