falsy skips the "-scaled" copy (needs a full decode) * intermediate_image_sizes_advanced -> empty makes _wp_make_subsizes() return early, * ahead of its wp_get_image_editor() call * * The original file is still attached and usable; only the derived sizes are skipped. * That trades ideal thumbnails for an import that completes, instead of a fatal that * takes the whole page down and repeats on every retry. * * @since 2.6.2 */ private static function wdkit_guard_oversized_images() { static $registered = false; // Registering twice would stack duplicate closures on both filters. if ( $registered ) { return; } $registered = true; if ( ! class_exists( 'Wdkit_Image_Guard' ) ) { require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php'; } add_filter( 'big_image_size_threshold', function ( $threshold, $imagesize = array(), $file = '', $attachment_id = 0 ) { if ( ! empty( $imagesize[0] ) && ! empty( $imagesize[1] ) && ! Wdkit_Image_Guard::decode_fits( $imagesize[0], $imagesize[1], self::wdkit_available_image_memory() ) ) { return false; } return $threshold; }, 99, 4 ); add_filter( 'intermediate_image_sizes_advanced', function ( $sizes, $image_meta = array(), $attachment_id = 0 ) { if ( ! empty( $image_meta['width'] ) && ! empty( $image_meta['height'] ) && ! Wdkit_Image_Guard::decode_fits( $image_meta['width'], $image_meta['height'], self::wdkit_available_image_memory() ) ) { return array(); } return $sizes; }, 99, 3 ); } /** * Get Wdkit Api Call Ajax. */ public function wdkit_api_call() { check_ajax_referer( 'wdkit_nonce', 'kit_nonce' ); if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'content' => __( 'Insufficient permissions.', 'wdesignkit' ) ) ); } $type = isset( $_POST['type'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['type'] ) ) ) : false; if ( ! $type ) { $this->wdkit_error_msg( __( 'Something went wrong.', 'wdesignkit' ) ); } switch ( $type ) { case 'onboarding_handler': $data = $this->wdkit_onboarding_handler(); break; case 'wkit_login': $data = apply_filters( 'wp_wdkit_login_ajax', 'wkit_login' ); break; case 'api_login': $data = apply_filters( 'wp_wdkit_login_ajax', 'api_login' ); break; case 'social_login': $data = apply_filters( 'wp_wdkit_login_ajax', 'social_login' ); break; case 'forgot_password': $data = apply_filters( 'wp_wdkit_login_ajax', 'forgot_password' ); break; case 'wdkit_user_signup': $data = apply_filters( 'wp_wdkit_login_ajax', 'wdkit_user_signup' ); break; case 'wkit_meta_data': $data = $this->wdkit_meta_data(); break; case 'get_user_info': $data = $this->wdkit_get_user_info(); break; case 'browse_page': $data = $this->wdkit_browse_page(); break; case 'kit_template': $data = $this->wdkit_template(); break; case 'wkit_preset_template': $data = apply_filters( 'wp_wdkit_preset_ajax', 'wdkit_preset_template' ); break; case 'wdkit_preset_dwnld_template': $data = apply_filters( 'wp_wdkit_preset_ajax', 'wdkit_preset_dwnld_template' ); break; case 'template_remove': $data = $this->wdkit_template_remove(); break; case 'save_template': $data = $this->wdkit_put_save_template(); break; case 'update_save_temp_image': $data = $this->wdkit_update_save_temp_image(); break; case 'save_wp_images': $data = $this->wdkit_save_wp_images(); break; case 'wkit_schedule_deferred_media_sync': $data = $this->wkit_schedule_deferred_media_sync(); break; case 'wkit_run_deferred_media_now': $data = $this->wkit_run_deferred_media_now(); break; case 'get_global_val': $data = $this->wdkit_get_global_val(); break; case 'update_global_val': $data = $this->wdkit_update_global_val(); break; case 'wdkit_get_site_setting': $data = $this->wdkit_get_site_setting(); break; case 'wdkit_update_site_setting': $data = $this->wdkit_update_site_setting(); break; case 'update_preset_setting': $data = $this->wdkit_update_preset(); break; case 'find_template': $data = $this->wdkit_find_existing_template(); break; case 'update_template': $data = $this->wdkit_update_template(); break; case 'manage_favorite': $data = $this->wdkit_manage_favorite(); break; case 'check_plugins_depends': $data = $this->wdkit_check_plugins_depends(); break; case 'install_plugins_depends': $data = $this->wdkit_install_plugins_depends(); break; case 'install_plugins_depends_batch': $data = $this->wdkit_install_plugins_depends_batch(); break; case 'generate_site_logo': $data = $this->wkit_generate_site_logo(); break; case 'generate_ai_content': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'generate_ai_content' ); break; case 'generate_ai_content_batch': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'generate_ai_content_batch' ); break; case 'reset_site': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'reset_site' ); break; case 'wdkit_nxt_thembuilder_reset': $data = $this->wdkit_nxt_thembuilder_reset(); break; case 'wdkit_check_user_credit': $data = $this->wdkit_check_user_credit(); break; case 'wdkit_remove_header_footer': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wdkit_remove_header_footer' ); break; case 'check_post_count': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'check_post_count' ); break; case 'wkit_check_product_count': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_check_product_count' ); break; case 'select_team_img': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'select_team_img' ); break; case 'wkit_ai_desc_keyword': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_ai_desc_keyword' ); break; case 'wkit_ai_credit_update': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_ai_credit_update' ); break; case 'wkit_generate_post_data': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_generate_post_data' ); break; case 'wkit_generate_product_data': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_generate_product_data' ); break; case 'wkit_cteate_product': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_cteate_product' ); break; case 'wkit_remove_dummy_post': $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_remove_dummy_post' ); break; case 'update_latest_plugin': $data = $this->wdkit_update_latest_plugin(); break; case 'activate_container': $data = $this->wdkit_activate_container(); break; case 'import_taxonomy': $data = $this->wdkit_import_taxonomy(); break; case 'import_template': $data = $this->wdkit_import_template(); break; case 'import_multi_template': $data = $this->wdkit_import_multi_template(); break; case 'import_page_section': $data = $this->import_page_section_content(); break; case 'wdkit_import_stage': $data = $this->wdkit_import_stage(); break; case 'wkit_update_elementor_template': $data = $this->wkit_update_elementor_template(); break; case 'wdkit_update_page_content': $data = $this->wdkit_update_page_content(); break; case 'update_plugin_setting': $data = $this->update_plugin_setting(); break; case 'update_theme_setting': $data = $this->update_theme_setting(); break; case 'update_site_setting': $data = $this->update_site_setting(); break; case 'import_kit_template': $data = $this->wdkit_import_kit_template(); break; case 'wkit_import_kit_bundle': $data = $this->wkit_import_kit_bundle(); break; case 'wkit_fetch_site_bundle': $data = $this->wkit_fetch_site_bundle(); break; case 'wkit_import_site_bundle': $data = $this->wkit_import_site_bundle(); break; case 'enable_template_widgets': $data = $this->wdkit_enable_template_widgets(); break; case 'scan_nexter_widgets': if ( ! empty( $_POST['blockNames'] ) && has_filter( 'nexter_block_list_merge' ) ) { $posted_blocks = json_decode( stripslashes( $_POST['blockNames'] ), true ); if ( is_array( $posted_blocks ) ) { $blockList = array_map( 'sanitize_text_field', $posted_blocks ); // अब filter call करो $result = apply_filters( 'nexter_block_list_merge', $blockList ); wp_send_json( $result ); wp_die(); } } wp_send_json( array( 'success' => false, 'message' => __( 'No block names received or filter not found.', 'wdesignkit' ), 'description' => 'Ensure blockNames are posted and the filter is attached.', ) ); wp_die(); $data = ''; break; case 'shared_with_me': $data = $this->wdkit_shared_with_me(); break; case 'manage_workspace': $data = $this->wdkit_manage_workspace(); break; case 'widget_browse_page': $data = apply_filters( 'wp_wdkit_widget_ajax', 'widget_browse_page' ); break; case 'wkit_create_widget': $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_create_widget' ); break; case 'wkit_import_widget': $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_import_widget' ); break; case 'wkit_export_widget': $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_export_widget' ); break; case 'wkit_delete_widget': $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_delete_widget' ); break; case 'wkit_widget_preview': $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_widget_preview' ); break; case 'wkit_check_widget_versions': $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_check_widget_versions' ); break; case 'wkit_plugin_download_get': $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_plugin_download_get' ); break; case 'wkit_manage_widget_workspace': $data = $this->wdkit_manage_widget_workspace(); break; case 'wkit_activate_key': $data = $this->wdkit_activate_key(); break; case 'wkit_manage_widget_category': $data = $this->wdkit_manage_widget_category(); break; case 'wkit_widget_json': $data = $this->wkit_widget_json(); break; case 'wkit_download_widget': $data = $this->wdkit_download_widget(); break; case 'wkit_public_download_widget': $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_public_download_widget' ); break; case 'wkit_add_widget': $data = $this->wdkit_add_widget(); break; case 'wkit_favourite_widget': $data = $this->wdkit_favourite_widget(); break; case 'wkit_setting_panel': $data = $this->wdkit_setting_panel(); break; case 'media_import': $data = $this->wdkit_media_import(); break; case 'active_licence': $data = $this->wdkit_activate_licence(); break; case 'delete_licence': $data = $this->wdkit_delete_licence_key(); break; case 'sync_licence': $data = $this->wdkit_sync_licence_key(); break; case 'get_wkit_version': $data = $this->wdkit_prev_version(); break; case 'rollback_wdkit': $data = $this->wdkit_rollback_check(); break; case 'wkit_logout': $data = $this->wdkit_logout(); break; case 'wkit_white_label': $this->wkit_white_label(); break; case 'wkit_reset_wl': $data = $this->wkit_reset_wl(); break; case 'wdkit_dark_mode': $data = $this->wdkit_dark_mode(); break; case 'wdkit_get_workspace_data': $data = $this->wdkit_get_workspace_data(); break; default: $this->wdkit_error_msg( __( 'Unknown request type.', 'wdesignkit' ) ); return; } $this->wdkit_success_msg( $data ); // wp_die(); } /** * * This Function is used for API call * * @since 1.0.0 * * @param array $data give array. * @param array $name store data. * @param int $timeout optional HTTP timeout in seconds. Default 100. */ protected function wkit_api_call( $data, $name, $timeout = 100 ) { $u_r_l = $this->wdkit_api; if ( empty( $u_r_l ) ) { return array( 'massage' => esc_html__( 'API Not Found', 'wdesignkit' ), 'success' => false, ); } $args = array( 'method' => 'POST', 'body' => $data, 'timeout' => $timeout, ); $response = wp_remote_post( $u_r_l . $name, $args ); if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); /* Translators: %s is a placeholder for the error message */ $error_message = sprintf( esc_html__( 'API request error: %s', 'wdesignkit' ), esc_html( $error_message ) ); return array( 'massage' => $error_message, 'success' => false, ); } $status_code = wp_remote_retrieve_response_code( $response ); if ( 200 === $status_code ) { return array( 'data' => json_decode( wp_remote_retrieve_body( $response ) ), 'massage' => esc_html__( 'Success', 'wdesignkit' ), 'status' => $status_code, 'success' => true, ); } $error_message = sprintf( 'Server error: %d', esc_html( $status_code ) ); if ( isset( $error_data->message ) ) { $error_message .= ' (' . $error_data->message . ')'; } return array( 'massage' => $error_message, 'status' => $status_code, 'success' => false, ); } /** * * It is Use for handle onboarding data. * * @since 1.0.9 */ public function wdkit_onboarding_handler() { $page_template = isset( $_POST['page_template'] ) ? json_decode( wp_unslash( $_POST['page_template'] ), true ) : ''; $page_builder = isset( $_POST['page_builder'] ) ? json_decode( wp_unslash( $_POST['page_builder'] ), true ) : ''; $elementor_plugin = isset( $_POST['elementor_plugin'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['elementor_plugin'] ) ) : 0; $tpag_plugin = isset( $_POST['tpag_plugin'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['tpag_plugin'] ) ) : 0; $bricks_theme = isset( $_POST['bricks_theme'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['bricks_theme'] ) ) : 0; $site_info = isset( $_POST['info'] ) ? sanitize_text_field( wp_unslash( $_POST['info'] ) ) : false; $server_software = ! empty( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : ''; $web_server = $server_software; $memory_limit = ini_get( 'memory_limit' ); $max_execution_time = ini_get( 'max_execution_time' ); $php_version = phpversion(); $wp_version = get_bloginfo( 'version' ); $email = get_option( 'admin_email' ); $siteurl = get_option( 'siteurl' ); $language = get_bloginfo( 'language' ); // Active Plugin Name. $act_plugin = array(); $actplu = get_option( 'active_plugins' ); if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); foreach ( $actplu as $p ) { if ( isset( $plugins[ $p ] ) ) { $act_plugin[] = $plugins[ $p ]['Name']; } } $plugin = wp_json_encode( $act_plugin ); $theme = ''; $acthemeobj = wp_get_theme(); if ( $acthemeobj->get( 'Name' ) !== null && ! empty( $acthemeobj->get( 'Name' ) ) ) { $theme = $acthemeobj->get( 'Name' ); } $basic_requirements = array( 'elementor_install' => $elementor_plugin, 'tpag_install' => $tpag_plugin, 'bricks_install' => $bricks_theme, ); if ( ! empty( $site_info ) ) { $final = array( 'web_server' => $web_server, 'memory_limit' => $memory_limit, 'max_execution_time' => $max_execution_time, 'php_version' => $php_version, 'wp_version' => $wp_version, 'email' => $email, 'site_url' => $siteurl, 'site_language' => $language, 'theme' => $theme, 'plugins' => $act_plugin, 'basic_requirements' => $basic_requirements, 'page_template' => $page_template, 'page_builder' => $page_builder, ); } else { $final = array(); } $response = wp_remote_post( $this->wdkit_onbording_api, array( 'method' => 'POST', 'body' => wp_json_encode( $final ), ) ); $existing_value = get_option( $this->wdkit_onbording_end ); if ( false === $existing_value ) { add_option( $this->wdkit_onbording_end, true ); } if ( is_wp_error( $response ) ) { $result = array( 'success' => false, 'messages' => 'Oops', 'description' => 'Description', ); wp_send_json( $result ); } else { $status_one = wp_remote_retrieve_response_code( $response ); if ( 200 === $status_one ) { $get_data_one = wp_remote_retrieve_body( $response ); $get_res = json_decode( json_decode( $get_data_one, true ), true ); $result = array( 'success' => ! empty( $get_res['success'] ) ? $get_res['success'] : false, 'messages' => ! empty( $get_res['messages'] ) ? $get_res['messages'] : 'success', 'description' => ! empty( $get_res['description'] ) ? $get_res['description'] : 'Description', ); wp_send_json( $result ); } else { $result = array( 'success' => false, 'messages' => 'Oops', 'description' => 'description', ); wp_send_json( $result ); } } wp_die(); } /** * * It is Use for get meta data for non login user * * @since 1.0.0 */ protected function wdkit_meta_data() { $type = isset( $_POST['meta_type'] ) ? sanitize_text_field( wp_unslash( $_POST['meta_type'] ) ) : ''; $data = array( 'type' => $type ); $response = $this->wkit_api_call( $data, 'meta' ); $success = ! empty( $response['success'] ) ? $response['success'] : false; $status = ! empty( $response['status'] ) ? (int) $response['status'] : 400; if ( empty( $success ) ) { $result = array( 'plugin' => array(), 'builder' => array(), 'category' => array(), 'widgetscategory' => array(), 'tags' => array(), 'widgetbuilder' => array(), 'message' => esc_html__( 'Server Error', 'wdesignkit' ), 'description' => esc_html__( 'Server Error, Data Not Found', 'wdesignkit' ), 'success' => false, ); wp_send_json( $result ); wp_die(); } $get_data_one = ! empty( $response['data'] ) ? $response['data'] : array(); $statuscode = array( 'HTTP_CODE' => $status ); $final = json_decode( wp_json_encode( $get_data_one ), true ); $final['Setting'] = self::wkit_get_settings_panel(); $final['widget_list'] = $this->wkit_manage_widget_sequence( array() ); $final = array( 'data' => $final, ); wp_send_json( $final ); wp_die(); } /** * * It is Use for get activate license key data from tpae and nexter blocks. * * @since 1.1.6 */ protected function wkit_manage_license_data() { $manage_licence = array(); $theplus_active_check = is_plugin_active( 'the-plus-addons-for-elementor-page-builder/theplus_elementor_addon.php' ); $nexter_active_check = is_plugin_active( 'the-plus-addons-for-block-editor/the-plus-addons-for-block-editor.php' ); $theplus_licence = get_option( 'tpaep_licence_data', array() ); // Also require the TPAE Pro plugin to be active (Pro defines THEPLUS_VERSION; // the free plugin defines L_THEPLUS_VERSION). This hides the "found active // key" notice when the Pro plugin is removed even though its licence option // still lingers in the database. if ( ! empty( $theplus_active_check ) && defined( 'THEPLUS_VERSION' ) && ! empty( $theplus_licence ) ) { $manage_licence['tpae'] = $theplus_licence; } $nexter_licence = get_option( 'tpgb_activate', array() ); // Also require the Nexter Blocks Pro plugin to be active (Pro defines // TPGBP_VERSION; the free plugin defines TPGB_VERSION), so the notice hides // when the Pro plugin is removed but its licence option persists. if ( ! empty( $nexter_active_check ) && defined( 'TPGBP_VERSION' ) && ! empty( $nexter_licence ) && ! empty( $nexter_licence['tpgb_activate_key'] ) ) { $tpgb_license_status = get_option( 'tpgbp_license_status', array() ); $tpgb_license_status['license_key'] = $nexter_licence['tpgb_activate_key']; $manage_licence['tpag'] = $tpgb_license_status; } return $manage_licence; } /** * * It is Use for get all info of user. * * @since 1.0.0 */ protected function wdkit_get_user_info() { $token = isset( $_POST['token'] ) ? wp_unslash( $_POST['token'] ) : false; $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false; $builder = isset( $_POST['builder'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['builder'] ) ) ) : ''; $site_url = isset( $_POST['site_url'] ) ? esc_url_raw( wp_unslash( $_POST['site_url'] ) ) : ''; $response = array(); if ( empty( $token ) ) { $response = array( 'success' => false, 'message' => $this->e_msg_login, 'description' => $this->e_desc_login, ); wp_send_json( $response ); wp_die(); } // $token = $this->wdkit_login_user_token( $email ); $args = array( 'token' => $token, 'builder' => $builder, 'site_url' => $site_url, ); $response = WDesignKit_Data_Query::get_data( 'get_user_info', $args ); if ( is_wp_error( $response ) ) { wp_send_json( array( 'success' => false, 'message' => $response->get_error_message(), 'description' => $response->get_error_message(), ) ); wp_die(); } $status = ( ! empty( $response['status'] ) ) ? sanitize_text_field( $response['status'] ) : 'error'; $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false; /**Condtion user for user logout & expire token*/ if ( 'Token is Expired' === $status || 'Authorization Token not found' === $status ) { delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) ); // Clear stored license data when token expires so banner shows again delete_option( 'wdkit_licence_data' ); } if ( empty( $response ) ) { $response['login_reset'] = 'yes'; } // Store WDesignKit license data locally if present in response if ( ! empty( $response['credits']['wdkit_licence'] ) && is_array( $response['credits']['wdkit_licence'] ) ) { $wdkit_licence = $response['credits']['wdkit_licence']; // Handle serialized data if ( is_string( $wdkit_licence ) && is_serialized( $wdkit_licence ) ) { $wdkit_licence = unserialize( $wdkit_licence, array( 'allowed_classes' => false ) ); } if ( ! empty( $wdkit_licence ) && is_array( $wdkit_licence ) ) { update_option( 'wdkit_licence_data', $wdkit_licence ); } } $response['Setting'] = $this->wkit_get_settings_panel(); $response['widget_list'] = $this->wkit_manage_widget_sequence( $response ); $response['manage_licence'] = $this->wkit_manage_license_data(); $response = array( 'data' => $response, 'success' => true, ); wp_send_json( $response ); wp_die(); } /** * * It is Use for get all widgets local and server. * * @since 1.0.19 * @param string $response userinfo store. */ protected function wkit_manage_widget_sequence( $response = array() ) { $credits = ! empty( $response['credits']['widget_limit']['meta_value'] ) ? $response['credits']['widget_limit']['meta_value'] : 10; $server_list = ! empty( $response['widgettemplate'] ) ? $response['widgettemplate'] : array(); $db_builder_list = ! empty( $response['widgetbuilder'] ) ? $response['widgetbuilder'] : array(); // Whether this call actually carried the server widget list that activation state is // derived from. Captured before the loops below, which unset() matched $server_list // entries as they go. wdkit_meta_data() calls this method with array(), and without // this flag that call rebuilt $db_widget from local widgets only — every one of which // is forced 'active' further down — and then wrote the empty result over // wkit_deactivate_widgets, erasing every deactivation the user had made. $has_server_widgets = ! empty( $server_list ); $placeholderimg = WDKIT_URL . 'assets/images/placeholder.jpg'; $local_list = $this->wdkit_get_local_widgets(); $server_w_unique = array_column( $server_list, 'w_unique' ); $idx_builder = array(); foreach ( $db_builder_list as $index => $value ) { $builder_name = ! empty( $value['builder_slug'] ) ? $value['builder_slug'] : ''; $w_id = ! empty( $value['w_id'] ) ? $value['w_id'] : ''; if ( ! empty( $builder_name ) ) { $idx_builder[ $w_id ] = strtolower( str_replace( ' ', '_', trim( $builder_name ) ) ); } } foreach ( $server_list as $index => $value ) { $get_id = $server_list[ $index ]['builder'] ? $server_list[ $index ]['builder'] : ''; $server_list[ $index ]['type'] = 'server'; $server_list[ $index ]['builder'] = ! empty( $idx_builder[ $get_id ] ) ? $idx_builder[ $get_id ] : ''; } $count = 0; foreach ( $local_list as $key => $value ) { $widget_id = ! empty( $value['widgetdata']['widget_id'] ) ? $value['widgetdata']['widget_id'] : ''; $allow_push = isset( $value['widgetdata']['allow_push'] ) ? $value['widgetdata']['allow_push'] : true; if ( in_array( $widget_id, $server_w_unique ) ) { $index = array_search( $widget_id, $server_w_unique ); $server_list[ $index ]['title'] = $local_list[ $key ]['widgetdata']['name']; $server_list[ $index ]['w_version'] = $local_list[ $key ]['widgetdata']['widget_version']; $server_list[ $index ]['allow_push'] = $allow_push; $server_list[ $index ]['builder'] = $local_list[ $key ]['widgetdata']['type']; $server_list[ $index ]['w_unique'] = $local_list[ $key ]['widgetdata']['widget_id']; $server_list[ $index ]['image'] = ! empty( $local_list[ $key ]['widgetdata']['w_image'] ) ? $local_list[ $key ]['widgetdata']['w_image'] : $placeholderimg; $local_list[ $key ] = $server_list[ $index ]; $local_list[ $key ]['type'] = 'done'; unset( $server_list[ $index ] ); } else { $local_list[ $key ]['widgetdata']['builder'] = $local_list[ $key ]['widgetdata']['type']; $local_list[ $key ]['widgetdata']['w_unique'] = $local_list[ $key ]['widgetdata']['widget_id']; $local_list[ $key ]['widgetdata']['allow_push'] = $allow_push; $local_list[ $key ]['widgetdata']['image'] = ! empty( $local_list[ $key ]['widgetdata']['w_image'] ) ? $local_list[ $key ]['widgetdata']['w_image'] : $placeholderimg; $local_list[ $key ]['widgetdata']['is_activated'] = 'active'; $local_list[ $key ]['widgetdata']['type'] = 'plugin'; $local_list[ $key ]['widgetdata']['title'] = $local_list[ $key ]['widgetdata']['name']; unset( $local_list[ $key ]['widgetdata']['name'] ); unset( $local_list[ $key ]['widgetdata']['widget_id'] ); $local_list[ $key ] = $local_list[ $key ]['widgetdata']; } } $final = array_merge( $local_list, $server_list ); $db_widget = array(); foreach ( $final as $key => $self ) { $is_activated = ! empty( $final[ $key ]['is_activated'] ) ? $final[ $key ]['is_activated'] : 'active'; if ( 'active' === $is_activated ) { ++$count; } if ( ( $count > $credits ) && ( 'unlimited' !== $credits ) ) { $final[ $key ]['is_activated'] = 'deactive'; } if ( ! empty( $self['is_activated'] ) && 'active' !== $self['is_activated'] ) { $db_widget[] = array( 'w_unique' => $self['w_unique'], 'builder' => $self['builder'], 'title' => $self['title'], 'is_activated' => $self['is_activated'], ); } } // Only persist activation state when the server list it is derived from was actually // supplied. See $has_server_widgets above. if ( $has_server_widgets ) { // update_option() creates the row when it is missing, so it covers both cases. // The previous add_option()/update_option() split was chosen on empty( $option ), // but wdkit_db_widgetlist() creates this row as an empty array on every install — // so the empty branch ran while the row already existed, and add_option() is a // no-op for an existing option. Deactivating from the My Widgets screen was // therefore silently discarded on effectively every site. Autoload stays 'yes', // matching the original add_option() call and wdkit_db_widgetlist(). update_option( 'wkit_deactivate_widgets', $db_widget, 'yes' ); // The cached widget registry bakes in wkit_deactivate_widgets membership and is // stored as a no-expiry transient, so it never self-heals. Without this the // loaders kept registering a widget the user had just switched off (and kept // hiding one they had switched back on) until the transient was flushed by hand. // The write above is not per-builder — one save can change any builder's set, and // a widget can move between builders — so clear all four. if ( function_exists( 'wdesignkit_invalidate_widget_registry' ) ) { foreach ( array( 'elementor', 'gutenberg', 'gutenberg_core', 'bricks' ) as $builder_slug ) { wdesignkit_invalidate_widget_registry( $builder_slug ); } } } return $final; } /** * Browse Page Filter * * @since 1.0.0 */ protected function wdkit_browse_page() { $args = $this->wdkit_parse_args( $_POST ); $response = WDesignKit_Data_Query::get_data( 'browse_page', $args ); if ( is_wp_error( $response ) ) { wp_send_json( array( 'success' => false, 'message' => $response->get_error_message(), ) ); wp_die(); } $manage_licence = array(); $manage_licence['theplus_elementor_addon'] = ! empty( defined( 'THEPLUS_VERSION' ) ) ? true : false; $manage_licence['tpag'] = ! empty( defined( 'TPGBP_VERSION' ) ) ? true : false; $manage_licence['elementor-pro'] = ! empty( defined( 'ELEMENTOR_PRO_VERSION' ) ) ? true : false; $response['manage_licence'] = $manage_licence; wp_send_json( $response ); wp_die(); } /** * * It is Use for get template from kit. * * @since 1.0.0 */ protected function wdkit_template() { $args = $this->wdkit_parse_args( $_POST ); $response = WDesignKit_Data_Query::get_data( 'kit_template', $args ); wp_send_json( $response ); wp_die(); } /** * * It is Use for remove or delete template. * * @since 1.0.0 */ protected function wdkit_template_remove() { $args = $this->wdkit_parse_args( $_POST ); $user_email = strtolower( sanitize_email( $args['email'] ) ); $response = ''; // Bug D fix: response()->json() is Laravel syntax — causes PHP fatal. Use plain array. if ( empty( $user_email ) || empty( $args['template_id'] ) ) { $response = array( 'message' => $this->e_msg_login, 'description' => $this->e_desc_login, 'success' => false, ); wp_send_json( $response ); wp_die(); } $args['token'] = $this->wdkit_login_user_token( $user_email ); unset( $user_email ); $response = WDesignKit_Data_Query::get_data( 'template_remove', $args ); wp_send_json( $response ); wp_die(); } /** * * It is Use for save template from page builder. * * @since 1.0.0 */ protected function wdkit_put_save_template() { $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false; $post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( wp_unslash( $_POST['post_id'] ) ) : ''; $builder = isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : ''; $response = ''; if ( empty( $email ) ) { $response = array( 'id' => 0, 'editpage' => '', 'message' => $this->e_msg_login, 'description' => $this->e_desc_login, 'success' => false, ); wp_send_json( $response ); wp_die(); } $args = $this->wdkit_parse_args( $_POST ); $args['token'] = $this->wdkit_login_user_token( $email ); unset( $args['email'] ); if( 'elementor' === $builder ){ $args['data'] = base64_decode( $args['data'] ); } else if ( 'gutenberg' === $builder ) { $args['data'] = base64_decode( $args['data'] ); } global $post; $custom_fields = array(); if ( ! empty( $post_id ) ) { $meta_fields = get_post_custom( $post_id ); foreach ( $meta_fields as $key => $value ) { if ( str_contains( $key, 'nxt-' ) ) { $custom_fields[ $key ] = $value; } } if ( ! empty( $custom_fields ) ) { $data = json_decode( $args['data'], true ); $data['custom_meta'] = $custom_fields; $args['data'] = wp_json_encode( $data ); } } $response = WDesignKit_Data_Query::get_data( 'save_template', $args ); /** * The cloud call can come back as a WP_Error (timeout, DNS, refused) or with an * empty / unparsable body, which json_decode()s to null. Forwarding that as-is * makes admin-ajax answer with a literal `null` that the editor then reads * `.id` off, killing the whole app. Normalise it to the failure shape used above. */ if ( is_wp_error( $response ) || ! is_array( $response ) ) { $response = array( 'id' => 0, 'editpage' => '', 'message' => esc_html__( 'Template Not Saved !', 'wdesignkit' ), 'description' => is_wp_error( $response ) ? $response->get_error_message() : esc_html__( 'Could not reach the WDesignKit server. Please try again.', 'wdesignkit' ), 'success' => false, ); } wp_send_json( $response ); wp_die(); } /** * * It is Use for update save template image. * * @since 2.0.6 */ protected function wdkit_update_save_temp_image() { $temp_content = isset( $_POST['temp_content'] ) ? esc_url_raw( $_POST['temp_content'] ) : ''; $content_name = isset( $_POST['content_name'] ) ? sanitize_text_field( $_POST['content_name'] ) : ''; $token = isset( $_POST['token'] ) ? sanitize_text_field( $_POST['token'] ) : ''; $user_type = isset( $_POST['user_type'] ) ? sanitize_text_field( $_POST['user_type'] ) : ''; $type = isset( $_POST['content_type'] ) ? sanitize_text_field( $_POST['content_type'] ) : ''; $id = isset( $_POST['id'] ) ? sanitize_text_field( $_POST['id'] ) : ''; if ( empty( $temp_content ) || empty( $user_type ) || empty( $id ) || empty( $token ) ) { $response = array( 'message' => __( 'Data not found', 'wdesignkit' ), 'description' => __( 'Data not found', 'wdesignkit' ), 'success' => false, ); } else { $temp_content = str_replace( '\\', '', $temp_content ); // SSRF guard (CWE-918): validate the resolved host before fetching a caller-supplied URL. $fetched = wdesignkit_safe_remote_get( $temp_content ); $temp_content = is_wp_error( $fetched ) ? '' : wp_remote_retrieve_body( $fetched ); $temp_content = base64_encode( $temp_content ); $args = array( 'token' => $token, 'template_id' => $id, 'name' => $content_name, 'content' => $temp_content, 'type' => $type, ); $response = $this->wkit_api_call( $args, 'save_images' ); $success = ! empty( $response['success'] ) ? $response['success'] : false; if ( $success ) { $response = array( 'data' => $response['data'], 'success' => true, ); } else { $response = array( 'message' => __( 'API Error', 'wdesignkit' ), 'description' => __( 'API Error', 'wdesignkit' ), 'data' => $response['data'], 'success' => false, ); } } wp_send_json( $response ); wp_die(); } /** * * It is Use for save image to WordPress Media Library. * * @since 2.3.3 */ protected function wdkit_save_wp_images() { $image_url = isset( $_POST['image'] ) ? sanitize_text_field( $_POST['image'] ) : ''; $response = self::wdkit_sideload_image_data( $image_url ); wp_send_json( $response ); wp_die(); } /** * Sideload one image into the media library and describe the outcome. * * Extracted verbatim from wdkit_save_wp_images() so the same code can be reached * without `$_POST` — Wdkit_Import_Media::sideload() calls this, which is why the PHP * runner does not need its own copy of the guard or the importer hash stamps. * * The AJAX action above is now a thin adapter over this and its response is byte-for-byte * what it was before. * * @since 2.6.5 * * @param string $image_url External image URL. * @return array{message:string,description:string,success:bool,url?:string} */ public static function wdkit_sideload_image_data( $image_url ) { // media_sideload_image() generates every registered thumbnail size, which decodes // the full source bitmap. Same guard as the page import. self::wdkit_guard_oversized_images(); $image_url = is_string( $image_url ) ? $image_url : ''; // And the same time-limit headroom the import requests take. A full-resolution stock // original can spend more than PHP's default 30s inside Imagick generating subsizes // on its own, and this endpoint is called once per picked image - so without this a // single large pick fatals the request and the image is silently never copied. // Harmless no-op where set_time_limit() is disabled. if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 300 ); } // The URL arrives as the argument, never from $_POST. wdkit_save_wp_images() is the // only caller that has a request to read, and it passes what it read; re-reading it // here blanked the URL for every other caller - Wdkit_Import_Media::sideload() runs // on cron, where there is no $_POST at all, so every blog-post featured image was // silently skipped. if ( empty( $image_url ) ) { $response = array( 'message' => __( 'No Image Provided', 'wdesignkit' ), 'description' => __( 'No Image URL provided for save.', 'wdesignkit' ), 'success' => false, ); } else { // Cap first, then validate, then fetch - so the exact string handed to the // fetcher is the string that was checked. Validating before cap_pexels_source() // rewrites the URL would leave a gap the moment the rewriter changes. $image_url = Wdkit_Image_Guard::cap_pexels_source( $image_url ); // The URL is client-controlled and goes straight to an outbound fetch that // writes into uploads, so it needs the same DNS + reserved-range check as // every other fetch in the plugin. sanitize_text_field() on the caller's side // is a formatting function and permits http://127.0.0.1/, http://[::1]/ and // internal hostnames. // // Returns rather than wp_send_json(): this is now a shared helper whose // contract is "describe the outcome", and half its callers are not serving a // request. Dying here would have taken down the cron worker mid-import. if ( ! function_exists( 'wdesignkit_validate_external_url' ) || ! wdesignkit_validate_external_url( $image_url ) ) { return array( 'message' => __( 'Upload Failed', 'wdesignkit' ), 'description' => __( 'That image URL was rejected.', 'wdesignkit' ), 'success' => false, ); } /* media_sideload_image() lives in wp-admin/includes/media.php and reaches * download_url() in wp-admin/includes/file.php. An admin-ajax request already has * both, which is why the browser importer never needed this — but a cron request * has neither, so every image this touched died with "Call to undefined function * download_url()". On a remote import that meant all six blog posts failed while * the run still reported success. The sibling call site in * class-wdkit-import-temp-ajax.php has always loaded these three. */ require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; $attachment_id = media_sideload_image( $image_url, 0, null, 'id' ); if ( is_wp_error( $attachment_id ) ) { $response = array( 'message' => __( 'Upload Failed', 'wdesignkit' ), 'description' => $attachment_id->get_error_message(), 'success' => false, ); } else { $saved_url = wp_get_attachment_url( $attachment_id ); // Elementor's importer skips an image only when it finds // _elementor_source_image_hash matching sha1 of the URL it is given. The // content we hand it now carries this local URL, so stamp the hash of that // URL too - without it Elementor re-downloads a file already on disk and // leaves a "-1" duplicate behind for every image on every page that uses it. if ( $saved_url ) { update_post_meta( $attachment_id, '_elementor_source_image_hash', sha1( $saved_url ) ); // Same purpose for the block importer, which keys off its own meta. update_post_meta( $attachment_id, 'tpgb_source_image_key', sha1( $saved_url ) ); } $response = array( 'message' => __( 'Image Saved', 'wdesignkit' ), 'description' => __( 'Image successfully saved to Media Library.', 'wdesignkit' ), 'success' => true, 'url' => $saved_url, 'id' => $attachment_id, ); } } return $response; } /** * The two bundle/media primitives the PHP import runner needs, reachable without a * request. * * Both of these are the optimisations that made the browser import fast, and both were * private to the AJAX actions that introduced them - so the runner could not use them * and grew its own slower equivalents instead: one cloud round trip per template rather * than one for the whole kit, and every image downloaded inside the import request * rather than swept up afterwards. These wrappers are the whole of what it took to * share them; the implementations are untouched and the AJAX actions still call them * exactly as before. * * @since 2.7.2 * * @param array $request Same fields the `wkit_fetch_site_bundle` action reads, except * `template_ids` may be an array. Pass `widgets_only` to have the * assembled bundle parked in the transient for later stages. * @param string $token Cloud token the caller already resolved. * @return array|WP_Error Decoded bundle. */ public function wdkit_site_bundle_for( $request, $token = '' ) { return $this->wdkit_fetch_site_bundle( is_array( $request ) ? $request : array(), $token ); } /** * Queue the background media sweep for pages the runner just created. * * @since 2.7.2 * * @param array $pages List of {post_id, builder, image_urls}. * @return array Same result the AJAX action returns. */ public function wdkit_schedule_media_sync_for( $pages ) { return $this->wkit_schedule_deferred_media_sync( is_array( $pages ) ? $pages : array() ); } /** * Run one stage of the PHP import runner for the Kit Import wizard. * * The wizard's replacement for its ~25-call orchestration: four calls, one per stage, * on this same action with the same nonce and the same `manage_options` check enforced * by the router above. Nothing about authentication changes, and no endpoint is added — * this is one more `case` on a router that already had 86. * * The response is shaped by Wdkit_Import_Wizard so the existing progress UI can consume * it without new widgets. See that class for the field-by-field mapping. * * @since 2.6.5 * * @return array */ protected function wdkit_import_stage() { if ( ! class_exists( 'Wdkit_Import_Wizard' ) ) { return array( 'success' => false, 'engine' => 'unavailable', 'message' => esc_html__( 'Import engine is not available.', 'wdesignkit' ), ); } $site_obj = isset( $_POST['site_obj'] ) ? json_decode( wp_unslash( $_POST['site_obj'] ), true ) : array(); $templates = isset( $_POST['templates'] ) ? json_decode( wp_unslash( $_POST['templates'] ), true ) : array(); $catalogue = isset( $_POST['plugin_catalogue'] ) ? json_decode( wp_unslash( $_POST['plugin_catalogue'] ), true ) : array(); $document = isset( $_POST['ai_document'] ) ? json_decode( wp_unslash( $_POST['ai_document'] ), true ) : null; return Wdkit_Import_Wizard::run_stage( array( 'stage' => isset( $_POST['stage'] ) ? sanitize_text_field( wp_unslash( $_POST['stage'] ) ) : '', 'session_id' => isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '', 'retry' => ! empty( $_POST['retry'] ) && 'yes' === sanitize_text_field( wp_unslash( $_POST['retry'] ) ), 'kit_id' => isset( $_POST['kit_id'] ) ? sanitize_text_field( wp_unslash( $_POST['kit_id'] ) ) : '', 'builder' => isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : 'elementor', 'site_obj' => is_array( $site_obj ) ? $site_obj : array(), 'templates' => is_array( $templates ) ? $templates : array(), 'plugin_catalogue' => is_array( $catalogue ) ? $catalogue : array(), 'ai_document' => $document, ) ); } /** * Register the pages a kit import just created for background media sideloading. * * Called once, after every page in the kit has been inserted with its content still * pointing at the source CDN's image URLs (see the `defer_media` branch of * import_page_section_content()/wdkit_media_import()). Nothing here downloads anything - * it only schedules one wp-cron event per page, so the import request itself returns * immediately and the user sees "Site Ready" without waiting on image processing. * * @since 2.6.6 * * @return array Response payload. */ protected function wkit_schedule_deferred_media_sync( $pages = null ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the AJAX router verified the nonce; the runner passes its own array and has no request to verify. if ( ! is_array( $pages ) ) { $pages = isset( $_POST['pages'] ) ? json_decode( wp_unslash( $_POST['pages'] ), true ) : array(); } if ( ! is_array( $pages ) || empty( $pages ) ) { return array( 'success' => false, 'message' => __( 'No pages to sync.', 'wdesignkit' ), ); } $scheduled = 0; $stagger = 0; $order = array(); // The home page goes first, with no delay. // // Every page used to be staggered two seconds apart, so on a nine-page kit the last // event was not even due until ~18s after the import finished - and nothing spawns // cron, so the queue sat until the next request arrived. That next request is the // user clicking "Preview Site", which is why the first look showed media that had // not been fetched yet and a refresh appeared to fix it. The home page is the one // they land on, so it is fetched first and immediately; the rest keep the stagger so // they do not all pile into one cron tick. // page_on_front is usually still unset here: this runs before the finalize request // that assigns it. So fall back to the same title rule the importer itself uses to // choose the front page (see "Front page and shop page" in wkit_import_site_bundle). $front_id = (int) get_option( 'page_on_front' ); if ( ! $front_id ) { foreach ( $pages as $page ) { $candidate = isset( $page['post_id'] ) ? (int) $page['post_id'] : 0; if ( ! $candidate ) { continue; } $title_lower = strtolower( (string) get_the_title( $candidate ) ); if ( false !== strpos( $title_lower, 'home' ) || false !== strpos( $title_lower, 'landing' ) ) { $front_id = $candidate; break; } } } usort( $pages, function ( $a, $b ) use ( $front_id ) { $a_id = isset( $a['post_id'] ) ? (int) $a['post_id'] : 0; $b_id = isset( $b['post_id'] ) ? (int) $b['post_id'] : 0; if ( $front_id && $a_id === $front_id ) { return -1; } if ( $front_id && $b_id === $front_id ) { return 1; } return 0; } ); foreach ( $pages as $page ) { $post_id = isset( $page['post_id'] ) ? (int) $page['post_id'] : 0; $builder = isset( $page['builder'] ) ? sanitize_key( $page['builder'] ) : ''; $image_urls = ( isset( $page['image_urls'] ) && is_array( $page['image_urls'] ) ) ? array_values( array_filter( array_map( 'esc_url_raw', $page['image_urls'] ) ) ) : array(); // esc_url_raw() formats, it does not authorise. This list came from the browser. $image_urls = self::wdkit_filter_fetchable_urls( $image_urls ); if ( ! $post_id || empty( $image_urls ) || ! get_post( $post_id ) ) { continue; } // Stagger each page's event a couple of seconds apart instead of firing every // page's sideload loop in the very same wp-cron tick - except the home page, // which is due right away because it is what the user previews. $is_front = ( $front_id && $post_id === $front_id ); wp_schedule_single_event( $is_front ? time() : ( time() + 2 + $stagger ), 'wdkit_async_sideload_page_images', array( $post_id, $builder, $image_urls ) ); if ( ! $is_front ) { $stagger += 2; } $order[] = $post_id; ++$scheduled; } return array( 'success' => true, 'message' => __( 'Media sync scheduled.', 'wdesignkit' ), 'scheduled' => $scheduled, // The order they were queued in, home page first. Returned so the success // screen's own drain uses the same priority instead of deciding again. 'order' => $order, ); } /** * Sideload one deferred page's media now, instead of waiting for wp-cron. * * The import deliberately leaves media to a background pass so the kit imports fast, and * that is worth keeping. What it cost was the first preview: nothing spawns cron, so the * queue waited for the next request - the user's own "Preview Site" click - and the page * they landed on still pointed at the source CDN with some references not written yet. * Refreshing appeared to fix it because that first view was what started the queue. * * So the success screen drives the queue itself over this endpoint once the import is * already reported done. It adds nothing to the import: by then the UI says complete and * the user is reading it. The scheduled cron events stay as the fallback for anyone who * navigates away, and each is cleared as its page is handled here so the work is not done * twice. It also covers hosts where a loopback cron spawn would never fire at all - * DISABLE_WP_CRON, or a system cron that only runs every few minutes. * * @since 2.7.1 * * @return array */ protected function wkit_run_deferred_media_now() { $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0; $builder = isset( $_POST['builder'] ) ? sanitize_key( wp_unslash( $_POST['builder'] ) ) : ''; $image_urls = isset( $_POST['image_urls'] ) ? json_decode( wp_unslash( $_POST['image_urls'] ), true ) : array(); $image_urls = is_array( $image_urls ) ? array_values( array_filter( array_map( 'esc_url_raw', $image_urls ) ) ) : array(); // esc_url_raw() formats, it does not authorise - same gate the scheduler applies, // because this list comes from the browser just the same. $image_urls = self::wdkit_filter_fetchable_urls( $image_urls ); if ( ! $post_id || ! get_post( $post_id ) || empty( $image_urls ) ) { return array( 'success' => false, 'message' => __( 'Nothing to sync for this page.', 'wdesignkit' ), ); } // Drop the queued event first, so a cron tick landing mid-run cannot repeat it. wp_clear_scheduled_hook( 'wdkit_async_sideload_page_images', array( $post_id, $builder, $image_urls ) ); $this->wdkit_async_sideload_page_images( $post_id, $builder, $image_urls ); return array( 'success' => true, 'message' => __( 'Media synced.', 'wdesignkit' ), 'post_id' => $post_id, ); } /** * Walk Elementor `elements` data replacing every deferred CDN media reference with the * local attachment sideloaded for it. * * Handles two shapes: a media control (`{ id, url }` siblings, e.g. an image control or a * has_sizes background-image control) gets both keys corrected, matching what * wdkit_repair_attachment_ids() does for the synchronous path. Any other string simply * gets the source URL substring swapped, which catches URLs embedded in HTML/text * controls that never had an `id` sibling to begin with. * * @since 2.6.6 * * @param mixed $node Elementor data, walked recursively. * @param array $url_map Source URL => array( 'id' => local attachment ID, 'url' => local URL ). * @return mixed Data with local media references. */ /** * Every spelling of one URL that can appear in stored content. * * A URL with a query string does not survive the round trip through the browser and * WordPress's own escaping as the raw string this code holds. `&` comes back HTML-entity * encoded as `&`, sometimes `&`, and - where a value is escaped twice on the way * into post content - as `&#038;`. So the picked stock images were downloaded and * stored correctly while the page kept the remote reference: the rewriters matched the * raw key, the content held an encoded one, and the substring test never fired. Measured: * 3 attachments created, 12 remote references still served on the landing page. * * Longest first, so a replacement pass cannot rewrite the `&` inside `&#038;` * and strand the remainder. * * @since 2.7.1 * * @param string $url Canonical URL as this code holds it. * @return array Distinct variants to search for, longest first. */ private static function wdkit_url_encoding_variants( $url ) { if ( ! is_string( $url ) || '' === $url || false === strpos( $url, '&' ) ) { return array( (string) $url ); } $variants = array( str_replace( '&', '&#038;', $url ), str_replace( '&', '&', $url ), str_replace( '&', '&amp;', $url ), str_replace( '&', '&', $url ), $url, ); $variants = array_values( array_unique( $variants ) ); usort( $variants, static function ( $a, $b ) { return strlen( $b ) - strlen( $a ); } ); return $variants; } /** * Replace every encoding of $source_url in $subject with $replacement. * * @since 2.7.1 * * @param string $subject Content to rewrite. * @param string $source_url URL to look for, in any encoding. * @param string $replacement Local URL to write in its place. * @return string */ private static function wdkit_replace_url_all_encodings( $subject, $source_url, $replacement ) { foreach ( self::wdkit_url_encoding_variants( $source_url ) as $variant ) { if ( false !== strpos( $subject, $variant ) ) { $subject = str_replace( $variant, $replacement, $subject ); } } return $subject; } /** * Re-point any media control still holding a foreign URL at the local file, when one * already exists. * * Downloads nothing. It only asks "has this exact source URL already been sideloaded?" * via the same _wdkit_deferred_source_url lookup the cron uses for de-duplication, and * rewrites url + id when the answer is yes. * * Why this is needed as a separate pass: the finalize sweep deliberately skips any page * with media still queued (see wdkit_sweep_attachment_ids() - sweeping there would turn * the import request into the media importer), and nothing re-sweeps once the queue * drains. wdkit_repair_attachment_ids() cannot cover it either - it is restricted to * local URLs by design. So a control whose rewrite was missed had nothing left to fix it. * * Observed on a Taj Bakery import: a tp-video-player kept * etemplates.wdesignkit.com/...mp4 in mp4_link.url with its id blanked, while the file * sat in the library as attachment 173 with a byte-identical _wdkit_deferred_source_url. * Every image on the same page had been rewritten; only that control was left behind. * * @since 2.7.1 * * @param mixed $node Elementor data, walked recursively. * @param int $fixed Running count of controls repaired, by reference. * @return mixed The data with any resolvable foreign reference pointed local. */ /** * Apply a url_map to every OTHER post that still references one of its source URLs. * * A deferred URL is scheduled per page, but the same file is often referenced by several * pages, and the de-duplication in this callback means only the first event actually * downloads it. Every event rewrites its own $post_id and nothing else - so a sibling * page whose own event did not carry that URL (a scheduling gap, or a page whose list was * assembled before the reference existed) keeps the remote URL with no later pass to fix * it: the finalize sweep skips queued pages by design, and wdkit_repair_attachment_ids() * only looks at local URLs. * * Measured on a Taj Bakery import: the About Us page kept * etemplates.wdesignkit.com/...mp4 in a tp-video-player while the file sat in the library * as a video/mp4 attachment. Handing that page's own callback the URL rewrote it * correctly, which is what showed the pipeline was sound and the routing was not. * * Bounded and cheap: one LIKE query per distinct source URL in this event's map, and only * posts that actually contain the string are touched. Downloads nothing. * * @since 2.7.1 * * @param array $url_map Source URL => array( id, url ), as built by this callback. * @param int $skip_id The post this callback already rewrote. * @return int Number of sibling posts rewritten. */ private function wdkit_apply_url_map_to_siblings( array $url_map, $skip_id ) { global $wpdb; if ( empty( $url_map ) ) { return 0; } $candidates = array(); foreach ( array_keys( $url_map ) as $source_url ) { // Both spellings: raw in post_content, slash-escaped inside _elementor_data JSON. foreach ( array( $source_url, str_replace( '/', '\/', $source_url ) ) as $needle ) { $ids = $wpdb->get_col( $wpdb->prepare( "SELECT DISTINCT p.ID FROM {$wpdb->posts} p LEFT JOIN {$wpdb->postmeta} m ON m.post_id = p.ID AND m.meta_key = '_elementor_data' WHERE p.post_status = 'publish' AND p.post_type IN ( 'page', 'post', 'nxt_builder', 'elementor_library' ) AND p.ID <> %d AND ( p.post_content LIKE %s OR m.meta_value LIKE %s ) LIMIT 50", (int) $skip_id, '%' . $wpdb->esc_like( $needle ) . '%', '%' . $wpdb->esc_like( $needle ) . '%' ) ); foreach ( (array) $ids as $id ) { $candidates[ (int) $id ] = true; } } } if ( empty( $candidates ) ) { return 0; } $rewritten = 0; foreach ( array_keys( $candidates ) as $sibling_id ) { $changed = false; $raw = get_post_meta( $sibling_id, '_elementor_data', true ); $data = is_array( $raw ) ? $raw : json_decode( (string) $raw, true ); if ( is_array( $data ) ) { $data = $this->wdkit_replace_deferred_media( $data, $url_map ); $relinked = 0; $data = self::wdkit_relink_known_foreign_media( $data, $relinked ); // wp_slash() for the same reason as the primary write above. update_post_meta( $sibling_id, '_elementor_data', wp_slash( wp_json_encode( $data ) ) ); $changed = true; } $post = get_post( $sibling_id ); if ( $post && ! empty( $post->post_content ) ) { $content = $post->post_content; foreach ( $url_map as $source_url => $local ) { $content = self::wdkit_replace_url_all_encodings( $content, $source_url, $local['url'] ); } if ( $content !== $post->post_content ) { self::wdkit_write_post_content( $sibling_id, $content ); $changed = true; } } if ( $changed ) { self::wdkit_invalidate_elementor_page_cache( $sibling_id ); ++$rewritten; } } return $rewritten; } private static function wdkit_relink_known_foreign_media( $node, &$fixed = 0 ) { if ( ! is_array( $node ) ) { return $node; } if ( isset( $node['url'] ) && is_string( $node['url'] ) && '' !== $node['url'] && array_key_exists( 'id', $node ) && self::wdkit_is_foreign_media_url( $node['url'] ) ) { $existing = self::wdkit_deferred_attachment_for_url( $node['url'] ); if ( $existing && self::wdkit_is_usable_attachment( $existing ) ) { $local = wp_get_attachment_url( $existing ); if ( $local ) { $node['url'] = $local; $node['id'] = (int) $existing; ++$fixed; } } } foreach ( $node as $key => $value ) { if ( is_array( $value ) ) { $node[ $key ] = self::wdkit_relink_known_foreign_media( $value, $fixed ); } } return $node; } private function wdkit_replace_deferred_media( $node, array $url_map ) { if ( is_array( $node ) ) { if ( isset( $node['url'] ) && is_string( $node['url'] ) && array_key_exists( 'id', $node ) ) { $hit = isset( $url_map[ $node['url'] ] ) ? $node['url'] : null; // The node's own url can be entity-encoded too, so an exact-key lookup alone // misses it - and this is the branch that repairs the `id`, without which the // widget resolves nothing even once the url is right. if ( null === $hit ) { $decoded = html_entity_decode( $node['url'], ENT_QUOTES, 'UTF-8' ); $decoded = html_entity_decode( $decoded, ENT_QUOTES, 'UTF-8' ); if ( isset( $url_map[ $decoded ] ) ) { $hit = $decoded; } } if ( null !== $hit ) { $node['id'] = $url_map[ $hit ]['id']; $node['url'] = $url_map[ $hit ]['url']; } } foreach ( $node as $key => $value ) { $node[ $key ] = $this->wdkit_replace_deferred_media( $value, $url_map ); } return $node; } if ( is_string( $node ) && '' !== $node ) { foreach ( $url_map as $source_url => $local ) { $node = self::wdkit_replace_url_all_encodings( $node, $source_url, $local['url'] ); } } return $node; } /** * Find the attachment a previous wdkit_async_sideload_page_images() run already made * for this exact source URL. * * @since 2.6.6 * * @param string $source_url Source CDN/stock-provider URL. * @return int Attachment ID, or 0 when none exists yet. */ private static function wdkit_deferred_attachment_for_url( $source_url ) { $existing = get_posts( array( 'post_type' => 'attachment', 'post_status' => 'inherit', 'posts_per_page' => 5, 'fields' => 'ids', 'no_found_rows' => true, 'meta_key' => '_wdkit_deferred_source_url', 'meta_value' => $source_url, ) ); // The row is not the answer - the file is. // // This used to return (int) $existing[0] straight out of the query, and every caller // then tested wp_get_attachment_url(), which is truthy for a record with nothing // behind it: with no _wp_attached_file it falls back to the guid, and wp_insert_post() // defaults an attachment's guid to its attachment-page permalink. So a dead record // answered the lookup with http://site/hand25-png/, the reuse branch took it as a hit // and skipped the download, and the page shipped an pointing at an HTML // page. Measured on all three shapes such a record comes in - no file with metadata, // no file without metadata, no _wp_attached_file at all - wp_get_attachment_url() // returned a truthy url for every one. // // A site that imported a kit on a build before Wdkit_Import_Images::import() started // refusing unknown file types is full of these, which is why the fixed build still // rendered broken images: the import adopted the old rows instead of downloading // again, and re-importing reused them just the same. Rejecting them here is what makes // a re-import repair the page - the caller falls through to its normal download and // then rewrites url + id from the attachment it actually created. // // Validated in the lookup rather than at each call site because there are three, they // were not all guarded, and a fourth would not be either. foreach ( (array) $existing as $candidate ) { if ( self::wdkit_is_usable_attachment( $candidate ) ) { return (int) $candidate; } self::wdkit_forget_reuse_keys( $candidate ); } return 0; } /** * Take a broken attachment out of every reuse index this pipeline consults. * * Gating the lookup stops this plugin adopting a dead record, but the record keeps * answering the other two indexes - `_elementor_source_image_hash`, which Elementor's own * Import_Images::import() resolves before it downloads anything, and * `tpgb_source_image_key`, which the block importer uses the same way. Left in place it * would still be handed back through them. * * Deletes the meta, never the attachment. A row with no file is worthless to this * importer, but it is not this importer's to remove: a media-offload plugin legitimately * has no local file, and a customer's library is not something an import routine should be * deleting from. Dropping it out of the indexes is enough to make the next import fetch a * fresh copy. * * @since 2.7.1 * * @param int $id Attachment that failed wdkit_is_usable_attachment(). * @return void */ private static function wdkit_forget_reuse_keys( $id ) { $id = (int) $id; if ( ! $id ) { return; } foreach ( array( '_wdkit_deferred_source_url', '_elementor_source_image_hash', 'tpgb_source_image_key' ) as $key ) { delete_post_meta( $id, $key ); } } /** * Largest media file the background pass will pull down, in bytes. * * Video is the only thing a kit references that can plausibly run to hundreds of megabytes. * Nothing decodes it - download_url() streams to a temp file - so this is not about memory; * it is about one oversized file monopolising a cron event's time limit and disk while * every URL queued behind it goes unprocessed. * * @since 2.7.1 */ const WDKIT_DEFERRED_MEDIA_MAX_BYTES = 52428800; // 50 MB. /** * Sideload one deferred media URL, choosing the right primitive for what it is. * * media_sideload_image() hard-rejects any extension outside jpg|jpeg|jpe|png|gif|webp * ("Invalid image URL") before it downloads anything, so it can never localise a video - * nor, as it happens, an .avif, which this importer has been collecting for the background * pass all along and which was therefore being dropped just as silently. * * Images keep going through media_sideload_image() exactly as before: it is the path that * has been exercised on every import, and nothing here is worth changing about it. Anything * else goes through download_url() + media_handle_sideload(), which is generic, validates * the real type with wp_check_filetype_and_ext(), and is already what this file uses for * featured images. * * @since 2.7.1 * * @param string $source_url Remote URL. * @return int|WP_Error Attachment ID, or the error that stopped it. */ /** * Keep only the URLs this site is willing to fetch. * * Applied where a client-supplied `image_urls` list is accepted, so a URL that points at * an internal address is never written into the cron table in the first place. The cron * callback validates again before fetching - that is the authoritative gate, since a cron * arg can outlive the request that created it. * * Drops only what is *provably* unsafe, which is deliberately narrower than the sink. * wdesignkit_validate_external_url() fails closed on a host it cannot resolve ("an * unresolvable host cannot be proven public"), and that is the right answer immediately * before a fetch - but the wrong one here. An import resolves dozens of hosts at once, so * a transient DNS failure during that burst is ordinary; refusing on it would drop the URL * before it was ever scheduled, and nothing would retry it - the image would stay remote * permanently. Deferring that case to the cron costs nothing: the sink re-checks and fails * closed there, by which time DNS has usually recovered. * * The threat this closes is an internal URL being persisted into a cron argument, and such * a URL resolves by definition - to a private or reserved address - so it is caught here. * * @since 2.7.1 * * @param array $urls Candidate URLs, already esc_url_raw()'d. * @return array The subset that is not provably unsafe. */ private static function wdkit_filter_fetchable_urls( $urls ) { if ( ! is_array( $urls ) || empty( $urls ) ) { return array(); } // One verdict per host, not per URL. A kit page carries well over a hundred media // URLs across a handful of hosts, and both the validator and the resolve check below // hit DNS - so without this, a page's worth of images costs a hundred-plus lookups // inside the import request, and on a host with slow resolution that is time the user // spends watching a progress bar. The decision is a property of the host, so caching // it changes nothing about the outcome. Request-scoped: a static here lives exactly // as long as the import request that built it. static $verdict = array(); $allowed = array(); foreach ( $urls as $url ) { if ( ! is_string( $url ) || '' === $url ) { continue; } $scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) ); $host = (string) wp_parse_url( $url, PHP_URL_HOST ); // Nothing downstream can make sense of these, whatever DNS says. if ( ! in_array( $scheme, array( 'http', 'https' ), true ) || '' === $host ) { continue; } if ( ! function_exists( 'wdesignkit_validate_external_url' ) ) { // No validator loaded: keep the URL and let the sink - which requires the // validator and refuses without it - make the call. Dropping everything here // would silently disable deferred media rather than secure it. $allowed[] = $url; continue; } $key = strtolower( $host ); if ( ! isset( $verdict[ $key ] ) ) { // The validator takes a URL, but everything it decides on - resolution and // address range - depends only on the host, so a bare origin is a faithful // probe for the whole host and the answer applies to every URL on it. $probe = $scheme . '://' . $host . '/'; // Keep it unless it is provably unsafe: rejected AND resolvable, which // together mean it resolved to an address we refuse to reach. A rejection // with no resolution is DNS being briefly unavailable - the sink re-checks // and fails closed there, so deferring that call loses nothing. $verdict[ $key ] = wdesignkit_validate_external_url( $probe ) || ! self::wdkit_host_resolves( $host ); } if ( $verdict[ $key ] ) { $allowed[] = $url; } } return $allowed; } /** * Does this host resolve to anything right now? * * Used only to tell a validation failure caused by a private address apart from one * caused by DNS being briefly unavailable. A literal IP always counts as resolved. * * @since 2.7.1 * * @param string $host Hostname from the URL. * @return bool True when at least one address came back. */ private static function wdkit_host_resolves( $host ) { // wp_parse_url() returns an IPv6 literal still wrapped in its URL brackets // (`[::1]`), which is not valid IP syntax - so without this the address fails the // test below, resolves to nothing, and is misread as "DNS is down" rather than // "this is loopback". $host = trim( $host, '[]' ); if ( filter_var( $host, FILTER_VALIDATE_IP ) ) { return true; } if ( function_exists( 'gethostbynamel' ) ) { $v4 = gethostbynamel( $host ); if ( is_array( $v4 ) && ! empty( $v4 ) ) { return true; } } if ( function_exists( 'dns_get_record' ) ) { $v6 = @dns_get_record( $host, DNS_AAAA ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a lookup failure is the answer, not an error. if ( is_array( $v6 ) && ! empty( $v6 ) ) { return true; } } $resolved = gethostbyname( $host ); // Returns the host unchanged on failure. return ( $resolved && $resolved !== $host && (bool) filter_var( $resolved, FILTER_VALIDATE_IP ) ); } private static function wdkit_sideload_deferred_media( $source_url ) { if ( ! class_exists( 'Wdkit_Image_Guard' ) ) { require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php'; } /** * Largest deferred media file to download, in bytes. * * Filterable because the right ceiling is a property of the host, not of the kit: * shared hosting with a 30s cap and a slow link needs a lower one than a VPS. * * @since 2.7.1 * * @param int $max_bytes Default limit. * @param string $source_url URL being considered. */ $max_bytes = (int) apply_filters( 'wdkit_deferred_media_max_bytes', self::WDKIT_DEFERRED_MEDIA_MAX_BYTES, $source_url ); // Same SSRF check every other outbound fetch in this plugin goes through - DNS // resolution plus a reserved-range test, so private, loopback and link-local // addresses are rejected. It gates BOTH branches below, and has to: this URL list // arrives from the browser in $_POST (see the `image_urls` contract on the bundle and // pages routes), so it is client-controlled, and esc_url_raw() is a formatting // function, not an access-control one. Running in a cron event does not make the // origin of the URL any more trustworthy - it removes the last chance to ask. // Nonce + manage_options do not close this: they establish who is asking, not where // the server may be pointed, and neither stops a CSRF-assisted request. download_url() // blocks only the plain 169.254.169.254 metadata case, leaving multi-A-record hosts, // internal IPv6 and same-host URLs reachable. if ( ! function_exists( 'wdesignkit_validate_external_url' ) || ! wdesignkit_validate_external_url( $source_url ) ) { return new WP_Error( 'wdkit_media_url_rejected', 'Refusing to fetch a URL that failed validation.' ); } $ext = strtolower( (string) pathinfo( (string) wp_parse_url( $source_url, PHP_URL_PATH ), PATHINFO_EXTENSION ) ); // The set WordPress's own image sideloader accepts. Everything it accepts, it keeps. if ( in_array( $ext, array( 'jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp' ), true ) ) { return media_sideload_image( $source_url, 0, null, 'id' ); } // Ask before fetching. A HEAD that answers with a size lets an oversized file be skipped // for the cost of one request; a host that does not answer with one is not a reason to // refuse, so the download proceeds and the on-disk size below is the real backstop. $head = wp_remote_head( $source_url, array( 'timeout' => 15, 'redirection' => 3 ) ); if ( ! is_wp_error( $head ) ) { $length = (int) wp_remote_retrieve_header( $head, 'content-length' ); if ( $length > $max_bytes ) { return new WP_Error( 'wdkit_media_too_large', sprintf( 'Skipped %s: %d bytes exceeds the %d byte limit.', $source_url, $length, $max_bytes ) ); } } $tmp = download_url( $source_url, 300 ); if ( is_wp_error( $tmp ) ) { return $tmp; } // The backstop for a host that sent no Content-Length, or lied about it. $size = (int) @filesize( $tmp ); if ( $size > $max_bytes ) { @unlink( $tmp ); return new WP_Error( 'wdkit_media_too_large', sprintf( 'Skipped %s: %d bytes on disk exceeds the %d byte limit.', $source_url, $size, $max_bytes ) ); } // From the URL *path*, so a query string never ends up in the filename - the same reason // Wdkit_Import_Images does this rather than basename() the whole URL. $file_name = Wdkit_Image_Guard::filename_from_url( $source_url ); // filename_from_url() only matches a path that already names a known media type, so // it returns '' for the extensionless CDN URLs this branch exists to handle. Fall back // to the last path segment and let the sniffing below name the type. if ( '' === $file_name ) { $path = (string) wp_parse_url( $source_url, PHP_URL_PATH ); $file_name = sanitize_file_name( (string) wp_basename( rtrim( $path, '/' ) ) ); } if ( '' === $file_name ) { @unlink( $tmp ); return new WP_Error( 'wdkit_media_no_filename', 'Could not derive a filename from the URL.' ); } // The template CDN serves media from extensionless URLs and declares the type in the // response, so there is nothing in the path to name the file after. Left as-is, // wp_check_filetype_and_ext() below has no extension to check and refuses the upload - // which is why extensionless media was never localised even once it was collected. // The type is read from the bytes on disk, not from the URL or a response header, so a // mislabelled file still cannot smuggle in an extension it does not match. if ( '' === (string) pathinfo( $file_name, PATHINFO_EXTENSION ) ) { $sniffed = wp_check_filetype_and_ext( $tmp, $file_name . '.jpg' ); $type = ! empty( $sniffed['type'] ) ? $sniffed['type'] : ''; if ( '' === $type && function_exists( 'getimagesize' ) ) { $info = @getimagesize( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- malformed input is the case being detected. $type = ( is_array( $info ) && ! empty( $info['mime'] ) ) ? $info['mime'] : ''; } $ext = ''; foreach ( wp_get_mime_types() as $exts => $mime ) { if ( $mime === $type ) { $ext = strtok( $exts, '|' ); break; } } if ( '' === $ext ) { @unlink( $tmp ); return new WP_Error( 'wdkit_media_unknown_type', sprintf( 'Skipped %s: no extension in the URL and the downloaded file is not a recognised media type.', $source_url ) ); } $file_name .= '.' . $ext; } // media_handle_sideload() runs the upload through wp_check_filetype_and_ext(), so a file // whose contents do not match its extension is refused here rather than stored. $attachment_id = media_handle_sideload( array( 'name' => $file_name, 'tmp_name' => $tmp, ), 0 ); // On failure media_handle_sideload() has already removed the temp file; on success it // moved it. This only catches the path where it returned early without doing either. if ( is_wp_error( $attachment_id ) && file_exists( $tmp ) ) { @unlink( $tmp ); } return $attachment_id; } /** * The original-image URL behind a WordPress size variant, or '' when it is not one. * * `photo-300x298.png` -> `photo.png`. Only the trailing -WIDTHxHEIGHT before the extension * is removed; query strings are kept. * * @param string $url Image URL. * @return string */ private static function wdkit_size_variant_of( $url ) { $original = preg_replace( '/-\d+x\d+(\.(?:jpe?g|png|gif|webp|avif))(?=$|\?)/i', '$1', (string) $url, 1 ); return ( is_string( $original ) && $original !== $url ) ? $original : ''; } /** * Background half of the deferred media sideload: download every image a kit page's * content deferred at create time and repoint the saved page content at the local copies. * * Runs off a `wp_schedule_single_event()` (registered in the constructor), well after the * import request that created the page has already returned. Updates post meta / the * post row directly rather than going through Elementor's/the block importer's normal * save path, so this never re-runs widget ID generation or reflows the layout. * * @since 2.6.6 * * @param int $post_id Page created with source CDN URLs still in its content. * @param string $builder 'elementor' or anything else (treated as Gutenberg/HTML content). * @param array $image_urls Source CDN image URLs referenced by that page. */ public function wdkit_async_sideload_page_images( $post_id, $builder, $image_urls ) { $post_id = (int) $post_id; $image_urls = is_array( $image_urls ) ? $image_urls : array(); if ( ! $post_id || empty( $image_urls ) || ! get_post( $post_id ) ) { return; } if ( ! function_exists( 'media_sideload_image' ) ) { require_once ABSPATH . 'wp-admin/includes/media.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/image.php'; } // The synchronous import paths all extend this (see import_page_section_content(), // wkit_import_site_bundle()) for the same reason: Imagick thumbnail generation on an // oversized source image can outrun PHP's default 30s limit on its own, before this // loop even gets to a second URL. Unlike those requests, nothing is waiting on this // one - it is a wp-cron callback - so there is no reason not to give it the same // headroom; without it, a fatal here kills the whole event mid-loop (no unschedule, // no url_map, no content rewrite) and every image after the slow one in $image_urls // is left pointing at the source CDN with nothing left to ever retry it. if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 300 ); } // Same guard the synchronous path uses - a background run is no less able to blow // through the memory limit on an oversized source image. self::wdkit_guard_oversized_images(); $url_map = array(); /* Attachment ids sideloaded or found in this run, keyed by fetch URL, so an image the * kit references in several sizes is fetched once. */ $attached = array(); /* Sideload one URL (or reuse an earlier copy), returning its attachment id or 0. */ $attach = function ( $fetch_url ) use ( &$attached ) { if ( isset( $attached[ $fetch_url ] ) ) { return $attached[ $fetch_url ]; } $attachment_id = self::wdkit_deferred_attachment_for_url( $fetch_url ); if ( ! $attachment_id ) { $attachment_id = self::wdkit_sideload_deferred_media( $fetch_url ); if ( is_wp_error( $attachment_id ) ) { if ( 'wdkit_media_too_large' === $attachment_id->get_error_code() ) { error_log( 'WDKIT deferred media: ' . $attachment_id->get_error_message() ); $attached[ $fetch_url ] = 0; return 0; } usleep( 300000 ); $attachment_id = self::wdkit_sideload_deferred_media( $fetch_url ); } if ( is_wp_error( $attachment_id ) || ! $attachment_id ) { $attached[ $fetch_url ] = 0; return 0; } $local_url = wp_get_attachment_url( $attachment_id ); if ( $local_url ) { update_post_meta( $attachment_id, '_elementor_source_image_hash', sha1( $local_url ) ); update_post_meta( $attachment_id, 'tpgb_source_image_key', sha1( $local_url ) ); } update_post_meta( $attachment_id, '_wdkit_deferred_source_url', $fetch_url ); } $attached[ $fetch_url ] = (int) $attachment_id; return (int) $attachment_id; }; $urls = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $image_urls ) ) ) ); /* Full-size images first, so a size variant further down the list finds its original * already in the library instead of starting another download. */ usort( $urls, function ( $a, $b ) { return (int) ( '' !== self::wdkit_size_variant_of( $a ) ) - (int) ( '' !== self::wdkit_size_variant_of( $b ) ); } ); foreach ( $urls as $source_url ) { $fetch_url = Wdkit_Image_Guard::cap_pexels_source( $source_url ); $map_keys = ( $fetch_url !== $source_url ) ? array( $source_url, $fetch_url ) : array( $source_url ); /* A WordPress size variant (photo-300x298.png) of an image whose original * (photo.png) can be fetched: import the original once and point this reference at * the closest size WordPress generates for it. A kit page carried 31 such variants * among 50 images, each downloaded and resized on its own (ClickUp single-template * speed-up). Falls back to the variant itself when the original is unavailable. */ $original = self::wdkit_size_variant_of( $fetch_url ); if ( '' !== $original ) { $original_id = $attach( $original ); if ( $original_id ) { preg_match( '/-(\d+)x(\d+)\.[a-z0-9]+(?:$|\?)/i', $fetch_url, $dims ); $sized_url = ! empty( $dims ) ? wp_get_attachment_image_url( $original_id, array( (int) $dims[1], (int) $dims[2] ) ) : ''; $sized_url = $sized_url ? $sized_url : wp_get_attachment_url( $original_id ); if ( $sized_url ) { foreach ( $map_keys as $map_key ) { $url_map[ $map_key ] = array( 'id' => $original_id, 'url' => $sized_url, ); } continue; } } } $attachment_id = $attach( $fetch_url ); $local_url = $attachment_id ? wp_get_attachment_url( $attachment_id ) : ''; if ( ! $local_url ) { continue; } foreach ( $map_keys as $map_key ) { $url_map[ $map_key ] = array( 'id' => $attachment_id, 'url' => $local_url, ); } } if ( empty( $url_map ) ) { return; } if ( 'elementor' === $builder && did_action( 'elementor/loaded' ) ) { $raw = get_post_meta( $post_id, '_elementor_data', true ); $data = is_array( $raw ) ? $raw : json_decode( $raw, true ); if ( is_array( $data ) ) { $data = $this->wdkit_replace_deferred_media( $data, $url_map ); // The finalize sweep deliberately skips a page while its media is queued here // (see wdkit_sweep_attachment_ids()), and the remap above only touches the URLs // this event was handed. Anything else on the page that already had a local URL // beside a stale id - the picked stock images the AI path substitutes by URL // alone, for instance - would otherwise never get its id corrected by anyone: // the sweep passed the page over, and this callback never looked. Same walk the // sweep would have done, restricted to local URLs so nothing downloads from here. $data = self::wdkit_repair_attachment_ids( $data, true ); // Last resort for anything the rewrite above missed: point it at a file that // already exists locally. Costs one indexed meta lookup per foreign control // and never downloads, so it is safe to run on every event. $relinked = 0; $data = self::wdkit_relink_known_foreign_media( $data, $relinked ); if ( $relinked && defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { error_log( sprintf( '[WDKIT] relinked %d already-local media reference(s) on post %d', $relinked, $post_id ) ); } // wp_slash() is not optional here. update_post_meta() runs wp_unslash() on // the value, so handing it raw JSON strips every backslash the encoder put // in - `\/`, `\"`, `\uXXXX` - and what lands in the row is no longer // parseable. Elementor then reads zero elements off the page: it regenerates // post-N.css as empty, enqueues nothing, and the theme falls back to the // post_content mirror, so the page renders its text completely unstyled. // Elementor's own writer does the same thing for the same reason - see the // wp_slash() in Document::save_elements(). update_post_meta( $post_id, '_elementor_data', wp_slash( wp_json_encode( $data ) ) ); // Elementor mirrors the rendered output into post_content as well, and that // copy is what themes and search read. Rewriting only _elementor_data leaves // it pointing at the template's own site - invisible in the editor, but the // front end still requests those images from the source CDN. Same substring // swap the non-Elementor branch below performs, and through the same writer, // so the markup is not put through kses in this userless context. $post = get_post( $post_id ); if ( $post && ! empty( $post->post_content ) ) { $content = $post->post_content; foreach ( $url_map as $source_url => $local ) { $content = self::wdkit_replace_url_all_encodings( $content, $source_url, $local['url'] ); } // Same last-resort relink as the builder data above. post_content is the // copy the theme actually renders, so a reference left here is visible // even when _elementor_data is correct. foreach ( self::wdkit_collect_foreign_media_urls( $content ) as $stray ) { $existing = self::wdkit_deferred_attachment_for_url( $stray ); if ( $existing && self::wdkit_is_usable_attachment( $existing ) ) { $local_stray = wp_get_attachment_url( $existing ); if ( $local_stray ) { $content = self::wdkit_replace_url_all_encodings( $content, $stray, $local_stray ); } } } if ( $content !== $post->post_content ) { self::wdkit_write_post_content( $post_id, $content ); } } // Invalidate last, and only for this page. Last, because a render that // starts between the two writes above would otherwise regenerate its CSS // and element cache from half-rewritten data and then cache that. Only this // page, because the alternative is a site-wide flush - see // wdkit_invalidate_elementor_page_cache(). self::wdkit_invalidate_elementor_page_cache( $post_id ); // Then fix any OTHER page referencing the same files. Only the first event to // see a URL downloads it; without this, a sibling that references the same // file but whose own event never carried that URL keeps the remote reference // permanently. See wdkit_apply_url_map_to_siblings(). $siblings = $this->wdkit_apply_url_map_to_siblings( $url_map, $post_id ); if ( $siblings && defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) { error_log( sprintf( '[WDKIT] repaired %d sibling page(s) sharing media with post %d', $siblings, $post_id ) ); } } } else { $post = get_post( $post_id ); if ( $post ) { $content = $post->post_content; foreach ( $url_map as $source_url => $local ) { $content = self::wdkit_replace_url_all_encodings( $content, $source_url, $local['url'] ); } if ( $content !== $post->post_content ) { // The replace above fixes every `url` but leaves the `id` sitting beside it // still pointing at the source site's attachment. Blocks that render from // the url look correct either way; the ones that render through the // attachment id (tp-team-listing, tp-testimonials, tp-image, and // tp-container backgrounds) resolve nothing and render empty. Declining // returns null, in which case the url-only content above is kept exactly // as it is today. $remapped = $this->wdkit_remap_attachment_ids( $content, $url_map ); if ( null !== $remapped ) { $content = $remapped; } // Separate concern, separate pass: the id remap above fixes the block // attribute, but process_image() also stamped the id into the rendered // tag's class at import time, when the image was still remote. // Declining leaves the content exactly as the step above produced it. $classes_fixed = $this->wdkit_remap_wp_image_classes( $content, $url_map ); if ( null !== $classes_fixed ) { $content = $classes_fixed; } self::wdkit_write_post_content( $post_id, $content ); // The page's per-post CSS file (backgrounds, and anything else the block // CSS generator resolves through an attachment id) was built synchronously // at import time, before this deferred pass ever ran - so it still reads // whatever the deferred images' source urls/ids were back then. Rebuild it // now that post_content points at the local copies, the same way the // synchronous create path already does right after wp_insert_post(). self::wdkit_rebuild_block_css( $post_id ); } } } clean_post_cache( $post_id ); } /** * Throw away Elementor's cached render of one page, and only that page. * * The deferred pass rewrites `_elementor_data` behind Elementor's back, so three * per-post caches are left describing the version that still pointed at the source * CDN: the generated CSS file (background-image rules resolve through the attachment * id), `_elementor_element_cache` (the rendered element markup) and * `_elementor_page_assets`. All three have to go for the next render to be correct. * * This used to be `files_manager->clear_cache()`, which is not a per-page call at all: * it globs and unlinks *every* file in uploads/elementor/css, then runs * delete_post_meta_by_key() for all three keys across *every* post, and drops the * global CSS option. One call measured here destroyed 16 CSS files and 15 meta rows. * * That is actively harmful in this context, because one of these events is scheduled * per imported page - eleven of them, two seconds apart, on a kit this size. A visitor * who loads any page during that window gets HTML whose `` points at * post-N.css, and the file is deleted out from under the browser before it is fetched: * the page renders completely unstyled. It self-heals on a later render, which is * exactly why it reads as "the layout breaks when I refresh". * * Scoping it also means pages the deferred pass never touched keep their CSS instead * of being made to regenerate for nothing. * * @since 2.6.6 * * @param int $post_id Page whose cached render is now stale. * @return void */ public static function wdkit_invalidate_elementor_page_cache( $post_id ) { $post_id = (int) $post_id; if ( ! $post_id ) { return; } // Same call Elementor's own Document::save() makes - deletes the css file and the // _elementor_css meta for this one post. if ( class_exists( '\\Elementor\\Core\\Files\\CSS\\Post' ) ) { \Elementor\Core\Files\CSS\Post::create( $post_id )->delete(); } else { // Older Elementor without the CSS file classes: the meta alone still forces a // regeneration, which is what actually matters here. delete_post_meta( $post_id, '_elementor_css' ); } // Document::save() pairs the css delete with delete_cache(); that method is // protected, and these are the keys it clears. delete_post_meta( $post_id, '_elementor_element_cache' ); delete_post_meta( $post_id, '_elementor_page_assets' ); } /** * * Get Elementor Global color and Typography. * * @since 1.1.16 */ /** * Kit settings holding The Plus Addons' own globals. * * These sit in the Elementor kit's `_elementor_page_settings` alongside Elementor's * system_colors / system_typography, but the save flow only ever collected the four * Elementor keys. Widgets reference an entry in these lists by its `_id` through a * `tp_global_preset` setting, so a template saved without them travels with the * reference but not the definition - which is why imported sections come in missing * their button styling, radii and shadows. * * @since 2.6.4 * * @return array Kit setting keys. */ private function wdkit_tp_global_kit_keys() { return array( 'tp_global_button_style_list', 'tp_global_dimensions_list', 'tp_global_box_shadow_list', 'tp_global_gradient_list', 'tp_global_gsap_list', 'tp_global_scroll_animation_list', 'tp_text_global_gsap_list', 'tp_image_global_gsap_list', ); } /** * Merge incoming Plus globals into the active kit, keyed by `_id`. * * Entries are matched on their `_id`, never on position: an existing entry is always * left as it is, and only genuinely new ones are appended. That matters because * widgets - and the entries themselves, a button style points at dimension and shadow * entries - resolve by `_id`. Renumbering or overwriting would repoint references on * the destination site's own content. * * @since 2.6.4 * * @param array $incoming Lists captured with the template. * @return bool True when the kit was changed. */ /** * Global colour / typography ids this site already defines. * * @since 2.6.4 * * @param array $kit_meta Kit `_elementor_page_settings`. * @return array{color:array,typography:array} */ private function wdkit_known_global_ids( $kit_meta ) { $known = array( 'color' => array(), 'typography' => array(), ); $sources = array( 'color' => array( 'system_colors', 'custom_colors' ), 'typography' => array( 'system_typography', 'custom_typography' ), ); foreach ( $sources as $kind => $keys ) { foreach ( $keys as $key ) { if ( empty( $kit_meta[ $key ] ) || ! is_array( $kit_meta[ $key ] ) ) { continue; } foreach ( $kit_meta[ $key ] as $entry ) { if ( ! empty( $entry['_id'] ) ) { $known[ $kind ][ $entry['_id'] ] = true; } } } } return $known; } /** * Make one incoming Plus global's colour / font references resolvable here. * * A Plus global can point at an Elementor global: the "Primary Button" entry holds * `__globals__: { text_color: "globals/colors?id=72e09b4", … }`, which The Plus Addons * turns into `var(--e-global-color-72e09b4)`. Elementor only emits that variable for ids * present in the kit, so on a site without `72e09b4` the button renders with no colour. * * Two cases, and the difference is deliberate: * * - The site ALREADY defines that id — leave the reference alone. The button then picks * up the destination's own colour, which is the point of a global. Their palette is * never read from or written to beyond this check. * - The site does NOT define it — write the captured value straight into the entry and * drop the reference, so it renders as designed. * * Nothing is ever added to the user's global colours or fonts. An earlier version injected * the missing definitions into their palette, which made the reference resolve but grew * their Site Settings by every colour an imported template happened to use. * * @since 2.6.4 * * @param array $entry One repeater entry. * @param array $refs Definitions captured with the template. * @param array $known Ids this site defines, from wdkit_known_global_ids(). * @return array Entry, with unresolvable references replaced by their values. */ private function wdkit_resolve_entry_globals( $entry, $refs, $known ) { if ( empty( $entry['__globals__'] ) || ! is_array( $entry['__globals__'] ) ) { return $entry; } foreach ( $entry['__globals__'] as $control => $ref ) { if ( ! is_string( $ref ) || false === strpos( $ref, 'id=' ) ) { continue; } if ( false !== strpos( $ref, 'globals/colors' ) ) { $kind = 'color'; } elseif ( false !== strpos( $ref, 'globals/typography' ) ) { $kind = 'typography'; } else { continue; } $id = substr( $ref, strpos( $ref, 'id=' ) + 3 ); if ( '' === $id || isset( $known[ $kind ][ $id ] ) ) { // Defined here already — their value wins. continue; } $definition = null; foreach ( ( $refs[ $kind ] ?? array() ) as $candidate ) { if ( is_array( $candidate ) && ( $candidate['_id'] ?? '' ) === $id ) { $definition = $candidate; break; } } if ( null === $definition ) { // Nothing captured for it, so leave the reference rather than blank the field. continue; } if ( 'color' === $kind ) { if ( empty( $definition['color'] ) ) { continue; } $entry[ $control ] = $definition['color']; } else { // A typography global expands into its own set of controls: the reference is // held under e.g. `typography_typography`, and each definition key replaces // that suffix — `typography_font_family`, `typography_font_weight`, and so on. foreach ( $definition as $def_key => $def_value ) { if ( '_id' === $def_key || 'title' === $def_key ) { continue; } $entry[ str_replace( 'typography_typography', $def_key, $control ) ] = $def_value; } } unset( $entry['__globals__'][ $control ] ); } return $entry; } private function wdkit_merge_tp_globals( $incoming, $refs = array() ) { if ( empty( $incoming ) || ! is_array( $incoming ) ) { return false; } $kit_id = get_option( 'elementor_active_kit' ); if ( empty( $kit_id ) ) { return false; } $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); if ( ! is_array( $kit_meta ) ) { $kit_meta = array(); } // Which global ids this site already defines. The Plus Addons turns a reference into // var(--e-global-color-<_id>), and Elementor only emits that variable for ids in the // kit — so a reference the destination does not define resolves to nothing at all. $known = $this->wdkit_known_global_ids( $kit_meta ); $changed = false; foreach ( $this->wdkit_tp_global_kit_keys() as $key ) { if ( empty( $incoming[ $key ] ) || ! is_array( $incoming[ $key ] ) ) { continue; } $existing = ( ! empty( $kit_meta[ $key ] ) && is_array( $kit_meta[ $key ] ) ) ? $kit_meta[ $key ] : array(); $seen = array(); foreach ( $existing as $entry ) { if ( ! empty( $entry['_id'] ) ) { $seen[ $entry['_id'] ] = true; } } foreach ( $incoming[ $key ] as $entry ) { if ( ! is_array( $entry ) || empty( $entry['_id'] ) || isset( $seen[ $entry['_id'] ] ) ) { continue; } // Only ever rewrite the entry being added — never one already in the kit. $existing[] = $this->wdkit_resolve_entry_globals( $entry, $refs, $known ); $seen[ $entry['_id'] ] = true; $changed = true; } $kit_meta[ $key ] = array_values( $existing ); } if ( $changed ) { update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta ); // Writing kit meta directly does not rebuild the kit stylesheet, so the // merged globals would never reach the frontend. $this->wdkit_regenerate_elementor_kit_css(); } return $changed; } protected function wdkit_get_global_val() { $builder = isset( $_POST['builder'] ) ? strtolower( sanitize_text_field( $_POST['builder'] ) ) : ''; /* Answered as-is for a builder that is neither of the two below - previously * `$response` was never set on that path and wp_send_json() read an undefined * variable (PHP warning, ClickUp 14ynqxz2tru). */ $response = array( 'message' => __( 'Unknown builder', 'wdesignkit' ), 'description' => __( 'The builder must be elementor or gutenberg.', 'wdesignkit' ), 'success' => false, ); if ( 'elementor' === $builder ) { $kit_id = get_option( 'elementor_active_kit' ); if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) { /* Elementor was installed moments ago in this import, and its kit is only * created by its own activation hook - which may not have run in a request * that loaded it (ClickUp 14ynqxz2tru). Same fallback as * wdkit_apply_site_globals_data(). */ \Elementor\Core\Kits\Manager::create_default_kit(); $kit_id = get_option( 'elementor_active_kit' ); } if ( empty( $kit_id ) ) { $response = array( 'message' => __( 'Elementor kit not found', 'wdesignkit' ), 'description' => __( 'No active Elementor kit found', 'wdesignkit' ), 'success' => false, ); wp_send_json( $response ); wp_die(); } $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); if ( empty( $kit_meta['system_colors'] ) ) { $static_meta = array( 'system_colors' => array( 0 => array( '_id' => 'primary', 'title' => 'Primary', 'color' => '#6EC1E4', ), 1 => array( '_id' => 'secondary', 'title' => 'Secondary', 'color' => '#54595F', ), 2 => array( '_id' => 'text', 'title' => 'Text', 'color' => '#7A7A7A', ), 3 => array( '_id' => 'accent', 'title' => 'Accent', 'color' => '#61CE70', ), ), 'custom_colors' => array(), 'system_typography' => array( 0 => array( '_id' => 'primary', 'title' => 'Primary', 'typography_typography' => 'custom', 'typography_font_family' => 'Roboto', 'typography_font_weight' => '600', ), 1 => array( '_id' => 'secondary', 'title' => 'Secondary', 'typography_typography' => 'custom', 'typography_font_family' => 'Roboto Slab', 'typography_font_weight' => '400', ), 2 => array( '_id' => 'text', 'title' => 'Text', 'typography_typography' => 'custom', 'typography_font_family' => 'Roboto', 'typography_font_weight' => '400', ), 3 => array( '_id' => 'accent', 'title' => 'Accent', 'typography_typography' => 'custom', 'typography_font_family' => 'Roboto', 'typography_font_weight' => '500', ), ), 'custom_typography' => array(), 'default_generic_fonts' => 'Sans-serif', 'site_name' => ! empty( get_bloginfo( 'name' ) ) ? get_bloginfo( 'name' ) : '', 'page_title_selector' => 'h1.entry-title', 'activeItemIndex' => 1, 'viewport_md' => 768, 'viewport_lg' => 1025, ); /* Fill in what is MISSING. Do not replace the kit. * * The test above is about ONE key — `system_colors`, absent on a kit Elementor * has not finished setting up — but this used to answer it with * `$kit_meta = $static_meta`, throwing away every other key the kit had. * `custom_colors` and `custom_typography` are in that set and are exactly * where a site's own global palette lives, so any site with custom Elementor * globals but no system colours lost all of them the moment this read endpoint * ran. A *read* endpoint. It is reached from the import wizard and from the * globals screen, so the loss looked like it came from importing. * * A key is filled when it is absent, or present but empty while the default * has something to offer — so a kit that already has custom colours keeps * them, a customised `page_title_selector` is not reset, and a genuinely bare * kit still comes out of here with the stock palette the UI needs to render. */ $kit_meta = is_array( $kit_meta ) ? $kit_meta : array(); $filled = false; foreach ( $static_meta as $meta_key => $meta_value ) { $missing = ! isset( $kit_meta[ $meta_key ] ) || ( empty( $kit_meta[ $meta_key ] ) && ! empty( $meta_value ) ); if ( $missing ) { $kit_meta[ $meta_key ] = $meta_value; $filled = true; } } if ( $filled ) { update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta ); } } $system_colors = ! empty( $kit_meta['system_colors'] ) ? $kit_meta['system_colors'] : array(); $custom_colors = ! empty( $kit_meta['custom_colors'] ) ? $kit_meta['custom_colors'] : array(); $system_typography = ! empty( $kit_meta['system_typography'] ) ? $kit_meta['system_typography'] : array(); $custom_typography = ! empty( $kit_meta['custom_typography'] ) ? $kit_meta['custom_typography'] : array(); $color_array = array_merge( $system_colors, $custom_colors ); $typo_array = array_merge( $system_typography, $custom_typography ); $global_data = array( 'color' => $color_array, 'typography' => $typo_array, ); $response = array( 'message' => __( 'Global data Found', 'wdesignkit' ), 'description' => __( 'Global Color and Typography found', 'wdesignkit' ), 'data' => $global_data, 'success' => true, ); } elseif ( 'gutenberg' === $builder ) { $plus_settings = get_option( 'tpgb_global_options', false ); $plus_settings = ! empty( $plus_settings ) ? json_decode( $plus_settings, true ) : json_decode( '[]' ); if ( empty( $plus_settings ) ) { $static_meta = self::wdkit_default_gutenberg_globals(); update_option( 'tpgb_global_options', json_encode( $static_meta ) ); $plus_settings = $static_meta; } $active_id = ! empty( $plus_settings['active'] ) ? $plus_settings['active'] : ''; $preset_array = ! empty( $plus_settings['presets'] ) ? $plus_settings['presets'] : array(); $act_preset = ! empty( $plus_settings['presets'][ $active_id ] ) ? $plus_settings['presets'][ $active_id ] : array(); foreach ( $act_preset['colors'] as $index => &$item ) { $item['id'] = $index + 1; } unset( $item ); foreach ( $act_preset['typography'] as $index => &$item ) { $item['id'] = $index + 1; } unset( $item ); $act_preset['color'] = $act_preset['colors']; unset( $act_preset['colors'] ); $response = array( 'message' => __( 'Global data Found', 'wdesignkit' ), 'description' => __( 'Global Color and Typography found', 'wdesignkit' ), 'data' => $act_preset, 'success' => true, ); } wp_send_json( $response ); wp_die(); } /** * * Get site settings. * * @since 2.1.3 */ protected function wdkit_get_site_setting() { $kit_id = get_option( 'elementor_active_kit' ); if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) { /* Same fallback as wdkit_get_global_val() (ClickUp 14ynqxz2tru). */ \Elementor\Core\Kits\Manager::create_default_kit(); $kit_id = get_option( 'elementor_active_kit' ); } if ( empty( $kit_id ) ) { $response = array( 'message' => __( 'Elementor kit not found', 'wdesignkit' ), 'description' => __( 'No active Elementor kit found', 'wdesignkit' ), 'success' => false, ); wp_send_json( $response ); wp_die(); } $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); $container_width = ! empty( $kit_meta['container_width'] ) ? $kit_meta['container_width'] : array(); $globals = ! empty( $kit_meta['__globals__'] ) ? $kit_meta['__globals__'] : array(); $body_background_color = ! empty( $kit_meta['body_background_color'] ) ? $kit_meta['body_background_color'] : array(); $site_globals = array( 'body_background_color' => $body_background_color, 'container_width' => $container_width, 'globals' => $globals, ); $response = array( 'message' => __( 'Global data Found', 'wdesignkit' ), 'description' => __( 'Global Color and Typography found', 'wdesignkit' ), 'data' => $site_globals, 'success' => true, ); wp_send_json( $response ); wp_die(); } /** * * update site settings. * * @since 2.1.3 */ protected function wdkit_update_site_setting() { $builder = ! empty( $_POST['builder'] ) ? sanitize_text_field( $_POST['builder'] ) : ''; $site_data = ! empty( $_POST['site_data'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['site_data'] ) ), true ) : array(); $response = $this->wdkit_apply_site_globals_data( $builder, $site_data ); wp_send_json( $response ); wp_die(); } /** * Apply a kit's site-level globals: container width, body background, `__globals__`. * * Extracted from wdkit_update_site_setting() so the PHP import runner can reach it * without `$_POST`. Direct assignment rather than a merge, so running it twice leaves * the same result — the browser behaves identically here. * * The AJAX action above is now a thin adapter over this and its response is unchanged. * * @since 2.6.5 * * @param string $builder 'elementor'|'gutenberg'. * @param array $site_data Global values collected from the kit's templates. * @return array Response array, exactly as the AJAX action used to emit. */ public function wdkit_apply_site_globals_data( $builder, $site_data ) { $builder = is_string( $builder ) ? $builder : ''; $site_data = is_array( $site_data ) ? $site_data : array(); if ( 'elementor' == $builder ) { $kit_id = get_option( 'elementor_active_kit' ); if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) { \Elementor\Core\Kits\Manager::create_default_kit(); $kit_id = get_option( 'elementor_active_kit' ); } if ( ! $kit_id ) { $response = array( 'message' => __( 'Elementor kit not found', 'wdesignkit' ), 'description' => __( 'No active Elementor kit found', 'wdesignkit' ), 'success' => false, ); return $response; } // A freshly created kit has no `_elementor_page_settings` meta yet, // so an empty result here is a valid starting point, not an error. $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); if ( ! is_array( $kit_meta ) ) { $kit_meta = array(); } $kit_meta['container_width'] = ! empty( $site_data['container_width'] ) ? $site_data['container_width'] : array(); $kit_meta['__globals__'] = ! empty( $site_data['globals'] ) ? $site_data['globals'] : array(); $kit_meta['body_background_color'] = ! empty( $site_data['body_background_color'] ) ? $site_data['body_background_color'] : array(); update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta ); // Regenerate Elementor's cached CSS. Writing the kit meta directly does // not rebuild the kit stylesheet, so the imported body background colour // and container width would otherwise never render on the frontend. $this->wdkit_regenerate_elementor_kit_css(); $response = array( 'message' => __( 'Site data Updated', 'wdesignkit' ), 'description' => __( 'Site Globals Updated', 'wdesignkit' ), 'success' => true, ); } elseif ( 'gutenberg' == $builder ) { $plus_settings = get_option( 'tpgb_global_options' ); $site_preset = json_decode( $plus_settings, true ); $site_preset['globalContainer'] = $site_data; update_option( 'tpgb_global_options', json_encode( $site_preset ) ); $response = array( 'message' => __( 'Site data Updated', 'wdesignkit' ), 'description' => __( 'Site Globals Updated', 'wdesignkit' ), 'success' => true, ); } else { $response = array( 'message' => __( 'Builder Not Found !', 'wdesignkit' ), 'description' => __( 'Template Builder not Found', 'wdesignkit' ), 'success' => true, ); } return $response; } /** * * Update Elementor Global color and Typography. * * @since 1.1.20 */ protected function wdkit_update_global_val() { $builder = ! empty( $_POST['builder'] ) ? sanitize_text_field( $_POST['builder'] ) : ''; $g_color = ! empty( $_POST['g_color'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['g_color'] ) ), true ) : array(); $g_typo = ! empty( $_POST['g_typography'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['g_typography'] ) ), true ) : array(); $new_preset = ! empty( $_POST['new_preset'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['new_preset'] ) ), true ) : array(); $response = $this->wdkit_apply_global_values_data( $builder, $g_color, $g_typo, $new_preset ); wp_send_json( $response ); wp_die(); } /** * Apply a kit's global colours and typography. * * Extracted from wdkit_update_global_val() so the PHP import runner can reach it * without `$_POST`. * * BEHAVIOUR NOTE — the Elementor branch merges with `array_merge( $new, $existing )` on * numeric-keyed lists, which appends rather than replaces. Running it twice therefore * accumulates duplicate entries. That is preserved here exactly, because it is the * shipped browser behaviour and this method backs the AJAX action. Callers that need an * idempotent apply should use Wdkit_Import_Globals::apply(), which de-duplicates by * `_id` before calling in. * * @since 2.6.5 * * @param string $builder 'elementor'|'gutenberg'. * @param array $g_color Global colour entries. * @param array $g_typo Global typography entries. * @param array $new_preset Gutenberg preset {key, ...}. * @return array Response array, exactly as the AJAX action used to emit. */ public function wdkit_apply_global_values_data( $builder, $g_color = array(), $g_typo = array(), $new_preset = array() ) { $builder = is_string( $builder ) ? $builder : ''; if ( 'elementor' == $builder ) { $g_color = is_array( $g_color ) ? $g_color : array(); $g_typo = is_array( $g_typo ) ? $g_typo : array(); // Get colors from Elementor Site Kit $kit_id = get_option( 'elementor_active_kit' ); if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) { // No kit has ever been created on this site (the option is only // ever populated by Elementor's own activation hook). Create one // via Elementor's own helper so the import has somewhere to write. \Elementor\Core\Kits\Manager::create_default_kit(); $kit_id = get_option( 'elementor_active_kit' ); } if ( ! $kit_id ) { $response = array( 'message' => __( 'Elementor kit not found', 'wdesignkit' ), 'description' => __( 'No active Elementor kit found', 'wdesignkit' ), 'success' => false, ); return $response; } // A freshly created kit has no `_elementor_page_settings` meta yet, // so an empty result here is a valid starting point, not an error. $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true ); if ( ! is_array( $kit_meta ) ) { $kit_meta = array(); } $kit_meta['custom_colors'] = array_merge( $g_color, $kit_meta['custom_colors'] ?? array() ); $kit_meta['custom_typography'] = array_merge( $g_typo, $kit_meta['custom_typography'] ?? array() ); update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta ); // Regenerate Elementor's cached CSS. Writing the kit meta directly does // not rebuild the kit stylesheet, so the imported global colours and // fonts would otherwise never render on the frontend. $this->wdkit_regenerate_elementor_kit_css(); $response = array( 'message' => __( 'Global data Updated', 'wdesignkit' ), 'description' => __( 'Global Color and Typography Updated', 'wdesignkit' ), 'success' => true, ); } elseif ( 'gutenberg' == $builder ) { $new_preset = is_array( $new_preset ) ? $new_preset : array(); $new_preset_id = ! empty( $new_preset['key'] ) ? $new_preset['key'] : ''; $plus_settings = get_option( 'tpgb_global_options' ); $site_preset = json_decode( $plus_settings, true ); $site_preset['presets'][ $new_preset_id ] = $new_preset; $site_preset['active'] = $new_preset_id; update_option( 'tpgb_global_options', json_encode( $site_preset ) ); $response = array( 'message' => __( 'Global data Updated', 'wdesignkit' ), 'description' => __( 'Global Color and Typography Updated', 'wdesignkit' ), 'success' => true, ); } else { $response = array( 'message' => __( 'Builder Not Found !', 'wdesignkit' ), 'description' => __( 'Template Builder not Found', 'wdesignkit' ), 'success' => true, ); } return $response; } /** * Regenerate Elementor's cached CSS files after the active kit's * `_elementor_page_settings` meta has been changed directly. * * Elementor renders global colours, global fonts and the body background * colour into a cached kit stylesheet. Updating the meta via * update_post_meta() does not rebuild that stylesheet, so imported site * settings never reach the frontend until the cache is cleared. This * mirrors the clear_cache() call already used by the page/section import. * * @since 2.3.2 * * @return void */ protected function wdkit_regenerate_elementor_kit_css() { if ( did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Plugin' ) ) { \Elementor\Plugin::$instance->files_manager->clear_cache(); } } /** * * Create Gutenberg page and save for re-generate css file. * * @since 1.2.3 */ protected function wdkit_update_preset() { $act_type = ! empty( $_POST['act_type'] ) ? sanitize_text_field( $_POST['act_type'] ) : ''; $post_id = ! empty( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : ''; if ( 'create' == $act_type ) { $page_id = wp_insert_post( array( 'post_title' => 'WDesignKit Gutenberg', 'post_status' => 'publish', 'post_type' => 'post', 'post_name' => sanitize_title( 'wdesignkit' ), 'post_content' => '

Add Your Heading Text Here

', 'meta_input' => array( 'gutenberg_preview' => true, '_wp_page_template' => 'default', ), ) ); if ( is_wp_error( $page_id ) || ! $page_id ) { $response = array( 'success' => true, 'message' => esc_html__( 'Page Not Found!', 'wdesignkit' ), 'description' => esc_html__( 'Page Not Found!', 'wdesignkit' ), ); wp_send_json( $response ); wp_die(); } update_post_meta( $page_id, '_edit_lock', time() . ':1' ); update_post_meta( $page_id, '_edit_last', get_current_user_id() ); $preview_url = admin_url( 'post.php?post=' . $page_id . '&action=edit' ); $response = array( 'success' => true, 'post_id' => $page_id, 'preview_url' => $preview_url, 'message' => esc_html__( 'Page Created', 'wdesignkit' ), 'description' => esc_html__( 'Page Created', 'wdesignkit' ), ); wp_send_json( $response ); wp_die(); } if ( ! empty( $post_id ) && ( $act_type == 'remove' ) ) { wp_delete_post( $post_id, true ); $response = array( 'success' => true, 'message' => esc_html__( 'post deleted', 'wdesignkit' ), 'description' => esc_html__( 'post deleted', 'wdesignkit' ), ); wp_send_json( $response ); wp_die(); } } /** * * It is For Find User Existing template List. * * @since 1.0.6 */ protected function wdkit_find_existing_template() { $array_data = array( 'search' => isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '', 'token' => isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '', 'type' => isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '', 'u_id' => isset( $_POST['u_id'] ) ? sanitize_text_field( wp_unslash( $_POST['u_id'] ) ) : '', 'builder' => isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : '', 'parpage' => isset( $_POST['parpage'] ) ? sanitize_text_field( wp_unslash( $_POST['parpage'] ) ) : 12, ); $response = $this->wkit_api_call( $array_data, 'existing_template' ); wp_send_json( $response ); wp_die(); } /** * * It is For Update User Existing template List. * * @since 1.0.6 */ protected function wdkit_update_template() { $array_data = array( 'data' => isset( $_POST['data'] ) ? wp_unslash( $_POST['data'] ) : '', 'post_id' => isset( $_POST['post_id'] ) ? sanitize_text_field( wp_unslash( $_POST['post_id'] ) ) : '', 'token' => isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '', 'type' => isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '', 'id' => isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '', 'global_data' => isset( $_POST['global_data'] ) ? wp_unslash( $_POST['global_data'] ) : array(), // 'global_font_family' => isset( $_POST['global_font_family'] ) ? wp_unslash( $_POST['global_font_family'] ) : array(), // 'global_color' => isset( $_POST['global_color'] ) ? wp_unslash( $_POST['global_color'] ) : array(), ); if ( ! empty( $array_data['post_id'] ) ) { $custom_fields = array(); $post_id = $array_data['post_id']; $meta_fields = get_post_custom( $post_id ); foreach ( $meta_fields as $key => $value ) { if ( str_contains( $key, 'nxt-' ) ) { $custom_fields[ $key ] = $value; } } if ( ! empty( $custom_fields ) ) { $data = json_decode( $array_data['data'], true ); $data['custom_meta'] = $custom_fields; $array_data['data'] = wp_json_encode( $data ); } } $array_data['remove'] = 'yes'; $response = $this->wkit_api_call( $array_data, 'existing_template' ); wp_send_json( $response ); wp_die(); } /** * * It is Use for manage favourite template. * * @since 1.0.0 */ protected function wdkit_manage_favorite() { $template_id = isset( $_POST['template_id'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['template_id'] ) ) ) : 0; $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false; if ( empty( $email ) || empty( $template_id ) ) { $response = array( 'message' => $this->e_msg_login, 'description' => $this->e_desc_login, 'success' => false, ); wp_send_json( $response ); wp_die(); } $args = $this->wdkit_parse_args( $_POST ); $args['token'] = $this->wdkit_login_user_token( $email ); unset( $args['email'] ); $response = WDesignKit_Data_Query::get_data( 'manage_favorite', $args ); wp_send_json( $response ); wp_die(); } /** * * It is Use for Check Plugin Dependency of template. * * @since 1.0.0 * @version 1.0.9 */ protected function wdkit_check_plugins_depends() { $plugins = isset( $_POST['plugins'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['plugins'] ) ) ) : array(); $update_plugin = array(); $update_theme = array(); if ( empty( $plugins ) || ! is_array( $plugins ) ) { $this->wdkit_error_msg( array( 'plugins' => 'No Plugins' ) ); } $all_plugins = $this->get_plugins(); foreach ( $plugins as $plugin ) { $pluginslug = ! empty( $plugin->plugin_slug ) ? sanitize_text_field( wp_unslash( $plugin->plugin_slug ) ) : ''; $free_pro = ! empty( $plugin->freepro ) ? sanitize_text_field( wp_unslash( $plugin->freepro ) ) : '0'; $type = ! empty( $plugin->type ) ? sanitize_text_field( wp_unslash( $plugin->type ) ) : 'plugin'; if ( is_null( $pluginslug ) ) { $plugin->status = 'warning'; $update_plugin[] = $plugin; $update_theme[] = $plugin; continue; } if ( 'plugin' === $type ) { if ( ! is_plugin_active( $pluginslug ) ) { if ( ! isset( $all_plugins[ $pluginslug ] ) ) { if ( isset( $free_pro ) && '1' === $free_pro ) { $plugin->status = 'manually'; } else { $plugin->status = 'unavailable'; } } else { $plugin->status = 'inactive'; } $update_plugin[] = $plugin; } elseif ( is_plugin_active( $pluginslug ) ) { $plugin->status = 'active'; $update_plugin[] = $plugin; } } elseif ( 'theme' === $type ) { $theme_array = array_keys( wp_get_themes() ); $theme_slug = get_stylesheet(); $parent_theme_slug = get_template(); if ( $theme_slug === $plugin->original_slug || $parent_theme_slug === $plugin->original_slug ) { $plugin->status = 'active'; } else { $theme_name = $plugin->original_slug; if ( ! in_array( $theme_name, $theme_array ) ) { if ( isset( $free_pro ) && '1' === $free_pro ) { $plugin->status = 'manually'; } else { $plugin->status = 'unavailable'; } } else { $plugin->status = 'inactive'; } } $update_theme[] = $plugin; } } $response = array( 'plugins' => $update_plugin, 'theme' => $update_theme, 'ele_container' => get_option( 'elementor_experiment-container', false ), ); $this->wdkit_success_msg( $response ); } /** * * It is Use for Install dependent plugin. * * @since 1.0.0 * @version 1.0.9 */ protected function wdkit_install_plugins_depends() { $plugins = isset( $_POST['plugins'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['plugins'] ) ), true ) : array(); $responce = $this->wdkit_install_dependency_data( $plugins ); wp_send_json( $responce ); wp_die(); } /** * Install or activate one kit dependency and describe the outcome. * * Extracted from wdkit_install_plugins_depends() so the PHP import runner can install * dependencies without `$_POST`. The plugin branch delegates to Wdkit_Depends_Installer * and the theme branch keeps its own activation path — themes are NOT installed through * the plugin installer, and routing one there would try to install a theme as a plugin. * * The AJAX action above is now a thin adapter over this and its response is unchanged. * * @since 2.6.5 * * @param array $plugins Dependency record: {type, p_id, original_slug, plugin_slug, ...}. * @return array Response array, exactly as the AJAX action used to emit. */ public function wdkit_install_dependency_data( $plugins ) { $plugins = is_array( $plugins ) ? $plugins : array(); $type = ! empty( $plugins['type'] ) ? $plugins['type'] : 'plugin'; $p_id = ! empty( $plugins['p_id'] ) ? $plugins['p_id'] : 'plugin'; $responce = ''; if ( 'plugin' === $type ) { $responce = Wdkit_Depends_Installer::get_instance()->wdkit_install_plugin( $plugins ); } elseif ( 'theme' === $type ) { $theme_name = ! empty( $plugins['original_slug'] ) ? $plugins['original_slug'] : ''; if ( ! empty( $theme_name ) ) { $theme_array = array_keys( wp_get_themes() ); $theme_slug = get_stylesheet(); if ( in_array( $theme_name, $theme_array ) ) { $activate_result = switch_theme( $theme_name ); if ( ! is_wp_error( $activate_result ) ) { $responce = array( 'message' => esc_html__( 'Theme activated successfully', 'wdesignkit' ), 'description' => esc_html__( 'Theme successfully activated', 'wdesignkit' ), 'slug' => 'nexter', 'p_id' => $p_id, 'status' => 'active', 'success' => true, ); } else { $responce = array( 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ), 'description' => $activate_result->get_error_message(), 'status' => 'inactive', 'p_id' => $p_id, 'success' => false, ); } } else { $result = $this->wdkit_install_theme_depends( $theme_name ); $message = ! empty( $result['message'] ) ? $result['message'] : esc_html__( 'Somthing Wrong', 'wdesignkit' ); $description = ! empty( $result['description'] ) ? $result['description'] : esc_html__( 'Error Somthing Wrong', 'wdesignkit' ); $status = ! empty( $result['status'] ) ? $result['status'] : esc_html__( 'inactive', 'wdesignkit' ); $success = ! empty( $result['success'] ) ? $result['success'] : false; $responce = array( 'message' => $message, 'description' => $description, 'p_id' => $p_id, 'status' => $status, 'success' => $success, ); } } else { $responce = array( 'message' => esc_html__( 'Theme Name not Found', 'wdesignkit' ), 'description' => esc_html__( 'Can Not Found Theme Name you Enterd.', 'wdesignkit' ), 'success' => false, ); } } return $responce; } /** * Install/activate every plugin and theme dependency in one request. * * wdkit_install_plugins_depends() above installs exactly one item per admin-ajax call, * which is what the browser used to drive in a sequential chain (install one, wait, * install the next) - one WP bootstrap + nonce check + network round trip per dependency * on top of the install itself. This reuses the identical per-item logic - the same * Wdkit_Depends_Installer::wdkit_install_plugin() for plugins and the same * switch_theme()/wdkit_install_theme_depends() branch for themes - just looped inside a * single PHP execution instead of one call per item. * * @since 2.7.0 */ protected function wdkit_install_plugins_depends_batch() { $items = isset( $_POST['plugins'] ) ? json_decode( wp_unslash( $_POST['plugins'] ), true ) : array(); if ( empty( $items ) || ! is_array( $items ) ) { wp_send_json( array( 'success' => false, 'message' => esc_html__( 'No plugins supplied', 'wdesignkit' ), ) ); wp_die(); } // Several downloads + unzips + activations in one request; harmless no-op where // set_time_limit() is disabled. if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 300 ); } $t_start = microtime( true ); $results = array(); foreach ( $items as $item ) { $item = (array) $item; $type = ! empty( $item['type'] ) ? $item['type'] : 'plugin'; $p_id = ! empty( $item['p_id'] ) ? $item['p_id'] : ''; if ( 'theme' === $type ) { $theme_name = ! empty( $item['original_slug'] ) ? $item['original_slug'] : ''; if ( empty( $theme_name ) ) { $results[] = array( 'p_id' => $p_id, 'success' => false, 'status' => 'inactive', 'message' => esc_html__( 'Theme name not found', 'wdesignkit' ), ); continue; } $theme_array = array_keys( wp_get_themes() ); if ( in_array( $theme_name, $theme_array, true ) ) { $activate_result = switch_theme( $theme_name ); $results[] = is_wp_error( $activate_result ) ? array( 'p_id' => $p_id, 'success' => false, 'status' => 'inactive', 'message' => $activate_result->get_error_message(), ) : array( 'p_id' => $p_id, 'success' => true, 'status' => 'active', 'message' => esc_html__( 'Theme activated successfully', 'wdesignkit' ), ); } else { $result = $this->wdkit_install_theme_depends( $theme_name ); $results[] = array( 'p_id' => $p_id, 'success' => ! empty( $result['success'] ) ? $result['success'] : false, 'status' => ! empty( $result['status'] ) ? $result['status'] : 'inactive', 'message' => ! empty( $result['message'] ) ? $result['message'] : esc_html__( 'Something went wrong', 'wdesignkit' ), ); } continue; } $results[] = Wdkit_Depends_Installer::get_instance()->wdkit_install_plugin( $item ); } self::wdkit_clear_activation_redirects(); $timing_ms = (int) round( ( microtime( true ) - $t_start ) * 1000 ); if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { error_log( 'WDKIT dependency batch timing: ' . $timing_ms . 'ms for ' . count( $results ) . ' item(s)' ); } wp_send_json( array( 'success' => true, 'results' => $results, 'timing_ms' => $timing_ms, ) ); wp_die(); } /** * Disarm the "welcome screen" redirects the plugins we just activated leave armed. * * Several plugins set a short-lived transient on activation and, on the next `admin_init`, * redirect to their own setup wizard. Rank Math's guard is the clearest example - it checks * the transient, the current page and the capability, but never wp_doing_ajax(): * * set_transient( '_rank_math_activation_redirect', 1, 30 ); // class-installer.php * $this->action( 'admin_init', 'redirect_to_welcome' ); // class-registration.php * * `admin_init` fires on admin-ajax.php too, so a wizard request landing in that window is * answered with an HTML admin page instead of its JSON, and the step that made the call * fails with nothing the user can act on. * * NOTE: this is a hardening measure, not a proven fix for any specific report. The * Global-Settings failure in QA 14ynqxyugbu still reproduces with this in place, and no * wp_redirect() is fired during it - so whatever hijacks that request does not go through * the transients below (a raw header() call would not). Clearing these remains correct on * its own terms; it is not the answer to that ticket. * * Deleting these is what an importer that activates plugins on the user's behalf has to do: * nobody asked for a setup wizard, and the user is mid-import. Each is a one-shot flag the * owning plugin deletes itself on first read, so removing it takes nothing else with it. * * The list is every such transient set by the plugins this importer installs - grep the * dependency set for `set_transient` before adding to it, rather than guessing at names. * * @since 2.7.1 * * @return void */ public static function wdkit_clear_activation_redirects() { $transients = array( '_rank_math_activation_redirect', '_wc_activation_redirect', 'elementor_activation_redirect', ); foreach ( $transients as $transient ) { delete_transient( $transient ); } } protected function wdkit_install_theme_depends( $name = 'nexter' ) { if ( ! current_user_can( 'install_themes' ) ) { $response = $this->tpae_set_response( false, 'Invalid nonce.', 'The security check failed. Please refresh the page and try again.' ); return $response; } $theme_slug = $name; $theme_api_url = 'https://api.wordpress.org/themes/info/1.0/'; // Parameters for the request $args = array( 'body' => array( 'action' => 'theme_information', 'request' => serialize( (object) array( 'slug' => $name, 'fields' => array( 'description' => false, 'sections' => false, 'rating' => true, 'ratings' => false, 'downloaded' => true, 'download_link' => true, 'last_updated' => true, 'homepage' => true, 'tags' => true, 'template' => true, 'active_installs' => false, 'parent' => false, 'versions' => false, 'screenshot_url' => true, 'active_installs' => false, ), ) ), ), ); // Make the request $response = wp_remote_post( $theme_api_url, $args ); // Check for errors if ( is_wp_error( $response ) ) { $error_message = $response->get_error_message(); $result = $this->tpae_set_response( false, 'oops', 'oops', '' ); } else { // api.wordpress.org's theme_information response is a serialized stdClass // (accessed below via ->name / ->download_link). allowed_classes => false // blocks stdClass too, turning it into an __PHP_Incomplete_Class whose // properties silently don't exist — allow only stdClass, still refusing any // other (potentially dangerous) class the payload might reference. $theme_info = unserialize( $response['body'], array( 'allowed_classes' => array( 'stdClass' ) ) ); $theme_name = $theme_info->name; $theme_zip_url = $theme_info->download_link; // SSRF guard (CWE-918): validate the resolved host before fetching the ZIP // referenced by the external theme_info response. if ( ! wdesignkit_validate_external_url( $theme_zip_url ) ) { return array( 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ), 'description' => esc_html__( 'The theme package URL is not allowed.', 'wdesignkit' ), 'status' => 'inactive', 'success' => false, ); } if ( ! function_exists( 'WP_Filesystem' ) ) { require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' ); } require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/class-wp-upgrader.php' ); require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/theme.php' ); WP_Filesystem(); $active_theme = wp_get_theme(); $theme_name = $active_theme->get( 'Name' ); // Install via WordPress core's Theme_Upgrader instead of manually fetching and // ZipArchive::extractTo()'ing the remote package: core already performs the // standard download -> unpack -> validate-package-structure -> move-into-place // flow (including cleanup on failure) used for every trusted theme install. $upgrader = new Theme_Upgrader( new Automatic_Upgrader_Skin() ); $install = $upgrader->install( $theme_zip_url ); if ( is_wp_error( $install ) || ! $install ) { return array( 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ), 'description' => is_wp_error( $install ) ? $install->get_error_message() : esc_html__( 'Theme could not be installed.', 'wdesignkit' ), 'status' => 'inactive', 'success' => false, ); } $activate_result = switch_theme( $name ); if ( ! is_wp_error( $activate_result ) ) { $response = array( 'message' => esc_html__( 'Theme activated successfully', 'wdesignkit' ), 'description' => esc_html__( 'Theme successfully activated', 'wdesignkit' ), 'status' => 'active', 'success' => true, ); } else { $response = array( 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ), 'description' => $activate_result->get_error_message(), 'status' => 'inactive', 'success' => false, ); } } return $response; } /** * * It is Use Update WDesignKit plugin latest version. * * @since 1.0.17 */ protected function wdkit_update_latest_plugin() { return Wdkit_Depends_Installer::get_instance()->wdkit_update_plugin(); } /** * * It is Use for get plugin list. * * @since 1.0.0 */ private function get_plugins() { if ( ! function_exists( 'get_plugins' ) ) { require_once \ABSPATH . 'wp-admin/includes/plugin.php'; } return get_plugins(); } /** * Get Download Template Content * * @since 1.0.0 */ protected function wdkit_activate_container() { $option_value = get_option( 'elementor_experiment-container', false ); if ( $option_value === false ) { add_option( 'elementor_experiment-container', 'active' ); } else { update_option( 'elementor_experiment-container', 'active' ); } $result = array( 'message' => esc_html__( 'Container Activated Successfully', 'wdesignkit' ), 'description' => esc_html__( 'Elementor Container Activated Successfully.', 'wdesignkit' ), 'success' => true, ); wp_send_json( $response ); wp_die(); } /** * import category and tags for post * * @since 2.0.0 */ protected function wdkit_import_taxonomy() { $category = isset( $_POST['category'] ) ? json_decode( wp_unslash( $_POST['category'] ) ) : array(); $tags = isset( $_POST['tags'] ) ? json_decode( wp_unslash( $_POST['tags'] ) ) : array(); $response = $this->wdkit_import_taxonomy_data( $category, $tags ); /* Emitted here so this action's output stays byte-for-byte what it was at * HEAD, where wp_send_json()/wp_die() sat at the end of this same method. */ wp_send_json( $response ); wp_die(); } /** * Create the categories and tags a kit's posts need, returning their term ids. * * Extracted from wdkit_import_taxonomy() so the PHP import runner can reach it without * `$_POST`. Already idempotent before this change and still is: every term is looked up * with term_exists() before an insert is attempted, so running it twice returns the * same ids rather than creating duplicates. * * The AJAX action above is now a thin adapter over this and its return value — which * the router passes to wdkit_success_msg() — is unchanged. * * @since 2.6.5 * * @param array $category Category names. * @param array $tags Tag names. * @return array {success, categories:[{name,term_id|error}], tags:[...]} */ public function wdkit_import_taxonomy_data( $category = array(), $tags = array() ) { $category = is_array( $category ) ? $category : array(); $tags = is_array( $tags ) ? $tags : array(); $response = array( 'success' => false, 'categories' => array(), 'tags' => array(), ); if ( ! empty( $category ) && count( $category ) > 0 ) { foreach ( $category as $category_name ) { $category_name = sanitize_text_field( $category_name ); $term_exists = term_exists( $category_name, 'category' ); if ( ! $term_exists ) { $result = wp_insert_term( $category_name, 'category' ); if ( ! is_wp_error( $result ) ) { $response['categories'][] = array( 'name' => $category_name, 'term_id' => $result['term_id'], ); } else { $response['categories'][] = array( 'name' => $category_name, 'error' => $result->get_error_message(), ); } } else { $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists; $response['categories'][] = array( 'name' => $category_name, 'term_id' => $term_id, ); } } $response['success'] = true; } if ( ! empty( $tags ) && count( $tags ) > 0 ) { foreach ( $tags as $tags_name ) { $tags_name = sanitize_text_field( $tags_name ); $term_exists = term_exists( $tags_name, 'post_tag' ); if ( ! $term_exists ) { $result = wp_insert_term( $tags_name, 'post_tag' ); if ( ! is_wp_error( $result ) ) { $response['tags'][] = array( 'name' => $tags_name, 'term_id' => $result['term_id'], ); } else { $response['tags'][] = array( 'name' => $tags_name, 'error' => $result->get_error_message(), ); } } else { $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists; $response['tags'][] = array( 'name' => $tags_name, 'term_id' => $term_id, ); } } $response['success'] = true; } /* Returns. It must NOT emit: the PHP runner calls this directly, and a * wp_send_json() here would print JSON and wp_die() in the middle of an * import. Found by running the runner against real WordPress -- see the * Phase 5 report. The AJAX response is unaffected, because * wdkit_import_taxonomy() emits this same array at the same point. */ return $response; } /** * Get Download Template Content * * @since 1.0.0 */ protected function wdkit_import_template() { $args = $this->wdkit_parse_args( $_POST ); $api_type = isset( $_POST['api_type'] ) ? sanitize_text_field( wp_unslash( $_POST['api_type'] ) ) : 'import_template'; $response = ''; if ( empty( $args['template_id'] ) ) { $result = array( 'content' => '', 'message' => esc_html__( 'Invalid import', 'wdesignkit' ), 'description' => esc_html__( 'Invalid import: Check your details and try again.', 'wdesignkit' ), 'success' => false, ); wp_send_json( $response ); wp_die(); } $args['token'] = $this->wdkit_login_user_token( $args['email'] ); unset( $args['email'] ); $args['unique_id'] = get_option( 'wdkit_unique_id' ) ?? ''; /* Importing one template from the library, rather than a whole kit. A sandbox has no * login for wdkit_login_user_token() to find, so without this the token is empty and * the cloud refuses - the same gap the kit paths already close. Merged last, so the * site's own identity wins over anything the request tried to supply. */ if ( function_exists( 'wdkit_kit_import_with_site_identity' ) ) { $args = wdkit_kit_import_with_site_identity( $args ); } $response = WDesignKit_Data_Query::get_data( $api_type, $args ); if ( is_wp_error( $response ) ) { wp_send_json( array( 'success' => false, 'message' => $response->get_error_message(), ) ); wp_die(); } $custom_meta = isset( $_POST['custom_meta'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_meta'] ) ) : false; /** Custom meta Field */ if ( ! empty( $custom_meta ) && 'true' === $custom_meta && ! empty( $response ) && ! empty( $response['content'] ) ) { $res_content = json_decode( $response['content'], true ); if ( isset( $res_content['custom_meta'] ) && ! empty( $res_content['custom_meta'] ) ) { $meta_data = $res_content['custom_meta']; if ( ! empty( $meta_data ) ) { foreach ( $meta_data as $meta_key => $meta_val ) { if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); } if ( get_post_meta( get_the_ID(), $meta_key, true ) === '' ) { add_post_meta( get_the_ID(), $meta_key, $meta_val[0] ); } else { update_post_meta( get_the_ID(), $meta_key, $meta_val[0] ); } } } } } /** * Fires after a template has been imported from the cloud. * * WDesignKit's templates live in the cloud, so nothing local records that an import * happened — there is no post type, no option, nothing to count after the fact. This is the * only moment the information exists. * * @since 2.6.4 * * @param string $kind 'single' or 'kit'. * @param string $builder Builder the template was imported for, e.g. 'elementor'. * @param int $count How many templates this import brought in. */ // Only a completed import counts. The cloud's failure shape for this endpoint family sets // content => 'error' (see the sibling check in wdkit_import_kit_template() above) — that is // non-empty, so the previous `||` fired the counter on failed imports too. Require success // AND an absent/non-'error' content instead. if ( ! empty( $response['success'] ) && ( ! isset( $response['content'] ) || 'error' !== $response['content'] ) ) { do_action( 'wdkit_template_imported', 'import_kit_template' === $api_type ? 'kit' : 'single', isset( $_POST['builder'] ) ? sanitize_key( wp_unslash( $_POST['builder'] ) ) : '', 1 ); } wp_send_json( $response ); wp_die(); } /** * This Function is Use For Media Import * * @since 1.0.0 * * @param array $content store media content. * @param string $editor it is check editor. */ /** * Resolve a local upload URL back to its attachment ID. * * Handles the "-scaled" copy WordPress makes for large originals and any * "-1920x1280" size suffix, both of which attachment_url_to_postid() misses because * they are not the value stored in _wp_attached_file. * * @since 2.6.2 * * @param string $url Local upload URL. * @return int Attachment ID, or 0. */ private static function wdkit_attachment_id_from_url( $url ) { static $cache = array(); if ( isset( $cache[ $url ] ) ) { return $cache[ $url ]; } $id = (int) attachment_url_to_postid( $url ); if ( ! $id ) { // Try the original file behind a -scaled or -WxH derivative. $stripped = preg_replace( '/-scaled(\.[a-z0-9]+)$/i', '$1', $url ); $stripped = preg_replace( '/-\d+x\d+(\.[a-z0-9]+)$/i', '$1', (string) $stripped ); if ( $stripped && $stripped !== $url ) { $id = (int) attachment_url_to_postid( $stripped ); } } // Only remember hits. Page imports run concurrently, so an attachment created by a // sibling request may not exist yet when this is first asked — caching that miss // would keep every later control in this request pointing at nothing. if ( $id ) { $cache[ $url ] = $id; } return $id; } /** * Is this media reference still pointing off-site? * * Template content arrives holding the URLs of wherever the media lived before. Those * carry that site's attachment IDs, which have no meaning here - and can collide with * unrelated local posts. * * @since 2.6.2 * * @param string $url URL from a media control. * @return bool True when the URL points at another site's uploads. */ /** * Localise every foreign SVG media control in a set of Elementor elements, in-request. * * The deferred path cannot carry SVGs (see wdkit_collect_foreign_media_urls()), so they * take the same route they always did: wdkit_localise_media_url(), which resolves through * Elementor's own importer and its `_elementor_source_image_hash` cache, so an SVG a * sibling page already imported costs a database lookup rather than a download. * * Only the `{ url, id }` media-control shape is handled here, matching * wdkit_repair_attachment_ids(). An SVG inlined into an HTML/text control has no `id` to * correct, and the finalize sweep still covers those - which is why the sweep refuses to * skip any page that still holds a foreign SVG reference. * * @since 2.7.1 * * @param mixed $node Elementor data, walked recursively. * @return mixed The data with local SVG references. */ private static function wdkit_localise_foreign_svgs( $node ) { if ( ! is_array( $node ) ) { return $node; } if ( isset( $node['url'] ) && is_string( $node['url'] ) && array_key_exists( 'id', $node ) && self::wdkit_is_foreign_media_url( $node['url'] ) && preg_match( '/\.svg$/i', (string) wp_parse_url( $node['url'], PHP_URL_PATH ) ) ) { $local = self::wdkit_localise_media_url( $node['url'], isset( $node['id'] ) ? (int) $node['id'] : 0 ); if ( ! empty( $local['id'] ) && ! empty( $local['url'] ) ) { $node['id'] = $local['id']; $node['url'] = $local['url']; } } foreach ( $node as $key => $value ) { if ( is_array( $value ) ) { $node[ $key ] = self::wdkit_localise_foreign_svgs( $value ); } } return $node; } /** * Empty the attachment ID on every media control whose image is being deferred. * * The IDs that arrive in a template's data belong to the site the kit was authored on. * Until the background pass replaces them (see wdkit_replace_deferred_media()), each one * lands in one of two states on this site, and neither is what Elementor expects: * * - It matches nothing locally. Elementor's `` path copes - it checks * wp_attachment_is_image() and falls back to the url - but the CSS path does not. * Control_Media::get_style_value() only takes the url branch when the ID is *empty*; * given a non-empty one it returns wp_get_attachment_image_url( $id ), which is false * for a dangling ID, so the declaration is dropped and a container background renders * as nothing at all. Measured on a Taj Bakery import: 9 of 9 background_image controls * produced 0 background-image rules for the whole deferred window. * - It happens to match an unrelated local attachment. Then wp_attachment_is_image() * passes and Elementor renders that other image - the wrong picture, silently. Whether * this bites is pure luck: it needs the template's ID range to overlap the IDs a fresh * import creates, which is why Zion hit it and Taj Bakery did not. * * An empty ID is the one state every affected renderer is written to handle, so this * turns both failures into "shows the source image until the local copy lands": * * - Control_Media::get_style_value() empty ID -> parent -> the raw url. * - Group_Control_Image_Size::get_attachment_image_html() empty ID -> url fallback. * - image-carousel `if ( ! $image_url && isset( $attachment['url'] ) )`. * - image-gallery `$image['url'] ?? ''`. * * So this is deliberately not a blanket "clear every ID": it keys off the exact URL list * that was just handed to the background pass. Anything already local keeps its ID, and * so do SVGs, which wdkit_localise_foreign_svgs() has already imported for real. * * One renderer is not helped by this: The Plus Addons' tp_get_image() returns '' when * wp_get_attachment_image_src() fails and has no url fallback, so its widgets show * nothing during the window either way. That is a limitation of that helper rather than * something this pass can fix, and the background pass repairs it on completion. * * @since 2.6.6 * * @param mixed $node Elementor data, walked recursively. * @param array $deferred Source URLs handed to the background pass. * @return mixed Data with the deferred controls' IDs emptied. */ private static function wdkit_blank_deferred_media_ids( $node, array $deferred ) { if ( ! is_array( $node ) || empty( $deferred ) ) { return $node; } $lookup = array_flip( $deferred ); $walk = function ( $item ) use ( &$walk, $lookup ) { if ( ! is_array( $item ) ) { return $item; } // A media control: `url` and `id` as siblings. Empty the ID only when this // exact URL is one the background pass is going to replace. if ( isset( $item['url'] ) && is_string( $item['url'] ) && array_key_exists( 'id', $item ) && isset( $lookup[ $item['url'] ] ) ) { // '' rather than 0: this is the value Elementor itself writes when it // rejects an ID (see get_attachment_image_html()), and what its // empty()-based checks are written against. $item['id'] = ''; } foreach ( $item as $key => $value ) { if ( is_array( $value ) ) { $item[ $key ] = $walk( $value ); } } return $item; }; return $walk( $node ); } /** * Build the demo-URL -> real-permalink substitution table for a finished kit import. * * The kit's templates link to each other by the *authoring* site's URLs, so every * internal link has to be rewritten once each page exists and its real permalink is * known. Two representations of the same URL have to be covered, because the two places * these links live store them differently: * * - `post_content` holds them raw, inside an attribute: `href="https://demo/about/"`. * - `_elementor_data` holds them JSON-encoded, so every slash is escaped: * `"url":"https:\/\/demo\/about\/"`. This is the one the widgets actually render from. * * Each key is anchored on the surrounding quote so `/about` cannot match inside * `/about-us`, and both the bare and trailing-slash forms are mapped because templates * are inconsistent about it. The trailing slash has to be escaped in the escaped * variants (`...\/about\/"`, not `...\/about/"`) - getting that wrong is invisible in * post_content and leaves the whole navigation pointing at the demo site. * * @since 2.6.6 * * @param array $imported Inserted templates: each entry needs `post_url` (the demo URL) * and `id` (the local post it became). * @return array strtr() substitution table. */ private static function wdkit_build_nav_url_map( $imported ) { $url_map = array(); foreach ( (array) $imported as $entry ) { $entry = (array) $entry; if ( empty( $entry['post_url'] ) || empty( $entry['id'] ) ) { continue; } $permalink = get_permalink( (int) $entry['id'] ); if ( ! $permalink ) { continue; } $demo = rtrim( (string) $entry['post_url'], '/' ); $target = rtrim( (string) $permalink, '/' ); if ( '' === $demo || $demo === $target ) { continue; } // On a site left on plain permalinks the target is a query string, not a path: // `http://site/?page_id=54`. Re-adding the trailing slash the demo URL carried // yields `?page_id=54/`, and joining a query string onto it with `?` yields a // second `?` - both malformed. This stayed invisible on a pretty-permalink site // because there the target really is a path and appending is correct. $has_query = ( false !== strpos( $target, '?' ) ); // The "…and a trailing slash" form of the target, which for a query-string // permalink is simply the permalink - there is nothing to put a slash after. $target_slash = $has_query ? $target : $target . '/'; // What a query string has to be joined on, given what the target already carries. $query_join = $has_query ? '&' : '?'; // Raw form, as it appears in post_content. foreach ( array( '"', '\\"' ) as $q ) { $url_map[ $q . $demo . $q ] = $q . $target . $q; $url_map[ $q . $demo . '/' . $q ] = $q . $target_slash . $q; } // JSON-escaped form, as it appears in _elementor_data - trailing slash escaped too. $demo_esc = str_replace( '/', '\\/', $demo ); $target_esc = str_replace( '/', '\\/', $target ); $target_slash_esc = $has_query ? $target_esc : $target_esc . '\\/'; $target_slash_raw = $has_query ? $target_esc : $target_esc . '/'; foreach ( array( '"', '\\"' ) as $q ) { $url_map[ $q . $demo_esc . $q ] = $q . $target_esc . $q; $url_map[ $q . $demo_esc . '\\/' . $q ] = $q . $target_slash_esc . $q; // Kept for templates that stored the slash unescaped next to escaped ones. $url_map[ $q . $demo_esc . '/' . $q ] = $q . $target_slash_raw . $q; } // An in-page link (`/taj/#menu-list`) or one carrying a query string never has the // quote straight after the path, so none of the anchored keys above can reach it and // the link keeps pointing at the demo site. `#` and `?` cannot occur inside a path // segment, so they anchor the match just as safely as the closing quote does. foreach ( array( '#', '?' ) as $sep ) { $join = ( '?' === $sep ) ? $query_join : $sep; $url_map[ $demo . $sep ] = $target . $join; $url_map[ $demo . '/' . $sep ] = $target_slash . $join; $url_map[ $demo_esc . $sep ] = $target_esc . $join; $url_map[ $demo_esc . '\\/' . $sep ] = $target_slash_esc . $join; } } return $url_map; } /** * Every source-site media URL inside a set of Elementor elements. * * Scans the serialised tree rather than walking control-by-control, for the same reason * the Gutenberg SVG prefetch does: a URL can sit in a media control's `url`, in a * responsive variant beneath it, or inline in an HTML/text control that never had an * `id` sibling - and wdkit_replace_deferred_media() rewrites all three shapes, the last * by plain substring swap. Matching the serialised form keeps this collector and that * rewriter looking at the same set. Over-collecting is harmless: the cron skips a URL it * cannot resolve, and an already-local one is filtered out below. * * SVGs are excluded by default, and deliberately: the background pass runs with no user, * where `svg` is not in get_allowed_mime_types() because the filter granting it is * capability-gated - so a deferred SVG sideload fails silently and the reference stays * remote forever. The Gutenberg walk carves SVGs out of deferral for the same reason * (see the `/\.svg$/i` test in Wdkit_Import_Images::wdkit_Import_media()); they are * localised inline instead, by wdkit_localise_foreign_svgs() below. * * Video IS included, and does not share that problem: mp4/m4v, webm and mov/qt are all in * get_allowed_mime_types() with no user, so a deferred video sideload succeeds where an * SVG one cannot. Deferral is also where video belongs - the files are the largest thing * a kit references, and nothing is waiting on the cron event. Until this listed them, a * template's video kept the authoring site's URL and attachment id forever, because the * deferred path has no other media discovery (see wdkit_media_import()'s elementor * branch) - which is why a video widget rendered as an empty frame. * * @since 2.7.1 * * @param mixed $elements Elementor `elements` data. * @param string|null $extensions Regex alternation of extensions; defaults to the shared list. * @return array Distinct foreign media URLs. */ private static function wdkit_collect_foreign_media_urls( $elements, $extensions = null ) { if ( ! class_exists( 'Wdkit_Image_Guard' ) ) { require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php'; } // One list for defer / blank-id / remap - see Wdkit_Image_Guard::DEFERRABLE_EXTENSIONS. $extensions = ( null === $extensions ) ? Wdkit_Image_Guard::DEFERRABLE_EXTENSIONS : $extensions; $json = wp_json_encode( $elements, JSON_UNESCAPED_SLASHES ); if ( ! is_string( $json ) || '' === $json ) { return array(); } // The trailing query group is required, not cosmetic. Without it the engine matches // greedily and then backtracks to the extension, so // `…/photo.jpeg?auto=compress&w=1260` was collected as `…/photo.jpeg` - a URL that // appears nowhere in the content. wdkit_blank_deferred_media_ids() compares the node's // url exactly and missed, leaving the dangling foreign id it exists to clear (the // measured symptom: 9 of 9 background_image controls produced no background-image // rule), and the cron's str_replace then produced `…/local.jpg?auto=compress&w=1260`. $candidates = array(); if ( preg_match_all( '#https?://[^"\'\s<>()]+\.(?:' . $extensions . ')(?:\?[^"\'\s<>()]*)?#i', $json, $matches ) ) { $candidates = $matches[0]; } // The regex above can only find a URL that spells its type. A media control whose url // has no extension at all - which the template CDN serves, resolving the type by // Content-Type - matched nothing, so it was never deferred, never recorded and never // localised: the reference stayed remote permanently, with no error anywhere. // // Walking the tree settles it without a single HTTP request: a node shaped // `{ url, id }` IS a media control, whatever the url looks like. This is the same // shape wdkit_blank_deferred_media_ids() and wdkit_replace_deferred_media() key on, // so what gets collected here is exactly what those two can act on. The regex pass // stays for URLs embedded in HTML and inline-CSS strings, which have no node shape. $structural = self::wdkit_collect_media_control_urls( $elements ); $found = array(); foreach ( array_unique( $candidates ) as $url ) { if ( self::wdkit_is_foreign_media_url( $url ) ) { $found[] = $url; } } // No extension requirement for these - the node shape is the proof. foreach ( array_unique( $structural ) as $url ) { if ( self::wdkit_is_foreign_media_url( $url, false ) ) { $found[] = $url; } } return array_values( array_unique( $found ) ); } /** * Collect the url of every `{ url, id }` media control in an element tree. * * Structural, not textual: no extension is required, so an extensionless CDN URL is * found here even though wdkit_collect_foreign_media_urls()'s regex cannot see it. * * SVG is still excluded, for the reason given on that method: the background pass runs * with no user, `svg` is therefore absent from get_allowed_mime_types(), and a deferred * SVG sideload fails silently. Those are localised in-request instead. * * @since 2.7.1 * * @param mixed $node Elementor data, walked recursively. * @return array Media-control URLs, in tree order. */ private static function wdkit_collect_media_control_urls( $node ) { if ( ! is_array( $node ) ) { return array(); } $found = array(); // `url` + `id` as siblings is Elementor's media-control shape. A link control carries // is_external/nofollow instead and never an id, so excluding those keys keeps a // button's href out of the media queue even if a theme adds an id to it. $is_media_control = isset( $node['url'] ) && is_string( $node['url'] ) && array_key_exists( 'id', $node ) && ! array_key_exists( 'is_external', $node ) && ! array_key_exists( 'nofollow', $node ); if ( $is_media_control ) { $url = $node['url']; $path = (string) wp_parse_url( $url, PHP_URL_PATH ); if ( 0 === strpos( $url, 'http' ) && ! preg_match( '/\.svg$/i', $path ) ) { $found[] = $url; } } foreach ( $node as $value ) { if ( is_array( $value ) ) { $found = array_merge( $found, self::wdkit_collect_media_control_urls( $value ) ); } } return $found; } /** * Whether a media URL points at something this site does not already host. * * Anything under the uploads baseurl is ours; everything else - the template CDN, the * authoring site, a stock provider - is foreign and has to be localised. Data URIs and * relative paths are not foreign either, and are refused here rather than downstream. * * @since 2.7.1 * * @param string $url Absolute media URL. * @return bool True when the URL needs localising. */ private static function wdkit_is_foreign_media_url( $url, $require_extension = true ) { if ( ! class_exists( 'Wdkit_Image_Guard' ) ) { require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php'; } $uploads = wp_get_upload_dir(); return Wdkit_Image_Guard::is_foreign_media( $url, isset( $uploads['baseurl'] ) ? $uploads['baseurl'] : '', $require_extension ); } /** * Find - or make - the local attachment behind a source-site media URL. * * Elementor stamps every image it imports with `_elementor_source_image_hash` * (sha1 of the URL it came from), and its importer consults that before doing any * network work. Delegating here means a URL already imported at create time resolves * from the database, and one that never made it is fetched exactly once. * * Only ever called for foreign URLs. Handing it a local URL would re-download the * file and leave a duplicate, because the stored hash is of the *remote* URL and so * would never match. * * @since 2.6.2 * * @param string $url Source-site media URL. * @param int $source_id The source site's attachment ID, used as Elementor's cache key. * @return array Local `id` and `url`, or an empty array when it cannot be resolved. */ private static function wdkit_localise_media_url( $url, $source_id = 0 ) { static $cache = array(); if ( isset( $cache[ $url ] ) ) { return $cache[ $url ]; } // SVGs: use the prefetch-aware sideloader. It consults the concurrent // warm-up cache (see the prefetch_remote_files() call in the Elementor // $defer_media branch of wdkit_media_import()); Elementor's own importer // below always makes its own blocking round trip and never looks at it. // Fall through on any miss/failure so behaviour is unchanged when the // warm-up did not run or the file could not be fetched. if ( preg_match( '/\.svg$/i', (string) wp_parse_url( $url, PHP_URL_PATH ) ) ) { self::wdkit_require_import_images(); $warm = Wdkit_Import_Images::wdkit_Import_media( array( 'id' => (int) $source_id, 'url' => $url, ) ); if ( is_array( $warm ) && ! empty( $warm['id'] ) && ! empty( $warm['url'] ) && ! self::wdkit_is_foreign_media_url( $warm['url'] ) && self::wdkit_is_usable_attachment( (int) $warm['id'] ) ) { $cache[ $url ] = array( 'id' => (int) $warm['id'], 'url' => $warm['url'], ); return $cache[ $url ]; } } if ( ! did_action( 'elementor/loaded' ) || ! class_exists( '\\Elementor\\Plugin' ) ) { return array(); } $images = \Elementor\Plugin::$instance->templates_manager->get_import_images_instance(); if ( ! $images ) { return array(); } // A download may happen, so keep the oversized-image guard in force. self::wdkit_guard_oversized_images(); // Elementor only consults its hash table when an id is present - but it also // memoises the result per source id in Import_Images::$_replace_image_ids, and that // memo is checked BEFORE the url is looked at. So a shared placeholder is not a // harmless "any non-zero value": the first url imported under it wins, and every // later url handed the same placeholder is silently answered with that first // attachment. Measured on a Zion import: one image served 10 of the page's media // controls and all 3 of its background rules, while the correct files sat in the // library unreferenced. // // A stable, url-derived stand-in keeps the hash-table path enabled while giving each // distinct url its own memo slot. crc32 is used for a compact positive integer, not // for collision resistance - a clash would only cost a redundant lookup, and the // per-url cache above already prevents repeats within a request. $probe_id = $source_id ? (int) $source_id : ( crc32( $url ) & 0x7FFFFFFF ); self::wdkit_watch_elementor_attachments(); $imported = $images->import( array( 'id' => $probe_id, 'url' => $url, ) ); // Elementor answers this from its own `_elementor_source_image_hash` index before it // downloads anything, so it hands back whatever attachment claimed that hash - a // record with no file included. Same reason the deferred lookup is gated: writing // that id into the page is what leaves a control resolving to nothing. The stale // hash is dropped on the way out so the next attempt is a miss and downloads. $local = array(); if ( ! empty( $imported['id'] ) && ! empty( $imported['url'] ) ) { if ( self::wdkit_is_usable_attachment( $imported['id'] ) ) { $local = array( 'id' => (int) $imported['id'], 'url' => $imported['url'], ); } else { self::wdkit_forget_reuse_keys( $imported['id'] ); } } // Drained inline as well as on shutdown: Elementor has finished writing this row's // metadata by the time import() returns, so it is safe to remove now, and removing it // now keeps a stub from being visible to the rest of the import. self::wdkit_sweep_stub_attachments(); // Remember hits only: a sibling request importing concurrently may simply not have // finished yet, and caching that miss would strand every later control on this page. if ( $local ) { $cache[ $url ] = $local; } return $local; } /** * Whether a page still references an SVG on the template's own site. * * @since 2.7.1 * * @param int $post_id Page to inspect. * @return bool True when a foreign SVG reference remains. */ private static function wdkit_page_has_foreign_svg( $post_id ) { $raw = get_post_meta( (int) $post_id, '_elementor_data', true ); if ( empty( $raw ) ) { return false; } $data = is_array( $raw ) ? $raw : json_decode( $raw, true ); if ( ! is_array( $data ) ) { return false; } return ! empty( self::wdkit_collect_foreign_media_urls( $data, 'svg' ) ); } /** * Post IDs that still have a deferred-media event waiting in wp-cron. * * Read off the cron array rather than tracked separately, so it cannot drift from what * wkit_schedule_deferred_media_sync() actually queued. * * @since 2.7.1 * * @return array Post IDs with a pending sideload event. */ private static function wdkit_pages_awaiting_deferred_media() { $crons = _get_cron_array(); if ( ! is_array( $crons ) ) { return array(); } $pending = array(); foreach ( $crons as $events ) { if ( empty( $events['wdkit_async_sideload_page_images'] ) || ! is_array( $events['wdkit_async_sideload_page_images'] ) ) { continue; } foreach ( $events['wdkit_async_sideload_page_images'] as $event ) { // args[0] is the post id - see wkit_schedule_deferred_media_sync(). if ( isset( $event['args'][0] ) ) { $pending[] = (int) $event['args'][0]; } } } return array_unique( $pending ); } /** * Repair dangling attachment IDs across every page of a finished import. * * The create-time repair in wdkit_media_import() can only see attachments that already * exist. Pages import concurrently and share images — an icon first imported by one * page is referenced by several others — so a page that runs early legitimately cannot * resolve an image a sibling request has not created yet. * * This runs at the finalize step, once every page and attachment exists, and fixes * whatever the per-page pass had to leave behind. * * @since 2.6.2 * * @param array $page_ids Imported post IDs. * @param bool $local_only Skip the download-triggering branch of * wdkit_repair_attachment_ids() and only fix dangling ids on * already-local media. The PHP runner passes true here — its * site_settings step (stage_setup) runs before * schedule_media_sweep() (stage_finalize) ever queues anything, * so wdkit_pages_awaiting_deferred_media() below is always empty * for it and every still-remote image used to be downloaded * synchronously right here instead of by the deferred pass - see * ClickUp 14ynqxywncc. The browser's own AJAX call already waits * for scheduling first, so it is unaffected by this defaulting to * false. * @return int Number of pages actually rewritten. */ private function wdkit_sweep_attachment_ids( $page_ids, $local_only = false ) { if ( empty( $page_ids ) || ! did_action( 'elementor/loaded' ) ) { return 0; } $fixed = 0; $ids = array_unique( array_map( 'intval', $page_ids ) ); // Leave alone any page whose media is already queued for the background pass. // // The repair walk itself is trivial - 15ms across 18 pages. The cost is that // wdkit_repair_attachment_ids() resolves foreign URLs through // wdkit_localise_media_url(), which downloads anything not already local. For an // Elementor kit that quietly made this sweep the media importer, inside the import // request, at 156s-225s. wdkit_async_sideload_page_images() does the same work per // page - rewriting _elementor_data (url and id), clearing _elementor_css, flushing // the files manager - but off the request, and only once that page's images exist. // // Safe only because the queue is genuinely populated for both builders now: the // Elementor branch of wdkit_media_import() reports what it deferred (see // wdkit_collect_foreign_media_urls()), and the client awaits the scheduling call // before this runs. Pages with nothing queued still sweep exactly as before. $queued = self::wdkit_pages_awaiting_deferred_media(); if ( ! empty( $queued ) ) { foreach ( $queued as $queued_id ) { // A queued page is only safe to skip once nothing on it still needs // in-request work. The deferred list is raster-only, and an SVG inlined into // an HTML control has no `id` for wdkit_localise_foreign_svgs() to correct - // so if any foreign SVG reference survives, this page still needs the sweep. if ( self::wdkit_page_has_foreign_svg( $queued_id ) ) { continue; } $ids = array_values( array_diff( $ids, array( $queued_id ) ) ); } if ( empty( $ids ) ) { return 0; } } foreach ( $ids as $post_id ) { if ( ! $post_id ) { continue; } $raw = get_post_meta( $post_id, '_elementor_data', true ); if ( empty( $raw ) ) { continue; } $data = is_array( $raw ) ? $raw : json_decode( $raw, true ); if ( ! is_array( $data ) ) { continue; } $repaired = self::wdkit_repair_attachment_ids( $data, $local_only ); if ( wp_json_encode( $repaired ) === wp_json_encode( $data ) ) { continue; } // Save through the document API so Elementor regenerates the page CSS — the // background-image rules are only emitted once the IDs resolve. $document = \Elementor\Plugin::$instance->documents->get( $post_id ); // Count only a save that actually happened. Document::save() returns false // without saving when the current user cannot edit the post, and reporting // those as repaired hides the fact that nothing changed. if ( $document && $document->save( array( 'elements' => $repaired ) ) ) { ++$fixed; } } // No cache flush here on purpose. Document::save() above already deletes the css // file, _elementor_css, and the element cache for each page it saved - see // wdkit_invalidate_elementor_page_cache() for why the site-wide // files_manager->clear_cache() that used to follow this loop is the wrong tool. return $fixed; } /** * Repair media controls whose attachment ID does not resolve. * * Elementor media controls store `{ url, id }`. Controls flagged `has_sizes` — the * container/section **background image** among them — do not render from `url` at all: * CSS generation resolves the image through the attachment ID, so a dangling ID * produces no `background-image` rule and the section renders with no image even * though its URL is perfectly correct. * * IDs arrive dangling whenever Elementor's own importer does not rewrite a control — * it carries the source site's ID, which means nothing locally. Now that the URL is * already a local upload before import, the ID can simply be looked up from it. * * @since 2.6.2 * * @param mixed $node Elementor data, walked recursively. * @return mixed Data with resolvable attachment IDs. */ /** * Is this URL a video the background pass owns? * * @since 2.7.1 * * @param string $url Media URL. * @return bool */ private static function wdkit_is_video_url( $url ) { if ( ! class_exists( 'Wdkit_Image_Guard' ) ) { require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php'; } return (bool) preg_match( '/\.(?:' . Wdkit_Image_Guard::VIDEO_EXTENSIONS . ')$/i', (string) wp_parse_url( (string) $url, PHP_URL_PATH ) ); } private static function wdkit_repair_attachment_ids( $node, $local_only = false ) { if ( ! is_array( $node ) ) { return $node; } // A media control value: has a url, and an id slot to correct. if ( isset( $node['url'] ) && is_string( $node['url'] ) && array_key_exists( 'id', $node ) ) { $current = (int) $node['id']; if ( self::wdkit_is_foreign_media_url( $node['url'] ) ) { // Still pointing at the source site. Ask Elementor for the local copy: its // _elementor_source_image_hash lookup returns the attachment the create-time // import already made, so this normally costs a single query and no download. // // Skipped entirely in $local_only mode: that caller is the deferred pass, // which owns the decision about what gets downloaded and when, and must not // have a repair walk start fetching things behind it. // // Video is skipped for the same reason in every mode. It is localised only by // the background pass, so a still-foreign video URL here is not a fault to // repair - it is work that has not run yet. Worse, sending one through // wdkit_localise_media_url() actively corrupts it: that helper hands // Elementor `id => 1` whenever the control's id is empty, and Elementor // caches its import results *by that source id*, so every blanked-id control // collides on key 1 and gets handed back whichever attachment claimed it // first. A video, whose id the deferred pass deliberately blanks and whose // URL stays foreign until the cron runs, is the one control that meets both // conditions - and it came back pointing at an unrelated PNG. if ( ! $local_only && ! self::wdkit_is_video_url( $node['url'] ) ) { $local = self::wdkit_localise_media_url( $node['url'], $current ); if ( ! empty( $local['id'] ) && ! empty( $local['url'] ) ) { $node['id'] = $local['id']; $node['url'] = $local['url']; } } } elseif ( false !== strpos( $node['url'], '/wp-content/uploads/' ) ) { // The URL is already local, so the attachment it names is the truth and the // id beside it may not be. Two ways it goes wrong, and both look the same // from here: the id is the authoring site's and matches nothing locally, or // it happens to match some unrelated attachment. Either way the URL wins. // // This matters most for a container background: Control_Media::get_style_value() // only falls back to the url when the id is *empty*, so a non-empty id that // resolves to nothing makes wp_get_attachment_image_url() return false and the // whole background-image declaration is dropped - the image simply vanishes. // An image widget survives it (get_attachment_image_html() clears an id that // fails wp_attachment_is_image() and renders from the url), which is why this // showed up as "some backgrounds missing" rather than as broken images. // // Only a real change is written: when the URL resolves to the id that is // already there, the node is left exactly as it was. $resolved = self::wdkit_attachment_id_from_url( $node['url'] ); // Held to the same bar as every other id this class writes. A local upload url // can still name a record with nothing behind it - that is the whole subject // of this repair - and writing its id back would reintroduce exactly the // dangling-id symptom this branch exists to remove. On a reject the node keeps // the id it arrived with, which is no worse than before. if ( $resolved && $resolved !== $current && self::wdkit_is_usable_attachment( $resolved ) ) { $node['id'] = $resolved; } } } foreach ( $node as $key => $value ) { if ( is_array( $value ) ) { $node[ $key ] = self::wdkit_repair_attachment_ids( $value, $local_only ); } } return $node; } public function wdkit_media_import( $content = array(), $editor = '', $defer_media = false ) { if ( empty( $content ) && empty( $editor ) ) { $args = $this->wdkit_parse_args( $_POST ); $content = ! empty( $args['content'] ) ? json_decode( $args['content'], true ) : array(); } else { $args = array( 'content' => $content, 'editor' => $editor, ); $content = ! empty( $args['content'] ) ? $args['content'] : array(); if ( 'elementor' === $args['editor'] ) { $content = json_decode( $content, true ); } } if ( ! class_exists( 'Wdkit_Import_Images' ) ) { require_once WDKIT_INCLUDES . 'admin/class-wdkit-import-images.php'; } if ( ! empty( $args['editor'] ) && 'gutenberg' === $args['editor'] && ! empty( $content ) ) { // parse_blocks() leaves a filler entry (blockName null/empty) for every stretch of // whitespace between real blocks. blocks_import_media_copy_content() below always // dropped these before recursing - but WDKIT_Nexter_Block_Processor::run() (which // runs unconditionally right after this, in import_page_section_content()) keys // each block's generated CSS/id off its position in this array, so leaving them in // shifts every block after the first and the page renders unstyled/mismatched. // This is a plain array filter, no network/media work, so it must run regardless // of $defer_media - only the actual per-block media copy below is skippable. if ( is_array( $content ) ) { foreach ( $content as $key => $val ) { if ( is_array( $val ) && array_key_exists( 'blockName', $val ) && ( empty( $val['blockName'] ) || null === $val['blockName'] || ' ' === $val['blockName'] ) ) { unset( $content[ $key ] ); } } } // Same contract as the Elementor branch below: the caller already collected this // content's image_urls from the template response and will hand them to // wkit_schedule_deferred_media_sync() once the page exists, so raster images can // stay pointed at the source CDN and be localised later, off this request, via // wdkit_async_sideload_page_images(). SVGs are excluded from that deferral (see // Wdkit_Import_Images::wdkit_Import_media()) and still import synchronously here - // the walk itself is cheap, so passing $defer_media through and letting the leaf // function decide per-image costs nothing extra when not deferring. // Warm every SVG this walk is about to import, concurrently, before the walk // starts. The walk itself is strictly one node at a time, so without this each // SVG pays its own blocking round trip to the template CDN - measured at ~44s // for the 65 SVGs in a 13-template kit, virtually all of it connection setup // and latency rather than work. SVGs specifically because they are the only // media still imported synchronously once $defer_media is on (see the carve-out // in Wdkit_Import_Images::wdkit_Import_media()), and because they are small // enough to hold in memory safely. // // Scanning the serialized content rather than pre-walking the tree keeps this // from having to duplicate - and drift from - the walk's own URL detection. // Over-collecting is harmless (an unused warm entry is just dropped) and // under-collecting only costs the original sequential fetch. $svg_scan = wp_json_encode( $content, JSON_UNESCAPED_SLASHES ); if ( is_string( $svg_scan ) && preg_match_all( '#https?://[^"\'\s<>()\\\\]+\.svg(?:\?[^"\'\s<>()\\\\]*)?#i', $svg_scan, $svg_matches ) ) { Wdkit_Import_Images::prefetch_remote_files( array_unique( $svg_matches[0] ) ); } // Snapshot before the walk so the ids cleared below are this content's alone. // peek_deferred_urls() returns the whole request-wide static list, so on a route // that imports more than one item, an earlier item's URLs were still in it - and // any of them that happened to appear on this page had its id zeroed here with // no event of its own coming to put the id back. self::wdkit_require_import_images(); $deferred_before = Wdkit_Import_Images::peek_deferred_urls(); $media_import = array( $content ); $media_import = self::blocks_import_media_copy_content( $media_import, $defer_media ); $content = $media_import[0]; // Blocks that rebuild their image from attributes on every render read the // attachment id ahead of the url, so the foreign id the template shipped hides // the still-remote url that would have rendered perfectly well. Zero those ids // for exactly the images the walk above chose to defer, so the page renders from // the source CDN until wdkit_async_sideload_page_images() localises it and // wdkit_remap_attachment_ids() writes the real id back. $deferred_here = array_values( array_diff( Wdkit_Import_Images::peek_deferred_urls(), $deferred_before ) ); $content = self::wdkit_clear_deferred_attachment_ids( $content, $deferred_here ); // The walk above only visits block ATTRIBUTES. A block that ships rendered markup // - tpgb/tp-testimonials is one - carries its images as `` inside its own // innerHTML, which no attribute walk can reach, so those URLs were never recorded // and the background pass never learned to rewrite them. Measured on a Gutenberg // AI import: 8 template-CDN PNGs left hotlinked across two pages, every one of // them inside block markup rather than an attribute. // // A regex sweep of the serialised content closes that gap, and is additive by // construction: it only ever adds URLs to the deferred list, and the cron's // str_replace over post_content is what localises them. A URL the cron cannot // resolve is skipped exactly as before. if ( $defer_media ) { // Hand the collector the content as-is. Pre-encoding it here was wrong twice // over: wp_json_encode() without JSON_UNESCAPED_SLASHES turns every `https://` // into `https:\/\/`, which the collector's regex cannot match, and the // collector encodes what it is given anyway - so a string argument was being // escaped a second time. Passing the parsed block array straight through lets // it encode once, with the right flags, and innerHTML is inside that encoding. $markup_urls = self::wdkit_collect_foreign_media_urls( $content ); if ( ! empty( $markup_urls ) ) { $missed = array_values( array_diff( $markup_urls, $deferred_here ) ); if ( ! empty( $missed ) ) { Wdkit_Import_Images::record_deferred_urls( $missed ); } } } // The warmed bodies have served their purpose for this template; do not carry // them across the rest of the request. Wdkit_Import_Images::clear_prefetched(); } elseif ( ! empty( $args['editor'] ) && 'elementor' === $args['editor'] && ! empty( $content ) ) { $media_import = array( $content ); // Element IDs must stay unique regardless of the media path, so this always runs. $media_import = self::widgets_elements_id_change( $media_import ); if ( $defer_media ) { // Skipping the walk above means Elementor never sees this page's media, so // nothing else in the request would notice what was left remote. Split it the // same way the Gutenberg walk does, per image: // // - SVGs are localised now. The background pass runs with no user and cannot // upload them (see wdkit_collect_foreign_media_urls()), so deferring one // loses it silently. These are cheap - small files, and usually already // imported by a sibling page. // - Everything else is recorded for the background pass, which is where the // expensive raster downloads and subsize generation belong. // // Warm every SVG this branch is about to import, concurrently, before the // one-node-at-a-time walk starts - the exact same fix the Gutenberg branch // above already carries. Without it each SVG paid its own blocking round // trip to the template CDN: measured at ~42s of sum_media_import for a // 15-template Elementor kit (matching that branch's own 65-SVG / ~44s // finding), virtually all of it connection setup and latency. // wdkit_localise_media_url() consults this cache for .svg URLs. $svg_scan = wp_json_encode( $media_import[0], JSON_UNESCAPED_SLASHES ); if ( is_string( $svg_scan ) && preg_match_all( '#https?://[^"\'\s<>()\\\\]+\.svg(?:\?[^"\'\s<>()\\\\]*)?#i', $svg_scan, $svg_matches ) ) { self::wdkit_require_import_images(); Wdkit_Import_Images::prefetch_remote_files( array_unique( $svg_matches[0] ) ); } $media_import[0] = self::wdkit_localise_foreign_svgs( $media_import[0] ); Wdkit_Import_Images::clear_prefetched(); $deferred = self::wdkit_collect_foreign_media_urls( $media_import[0] ); Wdkit_Import_Images::record_deferred_urls( $deferred ); // Leave the attachment IDs of those deferred controls empty rather than // carrying the template site's IDs through the window - see // wdkit_blank_deferred_media_ids(). $media_import[0] = self::wdkit_blank_deferred_media_ids( $media_import[0], $deferred ); return $media_import[0]; } $media_import = self::widgets_import_media_copy_content( $media_import ); $content = $media_import[0]; // Last: point any control Elementor left holding a foreign attachment ID at the // local attachment its URL already refers to. Without this, has_sizes controls // such as container background images resolve to nothing and render empty. $content = self::wdkit_repair_attachment_ids( $content ); } return $content; } /** * Widgets elements data * * @since 1.0.0 * @param string $media_import it is store media data. */ protected static function widgets_elements_id_change( $media_import ) { if ( did_action( 'elementor/loaded' ) ) { return \Elementor\Plugin::instance()->db->iterate_data( $media_import, function ( $element ) { $element['id'] = \Elementor\Utils::generate_random_string(); return $element; } ); } else { return $media_import; } } /** * Widgets Media import copy content. * * @since 1.0.0 * * @param string $media_import it is store media data. */ protected static function widgets_import_media_copy_content( $media_import ) { if ( did_action( 'elementor/loaded' ) ) { return \Elementor\Plugin::instance()->db->iterate_data( $media_import, function ( $element_data ) { $elements = \Elementor\Plugin::instance()->elements_manager->create_element_instance( $element_data ); if ( ! $elements ) { return null; } return self::widgets_element_import_start( $elements ); } ); } else { return $media_import; } } /** * Start element copy content for media import. * * @since 1.0.0 * * @param string \Elementor\Controls_Stack $element it is store elementor data. */ protected static function widgets_element_import_start( \Elementor\Controls_Stack $element ) { $get_element_instance = $element->get_data(); $tp_mi_on_fun = 'on_import'; if ( method_exists( $element, $tp_mi_on_fun ) ) { $get_element_instance = $element->{$tp_mi_on_fun}( $get_element_instance ); } foreach ( $element->get_controls() as $get_control ) { $control_type = \Elementor\Plugin::instance()->controls_manager->get_control( $get_control['type'] ); $control_name = $get_control['name']; if ( ! $control_type ) { // Skip just this control. Returning here would abandon every control after // it, so a single unregistered type - routine when a kit uses an addon that // is not fully active yet - would silently leave the rest of the element's // media pointing at the source site. continue; } if ( method_exists( $control_type, $tp_mi_on_fun ) ) { $get_element_instance['settings'][ $control_name ] = $control_type->{$tp_mi_on_fun}( $element->get_settings( $control_name ), $get_control ); } } return $get_element_instance; } /** * Blocks Recursively data * * @param string $data_import gutenber data import. */ public static function blocks_import_media_copy_content( $data_import, $defer_media = false ) { if ( ! empty( $data_import ) ) { foreach ( $data_import[0] as $key => $val ) { if ( array_key_exists( 'blockName', $val ) && ( empty( $val['blockName'] ) || null === $val['blockName'] || empty( $val['blockName'] ) || ' ' === $val['blockName'] ) ) { unset( $data_import[0][ $key ] ); } } } return self::blocks_array_recursively_data( $data_import, function ( $block_data, $args ) { $elements = self::blocks_data_instance( $block_data, $args ); return $elements; }, array( 'defer_media' => $defer_media ) ); } /** * Blocks Recursively data * * @param array $data store data. * @param string $callback store data. * @param string $args store data. */ public static function blocks_array_recursively_data( $data, $callback, $args = array() ) { if ( ( isset( $data['name'] ) && ! empty( $data['name'] ) ) || ( isset( $data['blockName'] ) && ! empty( $data['blockName'] ) ) ) { if ( ! empty( $data['innerBlocks'] ) ) { $data['innerBlocks'] = self::blocks_array_recursively_data( $data['innerBlocks'], $callback, $args ); } return call_user_func( $callback, $data, $args ); } if ( ! empty( $data ) ) { $data = (array) $data; foreach ( $data as $block_key => $block_value ) { $block_data = self::blocks_array_recursively_data( $data[ $block_key ], $callback, $args ); if ( null === $block_data ) { continue; } $data[ $block_key ] = $block_data; } } return $data; } /** * Check Blocks data media Url * * @param array $block_data store data. * @param array $args store data. * @param array $block_args store data. */ public static function blocks_data_instance( array $block_data, array $args = array(), $block_args = null ) { 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'] ) ) ) { $blocks_attr = isset( $block_data['attributes'] ) ? $block_data['attributes'] : ( isset( $block_data['attrs'] ) ? $block_data['attrs'] : array() ); $blocks_attr = self::wdkit_import_block_media( $blocks_attr, ! empty( $args['defer_media'] ) ); if ( isset( $block_data['attributes'] ) ) { $block_data['attributes'] = $blocks_attr; } elseif ( isset( $block_data['attrs'] ) ) { $block_data['attrs'] = $blocks_attr; } $block_data = self::wdkit_relink_block_markup( $block_data ); } return $block_data; } /** * Run block markup through the media import, the way the create path does. * * Used wherever block content is written from the browser: media import, then the Nexter * block processor so each block's rendered copy matches its attributes, then serialise. * * @since 2.6.2 * * @param string $content Block markup. * @return string Block markup with local media. */ private function wdkit_relink_gutenberg_content( $content ) { if ( ! is_string( $content ) || false === strpos( $content, ') which kses strips. Mirror the create path, which stores // the block markup slashed and unfiltered (endpoint is manage_options-gated // and the content is plugin-generated). $content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : ''; // This content comes straight from the browser and still carries the template // site's media URLs and attachment IDs, so it has to go through the same pipeline // the create path uses. Without this the save simply undid the import: the files // were fetched, then overwritten by a copy still pointing at the source site. // // Re-running is cheap. Every URL already handled resolves from the source-hash // lookup, and media that is already local resolves straight from its URL, so no // image is fetched or stored twice. $content = $this->wdkit_relink_gutenberg_content( $content ); // See wdkit_write_post_content(): this endpoint replaces the whole post_content // with markup the import itself produced, so kses here strips legitimate block // markup rather than untrusted input. Kept inline instead of routed through that // helper because this call needs wp_slash() and the WP_Error return. $had_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' ); if ( $had_kses ) { kses_remove_filters(); } $result = wp_update_post( array( 'ID' => $post_id, 'post_content' => wp_slash( $content ), ), true ); if ( $had_kses ) { kses_init_filters(); } if ( is_wp_error( $result ) ) { return array( 'success' => false, 'message' => $result->get_error_message(), ); } self::wdkit_rebuild_block_css( $post_id ); } else { $elements = isset( $_POST['content'] ) ? json_decode( wp_unslash( $_POST['content'] ), true ) : array(); if ( ! class_exists( '\\Elementor\\Plugin' ) ) { return array( 'success' => false, 'message' => esc_html__( 'Elementor not available', 'wdesignkit' ), ); } $document = \Elementor\Plugin::$instance->documents->get( $post_id ); if ( ! $document ) { return array( 'success' => false, 'message' => esc_html__( 'Elementor document not found', 'wdesignkit' ), ); } // Same as wkit_update_elementor_template(): browser-posted content keeps the // source template's attachment IDs, so repair them or this save undoes the // create-time fix and background images stop rendering. $elements = self::wdkit_repair_attachment_ids( $elements ); $unregistered = self::wdkit_unregistered_widget_types( self::wdkit_widget_types_in( $elements ) ); if ( ! empty( $unregistered ) ) { self::get_instance()->wdkit_enable_widgets_data( $unregistered ); } $document->save( array( 'elements' => $elements ) ); } return array( 'success' => true, 'message' => esc_html__( 'Page content updated', 'wdesignkit' ), ); } /** * Import single template and section from plugin only * * @param array $args store data. * @param array $template_id store data. * @param array $data store data. * @param array $temp_data store data. * */ protected function import_page_section_content() { // Elementor sideloads every image referenced by the page from inside this request. // A single oversized source image decodes to more than the whole memory limit, so // guard before any of that starts. $this->wdkit_guard_oversized_images(); // Sideloading images for image-heavy pages (wdkit_media_import → Imagick // thumbnail generation per image) can exceed the default 30s execution // limit and fatal the request mid-import. Give this single page import // more headroom; harmless no-op where set_time_limit() is disabled. if ( function_exists( 'set_time_limit' ) ) { @set_time_limit( 120 ); } if ( isset( $_POST['args'] ) ) { $args = ! empty( $_POST['args'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['args'] ) ), true ) : array(); } if ( isset( $_POST['temp_data'] ) ) { $temp_data = ! empty( $_POST['temp_data'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['temp_data'] ) ), true ) : array(); } if ( isset( $_POST['category_list'] ) ) { $category_list = ! empty( $_POST['category_list'] ) ? json_decode( wp_unslash( $_POST['category_list'] ), true ) : ''; } if ( isset( $_POST['tag_list'] ) ) { $tag_list = ! empty( $_POST['tag_list'] ) ? json_decode( wp_unslash( $_POST['tag_list'] ), true ) : ''; } if ( isset( $_POST['thumb_image'] ) ) { $thumb_image = ! empty( $_POST['thumb_image'] ) ? esc_url_raw( $_POST['thumb_image'] ) : ''; // Capped once here covers both download_url( $thumb_image ) calls below (Elementor // and Gutenberg branches share this variable). $thumb_image = Wdkit_Image_Guard::cap_pexels_source( $thumb_image ); } if ( isset( $_POST['template_id'] ) ) { $template_id = ! empty( $_POST['template_id'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['template_id'] ), true ) ) : ''; } $temp_type = isset( $_POST['temp_type'] ) ? sanitize_text_field( wp_unslash( $_POST['temp_type'] ) ) : 'normal'; // AI kit imports send this so the page saves with its source CDN image URLs still in // place - the JS side then registers it for wkit_schedule_deferred_media_sync() once // every page in the kit has been created, and the actual media_sideload_image() work // happens later, off this request. See wdkit_media_import()'s $defer_media branch. $defer_media = ! empty( $_POST['defer_media'] ); if ( isset( $_POST['data'] ) ) { $data = ! empty( $_POST['data'] ) ? json_decode( wp_unslash( $_POST['data'] ) ) : ''; } $enqueue_instance = new Wdkit_Enqueue(); $get_post_type = $enqueue_instance->wdkit_get_post_type_list(); $post_type = ! empty( $temp_data['type'] ) ? sanitize_text_field( wp_unslash( $temp_data['type'] ) ) : 'page'; if ( 'section' === $post_type ) { $post_type = $temp_data['wp_post_type']; } else { $post_type = $temp_data['wp_post_type']; } if ( ! array_key_exists( $post_type, $get_post_type ) ) { $post_type = 'page'; } if ( ! empty( $data ) && ! empty( $template_id ) && ! empty( $post_type ) && current_user_can( 'manage_options' ) ) { $post_content = $data; // Restore The Plus Addons' globals before the page is built, so the widgets' // tp_global_preset references resolve as soon as it renders. Done here rather // than in the save-template UI's confirmation dialog so that every import path // - the library, the abilities, the theme builder - gets it. if ( isset( $post_content->tp_globals ) && ! empty( $post_content->tp_globals ) ) { $this->wdkit_merge_tp_globals( json_decode( wp_json_encode( $post_content->tp_globals ), true ), isset( $post_content->tp_global_refs ) ? json_decode( wp_json_encode( $post_content->tp_global_refs ), true ) : array() ); } $post_title = isset( $post_content->title ) ? sanitize_text_field( $post_content->title ) : ''; // Same title cleanup the bundle path applies, so a kit imported through either // route lists identically in Pages. $post_title = self::wdkit_clean_page_title( $post_title, $post_type ); $post_slug = isset( $post_content->slug ) ? sanitize_text_field( $post_content->slug ) : ''; $file_type = isset( $post_content->file_type ) ? sanitize_text_field( $post_content->file_type ) : ''; $content = isset( $post_content->content ) ? wp_slash( $post_content->content ) : ''; $temp_con = ''; if ( 'gutenberg' === $args['editor'] || ( 'wdkit' === $args['editor'] && ! empty( $file_type ) && 'wp_block' === $file_type ) ) { if ( empty( $content ) ) { wp_send_json( array( 'template_id' => $template_id, 'message' => __( 'Content is Empty.', 'wdesignkit' ), ) ); wp_die(); } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'wp_block' === $file_type ) { $editor = ( 'wdkit' === $args['editor'] ) ? 'gutenberg' : $args['editor']; $blocks = parse_blocks( stripslashes( $content ) ); $blocks = $this->wdkit_media_import( $blocks, $editor, $defer_media ); // wdkit_media_import() above just skipped every raster image in this section's // own content (the "Blog Detail" dummy template's own decorative images, not a // user-picked stock photo) — this is this method's only chance to learn which // URLs those were, so the deferred-media response field below is built from it // rather than left to the bundle-only image_urls the client already tracks. $deferred_image_urls = $defer_media ? array_map( 'esc_url_raw', Wdkit_Import_Images::get_and_clear_deferred_urls() ) : array(); $processor = new WDKIT_Nexter_Block_Processor(); $blocks = $processor->run( $blocks ); $content = serialize_blocks( $blocks ); $content = $this->replace_unicode_glitch( serialize_blocks( $blocks ) ); $inserted_post = wp_insert_post( array( 'post_status' => 'publish', 'post_type' => $post_type, 'post_title' => $post_title, 'post_name' => $post_slug, 'post_content' => $content, ) ); if ( ! is_wp_error( $inserted_post ) && $inserted_post ) { self::wdkit_rebuild_block_css( $inserted_post ); } if ( is_wp_error( $inserted_post ) ) { wp_send_json( array( 'import_failed' => $inserted_post->get_error_message(), 'code' => $inserted_post->get_error_code(), ) ); wp_die(); } // A picked stock image is already in the media library by the time the posts // are created, so thumb_image is usually a url on this site rather than a // remote one. That can never go down the download path - the external-url // guard rejects loopback addresses by design, which silently dropped the // featured image - so recognise the existing attachment and point at it. // Anything genuinely remote still takes the untouched branch below. $local_thumb_id = ! empty( $thumb_image ) ? self::wdkit_local_attachment_from_url( $thumb_image ) : 0; if ( $local_thumb_id ) { set_post_thumbnail( $inserted_post, $local_thumb_id ); } elseif ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) { // $featured_image_url = esc_url_raw( $thumb_image ); $tmp = download_url( $thumb_image ); if ( is_wp_error( $tmp ) ) { error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) ); } else { $file_array = array( 'name' => wp_basename( $thumb_image ), 'tmp_name' => $tmp, ); $image_id = media_handle_sideload( $file_array, $inserted_post ); if ( is_wp_error( $image_id ) ) { error_log( 'Image sideload failed: ' . esc_html( $image_id->get_error_message() ) ); } else { set_post_thumbnail( $inserted_post, $image_id ); } @unlink( $tmp ); } } if ( ! empty( $category_list ) && is_array( $category_list ) ) { $category_ids = array_map( 'intval', $category_list ); wp_set_post_terms( $inserted_post, $category_ids, 'category' ); } if ( ! empty( $tag_list ) && is_array( $tag_list ) ) { $tag_ids = array_map( 'intval', $tag_list ); wp_set_post_terms( $inserted_post, $tag_ids, 'post_tag' ); } if ( ! empty( $args['custom_meta'] ) && 'true' == $args['custom_meta'] ) { $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : ''; if ( ! empty( $custom_meta ) ) { foreach ( $custom_meta as $meta_key => $meta_val ) { if ( isset( $meta_val[0] ) && ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); } if ( '' === get_post_meta( $inserted_post, $meta_key, true ) && isset( $meta_val[0] ) ) { add_post_meta( $inserted_post, $meta_key, $meta_val[0] ); } } } } $temp_detail = array( 'title' => get_the_title( $inserted_post ), 'edit_link' => get_edit_post_link( $inserted_post, 'internal' ), 'view' => get_permalink( $inserted_post ), 'id' => $inserted_post, ); if ( ! empty( $template_id->id ) ) { $temp_id = $template_id->id; } elseif ( ! empty( $template_id ) ) { $temp_id = $template_id; } else { $temp_id = ''; } wp_update_post([ 'ID' => $inserted_post, ]); if (class_exists('Tpgb_Library') && method_exists('Tpgb_Library', 'remove_backend_dir_files')) { Tpgb_Library()->remove_backend_dir_files(); } clean_post_cache( $inserted_post ); // This whole method imports exactly one section per call — unlike // wdkit_import_template()/wdkit_import_kit_template(), it never fired this hook // at all, so single-section imports (Header/Footer/CTA/etc., a primary import // path per the Template Type sidebar) were invisible to tracking entirely. do_action( 'wdkit_template_imported', 'single', sanitize_key( $editor ), 1 ); wp_send_json( array( $temp_id => $temp_detail, 'description' => 'Yay! Your Section has been Successfully Imported.', 'message' => __( 'Successfully Imported.', 'wdesignkit' ), 'inserted_id' => $inserted_post, // Raster images this call itself deferred - the caller unions this into // whatever image_urls its own template-fetch response already carries // before scheduling the background sideload pass. 'image_urls' => $deferred_image_urls, 'success' => true, ) ); wp_die(); } } elseif ( 'elementor' === $args['editor'] || ( 'wdkit' === $args['editor'] && ! empty( $file_type ) && 'elementor' === $file_type ) ) { if ( did_action( 'elementor/loaded' ) ) { if ( empty( $content ) ) { wp_send_json( array( 'template_id' => $template_id, 'message' => __( 'Content is Empty.', 'wdesignkit' ), ) ); wp_die(); } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'elementor' === $file_type ) { $content = $this->wdkit_content_remover( $content ); $post_attributes = array( 'post_title' => $post_title, 'post_type' => $post_type, 'post_status' => 'publish', 'post_name' => $post_slug, ); if ( 'elementor_library' === $post_type ) { $el_type = ( isset( $post_content->el_type ) && ! empty( $post_content->el_type ) ) ? sanitize_text_field( $post_content->el_type ) : 'page'; $new_document = \Elementor\Plugin::$instance->documents->create( $el_type, $post_attributes ); } else { $new_document = \Elementor\Plugin::$instance->documents->create( $post_attributes['post_type'], $post_attributes ); } if ( is_wp_error( $new_document ) ) { wp_send_json( array( 'import_failed' => $new_document->get_error_message(), 'code' => $new_document->get_error_code(), ) ); wp_die(); } $inserted_id = $new_document->get_main_id(); if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) { // $featured_image_url = esc_url_raw( $thumb_image ); $tmp = download_url( $thumb_image ); if ( is_wp_error( $tmp ) ) { error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) ); } else { $file_array = array( 'name' => wp_basename( $thumb_image ), 'tmp_name' => $tmp, ); $image_id = media_handle_sideload( $file_array, $inserted_id ); if ( is_wp_error( $image_id ) ) { error_log( 'Image sideload failed: ' . esc_html( $image_id->get_error_message() ) ); } else { set_post_thumbnail( $inserted_id, $image_id ); } @unlink( $tmp ); } } if ( ! empty( $category_list ) && is_array( $category_list ) ) { $category_ids = array_map( 'intval', $category_list ); wp_set_post_terms( $inserted_id, $category_ids, 'category' ); } if ( ! empty( $tag_list ) && is_array( $tag_list ) ) { $tag_ids = array_map( 'intval', $tag_list ); wp_set_post_terms( $inserted_id, $tag_ids, 'post_tag' ); } $settings = ( isset( $post_content->settings ) && ! empty( $post_content->settings ) ) ? json_decode( wp_json_encode( $post_content->settings ), true ) : array(); $content = wp_json_encode( $content ); $content = $this->wdkit_media_import( $content, $file_type, $defer_media ); // Same header-logo guarantee the bundle path applies, so a kit imported // through either route ends up with the same clickable logo. $content = self::wdkit_link_header_logo( $content, isset( $post_content->custom_meta ) ? $post_content->custom_meta : array() ); $widgets_in = self::wdkit_widget_types_in( $content ); $unregistered = self::wdkit_unregistered_widget_types( $widgets_in ); if ( ! empty( $unregistered ) ) { $this->wdkit_enable_widgets_data( $unregistered ); } $new_document->save( array( 'elements' => $content, 'settings' => ! empty( $settings ) ? $settings : array(), ) ); $inserted_id = $new_document->get_main_id(); if( $temp_type == 'navigation' ){ $temp_con = $content; } if ( ! empty( $args['custom_meta'] ) && 'true' == $args['custom_meta'] ) { $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : ''; if ( ! empty( $custom_meta ) ) { foreach ( $custom_meta as $meta_key => $meta_val ) { if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) { $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) ); } if ( '' === get_post_meta( $inserted_id, $meta_key, true ) ) { add_post_meta( $inserted_id, $meta_key, $meta_val[0] ); } } } } $temp_detail = array( 'title' => get_the_title( $inserted_id ), 'edit_link' => get_edit_post_link( $inserted_id, 'internal' ), 'view' => get_permalink( $inserted_id ), 'id' => $inserted_id, ); if ( ! empty( $template_id->id ) ) { $temp_id = $template_id->id; } elseif ( ! empty( $template_id ) ) { $temp_id = $template_id; } else { $temp_id = ''; } \Elementor\Plugin::$instance->files_manager->clear_cache(); // See the matching note in the Gutenberg branch above — this method never // fired the tracking hook for either editor. do_action( 'wdkit_template_imported', 'single', 'elementor', 1 ); wp_send_json( array( $temp_id => $temp_detail, 'content' => $temp_con, 'description' => 'Yay! Your Section has been Successfully Imported.', 'message' => __( 'Successfully Imported.', 'wdesignkit' ), 'inserted_id' => $inserted_id, // Same contract as the Gutenberg branch: the media this call // chose to defer, for the caller to schedule. 'image_urls' => $defer_media ? array_map( 'esc_url_raw', Wdkit_Import_Images::get_and_clear_deferred_urls() ) : array(), 'success' => true, ) ); wp_die(); } } else { wp_send_json( array( 'template_id' => $template_id, 'message' => esc_html__( 'Relevant Page Builder not installed or activated', 'wdesignkit' ), ) ); wp_die(); } } } wp_send_json( array( 'success' => false, 'message' => esc_html__( 'Something went wrong', 'wdesignkit' ), ) ); wp_die(); } /** * Replace unicode glitch * * @since 2.0.0 */ /** * Widened from private to public in 2.6.5 so the PHP page importer can apply the exact * same unicode normalisation the AJAX page import applies. Pure string transform, no * state, no capability implications. */ public function replace_unicode_glitch( $content ) { // Fix escaped unicode like \u003c → < $content = preg_replace_callback( '/\\\\u([0-9a-fA-F]{4})/', function ( $match ) { return html_entity_decode( mb_convert_encoding( pack('H*', $match[1]), 'UTF-8', 'UCS-2BE' ), ENT_QUOTES, 'UTF-8' ); }, $content ); return $content; } /** * change plugins setting for import kit * * @since 2.0.0 */ protected function update_plugin_setting() { $temp_id = isset( $_POST['plugin_type'] ) ? sanitize_text_field( $_POST['plugin_type'] ) : ''; $response = $this->wdkit_apply_plugin_settings_data( $temp_id ); wp_send_json( $response ); wp_die(); } /** * Apply the plugin-side settings an imported kit needs. * * Extracted from update_plugin_setting() so the PHP import runner can reach it without * `$_POST`. The option list is a closed set written by this method alone — there is no * path here for a caller to name an option, which is the point. * * The AJAX action above is now a thin adapter over this and its response is unchanged. * * @since 2.6.5 * * @param string $temp_id Plugin identifier; only 'elementor' is recognised. * @return array Response array, exactly as the AJAX action used to emit. */ public function wdkit_apply_plugin_settings_data( $temp_id ) { $temp_id = is_string( $temp_id ) ? $temp_id : ''; if ( $temp_id == 'elementor' ) { $unfiltered_files = get_option( 'elementor_unfiltered_files_upload', false ); $load_fa4 = get_option( 'elementor_load_fa4_shim', false ); $Inline_font_icons = get_option( 'elementor_experiment-e_font_icon_svg', false ); $container = get_option( 'elementor_experiment-container', false ); if ( isset( $unfiltered_files ) ) { update_option( 'elementor_unfiltered_files_upload', 1 ); } else { add_option( 'elementor_unfiltered_files_upload', 1 ); } if ( isset( $load_fa4 ) ) { update_option( 'elementor_load_fa4_shim', 'yes' ); } else { add_option( 'elementor_load_fa4_shim', 'yes' ); } if ( isset( $container ) ) { update_option( 'elementor_experiment-container', 'active' ); } else { add_option( 'elementor_experiment-container', 'active' ); } if ( isset( $Inline_font_icons ) ) { update_option( 'elementor_experiment-e_font_icon_svg', 'inactive' ); } else { add_option( 'elementor_experiment-e_font_icon_svg', 'inactive' ); } $response = array( 'message' => esc_html__( 'Plugin Setting updated', 'wdesignkit' ), 'description' => esc_html__( 'Plugin Setting updated', 'wdesignkit' ), 'success' => true, ); } else { $response = array( 'message' => esc_html__( 'Plugin not found', 'wdesignkit' ), 'description' => esc_html__( 'Plugin not found', 'wdesignkit' ), 'success' => false, ); } return $response; } /** * generate different color logo * * @since 2.0.0 */ protected function wkit_generate_site_logo() { if ( empty( $_POST['image_url'] ) ) { wp_send_json_error( 'Image URL not provided.' ); } if ( isset( $_POST['colors'] ) ) { $img_colors = ! empty( $_POST['colors'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['colors'] ) ), true ) : array(); } if ( empty( $img_colors ) ) { wp_send_json_error( 'Image color not provided.' ); } $image_url = esc_url_raw( $_POST['image_url'] ); if ( ! wdesignkit_validate_external_url( $image_url ) ) { wp_send_json_error( 'Image could not be downloaded.' ); } $tmp_file = download_url( $image_url ); if ( is_wp_error( $tmp_file ) ) { wp_send_json_error( 'Image could not be downloaded.' ); } if ( mime_content_type( $tmp_file ) !== 'image/png' ) { unlink( $tmp_file ); wp_send_json_error( 'Not a PNG file.' ); } $src = imagecreatefrompng( $tmp_file ); imagesavealpha( $src, true ); $width = imagesx( $src ); $height = imagesy( $src ); $hasTransparency = false; for ( $x = 0; $x < $width; $x++ ) { for ( $y = 0; $y < $height; $y++ ) { $rgba = imagecolorat( $src, $x, $y ); $alpha = ( $rgba & 0x7F000000 ) >> 24; if ( $alpha > 0 ) { $hasTransparency = true; break 2; } } } if ( ! $hasTransparency ) { unlink( $tmp_file ); wp_send_json_error( 'PNG has no transparent pixels.' ); } $upload_dir = wp_upload_dir(); $result_urls = array(); $colour_index = 0; foreach ( $img_colors as $name => $rgb ) { // $name is a key from the posted colours payload and went straight into the output // filename, so traversal sequences in it steered imagepng() outside the upload // directory (CWE-22, ClickUp 86d41ced6). sanitize_file_name() flattens it to one // path segment; a key made only of dots/separators sanitizes to empty, so fall back // to a positional index rather than writing to a bare "colored--