PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.1.8
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.1.8
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 +187 -385 3.7.33.1.8 View file →
@@ -2,9 +2,8 @@
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;
7 6 use Templately\Core\Importer\Utils\Utils;
8 7 use Templately\Utils\Helper;
9 8 use WP_Error;
10 9 use WP_Importer;
@@ -48,11 +47,8 @@
48 47 }
49 48 }
50 49
51 50 class WPImport extends WP_Importer {
52 - use LogHelper;
53 - use Loop;
54 -
55 51 const DEFAULT_BUMP_REQUEST_TIMEOUT = 60;
56 52 const DEFAULT_ALLOW_CREATE_USERS = true;
57 53 const DEFAULT_IMPORT_ATTACHMENT_SIZE_LIMIT = 0; // 0 = unlimited.
58 54
@@ -59,12 +55,8 @@
59 55 /**
60 56 * @var string
61 57 */
62 58 private $requested_file_path;
63 - /**
64 - * @var string
65 - */
66 - private $import_data_key;
67 59
68 60 /**
69 61 * @var array
70 62 */
@@ -87,18 +79,8 @@
87 79 * WXR attachment ID
88 80 */
89 81 private $id;
90 82
91 - /**
92 - * @var mixed
93 - */
94 - private $json;
95 -
96 - /**
97 - * @var string
98 - */
99 - private $session_id;
100 -
101 83 // Information to import from WXR file.
102 84 private $version;
103 85 private $authors = [];
104 86 public $posts = [];
@@ -119,10 +101,8 @@
119 101 private $menu_item_orphans = [];
120 102 private $mapped_terms_slug = [];
121 103
122 104 private $fetch_attachments = false;
123 - private $attachment_timeout = 300;
124 - private $attachment_retry_count = 3;
125 105 private $featured_images = [];
126 106
127 107 /**
128 108 * @var array[] [meta_key => meta_value] Meta value that should be set for every imported post.
@@ -135,25 +115,8 @@
135 115 private $terms_meta = [];
136 116
137 117 public static $_replace_image_ids = [];
138 118
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 -
156 119 /**
157 120 * Parses filename from a Content-Disposition header value.
158 121 *
159 122 * As per RFC6266:
@@ -329,10 +292,9 @@
329 292
330 293 wp_defer_term_counting( true );
331 294 wp_defer_comment_counting( true );
332 295
333 - do_action( 'import_start' );
334 - do_action( 'templately_import_start', $this );
296 + do_action( 'import_start', $this );
335 297
336 298 return true;
337 299 }
338 300
@@ -386,8 +348,30 @@
386 348 }
387 349 }
388 350 }
389 351
352 + private function retrieve_attributes($attributes){
353 + $params = $this->origin->get_request_params();
354 + $attr_values = $params["imported_data"][$this->import_data_key] ?? [];
355 + foreach ($attributes as $attribute) {
356 + if(isset($attr_values[$attribute])){
357 + $this->$attribute = $attr_values[$attribute];
358 + }
359 + }
360 + }
361 +
362 + private function backup_attributes($attributes){
363 + $params = $this->origin->get_request_params();
364 + $attr_values = $params["imported_data"][$this->import_data_key] ?? [];
365 +
366 + foreach ($attributes as $attribute) {
367 + if(isset($this->$attribute)){
368 + $attr_values[$attribute] = $this->$attribute;
369 + }
370 + }
371 + $this->origin->update_progress( null, [$this->import_data_key => $attr_values]);
372 + }
373 +
390 374 /**
391 375 * Map old author logins to local user IDs based on decisions made
392 376 * in import options form. Can map to an existing user, create a new user
393 377 * or falls back to the current user in case of error with either of the previous
@@ -396,11 +380,17 @@
396 380 if ( ! isset( $this->args['imported_authors'] ) ) {
397 381 return;
398 382 }
399 383
384 + $attributes = [
385 + 'processed_authors',
386 + 'author_mapping',
387 + 'output',
388 + ];
400 389
401 - $processed_templates = $this->get_loop_result([], $this->import_data_key);
390 + $processed_templates = $this->origin->get_progress([], $this->import_data_key);
402 391 if (!empty($processed_templates)) {
392 + $this->retrieve_attributes($attributes);
403 393 return;
404 394 }
405 395
406 396 $create_users = apply_filters( 'import_allow_create_users', self::DEFAULT_ALLOW_CREATE_USERS );
@@ -459,9 +449,10 @@
459 449 $this->author_mapping[ $santized_old_login ] = (int) get_current_user_id();
460 450 }
461 451 }
462 452
463 - $this->set_loop_result( true, $this->import_data_key );
453 + $this->backup_attributes($attributes);
454 + $this->origin->update_progress( true, null, $this->import_data_key );
464 455 }
465 456
466 457 /**
467 458 * Create new terms based on import information
@@ -470,16 +461,23 @@
470 461 *
471 462 * @return array|array[] the ids of succeed/failed imported terms.
472 463 */
473 464 private function process_terms(): array {
474 - $result = [
465 + $params = $this->origin->get_request_params();
466 + $result = $params["imported_data"]["wp_import_terms_" . $this->import_data_key] ?? [
475 467 'succeed' => [],
476 468 'failed' => [],
477 469 ];
478 470
479 - $processed_templates = $this->get_loop_result([], "wp_import_terms_" . $this->import_data_key);
471 + $attributes = [
472 + 'terms',
473 + 'output',
474 + 'mapped_terms_slug',
475 + 'processed_terms',
476 + ];
477 + $processed_templates = $this->origin->get_progress([], $this->import_data_key);
480 478 if (!empty($processed_templates)) {
481 - $result = $this->get_loop_result([], "wp_import_terms_" . $this->import_data_key);
479 + $this->retrieve_attributes($attributes);
482 480 return $result;
483 481 }
484 482
485 483 $this->terms = apply_filters( 'wp_import_terms', $this->terms );
@@ -562,10 +560,11 @@
562 560 }
563 561
564 562 unset( $this->terms );
565 563
564 + $this->backup_attributes($attributes);
566 565 // 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);
566 + $this->origin->update_progress( true, [ "wp_import_terms_" . $this->import_data_key => $result ]);
568 567 return $result;
569 568 }
570 569
571 570 /**
@@ -635,59 +634,65 @@
635 634 *
636 635 * @return array the ids of succeed/failed imported posts.
637 636 */
638 637 private function process_posts(): array {
639 - $backup_key = "wp_import_post_" . $this->import_data_key;
638 + $backup_key = "wp_import_" . $this->import_data_key;
639 + $params = $this->origin->get_request_params();
640 + $result = $params["imported_data"][$backup_key] ?? [
641 + 'succeed' => [],
642 + 'failed' => [],
643 + ];
640 644
645 + $attributes = [
646 + 'posts',
647 + 'output',
648 + 'menu_item_orphans',
649 + 'processed_menu_items',
650 + 'post_orphans',
651 + 'processed_posts',
652 + 'featured_images',
653 + ];
654 + $this->retrieve_attributes($attributes);
655 +
641 656 $this->posts = apply_filters( 'wp_import_posts', $this->posts );
642 657
643 - $results = $this->loop( $this->posts, function($key, $post, $result ) {
658 + $processed_templates = $this->origin->get_progress([], $this->import_data_key);
644 659
645 - $result = !empty($result) ? $result : [
646 - 'succeed' => [],
647 - 'failed' => [],
648 - ];
660 + foreach ( $this->posts as $key => $post ) {
661 + if (in_array($key, $processed_templates)) {
662 + continue;
663 + }
649 664
650 - $original_post_id = $post['post_id'];
651 - $post = apply_filters( 'wp_import_post_data_raw', $post, $this );
665 + // Add the template to the processed templates and update the session data
666 + $processed[] = $key;
667 + $this->origin->update_progress( $processed, null, $this->import_data_key);
652 668
653 - if(empty($post)){
654 - return $result;
655 - }
669 + $post = apply_filters( 'wp_import_post_data_raw', $post );
656 670
657 - if(!is_array($post) && is_numeric($post)){
658 - $result['succeed'][ $original_post_id ] = $post;
659 - return $result;
660 - }
661 -
662 671 if ( ! post_type_exists( $post['post_type'] ) ) {
663 672 /* translators: 1: Post title, 2: Post type. */
664 673 $this->output['errors'][] = sprintf( esc_html__( 'Failed to import %1$s: Invalid post type %2$s', 'elementor' ), $post['post_title'], $post['post_type'] );
665 674 do_action( 'wp_import_post_exists', $post );
666 - return $result;
675 + continue;
667 676 }
668 677
669 678 if ( isset( $this->processed_posts[ $post['post_id'] ] ) && ! empty( $post['post_id'] ) ) {
670 - return $result;
679 + continue;
671 680 }
672 681
673 682 if ( 'auto-draft' === $post['status'] ) {
674 - return $result;
683 + continue;
675 684 }
676 685
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 -
681 686 if ( 'nav_menu_item' === $post['post_type'] ) {
682 687 $result['succeed'] += $this->process_menu_item( $post );
683 - return $result;
688 + continue;
684 689 }
685 690
686 691 if ( 'wp_navigation' === $post['post_type'] ) {
687 692 $processed = $this->process_navigation( $post );
688 693 if ( ! $processed ) {
689 - return $result;
694 + continue;
690 695 }
691 696 }
692 697
693 698 $post_type_object = get_post_type_object( $post['post_type'] );
@@ -727,12 +732,9 @@
727 732 'post_type' => $post['post_type'],
728 733 'post_password' => $post['post_password'],
729 734 ];
730 735
731 - if(isset($post['original_attachment_url'])){
732 - $postdata['original_attachment_url'] = $post['original_attachment_url'];
733 - }
734 -
736 + $original_post_id = $post['post_id'];
735 737 $postdata = apply_filters( 'wp_import_post_data_processed', $postdata, $post );
736 738
737 739 $postdata = wp_slash( $postdata );
738 740
@@ -737,9 +739,9 @@
737 739 $postdata = wp_slash( $postdata );
738 740
739 741 if ( 'attachment' === $postdata['post_type'] ) {
740 742 $remote_url = ! empty( $post['attachment_url'] ) ? $post['attachment_url'] : $post['guid'];
741 - $attachment_sizes = [];
743 +
742 744 // try to use _wp_attached file for upload folder placement to ensure the same location as the export site
743 745 // e.g. location is 2003/05/image.jpg but the attachment post_date is 2010/09, see media_handle_upload()
744 746 $postdata['upload_date'] = $post['post_date'];
745 747 if ( isset( $post['postmeta'] ) ) {
@@ -747,19 +749,14 @@
747 749 if ( '_wp_attached_file' === $meta['key'] ) {
748 750 if ( preg_match( '%^[0-9]{4}/[0-9]{2}%', $meta['value'], $matches ) ) {
749 751 $postdata['upload_date'] = $matches[0];
750 752 }
751 - // break;
753 + break;
752 754 }
753 - else if ( '_wp_attachment_metadata' === $meta['key'] ) {
754 - $attachment_metadata = maybe_unserialize( $meta['value'] );
755 - $attachment_sizes = $attachment_metadata['sizes'] ?? [];
756 - // break;
757 - }
758 755 }
759 756 }
760 757
761 - $post_id = $this->process_attachment( $postdata, $remote_url, $attachment_sizes, $original_post_id );
758 + $post_id = $this->process_attachment( $postdata, $remote_url );
762 759 $comment_post_id = $post_id;
763 760 } else {
764 761 $post_id = wp_insert_post( $postdata, true );
765 762
@@ -780,13 +777,22 @@
780 777 $result['failed'][] = $original_post_id;
781 778
782 779 $this->output['errors'][] = $error;
783 780
784 - if ( 'attachment' === $postdata['post_type'] ) {
785 - do_action( 'templately_import.process_post', $post, $result, $this );
781 + // Add the template to the processed templates and update the session data
782 + $this->backup_attributes($attributes);
783 + $this->origin->update_progress( null, [ $backup_key => $result ], $this->import_data_key);
784 +
785 + // If it's not the last item, send the SSE message and exit
786 + if( end($this->posts) !== $post) {
787 + $this->origin->sse_message( [
788 + 'type' => 'continue',
789 + 'action' => 'continue',
790 + 'results' => __METHOD__ . '::' . __LINE__,
791 + ] );
792 + exit;
786 793 }
787 -
788 - return $result;
794 + continue;
789 795 }
790 796
791 797 $result['succeed'][ $original_post_id ] = $post_id;
792 798
@@ -941,14 +947,26 @@
941 947 }
942 948
943 949 do_action( 'templately_import.process_post', $post, $result, $this );
944 950
945 - return $result;
946 - }, $backup_key); //, true
951 + // Add the template to the processed templates and update the session data
952 + $this->backup_attributes($attributes);
953 + $this->origin->update_progress( null, [ $backup_key => $result ], $this->import_data_key);
947 954
955 + // If it's not the last item, send the SSE message and exit
956 + if( end($this->posts) !== $post) {
957 + $this->origin->sse_message( [
958 + 'type' => 'continue',
959 + 'action' => 'continue',
960 + 'results' => __METHOD__ . '::' . __LINE__,
961 + ] );
962 + exit;
963 + }
964 + }
965 +
948 966 unset( $this->posts );
949 967
950 - return $results;
968 + return $result;
951 969 }
952 970
953 971 /**
954 972 * Attempt to create a new menu item from import data
@@ -1129,9 +1147,9 @@
1129 1147 * @param string $url URL to fetch attachment from
1130 1148 *
1131 1149 * @return int|WP_Error Post ID on success, WP_Error otherwise
1132 1150 */
1133 - public function process_attachment( $post, $url, $sizes = [], $original_post_id = null ) {
1151 + public function process_attachment( $post, $url, $sizes = [] ) {
1134 1152 if ( ! $this->fetch_attachments ) {
1135 1153 return new WP_Error( 'attachment_processing_error', esc_html__( 'Fetching attachments is not enabled', 'elementor' ) );
1136 1154 }
1137 1155 if ( ! function_exists( 'wp_crop_image' ) ) {
@@ -1142,40 +1160,10 @@
1142 1160 $url = rtrim( $this->base_url, '/' ) . $url;
1143 1161 }
1144 1162
1145 1163 if($saved_image = $this->get_saved_image($url)){
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 - }
1164 + $this->url_remap[ $url ] = wp_get_attachment_url( $saved_image );
1165 + $this->url_remap[ $this->remove_extension($url) ] = $this->remove_extension(wp_get_attachment_url( $saved_image ));
1178 1166 return $saved_image;
1179 1167 }
1180 1168
1181 1169 // Check if the URL is from the wp-includes/images directory
@@ -1194,40 +1182,13 @@
1194 1182 return new WP_Error( 'upload_dir_error', $upload_dir['error'] );
1195 1183 }
1196 1184
1197 1185 // 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 - }
1186 + $file_name = basename( parse_url( $url, PHP_URL_PATH ) );
1212 1187 $file_name = wp_unique_filename( $upload_dir['path'], $file_name );
1213 1188 $dest_file = $upload_dir['path'] . "/$file_name";
1214 - $start = microtime(true);
1189 + $upload = $this->fetch_remote_file( $url, $dest_file, $upload_dir );
1215 1190
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 -
1230 1191 if ( is_wp_error( $upload ) ) {
1231 1192 return $upload;
1232 1193 }
1233 1194
@@ -1237,19 +1198,14 @@
1237 1198 } else {
1238 1199 return new WP_Error( 'attachment_processing_error', esc_html__( 'Invalid file type', 'elementor' ) );
1239 1200 }
1240 1201
1241 - // $this->url_remap[ $post['guid'] ] = $upload['url']; // r13735, really needed?
1242 - // $this->set_url_map($post['guid'], $upload['url']);
1202 + $this->url_remap[ $post['guid'] ] = $upload['url']; // r13735, really needed?
1243 1203 $post['guid'] = $upload['url'];
1244 1204
1245 1205 // As per wp-admin/includes/upload.php.
1246 1206 $post_id = wp_insert_attachment( $post, $upload['file'] );
1247 1207
1248 - if(is_wp_error($post_id)){
1249 - return $post_id;
1250 - }
1251 -
1252 1208 $this->update_post_meta( $post_id );
1253 1209
1254 1210 // Generate attachment metadata
1255 1211 $metadata = wp_generate_attachment_metadata( $post_id, $upload['file'] );
@@ -1256,46 +1212,21 @@
1256 1212
1257 1213 // error_log('Metadata: ' . print_r($metadata, true));
1258 1214
1259 1215 // For gutenberg pages
1260 - $updated_metadata = $this->import_sizes($sizes, $metadata, $upload['file'], $post_id);
1216 + $metadata = $this->import_sizes($sizes, $metadata, $upload, $upload_dir, $post_id);
1261 1217
1262 1218 // error_log('Metadata: ' . print_r($metadata, true));
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 - }
1219 + wp_update_attachment_metadata( $post_id, $metadata );
1268 1220
1269 1221 // @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 );
1273 1222
1274 - if(!empty($post['original_attachment_url'])){
1275 - update_post_meta( $post_id, '_templately_demo_url', $post['original_attachment_url'] );
1276 - }
1277 - }
1278 -
1279 1223 update_post_meta( $post_id, '_elementor_source_image_hash', sha1( $url ) );
1280 1224 self::$_replace_image_ids[ sha1( $url ) ] = $post_id;
1281 1225
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 -
1291 1226 // Remap resized image URLs, works by stripping the extension and remapping the URL stub.
1292 1227 if ( preg_match( '!^image/!', $info['type'] ) ) {
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 - }
1228 + $this->url_remap[ $this->remove_extension($url) ] = $this->remove_extension($upload['url']);
1298 1229 }
1299 1230
1300 1231 return $post_id;
1301 1232 }
@@ -1301,29 +1232,13 @@
1301 1232 }
1302 1233
1303 1234 private function remove_extension($url) {
1304 1235 $parts = pathinfo($url);
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
1236 + $name = basename($parts['basename'], ".{$parts['extension']}"); // PATHINFO_FILENAME in PHP 5.2
1313 1237
1314 - return $dirname . '/' . $name;
1238 + return $parts['dirname'] . '/' . $name;
1315 1239 }
1316 1240
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;
1323 - }
1324 - }
1325 -
1326 1241 /**
1327 1242 * Attempt to download a remote file attachment
1328 1243 *
1329 1244 * @param string $url URL of item to fetch
@@ -1346,13 +1261,13 @@
1346 1261 }
1347 1262
1348 1263 // Fetch the remote URL and write it to the placeholder file.
1349 1264 $attempt = 0;
1350 - $retry_count = $this->attachment_retry_count;
1265 + $retry_count = 3;
1351 1266 $remote_response = null;
1352 1267 do {
1353 1268 $remote_response = wp_safe_remote_get( $url, [
1354 - 'timeout' => $this->attachment_timeout,
1269 + 'timeout' => 300,
1355 1270 'stream' => true,
1356 1271 'filename' => $tmp_file_name,
1357 1272 'headers' => [
1358 1273 'Accept-Encoding' => 'identity',
@@ -1372,8 +1287,10 @@
1372 1287
1373 1288 // Make sure the fetch was successful.
1374 1289 if ( 200 !== $remote_response_code ) {
1375 1290 @unlink( $tmp_file_name );
1291 + Helper::log($url);
1292 + Helper::log($remote_response);
1376 1293 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 ) ) );
1377 1294 }
1378 1295
1379 1296 $headers = wp_remote_retrieve_headers( $remote_response );
@@ -1407,18 +1324,12 @@
1407 1324
1408 1325 return new WP_Error( 'import_file_error', sprintf( esc_html__( 'Remote file is too large, limit is %s', 'elementor' ), size_format( $max_size ) ) );
1409 1326 }
1410 1327
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.
1328 + // Override file name with Content-Disposition header value.
1415 1329 if ( ! empty( $headers['content-disposition'] ) ) {
1416 1330 $file_name_from_disposition = self::get_filename_from_disposition( (array) $headers['content-disposition'] );
1417 1331 if ( $file_name_from_disposition ) {
1418 - $file_name_from_disposition = sanitize_file_name( $file_name_from_disposition );
1419 - }
1420 - if ( $file_name_from_disposition ) {
1421 1332 $file_name = $file_name_from_disposition;
1422 1333 }
1423 1334 }
1424 1335
@@ -1442,29 +1353,11 @@
1442 1353 $file_name = $proper_filename;
1443 1354 }
1444 1355
1445 1356 if ( ( ! $type || ! $ext ) && ! current_user_can( 'unfiltered_upload' ) ) {
1446 - @unlink( $tmp_file_name );
1447 -
1448 1357 return new WP_Error( 'import_file_error', esc_html__( 'Sorry, this file type is not permitted for security reasons.', 'elementor' ) );
1449 1358 }
1450 1359
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";
1460 - }
1461 -
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 -
1467 1360 $move_new_file = copy( $tmp_file_name, $new_file );
1468 1361
1469 1362 if ( ! $move_new_file ) {
1470 1363 @unlink( $tmp_file_name );
@@ -1484,12 +1377,12 @@
1484 1377 'error' => false,
1485 1378 ];
1486 1379
1487 1380 // Keep track of the old and new urls so we can substitute them later.
1488 - $this->set_url_map($url, $upload['url']);
1381 + $this->url_remap[ $url ] = $upload['url'];
1489 1382 // Keep track of the destination if the remote url is redirected somewhere else.
1490 1383 if ( isset( $headers['x-final-location'] ) && $headers['x-final-location'] !== $url ) {
1491 - $this->set_url_map($headers['x-final-location'], $upload['url']);
1384 + $this->url_remap[ $headers['x-final-location'] ] = $upload['url'];
1492 1385 }
1493 1386
1494 1387 return $upload;
1495 1388 }
@@ -1503,9 +1396,9 @@
1503 1396 * @access private
1504 1397 *
1505 1398 * @param string $url The image URL.
1506 1399 *
1507 - * @return false|int New image ID or false.
1400 + * @return false|array New image ID or false.
1508 1401 */
1509 1402 private function get_saved_image( $url ) {
1510 1403 global $wpdb;
1511 1404
@@ -1514,26 +1407,13 @@
1514 1407 if ( isset( self::$_replace_image_ids[ $hash ] ) ) {
1515 1408 return self::$_replace_image_ids[ $hash ];
1516 1409 }
1517 1410
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.
1529 1411 $post_id = $wpdb->get_var(
1530 1412 $wpdb->prepare(
1531 1413 'SELECT `post_id` FROM `' . $wpdb->postmeta . '`
1532 1414 WHERE `meta_key` = \'_elementor_source_image_hash\'
1533 1415 AND `meta_value` = %s
1534 - ORDER BY `meta_id` DESC
1535 - LIMIT 1
1536 1416 ;',
1537 1417 $hash
1538 1418 )
1539 1419 );
@@ -1539,9 +1419,9 @@
1539 1419 );
1540 1420
1541 1421 if ( $post_id ) {
1542 1422 self::$_replace_image_ids[ $hash ] = $post_id;
1543 - return (int) $post_id;
1423 + return $post_id;
1544 1424 }
1545 1425
1546 1426 return false;
1547 1427 }
@@ -1546,97 +1426,69 @@
1546 1426 return false;
1547 1427 }
1548 1428
1549 1429
1550 - public function import_sizes($sizes, $metadata, $full_size_file, $post_id) {
1551 - $metadata_modified = false;
1430 + public function extract_sizes($sizes) {
1431 + return array_reduce($sizes, function($carry, $url) {
1432 + if (preg_match('/-((\d+)x(\d+))\./', $url, $matches)) {
1433 + $carry[$matches[1]] = $url;
1434 + }
1435 + return $carry;
1436 + }, []);
1437 + }
1552 1438
1553 - if (!empty($sizes)) {
1554 - do_action('templately_import.finalize_gutenberg_attachment', $post_id);
1439 + public function size_exists_in_metadata($size_dimension, $metadata) {
1440 + foreach ($metadata['sizes'] as $size => $size_info) {
1441 + if (strpos($size_info['file'], $size_dimension) !== false) {
1442 + return true;
1443 + }
1444 + }
1445 + return false;
1446 + }
1555 1447
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 - }
1448 + public function create_unique_destination_file($upload_file, $size_dimension) {
1449 + $destination_file = preg_replace('/(\.[^.]+)$/', '-' . $size_dimension . '$1', $upload_file);
1450 + $destination_dir = pathinfo($destination_file, PATHINFO_DIRNAME);
1451 + $destination_filename = pathinfo($destination_file, PATHINFO_BASENAME);
1452 + $unique_filename = wp_unique_filename($destination_dir, $destination_filename);
1453 + return $destination_dir . '/' . $unique_filename;
1454 + }
1574 1455
1575 - // Only return metadata if new sizes were actually added
1576 - return $metadata_modified ? $metadata : false;
1577 - }
1456 + public function create_size_array($destination_file, $size_dimension) {
1457 + list($width, $height) = explode('x', $size_dimension);
1458 + return array(
1459 + 'file' => basename($destination_file),
1460 + 'width' => $width,
1461 + 'height' => $height,
1462 + 'mime-type' => wp_check_filetype($destination_file)['type'],
1463 + 'filesize' => filesize($destination_file),
1464 + 'resized' => false,
1465 + );
1466 + }
1578 1467
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 - }
1468 + public function import_sizes($sizes, $metadata, $upload, $upload_dir, $post_id) {
1469 + if (!empty($sizes)) {
1470 + do_action( 'templately_import.finalize_gutenberg_attachment', $post_id );
1585 1471
1586 - $resized = $editor->resize($size['width'], $size['height'], true);
1587 - if (is_wp_error($resized)) {
1588 - return $resized;
1589 - }
1472 + $size_dimensions = $this->extract_sizes($sizes);
1473 + $size_dimensions = array_filter($size_dimensions);
1590 1474
1591 - $saved = $editor->save($destination_file);
1592 - if (is_wp_error($saved)) {
1593 - return $saved;
1594 - }
1475 + foreach ($size_dimensions as $size_dimension => $__url) {
1476 + if (!$this->size_exists_in_metadata($size_dimension, $metadata)) {
1477 + $unique_destination_file = $this->create_unique_destination_file($upload['file'], $size_dimension);
1478 + $missing_size = $this->fetch_remote_file($__url, $unique_destination_file, $upload_dir);
1595 1479
1596 - return $saved;
1597 - }
1480 + if($missing_size && !is_wp_error($missing_size)) {
1481 + $metadata['sizes'][$size_dimension] = $this->create_size_array($unique_destination_file, $size_dimension);
1482 + do_action( 'templately_import.finalize_gutenberg_attachment', $post_id, $size_dimension );
1483 + }
1484 + }
1485 + }
1598 1486
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 - }
1487 + }
1488 + return $metadata;
1489 + }
1609 1490
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 -
1639 1491 /**
1640 1492 * Attempt to associate posts and menu items with previously missing parents
1641 1493 *
1642 1494 * An imported post's parent may not have been imported when it was first created
@@ -1754,9 +1606,9 @@
1754 1606 */
1755 1607 private function is_valid_meta_key( $key ) {
1756 1608 // Skip attachment metadata since we'll regenerate it from scratch.
1757 1609 // Skip _edit_lock as not relevant for import
1758 - if ( in_array( $key, [ '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock', '_elementor_source_image_hash', 'sm_cloud' ] ) ) {
1610 + if ( in_array( $key, [ '_wp_attached_file', '_wp_attachment_metadata', '_edit_lock' ] ) ) {
1759 1611 return false;
1760 1612 }
1761 1613
1762 1614 return $key;
@@ -1803,9 +1655,9 @@
1803 1655 * @param $post_id
1804 1656 *
1805 1657 * @return void
1806 1658 */
1807 - public function update_post_meta( $post_id ) {
1659 + private function update_post_meta( $post_id ) {
1808 1660 foreach ( $this->posts_meta as $meta_key => $meta_value ) {
1809 1661 update_post_meta( $post_id, $meta_key, $meta_value );
1810 1662 }
1811 1663 }
@@ -1816,71 +1668,24 @@
1816 1668 return $this->output;
1817 1669 }
1818 1670
1819 1671 /**
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 - /**
1835 - * There is deliberately NO `parent::__construct()` call here.
1836 - *
1837 - * `WP_Importer` declared an EMPTY constructor up to WordPress 7.0 and
1838 - * removed it in 7.1. Calling a parent constructor the parent does not
1839 - * declare throws `Error: Cannot call constructor`, so the call fataled every
1840 - * Full Site Import on 7.1 — and it had never done anything on the versions
1841 - * where it resolved, because the body was empty.
1842 - *
1843 - * Nothing contained it: `Error` is not an `Exception` in PHP 7+ (siblings
1844 - * under `Throwable`), and the outer import handlers catch `Exception`. The
1845 - * import streams over SSE, so the fatal landed mid-stream with no `error`
1846 - * event: the connection just stopped.
1847 - *
1848 - * Do not "restore" it for symmetry. `tests/unit/Core/Importer/test-WPImportConstruction.php`
1849 - * pins this, and only discriminates on a WP 7.1+ host.
1850 - *
1851 1672 * @param $file
1852 1673 * @param array $args
1853 1674 */
1854 1675 public function __construct( $file, array $args = [] ) {
1855 - $this->args = $args;
1856 - $this->session_id = $args['session_id'];
1676 + parent::__construct();
1857 1677
1858 - if ( ! empty( $args['json'] ) ) {
1859 - $this->json = $args['json'];
1860 - }
1678 + $this->requested_file_path = $file;
1679 + $this->args = $args;
1680 + $this->origin = $args['origin'];
1681 + $this->origin = $args['origin'];
1682 + $this->import_data_key = 'wp_importer_attributes_' . md5($this->requested_file_path);
1861 1683
1862 - if ( ! empty( $args['origin'] ) ) {
1863 - $this->origin = $args['origin'];
1864 - }
1865 -
1866 - if ( ! empty( $file ) ) {
1867 - $this->requested_file_path = $file;
1868 - $this->import_data_key = 'wp_importer_attributes_' . md5($this->requested_file_path);
1869 - }
1870 -
1871 1684 if ( ! empty( $this->args['fetch_attachments'] ) ) {
1872 1685 $this->fetch_attachments = true;
1873 1686 }
1874 1687
1875 - if ( isset( $this->args['attachment_timeout'] ) && is_numeric( $this->args['attachment_timeout'] ) ) {
1876 - $this->attachment_timeout = max( 1, (int) $this->args['attachment_timeout'] );
1877 - }
1878 -
1879 - if ( isset( $this->args['attachment_retries'] ) && is_numeric( $this->args['attachment_retries'] ) ) {
1880 - $this->attachment_retry_count = max( 1, (int) $this->args['attachment_retries'] + 1 );
1881 - }
1882 -
1883 1688 if ( isset( $this->args['posts'] ) && is_array( $this->args['posts'] ) ) {
1884 1689 $this->processed_posts = $this->args['posts'];
1885 1690 }
1886 1691
@@ -1898,9 +1703,6 @@
1898 1703
1899 1704 if ( ! empty( $this->args['terms_meta'] ) ) {
1900 1705 $this->terms_meta = $this->args['terms_meta'];
1901 1706 }
1902 -
1903 - // Register with Jetpack Sync for better analytics tracking.
1904 - add_filter( 'jetpack_sync_known_importers', [ $this, 'register_jetpack_importer' ] );
1905 1707 }
1906 1708 }