PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.7.2
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.7.2
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 3.1.10 All 111 releases
← All changes | includes/Core/Importer/WPImport.php +410 -137 3.0.73.7.2 View file →
@@ -2,8 +2,9 @@
2 2
3 3 namespace Templately\Core\Importer;
4 4
5 5 use Templately\Core\Importer\Parsers\WXR_Parser;
6 +use Templately\Core\Importer\Runners\Loop;
6 7 use Templately\Core\Importer\Utils\Utils;
7 8 use Templately\Utils\Helper;
8 9 use WP_Error;
9 10 use WP_Importer;
@@ -47,8 +48,11 @@
47 48 }
48 49 }
49 50
50 51 class WPImport extends WP_Importer {
52 + use LogHelper;
53 + use Loop;
54 +
51 55 const DEFAULT_BUMP_REQUEST_TIMEOUT = 60;
52 56 const DEFAULT_ALLOW_CREATE_USERS = true;
53 57 const DEFAULT_IMPORT_ATTACHMENT_SIZE_LIMIT = 0; // 0 = unlimited.
54 58
@@ -55,8 +59,12 @@
55 59 /**
56 60 * @var string
57 61 */
58 62 private $requested_file_path;
63 + /**
64 + * @var string
65 + */
66 + private $import_data_key;
59 67
60 68 /**
61 69 * @var array
62 70 */
@@ -62,8 +70,13 @@
62 70 */
63 71 private $args;
64 72
65 73 /**
74 + * @var FullSiteImport
75 + */
76 + private $origin;
77 +
78 + /**
66 79 * @var array
67 80 */
68 81 private $output = [
69 82 'status' => 'failed',
@@ -74,8 +87,18 @@
74 87 * WXR attachment ID
75 88 */
76 89 private $id;
77 90
91 + /**
92 + * @var mixed
93 + */
94 + private $json;
95 +
96 + /**
97 + * @var string
98 + */
99 + private $session_id;
100 +
78 101 // Information to import from WXR file.
79 102 private $version;
80 103 private $authors = [];
81 104 public $posts = [];
@@ -96,8 +119,10 @@
96 119 private $menu_item_orphans = [];
97 120 private $mapped_terms_slug = [];
98 121
99 122 private $fetch_attachments = false;
123 + private $attachment_timeout = 300;
124 + private $attachment_retry_count = 3;
100 125 private $featured_images = [];
101 126
102 127 /**
103 128 * @var array[] [meta_key => meta_value] Meta value that should be set for every imported post.
@@ -110,8 +135,25 @@
110 135 private $terms_meta = [];
111 136
112 137 public static $_replace_image_ids = [];
113 138
139 +
140 + public $backup_attributes = [
141 + 'output',
142 + 'url_remap',
143 + '_replace_image_ids',
144 + 'menu_item_orphans',
145 + 'processed_menu_items',
146 + 'post_orphans',
147 + 'processed_posts',
148 + 'featured_images',
149 + 'mapped_terms_slug',
150 + 'processed_terms',
151 + 'processed_authors',
152 + 'author_mapping',
153 + ];
154 +
155 +
114 156 /**
115 157 * Parses filename from a Content-Disposition header value.
116 158 *
117 159 * As per RFC6266:
@@ -287,9 +329,10 @@
287 329
288 330 wp_defer_term_counting( true );
289 331 wp_defer_comment_counting( true );
290 332
291 - do_action( 'import_start', $this );
333 + do_action( 'import_start' );
334 + do_action( 'templately_import_start', $this );
292 335
293 336 return true;
294 337 }
295 338
@@ -353,8 +396,14 @@
353 396 if ( ! isset( $this->args['imported_authors'] ) ) {
354 397 return;
355 398 }
356 399
400 +
401 + $processed_templates = $this->get_loop_result([], $this->import_data_key);
402 + if (!empty($processed_templates)) {
403 + return;
404 + }
405 +
357 406 $create_users = apply_filters( 'import_allow_create_users', self::DEFAULT_ALLOW_CREATE_USERS );
358 407
359 408 foreach ( (array) $this->args['imported_authors'] as $i => $old_login ) {
360 409 // Multisite adds strtolower to sanitize_user. Need to sanitize here to stop breakage in process_posts.
@@ -409,8 +458,10 @@
409 458 }
410 459 $this->author_mapping[ $santized_old_login ] = (int) get_current_user_id();
411 460 }
412 461 }
462 +
463 + $this->set_loop_result( true, $this->import_data_key );
413 464 }
414 465
415 466 /**
416 467 * Create new terms based on import information
@@ -424,8 +475,14 @@
424 475 'succeed' => [],
425 476 'failed' => [],
426 477 ];
427 478
479 + $processed_templates = $this->get_loop_result([], "wp_import_terms_" . $this->import_data_key);
480 + if (!empty($processed_templates)) {
481 + $result = $this->get_loop_result([], "wp_import_terms_" . $this->import_data_key);
482 + return $result;
483 + }
484 +
428 485 $this->terms = apply_filters( 'wp_import_terms', $this->terms );
429 486 if ( empty( $this->terms ) ) {
430 487 return $result;
431 488 }
@@ -505,8 +562,10 @@
505 562 }
506 563
507 564 unset( $this->terms );
508 565
566 + // Add the template to the processed templates and update the session data
567 + $this->set_loop_result( $result, "wp_import_terms_" . $this->import_data_key);
509 568 return $result;
510 569 }
511 570
512 571 /**
@@ -576,42 +635,59 @@
576 635 *
577 636 * @return array the ids of succeed/failed imported posts.
578 637 */
579 638 private function process_posts(): array {
580 - $result = [
581 - 'succeed' => [],
582 - 'failed' => [],
583 - ];
639 + $backup_key = "wp_import_post_" . $this->import_data_key;
584 640
585 641 $this->posts = apply_filters( 'wp_import_posts', $this->posts );
586 642
587 - foreach ( $this->posts as $post ) {
588 - $post = apply_filters( 'wp_import_post_data_raw', $post );
643 + $results = $this->loop( $this->posts, function($key, $post, $result ) {
589 644
645 + $result = !empty($result) ? $result : [
646 + 'succeed' => [],
647 + 'failed' => [],
648 + ];
649 +
650 + $original_post_id = $post['post_id'];
651 + $post = apply_filters( 'wp_import_post_data_raw', $post, $this );
652 +
653 + if(empty($post)){
654 + return $result;
655 + }
656 +
657 + if(!is_array($post) && is_numeric($post)){
658 + $result['succeed'][ $original_post_id ] = $post;
659 + return $result;
660 + }
661 +
590 662 if ( ! post_type_exists( $post['post_type'] ) ) {
591 663 /* translators: 1: Post title, 2: Post type. */
592 664 $this->output['errors'][] = sprintf( esc_html__( 'Failed to import %1$s: Invalid post type %2$s', 'elementor' ), $post['post_title'], $post['post_type'] );
593 665 do_action( 'wp_import_post_exists', $post );
594 - continue;
666 + return $result;
595 667 }
596 668
597 669 if ( isset( $this->processed_posts[ $post['post_id'] ] ) && ! empty( $post['post_id'] ) ) {
598 - continue;
670 + return $result;
599 671 }
600 672
601 673 if ( 'auto-draft' === $post['status'] ) {
602 - continue;
674 + return $result;
603 675 }
604 676
677 + if(!empty($post['post_content']) && !empty($post['post_id'])){
678 + $post['post_content'] = Utils::import_and_replace_attachments($post['post_content'], $post['post_id']);
679 + }
680 +
605 681 if ( 'nav_menu_item' === $post['post_type'] ) {
606 682 $result['succeed'] += $this->process_menu_item( $post );
607 - continue;
683 + return $result;
608 684 }
609 685
610 686 if ( 'wp_navigation' === $post['post_type'] ) {
611 687 $processed = $this->process_navigation( $post );
612 688 if ( ! $processed ) {
613 - continue;
689 + return $result;
614 690 }
615 691 }
616 692
617 693 $post_type_object = get_post_type_object( $post['post_type'] );
@@ -651,9 +727,12 @@
651 727 'post_type' => $post['post_type'],
652 728 'post_password' => $post['post_password'],
653 729 ];
654 730
655 - $original_post_id = $post['post_id'];
731 + if(isset($post['original_attachment_url'])){
732 + $postdata['original_attachment_url'] = $post['original_attachment_url'];
733 + }
734 +
656 735 $postdata = apply_filters( 'wp_import_post_data_processed', $postdata, $post );
657 736
658 737 $postdata = wp_slash( $postdata );
659 738
@@ -658,9 +737,9 @@
658 737 $postdata = wp_slash( $postdata );
659 738
660 739 if ( 'attachment' === $postdata['post_type'] ) {
661 740 $remote_url = ! empty( $post['attachment_url'] ) ? $post['attachment_url'] : $post['guid'];
662 -
741 + $attachment_sizes = [];
663 742 // try to use _wp_attached file for upload folder placement to ensure the same location as the export site
664 743 // e.g. location is 2003/05/image.jpg but the attachment post_date is 2010/09, see media_handle_upload()
665 744 $postdata['upload_date'] = $post['post_date'];
666 745 if ( isset( $post['postmeta'] ) ) {
@@ -668,14 +747,19 @@
668 747 if ( '_wp_attached_file' === $meta['key'] ) {
669 748 if ( preg_match( '%^[0-9]{4}/[0-9]{2}%', $meta['value'], $matches ) ) {
670 749 $postdata['upload_date'] = $matches[0];
671 750 }
672 - break;
751 + // break;
673 752 }
753 + else if ( '_wp_attachment_metadata' === $meta['key'] ) {
754 + $attachment_metadata = maybe_unserialize( $meta['value'] );
755 + $attachment_sizes = $attachment_metadata['sizes'] ?? [];
756 + // break;
757 + }
674 758 }
675 759 }
676 760
677 - $post_id = $this->process_attachment( $postdata, $remote_url );
761 + $post_id = $this->process_attachment( $postdata, $remote_url, $attachment_sizes, $original_post_id );
678 762 $comment_post_id = $post_id;
679 763 } else {
680 764 $post_id = wp_insert_post( $postdata, true );
681 765
@@ -696,9 +780,13 @@
696 780 $result['failed'][] = $original_post_id;
697 781
698 782 $this->output['errors'][] = $error;
699 783
700 - continue;
784 + if ( 'attachment' === $postdata['post_type'] ) {
785 + do_action( 'templately_import.process_post', $post, $result, $this );
786 + }
787 +
788 + return $result;
701 789 }
702 790
703 791 $result['succeed'][ $original_post_id ] = $post_id;
704 792
@@ -852,13 +940,15 @@
852 940 }
853 941 }
854 942
855 943 do_action( 'templately_import.process_post', $post, $result, $this );
856 - }
857 944
945 + return $result;
946 + }, $backup_key); //, true
947 +
858 948 unset( $this->posts );
859 949
860 - return $result;
950 + return $results;
861 951 }
862 952
863 953 /**
864 954 * Attempt to create a new menu item from import data
@@ -1039,9 +1129,9 @@
1039 1129 * @param string $url URL to fetch attachment from
1040 1130 *
1041 1131 * @return int|WP_Error Post ID on success, WP_Error otherwise
1042 1132 */
1043 - public function process_attachment( $post, $url, $sizes = [] ) {
1133 + public function process_attachment( $post, $url, $sizes = [], $original_post_id = null ) {
1044 1134 if ( ! $this->fetch_attachments ) {
1045 1135 return new WP_Error( 'attachment_processing_error', esc_html__( 'Fetching attachments is not enabled', 'elementor' ) );
1046 1136 }
1047 1137 if ( ! function_exists( 'wp_crop_image' ) ) {
@@ -1052,10 +1142,40 @@
1052 1142 $url = rtrim( $this->base_url, '/' ) . $url;
1053 1143 }
1054 1144
1055 1145 if($saved_image = $this->get_saved_image($url)){
1056 - $this->url_remap[ $url ] = wp_get_attachment_url( $saved_image );
1057 - $this->url_remap[ $this->remove_extension($url) ] = $this->remove_extension(wp_get_attachment_url( $saved_image ));
1146 + // $this->url_remap[ $url ] = wp_get_attachment_url( $saved_image );
1147 + // $this->url_remap[ $this->remove_extension($url) ] = $this->remove_extension(wp_get_attachment_url( $saved_image ));
1148 + $upload_url = set_url_scheme( wp_get_attachment_url( $saved_image ) );
1149 + $this->set_url_map($url, $upload_url, true);
1150 +
1151 + // AI image replacement: the imported content still carries the ORIGINAL
1152 + // demo URL, so it must be remapped onto the replacement image here too.
1153 + // The fresh-upload branch below does this, but when the replacement
1154 + // image was already imported (a re-import, or the same stock photo
1155 + // reused) we land here instead — without the map the demo URL survives
1156 + // finalize and the pack's original images are silently kept.
1157 + if(!empty($post['original_attachment_url'])){
1158 + $this->set_url_map($post['original_attachment_url'], $upload_url, true);
1159 +
1160 + $original_hash = sha1( $post['original_attachment_url'] );
1161 +
1162 + // A stale hash from a previous import can still point the demo URL at
1163 + // the old, unreplaced attachment — re-point it at the replacement.
1164 + if( (int) $this->get_saved_image($post['original_attachment_url']) !== (int) $saved_image ){
1165 + $hash_meta_id = add_post_meta( $saved_image, '_elementor_source_image_hash', $original_hash );
1166 + add_post_meta( $saved_image, '_templately_image_hash_meta_id', $hash_meta_id );
1167 + }
1168 +
1169 + self::$_replace_image_ids[ $original_hash ] = (int) $saved_image;
1170 + }
1171 +
1172 + $full_size_path = get_attached_file($saved_image);
1173 + $metadata = wp_get_attachment_metadata($saved_image);
1174 + $updated_metadata = $this->import_sizes($sizes, $metadata, $full_size_path, $saved_image);
1175 + if ($updated_metadata !== false) {
1176 + wp_update_attachment_metadata( $saved_image, $updated_metadata );
1177 + }
1058 1178 return $saved_image;
1059 1179 }
1060 1180
1061 1181 // Check if the URL is from the wp-includes/images directory
@@ -1068,9 +1188,46 @@
1068 1188
1069 1189 return $updated_url;
1070 1190 }
1071 1191
1072 - $upload = $this->fetch_remote_file( $url, $post );
1192 + $upload_dir = wp_upload_dir( $post['upload_date'] );
1193 + if ( ! ( $upload_dir && false === $upload_dir['error'] ) ) {
1194 + return new WP_Error( 'upload_dir_error', $upload_dir['error'] );
1195 + }
1196 +
1197 + // Move the file to the uploads dir.
1198 + //
1199 + // The attachment url is attacker-reachable — a cloud template can point an
1200 + // image anywhere — so the url-derived name is sanitized, and an extension
1201 + // WordPress does not recognise as an uploadable type is dropped rather than
1202 + // carried into the destination path. fetch_remote_file() re-derives the
1203 + // extension from the response and validates it before writing, so dropping
1204 + // an unusable one here costs nothing.
1205 + $file_name = sanitize_file_name( basename( parse_url( $url, PHP_URL_PATH ) ) );
1206 + if ( pathinfo( $file_name, PATHINFO_EXTENSION ) ) {
1207 + $url_filetype = wp_check_filetype( $file_name );
1208 + if ( empty( $url_filetype['type'] ) ) {
1209 + $file_name = pathinfo( $file_name, PATHINFO_FILENAME );
1210 + }
1211 + }
1212 + $file_name = wp_unique_filename( $upload_dir['path'], $file_name );
1213 + $dest_file = $upload_dir['path'] . "/$file_name";
1214 + $start = microtime(true);
1215 +
1216 + $upload = apply_filters( 'templately_import_copy_attachment', null, $original_post_id, $dest_file, $upload_dir );
1217 + if ( null === $upload ) {
1218 + $upload = $this->fetch_remote_file( $url, $dest_file, $upload_dir );
1219 + }
1220 +
1221 + // Normalize URL scheme to match the current site scheme (fixes HTTP URLs on HTTPS multisites).
1222 + if ( ! is_wp_error( $upload ) && ! empty( $upload['url'] ) ) {
1223 + $upload['url'] = set_url_scheme( $upload['url'] );
1224 + }
1225 +
1226 + $end = microtime(true);
1227 + $duration = $end - $start;
1228 + error_log('Duration: ' . $duration);
1229 +
1073 1230 if ( is_wp_error( $upload ) ) {
1074 1231 return $upload;
1075 1232 }
1076 1233
@@ -1080,13 +1237,19 @@
1080 1237 } else {
1081 1238 return new WP_Error( 'attachment_processing_error', esc_html__( 'Invalid file type', 'elementor' ) );
1082 1239 }
1083 1240
1241 + // $this->url_remap[ $post['guid'] ] = $upload['url']; // r13735, really needed?
1242 + // $this->set_url_map($post['guid'], $upload['url']);
1084 1243 $post['guid'] = $upload['url'];
1085 1244
1086 1245 // As per wp-admin/includes/upload.php.
1087 1246 $post_id = wp_insert_attachment( $post, $upload['file'] );
1088 1247
1248 + if(is_wp_error($post_id)){
1249 + return $post_id;
1250 + }
1251 +
1089 1252 $this->update_post_meta( $post_id );
1090 1253
1091 1254 // Generate attachment metadata
1092 1255 $metadata = wp_generate_attachment_metadata( $post_id, $upload['file'] );
@@ -1093,21 +1256,46 @@
1093 1256
1094 1257 // error_log('Metadata: ' . print_r($metadata, true));
1095 1258
1096 1259 // For gutenberg pages
1097 - $metadata = $this->import_sizes($sizes, $metadata, $upload, $post_id);
1260 + $updated_metadata = $this->import_sizes($sizes, $metadata, $upload['file'], $post_id);
1098 1261
1099 1262 // error_log('Metadata: ' . print_r($metadata, true));
1100 - wp_update_attachment_metadata( $post_id, $metadata );
1263 + if ($updated_metadata !== false) {
1264 + wp_update_attachment_metadata( $post_id, $updated_metadata );
1265 + } else {
1266 + wp_update_attachment_metadata( $post_id, $metadata );
1267 + }
1101 1268
1102 1269 // @todo: add missing image sizes
1270 + if(defined('TEMPLATELY_DEV') && TEMPLATELY_DEV){
1271 + update_post_meta( $post_id, '_templately_original_id', $original_post_id );
1272 + update_post_meta( $post_id, '_templately_original_url', $url );
1103 1273
1274 + if(!empty($post['original_attachment_url'])){
1275 + update_post_meta( $post_id, '_templately_demo_url', $post['original_attachment_url'] );
1276 + }
1277 + }
1278 +
1104 1279 update_post_meta( $post_id, '_elementor_source_image_hash', sha1( $url ) );
1105 1280 self::$_replace_image_ids[ sha1( $url ) ] = $post_id;
1106 1281
1282 + // add a second hash for original demo url
1283 + // if user is replacing image.
1284 + // so we can also match original demo url to new image
1285 + if(!empty($post['original_attachment_url'])){
1286 + $hash_meta_id = add_post_meta( $post_id, '_elementor_source_image_hash', sha1( $post['original_attachment_url'] ) );
1287 + add_post_meta( $post_id, '_templately_image_hash_meta_id', $hash_meta_id );
1288 + self::$_replace_image_ids[ sha1( $post['original_attachment_url'] ) ] = $post_id;
1289 + }
1290 +
1107 1291 // Remap resized image URLs, works by stripping the extension and remapping the URL stub.
1108 1292 if ( preg_match( '!^image/!', $info['type'] ) ) {
1109 - $this->url_remap[ $this->remove_extension($url) ] = $this->remove_extension($upload['url']);
1293 + // $this->url_remap[ $this->remove_extension($url) ] = $this->remove_extension($upload['url']);
1294 + $this->set_url_map($url, $upload['url'], true);
1295 + if(!empty($post['original_attachment_url'])){
1296 + $this->set_url_map($post['original_attachment_url'], $upload['url'], true);
1297 + }
1110 1298 }
1111 1299
1112 1300 return $post_id;
1113 1301 }
@@ -1113,38 +1301,27 @@
1113 1301 }
1114 1302
1115 1303 private function remove_extension($url) {
1116 1304 $parts = pathinfo($url);
1117 - $name = basename($parts['basename'], ".{$parts['extension']}"); // PATHINFO_FILENAME in PHP 5.2
1305 + // pathinfo() omits 'extension' for a dotless name, and 'dirname' for an
1306 + // empty one. Remote attachment urls are not obliged to carry an extension
1307 + // (`.../media/1234`), and the url of an attachment that has since been
1308 + // deleted resolves to ''.
1309 + $dirname = isset( $parts['dirname'] ) ? $parts['dirname'] : '';
1310 + $basename = isset( $parts['basename'] ) ? $parts['basename'] : '';
1311 + $extension = isset( $parts['extension'] ) ? $parts['extension'] : '';
1312 + $name = '' === $extension ? $basename : basename($basename, ".{$extension}"); // PATHINFO_FILENAME in PHP 5.2
1118 1313
1119 - return $parts['dirname'] . '/' . $name;
1314 + return $dirname . '/' . $name;
1120 1315 }
1121 1316
1122 - private function import_size($remote_url, $destination_path) {
1123 - // Include the file for the download_url function
1124 - if(!function_exists('download_url')) {
1125 - require_once ABSPATH . 'wp-admin/includes/file.php';
1317 + public function set_url_map($original_url, $new_url, $remove_extension = false){
1318 + $this->url_remap[ $original_url ] = $new_url;
1319 + if($remove_extension){
1320 + $original_url = $this->remove_extension($original_url);
1321 + $new_url = $this->remove_extension($new_url);
1322 + $this->url_remap[ $original_url ] = $new_url;
1126 1323 }
1127 -
1128 - // Download file to temp dir
1129 - $temp_file = download_url($remote_url);
1130 -
1131 - // Check for download errors
1132 - if(is_wp_error($temp_file)) {
1133 - return $temp_file;
1134 - }
1135 -
1136 - // Move temp file to destination path
1137 - $result = copy($temp_file, $destination_path);
1138 -
1139 - // Remove temp file
1140 - @unlink($temp_file);
1141 -
1142 - if (!$result) {
1143 - return false;
1144 - }
1145 -
1146 - return true;
1147 1324 }
1148 1325
1149 1326 /**
1150 1327 * Attempt to download a remote file attachment
@@ -1153,16 +1330,12 @@
1153 1330 * @param array $post Attachment details
1154 1331 *
1155 1332 * @return array|WP_Error Local file location details on success, WP_Error otherwise
1156 1333 */
1157 - private function fetch_remote_file( $url, $post ) {
1158 - // Extract the file name from the URL.
1159 - $file_name = basename( parse_url( $url, PHP_URL_PATH ) );
1334 + private function fetch_remote_file( $url, $new_file, $uploads ) {
1335 + // Extract the file name from the new_file.
1336 + $file_name = basename( $new_file );
1160 1337
1161 - if ( ! $file_name ) {
1162 - $file_name = md5( $url );
1163 - }
1164 -
1165 1338 // Include the file for the download_url function
1166 1339 if(!function_exists('wp_tempnam')) {
1167 1340 require_once ABSPATH . 'wp-admin/includes/file.php';
1168 1341 }
@@ -1172,17 +1345,24 @@
1172 1345 return new WP_Error( 'import_no_file', esc_html__( 'Could not create temporary file.', 'elementor' ) );
1173 1346 }
1174 1347
1175 1348 // Fetch the remote URL and write it to the placeholder file.
1176 - $remote_response = wp_safe_remote_get( $url, [
1177 - 'timeout' => 300,
1178 - 'stream' => true,
1179 - 'filename' => $tmp_file_name,
1180 - 'headers' => [
1181 - 'Accept-Encoding' => 'identity',
1182 - ]
1183 - ] );
1349 + $attempt = 0;
1350 + $retry_count = $this->attachment_retry_count;
1351 + $remote_response = null;
1352 + do {
1353 + $remote_response = wp_safe_remote_get( $url, [
1354 + 'timeout' => $this->attachment_timeout,
1355 + 'stream' => true,
1356 + 'filename' => $tmp_file_name,
1357 + 'headers' => [
1358 + 'Accept-Encoding' => 'identity',
1359 + ]
1360 + ] );
1361 + $attempt++;
1362 + } while (is_wp_error( $remote_response ) && $attempt < $retry_count);
1184 1363
1364 +
1185 1365 if ( is_wp_error( $remote_response ) ) {
1186 1366 @unlink( $tmp_file_name );
1187 1367
1188 1368 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() ) ) );
@@ -1192,10 +1372,8 @@
1192 1372
1193 1373 // Make sure the fetch was successful.
1194 1374 if ( 200 !== $remote_response_code ) {
1195 1375 @unlink( $tmp_file_name );
1196 - Helper::log($url);
1197 - Helper::log($remote_response);
1198 1376 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 ) ) );
1199 1377 }
1200 1378
1201 1379 $headers = wp_remote_retrieve_headers( $remote_response );
@@ -1229,12 +1407,18 @@
1229 1407
1230 1408 return new WP_Error( 'import_file_error', sprintf( esc_html__( 'Remote file is too large, limit is %s', 'elementor' ), size_format( $max_size ) ) );
1231 1409 }
1232 1410
1233 - // Override file name with Content-Disposition header value.
1411 + // Override file name with Content-Disposition header value. The header is
1412 + // whatever the remote chose to send, so it is sanitized before it is allowed
1413 + // anywhere near a path: sanitize_file_name() strips directory separators and
1414 + // neutralizes the inner extension of a double-extension name.
1234 1415 if ( ! empty( $headers['content-disposition'] ) ) {
1235 1416 $file_name_from_disposition = self::get_filename_from_disposition( (array) $headers['content-disposition'] );
1236 1417 if ( $file_name_from_disposition ) {
1418 + $file_name_from_disposition = sanitize_file_name( $file_name_from_disposition );
1419 + }
1420 + if ( $file_name_from_disposition ) {
1237 1421 $file_name = $file_name_from_disposition;
1238 1422 }
1239 1423 }
1240 1424
@@ -1258,19 +1442,29 @@
1258 1442 $file_name = $proper_filename;
1259 1443 }
1260 1444
1261 1445 if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
1446 + @unlink( $tmp_file_name );
1447 +
1262 1448 return new WP_Error( 'import_file_error', esc_html__( 'Sorry, this file type is not permitted for security reasons.', 'elementor' ) );
1263 1449 }
1264 1450
1265 - $uploads = wp_upload_dir( $post['upload_date'] );
1266 - if ( ! ( $uploads && false === $uploads['error'] ) ) {
1267 - return new WP_Error( 'upload_dir_error', $uploads['error'] );
1451 + // The type check above ran against $file_name (Content-Disposition), but the
1452 + // write lands on $new_file, whose name came from the request url path. When
1453 + // those two extensions disagree the check guarantees nothing about what is
1454 + // actually written — an image/gif verdict on `ok.gif` would let the bytes go
1455 + // to a `.php` destination. Re-derive the destination from the validated name
1456 + // so the two can never diverge. (CVE-2026-18438)
1457 + if ( $ext && strtolower( pathinfo( $new_file, PATHINFO_EXTENSION ) ) !== strtolower( $ext ) ) {
1458 + $file_name = wp_unique_filename( $uploads['path'], $file_name );
1459 + $new_file = $uploads['path'] . "/$file_name";
1268 1460 }
1269 1461
1270 - // Move the file to the uploads dir.
1271 - $file_name = wp_unique_filename( $uploads['path'], $file_name );
1272 - $new_file = $uploads['path'] . "/$file_name";
1462 + // Whatever the branch above decided, the reported name must describe the file
1463 + // that is actually on disk — the remapped url in the imported content is built
1464 + // from it.
1465 + $file_name = wp_basename( $new_file );
1466 +
1273 1467 $move_new_file = copy( $tmp_file_name, $new_file );
1274 1468
1275 1469 if ( ! $move_new_file ) {
1276 1470 @unlink( $tmp_file_name );
@@ -1290,13 +1484,12 @@
1290 1484 'error' => false,
1291 1485 ];
1292 1486
1293 1487 // Keep track of the old and new urls so we can substitute them later.
1294 - $this->url_remap[ $url ] = $upload['url'];
1295 - $this->url_remap[ $post['guid'] ] = $upload['url']; // r13735, really needed?
1488 + $this->set_url_map($url, $upload['url']);
1296 1489 // Keep track of the destination if the remote url is redirected somewhere else.
1297 1490 if ( isset( $headers['x-final-location'] ) && $headers['x-final-location'] !== $url ) {
1298 - $this->url_remap[ $headers['x-final-location'] ] = $upload['url'];
1491 + $this->set_url_map($headers['x-final-location'], $upload['url']);
1299 1492 }
1300 1493
1301 1494 return $upload;
1302 1495 }
@@ -1310,9 +1503,9 @@
1310 1503 * @access private
1311 1504 *
1312 1505 * @param string $url The image URL.
1313 1506 *
1314 - * @return false|array New image ID or false.
1507 + * @return false|int New image ID or false.
1315 1508 */
1316 1509 private function get_saved_image( $url ) {
1317 1510 global $wpdb;
1318 1511
@@ -1321,13 +1514,26 @@
1321 1514 if ( isset( self::$_replace_image_ids[ $hash ] ) ) {
1322 1515 return self::$_replace_image_ids[ $hash ];
1323 1516 }
1324 1517
1518 + // Newest row wins. A URL can be claimed by more than one attachment: an
1519 + // AI/customizer image replacement registers the ORIGINAL demo URL against
1520 + // the REPLACEMENT attachment (see process_attachment), while a previous
1521 + // plain import of the same pack already claimed that URL for the original
1522 + // image — and that older row is never cleaned up (clear_old_el_cache only
1523 + // tracks replacement hashes). Without the ordering the stale original wins
1524 + // and the replacement is silently ignored on every re-import.
1525 + //
1526 + // The static cache above short-circuits this, but the import is split
1527 + // across many requests (each SSE `continue` is a fresh PHP process), so
1528 + // the DB is the real source of truth for anything set in an earlier step.
1325 1529 $post_id = $wpdb->get_var(
1326 1530 $wpdb->prepare(
1327 1531 'SELECT `post_id` FROM `' . $wpdb->postmeta . '`
1328 1532 WHERE `meta_key` = \'_elementor_source_image_hash\'
1329 1533 AND `meta_value` = %s
1534 + ORDER BY `meta_id` DESC
1535 + LIMIT 1
1330 1536 ;',
1331 1537 $hash
1332 1538 )
1333 1539 );
@@ -1333,9 +1539,9 @@
1333 1539 );
1334 1540
1335 1541 if ( $post_id ) {
1336 1542 self::$_replace_image_ids[ $hash ] = $post_id;
1337 - return $post_id;
1543 + return (int) $post_id;
1338 1544 }
1339 1545
1340 1546 return false;
1341 1547 }
@@ -1340,69 +1546,97 @@
1340 1546 return false;
1341 1547 }
1342 1548
1343 1549
1344 - public function extract_sizes($sizes) {
1345 - return array_reduce($sizes, function($carry, $url) {
1346 - if (preg_match('/-((\d+)x(\d+))\./', $url, $matches)) {
1347 - $carry[$matches[1]] = $url;
1348 - }
1349 - return $carry;
1350 - }, []);
1351 - }
1550 + public function import_sizes($sizes, $metadata, $full_size_file, $post_id) {
1551 + $metadata_modified = false;
1352 1552
1353 - public function size_exists_in_metadata($size_dimension, $metadata) {
1354 - foreach ($metadata['sizes'] as $size => $size_info) {
1355 - if (strpos($size_info['file'], $size_dimension) !== false) {
1356 - return true;
1357 - }
1358 - }
1359 - return false;
1360 - }
1553 + if (!empty($sizes)) {
1554 + do_action('templately_import.finalize_gutenberg_attachment', $post_id);
1361 1555
1362 - public function create_unique_destination_file($upload_file, $size_dimension) {
1363 - $destination_file = preg_replace('/(\.[^.]+)$/', '-' . $size_dimension . '$1', $upload_file);
1364 - $destination_dir = pathinfo($destination_file, PATHINFO_DIRNAME);
1365 - $destination_filename = pathinfo($destination_file, PATHINFO_BASENAME);
1366 - $unique_filename = wp_unique_filename($destination_dir, $destination_filename);
1367 - return $destination_dir . '/' . $unique_filename;
1368 - }
1556 + foreach ($sizes as $size_name => $size) {
1557 + $size_dimension = $size['width'] . 'x' . $size['height'];
1558 + $size_name = !is_string($size_name) ? $size_dimension : $size_name;
1559 + $unique_destination_file = $this->create_unique_destination_file($full_size_file, $size);
1560 + // check non cropped sizes. with dynamic height
1561 + if (!$this->size_exists_in_metadata(basename($unique_destination_file), $metadata)) {
1562 + if ($unique_destination_file) {
1563 + $missing_size = $this->generate_missing_size_from_full($full_size_file, $unique_destination_file, $size);
1564 + if ($missing_size && !is_wp_error($missing_size)) {
1565 + unset($missing_size['path']);
1566 + $metadata['sizes'][$size_name] = $missing_size;
1567 + $metadata_modified = true;
1568 + do_action('templately_import.finalize_gutenberg_attachment', $post_id, $size_dimension);
1569 + }
1570 + }
1571 + }
1572 + }
1573 + }
1369 1574
1370 - public function create_size_array($destination_file, $size_dimension) {
1371 - list($width, $height) = explode('x', $size_dimension);
1372 - return array(
1373 - 'file' => basename($destination_file),
1374 - 'width' => $width,
1375 - 'height' => $height,
1376 - 'mime-type' => wp_check_filetype($destination_file)['type'],
1377 - 'filesize' => filesize($destination_file),
1378 - 'resized' => false,
1379 - );
1380 - }
1575 + // Only return metadata if new sizes were actually added
1576 + return $metadata_modified ? $metadata : false;
1577 + }
1381 1578
1382 - public function import_sizes($sizes, $metadata, $upload, $post_id) {
1383 - if (!empty($sizes)) {
1384 - do_action( 'templately_import.finalize_gutenberg_attachment', $post_id );
1579 + public function generate_missing_size_from_full($full_size_file, $destination_file, $size) {
1580 + // Generate the missing size from the full-size image
1581 + $editor = wp_get_image_editor($full_size_file);
1582 + if (is_wp_error($editor)) {
1583 + return $editor;
1584 + }
1385 1585
1386 - $size_dimensions = $this->extract_sizes($sizes);
1387 - $size_dimensions = array_filter($size_dimensions);
1586 + $resized = $editor->resize($size['width'], $size['height'], true);
1587 + if (is_wp_error($resized)) {
1588 + return $resized;
1589 + }
1388 1590
1389 - foreach ($size_dimensions as $size_dimension => $__url) {
1390 - if (!$this->size_exists_in_metadata($size_dimension, $metadata)) {
1391 - $unique_destination_file = $this->create_unique_destination_file($upload['file'], $size_dimension);
1392 - $missing_size = $this->import_size($__url, $unique_destination_file);
1591 + $saved = $editor->save($destination_file);
1592 + if (is_wp_error($saved)) {
1593 + return $saved;
1594 + }
1393 1595
1394 - if($missing_size && !is_wp_error($missing_size)) {
1395 - $metadata['sizes'][$size_dimension] = $this->create_size_array($unique_destination_file, $size_dimension);
1396 - do_action( 'templately_import.finalize_gutenberg_attachment', $post_id, $size_dimension );
1397 - }
1398 - }
1399 - }
1596 + return $saved;
1597 + }
1400 1598
1401 - }
1402 - return $metadata;
1403 - }
1599 + public function size_exists_in_metadata($size_file, $metadata) {
1600 + if (isset($metadata['sizes']) && is_array($metadata['sizes'])) {
1601 + foreach ($metadata['sizes'] as $size_info) {
1602 + if ($size_info['file'] == $size_file) {
1603 + return true;
1604 + }
1605 + }
1606 + }
1607 + return false;
1608 + }
1404 1609
1610 + public function create_unique_destination_file($full_size_file, $size) {
1611 + $pathinfo = pathinfo($full_size_file);
1612 + $directory = $pathinfo['dirname'];
1613 + $filename = $pathinfo['filename'];
1614 + $extension = $pathinfo['extension'];
1615 +
1616 + $destination_file = $directory . '/' . $filename . '-' . $size['width'] . 'x' . $size['height'] . '.' . $extension;
1617 +
1618 + // Skip if the file already exists
1619 + if (file_exists($destination_file)) {
1620 + // return false;
1621 + }
1622 +
1623 + return $destination_file;
1624 + }
1625 +
1626 + public function create_size_array($destination_file, $size_dimension) {
1627 + list($width, $height) = explode('x', $size_dimension);
1628 + return array(
1629 + 'file' => basename($destination_file),
1630 + 'width' => $width,
1631 + 'height' => $height,
1632 + 'mime-type' => wp_check_filetype($destination_file)['type'],
1633 + 'filesize' => filesize($destination_file),
1634 + 'resized' => false,
1635 + );
1636 + }
1637 +
1638 +
1405 1639 /**
1406 1640 * Attempt to associate posts and menu items with previously missing parents
1407 1641 *
1408 1642 * An imported post's parent may not have been imported when it was first created
@@ -1520,9 +1754,9 @@
1520 1754 */
1521 1755 private function is_valid_meta_key( $key ) {
1522 1756 // Skip attachment metadata since we'll regenerate it from scratch.
1523 1757 // Skip _edit_lock as not relevant for import
1524 - if ( in_array( $key, [ '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock' ] ) ) {
1758 + if ( in_array( $key, [ '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock', '_elementor_source_image_hash', 'sm_cloud' ] ) ) {
1525 1759 return false;
1526 1760 }
1527 1761
1528 1762 return $key;
@@ -1569,9 +1803,9 @@
1569 1803 * @param $post_id
1570 1804 *
1571 1805 * @return void
1572 1806 */
1573 - private function update_post_meta( $post_id ) {
1807 + public function update_post_meta( $post_id ) {
1574 1808 foreach ( $this->posts_meta as $meta_key => $meta_value ) {
1575 1809 update_post_meta( $post_id, $meta_key, $meta_value );
1576 1810 }
1577 1811 }
@@ -1582,8 +1816,23 @@
1582 1816 return $this->output;
1583 1817 }
1584 1818
1585 1819 /**
1820 + * Register Templately with Jetpack Sync's known importers.
1821 + *
1822 + * This filter callback adds Templately to Jetpack's list of known importers,
1823 + * ensuring that 'templately' is sent to WordPress.com instead of the full class name.
1824 + * This provides cleaner analytics data and better tracking.
1825 + *
1826 + * @param array $known_importers Array of known importers with class names as keys and friendly names as values.
1827 + * @return array Modified array of known importers.
1828 + */
1829 + public function register_jetpack_importer( $known_importers ) {
1830 + $known_importers[ __CLASS__ ] = 'templately';
1831 + return $known_importers;
1832 + }
1833 +
1834 + /**
1586 1835 * @param $file
1587 1836 * @param array $args
1588 1837 */
1589 1838 public function __construct( $file, array $args = [] ) {
@@ -1588,15 +1837,36 @@
1588 1837 */
1589 1838 public function __construct( $file, array $args = [] ) {
1590 1839 parent::__construct();
1591 1840
1592 - $this->requested_file_path = $file;
1593 - $this->args = $args;
1841 + $this->args = $args;
1842 + $this->session_id = $args['session_id'];
1594 1843
1844 + if ( ! empty( $args['json'] ) ) {
1845 + $this->json = $args['json'];
1846 + }
1847 +
1848 + if ( ! empty( $args['origin'] ) ) {
1849 + $this->origin = $args['origin'];
1850 + }
1851 +
1852 + if ( ! empty( $file ) ) {
1853 + $this->requested_file_path = $file;
1854 + $this->import_data_key = 'wp_importer_attributes_' . md5($this->requested_file_path);
1855 + }
1856 +
1595 1857 if ( ! empty( $this->args['fetch_attachments'] ) ) {
1596 1858 $this->fetch_attachments = true;
1597 1859 }
1598 1860
1861 + if ( isset( $this->args['attachment_timeout'] ) && is_numeric( $this->args['attachment_timeout'] ) ) {
1862 + $this->attachment_timeout = max( 1, (int) $this->args['attachment_timeout'] );
1863 + }
1864 +
1865 + if ( isset( $this->args['attachment_retries'] ) && is_numeric( $this->args['attachment_retries'] ) ) {
1866 + $this->attachment_retry_count = max( 1, (int) $this->args['attachment_retries'] + 1 );
1867 + }
1868 +
1599 1869 if ( isset( $this->args['posts'] ) && is_array( $this->args['posts'] ) ) {
1600 1870 $this->processed_posts = $this->args['posts'];
1601 1871 }
1602 1872
@@ -1614,6 +1884,9 @@
1614 1884
1615 1885 if ( ! empty( $this->args['terms_meta'] ) ) {
1616 1886 $this->terms_meta = $this->args['terms_meta'];
1617 1887 }
1888 +
1889 + // Register with Jetpack Sync for better analytics tracking.
1890 + add_filter( 'jetpack_sync_known_importers', [ $this, 'register_jetpack_importer' ] );
1618 1891 }
1619 1892 }