| @@ -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': |
| @@ -424,8 +508,11 @@ | ||
| 424 | 508 | break; |
| 425 | 509 | case 'wdkit_get_workspace_data': |
| 426 | 510 | $data = $this->wdkit_get_workspace_data(); |
| 427 | 511 | break; |
| 512 | + default: | |
| 513 | + $this->wdkit_error_msg( __( 'Unknown request type.', 'wdesignkit' ) ); | |
| 514 | + return; | |
| 428 | 515 | } |
| 429 | 516 | |
| 430 | 517 | $this->wdkit_success_msg( $data ); |
| 431 | 518 | // wp_die(); |
| @@ -436,12 +523,13 @@ | ||
| 436 | 523 | * This Function is used for API call |
| 437 | 524 | * |
| 438 | 525 | * @since 1.0.0 |
| 439 | 526 | * |
| 440 | - * @param array $data give array. | |
| 441 | - * @param array $name store data. | |
| 527 | + * @param array $data give array. | |
| 528 | + * @param array $name store data. | |
| 529 | + * @param int $timeout optional HTTP timeout in seconds. Default 100. | |
| 442 | 530 | */ |
| 443 | - protected function wkit_api_call( $data, $name ) { | |
| 531 | + protected function wkit_api_call( $data, $name, $timeout = 100 ) { | |
| 444 | 532 | $u_r_l = $this->wdkit_api; |
| 445 | 533 | |
| 446 | 534 | if ( empty( $u_r_l ) ) { |
| 447 | 535 | return array( |
| @@ -452,9 +540,9 @@ | ||
| 452 | 540 | |
| 453 | 541 | $args = array( |
| 454 | 542 | 'method' => 'POST', |
| 455 | 543 | 'body' => $data, |
| 456 | - 'timeout' => 100, | |
| 544 | + 'timeout' => $timeout, | |
| 457 | 545 | ); |
| 458 | 546 | $response = wp_remote_post( $u_r_l . $name, $args ); |
| 459 | 547 | |
| 460 | 548 | if ( is_wp_error( $response ) ) { |
| @@ -460,9 +548,9 @@ | ||
| 460 | 548 | if ( is_wp_error( $response ) ) { |
| 461 | 549 | $error_message = $response->get_error_message(); |
| 462 | 550 | |
| 463 | 551 | /* Translators: %s is a placeholder for the error message */ |
| 464 | - $error_message = printf( esc_html__( 'API request error: %s', 'wdesignkit' ), esc_html( $error_message ) ); | |
| 552 | + $error_message = sprintf( esc_html__( 'API request error: %s', 'wdesignkit' ), esc_html( $error_message ) ); | |
| 465 | 553 | |
| 466 | 554 | return array( |
| 467 | 555 | 'massage' => $error_message, |
| 468 | 556 | 'success' => false, |
| @@ -479,9 +567,9 @@ | ||
| 479 | 567 | 'success' => true, |
| 480 | 568 | ); |
| 481 | 569 | } |
| 482 | 570 | |
| 483 | - $error_message = printf( 'Server error: %d', esc_html( $status_code ) ); | |
| 571 | + $error_message = sprintf( 'Server error: %d', esc_html( $status_code ) ); | |
| 484 | 572 | |
| 485 | 573 | if ( isset( $error_data->message ) ) { |
| 486 | 574 | $error_message .= ' (' . $error_data->message . ')'; |
| 487 | 575 | } |
| @@ -678,15 +766,22 @@ | ||
| 678 | 766 | $nexter_active_check = is_plugin_active( 'the-plus-addons-for-block-editor/the-plus-addons-for-block-editor.php' ); |
| 679 | 767 | |
| 680 | 768 | $theplus_licence = get_option( 'tpaep_licence_data', array() ); |
| 681 | 769 | |
| 682 | - 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 ) ) { | |
| 683 | 775 | $manage_licence['tpae'] = $theplus_licence; |
| 684 | 776 | } |
| 685 | 777 | |
| 686 | 778 | $nexter_licence = get_option( 'tpgb_activate', array() ); |
| 687 | 779 | |
| 688 | - 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'] ) ) { | |
| 689 | 784 | $tpgb_license_status = get_option( 'tpgbp_license_status', array() ); |
| 690 | 785 | $tpgb_license_status['license_key'] = $nexter_licence['tpgb_activate_key']; |
| 691 | 786 | $manage_licence['tpag'] = $tpgb_license_status; |
| 692 | 787 | } |
| @@ -727,14 +822,24 @@ | ||
| 727 | 822 | 'site_url' => $site_url, |
| 728 | 823 | ); |
| 729 | 824 | |
| 730 | 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 | + | |
| 731 | 836 | $status = ( ! empty( $response['status'] ) ) ? sanitize_text_field( $response['status'] ) : 'error'; |
| 732 | 837 | $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false; |
| 733 | 838 | |
| 734 | 839 | /**Condtion user for user logout & expire token*/ |
| 735 | 840 | if ( 'Token is Expired' === $status || 'Authorization Token not found' === $status ) { |
| 736 | - delete_transient( 'wdkit_auth_' . $email ); | |
| 841 | + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) ); | |
| 737 | 842 | // Clear stored license data when token expires so banner shows again |
| 738 | 843 | delete_option( 'wdkit_licence_data' ); |
| 739 | 844 | } |
| 740 | 845 | |
| @@ -746,9 +851,9 @@ | ||
| 746 | 851 | if ( ! empty( $response['credits']['wdkit_licence'] ) && is_array( $response['credits']['wdkit_licence'] ) ) { |
| 747 | 852 | $wdkit_licence = $response['credits']['wdkit_licence']; |
| 748 | 853 | // Handle serialized data |
| 749 | 854 | if ( is_string( $wdkit_licence ) && is_serialized( $wdkit_licence ) ) { |
| 750 | - $wdkit_licence = unserialize( $wdkit_licence ); | |
| 855 | + $wdkit_licence = unserialize( $wdkit_licence, array( 'allowed_classes' => false ) ); | |
| 751 | 856 | } |
| 752 | 857 | if ( ! empty( $wdkit_licence ) && is_array( $wdkit_licence ) ) { |
| 753 | 858 | update_option( 'wdkit_licence_data', $wdkit_licence ); |
| 754 | 859 | } |
| @@ -779,8 +884,16 @@ | ||
| 779 | 884 | $credits = ! empty( $response['credits']['widget_limit']['meta_value'] ) ? $response['credits']['widget_limit']['meta_value'] : 10; |
| 780 | 885 | $server_list = ! empty( $response['widgettemplate'] ) ? $response['widgettemplate'] : array(); |
| 781 | 886 | $db_builder_list = ! empty( $response['widgetbuilder'] ) ? $response['widgetbuilder'] : array(); |
| 782 | 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 | + | |
| 783 | 896 | $placeholderimg = WDKIT_URL . 'assets/images/placeholder.jpg'; |
| 784 | 897 | |
| 785 | 898 | $local_list = $this->wdkit_get_local_widgets(); |
| 786 | 899 | |
| @@ -866,13 +979,31 @@ | ||
| 866 | 979 | ); |
| 867 | 980 | } |
| 868 | 981 | } |
| 869 | 982 | |
| 870 | - $get_db_widget = get_option( 'wkit_deactivate_widgets', array() ); | |
| 871 | - if ( empty( $get_db_widget ) ) { | |
| 872 | - add_option( 'wkit_deactivate_widgets', $db_widget, '', 'yes' ); | |
| 873 | - } else { | |
| 874 | - 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 | + } | |
| 875 | 1006 | } |
| 876 | 1007 | |
| 877 | 1008 | return $final; |
| 878 | 1009 | } |
| @@ -886,8 +1017,16 @@ | ||
| 886 | 1017 | $args = $this->wdkit_parse_args( $_POST ); |
| 887 | 1018 | |
| 888 | 1019 | $response = WDesignKit_Data_Query::get_data( 'browse_page', $args ); |
| 889 | 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 | + | |
| 890 | 1029 | $manage_licence = array(); |
| 891 | 1030 | $manage_licence['theplus_elementor_addon'] = ! empty( defined( 'THEPLUS_VERSION' ) ) ? true : false; |
| 892 | 1031 | $manage_licence['tpag'] = ! empty( defined( 'TPGBP_VERSION' ) ) ? true : false; |
| 893 | 1032 | $manage_licence['elementor-pro'] = ! empty( defined( 'ELEMENTOR_PRO_VERSION' ) ) ? true : false; |
| @@ -923,15 +1062,14 @@ | ||
| 923 | 1062 | |
| 924 | 1063 | $user_email = strtolower( sanitize_email( $args['email'] ) ); |
| 925 | 1064 | $response = ''; |
| 926 | 1065 | |
| 1066 | + // Bug D fix: response()->json() is Laravel syntax — causes PHP fatal. Use plain array. | |
| 927 | 1067 | if ( empty( $user_email ) || empty( $args['template_id'] ) ) { |
| 928 | - $response = response()->json( | |
| 929 | - array( | |
| 930 | - 'message' => $this->e_msg_login, | |
| 931 | - 'description' => $this->e_desc_login, | |
| 932 | - 'success' => true, | |
| 933 | - ) | |
| 1068 | + $response = array( | |
| 1069 | + 'message' => $this->e_msg_login, | |
| 1070 | + 'description' => $this->e_desc_login, | |
| 1071 | + 'success' => false, | |
| 934 | 1072 | ); |
| 935 | 1073 | |
| 936 | 1074 | wp_send_json( $response ); |
| 937 | 1075 | wp_die(); |
| @@ -1002,8 +1140,24 @@ | ||
| 1002 | 1140 | } |
| 1003 | 1141 | |
| 1004 | 1142 | $response = WDesignKit_Data_Query::get_data( 'save_template', $args ); |
| 1005 | 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 | + | |
| 1006 | 1160 | wp_send_json( $response ); |
| 1007 | 1161 | wp_die(); |
| 1008 | 1162 | } |
| 1009 | 1163 | |
| @@ -1027,9 +1181,11 @@ | ||
| 1027 | 1181 | 'success' => false, |
| 1028 | 1182 | ); |
| 1029 | 1183 | } else { |
| 1030 | 1184 | $temp_content = str_replace( '\\', '', $temp_content ); |
| 1031 | - $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 ); | |
| 1032 | 1188 | $temp_content = base64_encode( $temp_content ); |
| 1033 | 1189 | |
| 1034 | 1190 | $args = array( |
| 1035 | 1191 | 'token' => $token, |
| @@ -1067,8 +1223,13 @@ | ||
| 1067 | 1223 | * |
| 1068 | 1224 | * @since 2.3.3 |
| 1069 | 1225 | */ |
| 1070 | 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 | + | |
| 1071 | 1232 | $image_url = isset( $_POST['image'] ) ? sanitize_text_field( $_POST['image'] ) : ''; |
| 1072 | 1233 | |
| 1073 | 1234 | if ( empty( $image_url ) ) { |
| 1074 | 1235 | $response = array( |
| @@ -1087,14 +1248,26 @@ | ||
| 1087 | 1248 | 'success' => false, |
| 1088 | 1249 | ); |
| 1089 | 1250 | } else { |
| 1090 | 1251 | $saved_url = wp_get_attachment_url( $attachment_id ); |
| 1091 | - | |
| 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 | + | |
| 1092 | 1265 | $response = array( |
| 1093 | 1266 | 'message' => __( 'Image Saved', 'wdesignkit' ), |
| 1094 | 1267 | 'description' => __( 'Image successfully saved to Media Library.', 'wdesignkit' ), |
| 1095 | 1268 | 'success' => true, |
| 1096 | - 'url' => $saved_url, | |
| 1269 | + 'url' => $saved_url, | |
| 1097 | 1270 | ); |
| 1098 | 1271 | } |
| 1099 | 1272 | |
| 1100 | 1273 | } |
| @@ -1108,8 +1281,235 @@ | ||
| 1108 | 1281 | * Get Elementor Global color and Typography. |
| 1109 | 1282 | * |
| 1110 | 1283 | * @since 1.1.16 |
| 1111 | 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 | + | |
| 1112 | 1512 | protected function wdkit_get_global_val() { |
| 1113 | 1513 | |
| 1114 | 1514 | $builder = isset( $_POST['builder'] ) ? strtolower( sanitize_text_field( $_POST['builder'] ) ) : ''; |
| 1115 | 1515 | |
| @@ -1851,8 +2251,13 @@ | ||
| 1851 | 2251 | $site_data = ! empty( $_POST['site_data'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['site_data'] ) ), true ) : array(); |
| 1852 | 2252 | |
| 1853 | 2253 | if ( 'elementor' == $builder ) { |
| 1854 | 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 | + | |
| 1855 | 2260 | if ( ! $kit_id ) { |
| 1856 | 2261 | $response = array( |
| 1857 | 2262 | 'message' => __( 'Elementor kit not found', 'wdesignkit' ), |
| 1858 | 2263 | 'description' => __( 'No active Elementor kit found', 'wdesignkit' ), |
| @@ -1862,18 +2267,13 @@ | ||
| 1862 | 2267 | wp_send_json( $response ); |
| 1863 | 2268 | wp_die(); |
| 1864 | 2269 | } |
| 1865 | 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. | |
| 1866 | 2273 | $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); |
| 1867 | - if ( empty( $kit_meta ) ) { | |
| 1868 | - $response = array( | |
| 1869 | - 'message' => __( 'Data Not Found', 'wdesignkit' ), | |
| 1870 | - 'description' => __( 'No site data found in kit', 'wdesignkit' ), | |
| 1871 | - 'success' => false, | |
| 1872 | - ); | |
| 1873 | - | |
| 1874 | - wp_send_json( $response ); | |
| 1875 | - wp_die(); | |
| 2274 | + if ( ! is_array( $kit_meta ) ) { | |
| 2275 | + $kit_meta = array(); | |
| 1876 | 2276 | } |
| 1877 | 2277 | |
| 1878 | 2278 | $kit_meta['container_width'] = ! empty( $site_data['container_width'] ) ? $site_data['container_width'] : array(); |
| 1879 | 2279 | $kit_meta['__globals__'] = ! empty( $site_data['globals'] ) ? $site_data['globals'] : array(); |
| @@ -1880,8 +2280,13 @@ | ||
| 1880 | 2280 | $kit_meta['body_background_color'] = ! empty( $site_data['body_background_color'] ) ? $site_data['body_background_color'] : array(); |
| 1881 | 2281 | |
| 1882 | 2282 | update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta ); |
| 1883 | 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 | + | |
| 1884 | 2289 | $response = array( |
| 1885 | 2290 | 'message' => __( 'Site data Updated', 'wdesignkit' ), |
| 1886 | 2291 | 'description' => __( 'Site Globals Updated', 'wdesignkit' ), |
| 1887 | 2292 | 'success' => true, |
| @@ -1929,8 +2334,16 @@ | ||
| 1929 | 2334 | $g_typo = ! empty( $_POST['g_typography'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['g_typography'] ) ), true ) : array(); |
| 1930 | 2335 | |
| 1931 | 2336 | // Get colors from Elementor Site Kit |
| 1932 | 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 | + | |
| 1933 | 2346 | if ( ! $kit_id ) { |
| 1934 | 2347 | $response = array( |
| 1935 | 2348 | 'message' => __( 'Elementor kit not found', 'wdesignkit' ), |
| 1936 | 2349 | 'description' => __( 'No active Elementor kit found', 'wdesignkit' ), |
| @@ -1940,25 +2353,25 @@ | ||
| 1940 | 2353 | wp_send_json( $response ); |
| 1941 | 2354 | wp_die(); |
| 1942 | 2355 | } |
| 1943 | 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. | |
| 1944 | 2359 | $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); |
| 1945 | - if ( empty( $kit_meta ) ) { | |
| 1946 | - $response = array( | |
| 1947 | - 'message' => __( 'Data Not Found', 'wdesignkit' ), | |
| 1948 | - 'description' => __( 'No meta data found in kit', 'wdesignkit' ), | |
| 1949 | - 'success' => false, | |
| 1950 | - ); | |
| 1951 | - | |
| 1952 | - wp_send_json( $response ); | |
| 1953 | - wp_die(); | |
| 2360 | + if ( ! is_array( $kit_meta ) ) { | |
| 2361 | + $kit_meta = array(); | |
| 1954 | 2362 | } |
| 1955 | 2363 | |
| 1956 | - $kit_meta['custom_colors'] = array_merge( $g_color, $kit_meta['custom_colors'] ); | |
| 1957 | - $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() ); | |
| 1958 | 2366 | |
| 1959 | 2367 | update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta ); |
| 1960 | 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 | + | |
| 1961 | 2374 | $response = array( |
| 1962 | 2375 | 'message' => __( 'Global data Updated', 'wdesignkit' ), |
| 1963 | 2376 | 'description' => __( 'Global Color and Typography Updated', 'wdesignkit' ), |
| 1964 | 2377 | 'success' => true, |
| @@ -1993,9 +2406,29 @@ | ||
| 1993 | 2406 | wp_die(); |
| 1994 | 2407 | } |
| 1995 | 2408 | |
| 1996 | 2409 | /** |
| 2410 | + * Regenerate Elementor's cached CSS files after the active kit's | |
| 2411 | + * `_elementor_page_settings` meta has been changed directly. | |
| 1997 | 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 | + * | |
| 1998 | 2431 | * Create Gutenberg page and save for re-generate css file. |
| 1999 | 2432 | * |
| 2000 | 2433 | * @since 1.2.3 |
| 2001 | 2434 | */ |
| @@ -2359,34 +2792,56 @@ | ||
| 2359 | 2792 | $error_message = $response->get_error_message(); |
| 2360 | 2793 | |
| 2361 | 2794 | $result = $this->tpae_set_response( false, 'oops', 'oops', '' ); |
| 2362 | 2795 | } else { |
| 2363 | - $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' ) ) ); | |
| 2364 | 2802 | $theme_name = $theme_info->name; |
| 2365 | 2803 | $theme_zip_url = $theme_info->download_link; |
| 2366 | 2804 | |
| 2367 | - global $wp_filesystem; | |
| 2368 | - // Install the theme | |
| 2369 | - $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 | + } | |
| 2370 | 2815 | |
| 2371 | 2816 | if ( ! function_exists( 'WP_Filesystem' ) ) { |
| 2372 | 2817 | require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' ); |
| 2373 | 2818 | } |
| 2374 | 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 | + | |
| 2375 | 2823 | WP_Filesystem(); |
| 2376 | 2824 | |
| 2377 | 2825 | $active_theme = wp_get_theme(); |
| 2378 | 2826 | $theme_name = $active_theme->get( 'Name' ); |
| 2379 | 2827 | |
| 2380 | - $wp_filesystem->put_contents( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip', $theme['body'] ); | |
| 2381 | - $zip = new ZipArchive(); | |
| 2382 | - if ( $zip->open( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip' ) === true ) { | |
| 2383 | - $zip->extractTo( WP_CONTENT_DIR . '/themes/' ); | |
| 2384 | - $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 | + ); | |
| 2385 | 2842 | } |
| 2386 | 2843 | |
| 2387 | - $wp_filesystem->delete( WP_CONTENT_DIR . '/themes/' . $theme_slug . '.zip' ); | |
| 2388 | - | |
| 2389 | 2844 | $activate_result = switch_theme( $name ); |
| 2390 | 2845 | |
| 2391 | 2846 | if ( ! is_wp_error( $activate_result ) ) { |
| 2392 | 2847 | $response = array( |
| @@ -2563,8 +3018,17 @@ | ||
| 2563 | 3018 | |
| 2564 | 3019 | unset( $args['email'] ); |
| 2565 | 3020 | $args['unique_id'] = get_option( 'wdkit_unique_id' ) ?? ''; |
| 2566 | 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 | + | |
| 2567 | 3031 | $custom_meta = isset( $_POST['custom_meta'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_meta'] ) ) : false; |
| 2568 | 3032 | |
| 2569 | 3033 | /** Custom meta Field */ |
| 2570 | 3034 | if ( ! empty( $custom_meta ) && 'true' === $custom_meta && ! empty( $response ) && ! empty( $response['content'] ) ) { |
| @@ -2575,9 +3039,9 @@ | ||
| 2575 | 3039 | |
| 2576 | 3040 | if ( ! empty( $meta_data ) ) { |
| 2577 | 3041 | foreach ( $meta_data as $meta_key => $meta_val ) { |
| 2578 | 3042 | if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { |
| 2579 | - $meta_val[0] = maybe_unserialize( $meta_val[0] ); | |
| 3043 | + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); | |
| 2580 | 3044 | } |
| 2581 | 3045 | |
| 2582 | 3046 | if ( get_post_meta( get_the_ID(), $meta_key, true ) === '' ) { |
| 2583 | 3047 | add_post_meta( get_the_ID(), $meta_key, $meta_val[0] ); |
| @@ -2600,8 +3064,265 @@ | ||
| 2600 | 3064 | * |
| 2601 | 3065 | * @param array $content store media content. |
| 2602 | 3066 | * @param string $editor it is check editor. |
| 2603 | 3067 | */ |
| 3068 | + /** | |
| 3069 | + * Resolve a local upload URL back to its attachment ID. | |
| 3070 | + * | |
| 3071 | + * Handles the "-scaled" copy WordPress makes for large originals and any | |
| 3072 | + * "-1920x1280" size suffix, both of which attachment_url_to_postid() misses because | |
| 3073 | + * they are not the value stored in _wp_attached_file. | |
| 3074 | + * | |
| 3075 | + * @since 2.6.2 | |
| 3076 | + * | |
| 3077 | + * @param string $url Local upload URL. | |
| 3078 | + * @return int Attachment ID, or 0. | |
| 3079 | + */ | |
| 3080 | + private static function wdkit_attachment_id_from_url( $url ) { | |
| 3081 | + static $cache = array(); | |
| 3082 | + | |
| 3083 | + if ( isset( $cache[ $url ] ) ) { | |
| 3084 | + return $cache[ $url ]; | |
| 3085 | + } | |
| 3086 | + | |
| 3087 | + $id = (int) attachment_url_to_postid( $url ); | |
| 3088 | + | |
| 3089 | + if ( ! $id ) { | |
| 3090 | + // Try the original file behind a -scaled or -WxH derivative. | |
| 3091 | + $stripped = preg_replace( '/-scaled(\.[a-z0-9]+)$/i', '$1', $url ); | |
| 3092 | + $stripped = preg_replace( '/-\d+x\d+(\.[a-z0-9]+)$/i', '$1', (string) $stripped ); | |
| 3093 | + | |
| 3094 | + if ( $stripped && $stripped !== $url ) { | |
| 3095 | + $id = (int) attachment_url_to_postid( $stripped ); | |
| 3096 | + } | |
| 3097 | + } | |
| 3098 | + | |
| 3099 | + // Only remember hits. Page imports run concurrently, so an attachment created by a | |
| 3100 | + // sibling request may not exist yet when this is first asked — caching that miss | |
| 3101 | + // would keep every later control in this request pointing at nothing. | |
| 3102 | + if ( $id ) { | |
| 3103 | + $cache[ $url ] = $id; | |
| 3104 | + } | |
| 3105 | + | |
| 3106 | + return $id; | |
| 3107 | + } | |
| 3108 | + | |
| 3109 | + /** | |
| 3110 | + * Is this media reference still pointing off-site? | |
| 3111 | + * | |
| 3112 | + * Template content arrives holding the URLs of wherever the media lived before. Those | |
| 3113 | + * carry that site's attachment IDs, which have no meaning here - and can collide with | |
| 3114 | + * unrelated local posts. | |
| 3115 | + * | |
| 3116 | + * @since 2.6.2 | |
| 3117 | + * | |
| 3118 | + * @param string $url URL from a media control. | |
| 3119 | + * @return bool True when the URL points at another site's uploads. | |
| 3120 | + */ | |
| 3121 | + private static function wdkit_is_foreign_media_url( $url ) { | |
| 3122 | + | |
| 3123 | + if ( ! class_exists( 'Wdkit_Image_Guard' ) ) { | |
| 3124 | + require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php'; | |
| 3125 | + } | |
| 3126 | + | |
| 3127 | + $uploads = wp_get_upload_dir(); | |
| 3128 | + | |
| 3129 | + return Wdkit_Image_Guard::is_foreign_media( $url, isset( $uploads['baseurl'] ) ? $uploads['baseurl'] : '' ); | |
| 3130 | + } | |
| 3131 | + | |
| 3132 | + /** | |
| 3133 | + * Find - or make - the local attachment behind a source-site media URL. | |
| 3134 | + * | |
| 3135 | + * Elementor stamps every image it imports with `_elementor_source_image_hash` | |
| 3136 | + * (sha1 of the URL it came from), and its importer consults that before doing any | |
| 3137 | + * network work. Delegating here means a URL already imported at create time resolves | |
| 3138 | + * from the database, and one that never made it is fetched exactly once. | |
| 3139 | + * | |
| 3140 | + * Only ever called for foreign URLs. Handing it a local URL would re-download the | |
| 3141 | + * file and leave a duplicate, because the stored hash is of the *remote* URL and so | |
| 3142 | + * would never match. | |
| 3143 | + * | |
| 3144 | + * @since 2.6.2 | |
| 3145 | + * | |
| 3146 | + * @param string $url Source-site media URL. | |
| 3147 | + * @param int $source_id The source site's attachment ID, used as Elementor's cache key. | |
| 3148 | + * @return array Local `id` and `url`, or an empty array when it cannot be resolved. | |
| 3149 | + */ | |
| 3150 | + private static function wdkit_localise_media_url( $url, $source_id = 0 ) { | |
| 3151 | + static $cache = array(); | |
| 3152 | + | |
| 3153 | + if ( isset( $cache[ $url ] ) ) { | |
| 3154 | + return $cache[ $url ]; | |
| 3155 | + } | |
| 3156 | + | |
| 3157 | + if ( ! did_action( 'elementor/loaded' ) || ! class_exists( '\\Elementor\\Plugin' ) ) { | |
| 3158 | + return array(); | |
| 3159 | + } | |
| 3160 | + | |
| 3161 | + $images = \Elementor\Plugin::$instance->templates_manager->get_import_images_instance(); | |
| 3162 | + | |
| 3163 | + if ( ! $images ) { | |
| 3164 | + return array(); | |
| 3165 | + } | |
| 3166 | + | |
| 3167 | + // A download may happen, so keep the oversized-image guard in force. | |
| 3168 | + self::wdkit_guard_oversized_images(); | |
| 3169 | + | |
| 3170 | + $imported = $images->import( | |
| 3171 | + array( | |
| 3172 | + // Elementor only checks its hash table when an id is present. | |
| 3173 | + 'id' => $source_id ? $source_id : 1, | |
| 3174 | + 'url' => $url, | |
| 3175 | + ) | |
| 3176 | + ); | |
| 3177 | + | |
| 3178 | + $local = ( ! empty( $imported['id'] ) && ! empty( $imported['url'] ) ) | |
| 3179 | + ? array( | |
| 3180 | + 'id' => (int) $imported['id'], | |
| 3181 | + 'url' => $imported['url'], | |
| 3182 | + ) | |
| 3183 | + : array(); | |
| 3184 | + | |
| 3185 | + // Remember hits only: a sibling request importing concurrently may simply not have | |
| 3186 | + // finished yet, and caching that miss would strand every later control on this page. | |
| 3187 | + if ( $local ) { | |
| 3188 | + $cache[ $url ] = $local; | |
| 3189 | + } | |
| 3190 | + | |
| 3191 | + return $local; | |
| 3192 | + } | |
| 3193 | + | |
| 3194 | + /** | |
| 3195 | + * Repair dangling attachment IDs across every page of a finished import. | |
| 3196 | + * | |
| 3197 | + * The create-time repair in wdkit_media_import() can only see attachments that already | |
| 3198 | + * exist. Pages import concurrently and share images — an icon first imported by one | |
| 3199 | + * page is referenced by several others — so a page that runs early legitimately cannot | |
| 3200 | + * resolve an image a sibling request has not created yet. | |
| 3201 | + * | |
| 3202 | + * This runs at the finalize step, once every page and attachment exists, and fixes | |
| 3203 | + * whatever the per-page pass had to leave behind. | |
| 3204 | + * | |
| 3205 | + * @since 2.6.2 | |
| 3206 | + * | |
| 3207 | + * @param array $page_ids Imported post IDs. | |
| 3208 | + * @return int Number of pages actually rewritten. | |
| 3209 | + */ | |
| 3210 | + private function wdkit_sweep_attachment_ids( $page_ids ) { | |
| 3211 | + | |
| 3212 | + if ( empty( $page_ids ) || ! did_action( 'elementor/loaded' ) ) { | |
| 3213 | + return 0; | |
| 3214 | + } | |
| 3215 | + | |
| 3216 | + $fixed = 0; | |
| 3217 | + $ids = array_unique( array_map( 'intval', $page_ids ) ); | |
| 3218 | + | |
| 3219 | + // Primes the meta cache for the whole batch in one query, so the | |
| 3220 | + // get_post_meta() call below hits the cache instead of issuing one query | |
| 3221 | + // per imported page. | |
| 3222 | + update_meta_cache( 'post', $ids ); | |
| 3223 | + | |
| 3224 | + foreach ( $ids as $post_id ) { | |
| 3225 | + | |
| 3226 | + if ( ! $post_id ) { | |
| 3227 | + continue; | |
| 3228 | + } | |
| 3229 | + | |
| 3230 | + $raw = get_post_meta( $post_id, '_elementor_data', true ); | |
| 3231 | + | |
| 3232 | + if ( empty( $raw ) ) { | |
| 3233 | + continue; | |
| 3234 | + } | |
| 3235 | + | |
| 3236 | + $data = is_array( $raw ) ? $raw : json_decode( $raw, true ); | |
| 3237 | + | |
| 3238 | + if ( ! is_array( $data ) ) { | |
| 3239 | + continue; | |
| 3240 | + } | |
| 3241 | + | |
| 3242 | + $repaired = self::wdkit_repair_attachment_ids( $data ); | |
| 3243 | + | |
| 3244 | + if ( wp_json_encode( $repaired ) === wp_json_encode( $data ) ) { | |
| 3245 | + continue; | |
| 3246 | + } | |
| 3247 | + | |
| 3248 | + // Save through the document API so Elementor regenerates the page CSS — the | |
| 3249 | + // background-image rules are only emitted once the IDs resolve. | |
| 3250 | + $document = \Elementor\Plugin::$instance->documents->get( $post_id ); | |
| 3251 | + | |
| 3252 | + // Count only a save that actually happened. Document::save() returns false | |
| 3253 | + // without saving when the current user cannot edit the post, and reporting | |
| 3254 | + // those as repaired hides the fact that nothing changed. | |
| 3255 | + if ( $document && $document->save( array( 'elements' => $repaired ) ) ) { | |
| 3256 | + ++$fixed; | |
| 3257 | + } | |
| 3258 | + } | |
| 3259 | + | |
| 3260 | + if ( $fixed ) { | |
| 3261 | + \Elementor\Plugin::$instance->files_manager->clear_cache(); | |
| 3262 | + } | |
| 3263 | + | |
| 3264 | + return $fixed; | |
| 3265 | + } | |
| 3266 | + | |
| 3267 | + /** | |
| 3268 | + * Repair media controls whose attachment ID does not resolve. | |
| 3269 | + * | |
| 3270 | + * Elementor media controls store `{ url, id }`. Controls flagged `has_sizes` — the | |
| 3271 | + * container/section **background image** among them — do not render from `url` at all: | |
| 3272 | + * CSS generation resolves the image through the attachment ID, so a dangling ID | |
| 3273 | + * produces no `background-image` rule and the section renders with no image even | |
| 3274 | + * though its URL is perfectly correct. | |
| 3275 | + * | |
| 3276 | + * IDs arrive dangling whenever Elementor's own importer does not rewrite a control — | |
| 3277 | + * it carries the source site's ID, which means nothing locally. Now that the URL is | |
| 3278 | + * already a local upload before import, the ID can simply be looked up from it. | |
| 3279 | + * | |
| 3280 | + * @since 2.6.2 | |
| 3281 | + * | |
| 3282 | + * @param mixed $node Elementor data, walked recursively. | |
| 3283 | + * @return mixed Data with resolvable attachment IDs. | |
| 3284 | + */ | |
| 3285 | + private static function wdkit_repair_attachment_ids( $node ) { | |
| 3286 | + | |
| 3287 | + if ( ! is_array( $node ) ) { | |
| 3288 | + return $node; | |
| 3289 | + } | |
| 3290 | + | |
| 3291 | + // A media control value: has a url, and an id slot to correct. | |
| 3292 | + if ( isset( $node['url'] ) && is_string( $node['url'] ) && array_key_exists( 'id', $node ) ) { | |
| 3293 | + | |
| 3294 | + $current = (int) $node['id']; | |
| 3295 | + $is_live = $current && 'attachment' === get_post_type( $current ); | |
| 3296 | + | |
| 3297 | + if ( self::wdkit_is_foreign_media_url( $node['url'] ) ) { | |
| 3298 | + // Still pointing at the source site. Ask Elementor for the local copy: its | |
| 3299 | + // _elementor_source_image_hash lookup returns the attachment the create-time | |
| 3300 | + // import already made, so this normally costs a single query and no download. | |
| 3301 | + $local = self::wdkit_localise_media_url( $node['url'], $current ); | |
| 3302 | + | |
| 3303 | + if ( ! empty( $local['id'] ) && ! empty( $local['url'] ) ) { | |
| 3304 | + $node['id'] = $local['id']; | |
| 3305 | + $node['url'] = $local['url']; | |
| 3306 | + } | |
| 3307 | + } elseif ( ! $is_live && false !== strpos( $node['url'], '/wp-content/uploads/' ) ) { | |
| 3308 | + $resolved = self::wdkit_attachment_id_from_url( $node['url'] ); | |
| 3309 | + | |
| 3310 | + if ( $resolved ) { | |
| 3311 | + $node['id'] = $resolved; | |
| 3312 | + } | |
| 3313 | + } | |
| 3314 | + } | |
| 3315 | + | |
| 3316 | + foreach ( $node as $key => $value ) { | |
| 3317 | + if ( is_array( $value ) ) { | |
| 3318 | + $node[ $key ] = self::wdkit_repair_attachment_ids( $value ); | |
| 3319 | + } | |
| 3320 | + } | |
| 3321 | + | |
| 3322 | + return $node; | |
| 3323 | + } | |
| 3324 | + | |
| 2604 | 3325 | public function wdkit_media_import( $content = array(), $editor = '' ) { |
| 2605 | 3326 | |
| 2606 | 3327 | if ( empty( $content ) && empty( $editor ) ) { |
| 2607 | 3328 | $args = $this->wdkit_parse_args( $_POST ); |
| @@ -2621,8 +3342,9 @@ | ||
| 2621 | 3342 | if ( ! class_exists( 'Wdkit_Import_Images' ) ) { |
| 2622 | 3343 | require_once WDKIT_INCLUDES . 'admin/class-wdkit-import-images.php'; |
| 2623 | 3344 | } |
| 2624 | 3345 | |
| 3346 | + | |
| 2625 | 3347 | if ( ! empty( $args['editor'] ) && 'gutenberg' === $args['editor'] && ! empty( $content ) ) { |
| 2626 | 3348 | $media_import = array( $content ); |
| 2627 | 3349 | $media_import = self::blocks_import_media_copy_content( $media_import ); |
| 2628 | 3350 | $content = $media_import[0]; |
| @@ -2630,8 +3352,13 @@ | ||
| 2630 | 3352 | $media_import = array( $content ); |
| 2631 | 3353 | $media_import = self::widgets_elements_id_change( $media_import ); |
| 2632 | 3354 | $media_import = self::widgets_import_media_copy_content( $media_import ); |
| 2633 | 3355 | $content = $media_import[0]; |
| 3356 | + | |
| 3357 | + // Last: point any control Elementor left holding a foreign attachment ID at the | |
| 3358 | + // local attachment its URL already refers to. Without this, has_sizes controls | |
| 3359 | + // such as container background images resolve to nothing and render empty. | |
| 3360 | + $content = self::wdkit_repair_attachment_ids( $content ); | |
| 2634 | 3361 | } |
| 2635 | 3362 | |
| 2636 | 3363 | return $content; |
| 2637 | 3364 | } |
| @@ -2702,9 +3429,13 @@ | ||
| 2702 | 3429 | $control_type = \Elementor\Plugin::instance()->controls_manager->get_control( $get_control['type'] ); |
| 2703 | 3430 | $control_name = $get_control['name']; |
| 2704 | 3431 | |
| 2705 | 3432 | if ( ! $control_type ) { |
| 2706 | - return $get_element_instance; | |
| 3433 | + // Skip just this control. Returning here would abandon every control after | |
| 3434 | + // it, so a single unregistered type - routine when a kit uses an addon that | |
| 3435 | + // is not fully active yet - would silently leave the rest of the element's | |
| 3436 | + // media pointing at the source site. | |
| 3437 | + continue; | |
| 2707 | 3438 | } |
| 2708 | 3439 | |
| 2709 | 3440 | if ( method_exists( $control_type, $tp_mi_on_fun ) ) { |
| 2710 | 3441 | $get_element_instance['settings'][ $control_name ] = $control_type->{$tp_mi_on_fun}( $element->get_settings( $control_name ), $get_control ); |
| @@ -2779,73 +3510,298 @@ | ||
| 2779 | 3510 | public static function blocks_data_instance( array $block_data, array $args = array(), $block_args = null ) { |
| 2780 | 3511 | |
| 2781 | 3512 | 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'] ) ) ) { |
| 2782 | 3513 | $blocks_attr = isset( $block_data['attributes'] ) ? $block_data['attributes'] : ( isset( $block_data['attrs'] ) ? $block_data['attrs'] : array() ); |
| 2783 | - foreach ( $blocks_attr as $block_key => $block_val ) { | |
| 2784 | - if ( isset( $block_val['url'] ) && isset( $block_val['id'] ) && ! empty( $block_val['url'] ) ) { | |
| 2785 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $block_val ); | |
| 2786 | - $blocks_attr[ $block_key ] = $new_media; | |
| 2787 | - } elseif ( isset( $block_val['url'] ) && ! empty( $block_val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $block_val['url'] ) ) { | |
| 2788 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $block_val ); | |
| 2789 | - $blocks_attr[ $block_key ] = $new_media; | |
| 2790 | - } elseif ( is_array( $block_val ) && ! empty( $block_val ) ) { | |
| 2791 | - 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 ) ) { | |
| 2792 | - foreach ( $block_val as $key => $val ) { | |
| 2793 | - if ( is_array( $val ) && ! empty( $val ) ) { | |
| 3514 | + $blocks_attr = self::wdkit_import_block_media( $blocks_attr ); | |
| 3515 | + if ( isset( $block_data['attributes'] ) ) { | |
| 3516 | + $block_data['attributes'] = $blocks_attr; | |
| 3517 | + } elseif ( isset( $block_data['attrs'] ) ) { | |
| 3518 | + $block_data['attrs'] = $blocks_attr; | |
| 3519 | + } | |
| 2794 | 3520 | |
| 2795 | - if ( isset( $val['url'] ) && ( isset( $val['Id'] ) || isset( $val['id'] ) ) && ! empty( $val['url'] ) ) { | |
| 2796 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $val ); | |
| 2797 | - $blocks_attr[ $block_key ][ $key ] = $new_media; | |
| 2798 | - } elseif ( isset( $val['url'] ) && ! empty( $val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $val['url'] ) ) { | |
| 2799 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $val ); | |
| 2800 | - $blocks_attr[ $block_key ][ $key ] = $new_media; | |
| 2801 | - } else { | |
| 2802 | - foreach ( $val as $sub_key => $sub_val ) { | |
| 2803 | - if ( isset( $sub_val['url'] ) && ( isset( $sub_val['Id'] ) || isset( $sub_val['id'] ) ) && ! empty( $sub_val['url'] ) ) { | |
| 2804 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val ); | |
| 3521 | + $block_data = self::wdkit_relink_block_markup( $block_data ); | |
| 3522 | + } | |
| 2805 | 3523 | |
| 2806 | - if ( is_array( $sub_val ) && is_array( $new_media ) ) { | |
| 2807 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = array_merge( $sub_val, $new_media ); | |
| 2808 | - } else { | |
| 2809 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = $new_media; | |
| 2810 | - } | |
| 2811 | - } elseif ( isset( $sub_val['url'] ) && ! empty( $sub_val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $sub_val['url'] ) ) { | |
| 2812 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val ); | |
| 2813 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = $new_media; | |
| 2814 | - } elseif ( is_array( $sub_val ) && ! empty( $sub_val ) ) { | |
| 2815 | - foreach ( $sub_val as $sub_key1 => $sub_val1 ) { | |
| 2816 | - if ( isset( $sub_val1['url'] ) && ( isset( $sub_val1['Id'] ) || isset( $sub_val1['id'] ) ) && ! empty( $sub_val1['url'] ) ) { | |
| 2817 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val1 ); | |
| 3524 | + return $block_data; | |
| 3525 | + } | |
| 2818 | 3526 | |
| 2819 | - if ( is_array( $sub_val1 ) && is_array( $new_media ) ) { | |
| 2820 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = array_merge( $sub_val1, $new_media ); | |
| 2821 | - } else { | |
| 2822 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = $new_media; | |
| 2823 | - } | |
| 2824 | - } elseif ( isset( $sub_val1['url'] ) && ! empty( $sub_val1['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $sub_val1['url'] ) ) { | |
| 2825 | - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val1 ); | |
| 2826 | - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = $new_media; | |
| 2827 | - } | |
| 2828 | - } | |
| 2829 | - } | |
| 2830 | - } | |
| 2831 | - } | |
| 2832 | - } | |
| 2833 | - } | |
| 2834 | - } | |
| 3527 | + /** | |
| 3528 | + * Run block markup through the media import, the way the create path does. | |
| 3529 | + * | |
| 3530 | + * Used wherever block content is written from the browser: media import, then the Nexter | |
| 3531 | + * block processor so each block's rendered copy matches its attributes, then serialise. | |
| 3532 | + * | |
| 3533 | + * @since 2.6.2 | |
| 3534 | + * | |
| 3535 | + * @param string $content Block markup. | |
| 3536 | + * @return string Block markup with local media. | |
| 3537 | + */ | |
| 3538 | + private function wdkit_relink_gutenberg_content( $content ) { | |
| 3539 | + | |
| 3540 | + if ( ! is_string( $content ) || false === strpos( $content, '<!-- wp:' ) ) { | |
| 3541 | + return $content; | |
| 3542 | + } | |
| 3543 | + | |
| 3544 | + // wdkit_media_import() loads this itself, but it is referenced before that below. | |
| 3545 | + if ( ! class_exists( 'Wdkit_Import_Images' ) ) { | |
| 3546 | + require_once WDKIT_INCLUDES . 'admin/class-wdkit-import-images.php'; | |
| 3547 | + } | |
| 3548 | + | |
| 3549 | + // Thumbnail generation decodes each image, so keep the oversized-image guard in force. | |
| 3550 | + self::wdkit_guard_oversized_images(); | |
| 3551 | + | |
| 3552 | + | |
| 3553 | + // Block attributes are JSON inside the block delimiters, so they only survive a parse | |
| 3554 | + // when the string carries exactly one level of escaping. Arrive with an extra level and | |
| 3555 | + // parse_blocks() reads no attributes at all - serialising that back out writes every | |
| 3556 | + // block bare, throwing away titles, body text, icons and styling. | |
| 3557 | + $parsable = self::wdkit_parsable_block_content( $content ); | |
| 3558 | + | |
| 3559 | + if ( null === $parsable ) { | |
| 3560 | + | |
| 3561 | + return $content; | |
| 3562 | + } | |
| 3563 | + | |
| 3564 | + $blocks = parse_blocks( $parsable ); | |
| 3565 | + $blocks = $this->wdkit_media_import( $blocks, 'gutenberg' ); | |
| 3566 | + | |
| 3567 | + if ( empty( $blocks ) || ! is_array( $blocks ) ) { | |
| 3568 | + return $content; | |
| 3569 | + } | |
| 3570 | + | |
| 3571 | + if ( class_exists( 'WDKIT_Nexter_Block_Processor' ) ) { | |
| 3572 | + $processor = new WDKIT_Nexter_Block_Processor(); | |
| 3573 | + $blocks = $processor->run( $blocks ); | |
| 3574 | + } | |
| 3575 | + | |
| 3576 | + $serialised = serialize_blocks( $blocks ); | |
| 3577 | + | |
| 3578 | + // Last line of defence. This function exists to repoint media, so a result carrying | |
| 3579 | + // fewer block attributes than it started with is a broken round trip, not a rewrite. | |
| 3580 | + // Leaving the media wrong is recoverable; saving gutted content is not. | |
| 3581 | + $before = self::wdkit_block_attr_count( $parsable ); | |
| 3582 | + $after = self::wdkit_block_attr_count( $serialised ); | |
| 3583 | + | |
| 3584 | + if ( $after < $before ) { | |
| 3585 | + | |
| 3586 | + return $content; | |
| 3587 | + } | |
| 3588 | + | |
| 3589 | + // Never hand back nothing: an empty result would blank the page. | |
| 3590 | + return ! empty( $serialised ) ? $serialised : $content; | |
| 3591 | + } | |
| 3592 | + | |
| 3593 | + /** | |
| 3594 | + * Rebuild the block stylesheet for a page whose content we just rewrote. | |
| 3595 | + * | |
| 3596 | + * The addon keeps each block's styling in a generated per-page stylesheet, and every rule | |
| 3597 | + * is keyed to the block id it was written for. That file is produced when the page is | |
| 3598 | + * saved through the editor - not by wp_update_post() from an AJAX handler - so rewriting | |
| 3599 | + * content here leaves the page pointing at a stylesheet built for the previous markup. | |
| 3600 | + * Blocks whose ids are not in that file get no rules at all and render unstyled. | |
| 3601 | + * | |
| 3602 | + * @since 2.6.2 | |
| 3603 | + * | |
| 3604 | + * @param int $post_id Page whose content changed. | |
| 3605 | + * @return bool True when a rebuild was triggered. | |
| 3606 | + */ | |
| 3607 | + private static function wdkit_rebuild_block_css( $post_id ) { | |
| 3608 | + | |
| 3609 | + if ( ! $post_id ) { | |
| 3610 | + return false; | |
| 3611 | + } | |
| 3612 | + | |
| 3613 | + foreach ( get_declared_classes() as $class ) { | |
| 3614 | + if ( ! method_exists( $class, 'make_block_css_by_post_id' ) ) { | |
| 3615 | + continue; | |
| 3616 | + } | |
| 3617 | + | |
| 3618 | + try { | |
| 3619 | + if ( method_exists( $class, 'instance' ) ) { | |
| 3620 | + $instance = $class::instance(); | |
| 3621 | + } elseif ( method_exists( $class, 'get_instance' ) ) { | |
| 3622 | + $instance = $class::get_instance(); | |
| 3623 | + } else { | |
| 3624 | + $instance = new $class(); | |
| 2835 | 3625 | } |
| 3626 | + | |
| 3627 | + $instance->make_block_css_by_post_id( $post_id ); | |
| 3628 | + | |
| 3629 | + | |
| 3630 | + return true; | |
| 3631 | + } catch ( \Throwable $e ) { | |
| 3632 | + // Styling is best-effort: a failure here must not fail the import. | |
| 3633 | + | |
| 3634 | + return false; | |
| 2836 | 3635 | } |
| 2837 | - if ( isset( $block_data['attributes'] ) ) { | |
| 2838 | - $block_data['attributes'] = $blocks_attr; | |
| 2839 | - } elseif ( isset( $block_data['attrs'] ) ) { | |
| 2840 | - $block_data['attrs'] = $blocks_attr; | |
| 3636 | + } | |
| 3637 | + | |
| 3638 | + return false; | |
| 3639 | + } | |
| 3640 | + | |
| 3641 | + /** | |
| 3642 | + * How many block attributes does this markup actually yield when parsed? | |
| 3643 | + * | |
| 3644 | + * Used as a before/after measure: block attributes are the part of block markup a round | |
| 3645 | + * trip can silently drop, so counting them is how we tell a rewrite from a mangling. | |
| 3646 | + * | |
| 3647 | + * @since 2.6.2 | |
| 3648 | + * | |
| 3649 | + * @param string $content Block markup. | |
| 3650 | + * @return int Total attributes across every block. | |
| 3651 | + */ | |
| 3652 | + private static function wdkit_block_attr_count( $content ) { | |
| 3653 | + $total = 0; | |
| 3654 | + | |
| 3655 | + $walk = function ( $blocks ) use ( &$walk, &$total ) { | |
| 3656 | + foreach ( $blocks as $block ) { | |
| 3657 | + if ( ! empty( $block['attrs'] ) && is_array( $block['attrs'] ) ) { | |
| 3658 | + $total += count( $block['attrs'] ); | |
| 3659 | + } | |
| 3660 | + | |
| 3661 | + if ( ! empty( $block['innerBlocks'] ) ) { | |
| 3662 | + $walk( $block['innerBlocks'] ); | |
| 3663 | + } | |
| 2841 | 3664 | } |
| 3665 | + }; | |
| 3666 | + | |
| 3667 | + $walk( parse_blocks( (string) $content ) ); | |
| 3668 | + | |
| 3669 | + return $total; | |
| 3670 | + } | |
| 3671 | + | |
| 3672 | + /** | |
| 3673 | + * Return this content in a form whose block attributes actually parse. | |
| 3674 | + * | |
| 3675 | + * Content written straight to post_content never had to parse, so an extra level of | |
| 3676 | + * escaping on the way in did no harm. Parsing it - which repointing media requires - makes | |
| 3677 | + * that escaping fatal: `{\"Title\":\"…\"}` is not JSON, so every attribute is discarded. | |
| 3678 | + * | |
| 3679 | + * Rather than assume a slash depth, this measures: if stripping one level yields more | |
| 3680 | + * attributes, the content was over-escaped and the stripped form is the real one. | |
| 3681 | + * | |
| 3682 | + * @since 2.6.2 | |
| 3683 | + * | |
| 3684 | + * @param string $content Block markup as received. | |
| 3685 | + * @return string|null Markup safe to parse, or null when no form of it parses. | |
| 3686 | + */ | |
| 3687 | + private static function wdkit_parsable_block_content( $content ) { | |
| 3688 | + | |
| 3689 | + $as_is = self::wdkit_block_attr_count( $content ); | |
| 3690 | + | |
| 3691 | + // Nothing claims to carry attributes, so there is nothing to lose either. | |
| 3692 | + if ( false === strpos( $content, '{' ) ) { | |
| 3693 | + return $content; | |
| 2842 | 3694 | } |
| 2843 | 3695 | |
| 3696 | + $stripped = wp_unslash( $content ); | |
| 3697 | + $stripped_attrs = self::wdkit_block_attr_count( $stripped ); | |
| 3698 | + | |
| 3699 | + if ( $stripped_attrs > $as_is ) { | |
| 3700 | + return $stripped; | |
| 3701 | + } | |
| 3702 | + | |
| 3703 | + if ( $as_is > 0 ) { | |
| 3704 | + return $content; | |
| 3705 | + } | |
| 3706 | + | |
| 3707 | + // Neither form parses into attributes even though the markup contains JSON: better to | |
| 3708 | + // leave the content exactly as it arrived than to rewrite it into something bare. | |
| 3709 | + return null; | |
| 3710 | + } | |
| 3711 | + | |
| 3712 | + /** | |
| 3713 | + * Point a block's saved markup at the media that was just localised. | |
| 3714 | + * | |
| 3715 | + * A block stores a rendered copy of itself in `innerHTML` / `innerContent`, and for many | |
| 3716 | + * blocks that copy is what the front end actually outputs. Importing the attributes alone | |
| 3717 | + * therefore fixes the editor while leaving the page still loading from the site the | |
| 3718 | + * template came from - and those hosts answer 403, so the image renders broken. | |
| 3719 | + * | |
| 3720 | + * @since 2.6.2 | |
| 3721 | + * | |
| 3722 | + * @param array $block_data One parsed block. | |
| 3723 | + * @return array The block with its markup repointed. | |
| 3724 | + */ | |
| 3725 | + private static function wdkit_relink_block_markup( $block_data ) { | |
| 3726 | + | |
| 3727 | + $map = Wdkit_Import_Images::get_url_map(); | |
| 3728 | + | |
| 3729 | + if ( empty( $map ) ) { | |
| 3730 | + return $block_data; | |
| 3731 | + } | |
| 3732 | + | |
| 3733 | + $from = array_keys( $map ); | |
| 3734 | + $to = array_values( $map ); | |
| 3735 | + | |
| 3736 | + if ( ! empty( $block_data['innerHTML'] ) && is_string( $block_data['innerHTML'] ) ) { | |
| 3737 | + $block_data['innerHTML'] = str_replace( $from, $to, $block_data['innerHTML'] ); | |
| 3738 | + } | |
| 3739 | + | |
| 3740 | + if ( ! empty( $block_data['innerContent'] ) && is_array( $block_data['innerContent'] ) ) { | |
| 3741 | + foreach ( $block_data['innerContent'] as $index => $chunk ) { | |
| 3742 | + if ( is_string( $chunk ) ) { | |
| 3743 | + $block_data['innerContent'][ $index ] = str_replace( $from, $to, $chunk ); | |
| 3744 | + } | |
| 3745 | + } | |
| 3746 | + } | |
| 3747 | + | |
| 2844 | 3748 | return $block_data; |
| 2845 | 3749 | } |
| 2846 | 3750 | |
| 2847 | 3751 | /** |
| 3752 | + * Import every media reference held in a block's attributes. | |
| 3753 | + * | |
| 3754 | + * Block attributes nest arbitrarily - a repeater of cards each with an image, responsive | |
| 3755 | + * variants, nested inner settings - so this recurses rather than reaching a fixed number | |
| 3756 | + * of levels down. The previous version was unrolled exactly four levels deep and also | |
| 3757 | + * skipped any subtree carrying an `md` key, which meant anything below that simply kept | |
| 3758 | + * the source site's URL and attachment ID and rendered as an empty placeholder. | |
| 3759 | + * | |
| 3760 | + * A node counts as media when it has a non-empty string `url` and either an id slot or a | |
| 3761 | + * URL that names an image file. That pairing is what distinguishes a media control from | |
| 3762 | + * a link, which also carries a `url`. | |
| 3763 | + * | |
| 3764 | + * @since 2.6.2 | |
| 3765 | + * | |
| 3766 | + * @param mixed $node Block attributes, walked recursively. | |
| 3767 | + * @return mixed Attributes with local media. | |
| 3768 | + */ | |
| 3769 | + private static function wdkit_import_block_media( $node ) { | |
| 3770 | + | |
| 3771 | + if ( ! is_array( $node ) ) { | |
| 3772 | + return $node; | |
| 3773 | + } | |
| 3774 | + | |
| 3775 | + $url = isset( $node['url'] ) && is_string( $node['url'] ) ? $node['url'] : ''; | |
| 3776 | + | |
| 3777 | + if ( '' !== $url | |
| 3778 | + && ( array_key_exists( 'id', $node ) || array_key_exists( 'Id', $node ) | |
| 3779 | + || preg_match( '/\.(?:jpe?g|png|gif|svg|webp|avif|bmp)$/i', (string) wp_parse_url( $url, PHP_URL_PATH ) ) ) | |
| 3780 | + ) { | |
| 3781 | + $imported = Wdkit_Import_Images::wdkit_Import_media( $node ); | |
| 3782 | + | |
| 3783 | + // Only accept a real result. The importer returns the node untouched when it | |
| 3784 | + // cannot localise the file, and anything falsy here would wipe out the URL and | |
| 3785 | + // leave the block with no image at all. | |
| 3786 | + if ( ! empty( $imported['url'] ) ) { | |
| 3787 | + $node = array_merge( $node, $imported ); | |
| 3788 | + } | |
| 3789 | + } | |
| 3790 | + | |
| 3791 | + // Keep walking even after importing this node. A media value carries its own `sizes` | |
| 3792 | + // map of per-size URLs, and returning here left every one of those pointing at the | |
| 3793 | + // site the template came from - which is what the widgets actually render from. | |
| 3794 | + foreach ( $node as $key => $value ) { | |
| 3795 | + if ( is_array( $value ) ) { | |
| 3796 | + $node[ $key ] = self::wdkit_import_block_media( $value ); | |
| 3797 | + } | |
| 3798 | + } | |
| 3799 | + | |
| 3800 | + return $node; | |
| 3801 | + } | |
| 3802 | + | |
| 3803 | + /** | |
| 2848 | 3804 | * Kit Template Import Pages/Sections |
| 2849 | 3805 | * |
| 2850 | 3806 | * @since 1.0.0 |
| 2851 | 3807 | * */ |
| @@ -3032,9 +3988,17 @@ | ||
| 3032 | 3988 | ); |
| 3033 | 3989 | |
| 3034 | 3990 | $response = WDesignKit_Data_Query::get_data( $api_type, $temp_args ); |
| 3035 | 3991 | |
| 3036 | - if ( 'error' === $response['content'] ) { | |
| 3992 | + if ( is_wp_error( $response ) ) { | |
| 3993 | + wp_send_json( array( | |
| 3994 | + 'success' => false, | |
| 3995 | + 'message' => $response->get_error_message(), | |
| 3996 | + ) ); | |
| 3997 | + wp_die(); | |
| 3998 | + } | |
| 3999 | + | |
| 4000 | + if ( isset( $response['content'] ) && 'error' === $response['content'] ) { | |
| 3037 | 4001 | wp_send_json( $response ); |
| 3038 | 4002 | wp_die(); |
| 3039 | 4003 | } |
| 3040 | 4004 | |
| @@ -3102,8 +4066,14 @@ | ||
| 3102 | 4066 | } |
| 3103 | 4067 | |
| 3104 | 4068 | $document = \Elementor\Plugin::$instance->documents->get($template_id); |
| 3105 | 4069 | |
| 4070 | + // This saves content posted straight from the browser, which carries local image | |
| 4071 | + // URLs but still the source template's attachment IDs. Without repairing them the | |
| 4072 | + // save undoes what wdkit_media_import() fixed on create, and has_sizes controls — | |
| 4073 | + // container background images especially — resolve to nothing and render empty. | |
| 4074 | + $content = self::wdkit_repair_attachment_ids( $content ); | |
| 4075 | + | |
| 3106 | 4076 | $document->save([ |
| 3107 | 4077 | 'elements' => $content |
| 3108 | 4078 | ]); |
| 3109 | 4079 | } |
| @@ -3108,8 +4078,94 @@ | ||
| 3108 | 4078 | ]); |
| 3109 | 4079 | } |
| 3110 | 4080 | |
| 3111 | 4081 | /** |
| 4082 | + * Update the content of an already-created page. | |
| 4083 | + * | |
| 4084 | + * Used by the async ("Site Ready first") import path: pages are created up front with | |
| 4085 | + * their un-rewritten template content, then this writes the AI-rewritten content into | |
| 4086 | + * each page in the background. Elementor saves via the document API (same as | |
| 4087 | + * wkit_update_elementor_template); Gutenberg writes post_content directly. | |
| 4088 | + * | |
| 4089 | + * @since 2.6.2 | |
| 4090 | + */ | |
| 4091 | + protected function wdkit_update_page_content() { | |
| 4092 | + $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0; | |
| 4093 | + $builder = isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : ''; | |
| 4094 | + | |
| 4095 | + if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) { | |
| 4096 | + return array( | |
| 4097 | + 'success' => false, | |
| 4098 | + 'message' => esc_html__( 'Invalid page or insufficient permission', 'wdesignkit' ), | |
| 4099 | + ); | |
| 4100 | + } | |
| 4101 | + | |
| 4102 | + if ( 'gutenberg' === $builder ) { | |
| 4103 | + // Do NOT run kses here: Gutenberg block delimiters are HTML comments | |
| 4104 | + // (<!-- wp:... -->) which kses strips. Mirror the create path, which stores | |
| 4105 | + // the block markup slashed and unfiltered (endpoint is manage_options-gated | |
| 4106 | + // and the content is plugin-generated). | |
| 4107 | + $content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : ''; | |
| 4108 | + | |
| 4109 | + // This content comes straight from the browser and still carries the template | |
| 4110 | + // site's media URLs and attachment IDs, so it has to go through the same pipeline | |
| 4111 | + // the create path uses. Without this the save simply undid the import: the files | |
| 4112 | + // were fetched, then overwritten by a copy still pointing at the source site. | |
| 4113 | + // | |
| 4114 | + // Re-running is cheap. Every URL already handled resolves from the source-hash | |
| 4115 | + // lookup, and media that is already local resolves straight from its URL, so no | |
| 4116 | + // image is fetched or stored twice. | |
| 4117 | + $content = $this->wdkit_relink_gutenberg_content( $content ); | |
| 4118 | + | |
| 4119 | + $result = wp_update_post( | |
| 4120 | + array( | |
| 4121 | + 'ID' => $post_id, | |
| 4122 | + 'post_content' => wp_slash( $content ), | |
| 4123 | + ), | |
| 4124 | + true | |
| 4125 | + ); | |
| 4126 | + | |
| 4127 | + if ( is_wp_error( $result ) ) { | |
| 4128 | + return array( | |
| 4129 | + 'success' => false, | |
| 4130 | + 'message' => $result->get_error_message(), | |
| 4131 | + ); | |
| 4132 | + } | |
| 4133 | + | |
| 4134 | + self::wdkit_rebuild_block_css( $post_id ); | |
| 4135 | + } else { | |
| 4136 | + $elements = isset( $_POST['content'] ) ? json_decode( wp_unslash( $_POST['content'] ), true ) : array(); | |
| 4137 | + | |
| 4138 | + if ( ! class_exists( '\\Elementor\\Plugin' ) ) { | |
| 4139 | + return array( | |
| 4140 | + 'success' => false, | |
| 4141 | + 'message' => esc_html__( 'Elementor not available', 'wdesignkit' ), | |
| 4142 | + ); | |
| 4143 | + } | |
| 4144 | + | |
| 4145 | + $document = \Elementor\Plugin::$instance->documents->get( $post_id ); | |
| 4146 | + if ( ! $document ) { | |
| 4147 | + return array( | |
| 4148 | + 'success' => false, | |
| 4149 | + 'message' => esc_html__( 'Elementor document not found', 'wdesignkit' ), | |
| 4150 | + ); | |
| 4151 | + } | |
| 4152 | + | |
| 4153 | + // Same as wkit_update_elementor_template(): browser-posted content keeps the | |
| 4154 | + // source template's attachment IDs, so repair them or this save undoes the | |
| 4155 | + // create-time fix and background images stop rendering. | |
| 4156 | + $elements = self::wdkit_repair_attachment_ids( $elements ); | |
| 4157 | + | |
| 4158 | + $document->save( array( 'elements' => $elements ) ); | |
| 4159 | + } | |
| 4160 | + | |
| 4161 | + return array( | |
| 4162 | + 'success' => true, | |
| 4163 | + 'message' => esc_html__( 'Page content updated', 'wdesignkit' ), | |
| 4164 | + ); | |
| 4165 | + } | |
| 4166 | + | |
| 4167 | + /** | |
| 3112 | 4168 | * Import single template and section from plugin only |
| 3113 | 4169 | * |
| 3114 | 4170 | * @param array $args store data. |
| 3115 | 4171 | * @param array $template_id store data. |
| @@ -3117,8 +4173,21 @@ | ||
| 3117 | 4173 | * @param array $temp_data store data. |
| 3118 | 4174 | * */ |
| 3119 | 4175 | protected function import_page_section_content() { |
| 3120 | 4176 | |
| 4177 | + // Elementor sideloads every image referenced by the page from inside this request. | |
| 4178 | + // A single oversized source image decodes to more than the whole memory limit, so | |
| 4179 | + // guard before any of that starts. | |
| 4180 | + $this->wdkit_guard_oversized_images(); | |
| 4181 | + | |
| 4182 | + // Sideloading images for image-heavy pages (wdkit_media_import → Imagick | |
| 4183 | + // thumbnail generation per image) can exceed the default 30s execution | |
| 4184 | + // limit and fatal the request mid-import. Give this single page import | |
| 4185 | + // more headroom; harmless no-op where set_time_limit() is disabled. | |
| 4186 | + if ( function_exists( 'set_time_limit' ) ) { | |
| 4187 | + @set_time_limit( 120 ); | |
| 4188 | + } | |
| 4189 | + | |
| 3121 | 4190 | if ( isset( $_POST['args'] ) ) { |
| 3122 | 4191 | $args = ! empty( $_POST['args'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['args'] ) ), true ) : array(); |
| 3123 | 4192 | } |
| 3124 | 4193 | |
| @@ -3141,11 +4210,9 @@ | ||
| 3141 | 4210 | if ( isset( $_POST['template_id'] ) ) { |
| 3142 | 4211 | $template_id = ! empty( $_POST['template_id'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['template_id'] ), true ) ) : ''; |
| 3143 | 4212 | } |
| 3144 | 4213 | |
| 3145 | - if ( isset( $_POST['temp_type'] ) ) { | |
| 3146 | - $temp_type = isset( $_POST['temp_type'] ) ? sanitize_text_field( wp_unslash( $_POST['temp_type'] ) ) : 'normal'; | |
| 3147 | - } | |
| 4214 | + $temp_type = isset( $_POST['temp_type'] ) ? sanitize_text_field( wp_unslash( $_POST['temp_type'] ) ) : 'normal'; | |
| 3148 | 4215 | |
| 3149 | 4216 | if ( isset( $_POST['data'] ) ) { |
| 3150 | 4217 | $data = ! empty( $_POST['data'] ) ? json_decode( wp_unslash( $_POST['data'] ) ) : ''; |
| 3151 | 4218 | } |
| @@ -3166,8 +4233,21 @@ | ||
| 3166 | 4233 | } |
| 3167 | 4234 | |
| 3168 | 4235 | if ( ! empty( $data ) && ! empty( $template_id ) && ! empty( $post_type ) && current_user_can( 'manage_options' ) ) { |
| 3169 | 4236 | $post_content = $data; |
| 4237 | + // Restore The Plus Addons' globals before the page is built, so the widgets' | |
| 4238 | + // tp_global_preset references resolve as soon as it renders. Done here rather | |
| 4239 | + // than in the save-template UI's confirmation dialog so that every import path | |
| 4240 | + // - the library, the abilities, the theme builder - gets it. | |
| 4241 | + if ( isset( $post_content->tp_globals ) && ! empty( $post_content->tp_globals ) ) { | |
| 4242 | + $this->wdkit_merge_tp_globals( | |
| 4243 | + json_decode( wp_json_encode( $post_content->tp_globals ), true ), | |
| 4244 | + isset( $post_content->tp_global_refs ) | |
| 4245 | + ? json_decode( wp_json_encode( $post_content->tp_global_refs ), true ) | |
| 4246 | + : array() | |
| 4247 | + ); | |
| 4248 | + } | |
| 4249 | + | |
| 3170 | 4250 | $post_title = isset( $post_content->title ) ? sanitize_text_field( $post_content->title ) : ''; |
| 3171 | 4251 | $post_slug = isset( $post_content->slug ) ? sanitize_text_field( $post_content->slug ) : ''; |
| 3172 | 4252 | $file_type = isset( $post_content->file_type ) ? sanitize_text_field( $post_content->file_type ) : ''; |
| 3173 | 4253 | $content = isset( $post_content->content ) ? wp_slash( $post_content->content ) : ''; |
| @@ -3177,9 +4257,9 @@ | ||
| 3177 | 4257 | if ( empty( $content ) ) { |
| 3178 | 4258 | wp_send_json( |
| 3179 | 4259 | array( |
| 3180 | 4260 | 'template_id' => $template_id, |
| 3181 | - 'message' => 'Content is Empty.', | |
| 4261 | + 'message' => __( 'Content is Empty.', 'wdesignkit' ), | |
| 3182 | 4262 | ) |
| 3183 | 4263 | ); |
| 3184 | 4264 | wp_die(); |
| 3185 | 4265 | } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'wp_block' === $file_type ) { |
| @@ -3185,27 +4265,17 @@ | ||
| 3185 | 4265 | } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'wp_block' === $file_type ) { |
| 3186 | 4266 | |
| 3187 | 4267 | $editor = ( 'wdkit' === $args['editor'] ) ? 'gutenberg' : $args['editor']; |
| 3188 | 4268 | $blocks = parse_blocks( stripslashes( $content ) ); |
| 3189 | - | |
| 4269 | + | |
| 3190 | 4270 | $blocks = $this->wdkit_media_import( $blocks, $editor ); |
| 3191 | 4271 | |
| 3192 | 4272 | $processor = new WDKIT_Nexter_Block_Processor(); |
| 3193 | 4273 | $blocks = $processor->run( $blocks ); |
| 3194 | 4274 | $content = serialize_blocks( $blocks ); |
| 3195 | - | |
| 4275 | + | |
| 3196 | 4276 | $content = $this->replace_unicode_glitch( serialize_blocks( $blocks ) ); |
| 3197 | 4277 | |
| 3198 | - if ( ! empty( $category_list ) && is_array( $category_list ) ) { | |
| 3199 | - $category_ids = array_map( 'intval', $category_list ); | |
| 3200 | - wp_set_post_terms( $inserted_id, $category_ids, 'category' ); | |
| 3201 | - } | |
| 3202 | - | |
| 3203 | - if ( ! empty( $tag_list ) && is_array( $tag_list ) ) { | |
| 3204 | - $tag_ids = array_map( 'intval', $tag_list ); | |
| 3205 | - wp_set_post_terms( $inserted_id, $tag_ids, 'post_tag' ); | |
| 3206 | - } | |
| 3207 | - | |
| 3208 | 4278 | $inserted_post = wp_insert_post( |
| 3209 | 4279 | array( |
| 3210 | 4280 | 'post_status' => 'publish', |
| 3211 | 4281 | 'post_type' => $post_type, |
| @@ -3224,9 +4294,9 @@ | ||
| 3224 | 4294 | ); |
| 3225 | 4295 | wp_die(); |
| 3226 | 4296 | } |
| 3227 | 4297 | |
| 3228 | - if ( ! empty( $thumb_image ) ) { | |
| 4298 | + if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) { | |
| 3229 | 4299 | // $featured_image_url = esc_url_raw( $thumb_image ); |
| 3230 | 4300 | $tmp = download_url( $thumb_image ); |
| 3231 | 4301 | if ( is_wp_error( $tmp ) ) { |
| 3232 | 4302 | error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) ); |
| @@ -3262,9 +4332,9 @@ | ||
| 3262 | 4332 | $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : ''; |
| 3263 | 4333 | if ( ! empty( $custom_meta ) ) { |
| 3264 | 4334 | foreach ( $custom_meta as $meta_key => $meta_val ) { |
| 3265 | 4335 | if ( isset( $meta_val[0] ) && ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { |
| 3266 | - $meta_val[0] = maybe_unserialize( $meta_val[0] ); | |
| 4336 | + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); | |
| 3267 | 4337 | } |
| 3268 | 4338 | |
| 3269 | 4339 | if ( '' === get_post_meta( $inserted_post, $meta_key, true ) && isset( $meta_val[0] ) ) { |
| 3270 | 4340 | add_post_meta( $inserted_post, $meta_key, $meta_val[0] ); |
| @@ -3300,9 +4370,9 @@ | ||
| 3300 | 4370 | wp_send_json( |
| 3301 | 4371 | array( |
| 3302 | 4372 | $temp_id => $temp_detail, |
| 3303 | 4373 | 'description' => 'Yay! Your Section has been Successfully Imported.', |
| 3304 | - 'message' => 'Successfully Imported.', | |
| 4374 | + 'message' => __( 'Successfully Imported.', 'wdesignkit' ), | |
| 3305 | 4375 | 'inserted_id' => $inserted_post, |
| 3306 | 4376 | 'success' => true, |
| 3307 | 4377 | ) |
| 3308 | 4378 | ); |
| @@ -3313,9 +4383,9 @@ | ||
| 3313 | 4383 | if ( empty( $content ) ) { |
| 3314 | 4384 | wp_send_json( |
| 3315 | 4385 | array( |
| 3316 | 4386 | 'template_id' => $template_id, |
| 3317 | - 'message' => 'Content is Empty.', | |
| 4387 | + 'message' => __( 'Content is Empty.', 'wdesignkit' ), | |
| 3318 | 4388 | ) |
| 3319 | 4389 | ); |
| 3320 | 4390 | wp_die(); |
| 3321 | 4391 | } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'elementor' === $file_type ) { |
| @@ -3353,9 +4423,9 @@ | ||
| 3353 | 4423 | } |
| 3354 | 4424 | |
| 3355 | 4425 | $inserted_id = $new_document->get_main_id(); |
| 3356 | 4426 | |
| 3357 | - if ( ! empty( $thumb_image ) ) { | |
| 4427 | + if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) { | |
| 3358 | 4428 | // $featured_image_url = esc_url_raw( $thumb_image ); |
| 3359 | 4429 | $tmp = download_url( $thumb_image ); |
| 3360 | 4430 | if ( is_wp_error( $tmp ) ) { |
| 3361 | 4431 | error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) ); |
| @@ -3409,9 +4479,9 @@ | ||
| 3409 | 4479 | $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : ''; |
| 3410 | 4480 | if ( ! empty( $custom_meta ) ) { |
| 3411 | 4481 | foreach ( $custom_meta as $meta_key => $meta_val ) { |
| 3412 | 4482 | if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { |
| 3413 | - $meta_val[0] = maybe_unserialize( $meta_val[0] ); | |
| 4483 | + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); | |
| 3414 | 4484 | } |
| 3415 | 4485 | if ( '' === get_post_meta( $inserted_id, $meta_key, true ) ) { |
| 3416 | 4486 | add_post_meta( $inserted_id, $meta_key, $meta_val[0] ); |
| 3417 | 4487 | } |
| @@ -3440,9 +4510,9 @@ | ||
| 3440 | 4510 | array( |
| 3441 | 4511 | $temp_id => $temp_detail, |
| 3442 | 4512 | 'content' => $temp_con, |
| 3443 | 4513 | 'description' => 'Yay! Your Section has been Successfully Imported.', |
| 3444 | - 'message' => 'Successfully Imported.', | |
| 4514 | + 'message' => __( 'Successfully Imported.', 'wdesignkit' ), | |
| 3445 | 4515 | 'inserted_id' => $inserted_id, |
| 3446 | 4516 | 'success' => true, |
| 3447 | 4517 | ) |
| 3448 | 4518 | ); |
| @@ -3572,8 +4642,12 @@ | ||
| 3572 | 4642 | } |
| 3573 | 4643 | |
| 3574 | 4644 | $image_url = esc_url_raw( $_POST['image_url'] ); |
| 3575 | 4645 | |
| 4646 | + if ( ! wdesignkit_validate_external_url( $image_url ) ) { | |
| 4647 | + wp_send_json_error( 'Image could not be downloaded.' ); | |
| 4648 | + } | |
| 4649 | + | |
| 3576 | 4650 | $tmp_file = download_url( $image_url ); |
| 3577 | 4651 | if ( is_wp_error( $tmp_file ) ) { |
| 3578 | 4652 | wp_send_json_error( 'Image could not be downloaded.' ); |
| 3579 | 4653 | } |
| @@ -3609,9 +4683,21 @@ | ||
| 3609 | 4683 | |
| 3610 | 4684 | $upload_dir = wp_upload_dir(); |
| 3611 | 4685 | $result_urls = array(); |
| 3612 | 4686 | |
| 4687 | + $colour_index = 0; | |
| 3613 | 4688 | foreach ( $img_colors as $name => $rgb ) { |
| 4689 | + // $name is a key from the posted colours payload and went straight into the output | |
| 4690 | + // filename, so traversal sequences in it steered imagepng() outside the upload | |
| 4691 | + // directory (CWE-22, ClickUp 86d41ced6). sanitize_file_name() flattens it to one | |
| 4692 | + // path segment; a key made only of dots/separators sanitizes to empty, so fall back | |
| 4693 | + // to a positional index rather than writing to a bare "colored--<time>.png". | |
| 4694 | + ++$colour_index; | |
| 4695 | + $safe_name = sanitize_file_name( (string) $name ); | |
| 4696 | + if ( '' === $safe_name ) { | |
| 4697 | + $safe_name = 'colour-' . $colour_index; | |
| 4698 | + } | |
| 4699 | + | |
| 3614 | 4700 | $new = imagecreatetruecolor( $width, $height ); |
| 3615 | 4701 | imagesavealpha( $new, true ); |
| 3616 | 4702 | imagealphablending( $new, false ); |
| 3617 | 4703 | |
| @@ -3637,9 +4723,9 @@ | ||
| 3637 | 4723 | imagesetpixel( $new, $x, $y, $color ); |
| 3638 | 4724 | } |
| 3639 | 4725 | } |
| 3640 | 4726 | |
| 3641 | - $filename = 'colored-' . $name . '-' . time() . '.png'; | |
| 4727 | + $filename = 'colored-' . $safe_name . '-' . time() . '.png'; | |
| 3642 | 4728 | $filepath = $upload_dir['path'] . '/' . $filename; |
| 3643 | 4729 | |
| 3644 | 4730 | imagepng( $new, $filepath ); |
| 3645 | 4731 | imagedestroy( $new ); |
| @@ -3855,8 +4941,13 @@ | ||
| 3855 | 4941 | $page_information = isset( $_POST['page_information'] ) ? sanitize_text_field( wp_unslash( $_POST['page_information'] ) ) : ''; |
| 3856 | 4942 | $page_information = json_decode( $page_information, true ); |
| 3857 | 4943 | |
| 3858 | 4944 | if ( ! empty( $page_information ) && is_array( $page_information ) ) { |
| 4945 | + | |
| 4946 | + // Every page and attachment now exists, so resolve any image ID the per-page | |
| 4947 | + // pass could not (siblings import concurrently and share icons). | |
| 4948 | + $this->wdkit_sweep_attachment_ids( wp_list_pluck( $page_information, 'inserted_id' ) ); | |
| 4949 | + | |
| 3859 | 4950 | // Step 1: banavo mapping [ old_id => new_id ] |
| 3860 | 4951 | $id_mapping = array(); |
| 3861 | 4952 | foreach ( $page_information as $page_info ) { |
| 3862 | 4953 | if ( ! empty( $page_info['old_page_id'] ) ) { |
| @@ -4066,9 +5157,10 @@ | ||
| 4066 | 5157 | protected function wdkit_activate_key() { |
| 4067 | 5158 | $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : ''; |
| 4068 | 5159 | $response = ''; |
| 4069 | 5160 | |
| 4070 | - if ( empty( $user_email ) ) { | |
| 5161 | + // Bug C fix: variable was $user_email but only $email is set above — always triggered empty() guard. | |
| 5162 | + if ( empty( $email ) ) { | |
| 4071 | 5163 | $response = array( |
| 4072 | 5164 | 'message' => $this->e_msg_login, |
| 4073 | 5165 | 'description' => $this->e_desc_login, |
| 4074 | 5166 | 'success' => false, |
| @@ -4286,10 +5378,35 @@ | ||
| 4286 | 5378 | 'description' => esc_html__( 'widget JSON file not found.', 'wdesignkit' ), |
| 4287 | 5379 | ); |
| 4288 | 5380 | } |
| 4289 | 5381 | |
| 4290 | - $json_path = WDKIT_BUILDER_PATH . "/{$widget_type}/{$folder_name}/{$file_name}"; | |
| 5382 | + // Read-side twin of the write and delete traversals fixed in 86d41cckh / 86d41ccz2: all | |
| 5383 | + // three segments arrive from $_POST with only wp_unslash() applied — which strips | |
| 5384 | + // nothing path-relevant — so "../" in any of them walked out of the builder directory | |
| 5385 | + // and this handler returned the decoded contents of any .json file the web server user | |
| 5386 | + // could read (CWE-22, ClickUp 86d41zaun). | |
| 5387 | + $safe_path = wdesignkit_widget_path_guard( $widget_type, $folder_name, $file_name ); | |
| 4291 | 5388 | |
| 5389 | + if ( false === $safe_path || '' === $safe_path['folder'] || '' === $safe_path['file'] ) { | |
| 5390 | + return array( | |
| 5391 | + 'success' => false, | |
| 5392 | + 'message' => esc_html__( 'Widget JSON not found', 'wdesignkit' ), | |
| 5393 | + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ), | |
| 5394 | + ); | |
| 5395 | + } | |
| 5396 | + | |
| 5397 | + $json_path = $safe_path['base']; | |
| 5398 | + | |
| 5399 | + // Re-check the resolved file: the component guard above cannot see a symlink. Returns | |
| 5400 | + // false for a path that does not exist, which is the same answer we want anyway. | |
| 5401 | + if ( ! wdesignkit_path_inside_builder_dir( "$json_path.json" ) ) { | |
| 5402 | + return array( | |
| 5403 | + 'success' => false, | |
| 5404 | + 'message' => esc_html__( 'Widget JSON not found', 'wdesignkit' ), | |
| 5405 | + 'description' => esc_html__( 'widget JSON file not found.', 'wdesignkit' ), | |
| 5406 | + ); | |
| 5407 | + } | |
| 5408 | + | |
| 4292 | 5409 | $json_data = wp_json_file_decode( "$json_path.json" ); |
| 4293 | 5410 | if ( ! empty( $json_data ) ) { |
| 4294 | 5411 | $result = (object) array( |
| 4295 | 5412 | 'success' => true, |
| @@ -4315,9 +5432,9 @@ | ||
| 4315 | 5432 | * |
| 4316 | 5433 | * @since 1.0.0 |
| 4317 | 5434 | */ |
| 4318 | 5435 | protected function wdkit_download_widget() { |
| 4319 | - $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'widget_info', 'none' ) : ''; | |
| 5436 | + $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_extract_post_field( $_POST, 'widget_info', 'none' ) : ''; | |
| 4320 | 5437 | $data = json_decode( stripslashes( $data ) ); |
| 4321 | 5438 | |
| 4322 | 5439 | $array_data = array( |
| 4323 | 5440 | 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '', |
| @@ -4322,8 +5439,10 @@ | ||
| 4322 | 5439 | $array_data = array( |
| 4323 | 5440 | 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '', |
| 4324 | 5441 | 'type' => isset( $data->type ) ? sanitize_text_field( $data->type ) : '', |
| 4325 | 5442 | 'w_unique' => isset( $data->w_uniq ) ? sanitize_text_field( $data->w_uniq ) : '', |
| 5443 | + // Bug F fix: u_id (widget owner's user ID) was missing — cloud cannot locate the widget without it. | |
| 5444 | + 'u_id' => isset( $data->u_id ) ? sanitize_text_field( $data->u_id ) : '', | |
| 4326 | 5445 | ); |
| 4327 | 5446 | |
| 4328 | 5447 | $response = $this->wkit_api_call( $array_data, 'save_widget' ); |
| 4329 | 5448 | $success = ! empty( $response['success'] ) ? $response['success'] : false; |
| @@ -4358,10 +5477,11 @@ | ||
| 4358 | 5477 | |
| 4359 | 5478 | $img_url = ! empty( $response['data']['image'] ) ? $response['data']['image'] : ''; |
| 4360 | 5479 | $json_data = ! empty( $response['data']['json'] ) ? json_decode( $response['data']['json'], true ) : ''; |
| 4361 | 5480 | |
| 5481 | + // Bug E fix (part 1): $responce was a typo of $response — sent undefined variable (null) to frontend. | |
| 4362 | 5482 | if ( empty( $response['success'] ) ) { |
| 4363 | - wp_send_json( $responce ); | |
| 5483 | + wp_send_json( $response ); | |
| 4364 | 5484 | wp_die(); |
| 4365 | 5485 | } |
| 4366 | 5486 | |
| 4367 | 5487 | if ( empty( $img_url ) && empty( $json_data ) ) { |
| @@ -4382,14 +5502,34 @@ | ||
| 4382 | 5502 | if ( ! is_array( $json_data ) ) { |
| 4383 | 5503 | $json_data = json_decode( $json_data, true ); |
| 4384 | 5504 | } |
| 4385 | 5505 | |
| 4386 | - $title = ! empty( $json_data['widget_data']['widgetdata']['name'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['name'] ) : ''; | |
| 4387 | - $builder = ! empty( $json_data['widget_data']['widgetdata']['type'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['type'] ) : ''; | |
| 4388 | - $w_uniq = ! empty( $json_data['widget_data']['widgetdata']['widget_id'] ) ? sanitize_text_field( $json_data['widget_data']['widgetdata']['widget_id'] ) : ''; | |
| 5506 | + // Sanitize as filenames before use in the widget path (CWE-22): sanitize_file_name() | |
| 5507 | + // on name/id and sanitize_key() + allowlist on the builder strip path separators and | |
| 5508 | + // dots so a crafted cloud response cannot escape WDKIT_BUILDER_PATH. | |
| 5509 | + $title = ! empty( $json_data['widget_data']['widgetdata']['name'] ) ? sanitize_file_name( $json_data['widget_data']['widgetdata']['name'] ) : ''; | |
| 5510 | + $builder = ! empty( $json_data['widget_data']['widgetdata']['type'] ) ? sanitize_key( $json_data['widget_data']['widgetdata']['type'] ) : ''; | |
| 5511 | + $w_uniq = ! empty( $json_data['widget_data']['widgetdata']['widget_id'] ) ? sanitize_file_name( $json_data['widget_data']['widgetdata']['widget_id'] ) : ''; | |
| 4389 | 5512 | |
| 4390 | - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq; | |
| 4391 | - $file_name = str_replace( ' ', '_', $title ) . '_' . $w_uniq; | |
| 5513 | + $allowed_builders = array( 'elementor', 'gutenberg', 'gutenberg_core', 'bricks' ); | |
| 5514 | + if ( '' === $title || '' === $w_uniq || ! in_array( $builder, $allowed_builders, true ) ) { | |
| 5515 | + $responce = (object) array( | |
| 5516 | + 'success' => false, | |
| 5517 | + 'message' => esc_html__( 'Operation Failed!', 'wdesignkit' ), | |
| 5518 | + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ), | |
| 5519 | + ); | |
| 5520 | + | |
| 5521 | + wp_send_json( $responce ); | |
| 5522 | + wp_die(); | |
| 5523 | + } | |
| 5524 | + | |
| 5525 | + // Canonical helpers replace spaces BEFORE sanitize_file_name(). $title above is | |
| 5526 | + // already sanitized, which collapsed spaces to hyphens and left the underscore pass | |
| 5527 | + // with nothing to do — a multi-word title wrote "My-Widget_id.json" next to the | |
| 5528 | + // "My_Widget_id.php" the builder's save path writes. The loader pairs the two by | |
| 5529 | + // swapping .php for .json, so the widget was silently dropped (ClickUp 86d41cck5). | |
| 5530 | + $folder_name = wdesignkit_widget_folder_name( $title, $w_uniq ); | |
| 5531 | + $file_name = wdesignkit_widget_file_name( $title, $w_uniq ); | |
| 4392 | 5532 | $builder_type_path = WDKIT_BUILDER_PATH . "/{$builder}/"; |
| 4393 | 5533 | |
| 4394 | 5534 | if ( ! is_dir( $builder_type_path ) ) { |
| 4395 | 5535 | wp_mkdir_p( $builder_type_path ); |
| @@ -4399,17 +5539,30 @@ | ||
| 4399 | 5539 | wp_mkdir_p( $builder_type_path . $folder_name ); |
| 4400 | 5540 | } |
| 4401 | 5541 | |
| 4402 | 5542 | if ( ! empty( $img_url ) ) { |
| 4403 | - $img_body = wp_remote_get( $img_url ); | |
| 4404 | - $img_ext = pathinfo( $img_url )['extension']; | |
| 5543 | + // SSRF guard (CWE-918): validate the resolved host before fetching. | |
| 5544 | + $img_body = wdesignkit_safe_remote_get( $img_url ); | |
| 5545 | + if ( ! is_wp_error( $img_body ) ) { | |
| 5546 | + // The remote extension was written verbatim here, so a cloud response naming a | |
| 5547 | + // ".php" image put executable PHP in the builder directory (CWE-434, | |
| 5548 | + // ClickUp 86d41cczd). An empty return means the bytes are not an image. | |
| 5549 | + $img_ext = wdesignkit_safe_image_extension( $img_url, $img_body['body'] ); | |
| 4405 | 5550 | |
| 4406 | - $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] ); | |
| 4407 | - $json_data['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext"; | |
| 5551 | + if ( '' !== $img_ext ) { | |
| 5552 | + $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] ); | |
| 5553 | + $json_data['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext"; | |
| 5554 | + } | |
| 5555 | + } | |
| 4408 | 5556 | } |
| 4409 | 5557 | |
| 5558 | + if ( function_exists( 'wdesignkit_invalidate_widget_registry' ) ) { | |
| 5559 | + wdesignkit_invalidate_widget_registry( $builder ); | |
| 5560 | + } | |
| 5561 | + | |
| 5562 | + // Bug E fix (part 2): success was hardcoded false on the successful download path — always reported failure. | |
| 4410 | 5563 | $result = (object) array( |
| 4411 | - 'success' => false, | |
| 5564 | + 'success' => true, | |
| 4412 | 5565 | 'message' => ! empty( $response['message'] ) ? $response['message'] : esc_html__( 'no message', 'wdesignkit' ), |
| 4413 | 5566 | 'description' => '', |
| 4414 | 5567 | 'json' => wp_json_encode( $json_data ), |
| 4415 | 5568 | ); |
| @@ -4424,9 +5577,9 @@ | ||
| 4424 | 5577 | * |
| 4425 | 5578 | * @since 1.0.0 |
| 4426 | 5579 | */ |
| 4427 | 5580 | protected function wdkit_add_widget() { |
| 4428 | - $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'widget_info', 'none' ) : ''; | |
| 5581 | + $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_extract_post_field( $_POST, 'widget_info', 'none' ) : ''; | |
| 4429 | 5582 | $data = base64_decode( $data ); |
| 4430 | 5583 | $data = json_decode( $data ); |
| 4431 | 5584 | |
| 4432 | 5585 | $title = isset( $data->title ) ? sanitize_text_field( $data->title ) : ''; |
| @@ -4435,9 +5588,11 @@ | ||
| 4435 | 5588 | $w_image = isset( $data->w_image ) ? esc_url_raw( $data->w_image ) : ''; |
| 4436 | 5589 | |
| 4437 | 5590 | if ( ! empty( $w_image ) ) { |
| 4438 | 5591 | $w_image = str_replace( '\\', '', $w_image ); |
| 4439 | - $w_image = wp_remote_get( $w_image )['body']; | |
| 5592 | + // SSRF guard (CWE-918): validate the resolved host before fetching. | |
| 5593 | + $fetched = wdesignkit_safe_remote_get( $w_image ); | |
| 5594 | + $w_image = is_wp_error( $fetched ) ? '' : wp_remote_retrieve_body( $fetched ); | |
| 4440 | 5595 | } |
| 4441 | 5596 | |
| 4442 | 5597 | $array_data = array( |
| 4443 | 5598 | 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '', |
| @@ -4477,22 +5632,51 @@ | ||
| 4477 | 5632 | $img_url = ! empty( $response['data']['imgurl'] ) ? $response['data']['imgurl'] : ''; |
| 4478 | 5633 | |
| 4479 | 5634 | if ( ! empty( $img_url ) && 'error' !== $res ) { |
| 4480 | 5635 | |
| 4481 | - $img_body = wp_remote_get( $img_url ); | |
| 4482 | - $img_ext = pathinfo( $img_url )['extension']; | |
| 4483 | - include_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 4484 | - \WP_Filesystem(); | |
| 4485 | - global $wp_filesystem; | |
| 4486 | - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq; | |
| 4487 | - $file_name = str_replace( ' ', '_', $title ) . '_' . $w_uniq; | |
| 4488 | - $file_path = WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name"; | |
| 5636 | + // SSRF guard (CWE-918): validate the resolved host before fetching. | |
| 5637 | + $img_body = wdesignkit_safe_remote_get( $img_url ); | |
| 5638 | + if ( ! is_wp_error( $img_body ) ) { | |
| 5639 | + // Verified against the payload rather than trusted from the URL (CWE-434, | |
| 5640 | + // ClickUp 86d41cczd); '' means the bytes are not an image we accept. | |
| 5641 | + $img_ext = wdesignkit_safe_image_extension( $img_url, $img_body['body'] ); | |
| 5642 | + include_once ABSPATH . 'wp-admin/includes/file.php'; | |
| 5643 | + \WP_Filesystem(); | |
| 5644 | + global $wp_filesystem; | |
| 5645 | + // Canonical helpers, so the JSON read and the image write here address the same | |
| 5646 | + // base name every other writer uses (ClickUp 86d41cck5). They also apply | |
| 5647 | + // sanitize_file_name(), which $title and $w_uniq had not been through. | |
| 5648 | + $folder_name = wdesignkit_widget_folder_name( $title, $w_uniq ); | |
| 5649 | + $file_name = wdesignkit_widget_file_name( $title, $w_uniq ); | |
| 4489 | 5650 | |
| 4490 | - $u_r_l = wp_json_file_decode( "$file_path.json" ); | |
| 4491 | - $u_r_l->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext"; | |
| 5651 | + // $builder reaches here with only sanitize_text_field() applied and no | |
| 5652 | + // allowlist, so it was a live traversal segment in this path (CWE-22, | |
| 5653 | + // ClickUp 86d41cckh). Unlike the download handler earlier in this file, this | |
| 5654 | + // one had neither the builder allowlist nor a containment check. | |
| 5655 | + $safe_path = wdesignkit_widget_path_guard( $builder, $folder_name, $file_name ); | |
| 5656 | + if ( false === $safe_path || ! wdesignkit_path_inside_builder_dir( $safe_path['dir'] ) ) { | |
| 5657 | + wp_send_json( | |
| 5658 | + (object) array( | |
| 5659 | + 'success' => false, | |
| 5660 | + 'message' => esc_html__( 'Operation Failed!', 'wdesignkit' ), | |
| 5661 | + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ), | |
| 5662 | + ) | |
| 5663 | + ); | |
| 5664 | + wp_die(); | |
| 5665 | + } | |
| 4492 | 5666 | |
| 4493 | - $wp_filesystem->put_contents( "$file_path.json", wp_json_encode( $u_r_l ) ); | |
| 4494 | - $wp_filesystem->put_contents( "$file_path.$img_ext", $img_body['body'] ); | |
| 5667 | + $builder = $safe_path['builder']; | |
| 5668 | + $file_path = $safe_path['base']; | |
| 5669 | + | |
| 5670 | + $u_r_l = wp_json_file_decode( "$file_path.json" ); | |
| 5671 | + | |
| 5672 | + if ( '' !== $img_ext ) { | |
| 5673 | + $u_r_l->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext"; | |
| 5674 | + $wp_filesystem->put_contents( "$file_path.$img_ext", $img_body['body'] ); | |
| 5675 | + } | |
| 5676 | + | |
| 5677 | + $wp_filesystem->put_contents( "$file_path.json", wp_json_encode( $u_r_l ) ); | |
| 5678 | + } | |
| 4495 | 5679 | } |
| 4496 | 5680 | |
| 4497 | 5681 | wp_send_json( $response ); |
| 4498 | 5682 | wp_die(); |
| @@ -4587,18 +5771,22 @@ | ||
| 4587 | 5771 | |
| 4588 | 5772 | $get_setting = get_option( 'wkit_settings_panel', false ); |
| 4589 | 5773 | |
| 4590 | 5774 | $setting_data = array( |
| 4591 | - 'builder' => isset( $get_setting['builder'] ) ? $get_setting['builder'] : true, | |
| 4592 | - 'template' => isset( $get_setting['template'] ) ? $get_setting['template'] : true, | |
| 4593 | - 'gutenberg_builder' => isset( $get_setting['gutenberg_builder'] ) ? $get_setting['gutenberg_builder'] : true, | |
| 4594 | - 'gutenberg_core_builder' => isset( $get_setting['gutenberg_core_builder'] ) ? $get_setting['gutenberg_core_builder'] : false, | |
| 4595 | - 'elementor_builder' => isset( $get_setting['elementor_builder'] ) ? $get_setting['elementor_builder'] : true, | |
| 4596 | - 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : false, | |
| 4597 | - 'gutenberg_template' => isset( $get_setting['gutenberg_template'] ) ? $get_setting['gutenberg_template'] : true, | |
| 4598 | - 'elementor_template' => isset( $get_setting['elementor_template'] ) ? $get_setting['elementor_template'] : true, | |
| 4599 | - 'code_snippet' => isset( $get_setting['code_snippet'] ) ? $get_setting['code_snippet'] : true, | |
| 4600 | - 'plugin_version' => $version_check, | |
| 5775 | + 'builder' => isset( $get_setting['builder'] ) ? $get_setting['builder'] : true, | |
| 5776 | + 'template' => isset( $get_setting['template'] ) ? $get_setting['template'] : true, | |
| 5777 | + 'gutenberg_builder' => isset( $get_setting['gutenberg_builder'] ) ? $get_setting['gutenberg_builder'] : true, | |
| 5778 | + 'gutenberg_core_builder' => isset( $get_setting['gutenberg_core_builder'] ) ? $get_setting['gutenberg_core_builder'] : false, | |
| 5779 | + 'elementor_builder' => isset( $get_setting['elementor_builder'] ) ? $get_setting['elementor_builder'] : true, | |
| 5780 | + 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : false, | |
| 5781 | + 'gutenberg_template' => isset( $get_setting['gutenberg_template'] ) ? $get_setting['gutenberg_template'] : true, | |
| 5782 | + 'elementor_template' => isset( $get_setting['elementor_template'] ) ? $get_setting['elementor_template'] : true, | |
| 5783 | + 'code_snippet' => isset( $get_setting['code_snippet'] ) ? $get_setting['code_snippet'] : true, | |
| 5784 | + 'cross_copy_paste' => isset( $get_setting['cross_copy_paste'] ) ? $get_setting['cross_copy_paste'] : false, | |
| 5785 | + 'cross_copy_paste_elementor' => isset( $get_setting['cross_copy_paste_elementor'] ) ? $get_setting['cross_copy_paste_elementor'] : false, | |
| 5786 | + 'cross_copy_paste_gutenberg' => isset( $get_setting['cross_copy_paste_gutenberg'] ) ? $get_setting['cross_copy_paste_gutenberg'] : false, | |
| 5787 | + 'cross_copy_paste_bricks' => isset( $get_setting['cross_copy_paste_bricks'] ) ? $get_setting['cross_copy_paste_bricks'] : false, | |
| 5788 | + 'plugin_version' => $version_check, | |
| 4601 | 5789 | ); |
| 4602 | 5790 | |
| 4603 | 5791 | if ( isset( $get_setting['remove_db'] ) ) { |
| 4604 | 5792 | $setting_data['remove_db'] = $get_setting['remove_db']; |
| @@ -4651,9 +5839,9 @@ | ||
| 4651 | 5839 | } |
| 4652 | 5840 | |
| 4653 | 5841 | $get_updated_data = get_option( 'wkit_white_label', false ); |
| 4654 | 5842 | $response = array( |
| 4655 | - 'message' => 'Data Added successfully', | |
| 5843 | + 'message' => __( 'Data Added successfully', 'wdesignkit' ), | |
| 4656 | 5844 | 'success' => true, |
| 4657 | 5845 | 'data' => $get_updated_data, |
| 4658 | 5846 | ); |
| 4659 | 5847 | |
| @@ -4700,21 +5888,21 @@ | ||
| 4700 | 5888 | if ( ! empty( $response['data'] ) ) { |
| 4701 | 5889 | $response = json_decode( wp_json_encode( $response['data'] ), true ); |
| 4702 | 5890 | |
| 4703 | 5891 | if ( ! empty( $response['data']['tpae_licence'] ) && is_serialized( $response['data']['tpae_licence'] ) ) { |
| 4704 | - $response['data']['tpae_licence'] = unserialize( $response['data']['tpae_licence'] ); | |
| 5892 | + $response['data']['tpae_licence'] = unserialize( $response['data']['tpae_licence'], array( 'allowed_classes' => false ) ); | |
| 4705 | 5893 | } |
| 4706 | 5894 | |
| 4707 | 5895 | if ( ! empty( $response['data']['tpag_licence'] ) && is_serialized( $response['data']['tpag_licence'] ) ) { |
| 4708 | - $response['data']['tpag_licence'] = unserialize( $response['data']['tpag_licence'] ); | |
| 5896 | + $response['data']['tpag_licence'] = unserialize( $response['data']['tpag_licence'], array( 'allowed_classes' => false ) ); | |
| 4709 | 5897 | } |
| 4710 | 5898 | |
| 4711 | 5899 | if ( ! empty( $response['data']['uichemy_licence'] ) && is_serialized( $response['data']['uichemy_licence'] ) ) { |
| 4712 | - $response['data']['uichemy_licence'] = unserialize( $response['data']['uichemy_licence'] ); | |
| 5900 | + $response['data']['uichemy_licence'] = unserialize( $response['data']['uichemy_licence'], array( 'allowed_classes' => false ) ); | |
| 4713 | 5901 | } |
| 4714 | 5902 | |
| 4715 | 5903 | if ( ! empty( $response['data']['wdkit_licence'] ) && is_serialized( $response['data']['wdkit_licence'] ) ) { |
| 4716 | - $response['data']['wdkit_licence'] = unserialize( $response['data']['wdkit_licence'] ); | |
| 5904 | + $response['data']['wdkit_licence'] = unserialize( $response['data']['wdkit_licence'], array( 'allowed_classes' => false ) ); | |
| 4717 | 5905 | |
| 4718 | 5906 | // Store WDesignKit license status locally for quick access |
| 4719 | 5907 | if ( ! empty( $response['data']['wdkit_licence'] ) && is_array( $response['data']['wdkit_licence'] ) ) { |
| 4720 | 5908 | update_option( 'wdkit_licence_data', $response['data']['wdkit_licence'] ); |
| @@ -4721,9 +5909,9 @@ | ||
| 4721 | 5909 | } |
| 4722 | 5910 | } |
| 4723 | 5911 | |
| 4724 | 5912 | if ( ! empty( $response['data']['wdkit_licence_extra'] ) && is_serialized( $response['data']['wdkit_licence_extra'] ) ) { |
| 4725 | - $response['data']['wdkit_licence_extra'] = unserialize( $response['data']['wdkit_licence_extra'] ); | |
| 5913 | + $response['data']['wdkit_licence_extra'] = unserialize( $response['data']['wdkit_licence_extra'], array( 'allowed_classes' => false ) ); | |
| 4726 | 5914 | } |
| 4727 | 5915 | } |
| 4728 | 5916 | |
| 4729 | 5917 | wp_send_json( $response ); |
| @@ -4766,12 +5954,16 @@ | ||
| 4766 | 5954 | */ |
| 4767 | 5955 | protected function wdkit_sync_licence_key() { |
| 4768 | 5956 | $token = ! empty( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : ''; |
| 4769 | 5957 | $licencename = ! empty( $_POST['licencename'] ) ? sanitize_text_field( wp_unslash( $_POST['licencename'] ) ) : ''; |
| 5958 | + // Needed to identify which extra-credit key to sync (wdkit_extra / wdkit_ai_extra | |
| 5959 | + // are arrays matched by the api key's last digits on the server). | |
| 5960 | + $apikey = ! empty( $_POST['apikey'] ) ? sanitize_text_field( wp_unslash( $_POST['apikey'] ) ) : ''; | |
| 4770 | 5961 | |
| 4771 | 5962 | $args = array( |
| 4772 | 5963 | 'token' => $token, |
| 4773 | 5964 | 'licencename' => $licencename, |
| 5965 | + 'apikey' => $apikey, | |
| 4774 | 5966 | ); |
| 4775 | 5967 | |
| 4776 | 5968 | $response = $this->wkit_api_call( $args, 'licence_sync' ); |
| 4777 | 5969 | |
| @@ -4904,9 +6096,9 @@ | ||
| 4904 | 6096 | $token = $this->wdkit_login_user_token( $email ); |
| 4905 | 6097 | $args = array( 'token' => $token ); |
| 4906 | 6098 | |
| 4907 | 6099 | if ( 'session' !== $logout_type ) { |
| 4908 | - delete_transient( 'wdkit_auth_' . $email ); | |
| 6100 | + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) ); | |
| 4909 | 6101 | // Clear stored license data on logout so banner shows again |
| 4910 | 6102 | delete_option( 'wdkit_licence_data' ); |
| 4911 | 6103 | $response = WDesignKit_Data_Query::get_data( 'logout', $args ); |
| 4912 | 6104 | } |
| @@ -4926,9 +6118,9 @@ | ||
| 4926 | 6118 | */ |
| 4927 | 6119 | protected function wdkit_login_user_token( $email = '' ) { |
| 4928 | 6120 | |
| 4929 | 6121 | if ( ! empty( $email ) ) { |
| 4930 | - $user_key = strstr( $email, '@', true ); | |
| 6122 | + $user_key = wdesignkit_cloud_session_key( $email ); | |
| 4931 | 6123 | $get_login = get_transient( 'wdkit_auth_' . $user_key ); |
| 4932 | 6124 | |
| 4933 | 6125 | if ( ! empty( $get_login ) && ! empty( $get_login['token'] ) ) { |
| 4934 | 6126 | return $get_login['token']; |
| @@ -4946,9 +6138,9 @@ | ||
| 4946 | 6138 | * @param string $data send all post data. |
| 4947 | 6139 | * @param string $type store text data. |
| 4948 | 6140 | * @param string $condition store text data. |
| 4949 | 6141 | */ |
| 4950 | - protected function wdkit_sanitizer_bypass( $data, $type, $condition = 'none' ) { | |
| 6142 | + protected function wdkit_extract_post_field( $data, $type, $condition = 'none' ) { | |
| 4951 | 6143 | |
| 4952 | 6144 | if ( 'none' === $condition ) { |
| 4953 | 6145 | return $data[ $type ]; |
| 4954 | 6146 | } elseif ( 'cr_widget' === $condition ) { |
| @@ -4953,8 +6145,10 @@ | ||
| 4953 | 6145 | return $data[ $type ]; |
| 4954 | 6146 | } elseif ( 'cr_widget' === $condition ) { |
| 4955 | 6147 | return $data[ $type ]; |
| 4956 | 6148 | } |
| 6149 | + | |
| 6150 | + return null; | |
| 4957 | 6151 | } |
| 4958 | 6152 | |
| 4959 | 6153 | |
| 4960 | 6154 | /** |