PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.6.6
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.6.6
2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.0 2.3.3 2.3.2 2.3.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 1.0.17 All 128 releases
← All changes | includes/admin/class-api.php +1539 -182 2.3.22.6.6 View file →
@@ -135,9 +135,84 @@
135 135 wp_send_json_success( $data, $status );
136 136 wp_die();
137 137 }
138 138
139 +
139 140 /**
141 + * Memory headroom left for image work, in bytes. 0 means unlimited.
142 + */
143 + private static function wdkit_available_image_memory() {
144 + $limit = wp_convert_hr_to_bytes( ini_get( 'memory_limit' ) );
145 +
146 + if ( $limit <= 0 ) {
147 + return 0;
148 + }
149 +
150 + return max( 0, $limit - memory_get_usage( true ) );
151 + }
152 +
153 + /**
154 + * Stop WordPress decoding images that cannot fit in the memory available.
155 + *
156 + * Both filters are consulted by wp_create_image_subsizes() *before* it loads an image
157 + * editor, so refusing here means the oversized image is never decoded:
158 + *
159 + * big_image_size_threshold -> falsy skips the "-scaled" copy (needs a full decode)
160 + * intermediate_image_sizes_advanced -> empty makes _wp_make_subsizes() return early,
161 + * ahead of its wp_get_image_editor() call
162 + *
163 + * The original file is still attached and usable; only the derived sizes are skipped.
164 + * That trades ideal thumbnails for an import that completes, instead of a fatal that
165 + * takes the whole page down and repeats on every retry.
166 + *
167 + * @since 2.6.2
168 + */
169 + private static function wdkit_guard_oversized_images() {
170 + static $registered = false;
171 +
172 + // Registering twice would stack duplicate closures on both filters.
173 + if ( $registered ) {
174 + return;
175 + }
176 +
177 + $registered = true;
178 +
179 + if ( ! class_exists( 'Wdkit_Image_Guard' ) ) {
180 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php';
181 + }
182 +
183 + add_filter(
184 + 'big_image_size_threshold',
185 + function ( $threshold, $imagesize = array(), $file = '', $attachment_id = 0 ) {
186 + if ( ! empty( $imagesize[0] ) && ! empty( $imagesize[1] )
187 + && ! Wdkit_Image_Guard::decode_fits( $imagesize[0], $imagesize[1], self::wdkit_available_image_memory() )
188 + ) {
189 + return false;
190 + }
191 +
192 + return $threshold;
193 + },
194 + 99,
195 + 4
196 + );
197 +
198 + add_filter(
199 + 'intermediate_image_sizes_advanced',
200 + function ( $sizes, $image_meta = array(), $attachment_id = 0 ) {
201 + if ( ! empty( $image_meta['width'] ) && ! empty( $image_meta['height'] )
202 + && ! Wdkit_Image_Guard::decode_fits( $image_meta['width'], $image_meta['height'], self::wdkit_available_image_memory() )
203 + ) {
204 + return array();
205 + }
206 +
207 + return $sizes;
208 + },
209 + 99,
210 + 3
211 + );
212 + }
213 +
214 + /**
140 215 * Get Wdkit Api Call Ajax.
141 216 */
142 217 public function wdkit_api_call() {
143 218
@@ -197,8 +272,11 @@
197 272 break;
198 273 case 'update_save_temp_image':
199 274 $data = $this->wdkit_update_save_temp_image();
200 275 break;
276 + case 'save_wp_images':
277 + $data = $this->wdkit_save_wp_images();
278 + break;
201 279 case 'get_global_val':
202 280 $data = $this->wdkit_get_global_val();
203 281 break;
204 282 case 'update_global_val':
@@ -233,8 +311,11 @@
233 311 break;
234 312 case 'generate_ai_content':
235 313 $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'generate_ai_content' );
236 314 break;
315 + case 'generate_ai_content_batch':
316 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'generate_ai_content_batch' );
317 + break;
237 318 case 'reset_site':
238 319 $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'reset_site' );
239 320 break;
240 321 case 'wdkit_nxt_thembuilder_reset':
@@ -293,8 +374,11 @@
293 374 break;
294 375 case 'wkit_update_elementor_template':
295 376 $data = $this->wkit_update_elementor_template();
296 377 break;
378 + case 'wdkit_update_page_content':
379 + $data = $this->wdkit_update_page_content();
380 + break;
297 381 case 'update_plugin_setting':
298 382 $data = $this->update_plugin_setting();
299 383 break;
300 384 case 'update_theme_setting':
@@ -327,9 +411,9 @@
327 411
328 412 wp_send_json(
329 413 array(
330 414 'success' => false,
331 - 'message' => 'No block names received or filter not found.',
415 + 'message' => __( 'No block names received or filter not found.', 'wdesignkit' ),
332 416 'description' => 'Ensure blockNames are posted and the filter is attached.',
333 417 )
334 418 );
335 419 wp_die();
@@ -361,8 +445,11 @@
361 445 break;
362 446 case 'wkit_check_widget_versions':
363 447 $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_check_widget_versions' );
364 448 break;
449 + case 'wkit_plugin_download_get':
450 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_plugin_download_get' );
451 + break;
365 452 case 'wkit_manage_widget_workspace':
366 453 $data = $this->wdkit_manage_widget_workspace();
367 454 break;
368 455 case 'wkit_activate_key':
@@ -421,8 +508,11 @@
421 508 break;
422 509 case 'wdkit_get_workspace_data':
423 510 $data = $this->wdkit_get_workspace_data();
424 511 break;
512 + default:
513 + $this->wdkit_error_msg( __( 'Unknown request type.', 'wdesignkit' ) );
514 + return;
425 515 }
426 516
427 517 $this->wdkit_success_msg( $data );
428 518 // wp_die();
@@ -433,12 +523,13 @@
433 523 * This Function is used for API call
434 524 *
435 525 * @since 1.0.0
436 526 *
437 - * @param array $data give array.
438 - * @param array $name store data.
527 + * @param array $data give array.
528 + * @param array $name store data.
529 + * @param int $timeout optional HTTP timeout in seconds. Default 100.
439 530 */
440 - protected function wkit_api_call( $data, $name ) {
531 + protected function wkit_api_call( $data, $name, $timeout = 100 ) {
441 532 $u_r_l = $this->wdkit_api;
442 533
443 534 if ( empty( $u_r_l ) ) {
444 535 return array(
@@ -449,9 +540,9 @@
449 540
450 541 $args = array(
451 542 'method' => 'POST',
452 543 'body' => $data,
453 - 'timeout' => 100,
544 + 'timeout' => $timeout,
454 545 );
455 546 $response = wp_remote_post( $u_r_l . $name, $args );
456 547
457 548 if ( is_wp_error( $response ) ) {
@@ -457,9 +548,9 @@
457 548 if ( is_wp_error( $response ) ) {
458 549 $error_message = $response->get_error_message();
459 550
460 551 /* Translators: %s is a placeholder for the error message */
461 - $error_message = printf( esc_html__( 'API request error: %s', 'wdesignkit' ), esc_html( $error_message ) );
552 + $error_message = sprintf( esc_html__( 'API request error: %s', 'wdesignkit' ), esc_html( $error_message ) );
462 553
463 554 return array(
464 555 'massage' => $error_message,
465 556 'success' => false,
@@ -476,9 +567,9 @@
476 567 'success' => true,
477 568 );
478 569 }
479 570
480 - $error_message = printf( 'Server error: %d', esc_html( $status_code ) );
571 + $error_message = sprintf( 'Server error: %d', esc_html( $status_code ) );
481 572
482 573 if ( isset( $error_data->message ) ) {
483 574 $error_message .= ' (' . $error_data->message . ')';
484 575 }
@@ -675,15 +766,22 @@
675 766 $nexter_active_check = is_plugin_active( 'the-plus-addons-for-block-editor/the-plus-addons-for-block-editor.php' );
676 767
677 768 $theplus_licence = get_option( 'tpaep_licence_data', array() );
678 769
679 - if ( ! empty( $theplus_active_check ) && ! empty( $theplus_licence ) ) {
770 + // Also require the TPAE Pro plugin to be active (Pro defines THEPLUS_VERSION;
771 + // the free plugin defines L_THEPLUS_VERSION). This hides the "found active
772 + // key" notice when the Pro plugin is removed even though its licence option
773 + // still lingers in the database.
774 + if ( ! empty( $theplus_active_check ) && defined( 'THEPLUS_VERSION' ) && ! empty( $theplus_licence ) ) {
680 775 $manage_licence['tpae'] = $theplus_licence;
681 776 }
682 777
683 778 $nexter_licence = get_option( 'tpgb_activate', array() );
684 779
685 - if ( ! empty( $nexter_active_check ) && ! empty( $nexter_licence ) && ! empty( $nexter_licence['tpgb_activate_key'] ) ) {
780 + // Also require the Nexter Blocks Pro plugin to be active (Pro defines
781 + // TPGBP_VERSION; the free plugin defines TPGB_VERSION), so the notice hides
782 + // when the Pro plugin is removed but its licence option persists.
783 + if ( ! empty( $nexter_active_check ) && defined( 'TPGBP_VERSION' ) && ! empty( $nexter_licence ) && ! empty( $nexter_licence['tpgb_activate_key'] ) ) {
686 784 $tpgb_license_status = get_option( 'tpgbp_license_status', array() );
687 785 $tpgb_license_status['license_key'] = $nexter_licence['tpgb_activate_key'];
688 786 $manage_licence['tpag'] = $tpgb_license_status;
689 787 }
@@ -724,14 +822,24 @@
724 822 'site_url' => $site_url,
725 823 );
726 824
727 825 $response = WDesignKit_Data_Query::get_data( 'get_user_info', $args );
826 +
827 + if ( is_wp_error( $response ) ) {
828 + wp_send_json( array(
829 + 'success' => false,
830 + 'message' => $response->get_error_message(),
831 + 'description' => $response->get_error_message(),
832 + ) );
833 + wp_die();
834 + }
835 +
728 836 $status = ( ! empty( $response['status'] ) ) ? sanitize_text_field( $response['status'] ) : 'error';
729 837 $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false;
730 838
731 839 /**Condtion user for user logout & expire token*/
732 840 if ( 'Token is Expired' === $status || 'Authorization Token not found' === $status ) {
733 - delete_transient( 'wdkit_auth_' . $email );
841 + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) );
734 842 // Clear stored license data when token expires so banner shows again
735 843 delete_option( 'wdkit_licence_data' );
736 844 }
737 845
@@ -743,9 +851,9 @@
743 851 if ( ! empty( $response['credits']['wdkit_licence'] ) && is_array( $response['credits']['wdkit_licence'] ) ) {
744 852 $wdkit_licence = $response['credits']['wdkit_licence'];
745 853 // Handle serialized data
746 854 if ( is_string( $wdkit_licence ) && is_serialized( $wdkit_licence ) ) {
747 - $wdkit_licence = unserialize( $wdkit_licence );
855 + $wdkit_licence = unserialize( $wdkit_licence, array( 'allowed_classes' => false ) );
748 856 }
749 857 if ( ! empty( $wdkit_licence ) && is_array( $wdkit_licence ) ) {
750 858 update_option( 'wdkit_licence_data', $wdkit_licence );
751 859 }
@@ -776,8 +884,16 @@
776 884 $credits = ! empty( $response['credits']['widget_limit']['meta_value'] ) ? $response['credits']['widget_limit']['meta_value'] : 10;
777 885 $server_list = ! empty( $response['widgettemplate'] ) ? $response['widgettemplate'] : array();
778 886 $db_builder_list = ! empty( $response['widgetbuilder'] ) ? $response['widgetbuilder'] : array();
779 887
888 + // Whether this call actually carried the server widget list that activation state is
889 + // derived from. Captured before the loops below, which unset() matched $server_list
890 + // entries as they go. wdkit_meta_data() calls this method with array(), and without
891 + // this flag that call rebuilt $db_widget from local widgets only — every one of which
892 + // is forced 'active' further down — and then wrote the empty result over
893 + // wkit_deactivate_widgets, erasing every deactivation the user had made.
894 + $has_server_widgets = ! empty( $server_list );
895 +
780 896 $placeholderimg = WDKIT_URL . 'assets/images/placeholder.jpg';
781 897
782 898 $local_list = $this->wdkit_get_local_widgets();
783 899
@@ -784,9 +900,9 @@
784 900 $server_w_unique = array_column( $server_list, 'w_unique' );
785 901
786 902 $idx_builder = array();
787 903 foreach ( $db_builder_list as $index => $value ) {
788 - $builder_name = ! empty( $value['builder_name'] ) ? $value['builder_name'] : '';
904 + $builder_name = ! empty( $value['builder_slug'] ) ? $value['builder_slug'] : '';
789 905 $w_id = ! empty( $value['w_id'] ) ? $value['w_id'] : '';
790 906
791 907 if ( ! empty( $builder_name ) ) {
792 908 $idx_builder[ $w_id ] = strtolower( str_replace( ' ', '_', trim( $builder_name ) ) );
@@ -863,13 +979,31 @@
863 979 );
864 980 }
865 981 }
866 982
867 - $get_db_widget = get_option( 'wkit_deactivate_widgets', array() );
868 - if ( empty( $get_db_widget ) ) {
869 - add_option( 'wkit_deactivate_widgets', $db_widget, '', 'yes' );
870 - } else {
871 - update_option( 'wkit_deactivate_widgets', $db_widget );
983 + // Only persist activation state when the server list it is derived from was actually
984 + // supplied. See $has_server_widgets above.
985 + if ( $has_server_widgets ) {
986 + // update_option() creates the row when it is missing, so it covers both cases.
987 + // The previous add_option()/update_option() split was chosen on empty( $option ),
988 + // but wdkit_db_widgetlist() creates this row as an empty array on every install —
989 + // so the empty branch ran while the row already existed, and add_option() is a
990 + // no-op for an existing option. Deactivating from the My Widgets screen was
991 + // therefore silently discarded on effectively every site. Autoload stays 'yes',
992 + // matching the original add_option() call and wdkit_db_widgetlist().
993 + update_option( 'wkit_deactivate_widgets', $db_widget, 'yes' );
994 +
995 + // The cached widget registry bakes in wkit_deactivate_widgets membership and is
996 + // stored as a no-expiry transient, so it never self-heals. Without this the
997 + // loaders kept registering a widget the user had just switched off (and kept
998 + // hiding one they had switched back on) until the transient was flushed by hand.
999 + // The write above is not per-builder — one save can change any builder's set, and
1000 + // a widget can move between builders — so clear all four.
1001 + if ( function_exists( 'wdesignkit_invalidate_widget_registry' ) ) {
1002 + foreach ( array( 'elementor', 'gutenberg', 'gutenberg_core', 'bricks' ) as $builder_slug ) {
1003 + wdesignkit_invalidate_widget_registry( $builder_slug );
1004 + }
1005 + }
872 1006 }
873 1007
874 1008 return $final;
875 1009 }
@@ -883,8 +1017,16 @@
883 1017 $args = $this->wdkit_parse_args( $_POST );
884 1018
885 1019 $response = WDesignKit_Data_Query::get_data( 'browse_page', $args );
886 1020
1021 + if ( is_wp_error( $response ) ) {
1022 + wp_send_json( array(
1023 + 'success' => false,
1024 + 'message' => $response->get_error_message(),
1025 + ) );
1026 + wp_die();
1027 + }
1028 +
887 1029 $manage_licence = array();
888 1030 $manage_licence['theplus_elementor_addon'] = ! empty( defined( 'THEPLUS_VERSION' ) ) ? true : false;
889 1031 $manage_licence['tpag'] = ! empty( defined( 'TPGBP_VERSION' ) ) ? true : false;
890 1032 $manage_licence['elementor-pro'] = ! empty( defined( 'ELEMENTOR_PRO_VERSION' ) ) ? true : false;
@@ -920,15 +1062,14 @@
920 1062
921 1063 $user_email = strtolower( sanitize_email( $args['email'] ) );
922 1064 $response = '';
923 1065
1066 + // Bug D fix: response()->json() is Laravel syntax — causes PHP fatal. Use plain array.
924 1067 if ( empty( $user_email ) || empty( $args['template_id'] ) ) {
925 - $response = response()->json(
926 - array(
927 - 'message' => $this->e_msg_login,
928 - 'description' => $this->e_desc_login,
929 - 'success' => true,
930 - )
1068 + $response = array(
1069 + 'message' => $this->e_msg_login,
1070 + 'description' => $this->e_desc_login,
1071 + 'success' => false,
931 1072 );
932 1073
933 1074 wp_send_json( $response );
934 1075 wp_die();
@@ -999,8 +1140,24 @@
999 1140 }
1000 1141
1001 1142 $response = WDesignKit_Data_Query::get_data( 'save_template', $args );
1002 1143
1144 + /**
1145 + * The cloud call can come back as a WP_Error (timeout, DNS, refused) or with an
1146 + * empty / unparsable body, which json_decode()s to null. Forwarding that as-is
1147 + * makes admin-ajax answer with a literal `null` that the editor then reads
1148 + * `.id` off, killing the whole app. Normalise it to the failure shape used above.
1149 + */
1150 + if ( is_wp_error( $response ) || ! is_array( $response ) ) {
1151 + $response = array(
1152 + 'id' => 0,
1153 + 'editpage' => '',
1154 + 'message' => esc_html__( 'Template Not Saved !', 'wdesignkit' ),
1155 + 'description' => is_wp_error( $response ) ? $response->get_error_message() : esc_html__( 'Could not reach the WDesignKit server. Please try again.', 'wdesignkit' ),
1156 + 'success' => false,
1157 + );
1158 + }
1159 +
1003 1160 wp_send_json( $response );
1004 1161 wp_die();
1005 1162 }
1006 1163
@@ -1024,9 +1181,11 @@
1024 1181 'success' => false,
1025 1182 );
1026 1183 } else {
1027 1184 $temp_content = str_replace( '\\', '', $temp_content );
1028 - $temp_content = wp_remote_get( $temp_content )['body'];
1185 + // SSRF guard (CWE-918): validate the resolved host before fetching a caller-supplied URL.
1186 + $fetched = wdesignkit_safe_remote_get( $temp_content );
1187 + $temp_content = is_wp_error( $fetched ) ? '' : wp_remote_retrieve_body( $fetched );
1029 1188 $temp_content = base64_encode( $temp_content );
1030 1189
1031 1190 $args = array(
1032 1191 'token' => $token,
@@ -1059,12 +1218,298 @@
1059 1218 }
1060 1219
1061 1220 /**
1062 1221 *
1222 + * It is Use for save image to WordPress Media Library.
1223 + *
1224 + * @since 2.3.3
1225 + */
1226 + protected function wdkit_save_wp_images() {
1227 +
1228 + // media_sideload_image() generates every registered thumbnail size, which decodes
1229 + // the full source bitmap. Same guard as the page import.
1230 + $this->wdkit_guard_oversized_images();
1231 +
1232 + $image_url = isset( $_POST['image'] ) ? sanitize_text_field( $_POST['image'] ) : '';
1233 +
1234 + if ( empty( $image_url ) ) {
1235 + $response = array(
1236 + 'message' => __( 'No Image Provided', 'wdesignkit' ),
1237 + 'description' => __( 'No Image URL provided for save.', 'wdesignkit' ),
1238 + 'success' => false,
1239 + );
1240 + } else {
1241 +
1242 + $attachment_id = media_sideload_image( $image_url, 0, null, 'id' );
1243 +
1244 + if ( is_wp_error( $attachment_id ) ) {
1245 + $response = array(
1246 + 'message' => __( 'Upload Failed', 'wdesignkit' ),
1247 + 'description' => $attachment_id->get_error_message(),
1248 + 'success' => false,
1249 + );
1250 + } else {
1251 + $saved_url = wp_get_attachment_url( $attachment_id );
1252 +
1253 + // Elementor's importer skips an image only when it finds
1254 + // _elementor_source_image_hash matching sha1 of the URL it is given. The
1255 + // content we hand it now carries this local URL, so stamp the hash of that
1256 + // URL too - without it Elementor re-downloads a file already on disk and
1257 + // leaves a "-1" duplicate behind for every image on every page that uses it.
1258 + if ( $saved_url ) {
1259 + update_post_meta( $attachment_id, '_elementor_source_image_hash', sha1( $saved_url ) );
1260 +
1261 + // Same purpose for the block importer, which keys off its own meta.
1262 + update_post_meta( $attachment_id, 'tpgb_source_image_key', sha1( $saved_url ) );
1263 + }
1264 +
1265 + $response = array(
1266 + 'message' => __( 'Image Saved', 'wdesignkit' ),
1267 + 'description' => __( 'Image successfully saved to Media Library.', 'wdesignkit' ),
1268 + 'success' => true,
1269 + 'url' => $saved_url,
1270 + );
1271 + }
1272 +
1273 + }
1274 +
1275 + wp_send_json( $response );
1276 + wp_die();
1277 + }
1278 +
1279 + /**
1280 + *
1063 1281 * Get Elementor Global color and Typography.
1064 1282 *
1065 1283 * @since 1.1.16
1066 1284 */
1285 + /**
1286 + * Kit settings holding The Plus Addons' own globals.
1287 + *
1288 + * These sit in the Elementor kit's `_elementor_page_settings` alongside Elementor's
1289 + * system_colors / system_typography, but the save flow only ever collected the four
1290 + * Elementor keys. Widgets reference an entry in these lists by its `_id` through a
1291 + * `tp_global_preset` setting, so a template saved without them travels with the
1292 + * reference but not the definition - which is why imported sections come in missing
1293 + * their button styling, radii and shadows.
1294 + *
1295 + * @since 2.6.4
1296 + *
1297 + * @return array Kit setting keys.
1298 + */
1299 + private function wdkit_tp_global_kit_keys() {
1300 + return array(
1301 + 'tp_global_button_style_list',
1302 + 'tp_global_dimensions_list',
1303 + 'tp_global_box_shadow_list',
1304 + 'tp_global_gradient_list',
1305 + 'tp_global_gsap_list',
1306 + 'tp_global_scroll_animation_list',
1307 + 'tp_text_global_gsap_list',
1308 + 'tp_image_global_gsap_list',
1309 + );
1310 + }
1311 +
1312 + /**
1313 + * Merge incoming Plus globals into the active kit, keyed by `_id`.
1314 + *
1315 + * Entries are matched on their `_id`, never on position: an existing entry is always
1316 + * left as it is, and only genuinely new ones are appended. That matters because
1317 + * widgets - and the entries themselves, a button style points at dimension and shadow
1318 + * entries - resolve by `_id`. Renumbering or overwriting would repoint references on
1319 + * the destination site's own content.
1320 + *
1321 + * @since 2.6.4
1322 + *
1323 + * @param array $incoming Lists captured with the template.
1324 + * @return bool True when the kit was changed.
1325 + */
1326 + /**
1327 + * Global colour / typography ids this site already defines.
1328 + *
1329 + * @since 2.6.4
1330 + *
1331 + * @param array $kit_meta Kit `_elementor_page_settings`.
1332 + * @return array{color:array<string,bool>,typography:array<string,bool>}
1333 + */
1334 + private function wdkit_known_global_ids( $kit_meta ) {
1335 + $known = array(
1336 + 'color' => array(),
1337 + 'typography' => array(),
1338 + );
1339 +
1340 + $sources = array(
1341 + 'color' => array( 'system_colors', 'custom_colors' ),
1342 + 'typography' => array( 'system_typography', 'custom_typography' ),
1343 + );
1344 +
1345 + foreach ( $sources as $kind => $keys ) {
1346 + foreach ( $keys as $key ) {
1347 + if ( empty( $kit_meta[ $key ] ) || ! is_array( $kit_meta[ $key ] ) ) {
1348 + continue;
1349 + }
1350 +
1351 + foreach ( $kit_meta[ $key ] as $entry ) {
1352 + if ( ! empty( $entry['_id'] ) ) {
1353 + $known[ $kind ][ $entry['_id'] ] = true;
1354 + }
1355 + }
1356 + }
1357 + }
1358 +
1359 + return $known;
1360 + }
1361 +
1362 + /**
1363 + * Make one incoming Plus global's colour / font references resolvable here.
1364 + *
1365 + * A Plus global can point at an Elementor global: the "Primary Button" entry holds
1366 + * `__globals__: { text_color: "globals/colors?id=72e09b4", … }`, which The Plus Addons
1367 + * turns into `var(--e-global-color-72e09b4)`. Elementor only emits that variable for ids
1368 + * present in the kit, so on a site without `72e09b4` the button renders with no colour.
1369 + *
1370 + * Two cases, and the difference is deliberate:
1371 + *
1372 + * - The site ALREADY defines that id — leave the reference alone. The button then picks
1373 + * up the destination's own colour, which is the point of a global. Their palette is
1374 + * never read from or written to beyond this check.
1375 + * - The site does NOT define it — write the captured value straight into the entry and
1376 + * drop the reference, so it renders as designed.
1377 + *
1378 + * Nothing is ever added to the user's global colours or fonts. An earlier version injected
1379 + * the missing definitions into their palette, which made the reference resolve but grew
1380 + * their Site Settings by every colour an imported template happened to use.
1381 + *
1382 + * @since 2.6.4
1383 + *
1384 + * @param array $entry One repeater entry.
1385 + * @param array $refs Definitions captured with the template.
1386 + * @param array $known Ids this site defines, from wdkit_known_global_ids().
1387 + * @return array Entry, with unresolvable references replaced by their values.
1388 + */
1389 + private function wdkit_resolve_entry_globals( $entry, $refs, $known ) {
1390 + if ( empty( $entry['__globals__'] ) || ! is_array( $entry['__globals__'] ) ) {
1391 + return $entry;
1392 + }
1393 +
1394 + foreach ( $entry['__globals__'] as $control => $ref ) {
1395 + if ( ! is_string( $ref ) || false === strpos( $ref, 'id=' ) ) {
1396 + continue;
1397 + }
1398 +
1399 + if ( false !== strpos( $ref, 'globals/colors' ) ) {
1400 + $kind = 'color';
1401 + } elseif ( false !== strpos( $ref, 'globals/typography' ) ) {
1402 + $kind = 'typography';
1403 + } else {
1404 + continue;
1405 + }
1406 +
1407 + $id = substr( $ref, strpos( $ref, 'id=' ) + 3 );
1408 + if ( '' === $id || isset( $known[ $kind ][ $id ] ) ) {
1409 + // Defined here already — their value wins.
1410 + continue;
1411 + }
1412 +
1413 + $definition = null;
1414 + foreach ( ( $refs[ $kind ] ?? array() ) as $candidate ) {
1415 + if ( is_array( $candidate ) && ( $candidate['_id'] ?? '' ) === $id ) {
1416 + $definition = $candidate;
1417 + break;
1418 + }
1419 + }
1420 +
1421 + if ( null === $definition ) {
1422 + // Nothing captured for it, so leave the reference rather than blank the field.
1423 + continue;
1424 + }
1425 +
1426 + if ( 'color' === $kind ) {
1427 + if ( empty( $definition['color'] ) ) {
1428 + continue;
1429 + }
1430 +
1431 + $entry[ $control ] = $definition['color'];
1432 + } else {
1433 + // A typography global expands into its own set of controls: the reference is
1434 + // held under e.g. `typography_typography`, and each definition key replaces
1435 + // that suffix — `typography_font_family`, `typography_font_weight`, and so on.
1436 + foreach ( $definition as $def_key => $def_value ) {
1437 + if ( '_id' === $def_key || 'title' === $def_key ) {
1438 + continue;
1439 + }
1440 +
1441 + $entry[ str_replace( 'typography_typography', $def_key, $control ) ] = $def_value;
1442 + }
1443 + }
1444 +
1445 + unset( $entry['__globals__'][ $control ] );
1446 + }
1447 +
1448 + return $entry;
1449 + }
1450 +
1451 + private function wdkit_merge_tp_globals( $incoming, $refs = array() ) {
1452 + if ( empty( $incoming ) || ! is_array( $incoming ) ) {
1453 + return false;
1454 + }
1455 +
1456 + $kit_id = get_option( 'elementor_active_kit' );
1457 + if ( empty( $kit_id ) ) {
1458 + return false;
1459 + }
1460 +
1461 + $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true );
1462 + if ( ! is_array( $kit_meta ) ) {
1463 + $kit_meta = array();
1464 + }
1465 +
1466 + // Which global ids this site already defines. The Plus Addons turns a reference into
1467 + // var(--e-global-color-<_id>), and Elementor only emits that variable for ids in the
1468 + // kit — so a reference the destination does not define resolves to nothing at all.
1469 + $known = $this->wdkit_known_global_ids( $kit_meta );
1470 +
1471 + $changed = false;
1472 +
1473 + foreach ( $this->wdkit_tp_global_kit_keys() as $key ) {
1474 + if ( empty( $incoming[ $key ] ) || ! is_array( $incoming[ $key ] ) ) {
1475 + continue;
1476 + }
1477 +
1478 + $existing = ( ! empty( $kit_meta[ $key ] ) && is_array( $kit_meta[ $key ] ) ) ? $kit_meta[ $key ] : array();
1479 +
1480 + $seen = array();
1481 + foreach ( $existing as $entry ) {
1482 + if ( ! empty( $entry['_id'] ) ) {
1483 + $seen[ $entry['_id'] ] = true;
1484 + }
1485 + }
1486 +
1487 + foreach ( $incoming[ $key ] as $entry ) {
1488 + if ( ! is_array( $entry ) || empty( $entry['_id'] ) || isset( $seen[ $entry['_id'] ] ) ) {
1489 + continue;
1490 + }
1491 +
1492 + // Only ever rewrite the entry being added — never one already in the kit.
1493 + $existing[] = $this->wdkit_resolve_entry_globals( $entry, $refs, $known );
1494 + $seen[ $entry['_id'] ] = true;
1495 + $changed = true;
1496 + }
1497 +
1498 + $kit_meta[ $key ] = array_values( $existing );
1499 + }
1500 +
1501 + if ( $changed ) {
1502 + update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta );
1503 +
1504 + // Writing kit meta directly does not rebuild the kit stylesheet, so the
1505 + // merged globals would never reach the frontend.
1506 + $this->wdkit_regenerate_elementor_kit_css();
1507 + }
1508 +
1509 + return $changed;
1510 + }
1511 +
1067 1512 protected function wdkit_get_global_val() {
1068 1513
1069 1514 $builder = isset( $_POST['builder'] ) ? strtolower( sanitize_text_field( $_POST['builder'] ) ) : '';
1070 1515
@@ -1806,8 +2251,13 @@
1806 2251 $site_data = ! empty( $_POST['site_data'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['site_data'] ) ), true ) : array();
1807 2252
1808 2253 if ( 'elementor' == $builder ) {
1809 2254 $kit_id = get_option( 'elementor_active_kit' );
2255 + if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) {
2256 + \Elementor\Core\Kits\Manager::create_default_kit();
2257 + $kit_id = get_option( 'elementor_active_kit' );
2258 + }
2259 +
1810 2260 if ( ! $kit_id ) {
1811 2261 $response = array(
1812 2262 'message' => __( 'Elementor kit not found', 'wdesignkit' ),
1813 2263 'description' => __( 'No active Elementor kit found', 'wdesignkit' ),
@@ -1817,18 +2267,13 @@
1817 2267 wp_send_json( $response );
1818 2268 wp_die();
1819 2269 }
1820 2270
2271 + // A freshly created kit has no `_elementor_page_settings` meta yet,
2272 + // so an empty result here is a valid starting point, not an error.
1821 2273 $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true );
1822 - if ( empty( $kit_meta ) ) {
1823 - $response = array(
1824 - 'message' => __( 'Data Not Found', 'wdesignkit' ),
1825 - 'description' => __( 'No site data found in kit', 'wdesignkit' ),
1826 - 'success' => false,
1827 - );
1828 -
1829 - wp_send_json( $response );
1830 - wp_die();
2274 + if ( ! is_array( $kit_meta ) ) {
2275 + $kit_meta = array();
1831 2276 }
1832 2277
1833 2278 $kit_meta['container_width'] = ! empty( $site_data['container_width'] ) ? $site_data['container_width'] : array();
1834 2279 $kit_meta['__globals__'] = ! empty( $site_data['globals'] ) ? $site_data['globals'] : array();
@@ -1835,8 +2280,13 @@
1835 2280 $kit_meta['body_background_color'] = ! empty( $site_data['body_background_color'] ) ? $site_data['body_background_color'] : array();
1836 2281
1837 2282 update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta );
1838 2283
2284 + // Regenerate Elementor's cached CSS. Writing the kit meta directly does
2285 + // not rebuild the kit stylesheet, so the imported body background colour
2286 + // and container width would otherwise never render on the frontend.
2287 + $this->wdkit_regenerate_elementor_kit_css();
2288 +
1839 2289 $response = array(
1840 2290 'message' => __( 'Site data Updated', 'wdesignkit' ),
1841 2291 'description' => __( 'Site Globals Updated', 'wdesignkit' ),
1842 2292 'success' => true,
@@ -1884,8 +2334,16 @@
1884 2334 $g_typo = ! empty( $_POST['g_typography'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['g_typography'] ) ), true ) : array();
1885 2335
1886 2336 // Get colors from Elementor Site Kit
1887 2337 $kit_id = get_option( 'elementor_active_kit' );
2338 + if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) {
2339 + // No kit has ever been created on this site (the option is only
2340 + // ever populated by Elementor's own activation hook). Create one
2341 + // via Elementor's own helper so the import has somewhere to write.
2342 + \Elementor\Core\Kits\Manager::create_default_kit();
2343 + $kit_id = get_option( 'elementor_active_kit' );
2344 + }
2345 +
1888 2346 if ( ! $kit_id ) {
1889 2347 $response = array(
1890 2348 'message' => __( 'Elementor kit not found', 'wdesignkit' ),
1891 2349 'description' => __( 'No active Elementor kit found', 'wdesignkit' ),
@@ -1895,25 +2353,25 @@
1895 2353 wp_send_json( $response );
1896 2354 wp_die();
1897 2355 }
1898 2356
2357 + // A freshly created kit has no `_elementor_page_settings` meta yet,
2358 + // so an empty result here is a valid starting point, not an error.
1899 2359 $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true );
1900 - if ( empty( $kit_meta ) ) {
1901 - $response = array(
1902 - 'message' => __( 'Data Not Found', 'wdesignkit' ),
1903 - 'description' => __( 'No meta data found in kit', 'wdesignkit' ),
1904 - 'success' => false,
1905 - );
1906 -
1907 - wp_send_json( $response );
1908 - wp_die();
2360 + if ( ! is_array( $kit_meta ) ) {
2361 + $kit_meta = array();
1909 2362 }
1910 2363
1911 - $kit_meta['custom_colors'] = array_merge( $g_color, $kit_meta['custom_colors'] );
1912 - $kit_meta['custom_typography'] = array_merge( $g_typo, $kit_meta['custom_typography'] );
2364 + $kit_meta['custom_colors'] = array_merge( $g_color, $kit_meta['custom_colors'] ?? array() );
2365 + $kit_meta['custom_typography'] = array_merge( $g_typo, $kit_meta['custom_typography'] ?? array() );
1913 2366
1914 2367 update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta );
1915 2368
2369 + // Regenerate Elementor's cached CSS. Writing the kit meta directly does
2370 + // not rebuild the kit stylesheet, so the imported global colours and
2371 + // fonts would otherwise never render on the frontend.
2372 + $this->wdkit_regenerate_elementor_kit_css();
2373 +
1916 2374 $response = array(
1917 2375 'message' => __( 'Global data Updated', 'wdesignkit' ),
1918 2376 'description' => __( 'Global Color and Typography Updated', 'wdesignkit' ),
1919 2377 'success' => true,
@@ -1948,9 +2406,29 @@
1948 2406 wp_die();
1949 2407 }
1950 2408
1951 2409 /**
2410 + * Regenerate Elementor's cached CSS files after the active kit's
2411 + * `_elementor_page_settings` meta has been changed directly.
1952 2412 *
2413 + * Elementor renders global colours, global fonts and the body background
2414 + * colour into a cached kit stylesheet. Updating the meta via
2415 + * update_post_meta() does not rebuild that stylesheet, so imported site
2416 + * settings never reach the frontend until the cache is cleared. This
2417 + * mirrors the clear_cache() call already used by the page/section import.
2418 + *
2419 + * @since 2.3.2
2420 + *
2421 + * @return void
2422 + */
2423 + protected function wdkit_regenerate_elementor_kit_css() {
2424 + if ( did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Plugin' ) ) {
2425 + \Elementor\Plugin::$instance->files_manager->clear_cache();
2426 + }
2427 + }
2428 +
2429 + /**
2430 + *
1953 2431 * Create Gutenberg page and save for re-generate css file.
1954 2432 *
1955 2433 * @since 1.2.3
1956 2434 */
@@ -2314,34 +2792,56 @@
2314 2792 $error_message = $response->get_error_message();
2315 2793
2316 2794 $result = $this->tpae_set_response( false, 'oops', 'oops', '' );
2317 2795 } else {
2318 - $theme_info = unserialize( $response['body'] );
2796 + // api.wordpress.org's theme_information response is a serialized stdClass
2797 + // (accessed below via ->name / ->download_link). allowed_classes => false
2798 + // blocks stdClass too, turning it into an __PHP_Incomplete_Class whose
2799 + // properties silently don't exist — allow only stdClass, still refusing any
2800 + // other (potentially dangerous) class the payload might reference.
2801 + $theme_info = unserialize( $response['body'], array( 'allowed_classes' => array( 'stdClass' ) ) );
2319 2802 $theme_name = $theme_info->name;
2320 2803 $theme_zip_url = $theme_info->download_link;
2321 2804
2322 - global $wp_filesystem;
2323 - // Install the theme
2324 - $theme = wp_remote_get( $theme_zip_url, array( 'timeout' => 30 ) );
2805 + // SSRF guard (CWE-918): validate the resolved host before fetching the ZIP
2806 + // referenced by the external theme_info response.
2807 + if ( ! wdesignkit_validate_external_url( $theme_zip_url ) ) {
2808 + return array(
2809 + 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ),
2810 + 'description' => esc_html__( 'The theme package URL is not allowed.', 'wdesignkit' ),
2811 + 'status' => 'inactive',
2812 + 'success' => false,
2813 + );
2814 + }
2325 2815
2326 2816 if ( ! function_exists( 'WP_Filesystem' ) ) {
2327 2817 require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
2328 2818 }
2329 2819
2820 + require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/class-wp-upgrader.php' );
2821 + require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/theme.php' );
2822 +
2330 2823 WP_Filesystem();
2331 2824
2332 2825 $active_theme = wp_get_theme();
2333 2826 $theme_name = $active_theme->get( 'Name' );
2334 2827
2335 - $wp_filesystem->put_contents( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip', $theme['body'] );
2336 - $zip = new ZipArchive();
2337 - if ( $zip->open( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip' ) === true ) {
2338 - $zip->extractTo( WP_CONTENT_DIR . '/themes/' );
2339 - $zip->close();
2828 + // Install via WordPress core's Theme_Upgrader instead of manually fetching and
2829 + // ZipArchive::extractTo()'ing the remote package: core already performs the
2830 + // standard download -> unpack -> validate-package-structure -> move-into-place
2831 + // flow (including cleanup on failure) used for every trusted theme install.
2832 + $upgrader = new Theme_Upgrader( new Automatic_Upgrader_Skin() );
2833 + $install = $upgrader->install( $theme_zip_url );
2834 +
2835 + if ( is_wp_error( $install ) || ! $install ) {
2836 + return array(
2837 + 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ),
2838 + 'description' => is_wp_error( $install ) ? $install->get_error_message() : esc_html__( 'Theme could not be installed.', 'wdesignkit' ),
2839 + 'status' => 'inactive',
2840 + 'success' => false,
2841 + );
2340 2842 }
2341 2843
2342 - $wp_filesystem->delete( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip' );
2343 -
2344 2844 $activate_result = switch_theme( $name );
2345 2845
2346 2846 if ( ! is_wp_error( $activate_result ) ) {
2347 2847 $response = array(
@@ -2518,8 +3018,17 @@
2518 3018
2519 3019 unset( $args['email'] );
2520 3020 $args['unique_id'] = get_option( 'wdkit_unique_id' ) ?? '';
2521 3021 $response = WDesignKit_Data_Query::get_data( $api_type, $args );
3022 +
3023 + if ( is_wp_error( $response ) ) {
3024 + wp_send_json( array(
3025 + 'success' => false,
3026 + 'message' => $response->get_error_message(),
3027 + ) );
3028 + wp_die();
3029 + }
3030 +
2522 3031 $custom_meta = isset( $_POST['custom_meta'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_meta'] ) ) : false;
2523 3032
2524 3033 /** Custom meta Field */
2525 3034 if ( ! empty( $custom_meta ) && 'true' === $custom_meta && ! empty( $response ) && ! empty( $response['content'] ) ) {
@@ -2530,9 +3039,9 @@
2530 3039
2531 3040 if ( ! empty( $meta_data ) ) {
2532 3041 foreach ( $meta_data as $meta_key => $meta_val ) {
2533 3042 if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) {
2534 - $meta_val[0] = maybe_unserialize( $meta_val[0] );
3043 + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) );
2535 3044 }
2536 3045
2537 3046 if ( get_post_meta( get_the_ID(), $meta_key, true ) === '' ) {
2538 3047 add_post_meta( get_the_ID(), $meta_key, $meta_val[0] );
@@ -2543,8 +3052,34 @@
2543 3052 }
2544 3053 }
2545 3054 }
2546 3055
3056 + /**
3057 + * Fires after a template has been imported from the cloud.
3058 + *
3059 + * WDesignKit's templates live in the cloud, so nothing local records that an import
3060 + * happened — there is no post type, no option, nothing to count after the fact. This is the
3061 + * only moment the information exists.
3062 + *
3063 + * @since 2.6.4
3064 + *
3065 + * @param string $kind 'single' or 'kit'.
3066 + * @param string $builder Builder the template was imported for, e.g. 'elementor'.
3067 + * @param int $count How many templates this import brought in.
3068 + */
3069 + // Only a completed import counts. The cloud's failure shape for this endpoint family sets
3070 + // content => 'error' (see the sibling check in wdkit_import_kit_template() above) — that is
3071 + // non-empty, so the previous `||` fired the counter on failed imports too. Require success
3072 + // AND an absent/non-'error' content instead.
3073 + if ( ! empty( $response['success'] ) && ( ! isset( $response['content'] ) || 'error' !== $response['content'] ) ) {
3074 + do_action(
3075 + 'wdkit_template_imported',
3076 + 'import_kit_template' === $api_type ? 'kit' : 'single',
3077 + isset( $_POST['builder'] ) ? sanitize_key( wp_unslash( $_POST['builder'] ) ) : '',
3078 + 1
3079 + );
3080 + }
3081 +
2547 3082 wp_send_json( $response );
2548 3083 wp_die();
2549 3084 }
2550 3085
@@ -2555,8 +3090,265 @@
2555 3090 *
2556 3091 * @param array $content store media content.
2557 3092 * @param string $editor it is check editor.
2558 3093 */
3094 + /**
3095 + * Resolve a local upload URL back to its attachment ID.
3096 + *
3097 + * Handles the "-scaled" copy WordPress makes for large originals and any
3098 + * "-1920x1280" size suffix, both of which attachment_url_to_postid() misses because
3099 + * they are not the value stored in _wp_attached_file.
3100 + *
3101 + * @since 2.6.2
3102 + *
3103 + * @param string $url Local upload URL.
3104 + * @return int Attachment ID, or 0.
3105 + */
3106 + private static function wdkit_attachment_id_from_url( $url ) {
3107 + static $cache = array();
3108 +
3109 + if ( isset( $cache[ $url ] ) ) {
3110 + return $cache[ $url ];
3111 + }
3112 +
3113 + $id = (int) attachment_url_to_postid( $url );
3114 +
3115 + if ( ! $id ) {
3116 + // Try the original file behind a -scaled or -WxH derivative.
3117 + $stripped = preg_replace( '/-scaled(\.[a-z0-9]+)$/i', '$1', $url );
3118 + $stripped = preg_replace( '/-\d+x\d+(\.[a-z0-9]+)$/i', '$1', (string) $stripped );
3119 +
3120 + if ( $stripped && $stripped !== $url ) {
3121 + $id = (int) attachment_url_to_postid( $stripped );
3122 + }
3123 + }
3124 +
3125 + // Only remember hits. Page imports run concurrently, so an attachment created by a
3126 + // sibling request may not exist yet when this is first asked — caching that miss
3127 + // would keep every later control in this request pointing at nothing.
3128 + if ( $id ) {
3129 + $cache[ $url ] = $id;
3130 + }
3131 +
3132 + return $id;
3133 + }
3134 +
3135 + /**
3136 + * Is this media reference still pointing off-site?
3137 + *
3138 + * Template content arrives holding the URLs of wherever the media lived before. Those
3139 + * carry that site's attachment IDs, which have no meaning here - and can collide with
3140 + * unrelated local posts.
3141 + *
3142 + * @since 2.6.2
3143 + *
3144 + * @param string $url URL from a media control.
3145 + * @return bool True when the URL points at another site's uploads.
3146 + */
3147 + private static function wdkit_is_foreign_media_url( $url ) {
3148 +
3149 + if ( ! class_exists( 'Wdkit_Image_Guard' ) ) {
3150 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php';
3151 + }
3152 +
3153 + $uploads = wp_get_upload_dir();
3154 +
3155 + return Wdkit_Image_Guard::is_foreign_media( $url, isset( $uploads['baseurl'] ) ? $uploads['baseurl'] : '' );
3156 + }
3157 +
3158 + /**
3159 + * Find - or make - the local attachment behind a source-site media URL.
3160 + *
3161 + * Elementor stamps every image it imports with `_elementor_source_image_hash`
3162 + * (sha1 of the URL it came from), and its importer consults that before doing any
3163 + * network work. Delegating here means a URL already imported at create time resolves
3164 + * from the database, and one that never made it is fetched exactly once.
3165 + *
3166 + * Only ever called for foreign URLs. Handing it a local URL would re-download the
3167 + * file and leave a duplicate, because the stored hash is of the *remote* URL and so
3168 + * would never match.
3169 + *
3170 + * @since 2.6.2
3171 + *
3172 + * @param string $url Source-site media URL.
3173 + * @param int $source_id The source site's attachment ID, used as Elementor's cache key.
3174 + * @return array Local `id` and `url`, or an empty array when it cannot be resolved.
3175 + */
3176 + private static function wdkit_localise_media_url( $url, $source_id = 0 ) {
3177 + static $cache = array();
3178 +
3179 + if ( isset( $cache[ $url ] ) ) {
3180 + return $cache[ $url ];
3181 + }
3182 +
3183 + if ( ! did_action( 'elementor/loaded' ) || ! class_exists( '\\Elementor\\Plugin' ) ) {
3184 + return array();
3185 + }
3186 +
3187 + $images = \Elementor\Plugin::$instance->templates_manager->get_import_images_instance();
3188 +
3189 + if ( ! $images ) {
3190 + return array();
3191 + }
3192 +
3193 + // A download may happen, so keep the oversized-image guard in force.
3194 + self::wdkit_guard_oversized_images();
3195 +
3196 + $imported = $images->import(
3197 + array(
3198 + // Elementor only checks its hash table when an id is present.
3199 + 'id' => $source_id ? $source_id : 1,
3200 + 'url' => $url,
3201 + )
3202 + );
3203 +
3204 + $local = ( ! empty( $imported['id'] ) && ! empty( $imported['url'] ) )
3205 + ? array(
3206 + 'id' => (int) $imported['id'],
3207 + 'url' => $imported['url'],
3208 + )
3209 + : array();
3210 +
3211 + // Remember hits only: a sibling request importing concurrently may simply not have
3212 + // finished yet, and caching that miss would strand every later control on this page.
3213 + if ( $local ) {
3214 + $cache[ $url ] = $local;
3215 + }
3216 +
3217 + return $local;
3218 + }
3219 +
3220 + /**
3221 + * Repair dangling attachment IDs across every page of a finished import.
3222 + *
3223 + * The create-time repair in wdkit_media_import() can only see attachments that already
3224 + * exist. Pages import concurrently and share images — an icon first imported by one
3225 + * page is referenced by several others — so a page that runs early legitimately cannot
3226 + * resolve an image a sibling request has not created yet.
3227 + *
3228 + * This runs at the finalize step, once every page and attachment exists, and fixes
3229 + * whatever the per-page pass had to leave behind.
3230 + *
3231 + * @since 2.6.2
3232 + *
3233 + * @param array $page_ids Imported post IDs.
3234 + * @return int Number of pages actually rewritten.
3235 + */
3236 + private function wdkit_sweep_attachment_ids( $page_ids ) {
3237 +
3238 + if ( empty( $page_ids ) || ! did_action( 'elementor/loaded' ) ) {
3239 + return 0;
3240 + }
3241 +
3242 + $fixed = 0;
3243 + $ids = array_unique( array_map( 'intval', $page_ids ) );
3244 +
3245 + // Primes the meta cache for the whole batch in one query, so the
3246 + // get_post_meta() call below hits the cache instead of issuing one query
3247 + // per imported page.
3248 + update_meta_cache( 'post', $ids );
3249 +
3250 + foreach ( $ids as $post_id ) {
3251 +
3252 + if ( ! $post_id ) {
3253 + continue;
3254 + }
3255 +
3256 + $raw = get_post_meta( $post_id, '_elementor_data', true );
3257 +
3258 + if ( empty( $raw ) ) {
3259 + continue;
3260 + }
3261 +
3262 + $data = is_array( $raw ) ? $raw : json_decode( $raw, true );
3263 +
3264 + if ( ! is_array( $data ) ) {
3265 + continue;
3266 + }
3267 +
3268 + $repaired = self::wdkit_repair_attachment_ids( $data );
3269 +
3270 + if ( wp_json_encode( $repaired ) === wp_json_encode( $data ) ) {
3271 + continue;
3272 + }
3273 +
3274 + // Save through the document API so Elementor regenerates the page CSS — the
3275 + // background-image rules are only emitted once the IDs resolve.
3276 + $document = \Elementor\Plugin::$instance->documents->get( $post_id );
3277 +
3278 + // Count only a save that actually happened. Document::save() returns false
3279 + // without saving when the current user cannot edit the post, and reporting
3280 + // those as repaired hides the fact that nothing changed.
3281 + if ( $document && $document->save( array( 'elements' => $repaired ) ) ) {
3282 + ++$fixed;
3283 + }
3284 + }
3285 +
3286 + if ( $fixed ) {
3287 + \Elementor\Plugin::$instance->files_manager->clear_cache();
3288 + }
3289 +
3290 + return $fixed;
3291 + }
3292 +
3293 + /**
3294 + * Repair media controls whose attachment ID does not resolve.
3295 + *
3296 + * Elementor media controls store `{ url, id }`. Controls flagged `has_sizes` — the
3297 + * container/section **background image** among them — do not render from `url` at all:
3298 + * CSS generation resolves the image through the attachment ID, so a dangling ID
3299 + * produces no `background-image` rule and the section renders with no image even
3300 + * though its URL is perfectly correct.
3301 + *
3302 + * IDs arrive dangling whenever Elementor's own importer does not rewrite a control —
3303 + * it carries the source site's ID, which means nothing locally. Now that the URL is
3304 + * already a local upload before import, the ID can simply be looked up from it.
3305 + *
3306 + * @since 2.6.2
3307 + *
3308 + * @param mixed $node Elementor data, walked recursively.
3309 + * @return mixed Data with resolvable attachment IDs.
3310 + */
3311 + private static function wdkit_repair_attachment_ids( $node ) {
3312 +
3313 + if ( ! is_array( $node ) ) {
3314 + return $node;
3315 + }
3316 +
3317 + // A media control value: has a url, and an id slot to correct.
3318 + if ( isset( $node['url'] ) && is_string( $node['url'] ) && array_key_exists( 'id', $node ) ) {
3319 +
3320 + $current = (int) $node['id'];
3321 + $is_live = $current && 'attachment' === get_post_type( $current );
3322 +
3323 + if ( self::wdkit_is_foreign_media_url( $node['url'] ) ) {
3324 + // Still pointing at the source site. Ask Elementor for the local copy: its
3325 + // _elementor_source_image_hash lookup returns the attachment the create-time
3326 + // import already made, so this normally costs a single query and no download.
3327 + $local = self::wdkit_localise_media_url( $node['url'], $current );
3328 +
3329 + if ( ! empty( $local['id'] ) && ! empty( $local['url'] ) ) {
3330 + $node['id'] = $local['id'];
3331 + $node['url'] = $local['url'];
3332 + }
3333 + } elseif ( ! $is_live && false !== strpos( $node['url'], '/wp-content/uploads/' ) ) {
3334 + $resolved = self::wdkit_attachment_id_from_url( $node['url'] );
3335 +
3336 + if ( $resolved ) {
3337 + $node['id'] = $resolved;
3338 + }
3339 + }
3340 + }
3341 +
3342 + foreach ( $node as $key => $value ) {
3343 + if ( is_array( $value ) ) {
3344 + $node[ $key ] = self::wdkit_repair_attachment_ids( $value );
3345 + }
3346 + }
3347 +
3348 + return $node;
3349 + }
3350 +
2559 3351 public function wdkit_media_import( $content = array(), $editor = '' ) {
2560 3352
2561 3353 if ( empty( $content ) && empty( $editor ) ) {
2562 3354 $args = $this->wdkit_parse_args( $_POST );
@@ -2576,8 +3368,9 @@
2576 3368 if ( ! class_exists( 'Wdkit_Import_Images' ) ) {
2577 3369 require_once WDKIT_INCLUDES . 'admin/class-wdkit-import-images.php';
2578 3370 }
2579 3371
3372 +
2580 3373 if ( ! empty( $args['editor'] ) && 'gutenberg' === $args['editor'] && ! empty( $content ) ) {
2581 3374 $media_import = array( $content );
2582 3375 $media_import = self::blocks_import_media_copy_content( $media_import );
2583 3376 $content = $media_import[0];
@@ -2585,8 +3378,13 @@
2585 3378 $media_import = array( $content );
2586 3379 $media_import = self::widgets_elements_id_change( $media_import );
2587 3380 $media_import = self::widgets_import_media_copy_content( $media_import );
2588 3381 $content = $media_import[0];
3382 +
3383 + // Last: point any control Elementor left holding a foreign attachment ID at the
3384 + // local attachment its URL already refers to. Without this, has_sizes controls
3385 + // such as container background images resolve to nothing and render empty.
3386 + $content = self::wdkit_repair_attachment_ids( $content );
2589 3387 }
2590 3388
2591 3389 return $content;
2592 3390 }
@@ -2657,9 +3455,13 @@
2657 3455 $control_type = \Elementor\Plugin::instance()->controls_manager->get_control( $get_control['type'] );
2658 3456 $control_name = $get_control['name'];
2659 3457
2660 3458 if ( ! $control_type ) {
2661 - return $get_element_instance;
3459 + // Skip just this control. Returning here would abandon every control after
3460 + // it, so a single unregistered type - routine when a kit uses an addon that
3461 + // is not fully active yet - would silently leave the rest of the element's
3462 + // media pointing at the source site.
3463 + continue;
2662 3464 }
2663 3465
2664 3466 if ( method_exists( $control_type, $tp_mi_on_fun ) ) {
2665 3467 $get_element_instance['settings'][ $control_name ] = $control_type->{$tp_mi_on_fun}( $element->get_settings( $control_name ), $get_control );
@@ -2734,73 +3536,298 @@
2734 3536 public static function blocks_data_instance( array $block_data, array $args = array(), $block_args = null ) {
2735 3537
2736 3538 if ( ( isset( $block_data['name'] ) && isset( $block_data['clientId'] ) && isset( $block_data['attributes'] ) ) || ( isset( $block_data['blockName'] ) && isset( $block_data['attrs'] ) && ! empty( $block_data['attrs'] ) ) ) {
2737 3539 $blocks_attr = isset( $block_data['attributes'] ) ? $block_data['attributes'] : ( isset( $block_data['attrs'] ) ? $block_data['attrs'] : array() );
2738 - foreach ( $blocks_attr as $block_key => $block_val ) {
2739 - if ( isset( $block_val['url'] ) && isset( $block_val['id'] ) && ! empty( $block_val['url'] ) ) {
2740 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $block_val );
2741 - $blocks_attr[ $block_key ] = $new_media;
2742 - } elseif ( isset( $block_val['url'] ) && ! empty( $block_val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $block_val['url'] ) ) {
2743 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $block_val );
2744 - $blocks_attr[ $block_key ] = $new_media;
2745 - } elseif ( is_array( $block_val ) && ! empty( $block_val ) ) {
2746 - if ( ! array_key_exists( 'md', $block_val ) && ! array_key_exists( 'openTypography', $block_val ) && ! array_key_exists( 'openBorder', $block_val ) && ! array_key_exists( 'openShadow', $block_val ) && ! array_key_exists( 'openFilter', $block_val ) ) {
2747 - foreach ( $block_val as $key => $val ) {
2748 - if ( is_array( $val ) && ! empty( $val ) ) {
3540 + $blocks_attr = self::wdkit_import_block_media( $blocks_attr );
3541 + if ( isset( $block_data['attributes'] ) ) {
3542 + $block_data['attributes'] = $blocks_attr;
3543 + } elseif ( isset( $block_data['attrs'] ) ) {
3544 + $block_data['attrs'] = $blocks_attr;
3545 + }
2749 3546
2750 - if ( isset( $val['url'] ) && ( isset( $val['Id'] ) || isset( $val['id'] ) ) && ! empty( $val['url'] ) ) {
2751 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $val );
2752 - $blocks_attr[ $block_key ][ $key ] = $new_media;
2753 - } elseif ( isset( $val['url'] ) && ! empty( $val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $val['url'] ) ) {
2754 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $val );
2755 - $blocks_attr[ $block_key ][ $key ] = $new_media;
2756 - } else {
2757 - foreach ( $val as $sub_key => $sub_val ) {
2758 - if ( isset( $sub_val['url'] ) && ( isset( $sub_val['Id'] ) || isset( $sub_val['id'] ) ) && ! empty( $sub_val['url'] ) ) {
2759 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val );
3547 + $block_data = self::wdkit_relink_block_markup( $block_data );
3548 + }
2760 3549
2761 - if ( is_array( $sub_val ) && is_array( $new_media ) ) {
2762 - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = array_merge( $sub_val, $new_media );
2763 - } else {
2764 - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = $new_media;
2765 - }
2766 - } elseif ( isset( $sub_val['url'] ) && ! empty( $sub_val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $sub_val['url'] ) ) {
2767 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val );
2768 - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = $new_media;
2769 - } elseif ( is_array( $sub_val ) && ! empty( $sub_val ) ) {
2770 - foreach ( $sub_val as $sub_key1 => $sub_val1 ) {
2771 - if ( isset( $sub_val1['url'] ) && ( isset( $sub_val1['Id'] ) || isset( $sub_val1['id'] ) ) && ! empty( $sub_val1['url'] ) ) {
2772 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val1 );
3550 + return $block_data;
3551 + }
2773 3552
2774 - if ( is_array( $sub_val1 ) && is_array( $new_media ) ) {
2775 - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = array_merge( $sub_val1, $new_media );
2776 - } else {
2777 - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = $new_media;
2778 - }
2779 - } elseif ( isset( $sub_val1['url'] ) && ! empty( $sub_val1['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $sub_val1['url'] ) ) {
2780 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val1 );
2781 - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = $new_media;
2782 - }
2783 - }
2784 - }
2785 - }
2786 - }
2787 - }
2788 - }
2789 - }
3553 + /**
3554 + * Run block markup through the media import, the way the create path does.
3555 + *
3556 + * Used wherever block content is written from the browser: media import, then the Nexter
3557 + * block processor so each block's rendered copy matches its attributes, then serialise.
3558 + *
3559 + * @since 2.6.2
3560 + *
3561 + * @param string $content Block markup.
3562 + * @return string Block markup with local media.
3563 + */
3564 + private function wdkit_relink_gutenberg_content( $content ) {
3565 +
3566 + if ( ! is_string( $content ) || false === strpos( $content, '<!-- wp:' ) ) {
3567 + return $content;
3568 + }
3569 +
3570 + // wdkit_media_import() loads this itself, but it is referenced before that below.
3571 + if ( ! class_exists( 'Wdkit_Import_Images' ) ) {
3572 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-import-images.php';
3573 + }
3574 +
3575 + // Thumbnail generation decodes each image, so keep the oversized-image guard in force.
3576 + self::wdkit_guard_oversized_images();
3577 +
3578 +
3579 + // Block attributes are JSON inside the block delimiters, so they only survive a parse
3580 + // when the string carries exactly one level of escaping. Arrive with an extra level and
3581 + // parse_blocks() reads no attributes at all - serialising that back out writes every
3582 + // block bare, throwing away titles, body text, icons and styling.
3583 + $parsable = self::wdkit_parsable_block_content( $content );
3584 +
3585 + if ( null === $parsable ) {
3586 +
3587 + return $content;
3588 + }
3589 +
3590 + $blocks = parse_blocks( $parsable );
3591 + $blocks = $this->wdkit_media_import( $blocks, 'gutenberg' );
3592 +
3593 + if ( empty( $blocks ) || ! is_array( $blocks ) ) {
3594 + return $content;
3595 + }
3596 +
3597 + if ( class_exists( 'WDKIT_Nexter_Block_Processor' ) ) {
3598 + $processor = new WDKIT_Nexter_Block_Processor();
3599 + $blocks = $processor->run( $blocks );
3600 + }
3601 +
3602 + $serialised = serialize_blocks( $blocks );
3603 +
3604 + // Last line of defence. This function exists to repoint media, so a result carrying
3605 + // fewer block attributes than it started with is a broken round trip, not a rewrite.
3606 + // Leaving the media wrong is recoverable; saving gutted content is not.
3607 + $before = self::wdkit_block_attr_count( $parsable );
3608 + $after = self::wdkit_block_attr_count( $serialised );
3609 +
3610 + if ( $after < $before ) {
3611 +
3612 + return $content;
3613 + }
3614 +
3615 + // Never hand back nothing: an empty result would blank the page.
3616 + return ! empty( $serialised ) ? $serialised : $content;
3617 + }
3618 +
3619 + /**
3620 + * Rebuild the block stylesheet for a page whose content we just rewrote.
3621 + *
3622 + * The addon keeps each block's styling in a generated per-page stylesheet, and every rule
3623 + * is keyed to the block id it was written for. That file is produced when the page is
3624 + * saved through the editor - not by wp_update_post() from an AJAX handler - so rewriting
3625 + * content here leaves the page pointing at a stylesheet built for the previous markup.
3626 + * Blocks whose ids are not in that file get no rules at all and render unstyled.
3627 + *
3628 + * @since 2.6.2
3629 + *
3630 + * @param int $post_id Page whose content changed.
3631 + * @return bool True when a rebuild was triggered.
3632 + */
3633 + private static function wdkit_rebuild_block_css( $post_id ) {
3634 +
3635 + if ( ! $post_id ) {
3636 + return false;
3637 + }
3638 +
3639 + foreach ( get_declared_classes() as $class ) {
3640 + if ( ! method_exists( $class, 'make_block_css_by_post_id' ) ) {
3641 + continue;
3642 + }
3643 +
3644 + try {
3645 + if ( method_exists( $class, 'instance' ) ) {
3646 + $instance = $class::instance();
3647 + } elseif ( method_exists( $class, 'get_instance' ) ) {
3648 + $instance = $class::get_instance();
3649 + } else {
3650 + $instance = new $class();
2790 3651 }
3652 +
3653 + $instance->make_block_css_by_post_id( $post_id );
3654 +
3655 +
3656 + return true;
3657 + } catch ( \Throwable $e ) {
3658 + // Styling is best-effort: a failure here must not fail the import.
3659 +
3660 + return false;
2791 3661 }
2792 - if ( isset( $block_data['attributes'] ) ) {
2793 - $block_data['attributes'] = $blocks_attr;
2794 - } elseif ( isset( $block_data['attrs'] ) ) {
2795 - $block_data['attrs'] = $blocks_attr;
3662 + }
3663 +
3664 + return false;
3665 + }
3666 +
3667 + /**
3668 + * How many block attributes does this markup actually yield when parsed?
3669 + *
3670 + * Used as a before/after measure: block attributes are the part of block markup a round
3671 + * trip can silently drop, so counting them is how we tell a rewrite from a mangling.
3672 + *
3673 + * @since 2.6.2
3674 + *
3675 + * @param string $content Block markup.
3676 + * @return int Total attributes across every block.
3677 + */
3678 + private static function wdkit_block_attr_count( $content ) {
3679 + $total = 0;
3680 +
3681 + $walk = function ( $blocks ) use ( &$walk, &$total ) {
3682 + foreach ( $blocks as $block ) {
3683 + if ( ! empty( $block['attrs'] ) && is_array( $block['attrs'] ) ) {
3684 + $total += count( $block['attrs'] );
3685 + }
3686 +
3687 + if ( ! empty( $block['innerBlocks'] ) ) {
3688 + $walk( $block['innerBlocks'] );
3689 + }
2796 3690 }
3691 + };
3692 +
3693 + $walk( parse_blocks( (string) $content ) );
3694 +
3695 + return $total;
3696 + }
3697 +
3698 + /**
3699 + * Return this content in a form whose block attributes actually parse.
3700 + *
3701 + * Content written straight to post_content never had to parse, so an extra level of
3702 + * escaping on the way in did no harm. Parsing it - which repointing media requires - makes
3703 + * that escaping fatal: `{\"Title\":\"…\"}` is not JSON, so every attribute is discarded.
3704 + *
3705 + * Rather than assume a slash depth, this measures: if stripping one level yields more
3706 + * attributes, the content was over-escaped and the stripped form is the real one.
3707 + *
3708 + * @since 2.6.2
3709 + *
3710 + * @param string $content Block markup as received.
3711 + * @return string|null Markup safe to parse, or null when no form of it parses.
3712 + */
3713 + private static function wdkit_parsable_block_content( $content ) {
3714 +
3715 + $as_is = self::wdkit_block_attr_count( $content );
3716 +
3717 + // Nothing claims to carry attributes, so there is nothing to lose either.
3718 + if ( false === strpos( $content, '{' ) ) {
3719 + return $content;
2797 3720 }
2798 3721
3722 + $stripped = wp_unslash( $content );
3723 + $stripped_attrs = self::wdkit_block_attr_count( $stripped );
3724 +
3725 + if ( $stripped_attrs > $as_is ) {
3726 + return $stripped;
3727 + }
3728 +
3729 + if ( $as_is > 0 ) {
3730 + return $content;
3731 + }
3732 +
3733 + // Neither form parses into attributes even though the markup contains JSON: better to
3734 + // leave the content exactly as it arrived than to rewrite it into something bare.
3735 + return null;
3736 + }
3737 +
3738 + /**
3739 + * Point a block's saved markup at the media that was just localised.
3740 + *
3741 + * A block stores a rendered copy of itself in `innerHTML` / `innerContent`, and for many
3742 + * blocks that copy is what the front end actually outputs. Importing the attributes alone
3743 + * therefore fixes the editor while leaving the page still loading from the site the
3744 + * template came from - and those hosts answer 403, so the image renders broken.
3745 + *
3746 + * @since 2.6.2
3747 + *
3748 + * @param array $block_data One parsed block.
3749 + * @return array The block with its markup repointed.
3750 + */
3751 + private static function wdkit_relink_block_markup( $block_data ) {
3752 +
3753 + $map = Wdkit_Import_Images::get_url_map();
3754 +
3755 + if ( empty( $map ) ) {
3756 + return $block_data;
3757 + }
3758 +
3759 + $from = array_keys( $map );
3760 + $to = array_values( $map );
3761 +
3762 + if ( ! empty( $block_data['innerHTML'] ) && is_string( $block_data['innerHTML'] ) ) {
3763 + $block_data['innerHTML'] = str_replace( $from, $to, $block_data['innerHTML'] );
3764 + }
3765 +
3766 + if ( ! empty( $block_data['innerContent'] ) && is_array( $block_data['innerContent'] ) ) {
3767 + foreach ( $block_data['innerContent'] as $index => $chunk ) {
3768 + if ( is_string( $chunk ) ) {
3769 + $block_data['innerContent'][ $index ] = str_replace( $from, $to, $chunk );
3770 + }
3771 + }
3772 + }
3773 +
2799 3774 return $block_data;
2800 3775 }
2801 3776
2802 3777 /**
3778 + * Import every media reference held in a block's attributes.
3779 + *
3780 + * Block attributes nest arbitrarily - a repeater of cards each with an image, responsive
3781 + * variants, nested inner settings - so this recurses rather than reaching a fixed number
3782 + * of levels down. The previous version was unrolled exactly four levels deep and also
3783 + * skipped any subtree carrying an `md` key, which meant anything below that simply kept
3784 + * the source site's URL and attachment ID and rendered as an empty placeholder.
3785 + *
3786 + * A node counts as media when it has a non-empty string `url` and either an id slot or a
3787 + * URL that names an image file. That pairing is what distinguishes a media control from
3788 + * a link, which also carries a `url`.
3789 + *
3790 + * @since 2.6.2
3791 + *
3792 + * @param mixed $node Block attributes, walked recursively.
3793 + * @return mixed Attributes with local media.
3794 + */
3795 + private static function wdkit_import_block_media( $node ) {
3796 +
3797 + if ( ! is_array( $node ) ) {
3798 + return $node;
3799 + }
3800 +
3801 + $url = isset( $node['url'] ) && is_string( $node['url'] ) ? $node['url'] : '';
3802 +
3803 + if ( '' !== $url
3804 + && ( array_key_exists( 'id', $node ) || array_key_exists( 'Id', $node )
3805 + || preg_match( '/\.(?:jpe?g|png|gif|svg|webp|avif|bmp)$/i', (string) wp_parse_url( $url, PHP_URL_PATH ) ) )
3806 + ) {
3807 + $imported = Wdkit_Import_Images::wdkit_Import_media( $node );
3808 +
3809 + // Only accept a real result. The importer returns the node untouched when it
3810 + // cannot localise the file, and anything falsy here would wipe out the URL and
3811 + // leave the block with no image at all.
3812 + if ( ! empty( $imported['url'] ) ) {
3813 + $node = array_merge( $node, $imported );
3814 + }
3815 + }
3816 +
3817 + // Keep walking even after importing this node. A media value carries its own `sizes`
3818 + // map of per-size URLs, and returning here left every one of those pointing at the
3819 + // site the template came from - which is what the widgets actually render from.
3820 + foreach ( $node as $key => $value ) {
3821 + if ( is_array( $value ) ) {
3822 + $node[ $key ] = self::wdkit_import_block_media( $value );
3823 + }
3824 + }
3825 +
3826 + return $node;
3827 + }
3828 +
3829 + /**
2803 3830 * Kit Template Import Pages/Sections
2804 3831 *
2805 3832 * @since 1.0.0
2806 3833 * */
@@ -2879,8 +3906,28 @@
2879 3906 $output['description'] = $response['description'];
2880 3907 $output['data'] = $result;
2881 3908 $output['success'] = $response['success'];
2882 3909
3910 + // Counts the IMPORT ACTION, not what it brought in. A kit import always counts as 1 kit,
3911 + // no matter how many blocks/pages that kit contains — confirmed live: a single gutenberg
3912 + // kit import recorded total=680, kinds.kit=680, because $template_ids for that call was a
3913 + // 680-element array of the kit's own blocks and count( $template_ids ) counted every one of
3914 + // them. A page-kit's *size* is not tracking's concern; "was a kit imported" is.
3915 + //
3916 + // The 'single' branch keeps a defensive fallback for the one shape this endpoint's own
3917 + // $template_ids reliably takes when it is not a kit — a single {id, name, slug, thumb...}
3918 + // object — where count() would likewise count JSON keys instead of "1 template imported".
3919 + if ( ! empty( $output['success'] ) ) {
3920 + $is_kit = ( '' !== $website_kit );
3921 + $import_count = $is_kit ? 1 : ( isset( $template_ids['id'] ) ? 1 : ( is_array( $template_ids ) ? count( $template_ids ) : 1 ) );
3922 + do_action(
3923 + 'wdkit_template_imported',
3924 + $is_kit ? 'kit' : 'single',
3925 + sanitize_key( $builder ),
3926 + $import_count
3927 + );
3928 + }
3929 +
2883 3930 wp_send_json( $output );
2884 3931 wp_die();
2885 3932 }
2886 3933
@@ -2987,9 +4034,17 @@
2987 4034 );
2988 4035
2989 4036 $response = WDesignKit_Data_Query::get_data( $api_type, $temp_args );
2990 4037
2991 - if ( 'error' === $response['content'] ) {
4038 + if ( is_wp_error( $response ) ) {
4039 + wp_send_json( array(
4040 + 'success' => false,
4041 + 'message' => $response->get_error_message(),
4042 + ) );
4043 + wp_die();
4044 + }
4045 +
4046 + if ( isset( $response['content'] ) && 'error' === $response['content'] ) {
2992 4047 wp_send_json( $response );
2993 4048 wp_die();
2994 4049 }
2995 4050
@@ -3057,8 +4112,14 @@
3057 4112 }
3058 4113
3059 4114 $document = \Elementor\Plugin::$instance->documents->get($template_id);
3060 4115
4116 + // This saves content posted straight from the browser, which carries local image
4117 + // URLs but still the source template's attachment IDs. Without repairing them the
4118 + // save undoes what wdkit_media_import() fixed on create, and has_sizes controls —
4119 + // container background images especially — resolve to nothing and render empty.
4120 + $content = self::wdkit_repair_attachment_ids( $content );
4121 +
3061 4122 $document->save([
3062 4123 'elements' => $content
3063 4124 ]);
3064 4125 }
@@ -3063,8 +4124,94 @@
3063 4124 ]);
3064 4125 }
3065 4126
3066 4127 /**
4128 + * Update the content of an already-created page.
4129 + *
4130 + * Used by the async ("Site Ready first") import path: pages are created up front with
4131 + * their un-rewritten template content, then this writes the AI-rewritten content into
4132 + * each page in the background. Elementor saves via the document API (same as
4133 + * wkit_update_elementor_template); Gutenberg writes post_content directly.
4134 + *
4135 + * @since 2.6.2
4136 + */
4137 + protected function wdkit_update_page_content() {
4138 + $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
4139 + $builder = isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : '';
4140 +
4141 + if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
4142 + return array(
4143 + 'success' => false,
4144 + 'message' => esc_html__( 'Invalid page or insufficient permission', 'wdesignkit' ),
4145 + );
4146 + }
4147 +
4148 + if ( 'gutenberg' === $builder ) {
4149 + // Do NOT run kses here: Gutenberg block delimiters are HTML comments
4150 + // (<!-- wp:... -->) which kses strips. Mirror the create path, which stores
4151 + // the block markup slashed and unfiltered (endpoint is manage_options-gated
4152 + // and the content is plugin-generated).
4153 + $content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : '';
4154 +
4155 + // This content comes straight from the browser and still carries the template
4156 + // site's media URLs and attachment IDs, so it has to go through the same pipeline
4157 + // the create path uses. Without this the save simply undid the import: the files
4158 + // were fetched, then overwritten by a copy still pointing at the source site.
4159 + //
4160 + // Re-running is cheap. Every URL already handled resolves from the source-hash
4161 + // lookup, and media that is already local resolves straight from its URL, so no
4162 + // image is fetched or stored twice.
4163 + $content = $this->wdkit_relink_gutenberg_content( $content );
4164 +
4165 + $result = wp_update_post(
4166 + array(
4167 + 'ID' => $post_id,
4168 + 'post_content' => wp_slash( $content ),
4169 + ),
4170 + true
4171 + );
4172 +
4173 + if ( is_wp_error( $result ) ) {
4174 + return array(
4175 + 'success' => false,
4176 + 'message' => $result->get_error_message(),
4177 + );
4178 + }
4179 +
4180 + self::wdkit_rebuild_block_css( $post_id );
4181 + } else {
4182 + $elements = isset( $_POST['content'] ) ? json_decode( wp_unslash( $_POST['content'] ), true ) : array();
4183 +
4184 + if ( ! class_exists( '\\Elementor\\Plugin' ) ) {
4185 + return array(
4186 + 'success' => false,
4187 + 'message' => esc_html__( 'Elementor not available', 'wdesignkit' ),
4188 + );
4189 + }
4190 +
4191 + $document = \Elementor\Plugin::$instance->documents->get( $post_id );
4192 + if ( ! $document ) {
4193 + return array(
4194 + 'success' => false,
4195 + 'message' => esc_html__( 'Elementor document not found', 'wdesignkit' ),
4196 + );
4197 + }
4198 +
4199 + // Same as wkit_update_elementor_template(): browser-posted content keeps the
4200 + // source template's attachment IDs, so repair them or this save undoes the
4201 + // create-time fix and background images stop rendering.
4202 + $elements = self::wdkit_repair_attachment_ids( $elements );
4203 +
4204 + $document->save( array( 'elements' => $elements ) );
4205 + }
4206 +
4207 + return array(
4208 + 'success' => true,
4209 + 'message' => esc_html__( 'Page content updated', 'wdesignkit' ),
4210 + );
4211 + }
4212 +
4213 + /**
3067 4214 * Import single template and section from plugin only
3068 4215 *
3069 4216 * @param array $args store data.
3070 4217 * @param array $template_id store data.
@@ -3072,8 +4219,21 @@
3072 4219 * @param array $temp_data store data.
3073 4220 * */
3074 4221 protected function import_page_section_content() {
3075 4222
4223 + // Elementor sideloads every image referenced by the page from inside this request.
4224 + // A single oversized source image decodes to more than the whole memory limit, so
4225 + // guard before any of that starts.
4226 + $this->wdkit_guard_oversized_images();
4227 +
4228 + // Sideloading images for image-heavy pages (wdkit_media_import → Imagick
4229 + // thumbnail generation per image) can exceed the default 30s execution
4230 + // limit and fatal the request mid-import. Give this single page import
4231 + // more headroom; harmless no-op where set_time_limit() is disabled.
4232 + if ( function_exists( 'set_time_limit' ) ) {
4233 + @set_time_limit( 120 );
4234 + }
4235 +
3076 4236 if ( isset( $_POST['args'] ) ) {
3077 4237 $args = ! empty( $_POST['args'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['args'] ) ), true ) : array();
3078 4238 }
3079 4239
@@ -3096,11 +4256,9 @@
3096 4256 if ( isset( $_POST['template_id'] ) ) {
3097 4257 $template_id = ! empty( $_POST['template_id'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['template_id'] ), true ) ) : '';
3098 4258 }
3099 4259
3100 - if ( isset( $_POST['temp_type'] ) ) {
3101 - $temp_type = isset( $_POST['temp_type'] ) ? sanitize_text_field( wp_unslash( $_POST['temp_type'] ) ) : 'normal';
3102 - }
4260 + $temp_type = isset( $_POST['temp_type'] ) ? sanitize_text_field( wp_unslash( $_POST['temp_type'] ) ) : 'normal';
3103 4261
3104 4262 if ( isset( $_POST['data'] ) ) {
3105 4263 $data = ! empty( $_POST['data'] ) ? json_decode( wp_unslash( $_POST['data'] ) ) : '';
3106 4264 }
@@ -3121,8 +4279,21 @@
3121 4279 }
3122 4280
3123 4281 if ( ! empty( $data ) && ! empty( $template_id ) && ! empty( $post_type ) && current_user_can( 'manage_options' ) ) {
3124 4282 $post_content = $data;
4283 + // Restore The Plus Addons' globals before the page is built, so the widgets'
4284 + // tp_global_preset references resolve as soon as it renders. Done here rather
4285 + // than in the save-template UI's confirmation dialog so that every import path
4286 + // - the library, the abilities, the theme builder - gets it.
4287 + if ( isset( $post_content->tp_globals ) && ! empty( $post_content->tp_globals ) ) {
4288 + $this->wdkit_merge_tp_globals(
4289 + json_decode( wp_json_encode( $post_content->tp_globals ), true ),
4290 + isset( $post_content->tp_global_refs )
4291 + ? json_decode( wp_json_encode( $post_content->tp_global_refs ), true )
4292 + : array()
4293 + );
4294 + }
4295 +
3125 4296 $post_title = isset( $post_content->title ) ? sanitize_text_field( $post_content->title ) : '';
3126 4297 $post_slug = isset( $post_content->slug ) ? sanitize_text_field( $post_content->slug ) : '';
3127 4298 $file_type = isset( $post_content->file_type ) ? sanitize_text_field( $post_content->file_type ) : '';
3128 4299 $content = isset( $post_content->content ) ? wp_slash( $post_content->content ) : '';
@@ -3132,9 +4303,9 @@
3132 4303 if ( empty( $content ) ) {
3133 4304 wp_send_json(
3134 4305 array(
3135 4306 'template_id' => $template_id,
3136 - 'message' => 'Content is Empty.',
4307 + 'message' => __( 'Content is Empty.', 'wdesignkit' ),
3137 4308 )
3138 4309 );
3139 4310 wp_die();
3140 4311 } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'wp_block' === $file_type ) {
@@ -3140,27 +4311,17 @@
3140 4311 } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'wp_block' === $file_type ) {
3141 4312
3142 4313 $editor = ( 'wdkit' === $args['editor'] ) ? 'gutenberg' : $args['editor'];
3143 4314 $blocks = parse_blocks( stripslashes( $content ) );
3144 -
4315 +
3145 4316 $blocks = $this->wdkit_media_import( $blocks, $editor );
3146 4317
3147 4318 $processor = new WDKIT_Nexter_Block_Processor();
3148 4319 $blocks = $processor->run( $blocks );
3149 4320 $content = serialize_blocks( $blocks );
3150 -
4321 +
3151 4322 $content = $this->replace_unicode_glitch( serialize_blocks( $blocks ) );
3152 4323
3153 - if ( ! empty( $category_list ) && is_array( $category_list ) ) {
3154 - $category_ids = array_map( 'intval', $category_list );
3155 - wp_set_post_terms( $inserted_id, $category_ids, 'category' );
3156 - }
3157 -
3158 - if ( ! empty( $tag_list ) && is_array( $tag_list ) ) {
3159 - $tag_ids = array_map( 'intval', $tag_list );
3160 - wp_set_post_terms( $inserted_id, $tag_ids, 'post_tag' );
3161 - }
3162 -
3163 4324 $inserted_post = wp_insert_post(
3164 4325 array(
3165 4326 'post_status' => 'publish',
3166 4327 'post_type' => $post_type,
@@ -3179,9 +4340,9 @@
3179 4340 );
3180 4341 wp_die();
3181 4342 }
3182 4343
3183 - if ( ! empty( $thumb_image ) ) {
4344 + if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) {
3184 4345 // $featured_image_url = esc_url_raw( $thumb_image );
3185 4346 $tmp = download_url( $thumb_image );
3186 4347 if ( is_wp_error( $tmp ) ) {
3187 4348 error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) );
@@ -3217,9 +4378,9 @@
3217 4378 $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : '';
3218 4379 if ( ! empty( $custom_meta ) ) {
3219 4380 foreach ( $custom_meta as $meta_key => $meta_val ) {
3220 4381 if ( isset( $meta_val[0] ) && ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) {
3221 - $meta_val[0] = maybe_unserialize( $meta_val[0] );
4382 + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) );
3222 4383 }
3223 4384
3224 4385 if ( '' === get_post_meta( $inserted_post, $meta_key, true ) && isset( $meta_val[0] ) ) {
3225 4386 add_post_meta( $inserted_post, $meta_key, $meta_val[0] );
@@ -3251,13 +4412,20 @@
3251 4412 Tpgb_Library()->remove_backend_dir_files();
3252 4413 }
3253 4414
3254 4415 clean_post_cache( $inserted_post );
4416 +
4417 + // This whole method imports exactly one section per call — unlike
4418 + // wdkit_import_template()/wdkit_import_kit_template(), it never fired this hook
4419 + // at all, so single-section imports (Header/Footer/CTA/etc., a primary import
4420 + // path per the Template Type sidebar) were invisible to tracking entirely.
4421 + do_action( 'wdkit_template_imported', 'single', sanitize_key( $editor ), 1 );
4422 +
3255 4423 wp_send_json(
3256 4424 array(
3257 4425 $temp_id => $temp_detail,
3258 4426 'description' => 'Yay! Your Section has been Successfully Imported.',
3259 - 'message' => 'Successfully Imported.',
4427 + 'message' => __( 'Successfully Imported.', 'wdesignkit' ),
3260 4428 'inserted_id' => $inserted_post,
3261 4429 'success' => true,
3262 4430 )
3263 4431 );
@@ -3268,9 +4436,9 @@
3268 4436 if ( empty( $content ) ) {
3269 4437 wp_send_json(
3270 4438 array(
3271 4439 'template_id' => $template_id,
3272 - 'message' => 'Content is Empty.',
4440 + 'message' => __( 'Content is Empty.', 'wdesignkit' ),
3273 4441 )
3274 4442 );
3275 4443 wp_die();
3276 4444 } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'elementor' === $file_type ) {
@@ -3308,9 +4476,9 @@
3308 4476 }
3309 4477
3310 4478 $inserted_id = $new_document->get_main_id();
3311 4479
3312 - if ( ! empty( $thumb_image ) ) {
4480 + if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) {
3313 4481 // $featured_image_url = esc_url_raw( $thumb_image );
3314 4482 $tmp = download_url( $thumb_image );
3315 4483 if ( is_wp_error( $tmp ) ) {
3316 4484 error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) );
@@ -3364,9 +4532,9 @@
3364 4532 $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : '';
3365 4533 if ( ! empty( $custom_meta ) ) {
3366 4534 foreach ( $custom_meta as $meta_key => $meta_val ) {
3367 4535 if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) {
3368 - $meta_val[0] = maybe_unserialize( $meta_val[0] );
4536 + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) );
3369 4537 }
3370 4538 if ( '' === get_post_meta( $inserted_id, $meta_key, true ) ) {
3371 4539 add_post_meta( $inserted_id, $meta_key, $meta_val[0] );
3372 4540 }
@@ -3390,14 +4558,18 @@
3390 4558 }
3391 4559
3392 4560 \Elementor\Plugin::$instance->files_manager->clear_cache();
3393 4561
4562 + // See the matching note in the Gutenberg branch above — this method never
4563 + // fired the tracking hook for either editor.
4564 + do_action( 'wdkit_template_imported', 'single', 'elementor', 1 );
4565 +
3394 4566 wp_send_json(
3395 4567 array(
3396 4568 $temp_id => $temp_detail,
3397 4569 'content' => $temp_con,
3398 4570 'description' => 'Yay! Your Section has been Successfully Imported.',
3399 - 'message' => 'Successfully Imported.',
4571 + 'message' => __( 'Successfully Imported.', 'wdesignkit' ),
3400 4572 'inserted_id' => $inserted_id,
3401 4573 'success' => true,
3402 4574 )
3403 4575 );
@@ -3527,8 +4699,12 @@
3527 4699 }
3528 4700
3529 4701 $image_url = esc_url_raw( $_POST['image_url'] );
3530 4702
4703 + if ( ! wdesignkit_validate_external_url( $image_url ) ) {
4704 + wp_send_json_error( 'Image could not be downloaded.' );
4705 + }
4706 +
3531 4707 $tmp_file = download_url( $image_url );
3532 4708 if ( is_wp_error( $tmp_file ) ) {
3533 4709 wp_send_json_error( 'Image could not be downloaded.' );
3534 4710 }
@@ -3564,9 +4740,21 @@
3564 4740
3565 4741 $upload_dir = wp_upload_dir();
3566 4742 $result_urls = array();
3567 4743
4744 + $colour_index = 0;
3568 4745 foreach ( $img_colors as $name => $rgb ) {
4746 + // $name is a key from the posted colours payload and went straight into the output
4747 + // filename, so traversal sequences in it steered imagepng() outside the upload
4748 + // directory (CWE-22, ClickUp 86d41ced6). sanitize_file_name() flattens it to one
4749 + // path segment; a key made only of dots/separators sanitizes to empty, so fall back
4750 + // to a positional index rather than writing to a bare "colored--<time>.png".
4751 + ++$colour_index;
4752 + $safe_name = sanitize_file_name( (string) $name );
4753 + if ( '' === $safe_name ) {
4754 + $safe_name = 'colour-' . $colour_index;
4755 + }
4756 +
3569 4757 $new = imagecreatetruecolor( $width, $height );
3570 4758 imagesavealpha( $new, true );
3571 4759 imagealphablending( $new, false );
3572 4760
@@ -3592,9 +4780,9 @@
3592 4780 imagesetpixel( $new, $x, $y, $color );
3593 4781 }
3594 4782 }
3595 4783
3596 - $filename = 'colored-' . $name . '-' . time() . '.png';
4784 + $filename = 'colored-' . $safe_name . '-' . time() . '.png';
3597 4785 $filepath = $upload_dir['path'] . '/' . $filename;
3598 4786
3599 4787 imagepng( $new, $filepath );
3600 4788 imagedestroy( $new );
@@ -3810,8 +4998,13 @@
3810 4998 $page_information = isset( $_POST['page_information'] ) ? sanitize_text_field( wp_unslash( $_POST['page_information'] ) ) : '';
3811 4999 $page_information = json_decode( $page_information, true );
3812 5000
3813 5001 if ( ! empty( $page_information ) && is_array( $page_information ) ) {
5002 +
5003 + // Every page and attachment now exists, so resolve any image ID the per-page
5004 + // pass could not (siblings import concurrently and share icons).
5005 + $this->wdkit_sweep_attachment_ids( wp_list_pluck( $page_information, 'inserted_id' ) );
5006 +
3814 5007 // Step 1: banavo mapping [ old_id => new_id ]
3815 5008 $id_mapping = array();
3816 5009 foreach ( $page_information as $page_info ) {
3817 5010 if ( ! empty( $page_info['old_page_id'] ) ) {
@@ -3904,12 +5097,73 @@
3904 5097 }
3905 5098
3906 5099 $response = json_decode( wp_json_encode( $response['data'] ), true );
3907 5100
5101 + $this->wdkit_cache_cloud_usage( $response );
5102 +
3908 5103 wp_send_json( $response );
3909 5104 wp_die();
3910 5105 }
3911 5106
5107 + /**
5108 + * Caches the storage / credit figures this response carried.
5109 + *
5110 + * This handler is the ONLY place those numbers ever exist on the site: the cloud endpoint
5111 + * authenticates with a user token that only a logged-in dashboard request carries, so the
5112 + * analytics heartbeat — which runs on cron with no user at all — can never fetch them itself.
5113 + * Caching them here is what lets Posimyth_Tracker_WDK report them, and it reports the cache's
5114 + * age alongside so a stale reading is recognisable as one.
5115 + *
5116 + * Field names are probed rather than assumed: the cloud has renamed these before, and the
5117 + * licence ability already carries six spellings of its own key field for the same reason. An
5118 + * unrecognised shape simply caches nothing rather than storing a wrong number.
5119 + *
5120 + * Only the figures are kept. No token, no account id, no email — the analytics consent copy
5121 + * promises non-sensitive data only, and this is read by the payload builder.
5122 + *
5123 + * @since 2.6.4
5124 + *
5125 + * @param mixed $data Decoded `data` object from the credits endpoint.
5126 + * @return void
5127 + */
5128 + private function wdkit_cache_cloud_usage( $data ) {
5129 + if ( ! is_array( $data ) ) {
5130 + return;
5131 + }
5132 +
5133 + $pick = static function ( $source, array $fields ) {
5134 + foreach ( $fields as $field ) {
5135 + if ( isset( $source[ $field ] ) && is_numeric( $source[ $field ] ) ) {
5136 + return (float) $source[ $field ];
5137 + }
5138 + }
5139 + return null;
5140 + };
5141 +
5142 + $usage = array(
5143 + 'storage_used' => $pick( $data, array( 'used_storage', 'storage_used', 'used_space' ) ),
5144 + 'storage_total' => $pick( $data, array( 'total_storage', 'storage_total', 'storage', 'total_space' ) ),
5145 + 'credit_used' => $pick( $data, array( 'used_credit', 'credit_used', 'used_credits' ) ),
5146 + 'credit_total' => $pick( $data, array( 'total_credit', 'credit_total', 'credits', 'real_credit' ) ),
5147 + );
5148 +
5149 + $usage = array_filter(
5150 + $usage,
5151 + static function ( $value ) {
5152 + return null !== $value;
5153 + }
5154 + );
5155 +
5156 + if ( empty( $usage ) ) {
5157 + return;
5158 + }
5159 +
5160 + $usage['cached_at'] = gmdate( 'Y-m-d H:i:s' );
5161 +
5162 + // Not autoloaded: read once a week by the heartbeat, never on a front-end request.
5163 + update_option( 'wdkit_cloud_usage', $usage, false );
5164 + }
5165 +
3912 5166 public function wdkit_nxt_thembuilder_reset() {
3913 5167 $post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : '';
3914 5168 $sections_layout = get_post_meta( $post_id, 'nxt-hooks-layout-sections', true );
3915 5169
@@ -4021,9 +5275,10 @@
4021 5275 protected function wdkit_activate_key() {
4022 5276 $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
4023 5277 $response = '';
4024 5278
4025 - if ( empty( $user_email ) ) {
5279 + // Bug C fix: variable was $user_email but only $email is set above — always triggered empty() guard.
5280 + if ( empty( $email ) ) {
4026 5281 $response = array(
4027 5282 'message' => $this->e_msg_login,
4028 5283 'description' => $this->e_desc_login,
4029 5284 'success' => false,
@@ -4241,10 +5496,35 @@
4241 5496 'description' => esc_html__( 'widget JSON file not found.', 'wdesignkit' ),
4242 5497 );
4243 5498 }
4244 5499
4245 - $json_path = WDKIT_BUILDER_PATH . "/{$widget_type}/{$folder_name}/{$file_name}";
5500 + // Read-side twin of the write and delete traversals fixed in 86d41cckh / 86d41ccz2: all
5501 + // three segments arrive from $_POST with only wp_unslash() applied — which strips
5502 + // nothing path-relevant — so "../" in any of them walked out of the builder directory
5503 + // and this handler returned the decoded contents of any .json file the web server user
5504 + // could read (CWE-22, ClickUp 86d41zaun).
5505 + $safe_path = wdesignkit_widget_path_guard( $widget_type, $folder_name, $file_name );
4246 5506
5507 + if ( false === $safe_path || '' === $safe_path['folder'] || '' === $safe_path['file'] ) {
5508 + return array(
5509 + 'success' => false,
5510 + 'message' => esc_html__( 'Widget JSON not found', 'wdesignkit' ),
5511 + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ),
5512 + );
5513 + }
5514 +
5515 + $json_path = $safe_path['base'];
5516 +
5517 + // Re-check the resolved file: the component guard above cannot see a symlink. Returns
5518 + // false for a path that does not exist, which is the same answer we want anyway.
5519 + if ( ! wdesignkit_path_inside_builder_dir( "$json_path.json" ) ) {
5520 + return array(
5521 + 'success' => false,
5522 + 'message' => esc_html__( 'Widget JSON not found', 'wdesignkit' ),
5523 + 'description' => esc_html__( 'widget JSON file not found.', 'wdesignkit' ),
5524 + );
5525 + }
5526 +
4247 5527 $json_data = wp_json_file_decode( "$json_path.json" );
4248 5528 if ( ! empty( $json_data ) ) {
4249 5529 $result = (object) array(
4250 5530 'success' => true,
@@ -4270,9 +5550,9 @@
4270 5550 *
4271 5551 * @since 1.0.0
4272 5552 */
4273 5553 protected function wdkit_download_widget() {
4274 - $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'widget_info', 'none' ) : '';
5554 + $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_extract_post_field( $_POST, 'widget_info', 'none' ) : '';
4275 5555 $data = json_decode( stripslashes( $data ) );
4276 5556
4277 5557 $array_data = array(
4278 5558 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '',
@@ -4277,8 +5557,10 @@
4277 5557 $array_data = array(
4278 5558 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '',
4279 5559 'type' => isset( $data->type ) ? sanitize_text_field( $data->type ) : '',
4280 5560 'w_unique' => isset( $data->w_uniq ) ? sanitize_text_field( $data->w_uniq ) : '',
5561 + // Bug F fix: u_id (widget owner's user ID) was missing — cloud cannot locate the widget without it.
5562 + 'u_id' => isset( $data->u_id ) ? sanitize_text_field( $data->u_id ) : '',
4281 5563 );
4282 5564
4283 5565 $response = $this->wkit_api_call( $array_data, 'save_widget' );
4284 5566 $success = ! empty( $response['success'] ) ? $response['success'] : false;
@@ -4313,10 +5595,11 @@
4313 5595
4314 5596 $img_url = ! empty( $response['data']['image'] ) ? $response['data']['image'] : '';
4315 5597 $json_data = ! empty( $response['data']['json'] ) ? json_decode( $response['data']['json'], true ) : '';
4316 5598
5599 + // Bug E fix (part 1): $responce was a typo of $response — sent undefined variable (null) to frontend.
4317 5600 if ( empty( $response['success'] ) ) {
4318 - wp_send_json( $responce );
5601 + wp_send_json( $response );
4319 5602 wp_die();
4320 5603 }
4321 5604
4322 5605 if ( empty( $img_url ) && empty( $json_data ) ) {
@@ -4337,14 +5620,34 @@
4337 5620 if ( ! is_array( $json_data ) ) {
4338 5621 $json_data = json_decode( $json_data, true );
4339 5622 }
4340 5623
4341 - $title = ! empty( $json_data['widget_data']['widgetdata']['name'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['name'] ) : '';
4342 - $builder = ! empty( $json_data['widget_data']['widgetdata']['type'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['type'] ) : '';
4343 - $w_uniq = ! empty( $json_data['widget_data']['widgetdata']['widget_id'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['widget_id'] ) : '';
5624 + // Sanitize as filenames before use in the widget path (CWE-22): sanitize_file_name()
5625 + // on name/id and sanitize_key() + allowlist on the builder strip path separators and
5626 + // dots so a crafted cloud response cannot escape WDKIT_BUILDER_PATH.
5627 + $title = ! empty( $json_data['widget_data']['widgetdata']['name'] ) ? sanitize_file_name( $json_data['widget_data']['widgetdata']['name'] ) : '';
5628 + $builder = ! empty( $json_data['widget_data']['widgetdata']['type'] ) ? sanitize_key( $json_data['widget_data']['widgetdata']['type'] ) : '';
5629 + $w_uniq = ! empty( $json_data['widget_data']['widgetdata']['widget_id'] ) ? sanitize_file_name( $json_data['widget_data']['widgetdata']['widget_id'] ) : '';
4344 5630
4345 - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq;
4346 - $file_name = str_replace( ' ', '_', $title ) . '_' . $w_uniq;
5631 + $allowed_builders = array( 'elementor', 'gutenberg', 'gutenberg_core', 'bricks' );
5632 + if ( '' === $title || '' === $w_uniq || ! in_array( $builder, $allowed_builders, true ) ) {
5633 + $responce = (object) array(
5634 + 'success' => false,
5635 + 'message' => esc_html__( 'Operation Failed!', 'wdesignkit' ),
5636 + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ),
5637 + );
5638 +
5639 + wp_send_json( $responce );
5640 + wp_die();
5641 + }
5642 +
5643 + // Canonical helpers replace spaces BEFORE sanitize_file_name(). $title above is
5644 + // already sanitized, which collapsed spaces to hyphens and left the underscore pass
5645 + // with nothing to do — a multi-word title wrote "My-Widget_id.json" next to the
5646 + // "My_Widget_id.php" the builder's save path writes. The loader pairs the two by
5647 + // swapping .php for .json, so the widget was silently dropped (ClickUp 86d41cck5).
5648 + $folder_name = wdesignkit_widget_folder_name( $title, $w_uniq );
5649 + $file_name = wdesignkit_widget_file_name( $title, $w_uniq );
4347 5650 $builder_type_path = WDKIT_BUILDER_PATH . "/{$builder}/";
4348 5651
4349 5652 if ( ! is_dir( $builder_type_path ) ) {
4350 5653 wp_mkdir_p( $builder_type_path );
@@ -4354,17 +5657,30 @@
4354 5657 wp_mkdir_p( $builder_type_path . $folder_name );
4355 5658 }
4356 5659
4357 5660 if ( ! empty( $img_url ) ) {
4358 - $img_body = wp_remote_get( $img_url );
4359 - $img_ext = pathinfo( $img_url )['extension'];
5661 + // SSRF guard (CWE-918): validate the resolved host before fetching.
5662 + $img_body = wdesignkit_safe_remote_get( $img_url );
5663 + if ( ! is_wp_error( $img_body ) ) {
5664 + // The remote extension was written verbatim here, so a cloud response naming a
5665 + // ".php" image put executable PHP in the builder directory (CWE-434,
5666 + // ClickUp 86d41cczd). An empty return means the bytes are not an image.
5667 + $img_ext = wdesignkit_safe_image_extension( $img_url, $img_body['body'] );
4360 5668
4361 - $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] );
4362 - $json_data['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext";
5669 + if ( '' !== $img_ext ) {
5670 + $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] );
5671 + $json_data['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext";
5672 + }
5673 + }
4363 5674 }
4364 5675
5676 + if ( function_exists( 'wdesignkit_invalidate_widget_registry' ) ) {
5677 + wdesignkit_invalidate_widget_registry( $builder );
5678 + }
5679 +
5680 + // Bug E fix (part 2): success was hardcoded false on the successful download path — always reported failure.
4365 5681 $result = (object) array(
4366 - 'success' => false,
5682 + 'success' => true,
4367 5683 'message' => ! empty( $response['message'] ) ? $response['message'] : esc_html__( 'no message', 'wdesignkit' ),
4368 5684 'description' => '',
4369 5685 'json' => wp_json_encode( $json_data ),
4370 5686 );
@@ -4379,9 +5695,9 @@
4379 5695 *
4380 5696 * @since 1.0.0
4381 5697 */
4382 5698 protected function wdkit_add_widget() {
4383 - $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'widget_info', 'none' ) : '';
5699 + $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_extract_post_field( $_POST, 'widget_info', 'none' ) : '';
4384 5700 $data = base64_decode( $data );
4385 5701 $data = json_decode( $data );
4386 5702
4387 5703 $title = isset( $data->title ) ? sanitize_text_field( $data->title ) : '';
@@ -4390,9 +5706,11 @@
4390 5706 $w_image = isset( $data->w_image ) ? esc_url_raw( $data->w_image ) : '';
4391 5707
4392 5708 if ( ! empty( $w_image ) ) {
4393 5709 $w_image = str_replace( '\\', '', $w_image );
4394 - $w_image = wp_remote_get( $w_image )['body'];
5710 + // SSRF guard (CWE-918): validate the resolved host before fetching.
5711 + $fetched = wdesignkit_safe_remote_get( $w_image );
5712 + $w_image = is_wp_error( $fetched ) ? '' : wp_remote_retrieve_body( $fetched );
4395 5713 }
4396 5714
4397 5715 $array_data = array(
4398 5716 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '',
@@ -4432,22 +5750,51 @@
4432 5750 $img_url = ! empty( $response['data']['imgurl'] ) ? $response['data']['imgurl'] : '';
4433 5751
4434 5752 if ( ! empty( $img_url ) && 'error' !== $res ) {
4435 5753
4436 - $img_body = wp_remote_get( $img_url );
4437 - $img_ext = pathinfo( $img_url )['extension'];
4438 - include_once ABSPATH . 'wp-admin/includes/file.php';
4439 - \WP_Filesystem();
4440 - global $wp_filesystem;
4441 - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq;
4442 - $file_name = str_replace( ' ', '_', $title ) . '_' . $w_uniq;
4443 - $file_path = WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name";
5754 + // SSRF guard (CWE-918): validate the resolved host before fetching.
5755 + $img_body = wdesignkit_safe_remote_get( $img_url );
5756 + if ( ! is_wp_error( $img_body ) ) {
5757 + // Verified against the payload rather than trusted from the URL (CWE-434,
5758 + // ClickUp 86d41cczd); '' means the bytes are not an image we accept.
5759 + $img_ext = wdesignkit_safe_image_extension( $img_url, $img_body['body'] );
5760 + include_once ABSPATH . 'wp-admin/includes/file.php';
5761 + \WP_Filesystem();
5762 + global $wp_filesystem;
5763 + // Canonical helpers, so the JSON read and the image write here address the same
5764 + // base name every other writer uses (ClickUp 86d41cck5). They also apply
5765 + // sanitize_file_name(), which $title and $w_uniq had not been through.
5766 + $folder_name = wdesignkit_widget_folder_name( $title, $w_uniq );
5767 + $file_name = wdesignkit_widget_file_name( $title, $w_uniq );
4444 5768
4445 - $u_r_l = wp_json_file_decode( "$file_path.json" );
4446 - $u_r_l->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext";
5769 + // $builder reaches here with only sanitize_text_field() applied and no
5770 + // allowlist, so it was a live traversal segment in this path (CWE-22,
5771 + // ClickUp 86d41cckh). Unlike the download handler earlier in this file, this
5772 + // one had neither the builder allowlist nor a containment check.
5773 + $safe_path = wdesignkit_widget_path_guard( $builder, $folder_name, $file_name );
5774 + if ( false === $safe_path || ! wdesignkit_path_inside_builder_dir( $safe_path['dir'] ) ) {
5775 + wp_send_json(
5776 + (object) array(
5777 + 'success' => false,
5778 + 'message' => esc_html__( 'Operation Failed!', 'wdesignkit' ),
5779 + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ),
5780 + )
5781 + );
5782 + wp_die();
5783 + }
4447 5784
4448 - $wp_filesystem->put_contents( "$file_path.json", wp_json_encode( $u_r_l ) );
4449 - $wp_filesystem->put_contents( "$file_path.$img_ext", $img_body['body'] );
5785 + $builder = $safe_path['builder'];
5786 + $file_path = $safe_path['base'];
5787 +
5788 + $u_r_l = wp_json_file_decode( "$file_path.json" );
5789 +
5790 + if ( '' !== $img_ext ) {
5791 + $u_r_l->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext";
5792 + $wp_filesystem->put_contents( "$file_path.$img_ext", $img_body['body'] );
5793 + }
5794 +
5795 + $wp_filesystem->put_contents( "$file_path.json", wp_json_encode( $u_r_l ) );
5796 + }
4450 5797 }
4451 5798
4452 5799 wp_send_json( $response );
4453 5800 wp_die();
@@ -4542,18 +5889,22 @@
4542 5889
4543 5890 $get_setting = get_option( 'wkit_settings_panel', false );
4544 5891
4545 5892 $setting_data = array(
4546 - 'builder' => isset( $get_setting['builder'] ) ? $get_setting['builder'] : true,
4547 - 'template' => isset( $get_setting['template'] ) ? $get_setting['template'] : true,
4548 - 'gutenberg_builder' => isset( $get_setting['gutenberg_builder'] ) ? $get_setting['gutenberg_builder'] : true,
4549 - 'gutenberg_core_builder' => isset( $get_setting['gutenberg_core_builder'] ) ? $get_setting['gutenberg_core_builder'] : false,
4550 - 'elementor_builder' => isset( $get_setting['elementor_builder'] ) ? $get_setting['elementor_builder'] : true,
4551 - 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : false,
4552 - 'gutenberg_template' => isset( $get_setting['gutenberg_template'] ) ? $get_setting['gutenberg_template'] : true,
4553 - 'elementor_template' => isset( $get_setting['elementor_template'] ) ? $get_setting['elementor_template'] : true,
4554 - 'code_snippet' => isset( $get_setting['code_snippet'] ) ? $get_setting['code_snippet'] : true,
4555 - 'plugin_version' => $version_check,
5893 + 'builder' => isset( $get_setting['builder'] ) ? $get_setting['builder'] : true,
5894 + 'template' => isset( $get_setting['template'] ) ? $get_setting['template'] : true,
5895 + 'gutenberg_builder' => isset( $get_setting['gutenberg_builder'] ) ? $get_setting['gutenberg_builder'] : true,
5896 + 'gutenberg_core_builder' => isset( $get_setting['gutenberg_core_builder'] ) ? $get_setting['gutenberg_core_builder'] : false,
5897 + 'elementor_builder' => isset( $get_setting['elementor_builder'] ) ? $get_setting['elementor_builder'] : true,
5898 + 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : true,
5899 + 'gutenberg_template' => isset( $get_setting['gutenberg_template'] ) ? $get_setting['gutenberg_template'] : true,
5900 + 'elementor_template' => isset( $get_setting['elementor_template'] ) ? $get_setting['elementor_template'] : true,
5901 + 'code_snippet' => isset( $get_setting['code_snippet'] ) ? $get_setting['code_snippet'] : true,
5902 + 'cross_copy_paste' => isset( $get_setting['cross_copy_paste'] ) ? $get_setting['cross_copy_paste'] : false,
5903 + 'cross_copy_paste_elementor' => isset( $get_setting['cross_copy_paste_elementor'] ) ? $get_setting['cross_copy_paste_elementor'] : false,
5904 + 'cross_copy_paste_gutenberg' => isset( $get_setting['cross_copy_paste_gutenberg'] ) ? $get_setting['cross_copy_paste_gutenberg'] : false,
5905 + 'cross_copy_paste_bricks' => isset( $get_setting['cross_copy_paste_bricks'] ) ? $get_setting['cross_copy_paste_bricks'] : false,
5906 + 'plugin_version' => $version_check,
4556 5907 );
4557 5908
4558 5909 if ( isset( $get_setting['remove_db'] ) ) {
4559 5910 $setting_data['remove_db'] = $get_setting['remove_db'];
@@ -4606,9 +5957,9 @@
4606 5957 }
4607 5958
4608 5959 $get_updated_data = get_option( 'wkit_white_label', false );
4609 5960 $response = array(
4610 - 'message' => 'Data Added successfully',
5961 + 'message' => __( 'Data Added successfully', 'wdesignkit' ),
4611 5962 'success' => true,
4612 5963 'data' => $get_updated_data,
4613 5964 );
4614 5965
@@ -4655,21 +6006,21 @@
4655 6006 if ( ! empty( $response['data'] ) ) {
4656 6007 $response = json_decode( wp_json_encode( $response['data'] ), true );
4657 6008
4658 6009 if ( ! empty( $response['data']['tpae_licence'] ) && is_serialized( $response['data']['tpae_licence'] ) ) {
4659 - $response['data']['tpae_licence'] = unserialize( $response['data']['tpae_licence'] );
6010 + $response['data']['tpae_licence'] = unserialize( $response['data']['tpae_licence'], array( 'allowed_classes' => false ) );
4660 6011 }
4661 6012
4662 6013 if ( ! empty( $response['data']['tpag_licence'] ) && is_serialized( $response['data']['tpag_licence'] ) ) {
4663 - $response['data']['tpag_licence'] = unserialize( $response['data']['tpag_licence'] );
6014 + $response['data']['tpag_licence'] = unserialize( $response['data']['tpag_licence'], array( 'allowed_classes' => false ) );
4664 6015 }
4665 6016
4666 6017 if ( ! empty( $response['data']['uichemy_licence'] ) && is_serialized( $response['data']['uichemy_licence'] ) ) {
4667 - $response['data']['uichemy_licence'] = unserialize( $response['data']['uichemy_licence'] );
6018 + $response['data']['uichemy_licence'] = unserialize( $response['data']['uichemy_licence'], array( 'allowed_classes' => false ) );
4668 6019 }
4669 6020
4670 6021 if ( ! empty( $response['data']['wdkit_licence'] ) && is_serialized( $response['data']['wdkit_licence'] ) ) {
4671 - $response['data']['wdkit_licence'] = unserialize( $response['data']['wdkit_licence'] );
6022 + $response['data']['wdkit_licence'] = unserialize( $response['data']['wdkit_licence'], array( 'allowed_classes' => false ) );
4672 6023
4673 6024 // Store WDesignKit license status locally for quick access
4674 6025 if ( ! empty( $response['data']['wdkit_licence'] ) && is_array( $response['data']['wdkit_licence'] ) ) {
4675 6026 update_option( 'wdkit_licence_data', $response['data']['wdkit_licence'] );
@@ -4676,9 +6027,9 @@
4676 6027 }
4677 6028 }
4678 6029
4679 6030 if ( ! empty( $response['data']['wdkit_licence_extra'] ) && is_serialized( $response['data']['wdkit_licence_extra'] ) ) {
4680 - $response['data']['wdkit_licence_extra'] = unserialize( $response['data']['wdkit_licence_extra'] );
6031 + $response['data']['wdkit_licence_extra'] = unserialize( $response['data']['wdkit_licence_extra'], array( 'allowed_classes' => false ) );
4681 6032 }
4682 6033 }
4683 6034
4684 6035 wp_send_json( $response );
@@ -4721,12 +6072,16 @@
4721 6072 */
4722 6073 protected function wdkit_sync_licence_key() {
4723 6074 $token = ! empty( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
4724 6075 $licencename = ! empty( $_POST['licencename'] ) ? sanitize_text_field( wp_unslash( $_POST['licencename'] ) ) : '';
6076 + // Needed to identify which extra-credit key to sync (wdkit_extra / wdkit_ai_extra
6077 + // are arrays matched by the api key's last digits on the server).
6078 + $apikey = ! empty( $_POST['apikey'] ) ? sanitize_text_field( wp_unslash( $_POST['apikey'] ) ) : '';
4725 6079
4726 6080 $args = array(
4727 6081 'token' => $token,
4728 6082 'licencename' => $licencename,
6083 + 'apikey' => $apikey,
4729 6084 );
4730 6085
4731 6086 $response = $this->wkit_api_call( $args, 'licence_sync' );
4732 6087
@@ -4859,9 +6214,9 @@
4859 6214 $token = $this->wdkit_login_user_token( $email );
4860 6215 $args = array( 'token' => $token );
4861 6216
4862 6217 if ( 'session' !== $logout_type ) {
4863 - delete_transient( 'wdkit_auth_' . $email );
6218 + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) );
4864 6219 // Clear stored license data on logout so banner shows again
4865 6220 delete_option( 'wdkit_licence_data' );
4866 6221 $response = WDesignKit_Data_Query::get_data( 'logout', $args );
4867 6222 }
@@ -4881,9 +6236,9 @@
4881 6236 */
4882 6237 protected function wdkit_login_user_token( $email = '' ) {
4883 6238
4884 6239 if ( ! empty( $email ) ) {
4885 - $user_key = strstr( $email, '@', true );
6240 + $user_key = wdesignkit_cloud_session_key( $email );
4886 6241 $get_login = get_transient( 'wdkit_auth_' . $user_key );
4887 6242
4888 6243 if ( ! empty( $get_login ) && ! empty( $get_login['token'] ) ) {
4889 6244 return $get_login['token'];
@@ -4901,9 +6256,9 @@
4901 6256 * @param string $data send all post data.
4902 6257 * @param string $type store text data.
4903 6258 * @param string $condition store text data.
4904 6259 */
4905 - protected function wdkit_sanitizer_bypass( $data, $type, $condition = 'none' ) {
6260 + protected function wdkit_extract_post_field( $data, $type, $condition = 'none' ) {
4906 6261
4907 6262 if ( 'none' === $condition ) {
4908 6263 return $data[ $type ];
4909 6264 } elseif ( 'cr_widget' === $condition ) {
@@ -4908,8 +6263,10 @@
4908 6263 return $data[ $type ];
4909 6264 } elseif ( 'cr_widget' === $condition ) {
4910 6265 return $data[ $type ];
4911 6266 }
6267 +
6268 + return null;
4912 6269 }
4913 6270
4914 6271
4915 6272 /**