| @@ -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 | |
| @@ -236,8 +311,11 @@ | ||
| 236 | 311 | break; |
| 237 | 312 | case 'generate_ai_content': |
| 238 | 313 | $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'generate_ai_content' ); |
| 239 | 314 | break; |
| 315 | + case 'generate_ai_content_batch': | |
| 316 | + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'generate_ai_content_batch' ); | |
| 317 | + break; | |
| 240 | 318 | case 'reset_site': |
| 241 | 319 | $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'reset_site' ); |
| 242 | 320 | break; |
| 243 | 321 | case 'wdkit_nxt_thembuilder_reset': |
| @@ -296,8 +374,11 @@ | ||
| 296 | 374 | break; |
| 297 | 375 | case 'wkit_update_elementor_template': |
| 298 | 376 | $data = $this->wkit_update_elementor_template(); |
| 299 | 377 | break; |
| 378 | + case 'wdkit_update_page_content': | |
| 379 | + $data = $this->wdkit_update_page_content(); | |
| 380 | + break; | |
| 300 | 381 | case 'update_plugin_setting': |
| 301 | 382 | $data = $this->update_plugin_setting(); |
| 302 | 383 | break; |
| 303 | 384 | case 'update_theme_setting': |
| @@ -330,9 +411,9 @@ | ||
| 330 | 411 | |
| 331 | 412 | wp_send_json( |
| 332 | 413 | array( |
| 333 | 414 | 'success' => false, |
| 334 | - 'message' => 'No block names received or filter not found.', | |
| 415 | + 'message' => __( 'No block names received or filter not found.', 'wdesignkit' ), | |
| 335 | 416 | 'description' => 'Ensure blockNames are posted and the filter is attached.', |
| 336 | 417 | ) |
| 337 | 418 | ); |
| 338 | 419 | wp_die(); |
| @@ -364,8 +445,11 @@ | ||
| 364 | 445 | break; |
| 365 | 446 | case 'wkit_check_widget_versions': |
| 366 | 447 | $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_check_widget_versions' ); |
| 367 | 448 | break; |
| 449 | + case 'wkit_plugin_download_get': | |
| 450 | + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_plugin_download_get' ); | |
| 451 | + break; | |
| 368 | 452 | case 'wkit_manage_widget_workspace': |
| 369 | 453 | $data = $this->wdkit_manage_widget_workspace(); |
| 370 | 454 | break; |
| 371 | 455 | case 'wkit_activate_key': |
| @@ -682,15 +766,22 @@ | ||
| 682 | 766 | $nexter_active_check = is_plugin_active( 'the-plus-addons-for-block-editor/the-plus-addons-for-block-editor.php' ); |
| 683 | 767 | |
| 684 | 768 | $theplus_licence = get_option( 'tpaep_licence_data', array() ); |
| 685 | 769 | |
| 686 | - 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 ) ) { | |
| 687 | 775 | $manage_licence['tpae'] = $theplus_licence; |
| 688 | 776 | } |
| 689 | 777 | |
| 690 | 778 | $nexter_licence = get_option( 'tpgb_activate', array() ); |
| 691 | 779 | |
| 692 | - 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'] ) ) { | |
| 693 | 784 | $tpgb_license_status = get_option( 'tpgbp_license_status', array() ); |
| 694 | 785 | $tpgb_license_status['license_key'] = $nexter_licence['tpgb_activate_key']; |
| 695 | 786 | $manage_licence['tpag'] = $tpgb_license_status; |
| 696 | 787 | } |
| @@ -731,14 +822,24 @@ | ||
| 731 | 822 | 'site_url' => $site_url, |
| 732 | 823 | ); |
| 733 | 824 | |
| 734 | 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 | + | |
| 735 | 836 | $status = ( ! empty( $response['status'] ) ) ? sanitize_text_field( $response['status'] ) : 'error'; |
| 736 | 837 | $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false; |
| 737 | 838 | |
| 738 | 839 | /**Condtion user for user logout & expire token*/ |
| 739 | 840 | if ( 'Token is Expired' === $status || 'Authorization Token not found' === $status ) { |
| 740 | - delete_transient( 'wdkit_auth_' . $email ); | |
| 841 | + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) ); | |
| 741 | 842 | // Clear stored license data when token expires so banner shows again |
| 742 | 843 | delete_option( 'wdkit_licence_data' ); |
| 743 | 844 | } |
| 744 | 845 | |
| @@ -750,9 +851,9 @@ | ||
| 750 | 851 | if ( ! empty( $response['credits']['wdkit_licence'] ) && is_array( $response['credits']['wdkit_licence'] ) ) { |
| 751 | 852 | $wdkit_licence = $response['credits']['wdkit_licence']; |
| 752 | 853 | // Handle serialized data |
| 753 | 854 | if ( is_string( $wdkit_licence ) && is_serialized( $wdkit_licence ) ) { |
| 754 | - $wdkit_licence = unserialize( $wdkit_licence ); | |
| 855 | + $wdkit_licence = unserialize( $wdkit_licence, array( 'allowed_classes' => false ) ); | |
| 755 | 856 | } |
| 756 | 857 | if ( ! empty( $wdkit_licence ) && is_array( $wdkit_licence ) ) { |
| 757 | 858 | update_option( 'wdkit_licence_data', $wdkit_licence ); |
| 758 | 859 | } |
| @@ -783,8 +884,16 @@ | ||
| 783 | 884 | $credits = ! empty( $response['credits']['widget_limit']['meta_value'] ) ? $response['credits']['widget_limit']['meta_value'] : 10; |
| 784 | 885 | $server_list = ! empty( $response['widgettemplate'] ) ? $response['widgettemplate'] : array(); |
| 785 | 886 | $db_builder_list = ! empty( $response['widgetbuilder'] ) ? $response['widgetbuilder'] : array(); |
| 786 | 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 | + | |
| 787 | 896 | $placeholderimg = WDKIT_URL . 'assets/images/placeholder.jpg'; |
| 788 | 897 | |
| 789 | 898 | $local_list = $this->wdkit_get_local_widgets(); |
| 790 | 899 | |
| @@ -870,13 +979,31 @@ | ||
| 870 | 979 | ); |
| 871 | 980 | } |
| 872 | 981 | } |
| 873 | 982 | |
| 874 | - $get_db_widget = get_option( 'wkit_deactivate_widgets', array() ); | |
| 875 | - if ( empty( $get_db_widget ) ) { | |
| 876 | - add_option( 'wkit_deactivate_widgets', $db_widget, '', 'yes' ); | |
| 877 | - } else { | |
| 878 | - 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 | + } | |
| 879 | 1006 | } |
| 880 | 1007 | |
| 881 | 1008 | return $final; |
| 882 | 1009 | } |
| @@ -890,8 +1017,16 @@ | ||
| 890 | 1017 | $args = $this->wdkit_parse_args( $_POST ); |
| 891 | 1018 | |
| 892 | 1019 | $response = WDesignKit_Data_Query::get_data( 'browse_page', $args ); |
| 893 | 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 | + | |
| 894 | 1029 | $manage_licence = array(); |
| 895 | 1030 | $manage_licence['theplus_elementor_addon'] = ! empty( defined( 'THEPLUS_VERSION' ) ) ? true : false; |
| 896 | 1031 | $manage_licence['tpag'] = ! empty( defined( 'TPGBP_VERSION' ) ) ? true : false; |
| 897 | 1032 | $manage_licence['elementor-pro'] = ! empty( defined( 'ELEMENTOR_PRO_VERSION' ) ) ? true : false; |
| @@ -1005,8 +1140,24 @@ | ||
| 1005 | 1140 | } |
| 1006 | 1141 | |
| 1007 | 1142 | $response = WDesignKit_Data_Query::get_data( 'save_template', $args ); |
| 1008 | 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 | + | |
| 1009 | 1160 | wp_send_json( $response ); |
| 1010 | 1161 | wp_die(); |
| 1011 | 1162 | } |
| 1012 | 1163 | |
| @@ -1030,9 +1181,11 @@ | ||
| 1030 | 1181 | 'success' => false, |
| 1031 | 1182 | ); |
| 1032 | 1183 | } else { |
| 1033 | 1184 | $temp_content = str_replace( '\\', '', $temp_content ); |
| 1034 | - $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 ); | |
| 1035 | 1188 | $temp_content = base64_encode( $temp_content ); |
| 1036 | 1189 | |
| 1037 | 1190 | $args = array( |
| 1038 | 1191 | 'token' => $token, |
| @@ -1070,8 +1223,13 @@ | ||
| 1070 | 1223 | * |
| 1071 | 1224 | * @since 2.3.3 |
| 1072 | 1225 | */ |
| 1073 | 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 | + | |
| 1074 | 1232 | $image_url = isset( $_POST['image'] ) ? sanitize_text_field( $_POST['image'] ) : ''; |
| 1075 | 1233 | |
| 1076 | 1234 | if ( empty( $image_url ) ) { |
| 1077 | 1235 | $response = array( |
| @@ -1090,14 +1248,26 @@ | ||
| 1090 | 1248 | 'success' => false, |
| 1091 | 1249 | ); |
| 1092 | 1250 | } else { |
| 1093 | 1251 | $saved_url = wp_get_attachment_url( $attachment_id ); |
| 1094 | - | |
| 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 | + | |
| 1095 | 1265 | $response = array( |
| 1096 | 1266 | 'message' => __( 'Image Saved', 'wdesignkit' ), |
| 1097 | 1267 | 'description' => __( 'Image successfully saved to Media Library.', 'wdesignkit' ), |
| 1098 | 1268 | 'success' => true, |
| 1099 | - 'url' => $saved_url, | |
| 1269 | + 'url' => $saved_url, | |
| 1100 | 1270 | ); |
| 1101 | 1271 | } |
| 1102 | 1272 | |
| 1103 | 1273 | } |
| @@ -1111,8 +1281,235 @@ | ||
| 1111 | 1281 | * Get Elementor Global color and Typography. |
| 1112 | 1282 | * |
| 1113 | 1283 | * @since 1.1.16 |
| 1114 | 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 | + | |
| 1115 | 1512 | protected function wdkit_get_global_val() { |
| 1116 | 1513 | |
| 1117 | 1514 | $builder = isset( $_POST['builder'] ) ? strtolower( sanitize_text_field( $_POST['builder'] ) ) : ''; |
| 1118 | 1515 | |
| @@ -1854,8 +2251,13 @@ | ||
| 1854 | 2251 | $site_data = ! empty( $_POST['site_data'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['site_data'] ) ), true ) : array(); |
| 1855 | 2252 | |
| 1856 | 2253 | if ( 'elementor' == $builder ) { |
| 1857 | 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 | + | |
| 1858 | 2260 | if ( ! $kit_id ) { |
| 1859 | 2261 | $response = array( |
| 1860 | 2262 | 'message' => __( 'Elementor kit not found', 'wdesignkit' ), |
| 1861 | 2263 | 'description' => __( 'No active Elementor kit found', 'wdesignkit' ), |
| @@ -1865,18 +2267,13 @@ | ||
| 1865 | 2267 | wp_send_json( $response ); |
| 1866 | 2268 | wp_die(); |
| 1867 | 2269 | } |
| 1868 | 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. | |
| 1869 | 2273 | $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); |
| 1870 | - if ( empty( $kit_meta ) ) { | |
| 1871 | - $response = array( | |
| 1872 | - 'message' => __( 'Data Not Found', 'wdesignkit' ), | |
| 1873 | - 'description' => __( 'No site data found in kit', 'wdesignkit' ), | |
| 1874 | - 'success' => false, | |
| 1875 | - ); | |
| 1876 | - | |
| 1877 | - wp_send_json( $response ); | |
| 1878 | - wp_die(); | |
| 2274 | + if ( ! is_array( $kit_meta ) ) { | |
| 2275 | + $kit_meta = array(); | |
| 1879 | 2276 | } |
| 1880 | 2277 | |
| 1881 | 2278 | $kit_meta['container_width'] = ! empty( $site_data['container_width'] ) ? $site_data['container_width'] : array(); |
| 1882 | 2279 | $kit_meta['__globals__'] = ! empty( $site_data['globals'] ) ? $site_data['globals'] : array(); |
| @@ -1883,8 +2280,13 @@ | ||
| 1883 | 2280 | $kit_meta['body_background_color'] = ! empty( $site_data['body_background_color'] ) ? $site_data['body_background_color'] : array(); |
| 1884 | 2281 | |
| 1885 | 2282 | update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta ); |
| 1886 | 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 | + | |
| 1887 | 2289 | $response = array( |
| 1888 | 2290 | 'message' => __( 'Site data Updated', 'wdesignkit' ), |
| 1889 | 2291 | 'description' => __( 'Site Globals Updated', 'wdesignkit' ), |
| 1890 | 2292 | 'success' => true, |
| @@ -1932,8 +2334,16 @@ | ||
| 1932 | 2334 | $g_typo = ! empty( $_POST['g_typography'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['g_typography'] ) ), true ) : array(); |
| 1933 | 2335 | |
| 1934 | 2336 | // Get colors from Elementor Site Kit |
| 1935 | 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 | + | |
| 1936 | 2346 | if ( ! $kit_id ) { |
| 1937 | 2347 | $response = array( |
| 1938 | 2348 | 'message' => __( 'Elementor kit not found', 'wdesignkit' ), |
| 1939 | 2349 | 'description' => __( 'No active Elementor kit found', 'wdesignkit' ), |
| @@ -1943,25 +2353,25 @@ | ||
| 1943 | 2353 | wp_send_json( $response ); |
| 1944 | 2354 | wp_die(); |
| 1945 | 2355 | } |
| 1946 | 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. | |
| 1947 | 2359 | $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); |
| 1948 | - if ( empty( $kit_meta ) ) { | |
| 1949 | - $response = array( | |
| 1950 | - 'message' => __( 'Data Not Found', 'wdesignkit' ), | |
| 1951 | - 'description' => __( 'No meta data found in kit', 'wdesignkit' ), | |
| 1952 | - 'success' => false, | |
| 1953 | - ); | |
| 1954 | - | |
| 1955 | - wp_send_json( $response ); | |
| 1956 | - wp_die(); | |
| 2360 | + if ( ! is_array( $kit_meta ) ) { | |
| 2361 | + $kit_meta = array(); | |
| 1957 | 2362 | } |
| 1958 | 2363 | |
| 1959 | - $kit_meta['custom_colors'] = array_merge( $g_color, $kit_meta['custom_colors'] ); | |
| 1960 | - $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() ); | |
| 1961 | 2366 | |
| 1962 | 2367 | update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta ); |
| 1963 | 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 | + | |
| 1964 | 2374 | $response = array( |
| 1965 | 2375 | 'message' => __( 'Global data Updated', 'wdesignkit' ), |
| 1966 | 2376 | 'description' => __( 'Global Color and Typography Updated', 'wdesignkit' ), |
| 1967 | 2377 | 'success' => true, |
| @@ -1996,9 +2406,29 @@ | ||
| 1996 | 2406 | wp_die(); |
| 1997 | 2407 | } |
| 1998 | 2408 | |
| 1999 | 2409 | /** |
| 2410 | + * Regenerate Elementor's cached CSS files after the active kit's | |
| 2411 | + * `_elementor_page_settings` meta has been changed directly. | |
| 2000 | 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 | + * | |
| 2001 | 2431 | * Create Gutenberg page and save for re-generate css file. |
| 2002 | 2432 | * |
| 2003 | 2433 | * @since 1.2.3 |
| 2004 | 2434 | */ |
| @@ -2362,34 +2792,56 @@ | ||
| 2362 | 2792 | $error_message = $response->get_error_message(); |
| 2363 | 2793 | |
| 2364 | 2794 | $result = $this->tpae_set_response( false, 'oops', 'oops', '' ); |
| 2365 | 2795 | } else { |
| 2366 | - $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' ) ) ); | |
| 2367 | 2802 | $theme_name = $theme_info->name; |
| 2368 | 2803 | $theme_zip_url = $theme_info->download_link; |
| 2369 | 2804 | |
| 2370 | - global $wp_filesystem; | |
| 2371 | - // Install the theme | |
| 2372 | - $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 | + } | |
| 2373 | 2815 | |
| 2374 | 2816 | if ( ! function_exists( 'WP_Filesystem' ) ) { |
| 2375 | 2817 | require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' ); |
| 2376 | 2818 | } |
| 2377 | 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 | + | |
| 2378 | 2823 | WP_Filesystem(); |
| 2379 | 2824 | |
| 2380 | 2825 | $active_theme = wp_get_theme(); |
| 2381 | 2826 | $theme_name = $active_theme->get( 'Name' ); |
| 2382 | 2827 | |
| 2383 | - $wp_filesystem->put_contents( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip', $theme['body'] ); | |
| 2384 | - $zip = new ZipArchive(); | |
| 2385 | - if ( $zip->open( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip' ) === true ) { | |
| 2386 | - $zip->extractTo( WP_CONTENT_DIR . '/themes/' ); | |
| 2387 | - $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 | + ); | |
| 2388 | 2842 | } |
| 2389 | 2843 | |
| 2390 | - $wp_filesystem->delete( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip' ); | |
| 2391 | - | |
| 2392 | 2844 | $activate_result = switch_theme( $name ); |
| 2393 | 2845 | |
| 2394 | 2846 | if ( ! is_wp_error( $activate_result ) ) { |
| 2395 | 2847 | $response = array( |
| @@ -2566,8 +3018,17 @@ | ||
| 2566 | 3018 | |
| 2567 | 3019 | unset( $args['email'] ); |
| 2568 | 3020 | $args['unique_id'] = get_option( 'wdkit_unique_id' ) ?? ''; |
| 2569 | 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 | + | |
| 2570 | 3031 | $custom_meta = isset( $_POST['custom_meta'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_meta'] ) ) : false; |
| 2571 | 3032 | |
| 2572 | 3033 | /** Custom meta Field */ |
| 2573 | 3034 | if ( ! empty( $custom_meta ) && 'true' === $custom_meta && ! empty( $response ) && ! empty( $response['content'] ) ) { |
| @@ -2578,9 +3039,9 @@ | ||
| 2578 | 3039 | |
| 2579 | 3040 | if ( ! empty( $meta_data ) ) { |
| 2580 | 3041 | foreach ( $meta_data as $meta_key => $meta_val ) { |
| 2581 | 3042 | if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { |
| 2582 | - $meta_val[0] = maybe_unserialize( $meta_val[0] ); | |
| 3043 | + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); | |
| 2583 | 3044 | } |
| 2584 | 3045 | |
| 2585 | 3046 | if ( get_post_meta( get_the_ID(), $meta_key, true ) === '' ) { |
| 2586 | 3047 | add_post_meta( get_the_ID(), $meta_key, $meta_val[0] ); |
| @@ -2591,8 +3052,34 @@ | ||
| 2591 | 3052 | } |
| 2592 | 3053 | } |
| 2593 | 3054 | } |
| 2594 | 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 | + | |
| 2595 | 3082 | wp_send_json( $response ); |
| 2596 | 3083 | wp_die(); |
| 2597 | 3084 | } |
| 2598 | 3085 | |
| @@ -2603,8 +3090,265 @@ | ||
| 2603 | 3090 | * |
| 2604 | 3091 | * @param array $content store media content. |
| 2605 | 3092 | * @param string $editor it is check editor. |
| 2606 | 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 | + | |
| 2607 | 3351 | public function wdkit_media_import( $content = array(), $editor = '' ) { |
| 2608 | 3352 | |
| 2609 | 3353 | if ( empty( $content ) && empty( $editor ) ) { |
| 2610 | 3354 | $args = $this->wdkit_parse_args( $_POST ); |
| @@ -2624,8 +3368,9 @@ | ||
| 2624 | 3368 | if ( ! class_exists( 'Wdkit_Import_Images' ) ) { |
| 2625 | 3369 | require_once WDKIT_INCLUDES . 'admin/class-wdkit-import-images.php'; |
| 2626 | 3370 | } |
| 2627 | 3371 | |
| 3372 | + | |
| 2628 | 3373 | if ( ! empty( $args['editor'] ) && 'gutenberg' === $args['editor'] && ! empty( $content ) ) { |
| 2629 | 3374 | $media_import = array( $content ); |
| 2630 | 3375 | $media_import = self::blocks_import_media_copy_content( $media_import ); |
| 2631 | 3376 | $content = $media_import[0]; |
| @@ -2633,8 +3378,13 @@ | ||
| 2633 | 3378 | $media_import = array( $content ); |
| 2634 | 3379 | $media_import = self::widgets_elements_id_change( $media_import ); |
| 2635 | 3380 | $media_import = self::widgets_import_media_copy_content( $media_import ); |
| 2636 | 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 ); | |
| 2637 | 3387 | } |
| 2638 | 3388 | |
| 2639 | 3389 | return $content; |
| 2640 | 3390 | } |
| @@ -2705,9 +3455,13 @@ | ||
| 2705 | 3455 | $control_type = \Elementor\Plugin::instance()->controls_manager->get_control( $get_control['type'] ); |
| 2706 | 3456 | $control_name = $get_control['name']; |
| 2707 | 3457 | |
| 2708 | 3458 | if ( ! $control_type ) { |
| 2709 | - 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; | |
| 2710 | 3464 | } |
| 2711 | 3465 | |
| 2712 | 3466 | if ( method_exists( $control_type, $tp_mi_on_fun ) ) { |
| 2713 | 3467 | $get_element_instance['settings'][ $control_name ] = $control_type->{$tp_mi_on_fun}( $element->get_settings( $control_name ), $get_control ); |
| @@ -2782,73 +3536,298 @@ | ||
| 2782 | 3536 | public static function blocks_data_instance( array $block_data, array $args = array(), $block_args = null ) { |
| 2783 | 3537 | |
| 2784 | 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'] ) ) ) { |
| 2785 | 3539 | $blocks_attr = isset( $block_data['attributes'] ) ? $block_data['attributes'] : ( isset( $block_data['attrs'] ) ? $block_data['attrs'] : array() ); |
| 2786 | - foreach ( $blocks_attr as $block_key => $block_val ) { | |
| 2787 | - if ( isset( $block_val['url'] ) && isset( $block_val['id'] ) && ! empty( $block_val['url'] ) ) { | |
| 2788 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $block_val ); | |
| 2789 | - $blocks_attr[ $block_key ] = $new_media; | |
| 2790 | - } elseif ( isset( $block_val['url'] ) && ! empty( $block_val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $block_val['url'] ) ) { | |
| 2791 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $block_val ); | |
| 2792 | - $blocks_attr[ $block_key ] = $new_media; | |
| 2793 | - } elseif ( is_array( $block_val ) && ! empty( $block_val ) ) { | |
| 2794 | - 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 ) ) { | |
| 2795 | - foreach ( $block_val as $key => $val ) { | |
| 2796 | - 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 | + } | |
| 2797 | 3546 | |
| 2798 | - if ( isset( $val['url'] ) && ( isset( $val['Id'] ) || isset( $val['id'] ) ) && ! empty( $val['url'] ) ) { | |
| 2799 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $val ); | |
| 2800 | - $blocks_attr[ $block_key ][ $key ] = $new_media; | |
| 2801 | - } elseif ( isset( $val['url'] ) && ! empty( $val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $val['url'] ) ) { | |
| 2802 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $val ); | |
| 2803 | - $blocks_attr[ $block_key ][ $key ] = $new_media; | |
| 2804 | - } else { | |
| 2805 | - foreach ( $val as $sub_key => $sub_val ) { | |
| 2806 | - if ( isset( $sub_val['url'] ) && ( isset( $sub_val['Id'] ) || isset( $sub_val['id'] ) ) && ! empty( $sub_val['url'] ) ) { | |
| 2807 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val ); | |
| 3547 | + $block_data = self::wdkit_relink_block_markup( $block_data ); | |
| 3548 | + } | |
| 2808 | 3549 | |
| 2809 | - if ( is_array( $sub_val ) && is_array( $new_media ) ) { | |
| 2810 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = array_merge( $sub_val, $new_media ); | |
| 2811 | - } else { | |
| 2812 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = $new_media; | |
| 2813 | - } | |
| 2814 | - } elseif ( isset( $sub_val['url'] ) && ! empty( $sub_val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $sub_val['url'] ) ) { | |
| 2815 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val ); | |
| 2816 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = $new_media; | |
| 2817 | - } elseif ( is_array( $sub_val ) && ! empty( $sub_val ) ) { | |
| 2818 | - foreach ( $sub_val as $sub_key1 => $sub_val1 ) { | |
| 2819 | - if ( isset( $sub_val1['url'] ) && ( isset( $sub_val1['Id'] ) || isset( $sub_val1['id'] ) ) && ! empty( $sub_val1['url'] ) ) { | |
| 2820 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val1 ); | |
| 3550 | + return $block_data; | |
| 3551 | + } | |
| 2821 | 3552 | |
| 2822 | - if ( is_array( $sub_val1 ) && is_array( $new_media ) ) { | |
| 2823 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = array_merge( $sub_val1, $new_media ); | |
| 2824 | - } else { | |
| 2825 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = $new_media; | |
| 2826 | - } | |
| 2827 | - } elseif ( isset( $sub_val1['url'] ) && ! empty( $sub_val1['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $sub_val1['url'] ) ) { | |
| 2828 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val1 ); | |
| 2829 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = $new_media; | |
| 2830 | - } | |
| 2831 | - } | |
| 2832 | - } | |
| 2833 | - } | |
| 2834 | - } | |
| 2835 | - } | |
| 2836 | - } | |
| 2837 | - } | |
| 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(); | |
| 2838 | 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; | |
| 2839 | 3661 | } |
| 2840 | - if ( isset( $block_data['attributes'] ) ) { | |
| 2841 | - $block_data['attributes'] = $blocks_attr; | |
| 2842 | - } elseif ( isset( $block_data['attrs'] ) ) { | |
| 2843 | - $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 | + } | |
| 2844 | 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; | |
| 2845 | 3720 | } |
| 2846 | 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 | + | |
| 2847 | 3774 | return $block_data; |
| 2848 | 3775 | } |
| 2849 | 3776 | |
| 2850 | 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 | + /** | |
| 2851 | 3830 | * Kit Template Import Pages/Sections |
| 2852 | 3831 | * |
| 2853 | 3832 | * @since 1.0.0 |
| 2854 | 3833 | * */ |
| @@ -2927,8 +3906,28 @@ | ||
| 2927 | 3906 | $output['description'] = $response['description']; |
| 2928 | 3907 | $output['data'] = $result; |
| 2929 | 3908 | $output['success'] = $response['success']; |
| 2930 | 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 | + | |
| 2931 | 3930 | wp_send_json( $output ); |
| 2932 | 3931 | wp_die(); |
| 2933 | 3932 | } |
| 2934 | 3933 | |
| @@ -3035,9 +4034,17 @@ | ||
| 3035 | 4034 | ); |
| 3036 | 4035 | |
| 3037 | 4036 | $response = WDesignKit_Data_Query::get_data( $api_type, $temp_args ); |
| 3038 | 4037 | |
| 3039 | - 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'] ) { | |
| 3040 | 4047 | wp_send_json( $response ); |
| 3041 | 4048 | wp_die(); |
| 3042 | 4049 | } |
| 3043 | 4050 | |
| @@ -3105,8 +4112,14 @@ | ||
| 3105 | 4112 | } |
| 3106 | 4113 | |
| 3107 | 4114 | $document = \Elementor\Plugin::$instance->documents->get($template_id); |
| 3108 | 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 | + | |
| 3109 | 4122 | $document->save([ |
| 3110 | 4123 | 'elements' => $content |
| 3111 | 4124 | ]); |
| 3112 | 4125 | } |
| @@ -3111,8 +4124,94 @@ | ||
| 3111 | 4124 | ]); |
| 3112 | 4125 | } |
| 3113 | 4126 | |
| 3114 | 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 | + /** | |
| 3115 | 4214 | * Import single template and section from plugin only |
| 3116 | 4215 | * |
| 3117 | 4216 | * @param array $args store data. |
| 3118 | 4217 | * @param array $template_id store data. |
| @@ -3120,8 +4219,21 @@ | ||
| 3120 | 4219 | * @param array $temp_data store data. |
| 3121 | 4220 | * */ |
| 3122 | 4221 | protected function import_page_section_content() { |
| 3123 | 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 | + | |
| 3124 | 4236 | if ( isset( $_POST['args'] ) ) { |
| 3125 | 4237 | $args = ! empty( $_POST['args'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['args'] ) ), true ) : array(); |
| 3126 | 4238 | } |
| 3127 | 4239 | |
| @@ -3144,11 +4256,9 @@ | ||
| 3144 | 4256 | if ( isset( $_POST['template_id'] ) ) { |
| 3145 | 4257 | $template_id = ! empty( $_POST['template_id'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['template_id'] ), true ) ) : ''; |
| 3146 | 4258 | } |
| 3147 | 4259 | |
| 3148 | - if ( isset( $_POST['temp_type'] ) ) { | |
| 3149 | - $temp_type = isset( $_POST['temp_type'] ) ? sanitize_text_field( wp_unslash( $_POST['temp_type'] ) ) : 'normal'; | |
| 3150 | - } | |
| 4260 | + $temp_type = isset( $_POST['temp_type'] ) ? sanitize_text_field( wp_unslash( $_POST['temp_type'] ) ) : 'normal'; | |
| 3151 | 4261 | |
| 3152 | 4262 | if ( isset( $_POST['data'] ) ) { |
| 3153 | 4263 | $data = ! empty( $_POST['data'] ) ? json_decode( wp_unslash( $_POST['data'] ) ) : ''; |
| 3154 | 4264 | } |
| @@ -3169,8 +4279,21 @@ | ||
| 3169 | 4279 | } |
| 3170 | 4280 | |
| 3171 | 4281 | if ( ! empty( $data ) && ! empty( $template_id ) && ! empty( $post_type ) && current_user_can( 'manage_options' ) ) { |
| 3172 | 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 | + | |
| 3173 | 4296 | $post_title = isset( $post_content->title ) ? sanitize_text_field( $post_content->title ) : ''; |
| 3174 | 4297 | $post_slug = isset( $post_content->slug ) ? sanitize_text_field( $post_content->slug ) : ''; |
| 3175 | 4298 | $file_type = isset( $post_content->file_type ) ? sanitize_text_field( $post_content->file_type ) : ''; |
| 3176 | 4299 | $content = isset( $post_content->content ) ? wp_slash( $post_content->content ) : ''; |
| @@ -3180,9 +4303,9 @@ | ||
| 3180 | 4303 | if ( empty( $content ) ) { |
| 3181 | 4304 | wp_send_json( |
| 3182 | 4305 | array( |
| 3183 | 4306 | 'template_id' => $template_id, |
| 3184 | - 'message' => 'Content is Empty.', | |
| 4307 | + 'message' => __( 'Content is Empty.', 'wdesignkit' ), | |
| 3185 | 4308 | ) |
| 3186 | 4309 | ); |
| 3187 | 4310 | wp_die(); |
| 3188 | 4311 | } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'wp_block' === $file_type ) { |
| @@ -3188,27 +4311,17 @@ | ||
| 3188 | 4311 | } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'wp_block' === $file_type ) { |
| 3189 | 4312 | |
| 3190 | 4313 | $editor = ( 'wdkit' === $args['editor'] ) ? 'gutenberg' : $args['editor']; |
| 3191 | 4314 | $blocks = parse_blocks( stripslashes( $content ) ); |
| 3192 | - | |
| 4315 | + | |
| 3193 | 4316 | $blocks = $this->wdkit_media_import( $blocks, $editor ); |
| 3194 | 4317 | |
| 3195 | 4318 | $processor = new WDKIT_Nexter_Block_Processor(); |
| 3196 | 4319 | $blocks = $processor->run( $blocks ); |
| 3197 | 4320 | $content = serialize_blocks( $blocks ); |
| 3198 | - | |
| 4321 | + | |
| 3199 | 4322 | $content = $this->replace_unicode_glitch( serialize_blocks( $blocks ) ); |
| 3200 | 4323 | |
| 3201 | - if ( ! empty( $category_list ) && is_array( $category_list ) ) { | |
| 3202 | - $category_ids = array_map( 'intval', $category_list ); | |
| 3203 | - wp_set_post_terms( $inserted_id, $category_ids, 'category' ); | |
| 3204 | - } | |
| 3205 | - | |
| 3206 | - if ( ! empty( $tag_list ) && is_array( $tag_list ) ) { | |
| 3207 | - $tag_ids = array_map( 'intval', $tag_list ); | |
| 3208 | - wp_set_post_terms( $inserted_id, $tag_ids, 'post_tag' ); | |
| 3209 | - } | |
| 3210 | - | |
| 3211 | 4324 | $inserted_post = wp_insert_post( |
| 3212 | 4325 | array( |
| 3213 | 4326 | 'post_status' => 'publish', |
| 3214 | 4327 | 'post_type' => $post_type, |
| @@ -3227,9 +4340,9 @@ | ||
| 3227 | 4340 | ); |
| 3228 | 4341 | wp_die(); |
| 3229 | 4342 | } |
| 3230 | 4343 | |
| 3231 | - if ( ! empty( $thumb_image ) ) { | |
| 4344 | + if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) { | |
| 3232 | 4345 | // $featured_image_url = esc_url_raw( $thumb_image ); |
| 3233 | 4346 | $tmp = download_url( $thumb_image ); |
| 3234 | 4347 | if ( is_wp_error( $tmp ) ) { |
| 3235 | 4348 | error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) ); |
| @@ -3265,9 +4378,9 @@ | ||
| 3265 | 4378 | $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : ''; |
| 3266 | 4379 | if ( ! empty( $custom_meta ) ) { |
| 3267 | 4380 | foreach ( $custom_meta as $meta_key => $meta_val ) { |
| 3268 | 4381 | if ( isset( $meta_val[0] ) && ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { |
| 3269 | - $meta_val[0] = maybe_unserialize( $meta_val[0] ); | |
| 4382 | + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); | |
| 3270 | 4383 | } |
| 3271 | 4384 | |
| 3272 | 4385 | if ( '' === get_post_meta( $inserted_post, $meta_key, true ) && isset( $meta_val[0] ) ) { |
| 3273 | 4386 | add_post_meta( $inserted_post, $meta_key, $meta_val[0] ); |
| @@ -3299,13 +4412,20 @@ | ||
| 3299 | 4412 | Tpgb_Library()->remove_backend_dir_files(); |
| 3300 | 4413 | } |
| 3301 | 4414 | |
| 3302 | 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 | + | |
| 3303 | 4423 | wp_send_json( |
| 3304 | 4424 | array( |
| 3305 | 4425 | $temp_id => $temp_detail, |
| 3306 | 4426 | 'description' => 'Yay! Your Section has been Successfully Imported.', |
| 3307 | - 'message' => 'Successfully Imported.', | |
| 4427 | + 'message' => __( 'Successfully Imported.', 'wdesignkit' ), | |
| 3308 | 4428 | 'inserted_id' => $inserted_post, |
| 3309 | 4429 | 'success' => true, |
| 3310 | 4430 | ) |
| 3311 | 4431 | ); |
| @@ -3316,9 +4436,9 @@ | ||
| 3316 | 4436 | if ( empty( $content ) ) { |
| 3317 | 4437 | wp_send_json( |
| 3318 | 4438 | array( |
| 3319 | 4439 | 'template_id' => $template_id, |
| 3320 | - 'message' => 'Content is Empty.', | |
| 4440 | + 'message' => __( 'Content is Empty.', 'wdesignkit' ), | |
| 3321 | 4441 | ) |
| 3322 | 4442 | ); |
| 3323 | 4443 | wp_die(); |
| 3324 | 4444 | } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'elementor' === $file_type ) { |
| @@ -3356,9 +4476,9 @@ | ||
| 3356 | 4476 | } |
| 3357 | 4477 | |
| 3358 | 4478 | $inserted_id = $new_document->get_main_id(); |
| 3359 | 4479 | |
| 3360 | - if ( ! empty( $thumb_image ) ) { | |
| 4480 | + if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) { | |
| 3361 | 4481 | // $featured_image_url = esc_url_raw( $thumb_image ); |
| 3362 | 4482 | $tmp = download_url( $thumb_image ); |
| 3363 | 4483 | if ( is_wp_error( $tmp ) ) { |
| 3364 | 4484 | error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) ); |
| @@ -3412,9 +4532,9 @@ | ||
| 3412 | 4532 | $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : ''; |
| 3413 | 4533 | if ( ! empty( $custom_meta ) ) { |
| 3414 | 4534 | foreach ( $custom_meta as $meta_key => $meta_val ) { |
| 3415 | 4535 | if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { |
| 3416 | - $meta_val[0] = maybe_unserialize( $meta_val[0] ); | |
| 4536 | + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); | |
| 3417 | 4537 | } |
| 3418 | 4538 | if ( '' === get_post_meta( $inserted_id, $meta_key, true ) ) { |
| 3419 | 4539 | add_post_meta( $inserted_id, $meta_key, $meta_val[0] ); |
| 3420 | 4540 | } |
| @@ -3438,14 +4558,18 @@ | ||
| 3438 | 4558 | } |
| 3439 | 4559 | |
| 3440 | 4560 | \Elementor\Plugin::$instance->files_manager->clear_cache(); |
| 3441 | 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 | + | |
| 3442 | 4566 | wp_send_json( |
| 3443 | 4567 | array( |
| 3444 | 4568 | $temp_id => $temp_detail, |
| 3445 | 4569 | 'content' => $temp_con, |
| 3446 | 4570 | 'description' => 'Yay! Your Section has been Successfully Imported.', |
| 3447 | - 'message' => 'Successfully Imported.', | |
| 4571 | + 'message' => __( 'Successfully Imported.', 'wdesignkit' ), | |
| 3448 | 4572 | 'inserted_id' => $inserted_id, |
| 3449 | 4573 | 'success' => true, |
| 3450 | 4574 | ) |
| 3451 | 4575 | ); |
| @@ -3575,8 +4699,12 @@ | ||
| 3575 | 4699 | } |
| 3576 | 4700 | |
| 3577 | 4701 | $image_url = esc_url_raw( $_POST['image_url'] ); |
| 3578 | 4702 | |
| 4703 | + if ( ! wdesignkit_validate_external_url( $image_url ) ) { | |
| 4704 | + wp_send_json_error( 'Image could not be downloaded.' ); | |
| 4705 | + } | |
| 4706 | + | |
| 3579 | 4707 | $tmp_file = download_url( $image_url ); |
| 3580 | 4708 | if ( is_wp_error( $tmp_file ) ) { |
| 3581 | 4709 | wp_send_json_error( 'Image could not be downloaded.' ); |
| 3582 | 4710 | } |
| @@ -3612,9 +4740,21 @@ | ||
| 3612 | 4740 | |
| 3613 | 4741 | $upload_dir = wp_upload_dir(); |
| 3614 | 4742 | $result_urls = array(); |
| 3615 | 4743 | |
| 4744 | + $colour_index = 0; | |
| 3616 | 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 | + | |
| 3617 | 4757 | $new = imagecreatetruecolor( $width, $height ); |
| 3618 | 4758 | imagesavealpha( $new, true ); |
| 3619 | 4759 | imagealphablending( $new, false ); |
| 3620 | 4760 | |
| @@ -3640,9 +4780,9 @@ | ||
| 3640 | 4780 | imagesetpixel( $new, $x, $y, $color ); |
| 3641 | 4781 | } |
| 3642 | 4782 | } |
| 3643 | 4783 | |
| 3644 | - $filename = 'colored-' . $name . '-' . time() . '.png'; | |
| 4784 | + $filename = 'colored-' . $safe_name . '-' . time() . '.png'; | |
| 3645 | 4785 | $filepath = $upload_dir['path'] . '/' . $filename; |
| 3646 | 4786 | |
| 3647 | 4787 | imagepng( $new, $filepath ); |
| 3648 | 4788 | imagedestroy( $new ); |
| @@ -3858,8 +4998,13 @@ | ||
| 3858 | 4998 | $page_information = isset( $_POST['page_information'] ) ? sanitize_text_field( wp_unslash( $_POST['page_information'] ) ) : ''; |
| 3859 | 4999 | $page_information = json_decode( $page_information, true ); |
| 3860 | 5000 | |
| 3861 | 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 | + | |
| 3862 | 5007 | // Step 1: banavo mapping [ old_id => new_id ] |
| 3863 | 5008 | $id_mapping = array(); |
| 3864 | 5009 | foreach ( $page_information as $page_info ) { |
| 3865 | 5010 | if ( ! empty( $page_info['old_page_id'] ) ) { |
| @@ -3952,12 +5097,73 @@ | ||
| 3952 | 5097 | } |
| 3953 | 5098 | |
| 3954 | 5099 | $response = json_decode( wp_json_encode( $response['data'] ), true ); |
| 3955 | 5100 | |
| 5101 | + $this->wdkit_cache_cloud_usage( $response ); | |
| 5102 | + | |
| 3956 | 5103 | wp_send_json( $response ); |
| 3957 | 5104 | wp_die(); |
| 3958 | 5105 | } |
| 3959 | 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 | + | |
| 3960 | 5166 | public function wdkit_nxt_thembuilder_reset() { |
| 3961 | 5167 | $post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : ''; |
| 3962 | 5168 | $sections_layout = get_post_meta( $post_id, 'nxt-hooks-layout-sections', true ); |
| 3963 | 5169 | |
| @@ -4290,10 +5496,35 @@ | ||
| 4290 | 5496 | 'description' => esc_html__( 'widget JSON file not found.', 'wdesignkit' ), |
| 4291 | 5497 | ); |
| 4292 | 5498 | } |
| 4293 | 5499 | |
| 4294 | - $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 ); | |
| 4295 | 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 | + | |
| 4296 | 5527 | $json_data = wp_json_file_decode( "$json_path.json" ); |
| 4297 | 5528 | if ( ! empty( $json_data ) ) { |
| 4298 | 5529 | $result = (object) array( |
| 4299 | 5530 | 'success' => true, |
| @@ -4319,9 +5550,9 @@ | ||
| 4319 | 5550 | * |
| 4320 | 5551 | * @since 1.0.0 |
| 4321 | 5552 | */ |
| 4322 | 5553 | protected function wdkit_download_widget() { |
| 4323 | - $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' ) : ''; | |
| 4324 | 5555 | $data = json_decode( stripslashes( $data ) ); |
| 4325 | 5556 | |
| 4326 | 5557 | $array_data = array( |
| 4327 | 5558 | 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '', |
| @@ -4389,14 +5620,34 @@ | ||
| 4389 | 5620 | if ( ! is_array( $json_data ) ) { |
| 4390 | 5621 | $json_data = json_decode( $json_data, true ); |
| 4391 | 5622 | } |
| 4392 | 5623 | |
| 4393 | - $title = ! empty( $json_data['widget_data']['widgetdata']['name'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['name'] ) : ''; | |
| 4394 | - $builder = ! empty( $json_data['widget_data']['widgetdata']['type'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['type'] ) : ''; | |
| 4395 | - $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'] ) : ''; | |
| 4396 | 5630 | |
| 4397 | - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq; | |
| 4398 | - $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 ); | |
| 4399 | 5650 | $builder_type_path = WDKIT_BUILDER_PATH . "/{$builder}/"; |
| 4400 | 5651 | |
| 4401 | 5652 | if ( ! is_dir( $builder_type_path ) ) { |
| 4402 | 5653 | wp_mkdir_p( $builder_type_path ); |
| @@ -4406,15 +5657,27 @@ | ||
| 4406 | 5657 | wp_mkdir_p( $builder_type_path . $folder_name ); |
| 4407 | 5658 | } |
| 4408 | 5659 | |
| 4409 | 5660 | if ( ! empty( $img_url ) ) { |
| 4410 | - $img_body = wp_remote_get( $img_url ); | |
| 4411 | - $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'] ); | |
| 4412 | 5668 | |
| 4413 | - $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] ); | |
| 4414 | - $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 | + } | |
| 4415 | 5674 | } |
| 4416 | 5675 | |
| 5676 | + if ( function_exists( 'wdesignkit_invalidate_widget_registry' ) ) { | |
| 5677 | + wdesignkit_invalidate_widget_registry( $builder ); | |
| 5678 | + } | |
| 5679 | + | |
| 4417 | 5680 | // Bug E fix (part 2): success was hardcoded false on the successful download path — always reported failure. |
| 4418 | 5681 | $result = (object) array( |
| 4419 | 5682 | 'success' => true, |
| 4420 | 5683 | 'message' => ! empty( $response['message'] ) ? $response['message'] : esc_html__( 'no message', 'wdesignkit' ), |
| @@ -4432,9 +5695,9 @@ | ||
| 4432 | 5695 | * |
| 4433 | 5696 | * @since 1.0.0 |
| 4434 | 5697 | */ |
| 4435 | 5698 | protected function wdkit_add_widget() { |
| 4436 | - $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' ) : ''; | |
| 4437 | 5700 | $data = base64_decode( $data ); |
| 4438 | 5701 | $data = json_decode( $data ); |
| 4439 | 5702 | |
| 4440 | 5703 | $title = isset( $data->title ) ? sanitize_text_field( $data->title ) : ''; |
| @@ -4443,9 +5706,11 @@ | ||
| 4443 | 5706 | $w_image = isset( $data->w_image ) ? esc_url_raw( $data->w_image ) : ''; |
| 4444 | 5707 | |
| 4445 | 5708 | if ( ! empty( $w_image ) ) { |
| 4446 | 5709 | $w_image = str_replace( '\\', '', $w_image ); |
| 4447 | - $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 ); | |
| 4448 | 5713 | } |
| 4449 | 5714 | |
| 4450 | 5715 | $array_data = array( |
| 4451 | 5716 | 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '', |
| @@ -4485,22 +5750,51 @@ | ||
| 4485 | 5750 | $img_url = ! empty( $response['data']['imgurl'] ) ? $response['data']['imgurl'] : ''; |
| 4486 | 5751 | |
| 4487 | 5752 | if ( ! empty( $img_url ) && 'error' !== $res ) { |
| 4488 | 5753 | |
| 4489 | - $img_body = wp_remote_get( $img_url ); | |
| 4490 | - $img_ext = pathinfo( $img_url )['extension']; | |
| 4491 | - include_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 4492 | - \WP_Filesystem(); | |
| 4493 | - global $wp_filesystem; | |
| 4494 | - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq; | |
| 4495 | - $file_name = str_replace( ' ', '_', $title ) . '_' . $w_uniq; | |
| 4496 | - $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 ); | |
| 4497 | 5768 | |
| 4498 | - $u_r_l = wp_json_file_decode( "$file_path.json" ); | |
| 4499 | - $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 | + } | |
| 4500 | 5784 | |
| 4501 | - $wp_filesystem->put_contents( "$file_path.json", wp_json_encode( $u_r_l ) ); | |
| 4502 | - $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 | + } | |
| 4503 | 5797 | } |
| 4504 | 5798 | |
| 4505 | 5799 | wp_send_json( $response ); |
| 4506 | 5800 | wp_die(); |
| @@ -4600,9 +5894,9 @@ | ||
| 4600 | 5894 | 'template' => isset( $get_setting['template'] ) ? $get_setting['template'] : true, |
| 4601 | 5895 | 'gutenberg_builder' => isset( $get_setting['gutenberg_builder'] ) ? $get_setting['gutenberg_builder'] : true, |
| 4602 | 5896 | 'gutenberg_core_builder' => isset( $get_setting['gutenberg_core_builder'] ) ? $get_setting['gutenberg_core_builder'] : false, |
| 4603 | 5897 | 'elementor_builder' => isset( $get_setting['elementor_builder'] ) ? $get_setting['elementor_builder'] : true, |
| 4604 | - 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : false, | |
| 5898 | + 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : true, | |
| 4605 | 5899 | 'gutenberg_template' => isset( $get_setting['gutenberg_template'] ) ? $get_setting['gutenberg_template'] : true, |
| 4606 | 5900 | 'elementor_template' => isset( $get_setting['elementor_template'] ) ? $get_setting['elementor_template'] : true, |
| 4607 | 5901 | 'code_snippet' => isset( $get_setting['code_snippet'] ) ? $get_setting['code_snippet'] : true, |
| 4608 | 5902 | 'cross_copy_paste' => isset( $get_setting['cross_copy_paste'] ) ? $get_setting['cross_copy_paste'] : false, |
| @@ -4663,9 +5957,9 @@ | ||
| 4663 | 5957 | } |
| 4664 | 5958 | |
| 4665 | 5959 | $get_updated_data = get_option( 'wkit_white_label', false ); |
| 4666 | 5960 | $response = array( |
| 4667 | - 'message' => 'Data Added successfully', | |
| 5961 | + 'message' => __( 'Data Added successfully', 'wdesignkit' ), | |
| 4668 | 5962 | 'success' => true, |
| 4669 | 5963 | 'data' => $get_updated_data, |
| 4670 | 5964 | ); |
| 4671 | 5965 | |
| @@ -4712,21 +6006,21 @@ | ||
| 4712 | 6006 | if ( ! empty( $response['data'] ) ) { |
| 4713 | 6007 | $response = json_decode( wp_json_encode( $response['data'] ), true ); |
| 4714 | 6008 | |
| 4715 | 6009 | if ( ! empty( $response['data']['tpae_licence'] ) && is_serialized( $response['data']['tpae_licence'] ) ) { |
| 4716 | - $response['data']['tpae_licence'] = unserialize( $response['data']['tpae_licence'] ); | |
| 6010 | + $response['data']['tpae_licence'] = unserialize( $response['data']['tpae_licence'], array( 'allowed_classes' => false ) ); | |
| 4717 | 6011 | } |
| 4718 | 6012 | |
| 4719 | 6013 | if ( ! empty( $response['data']['tpag_licence'] ) && is_serialized( $response['data']['tpag_licence'] ) ) { |
| 4720 | - $response['data']['tpag_licence'] = unserialize( $response['data']['tpag_licence'] ); | |
| 6014 | + $response['data']['tpag_licence'] = unserialize( $response['data']['tpag_licence'], array( 'allowed_classes' => false ) ); | |
| 4721 | 6015 | } |
| 4722 | 6016 | |
| 4723 | 6017 | if ( ! empty( $response['data']['uichemy_licence'] ) && is_serialized( $response['data']['uichemy_licence'] ) ) { |
| 4724 | - $response['data']['uichemy_licence'] = unserialize( $response['data']['uichemy_licence'] ); | |
| 6018 | + $response['data']['uichemy_licence'] = unserialize( $response['data']['uichemy_licence'], array( 'allowed_classes' => false ) ); | |
| 4725 | 6019 | } |
| 4726 | 6020 | |
| 4727 | 6021 | if ( ! empty( $response['data']['wdkit_licence'] ) && is_serialized( $response['data']['wdkit_licence'] ) ) { |
| 4728 | - $response['data']['wdkit_licence'] = unserialize( $response['data']['wdkit_licence'] ); | |
| 6022 | + $response['data']['wdkit_licence'] = unserialize( $response['data']['wdkit_licence'], array( 'allowed_classes' => false ) ); | |
| 4729 | 6023 | |
| 4730 | 6024 | // Store WDesignKit license status locally for quick access |
| 4731 | 6025 | if ( ! empty( $response['data']['wdkit_licence'] ) && is_array( $response['data']['wdkit_licence'] ) ) { |
| 4732 | 6026 | update_option( 'wdkit_licence_data', $response['data']['wdkit_licence'] ); |
| @@ -4733,9 +6027,9 @@ | ||
| 4733 | 6027 | } |
| 4734 | 6028 | } |
| 4735 | 6029 | |
| 4736 | 6030 | if ( ! empty( $response['data']['wdkit_licence_extra'] ) && is_serialized( $response['data']['wdkit_licence_extra'] ) ) { |
| 4737 | - $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 ) ); | |
| 4738 | 6032 | } |
| 4739 | 6033 | } |
| 4740 | 6034 | |
| 4741 | 6035 | wp_send_json( $response ); |
| @@ -4778,12 +6072,16 @@ | ||
| 4778 | 6072 | */ |
| 4779 | 6073 | protected function wdkit_sync_licence_key() { |
| 4780 | 6074 | $token = ! empty( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; |
| 4781 | 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'] ) ) : ''; | |
| 4782 | 6079 | |
| 4783 | 6080 | $args = array( |
| 4784 | 6081 | 'token' => $token, |
| 4785 | 6082 | 'licencename' => $licencename, |
| 6083 | + 'apikey' => $apikey, | |
| 4786 | 6084 | ); |
| 4787 | 6085 | |
| 4788 | 6086 | $response = $this->wkit_api_call( $args, 'licence_sync' ); |
| 4789 | 6087 | |
| @@ -4916,9 +6214,9 @@ | ||
| 4916 | 6214 | $token = $this->wdkit_login_user_token( $email ); |
| 4917 | 6215 | $args = array( 'token' => $token ); |
| 4918 | 6216 | |
| 4919 | 6217 | if ( 'session' !== $logout_type ) { |
| 4920 | - delete_transient( 'wdkit_auth_' . $email ); | |
| 6218 | + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) ); | |
| 4921 | 6219 | // Clear stored license data on logout so banner shows again |
| 4922 | 6220 | delete_option( 'wdkit_licence_data' ); |
| 4923 | 6221 | $response = WDesignKit_Data_Query::get_data( 'logout', $args ); |
| 4924 | 6222 | } |
| @@ -4938,9 +6236,9 @@ | ||
| 4938 | 6236 | */ |
| 4939 | 6237 | protected function wdkit_login_user_token( $email = '' ) { |
| 4940 | 6238 | |
| 4941 | 6239 | if ( ! empty( $email ) ) { |
| 4942 | - $user_key = strstr( $email, '@', true ); | |
| 6240 | + $user_key = wdesignkit_cloud_session_key( $email ); | |
| 4943 | 6241 | $get_login = get_transient( 'wdkit_auth_' . $user_key ); |
| 4944 | 6242 | |
| 4945 | 6243 | if ( ! empty( $get_login ) && ! empty( $get_login['token'] ) ) { |
| 4946 | 6244 | return $get_login['token']; |
| @@ -4958,9 +6256,9 @@ | ||
| 4958 | 6256 | * @param string $data send all post data. |
| 4959 | 6257 | * @param string $type store text data. |
| 4960 | 6258 | * @param string $condition store text data. |
| 4961 | 6259 | */ |
| 4962 | - protected function wdkit_sanitizer_bypass( $data, $type, $condition = 'none' ) { | |
| 6260 | + protected function wdkit_extract_post_field( $data, $type, $condition = 'none' ) { | |
| 4963 | 6261 | |
| 4964 | 6262 | if ( 'none' === $condition ) { |
| 4965 | 6263 | return $data[ $type ]; |
| 4966 | 6264 | } elseif ( 'cr_widget' === $condition ) { |
| @@ -4965,8 +6263,10 @@ | ||
| 4965 | 6263 | return $data[ $type ]; |
| 4966 | 6264 | } elseif ( 'cr_widget' === $condition ) { |
| 4967 | 6265 | return $data[ $type ]; |
| 4968 | 6266 | } |
| 6267 | + | |
| 6268 | + return null; | |
| 4969 | 6269 | } |
| 4970 | 6270 | |
| 4971 | 6271 | |
| 4972 | 6272 | /** |