PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.0
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.0
2.7.0 2.6.6 2.6.5 2.6.4 2.6.3 2.6.2 2.6.1 2.6.0 2.5.5 2.5.4 2.5.3 2.5.2 2.5.1 2.5.0 2.4.0 2.3.3 2.3.2 2.3.1 1.0.10 1.0.11 1.0.12 1.0.13 1.0.14 1.0.15 1.0.16 All 129 releases
← All changes | includes/admin/class-api.php +9872 -1049 1.0.16 → 2.7.0 View file →
@@ -36,15 +36,31 @@
36 36
37 37 /**
38 38 * Member Variable
39 39 *
40 + * @var staring widgets_with_post_category
41 + */
42 + public $widgets_with_post_category = array(
43 + 'post_category', 'include_products',
44 + );
45 +
46 + /**
47 + * Member Variable
48 + *
40 49 * @var staring $wdkit_api
41 50 */
42 - public $wdkit_api = WDKIT_SERVER_SITE_URL . 'api/wp/';
51 + public $wdkit_api = WDKIT_SERVER_API_URL . 'api/wp/';
43 52
44 53 /**
45 54 * Member Variable
46 55 *
56 + * @var staring $wdkit_api_v2
57 + */
58 + public $wdkit_api_v2 = WDKIT_SERVER_API_URL . 'api/v2/wp/';
59 +
60 + /**
61 + * Member Variable
62 + *
47 63 * @var staring $widget_folder_u_r_l
48 64 */
49 65 public $widget_folder_u_r_l = '';
50 66
@@ -91,8 +107,14 @@
91 107 * Define the core functionality of the plugin.
92 108 */
93 109 public function __construct() {
94 110 add_action( 'wp_ajax_get_wdesignkit', array( $this, 'wdkit_api_call' ) );
111 +
112 + // Background half of the deferred-media-sideload flow (see
113 + // wkit_schedule_deferred_media_sync()/import_page_section_content()): each kit page
114 + // is created instantly with its source CDN image URLs still in place, and this cron
115 + // event does the slow media_sideload_image() work afterwards, off the import request.
116 + add_action( 'wdkit_async_sideload_page_images', array( $this, 'wdkit_async_sideload_page_images' ), 10, 3 );
95 117 }
96 118
97 119 /**
98 120 * Error JSON message
@@ -98,8 +120,10 @@
98 120 * Error JSON message
99 121 *
100 122 * @param array $data give array.
101 123 * @param string $status api code number.
124 + *
125 + * @since 1.0.0
102 126 * */
103 127 public function wdkit_error_msg( $data = null, $status = null ) {
104 128 wp_send_json_error( $data );
105 129 wp_die();
@@ -109,8 +133,10 @@
109 133 * Success JSON message
110 134 *
111 135 * @param array $data give array.
112 136 * @param string $status api code number.
137 + *
138 + * @since 1.0.0
113 139 * */
114 140 public function wdkit_success_msg( $data = null, $status = null ) {
115 141 wp_send_json_success( $data, $status );
116 142 wp_die();
@@ -115,9 +141,84 @@
115 141 wp_send_json_success( $data, $status );
116 142 wp_die();
117 143 }
118 144
145 +
119 146 /**
147 + * Memory headroom left for image work, in bytes. 0 means unlimited.
148 + */
149 + private static function wdkit_available_image_memory() {
150 + $limit = wp_convert_hr_to_bytes( ini_get( 'memory_limit' ) );
151 +
152 + if ( $limit <= 0 ) {
153 + return 0;
154 + }
155 +
156 + return max( 0, $limit - memory_get_usage( true ) );
157 + }
158 +
159 + /**
160 + * Stop WordPress decoding images that cannot fit in the memory available.
161 + *
162 + * Both filters are consulted by wp_create_image_subsizes() *before* it loads an image
163 + * editor, so refusing here means the oversized image is never decoded:
164 + *
165 + * big_image_size_threshold -> falsy skips the "-scaled" copy (needs a full decode)
166 + * intermediate_image_sizes_advanced -> empty makes _wp_make_subsizes() return early,
167 + * ahead of its wp_get_image_editor() call
168 + *
169 + * The original file is still attached and usable; only the derived sizes are skipped.
170 + * That trades ideal thumbnails for an import that completes, instead of a fatal that
171 + * takes the whole page down and repeats on every retry.
172 + *
173 + * @since 2.6.2
174 + */
175 + private static function wdkit_guard_oversized_images() {
176 + static $registered = false;
177 +
178 + // Registering twice would stack duplicate closures on both filters.
179 + if ( $registered ) {
180 + return;
181 + }
182 +
183 + $registered = true;
184 +
185 + if ( ! class_exists( 'Wdkit_Image_Guard' ) ) {
186 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php';
187 + }
188 +
189 + add_filter(
190 + 'big_image_size_threshold',
191 + function ( $threshold, $imagesize = array(), $file = '', $attachment_id = 0 ) {
192 + if ( ! empty( $imagesize[0] ) && ! empty( $imagesize[1] )
193 + && ! Wdkit_Image_Guard::decode_fits( $imagesize[0], $imagesize[1], self::wdkit_available_image_memory() )
194 + ) {
195 + return false;
196 + }
197 +
198 + return $threshold;
199 + },
200 + 99,
201 + 4
202 + );
203 +
204 + add_filter(
205 + 'intermediate_image_sizes_advanced',
206 + function ( $sizes, $image_meta = array(), $attachment_id = 0 ) {
207 + if ( ! empty( $image_meta['width'] ) && ! empty( $image_meta['height'] )
208 + && ! Wdkit_Image_Guard::decode_fits( $image_meta['width'], $image_meta['height'], self::wdkit_available_image_memory() )
209 + ) {
210 + return array();
211 + }
212 +
213 + return $sizes;
214 + },
215 + 99,
216 + 3
217 + );
218 + }
219 +
220 + /**
120 221 * Get Wdkit Api Call Ajax.
121 222 */
122 223 public function wdkit_api_call() {
123 224
@@ -136,33 +237,40 @@
136 237 case 'onboarding_handler':
137 238 $data = $this->wdkit_onboarding_handler();
138 239 break;
139 240 case 'wkit_login':
140 - $data = $this->wdkit_login();
241 + $data = apply_filters( 'wp_wdkit_login_ajax', 'wkit_login' );
141 242 break;
142 243 case 'api_login':
143 - $data = $this->wdkit_api_login();
244 + $data = apply_filters( 'wp_wdkit_login_ajax', 'api_login' );
144 245 break;
145 246 case 'social_login':
146 - $data = $this->wdkit_social_login();
247 + $data = apply_filters( 'wp_wdkit_login_ajax', 'social_login' );
147 248 break;
249 + case 'forgot_password':
250 + $data = apply_filters( 'wp_wdkit_login_ajax', 'forgot_password' );
251 + break;
252 + case 'wdkit_user_signup':
253 + $data = apply_filters( 'wp_wdkit_login_ajax', 'wdkit_user_signup' );
254 + break;
148 255 case 'wkit_meta_data':
149 256 $data = $this->wdkit_meta_data();
150 257 break;
151 258 case 'get_user_info':
152 - $id = isset( $_POST['id'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['id'] ) ) ) : false;
153 259 $data = $this->wdkit_get_user_info();
154 260 break;
155 261 case 'browse_page':
156 262 $data = $this->wdkit_browse_page();
157 263 break;
158 - case 'widget_browse_page':
159 - $data = $this->wdkit_widget_browse_page();
160 - break;
161 264 case 'kit_template':
162 - $id = isset( $_POST['id'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['id'] ) ) ) : false;
163 265 $data = $this->wdkit_template();
164 266 break;
267 + case 'wkit_preset_template':
268 + $data = apply_filters( 'wp_wdkit_preset_ajax', 'wdkit_preset_template' );
269 + break;
270 + case 'wdkit_preset_dwnld_template':
271 + $data = apply_filters( 'wp_wdkit_preset_ajax', 'wdkit_preset_dwnld_template' );
272 + break;
165 273 case 'template_remove':
166 274 $data = $this->wdkit_template_remove();
167 275 break;
168 276 case 'save_template':
@@ -167,8 +275,35 @@
167 275 break;
168 276 case 'save_template':
169 277 $data = $this->wdkit_put_save_template();
170 278 break;
279 + case 'update_save_temp_image':
280 + $data = $this->wdkit_update_save_temp_image();
281 + break;
282 + case 'save_wp_images':
283 + $data = $this->wdkit_save_wp_images();
284 + break;
285 + case 'wkit_schedule_deferred_media_sync':
286 + $data = $this->wkit_schedule_deferred_media_sync();
287 + break;
288 + case 'wkit_run_deferred_media_now':
289 + $data = $this->wkit_run_deferred_media_now();
290 + break;
291 + case 'get_global_val':
292 + $data = $this->wdkit_get_global_val();
293 + break;
294 + case 'update_global_val':
295 + $data = $this->wdkit_update_global_val();
296 + break;
297 + case 'wdkit_get_site_setting':
298 + $data = $this->wdkit_get_site_setting();
299 + break;
300 + case 'wdkit_update_site_setting':
301 + $data = $this->wdkit_update_site_setting();
302 + break;
303 + case 'update_preset_setting':
304 + $data = $this->wdkit_update_preset();
305 + break;
171 306 case 'find_template':
172 307 $data = $this->wdkit_find_existing_template();
173 308 break;
174 309 case 'update_template':
@@ -182,8 +317,68 @@
182 317 break;
183 318 case 'install_plugins_depends':
184 319 $data = $this->wdkit_install_plugins_depends();
185 320 break;
321 + case 'install_plugins_depends_batch':
322 + $data = $this->wdkit_install_plugins_depends_batch();
323 + break;
324 + case 'generate_site_logo':
325 + $data = $this->wkit_generate_site_logo();
326 + break;
327 + case 'generate_ai_content':
328 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'generate_ai_content' );
329 + break;
330 + case 'generate_ai_content_batch':
331 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'generate_ai_content_batch' );
332 + break;
333 + case 'reset_site':
334 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'reset_site' );
335 + break;
336 + case 'wdkit_nxt_thembuilder_reset':
337 + $data = $this->wdkit_nxt_thembuilder_reset();
338 + break;
339 + case 'wdkit_check_user_credit':
340 + $data = $this->wdkit_check_user_credit();
341 + break;
342 + case 'wdkit_remove_header_footer':
343 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wdkit_remove_header_footer' );
344 + break;
345 + case 'check_post_count':
346 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'check_post_count' );
347 + break;
348 + case 'wkit_check_product_count':
349 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_check_product_count' );
350 + break;
351 + case 'select_team_img':
352 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'select_team_img' );
353 + break;
354 + case 'wkit_ai_desc_keyword':
355 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_ai_desc_keyword' );
356 + break;
357 + case 'wkit_ai_credit_update':
358 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_ai_credit_update' );
359 + break;
360 + case 'wkit_generate_post_data':
361 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_generate_post_data' );
362 + break;
363 + case 'wkit_generate_product_data':
364 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_generate_product_data' );
365 + break;
366 + case 'wkit_cteate_product':
367 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_cteate_product' );
368 + break;
369 + case 'wkit_remove_dummy_post':
370 + $data = apply_filters( 'wp_wdkit_import_temp_ajax', 'wkit_remove_dummy_post' );
371 + break;
372 + case 'update_latest_plugin':
373 + $data = $this->wdkit_update_latest_plugin();
374 + break;
375 + case 'activate_container':
376 + $data = $this->wdkit_activate_container();
377 + break;
378 + case 'import_taxonomy':
379 + $data = $this->wdkit_import_taxonomy();
380 + break;
186 381 case 'import_template':
187 382 $data = $this->wdkit_import_template();
188 383 break;
189 384 case 'import_multi_template':
@@ -188,11 +383,70 @@
188 383 break;
189 384 case 'import_multi_template':
190 385 $data = $this->wdkit_import_multi_template();
191 386 break;
387 + case 'import_page_section':
388 + $data = $this->import_page_section_content();
389 + break;
390 + case 'wdkit_import_stage':
391 + $data = $this->wdkit_import_stage();
392 + break;
393 + case 'wkit_update_elementor_template':
394 + $data = $this->wkit_update_elementor_template();
395 + break;
396 + case 'wdkit_update_page_content':
397 + $data = $this->wdkit_update_page_content();
398 + break;
399 + case 'update_plugin_setting':
400 + $data = $this->update_plugin_setting();
401 + break;
402 + case 'update_theme_setting':
403 + $data = $this->update_theme_setting();
404 + break;
405 + case 'update_site_setting':
406 + $data = $this->update_site_setting();
407 + break;
192 408 case 'import_kit_template':
193 409 $data = $this->wdkit_import_kit_template();
194 410 break;
411 + case 'wkit_import_kit_bundle':
412 + $data = $this->wkit_import_kit_bundle();
413 + break;
414 + case 'wkit_fetch_site_bundle':
415 + $data = $this->wkit_fetch_site_bundle();
416 + break;
417 + case 'wkit_import_site_bundle':
418 + $data = $this->wkit_import_site_bundle();
419 + break;
420 + case 'enable_template_widgets':
421 + $data = $this->wdkit_enable_template_widgets();
422 + break;
423 + case 'scan_nexter_widgets':
424 + if ( ! empty( $_POST['blockNames'] ) && has_filter( 'nexter_block_list_merge' ) ) {
425 +
426 + $posted_blocks = json_decode( stripslashes( $_POST['blockNames'] ), true );
427 +
428 + if ( is_array( $posted_blocks ) ) {
429 + $blockList = array_map( 'sanitize_text_field', $posted_blocks );
430 +
431 + // अब filter call करो
432 + $result = apply_filters( 'nexter_block_list_merge', $blockList );
433 +
434 + wp_send_json( $result );
435 + wp_die();
436 + }
437 + }
438 +
439 + wp_send_json(
440 + array(
441 + 'success' => false,
442 + 'message' => __( 'No block names received or filter not found.', 'wdesignkit' ),
443 + 'description' => 'Ensure blockNames are posted and the filter is attached.',
444 + )
445 + );
446 + wp_die();
447 + $data = '';
448 + break;
195 449 case 'shared_with_me':
196 450 $data = $this->wdkit_shared_with_me();
197 451 break;
198 452 case 'manage_workspace':
@@ -197,8 +451,32 @@
197 451 break;
198 452 case 'manage_workspace':
199 453 $data = $this->wdkit_manage_workspace();
200 454 break;
455 + case 'widget_browse_page':
456 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'widget_browse_page' );
457 + break;
458 + case 'wkit_create_widget':
459 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_create_widget' );
460 + break;
461 + case 'wkit_import_widget':
462 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_import_widget' );
463 + break;
464 + case 'wkit_export_widget':
465 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_export_widget' );
466 + break;
467 + case 'wkit_delete_widget':
468 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_delete_widget' );
469 + break;
470 + case 'wkit_widget_preview':
471 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_widget_preview' );
472 + break;
473 + case 'wkit_check_widget_versions':
474 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_check_widget_versions' );
475 + break;
476 + case 'wkit_plugin_download_get':
477 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_plugin_download_get' );
478 + break;
201 479 case 'wkit_manage_widget_workspace':
202 480 $data = $this->wdkit_manage_widget_workspace();
203 481 break;
204 482 case 'wkit_activate_key':
@@ -203,31 +481,19 @@
203 481 break;
204 482 case 'wkit_activate_key':
205 483 $data = $this->wdkit_activate_key();
206 484 break;
207 - case 'wkit_get_widget_list':
208 - $data = $this->wdkit_get_widget_list();
209 - break;
210 485 case 'wkit_manage_widget_category':
211 486 $data = $this->wdkit_manage_widget_category();
212 487 break;
213 - case 'wkit_create_widget':
214 - $data = $this->wdkit_create_widget();
488 + case 'wkit_widget_json':
489 + $data = $this->wkit_widget_json();
215 490 break;
216 - case 'wkit_export_widget':
217 - $data = $this->wdkit_export_widget();
218 - break;
219 - case 'wkit_import_widget':
220 - $data = $this->wdkit_import_widget();
221 - break;
222 - case 'wkit_delete_widget':
223 - $data = $this->wdkit_delete_widget();
224 - break;
225 491 case 'wkit_download_widget':
226 492 $data = $this->wdkit_download_widget();
227 493 break;
228 494 case 'wkit_public_download_widget':
229 - $data = $this->wdkit_public_download_widget();
495 + $data = apply_filters( 'wp_wdkit_widget_ajax', 'wkit_public_download_widget' );
230 496 break;
231 497 case 'wkit_add_widget':
232 498 $data = $this->wdkit_add_widget();
233 499 break;
@@ -248,14 +514,36 @@
248 514 break;
249 515 case 'sync_licence':
250 516 $data = $this->wdkit_sync_licence_key();
251 517 break;
518 + case 'get_wkit_version':
519 + $data = $this->wdkit_prev_version();
520 + break;
521 + case 'rollback_wdkit':
522 + $data = $this->wdkit_rollback_check();
523 + break;
252 524 case 'wkit_logout':
253 525 $data = $this->wdkit_logout();
254 526 break;
527 + case 'wkit_white_label':
528 + $this->wkit_white_label();
529 + break;
530 + case 'wkit_reset_wl':
531 + $data = $this->wkit_reset_wl();
532 + break;
533 + case 'wdkit_dark_mode':
534 + $data = $this->wdkit_dark_mode();
535 + break;
536 + case 'wdkit_get_workspace_data':
537 + $data = $this->wdkit_get_workspace_data();
538 + break;
539 + default:
540 + $this->wdkit_error_msg( __( 'Unknown request type.', 'wdesignkit' ) );
541 + return;
255 542 }
256 543
257 544 $this->wdkit_success_msg( $data );
545 + // wp_die();
258 546 }
259 547
260 548 /**
261 549 *
@@ -262,12 +550,13 @@
262 550 * This Function is used for API call
263 551 *
264 552 * @since 1.0.0
265 553 *
266 - * @param array $data give array.
267 - * @param array $name store data.
554 + * @param array $data give array.
555 + * @param array $name store data.
556 + * @param int $timeout optional HTTP timeout in seconds. Default 100.
268 557 */
269 - protected function wkit_api_call( $data, $name ) {
558 + protected function wkit_api_call( $data, $name, $timeout = 100 ) {
270 559 $u_r_l = $this->wdkit_api;
271 560
272 561 if ( empty( $u_r_l ) ) {
273 562 return array(
@@ -278,9 +567,9 @@
278 567
279 568 $args = array(
280 569 'method' => 'POST',
281 570 'body' => $data,
282 - 'timeout' => 100,
571 + 'timeout' => $timeout,
283 572 );
284 573 $response = wp_remote_post( $u_r_l . $name, $args );
285 574
286 575 if ( is_wp_error( $response ) ) {
@@ -286,9 +575,9 @@
286 575 if ( is_wp_error( $response ) ) {
287 576 $error_message = $response->get_error_message();
288 577
289 578 /* Translators: %s is a placeholder for the error message */
290 - $error_message = printf( esc_html__( 'API request error: %s', 'wdesignkit' ), esc_html( $error_message ) );
579 + $error_message = sprintf( esc_html__( 'API request error: %s', 'wdesignkit' ), esc_html( $error_message ) );
291 580
292 581 return array(
293 582 'massage' => $error_message,
294 583 'success' => false,
@@ -305,9 +594,9 @@
305 594 'success' => true,
306 595 );
307 596 }
308 597
309 - $error_message = printf( 'Server error: %d', esc_html( $status_code ) );
598 + $error_message = sprintf( 'Server error: %d', esc_html( $status_code ) );
310 599
311 600 if ( isset( $error_data->message ) ) {
312 601 $error_message .= ' (' . $error_data->message . ')';
313 602 }
@@ -319,41 +608,9 @@
319 608 );
320 609 }
321 610
322 611 /**
323 - * This Function is used for API call
324 612 *
325 - * @since 1.0.0
326 - *
327 - * @param string $user_key Dynamic key.
328 - * @param string $user_email User email.
329 - * @param string $token User token.
330 - */
331 - protected function wdkit_set_time_out( $user_key, $user_email, $token, $login_type = '' ) {
332 -
333 - if ( 'normal' === $login_type ) {
334 - set_transient(
335 - 'wdkit_auth_' . $user_key,
336 - array(
337 - 'user_email' => sanitize_email( $user_email ),
338 - 'token' => $token,
339 - ),
340 - 7776000
341 - );
342 - } else {
343 - set_transient(
344 - 'wdkit_auth_' . $user_key,
345 - array(
346 - 'user_email' => sanitize_email( $user_email ),
347 - 'token' => $token,
348 - ),
349 - 86400
350 - );
351 - }
352 - }
353 -
354 - /**
355 - *
356 613 * It is Use for handle onboarding data.
357 614 *
358 615 * @since 1.0.9
359 616 */
@@ -363,8 +620,9 @@
363 620
364 621 $elementor_plugin = isset( $_POST['elementor_plugin'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['elementor_plugin'] ) ) : 0;
365 622 $tpag_plugin = isset( $_POST['tpag_plugin'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['tpag_plugin'] ) ) : 0;
366 623 $bricks_theme = isset( $_POST['bricks_theme'] ) ? (int) sanitize_text_field( wp_unslash( $_POST['bricks_theme'] ) ) : 0;
624 + $site_info = isset( $_POST['info'] ) ? sanitize_text_field( wp_unslash( $_POST['info'] ) ) : false;
367 625
368 626 $server_software = ! empty( $_SERVER['SERVER_SOFTWARE'] ) ? sanitize_text_field( wp_unslash( $_SERVER['SERVER_SOFTWARE'] ) ) : '';
369 627
370 628 $web_server = $server_software;
@@ -403,23 +661,27 @@
403 661 'tpag_install' => $tpag_plugin,
404 662 'bricks_install' => $bricks_theme,
405 663 );
406 664
407 - $final = array(
408 - 'web_server' => $web_server,
409 - 'memory_limit' => $memory_limit,
410 - 'max_execution_time' => $max_execution_time,
411 - 'php_version' => $php_version,
412 - 'wp_version' => $wp_version,
413 - 'email' => $email,
414 - 'site_url' => $siteurl,
415 - 'site_language' => $language,
416 - 'theme' => $theme,
417 - 'plugins' => $act_plugin,
418 - 'basic_requirements' => $basic_requirements,
419 - 'page_template' => $page_template,
420 - 'page_builder' => $page_builder,
421 - );
665 + if ( ! empty( $site_info ) ) {
666 + $final = array(
667 + 'web_server' => $web_server,
668 + 'memory_limit' => $memory_limit,
669 + 'max_execution_time' => $max_execution_time,
670 + 'php_version' => $php_version,
671 + 'wp_version' => $wp_version,
672 + 'email' => $email,
673 + 'site_url' => $siteurl,
674 + 'site_language' => $language,
675 + 'theme' => $theme,
676 + 'plugins' => $act_plugin,
677 + 'basic_requirements' => $basic_requirements,
678 + 'page_template' => $page_template,
679 + 'page_builder' => $page_builder,
680 + );
681 + } else {
682 + $final = array();
683 + }
422 684
423 685 $response = wp_remote_post(
424 686 $this->wdkit_onbording_api,
425 687 array(
@@ -472,175 +734,12 @@
472 734 }
473 735
474 736 /**
475 737 *
476 - * It is Use for user login with email and password.
738 + * It is Use for get meta data for non login user
477 739 *
478 740 * @since 1.0.0
479 741 */
480 - protected function wdkit_login() {
481 - $user_email = isset( $_POST['user_email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['user_email'] ) ) ) : false;
482 - $user_password = isset( $_POST['user_password'] ) ? sanitize_text_field( wp_unslash( $_POST['user_password'] ) ) : false;
483 - $login_type = isset( $_POST['login_type'] ) ? sanitize_text_field( wp_unslash( $_POST['login_type'] ) ) : false;
484 - $site_url = isset( $_POST['site_url'] ) ? esc_url_raw( wp_unslash( $_POST['site_url'] ) ) : '';
485 -
486 - $user_key = strstr( $user_email, '@', true );
487 - $response = '';
488 -
489 - delete_transient( 'wdkit_auth_' . $user_key );
490 -
491 - $get_login = get_transient( 'wdkit_auth_' . $user_key );
492 -
493 - if ( ! empty( $user_email ) && ! empty( $user_password ) && false === $get_login ) {
494 - $response = WDesignKit_Data_Query::get_data(
495 - 'login',
496 - array(
497 - 'user_email' => $user_email,
498 - 'password' => $user_password,
499 - 'site_url' => $site_url,
500 - )
501 - );
502 -
503 - if ( ! empty( $response ) && ! empty( $response['success'] ) ) {
504 - if ( ! empty( $response['message'] ) && ! empty( $response['token'] ) ) {
505 - if ( false === get_transient( 'wdkit_auth_' . $user_key ) ) {
506 - $this->wdkit_set_time_out( $user_key, $user_email, $response['token'], $login_type );
507 - }
508 - }
509 - }
510 - } elseif ( ! empty( $get_login ) && ! empty( $get_login['token'] ) ) {
511 - $response = array_merge(
512 - array(
513 - 'success' => true,
514 - 'message' => esc_html__( 'Success! Login successful.', 'wdesignkit' ),
515 - 'description' => esc_html__( 'Login successful. Keep it up!', 'wdesignkit' ),
516 - ),
517 - $get_login
518 - );
519 - }
520 -
521 - wp_send_json( $response );
522 - wp_die();
523 - }
524 -
525 - /**
526 - *
527 - * This Function is used for Login with Api (token)
528 - *
529 - * @version 1.0.0
530 - * @access public
531 - */
532 - protected function wdkit_api_login() {
533 - $user_token = isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
534 - $login_type = isset( $_POST['login_type'] ) ? sanitize_text_field( wp_unslash( $_POST['login_type'] ) ) : '';
535 -
536 - $site_url = isset( $_POST['site_url'] ) ? esc_url_raw( wp_unslash( $_POST['site_url'] ) ) : '';
537 -
538 - if ( empty( $user_token ) ) {
539 - $result = array(
540 - 'success' => false,
541 - 'token' => '',
542 - 'data' => array(
543 - 'message' => $this->e_msg_login,
544 - 'description' => $this->e_desc_login,
545 - ),
546 - );
547 -
548 - wp_send_json( $result );
549 - wp_die();
550 - }
551 -
552 - $array_data = array(
553 - 'token' => $user_token,
554 - 'site_url' => $site_url,
555 - );
556 -
557 - $response = $this->wkit_api_call( $array_data, 'login/api' );
558 -
559 - $success = ! empty( $response['success'] ) ? is_bool( $response['success'] ) : false;
560 -
561 - if ( empty( $success ) ) {
562 - $result = array(
563 - 'data' => $response,
564 - 'token' => '',
565 - 'success' => false,
566 - );
567 -
568 - wp_send_json( $result );
569 - wp_die();
570 - }
571 -
572 - $response = json_decode( wp_json_encode( $response['data'] ), true );
573 - $user_email = ! empty( $response['user']['user_email'] ) ? sanitize_email( $response['user']['user_email'] ) : '';
574 - $user_key = strstr( $user_email, '@', true );
575 -
576 - $this->wdkit_set_time_out( $user_key, $user_email, $user_token, $login_type );
577 -
578 - $result = array(
579 - 'success' => true,
580 - 'data' => $response,
581 - 'token' => $user_token,
582 - );
583 -
584 - wp_send_json( $result );
585 - wp_die();
586 - }
587 -
588 - /**
589 - *
590 - * This Function is used for social Login
591 - *
592 - * @version 1.0.0
593 - * @access public
594 - */
595 - protected function wdkit_social_login() {
596 - $user_state = isset( $_POST['state'] ) ? sanitize_text_field( wp_unslash( $_POST['state'] ) ) : '';
597 - $login_type = isset( $_POST['login_type'] ) ? sanitize_text_field( wp_unslash( $_POST['login_type'] ) ) : '';
598 -
599 - $site_url = isset( $_POST['site_url'] ) ? esc_url_raw( wp_unslash( $_POST['site_url'] ) ) : '';
600 -
601 - $array_data = array(
602 - 'state' => $user_state,
603 - 'site_url' => $site_url,
604 - );
605 -
606 - $response = $this->wkit_api_call( $array_data, 'login/ip' );
607 - $success = ! empty( $response['success'] ) ? $response['success'] : false;
608 -
609 - if ( empty( $success ) ) {
610 - $result = array(
611 - 'data' => $response,
612 - 'success' => false,
613 - );
614 -
615 - wp_send_json( $result );
616 - wp_die();
617 - }
618 -
619 - $response = json_decode( wp_json_encode( $response['data'] ), true );
620 - $user_email = ! empty( $response['user']['user_email'] ) ? sanitize_email( $response['user']['user_email'] ) : '';
621 - $user_token = ! empty( $response['token'] ) ? sanitize_text_field( $response['token'] ) : '';
622 - $user_key = strstr( $user_email, '@', true );
623 -
624 - if ( ! empty( $response ) && ! empty( $user_token ) ) {
625 - $this->wdkit_set_time_out( $user_key, $user_email, $user_token, $login_type );
626 - }
627 -
628 - $result = array(
629 - 'data' => $response,
630 - 'token' => $user_token,
631 - );
632 -
633 - wp_send_json( $result );
634 - wp_die();
635 - }
636 -
637 - /**
638 - *
639 - * It is Use for get meta data for non login user
640 - *
641 - * @since 1.0.0\
642 - */
643 742 protected function wdkit_meta_data() {
644 743 $type = isset( $_POST['meta_type'] ) ? sanitize_text_field( wp_unslash( $_POST['meta_type'] ) ) : '';
645 744 $data = array( 'type' => $type );
646 745
@@ -670,9 +769,10 @@
670 769 $statuscode = array( 'HTTP_CODE' => $status );
671 770
672 771 $final = json_decode( wp_json_encode( $get_data_one ), true );
673 772
674 - $final['Setting'] = self::wkit_get_settings_panel();
773 + $final['Setting'] = self::wkit_get_settings_panel();
774 + $final['widget_list'] = $this->wkit_manage_widget_sequence( array() );
675 775
676 776 $final = array(
677 777 'data' => $final,
678 778 );
@@ -682,14 +782,49 @@
682 782 }
683 783
684 784 /**
685 785 *
786 + * It is Use for get activate license key data from tpae and nexter blocks.
787 + *
788 + * @since 1.1.6
789 + */
790 + protected function wkit_manage_license_data() {
791 + $manage_licence = array();
792 + $theplus_active_check = is_plugin_active( 'the-plus-addons-for-elementor-page-builder/theplus_elementor_addon.php' );
793 + $nexter_active_check = is_plugin_active( 'the-plus-addons-for-block-editor/the-plus-addons-for-block-editor.php' );
794 +
795 + $theplus_licence = get_option( 'tpaep_licence_data', array() );
796 +
797 + // Also require the TPAE Pro plugin to be active (Pro defines THEPLUS_VERSION;
798 + // the free plugin defines L_THEPLUS_VERSION). This hides the "found active
799 + // key" notice when the Pro plugin is removed even though its licence option
800 + // still lingers in the database.
801 + if ( ! empty( $theplus_active_check ) && defined( 'THEPLUS_VERSION' ) && ! empty( $theplus_licence ) ) {
802 + $manage_licence['tpae'] = $theplus_licence;
803 + }
804 +
805 + $nexter_licence = get_option( 'tpgb_activate', array() );
806 +
807 + // Also require the Nexter Blocks Pro plugin to be active (Pro defines
808 + // TPGBP_VERSION; the free plugin defines TPGB_VERSION), so the notice hides
809 + // when the Pro plugin is removed but its licence option persists.
810 + if ( ! empty( $nexter_active_check ) && defined( 'TPGBP_VERSION' ) && ! empty( $nexter_licence ) && ! empty( $nexter_licence['tpgb_activate_key'] ) ) {
811 + $tpgb_license_status = get_option( 'tpgbp_license_status', array() );
812 + $tpgb_license_status['license_key'] = $nexter_licence['tpgb_activate_key'];
813 + $manage_licence['tpag'] = $tpgb_license_status;
814 + }
815 +
816 + return $manage_licence;
817 + }
818 +
819 + /**
820 + *
686 821 * It is Use for get all info of user.
687 822 *
688 823 * @since 1.0.0
689 - * @access public
690 824 */
691 825 protected function wdkit_get_user_info() {
826 + $token = isset( $_POST['token'] ) ? wp_unslash( $_POST['token'] ) : false;
692 827 $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false;
693 828 $builder = isset( $_POST['builder'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['builder'] ) ) ) : '';
694 829
695 830 $site_url = isset( $_POST['site_url'] ) ? esc_url_raw( wp_unslash( $_POST['site_url'] ) ) : '';
@@ -695,9 +830,9 @@
695 830 $site_url = isset( $_POST['site_url'] ) ? esc_url_raw( wp_unslash( $_POST['site_url'] ) ) : '';
696 831
697 832 $response = array();
698 833
699 - if ( empty( $email ) ) {
834 + if ( empty( $token ) ) {
700 835 $response = array(
701 836 'success' => false,
702 837 'message' => $this->e_msg_login,
703 838 'description' => $this->e_desc_login,
@@ -706,10 +841,10 @@
706 841 wp_send_json( $response );
707 842 wp_die();
708 843 }
709 844
710 - $token = $this->wdkit_login_user_token( $email );
711 - $args = array(
845 + // $token = $this->wdkit_login_user_token( $email );
846 + $args = array(
712 847 'token' => $token,
713 848 'builder' => $builder,
714 849 'site_url' => $site_url,
715 850 );
@@ -714,18 +849,48 @@
714 849 'site_url' => $site_url,
715 850 );
716 851
717 852 $response = WDesignKit_Data_Query::get_data( 'get_user_info', $args );
853 +
854 + if ( is_wp_error( $response ) ) {
855 + wp_send_json( array(
856 + 'success' => false,
857 + 'message' => $response->get_error_message(),
858 + 'description' => $response->get_error_message(),
859 + ) );
860 + wp_die();
861 + }
862 +
718 863 $status = ( ! empty( $response['status'] ) ) ? sanitize_text_field( $response['status'] ) : 'error';
719 864 $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false;
720 865
721 866 /**Condtion user for user logout & expire token*/
722 867 if ( 'Token is Expired' === $status || 'Authorization Token not found' === $status ) {
723 - delete_transient( 'wdkit_auth_' . $email );
868 + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) );
869 + // Clear stored license data when token expires so banner shows again
870 + delete_option( 'wdkit_licence_data' );
724 871 }
725 872
726 - $response['Setting'] = self::wkit_get_settings_panel();
873 + if ( empty( $response ) ) {
874 + $response['login_reset'] = 'yes';
875 + }
727 876
877 + // Store WDesignKit license data locally if present in response
878 + if ( ! empty( $response['credits']['wdkit_licence'] ) && is_array( $response['credits']['wdkit_licence'] ) ) {
879 + $wdkit_licence = $response['credits']['wdkit_licence'];
880 + // Handle serialized data
881 + if ( is_string( $wdkit_licence ) && is_serialized( $wdkit_licence ) ) {
882 + $wdkit_licence = unserialize( $wdkit_licence, array( 'allowed_classes' => false ) );
883 + }
884 + if ( ! empty( $wdkit_licence ) && is_array( $wdkit_licence ) ) {
885 + update_option( 'wdkit_licence_data', $wdkit_licence );
886 + }
887 + }
888 +
889 + $response['Setting'] = $this->wkit_get_settings_panel();
890 + $response['widget_list'] = $this->wkit_manage_widget_sequence( $response );
891 + $response['manage_licence'] = $this->wkit_manage_license_data();
892 +
728 893 $response = array(
729 894 'data' => $response,
730 895 'success' => true,
731 896 );
@@ -734,57 +899,166 @@
734 899 wp_die();
735 900 }
736 901
737 902 /**
738 - * Browse Page Filter
739 903 *
740 - * @since 1.0.0
904 + * It is Use for get all widgets local and server.
905 + *
906 + * @since 1.0.19
907 + * @param string $response userinfo store.
741 908 */
742 - protected function wdkit_browse_page() {
743 - $args = $this->wdkit_parse_args( $_POST );
909 + protected function wkit_manage_widget_sequence( $response = array() ) {
744 910
745 - $response = WDesignKit_Data_Query::get_data( 'browse_page', $args );
911 + $credits = ! empty( $response['credits']['widget_limit']['meta_value'] ) ? $response['credits']['widget_limit']['meta_value'] : 10;
912 + $server_list = ! empty( $response['widgettemplate'] ) ? $response['widgettemplate'] : array();
913 + $db_builder_list = ! empty( $response['widgetbuilder'] ) ? $response['widgetbuilder'] : array();
746 914
747 - wp_send_json( $response );
748 - wp_die();
915 + // Whether this call actually carried the server widget list that activation state is
916 + // derived from. Captured before the loops below, which unset() matched $server_list
917 + // entries as they go. wdkit_meta_data() calls this method with array(), and without
918 + // this flag that call rebuilt $db_widget from local widgets only — every one of which
919 + // is forced 'active' further down — and then wrote the empty result over
920 + // wkit_deactivate_widgets, erasing every deactivation the user had made.
921 + $has_server_widgets = ! empty( $server_list );
922 +
923 + $placeholderimg = WDKIT_URL . 'assets/images/placeholder.jpg';
924 +
925 + $local_list = $this->wdkit_get_local_widgets();
926 +
927 + $server_w_unique = array_column( $server_list, 'w_unique' );
928 +
929 + $idx_builder = array();
930 + foreach ( $db_builder_list as $index => $value ) {
931 + $builder_name = ! empty( $value['builder_slug'] ) ? $value['builder_slug'] : '';
932 + $w_id = ! empty( $value['w_id'] ) ? $value['w_id'] : '';
933 +
934 + if ( ! empty( $builder_name ) ) {
935 + $idx_builder[ $w_id ] = strtolower( str_replace( ' ', '_', trim( $builder_name ) ) );
936 + }
937 + }
938 +
939 + foreach ( $server_list as $index => $value ) {
940 + $get_id = $server_list[ $index ]['builder'] ? $server_list[ $index ]['builder'] : '';
941 +
942 + $server_list[ $index ]['type'] = 'server';
943 + $server_list[ $index ]['builder'] = ! empty( $idx_builder[ $get_id ] ) ? $idx_builder[ $get_id ] : '';
944 + }
945 +
946 + $count = 0;
947 +
948 + foreach ( $local_list as $key => $value ) {
949 + $widget_id = ! empty( $value['widgetdata']['widget_id'] ) ? $value['widgetdata']['widget_id'] : '';
950 + $allow_push = isset( $value['widgetdata']['allow_push'] ) ? $value['widgetdata']['allow_push'] : true;
951 +
952 + if ( in_array( $widget_id, $server_w_unique ) ) {
953 +
954 + $index = array_search( $widget_id, $server_w_unique );
955 +
956 + $server_list[ $index ]['title'] = $local_list[ $key ]['widgetdata']['name'];
957 + $server_list[ $index ]['w_version'] = $local_list[ $key ]['widgetdata']['widget_version'];
958 + $server_list[ $index ]['allow_push'] = $allow_push;
959 + $server_list[ $index ]['builder'] = $local_list[ $key ]['widgetdata']['type'];
960 + $server_list[ $index ]['w_unique'] = $local_list[ $key ]['widgetdata']['widget_id'];
961 + $server_list[ $index ]['image'] = ! empty( $local_list[ $key ]['widgetdata']['w_image'] ) ? $local_list[ $key ]['widgetdata']['w_image'] : $placeholderimg;
962 +
963 + $local_list[ $key ] = $server_list[ $index ];
964 +
965 + $local_list[ $key ]['type'] = 'done';
966 +
967 + unset( $server_list[ $index ] );
968 + } else {
969 + $local_list[ $key ]['widgetdata']['builder'] = $local_list[ $key ]['widgetdata']['type'];
970 + $local_list[ $key ]['widgetdata']['w_unique'] = $local_list[ $key ]['widgetdata']['widget_id'];
971 + $local_list[ $key ]['widgetdata']['allow_push'] = $allow_push;
972 + $local_list[ $key ]['widgetdata']['image'] = ! empty( $local_list[ $key ]['widgetdata']['w_image'] ) ? $local_list[ $key ]['widgetdata']['w_image'] : $placeholderimg;
973 + $local_list[ $key ]['widgetdata']['is_activated'] = 'active';
974 +
975 + $local_list[ $key ]['widgetdata']['type'] = 'plugin';
976 +
977 + $local_list[ $key ]['widgetdata']['title'] = $local_list[ $key ]['widgetdata']['name'];
978 + unset( $local_list[ $key ]['widgetdata']['name'] );
979 + unset( $local_list[ $key ]['widgetdata']['widget_id'] );
980 +
981 + $local_list[ $key ] = $local_list[ $key ]['widgetdata'];
982 + }
983 + }
984 +
985 + $final = array_merge( $local_list, $server_list );
986 +
987 + $db_widget = array();
988 +
989 + foreach ( $final as $key => $self ) {
990 + $is_activated = ! empty( $final[ $key ]['is_activated'] ) ? $final[ $key ]['is_activated'] : 'active';
991 +
992 + if ( 'active' === $is_activated ) {
993 + ++$count;
994 + }
995 +
996 + if ( ( $count > $credits ) && ( 'unlimited' !== $credits ) ) {
997 + $final[ $key ]['is_activated'] = 'deactive';
998 + }
999 +
1000 + if ( ! empty( $self['is_activated'] ) && 'active' !== $self['is_activated'] ) {
1001 + $db_widget[] = array(
1002 + 'w_unique' => $self['w_unique'],
1003 + 'builder' => $self['builder'],
1004 + 'title' => $self['title'],
1005 + 'is_activated' => $self['is_activated'],
1006 + );
1007 + }
1008 + }
1009 +
1010 + // Only persist activation state when the server list it is derived from was actually
1011 + // supplied. See $has_server_widgets above.
1012 + if ( $has_server_widgets ) {
1013 + // update_option() creates the row when it is missing, so it covers both cases.
1014 + // The previous add_option()/update_option() split was chosen on empty( $option ),
1015 + // but wdkit_db_widgetlist() creates this row as an empty array on every install —
1016 + // so the empty branch ran while the row already existed, and add_option() is a
1017 + // no-op for an existing option. Deactivating from the My Widgets screen was
1018 + // therefore silently discarded on effectively every site. Autoload stays 'yes',
1019 + // matching the original add_option() call and wdkit_db_widgetlist().
1020 + update_option( 'wkit_deactivate_widgets', $db_widget, 'yes' );
1021 +
1022 + // The cached widget registry bakes in wkit_deactivate_widgets membership and is
1023 + // stored as a no-expiry transient, so it never self-heals. Without this the
1024 + // loaders kept registering a widget the user had just switched off (and kept
1025 + // hiding one they had switched back on) until the transient was flushed by hand.
1026 + // The write above is not per-builder — one save can change any builder's set, and
1027 + // a widget can move between builders — so clear all four.
1028 + if ( function_exists( 'wdesignkit_invalidate_widget_registry' ) ) {
1029 + foreach ( array( 'elementor', 'gutenberg', 'gutenberg_core', 'bricks' ) as $builder_slug ) {
1030 + wdesignkit_invalidate_widget_registry( $builder_slug );
1031 + }
1032 + }
1033 + }
1034 +
1035 + return $final;
749 1036 }
750 1037
751 1038 /**
1039 + * Browse Page Filter
752 1040 *
753 - * It is Use to get data for widget browse page
754 - *
755 1041 * @since 1.0.0
756 - * @access public
757 1042 */
758 - protected function wdkit_widget_browse_page() {
759 - $array_data = array(
760 - 'CurrentPage' => isset( $_POST['page'] ) ? (int) $_POST['page'] : 1,
761 - 'builder' => isset( $_POST['buildertype'] ) ? wp_unslash( $_POST['buildertype'] ) : '',
762 - 'category' => isset( $_POST['category'] ) ? sanitize_text_field( wp_unslash( $_POST['category'] ) ) : '',
763 - 'ParPage' => isset( $_POST['perpage'] ) ? (int) $_POST['perpage'] : 12,
764 - 'search' => isset( $_POST['search'] ) ? sanitize_text_field( wp_unslash( $_POST['search'] ) ) : '',
765 - 'free_pro' => isset( $_POST['free_pro'] ) ? sanitize_text_field( wp_unslash( $_POST['free_pro'] ) ) : '',
766 - );
1043 + protected function wdkit_browse_page() {
1044 + $args = $this->wdkit_parse_args( $_POST );
767 1045
768 - $response = $this->wkit_api_call( $array_data, 'browse_widget' );
769 - $success = ! empty( $response['success'] ) ? $response['success'] : false;
1046 + $response = WDesignKit_Data_Query::get_data( 'browse_page', $args );
770 1047
771 - if ( empty( $success ) ) {
772 - $response = array(
773 - 'success' => false,
774 - 'message' => esc_html__( 'Data Not Found', 'wdesignkit' ),
775 - 'description' => esc_html__( 'Widget List Not Found', 'wdesignkit' ),
776 -
777 - 'widgets' => array(),
778 - 'widgetscount' => 0,
779 - 'showwidgets' => 0,
780 - );
781 -
782 - wp_send_json( $response );
1048 + if ( is_wp_error( $response ) ) {
1049 + wp_send_json( array(
1050 + 'success' => false,
1051 + 'message' => $response->get_error_message(),
1052 + ) );
783 1053 wp_die();
784 1054 }
785 1055
786 - $response = json_decode( wp_json_encode( $response['data'] ), true );
1056 + $manage_licence = array();
1057 + $manage_licence['theplus_elementor_addon'] = ! empty( defined( 'THEPLUS_VERSION' ) ) ? true : false;
1058 + $manage_licence['tpag'] = ! empty( defined( 'TPGBP_VERSION' ) ) ? true : false;
1059 + $manage_licence['elementor-pro'] = ! empty( defined( 'ELEMENTOR_PRO_VERSION' ) ) ? true : false;
1060 + $response['manage_licence'] = $manage_licence;
787 1061
788 1062 wp_send_json( $response );
789 1063 wp_die();
790 1064 }
@@ -808,9 +1082,8 @@
808 1082 *
809 1083 * It is Use for remove or delete template.
810 1084 *
811 1085 * @since 1.0.0
812 - * @access public
813 1086 */
814 1087 protected function wdkit_template_remove() {
815 1088 $args = $this->wdkit_parse_args( $_POST );
816 1089
@@ -816,15 +1089,14 @@
816 1089
817 1090 $user_email = strtolower( sanitize_email( $args['email'] ) );
818 1091 $response = '';
819 1092
1093 + // Bug D fix: response()->json() is Laravel syntax — causes PHP fatal. Use plain array.
820 1094 if ( empty( $user_email ) || empty( $args['template_id'] ) ) {
821 - $response = response()->json(
822 - array(
823 - 'message' => $this->e_msg_login,
824 - 'description' => $this->e_desc_login,
825 - 'success' => true,
826 - )
1095 + $response = array(
1096 + 'message' => $this->e_msg_login,
1097 + 'description' => $this->e_desc_login,
1098 + 'success' => false,
827 1099 );
828 1100
829 1101 wp_send_json( $response );
830 1102 wp_die();
@@ -845,9 +1117,12 @@
845 1117 *
846 1118 * @since 1.0.0
847 1119 */
848 1120 protected function wdkit_put_save_template() {
849 - $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false;
1121 + $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false;
1122 + $post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( wp_unslash( $_POST['post_id'] ) ) : '';
1123 + $builder = isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : '';
1124 +
850 1125 $response = '';
851 1126
852 1127 if ( empty( $email ) ) {
853 1128 $response = array(
@@ -865,14 +1140,19 @@
865 1140 $args = $this->wdkit_parse_args( $_POST );
866 1141 $args['token'] = $this->wdkit_login_user_token( $email );
867 1142 unset( $args['email'] );
868 1143
1144 + if( 'elementor' === $builder ){
1145 + $args['data'] = base64_decode( $args['data'] );
1146 + } else if ( 'gutenberg' === $builder ) {
1147 + $args['data'] = base64_decode( $args['data'] );
1148 + }
1149 +
869 1150 global $post;
870 1151
871 - $post_id = get_the_ID();
872 1152 $custom_fields = array();
873 1153 if ( ! empty( $post_id ) ) {
874 - $meta_fields = get_post_custom( get_the_ID() );
1154 + $meta_fields = get_post_custom( $post_id );
875 1155
876 1156 foreach ( $meta_fields as $key => $value ) {
877 1157 if ( str_contains( $key, 'nxt-' ) ) {
878 1158 $custom_fields[ $key ] = $value;
@@ -887,8 +1167,24 @@
887 1167 }
888 1168
889 1169 $response = WDesignKit_Data_Query::get_data( 'save_template', $args );
890 1170
1171 + /**
1172 + * The cloud call can come back as a WP_Error (timeout, DNS, refused) or with an
1173 + * empty / unparsable body, which json_decode()s to null. Forwarding that as-is
1174 + * makes admin-ajax answer with a literal `null` that the editor then reads
1175 + * `.id` off, killing the whole app. Normalise it to the failure shape used above.
1176 + */
1177 + if ( is_wp_error( $response ) || ! is_array( $response ) ) {
1178 + $response = array(
1179 + 'id' => 0,
1180 + 'editpage' => '',
1181 + 'message' => esc_html__( 'Template Not Saved !', 'wdesignkit' ),
1182 + 'description' => is_wp_error( $response ) ? $response->get_error_message() : esc_html__( 'Could not reach the WDesignKit server. Please try again.', 'wdesignkit' ),
1183 + 'success' => false,
1184 + );
1185 + }
1186 +
891 1187 wp_send_json( $response );
892 1188 wp_die();
893 1189 }
894 1190
@@ -893,8 +1189,2288 @@
893 1189 }
894 1190
895 1191 /**
896 1192 *
1193 + * It is Use for update save template image.
1194 + *
1195 + * @since 2.0.6
1196 + */
1197 + protected function wdkit_update_save_temp_image() {
1198 + $temp_content = isset( $_POST['temp_content'] ) ? esc_url_raw( $_POST['temp_content'] ) : '';
1199 + $content_name = isset( $_POST['content_name'] ) ? sanitize_text_field( $_POST['content_name'] ) : '';
1200 + $token = isset( $_POST['token'] ) ? sanitize_text_field( $_POST['token'] ) : '';
1201 + $user_type = isset( $_POST['user_type'] ) ? sanitize_text_field( $_POST['user_type'] ) : '';
1202 + $type = isset( $_POST['content_type'] ) ? sanitize_text_field( $_POST['content_type'] ) : '';
1203 + $id = isset( $_POST['id'] ) ? sanitize_text_field( $_POST['id'] ) : '';
1204 + if ( empty( $temp_content ) || empty( $user_type ) || empty( $id ) || empty( $token ) ) {
1205 + $response = array(
1206 + 'message' => __( 'Data not found', 'wdesignkit' ),
1207 + 'description' => __( 'Data not found', 'wdesignkit' ),
1208 + 'success' => false,
1209 + );
1210 + } else {
1211 + $temp_content = str_replace( '\\', '', $temp_content );
1212 + // SSRF guard (CWE-918): validate the resolved host before fetching a caller-supplied URL.
1213 + $fetched = wdesignkit_safe_remote_get( $temp_content );
1214 + $temp_content = is_wp_error( $fetched ) ? '' : wp_remote_retrieve_body( $fetched );
1215 + $temp_content = base64_encode( $temp_content );
1216 +
1217 + $args = array(
1218 + 'token' => $token,
1219 + 'template_id' => $id,
1220 + 'name' => $content_name,
1221 + 'content' => $temp_content,
1222 + 'type' => $type,
1223 + );
1224 +
1225 + $response = $this->wkit_api_call( $args, 'save_images' );
1226 + $success = ! empty( $response['success'] ) ? $response['success'] : false;
1227 +
1228 + if ( $success ) {
1229 + $response = array(
1230 + 'data' => $response['data'],
1231 + 'success' => true,
1232 + );
1233 + } else {
1234 + $response = array(
1235 + 'message' => __( 'API Error', 'wdesignkit' ),
1236 + 'description' => __( 'API Error', 'wdesignkit' ),
1237 + 'data' => $response['data'],
1238 + 'success' => false,
1239 + );
1240 + }
1241 + }
1242 +
1243 + wp_send_json( $response );
1244 + wp_die();
1245 + }
1246 +
1247 + /**
1248 + *
1249 + * It is Use for save image to WordPress Media Library.
1250 + *
1251 + * @since 2.3.3
1252 + */
1253 + protected function wdkit_save_wp_images() {
1254 +
1255 + $image_url = isset( $_POST['image'] ) ? sanitize_text_field( $_POST['image'] ) : '';
1256 +
1257 + $response = self::wdkit_sideload_image_data( $image_url );
1258 +
1259 + wp_send_json( $response );
1260 + wp_die();
1261 + }
1262 +
1263 + /**
1264 + * Sideload one image into the media library and describe the outcome.
1265 + *
1266 + * Extracted verbatim from wdkit_save_wp_images() so the same code can be reached
1267 + * without `$_POST` — Wdkit_Import_Media::sideload() calls this, which is why the PHP
1268 + * runner does not need its own copy of the guard or the importer hash stamps.
1269 + *
1270 + * The AJAX action above is now a thin adapter over this and its response is byte-for-byte
1271 + * what it was before.
1272 + *
1273 + * @since 2.6.5
1274 + *
1275 + * @param string $image_url External image URL.
1276 + * @return array{message:string,description:string,success:bool,url?:string}
1277 + */
1278 + public static function wdkit_sideload_image_data( $image_url ) {
1279 +
1280 + // media_sideload_image() generates every registered thumbnail size, which decodes
1281 + // the full source bitmap. Same guard as the page import.
1282 + self::wdkit_guard_oversized_images();
1283 +
1284 + $image_url = is_string( $image_url ) ? $image_url : '';
1285 +
1286 + // And the same time-limit headroom the import requests take. A full-resolution stock
1287 + // original can spend more than PHP's default 30s inside Imagick generating subsizes
1288 + // on its own, and this endpoint is called once per picked image - so without this a
1289 + // single large pick fatals the request and the image is silently never copied.
1290 + // Harmless no-op where set_time_limit() is disabled.
1291 + if ( function_exists( 'set_time_limit' ) ) {
1292 + @set_time_limit( 300 );
1293 + }
1294 +
1295 + // The URL arrives as the argument, never from $_POST. wdkit_save_wp_images() is the
1296 + // only caller that has a request to read, and it passes what it read; re-reading it
1297 + // here blanked the URL for every other caller - Wdkit_Import_Media::sideload() runs
1298 + // on cron, where there is no $_POST at all, so every blog-post featured image was
1299 + // silently skipped.
1300 + if ( empty( $image_url ) ) {
1301 + $response = array(
1302 + 'message' => __( 'No Image Provided', 'wdesignkit' ),
1303 + 'description' => __( 'No Image URL provided for save.', 'wdesignkit' ),
1304 + 'success' => false,
1305 + );
1306 + } else {
1307 +
1308 + // Cap first, then validate, then fetch - so the exact string handed to the
1309 + // fetcher is the string that was checked. Validating before cap_pexels_source()
1310 + // rewrites the URL would leave a gap the moment the rewriter changes.
1311 + $image_url = Wdkit_Image_Guard::cap_pexels_source( $image_url );
1312 +
1313 + // The URL is client-controlled and goes straight to an outbound fetch that
1314 + // writes into uploads, so it needs the same DNS + reserved-range check as
1315 + // every other fetch in the plugin. sanitize_text_field() on the caller's side
1316 + // is a formatting function and permits http://127.0.0.1/, http://[::1]/ and
1317 + // internal hostnames.
1318 + //
1319 + // Returns rather than wp_send_json(): this is now a shared helper whose
1320 + // contract is "describe the outcome", and half its callers are not serving a
1321 + // request. Dying here would have taken down the cron worker mid-import.
1322 + if ( ! function_exists( 'wdesignkit_validate_external_url' ) || ! wdesignkit_validate_external_url( $image_url ) ) {
1323 + return array(
1324 + 'message' => __( 'Upload Failed', 'wdesignkit' ),
1325 + 'description' => __( 'That image URL was rejected.', 'wdesignkit' ),
1326 + 'success' => false,
1327 + );
1328 + }
1329 +
1330 + /* media_sideload_image() lives in wp-admin/includes/media.php and reaches
1331 + * download_url() in wp-admin/includes/file.php. An admin-ajax request already has
1332 + * both, which is why the browser importer never needed this — but a cron request
1333 + * has neither, so every image this touched died with "Call to undefined function
1334 + * download_url()". On a remote import that meant all six blog posts failed while
1335 + * the run still reported success. The sibling call site in
1336 + * class-wdkit-import-temp-ajax.php has always loaded these three. */
1337 + require_once ABSPATH . 'wp-admin/includes/file.php';
1338 + require_once ABSPATH . 'wp-admin/includes/media.php';
1339 + require_once ABSPATH . 'wp-admin/includes/image.php';
1340 +
1341 + $attachment_id = media_sideload_image( $image_url, 0, null, 'id' );
1342 +
1343 + if ( is_wp_error( $attachment_id ) ) {
1344 + $response = array(
1345 + 'message' => __( 'Upload Failed', 'wdesignkit' ),
1346 + 'description' => $attachment_id->get_error_message(),
1347 + 'success' => false,
1348 + );
1349 + } else {
1350 + $saved_url = wp_get_attachment_url( $attachment_id );
1351 +
1352 + // Elementor's importer skips an image only when it finds
1353 + // _elementor_source_image_hash matching sha1 of the URL it is given. The
1354 + // content we hand it now carries this local URL, so stamp the hash of that
1355 + // URL too - without it Elementor re-downloads a file already on disk and
1356 + // leaves a "-1" duplicate behind for every image on every page that uses it.
1357 + if ( $saved_url ) {
1358 + update_post_meta( $attachment_id, '_elementor_source_image_hash', sha1( $saved_url ) );
1359 +
1360 + // Same purpose for the block importer, which keys off its own meta.
1361 + update_post_meta( $attachment_id, 'tpgb_source_image_key', sha1( $saved_url ) );
1362 + }
1363 +
1364 + $response = array(
1365 + 'message' => __( 'Image Saved', 'wdesignkit' ),
1366 + 'description' => __( 'Image successfully saved to Media Library.', 'wdesignkit' ),
1367 + 'success' => true,
1368 + 'url' => $saved_url,
1369 + 'id' => $attachment_id,
1370 + );
1371 + }
1372 +
1373 + }
1374 +
1375 + return $response;
1376 + }
1377 +
1378 + /**
1379 + * The two bundle/media primitives the PHP import runner needs, reachable without a
1380 + * request.
1381 + *
1382 + * Both of these are the optimisations that made the browser import fast, and both were
1383 + * private to the AJAX actions that introduced them - so the runner could not use them
1384 + * and grew its own slower equivalents instead: one cloud round trip per template rather
1385 + * than one for the whole kit, and every image downloaded inside the import request
1386 + * rather than swept up afterwards. These wrappers are the whole of what it took to
1387 + * share them; the implementations are untouched and the AJAX actions still call them
1388 + * exactly as before.
1389 + *
1390 + * @since 2.7.2
1391 + *
1392 + * @param array $request Same fields the `wkit_fetch_site_bundle` action reads, except
1393 + * `template_ids` may be an array. Pass `widgets_only` to have the
1394 + * assembled bundle parked in the transient for later stages.
1395 + * @param string $token Cloud token the caller already resolved.
1396 + * @return array|WP_Error Decoded bundle.
1397 + */
1398 + public function wdkit_site_bundle_for( $request, $token = '' ) {
1399 + return $this->wdkit_fetch_site_bundle( is_array( $request ) ? $request : array(), $token );
1400 + }
1401 +
1402 + /**
1403 + * Queue the background media sweep for pages the runner just created.
1404 + *
1405 + * @since 2.7.2
1406 + *
1407 + * @param array $pages List of {post_id, builder, image_urls}.
1408 + * @return array Same result the AJAX action returns.
1409 + */
1410 + public function wdkit_schedule_media_sync_for( $pages ) {
1411 + return $this->wkit_schedule_deferred_media_sync( is_array( $pages ) ? $pages : array() );
1412 + }
1413 +
1414 + /**
1415 + * Run one stage of the PHP import runner for the Kit Import wizard.
1416 + *
1417 + * The wizard's replacement for its ~25-call orchestration: four calls, one per stage,
1418 + * on this same action with the same nonce and the same `manage_options` check enforced
1419 + * by the router above. Nothing about authentication changes, and no endpoint is added —
1420 + * this is one more `case` on a router that already had 86.
1421 + *
1422 + * The response is shaped by Wdkit_Import_Wizard so the existing progress UI can consume
1423 + * it without new widgets. See that class for the field-by-field mapping.
1424 + *
1425 + * @since 2.6.5
1426 + *
1427 + * @return array
1428 + */
1429 + protected function wdkit_import_stage() {
1430 + if ( ! class_exists( 'Wdkit_Import_Wizard' ) ) {
1431 + return array(
1432 + 'success' => false,
1433 + 'engine' => 'unavailable',
1434 + 'message' => esc_html__( 'Import engine is not available.', 'wdesignkit' ),
1435 + );
1436 + }
1437 +
1438 + $site_obj = isset( $_POST['site_obj'] ) ? json_decode( wp_unslash( $_POST['site_obj'] ), true ) : array();
1439 + $templates = isset( $_POST['templates'] ) ? json_decode( wp_unslash( $_POST['templates'] ), true ) : array();
1440 + $catalogue = isset( $_POST['plugin_catalogue'] ) ? json_decode( wp_unslash( $_POST['plugin_catalogue'] ), true ) : array();
1441 + $document = isset( $_POST['ai_document'] ) ? json_decode( wp_unslash( $_POST['ai_document'] ), true ) : null;
1442 +
1443 + return Wdkit_Import_Wizard::run_stage(
1444 + array(
1445 + 'stage' => isset( $_POST['stage'] ) ? sanitize_text_field( wp_unslash( $_POST['stage'] ) ) : '',
1446 + 'session_id' => isset( $_POST['session_id'] ) ? sanitize_text_field( wp_unslash( $_POST['session_id'] ) ) : '',
1447 + 'retry' => ! empty( $_POST['retry'] ) && 'yes' === sanitize_text_field( wp_unslash( $_POST['retry'] ) ),
1448 + 'kit_id' => isset( $_POST['kit_id'] ) ? sanitize_text_field( wp_unslash( $_POST['kit_id'] ) ) : '',
1449 + 'builder' => isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : 'elementor',
1450 + 'site_obj' => is_array( $site_obj ) ? $site_obj : array(),
1451 + 'templates' => is_array( $templates ) ? $templates : array(),
1452 + 'plugin_catalogue' => is_array( $catalogue ) ? $catalogue : array(),
1453 + 'ai_document' => $document,
1454 + )
1455 + );
1456 + }
1457 +
1458 + /**
1459 + * Register the pages a kit import just created for background media sideloading.
1460 + *
1461 + * Called once, after every page in the kit has been inserted with its content still
1462 + * pointing at the source CDN's image URLs (see the `defer_media` branch of
1463 + * import_page_section_content()/wdkit_media_import()). Nothing here downloads anything -
1464 + * it only schedules one wp-cron event per page, so the import request itself returns
1465 + * immediately and the user sees "Site Ready" without waiting on image processing.
1466 + *
1467 + * @since 2.6.6
1468 + *
1469 + * @return array Response payload.
1470 + */
1471 + protected function wkit_schedule_deferred_media_sync( $pages = null ) {
1472 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the AJAX router verified the nonce; the runner passes its own array and has no request to verify.
1473 + if ( ! is_array( $pages ) ) {
1474 + $pages = isset( $_POST['pages'] ) ? json_decode( wp_unslash( $_POST['pages'] ), true ) : array();
1475 + }
1476 +
1477 + if ( ! is_array( $pages ) || empty( $pages ) ) {
1478 + return array(
1479 + 'success' => false,
1480 + 'message' => __( 'No pages to sync.', 'wdesignkit' ),
1481 + );
1482 + }
1483 +
1484 + $scheduled = 0;
1485 + $stagger = 0;
1486 + $order = array();
1487 +
1488 + // The home page goes first, with no delay.
1489 + //
1490 + // Every page used to be staggered two seconds apart, so on a nine-page kit the last
1491 + // event was not even due until ~18s after the import finished - and nothing spawns
1492 + // cron, so the queue sat until the next request arrived. That next request is the
1493 + // user clicking "Preview Site", which is why the first look showed media that had
1494 + // not been fetched yet and a refresh appeared to fix it. The home page is the one
1495 + // they land on, so it is fetched first and immediately; the rest keep the stagger so
1496 + // they do not all pile into one cron tick.
1497 + // page_on_front is usually still unset here: this runs before the finalize request
1498 + // that assigns it. So fall back to the same title rule the importer itself uses to
1499 + // choose the front page (see "Front page and shop page" in wkit_import_site_bundle).
1500 + $front_id = (int) get_option( 'page_on_front' );
1501 +
1502 + if ( ! $front_id ) {
1503 + foreach ( $pages as $page ) {
1504 + $candidate = isset( $page['post_id'] ) ? (int) $page['post_id'] : 0;
1505 +
1506 + if ( ! $candidate ) {
1507 + continue;
1508 + }
1509 +
1510 + $title_lower = strtolower( (string) get_the_title( $candidate ) );
1511 +
1512 + if ( false !== strpos( $title_lower, 'home' ) || false !== strpos( $title_lower, 'landing' ) ) {
1513 + $front_id = $candidate;
1514 + break;
1515 + }
1516 + }
1517 + }
1518 +
1519 + usort(
1520 + $pages,
1521 + function ( $a, $b ) use ( $front_id ) {
1522 + $a_id = isset( $a['post_id'] ) ? (int) $a['post_id'] : 0;
1523 + $b_id = isset( $b['post_id'] ) ? (int) $b['post_id'] : 0;
1524 +
1525 + if ( $front_id && $a_id === $front_id ) {
1526 + return -1;
1527 + }
1528 +
1529 + if ( $front_id && $b_id === $front_id ) {
1530 + return 1;
1531 + }
1532 +
1533 + return 0;
1534 + }
1535 + );
1536 +
1537 + foreach ( $pages as $page ) {
1538 + $post_id = isset( $page['post_id'] ) ? (int) $page['post_id'] : 0;
1539 + $builder = isset( $page['builder'] ) ? sanitize_key( $page['builder'] ) : '';
1540 +
1541 + $image_urls = ( isset( $page['image_urls'] ) && is_array( $page['image_urls'] ) )
1542 + ? array_values( array_filter( array_map( 'esc_url_raw', $page['image_urls'] ) ) )
1543 + : array();
1544 +
1545 + // esc_url_raw() formats, it does not authorise. This list came from the browser.
1546 + $image_urls = self::wdkit_filter_fetchable_urls( $image_urls );
1547 +
1548 + if ( ! $post_id || empty( $image_urls ) || ! get_post( $post_id ) ) {
1549 + continue;
1550 + }
1551 +
1552 + // Stagger each page's event a couple of seconds apart instead of firing every
1553 + // page's sideload loop in the very same wp-cron tick - except the home page,
1554 + // which is due right away because it is what the user previews.
1555 + $is_front = ( $front_id && $post_id === $front_id );
1556 +
1557 + wp_schedule_single_event(
1558 + $is_front ? time() : ( time() + 2 + $stagger ),
1559 + 'wdkit_async_sideload_page_images',
1560 + array( $post_id, $builder, $image_urls )
1561 + );
1562 +
1563 + if ( ! $is_front ) {
1564 + $stagger += 2;
1565 + }
1566 +
1567 + $order[] = $post_id;
1568 + ++$scheduled;
1569 + }
1570 +
1571 + return array(
1572 + 'success' => true,
1573 + 'message' => __( 'Media sync scheduled.', 'wdesignkit' ),
1574 + 'scheduled' => $scheduled,
1575 + // The order they were queued in, home page first. Returned so the success
1576 + // screen's own drain uses the same priority instead of deciding again.
1577 + 'order' => $order,
1578 + );
1579 + }
1580 +
1581 + /**
1582 + * Sideload one deferred page's media now, instead of waiting for wp-cron.
1583 + *
1584 + * The import deliberately leaves media to a background pass so the kit imports fast, and
1585 + * that is worth keeping. What it cost was the first preview: nothing spawns cron, so the
1586 + * queue waited for the next request - the user's own "Preview Site" click - and the page
1587 + * they landed on still pointed at the source CDN with some references not written yet.
1588 + * Refreshing appeared to fix it because that first view was what started the queue.
1589 + *
1590 + * So the success screen drives the queue itself over this endpoint once the import is
1591 + * already reported done. It adds nothing to the import: by then the UI says complete and
1592 + * the user is reading it. The scheduled cron events stay as the fallback for anyone who
1593 + * navigates away, and each is cleared as its page is handled here so the work is not done
1594 + * twice. It also covers hosts where a loopback cron spawn would never fire at all -
1595 + * DISABLE_WP_CRON, or a system cron that only runs every few minutes.
1596 + *
1597 + * @since 2.7.1
1598 + *
1599 + * @return array
1600 + */
1601 + protected function wkit_run_deferred_media_now() {
1602 +
1603 + $post_id = isset( $_POST['post_id'] ) ? (int) $_POST['post_id'] : 0;
1604 + $builder = isset( $_POST['builder'] ) ? sanitize_key( wp_unslash( $_POST['builder'] ) ) : '';
1605 +
1606 + $image_urls = isset( $_POST['image_urls'] ) ? json_decode( wp_unslash( $_POST['image_urls'] ), true ) : array();
1607 + $image_urls = is_array( $image_urls )
1608 + ? array_values( array_filter( array_map( 'esc_url_raw', $image_urls ) ) )
1609 + : array();
1610 +
1611 + // esc_url_raw() formats, it does not authorise - same gate the scheduler applies,
1612 + // because this list comes from the browser just the same.
1613 + $image_urls = self::wdkit_filter_fetchable_urls( $image_urls );
1614 +
1615 + if ( ! $post_id || ! get_post( $post_id ) || empty( $image_urls ) ) {
1616 + return array(
1617 + 'success' => false,
1618 + 'message' => __( 'Nothing to sync for this page.', 'wdesignkit' ),
1619 + );
1620 + }
1621 +
1622 + // Drop the queued event first, so a cron tick landing mid-run cannot repeat it.
1623 + wp_clear_scheduled_hook( 'wdkit_async_sideload_page_images', array( $post_id, $builder, $image_urls ) );
1624 +
1625 + $this->wdkit_async_sideload_page_images( $post_id, $builder, $image_urls );
1626 +
1627 + return array(
1628 + 'success' => true,
1629 + 'message' => __( 'Media synced.', 'wdesignkit' ),
1630 + 'post_id' => $post_id,
1631 + );
1632 + }
1633 +
1634 + /**
1635 + * Walk Elementor `elements` data replacing every deferred CDN media reference with the
1636 + * local attachment sideloaded for it.
1637 + *
1638 + * Handles two shapes: a media control (`{ id, url }` siblings, e.g. an image control or a
1639 + * has_sizes background-image control) gets both keys corrected, matching what
1640 + * wdkit_repair_attachment_ids() does for the synchronous path. Any other string simply
1641 + * gets the source URL substring swapped, which catches URLs embedded in HTML/text
1642 + * controls that never had an `id` sibling to begin with.
1643 + *
1644 + * @since 2.6.6
1645 + *
1646 + * @param mixed $node Elementor data, walked recursively.
1647 + * @param array $url_map Source URL => array( 'id' => local attachment ID, 'url' => local URL ).
1648 + * @return mixed Data with local media references.
1649 + */
1650 + /**
1651 + * Every spelling of one URL that can appear in stored content.
1652 + *
1653 + * A URL with a query string does not survive the round trip through the browser and
1654 + * WordPress's own escaping as the raw string this code holds. `&` comes back HTML-entity
1655 + * encoded as `&#038;`, sometimes `&amp;`, and - where a value is escaped twice on the way
1656 + * into post content - as `&amp;#038;`. So the picked stock images were downloaded and
1657 + * stored correctly while the page kept the remote reference: the rewriters matched the
1658 + * raw key, the content held an encoded one, and the substring test never fired. Measured:
1659 + * 3 attachments created, 12 remote references still served on the landing page.
1660 + *
1661 + * Longest first, so a replacement pass cannot rewrite the `&amp;` inside `&amp;#038;`
1662 + * and strand the remainder.
1663 + *
1664 + * @since 2.7.1
1665 + *
1666 + * @param string $url Canonical URL as this code holds it.
1667 + * @return array<string> Distinct variants to search for, longest first.
1668 + */
1669 + private static function wdkit_url_encoding_variants( $url ) {
1670 +
1671 + if ( ! is_string( $url ) || '' === $url || false === strpos( $url, '&' ) ) {
1672 + return array( (string) $url );
1673 + }
1674 +
1675 + $variants = array(
1676 + str_replace( '&', '&amp;#038;', $url ),
1677 + str_replace( '&', '&#038;', $url ),
1678 + str_replace( '&', '&amp;amp;', $url ),
1679 + str_replace( '&', '&amp;', $url ),
1680 + $url,
1681 + );
1682 +
1683 + $variants = array_values( array_unique( $variants ) );
1684 +
1685 + usort(
1686 + $variants,
1687 + static function ( $a, $b ) {
1688 + return strlen( $b ) - strlen( $a );
1689 + }
1690 + );
1691 +
1692 + return $variants;
1693 + }
1694 +
1695 + /**
1696 + * Replace every encoding of $source_url in $subject with $replacement.
1697 + *
1698 + * @since 2.7.1
1699 + *
1700 + * @param string $subject Content to rewrite.
1701 + * @param string $source_url URL to look for, in any encoding.
1702 + * @param string $replacement Local URL to write in its place.
1703 + * @return string
1704 + */
1705 + private static function wdkit_replace_url_all_encodings( $subject, $source_url, $replacement ) {
1706 +
1707 + foreach ( self::wdkit_url_encoding_variants( $source_url ) as $variant ) {
1708 + if ( false !== strpos( $subject, $variant ) ) {
1709 + $subject = str_replace( $variant, $replacement, $subject );
1710 + }
1711 + }
1712 +
1713 + return $subject;
1714 + }
1715 +
1716 + /**
1717 + * Re-point any media control still holding a foreign URL at the local file, when one
1718 + * already exists.
1719 + *
1720 + * Downloads nothing. It only asks "has this exact source URL already been sideloaded?"
1721 + * via the same _wdkit_deferred_source_url lookup the cron uses for de-duplication, and
1722 + * rewrites url + id when the answer is yes.
1723 + *
1724 + * Why this is needed as a separate pass: the finalize sweep deliberately skips any page
1725 + * with media still queued (see wdkit_sweep_attachment_ids() - sweeping there would turn
1726 + * the import request into the media importer), and nothing re-sweeps once the queue
1727 + * drains. wdkit_repair_attachment_ids() cannot cover it either - it is restricted to
1728 + * local URLs by design. So a control whose rewrite was missed had nothing left to fix it.
1729 + *
1730 + * Observed on a Taj Bakery import: a tp-video-player kept
1731 + * etemplates.wdesignkit.com/...mp4 in mp4_link.url with its id blanked, while the file
1732 + * sat in the library as attachment 173 with a byte-identical _wdkit_deferred_source_url.
1733 + * Every image on the same page had been rewritten; only that control was left behind.
1734 + *
1735 + * @since 2.7.1
1736 + *
1737 + * @param mixed $node Elementor data, walked recursively.
1738 + * @param int $fixed Running count of controls repaired, by reference.
1739 + * @return mixed The data with any resolvable foreign reference pointed local.
1740 + */
1741 + /**
1742 + * Apply a url_map to every OTHER post that still references one of its source URLs.
1743 + *
1744 + * A deferred URL is scheduled per page, but the same file is often referenced by several
1745 + * pages, and the de-duplication in this callback means only the first event actually
1746 + * downloads it. Every event rewrites its own $post_id and nothing else - so a sibling
1747 + * page whose own event did not carry that URL (a scheduling gap, or a page whose list was
1748 + * assembled before the reference existed) keeps the remote URL with no later pass to fix
1749 + * it: the finalize sweep skips queued pages by design, and wdkit_repair_attachment_ids()
1750 + * only looks at local URLs.
1751 + *
1752 + * Measured on a Taj Bakery import: the About Us page kept
1753 + * etemplates.wdesignkit.com/...mp4 in a tp-video-player while the file sat in the library
1754 + * as a video/mp4 attachment. Handing that page's own callback the URL rewrote it
1755 + * correctly, which is what showed the pipeline was sound and the routing was not.
1756 + *
1757 + * Bounded and cheap: one LIKE query per distinct source URL in this event's map, and only
1758 + * posts that actually contain the string are touched. Downloads nothing.
1759 + *
1760 + * @since 2.7.1
1761 + *
1762 + * @param array $url_map Source URL => array( id, url ), as built by this callback.
1763 + * @param int $skip_id The post this callback already rewrote.
1764 + * @return int Number of sibling posts rewritten.
1765 + */
1766 + private function wdkit_apply_url_map_to_siblings( array $url_map, $skip_id ) {
1767 +
1768 + global $wpdb;
1769 +
1770 + if ( empty( $url_map ) ) {
1771 + return 0;
1772 + }
1773 +
1774 + $candidates = array();
1775 +
1776 + foreach ( array_keys( $url_map ) as $source_url ) {
1777 + // Both spellings: raw in post_content, slash-escaped inside _elementor_data JSON.
1778 + foreach ( array( $source_url, str_replace( '/', '\/', $source_url ) ) as $needle ) {
1779 + $ids = $wpdb->get_col(
1780 + $wpdb->prepare(
1781 + "SELECT DISTINCT p.ID FROM {$wpdb->posts} p
1782 + LEFT JOIN {$wpdb->postmeta} m ON m.post_id = p.ID AND m.meta_key = '_elementor_data'
1783 + WHERE p.post_status = 'publish'
1784 + AND p.post_type IN ( 'page', 'post', 'nxt_builder', 'elementor_library' )
1785 + AND p.ID <> %d
1786 + AND ( p.post_content LIKE %s OR m.meta_value LIKE %s )
1787 + LIMIT 50",
1788 + (int) $skip_id,
1789 + '%' . $wpdb->esc_like( $needle ) . '%',
1790 + '%' . $wpdb->esc_like( $needle ) . '%'
1791 + )
1792 + );
1793 +
1794 + foreach ( (array) $ids as $id ) {
1795 + $candidates[ (int) $id ] = true;
1796 + }
1797 + }
1798 + }
1799 +
1800 + if ( empty( $candidates ) ) {
1801 + return 0;
1802 + }
1803 +
1804 + $rewritten = 0;
1805 +
1806 + foreach ( array_keys( $candidates ) as $sibling_id ) {
1807 + $changed = false;
1808 + $raw = get_post_meta( $sibling_id, '_elementor_data', true );
1809 + $data = is_array( $raw ) ? $raw : json_decode( (string) $raw, true );
1810 +
1811 + if ( is_array( $data ) ) {
1812 + $data = $this->wdkit_replace_deferred_media( $data, $url_map );
1813 +
1814 + $relinked = 0;
1815 + $data = self::wdkit_relink_known_foreign_media( $data, $relinked );
1816 +
1817 + // wp_slash() for the same reason as the primary write above.
1818 + update_post_meta( $sibling_id, '_elementor_data', wp_slash( wp_json_encode( $data ) ) );
1819 + $changed = true;
1820 + }
1821 +
1822 + $post = get_post( $sibling_id );
1823 +
1824 + if ( $post && ! empty( $post->post_content ) ) {
1825 + $content = $post->post_content;
1826 +
1827 + foreach ( $url_map as $source_url => $local ) {
1828 + $content = self::wdkit_replace_url_all_encodings( $content, $source_url, $local['url'] );
1829 + }
1830 +
1831 + if ( $content !== $post->post_content ) {
1832 + self::wdkit_write_post_content( $sibling_id, $content );
1833 + $changed = true;
1834 + }
1835 + }
1836 +
1837 + if ( $changed ) {
1838 + self::wdkit_invalidate_elementor_page_cache( $sibling_id );
1839 + ++$rewritten;
1840 + }
1841 + }
1842 +
1843 + return $rewritten;
1844 + }
1845 +
1846 + private static function wdkit_relink_known_foreign_media( $node, &$fixed = 0 ) {
1847 +
1848 + if ( ! is_array( $node ) ) {
1849 + return $node;
1850 + }
1851 +
1852 + if ( isset( $node['url'] ) && is_string( $node['url'] ) && '' !== $node['url']
1853 + && array_key_exists( 'id', $node ) && self::wdkit_is_foreign_media_url( $node['url'] )
1854 + ) {
1855 + $existing = self::wdkit_deferred_attachment_for_url( $node['url'] );
1856 +
1857 + if ( $existing && self::wdkit_is_usable_attachment( $existing ) ) {
1858 + $local = wp_get_attachment_url( $existing );
1859 +
1860 + if ( $local ) {
1861 + $node['url'] = $local;
1862 + $node['id'] = (int) $existing;
1863 + ++$fixed;
1864 + }
1865 + }
1866 + }
1867 +
1868 + foreach ( $node as $key => $value ) {
1869 + if ( is_array( $value ) ) {
1870 + $node[ $key ] = self::wdkit_relink_known_foreign_media( $value, $fixed );
1871 + }
1872 + }
1873 +
1874 + return $node;
1875 + }
1876 +
1877 + private function wdkit_replace_deferred_media( $node, array $url_map ) {
1878 +
1879 + if ( is_array( $node ) ) {
1880 +
1881 + if ( isset( $node['url'] ) && is_string( $node['url'] ) && array_key_exists( 'id', $node ) ) {
1882 +
1883 + $hit = isset( $url_map[ $node['url'] ] ) ? $node['url'] : null;
1884 +
1885 + // The node's own url can be entity-encoded too, so an exact-key lookup alone
1886 + // misses it - and this is the branch that repairs the `id`, without which the
1887 + // widget resolves nothing even once the url is right.
1888 + if ( null === $hit ) {
1889 + $decoded = html_entity_decode( $node['url'], ENT_QUOTES, 'UTF-8' );
1890 + $decoded = html_entity_decode( $decoded, ENT_QUOTES, 'UTF-8' );
1891 +
1892 + if ( isset( $url_map[ $decoded ] ) ) {
1893 + $hit = $decoded;
1894 + }
1895 + }
1896 +
1897 + if ( null !== $hit ) {
1898 + $node['id'] = $url_map[ $hit ]['id'];
1899 + $node['url'] = $url_map[ $hit ]['url'];
1900 + }
1901 + }
1902 +
1903 + foreach ( $node as $key => $value ) {
1904 + $node[ $key ] = $this->wdkit_replace_deferred_media( $value, $url_map );
1905 + }
1906 +
1907 + return $node;
1908 + }
1909 +
1910 + if ( is_string( $node ) && '' !== $node ) {
1911 + foreach ( $url_map as $source_url => $local ) {
1912 + $node = self::wdkit_replace_url_all_encodings( $node, $source_url, $local['url'] );
1913 + }
1914 + }
1915 +
1916 + return $node;
1917 + }
1918 +
1919 + /**
1920 + * Find the attachment a previous wdkit_async_sideload_page_images() run already made
1921 + * for this exact source URL.
1922 + *
1923 + * @since 2.6.6
1924 + *
1925 + * @param string $source_url Source CDN/stock-provider URL.
1926 + * @return int Attachment ID, or 0 when none exists yet.
1927 + */
1928 + private static function wdkit_deferred_attachment_for_url( $source_url ) {
1929 + $existing = get_posts(
1930 + array(
1931 + 'post_type' => 'attachment',
1932 + 'post_status' => 'inherit',
1933 + 'posts_per_page' => 5,
1934 + 'fields' => 'ids',
1935 + 'no_found_rows' => true,
1936 + 'meta_key' => '_wdkit_deferred_source_url',
1937 + 'meta_value' => $source_url,
1938 + )
1939 + );
1940 +
1941 + // The row is not the answer - the file is.
1942 + //
1943 + // This used to return (int) $existing[0] straight out of the query, and every caller
1944 + // then tested wp_get_attachment_url(), which is truthy for a record with nothing
1945 + // behind it: with no _wp_attached_file it falls back to the guid, and wp_insert_post()
1946 + // defaults an attachment's guid to its attachment-page permalink. So a dead record
1947 + // answered the lookup with http://site/hand25-png/, the reuse branch took it as a hit
1948 + // and skipped the download, and the page shipped an <img src> pointing at an HTML
1949 + // page. Measured on all three shapes such a record comes in - no file with metadata,
1950 + // no file without metadata, no _wp_attached_file at all - wp_get_attachment_url()
1951 + // returned a truthy url for every one.
1952 + //
1953 + // A site that imported a kit on a build before Wdkit_Import_Images::import() started
1954 + // refusing unknown file types is full of these, which is why the fixed build still
1955 + // rendered broken images: the import adopted the old rows instead of downloading
1956 + // again, and re-importing reused them just the same. Rejecting them here is what makes
1957 + // a re-import repair the page - the caller falls through to its normal download and
1958 + // then rewrites url + id from the attachment it actually created.
1959 + //
1960 + // Validated in the lookup rather than at each call site because there are three, they
1961 + // were not all guarded, and a fourth would not be either.
1962 + foreach ( (array) $existing as $candidate ) {
1963 + if ( self::wdkit_is_usable_attachment( $candidate ) ) {
1964 + return (int) $candidate;
1965 + }
1966 +
1967 + self::wdkit_forget_reuse_keys( $candidate );
1968 + }
1969 +
1970 + return 0;
1971 + }
1972 +
1973 + /**
1974 + * Take a broken attachment out of every reuse index this pipeline consults.
1975 + *
1976 + * Gating the lookup stops this plugin adopting a dead record, but the record keeps
1977 + * answering the other two indexes - `_elementor_source_image_hash`, which Elementor's own
1978 + * Import_Images::import() resolves before it downloads anything, and
1979 + * `tpgb_source_image_key`, which the block importer uses the same way. Left in place it
1980 + * would still be handed back through them.
1981 + *
1982 + * Deletes the meta, never the attachment. A row with no file is worthless to this
1983 + * importer, but it is not this importer's to remove: a media-offload plugin legitimately
1984 + * has no local file, and a customer's library is not something an import routine should be
1985 + * deleting from. Dropping it out of the indexes is enough to make the next import fetch a
1986 + * fresh copy.
1987 + *
1988 + * @since 2.7.1
1989 + *
1990 + * @param int $id Attachment that failed wdkit_is_usable_attachment().
1991 + * @return void
1992 + */
1993 + private static function wdkit_forget_reuse_keys( $id ) {
1994 +
1995 + $id = (int) $id;
1996 +
1997 + if ( ! $id ) {
1998 + return;
1999 + }
2000 +
2001 + foreach ( array( '_wdkit_deferred_source_url', '_elementor_source_image_hash', 'tpgb_source_image_key' ) as $key ) {
2002 + delete_post_meta( $id, $key );
2003 + }
2004 + }
2005 +
2006 + /**
2007 + * Largest media file the background pass will pull down, in bytes.
2008 + *
2009 + * Video is the only thing a kit references that can plausibly run to hundreds of megabytes.
2010 + * Nothing decodes it - download_url() streams to a temp file - so this is not about memory;
2011 + * it is about one oversized file monopolising a cron event's time limit and disk while
2012 + * every URL queued behind it goes unprocessed.
2013 + *
2014 + * @since 2.7.1
2015 + */
2016 + const WDKIT_DEFERRED_MEDIA_MAX_BYTES = 52428800; // 50 MB.
2017 +
2018 + /**
2019 + * Sideload one deferred media URL, choosing the right primitive for what it is.
2020 + *
2021 + * media_sideload_image() hard-rejects any extension outside jpg|jpeg|jpe|png|gif|webp
2022 + * ("Invalid image URL") before it downloads anything, so it can never localise a video -
2023 + * nor, as it happens, an .avif, which this importer has been collecting for the background
2024 + * pass all along and which was therefore being dropped just as silently.
2025 + *
2026 + * Images keep going through media_sideload_image() exactly as before: it is the path that
2027 + * has been exercised on every import, and nothing here is worth changing about it. Anything
2028 + * else goes through download_url() + media_handle_sideload(), which is generic, validates
2029 + * the real type with wp_check_filetype_and_ext(), and is already what this file uses for
2030 + * featured images.
2031 + *
2032 + * @since 2.7.1
2033 + *
2034 + * @param string $source_url Remote URL.
2035 + * @return int|WP_Error Attachment ID, or the error that stopped it.
2036 + */
2037 + /**
2038 + * Keep only the URLs this site is willing to fetch.
2039 + *
2040 + * Applied where a client-supplied `image_urls` list is accepted, so a URL that points at
2041 + * an internal address is never written into the cron table in the first place. The cron
2042 + * callback validates again before fetching - that is the authoritative gate, since a cron
2043 + * arg can outlive the request that created it.
2044 + *
2045 + * Drops only what is *provably* unsafe, which is deliberately narrower than the sink.
2046 + * wdesignkit_validate_external_url() fails closed on a host it cannot resolve ("an
2047 + * unresolvable host cannot be proven public"), and that is the right answer immediately
2048 + * before a fetch - but the wrong one here. An import resolves dozens of hosts at once, so
2049 + * a transient DNS failure during that burst is ordinary; refusing on it would drop the URL
2050 + * before it was ever scheduled, and nothing would retry it - the image would stay remote
2051 + * permanently. Deferring that case to the cron costs nothing: the sink re-checks and fails
2052 + * closed there, by which time DNS has usually recovered.
2053 + *
2054 + * The threat this closes is an internal URL being persisted into a cron argument, and such
2055 + * a URL resolves by definition - to a private or reserved address - so it is caught here.
2056 + *
2057 + * @since 2.7.1
2058 + *
2059 + * @param array $urls Candidate URLs, already esc_url_raw()'d.
2060 + * @return array<string> The subset that is not provably unsafe.
2061 + */
2062 + private static function wdkit_filter_fetchable_urls( $urls ) {
2063 +
2064 + if ( ! is_array( $urls ) || empty( $urls ) ) {
2065 + return array();
2066 + }
2067 +
2068 + // One verdict per host, not per URL. A kit page carries well over a hundred media
2069 + // URLs across a handful of hosts, and both the validator and the resolve check below
2070 + // hit DNS - so without this, a page's worth of images costs a hundred-plus lookups
2071 + // inside the import request, and on a host with slow resolution that is time the user
2072 + // spends watching a progress bar. The decision is a property of the host, so caching
2073 + // it changes nothing about the outcome. Request-scoped: a static here lives exactly
2074 + // as long as the import request that built it.
2075 + static $verdict = array();
2076 +
2077 + $allowed = array();
2078 +
2079 + foreach ( $urls as $url ) {
2080 + if ( ! is_string( $url ) || '' === $url ) {
2081 + continue;
2082 + }
2083 +
2084 + $scheme = strtolower( (string) wp_parse_url( $url, PHP_URL_SCHEME ) );
2085 + $host = (string) wp_parse_url( $url, PHP_URL_HOST );
2086 +
2087 + // Nothing downstream can make sense of these, whatever DNS says.
2088 + if ( ! in_array( $scheme, array( 'http', 'https' ), true ) || '' === $host ) {
2089 + continue;
2090 + }
2091 +
2092 + if ( ! function_exists( 'wdesignkit_validate_external_url' ) ) {
2093 + // No validator loaded: keep the URL and let the sink - which requires the
2094 + // validator and refuses without it - make the call. Dropping everything here
2095 + // would silently disable deferred media rather than secure it.
2096 + $allowed[] = $url;
2097 + continue;
2098 + }
2099 +
2100 + $key = strtolower( $host );
2101 +
2102 + if ( ! isset( $verdict[ $key ] ) ) {
2103 + // The validator takes a URL, but everything it decides on - resolution and
2104 + // address range - depends only on the host, so a bare origin is a faithful
2105 + // probe for the whole host and the answer applies to every URL on it.
2106 + $probe = $scheme . '://' . $host . '/';
2107 +
2108 + // Keep it unless it is provably unsafe: rejected AND resolvable, which
2109 + // together mean it resolved to an address we refuse to reach. A rejection
2110 + // with no resolution is DNS being briefly unavailable - the sink re-checks
2111 + // and fails closed there, so deferring that call loses nothing.
2112 + $verdict[ $key ] = wdesignkit_validate_external_url( $probe )
2113 + || ! self::wdkit_host_resolves( $host );
2114 + }
2115 +
2116 + if ( $verdict[ $key ] ) {
2117 + $allowed[] = $url;
2118 + }
2119 + }
2120 +
2121 + return $allowed;
2122 + }
2123 +
2124 + /**
2125 + * Does this host resolve to anything right now?
2126 + *
2127 + * Used only to tell a validation failure caused by a private address apart from one
2128 + * caused by DNS being briefly unavailable. A literal IP always counts as resolved.
2129 + *
2130 + * @since 2.7.1
2131 + *
2132 + * @param string $host Hostname from the URL.
2133 + * @return bool True when at least one address came back.
2134 + */
2135 + private static function wdkit_host_resolves( $host ) {
2136 +
2137 + // wp_parse_url() returns an IPv6 literal still wrapped in its URL brackets
2138 + // (`[::1]`), which is not valid IP syntax - so without this the address fails the
2139 + // test below, resolves to nothing, and is misread as "DNS is down" rather than
2140 + // "this is loopback".
2141 + $host = trim( $host, '[]' );
2142 +
2143 + if ( filter_var( $host, FILTER_VALIDATE_IP ) ) {
2144 + return true;
2145 + }
2146 +
2147 + if ( function_exists( 'gethostbynamel' ) ) {
2148 + $v4 = gethostbynamel( $host );
2149 +
2150 + if ( is_array( $v4 ) && ! empty( $v4 ) ) {
2151 + return true;
2152 + }
2153 + }
2154 +
2155 + if ( function_exists( 'dns_get_record' ) ) {
2156 + $v6 = @dns_get_record( $host, DNS_AAAA ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- a lookup failure is the answer, not an error.
2157 +
2158 + if ( is_array( $v6 ) && ! empty( $v6 ) ) {
2159 + return true;
2160 + }
2161 + }
2162 +
2163 + $resolved = gethostbyname( $host ); // Returns the host unchanged on failure.
2164 +
2165 + return ( $resolved && $resolved !== $host && (bool) filter_var( $resolved, FILTER_VALIDATE_IP ) );
2166 + }
2167 +
2168 + private static function wdkit_sideload_deferred_media( $source_url ) {
2169 +
2170 + if ( ! class_exists( 'Wdkit_Image_Guard' ) ) {
2171 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php';
2172 + }
2173 +
2174 + /**
2175 + * Largest deferred media file to download, in bytes.
2176 + *
2177 + * Filterable because the right ceiling is a property of the host, not of the kit:
2178 + * shared hosting with a 30s cap and a slow link needs a lower one than a VPS.
2179 + *
2180 + * @since 2.7.1
2181 + *
2182 + * @param int $max_bytes Default limit.
2183 + * @param string $source_url URL being considered.
2184 + */
2185 + $max_bytes = (int) apply_filters( 'wdkit_deferred_media_max_bytes', self::WDKIT_DEFERRED_MEDIA_MAX_BYTES, $source_url );
2186 +
2187 + // Same SSRF check every other outbound fetch in this plugin goes through - DNS
2188 + // resolution plus a reserved-range test, so private, loopback and link-local
2189 + // addresses are rejected. It gates BOTH branches below, and has to: this URL list
2190 + // arrives from the browser in $_POST (see the `image_urls` contract on the bundle and
2191 + // pages routes), so it is client-controlled, and esc_url_raw() is a formatting
2192 + // function, not an access-control one. Running in a cron event does not make the
2193 + // origin of the URL any more trustworthy - it removes the last chance to ask.
2194 + // Nonce + manage_options do not close this: they establish who is asking, not where
2195 + // the server may be pointed, and neither stops a CSRF-assisted request. download_url()
2196 + // blocks only the plain 169.254.169.254 metadata case, leaving multi-A-record hosts,
2197 + // internal IPv6 and same-host URLs reachable.
2198 + if ( ! function_exists( 'wdesignkit_validate_external_url' ) || ! wdesignkit_validate_external_url( $source_url ) ) {
2199 + return new WP_Error( 'wdkit_media_url_rejected', 'Refusing to fetch a URL that failed validation.' );
2200 + }
2201 +
2202 + $ext = strtolower( (string) pathinfo( (string) wp_parse_url( $source_url, PHP_URL_PATH ), PATHINFO_EXTENSION ) );
2203 +
2204 + // The set WordPress's own image sideloader accepts. Everything it accepts, it keeps.
2205 + if ( in_array( $ext, array( 'jpg', 'jpeg', 'jpe', 'png', 'gif', 'webp' ), true ) ) {
2206 + return media_sideload_image( $source_url, 0, null, 'id' );
2207 + }
2208 +
2209 + // Ask before fetching. A HEAD that answers with a size lets an oversized file be skipped
2210 + // for the cost of one request; a host that does not answer with one is not a reason to
2211 + // refuse, so the download proceeds and the on-disk size below is the real backstop.
2212 + $head = wp_remote_head( $source_url, array( 'timeout' => 15, 'redirection' => 3 ) );
2213 +
2214 + if ( ! is_wp_error( $head ) ) {
2215 + $length = (int) wp_remote_retrieve_header( $head, 'content-length' );
2216 +
2217 + if ( $length > $max_bytes ) {
2218 + return new WP_Error(
2219 + 'wdkit_media_too_large',
2220 + sprintf( 'Skipped %s: %d bytes exceeds the %d byte limit.', $source_url, $length, $max_bytes )
2221 + );
2222 + }
2223 + }
2224 +
2225 + $tmp = download_url( $source_url, 300 );
2226 +
2227 + if ( is_wp_error( $tmp ) ) {
2228 + return $tmp;
2229 + }
2230 +
2231 + // The backstop for a host that sent no Content-Length, or lied about it.
2232 + $size = (int) @filesize( $tmp );
2233 +
2234 + if ( $size > $max_bytes ) {
2235 + @unlink( $tmp );
2236 +
2237 + return new WP_Error(
2238 + 'wdkit_media_too_large',
2239 + sprintf( 'Skipped %s: %d bytes on disk exceeds the %d byte limit.', $source_url, $size, $max_bytes )
2240 + );
2241 + }
2242 +
2243 + // From the URL *path*, so a query string never ends up in the filename - the same reason
2244 + // Wdkit_Import_Images does this rather than basename() the whole URL.
2245 + $file_name = Wdkit_Image_Guard::filename_from_url( $source_url );
2246 +
2247 + // filename_from_url() only matches a path that already names a known media type, so
2248 + // it returns '' for the extensionless CDN URLs this branch exists to handle. Fall back
2249 + // to the last path segment and let the sniffing below name the type.
2250 + if ( '' === $file_name ) {
2251 + $path = (string) wp_parse_url( $source_url, PHP_URL_PATH );
2252 + $file_name = sanitize_file_name( (string) wp_basename( rtrim( $path, '/' ) ) );
2253 + }
2254 +
2255 + if ( '' === $file_name ) {
2256 + @unlink( $tmp );
2257 +
2258 + return new WP_Error( 'wdkit_media_no_filename', 'Could not derive a filename from the URL.' );
2259 + }
2260 +
2261 + // The template CDN serves media from extensionless URLs and declares the type in the
2262 + // response, so there is nothing in the path to name the file after. Left as-is,
2263 + // wp_check_filetype_and_ext() below has no extension to check and refuses the upload -
2264 + // which is why extensionless media was never localised even once it was collected.
2265 + // The type is read from the bytes on disk, not from the URL or a response header, so a
2266 + // mislabelled file still cannot smuggle in an extension it does not match.
2267 + if ( '' === (string) pathinfo( $file_name, PATHINFO_EXTENSION ) ) {
2268 + $sniffed = wp_check_filetype_and_ext( $tmp, $file_name . '.jpg' );
2269 + $type = ! empty( $sniffed['type'] ) ? $sniffed['type'] : '';
2270 +
2271 + if ( '' === $type && function_exists( 'getimagesize' ) ) {
2272 + $info = @getimagesize( $tmp ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- malformed input is the case being detected.
2273 + $type = ( is_array( $info ) && ! empty( $info['mime'] ) ) ? $info['mime'] : '';
2274 + }
2275 +
2276 + $ext = '';
2277 +
2278 + foreach ( wp_get_mime_types() as $exts => $mime ) {
2279 + if ( $mime === $type ) {
2280 + $ext = strtok( $exts, '|' );
2281 + break;
2282 + }
2283 + }
2284 +
2285 + if ( '' === $ext ) {
2286 + @unlink( $tmp );
2287 +
2288 + return new WP_Error(
2289 + 'wdkit_media_unknown_type',
2290 + sprintf( 'Skipped %s: no extension in the URL and the downloaded file is not a recognised media type.', $source_url )
2291 + );
2292 + }
2293 +
2294 + $file_name .= '.' . $ext;
2295 + }
2296 +
2297 + // media_handle_sideload() runs the upload through wp_check_filetype_and_ext(), so a file
2298 + // whose contents do not match its extension is refused here rather than stored.
2299 + $attachment_id = media_handle_sideload(
2300 + array(
2301 + 'name' => $file_name,
2302 + 'tmp_name' => $tmp,
2303 + ),
2304 + 0
2305 + );
2306 +
2307 + // On failure media_handle_sideload() has already removed the temp file; on success it
2308 + // moved it. This only catches the path where it returned early without doing either.
2309 + if ( is_wp_error( $attachment_id ) && file_exists( $tmp ) ) {
2310 + @unlink( $tmp );
2311 + }
2312 +
2313 + return $attachment_id;
2314 + }
2315 +
2316 + /**
2317 + * The original-image URL behind a WordPress size variant, or '' when it is not one.
2318 + *
2319 + * `photo-300x298.png` -> `photo.png`. Only the trailing -WIDTHxHEIGHT before the extension
2320 + * is removed; query strings are kept.
2321 + *
2322 + * @param string $url Image URL.
2323 + * @return string
2324 + */
2325 + private static function wdkit_size_variant_of( $url ) {
2326 + $original = preg_replace( '/-\d+x\d+(\.(?:jpe?g|png|gif|webp|avif))(?=$|\?)/i', '$1', (string) $url, 1 );
2327 +
2328 + return ( is_string( $original ) && $original !== $url ) ? $original : '';
2329 + }
2330 +
2331 + /**
2332 + * Background half of the deferred media sideload: download every image a kit page's
2333 + * content deferred at create time and repoint the saved page content at the local copies.
2334 + *
2335 + * Runs off a `wp_schedule_single_event()` (registered in the constructor), well after the
2336 + * import request that created the page has already returned. Updates post meta / the
2337 + * post row directly rather than going through Elementor's/the block importer's normal
2338 + * save path, so this never re-runs widget ID generation or reflows the layout.
2339 + *
2340 + * @since 2.6.6
2341 + *
2342 + * @param int $post_id Page created with source CDN URLs still in its content.
2343 + * @param string $builder 'elementor' or anything else (treated as Gutenberg/HTML content).
2344 + * @param array $image_urls Source CDN image URLs referenced by that page.
2345 + */
2346 + public function wdkit_async_sideload_page_images( $post_id, $builder, $image_urls ) {
2347 +
2348 + $post_id = (int) $post_id;
2349 + $image_urls = is_array( $image_urls ) ? $image_urls : array();
2350 +
2351 + if ( ! $post_id || empty( $image_urls ) || ! get_post( $post_id ) ) {
2352 + return;
2353 + }
2354 +
2355 + if ( ! function_exists( 'media_sideload_image' ) ) {
2356 + require_once ABSPATH . 'wp-admin/includes/media.php';
2357 + require_once ABSPATH . 'wp-admin/includes/file.php';
2358 + require_once ABSPATH . 'wp-admin/includes/image.php';
2359 + }
2360 +
2361 + // The synchronous import paths all extend this (see import_page_section_content(),
2362 + // wkit_import_site_bundle()) for the same reason: Imagick thumbnail generation on an
2363 + // oversized source image can outrun PHP's default 30s limit on its own, before this
2364 + // loop even gets to a second URL. Unlike those requests, nothing is waiting on this
2365 + // one - it is a wp-cron callback - so there is no reason not to give it the same
2366 + // headroom; without it, a fatal here kills the whole event mid-loop (no unschedule,
2367 + // no url_map, no content rewrite) and every image after the slow one in $image_urls
2368 + // is left pointing at the source CDN with nothing left to ever retry it.
2369 + if ( function_exists( 'set_time_limit' ) ) {
2370 + @set_time_limit( 300 );
2371 + }
2372 +
2373 + // Same guard the synchronous path uses - a background run is no less able to blow
2374 + // through the memory limit on an oversized source image.
2375 + self::wdkit_guard_oversized_images();
2376 +
2377 + $url_map = array();
2378 +
2379 + /* Attachment ids sideloaded or found in this run, keyed by fetch URL, so an image the
2380 + * kit references in several sizes is fetched once. */
2381 + $attached = array();
2382 +
2383 + /* Sideload one URL (or reuse an earlier copy), returning its attachment id or 0. */
2384 + $attach = function ( $fetch_url ) use ( &$attached ) {
2385 + if ( isset( $attached[ $fetch_url ] ) ) {
2386 + return $attached[ $fetch_url ];
2387 + }
2388 +
2389 + $attachment_id = self::wdkit_deferred_attachment_for_url( $fetch_url );
2390 +
2391 + if ( ! $attachment_id ) {
2392 + $attachment_id = self::wdkit_sideload_deferred_media( $fetch_url );
2393 +
2394 + if ( is_wp_error( $attachment_id ) ) {
2395 + if ( 'wdkit_media_too_large' === $attachment_id->get_error_code() ) {
2396 + error_log( 'WDKIT deferred media: ' . $attachment_id->get_error_message() );
2397 + $attached[ $fetch_url ] = 0;
2398 + return 0;
2399 + }
2400 +
2401 + usleep( 300000 );
2402 + $attachment_id = self::wdkit_sideload_deferred_media( $fetch_url );
2403 + }
2404 +
2405 + if ( is_wp_error( $attachment_id ) || ! $attachment_id ) {
2406 + $attached[ $fetch_url ] = 0;
2407 + return 0;
2408 + }
2409 +
2410 + $local_url = wp_get_attachment_url( $attachment_id );
2411 +
2412 + if ( $local_url ) {
2413 + update_post_meta( $attachment_id, '_elementor_source_image_hash', sha1( $local_url ) );
2414 + update_post_meta( $attachment_id, 'tpgb_source_image_key', sha1( $local_url ) );
2415 + }
2416 +
2417 + update_post_meta( $attachment_id, '_wdkit_deferred_source_url', $fetch_url );
2418 + }
2419 +
2420 + $attached[ $fetch_url ] = (int) $attachment_id;
2421 +
2422 + return (int) $attachment_id;
2423 + };
2424 +
2425 + $urls = array_values( array_unique( array_filter( array_map( 'esc_url_raw', $image_urls ) ) ) );
2426 +
2427 + /* Full-size images first, so a size variant further down the list finds its original
2428 + * already in the library instead of starting another download. */
2429 + usort(
2430 + $urls,
2431 + function ( $a, $b ) {
2432 + return (int) ( '' !== self::wdkit_size_variant_of( $a ) ) - (int) ( '' !== self::wdkit_size_variant_of( $b ) );
2433 + }
2434 + );
2435 +
2436 + foreach ( $urls as $source_url ) {
2437 + $fetch_url = Wdkit_Image_Guard::cap_pexels_source( $source_url );
2438 + $map_keys = ( $fetch_url !== $source_url ) ? array( $source_url, $fetch_url ) : array( $source_url );
2439 +
2440 + /* A WordPress size variant (photo-300x298.png) of an image whose original
2441 + * (photo.png) can be fetched: import the original once and point this reference at
2442 + * the closest size WordPress generates for it. A kit page carried 31 such variants
2443 + * among 50 images, each downloaded and resized on its own (ClickUp single-template
2444 + * speed-up). Falls back to the variant itself when the original is unavailable. */
2445 + $original = self::wdkit_size_variant_of( $fetch_url );
2446 +
2447 + if ( '' !== $original ) {
2448 + $original_id = $attach( $original );
2449 +
2450 + if ( $original_id ) {
2451 + preg_match( '/-(\d+)x(\d+)\.[a-z0-9]+(?:$|\?)/i', $fetch_url, $dims );
2452 +
2453 + $sized_url = ! empty( $dims )
2454 + ? wp_get_attachment_image_url( $original_id, array( (int) $dims[1], (int) $dims[2] ) )
2455 + : '';
2456 + $sized_url = $sized_url ? $sized_url : wp_get_attachment_url( $original_id );
2457 +
2458 + if ( $sized_url ) {
2459 + foreach ( $map_keys as $map_key ) {
2460 + $url_map[ $map_key ] = array(
2461 + 'id' => $original_id,
2462 + 'url' => $sized_url,
2463 + );
2464 + }
2465 +
2466 + continue;
2467 + }
2468 + }
2469 + }
2470 +
2471 + $attachment_id = $attach( $fetch_url );
2472 + $local_url = $attachment_id ? wp_get_attachment_url( $attachment_id ) : '';
2473 +
2474 + if ( ! $local_url ) {
2475 + continue;
2476 + }
2477 +
2478 + foreach ( $map_keys as $map_key ) {
2479 + $url_map[ $map_key ] = array(
2480 + 'id' => $attachment_id,
2481 + 'url' => $local_url,
2482 + );
2483 + }
2484 + }
2485 +
2486 + if ( empty( $url_map ) ) {
2487 + return;
2488 + }
2489 +
2490 + if ( 'elementor' === $builder && did_action( 'elementor/loaded' ) ) {
2491 + $raw = get_post_meta( $post_id, '_elementor_data', true );
2492 + $data = is_array( $raw ) ? $raw : json_decode( $raw, true );
2493 +
2494 + if ( is_array( $data ) ) {
2495 + $data = $this->wdkit_replace_deferred_media( $data, $url_map );
2496 +
2497 + // The finalize sweep deliberately skips a page while its media is queued here
2498 + // (see wdkit_sweep_attachment_ids()), and the remap above only touches the URLs
2499 + // this event was handed. Anything else on the page that already had a local URL
2500 + // beside a stale id - the picked stock images the AI path substitutes by URL
2501 + // alone, for instance - would otherwise never get its id corrected by anyone:
2502 + // the sweep passed the page over, and this callback never looked. Same walk the
2503 + // sweep would have done, restricted to local URLs so nothing downloads from here.
2504 + $data = self::wdkit_repair_attachment_ids( $data, true );
2505 +
2506 + // Last resort for anything the rewrite above missed: point it at a file that
2507 + // already exists locally. Costs one indexed meta lookup per foreign control
2508 + // and never downloads, so it is safe to run on every event.
2509 + $relinked = 0;
2510 + $data = self::wdkit_relink_known_foreign_media( $data, $relinked );
2511 +
2512 + if ( $relinked && defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
2513 + error_log( sprintf( '[WDKIT] relinked %d already-local media reference(s) on post %d', $relinked, $post_id ) );
2514 + }
2515 +
2516 + // wp_slash() is not optional here. update_post_meta() runs wp_unslash() on
2517 + // the value, so handing it raw JSON strips every backslash the encoder put
2518 + // in - `\/`, `\"`, `\uXXXX` - and what lands in the row is no longer
2519 + // parseable. Elementor then reads zero elements off the page: it regenerates
2520 + // post-N.css as empty, enqueues nothing, and the theme falls back to the
2521 + // post_content mirror, so the page renders its text completely unstyled.
2522 + // Elementor's own writer does the same thing for the same reason - see the
2523 + // wp_slash() in Document::save_elements().
2524 + update_post_meta( $post_id, '_elementor_data', wp_slash( wp_json_encode( $data ) ) );
2525 +
2526 + // Elementor mirrors the rendered output into post_content as well, and that
2527 + // copy is what themes and search read. Rewriting only _elementor_data leaves
2528 + // it pointing at the template's own site - invisible in the editor, but the
2529 + // front end still requests those images from the source CDN. Same substring
2530 + // swap the non-Elementor branch below performs, and through the same writer,
2531 + // so the markup is not put through kses in this userless context.
2532 + $post = get_post( $post_id );
2533 +
2534 + if ( $post && ! empty( $post->post_content ) ) {
2535 + $content = $post->post_content;
2536 +
2537 + foreach ( $url_map as $source_url => $local ) {
2538 + $content = self::wdkit_replace_url_all_encodings( $content, $source_url, $local['url'] );
2539 + }
2540 +
2541 + // Same last-resort relink as the builder data above. post_content is the
2542 + // copy the theme actually renders, so a reference left here is visible
2543 + // even when _elementor_data is correct.
2544 + foreach ( self::wdkit_collect_foreign_media_urls( $content ) as $stray ) {
2545 + $existing = self::wdkit_deferred_attachment_for_url( $stray );
2546 +
2547 + if ( $existing && self::wdkit_is_usable_attachment( $existing ) ) {
2548 + $local_stray = wp_get_attachment_url( $existing );
2549 +
2550 + if ( $local_stray ) {
2551 + $content = self::wdkit_replace_url_all_encodings( $content, $stray, $local_stray );
2552 + }
2553 + }
2554 + }
2555 +
2556 + if ( $content !== $post->post_content ) {
2557 + self::wdkit_write_post_content( $post_id, $content );
2558 + }
2559 + }
2560 +
2561 + // Invalidate last, and only for this page. Last, because a render that
2562 + // starts between the two writes above would otherwise regenerate its CSS
2563 + // and element cache from half-rewritten data and then cache that. Only this
2564 + // page, because the alternative is a site-wide flush - see
2565 + // wdkit_invalidate_elementor_page_cache().
2566 + self::wdkit_invalidate_elementor_page_cache( $post_id );
2567 +
2568 + // Then fix any OTHER page referencing the same files. Only the first event to
2569 + // see a URL downloads it; without this, a sibling that references the same
2570 + // file but whose own event never carried that URL keeps the remote reference
2571 + // permanently. See wdkit_apply_url_map_to_siblings().
2572 + $siblings = $this->wdkit_apply_url_map_to_siblings( $url_map, $post_id );
2573 +
2574 + if ( $siblings && defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
2575 + error_log( sprintf( '[WDKIT] repaired %d sibling page(s) sharing media with post %d', $siblings, $post_id ) );
2576 + }
2577 + }
2578 + } else {
2579 + $post = get_post( $post_id );
2580 +
2581 + if ( $post ) {
2582 + $content = $post->post_content;
2583 +
2584 + foreach ( $url_map as $source_url => $local ) {
2585 + $content = self::wdkit_replace_url_all_encodings( $content, $source_url, $local['url'] );
2586 + }
2587 +
2588 + if ( $content !== $post->post_content ) {
2589 + // The replace above fixes every `url` but leaves the `id` sitting beside it
2590 + // still pointing at the source site's attachment. Blocks that render from
2591 + // the url look correct either way; the ones that render through the
2592 + // attachment id (tp-team-listing, tp-testimonials, tp-image, and
2593 + // tp-container backgrounds) resolve nothing and render empty. Declining
2594 + // returns null, in which case the url-only content above is kept exactly
2595 + // as it is today.
2596 + $remapped = $this->wdkit_remap_attachment_ids( $content, $url_map );
2597 +
2598 + if ( null !== $remapped ) {
2599 + $content = $remapped;
2600 + }
2601 +
2602 + // Separate concern, separate pass: the id remap above fixes the block
2603 + // attribute, but process_image() also stamped the id into the rendered
2604 + // <img> tag's class at import time, when the image was still remote.
2605 + // Declining leaves the content exactly as the step above produced it.
2606 + $classes_fixed = $this->wdkit_remap_wp_image_classes( $content, $url_map );
2607 +
2608 + if ( null !== $classes_fixed ) {
2609 + $content = $classes_fixed;
2610 + }
2611 +
2612 + self::wdkit_write_post_content( $post_id, $content );
2613 +
2614 + // The page's per-post CSS file (backgrounds, and anything else the block
2615 + // CSS generator resolves through an attachment id) was built synchronously
2616 + // at import time, before this deferred pass ever ran - so it still reads
2617 + // whatever the deferred images' source urls/ids were back then. Rebuild it
2618 + // now that post_content points at the local copies, the same way the
2619 + // synchronous create path already does right after wp_insert_post().
2620 + self::wdkit_rebuild_block_css( $post_id );
2621 + }
2622 + }
2623 + }
2624 +
2625 + clean_post_cache( $post_id );
2626 + }
2627 +
2628 + /**
2629 + * Throw away Elementor's cached render of one page, and only that page.
2630 + *
2631 + * The deferred pass rewrites `_elementor_data` behind Elementor's back, so three
2632 + * per-post caches are left describing the version that still pointed at the source
2633 + * CDN: the generated CSS file (background-image rules resolve through the attachment
2634 + * id), `_elementor_element_cache` (the rendered element markup) and
2635 + * `_elementor_page_assets`. All three have to go for the next render to be correct.
2636 + *
2637 + * This used to be `files_manager->clear_cache()`, which is not a per-page call at all:
2638 + * it globs and unlinks *every* file in uploads/elementor/css, then runs
2639 + * delete_post_meta_by_key() for all three keys across *every* post, and drops the
2640 + * global CSS option. One call measured here destroyed 16 CSS files and 15 meta rows.
2641 + *
2642 + * That is actively harmful in this context, because one of these events is scheduled
2643 + * per imported page - eleven of them, two seconds apart, on a kit this size. A visitor
2644 + * who loads any page during that window gets HTML whose `<link>` points at
2645 + * post-N.css, and the file is deleted out from under the browser before it is fetched:
2646 + * the page renders completely unstyled. It self-heals on a later render, which is
2647 + * exactly why it reads as "the layout breaks when I refresh".
2648 + *
2649 + * Scoping it also means pages the deferred pass never touched keep their CSS instead
2650 + * of being made to regenerate for nothing.
2651 + *
2652 + * @since 2.6.6
2653 + *
2654 + * @param int $post_id Page whose cached render is now stale.
2655 + * @return void
2656 + */
2657 + public static function wdkit_invalidate_elementor_page_cache( $post_id ) {
2658 +
2659 + $post_id = (int) $post_id;
2660 +
2661 + if ( ! $post_id ) {
2662 + return;
2663 + }
2664 +
2665 + // Same call Elementor's own Document::save() makes - deletes the css file and the
2666 + // _elementor_css meta for this one post.
2667 + if ( class_exists( '\\Elementor\\Core\\Files\\CSS\\Post' ) ) {
2668 + \Elementor\Core\Files\CSS\Post::create( $post_id )->delete();
2669 + } else {
2670 + // Older Elementor without the CSS file classes: the meta alone still forces a
2671 + // regeneration, which is what actually matters here.
2672 + delete_post_meta( $post_id, '_elementor_css' );
2673 + }
2674 +
2675 + // Document::save() pairs the css delete with delete_cache(); that method is
2676 + // protected, and these are the keys it clears.
2677 + delete_post_meta( $post_id, '_elementor_element_cache' );
2678 + delete_post_meta( $post_id, '_elementor_page_assets' );
2679 + }
2680 +
2681 + /**
2682 + *
2683 + * Get Elementor Global color and Typography.
2684 + *
2685 + * @since 1.1.16
2686 + */
2687 + /**
2688 + * Kit settings holding The Plus Addons' own globals.
2689 + *
2690 + * These sit in the Elementor kit's `_elementor_page_settings` alongside Elementor's
2691 + * system_colors / system_typography, but the save flow only ever collected the four
2692 + * Elementor keys. Widgets reference an entry in these lists by its `_id` through a
2693 + * `tp_global_preset` setting, so a template saved without them travels with the
2694 + * reference but not the definition - which is why imported sections come in missing
2695 + * their button styling, radii and shadows.
2696 + *
2697 + * @since 2.6.4
2698 + *
2699 + * @return array Kit setting keys.
2700 + */
2701 + private function wdkit_tp_global_kit_keys() {
2702 + return array(
2703 + 'tp_global_button_style_list',
2704 + 'tp_global_dimensions_list',
2705 + 'tp_global_box_shadow_list',
2706 + 'tp_global_gradient_list',
2707 + 'tp_global_gsap_list',
2708 + 'tp_global_scroll_animation_list',
2709 + 'tp_text_global_gsap_list',
2710 + 'tp_image_global_gsap_list',
2711 + );
2712 + }
2713 +
2714 + /**
2715 + * Merge incoming Plus globals into the active kit, keyed by `_id`.
2716 + *
2717 + * Entries are matched on their `_id`, never on position: an existing entry is always
2718 + * left as it is, and only genuinely new ones are appended. That matters because
2719 + * widgets - and the entries themselves, a button style points at dimension and shadow
2720 + * entries - resolve by `_id`. Renumbering or overwriting would repoint references on
2721 + * the destination site's own content.
2722 + *
2723 + * @since 2.6.4
2724 + *
2725 + * @param array $incoming Lists captured with the template.
2726 + * @return bool True when the kit was changed.
2727 + */
2728 + /**
2729 + * Global colour / typography ids this site already defines.
2730 + *
2731 + * @since 2.6.4
2732 + *
2733 + * @param array $kit_meta Kit `_elementor_page_settings`.
2734 + * @return array{color:array<string,bool>,typography:array<string,bool>}
2735 + */
2736 + private function wdkit_known_global_ids( $kit_meta ) {
2737 + $known = array(
2738 + 'color' => array(),
2739 + 'typography' => array(),
2740 + );
2741 +
2742 + $sources = array(
2743 + 'color' => array( 'system_colors', 'custom_colors' ),
2744 + 'typography' => array( 'system_typography', 'custom_typography' ),
2745 + );
2746 +
2747 + foreach ( $sources as $kind => $keys ) {
2748 + foreach ( $keys as $key ) {
2749 + if ( empty( $kit_meta[ $key ] ) || ! is_array( $kit_meta[ $key ] ) ) {
2750 + continue;
2751 + }
2752 +
2753 + foreach ( $kit_meta[ $key ] as $entry ) {
2754 + if ( ! empty( $entry['_id'] ) ) {
2755 + $known[ $kind ][ $entry['_id'] ] = true;
2756 + }
2757 + }
2758 + }
2759 + }
2760 +
2761 + return $known;
2762 + }
2763 +
2764 + /**
2765 + * Make one incoming Plus global's colour / font references resolvable here.
2766 + *
2767 + * A Plus global can point at an Elementor global: the "Primary Button" entry holds
2768 + * `__globals__: { text_color: "globals/colors?id=72e09b4", … }`, which The Plus Addons
2769 + * turns into `var(--e-global-color-72e09b4)`. Elementor only emits that variable for ids
2770 + * present in the kit, so on a site without `72e09b4` the button renders with no colour.
2771 + *
2772 + * Two cases, and the difference is deliberate:
2773 + *
2774 + * - The site ALREADY defines that id — leave the reference alone. The button then picks
2775 + * up the destination's own colour, which is the point of a global. Their palette is
2776 + * never read from or written to beyond this check.
2777 + * - The site does NOT define it — write the captured value straight into the entry and
2778 + * drop the reference, so it renders as designed.
2779 + *
2780 + * Nothing is ever added to the user's global colours or fonts. An earlier version injected
2781 + * the missing definitions into their palette, which made the reference resolve but grew
2782 + * their Site Settings by every colour an imported template happened to use.
2783 + *
2784 + * @since 2.6.4
2785 + *
2786 + * @param array $entry One repeater entry.
2787 + * @param array $refs Definitions captured with the template.
2788 + * @param array $known Ids this site defines, from wdkit_known_global_ids().
2789 + * @return array Entry, with unresolvable references replaced by their values.
2790 + */
2791 + private function wdkit_resolve_entry_globals( $entry, $refs, $known ) {
2792 + if ( empty( $entry['__globals__'] ) || ! is_array( $entry['__globals__'] ) ) {
2793 + return $entry;
2794 + }
2795 +
2796 + foreach ( $entry['__globals__'] as $control => $ref ) {
2797 + if ( ! is_string( $ref ) || false === strpos( $ref, 'id=' ) ) {
2798 + continue;
2799 + }
2800 +
2801 + if ( false !== strpos( $ref, 'globals/colors' ) ) {
2802 + $kind = 'color';
2803 + } elseif ( false !== strpos( $ref, 'globals/typography' ) ) {
2804 + $kind = 'typography';
2805 + } else {
2806 + continue;
2807 + }
2808 +
2809 + $id = substr( $ref, strpos( $ref, 'id=' ) + 3 );
2810 + if ( '' === $id || isset( $known[ $kind ][ $id ] ) ) {
2811 + // Defined here already — their value wins.
2812 + continue;
2813 + }
2814 +
2815 + $definition = null;
2816 + foreach ( ( $refs[ $kind ] ?? array() ) as $candidate ) {
2817 + if ( is_array( $candidate ) && ( $candidate['_id'] ?? '' ) === $id ) {
2818 + $definition = $candidate;
2819 + break;
2820 + }
2821 + }
2822 +
2823 + if ( null === $definition ) {
2824 + // Nothing captured for it, so leave the reference rather than blank the field.
2825 + continue;
2826 + }
2827 +
2828 + if ( 'color' === $kind ) {
2829 + if ( empty( $definition['color'] ) ) {
2830 + continue;
2831 + }
2832 +
2833 + $entry[ $control ] = $definition['color'];
2834 + } else {
2835 + // A typography global expands into its own set of controls: the reference is
2836 + // held under e.g. `typography_typography`, and each definition key replaces
2837 + // that suffix — `typography_font_family`, `typography_font_weight`, and so on.
2838 + foreach ( $definition as $def_key => $def_value ) {
2839 + if ( '_id' === $def_key || 'title' === $def_key ) {
2840 + continue;
2841 + }
2842 +
2843 + $entry[ str_replace( 'typography_typography', $def_key, $control ) ] = $def_value;
2844 + }
2845 + }
2846 +
2847 + unset( $entry['__globals__'][ $control ] );
2848 + }
2849 +
2850 + return $entry;
2851 + }
2852 +
2853 + private function wdkit_merge_tp_globals( $incoming, $refs = array() ) {
2854 + if ( empty( $incoming ) || ! is_array( $incoming ) ) {
2855 + return false;
2856 + }
2857 +
2858 + $kit_id = get_option( 'elementor_active_kit' );
2859 + if ( empty( $kit_id ) ) {
2860 + return false;
2861 + }
2862 +
2863 + $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true );
2864 + if ( ! is_array( $kit_meta ) ) {
2865 + $kit_meta = array();
2866 + }
2867 +
2868 + // Which global ids this site already defines. The Plus Addons turns a reference into
2869 + // var(--e-global-color-<_id>), and Elementor only emits that variable for ids in the
2870 + // kit — so a reference the destination does not define resolves to nothing at all.
2871 + $known = $this->wdkit_known_global_ids( $kit_meta );
2872 +
2873 + $changed = false;
2874 +
2875 + foreach ( $this->wdkit_tp_global_kit_keys() as $key ) {
2876 + if ( empty( $incoming[ $key ] ) || ! is_array( $incoming[ $key ] ) ) {
2877 + continue;
2878 + }
2879 +
2880 + $existing = ( ! empty( $kit_meta[ $key ] ) && is_array( $kit_meta[ $key ] ) ) ? $kit_meta[ $key ] : array();
2881 +
2882 + $seen = array();
2883 + foreach ( $existing as $entry ) {
2884 + if ( ! empty( $entry['_id'] ) ) {
2885 + $seen[ $entry['_id'] ] = true;
2886 + }
2887 + }
2888 +
2889 + foreach ( $incoming[ $key ] as $entry ) {
2890 + if ( ! is_array( $entry ) || empty( $entry['_id'] ) || isset( $seen[ $entry['_id'] ] ) ) {
2891 + continue;
2892 + }
2893 +
2894 + // Only ever rewrite the entry being added — never one already in the kit.
2895 + $existing[] = $this->wdkit_resolve_entry_globals( $entry, $refs, $known );
2896 + $seen[ $entry['_id'] ] = true;
2897 + $changed = true;
2898 + }
2899 +
2900 + $kit_meta[ $key ] = array_values( $existing );
2901 + }
2902 +
2903 + if ( $changed ) {
2904 + update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta );
2905 +
2906 + // Writing kit meta directly does not rebuild the kit stylesheet, so the
2907 + // merged globals would never reach the frontend.
2908 + $this->wdkit_regenerate_elementor_kit_css();
2909 + }
2910 +
2911 + return $changed;
2912 + }
2913 +
2914 + protected function wdkit_get_global_val() {
2915 +
2916 + $builder = isset( $_POST['builder'] ) ? strtolower( sanitize_text_field( $_POST['builder'] ) ) : '';
2917 +
2918 + /* Answered as-is for a builder that is neither of the two below - previously
2919 + * `$response` was never set on that path and wp_send_json() read an undefined
2920 + * variable (PHP warning, ClickUp 14ynqxz2tru). */
2921 + $response = array(
2922 + 'message' => __( 'Unknown builder', 'wdesignkit' ),
2923 + 'description' => __( 'The builder must be elementor or gutenberg.', 'wdesignkit' ),
2924 + 'success' => false,
2925 + );
2926 +
2927 + if ( 'elementor' === $builder ) {
2928 + $kit_id = get_option( 'elementor_active_kit' );
2929 + if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) {
2930 + /* Elementor was installed moments ago in this import, and its kit is only
2931 + * created by its own activation hook - which may not have run in a request
2932 + * that loaded it (ClickUp 14ynqxz2tru). Same fallback as
2933 + * wdkit_apply_site_globals_data(). */
2934 + \Elementor\Core\Kits\Manager::create_default_kit();
2935 + $kit_id = get_option( 'elementor_active_kit' );
2936 + }
2937 + if ( empty( $kit_id ) ) {
2938 + $response = array(
2939 + 'message' => __( 'Elementor kit not found', 'wdesignkit' ),
2940 + 'description' => __( 'No active Elementor kit found', 'wdesignkit' ),
2941 + 'success' => false,
2942 + );
2943 +
2944 + wp_send_json( $response );
2945 + wp_die();
2946 + }
2947 +
2948 + $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true );
2949 + if ( empty( $kit_meta['system_colors'] ) ) {
2950 + $static_meta = array(
2951 + 'system_colors' => array(
2952 + 0 => array(
2953 + '_id' => 'primary',
2954 + 'title' => 'Primary',
2955 + 'color' => '#6EC1E4',
2956 + ),
2957 + 1 => array(
2958 + '_id' => 'secondary',
2959 + 'title' => 'Secondary',
2960 + 'color' => '#54595F',
2961 + ),
2962 + 2 => array(
2963 + '_id' => 'text',
2964 + 'title' => 'Text',
2965 + 'color' => '#7A7A7A',
2966 + ),
2967 + 3 => array(
2968 + '_id' => 'accent',
2969 + 'title' => 'Accent',
2970 + 'color' => '#61CE70',
2971 + ),
2972 + ),
2973 + 'custom_colors' => array(),
2974 + 'system_typography' => array(
2975 + 0 => array(
2976 + '_id' => 'primary',
2977 + 'title' => 'Primary',
2978 + 'typography_typography' => 'custom',
2979 + 'typography_font_family' => 'Roboto',
2980 + 'typography_font_weight' => '600',
2981 + ),
2982 + 1 => array(
2983 + '_id' => 'secondary',
2984 + 'title' => 'Secondary',
2985 + 'typography_typography' => 'custom',
2986 + 'typography_font_family' => 'Roboto Slab',
2987 + 'typography_font_weight' => '400',
2988 + ),
2989 + 2 => array(
2990 + '_id' => 'text',
2991 + 'title' => 'Text',
2992 + 'typography_typography' => 'custom',
2993 + 'typography_font_family' => 'Roboto',
2994 + 'typography_font_weight' => '400',
2995 + ),
2996 + 3 => array(
2997 + '_id' => 'accent',
2998 + 'title' => 'Accent',
2999 + 'typography_typography' => 'custom',
3000 + 'typography_font_family' => 'Roboto',
3001 + 'typography_font_weight' => '500',
3002 + ),
3003 + ),
3004 + 'custom_typography' => array(),
3005 + 'default_generic_fonts' => 'Sans-serif',
3006 + 'site_name' => ! empty( get_bloginfo( 'name' ) ) ? get_bloginfo( 'name' ) : '',
3007 + 'page_title_selector' => 'h1.entry-title',
3008 + 'activeItemIndex' => 1,
3009 + 'viewport_md' => 768,
3010 + 'viewport_lg' => 1025,
3011 + );
3012 +
3013 + /* Fill in what is MISSING. Do not replace the kit.
3014 + *
3015 + * The test above is about ONE key — `system_colors`, absent on a kit Elementor
3016 + * has not finished setting up — but this used to answer it with
3017 + * `$kit_meta = $static_meta`, throwing away every other key the kit had.
3018 + * `custom_colors` and `custom_typography` are in that set and are exactly
3019 + * where a site's own global palette lives, so any site with custom Elementor
3020 + * globals but no system colours lost all of them the moment this read endpoint
3021 + * ran. A *read* endpoint. It is reached from the import wizard and from the
3022 + * globals screen, so the loss looked like it came from importing.
3023 + *
3024 + * A key is filled when it is absent, or present but empty while the default
3025 + * has something to offer — so a kit that already has custom colours keeps
3026 + * them, a customised `page_title_selector` is not reset, and a genuinely bare
3027 + * kit still comes out of here with the stock palette the UI needs to render.
3028 + */
3029 + $kit_meta = is_array( $kit_meta ) ? $kit_meta : array();
3030 + $filled = false;
3031 +
3032 + foreach ( $static_meta as $meta_key => $meta_value ) {
3033 + $missing = ! isset( $kit_meta[ $meta_key ] )
3034 + || ( empty( $kit_meta[ $meta_key ] ) && ! empty( $meta_value ) );
3035 +
3036 + if ( $missing ) {
3037 + $kit_meta[ $meta_key ] = $meta_value;
3038 + $filled = true;
3039 + }
3040 + }
3041 +
3042 + if ( $filled ) {
3043 + update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta );
3044 + }
3045 + }
3046 +
3047 + $system_colors = ! empty( $kit_meta['system_colors'] ) ? $kit_meta['system_colors'] : array();
3048 + $custom_colors = ! empty( $kit_meta['custom_colors'] ) ? $kit_meta['custom_colors'] : array();
3049 + $system_typography = ! empty( $kit_meta['system_typography'] ) ? $kit_meta['system_typography'] : array();
3050 + $custom_typography = ! empty( $kit_meta['custom_typography'] ) ? $kit_meta['custom_typography'] : array();
3051 +
3052 + $color_array = array_merge( $system_colors, $custom_colors );
3053 + $typo_array = array_merge( $system_typography, $custom_typography );
3054 +
3055 + $global_data = array(
3056 + 'color' => $color_array,
3057 + 'typography' => $typo_array,
3058 + );
3059 +
3060 + $response = array(
3061 + 'message' => __( 'Global data Found', 'wdesignkit' ),
3062 + 'description' => __( 'Global Color and Typography found', 'wdesignkit' ),
3063 + 'data' => $global_data,
3064 + 'success' => true,
3065 + );
3066 +
3067 + } elseif ( 'gutenberg' === $builder ) {
3068 +
3069 + $plus_settings = get_option( 'tpgb_global_options', false );
3070 + $plus_settings = ! empty( $plus_settings ) ? json_decode( $plus_settings, true ) : json_decode( '[]' );
3071 +
3072 + if ( empty( $plus_settings ) ) {
3073 +
3074 + $static_meta = self::wdkit_default_gutenberg_globals();
3075 +
3076 + update_option( 'tpgb_global_options', json_encode( $static_meta ) );
3077 + $plus_settings = $static_meta;
3078 + }
3079 +
3080 + $active_id = ! empty( $plus_settings['active'] ) ? $plus_settings['active'] : '';
3081 + $preset_array = ! empty( $plus_settings['presets'] ) ? $plus_settings['presets'] : array();
3082 + $act_preset = ! empty( $plus_settings['presets'][ $active_id ] ) ? $plus_settings['presets'][ $active_id ] : array();
3083 +
3084 + foreach ( $act_preset['colors'] as $index => &$item ) {
3085 + $item['id'] = $index + 1;
3086 + }
3087 + unset( $item );
3088 +
3089 + foreach ( $act_preset['typography'] as $index => &$item ) {
3090 + $item['id'] = $index + 1;
3091 + }
3092 + unset( $item );
3093 +
3094 + $act_preset['color'] = $act_preset['colors'];
3095 + unset( $act_preset['colors'] );
3096 +
3097 + $response = array(
3098 + 'message' => __( 'Global data Found', 'wdesignkit' ),
3099 + 'description' => __( 'Global Color and Typography found', 'wdesignkit' ),
3100 + 'data' => $act_preset,
3101 + 'success' => true,
3102 + );
3103 + }
3104 +
3105 + wp_send_json( $response );
3106 + wp_die();
3107 + }
3108 +
3109 + /**
3110 + *
3111 + * Get site settings.
3112 + *
3113 + * @since 2.1.3
3114 + */
3115 + protected function wdkit_get_site_setting() {
3116 +
3117 + $kit_id = get_option( 'elementor_active_kit' );
3118 + if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) {
3119 + /* Same fallback as wdkit_get_global_val() (ClickUp 14ynqxz2tru). */
3120 + \Elementor\Core\Kits\Manager::create_default_kit();
3121 + $kit_id = get_option( 'elementor_active_kit' );
3122 + }
3123 +
3124 + if ( empty( $kit_id ) ) {
3125 + $response = array(
3126 + 'message' => __( 'Elementor kit not found', 'wdesignkit' ),
3127 + 'description' => __( 'No active Elementor kit found', 'wdesignkit' ),
3128 + 'success' => false,
3129 + );
3130 +
3131 + wp_send_json( $response );
3132 + wp_die();
3133 + }
3134 +
3135 + $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true );
3136 +
3137 + $container_width = ! empty( $kit_meta['container_width'] ) ? $kit_meta['container_width'] : array();
3138 + $globals = ! empty( $kit_meta['__globals__'] ) ? $kit_meta['__globals__'] : array();
3139 + $body_background_color = ! empty( $kit_meta['body_background_color'] ) ? $kit_meta['body_background_color'] : array();
3140 +
3141 + $site_globals = array(
3142 + 'body_background_color' => $body_background_color,
3143 + 'container_width' => $container_width,
3144 + 'globals' => $globals,
3145 + );
3146 +
3147 + $response = array(
3148 + 'message' => __( 'Global data Found', 'wdesignkit' ),
3149 + 'description' => __( 'Global Color and Typography found', 'wdesignkit' ),
3150 + 'data' => $site_globals,
3151 + 'success' => true,
3152 + );
3153 +
3154 + wp_send_json( $response );
3155 + wp_die();
3156 + }
3157 +
3158 + /**
3159 + *
3160 + * update site settings.
3161 + *
3162 + * @since 2.1.3
3163 + */
3164 + protected function wdkit_update_site_setting() {
3165 +
3166 + $builder = ! empty( $_POST['builder'] ) ? sanitize_text_field( $_POST['builder'] ) : '';
3167 + $site_data = ! empty( $_POST['site_data'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['site_data'] ) ), true ) : array();
3168 +
3169 + $response = $this->wdkit_apply_site_globals_data( $builder, $site_data );
3170 +
3171 + wp_send_json( $response );
3172 + wp_die();
3173 + }
3174 +
3175 + /**
3176 + * Apply a kit's site-level globals: container width, body background, `__globals__`.
3177 + *
3178 + * Extracted from wdkit_update_site_setting() so the PHP import runner can reach it
3179 + * without `$_POST`. Direct assignment rather than a merge, so running it twice leaves
3180 + * the same result — the browser behaves identically here.
3181 + *
3182 + * The AJAX action above is now a thin adapter over this and its response is unchanged.
3183 + *
3184 + * @since 2.6.5
3185 + *
3186 + * @param string $builder 'elementor'|'gutenberg'.
3187 + * @param array $site_data Global values collected from the kit's templates.
3188 + * @return array Response array, exactly as the AJAX action used to emit.
3189 + */
3190 + public function wdkit_apply_site_globals_data( $builder, $site_data ) {
3191 + $builder = is_string( $builder ) ? $builder : '';
3192 + $site_data = is_array( $site_data ) ? $site_data : array();
3193 +
3194 + if ( 'elementor' == $builder ) {
3195 + $kit_id = get_option( 'elementor_active_kit' );
3196 + if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) {
3197 + \Elementor\Core\Kits\Manager::create_default_kit();
3198 + $kit_id = get_option( 'elementor_active_kit' );
3199 + }
3200 +
3201 + if ( ! $kit_id ) {
3202 + $response = array(
3203 + 'message' => __( 'Elementor kit not found', 'wdesignkit' ),
3204 + 'description' => __( 'No active Elementor kit found', 'wdesignkit' ),
3205 + 'success' => false,
3206 + );
3207 +
3208 + return $response;
3209 + }
3210 +
3211 + // A freshly created kit has no `_elementor_page_settings` meta yet,
3212 + // so an empty result here is a valid starting point, not an error.
3213 + $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true );
3214 + if ( ! is_array( $kit_meta ) ) {
3215 + $kit_meta = array();
3216 + }
3217 +
3218 + $kit_meta['container_width'] = ! empty( $site_data['container_width'] ) ? $site_data['container_width'] : array();
3219 + $kit_meta['__globals__'] = ! empty( $site_data['globals'] ) ? $site_data['globals'] : array();
3220 + $kit_meta['body_background_color'] = ! empty( $site_data['body_background_color'] ) ? $site_data['body_background_color'] : array();
3221 +
3222 + update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta );
3223 +
3224 + // Regenerate Elementor's cached CSS. Writing the kit meta directly does
3225 + // not rebuild the kit stylesheet, so the imported body background colour
3226 + // and container width would otherwise never render on the frontend.
3227 + $this->wdkit_regenerate_elementor_kit_css();
3228 +
3229 + $response = array(
3230 + 'message' => __( 'Site data Updated', 'wdesignkit' ),
3231 + 'description' => __( 'Site Globals Updated', 'wdesignkit' ),
3232 + 'success' => true,
3233 + );
3234 +
3235 + } elseif ( 'gutenberg' == $builder ) {
3236 + $plus_settings = get_option( 'tpgb_global_options' );
3237 +
3238 + $site_preset = json_decode( $plus_settings, true );
3239 + $site_preset['globalContainer'] = $site_data;
3240 +
3241 + update_option( 'tpgb_global_options', json_encode( $site_preset ) );
3242 +
3243 + $response = array(
3244 + 'message' => __( 'Site data Updated', 'wdesignkit' ),
3245 + 'description' => __( 'Site Globals Updated', 'wdesignkit' ),
3246 + 'success' => true,
3247 + );
3248 +
3249 + } else {
3250 + $response = array(
3251 + 'message' => __( 'Builder Not Found !', 'wdesignkit' ),
3252 + 'description' => __( 'Template Builder not Found', 'wdesignkit' ),
3253 + 'success' => true,
3254 + );
3255 + }
3256 +
3257 + return $response;
3258 + }
3259 +
3260 + /**
3261 + *
3262 + * Update Elementor Global color and Typography.
3263 + *
3264 + * @since 1.1.20
3265 + */
3266 + protected function wdkit_update_global_val() {
3267 +
3268 + $builder = ! empty( $_POST['builder'] ) ? sanitize_text_field( $_POST['builder'] ) : '';
3269 +
3270 + $g_color = ! empty( $_POST['g_color'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['g_color'] ) ), true ) : array();
3271 + $g_typo = ! empty( $_POST['g_typography'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['g_typography'] ) ), true ) : array();
3272 + $new_preset = ! empty( $_POST['new_preset'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['new_preset'] ) ), true ) : array();
3273 +
3274 + $response = $this->wdkit_apply_global_values_data( $builder, $g_color, $g_typo, $new_preset );
3275 +
3276 + wp_send_json( $response );
3277 + wp_die();
3278 + }
3279 +
3280 + /**
3281 + * Apply a kit's global colours and typography.
3282 + *
3283 + * Extracted from wdkit_update_global_val() so the PHP import runner can reach it
3284 + * without `$_POST`.
3285 + *
3286 + * BEHAVIOUR NOTE — the Elementor branch merges with `array_merge( $new, $existing )` on
3287 + * numeric-keyed lists, which appends rather than replaces. Running it twice therefore
3288 + * accumulates duplicate entries. That is preserved here exactly, because it is the
3289 + * shipped browser behaviour and this method backs the AJAX action. Callers that need an
3290 + * idempotent apply should use Wdkit_Import_Globals::apply(), which de-duplicates by
3291 + * `_id` before calling in.
3292 + *
3293 + * @since 2.6.5
3294 + *
3295 + * @param string $builder 'elementor'|'gutenberg'.
3296 + * @param array $g_color Global colour entries.
3297 + * @param array $g_typo Global typography entries.
3298 + * @param array $new_preset Gutenberg preset {key, ...}.
3299 + * @return array Response array, exactly as the AJAX action used to emit.
3300 + */
3301 + public function wdkit_apply_global_values_data( $builder, $g_color = array(), $g_typo = array(), $new_preset = array() ) {
3302 + $builder = is_string( $builder ) ? $builder : '';
3303 +
3304 + if ( 'elementor' == $builder ) {
3305 +
3306 + $g_color = is_array( $g_color ) ? $g_color : array();
3307 + $g_typo = is_array( $g_typo ) ? $g_typo : array();
3308 +
3309 + // Get colors from Elementor Site Kit
3310 + $kit_id = get_option( 'elementor_active_kit' );
3311 + if ( ! $kit_id && did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Core\Kits\Manager' ) ) {
3312 + // No kit has ever been created on this site (the option is only
3313 + // ever populated by Elementor's own activation hook). Create one
3314 + // via Elementor's own helper so the import has somewhere to write.
3315 + \Elementor\Core\Kits\Manager::create_default_kit();
3316 + $kit_id = get_option( 'elementor_active_kit' );
3317 + }
3318 +
3319 + if ( ! $kit_id ) {
3320 + $response = array(
3321 + 'message' => __( 'Elementor kit not found', 'wdesignkit' ),
3322 + 'description' => __( 'No active Elementor kit found', 'wdesignkit' ),
3323 + 'success' => false,
3324 + );
3325 +
3326 + return $response;
3327 + }
3328 +
3329 + // A freshly created kit has no `_elementor_page_settings` meta yet,
3330 + // so an empty result here is a valid starting point, not an error.
3331 + $kit_meta = get_post_meta( $kit_id, '_elementor_page_settings', true );
3332 + if ( ! is_array( $kit_meta ) ) {
3333 + $kit_meta = array();
3334 + }
3335 +
3336 + $kit_meta['custom_colors'] = array_merge( $g_color, $kit_meta['custom_colors'] ?? array() );
3337 + $kit_meta['custom_typography'] = array_merge( $g_typo, $kit_meta['custom_typography'] ?? array() );
3338 +
3339 + update_post_meta( $kit_id, '_elementor_page_settings', $kit_meta );
3340 +
3341 + // Regenerate Elementor's cached CSS. Writing the kit meta directly does
3342 + // not rebuild the kit stylesheet, so the imported global colours and
3343 + // fonts would otherwise never render on the frontend.
3344 + $this->wdkit_regenerate_elementor_kit_css();
3345 +
3346 + $response = array(
3347 + 'message' => __( 'Global data Updated', 'wdesignkit' ),
3348 + 'description' => __( 'Global Color and Typography Updated', 'wdesignkit' ),
3349 + 'success' => true,
3350 + );
3351 +
3352 + } elseif ( 'gutenberg' == $builder ) {
3353 + $new_preset = is_array( $new_preset ) ? $new_preset : array();
3354 + $new_preset_id = ! empty( $new_preset['key'] ) ? $new_preset['key'] : '';
3355 + $plus_settings = get_option( 'tpgb_global_options' );
3356 + $site_preset = json_decode( $plus_settings, true );
3357 +
3358 + $site_preset['presets'][ $new_preset_id ] = $new_preset;
3359 + $site_preset['active'] = $new_preset_id;
3360 +
3361 + update_option( 'tpgb_global_options', json_encode( $site_preset ) );
3362 +
3363 + $response = array(
3364 + 'message' => __( 'Global data Updated', 'wdesignkit' ),
3365 + 'description' => __( 'Global Color and Typography Updated', 'wdesignkit' ),
3366 + 'success' => true,
3367 + );
3368 +
3369 + } else {
3370 + $response = array(
3371 + 'message' => __( 'Builder Not Found !', 'wdesignkit' ),
3372 + 'description' => __( 'Template Builder not Found', 'wdesignkit' ),
3373 + 'success' => true,
3374 + );
3375 + }
3376 +
3377 + return $response;
3378 + }
3379 +
3380 + /**
3381 + * Regenerate Elementor's cached CSS files after the active kit's
3382 + * `_elementor_page_settings` meta has been changed directly.
3383 + *
3384 + * Elementor renders global colours, global fonts and the body background
3385 + * colour into a cached kit stylesheet. Updating the meta via
3386 + * update_post_meta() does not rebuild that stylesheet, so imported site
3387 + * settings never reach the frontend until the cache is cleared. This
3388 + * mirrors the clear_cache() call already used by the page/section import.
3389 + *
3390 + * @since 2.3.2
3391 + *
3392 + * @return void
3393 + */
3394 + protected function wdkit_regenerate_elementor_kit_css() {
3395 + if ( did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Plugin' ) ) {
3396 + \Elementor\Plugin::$instance->files_manager->clear_cache();
3397 + }
3398 + }
3399 +
3400 + /**
3401 + *
3402 + * Create Gutenberg page and save for re-generate css file.
3403 + *
3404 + * @since 1.2.3
3405 + */
3406 + protected function wdkit_update_preset() {
3407 +
3408 + $act_type = ! empty( $_POST['act_type'] ) ? sanitize_text_field( $_POST['act_type'] ) : '';
3409 + $post_id = ! empty( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : '';
3410 +
3411 + if ( 'create' == $act_type ) {
3412 +
3413 + $page_id = wp_insert_post(
3414 + array(
3415 + 'post_title' => 'WDesignKit Gutenberg',
3416 + 'post_status' => 'publish',
3417 + 'post_type' => 'post',
3418 + 'post_name' => sanitize_title( 'wdesignkit' ),
3419 + 'post_content' => '<!-- wp:heading --><h2 class="wp-block-heading">Add Your Heading Text Here<h2><!-- /wp:heading -->',
3420 + 'meta_input' => array(
3421 + 'gutenberg_preview' => true,
3422 + '_wp_page_template' => 'default',
3423 + ),
3424 + )
3425 + );
3426 +
3427 + if ( is_wp_error( $page_id ) || ! $page_id ) {
3428 + $response = array(
3429 + 'success' => true,
3430 + 'message' => esc_html__( 'Page Not Found!', 'wdesignkit' ),
3431 + 'description' => esc_html__( 'Page Not Found!', 'wdesignkit' ),
3432 + );
3433 +
3434 + wp_send_json( $response );
3435 + wp_die();
3436 + }
3437 +
3438 + update_post_meta( $page_id, '_edit_lock', time() . ':1' );
3439 + update_post_meta( $page_id, '_edit_last', get_current_user_id() );
3440 +
3441 + $preview_url = admin_url( 'post.php?post=' . $page_id . '&action=edit' );
3442 +
3443 + $response = array(
3444 + 'success' => true,
3445 + 'post_id' => $page_id,
3446 + 'preview_url' => $preview_url,
3447 + 'message' => esc_html__( 'Page Created', 'wdesignkit' ),
3448 + 'description' => esc_html__( 'Page Created', 'wdesignkit' ),
3449 + );
3450 +
3451 + wp_send_json( $response );
3452 + wp_die();
3453 +
3454 + }
3455 +
3456 + if ( ! empty( $post_id ) && ( $act_type == 'remove' ) ) {
3457 +
3458 + wp_delete_post( $post_id, true );
3459 +
3460 + $response = array(
3461 + 'success' => true,
3462 + 'message' => esc_html__( 'post deleted', 'wdesignkit' ),
3463 + 'description' => esc_html__( 'post deleted', 'wdesignkit' ),
3464 + );
3465 +
3466 + wp_send_json( $response );
3467 + wp_die();
3468 + }
3469 + }
3470 +
3471 + /**
3472 + *
897 3473 * It is For Find User Existing template List.
898 3474 *
899 3475 * @since 1.0.6
900 3476 */
@@ -921,16 +3497,39 @@
921 3497 * @since 1.0.6
922 3498 */
923 3499 protected function wdkit_update_template() {
924 3500 $array_data = array(
925 - 'data' => isset( $_POST['data'] ) ? wp_unslash( $_POST['data'] ) : '',
926 - 'token' => isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '',
927 - 'type' => isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '',
928 - 'id' => isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '',
3501 + 'data' => isset( $_POST['data'] ) ? wp_unslash( $_POST['data'] ) : '',
3502 + 'post_id' => isset( $_POST['post_id'] ) ? sanitize_text_field( wp_unslash( $_POST['post_id'] ) ) : '',
3503 + 'token' => isset( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '',
3504 + 'type' => isset( $_POST['type'] ) ? sanitize_text_field( wp_unslash( $_POST['type'] ) ) : '',
3505 + 'id' => isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '',
3506 + 'global_data' => isset( $_POST['global_data'] ) ? wp_unslash( $_POST['global_data'] ) : array(),
3507 + // 'global_font_family' => isset( $_POST['global_font_family'] ) ? wp_unslash( $_POST['global_font_family'] ) : array(),
3508 + // 'global_color' => isset( $_POST['global_color'] ) ? wp_unslash( $_POST['global_color'] ) : array(),
929 3509 );
930 3510
931 - $response = $this->wkit_api_call( $array_data, 'existing_template' );
3511 + if ( ! empty( $array_data['post_id'] ) ) {
3512 + $custom_fields = array();
3513 + $post_id = $array_data['post_id'];
932 3514
3515 + $meta_fields = get_post_custom( $post_id );
3516 +
3517 + foreach ( $meta_fields as $key => $value ) {
3518 + if ( str_contains( $key, 'nxt-' ) ) {
3519 + $custom_fields[ $key ] = $value;
3520 + }
3521 + }
3522 +
3523 + if ( ! empty( $custom_fields ) ) {
3524 + $data = json_decode( $array_data['data'], true );
3525 + $data['custom_meta'] = $custom_fields;
3526 + $array_data['data'] = wp_json_encode( $data );
3527 + }
3528 + }
3529 +
3530 + $array_data['remove'] = 'yes';
3531 + $response = $this->wkit_api_call( $array_data, 'existing_template' );
933 3532 wp_send_json( $response );
934 3533 wp_die();
935 3534 }
936 3535
@@ -974,8 +3573,9 @@
974 3573 */
975 3574 protected function wdkit_check_plugins_depends() {
976 3575 $plugins = isset( $_POST['plugins'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['plugins'] ) ) ) : array();
977 3576 $update_plugin = array();
3577 + $update_theme = array();
978 3578
979 3579 if ( empty( $plugins ) || ! is_array( $plugins ) ) {
980 3580 $this->wdkit_error_msg( array( 'plugins' => 'No Plugins' ) );
981 3581 }
@@ -988,8 +3588,9 @@
988 3588
989 3589 if ( is_null( $pluginslug ) ) {
990 3590 $plugin->status = 'warning';
991 3591 $update_plugin[] = $plugin;
3592 + $update_theme[] = $plugin;
992 3593
993 3594 continue;
994 3595 }
995 3596
@@ -1010,13 +3611,13 @@
1010 3611 $plugin->status = 'active';
1011 3612 $update_plugin[] = $plugin;
1012 3613 }
1013 3614 } elseif ( 'theme' === $type ) {
1014 - $theme_array = array_keys( wp_get_themes() );
1015 - // $current_themes = wp_get_theme()->get('Name');
1016 - $theme_slug = get_stylesheet();
3615 + $theme_array = array_keys( wp_get_themes() );
3616 + $theme_slug = get_stylesheet();
3617 + $parent_theme_slug = get_template();
1017 3618
1018 - if ( $theme_slug === $plugin->original_slug ) {
3619 + if ( $theme_slug === $plugin->original_slug || $parent_theme_slug === $plugin->original_slug ) {
1019 3620
1020 3621 $plugin->status = 'active';
1021 3622 } else {
1022 3623 $theme_name = $plugin->original_slug;
@@ -1030,13 +3631,19 @@
1030 3631 $plugin->status = 'inactive';
1031 3632 }
1032 3633 }
1033 3634
1034 - $update_plugin[] = $plugin;
3635 + $update_theme[] = $plugin;
1035 3636 }
1036 3637 }
1037 3638
1038 - $this->wdkit_success_msg( array( 'plugins' => $update_plugin ) );
3639 + $response = array(
3640 + 'plugins' => $update_plugin,
3641 + 'theme' => $update_theme,
3642 + 'ele_container' => get_option( 'elementor_experiment-container', false ),
3643 + );
3644 +
3645 + $this->wdkit_success_msg( $response );
1039 3646 }
1040 3647
1041 3648 /**
1042 3649 *
@@ -1046,9 +3653,34 @@
1046 3653 * @version 1.0.9
1047 3654 */
1048 3655 protected function wdkit_install_plugins_depends() {
1049 3656 $plugins = isset( $_POST['plugins'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['plugins'] ) ), true ) : array();
3657 +
3658 + $responce = $this->wdkit_install_dependency_data( $plugins );
3659 +
3660 + wp_send_json( $responce );
3661 + wp_die();
3662 + }
3663 +
3664 + /**
3665 + * Install or activate one kit dependency and describe the outcome.
3666 + *
3667 + * Extracted from wdkit_install_plugins_depends() so the PHP import runner can install
3668 + * dependencies without `$_POST`. The plugin branch delegates to Wdkit_Depends_Installer
3669 + * and the theme branch keeps its own activation path — themes are NOT installed through
3670 + * the plugin installer, and routing one there would try to install a theme as a plugin.
3671 + *
3672 + * The AJAX action above is now a thin adapter over this and its response is unchanged.
3673 + *
3674 + * @since 2.6.5
3675 + *
3676 + * @param array $plugins Dependency record: {type, p_id, original_slug, plugin_slug, ...}.
3677 + * @return array Response array, exactly as the AJAX action used to emit.
3678 + */
3679 + public function wdkit_install_dependency_data( $plugins ) {
3680 + $plugins = is_array( $plugins ) ? $plugins : array();
1050 3681 $type = ! empty( $plugins['type'] ) ? $plugins['type'] : 'plugin';
3682 + $p_id = ! empty( $plugins['p_id'] ) ? $plugins['p_id'] : 'plugin';
1051 3683
1052 3684 $responce = '';
1053 3685 if ( 'plugin' === $type ) {
1054 3686 $responce = Wdkit_Depends_Installer::get_instance()->wdkit_install_plugin( $plugins );
@@ -1055,24 +3687,46 @@
1055 3687 } elseif ( 'theme' === $type ) {
1056 3688 $theme_name = ! empty( $plugins['original_slug'] ) ? $plugins['original_slug'] : '';
1057 3689 if ( ! empty( $theme_name ) ) {
1058 3690
1059 - $activate_result = switch_theme( $theme_name );
3691 + $theme_array = array_keys( wp_get_themes() );
3692 + $theme_slug = get_stylesheet();
1060 3693
1061 - if ( ! is_wp_error( $activate_result ) ) {
1062 - $responce = array(
1063 - 'message' => esc_html__( 'Theme activated successfully', 'wdesignkit' ),
1064 - 'description' => esc_html__( 'Theme successfully activated', 'wdesignkit' ),
1065 - 'slug' => 'the-plus-addons-for-block-editor',
1066 - 'status' => 'active',
1067 - 'success' => true,
1068 - );
3694 + if ( in_array( $theme_name, $theme_array ) ) {
3695 + $activate_result = switch_theme( $theme_name );
3696 +
3697 + if ( ! is_wp_error( $activate_result ) ) {
3698 + $responce = array(
3699 + 'message' => esc_html__( 'Theme activated successfully', 'wdesignkit' ),
3700 + 'description' => esc_html__( 'Theme successfully activated', 'wdesignkit' ),
3701 + 'slug' => 'nexter',
3702 + 'p_id' => $p_id,
3703 + 'status' => 'active',
3704 + 'success' => true,
3705 + );
3706 + } else {
3707 + $responce = array(
3708 + 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ),
3709 + 'description' => $activate_result->get_error_message(),
3710 + 'status' => 'inactive',
3711 + 'p_id' => $p_id,
3712 + 'success' => false,
3713 + );
3714 + }
1069 3715 } else {
3716 + $result = $this->wdkit_install_theme_depends( $theme_name );
3717 +
3718 + $message = ! empty( $result['message'] ) ? $result['message'] : esc_html__( 'Somthing Wrong', 'wdesignkit' );
3719 + $description = ! empty( $result['description'] ) ? $result['description'] : esc_html__( 'Error Somthing Wrong', 'wdesignkit' );
3720 + $status = ! empty( $result['status'] ) ? $result['status'] : esc_html__( 'inactive', 'wdesignkit' );
3721 + $success = ! empty( $result['success'] ) ? $result['success'] : false;
3722 +
1070 3723 $responce = array(
1071 - 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ),
1072 - 'description' => $activate_result->get_error_message(),
1073 - 'status' => 'inactive',
1074 - 'success' => false,
3724 + 'message' => $message,
3725 + 'description' => $description,
3726 + 'p_id' => $p_id,
3727 + 'status' => $status,
3728 + 'success' => $success,
1075 3729 );
1076 3730 }
1077 3731 } else {
1078 3732 $responce = array(
@@ -1077,19 +3731,295 @@
1077 3731 } else {
1078 3732 $responce = array(
1079 3733 'message' => esc_html__( 'Theme Name not Found', 'wdesignkit' ),
1080 3734 'description' => esc_html__( 'Can Not Found Theme Name you Enterd.', 'wdesignkit' ),
3735 + 'success' => false,
3736 + );
3737 + }
3738 + }
3739 +
3740 + return $responce;
3741 + }
3742 +
3743 + /**
3744 + * Install/activate every plugin and theme dependency in one request.
3745 + *
3746 + * wdkit_install_plugins_depends() above installs exactly one item per admin-ajax call,
3747 + * which is what the browser used to drive in a sequential chain (install one, wait,
3748 + * install the next) - one WP bootstrap + nonce check + network round trip per dependency
3749 + * on top of the install itself. This reuses the identical per-item logic - the same
3750 + * Wdkit_Depends_Installer::wdkit_install_plugin() for plugins and the same
3751 + * switch_theme()/wdkit_install_theme_depends() branch for themes - just looped inside a
3752 + * single PHP execution instead of one call per item.
3753 + *
3754 + * @since 2.7.0
3755 + */
3756 + protected function wdkit_install_plugins_depends_batch() {
3757 + $items = isset( $_POST['plugins'] ) ? json_decode( wp_unslash( $_POST['plugins'] ), true ) : array();
3758 +
3759 + if ( empty( $items ) || ! is_array( $items ) ) {
3760 + wp_send_json(
3761 + array(
3762 + 'success' => false,
3763 + 'message' => esc_html__( 'No plugins supplied', 'wdesignkit' ),
3764 + )
3765 + );
3766 + wp_die();
3767 + }
3768 +
3769 + // Several downloads + unzips + activations in one request; harmless no-op where
3770 + // set_time_limit() is disabled.
3771 + if ( function_exists( 'set_time_limit' ) ) {
3772 + @set_time_limit( 300 );
3773 + }
3774 +
3775 + $t_start = microtime( true );
3776 + $results = array();
3777 +
3778 + foreach ( $items as $item ) {
3779 + $item = (array) $item;
3780 + $type = ! empty( $item['type'] ) ? $item['type'] : 'plugin';
3781 + $p_id = ! empty( $item['p_id'] ) ? $item['p_id'] : '';
3782 +
3783 + if ( 'theme' === $type ) {
3784 + $theme_name = ! empty( $item['original_slug'] ) ? $item['original_slug'] : '';
3785 +
3786 + if ( empty( $theme_name ) ) {
3787 + $results[] = array(
3788 + 'p_id' => $p_id,
3789 + 'success' => false,
3790 + 'status' => 'inactive',
3791 + 'message' => esc_html__( 'Theme name not found', 'wdesignkit' ),
3792 + );
3793 + continue;
3794 + }
3795 +
3796 + $theme_array = array_keys( wp_get_themes() );
3797 +
3798 + if ( in_array( $theme_name, $theme_array, true ) ) {
3799 + $activate_result = switch_theme( $theme_name );
3800 +
3801 + $results[] = is_wp_error( $activate_result )
3802 + ? array(
3803 + 'p_id' => $p_id,
3804 + 'success' => false,
3805 + 'status' => 'inactive',
3806 + 'message' => $activate_result->get_error_message(),
3807 + )
3808 + : array(
3809 + 'p_id' => $p_id,
3810 + 'success' => true,
3811 + 'status' => 'active',
3812 + 'message' => esc_html__( 'Theme activated successfully', 'wdesignkit' ),
3813 + );
3814 + } else {
3815 + $result = $this->wdkit_install_theme_depends( $theme_name );
3816 + $results[] = array(
3817 + 'p_id' => $p_id,
3818 + 'success' => ! empty( $result['success'] ) ? $result['success'] : false,
3819 + 'status' => ! empty( $result['status'] ) ? $result['status'] : 'inactive',
3820 + 'message' => ! empty( $result['message'] ) ? $result['message'] : esc_html__( 'Something went wrong', 'wdesignkit' ),
3821 + );
3822 + }
3823 +
3824 + continue;
3825 + }
3826 +
3827 + $results[] = Wdkit_Depends_Installer::get_instance()->wdkit_install_plugin( $item );
3828 + }
3829 +
3830 + self::wdkit_clear_activation_redirects();
3831 +
3832 + $timing_ms = (int) round( ( microtime( true ) - $t_start ) * 1000 );
3833 +
3834 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
3835 + error_log( 'WDKIT dependency batch timing: ' . $timing_ms . 'ms for ' . count( $results ) . ' item(s)' );
3836 + }
3837 +
3838 + wp_send_json(
3839 + array(
3840 + 'success' => true,
3841 + 'results' => $results,
3842 + 'timing_ms' => $timing_ms,
3843 + )
3844 + );
3845 + wp_die();
3846 + }
3847 +
3848 + /**
3849 + * Disarm the "welcome screen" redirects the plugins we just activated leave armed.
3850 + *
3851 + * Several plugins set a short-lived transient on activation and, on the next `admin_init`,
3852 + * redirect to their own setup wizard. Rank Math's guard is the clearest example - it checks
3853 + * the transient, the current page and the capability, but never wp_doing_ajax():
3854 + *
3855 + * set_transient( '_rank_math_activation_redirect', 1, 30 ); // class-installer.php
3856 + * $this->action( 'admin_init', 'redirect_to_welcome' ); // class-registration.php
3857 + *
3858 + * `admin_init` fires on admin-ajax.php too, so a wizard request landing in that window is
3859 + * answered with an HTML admin page instead of its JSON, and the step that made the call
3860 + * fails with nothing the user can act on.
3861 + *
3862 + * NOTE: this is a hardening measure, not a proven fix for any specific report. The
3863 + * Global-Settings failure in QA 14ynqxyugbu still reproduces with this in place, and no
3864 + * wp_redirect() is fired during it - so whatever hijacks that request does not go through
3865 + * the transients below (a raw header() call would not). Clearing these remains correct on
3866 + * its own terms; it is not the answer to that ticket.
3867 + *
3868 + * Deleting these is what an importer that activates plugins on the user's behalf has to do:
3869 + * nobody asked for a setup wizard, and the user is mid-import. Each is a one-shot flag the
3870 + * owning plugin deletes itself on first read, so removing it takes nothing else with it.
3871 + *
3872 + * The list is every such transient set by the plugins this importer installs - grep the
3873 + * dependency set for `set_transient` before adding to it, rather than guessing at names.
3874 + *
3875 + * @since 2.7.1
3876 + *
3877 + * @return void
3878 + */
3879 + public static function wdkit_clear_activation_redirects() {
3880 +
3881 + $transients = array(
3882 + '_rank_math_activation_redirect',
3883 + '_wc_activation_redirect',
3884 + 'elementor_activation_redirect',
3885 + );
3886 +
3887 + foreach ( $transients as $transient ) {
3888 + delete_transient( $transient );
3889 + }
3890 + }
3891 +
3892 + protected function wdkit_install_theme_depends( $name = 'nexter' ) {
3893 +
3894 + if ( ! current_user_can( 'install_themes' ) ) {
3895 + $response = $this->tpae_set_response( false, 'Invalid nonce.', 'The security check failed. Please refresh the page and try again.' );
3896 + return $response;
3897 + }
3898 +
3899 + $theme_slug = $name;
3900 + $theme_api_url = 'https://api.wordpress.org/themes/info/1.0/';
3901 +
3902 + // Parameters for the request
3903 + $args = array(
3904 + 'body' => array(
3905 + 'action' => 'theme_information',
3906 + 'request' => serialize(
3907 + (object) array(
3908 + 'slug' => $name,
3909 + 'fields' => array(
3910 + 'description' => false,
3911 + 'sections' => false,
3912 + 'rating' => true,
3913 + 'ratings' => false,
3914 + 'downloaded' => true,
3915 + 'download_link' => true,
3916 + 'last_updated' => true,
3917 + 'homepage' => true,
3918 + 'tags' => true,
3919 + 'template' => true,
3920 + 'active_installs' => false,
3921 + 'parent' => false,
3922 + 'versions' => false,
3923 + 'screenshot_url' => true,
3924 + 'active_installs' => false,
3925 + ),
3926 + )
3927 + ),
3928 + ),
3929 + );
3930 +
3931 + // Make the request
3932 + $response = wp_remote_post( $theme_api_url, $args );
3933 + // Check for errors
3934 + if ( is_wp_error( $response ) ) {
3935 + $error_message = $response->get_error_message();
3936 +
3937 + $result = $this->tpae_set_response( false, 'oops', 'oops', '' );
3938 + } else {
3939 + // api.wordpress.org's theme_information response is a serialized stdClass
3940 + // (accessed below via ->name / ->download_link). allowed_classes => false
3941 + // blocks stdClass too, turning it into an __PHP_Incomplete_Class whose
3942 + // properties silently don't exist — allow only stdClass, still refusing any
3943 + // other (potentially dangerous) class the payload might reference.
3944 + $theme_info = unserialize( $response['body'], array( 'allowed_classes' => array( 'stdClass' ) ) );
3945 + $theme_name = $theme_info->name;
3946 + $theme_zip_url = $theme_info->download_link;
3947 +
3948 + // SSRF guard (CWE-918): validate the resolved host before fetching the ZIP
3949 + // referenced by the external theme_info response.
3950 + if ( ! wdesignkit_validate_external_url( $theme_zip_url ) ) {
3951 + return array(
3952 + 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ),
3953 + 'description' => esc_html__( 'The theme package URL is not allowed.', 'wdesignkit' ),
3954 + 'status' => 'inactive',
3955 + 'success' => false,
3956 + );
3957 + }
3958 +
3959 + if ( ! function_exists( 'WP_Filesystem' ) ) {
3960 + require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/file.php' );
3961 + }
3962 +
3963 + require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/class-wp-upgrader.php' );
3964 + require_once wp_normalize_path( ABSPATH . '/wp-admin/includes/theme.php' );
3965 +
3966 + WP_Filesystem();
3967 +
3968 + $active_theme = wp_get_theme();
3969 + $theme_name = $active_theme->get( 'Name' );
3970 +
3971 + // Install via WordPress core's Theme_Upgrader instead of manually fetching and
3972 + // ZipArchive::extractTo()'ing the remote package: core already performs the
3973 + // standard download -> unpack -> validate-package-structure -> move-into-place
3974 + // flow (including cleanup on failure) used for every trusted theme install.
3975 + $upgrader = new Theme_Upgrader( new Automatic_Upgrader_Skin() );
3976 + $install = $upgrader->install( $theme_zip_url );
3977 +
3978 + if ( is_wp_error( $install ) || ! $install ) {
3979 + return array(
3980 + 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ),
3981 + 'description' => is_wp_error( $install ) ? $install->get_error_message() : esc_html__( 'Theme could not be installed.', 'wdesignkit' ),
3982 + 'status' => 'inactive',
3983 + 'success' => false,
3984 + );
3985 + }
3986 +
3987 + $activate_result = switch_theme( $name );
3988 +
3989 + if ( ! is_wp_error( $activate_result ) ) {
3990 + $response = array(
3991 + 'message' => esc_html__( 'Theme activated successfully', 'wdesignkit' ),
3992 + 'description' => esc_html__( 'Theme successfully activated', 'wdesignkit' ),
3993 + 'status' => 'active',
1081 3994 'success' => true,
1082 3995 );
3996 + } else {
3997 + $response = array(
3998 + 'message' => esc_html__( 'Theme Not Activated !', 'wdesignkit' ),
3999 + 'description' => $activate_result->get_error_message(),
4000 + 'status' => 'inactive',
4001 + 'success' => false,
4002 + );
1083 4003 }
1084 4004 }
1085 4005
1086 - wp_send_json( $responce );
1087 - wp_die();
4006 + return $response;
1088 4007 }
1089 4008
1090 4009 /**
1091 4010 *
4011 + * It is Use Update WDesignKit plugin latest version.
4012 + *
4013 + * @since 1.0.17
4014 + */
4015 + protected function wdkit_update_latest_plugin() {
4016 +
4017 + return Wdkit_Depends_Installer::get_instance()->wdkit_update_plugin();
4018 + }
4019 +
4020 + /**
4021 + *
1092 4022 * It is Use for get plugin list.
1093 4023 *
1094 4024 * @since 1.0.0
1095 4025 */
@@ -1105,25 +4035,151 @@
1105 4035 * Get Download Template Content
1106 4036 *
1107 4037 * @since 1.0.0
1108 4038 */
1109 - protected function wdkit_import_template() {
1110 - $args = $this->wdkit_parse_args( $_POST );
4039 + protected function wdkit_activate_container() {
1111 4040
1112 - if ( empty( $args['email'] ) ) {
1113 - $response = array(
1114 - 'content' => '',
1115 - 'message' => esc_html__( 'Invalid import', 'wdesignkit' ),
1116 - 'description' => esc_html__( 'Invalid import: Check your details and try again.', 'wdesignkit' ),
1117 - 'success' => true,
1118 - );
4041 + $option_value = get_option( 'elementor_experiment-container', false );
1119 4042
1120 - wp_send_json( $response );
1121 - wp_die();
4043 + if ( $option_value === false ) {
4044 + add_option( 'elementor_experiment-container', 'active' );
4045 + } else {
4046 + update_option( 'elementor_experiment-container', 'active' );
1122 4047 }
1123 4048
4049 + $result = array(
4050 + 'message' => esc_html__( 'Container Activated Successfully', 'wdesignkit' ),
4051 + 'description' => esc_html__( 'Elementor Container Activated Successfully.', 'wdesignkit' ),
4052 + 'success' => true,
4053 + );
4054 +
4055 + wp_send_json( $response );
4056 + wp_die();
4057 + }
4058 +
4059 + /**
4060 + * import category and tags for post
4061 + *
4062 + * @since 2.0.0
4063 + */
4064 + protected function wdkit_import_taxonomy() {
4065 + $category = isset( $_POST['category'] ) ? json_decode( wp_unslash( $_POST['category'] ) ) : array();
4066 + $tags = isset( $_POST['tags'] ) ? json_decode( wp_unslash( $_POST['tags'] ) ) : array();
4067 +
4068 + $response = $this->wdkit_import_taxonomy_data( $category, $tags );
4069 +
4070 + /* Emitted here so this action's output stays byte-for-byte what it was at
4071 + * HEAD, where wp_send_json()/wp_die() sat at the end of this same method. */
4072 + wp_send_json( $response );
4073 + wp_die();
4074 + }
4075 +
4076 + /**
4077 + * Create the categories and tags a kit's posts need, returning their term ids.
4078 + *
4079 + * Extracted from wdkit_import_taxonomy() so the PHP import runner can reach it without
4080 + * `$_POST`. Already idempotent before this change and still is: every term is looked up
4081 + * with term_exists() before an insert is attempted, so running it twice returns the
4082 + * same ids rather than creating duplicates.
4083 + *
4084 + * The AJAX action above is now a thin adapter over this and its return value — which
4085 + * the router passes to wdkit_success_msg() — is unchanged.
4086 + *
4087 + * @since 2.6.5
4088 + *
4089 + * @param array $category Category names.
4090 + * @param array $tags Tag names.
4091 + * @return array {success, categories:[{name,term_id|error}], tags:[...]}
4092 + */
4093 + public function wdkit_import_taxonomy_data( $category = array(), $tags = array() ) {
4094 + $category = is_array( $category ) ? $category : array();
4095 + $tags = is_array( $tags ) ? $tags : array();
4096 +
4097 + $response = array(
4098 + 'success' => false,
4099 + 'categories' => array(),
4100 + 'tags' => array(),
4101 + );
4102 +
4103 + if ( ! empty( $category ) && count( $category ) > 0 ) {
4104 + foreach ( $category as $category_name ) {
4105 + $category_name = sanitize_text_field( $category_name );
4106 +
4107 + $term_exists = term_exists( $category_name, 'category' );
4108 + if ( ! $term_exists ) {
4109 + $result = wp_insert_term( $category_name, 'category' );
4110 + if ( ! is_wp_error( $result ) ) {
4111 + $response['categories'][] = array(
4112 + 'name' => $category_name,
4113 + 'term_id' => $result['term_id'],
4114 + );
4115 + } else {
4116 + $response['categories'][] = array(
4117 + 'name' => $category_name,
4118 + 'error' => $result->get_error_message(),
4119 + );
4120 + }
4121 + } else {
4122 + $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists;
4123 + $response['categories'][] = array(
4124 + 'name' => $category_name,
4125 + 'term_id' => $term_id,
4126 + );
4127 + }
4128 + }
4129 +
4130 + $response['success'] = true;
4131 + }
4132 +
4133 + if ( ! empty( $tags ) && count( $tags ) > 0 ) {
4134 + foreach ( $tags as $tags_name ) {
4135 + $tags_name = sanitize_text_field( $tags_name );
4136 +
4137 + $term_exists = term_exists( $tags_name, 'post_tag' );
4138 + if ( ! $term_exists ) {
4139 + $result = wp_insert_term( $tags_name, 'post_tag' );
4140 + if ( ! is_wp_error( $result ) ) {
4141 + $response['tags'][] = array(
4142 + 'name' => $tags_name,
4143 + 'term_id' => $result['term_id'],
4144 + );
4145 + } else {
4146 + $response['tags'][] = array(
4147 + 'name' => $tags_name,
4148 + 'error' => $result->get_error_message(),
4149 + );
4150 + }
4151 + } else {
4152 + $term_id = is_array( $term_exists ) ? $term_exists['term_id'] : $term_exists;
4153 + $response['tags'][] = array(
4154 + 'name' => $tags_name,
4155 + 'term_id' => $term_id,
4156 + );
4157 + }
4158 + }
4159 +
4160 + $response['success'] = true;
4161 + }
4162 +
4163 + /* Returns. It must NOT emit: the PHP runner calls this directly, and a
4164 + * wp_send_json() here would print JSON and wp_die() in the middle of an
4165 + * import. Found by running the runner against real WordPress -- see the
4166 + * Phase 5 report. The AJAX response is unaffected, because
4167 + * wdkit_import_taxonomy() emits this same array at the same point. */
4168 + return $response;
4169 + }
4170 +
4171 + /**
4172 + * Get Download Template Content
4173 + *
4174 + * @since 1.0.0
4175 + */
4176 + protected function wdkit_import_template() {
4177 + $args = $this->wdkit_parse_args( $_POST );
4178 + $api_type = isset( $_POST['api_type'] ) ? sanitize_text_field( wp_unslash( $_POST['api_type'] ) ) : 'import_template';
4179 +
1124 4180 $response = '';
1125 - if ( empty( $args['email'] ) || empty( $args['template_id'] ) ) {
4181 + if ( empty( $args['template_id'] ) ) {
1126 4182 $result = array(
1127 4183 'content' => '',
1128 4184 'message' => esc_html__( 'Invalid import', 'wdesignkit' ),
1129 4185 'description' => esc_html__( 'Invalid import: Check your details and try again.', 'wdesignkit' ),
@@ -1136,9 +4192,28 @@
1136 4192
1137 4193 $args['token'] = $this->wdkit_login_user_token( $args['email'] );
1138 4194
1139 4195 unset( $args['email'] );
1140 - $response = WDesignKit_Data_Query::get_data( 'import_template', $args );
4196 + $args['unique_id'] = get_option( 'wdkit_unique_id' ) ?? '';
4197 +
4198 + /* Importing one template from the library, rather than a whole kit. A sandbox has no
4199 + * login for wdkit_login_user_token() to find, so without this the token is empty and
4200 + * the cloud refuses - the same gap the kit paths already close. Merged last, so the
4201 + * site's own identity wins over anything the request tried to supply. */
4202 + if ( function_exists( 'wdkit_kit_import_with_site_identity' ) ) {
4203 + $args = wdkit_kit_import_with_site_identity( $args );
4204 + }
4205 +
4206 + $response = WDesignKit_Data_Query::get_data( $api_type, $args );
4207 +
4208 + if ( is_wp_error( $response ) ) {
4209 + wp_send_json( array(
4210 + 'success' => false,
4211 + 'message' => $response->get_error_message(),
4212 + ) );
4213 + wp_die();
4214 + }
4215 +
1141 4216 $custom_meta = isset( $_POST['custom_meta'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_meta'] ) ) : false;
1142 4217
1143 4218 /** Custom meta Field */
1144 4219 if ( ! empty( $custom_meta ) && 'true' === $custom_meta && ! empty( $response ) && ! empty( $response['content'] ) ) {
@@ -1149,9 +4224,9 @@
1149 4224
1150 4225 if ( ! empty( $meta_data ) ) {
1151 4226 foreach ( $meta_data as $meta_key => $meta_val ) {
1152 4227 if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) {
1153 - $meta_val[0] = maybe_unserialize( $meta_val[0] );
4228 + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) );
1154 4229 }
1155 4230
1156 4231 if ( get_post_meta( get_the_ID(), $meta_key, true ) === '' ) {
1157 4232 add_post_meta( get_the_ID(), $meta_key, $meta_val[0] );
@@ -1162,8 +4237,34 @@
1162 4237 }
1163 4238 }
1164 4239 }
1165 4240
4241 + /**
4242 + * Fires after a template has been imported from the cloud.
4243 + *
4244 + * WDesignKit's templates live in the cloud, so nothing local records that an import
4245 + * happened — there is no post type, no option, nothing to count after the fact. This is the
4246 + * only moment the information exists.
4247 + *
4248 + * @since 2.6.4
4249 + *
4250 + * @param string $kind 'single' or 'kit'.
4251 + * @param string $builder Builder the template was imported for, e.g. 'elementor'.
4252 + * @param int $count How many templates this import brought in.
4253 + */
4254 + // Only a completed import counts. The cloud's failure shape for this endpoint family sets
4255 + // content => 'error' (see the sibling check in wdkit_import_kit_template() above) — that is
4256 + // non-empty, so the previous `||` fired the counter on failed imports too. Require success
4257 + // AND an absent/non-'error' content instead.
4258 + if ( ! empty( $response['success'] ) && ( ! isset( $response['content'] ) || 'error' !== $response['content'] ) ) {
4259 + do_action(
4260 + 'wdkit_template_imported',
4261 + 'import_kit_template' === $api_type ? 'kit' : 'single',
4262 + isset( $_POST['builder'] ) ? sanitize_key( wp_unslash( $_POST['builder'] ) ) : '',
4263 + 1
4264 + );
4265 + }
4266 +
1166 4267 wp_send_json( $response );
1167 4268 wp_die();
1168 4269 }
1169 4270
@@ -1174,10 +4275,860 @@
1174 4275 *
1175 4276 * @param array $content store media content.
1176 4277 * @param string $editor it is check editor.
1177 4278 */
1178 - public function wdkit_media_import( $content = array(), $editor = '' ) {
4279 + /**
4280 + * Resolve a local upload URL back to its attachment ID.
4281 + *
4282 + * Handles the "-scaled" copy WordPress makes for large originals and any
4283 + * "-1920x1280" size suffix, both of which attachment_url_to_postid() misses because
4284 + * they are not the value stored in _wp_attached_file.
4285 + *
4286 + * @since 2.6.2
4287 + *
4288 + * @param string $url Local upload URL.
4289 + * @return int Attachment ID, or 0.
4290 + */
4291 + private static function wdkit_attachment_id_from_url( $url ) {
4292 + static $cache = array();
1179 4293
4294 + if ( isset( $cache[ $url ] ) ) {
4295 + return $cache[ $url ];
4296 + }
4297 +
4298 + $id = (int) attachment_url_to_postid( $url );
4299 +
4300 + if ( ! $id ) {
4301 + // Try the original file behind a -scaled or -WxH derivative.
4302 + $stripped = preg_replace( '/-scaled(\.[a-z0-9]+)$/i', '$1', $url );
4303 + $stripped = preg_replace( '/-\d+x\d+(\.[a-z0-9]+)$/i', '$1', (string) $stripped );
4304 +
4305 + if ( $stripped && $stripped !== $url ) {
4306 + $id = (int) attachment_url_to_postid( $stripped );
4307 + }
4308 + }
4309 +
4310 + // Only remember hits. Page imports run concurrently, so an attachment created by a
4311 + // sibling request may not exist yet when this is first asked — caching that miss
4312 + // would keep every later control in this request pointing at nothing.
4313 + if ( $id ) {
4314 + $cache[ $url ] = $id;
4315 + }
4316 +
4317 + return $id;
4318 + }
4319 +
4320 + /**
4321 + * Is this media reference still pointing off-site?
4322 + *
4323 + * Template content arrives holding the URLs of wherever the media lived before. Those
4324 + * carry that site's attachment IDs, which have no meaning here - and can collide with
4325 + * unrelated local posts.
4326 + *
4327 + * @since 2.6.2
4328 + *
4329 + * @param string $url URL from a media control.
4330 + * @return bool True when the URL points at another site's uploads.
4331 + */
4332 + /**
4333 + * Localise every foreign SVG media control in a set of Elementor elements, in-request.
4334 + *
4335 + * The deferred path cannot carry SVGs (see wdkit_collect_foreign_media_urls()), so they
4336 + * take the same route they always did: wdkit_localise_media_url(), which resolves through
4337 + * Elementor's own importer and its `_elementor_source_image_hash` cache, so an SVG a
4338 + * sibling page already imported costs a database lookup rather than a download.
4339 + *
4340 + * Only the `{ url, id }` media-control shape is handled here, matching
4341 + * wdkit_repair_attachment_ids(). An SVG inlined into an HTML/text control has no `id` to
4342 + * correct, and the finalize sweep still covers those - which is why the sweep refuses to
4343 + * skip any page that still holds a foreign SVG reference.
4344 + *
4345 + * @since 2.7.1
4346 + *
4347 + * @param mixed $node Elementor data, walked recursively.
4348 + * @return mixed The data with local SVG references.
4349 + */
4350 + private static function wdkit_localise_foreign_svgs( $node ) {
4351 +
4352 + if ( ! is_array( $node ) ) {
4353 + return $node;
4354 + }
4355 +
4356 + if ( isset( $node['url'] ) && is_string( $node['url'] ) && array_key_exists( 'id', $node )
4357 + && self::wdkit_is_foreign_media_url( $node['url'] )
4358 + && preg_match( '/\.svg$/i', (string) wp_parse_url( $node['url'], PHP_URL_PATH ) )
4359 + ) {
4360 + $local = self::wdkit_localise_media_url( $node['url'], isset( $node['id'] ) ? (int) $node['id'] : 0 );
4361 +
4362 + if ( ! empty( $local['id'] ) && ! empty( $local['url'] ) ) {
4363 + $node['id'] = $local['id'];
4364 + $node['url'] = $local['url'];
4365 + }
4366 + }
4367 +
4368 + foreach ( $node as $key => $value ) {
4369 + if ( is_array( $value ) ) {
4370 + $node[ $key ] = self::wdkit_localise_foreign_svgs( $value );
4371 + }
4372 + }
4373 +
4374 + return $node;
4375 + }
4376 +
4377 + /**
4378 + * Empty the attachment ID on every media control whose image is being deferred.
4379 + *
4380 + * The IDs that arrive in a template's data belong to the site the kit was authored on.
4381 + * Until the background pass replaces them (see wdkit_replace_deferred_media()), each one
4382 + * lands in one of two states on this site, and neither is what Elementor expects:
4383 + *
4384 + * - It matches nothing locally. Elementor's `<img>` path copes - it checks
4385 + * wp_attachment_is_image() and falls back to the url - but the CSS path does not.
4386 + * Control_Media::get_style_value() only takes the url branch when the ID is *empty*;
4387 + * given a non-empty one it returns wp_get_attachment_image_url( $id ), which is false
4388 + * for a dangling ID, so the declaration is dropped and a container background renders
4389 + * as nothing at all. Measured on a Taj Bakery import: 9 of 9 background_image controls
4390 + * produced 0 background-image rules for the whole deferred window.
4391 + * - It happens to match an unrelated local attachment. Then wp_attachment_is_image()
4392 + * passes and Elementor renders that other image - the wrong picture, silently. Whether
4393 + * this bites is pure luck: it needs the template's ID range to overlap the IDs a fresh
4394 + * import creates, which is why Zion hit it and Taj Bakery did not.
4395 + *
4396 + * An empty ID is the one state every affected renderer is written to handle, so this
4397 + * turns both failures into "shows the source image until the local copy lands":
4398 + *
4399 + * - Control_Media::get_style_value() empty ID -> parent -> the raw url.
4400 + * - Group_Control_Image_Size::get_attachment_image_html() empty ID -> url fallback.
4401 + * - image-carousel `if ( ! $image_url && isset( $attachment['url'] ) )`.
4402 + * - image-gallery `$image['url'] ?? ''`.
4403 + *
4404 + * So this is deliberately not a blanket "clear every ID": it keys off the exact URL list
4405 + * that was just handed to the background pass. Anything already local keeps its ID, and
4406 + * so do SVGs, which wdkit_localise_foreign_svgs() has already imported for real.
4407 + *
4408 + * One renderer is not helped by this: The Plus Addons' tp_get_image() returns '' when
4409 + * wp_get_attachment_image_src() fails and has no url fallback, so its widgets show
4410 + * nothing during the window either way. That is a limitation of that helper rather than
4411 + * something this pass can fix, and the background pass repairs it on completion.
4412 + *
4413 + * @since 2.6.6
4414 + *
4415 + * @param mixed $node Elementor data, walked recursively.
4416 + * @param array $deferred Source URLs handed to the background pass.
4417 + * @return mixed Data with the deferred controls' IDs emptied.
4418 + */
4419 + private static function wdkit_blank_deferred_media_ids( $node, array $deferred ) {
4420 +
4421 + if ( ! is_array( $node ) || empty( $deferred ) ) {
4422 + return $node;
4423 + }
4424 +
4425 + $lookup = array_flip( $deferred );
4426 +
4427 + $walk = function ( $item ) use ( &$walk, $lookup ) {
4428 +
4429 + if ( ! is_array( $item ) ) {
4430 + return $item;
4431 + }
4432 +
4433 + // A media control: `url` and `id` as siblings. Empty the ID only when this
4434 + // exact URL is one the background pass is going to replace.
4435 + if ( isset( $item['url'] ) && is_string( $item['url'] )
4436 + && array_key_exists( 'id', $item )
4437 + && isset( $lookup[ $item['url'] ] )
4438 + ) {
4439 + // '' rather than 0: this is the value Elementor itself writes when it
4440 + // rejects an ID (see get_attachment_image_html()), and what its
4441 + // empty()-based checks are written against.
4442 + $item['id'] = '';
4443 + }
4444 +
4445 + foreach ( $item as $key => $value ) {
4446 + if ( is_array( $value ) ) {
4447 + $item[ $key ] = $walk( $value );
4448 + }
4449 + }
4450 +
4451 + return $item;
4452 + };
4453 +
4454 + return $walk( $node );
4455 + }
4456 +
4457 + /**
4458 + * Build the demo-URL -> real-permalink substitution table for a finished kit import.
4459 + *
4460 + * The kit's templates link to each other by the *authoring* site's URLs, so every
4461 + * internal link has to be rewritten once each page exists and its real permalink is
4462 + * known. Two representations of the same URL have to be covered, because the two places
4463 + * these links live store them differently:
4464 + *
4465 + * - `post_content` holds them raw, inside an attribute: `href="https://demo/about/"`.
4466 + * - `_elementor_data` holds them JSON-encoded, so every slash is escaped:
4467 + * `"url":"https:\/\/demo\/about\/"`. This is the one the widgets actually render from.
4468 + *
4469 + * Each key is anchored on the surrounding quote so `/about` cannot match inside
4470 + * `/about-us`, and both the bare and trailing-slash forms are mapped because templates
4471 + * are inconsistent about it. The trailing slash has to be escaped in the escaped
4472 + * variants (`...\/about\/"`, not `...\/about/"`) - getting that wrong is invisible in
4473 + * post_content and leaves the whole navigation pointing at the demo site.
4474 + *
4475 + * @since 2.6.6
4476 + *
4477 + * @param array $imported Inserted templates: each entry needs `post_url` (the demo URL)
4478 + * and `id` (the local post it became).
4479 + * @return array strtr() substitution table.
4480 + */
4481 + private static function wdkit_build_nav_url_map( $imported ) {
4482 +
4483 + $url_map = array();
4484 +
4485 + foreach ( (array) $imported as $entry ) {
4486 +
4487 + $entry = (array) $entry;
4488 +
4489 + if ( empty( $entry['post_url'] ) || empty( $entry['id'] ) ) {
4490 + continue;
4491 + }
4492 +
4493 + $permalink = get_permalink( (int) $entry['id'] );
4494 +
4495 + if ( ! $permalink ) {
4496 + continue;
4497 + }
4498 +
4499 + $demo = rtrim( (string) $entry['post_url'], '/' );
4500 + $target = rtrim( (string) $permalink, '/' );
4501 +
4502 + if ( '' === $demo || $demo === $target ) {
4503 + continue;
4504 + }
4505 +
4506 + // On a site left on plain permalinks the target is a query string, not a path:
4507 + // `http://site/?page_id=54`. Re-adding the trailing slash the demo URL carried
4508 + // yields `?page_id=54/`, and joining a query string onto it with `?` yields a
4509 + // second `?` - both malformed. This stayed invisible on a pretty-permalink site
4510 + // because there the target really is a path and appending is correct.
4511 + $has_query = ( false !== strpos( $target, '?' ) );
4512 +
4513 + // The "…and a trailing slash" form of the target, which for a query-string
4514 + // permalink is simply the permalink - there is nothing to put a slash after.
4515 + $target_slash = $has_query ? $target : $target . '/';
4516 +
4517 + // What a query string has to be joined on, given what the target already carries.
4518 + $query_join = $has_query ? '&' : '?';
4519 +
4520 + // Raw form, as it appears in post_content.
4521 + foreach ( array( '"', '\\"' ) as $q ) {
4522 + $url_map[ $q . $demo . $q ] = $q . $target . $q;
4523 + $url_map[ $q . $demo . '/' . $q ] = $q . $target_slash . $q;
4524 + }
4525 +
4526 + // JSON-escaped form, as it appears in _elementor_data - trailing slash escaped too.
4527 + $demo_esc = str_replace( '/', '\\/', $demo );
4528 + $target_esc = str_replace( '/', '\\/', $target );
4529 + $target_slash_esc = $has_query ? $target_esc : $target_esc . '\\/';
4530 + $target_slash_raw = $has_query ? $target_esc : $target_esc . '/';
4531 +
4532 + foreach ( array( '"', '\\"' ) as $q ) {
4533 + $url_map[ $q . $demo_esc . $q ] = $q . $target_esc . $q;
4534 + $url_map[ $q . $demo_esc . '\\/' . $q ] = $q . $target_slash_esc . $q;
4535 + // Kept for templates that stored the slash unescaped next to escaped ones.
4536 + $url_map[ $q . $demo_esc . '/' . $q ] = $q . $target_slash_raw . $q;
4537 + }
4538 +
4539 + // An in-page link (`/taj/#menu-list`) or one carrying a query string never has the
4540 + // quote straight after the path, so none of the anchored keys above can reach it and
4541 + // the link keeps pointing at the demo site. `#` and `?` cannot occur inside a path
4542 + // segment, so they anchor the match just as safely as the closing quote does.
4543 + foreach ( array( '#', '?' ) as $sep ) {
4544 + $join = ( '?' === $sep ) ? $query_join : $sep;
4545 +
4546 + $url_map[ $demo . $sep ] = $target . $join;
4547 + $url_map[ $demo . '/' . $sep ] = $target_slash . $join;
4548 + $url_map[ $demo_esc . $sep ] = $target_esc . $join;
4549 + $url_map[ $demo_esc . '\\/' . $sep ] = $target_slash_esc . $join;
4550 + }
4551 + }
4552 +
4553 + return $url_map;
4554 + }
4555 +
4556 + /**
4557 + * Every source-site media URL inside a set of Elementor elements.
4558 + *
4559 + * Scans the serialised tree rather than walking control-by-control, for the same reason
4560 + * the Gutenberg SVG prefetch does: a URL can sit in a media control's `url`, in a
4561 + * responsive variant beneath it, or inline in an HTML/text control that never had an
4562 + * `id` sibling - and wdkit_replace_deferred_media() rewrites all three shapes, the last
4563 + * by plain substring swap. Matching the serialised form keeps this collector and that
4564 + * rewriter looking at the same set. Over-collecting is harmless: the cron skips a URL it
4565 + * cannot resolve, and an already-local one is filtered out below.
4566 + *
4567 + * SVGs are excluded by default, and deliberately: the background pass runs with no user,
4568 + * where `svg` is not in get_allowed_mime_types() because the filter granting it is
4569 + * capability-gated - so a deferred SVG sideload fails silently and the reference stays
4570 + * remote forever. The Gutenberg walk carves SVGs out of deferral for the same reason
4571 + * (see the `/\.svg$/i` test in Wdkit_Import_Images::wdkit_Import_media()); they are
4572 + * localised inline instead, by wdkit_localise_foreign_svgs() below.
4573 + *
4574 + * Video IS included, and does not share that problem: mp4/m4v, webm and mov/qt are all in
4575 + * get_allowed_mime_types() with no user, so a deferred video sideload succeeds where an
4576 + * SVG one cannot. Deferral is also where video belongs - the files are the largest thing
4577 + * a kit references, and nothing is waiting on the cron event. Until this listed them, a
4578 + * template's video kept the authoring site's URL and attachment id forever, because the
4579 + * deferred path has no other media discovery (see wdkit_media_import()'s elementor
4580 + * branch) - which is why a video widget rendered as an empty frame.
4581 + *
4582 + * @since 2.7.1
4583 + *
4584 + * @param mixed $elements Elementor `elements` data.
4585 + * @param string|null $extensions Regex alternation of extensions; defaults to the shared list.
4586 + * @return array<string> Distinct foreign media URLs.
4587 + */
4588 + private static function wdkit_collect_foreign_media_urls( $elements, $extensions = null ) {
4589 +
4590 + if ( ! class_exists( 'Wdkit_Image_Guard' ) ) {
4591 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php';
4592 + }
4593 +
4594 + // One list for defer / blank-id / remap - see Wdkit_Image_Guard::DEFERRABLE_EXTENSIONS.
4595 + $extensions = ( null === $extensions ) ? Wdkit_Image_Guard::DEFERRABLE_EXTENSIONS : $extensions;
4596 +
4597 + $json = wp_json_encode( $elements, JSON_UNESCAPED_SLASHES );
4598 +
4599 + if ( ! is_string( $json ) || '' === $json ) {
4600 + return array();
4601 + }
4602 +
4603 + // The trailing query group is required, not cosmetic. Without it the engine matches
4604 + // greedily and then backtracks to the extension, so
4605 + // `…/photo.jpeg?auto=compress&w=1260` was collected as `…/photo.jpeg` - a URL that
4606 + // appears nowhere in the content. wdkit_blank_deferred_media_ids() compares the node's
4607 + // url exactly and missed, leaving the dangling foreign id it exists to clear (the
4608 + // measured symptom: 9 of 9 background_image controls produced no background-image
4609 + // rule), and the cron's str_replace then produced `…/local.jpg?auto=compress&w=1260`.
4610 + $candidates = array();
4611 +
4612 + if ( preg_match_all( '#https?://[^"\'\s<>()]+\.(?:' . $extensions . ')(?:\?[^"\'\s<>()]*)?#i', $json, $matches ) ) {
4613 + $candidates = $matches[0];
4614 + }
4615 +
4616 + // The regex above can only find a URL that spells its type. A media control whose url
4617 + // has no extension at all - which the template CDN serves, resolving the type by
4618 + // Content-Type - matched nothing, so it was never deferred, never recorded and never
4619 + // localised: the reference stayed remote permanently, with no error anywhere.
4620 + //
4621 + // Walking the tree settles it without a single HTTP request: a node shaped
4622 + // `{ url, id }` IS a media control, whatever the url looks like. This is the same
4623 + // shape wdkit_blank_deferred_media_ids() and wdkit_replace_deferred_media() key on,
4624 + // so what gets collected here is exactly what those two can act on. The regex pass
4625 + // stays for URLs embedded in HTML and inline-CSS strings, which have no node shape.
4626 + $structural = self::wdkit_collect_media_control_urls( $elements );
4627 +
4628 + $found = array();
4629 +
4630 + foreach ( array_unique( $candidates ) as $url ) {
4631 + if ( self::wdkit_is_foreign_media_url( $url ) ) {
4632 + $found[] = $url;
4633 + }
4634 + }
4635 +
4636 + // No extension requirement for these - the node shape is the proof.
4637 + foreach ( array_unique( $structural ) as $url ) {
4638 + if ( self::wdkit_is_foreign_media_url( $url, false ) ) {
4639 + $found[] = $url;
4640 + }
4641 + }
4642 +
4643 + return array_values( array_unique( $found ) );
4644 + }
4645 +
4646 + /**
4647 + * Collect the url of every `{ url, id }` media control in an element tree.
4648 + *
4649 + * Structural, not textual: no extension is required, so an extensionless CDN URL is
4650 + * found here even though wdkit_collect_foreign_media_urls()'s regex cannot see it.
4651 + *
4652 + * SVG is still excluded, for the reason given on that method: the background pass runs
4653 + * with no user, `svg` is therefore absent from get_allowed_mime_types(), and a deferred
4654 + * SVG sideload fails silently. Those are localised in-request instead.
4655 + *
4656 + * @since 2.7.1
4657 + *
4658 + * @param mixed $node Elementor data, walked recursively.
4659 + * @return array<string> Media-control URLs, in tree order.
4660 + */
4661 + private static function wdkit_collect_media_control_urls( $node ) {
4662 +
4663 + if ( ! is_array( $node ) ) {
4664 + return array();
4665 + }
4666 +
4667 + $found = array();
4668 +
4669 + // `url` + `id` as siblings is Elementor's media-control shape. A link control carries
4670 + // is_external/nofollow instead and never an id, so excluding those keys keeps a
4671 + // button's href out of the media queue even if a theme adds an id to it.
4672 + $is_media_control = isset( $node['url'] ) && is_string( $node['url'] )
4673 + && array_key_exists( 'id', $node )
4674 + && ! array_key_exists( 'is_external', $node )
4675 + && ! array_key_exists( 'nofollow', $node );
4676 +
4677 + if ( $is_media_control ) {
4678 + $url = $node['url'];
4679 + $path = (string) wp_parse_url( $url, PHP_URL_PATH );
4680 +
4681 + if ( 0 === strpos( $url, 'http' ) && ! preg_match( '/\.svg$/i', $path ) ) {
4682 + $found[] = $url;
4683 + }
4684 + }
4685 +
4686 + foreach ( $node as $value ) {
4687 + if ( is_array( $value ) ) {
4688 + $found = array_merge( $found, self::wdkit_collect_media_control_urls( $value ) );
4689 + }
4690 + }
4691 +
4692 + return $found;
4693 + }
4694 +
4695 + /**
4696 + * Whether a media URL points at something this site does not already host.
4697 + *
4698 + * Anything under the uploads baseurl is ours; everything else - the template CDN, the
4699 + * authoring site, a stock provider - is foreign and has to be localised. Data URIs and
4700 + * relative paths are not foreign either, and are refused here rather than downstream.
4701 + *
4702 + * @since 2.7.1
4703 + *
4704 + * @param string $url Absolute media URL.
4705 + * @return bool True when the URL needs localising.
4706 + */
4707 + private static function wdkit_is_foreign_media_url( $url, $require_extension = true ) {
4708 +
4709 + if ( ! class_exists( 'Wdkit_Image_Guard' ) ) {
4710 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php';
4711 + }
4712 +
4713 + $uploads = wp_get_upload_dir();
4714 +
4715 + return Wdkit_Image_Guard::is_foreign_media( $url, isset( $uploads['baseurl'] ) ? $uploads['baseurl'] : '', $require_extension );
4716 + }
4717 +
4718 + /**
4719 + * Find - or make - the local attachment behind a source-site media URL.
4720 + *
4721 + * Elementor stamps every image it imports with `_elementor_source_image_hash`
4722 + * (sha1 of the URL it came from), and its importer consults that before doing any
4723 + * network work. Delegating here means a URL already imported at create time resolves
4724 + * from the database, and one that never made it is fetched exactly once.
4725 + *
4726 + * Only ever called for foreign URLs. Handing it a local URL would re-download the
4727 + * file and leave a duplicate, because the stored hash is of the *remote* URL and so
4728 + * would never match.
4729 + *
4730 + * @since 2.6.2
4731 + *
4732 + * @param string $url Source-site media URL.
4733 + * @param int $source_id The source site's attachment ID, used as Elementor's cache key.
4734 + * @return array Local `id` and `url`, or an empty array when it cannot be resolved.
4735 + */
4736 + private static function wdkit_localise_media_url( $url, $source_id = 0 ) {
4737 + static $cache = array();
4738 +
4739 + if ( isset( $cache[ $url ] ) ) {
4740 + return $cache[ $url ];
4741 + }
4742 +
4743 + // SVGs: use the prefetch-aware sideloader. It consults the concurrent
4744 + // warm-up cache (see the prefetch_remote_files() call in the Elementor
4745 + // $defer_media branch of wdkit_media_import()); Elementor's own importer
4746 + // below always makes its own blocking round trip and never looks at it.
4747 + // Fall through on any miss/failure so behaviour is unchanged when the
4748 + // warm-up did not run or the file could not be fetched.
4749 + if ( preg_match( '/\.svg$/i', (string) wp_parse_url( $url, PHP_URL_PATH ) ) ) {
4750 + self::wdkit_require_import_images();
4751 +
4752 + $warm = Wdkit_Import_Images::wdkit_Import_media(
4753 + array(
4754 + 'id' => (int) $source_id,
4755 + 'url' => $url,
4756 + )
4757 + );
4758 +
4759 + if ( is_array( $warm ) && ! empty( $warm['id'] ) && ! empty( $warm['url'] )
4760 + && ! self::wdkit_is_foreign_media_url( $warm['url'] )
4761 + && self::wdkit_is_usable_attachment( (int) $warm['id'] )
4762 + ) {
4763 + $cache[ $url ] = array(
4764 + 'id' => (int) $warm['id'],
4765 + 'url' => $warm['url'],
4766 + );
4767 +
4768 + return $cache[ $url ];
4769 + }
4770 + }
4771 +
4772 + if ( ! did_action( 'elementor/loaded' ) || ! class_exists( '\\Elementor\\Plugin' ) ) {
4773 + return array();
4774 + }
4775 +
4776 + $images = \Elementor\Plugin::$instance->templates_manager->get_import_images_instance();
4777 +
4778 + if ( ! $images ) {
4779 + return array();
4780 + }
4781 +
4782 + // A download may happen, so keep the oversized-image guard in force.
4783 + self::wdkit_guard_oversized_images();
4784 +
4785 + // Elementor only consults its hash table when an id is present - but it also
4786 + // memoises the result per source id in Import_Images::$_replace_image_ids, and that
4787 + // memo is checked BEFORE the url is looked at. So a shared placeholder is not a
4788 + // harmless "any non-zero value": the first url imported under it wins, and every
4789 + // later url handed the same placeholder is silently answered with that first
4790 + // attachment. Measured on a Zion import: one image served 10 of the page's media
4791 + // controls and all 3 of its background rules, while the correct files sat in the
4792 + // library unreferenced.
4793 + //
4794 + // A stable, url-derived stand-in keeps the hash-table path enabled while giving each
4795 + // distinct url its own memo slot. crc32 is used for a compact positive integer, not
4796 + // for collision resistance - a clash would only cost a redundant lookup, and the
4797 + // per-url cache above already prevents repeats within a request.
4798 + $probe_id = $source_id ? (int) $source_id : ( crc32( $url ) & 0x7FFFFFFF );
4799 +
4800 + self::wdkit_watch_elementor_attachments();
4801 +
4802 + $imported = $images->import(
4803 + array(
4804 + 'id' => $probe_id,
4805 + 'url' => $url,
4806 + )
4807 + );
4808 +
4809 + // Elementor answers this from its own `_elementor_source_image_hash` index before it
4810 + // downloads anything, so it hands back whatever attachment claimed that hash - a
4811 + // record with no file included. Same reason the deferred lookup is gated: writing
4812 + // that id into the page is what leaves a control resolving to nothing. The stale
4813 + // hash is dropped on the way out so the next attempt is a miss and downloads.
4814 + $local = array();
4815 +
4816 + if ( ! empty( $imported['id'] ) && ! empty( $imported['url'] ) ) {
4817 + if ( self::wdkit_is_usable_attachment( $imported['id'] ) ) {
4818 + $local = array(
4819 + 'id' => (int) $imported['id'],
4820 + 'url' => $imported['url'],
4821 + );
4822 + } else {
4823 + self::wdkit_forget_reuse_keys( $imported['id'] );
4824 + }
4825 + }
4826 +
4827 + // Drained inline as well as on shutdown: Elementor has finished writing this row's
4828 + // metadata by the time import() returns, so it is safe to remove now, and removing it
4829 + // now keeps a stub from being visible to the rest of the import.
4830 + self::wdkit_sweep_stub_attachments();
4831 +
4832 + // Remember hits only: a sibling request importing concurrently may simply not have
4833 + // finished yet, and caching that miss would strand every later control on this page.
4834 + if ( $local ) {
4835 + $cache[ $url ] = $local;
4836 + }
4837 +
4838 + return $local;
4839 + }
4840 +
4841 + /**
4842 + * Whether a page still references an SVG on the template's own site.
4843 + *
4844 + * @since 2.7.1
4845 + *
4846 + * @param int $post_id Page to inspect.
4847 + * @return bool True when a foreign SVG reference remains.
4848 + */
4849 + private static function wdkit_page_has_foreign_svg( $post_id ) {
4850 +
4851 + $raw = get_post_meta( (int) $post_id, '_elementor_data', true );
4852 +
4853 + if ( empty( $raw ) ) {
4854 + return false;
4855 + }
4856 +
4857 + $data = is_array( $raw ) ? $raw : json_decode( $raw, true );
4858 +
4859 + if ( ! is_array( $data ) ) {
4860 + return false;
4861 + }
4862 +
4863 + return ! empty( self::wdkit_collect_foreign_media_urls( $data, 'svg' ) );
4864 + }
4865 +
4866 + /**
4867 + * Post IDs that still have a deferred-media event waiting in wp-cron.
4868 + *
4869 + * Read off the cron array rather than tracked separately, so it cannot drift from what
4870 + * wkit_schedule_deferred_media_sync() actually queued.
4871 + *
4872 + * @since 2.7.1
4873 + *
4874 + * @return array<int> Post IDs with a pending sideload event.
4875 + */
4876 + private static function wdkit_pages_awaiting_deferred_media() {
4877 +
4878 + $crons = _get_cron_array();
4879 +
4880 + if ( ! is_array( $crons ) ) {
4881 + return array();
4882 + }
4883 +
4884 + $pending = array();
4885 +
4886 + foreach ( $crons as $events ) {
4887 + if ( empty( $events['wdkit_async_sideload_page_images'] ) || ! is_array( $events['wdkit_async_sideload_page_images'] ) ) {
4888 + continue;
4889 + }
4890 +
4891 + foreach ( $events['wdkit_async_sideload_page_images'] as $event ) {
4892 + // args[0] is the post id - see wkit_schedule_deferred_media_sync().
4893 + if ( isset( $event['args'][0] ) ) {
4894 + $pending[] = (int) $event['args'][0];
4895 + }
4896 + }
4897 + }
4898 +
4899 + return array_unique( $pending );
4900 + }
4901 +
4902 + /**
4903 + * Repair dangling attachment IDs across every page of a finished import.
4904 + *
4905 + * The create-time repair in wdkit_media_import() can only see attachments that already
4906 + * exist. Pages import concurrently and share images — an icon first imported by one
4907 + * page is referenced by several others — so a page that runs early legitimately cannot
4908 + * resolve an image a sibling request has not created yet.
4909 + *
4910 + * This runs at the finalize step, once every page and attachment exists, and fixes
4911 + * whatever the per-page pass had to leave behind.
4912 + *
4913 + * @since 2.6.2
4914 + *
4915 + * @param array $page_ids Imported post IDs.
4916 + * @param bool $local_only Skip the download-triggering branch of
4917 + * wdkit_repair_attachment_ids() and only fix dangling ids on
4918 + * already-local media. The PHP runner passes true here — its
4919 + * site_settings step (stage_setup) runs before
4920 + * schedule_media_sweep() (stage_finalize) ever queues anything,
4921 + * so wdkit_pages_awaiting_deferred_media() below is always empty
4922 + * for it and every still-remote image used to be downloaded
4923 + * synchronously right here instead of by the deferred pass - see
4924 + * ClickUp 14ynqxywncc. The browser's own AJAX call already waits
4925 + * for scheduling first, so it is unaffected by this defaulting to
4926 + * false.
4927 + * @return int Number of pages actually rewritten.
4928 + */
4929 + private function wdkit_sweep_attachment_ids( $page_ids, $local_only = false ) {
4930 +
4931 + if ( empty( $page_ids ) || ! did_action( 'elementor/loaded' ) ) {
4932 + return 0;
4933 + }
4934 +
4935 + $fixed = 0;
4936 + $ids = array_unique( array_map( 'intval', $page_ids ) );
4937 +
4938 + // Leave alone any page whose media is already queued for the background pass.
4939 + //
4940 + // The repair walk itself is trivial - 15ms across 18 pages. The cost is that
4941 + // wdkit_repair_attachment_ids() resolves foreign URLs through
4942 + // wdkit_localise_media_url(), which downloads anything not already local. For an
4943 + // Elementor kit that quietly made this sweep the media importer, inside the import
4944 + // request, at 156s-225s. wdkit_async_sideload_page_images() does the same work per
4945 + // page - rewriting _elementor_data (url and id), clearing _elementor_css, flushing
4946 + // the files manager - but off the request, and only once that page's images exist.
4947 + //
4948 + // Safe only because the queue is genuinely populated for both builders now: the
4949 + // Elementor branch of wdkit_media_import() reports what it deferred (see
4950 + // wdkit_collect_foreign_media_urls()), and the client awaits the scheduling call
4951 + // before this runs. Pages with nothing queued still sweep exactly as before.
4952 + $queued = self::wdkit_pages_awaiting_deferred_media();
4953 +
4954 + if ( ! empty( $queued ) ) {
4955 + foreach ( $queued as $queued_id ) {
4956 + // A queued page is only safe to skip once nothing on it still needs
4957 + // in-request work. The deferred list is raster-only, and an SVG inlined into
4958 + // an HTML control has no `id` for wdkit_localise_foreign_svgs() to correct -
4959 + // so if any foreign SVG reference survives, this page still needs the sweep.
4960 + if ( self::wdkit_page_has_foreign_svg( $queued_id ) ) {
4961 + continue;
4962 + }
4963 +
4964 + $ids = array_values( array_diff( $ids, array( $queued_id ) ) );
4965 + }
4966 +
4967 + if ( empty( $ids ) ) {
4968 + return 0;
4969 + }
4970 + }
4971 +
4972 + foreach ( $ids as $post_id ) {
4973 +
4974 + if ( ! $post_id ) {
4975 + continue;
4976 + }
4977 +
4978 + $raw = get_post_meta( $post_id, '_elementor_data', true );
4979 +
4980 + if ( empty( $raw ) ) {
4981 + continue;
4982 + }
4983 +
4984 + $data = is_array( $raw ) ? $raw : json_decode( $raw, true );
4985 +
4986 + if ( ! is_array( $data ) ) {
4987 + continue;
4988 + }
4989 +
4990 + $repaired = self::wdkit_repair_attachment_ids( $data, $local_only );
4991 +
4992 + if ( wp_json_encode( $repaired ) === wp_json_encode( $data ) ) {
4993 + continue;
4994 + }
4995 +
4996 + // Save through the document API so Elementor regenerates the page CSS — the
4997 + // background-image rules are only emitted once the IDs resolve.
4998 + $document = \Elementor\Plugin::$instance->documents->get( $post_id );
4999 +
5000 + // Count only a save that actually happened. Document::save() returns false
5001 + // without saving when the current user cannot edit the post, and reporting
5002 + // those as repaired hides the fact that nothing changed.
5003 + if ( $document && $document->save( array( 'elements' => $repaired ) ) ) {
5004 + ++$fixed;
5005 + }
5006 + }
5007 +
5008 + // No cache flush here on purpose. Document::save() above already deletes the css
5009 + // file, _elementor_css, and the element cache for each page it saved - see
5010 + // wdkit_invalidate_elementor_page_cache() for why the site-wide
5011 + // files_manager->clear_cache() that used to follow this loop is the wrong tool.
5012 +
5013 + return $fixed;
5014 + }
5015 +
5016 + /**
5017 + * Repair media controls whose attachment ID does not resolve.
5018 + *
5019 + * Elementor media controls store `{ url, id }`. Controls flagged `has_sizes` — the
5020 + * container/section **background image** among them — do not render from `url` at all:
5021 + * CSS generation resolves the image through the attachment ID, so a dangling ID
5022 + * produces no `background-image` rule and the section renders with no image even
5023 + * though its URL is perfectly correct.
5024 + *
5025 + * IDs arrive dangling whenever Elementor's own importer does not rewrite a control —
5026 + * it carries the source site's ID, which means nothing locally. Now that the URL is
5027 + * already a local upload before import, the ID can simply be looked up from it.
5028 + *
5029 + * @since 2.6.2
5030 + *
5031 + * @param mixed $node Elementor data, walked recursively.
5032 + * @return mixed Data with resolvable attachment IDs.
5033 + */
5034 + /**
5035 + * Is this URL a video the background pass owns?
5036 + *
5037 + * @since 2.7.1
5038 + *
5039 + * @param string $url Media URL.
5040 + * @return bool
5041 + */
5042 + private static function wdkit_is_video_url( $url ) {
5043 + if ( ! class_exists( 'Wdkit_Image_Guard' ) ) {
5044 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php';
5045 + }
5046 +
5047 + return (bool) preg_match(
5048 + '/\.(?:' . Wdkit_Image_Guard::VIDEO_EXTENSIONS . ')$/i',
5049 + (string) wp_parse_url( (string) $url, PHP_URL_PATH )
5050 + );
5051 + }
5052 +
5053 + private static function wdkit_repair_attachment_ids( $node, $local_only = false ) {
5054 +
5055 + if ( ! is_array( $node ) ) {
5056 + return $node;
5057 + }
5058 +
5059 + // A media control value: has a url, and an id slot to correct.
5060 + if ( isset( $node['url'] ) && is_string( $node['url'] ) && array_key_exists( 'id', $node ) ) {
5061 +
5062 + $current = (int) $node['id'];
5063 +
5064 + if ( self::wdkit_is_foreign_media_url( $node['url'] ) ) {
5065 + // Still pointing at the source site. Ask Elementor for the local copy: its
5066 + // _elementor_source_image_hash lookup returns the attachment the create-time
5067 + // import already made, so this normally costs a single query and no download.
5068 + //
5069 + // Skipped entirely in $local_only mode: that caller is the deferred pass,
5070 + // which owns the decision about what gets downloaded and when, and must not
5071 + // have a repair walk start fetching things behind it.
5072 + //
5073 + // Video is skipped for the same reason in every mode. It is localised only by
5074 + // the background pass, so a still-foreign video URL here is not a fault to
5075 + // repair - it is work that has not run yet. Worse, sending one through
5076 + // wdkit_localise_media_url() actively corrupts it: that helper hands
5077 + // Elementor `id => 1` whenever the control's id is empty, and Elementor
5078 + // caches its import results *by that source id*, so every blanked-id control
5079 + // collides on key 1 and gets handed back whichever attachment claimed it
5080 + // first. A video, whose id the deferred pass deliberately blanks and whose
5081 + // URL stays foreign until the cron runs, is the one control that meets both
5082 + // conditions - and it came back pointing at an unrelated PNG.
5083 + if ( ! $local_only && ! self::wdkit_is_video_url( $node['url'] ) ) {
5084 + $local = self::wdkit_localise_media_url( $node['url'], $current );
5085 +
5086 + if ( ! empty( $local['id'] ) && ! empty( $local['url'] ) ) {
5087 + $node['id'] = $local['id'];
5088 + $node['url'] = $local['url'];
5089 + }
5090 + }
5091 + } elseif ( false !== strpos( $node['url'], '/wp-content/uploads/' ) ) {
5092 + // The URL is already local, so the attachment it names is the truth and the
5093 + // id beside it may not be. Two ways it goes wrong, and both look the same
5094 + // from here: the id is the authoring site's and matches nothing locally, or
5095 + // it happens to match some unrelated attachment. Either way the URL wins.
5096 + //
5097 + // This matters most for a container background: Control_Media::get_style_value()
5098 + // only falls back to the url when the id is *empty*, so a non-empty id that
5099 + // resolves to nothing makes wp_get_attachment_image_url() return false and the
5100 + // whole background-image declaration is dropped - the image simply vanishes.
5101 + // An image widget survives it (get_attachment_image_html() clears an id that
5102 + // fails wp_attachment_is_image() and renders from the url), which is why this
5103 + // showed up as "some backgrounds missing" rather than as broken images.
5104 + //
5105 + // Only a real change is written: when the URL resolves to the id that is
5106 + // already there, the node is left exactly as it was.
5107 + $resolved = self::wdkit_attachment_id_from_url( $node['url'] );
5108 +
5109 + // Held to the same bar as every other id this class writes. A local upload url
5110 + // can still name a record with nothing behind it - that is the whole subject
5111 + // of this repair - and writing its id back would reintroduce exactly the
5112 + // dangling-id symptom this branch exists to remove. On a reject the node keeps
5113 + // the id it arrived with, which is no worse than before.
5114 + if ( $resolved && $resolved !== $current && self::wdkit_is_usable_attachment( $resolved ) ) {
5115 + $node['id'] = $resolved;
5116 + }
5117 + }
5118 + }
5119 +
5120 + foreach ( $node as $key => $value ) {
5121 + if ( is_array( $value ) ) {
5122 + $node[ $key ] = self::wdkit_repair_attachment_ids( $value, $local_only );
5123 + }
5124 + }
5125 +
5126 + return $node;
5127 + }
5128 +
5129 + public function wdkit_media_import( $content = array(), $editor = '', $defer_media = false ) {
5130 +
1180 5131 if ( empty( $content ) && empty( $editor ) ) {
1181 5132 $args = $this->wdkit_parse_args( $_POST );
1182 5133 $content = ! empty( $args['content'] ) ? json_decode( $args['content'], true ) : array();
1183 5134 } else {
@@ -1195,17 +5146,164 @@
1195 5146 if ( ! class_exists( 'Wdkit_Import_Images' ) ) {
1196 5147 require_once WDKIT_INCLUDES . 'admin/class-wdkit-import-images.php';
1197 5148 }
1198 5149
5150 +
1199 5151 if ( ! empty( $args['editor'] ) && 'gutenberg' === $args['editor'] && ! empty( $content ) ) {
5152 +
5153 + // parse_blocks() leaves a filler entry (blockName null/empty) for every stretch of
5154 + // whitespace between real blocks. blocks_import_media_copy_content() below always
5155 + // dropped these before recursing - but WDKIT_Nexter_Block_Processor::run() (which
5156 + // runs unconditionally right after this, in import_page_section_content()) keys
5157 + // each block's generated CSS/id off its position in this array, so leaving them in
5158 + // shifts every block after the first and the page renders unstyled/mismatched.
5159 + // This is a plain array filter, no network/media work, so it must run regardless
5160 + // of $defer_media - only the actual per-block media copy below is skippable.
5161 + if ( is_array( $content ) ) {
5162 + foreach ( $content as $key => $val ) {
5163 + if ( is_array( $val ) && array_key_exists( 'blockName', $val )
5164 + && ( empty( $val['blockName'] ) || null === $val['blockName'] || ' ' === $val['blockName'] )
5165 + ) {
5166 + unset( $content[ $key ] );
5167 + }
5168 + }
5169 + }
5170 +
5171 + // Same contract as the Elementor branch below: the caller already collected this
5172 + // content's image_urls from the template response and will hand them to
5173 + // wkit_schedule_deferred_media_sync() once the page exists, so raster images can
5174 + // stay pointed at the source CDN and be localised later, off this request, via
5175 + // wdkit_async_sideload_page_images(). SVGs are excluded from that deferral (see
5176 + // Wdkit_Import_Images::wdkit_Import_media()) and still import synchronously here -
5177 + // the walk itself is cheap, so passing $defer_media through and letting the leaf
5178 + // function decide per-image costs nothing extra when not deferring.
5179 + // Warm every SVG this walk is about to import, concurrently, before the walk
5180 + // starts. The walk itself is strictly one node at a time, so without this each
5181 + // SVG pays its own blocking round trip to the template CDN - measured at ~44s
5182 + // for the 65 SVGs in a 13-template kit, virtually all of it connection setup
5183 + // and latency rather than work. SVGs specifically because they are the only
5184 + // media still imported synchronously once $defer_media is on (see the carve-out
5185 + // in Wdkit_Import_Images::wdkit_Import_media()), and because they are small
5186 + // enough to hold in memory safely.
5187 + //
5188 + // Scanning the serialized content rather than pre-walking the tree keeps this
5189 + // from having to duplicate - and drift from - the walk's own URL detection.
5190 + // Over-collecting is harmless (an unused warm entry is just dropped) and
5191 + // under-collecting only costs the original sequential fetch.
5192 + $svg_scan = wp_json_encode( $content, JSON_UNESCAPED_SLASHES );
5193 +
5194 + if ( is_string( $svg_scan ) && preg_match_all( '#https?://[^"\'\s<>()\\\\]+\.svg(?:\?[^"\'\s<>()\\\\]*)?#i', $svg_scan, $svg_matches ) ) {
5195 + Wdkit_Import_Images::prefetch_remote_files( array_unique( $svg_matches[0] ) );
5196 + }
5197 +
5198 + // Snapshot before the walk so the ids cleared below are this content's alone.
5199 + // peek_deferred_urls() returns the whole request-wide static list, so on a route
5200 + // that imports more than one item, an earlier item's URLs were still in it - and
5201 + // any of them that happened to appear on this page had its id zeroed here with
5202 + // no event of its own coming to put the id back.
5203 + self::wdkit_require_import_images();
5204 + $deferred_before = Wdkit_Import_Images::peek_deferred_urls();
5205 +
1200 5206 $media_import = array( $content );
1201 - $media_import = self::blocks_import_media_copy_content( $media_import );
5207 + $media_import = self::blocks_import_media_copy_content( $media_import, $defer_media );
1202 5208 $content = $media_import[0];
5209 +
5210 + // Blocks that rebuild their image from attributes on every render read the
5211 + // attachment id ahead of the url, so the foreign id the template shipped hides
5212 + // the still-remote url that would have rendered perfectly well. Zero those ids
5213 + // for exactly the images the walk above chose to defer, so the page renders from
5214 + // the source CDN until wdkit_async_sideload_page_images() localises it and
5215 + // wdkit_remap_attachment_ids() writes the real id back.
5216 + $deferred_here = array_values( array_diff( Wdkit_Import_Images::peek_deferred_urls(), $deferred_before ) );
5217 +
5218 + $content = self::wdkit_clear_deferred_attachment_ids( $content, $deferred_here );
5219 +
5220 + // The walk above only visits block ATTRIBUTES. A block that ships rendered markup
5221 + // - tpgb/tp-testimonials is one - carries its images as `<img src>` inside its own
5222 + // innerHTML, which no attribute walk can reach, so those URLs were never recorded
5223 + // and the background pass never learned to rewrite them. Measured on a Gutenberg
5224 + // AI import: 8 template-CDN PNGs left hotlinked across two pages, every one of
5225 + // them inside block markup rather than an attribute.
5226 + //
5227 + // A regex sweep of the serialised content closes that gap, and is additive by
5228 + // construction: it only ever adds URLs to the deferred list, and the cron's
5229 + // str_replace over post_content is what localises them. A URL the cron cannot
5230 + // resolve is skipped exactly as before.
5231 + if ( $defer_media ) {
5232 + // Hand the collector the content as-is. Pre-encoding it here was wrong twice
5233 + // over: wp_json_encode() without JSON_UNESCAPED_SLASHES turns every `https://`
5234 + // into `https:\/\/`, which the collector's regex cannot match, and the
5235 + // collector encodes what it is given anyway - so a string argument was being
5236 + // escaped a second time. Passing the parsed block array straight through lets
5237 + // it encode once, with the right flags, and innerHTML is inside that encoding.
5238 + $markup_urls = self::wdkit_collect_foreign_media_urls( $content );
5239 +
5240 + if ( ! empty( $markup_urls ) ) {
5241 + $missed = array_values( array_diff( $markup_urls, $deferred_here ) );
5242 +
5243 + if ( ! empty( $missed ) ) {
5244 + Wdkit_Import_Images::record_deferred_urls( $missed );
5245 + }
5246 + }
5247 + }
5248 +
5249 + // The warmed bodies have served their purpose for this template; do not carry
5250 + // them across the rest of the request.
5251 + Wdkit_Import_Images::clear_prefetched();
1203 5252 } elseif ( ! empty( $args['editor'] ) && 'elementor' === $args['editor'] && ! empty( $content ) ) {
1204 5253 $media_import = array( $content );
5254 + // Element IDs must stay unique regardless of the media path, so this always runs.
1205 5255 $media_import = self::widgets_elements_id_change( $media_import );
5256 +
5257 + if ( $defer_media ) {
5258 + // Skipping the walk above means Elementor never sees this page's media, so
5259 + // nothing else in the request would notice what was left remote. Split it the
5260 + // same way the Gutenberg walk does, per image:
5261 + //
5262 + // - SVGs are localised now. The background pass runs with no user and cannot
5263 + // upload them (see wdkit_collect_foreign_media_urls()), so deferring one
5264 + // loses it silently. These are cheap - small files, and usually already
5265 + // imported by a sibling page.
5266 + // - Everything else is recorded for the background pass, which is where the
5267 + // expensive raster downloads and subsize generation belong.
5268 + //
5269 + // Warm every SVG this branch is about to import, concurrently, before the
5270 + // one-node-at-a-time walk starts - the exact same fix the Gutenberg branch
5271 + // above already carries. Without it each SVG paid its own blocking round
5272 + // trip to the template CDN: measured at ~42s of sum_media_import for a
5273 + // 15-template Elementor kit (matching that branch's own 65-SVG / ~44s
5274 + // finding), virtually all of it connection setup and latency.
5275 + // wdkit_localise_media_url() consults this cache for .svg URLs.
5276 + $svg_scan = wp_json_encode( $media_import[0], JSON_UNESCAPED_SLASHES );
5277 +
5278 + if ( is_string( $svg_scan ) && preg_match_all( '#https?://[^"\'\s<>()\\\\]+\.svg(?:\?[^"\'\s<>()\\\\]*)?#i', $svg_scan, $svg_matches ) ) {
5279 + self::wdkit_require_import_images();
5280 + Wdkit_Import_Images::prefetch_remote_files( array_unique( $svg_matches[0] ) );
5281 + }
5282 +
5283 + $media_import[0] = self::wdkit_localise_foreign_svgs( $media_import[0] );
5284 +
5285 + Wdkit_Import_Images::clear_prefetched();
5286 +
5287 + $deferred = self::wdkit_collect_foreign_media_urls( $media_import[0] );
5288 +
5289 + Wdkit_Import_Images::record_deferred_urls( $deferred );
5290 +
5291 + // Leave the attachment IDs of those deferred controls empty rather than
5292 + // carrying the template site's IDs through the window - see
5293 + // wdkit_blank_deferred_media_ids().
5294 + $media_import[0] = self::wdkit_blank_deferred_media_ids( $media_import[0], $deferred );
5295 +
5296 + return $media_import[0];
5297 + }
5298 +
1206 5299 $media_import = self::widgets_import_media_copy_content( $media_import );
1207 5300 $content = $media_import[0];
5301 +
5302 + // Last: point any control Elementor left holding a foreign attachment ID at the
5303 + // local attachment its URL already refers to. Without this, has_sizes controls
5304 + // such as container background images resolve to nothing and render empty.
5305 + $content = self::wdkit_repair_attachment_ids( $content );
1208 5306 }
1209 5307
1210 5308 return $content;
1211 5309 }
@@ -1276,9 +5374,13 @@
1276 5374 $control_type = \Elementor\Plugin::instance()->controls_manager->get_control( $get_control['type'] );
1277 5375 $control_name = $get_control['name'];
1278 5376
1279 5377 if ( ! $control_type ) {
1280 - return $get_element_instance;
5378 + // Skip just this control. Returning here would abandon every control after
5379 + // it, so a single unregistered type - routine when a kit uses an addon that
5380 + // is not fully active yet - would silently leave the rest of the element's
5381 + // media pointing at the source site.
5382 + continue;
1281 5383 }
1282 5384
1283 5385 if ( method_exists( $control_type, $tp_mi_on_fun ) ) {
1284 5386 $get_element_instance['settings'][ $control_name ] = $control_type->{$tp_mi_on_fun}( $element->get_settings( $control_name ), $get_control );
@@ -1292,9 +5394,9 @@
1292 5394 * Blocks Recursively data
1293 5395 *
1294 5396 * @param string $data_import gutenber data import.
1295 5397 */
1296 - public static function blocks_import_media_copy_content( $data_import ) {
5398 + public static function blocks_import_media_copy_content( $data_import, $defer_media = false ) {
1297 5399 if ( ! empty( $data_import ) ) {
1298 5400 foreach ( $data_import[0] as $key => $val ) {
1299 5401 if ( array_key_exists( 'blockName', $val ) && ( empty( $val['blockName'] ) || null === $val['blockName'] || empty( $val['blockName'] ) || ' ' === $val['blockName'] ) ) {
1300 5402 unset( $data_import[0][ $key ] );
@@ -1303,12 +5405,13 @@
1303 5405 }
1304 5406
1305 5407 return self::blocks_array_recursively_data(
1306 5408 $data_import,
1307 - function ( $block_data ) {
1308 - $elements = self::blocks_data_instance( $block_data );
5409 + function ( $block_data, $args ) {
5410 + $elements = self::blocks_data_instance( $block_data, $args );
1309 5411 return $elements;
1310 - }
5412 + },
5413 + array( 'defer_media' => $defer_media )
1311 5414 );
1312 5415 }
1313 5416
1314 5417 /**
@@ -1353,63 +5456,1115 @@
1353 5456 public static function blocks_data_instance( array $block_data, array $args = array(), $block_args = null ) {
1354 5457
1355 5458 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'] ) ) ) {
1356 5459 $blocks_attr = isset( $block_data['attributes'] ) ? $block_data['attributes'] : ( isset( $block_data['attrs'] ) ? $block_data['attrs'] : array() );
1357 - foreach ( $blocks_attr as $block_key => $block_val ) {
1358 - if ( isset( $block_val['url'] ) && isset( $block_val['id'] ) && ! empty( $block_val['url'] ) ) {
1359 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $block_val );
1360 - $blocks_attr[ $block_key ] = $new_media;
1361 - } elseif ( isset( $block_val['url'] ) && ! empty( $block_val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $block_val['url'] ) ) {
1362 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $block_val );
1363 - $blocks_attr[ $block_key ] = $new_media;
1364 - } elseif ( is_array( $block_val ) && ! empty( $block_val ) ) {
1365 - if ( ! array_key_exists( 'md', $block_val ) && ! array_key_exists( 'openTypography', $block_val ) && ! array_key_exists( 'openBorder', $block_val ) && ! array_key_exists( 'openShadow', $block_val ) && ! array_key_exists( 'openFilter', $block_val ) ) {
1366 - foreach ( $block_val as $key => $val ) {
1367 - if ( is_array( $val ) && ! empty( $val ) ) {
5460 + $blocks_attr = self::wdkit_import_block_media( $blocks_attr, ! empty( $args['defer_media'] ) );
5461 + if ( isset( $block_data['attributes'] ) ) {
5462 + $block_data['attributes'] = $blocks_attr;
5463 + } elseif ( isset( $block_data['attrs'] ) ) {
5464 + $block_data['attrs'] = $blocks_attr;
5465 + }
1368 5466
1369 - if ( isset( $val['url'] ) && ( isset( $val['Id'] ) || isset( $val['id'] ) ) && ! empty( $val['url'] ) ) {
1370 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $val );
1371 - $blocks_attr[ $block_key ][ $key ] = $new_media;
1372 - } elseif ( isset( $val['url'] ) && ! empty( $val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $val['url'] ) ) {
1373 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $val );
1374 - $blocks_attr[ $block_key ][ $key ] = $new_media;
1375 - } else {
1376 - foreach ( $val as $sub_key => $sub_val ) {
1377 - if ( isset( $sub_val['url'] ) && ( isset( $sub_val['Id'] ) || isset( $sub_val['id'] ) ) && ! empty( $sub_val['url'] ) ) {
1378 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val );
1379 - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = $new_media;
1380 - } elseif ( isset( $sub_val['url'] ) && ! empty( $sub_val['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $sub_val['url'] ) ) {
1381 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val );
1382 - $blocks_attr[ $block_key ][ $key ][ $sub_key ] = $new_media;
1383 - } elseif ( is_array( $sub_val ) && ! empty( $sub_val ) ) {
1384 - foreach ( $sub_val as $sub_key1 => $sub_val1 ) {
1385 - if ( isset( $sub_val1['url'] ) && ( isset( $sub_val1['Id'] ) || isset( $sub_val1['id'] ) ) && ! empty( $sub_val1['url'] ) ) {
1386 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val1 );
1387 - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = $new_media;
1388 - } elseif ( isset( $sub_val1['url'] ) && ! empty( $sub_val1['url'] ) && preg_match( '/\.(jpg|png|jpeg|gif|svg|webp)$/', $sub_val1['url'] ) ) {
1389 - $new_media = Wdkit_Import_Images::wdkit_Import_media( $sub_val1 );
1390 - $blocks_attr[ $block_key ][ $key ][ $sub_key ][ $sub_key1 ] = $new_media;
1391 - }
1392 - }
1393 - }
1394 - }
1395 - }
1396 - }
5467 + $block_data = self::wdkit_relink_block_markup( $block_data );
5468 + }
5469 +
5470 + return $block_data;
5471 + }
5472 +
5473 + /**
5474 + * Run block markup through the media import, the way the create path does.
5475 + *
5476 + * Used wherever block content is written from the browser: media import, then the Nexter
5477 + * block processor so each block's rendered copy matches its attributes, then serialise.
5478 + *
5479 + * @since 2.6.2
5480 + *
5481 + * @param string $content Block markup.
5482 + * @return string Block markup with local media.
5483 + */
5484 + private function wdkit_relink_gutenberg_content( $content ) {
5485 +
5486 + if ( ! is_string( $content ) || false === strpos( $content, '<!-- wp:' ) ) {
5487 + return $content;
5488 + }
5489 +
5490 + // wdkit_media_import() loads this itself, but it is referenced before that below.
5491 + if ( ! class_exists( 'Wdkit_Import_Images' ) ) {
5492 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-import-images.php';
5493 + }
5494 +
5495 + // Thumbnail generation decodes each image, so keep the oversized-image guard in force.
5496 + self::wdkit_guard_oversized_images();
5497 +
5498 +
5499 + // Block attributes are JSON inside the block delimiters, so they only survive a parse
5500 + // when the string carries exactly one level of escaping. Arrive with an extra level and
5501 + // parse_blocks() reads no attributes at all - serialising that back out writes every
5502 + // block bare, throwing away titles, body text, icons and styling.
5503 + $parsable = self::wdkit_parsable_block_content( $content );
5504 +
5505 + if ( null === $parsable ) {
5506 +
5507 + return $content;
5508 + }
5509 +
5510 + $blocks = parse_blocks( $parsable );
5511 + $blocks = $this->wdkit_media_import( $blocks, 'gutenberg' );
5512 +
5513 + if ( empty( $blocks ) || ! is_array( $blocks ) ) {
5514 + return $content;
5515 + }
5516 +
5517 + if ( class_exists( 'WDKIT_Nexter_Block_Processor' ) ) {
5518 + $processor = new WDKIT_Nexter_Block_Processor();
5519 + $blocks = $processor->run( $blocks );
5520 + }
5521 +
5522 + $serialised = serialize_blocks( $blocks );
5523 +
5524 + // serialize_blocks() re-escapes any literal "--" left in attributes (WP core's own
5525 + // protection against breaking the HTML comment delimiters), the same way Path A/B's
5526 + // wdkit_bundle_insert_item()/import_page_section_content() do - so it needs the same
5527 + // decode step afterward, or those - escapes are left in the saved content.
5528 + $serialised = $this->replace_unicode_glitch( $serialised );
5529 +
5530 + // Last line of defence. This function exists to repoint media, so a result carrying
5531 + // fewer block attributes than it started with is a broken round trip, not a rewrite.
5532 + // Leaving the media wrong is recoverable; saving gutted content is not.
5533 + $before = self::wdkit_block_attr_count( $parsable );
5534 + $after = self::wdkit_block_attr_count( $serialised );
5535 +
5536 + if ( $after < $before ) {
5537 +
5538 + return $content;
5539 + }
5540 +
5541 + // Never hand back nothing: an empty result would blank the page.
5542 + return ! empty( $serialised ) ? $serialised : $content;
5543 + }
5544 +
5545 + /**
5546 + * Attachment rows Elementor's importer created during this request.
5547 + *
5548 + * @since 2.7.1
5549 + *
5550 + * @var array<int>
5551 + */
5552 + private static $elementor_new_attachments = array();
5553 +
5554 + /**
5555 + * Watch Elementor's importer so a row it leaves behind can be rolled back.
5556 + *
5557 + * Elementor inserts the attachment row before it knows whether the upload worked.
5558 + * class-import-images.php gates the mime type on `if ( $info )`, where `$info` is
5559 + * wp_check_filetype()'s return - always an array, so always true - and it never looks at
5560 + * wp_upload_bits()'s own error. So when the upload fails, which on a real site means an
5561 + * unwritable or full uploads directory, `$upload['file']` is simply absent: Elementor
5562 + * warns "Undefined array key file", inserts a row with post_mime_type '' and no
5563 + * _wp_attached_file, and carries on. Reproduced here by pointing upload_dir at a path
5564 + * that cannot be created.
5565 + *
5566 + * Nothing renders from such a row - wdkit_localise_media_url() refuses the id - but it
5567 + * stays in the customer's media library, and it is exactly the record the reuse lookup
5568 + * used to adopt on a re-import.
5569 + *
5570 + * Registered lazily and only from the import paths, so an ordinary page load never
5571 + * carries either hook. The capture is Elementor's own new_attachment filter rather than
5572 + * an id diff, so only a row Elementor itself just created is ever a candidate.
5573 + *
5574 + * @since 2.7.1
5575 + *
5576 + * @return void
5577 + */
5578 + private static function wdkit_watch_elementor_attachments() {
5579 +
5580 + static $watching = false;
5581 +
5582 + if ( $watching ) {
5583 + return;
5584 + }
5585 +
5586 + $watching = true;
5587 +
5588 + add_filter(
5589 + 'elementor/template_library/import_images/new_attachment',
5590 + static function ( $id ) {
5591 + self::$elementor_new_attachments[] = (int) $id;
5592 +
5593 + return $id;
5594 + }
5595 + );
5596 +
5597 + // Elementor writes the attachment metadata AFTER the filter above fires, so nothing
5598 + // may be deleted from inside it: update_post_meta() on a removed post would leave an
5599 + // orphan meta row, which is worse than the orphan post. The sweep therefore runs once
5600 + // the request is over, and wdkit_localise_media_url() drains it inline as well so a
5601 + // bad id is gone before anything downstream can read it.
5602 + add_action( 'shutdown', array( __CLASS__, 'wdkit_sweep_stub_attachments' ) );
5603 + }
5604 +
5605 + /**
5606 + * Delete the attachment rows Elementor's importer created without a file behind them.
5607 + *
5608 + * Both markers are required, not either: an empty post_mime_type AND no attached file.
5609 + * A media-offload plugin legitimately has no local file but keeps both of those, and an
5610 + * SVG keeps both too - so neither can be reached by this. A row that has a mime type but
5611 + * whose file has since gone missing is deliberately left alone for the same reason: from
5612 + * here it is indistinguishable from offloaded media, and deleting it would take a working
5613 + * image out of a customer's library.
5614 + *
5615 + * @since 2.7.1
5616 + *
5617 + * @return int Rows removed.
5618 + */
5619 + public static function wdkit_sweep_stub_attachments() {
5620 +
5621 + $removed = 0;
5622 +
5623 + foreach ( array_unique( self::$elementor_new_attachments ) as $id ) {
5624 + $id = (int) $id;
5625 +
5626 + if ( ! $id || 'attachment' !== get_post_type( $id ) ) {
5627 + continue;
5628 + }
5629 +
5630 + $file = get_attached_file( $id );
5631 +
5632 + if ( '' !== (string) get_post_mime_type( $id ) || ! empty( $file ) ) {
5633 + continue;
5634 + }
5635 +
5636 + if ( self::wdkit_is_usable_attachment( $id ) ) {
5637 + continue;
5638 + }
5639 +
5640 + wp_delete_attachment( $id, true );
5641 + ++$removed;
5642 + }
5643 +
5644 + self::$elementor_new_attachments = array();
5645 +
5646 + return $removed;
5647 + }
5648 +
5649 + /**
5650 + * Is this id a usable image attachment on THIS site?
5651 + *
5652 + * get_post() is not the question. A template authored elsewhere carries that site's
5653 + * attachment ids, and on the destination those integers frequently already belong to
5654 + * something else entirely - a page, a nav menu item, a revision. Zion is a live example:
5655 + * its team images arrive as ids 93-96, which here are two pages, an nxt_builder template
5656 + * and a nav_menu_item. get_post() happily returns those, so a "does a post exist" test
5657 + * concludes the id is fine and leaves it in place, and every consumer that resolves media
5658 + * through the id gets nothing back.
5659 + *
5660 + * The collision is generic, not specific to those four numbers: any id range that overlaps
5661 + * existing local content behaves the same way. So the test is what the id has to satisfy to
5662 + * actually work - it must be an attachment, and it must have something to serve.
5663 + *
5664 + * SVGs are accepted on the strength of a real file rather than image dimensions: they are
5665 + * imported synchronously and legitimately carry no width/height/sizes metadata.
5666 + *
5667 + * An empty post_mime_type is disqualifying on its own, whatever else the record has.
5668 + * wp_attachment_is_image() is false without one, so wp_get_attachment_image_src() returns
5669 + * false and Elementor's Control_Media::get_style_value() emits no background-image rule at
5670 + * all - the section renders empty even though the id resolves and the url looks right.
5671 + * Every path in this plugin that creates an attachment sets a real mime type (see
5672 + * Wdkit_Import_Images::import(), which deletes the upload rather than insert an unknown
5673 + * type), so a blank one only ever comes from a record an earlier build left behind.
5674 + *
5675 + * @since 2.7.1
5676 + *
5677 + * @param mixed $id Candidate attachment id.
5678 + * @return bool True when the id resolves to an attachment that can actually be rendered.
5679 + */
5680 + private static function wdkit_is_usable_attachment( $id ) {
5681 +
5682 + $id = (int) $id;
5683 +
5684 + if ( ! $id || 'attachment' !== get_post_type( $id ) ) {
5685 + return false;
5686 + }
5687 +
5688 + if ( '' === (string) get_post_mime_type( $id ) ) {
5689 + return false;
5690 + }
5691 +
5692 + $meta = wp_get_attachment_metadata( $id );
5693 +
5694 + if ( is_array( $meta ) && ( ! empty( $meta['file'] ) || ! empty( $meta['sizes'] ) ) ) {
5695 + return true;
5696 + }
5697 +
5698 + // No image metadata is normal for an SVG - fall back to the file itself.
5699 + $file = get_attached_file( $id );
5700 +
5701 + return ! empty( $file ) && file_exists( $file );
5702 + }
5703 +
5704 + /**
5705 + * The attachment a url on this site already refers to, or 0.
5706 + *
5707 + * Exists for the featured-image path: a picked stock image is copied into the media
5708 + * library before the blog posts are created (see localised_images()), so the thumb_image
5709 + * the client sends is normally a url on this very site by then. Handing that to
5710 + * download_url() cannot work - wdesignkit_validate_external_url() rejects loopback and
5711 + * reserved addresses by design - so the file has to be recognised as already-imported
5712 + * instead of fetched again.
5713 + *
5714 + * Host-matched first so nothing offsite can reach attachment_url_to_postid(), then held
5715 + * to the same wdkit_is_usable_attachment() bar as every other id this class writes: an
5716 + * id that resolves to no usable attachment returns 0 and the caller falls through to its
5717 + * normal external handling.
5718 + *
5719 + * @since 2.7.1
5720 + *
5721 + * @param string $url Candidate url.
5722 + * @return int Usable local attachment id, or 0.
5723 + */
5724 + /**
5725 + * Store post_content that this import already produced, without kses rewriting it.
5726 + *
5727 + * wp_update_post() runs post_content through kses whenever the current user cannot
5728 + * `unfiltered_html`. WP-Cron has no user at all, so the deferred media pass always hits
5729 + * that branch - and kses is not a no-op on block markup. It deletes <input>, <iframe>,
5730 + * <form>, <select> and inline <svg> outright, and strips every attribute off <object>.
5731 + *
5732 + * The visible casualty is the pricing switcher: its markup carries
5733 + * `<input class="switch-toggle" type="checkbox">`, and every rule that paints the control
5734 + * is an adjacent-sibling selector on it - `.switch-toggle + .switch-slider` for the track,
5735 + * `.switch-toggle:checked + .switch-slider` for the on state. Delete the input and the
5736 + * track has no background at all and the Monthly/Yearly panels can never switch, which is
5737 + * why the page had to be opened and saved by hand to come back: an administrator does hold
5738 + * `unfiltered_html`, so the editor's own save writes the markup back intact.
5739 + *
5740 + * Suspending the filters around the write is what core's own importer does, for this exact
5741 + * reason. Nothing user-supplied passes through here - this only rewrites image urls inside
5742 + * content an authenticated administrator imported moments earlier - and the filters are
5743 + * restored immediately, so nothing else in the request loses its sanitising.
5744 + *
5745 + * @since 2.7.1
5746 + *
5747 + * @param int $post_id Post to write.
5748 + * @param string $content Block markup to store.
5749 + * @return int|WP_Error Whatever wp_update_post() returned.
5750 + */
5751 + private static function wdkit_write_post_content( $post_id, $content ) {
5752 +
5753 + $had_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
5754 +
5755 + if ( $had_kses ) {
5756 + kses_remove_filters();
5757 + }
5758 +
5759 + // Nexter Blocks' save_post handler treats a save of anything that is not a page,
5760 + // post or product - a header/footer template, say - as a site-wide change and bumps
5761 + // the stamp every page's block bundle is validated against, sending every page back
5762 + // to its unbundled first-view render (block CSS arriving late, from the footer). Every
5763 + // caller here only rewrites URLs; the blocks, and so the bundles, are unchanged, and
5764 + // the page's own stylesheet is rebuilt with a fresh version by
5765 + // wdkit_rebuild_block_css(). So that one handler sits this write out. Measured: one
5766 + // unchanged re-save of the Taj Bakery header took 10 of 10 built pages to 0.
5767 + $detached = self::wdkit_detach_block_bundle_invalidation();
5768 +
5769 + // wp_update_post() unslashes what it is given, exactly like update_post_meta(),
5770 + // so the content has to arrive slashed or every backslash in the markup is eaten.
5771 + // The synchronous import paths already do this (see the wp_slash() calls around
5772 + // wdkit_insert_post_content()); this writer was the one that did not.
5773 + $result = wp_update_post(
5774 + array(
5775 + 'ID' => $post_id,
5776 + 'post_content' => wp_slash( $content ),
5777 + )
5778 + );
5779 +
5780 + // Restore exactly what was there, so the rest of the request sanitises as it did.
5781 + if ( $had_kses ) {
5782 + kses_init_filters();
5783 + }
5784 +
5785 + foreach ( $detached as $hooked ) {
5786 + add_action( 'save_post', $hooked['callback'], $hooked['priority'], $hooked['args'] );
5787 + }
5788 +
5789 + return $result;
5790 + }
5791 +
5792 + /**
5793 + * Take Nexter Blocks' save_post bundle invalidation off the hook, returning what was removed.
5794 + *
5795 + * Found by method name rather than class, the same way the CSS generator is located, so a
5796 + * renamed class does not silently turn this into a no-op - and a site without Nexter
5797 + * Blocks simply gets an empty list back.
5798 + *
5799 + * @since 2.7.2
5800 + *
5801 + * @return array[] Each { callback, priority, args }, for add_action() to put back.
5802 + */
5803 + private static function wdkit_detach_block_bundle_invalidation() {
5804 + global $wp_filter;
5805 +
5806 + $removed = array();
5807 +
5808 + if ( empty( $wp_filter['save_post'] ) || ! is_object( $wp_filter['save_post'] ) || empty( $wp_filter['save_post']->callbacks ) ) {
5809 + return $removed;
5810 + }
5811 +
5812 + foreach ( $wp_filter['save_post']->callbacks as $priority => $callbacks ) {
5813 + foreach ( $callbacks as $hooked ) {
5814 + $callback = isset( $hooked['function'] ) ? $hooked['function'] : null;
5815 +
5816 + if ( is_array( $callback ) && isset( $callback[1] ) && 'plus_post_save_transient' === $callback[1] ) {
5817 + $removed[] = array(
5818 + 'callback' => $callback,
5819 + 'priority' => (int) $priority,
5820 + 'args' => isset( $hooked['accepted_args'] ) ? (int) $hooked['accepted_args'] : 1,
5821 + );
5822 + }
5823 + }
5824 + }
5825 +
5826 + foreach ( $removed as $hooked ) {
5827 + remove_action( 'save_post', $hooked['callback'], $hooked['priority'] );
5828 + }
5829 +
5830 + return $removed;
5831 + }
5832 +
5833 + private static function wdkit_local_attachment_from_url( $url ) {
5834 +
5835 + if ( ! is_string( $url ) || '' === $url ) {
5836 + return 0;
5837 + }
5838 +
5839 + $host = strtolower( (string) wp_parse_url( $url, PHP_URL_HOST ) );
5840 +
5841 + if ( '' === $host ) {
5842 + return 0;
5843 + }
5844 +
5845 + // Both, because a site can be served on a different host than it stores.
5846 + $local_hosts = array_filter(
5847 + array_unique(
5848 + array(
5849 + strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) ),
5850 + strtolower( (string) wp_parse_url( site_url(), PHP_URL_HOST ) ),
5851 + )
5852 + )
5853 + );
5854 +
5855 + if ( ! in_array( $host, $local_hosts, true ) ) {
5856 + return 0;
5857 + }
5858 +
5859 + $id = (int) attachment_url_to_postid( $url );
5860 +
5861 + // A sized rendition (…-300x122.png) is not itself an attachment; the full size is.
5862 + if ( ! $id ) {
5863 + $full = preg_replace( '/-\d+x\d+(?=\.[A-Za-z0-9]+$)/', '', $url );
5864 +
5865 + if ( is_string( $full ) && $full !== $url ) {
5866 + $id = (int) attachment_url_to_postid( $full );
5867 + }
5868 + }
5869 +
5870 + return self::wdkit_is_usable_attachment( $id ) ? $id : 0;
5871 + }
5872 +
5873 + /**
5874 + * Block attributes whose image is rebuilt from the attributes on every render, keyed by
5875 + * block name and listing the attribute key the image object sits under.
5876 + *
5877 + * Only these can be helped by wdkit_clear_deferred_attachment_ids(): their renderer does
5878 + *
5879 + * if ( ! empty( $img['id'] ) ) -> wp_get_attachment_image( $img['id'], ... )
5880 + * elseif ( ! empty( $img['url'] ) ) -> <img src="{$img['url']}">
5881 + *
5882 + * so an id that resolves to nothing wins over a url that works, and nothing is emitted at
5883 + * all. Zeroing the id lets the url branch run until the deferred cron supplies the real id.
5884 + *
5885 + * Deliberately an allow-list rather than a sweep, because none of the cheap ways to tell
5886 + * "the renderer rebuilds this from attributes" actually hold here:
5887 + *
5888 + * - WP_Block_Type_Registry::is_dynamic() reports every TPGB block dynamic (they all get a
5889 + * render_callback for css injection), tpgb/tp-image included - and tp-image is served
5890 + * from its stored innerHTML, whose save() derives wp-image-{id} from this very
5891 + * attribute. Zeroing it there would make the block fail editor validation.
5892 + * - "the url also appears in innerHTML" does not mean innerHTML is what gets served:
5893 + * tp-team-listing and tp-testimonials both carry the source site's last-saved markup
5894 + * even though their php renderer ignores it.
5895 + *
5896 + * A block missing from this list simply keeps today's behaviour, so an omission costs a
5897 + * fix, never a regression.
5898 + *
5899 + * @since 2.7.1
5900 + *
5901 + * @return array block name => list of attribute keys holding an image object.
5902 + */
5903 + private static function wdkit_deferred_image_attrs() {
5904 +
5905 + return array(
5906 + 'tpgb/tp-team-listing' => array( 'TImage' ),
5907 + 'tpgb/tp-testimonials' => array( 'avatar' ),
5908 + 'tpgb/tp-infobox' => array( 'imageName' ),
5909 + 'tpgb/tp-heading-title' => array( 'imgName' ),
5910 + 'tpgb/tp-button' => array( 'imageName' ),
5911 + 'tpgb/tp-navigation-builder' => array( 'openImg', 'closeImg', 'menuImg' ),
5912 + 'tpgb/tp-pricing-list' => array( 'imageField' ),
5913 + 'tpgb/tp-social-icons' => array( 'imgField' ),
5914 + 'tpgb/tp-stylist-list' => array( 'iconImg' ),
5915 + 'tpgb/tp-breadcrumbs' => array( 'iconsImg', 'sepIconImg' ),
5916 + 'tpgb/tp-google-map' => array( 'pinIcon' ),
5917 + 'tpgb/tp-hovercard' => array( 'cntImg' ),
5918 + 'tpgb/tp-video' => array( 'BannerImg' ),
5919 + // The header logo. Missing this one cost every kit whose header uses the block
5920 + // with a raster logo its logo entirely: the file is deferred like any other PNG,
5921 + // so the block was left holding the authoring site's attachment id, and its
5922 + // renderer takes the id branch first - wp_get_attachment_image() on an id that
5923 + // is not here returns nothing, and the block substitutes its OWN
5924 + // tpgb-placeholder.jpg rather than falling through to the url that works. So the
5925 + // header came out with a grey placeholder box where the logo should be
5926 + // (reproduced on the PawFusion Gutenberg kit, whose logo is a .png; kits with an
5927 + // .svg logo were unaffected because SVGs never defer and keep a real local id).
5928 + // All three of its image slots read the id first, hover and sticky included.
5929 + 'tpgb/tp-site-logo' => array( 'imageStore', 'hvrImageStore', 'stickyImg' ),
5930 + 'tpgb/tp-progress-bar' => array( 'imageName' ),
5931 + // Pro blocks, same renderer shape.
5932 + 'tpgb/tp-circle-menu' => array( 'imageStore' ),
5933 + 'tpgb/tp-timeline' => array( 'StartImage', 'EndImage' ),
5934 + );
5935 + }
5936 +
5937 + /**
5938 + * Point the blocks in wdkit_deferred_image_attrs() at their remote url for as long as
5939 + * their image is still deferred, by zeroing the foreign attachment id sitting next to it.
5940 + *
5941 + * Runs on the parsed block array the media walk just returned, so there is no markup
5942 + * round trip here at all - no escaping depth to resolve, no re-serialise, no attribute
5943 + * count to compare. Only the integer value of one already-present key changes.
5944 + *
5945 + * Zero specifically, and never '' / null / unset: those all read as "no id" to the
5946 + * renderer too, but wdkit_remap_attachment_ids() gates on
5947 + * isset( $node['id'] ) && '' !== $node['id'], so only an integer 0 both silences the
5948 + * renderer now and still gets picked up and replaced with the real id when the deferred
5949 + * cron localises the file. Dropping the key would also shrink the attribute count that
5950 + * that pass's own guard compares.
5951 + *
5952 + * If the cron never runs at all, the id stays 0 and the url stays remote - which renders
5953 + * the image rather than nothing, so this is also the safer resting state.
5954 + *
5955 + * @since 2.7.1
5956 + *
5957 + * @param array $blocks parsed block array, post media walk.
5958 + * @param array $deferred urls this walk chose to localise later.
5959 + * @return array The same block array, with unusable deferred ids zeroed.
5960 + */
5961 + private static function wdkit_clear_deferred_attachment_ids( $blocks, $deferred ) {
5962 +
5963 + if ( empty( $blocks ) || ! is_array( $blocks ) || empty( $deferred ) || ! is_array( $deferred ) ) {
5964 + return $blocks;
5965 + }
5966 +
5967 + $allow = self::wdkit_deferred_image_attrs();
5968 + $deferred = array_fill_keys( array_map( 'strval', $deferred ), true );
5969 +
5970 + $clear_node = function ( &$node ) use ( $deferred ) {
5971 + if ( ! is_array( $node ) || ! isset( $node['url'], $node['id'] ) ) {
5972 + return;
5973 + }
5974 +
5975 + if ( ! is_string( $node['url'] ) || '' === $node['url'] ) {
5976 + return;
5977 + }
5978 +
5979 + // A dynamic-source image resolves through TPGB Pro's own url helper, and in
5980 + // several renderers that branch sits ahead of the plain url fallback - so an
5981 + // empty id would route somewhere else entirely. Leave those untouched.
5982 + if ( isset( $node['dynamic'] ) ) {
5983 + return;
5984 + }
5985 +
5986 + // Only images this same walk actually deferred. Anything else either imported
5987 + // synchronously already or was never ours to touch - link fields carry a
5988 + // url/id pair too, and a menu item's id is a post id, not an attachment.
5989 + if ( ! isset( $deferred[ (string) $node['url'] ] ) ) {
5990 + return;
5991 + }
5992 +
5993 + // Same list the collector defers and wdkit_remap_attachment_ids() restores: an id
5994 + // cleared for an extension one of those passes will not revisit would never come
5995 + // back. This used to omit avif, which the collector already deferred.
5996 + $ext = strtolower( (string) pathinfo( (string) wp_parse_url( $node['url'], PHP_URL_PATH ), PATHINFO_EXTENSION ) );
5997 +
5998 + if ( ! in_array( $ext, Wdkit_Image_Guard::deferrable_extensions(), true ) ) {
5999 + return;
6000 + }
6001 +
6002 + // An id that already resolves here is never second-guessed.
6003 + if ( 0 === $node['id'] || self::wdkit_is_usable_attachment( $node['id'] ) ) {
6004 + return;
6005 + }
6006 +
6007 + $node['id'] = 0;
6008 + };
6009 +
6010 + $walk_attrs = function ( &$node, $keys ) use ( &$walk_attrs, $clear_node ) {
6011 + foreach ( $node as $key => &$value ) {
6012 + if ( ! is_array( $value ) ) {
6013 + continue;
6014 + }
6015 +
6016 + // The image object itself: clear it and stop. Its own 'sizes' map holds the
6017 + // source site's other renditions, which are separate files that this import
6018 + // may never localise - a 0 written in there could never be restored.
6019 + if ( in_array( (string) $key, $keys, true ) ) {
6020 + $clear_node( $value );
6021 + continue;
6022 + }
6023 +
6024 + if ( 'sizes' === (string) $key ) {
6025 + continue;
6026 + }
6027 +
6028 + $walk_attrs( $value, $keys );
6029 + }
6030 +
6031 + unset( $value );
6032 + };
6033 +
6034 + $walk_blocks = function ( &$list ) use ( &$walk_blocks, &$walk_attrs, $allow ) {
6035 + foreach ( $list as &$block ) {
6036 + if ( ! is_array( $block ) ) {
6037 + continue;
6038 + }
6039 +
6040 + $name = isset( $block['blockName'] ) ? (string) $block['blockName'] : '';
6041 +
6042 + if ( '' !== $name && isset( $allow[ $name ] ) && ! empty( $block['attrs'] ) && is_array( $block['attrs'] ) ) {
6043 + $walk_attrs( $block['attrs'], $allow[ $name ] );
6044 + }
6045 +
6046 + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
6047 + $walk_blocks( $block['innerBlocks'] );
6048 + }
6049 + }
6050 +
6051 + unset( $block );
6052 + };
6053 +
6054 + $walk_blocks( $blocks );
6055 +
6056 + return $blocks;
6057 + }
6058 +
6059 + /**
6060 + * Repoint attachment ids left behind by the deferred media pass.
6061 + *
6062 + * wdkit_async_sideload_page_images() rewrites every source url in post_content to its local
6063 + * copy, but a Gutenberg media attribute is a pair - `{"url":…,"id":…}` - and only the url
6064 + * half was ever touched. The id keeps pointing at the attachment id from the site the
6065 + * template was authored on, which does not exist here. Blocks that render from the url are
6066 + * unaffected, which is why this went unnoticed; blocks that resolve the attachment id
6067 + * instead (tp-team-listing, tp-testimonials, tp-image, tp-container backgrounds) get
6068 + * nothing back and render empty. The synchronous path never had this problem because
6069 + * wdkit_Import_media() returns both halves and its caller writes both.
6070 + *
6071 + * Declines by returning null rather than guessing. Every caller keeps the url-only content
6072 + * in that case, so the worst outcome is exactly today's behaviour and never worse than it:
6073 + *
6074 + * - not block markup, or the escaping depth cannot be resolved unambiguously
6075 + * - nothing actually needs remapping (no serialise round trip is attempted at all)
6076 + * - the round trip came back with fewer block attributes than it started with
6077 + * - the round trip came back empty
6078 + *
6079 + * Only the id half of a media pair is written, only when the current id resolves to no
6080 + * attachment at all, and only to an id this same pass created ($url_map) - so an id that
6081 + * already resolves is never touched and no unrelated attribute is rewritten.
6082 + *
6083 + * @since 2.7.1
6084 + *
6085 + * @param string $content post_content with urls already localised.
6086 + * @param array $url_map source url => array( 'id' => int, 'url' => string ) from this pass.
6087 + * @return string|null Rewritten content, or null to keep the caller's own content.
6088 + */
6089 + private function wdkit_remap_attachment_ids( $content, $url_map ) {
6090 +
6091 + if ( ! is_string( $content ) || false === strpos( $content, '<!-- wp:' ) || empty( $url_map ) ) {
6092 + return null;
6093 + }
6094 +
6095 + // Local url => the attachment id this pass just created for it. This is the primary
6096 + // mapping: these ids were made moments ago in this same request, so no lookup is needed.
6097 + $by_url = array();
6098 +
6099 + foreach ( $url_map as $local ) {
6100 + // Only ids that are genuinely renderable attachments here - see
6101 + // wdkit_is_usable_attachment(). Writing an id that does not resolve would trade one
6102 + // broken reference for another.
6103 + if ( ! empty( $local['url'] ) && ! empty( $local['id'] ) && self::wdkit_is_usable_attachment( $local['id'] ) ) {
6104 + $by_url[ (string) $local['url'] ] = (int) $local['id'];
6105 + }
6106 + }
6107 +
6108 + if ( empty( $by_url ) ) {
6109 + return null;
6110 + }
6111 +
6112 + // Same normalisation the relink path uses: an extra level of escaping makes
6113 + // parse_blocks() read no attributes at all, and serialising that back out would write
6114 + // every block bare. Null means the depth is ambiguous - decline rather than risk it.
6115 + $parsable = self::wdkit_parsable_block_content( $content );
6116 +
6117 + if ( null === $parsable ) {
6118 + return null;
6119 + }
6120 +
6121 + $blocks = parse_blocks( $parsable );
6122 + $changed = 0;
6123 +
6124 + $remap = function ( &$node ) use ( &$remap, $by_url, &$changed ) {
6125 + if ( ! is_array( $node ) ) {
6126 + return;
6127 + }
6128 +
6129 + if ( isset( $node['url'], $node['id'] ) && is_string( $node['url'] ) && '' !== $node['url'] && '' !== $node['id'] ) {
6130 + $ext = strtolower( (string) pathinfo( (string) wp_parse_url( $node['url'], PHP_URL_PATH ), PATHINFO_EXTENSION ) );
6131 +
6132 + // Same list the collector defers and wdkit_blank_deferred_media_ids() clears -
6133 + // the SVG path imports synchronously and already carries a real id. And only
6134 + // when the id resolves to nothing: an id that already works is never
6135 + // second-guessed, whatever the url says.
6136 + if ( in_array( $ext, Wdkit_Image_Guard::deferrable_extensions(), true ) && ! self::wdkit_is_usable_attachment( $node['id'] ) ) {
6137 + $new_id = isset( $by_url[ $node['url'] ] ) ? $by_url[ $node['url'] ] : 0;
6138 +
6139 + // A sized rendition (…-800x600.png) of something this pass localised: the map
6140 + // is keyed by the full-size url, so try that before any database work.
6141 + $full = preg_replace( '/-\d+x\d+(?=\.[A-Za-z0-9]+$)/', '', $node['url'] );
6142 +
6143 + if ( ! $new_id && is_string( $full ) && $full !== $node['url'] && isset( $by_url[ $full ] ) ) {
6144 + $new_id = $by_url[ $full ];
6145 + }
6146 +
6147 + // Defensive last resort for a shape the map does not cover.
6148 + if ( ! $new_id ) {
6149 + $probe = (int) attachment_url_to_postid( $node['url'] );
6150 +
6151 + if ( ! $probe && is_string( $full ) && $full !== $node['url'] ) {
6152 + $probe = (int) attachment_url_to_postid( $full );
1397 6153 }
6154 +
6155 + // Held to the same bar as the map above.
6156 + if ( $probe && self::wdkit_is_usable_attachment( $probe ) ) {
6157 + $new_id = $probe;
6158 + }
1398 6159 }
6160 +
6161 + if ( $new_id && (int) $node['id'] !== $new_id ) {
6162 + $node['id'] = $new_id;
6163 + ++$changed;
6164 + }
1399 6165 }
1400 6166 }
1401 - if ( isset( $block_data['attributes'] ) ) {
1402 - $block_data['attributes'] = $blocks_attr;
1403 - } elseif ( isset( $block_data['attrs'] ) ) {
1404 - $block_data['attrs'] = $blocks_attr;
6167 +
6168 + foreach ( $node as $key => &$child ) {
6169 + if ( is_array( $child ) ) {
6170 + $remap( $child );
6171 + }
1405 6172 }
6173 +
6174 + unset( $child );
6175 + };
6176 +
6177 + $walk_blocks = function ( &$list ) use ( &$walk_blocks, &$remap ) {
6178 + foreach ( $list as &$block ) {
6179 + if ( ! empty( $block['attrs'] ) && is_array( $block['attrs'] ) ) {
6180 + $remap( $block['attrs'] );
6181 + }
6182 +
6183 + if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) {
6184 + $walk_blocks( $block['innerBlocks'] );
6185 + }
6186 + }
6187 +
6188 + unset( $block );
6189 + };
6190 +
6191 + $walk_blocks( $blocks );
6192 +
6193 + // Nothing to fix: do not serialise at all. A round trip that changes nothing is still a
6194 + // round trip, and not taking it is strictly safer than taking it.
6195 + if ( ! $changed ) {
6196 + return null;
1406 6197 }
1407 6198
6199 + $serialised = serialize_blocks( $blocks );
6200 +
6201 + // serialize_blocks() re-escapes any literal "--" left in attributes, so undo that here
6202 + // exactly as the relink path does.
6203 + $serialised = $this->replace_unicode_glitch( $serialised );
6204 +
6205 + if ( empty( $serialised ) ) {
6206 + return null;
6207 + }
6208 +
6209 + // Last line of defence, same test the relink path applies: fewer attributes out than in
6210 + // is a broken round trip, not a rewrite. A wrong id is recoverable; gutted content is not.
6211 + if ( self::wdkit_block_attr_count( $serialised ) < self::wdkit_block_attr_count( $parsable ) ) {
6212 + return null;
6213 + }
6214 +
6215 + return $serialised;
6216 + }
6217 +
6218 + /**
6219 + * Repoint the `wp-image-{id}` class that the import stamped into rendered <img> tags.
6220 + *
6221 + * WDKIT_Nexter_Block_Processor::process_image() writes both halves of the tag from the block
6222 + * attributes as they stand when it runs - `src` and `class="… wp-image-{id}"`. With deferred
6223 + * media that is before the image exists locally, so the class carries the id from the site
6224 + * the template was authored on. The deferred pass then repairs the `src` (it is the same url
6225 + * string the replace targets) and the attribute id, but the integer inside the class matches
6226 + * no url and no attribute, so it is left pointing at an attachment that does not exist here.
6227 + *
6228 + * Nothing renders wrong as a result - display comes from `src` - but the class is what links
6229 + * the block back to a Media Library item, so in the editor the image looks detached from any
6230 + * attachment.
6231 + *
6232 + * Deliberately a targeted string rewrite and NOT a parse_blocks()/serialize_blocks() round
6233 + * trip. The change is a run of digits inside one attribute of one tag whose `src` is already
6234 + * known, so nothing here needs to understand block structure - and skipping the round trip
6235 + * means the escaping-depth and attribute-loss failure modes that guard the sibling id remap
6236 + * cannot arise at all.
6237 + *
6238 + * Declines by returning null - not block-ish content, no mapping, a regex that failed, or
6239 + * nothing to change - and the caller then keeps its own content untouched.
6240 + *
6241 + * @since 2.7.1
6242 + *
6243 + * @param string $content post_content with urls and attribute ids already repaired.
6244 + * @param array $url_map source url => array( 'id' => int, 'url' => string ) from this pass.
6245 + * @return string|null Rewritten content, or null to keep the caller's own content.
6246 + */
6247 + private function wdkit_remap_wp_image_classes( $content, $url_map ) {
6248 +
6249 + if ( ! is_string( $content ) || '' === $content || empty( $url_map ) ) {
6250 + return null;
6251 + }
6252 +
6253 + // Nothing stamped a class in this content - no work, and no reason to run a regex over it.
6254 + if ( false === stripos( $content, 'wp-image-' ) || false === stripos( $content, '<img' ) ) {
6255 + return null;
6256 + }
6257 +
6258 + // Local url => the attachment id this pass created for it. Same map the id remap uses, so
6259 + // the class and the attribute cannot end up disagreeing with each other.
6260 + $by_url = array();
6261 +
6262 + foreach ( $url_map as $local ) {
6263 + // Only ids that are genuinely renderable attachments here - see
6264 + // wdkit_is_usable_attachment(). Writing an id that does not resolve would trade one
6265 + // broken reference for another.
6266 + if ( ! empty( $local['url'] ) && ! empty( $local['id'] ) && self::wdkit_is_usable_attachment( $local['id'] ) ) {
6267 + $by_url[ (string) $local['url'] ] = (int) $local['id'];
6268 + }
6269 + }
6270 +
6271 + if ( empty( $by_url ) ) {
6272 + return null;
6273 + }
6274 +
6275 + // Structural fingerprint taken before the rewrite. The only thing this function may
6276 + // change is digits inside a class attribute, so every one of these has to come back
6277 + // identical - anything else means the regex reshaped the markup and the result is refused.
6278 + $img_before = preg_match_all( '/<img\b/i', $content );
6279 + $src_before = preg_match_all( '/\ssrc=/i', $content );
6280 + $changed = 0;
6281 +
6282 + // One pass over the <img> tags, rather than one pass per url: a kit page carries dozens of
6283 + // localised urls and re-scanning the whole document for each of them is needless work.
6284 + $result = preg_replace_callback(
6285 + '/<img\b[^>]*>/i',
6286 + function ( $matches ) use ( $by_url, &$changed ) {
6287 + $tag = $matches[0];
6288 +
6289 + if ( false === stripos( $tag, 'wp-image-' ) ) {
6290 + return $tag;
6291 + }
6292 +
6293 + // Only ever act on a tag whose src is exactly a url this pass localised. A tag
6294 + // pointing anywhere else is left alone, whatever its class says.
6295 + if ( ! preg_match( '/\ssrc=(["\'])(.*?)\1/i', $tag, $src ) ) {
6296 + return $tag;
6297 + }
6298 +
6299 + if ( ! isset( $by_url[ $src[2] ] ) ) {
6300 + return $tag;
6301 + }
6302 +
6303 + $new_id = $by_url[ $src[2] ];
6304 +
6305 + $rewritten = preg_replace_callback(
6306 + '/wp-image-(\d+)/',
6307 + function ( $class_match ) use ( $new_id, &$changed ) {
6308 + if ( (int) $class_match[1] === $new_id ) {
6309 + return $class_match[0];
6310 + }
6311 +
6312 + ++$changed;
6313 +
6314 + return 'wp-image-' . $new_id;
6315 + },
6316 + $tag
6317 + );
6318 +
6319 + return null === $rewritten ? $tag : $rewritten;
6320 + },
6321 + $content
6322 + );
6323 +
6324 + // preg_replace_callback() returns null on failure (backtrack limit on a large page, for
6325 + // instance). Never hand a partial result back.
6326 + if ( null === $result || '' === $result ) {
6327 + return null;
6328 + }
6329 +
6330 + if ( ! $changed ) {
6331 + return null;
6332 + }
6333 +
6334 + // Same tag count, same src count: the rewrite touched classes and nothing structural.
6335 + if ( preg_match_all( '/<img\b/i', $result ) !== $img_before
6336 + || preg_match_all( '/\ssrc=/i', $result ) !== $src_before ) {
6337 + return null;
6338 + }
6339 +
6340 + return $result;
6341 + }
6342 +
6343 + /**
6344 + * Rebuild the block stylesheet for a page whose content we just rewrote.
6345 + *
6346 + * The addon keeps each block's styling in a generated per-page stylesheet, and every rule
6347 + * is keyed to the block id it was written for. That file is produced when the page is
6348 + * saved through the editor - not by wp_update_post() from an AJAX handler - so rewriting
6349 + * content here leaves the page pointing at a stylesheet built for the previous markup.
6350 + * Blocks whose ids are not in that file get no rules at all and render unstyled.
6351 + *
6352 + * @since 2.6.2
6353 + *
6354 + * @param int $post_id Page whose content changed.
6355 + * @return bool True when a rebuild was triggered.
6356 + */
6357 + private static function wdkit_rebuild_block_css( $post_id ) {
6358 +
6359 + if ( ! $post_id ) {
6360 + return false;
6361 + }
6362 +
6363 + // Same build, plus the `_block_css` version the front end caches the stylesheets by
6364 + // (and without the stray term-meta row the generator writes outside the editor) -
6365 + // see Wdkit_Import_Css::rebuild(). Without the version, the rebuilt file kept its old
6366 + // URL and browsers went on serving the copy built for the previous content.
6367 + if ( class_exists( 'Wdkit_Import_Css' ) ) {
6368 + return Wdkit_Import_Css::rebuild( $post_id );
6369 + }
6370 +
6371 + foreach ( get_declared_classes() as $class ) {
6372 + if ( ! method_exists( $class, 'make_block_css_by_post_id' ) ) {
6373 + continue;
6374 + }
6375 +
6376 + try {
6377 + if ( method_exists( $class, 'instance' ) ) {
6378 + $instance = $class::instance();
6379 + } elseif ( method_exists( $class, 'get_instance' ) ) {
6380 + $instance = $class::get_instance();
6381 + } else {
6382 + $instance = new $class();
6383 + }
6384 +
6385 + $instance->make_block_css_by_post_id( $post_id );
6386 +
6387 +
6388 + return true;
6389 + } catch ( \Throwable $e ) {
6390 + // Styling is best-effort: a failure here must not fail the import.
6391 +
6392 + return false;
6393 + }
6394 + }
6395 +
6396 + return false;
6397 + }
6398 +
6399 + /**
6400 + * How many block attributes does this markup actually yield when parsed?
6401 + *
6402 + * Used as a before/after measure: block attributes are the part of block markup a round
6403 + * trip can silently drop, so counting them is how we tell a rewrite from a mangling.
6404 + *
6405 + * @since 2.6.2
6406 + *
6407 + * @param string $content Block markup.
6408 + * @return int Total attributes across every block.
6409 + */
6410 + private static function wdkit_block_attr_count( $content ) {
6411 + $total = 0;
6412 +
6413 + $walk = function ( $blocks ) use ( &$walk, &$total ) {
6414 + foreach ( $blocks as $block ) {
6415 + if ( ! empty( $block['attrs'] ) && is_array( $block['attrs'] ) ) {
6416 + $total += count( $block['attrs'] );
6417 + }
6418 +
6419 + if ( ! empty( $block['innerBlocks'] ) ) {
6420 + $walk( $block['innerBlocks'] );
6421 + }
6422 + }
6423 + };
6424 +
6425 + $walk( parse_blocks( (string) $content ) );
6426 +
6427 + return $total;
6428 + }
6429 +
6430 + /**
6431 + * Return this content in a form whose block attributes actually parse.
6432 + *
6433 + * Content written straight to post_content never had to parse, so an extra level of
6434 + * escaping on the way in did no harm. Parsing it - which repointing media requires - makes
6435 + * that escaping fatal: `{\"Title\":\"…\"}` is not JSON, so every attribute is discarded.
6436 + *
6437 + * Rather than assume a slash depth, this measures: if stripping one level yields more
6438 + * attributes, the content was over-escaped and the stripped form is the real one.
6439 + *
6440 + * @since 2.6.2
6441 + *
6442 + * @param string $content Block markup as received.
6443 + * @return string|null Markup safe to parse, or null when no form of it parses.
6444 + */
6445 + private static function wdkit_parsable_block_content( $content ) {
6446 +
6447 + $as_is = self::wdkit_block_attr_count( $content );
6448 +
6449 + // Nothing claims to carry attributes, so there is nothing to lose either.
6450 + if ( false === strpos( $content, '{' ) ) {
6451 + return $content;
6452 + }
6453 +
6454 + // Already parses into real attributes: this is a single, correctly escaped level.
6455 + // Stripping it again would remove backslashes the JSON itself needs (\n, \", \/, the
6456 + // -- guard around a literal "--") and corrupt otherwise-valid content -
6457 + // checking the stripped form is only safe once the as-is form has proven broken.
6458 + if ( $as_is > 0 ) {
6459 + return $content;
6460 + }
6461 +
6462 + $stripped = wp_unslash( $content );
6463 + $stripped_attrs = self::wdkit_block_attr_count( $stripped );
6464 +
6465 + if ( $stripped_attrs > $as_is ) {
6466 + return $stripped;
6467 + }
6468 +
6469 + // Neither form parses into attributes even though the markup contains JSON: better to
6470 + // leave the content exactly as it arrived than to rewrite it into something bare.
6471 + return null;
6472 + }
6473 +
6474 + /**
6475 + * Point a block's saved markup at the media that was just localised.
6476 + *
6477 + * A block stores a rendered copy of itself in `innerHTML` / `innerContent`, and for many
6478 + * blocks that copy is what the front end actually outputs. Importing the attributes alone
6479 + * therefore fixes the editor while leaving the page still loading from the site the
6480 + * template came from - and those hosts answer 403, so the image renders broken.
6481 + *
6482 + * @since 2.6.2
6483 + *
6484 + * @param array $block_data One parsed block.
6485 + * @return array The block with its markup repointed.
6486 + */
6487 + private static function wdkit_relink_block_markup( $block_data ) {
6488 +
6489 + self::wdkit_require_import_images();
6490 + $map = Wdkit_Import_Images::get_url_map();
6491 +
6492 + if ( empty( $map ) ) {
6493 + return $block_data;
6494 + }
6495 +
6496 + $from = array_keys( $map );
6497 + $to = array_values( $map );
6498 +
6499 + if ( ! empty( $block_data['innerHTML'] ) && is_string( $block_data['innerHTML'] ) ) {
6500 + $block_data['innerHTML'] = str_replace( $from, $to, $block_data['innerHTML'] );
6501 + }
6502 +
6503 + if ( ! empty( $block_data['innerContent'] ) && is_array( $block_data['innerContent'] ) ) {
6504 + foreach ( $block_data['innerContent'] as $index => $chunk ) {
6505 + if ( is_string( $chunk ) ) {
6506 + $block_data['innerContent'][ $index ] = str_replace( $from, $to, $chunk );
6507 + }
6508 + }
6509 + }
6510 +
1408 6511 return $block_data;
1409 6512 }
1410 6513
1411 6514 /**
6515 + * Import every media reference held in a block's attributes.
6516 + *
6517 + * Block attributes nest arbitrarily - a repeater of cards each with an image, responsive
6518 + * variants, nested inner settings - so this recurses rather than reaching a fixed number
6519 + * of levels down. The previous version was unrolled exactly four levels deep and also
6520 + * skipped any subtree carrying an `md` key, which meant anything below that simply kept
6521 + * the source site's URL and attachment ID and rendered as an empty placeholder.
6522 + *
6523 + * A node counts as media when it has a non-empty string `url` and either an id slot or a
6524 + * URL that names an image file. That pairing is what distinguishes a media control from
6525 + * a link, which also carries a `url`.
6526 + *
6527 + * @since 2.6.2
6528 + *
6529 + * @param mixed $node Block attributes, walked recursively.
6530 + * @return mixed Attributes with local media.
6531 + */
6532 + private static function wdkit_import_block_media( $node, $defer_media = false ) {
6533 +
6534 + if ( ! is_array( $node ) ) {
6535 + return $node;
6536 + }
6537 +
6538 + $url = isset( $node['url'] ) && is_string( $node['url'] ) ? $node['url'] : '';
6539 +
6540 + if ( '' !== $url
6541 + && ( array_key_exists( 'id', $node ) || array_key_exists( 'Id', $node )
6542 + || preg_match( '/\.(?:jpe?g|png|gif|svg|webp|avif|bmp)$/i', (string) wp_parse_url( $url, PHP_URL_PATH ) ) )
6543 + ) {
6544 + $imported = Wdkit_Import_Images::wdkit_Import_media( $node, $defer_media );
6545 +
6546 + // Only accept a real result. The importer returns the node untouched when it
6547 + // cannot localise the file, and anything falsy here would wipe out the URL and
6548 + // leave the block with no image at all.
6549 + if ( ! empty( $imported['url'] ) ) {
6550 + $node = array_merge( $node, $imported );
6551 + }
6552 + }
6553 +
6554 + // Keep walking even after importing this node. A media value carries its own `sizes`
6555 + // map of per-size URLs, and returning here left every one of those pointing at the
6556 + // site the template came from - which is what the widgets actually render from.
6557 + foreach ( $node as $key => $value ) {
6558 + if ( is_array( $value ) ) {
6559 + $node[ $key ] = self::wdkit_import_block_media( $value, $defer_media );
6560 + }
6561 + }
6562 +
6563 + return $node;
6564 + }
6565 +
6566 + /**
1412 6567 * Kit Template Import Pages/Sections
1413 6568 *
1414 6569 * @since 1.0.0
1415 6570 * */
@@ -1418,8 +6573,10 @@
1418 6573 if ( ! current_user_can( 'manage_options' ) ) {
1419 6574 return false;
1420 6575 }
1421 6576
6577 + $builder = isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : '';
6578 +
1422 6579 $page_section = ! empty( $_POST['page_section'] ) ? sanitize_text_field( wp_unslash( $_POST['page_section'] ) ) : '';
1423 6580
1424 6581 if ( isset( $page_section ) ) {
1425 6582 $args['page_section'] = ! empty( $page_section ) ? sanitize_text_field( wp_unslash( $page_section ) ) : '';
@@ -1424,14 +6581,16 @@
1424 6581 if ( isset( $page_section ) ) {
1425 6582 $args['page_section'] = ! empty( $page_section ) ? sanitize_text_field( wp_unslash( $page_section ) ) : '';
1426 6583 }
1427 6584
1428 - $template_ids = ! empty( $_POST['template_ids'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['template_ids'] ) ), true ) : array();
1429 - $email = ! empty( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : '';
1430 - $editor = isset( $_POST['editor'] ) ? sanitize_text_field( wp_unslash( $_POST['editor'] ) ) : '';
1431 - $website_kit = isset( $_POST['website_kit'] ) ? sanitize_text_field( wp_unslash( $_POST['website_kit'] ) ) : '';
6585 + $template_ids = ! empty( $_POST['template_ids'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['template_ids'] ) ), true ) : array();
6586 + $email = ! empty( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : '';
6587 + $editor = isset( $_POST['editor'] ) ? sanitize_text_field( wp_unslash( $_POST['editor'] ) ) : '';
6588 + $website_kit = isset( $_POST['website_kit'] ) ? sanitize_text_field( wp_unslash( $_POST['website_kit'] ) ) : '';
6589 + $api_type = isset( $_POST['api_type'] ) ? sanitize_text_field( wp_unslash( $_POST['api_type'] ) ) : 'import_template';
6590 + $ai_compitible = isset( $_POST['ai_compitible'] ) ? sanitize_text_field( wp_unslash( $_POST['ai_compitible'] ) ) : false;
1432 6591
1433 - if ( empty( $email ) || empty( $template_ids ) ) {
6592 + if ( empty( $template_ids ) ) {
1434 6593 $output = array(
1435 6594 'message' => esc_html__( 'Invalid import', 'wdesignkit' ),
1436 6595 'description' => esc_html__( 'Invalid import: Check your details and try again.', 'wdesignkit' ),
1437 6596 'success' => false,
@@ -1455,32 +6614,1991 @@
1455 6614 'token' => $token,
1456 6615 'template_id' => $template_ids['id'],
1457 6616 'editor' => $editor,
1458 6617 'website_kit' => $website_kit,
6618 + 'unique_id' => get_option( 'wdkit_unique_id' ) ?? '',
1459 6619 );
1460 6620
1461 - $response = WDesignKit_Data_Query::get_data( 'import_template', $temp_args );
6621 + if ( function_exists( 'wdkit_kit_import_with_site_identity' ) ) {
6622 + $temp_args = wdkit_kit_import_with_site_identity( $temp_args );
6623 + }
6624 +
6625 + $response = WDesignKit_Data_Query::get_data( $api_type, $temp_args );
1462 6626 $output = array();
1463 6627
1464 - if ( 'error' === $response['content'] ) {
6628 + if ( is_wp_error( $response ) ) {
6629 +
1465 6630 wp_send_json( $response );
1466 6631 wp_die();
6632 + }
6633 +
6634 + if ( isset( $response['content'] ) && 'error' === $response['content'] ) {
6635 + wp_send_json( $response );
6636 + wp_die();
6637 + }
6638 +
6639 + $result = array(
6640 + 'response' => $response,
6641 + 'args' => $args,
6642 + 'id' => $template_ids['id'],
6643 + 'temp_data' => $template_ids,
6644 + );
6645 +
6646 + $output['message'] = $response['message'];
6647 + $output['description'] = $response['description'];
6648 + $output['data'] = $result;
6649 + $output['success'] = $response['success'];
6650 +
6651 + // Counts the IMPORT ACTION, not what it brought in. A kit import always counts as 1 kit,
6652 + // no matter how many blocks/pages that kit contains — confirmed live: a single gutenberg
6653 + // kit import recorded total=680, kinds.kit=680, because $template_ids for that call was a
6654 + // 680-element array of the kit's own blocks and count( $template_ids ) counted every one of
6655 + // them. A page-kit's *size* is not tracking's concern; "was a kit imported" is.
6656 + //
6657 + // The 'single' branch keeps a defensive fallback for the one shape this endpoint's own
6658 + // $template_ids reliably takes when it is not a kit — a single {id, name, slug, thumb...}
6659 + // object — where count() would likewise count JSON keys instead of "1 template imported".
6660 + if ( ! empty( $output['success'] ) ) {
6661 + $is_kit = ( '' !== $website_kit );
6662 + $import_count = $is_kit ? 1 : ( isset( $template_ids['id'] ) ? 1 : ( is_array( $template_ids ) ? count( $template_ids ) : 1 ) );
6663 + do_action(
6664 + 'wdkit_template_imported',
6665 + $is_kit ? 'kit' : 'single',
6666 + sanitize_key( $builder ),
6667 + $import_count
6668 + );
6669 + }
6670 +
6671 + wp_send_json( $output );
6672 + wp_die();
6673 + }
6674 +
6675 + /**
6676 + * Batched template-JSON fetch for a full AI kit import.
6677 + *
6678 + * Mirrors wdkit_import_kit_template() but accepts every template of a kit at once and
6679 + * forwards their ids to the Laravel `import_template_batch` route in a single request,
6680 + * instead of one `import_template` round trip per template. The per-template
6681 + * `response`/`args`/`temp_data` shape returned for each item is identical to what
6682 + * wdkit_import_kit_template() returns for a single template, so the existing
6683 + * import_kit_temps()/import_page_section_content() insertion path on the JS/PHP side
6684 + * is unaffected — only the template-fetch round trips are collapsed.
6685 + *
6686 + * Any failure (network, missing route on an older/self-hosted backend, malformed
6687 + * response) returns success=false so the caller falls back to the existing
6688 + * one-call-per-template flow.
6689 + *
6690 + * @since 2.7.0
6691 + */
6692 + protected function wkit_import_kit_bundle() {
6693 +
6694 + if ( ! current_user_can( 'manage_options' ) ) {
6695 + return false;
6696 + }
6697 +
6698 + $builder = isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : '';
6699 + $templates = ! empty( $_POST['template_ids'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['template_ids'] ) ), true ) : array();
6700 + $email = ! empty( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : '';
6701 + $editor = isset( $_POST['editor'] ) ? sanitize_text_field( wp_unslash( $_POST['editor'] ) ) : '';
6702 + $website_kit = isset( $_POST['website_kit'] ) ? sanitize_text_field( wp_unslash( $_POST['website_kit'] ) ) : '';
6703 + $custom_meta = isset( $_POST['custom_meta'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_meta'] ) ) : false;
6704 +
6705 + if ( empty( $templates ) || ! is_array( $templates ) ) {
6706 + wp_send_json(
6707 + array(
6708 + 'message' => esc_html__( 'Invalid import', 'wdesignkit' ),
6709 + 'description' => esc_html__( 'Invalid import: Check your details and try again.', 'wdesignkit' ),
6710 + 'success' => false,
6711 + )
6712 + );
6713 + wp_die();
6714 + }
6715 +
6716 + $ids = array();
6717 + foreach ( $templates as $temp ) {
6718 + if ( ! empty( $temp['id'] ) ) {
6719 + $ids[] = (int) $temp['id'];
6720 + }
6721 + }
6722 +
6723 + if ( empty( $ids ) ) {
6724 + wp_send_json(
6725 + array(
6726 + 'message' => esc_html__( 'Invalid import', 'wdesignkit' ),
6727 + 'success' => false,
6728 + )
6729 + );
6730 + wp_die();
6731 + }
6732 +
6733 + $token = $this->wdkit_login_user_token( $email );
6734 +
6735 + $batch_args = array(
6736 + 'token' => $token,
6737 + 'template_ids' => $ids,
6738 + 'website_kit' => $website_kit,
6739 + 'unique_id' => get_option( 'wdkit_unique_id' ) ?? '',
6740 + );
6741 +
6742 + if ( function_exists( 'wdkit_kit_import_with_site_identity' ) ) {
6743 + $batch_args = wdkit_kit_import_with_site_identity( $batch_args );
6744 + }
6745 +
6746 + $response = WDesignKit_Data_Query::get_data( 'import_template_batch', $batch_args );
6747 +
6748 + if ( is_wp_error( $response ) || empty( $response['success'] ) ) {
6749 + wp_send_json(
6750 + array(
6751 + 'message' => is_wp_error( $response ) ? $response->get_error_message() : ( $response['message'] ?? esc_html__( 'Batch import failed', 'wdesignkit' ) ),
6752 + 'success' => false,
6753 + )
6754 + );
6755 + wp_die();
6756 + }
6757 +
6758 + $by_id = array();
6759 + foreach ( (array) $response['templates'] as $tpl_result ) {
6760 + if ( isset( $tpl_result['id'] ) ) {
6761 + $by_id[ (int) $tpl_result['id'] ] = $tpl_result;
6762 + }
6763 + }
6764 +
6765 + $results = array();
6766 + foreach ( $templates as $temp ) {
6767 + $tid = ! empty( $temp['id'] ) ? (int) $temp['id'] : 0;
6768 + $tpl_response = isset( $by_id[ $tid ] ) ? $by_id[ $tid ] : array(
6769 + 'success' => false,
6770 + 'message' => esc_html__( 'Template not found in batch response', 'wdesignkit' ),
6771 + );
6772 +
6773 + // import_temp_json() resolves `editor` per template from that template's own
6774 + // post_builder (Get_temp_builer()), not one value for the whole kit — a kit can
6775 + // mix builders. The client sends that already-resolved value as `_editor` on
6776 + // each item; fall back to the request-level $editor if it's missing.
6777 + $args = array(
6778 + 'editor' => ! empty( $temp['_editor'] ) ? sanitize_text_field( $temp['_editor'] ) : $editor,
6779 + 'custom_meta' => $custom_meta,
6780 + );
6781 +
6782 + $results[] = array(
6783 + 'id' => $tid,
6784 + 'success' => ! empty( $tpl_response['success'] ),
6785 + 'message' => $tpl_response['message'] ?? '',
6786 + 'data' => array(
6787 + 'response' => $tpl_response,
6788 + 'args' => $args,
6789 + 'id' => $tid,
6790 + 'temp_data' => $temp,
6791 + ),
6792 + );
6793 + }
6794 +
6795 + do_action( 'wdkit_template_imported', 'kit', sanitize_key( $builder ), count( $ids ) );
6796 +
6797 + wp_send_json(
6798 + array(
6799 + 'results' => $results,
6800 + 'success' => true,
6801 + )
6802 + );
6803 + wp_die();
6804 + }
6805 +
6806 + /**
6807 + * Hand the assembled site bundle to the client without importing it.
6808 + *
6809 + * The AI import path needs this: personalizing a template runs ~1,600 lines of browser
6810 + * logic (replace_elementor_txt and the media/global chain) against site_obj.site_info,
6811 + * which only exists in the browser. So an AI kit fetches the bundle, transforms every
6812 + * template with the same proven code the per-template path uses, and posts the result
6813 + * back to wkit_import_site_bundle(). A non-AI kit never calls this — it lets
6814 + * wkit_import_site_bundle() fetch and build in one request, with no payload leaving
6815 + * the server.
6816 + *
6817 + * @since 2.7.0
6818 + */
6819 + protected function wkit_fetch_site_bundle() {
6820 +
6821 + $t_fetch = microtime( true );
6822 + $bundle = $this->wdkit_fetch_site_bundle();
6823 +
6824 + if ( is_wp_error( $bundle ) ) {
6825 + wp_send_json(
6826 + array(
6827 + 'message' => $bundle->get_error_message(),
6828 + 'description' => $bundle->get_error_message(),
6829 + 'success' => false,
6830 + )
6831 + );
6832 + wp_die();
6833 + }
6834 +
6835 + // Widget manifest only. The non-AI path needs to know what to enable before it
6836 + // imports, but has no use for the content itself — sending a multi-megabyte kit down
6837 + // just to count widget names would cost more than the problem it solves. The bundle
6838 + // stays parked in the cache wdkit_fetch_site_bundle() just filled, so the import call
6839 + // that follows reuses it instead of assembling the kit a second time.
6840 + if ( ! empty( $_POST['widgets_only'] ) ) {
6841 + $manifest = self::wdkit_bundle_widget_manifest( is_array( $bundle ) ? $bundle : array() );
6842 +
6843 + wp_send_json(
6844 + array(
6845 + 'success' => true,
6846 + 'widgets' => $manifest['widgets'],
6847 + 'extensions' => $manifest['extensions'],
6848 + 'timing_ms' => array(
6849 + 'cloud_assemble' => isset( $bundle['cloud_assemble_ms'] ) ? (int) $bundle['cloud_assemble_ms'] : 0,
6850 + 'wp_total' => (int) round( ( microtime( true ) - $t_fetch ) * 1000 ),
6851 + ),
6852 + )
6853 + );
6854 + wp_die();
6855 + }
6856 +
6857 + if ( is_array( $bundle ) ) {
6858 + unset( $bundle['cache_key'] );
6859 +
6860 + $bundle['timing_ms'] = array(
6861 + 'cloud_assemble' => isset( $bundle['cloud_assemble_ms'] ) ? (int) $bundle['cloud_assemble_ms'] : 0,
6862 + 'wp_total' => (int) round( ( microtime( true ) - $t_fetch ) * 1000 ),
6863 + 'payload_kb' => (int) round( strlen( (string) wp_json_encode( $bundle ) ) / 1024 ),
6864 + );
6865 + }
6866 +
6867 + wp_send_json( $bundle );
6868 + wp_die();
6869 + }
6870 +
6871 + /**
6872 + * Ingest a pre-assembled kit site bundle in one request.
6873 + *
6874 + * The cloud half is GenerateKitSiteBundle(): it resolves every template in the kit,
6875 + * splits them into pages/sections, and pre-merges the kit's The Plus globals into one
6876 + * object. This side builds all of it inside a single PHP request, in place of the
6877 + * historical one-AJAX-call-per-template walk.
6878 + *
6879 + * Per-template insertion is NOT reimplemented here — wdkit_bundle_insert_item() below is
6880 + * a port of import_page_section_content(), the canonical shipped import path: Elementor
6881 + * templates go through \Elementor\Plugin::$instance->documents->create() plus
6882 + * $document->save() rather than hand-written meta, Gutenberg templates through the Nexter
6883 + * block processor, and images through wdkit_media_import() under the same $defer_media
6884 + * contract. All this method removes is the HTTP round trip that used to sit around each
6885 + * one; every legacy route and handler is left untouched.
6886 + *
6887 + * @since 2.7.0
6888 + */
6889 + protected function wkit_import_site_bundle() {
6890 + $response = $this->wdkit_import_site_bundle_data();
6891 +
6892 + wp_send_json( $response );
6893 + wp_die();
6894 + }
6895 +
6896 + /**
6897 + * The kit importer itself, reachable without a request.
6898 + *
6899 + * Extracted from the AJAX action above so the PHP import runner imports through THIS
6900 + * engine rather than carrying a second one.
6901 + *
6902 + * ── Why this extraction is the whole point of the merge ─────────────────────
6903 + *
6904 + * The runner grew its own content pipeline: one cloud round trip per template, its own
6905 + * media handling, its own page store writing `_elementor_data` by hand. Measured on a
6906 + * 15-template kit that came to 71-139s against ~8s here, and it also skipped things
6907 + * this path does - Elementor's own documents->create()/save(), unique element ids,
6908 + * server-side block CSS, the theme-builder remap, the nav menu build. Two pipelines
6909 + * meant two sets of behaviour to keep in step, and only one of them was fast.
6910 + *
6911 + * So the runner now hands its (optionally AI-merged) bundle straight to this method.
6912 + * The browser flow reaches the identical code through the action above, so "the fast
6913 + * flow" and "the runner" are the same importer, and speed cannot drift between them.
6914 + *
6915 + * @since 2.7.2
6916 + *
6917 + * @param array|null $request Request fields to read instead of `$_POST`. The runner
6918 + * passes its own array - normally carrying `bundle`
6919 + * directly, already fetched and AI-merged, which is the
6920 + * same entry tests and WP-CLI have always used.
6921 + * @return array The response the AJAX action emits.
6922 + */
6923 + public function wdkit_import_site_bundle_data( $request = null ) {
6924 +
6925 + // Elementor sideloads every image a page references from inside this request, and a
6926 + // single oversized source image decodes to more than the whole memory limit. Same
6927 + // guard import_page_section_content() takes, for the same reason.
6928 + $this->wdkit_guard_oversized_images();
6929 +
6930 + // Kit-level millisecond costs for this request, returned in the response.
6931 + $kit_timing = array();
6932 + $t_request = microtime( true );
6933 +
6934 + // A whole kit in one request, where import_page_section_content() handled one
6935 + // template in 120s. Harmless no-op where set_time_limit() is disabled.
6936 + if ( function_exists( 'set_time_limit' ) ) {
6937 + @set_time_limit( 300 );
6938 + }
6939 +
6940 + // Whether the fields came off the wire. Only then is the content slashed, and only
6941 + // then may it be unslashed: running wp_unslash() over a bundle a PHP caller built
6942 + // would strip the one level of escaping block markup depends on - the
6943 + // `var(--tpgb-C7)` global references - and every global colour on every
6944 + // page would silently stop resolving.
6945 + $from_request = ! is_array( $request );
6946 +
6947 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the AJAX router verified the nonce before dispatching; the runner supplies its own array.
6948 + $request = $from_request ? $_POST : $request;
6949 +
6950 + $raw_bundle = isset( $request['bundle'] ) ? $request['bundle'] : '';
6951 +
6952 + if ( $from_request ) {
6953 + $raw_bundle = wp_unslash( $raw_bundle );
6954 + }
6955 +
6956 + $bundle = is_array( $raw_bundle ) ? $raw_bundle : json_decode( $raw_bundle, true );
6957 +
6958 + // Normal path: the client sends only the kit's template ids and the bundle is fetched
6959 + // here. Sending it down to the browser and straight back up would double the transfer
6960 + // and put a multi-megabyte kit up against post_max_size on the return trip, so the
6961 + // assembled JSON never leaves the server. Passing `bundle` directly still works and is
6962 + // what the import can be driven with directly (tests, WP-CLI, other callers).
6963 + if ( ( empty( $bundle ) || ! is_array( $bundle ) ) && ! empty( $request['template_ids'] ) ) {
6964 + // Normally a cache hit: the widgets_only call immediately before this one already
6965 + // assembled the kit, so this reuses it rather than paying the cloud round trip
6966 + // again. A cold call (no preceding fetch, or the cache expired) still assembles.
6967 + $bundle = $this->wdkit_fetch_site_bundle( $request, isset( $request['token'] ) ? (string) $request['token'] : '' );
6968 +
6969 + // Error check FIRST. This used to sit below the cache-key cleanup, which indexes
6970 + // $bundle as an array - and on the failure path $bundle is a WP_Error object, so
6971 + // the cleanup fataled with "Cannot use object of type WP_Error as array" and took
6972 + // the whole chunk request down. That matters far beyond one lost error message:
6973 + // a chunk that dies never returns its `imported` entries, so the client never
6974 + // seeds post_ids with the demo-id -> local-id mapping, and update_section_id()
6975 + // then silently skips remapping every dynamic widget (tp-switcher, tp-tabs-tours,
6976 + // tp-accordion...). The final chunk dying also means wdkit_bundle_build_menu()
6977 + // never runs, so the kit imports with no navigation menu.
6978 + if ( is_wp_error( $bundle ) ) {
6979 + return array(
6980 + 'message' => $bundle->get_error_message(),
6981 + 'description' => $bundle->get_error_message(),
6982 + 'success' => false,
6983 + );
6984 + }
6985 +
6986 + // Consumed. Holding a whole kit in the options table past the import it was
6987 + // assembled for is dead weight, and a retry after a failure should get a fresh
6988 + // copy rather than whatever this attempt was handed.
6989 + if ( ! empty( $bundle['cache_key'] ) ) {
6990 + delete_transient( $bundle['cache_key'] );
6991 + }
6992 + }
6993 +
6994 + if ( empty( $bundle ) || ! is_array( $bundle ) ) {
6995 + return array(
6996 + 'message' => esc_html__( 'Invalid bundle', 'wdesignkit' ),
6997 + 'description' => esc_html__( 'Invalid bundle payload: nothing to import.', 'wdesignkit' ),
6998 + 'success' => false,
6999 + );
7000 + }
7001 +
7002 + $builder = ! empty( $bundle['builder'] ) ? sanitize_key( $bundle['builder'] ) : 'elementor';
7003 + $pages = ( ! empty( $bundle['pages'] ) && is_array( $bundle['pages'] ) ) ? $bundle['pages'] : array();
7004 + $sections = ( ! empty( $bundle['sections'] ) && is_array( $bundle['sections'] ) ) ? $bundle['sections'] : array();
7005 + $nav_menu = ( ! empty( $bundle['nav_menu'] ) && is_array( $bundle['nav_menu'] ) ) ? $bundle['nav_menu'] : array();
7006 + $defer_media = ! empty( $request['defer_media'] );
7007 + $custom_meta = ! empty( $request['custom_meta'] );
7008 + $wireframe = ! empty( $request['wirefram_import'] );
7009 +
7010 + // Chunked ingestion, used by the AI path: the browser transforms every template and
7011 + // posts them back a few at a time so a large kit never meets post_max_size in one
7012 + // POST. Each chunk carries the same `session`; the last one sets `finalize`. With no
7013 + // session at all (the non-AI single-call path) this behaves exactly as before —
7014 + // one request that imports and finalizes.
7015 + $session = isset( $request['session'] ) ? sanitize_key( $request['session'] ) : '';
7016 + $is_final = empty( $session ) || ! empty( $request['finalize'] );
7017 + $session_key = $session ? 'wdkit_sb_' . $session : '';
7018 + $carried = $session_key ? get_transient( $session_key ) : false;
7019 + $carried = is_array( $carried ) ? $carried : array();
7020 +
7021 + if ( empty( $pages ) && empty( $sections ) ) {
7022 + return array(
7023 + 'message' => esc_html__( 'Invalid bundle', 'wdesignkit' ),
7024 + 'description' => esc_html__( 'The bundle contained no templates to import.', 'wdesignkit' ),
7025 + 'success' => false,
7026 + );
7027 + }
7028 +
7029 + // ── 1. Kit globals, once for the whole kit ──────────────────────────────────────
7030 + // The cloud already merged every template's globals into this one object (same
7031 + // dedupe-by-_id rule wdkit_merge_tp_globals() applies), so the presets land before
7032 + // the first page renders instead of accumulating one template at a time.
7033 + $site_settings = ( ! empty( $bundle['site_settings'] ) && is_array( $bundle['site_settings'] ) ) ? $bundle['site_settings'] : array();
7034 +
7035 + // Only on the first chunk: the merge dedupes by `_id` so repeating it is harmless,
7036 + // but it rewrites kit meta and regenerates the kit stylesheet every time.
7037 + if ( empty( $carried ) && ! empty( $site_settings['tp_globals'] ) && is_array( $site_settings['tp_globals'] ) ) {
7038 + $t_globals = microtime( true );
7039 + $this->wdkit_merge_tp_globals(
7040 + $site_settings['tp_globals'],
7041 + ( ! empty( $site_settings['tp_global_refs'] ) && is_array( $site_settings['tp_global_refs'] ) )
7042 + ? $site_settings['tp_global_refs']
7043 + : array()
7044 + );
7045 + // This one also regenerates the kit stylesheet, so it is worth its own number.
7046 + $kit_timing['globals_merge'] = (int) round( ( microtime( true ) - $t_globals ) * 1000 );
7047 + }
7048 +
7049 + // ── 2. Sections first, then pages ───────────────────────────────────────────────
7050 + // Headers, footers and nav are what the pages reference, and a theme-builder
7051 + // condition attached to a section should exist before the first page renders.
7052 + $imported = array();
7053 + $errors = array();
7054 + $widgets_to_enable = array();
7055 + $deferred_pages = array();
7056 + $template_map = array();
7057 +
7058 + $groups = array(
7059 + 'section' => $sections,
7060 + 'page' => $pages,
7061 + );
7062 +
7063 + foreach ( $groups as $group => $items ) {
7064 + foreach ( $items as $item ) {
7065 + if ( ! is_array( $item ) ) {
7066 + continue;
7067 + }
7068 +
7069 + if ( $wireframe ) {
7070 + $item = self::wdkit_wireframe_bundle_item( $item );
7071 + }
7072 +
7073 + $result = $this->wdkit_bundle_insert_item( $item, $builder, $defer_media, $custom_meta );
7074 +
7075 + // One template failing (empty content, builder missing, insert error) is
7076 + // recorded and skipped — it must not cost the rest of the site.
7077 + if ( empty( $result['success'] ) ) {
7078 + $errors[] = array(
7079 + 'id' => isset( $item['id'] ) ? sanitize_text_field( (string) $item['id'] ) : '',
7080 + 'title' => isset( $item['title'] ) ? sanitize_text_field( (string) $item['title'] ) : '',
7081 + 'message' => ! empty( $result['message'] ) ? $result['message'] : esc_html__( 'Template could not be imported.', 'wdesignkit' ),
7082 + );
7083 + continue;
7084 + }
7085 +
7086 + // A template that stored fewer widgets than it arrived with imported "successfully"
7087 + // — the page exists and is linked — but its content is incomplete. Report it as
7088 + // an error as well as an import so the row fails loudly instead of the kit
7089 + // looking clean while a page renders empty. Deleting the page instead would
7090 + // trade a visibly broken page for a silently missing one.
7091 + if ( ! empty( $result['unregistered'] ) ) {
7092 + $errors[] = array(
7093 + 'id' => isset( $item['id'] ) ? sanitize_text_field( (string) $item['id'] ) : '',
7094 + 'title' => isset( $item['title'] ) ? sanitize_text_field( (string) $item['title'] ) : '',
7095 + 'message' => sprintf(
7096 + /* translators: %s: comma-separated list of widget type slugs. */
7097 + esc_html__( 'Imported without these widgets, which are not registered on this site: %s', 'wdesignkit' ),
7098 + implode( ', ', array_map( 'sanitize_text_field', $result['unregistered'] ) )
7099 + ),
7100 + );
7101 + }
7102 +
7103 + // The loader's progress rows are keyed by cloud template id, not post id.
7104 + $result['group'] = $group;
7105 + $result['template_id'] = isset( $item['id'] ) ? $item['id'] : '';
7106 +
7107 + // Sum each phase across every template in this chunk.
7108 + if ( ! empty( $result['timing_ms'] ) && is_array( $result['timing_ms'] ) ) {
7109 + foreach ( $result['timing_ms'] as $phase => $ms ) {
7110 + $key = 'sum_' . $phase;
7111 + $kit_timing[ $key ] = ( isset( $kit_timing[ $key ] ) ? $kit_timing[ $key ] : 0 ) + (int) $ms;
7112 + }
7113 + }
7114 + $imported[] = $result;
7115 +
7116 + if ( ! empty( $item['id'] ) ) {
7117 + $template_map[ (string) $item['id'] ] = $result['id'];
7118 + }
7119 +
7120 + if ( ! empty( $result['widget_list'] ) ) {
7121 + $widgets_to_enable = array_unique( array_merge( $widgets_to_enable, $result['widget_list'] ) );
7122 + }
7123 +
7124 + // Same payload wkit_schedule_deferred_media_sync() receives from the JS side.
7125 + // Built here because this request already knows every post id it created, so
7126 + // the kit needs no extra round trip to register its media work.
7127 + if ( $defer_media && ! empty( $result['image_urls'] ) ) {
7128 + $deferred_pages[] = array(
7129 + 'post_id' => $result['id'],
7130 + 'builder' => $result['editor'],
7131 + 'image_urls' => $result['image_urls'],
7132 + );
7133 + }
7134 + }
7135 + }
7136 +
7137 + // Fold in everything earlier chunks of this session already built, so the finalize
7138 + // steps below see the whole kit and not just this chunk.
7139 + if ( ! empty( $carried ) ) {
7140 + $imported = array_merge( isset( $carried['imported'] ) ? $carried['imported'] : array(), $imported );
7141 + $errors = array_merge( isset( $carried['errors'] ) ? $carried['errors'] : array(), $errors );
7142 + $widgets_to_enable = array_unique( array_merge( isset( $carried['widgets'] ) ? $carried['widgets'] : array(), $widgets_to_enable ) );
7143 + $deferred_pages = array_merge( isset( $carried['deferred'] ) ? $carried['deferred'] : array(), $deferred_pages );
7144 + // `+` and NOT array_merge(): template ids are numeric keys, and array_merge()
7145 + // renumbers those into 0,1,2... which silently destroys the id => post_id mapping
7146 + // the nav menu resolves against. Left operand wins, so this chunk's entries take
7147 + // precedence over an earlier chunk's.
7148 + $carried_map = ( isset( $carried['template_map'] ) && is_array( $carried['template_map'] ) ) ? $carried['template_map'] : array();
7149 + $template_map = $template_map + $carried_map;
7150 + }
7151 +
7152 + // Not the last chunk: bank what this one made and let the client send the next.
7153 + if ( ! $is_final ) {
7154 + set_transient(
7155 + $session_key,
7156 + array(
7157 + 'imported' => $imported,
7158 + 'errors' => $errors,
7159 + 'widgets' => array_values( $widgets_to_enable ),
7160 + 'deferred' => $deferred_pages,
7161 + 'template_map' => $template_map,
7162 + ),
7163 + 15 * MINUTE_IN_SECONDS
7164 + );
7165 +
7166 + return array(
7167 + 'message' => esc_html__( 'Bundle chunk imported.', 'wdesignkit' ),
7168 + 'imported' => $imported,
7169 + 'errors' => $errors,
7170 + 'partial' => true,
7171 + 'success' => true,
7172 + );
7173 + }
7174 +
7175 + if ( $session_key ) {
7176 + delete_transient( $session_key );
7177 + }
7178 +
7179 + if ( empty( $imported ) ) {
7180 + return array(
7181 + 'message' => esc_html__( 'Import failed', 'wdesignkit' ),
7182 + 'description' => esc_html__( 'No template in the bundle could be imported.', 'wdesignkit' ),
7183 + 'errors' => $errors,
7184 + 'success' => false,
7185 + );
7186 + }
7187 +
7188 + // ── 3. Front page and shop page ─────────────────────────────────────────────────
7189 + // An explicit flag from the bundle wins; the title heuristic is the same fallback
7190 + // wdkit_handle_create_full_site() and update_site_setting() already rely on.
7191 + $front_page_id = 0;
7192 + $shop_page_id = 0;
7193 +
7194 + foreach ( $imported as $entry ) {
7195 + if ( 'page' !== $entry['group'] ) {
7196 + continue;
7197 + }
7198 +
7199 + $title_lower = strtolower( (string) $entry['title'] );
7200 +
7201 + if ( ! $front_page_id
7202 + && ( ! empty( $entry['is_front_page'] )
7203 + || false !== strpos( $title_lower, 'home' )
7204 + || false !== strpos( $title_lower, 'landing' ) )
7205 + ) {
7206 + $front_page_id = $entry['id'];
7207 + }
7208 +
7209 + if ( ! $shop_page_id
7210 + && ( ! empty( $entry['is_shop_page'] )
7211 + || false !== strpos( $title_lower, 'shop' )
7212 + || false !== strpos( $title_lower, 'store' ) )
7213 + ) {
7214 + $shop_page_id = $entry['id'];
7215 + }
7216 + }
7217 +
7218 + if ( $front_page_id ) {
7219 + update_option( 'show_on_front', 'page' );
7220 + update_option( 'page_on_front', $front_page_id );
7221 + }
7222 +
7223 + if ( $shop_page_id && function_exists( 'wc_get_page_id' ) ) {
7224 + update_option( 'woocommerce_shop_page_id', $shop_page_id );
7225 + }
7226 +
7227 + // ── 4. Primary menu ─────────────────────────────────────────────────────────────
7228 + $t_menu = microtime( true );
7229 + $menu_id = $this->wdkit_bundle_build_menu( $nav_menu, $imported, $template_map );
7230 + $kit_timing['menu_build'] = (int) round( ( microtime( true ) - $t_menu ) * 1000 );
7231 +
7232 + // ── 4.5. Remap demo navigation links to actual site URLs ────────────────────────
7233 + $url_map = self::wdkit_build_nav_url_map( $imported );
7234 +
7235 + if ( ! empty( $url_map ) ) {
7236 + foreach ( $imported as $entry ) {
7237 + $pid = (int) $entry['id'];
7238 + $post = get_post( $pid );
7239 + if ( ! $post ) {
7240 + continue;
7241 + }
7242 +
7243 + $orig_content = $post->post_content;
7244 + $new_content = strtr( $orig_content, $url_map );
7245 + if ( $new_content !== $orig_content ) {
7246 + // Same reason as the deferred pass: on multisite, and on any host that
7247 + // defines DISALLOW_UNFILTERED_HTML, an administrator does not hold
7248 + // `unfiltered_html` either, so this admin-driven write would also lose
7249 + // <input>/<iframe>/<form> from content the import just produced.
7250 + self::wdkit_write_post_content( $pid, $new_content );
7251 +
7252 + // wdkit_bundle_insert_item() already built this page's CSS file once, right
7253 + // after its own wp_insert_post() - against the pre-remap content, since this
7254 + // step runs afterward, once every page in the kit exists and every other
7255 + // page's real permalink is known. The block hashes that CSS was keyed to no
7256 + // longer line up with what post_content now says (Gutenberg's own class
7257 + // names embed a hash of the block's attributes), so without this the page
7258 + // renders with whatever styling the two versions still happen to share and
7259 + // silently drops the rest - the same class of bug the deferred-media cron
7260 + // callback was fixed for earlier.
7261 + self::wdkit_rebuild_block_css( $pid );
7262 + }
7263 +
7264 + $elem_data = get_post_meta( $pid, '_elementor_data', true );
7265 + if ( ! empty( $elem_data ) && is_string( $elem_data ) ) {
7266 + $new_elem = strtr( $elem_data, $url_map );
7267 + if ( $new_elem !== $elem_data ) {
7268 + // get_post_meta() hands back the unslashed JSON, and
7269 + // update_post_meta() unslashes again on the way in - so writing it
7270 + // straight back corrupts it. Same reason as the deferred cron's
7271 + // write above.
7272 + update_post_meta( $pid, '_elementor_data', wp_slash( $new_elem ) );
7273 + }
7274 + }
7275 + }
7276 + }
7277 +
7278 + // ── 5. Every widget the kit uses, enabled in one call ───────────────────────────
7279 + if ( ! empty( $widgets_to_enable ) && has_filter( 'tpae_enable_selected_widgets' ) ) {
7280 + apply_filters(
7281 + 'tpae_enable_selected_widgets',
7282 + array(
7283 + 'widgets' => array_values( $widgets_to_enable ),
7284 + 'extensions' => array(),
7285 + )
7286 + );
7287 + }
7288 +
7289 + // ── 6. Deferred image sideloading, off this request ─────────────────────────────
7290 + $scheduled = 0;
7291 + $stagger = 0;
7292 +
7293 + foreach ( $deferred_pages as $deferred ) {
7294 + // Stagger so every page's sideload loop does not fire in one wp-cron tick —
7295 + // see the identical stagger in wkit_schedule_deferred_media_sync().
7296 + wp_schedule_single_event(
7297 + time() + 2 + $stagger,
7298 + 'wdkit_async_sideload_page_images',
7299 + array( $deferred['post_id'], $deferred['builder'], $deferred['image_urls'] )
7300 + );
7301 +
7302 + $stagger += 2;
7303 + ++$scheduled;
7304 + }
7305 +
7306 + // ── 7. One cache clear for the kit, not one per page ────────────────────────────
7307 + $t_clear = microtime( true );
7308 + if ( did_action( 'elementor/loaded' ) ) {
7309 + \Elementor\Plugin::$instance->files_manager->clear_cache();
7310 + }
7311 +
7312 + if ( class_exists( 'Tpgb_Library' ) && method_exists( 'Tpgb_Library', 'remove_backend_dir_files' ) ) {
7313 + Tpgb_Library()->remove_backend_dir_files();
7314 + }
7315 + $kit_timing['cache_clear'] = (int) round( ( microtime( true ) - $t_clear ) * 1000 );
7316 +
7317 + do_action( 'wdkit_template_imported', 'kit', sanitize_key( $builder ), count( $imported ) );
7318 +
7319 + if ( isset( $bundle['cloud_assemble_ms'] ) ) {
7320 + $kit_timing['cloud_assemble'] = (int) $bundle['cloud_assemble_ms'];
7321 + }
7322 + $kit_timing['templates'] = count( $imported );
7323 + $kit_timing['request_total'] = (int) round( ( microtime( true ) - $t_request ) * 1000 );
7324 + $kit_timing['peak_memory_mb'] = round( memory_get_peak_usage( true ) / 1048576, 1 );
7325 +
7326 + // Also to the log when debugging, so a staging run leaves a trace even if nobody had
7327 + // the network tab open.
7328 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
7329 + error_log( 'WDKIT site bundle timing: ' . wp_json_encode( $kit_timing ) );
7330 + }
7331 +
7332 + return array(
7333 + 'message' => esc_html__( 'Successfully Imported.', 'wdesignkit' ),
7334 + 'description' => esc_html__( 'Yay! Your site has been successfully imported.', 'wdesignkit' ),
7335 + 'timing_ms' => $kit_timing,
7336 + 'imported' => $imported,
7337 + 'errors' => $errors,
7338 + 'front_page_id' => $front_page_id,
7339 + 'shop_page_id' => $shop_page_id,
7340 + 'menu_id' => $menu_id,
7341 + 'media_scheduled' => $scheduled,
7342 + 'site_link' => get_site_url(),
7343 + 'success' => true,
7344 + );
7345 + }
7346 +
7347 + /**
7348 + * Fetch the assembled site bundle for this kit from the cloud.
7349 + *
7350 + * Mirrors wkit_import_kit_bundle()'s argument handling: the token is resolved from the
7351 + * stored cloud session for $email, never taken from the request. Returns the decoded
7352 + * bundle, or a WP_Error the caller reports so the client can fall back to the legacy
7353 + * per-template path (an older or self-hosted backend has no v2 route at all).
7354 + *
7355 + * @since 2.7.0
7356 + *
7357 + * @param array|null $request Request fields to read instead of `$_POST`. The AJAX
7358 + * actions pass nothing and this behaves exactly as before;
7359 + * the PHP import runner passes its own array, because it
7360 + * runs on cron where there is no request to read. Same
7361 + * shape either way, except `template_ids` may already be an
7362 + * array rather than a JSON string.
7363 + * @param string $token Cloud token, for callers that already resolved one. Only
7364 + * ever supplied in PHP: a token is never read out of
7365 + * `$request`, so a browser request cannot present its own.
7366 + * @return array|WP_Error
7367 + */
7368 + private function wdkit_fetch_site_bundle( $request = null, $token = '' ) {
7369 +
7370 + // phpcs:ignore WordPress.Security.NonceVerification.Missing -- the AJAX router verified the nonce before dispatching; the runner supplies its own array.
7371 + $request = is_array( $request ) ? $request : $_POST;
7372 +
7373 + $raw_templates = array();
7374 +
7375 + if ( ! empty( $request['template_ids'] ) ) {
7376 + $raw_templates = is_array( $request['template_ids'] )
7377 + ? $request['template_ids']
7378 + : json_decode( wp_unslash( $request['template_ids'] ), true );
7379 + }
7380 +
7381 + if ( empty( $raw_templates ) || ! is_array( $raw_templates ) ) {
7382 + return new WP_Error( 'wdkit_bundle_no_templates', esc_html__( 'No templates supplied for the site bundle.', 'wdesignkit' ) );
7383 + }
7384 +
7385 + // Sanitize AFTER decoding — running sanitize_text_field() over the raw JSON strips
7386 + // `<` and `>` and corrupts any template whose title contains them.
7387 + $templates = array();
7388 + foreach ( $raw_templates as $raw ) {
7389 + if ( is_array( $raw ) ) {
7390 + $id = ! empty( $raw['id'] ) ? (int) $raw['id'] : 0;
7391 +
7392 + if ( ! $id ) {
7393 + continue;
7394 + }
7395 +
7396 + $templates[] = array(
7397 + 'id' => $id,
7398 + 'title' => isset( $raw['title'] ) ? sanitize_text_field( $raw['title'] ) : '',
7399 + 'wp_post_type' => isset( $raw['wp_post_type'] ) ? sanitize_key( $raw['wp_post_type'] ) : 'page',
7400 + '_editor' => isset( $raw['_editor'] ) ? sanitize_text_field( $raw['_editor'] ) : '',
7401 + );
7402 + } elseif ( (int) $raw ) {
7403 + $templates[] = array( 'id' => (int) $raw );
7404 + }
7405 + }
7406 +
7407 + if ( empty( $templates ) ) {
7408 + return new WP_Error( 'wdkit_bundle_no_templates', esc_html__( 'No usable templates supplied for the site bundle.', 'wdesignkit' ) );
7409 + }
7410 +
7411 + $email = ! empty( $request['email'] ) ? strtolower( sanitize_email( wp_unslash( $request['email'] ) ) ) : '';
7412 +
7413 + $args = array(
7414 + 'token' => ( is_string( $token ) && '' !== $token ) ? $token : $this->wdkit_login_user_token( $email ),
7415 + 'template_ids' => $templates,
7416 + 'website_kit' => isset( $request['website_kit'] ) ? sanitize_text_field( wp_unslash( $request['website_kit'] ) ) : '',
7417 + 'editor' => isset( $request['editor'] ) ? sanitize_text_field( wp_unslash( $request['editor'] ) ) : '',
7418 + 'builder' => isset( $request['builder'] ) ? sanitize_text_field( wp_unslash( $request['builder'] ) ) : '',
7419 + // get_option() returns false when unset, which `?? ''` does not catch.
7420 + 'unique_id' => get_option( 'wdkit_unique_id', '' ),
7421 + );
7422 +
7423 + if ( function_exists( 'wdkit_kit_import_with_site_identity' ) ) {
7424 + $args = wdkit_kit_import_with_site_identity( $args );
7425 + }
7426 +
7427 + // Assembling a whole kit reads every template's file server-side, so this needs more
7428 + // than get_data()'s 60s default.
7429 + $t_cloud = microtime( true );
7430 +
7431 + // The non-AI path asks for this bundle twice in a row - once to learn which widgets
7432 + // the kit needs (so they can be enabled in their own request, because a widget
7433 + // enabled mid-request is not registered until the next one), then again to import
7434 + // it. Assembling it twice would pay the whole cloud round trip twice, so the first
7435 + // call parks it here for the second. Keyed on the request that produced it, minus
7436 + // the credentials, so a different kit or a different template set never reads this.
7437 + $cache_key = 'wdkit_bundle_' . md5(
7438 + (string) wp_json_encode(
7439 + array_diff_key( $args, array( 'token' => '', 'poll_token' => '', 'site_url' => '' ) )
7440 + )
7441 + );
7442 + $cached = get_transient( $cache_key );
7443 +
7444 + if ( is_array( $cached ) && ! empty( $cached['success'] ) ) {
7445 + $cached['cloud_assemble_ms'] = 0;
7446 + $cached['from_cache'] = true;
7447 + $cached['cache_key'] = $cache_key;
7448 +
7449 + return $cached;
7450 + }
7451 +
7452 + $response = WDesignKit_Data_Query::get_data( 'v2/generate_kit_site_bundle', $args, array(), 180 );
7453 + $cloud_ms = (int) round( ( microtime( true ) - $t_cloud ) * 1000 );
7454 +
7455 + // The cloud assembly is its own round trip - credit checks, disk reads and tracking
7456 + // for every template in the kit - so it gets its own number rather than hiding inside
7457 + // whatever the caller measures.
7458 + if ( is_array( $response ) ) {
7459 + $response['cloud_assemble_ms'] = $cloud_ms;
7460 + }
7461 +
7462 + // Thirty minutes, not ten.
7463 + //
7464 + // Ten was sized for the browser's two-call sequence, where the gap is seconds. The
7465 + // runner reads this copy on every stage slice instead, so the window it has to
7466 + // survive is the WHOLE import — and an import is not always the ~1 minute it takes on
7467 + // a healthy host. A slow host, a retry, or a person who stops to answer the door
7468 + // pushes it past ten minutes, and then the cache silently expires mid-run: the next
7469 + // slice re-assembles the entire kit in the cloud, which is the one round trip the
7470 + // bundle exists to avoid, and it does it again on the slice after that.
7471 + //
7472 + // Still short enough that a kit edited upstream is not served stale for long, and
7473 + // wkit_import_site_bundle() deletes it as soon as it has consumed it, so the ceiling
7474 + // is only ever reached by a run that never finished.
7475 + // Only the widgets_only call parks a copy. The AI path fetches the whole bundle and
7476 + // posts the personalized content back, so it never re-reads this — caching there
7477 + // would write a megabyte per import that nothing ever looks at.
7478 + // The runner asks for the same parking, for the same reason: a stage request that
7479 + // runs out of time hands the rest of the kit to the next one, and every one of
7480 + // those must read this copy rather than re-assembling the kit in the cloud.
7481 + if ( is_array( $response ) && ! empty( $response['success'] ) && ! empty( $request['widgets_only'] ) ) {
7482 + set_transient( $cache_key, $response, 30 * MINUTE_IN_SECONDS );
7483 +
7484 + $response['cache_key'] = $cache_key;
7485 + }
7486 +
7487 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
7488 + error_log( 'WDKIT bundle cloud assemble: ' . $cloud_ms . 'ms' );
7489 + }
7490 +
7491 + if ( is_wp_error( $response ) ) {
7492 + return $response;
7493 + }
7494 +
7495 + if ( empty( $response ) || ! is_array( $response ) || empty( $response['success'] ) ) {
7496 + // Diagnostic: the client silently falls back to the slower per-template path when
7497 + // this fails, so without this the only symptom is "the import behaved differently
7498 + // this time". Log what the cloud actually said.
7499 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
7500 + error_log(
7501 + 'WDKIT bundle assemble FAILED: type=' . gettype( $response )
7502 + . ' keys=' . ( is_array( $response ) ? implode( ',', array_keys( $response ) ) : '-' )
7503 + . ' body=' . substr( (string) wp_json_encode( $response ), 0, 700 )
7504 + );
7505 + }
7506 +
7507 + return new WP_Error(
7508 + 'wdkit_bundle_failed',
7509 + ! empty( $response['message'] )
7510 + ? $response['message']
7511 + : esc_html__( 'Could not assemble the site bundle.', 'wdesignkit' )
7512 + );
7513 + }
7514 +
7515 + return $response;
7516 + }
7517 +
7518 + /**
7519 + * Insert one bundle template — page or section.
7520 + *
7521 + * A port of import_page_section_content()'s two editor branches with the AJAX plumbing
7522 + * stripped out: same Elementor document creation, same media import, same block
7523 + * processing, same custom-meta restore. It returns a result array instead of calling
7524 + * wp_send_json(), so the caller can record one bad template and carry on with the kit.
7525 + *
7526 + * @since 2.7.0
7527 + *
7528 + * @param array $item One entry from the bundle's pages/sections list.
7529 + * @param string $builder Kit-level builder, used when the item names no editor.
7530 + * @param bool $defer_media Sideload this template's images later, off this request.
7531 + * @param bool $custom_meta Restore the template's nxt-* post meta.
7532 + * @return array Result array; `success` false carries a `message`.
7533 + */
7534 + /**
7535 + * The widget and extension names an assembled bundle needs enabled.
7536 + *
7537 + * The server-side twin of the browser's collect_widget_list(): same extension key map, and
7538 + * the same union across every template in the kit. It exists because the non-AI path never
7539 + * routes template content through the browser, so nothing there can walk it.
7540 + *
7541 + * Extensions are matched on the settings key being present rather than truthy. Over-
7542 + * collecting is harmless — the addon ignores any name outside its own catalogue, and an
7543 + * extension enabled but unused costs nothing — whereas under-collecting silently loses the
7544 + * styling it drives.
7545 + *
7546 + * @since 2.7.0
7547 + *
7548 + * @param array $bundle Assembled bundle.
7549 + * @return array{widgets: array, extensions: array}
7550 + */
7551 + private static function wdkit_bundle_widget_manifest( array $bundle ) {
7552 + $extension_keys = array(
7553 + 'sc_link_switch' => 'plus_section_column_link',
7554 + 'scwbf_options' => 'plus_glass_morphism',
7555 + 'seh_switch' => 'plus_equal_height',
7556 + );
7557 +
7558 + $widgets = array();
7559 + $extensions = array();
7560 +
7561 + foreach ( array( 'sections', 'pages' ) as $group ) {
7562 + $items = ( ! empty( $bundle[ $group ] ) && is_array( $bundle[ $group ] ) ) ? $bundle[ $group ] : array();
7563 +
7564 + foreach ( $items as $item ) {
7565 + if ( ! is_array( $item ) || ! isset( $item['content'] ) ) {
7566 + continue;
7567 + }
7568 +
7569 + $json = is_string( $item['content'] ) ? $item['content'] : (string) wp_json_encode( $item['content'] );
7570 +
7571 + $widgets = array_merge( $widgets, self::wdkit_widget_types_in( $json ) );
7572 +
7573 + foreach ( $extension_keys as $key => $extension ) {
7574 + if ( false !== strpos( $json, '"' . $key . '"' ) ) {
7575 + $extensions[] = $extension;
7576 + }
7577 + }
7578 + }
7579 + }
7580 +
7581 + return array(
7582 + 'widgets' => array_values( array_unique( $widgets ) ),
7583 + 'extensions' => array_values( array_unique( $extensions ) ),
7584 + );
7585 + }
7586 +
7587 + /**
7588 + * Which of $types the addon has not actually recorded as enabled.
7589 + *
7590 + * This deliberately reads the persisted enable list rather than the live widget registry,
7591 + * because within the enable request itself the registry is always stale: TPAE registers on
7592 + * `elementor/widgets/register` (priority 100) from whatever `check_elements` held at that
7593 + * moment, so a widget enabled later in the same request cannot appear until the NEXT one.
7594 + * Measured against the real endpoint: first call reports the just-enabled widgets missing,
7595 + * an identical second call reports none. Verifying against the registry here would
7596 + * therefore fail every healthy import and trigger a pointless retry every time; the
7597 + * persisted list is what the next request will build the registry from, so it is the
7598 + * honest answer to "did the enable take".
7599 + *
7600 + * The registry check still matters — it just belongs in the request that saves, which is a
7601 + * later request. See wdkit_unregistered_widget_types().
7602 + *
7603 + * @since 2.7.0
7604 + *
7605 + * @param array $types Widget type slugs as the client sends them (`tp-post-title`).
7606 + * @return array The subset not recorded as enabled, in the same slug form.
7607 + */
7608 + private static function wdkit_not_enabled_widget_types( array $types ) {
7609 + $types = array_values( array_unique( array_filter( array_map( 'strval', $types ) ) ) );
7610 +
7611 + if ( empty( $types ) ) {
7612 + return array();
7613 + }
7614 +
7615 + $options = get_option( 'theplus_options' );
7616 + $enabled = ( is_array( $options ) && ! empty( $options['check_elements'] ) && is_array( $options['check_elements'] ) )
7617 + ? $options['check_elements']
7618 + : null;
7619 +
7620 + // Unverifiable — the addon's settings row does not exist yet, which is itself a state
7621 + // where the enable cannot have worked (tpae_enable_selected_widgets() writes nothing
7622 + // when that option is empty, and still reports success). Report everything as not
7623 + // enabled so the caller retries rather than trusting a success it cannot confirm.
7624 + if ( null === $enabled ) {
7625 + return $types;
7626 + }
7627 +
7628 + // The caller sends every widget a template uses, core Elementor ones included, and the
7629 + // addon quietly ignores anything outside its own catalogue — so `heading`, `image` and
7630 + // `divider` are never going to appear in its enabled list no matter how many times we
7631 + // ask. Scoping to the catalogue is what keeps this from reporting a permanent failure
7632 + // on every healthy import, which would make the retry fire every time.
7633 + $managed = null;
7634 +
7635 + // has_filter() first: it is only true once the addon has constructed its hooks
7636 + // singleton, so get_instance() below is guaranteed to hand back the existing object
7637 + // rather than build one — asking a question must not register another plugin's hooks.
7638 + if ( has_filter( 'tpae_enable_selected_widgets' )
7639 + && class_exists( 'Tpae_Hooks' )
7640 + && method_exists( 'Tpae_Hooks', 'get_instance' ) ) {
7641 + $hooks = Tpae_Hooks::get_instance();
7642 +
7643 + if ( isset( $hooks->all_widgets ) && is_array( $hooks->all_widgets ) ) {
7644 + $managed = $hooks->all_widgets;
7645 + }
7646 + }
7647 +
7648 + // No catalogue to scope against. The settings row exists, so the enable had somewhere
7649 + // to write; treat that as verified rather than inventing a failure we cannot support.
7650 + // The save-time guard still catches anything that really did not register.
7651 + if ( null === $managed ) {
7652 + return array();
7653 + }
7654 +
7655 + $missing = array();
7656 +
7657 + foreach ( $types as $type ) {
7658 + // Same normalisation the addon applies before storing (`tp-post-title` -> `tp_post_title`).
7659 + $slug = str_replace( '-', '_', $type );
7660 +
7661 + if ( in_array( $slug, $managed, true ) && ! in_array( $slug, $enabled, true ) ) {
7662 + $missing[] = $type;
7663 + }
7664 + }
7665 +
7666 + return $missing;
7667 + }
7668 +
7669 + /**
7670 + * Every widgetType named anywhere in a payload, whatever shape it arrives in.
7671 + *
7672 + * Scans the serialized form rather than walking the tree so it cannot drift from the
7673 + * nesting rules Elementor actually uses (containers, inner sections, nested repeaters).
7674 + *
7675 + * @since 2.7.0
7676 + *
7677 + * @param mixed $payload Array, object or JSON string.
7678 + * @return array Unique widget type slugs.
7679 + */
7680 + /**
7681 + * Drop the redundant "Page" from an imported page's title.
7682 + *
7683 + * Catalog titles are written for browsing a list of kits ("About Us Page | Taj Bakery"),
7684 + * where the word does useful work. On the site itself every entry in Pages is a page, so it
7685 + * is noise on every row - and it rides along into the fallback slug too.
7686 + *
7687 + * Only stripped when it is a standalone word sitting immediately before the kit separator
7688 + * or at the end of the title, so a kit or page whose name legitimately contains the word
7689 + * ("Page Builder | …", "Landing Pages Bundle") keeps it. Pages only: a theme-builder
7690 + * template called "Blog Single Page" is describing what it is, not repeating itself, and
7691 + * those live in a different list entirely.
7692 + *
7693 + * @since 2.7.1
7694 + *
7695 + * @param string $title Catalog title.
7696 + * @param string $post_type Destination post type.
7697 + * @return string Title to save.
7698 + */
7699 + private static function wdkit_clean_page_title( $title, $post_type ) {
7700 +
7701 + if ( 'page' !== $post_type || '' === (string) $title ) {
7702 + return $title;
7703 + }
7704 +
7705 + $clean = preg_replace( '/\s*\bPages?\b(?=\s*(?:\||$))/i', '', (string) $title );
7706 + $clean = trim( preg_replace( '/\s+/', ' ', (string) $clean ) );
7707 +
7708 + // A title that was only ever the word itself ("Page", "Page | Kit") would come back
7709 + // empty or orphaned from its separator; leave those exactly as the catalog wrote them.
7710 + if ( '' === $clean || 0 === strpos( $clean, '|' ) ) {
7711 + return $title;
7712 + }
7713 +
7714 + /* Drop the kit's name: "About Us | Zion Technology" -> "About Us".
7715 + *
7716 + * Catalog titles are "<page> | <kit>", and the kit half is the demo brand. Keeping it
7717 + * named every imported page after someone else's business — it showed in the browser
7718 + * tab and in the theme's page heading ("About Us | Zion Technology" on a freight
7719 + * company's site). Both other ways a page can be created already cut here: the
7720 + * browser's import_new_temp() does `title.split("|")[0]`, and
7721 + * Wdkit_Page_Importer::store() does `explode( '|', … )[0]`. This path — the one-request
7722 + * bundle insert, which is the one the runner actually uses — was the only one that
7723 + * did not. */
7724 + $parts = explode( '|', $clean );
7725 + $name = trim( $parts[0] );
7726 +
7727 + return '' !== $name ? $name : $clean;
7728 + }
7729 +
7730 + /**
7731 + * Point a header's logo at the home page when the template shipped it unlinked.
7732 + *
7733 + * Kits are inconsistent here: some ship the logo image with `link_to: custom` and the
7734 + * authoring site's home URL (which the nav remap then rewrites to this site's), and some
7735 + * ship it with no link control at all. Elementor's image widget defaults to `link_to: none`,
7736 + * so the second kind renders a bare <img> and the logo is not clickable - which is not a
7737 + * choice any of these templates meant to make.
7738 + *
7739 + * Deliberately narrow, because "which image is the logo" is a guess everywhere else:
7740 + *
7741 + * - headers only, identified by the theme-builder section meta rather than the title, which
7742 + * is translated and kit-specific;
7743 + * - the core `image` widget only;
7744 + * - and only when the widget has no link at all. A template that set one - to home or
7745 + * anywhere else - is left exactly as its author wrote it.
7746 + *
7747 + * @since 2.7.1
7748 + *
7749 + * @param mixed $elements Elementor `elements` data.
7750 + * @param mixed $custom_meta The template's custom_meta (meta_key => array(value)).
7751 + * @return mixed Elements, with the logo linked where it was not.
7752 + */
7753 + private static function wdkit_link_header_logo( $elements, $custom_meta ) {
7754 +
7755 + $meta = is_object( $custom_meta ) ? get_object_vars( $custom_meta ) : $custom_meta;
7756 +
7757 + if ( ! is_array( $meta ) || empty( $meta['nxt-hooks-layout-sections'] ) ) {
7758 + return $elements;
7759 + }
7760 +
7761 + $section = $meta['nxt-hooks-layout-sections'];
7762 + $section = is_array( $section ) ? reset( $section ) : $section;
7763 +
7764 + if ( 'header' !== $section ) {
7765 + return $elements;
7766 + }
7767 +
7768 + $home = home_url( '/' );
7769 +
7770 + $walk = function ( $node ) use ( &$walk, $home ) {
7771 + if ( ! is_array( $node ) ) {
7772 + return $node;
7773 + }
7774 +
7775 + if ( isset( $node['elType'], $node['widgetType'] )
7776 + && 'widget' === $node['elType'] && 'image' === $node['widgetType'] ) {
7777 +
7778 + $settings = isset( $node['settings'] ) && is_array( $node['settings'] ) ? $node['settings'] : array();
7779 + $link_to = isset( $settings['link_to'] ) ? (string) $settings['link_to'] : '';
7780 + $link_url = isset( $settings['link']['url'] ) ? (string) $settings['link']['url'] : '';
7781 +
7782 + if ( '' === $link_url && ( '' === $link_to || 'none' === $link_to ) ) {
7783 + $settings['link_to'] = 'custom';
7784 + $settings['link'] = array(
7785 + 'url' => $home,
7786 + 'is_external' => '',
7787 + 'nofollow' => '',
7788 + 'custom_attributes' => '',
7789 + );
7790 +
7791 + $node['settings'] = $settings;
7792 + }
7793 + }
7794 +
7795 + if ( ! empty( $node['elements'] ) && is_array( $node['elements'] ) ) {
7796 + foreach ( $node['elements'] as $i => $child ) {
7797 + $node['elements'][ $i ] = $walk( $child );
7798 + }
7799 + }
7800 +
7801 + return $node;
7802 + };
7803 +
7804 + if ( ! is_array( $elements ) ) {
7805 + return $elements;
7806 + }
7807 +
7808 + foreach ( $elements as $i => $node ) {
7809 + $elements[ $i ] = $walk( $node );
7810 + }
7811 +
7812 + return $elements;
7813 + }
7814 +
7815 + private static function wdkit_widget_types_in( $payload ) {
7816 + $json = is_string( $payload ) ? $payload : (string) wp_json_encode( $payload );
7817 +
7818 + if ( ! is_string( $json ) || ! preg_match_all( '/"widgetType"\s*:\s*"([^"]+)"/', $json, $matches ) ) {
7819 + return array();
7820 + }
7821 +
7822 + return array_values( array_unique( $matches[1] ) );
7823 + }
7824 +
7825 + /**
7826 + * Which of $types Elementor cannot resolve right now.
7827 + *
7828 + * This is the check that makes an unregistered widget visible. Document::save() walks
7829 + * every node through Elements_Manager::create_element_instance(), which returns null for a
7830 + * widgetType the widgets manager does not know, and get_elements_raw_data() then
7831 + * `continue`s past it — the container survives, the widget is dropped, and the save still
7832 + * reports success. TPAE documents the same behaviour from the other side (see
7833 + * theplus-include-widgets.php: "without a registered type Elementor deletes their saved
7834 + * nodes on the next save"). So an unregistered type is silent data loss, not an error, and
7835 + * nothing downstream will ever tell us it happened.
7836 + *
7837 + * @since 2.7.0
7838 + *
7839 + * @param array $types Widget type slugs.
7840 + * @return array The subset that has no registered type.
7841 + */
7842 + private static function wdkit_unregistered_widget_types( array $types ) {
7843 + $types = array_values( array_unique( array_filter( array_map( 'strval', $types ) ) ) );
7844 +
7845 + if ( empty( $types ) ) {
7846 + return array();
7847 + }
7848 +
7849 + // Nothing can be resolved without the widgets manager, so everything is missing —
7850 + // which is the honest answer, and the one that stops a stripped save going out quietly.
7851 + if ( ! did_action( 'elementor/loaded' ) || ! isset( \Elementor\Plugin::$instance->widgets_manager ) ) {
7852 + return $types;
7853 + }
7854 +
7855 + $known = \Elementor\Plugin::$instance->widgets_manager->get_widget_types();
7856 + $known = is_array( $known ) ? array_keys( $known ) : array();
7857 +
7858 + return array_values( array_diff( $types, $known ) );
7859 + }
7860 +
7861 + /**
7862 + * Make sure the media importer class is loaded before it is used.
7863 + *
7864 + * The class was only required deep inside the two media walks, so any call reached before
7865 + * one of those ran hit a fatal - "Class \"Wdkit_Import_Images\" not found". The per-item
7866 + * deferred-list clear at the top of wdkit_bundle_insert_item() is exactly such a call: it
7867 + * runs before any walk, by design, so that per-item isolation does not depend on which
7868 + * branch happens to execute.
7869 + *
7870 + * @since 2.7.1
7871 + *
7872 + * @return void
7873 + */
7874 + private static function wdkit_require_import_images() {
7875 + if ( ! class_exists( 'Wdkit_Import_Images' ) ) {
7876 + require_once WDKIT_INCLUDES . 'admin/class-wdkit-import-images.php';
7877 + }
7878 + }
7879 +
7880 + /**
7881 + * The four placeholder shapes a wireframe import swaps images for.
7882 + *
7883 + * Same files the browser uses, so a wireframe site looks identical whichever route
7884 + * built it.
7885 + *
7886 + * @since 2.7.1
7887 + *
7888 + * @return array<string,string> ratio-name => absolute url.
7889 + */
7890 + private static function wdkit_wireframe_placeholders() {
7891 +
7892 + $base = WDKIT_SERVER_API_URL . 'images/v2/plugin/template/';
7893 +
7894 + return array(
7895 + 'hero' => $base . 'hero-placeholder.png',
7896 + 'long' => $base . 'long-placeholder.png',
7897 + 'square' => $base . 'square-placeholder.png',
7898 + 'vertical' => $base . 'vertical-placeholder.png',
7899 + );
7900 + }
7901 +
7902 + /**
7903 + * Swap a bundle item's images for wireframe placeholders, server-side.
7904 + *
7905 + * "Import as Wireframe" was only ever honoured on routes that hand the template content
7906 + * to the browser - the AI path, and the legacy per-template path. A dummy import takes
7907 + * the one-request route instead (see import_site_bundle() in import_loader.js), where the
7908 + * server fetches and builds the whole kit and the content never reaches the browser at
7909 + * all, so the client-side swap could not run and the toggle silently did nothing.
7910 + *
7911 + * It looked intermittent because the fast route only wins when it can: on a site whose
7912 + * plugins were just installed it bails and the legacy path takes over, which DOES swap.
7913 + * So wireframe worked on a fresh site and stopped working on a warm one - reproduced by
7914 + * importing a kit with AI first, then re-importing it as dummy with wireframe on.
7915 + *
7916 + * Shape is chosen from the WordPress size suffix when the URL carries one
7917 + * (`-700x700` -> square); everything else gets the hero 4:3, matching what the browser
7918 + * falls back to when it cannot measure a source image. SVGs are deliberately untouched:
7919 + * the browser's swap skips them too, so icons and logos survive on both routes.
7920 + *
7921 + * @since 2.7.1
7922 + *
7923 + * @param array $item Bundle item, whose `content` holds the template payload.
7924 + * @return array The item with its raster image URLs pointed at placeholders.
7925 + */
7926 + private static function wdkit_wireframe_bundle_item( $item ) {
7927 +
7928 + if ( empty( $item['content'] ) ) {
7929 + return $item;
7930 + }
7931 +
7932 + $is_string = is_string( $item['content'] );
7933 + $raw = $is_string ? $item['content'] : wp_json_encode( $item['content'] );
7934 +
7935 + if ( ! is_string( $raw ) || '' === $raw ) {
7936 + return $item;
7937 + }
7938 +
7939 + $shapes = self::wdkit_wireframe_placeholders();
7940 +
7941 + // Both spellings, because a url sits raw in markup and slash-escaped inside the
7942 + // builder's JSON - the same pairing wdkit_replace_url_all_encodings() handles.
7943 + $flat = str_replace( '\\/', '/', $raw );
7944 +
7945 + if ( ! preg_match_all( '#https?://[^"\'\s<>()\\\\]+\.(?:jpe?g|png|gif|webp|avif)#i', $flat, $matches ) ) {
7946 + return $item;
7947 + }
7948 +
7949 + $map = array();
7950 +
7951 + foreach ( array_unique( $matches[0] ) as $url ) {
7952 + // Already a placeholder (a re-import of an already-wireframed kit) - leave it.
7953 + if ( preg_match( '/(hero|long|square|vertical)-placeholder/', $url ) ) {
7954 + continue;
7955 + }
7956 +
7957 + $shape = 'hero';
7958 +
7959 + if ( preg_match( '/-(\d+)x(\d+)\.[A-Za-z0-9]+$/', $url, $dim ) ) {
7960 + $w = (int) $dim[1];
7961 + $h = (int) $dim[2];
7962 +
7963 + if ( $w > 0 && $h > 0 ) {
7964 + $ratio = $w / $h;
7965 +
7966 + if ( $ratio >= 1.6 ) {
7967 + $shape = 'long';
7968 + } elseif ( $ratio <= 0.75 ) {
7969 + $shape = 'vertical';
7970 + } elseif ( $ratio >= 0.9 && $ratio <= 1.1 ) {
7971 + $shape = 'square';
7972 + }
7973 + }
7974 + }
7975 +
7976 + $map[ $url ] = $shapes[ $shape ];
7977 + }
7978 +
7979 + if ( empty( $map ) ) {
7980 + return $item;
7981 + }
7982 +
7983 + foreach ( $map as $from => $to ) {
7984 + $raw = str_replace(
7985 + array( $from, str_replace( '/', '\\/', $from ) ),
7986 + array( $to, str_replace( '/', '\\/', $to ) ),
7987 + $raw
7988 + );
7989 + }
7990 +
7991 + $item['content'] = $is_string ? $raw : json_decode( $raw, true );
7992 +
7993 + return $item;
7994 + }
7995 +
7996 + private function wdkit_bundle_insert_item( $item, $builder, $defer_media, $custom_meta ) {
7997 +
7998 + // Start every item with an empty deferred list, unconditionally.
7999 + //
8000 + // Wdkit_Import_Images::$deferred_urls is static, and only the two `if ( $defer_media )`
8001 + // unions below drain it. Anything recorded on a path that does not reach one of them
8002 + // stays in the array and is handed to the NEXT item in the batch, which then schedules
8003 + // a cron event that rewrites a different page's media. Video makes this reachable in
8004 + // normal use: it defers regardless of $defer_media (see wdkit_Import_media()), so a
8005 + // non-deferred item carrying one leaves a URL behind. Clearing here means per-item
8006 + // isolation does not depend on which branch happens to run.
8007 + self::wdkit_require_import_images();
8008 + Wdkit_Import_Images::get_and_clear_deferred_urls();
8009 +
8010 + $raw_content = isset( $item['content'] ) ? $item['content'] : '';
8011 +
8012 + // The cloud ships each template's file byte-for-byte as the per-template route does,
8013 + // so this decodes to the same object import_page_section_content() works with.
8014 + $post_content = is_string( $raw_content )
8015 + ? json_decode( $raw_content )
8016 + : json_decode( wp_json_encode( $raw_content ) );
8017 +
8018 + if ( ! is_object( $post_content ) ) {
8019 + return array(
8020 + 'success' => false,
8021 + 'message' => esc_html__( 'Template content could not be read.', 'wdesignkit' ),
8022 + );
8023 + }
8024 +
8025 + $editor = ! empty( $item['editor'] ) ? sanitize_text_field( $item['editor'] ) : $builder;
8026 + $file_type = ! empty( $post_content->file_type ) ? sanitize_text_field( $post_content->file_type ) : '';
8027 +
8028 + // import_page_section_content() requires file_type to name the editor and falls
8029 + // through to "Something went wrong" when it does not. Silently dropping a page is
8030 + // worse in a whole-site import, so fall back to the editor the bundle resolved for
8031 + // this template rather than bailing.
8032 + if ( '' === $file_type ) {
8033 + $file_type = ( 'gutenberg' === $editor ) ? 'wp_block' : 'elementor';
8034 + }
8035 +
8036 + $editor = ( 'wp_block' === $file_type ) ? 'gutenberg' : 'elementor';
8037 +
8038 + $post_title = isset( $post_content->title ) ? sanitize_text_field( $post_content->title ) : '';
8039 + if ( '' === $post_title ) {
8040 + $post_title = isset( $item['title'] ) ? sanitize_text_field( (string) $item['title'] ) : '';
8041 + }
8042 +
8043 + // Before the slug is derived below, so the fallback slug loses the word too.
8044 + $post_title = self::wdkit_clean_page_title(
8045 + $post_title,
8046 + ! empty( $item['wp_post_type'] ) ? sanitize_key( $item['wp_post_type'] ) : 'page'
8047 + );
8048 +
8049 + $post_slug = isset( $post_content->slug ) ? sanitize_title( $post_content->slug ) : '';
8050 +
8051 + // Falling straight back to the title gives every page a slug like
8052 + // "about-us-page-zion-technology" - the catalog's own display title, kit name and
8053 + // all, since that title is written for browsing a list of kits, not for a URL. The
8054 + // page's own path on the demo site (already fetched for the nav-link remap above) is
8055 + // a real slug someone chose for this exact page, so prefer that - except for the
8056 + // pages whose whole demo path IS the kit's folder (the homepage, and section-only
8057 + // entries like Header/Footer with no page of their own), where the segment left after
8058 + // stripping the kit folder is empty and there is nothing better to use than the title.
8059 + if ( '' === $post_slug && ! empty( $item['post_url'] ) ) {
8060 + $path_segments = array_values( array_filter( explode( '/', (string) wp_parse_url( (string) $item['post_url'], PHP_URL_PATH ) ) ) );
8061 +
8062 + if ( count( $path_segments ) > 1 ) {
8063 + $post_slug = sanitize_title( end( $path_segments ) );
8064 + }
8065 + }
8066 +
8067 + if ( '' === $post_slug ) {
8068 + $post_slug = sanitize_title( $post_title );
8069 + }
8070 +
8071 + $content = isset( $post_content->content ) ? wp_slash( $post_content->content ) : '';
8072 +
8073 + if ( empty( $content ) ) {
8074 + return array(
8075 + 'success' => false,
8076 + 'message' => esc_html__( 'Content is Empty.', 'wdesignkit' ),
8077 + );
8078 + }
8079 +
8080 + // Whitelist the destination post type against the plugin's own list instead of
8081 + // trusting whatever the payload names.
8082 + $enqueue_instance = new Wdkit_Enqueue();
8083 + $allowed_types = $enqueue_instance->wdkit_get_post_type_list();
8084 + $post_type = ! empty( $item['wp_post_type'] ) ? sanitize_key( $item['wp_post_type'] ) : 'page';
8085 +
8086 + if ( ! array_key_exists( $post_type, $allowed_types ) ) {
8087 + $post_type = 'page';
8088 + }
8089 +
8090 + $widget_list = ! empty( $post_content->widget_list )
8091 + ? json_decode( wp_json_encode( $post_content->widget_list ), true )
8092 + : array();
8093 +
8094 + $image_urls = ( ! empty( $item['image_urls'] ) && is_array( $item['image_urls'] ) )
8095 + ? array_values( array_filter( array_map( 'esc_url_raw', $item['image_urls'] ) ) )
8096 + : array();
8097 +
8098 + // Same gate as the pages route - this half of the bundle payload is client-supplied too.
8099 + $image_urls = self::wdkit_filter_fetchable_urls( $image_urls );
8100 +
8101 + // Per-template millisecond costs, returned to the client so a staging run can be read
8102 + // straight off the network tab instead of from a log file.
8103 + $timing = array();
8104 + $t_item = microtime( true );
8105 + $inserted_id = 0;
8106 +
8107 + // Only the Elementor branch can lose widgets this way; Gutenberg blocks are stored
8108 + // as serialized markup and are not resolved through a type registry on save.
8109 + $unregistered = array();
8110 +
8111 + if ( 'gutenberg' === $editor ) {
8112 +
8113 + $blocks = parse_blocks( stripslashes( $content ) );
8114 +
8115 + $t_media = microtime( true );
8116 + $blocks = $this->wdkit_media_import( $blocks, 'gutenberg', $defer_media );
8117 + $timing['media_import'] = (int) round( ( microtime( true ) - $t_media ) * 1000 );
8118 +
8119 + // The cloud's image_urls (from the per-template site_images DB field) is not a
8120 + // reliable manifest - most templates never have it populated, so the background
8121 + // sideload pass would otherwise never get scheduled and these images would stay
8122 + // pointed at the source CDN forever. wdkit_Import_media() already recorded every
8123 + // URL it deferred while walking $blocks above; union that in here instead of
8124 + // depending on the cloud field alone.
8125 + if ( $defer_media ) {
8126 + $discovered_urls = array_map( 'esc_url_raw', Wdkit_Import_Images::get_and_clear_deferred_urls() );
8127 + $image_urls = array_values( array_unique( array_merge( $image_urls, array_filter( $discovered_urls ) ) ) );
8128 + }
8129 +
8130 + if ( class_exists( 'WDKIT_Nexter_Block_Processor' ) ) {
8131 + $t_proc = microtime( true );
8132 + $processor = new WDKIT_Nexter_Block_Processor();
8133 + $blocks = $processor->run( $blocks );
8134 + $timing['block_processor'] = (int) round( ( microtime( true ) - $t_proc ) * 1000 );
8135 + }
8136 +
8137 + $content = $this->replace_unicode_glitch( serialize_blocks( $blocks ) );
8138 +
8139 + $t_insert = microtime( true );
8140 + $inserted_id = wp_insert_post(
8141 + array(
8142 + 'post_status' => 'publish',
8143 + 'post_type' => $post_type,
8144 + 'post_title' => $post_title,
8145 + 'post_name' => $post_slug,
8146 + 'post_content' => $content,
8147 + )
8148 + );
8149 +
8150 + $timing['post_insert'] = (int) round( ( microtime( true ) - $t_insert ) * 1000 );
8151 +
8152 + if ( is_wp_error( $inserted_id ) ) {
8153 + return array(
8154 + 'success' => false,
8155 + 'message' => $inserted_id->get_error_message(),
8156 + );
8157 + }
8158 +
8159 + $t_css = microtime( true );
8160 + self::wdkit_rebuild_block_css( $inserted_id );
8161 + $timing['block_css'] = (int) round( ( microtime( true ) - $t_css ) * 1000 );
8162 +
1467 8163 } else {
1468 - $output[ $template_ids['id'] ] = $this->import_page_section_content( $args, $template_ids['id'], $response, $template_ids );
1469 - $output['message'] = $response['message'];
1470 - $output['description'] = $response['description'];
1471 - $output['success'] = $response['success'];
8164 +
8165 + if ( ! did_action( 'elementor/loaded' ) ) {
8166 + return array(
8167 + 'success' => false,
8168 + 'message' => esc_html__( 'Relevant Page Builder not installed or activated', 'wdesignkit' ),
8169 + );
8170 + }
8171 +
8172 + $content = $this->wdkit_content_remover( $content );
8173 +
8174 + $post_attributes = array(
8175 + 'post_title' => $post_title,
8176 + 'post_type' => $post_type,
8177 + 'post_status' => 'publish',
8178 + 'post_name' => $post_slug,
8179 + );
8180 +
8181 + // documents->create() is what makes this a real Elementor document — it owns
8182 + // _elementor_data, _elementor_edit_mode, _elementor_version and the template
8183 + // type. Writing that meta by hand is exactly what leaves a page opening as raw
8184 + // JSON in the Text Editor, so the canonical path never does.
8185 + $t_create = microtime( true );
8186 + if ( 'elementor_library' === $post_type ) {
8187 + $el_type = ! empty( $post_content->el_type ) ? sanitize_text_field( $post_content->el_type ) : 'page';
8188 + $new_document = \Elementor\Plugin::$instance->documents->create( $el_type, $post_attributes );
8189 + } else {
8190 + $new_document = \Elementor\Plugin::$instance->documents->create( $post_type, $post_attributes );
8191 + }
8192 + $timing['document_create'] = (int) round( ( microtime( true ) - $t_create ) * 1000 );
8193 +
8194 + if ( is_wp_error( $new_document ) ) {
8195 + return array(
8196 + 'success' => false,
8197 + 'message' => $new_document->get_error_message(),
8198 + );
8199 + }
8200 +
8201 + $settings = ! empty( $post_content->settings )
8202 + ? json_decode( wp_json_encode( $post_content->settings ), true )
8203 + : array();
8204 +
8205 + $elements = wp_json_encode( $content );
8206 +
8207 + $t_media = microtime( true );
8208 + $elements = $this->wdkit_media_import( $elements, 'elementor', $defer_media );
8209 + $timing['media_import'] = (int) round( ( microtime( true ) - $t_media ) * 1000 );
8210 +
8211 + // Same union the Gutenberg branch above does, and for the same reason: the cloud's
8212 + // image_urls is a manifest of the template's *images*, so anything the walk itself
8213 + // discovered - a video especially, which nothing else lists - is only ever going to
8214 + // reach the background pass if it is added here. Without this, wdkit_media_import()
8215 + // recorded those URLs and the schedule then ignored them, leaving the reference
8216 + // pointing at the authoring site for good.
8217 + if ( $defer_media ) {
8218 + $discovered_urls = array_map( 'esc_url_raw', Wdkit_Import_Images::get_and_clear_deferred_urls() );
8219 + $image_urls = array_values( array_unique( array_merge( $image_urls, array_filter( $discovered_urls ) ) ) );
8220 + }
8221 +
8222 + // A header whose logo the template left unlinked gets one to the home page, before
8223 + // the save rather than as a second write.
8224 + $elements = self::wdkit_link_header_logo( $elements, isset( $post_content->custom_meta ) ? $post_content->custom_meta : array() );
8225 +
8226 + // Read the registry as late as possible — right before the save that consumes it.
8227 + // Anything named here is about to be dropped on the floor by Document::save()
8228 + // (see wdkit_unregistered_widget_types()), and this is the only moment the
8229 + // information still exists: afterwards the row simply looks like a page that was
8230 + // authored without those widgets.
8231 + $unregistered = self::wdkit_unregistered_widget_types( self::wdkit_widget_types_in( $elements ) );
8232 +
8233 + if ( ! empty( $unregistered ) ) {
8234 + self::get_instance()->wdkit_enable_widgets_data( $unregistered );
8235 + $unregistered = self::wdkit_unregistered_widget_types( self::wdkit_widget_types_in( $elements ) );
8236 + }
8237 +
8238 + $t_save = microtime( true );
8239 + $new_document->save(
8240 + array(
8241 + 'elements' => $elements,
8242 + 'settings' => ! empty( $settings ) ? $settings : array(),
8243 + )
8244 + );
8245 + $timing['document_save'] = (int) round( ( microtime( true ) - $t_save ) * 1000 );
8246 +
8247 + $inserted_id = $new_document->get_main_id();
1472 8248 }
1473 8249
1474 - wp_send_json( $output );
8250 + if ( empty( $inserted_id ) ) {
8251 + return array(
8252 + 'success' => false,
8253 + 'message' => esc_html__( 'Something went wrong', 'wdesignkit' ),
8254 + );
8255 + }
8256 +
8257 + // nxt-* meta: theme-builder display conditions and similar. allowed_classes => false
8258 + // because the value comes from the imported kit body, where a serialized object could
8259 + // fire a POP gadget chain in any loaded plugin or theme (CWE-502) — see the identical
8260 + // restore in import_page_section_content().
8261 + if ( $custom_meta && ! empty( $post_content->custom_meta ) ) {
8262 + $meta_list = json_decode( wp_json_encode( $post_content->custom_meta ), true );
8263 +
8264 + if ( is_array( $meta_list ) ) {
8265 + foreach ( $meta_list as $meta_key => $meta_val ) {
8266 + if ( ! isset( $meta_val[0] ) ) {
8267 + continue;
8268 + }
8269 +
8270 + $value = $meta_val[0];
8271 +
8272 + if ( is_string( $value ) && is_serialized( $value ) ) {
8273 + $value = unserialize( $value, array( 'allowed_classes' => false ) );
8274 + }
8275 +
8276 + if ( '' === get_post_meta( $inserted_id, $meta_key, true ) ) {
8277 + add_post_meta( $inserted_id, $meta_key, $value );
8278 + }
8279 + }
8280 + }
8281 + }
8282 +
8283 + clean_post_cache( $inserted_id );
8284 +
8285 + // Diagnostic: how many widgets arrived for this template versus how many actually
8286 + // landed in the row. A template that comes in with content and stores none is a
8287 + // silent data loss - the page still imports, reports success, and renders empty.
8288 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
8289 + $in_widgets = preg_match_all( '/"widgetType":/', (string) wp_json_encode( $post_content ) );
8290 + $stored_raw = get_post_meta( $inserted_id, '_elementor_data', true );
8291 + $stored_widgets = preg_match_all( '/"widgetType":/', is_string( $stored_raw ) ? $stored_raw : (string) wp_json_encode( $stored_raw ) );
8292 +
8293 + error_log(
8294 + sprintf(
8295 + 'WDKIT insert: cloud=%s post=%d editor=%s title=%s widgets_in=%d widgets_stored=%d%s',
8296 + isset( $item['id'] ) ? $item['id'] : '?',
8297 + (int) $inserted_id,
8298 + $editor,
8299 + $post_title,
8300 + (int) $in_widgets,
8301 + (int) $stored_widgets,
8302 + ( $in_widgets > 0 && 0 === (int) $stored_widgets ) ? ' <-- LOST' : ''
8303 + )
8304 + );
8305 + }
8306 +
8307 + $timing['custom_meta'] = isset( $timing['custom_meta'] ) ? $timing['custom_meta'] : 0;
8308 + $timing['item_total'] = (int) round( ( microtime( true ) - $t_item ) * 1000 );
8309 +
8310 + // Deliberately not behind WP_DEBUG. The page imported, reported success and renders —
8311 + // it is simply missing widgets nobody asked it to drop, so a log line the site owner
8312 + // never enabled is the difference between a caught regression and a silent one.
8313 + if ( ! empty( $unregistered ) ) {
8314 + error_log(
8315 + sprintf(
8316 + 'WDKIT: %s (post %d) was saved without %d widget type(s) Elementor could not resolve: %s',
8317 + $post_title,
8318 + (int) $inserted_id,
8319 + count( $unregistered ),
8320 + implode( ', ', $unregistered )
8321 + )
8322 + );
8323 + }
8324 +
8325 + return array(
8326 + 'success' => true,
8327 + 'timing_ms' => $timing,
8328 + // Empty on every healthy import. Non-empty means this template was stored with
8329 + // content missing — wkit_import_site_bundle() turns it into a reported error.
8330 + 'unregistered' => ! empty( $unregistered ) ? $unregistered : array(),
8331 + 'id' => (int) $inserted_id,
8332 + // The template's id on the source site. wdkit_nxt_thembuilder_update() needs it to
8333 + // remap nxt_builder display conditions onto the ids this import just created.
8334 + 'old_page_id' => isset( $post_content->page_id ) ? $post_content->page_id : '',
8335 + 'title' => get_the_title( $inserted_id ),
8336 + 'edit_link' => get_edit_post_link( $inserted_id, 'internal' ),
8337 + 'view' => get_permalink( $inserted_id ),
8338 + 'editor' => $editor,
8339 + 'post_type' => $post_type,
8340 + 'widget_list' => is_array( $widget_list ) ? $widget_list : array(),
8341 + 'image_urls' => $image_urls,
8342 + 'post_url' => isset( $item['post_url'] ) ? sanitize_text_field( (string) $item['post_url'] ) : '',
8343 + 'is_front_page' => ! empty( $item['is_front_page'] ),
8344 + 'is_shop_page' => ! empty( $item['is_shop_page'] ),
8345 + );
8346 + }
8347 +
8348 + /**
8349 + * Create — or extend — the primary menu from the bundle's pre-ordered nav list.
8350 + *
8351 + * Two things here that a naive rebuild gets wrong on a site that is not empty: pages
8352 + * already in the menu are skipped, so re-importing a kit does not stack duplicate items;
8353 + * and only theme menu locations the site has left EMPTY get filled, so an existing
8354 + * menu assignment is never replaced by the imported one.
8355 + *
8356 + * @since 2.7.0
8357 + *
8358 + * @param array $nav_menu Bundle nav entries: template_id, title, position.
8359 + * @param array $imported Results from wdkit_bundle_insert_item().
8360 + * @param array $template_map Cloud template id => inserted post id.
8361 + * @return int Menu term id, or 0 when no menu was touched.
8362 + */
8363 + private function wdkit_bundle_build_menu( $nav_menu, $imported, $template_map ) {
8364 +
8365 + // Older bundles (and any kit whose pages are all untitled) carry no nav list — fall
8366 + // back to the pages this request created, in insertion order.
8367 + if ( empty( $nav_menu ) ) {
8368 + foreach ( $imported as $entry ) {
8369 + if ( 'page' === $entry['group'] && 'page' === $entry['post_type'] ) {
8370 + $nav_menu[] = array(
8371 + 'post_id' => $entry['id'],
8372 + 'title' => $entry['title'],
8373 + );
8374 + }
8375 + }
8376 + }
8377 +
8378 + if ( empty( $nav_menu ) ) {
8379 + return 0;
8380 + }
8381 +
8382 + // Deliberately untranslated: this string is the menu's identity across re-imports,
8383 + // so it must not change with the admin locale.
8384 + $menu_name = 'Primary Menu';
8385 + $menu_obj = wp_get_nav_menu_object( $menu_name );
8386 + $menu_id = $menu_obj ? (int) $menu_obj->term_id : wp_create_nav_menu( $menu_name );
8387 +
8388 + if ( is_wp_error( $menu_id ) || empty( $menu_id ) ) {
8389 + return 0;
8390 + }
8391 +
8392 + $existing = wp_get_nav_menu_items( $menu_id, array( 'post_status' => 'any' ) );
8393 + $existing = is_array( $existing ) ? $existing : array();
8394 +
8395 + $already = array();
8396 + foreach ( $existing as $menu_item ) {
8397 + if ( ! empty( $menu_item->object_id ) ) {
8398 + $already[ (int) $menu_item->object_id ] = true;
8399 + }
8400 + }
8401 +
8402 + $position = count( $existing );
8403 +
8404 + foreach ( $nav_menu as $nav_item ) {
8405 + $post_id = 0;
8406 +
8407 + if ( ! empty( $nav_item['post_id'] ) ) {
8408 + $post_id = (int) $nav_item['post_id'];
8409 + } elseif ( ! empty( $nav_item['template_id'] ) && isset( $template_map[ (string) $nav_item['template_id'] ] ) ) {
8410 + $post_id = (int) $template_map[ (string) $nav_item['template_id'] ];
8411 + }
8412 +
8413 + if ( ! $post_id || isset( $already[ $post_id ] ) ) {
8414 + continue;
8415 + }
8416 +
8417 + ++$position;
8418 +
8419 + wp_update_nav_menu_item(
8420 + $menu_id,
8421 + 0,
8422 + array(
8423 + 'menu-item-title' => ! empty( $nav_item['title'] )
8424 + ? sanitize_text_field( (string) $nav_item['title'] )
8425 + : get_the_title( $post_id ),
8426 + 'menu-item-object-id' => $post_id,
8427 + 'menu-item-object' => get_post_type( $post_id ),
8428 + 'menu-item-type' => 'post_type',
8429 + 'menu-item-status' => 'publish',
8430 + 'menu-item-position' => $position,
8431 + )
8432 + );
8433 +
8434 + $already[ $post_id ] = true;
8435 + }
8436 +
8437 + // Only ever fill a location the active theme actually registers and the site has
8438 + // left empty. The old hardcoded primary/main-menu/header-menu write clobbered live
8439 + // menu assignments on any site that already had them.
8440 + $registered = function_exists( 'get_registered_nav_menus' ) ? get_registered_nav_menus() : array();
8441 + $locations = get_theme_mod( 'nav_menu_locations', array() );
8442 + $locations = is_array( $locations ) ? $locations : array();
8443 + $changed = false;
8444 +
8445 + foreach ( array_keys( $registered ) as $location ) {
8446 + if ( empty( $locations[ $location ] ) ) {
8447 + $locations[ $location ] = $menu_id;
8448 + $changed = true;
8449 + }
8450 + }
8451 +
8452 + if ( $changed ) {
8453 + set_theme_mod( 'nav_menu_locations', $locations );
8454 + }
8455 +
8456 + return (int) $menu_id;
8457 + }
8458 +
8459 + public function wdkit_enable_template_widgets() {
8460 + $widget_list = ! empty( $_POST['widget_list'] ) ? json_decode( wp_unslash( $_POST['widget_list'] ), true ) : array();
8461 + $extensions_list = ! empty( $_POST['extensions_list'] ) ? json_decode( wp_unslash( $_POST['extensions_list'] ), true ) : array();
8462 +
8463 + $res = $this->wdkit_enable_widgets_data( $widget_list, $extensions_list );
8464 +
8465 + wp_send_json( $res );
1475 8466 wp_die();
1476 8467 }
1477 8468
1478 8469 /**
8470 + * Enable the widgets and extensions a kit's content actually uses.
8471 + *
8472 + * Extracted from wdkit_enable_template_widgets() so the PHP import runner can reach it
8473 + * without `$_POST`. The lists are derived from imported kit content by the caller — they
8474 + * are never accepted from a remote payload.
8475 + *
8476 + * The AJAX action above is now a thin adapter over this and its response is unchanged.
8477 + *
8478 + * @since 2.6.5
8479 + *
8480 + * @param array $widget_list Widget names used by the kit.
8481 + * @param array $extensions_list Extension names used by the kit.
8482 + * @return array Response array, exactly as the AJAX action used to emit.
8483 + */
8484 + public function wdkit_enable_widgets_data( $widget_list = array(), $extensions_list = array() ) {
8485 + $widget_list = is_array( $widget_list ) ? $widget_list : array();
8486 + $extensions_list = is_array( $extensions_list ) ? $extensions_list : array();
8487 +
8488 + if ( empty( $widget_list ) && empty( $extensions_list ) ) {
8489 + return array(
8490 + 'massage' => __( 'Widget array not found', 'wdesignkit' ),
8491 + 'description' => __( 'Widget array not found', 'wdesignkit' ),
8492 + 'success' => false,
8493 + );
8494 + }
8495 +
8496 + // The widget list is what the caller needs verified; the extensions ride along but
8497 + // have no registry to check against.
8498 + $requested = is_array( $widget_list ) ? $widget_list : array();
8499 +
8500 + if ( ! has_filter( 'tpae_enable_selected_widgets' ) ) {
8501 + if ( class_exists( 'Tpae_Hooks' ) && method_exists( 'Tpae_Hooks', 'get_instance' ) ) {
8502 + Tpae_Hooks::get_instance();
8503 + } elseif ( defined( 'L_THEPLUS_PATH' ) && file_exists( L_THEPLUS_PATH . 'includes/admin/tpae_hooks/class-tpae-hooks.php' ) ) {
8504 + require_once L_THEPLUS_PATH . 'includes/admin/tpae_hooks/class-tpae-hooks.php';
8505 + } elseif ( defined( 'THEPLUS_PATH' ) && file_exists( THEPLUS_PATH . 'includes/admin/tpae_hooks/class-tpae-hooks.php' ) ) {
8506 + require_once THEPLUS_PATH . 'includes/admin/tpae_hooks/class-tpae-hooks.php';
8507 + }
8508 + }
8509 +
8510 + // Ensure theplus_options exists so tpae_enable_selected_widgets does not bail silently
8511 + // on a fresh install where the option row has not been created yet.
8512 + $theplus_options = get_option( 'theplus_options', false );
8513 + if ( empty( $theplus_options ) || ! is_array( $theplus_options ) ) {
8514 + $theplus_options = array(
8515 + 'check_elements' => array(),
8516 + 'extras_elements' => array(),
8517 + );
8518 + update_option( 'theplus_options', $theplus_options );
8519 + }
8520 +
8521 + if ( has_filter( 'tpae_enable_selected_widgets' ) ) {
8522 + $w_list = array(
8523 + 'widgets' => $widget_list,
8524 + 'extensions' => $extensions_list,
8525 + );
8526 +
8527 + $result = apply_filters( 'tpae_enable_selected_widgets', $w_list );
8528 + } else {
8529 + // Fallback: If filter is not loaded in this process (e.g. right after activation or CLI),
8530 + // directly merge into theplus_options so subsequent requests and Elementor boot find them.
8531 + $to_add = array();
8532 + foreach ( $widget_list as $w ) {
8533 + $to_add[] = str_replace( '-', '_', $w );
8534 + }
8535 + $ext_to_add = array();
8536 + foreach ( $extensions_list as $e ) {
8537 + $ext_to_add[] = str_replace( '-', '_', $e );
8538 + }
8539 +
8540 + $theplus_options['check_elements'] = array_values( array_unique( array_merge( isset( $theplus_options['check_elements'] ) && is_array( $theplus_options['check_elements'] ) ? $theplus_options['check_elements'] : array(), $to_add ) ) );
8541 + $theplus_options['extras_elements'] = array_values( array_unique( array_merge( isset( $theplus_options['extras_elements'] ) && is_array( $theplus_options['extras_elements'] ) ? $theplus_options['extras_elements'] : array(), $ext_to_add ) ) );
8542 + update_option( 'theplus_options', $theplus_options );
8543 +
8544 + $result = array( 'success' => true );
8545 + }
8546 +
8547 + // Register newly enabled widgets into Elementor\'s in-memory manager so Document::save()
8548 + // in the current PHP execution does not silently drop them.
8549 + if ( did_action( 'elementor/loaded' ) && class_exists( '\Elementor\Plugin' ) && isset( \Elementor\Plugin::$instance->widgets_manager ) ) {
8550 + try {
8551 + $widgets_manager = \Elementor\Plugin::$instance->widgets_manager;
8552 + if ( class_exists( '\TheplusAddons\L_Theplus_Widgets_Include' ) && method_exists( '\TheplusAddons\L_Theplus_Widgets_Include', 'get_instance' ) ) {
8553 + \TheplusAddons\L_Theplus_Widgets_Include::get_instance()->add_widgets( $widgets_manager );
8554 + }
8555 + if ( class_exists( '\TheplusAddons\Theplus_Widgets_Include' ) && method_exists( '\TheplusAddons\Theplus_Widgets_Include', 'get_instance' ) ) {
8556 + \TheplusAddons\Theplus_Widgets_Include::get_instance()->add_widgets( $widgets_manager );
8557 + }
8558 + } catch ( \Throwable $e ) {
8559 + // Best-effort in-memory registration.
8560 + }
8561 + }
8562 +
8563 + // What the caller actually needs to know. A 200 with success:true only says the filter
8564 + // ran — tpae_enable_selected_widgets() reports success even when it wrote nothing
8565 + // (it bails silently if the addon's own settings row is still empty). So answer from
8566 + // the state it was supposed to write.
8567 + $not_enabled = self::wdkit_not_enabled_widget_types( $requested );
8568 +
8569 + if ( ! empty( $result['success'] ) ) {
8570 + $res = array(
8571 + 'massage' => __( 'Enabled widgets successfully', 'wdesignkit' ),
8572 + 'description' => __( 'Used widgets have been enabled successfully', 'wdesignkit' ),
8573 + 'success' => true,
8574 + );
8575 + } else {
8576 +
8577 + $message = isset( $result['message'] ) ? $result['message'] : __( 'Failed to enable widgets', 'wdesignkit' );
8578 + $description = isset( $result['description'] ) ? $result['description'] : __( 'Failed to enable widgets', 'wdesignkit' );
8579 +
8580 + $res = array(
8581 + 'massage' => $message,
8582 + 'description' => $description,
8583 + 'success' => false,
8584 + );
8585 + }
8586 +
8587 + // Authoritative result of the enable, and the only field the caller should retry on.
8588 + // Both callers need it: the browser importer retries on it, and the runner's
8589 + // enable_kit_widgets() decides from it whether the content stage may start.
8590 + $res['not_enabled'] = $not_enabled;
8591 +
8592 + return $res;
8593 + }
8594 +
8595 + /**
1479 8596 * Import single template and section from plugin only
1480 8597 * */
1481 8598 protected function wdkit_import_multi_template() {
1482 - $args = $this->wdkit_parse_args( $_POST );
8599 + $args = $this->wdkit_parse_args( $_POST );
8600 + $api_type = isset( $_POST['api_type'] ) ? sanitize_text_field( wp_unslash( $_POST['api_type'] ) ) : 'import_template';
1483 8601
1484 8602 if ( ! current_user_can( 'manage_options' ) ) {
1485 8603 return false;
1486 8604 }
@@ -1509,62 +8627,227 @@
1509 8627 $args['post_type'] = ! empty( $_POST['select'] ) ? sanitize_text_field( wp_unslash( $_POST['select'] ) ) : '';
1510 8628 }
1511 8629
1512 8630 $args['custom_meta'] = isset( $_POST['custom_meta'] ) ? sanitize_text_field( wp_unslash( $_POST['custom_meta'] ) ) : false;
8631 +
1513 8632 if ( ! empty( $args['template_ids'] ) && ! empty( $args['page_section'] ) ) {
1514 8633 $output = array();
1515 - if ( is_array( $args['template_ids'] ) ) {
1516 - foreach ( $args['template_ids'] as $key => $value ) {
1517 - if ( ! empty( $value['id'] ) ) {
1518 - $token = $this->wdkit_login_user_token( $args['email'] );
1519 - $temp_args = array(
1520 - 'token' => $token,
1521 - 'template_id' => $value['id'],
1522 - 'editor' => $args['editor'],
1523 - );
8634 + if ( ! empty( $args['template_ids']['id'] ) ) {
8635 + $token = $this->wdkit_login_user_token( $args['email'] );
1524 8636
1525 - $response = WDesignKit_Data_Query::get_data( 'import_template', $temp_args );
1526 -
1527 - if ( 'error' === $response['content'] ) {
1528 - wp_send_json( $response );
1529 - wp_die();
1530 - } else {
1531 - $output[ $value['id'] ] = $this->import_page_section_content( $args, $value['id'], $response, $value );
1532 - $output['message'] = $response['message'];
1533 - $output['description'] = $response['description'];
1534 - $output['success'] = $response['success'];
1535 - }
1536 - }
1537 - }
1538 - } else {
1539 - $token = $this->wdkit_login_user_token( $args['email'] );
1540 8637 $temp_args = array(
1541 8638 'token' => $token,
1542 - 'template_id' => $args['template_ids'],
8639 + 'template_id' => $args['template_ids']['id'],
1543 8640 'editor' => $args['editor'],
8641 + 'unique_id' => get_option( 'wdkit_unique_id' ) ?? '',
1544 8642 );
1545 8643
1546 - $response = WDesignKit_Data_Query::get_data( 'import_template', $temp_args );
8644 + if ( function_exists( 'wdkit_kit_import_with_site_identity' ) ) {
8645 + $temp_args = wdkit_kit_import_with_site_identity( $temp_args );
8646 + }
1547 8647
1548 - if ( 'error' === $response['content'] ) {
8648 + $response = WDesignKit_Data_Query::get_data( $api_type, $temp_args );
8649 +
8650 + if ( is_wp_error( $response ) ) {
8651 + wp_send_json( array(
8652 + 'success' => false,
8653 + 'message' => $response->get_error_message(),
8654 + ) );
8655 + wp_die();
8656 + }
8657 +
8658 + if ( isset( $response['content'] ) && 'error' === $response['content'] ) {
1549 8659 wp_send_json( $response );
1550 8660 wp_die();
8661 + }
8662 +
8663 + $result = array(
8664 + 'response' => $response,
8665 + 'args' => $args,
8666 + 'id' => $args['template_ids'],
8667 + 'value' => $args['template_ids'],
8668 + );
8669 +
8670 + $output['message'] = $response['message'];
8671 + $output['description'] = $response['description'];
8672 + $output['data'] = $result;
8673 + $output['success'] = true;
8674 +
8675 + }
8676 +
8677 + wp_send_json( $output );
8678 + wp_die();
8679 + }
8680 + }
8681 +
8682 + /**
8683 + * It is Use for remove selected category from content.
8684 + *
8685 + * @since 2.0.5
8686 + */
8687 + public function wdkit_content_remover( &$data ) {
8688 +
8689 + if ( is_array( $data ) ) {
8690 +
8691 + foreach ( $data as $key => &$value ) {
8692 + if ( $key === 'post_category' ) {
8693 + $data[ $key ] = array();
8694 + } else if ($key === 'include_products'){
8695 + $data[ $key ] = "";
1551 8696 } else {
1552 - $output[ $args['template_ids'] ] = $this->import_page_section_content( $args, $args['template_ids'], $response );
1553 - $output['message'] = $response['message'];
1554 - $output['description'] = $response['description'];
1555 - $output['success'] = $response['success'];
8697 + $this->wdkit_content_remover( $value );
1556 8698 }
1557 8699 }
8700 + } elseif ( is_object( $data ) ) {
1558 8701
1559 - $output['success'] = true;
8702 + foreach ( $data as $key => &$value ) {
8703 + if ( $key === 'post_category' ) {
8704 + $data->$key = array();
8705 + } else if ($key === 'include_products'){
8706 + $data->$key = "";
8707 + } else {
8708 + $this->wdkit_content_remover( $value );
8709 + }
8710 + }
8711 + }
1560 8712
1561 - wp_send_json( $output );
1562 - wp_die();
8713 + return $data;
8714 + }
8715 +
8716 + protected function wkit_update_elementor_template(){
8717 +
8718 + if ( isset( $_POST['data'] ) ) {
8719 + $content = ! empty( $_POST['data'] ) ? json_decode( wp_unslash( $_POST['data'] ), true ) : '';
1563 8720 }
8721 +
8722 + if ( isset( $_POST['template_id'] ) ) {
8723 + $template_id = ! empty( $_POST['template_id'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['template_id'] ), true ) ) : '';
8724 + }
8725 +
8726 + $document = \Elementor\Plugin::$instance->documents->get($template_id);
8727 +
8728 + // This saves content posted straight from the browser, which carries local image
8729 + // URLs but still the source template's attachment IDs. Without repairing them the
8730 + // save undoes what wdkit_media_import() fixed on create, and has_sizes controls —
8731 + // container background images especially — resolve to nothing and render empty.
8732 + $content = self::wdkit_repair_attachment_ids( $content );
8733 +
8734 + $unregistered = self::wdkit_unregistered_widget_types( self::wdkit_widget_types_in( $content ) );
8735 + if ( ! empty( $unregistered ) ) {
8736 + self::get_instance()->wdkit_enable_widgets_data( $unregistered );
8737 + }
8738 +
8739 + $document->save([
8740 + 'elements' => $content
8741 + ]);
1564 8742 }
1565 8743
1566 8744 /**
8745 + * Update the content of an already-created page.
8746 + *
8747 + * Used by the async ("Site Ready first") import path: pages are created up front with
8748 + * their un-rewritten template content, then this writes the AI-rewritten content into
8749 + * each page in the background. Elementor saves via the document API (same as
8750 + * wkit_update_elementor_template); Gutenberg writes post_content directly.
8751 + *
8752 + * @since 2.6.2
8753 + */
8754 + protected function wdkit_update_page_content() {
8755 + $post_id = isset( $_POST['post_id'] ) ? intval( $_POST['post_id'] ) : 0;
8756 + $builder = isset( $_POST['builder'] ) ? sanitize_text_field( wp_unslash( $_POST['builder'] ) ) : '';
8757 +
8758 + if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
8759 + return array(
8760 + 'success' => false,
8761 + 'message' => esc_html__( 'Invalid page or insufficient permission', 'wdesignkit' ),
8762 + );
8763 + }
8764 +
8765 + if ( 'gutenberg' === $builder ) {
8766 + // Do NOT run kses here: Gutenberg block delimiters are HTML comments
8767 + // (<!-- wp:... -->) which kses strips. Mirror the create path, which stores
8768 + // the block markup slashed and unfiltered (endpoint is manage_options-gated
8769 + // and the content is plugin-generated).
8770 + $content = isset( $_POST['content'] ) ? wp_unslash( $_POST['content'] ) : '';
8771 +
8772 + // This content comes straight from the browser and still carries the template
8773 + // site's media URLs and attachment IDs, so it has to go through the same pipeline
8774 + // the create path uses. Without this the save simply undid the import: the files
8775 + // were fetched, then overwritten by a copy still pointing at the source site.
8776 + //
8777 + // Re-running is cheap. Every URL already handled resolves from the source-hash
8778 + // lookup, and media that is already local resolves straight from its URL, so no
8779 + // image is fetched or stored twice.
8780 + $content = $this->wdkit_relink_gutenberg_content( $content );
8781 +
8782 + // See wdkit_write_post_content(): this endpoint replaces the whole post_content
8783 + // with markup the import itself produced, so kses here strips legitimate block
8784 + // markup rather than untrusted input. Kept inline instead of routed through that
8785 + // helper because this call needs wp_slash() and the WP_Error return.
8786 + $had_kses = false !== has_filter( 'content_save_pre', 'wp_filter_post_kses' );
8787 +
8788 + if ( $had_kses ) {
8789 + kses_remove_filters();
8790 + }
8791 +
8792 + $result = wp_update_post(
8793 + array(
8794 + 'ID' => $post_id,
8795 + 'post_content' => wp_slash( $content ),
8796 + ),
8797 + true
8798 + );
8799 +
8800 + if ( $had_kses ) {
8801 + kses_init_filters();
8802 + }
8803 +
8804 + if ( is_wp_error( $result ) ) {
8805 + return array(
8806 + 'success' => false,
8807 + 'message' => $result->get_error_message(),
8808 + );
8809 + }
8810 +
8811 + self::wdkit_rebuild_block_css( $post_id );
8812 + } else {
8813 + $elements = isset( $_POST['content'] ) ? json_decode( wp_unslash( $_POST['content'] ), true ) : array();
8814 +
8815 + if ( ! class_exists( '\\Elementor\\Plugin' ) ) {
8816 + return array(
8817 + 'success' => false,
8818 + 'message' => esc_html__( 'Elementor not available', 'wdesignkit' ),
8819 + );
8820 + }
8821 +
8822 + $document = \Elementor\Plugin::$instance->documents->get( $post_id );
8823 + if ( ! $document ) {
8824 + return array(
8825 + 'success' => false,
8826 + 'message' => esc_html__( 'Elementor document not found', 'wdesignkit' ),
8827 + );
8828 + }
8829 +
8830 + // Same as wkit_update_elementor_template(): browser-posted content keeps the
8831 + // source template's attachment IDs, so repair them or this save undoes the
8832 + // create-time fix and background images stop rendering.
8833 + $elements = self::wdkit_repair_attachment_ids( $elements );
8834 +
8835 + $unregistered = self::wdkit_unregistered_widget_types( self::wdkit_widget_types_in( $elements ) );
8836 + if ( ! empty( $unregistered ) ) {
8837 + self::get_instance()->wdkit_enable_widgets_data( $unregistered );
8838 + }
8839 +
8840 + $document->save( array( 'elements' => $elements ) );
8841 + }
8842 +
8843 + return array(
8844 + 'success' => true,
8845 + 'message' => esc_html__( 'Page content updated', 'wdesignkit' ),
8846 + );
8847 + }
8848 +
8849 + /**
1567 8850 * Import single template and section from plugin only
1568 8851 *
1569 8852 * @param array $args store data.
1570 8853 * @param array $template_id store data.
@@ -1570,16 +8853,71 @@
1570 8853 * @param array $template_id store data.
1571 8854 * @param array $data store data.
1572 8855 * @param array $temp_data store data.
1573 8856 * */
1574 - private function import_page_section_content( $args, $template_id, $data, $temp_data = array() ) {
8857 + protected function import_page_section_content() {
8858 +
8859 + // Elementor sideloads every image referenced by the page from inside this request.
8860 + // A single oversized source image decodes to more than the whole memory limit, so
8861 + // guard before any of that starts.
8862 + $this->wdkit_guard_oversized_images();
8863 +
8864 + // Sideloading images for image-heavy pages (wdkit_media_import → Imagick
8865 + // thumbnail generation per image) can exceed the default 30s execution
8866 + // limit and fatal the request mid-import. Give this single page import
8867 + // more headroom; harmless no-op where set_time_limit() is disabled.
8868 + if ( function_exists( 'set_time_limit' ) ) {
8869 + @set_time_limit( 120 );
8870 + }
8871 +
8872 + if ( isset( $_POST['args'] ) ) {
8873 + $args = ! empty( $_POST['args'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['args'] ) ), true ) : array();
8874 + }
8875 +
8876 + if ( isset( $_POST['temp_data'] ) ) {
8877 + $temp_data = ! empty( $_POST['temp_data'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['temp_data'] ) ), true ) : array();
8878 + }
8879 +
8880 + if ( isset( $_POST['category_list'] ) ) {
8881 + $category_list = ! empty( $_POST['category_list'] ) ? json_decode( wp_unslash( $_POST['category_list'] ), true ) : '';
8882 + }
8883 +
8884 + if ( isset( $_POST['tag_list'] ) ) {
8885 + $tag_list = ! empty( $_POST['tag_list'] ) ? json_decode( wp_unslash( $_POST['tag_list'] ), true ) : '';
8886 + }
8887 +
8888 + if ( isset( $_POST['thumb_image'] ) ) {
8889 + $thumb_image = ! empty( $_POST['thumb_image'] ) ? esc_url_raw( $_POST['thumb_image'] ) : '';
8890 + // Capped once here covers both download_url( $thumb_image ) calls below (Elementor
8891 + // and Gutenberg branches share this variable).
8892 + $thumb_image = Wdkit_Image_Guard::cap_pexels_source( $thumb_image );
8893 + }
8894 +
8895 + if ( isset( $_POST['template_id'] ) ) {
8896 + $template_id = ! empty( $_POST['template_id'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['template_id'] ), true ) ) : '';
8897 + }
8898 +
8899 + $temp_type = isset( $_POST['temp_type'] ) ? sanitize_text_field( wp_unslash( $_POST['temp_type'] ) ) : 'normal';
8900 +
8901 + // AI kit imports send this so the page saves with its source CDN image URLs still in
8902 + // place - the JS side then registers it for wkit_schedule_deferred_media_sync() once
8903 + // every page in the kit has been created, and the actual media_sideload_image() work
8904 + // happens later, off this request. See wdkit_media_import()'s $defer_media branch.
8905 + $defer_media = ! empty( $_POST['defer_media'] );
8906 +
8907 + if ( isset( $_POST['data'] ) ) {
8908 + $data = ! empty( $_POST['data'] ) ? json_decode( wp_unslash( $_POST['data'] ) ) : '';
8909 + }
8910 +
1575 8911 $enqueue_instance = new Wdkit_Enqueue();
1576 8912 $get_post_type = $enqueue_instance->wdkit_get_post_type_list();
1577 8913
1578 8914 $post_type = ! empty( $temp_data['type'] ) ? sanitize_text_field( wp_unslash( $temp_data['type'] ) ) : 'page';
1579 8915
1580 - if ( 'section' == $post_type ) {
8916 + if ( 'section' === $post_type ) {
1581 8917 $post_type = $temp_data['wp_post_type'];
8918 + } else {
8919 + $post_type = $temp_data['wp_post_type'];
1582 8920 }
1583 8921
1584 8922 if ( ! array_key_exists( $post_type, $get_post_type ) ) {
1585 8923 $post_type = 'page';
@@ -1584,13 +8922,33 @@
1584 8922 if ( ! array_key_exists( $post_type, $get_post_type ) ) {
1585 8923 $post_type = 'page';
1586 8924 }
1587 8925
1588 - if ( ! empty( $data ) && ! empty( $data['content'] ) && ! empty( $template_id ) && ! empty( $post_type ) && current_user_can( 'manage_options' ) ) {
1589 - $post_content = json_decode( $data['content'] );
8926 + if ( ! empty( $data ) && ! empty( $template_id ) && ! empty( $post_type ) && current_user_can( 'manage_options' ) ) {
8927 + $post_content = $data;
8928 + // Restore The Plus Addons' globals before the page is built, so the widgets'
8929 + // tp_global_preset references resolve as soon as it renders. Done here rather
8930 + // than in the save-template UI's confirmation dialog so that every import path
8931 + // - the library, the abilities, the theme builder - gets it.
8932 + if ( isset( $post_content->tp_globals ) && ! empty( $post_content->tp_globals ) ) {
8933 + $this->wdkit_merge_tp_globals(
8934 + json_decode( wp_json_encode( $post_content->tp_globals ), true ),
8935 + isset( $post_content->tp_global_refs )
8936 + ? json_decode( wp_json_encode( $post_content->tp_global_refs ), true )
8937 + : array()
8938 + );
8939 + }
8940 +
1590 8941 $post_title = isset( $post_content->title ) ? sanitize_text_field( $post_content->title ) : '';
8942 +
8943 + // Same title cleanup the bundle path applies, so a kit imported through either
8944 + // route lists identically in Pages.
8945 + $post_title = self::wdkit_clean_page_title( $post_title, $post_type );
8946 +
8947 + $post_slug = isset( $post_content->slug ) ? sanitize_text_field( $post_content->slug ) : '';
1591 8948 $file_type = isset( $post_content->file_type ) ? sanitize_text_field( $post_content->file_type ) : '';
1592 8949 $content = isset( $post_content->content ) ? wp_slash( $post_content->content ) : '';
8950 + $temp_con = '';
1593 8951
1594 8952 if ( 'gutenberg' === $args['editor'] || ( 'wdkit' === $args['editor'] && ! empty( $file_type ) && 'wp_block' === $file_type ) ) {
1595 8953 if ( empty( $content ) ) {
1596 8954 wp_send_json(
@@ -1595,28 +8953,49 @@
1595 8953 if ( empty( $content ) ) {
1596 8954 wp_send_json(
1597 8955 array(
1598 8956 'template_id' => $template_id,
1599 - 'message' => 'Content is Empty.',
8957 + 'message' => __( 'Content is Empty.', 'wdesignkit' ),
1600 8958 )
1601 8959 );
1602 8960 wp_die();
1603 8961 } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'wp_block' === $file_type ) {
1604 - $parse_blocks = parse_blocks( stripslashes( $content ) );
1605 8962
1606 8963 $editor = ( 'wdkit' === $args['editor'] ) ? 'gutenberg' : $args['editor'];
1607 - $content = $this->wdkit_media_import( $parse_blocks, $editor );
1608 - $content = addslashes( serialize_blocks( $content ) );
1609 8964
8965 + $blocks = parse_blocks( stripslashes( $content ) );
8966 + $blocks = $this->wdkit_media_import( $blocks, $editor, $defer_media );
8967 +
8968 + // wdkit_media_import() above just skipped every raster image in this section's
8969 + // own content (the "Blog Detail" dummy template's own decorative images, not a
8970 + // user-picked stock photo) — this is this method's only chance to learn which
8971 + // URLs those were, so the deferred-media response field below is built from it
8972 + // rather than left to the bundle-only image_urls the client already tracks.
8973 + $deferred_image_urls = $defer_media
8974 + ? array_map( 'esc_url_raw', Wdkit_Import_Images::get_and_clear_deferred_urls() )
8975 + : array();
8976 +
8977 + $processor = new WDKIT_Nexter_Block_Processor();
8978 + $blocks = $processor->run( $blocks );
8979 +
8980 + $content = serialize_blocks( $blocks );
8981 +
8982 + $content = $this->replace_unicode_glitch( serialize_blocks( $blocks ) );
8983 +
1610 8984 $inserted_post = wp_insert_post(
1611 8985 array(
1612 8986 'post_status' => 'publish',
1613 8987 'post_type' => $post_type,
1614 8988 'post_title' => $post_title,
8989 + 'post_name' => $post_slug,
1615 8990 'post_content' => $content,
1616 8991 )
1617 8992 );
1618 8993
8994 + if ( ! is_wp_error( $inserted_post ) && $inserted_post ) {
8995 + self::wdkit_rebuild_block_css( $inserted_post );
8996 + }
8997 +
1619 8998 if ( is_wp_error( $inserted_post ) ) {
1620 8999 wp_send_json(
1621 9000 array(
1622 9001 'import_failed' => $inserted_post->get_error_message(),
@@ -1625,14 +9004,57 @@
1625 9004 );
1626 9005 wp_die();
1627 9006 }
1628 9007
9008 + // A picked stock image is already in the media library by the time the posts
9009 + // are created, so thumb_image is usually a url on this site rather than a
9010 + // remote one. That can never go down the download path - the external-url
9011 + // guard rejects loopback addresses by design, which silently dropped the
9012 + // featured image - so recognise the existing attachment and point at it.
9013 + // Anything genuinely remote still takes the untouched branch below.
9014 + $local_thumb_id = ! empty( $thumb_image ) ? self::wdkit_local_attachment_from_url( $thumb_image ) : 0;
9015 +
9016 + if ( $local_thumb_id ) {
9017 + set_post_thumbnail( $inserted_post, $local_thumb_id );
9018 + } elseif ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) {
9019 + // $featured_image_url = esc_url_raw( $thumb_image );
9020 + $tmp = download_url( $thumb_image );
9021 + if ( is_wp_error( $tmp ) ) {
9022 + error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) );
9023 + } else {
9024 + $file_array = array(
9025 + 'name' => wp_basename( $thumb_image ),
9026 + 'tmp_name' => $tmp,
9027 + );
9028 +
9029 + $image_id = media_handle_sideload( $file_array, $inserted_post );
9030 +
9031 + if ( is_wp_error( $image_id ) ) {
9032 + error_log( 'Image sideload failed: ' . esc_html( $image_id->get_error_message() ) );
9033 + } else {
9034 + set_post_thumbnail( $inserted_post, $image_id );
9035 + }
9036 +
9037 + @unlink( $tmp );
9038 + }
9039 + }
9040 +
9041 + if ( ! empty( $category_list ) && is_array( $category_list ) ) {
9042 + $category_ids = array_map( 'intval', $category_list );
9043 + wp_set_post_terms( $inserted_post, $category_ids, 'category' );
9044 + }
9045 +
9046 + if ( ! empty( $tag_list ) && is_array( $tag_list ) ) {
9047 + $tag_ids = array_map( 'intval', $tag_list );
9048 + wp_set_post_terms( $inserted_post, $tag_ids, 'post_tag' );
9049 + }
9050 +
1629 9051 if ( ! empty( $args['custom_meta'] ) && 'true' == $args['custom_meta'] ) {
1630 9052 $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : '';
1631 9053 if ( ! empty( $custom_meta ) ) {
1632 9054 foreach ( $custom_meta as $meta_key => $meta_val ) {
1633 9055 if ( isset( $meta_val[0] ) && ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) {
1634 - $meta_val[0] = maybe_unserialize( $meta_val[0] );
9056 + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) );
1635 9057 }
1636 9058
1637 9059 if ( '' === get_post_meta( $inserted_post, $meta_key, true ) && isset( $meta_val[0] ) ) {
1638 9060 add_post_meta( $inserted_post, $meta_key, $meta_val[0] );
@@ -1640,13 +9062,53 @@
1640 9062 }
1641 9063 }
1642 9064 }
1643 9065
1644 - return array(
9066 + $temp_detail = array(
1645 9067 'title' => get_the_title( $inserted_post ),
1646 9068 'edit_link' => get_edit_post_link( $inserted_post, 'internal' ),
1647 9069 'view' => get_permalink( $inserted_post ),
9070 + 'id' => $inserted_post,
1648 9071 );
9072 +
9073 + if ( ! empty( $template_id->id ) ) {
9074 + $temp_id = $template_id->id;
9075 + } elseif ( ! empty( $template_id ) ) {
9076 + $temp_id = $template_id;
9077 + } else {
9078 + $temp_id = '';
9079 + }
9080 +
9081 + wp_update_post([
9082 + 'ID' => $inserted_post,
9083 + ]);
9084 +
9085 + if (class_exists('Tpgb_Library') && method_exists('Tpgb_Library', 'remove_backend_dir_files')) {
9086 + Tpgb_Library()->remove_backend_dir_files();
9087 + }
9088 +
9089 + clean_post_cache( $inserted_post );
9090 +
9091 + // This whole method imports exactly one section per call — unlike
9092 + // wdkit_import_template()/wdkit_import_kit_template(), it never fired this hook
9093 + // at all, so single-section imports (Header/Footer/CTA/etc., a primary import
9094 + // path per the Template Type sidebar) were invisible to tracking entirely.
9095 + do_action( 'wdkit_template_imported', 'single', sanitize_key( $editor ), 1 );
9096 +
9097 + wp_send_json(
9098 + array(
9099 + $temp_id => $temp_detail,
9100 + 'description' => 'Yay! Your Section has been Successfully Imported.',
9101 + 'message' => __( 'Successfully Imported.', 'wdesignkit' ),
9102 + 'inserted_id' => $inserted_post,
9103 + // Raster images this call itself deferred - the caller unions this into
9104 + // whatever image_urls its own template-fetch response already carries
9105 + // before scheduling the background sideload pass.
9106 + 'image_urls' => $deferred_image_urls,
9107 + 'success' => true,
9108 + )
9109 + );
9110 + wp_die();
1649 9111 }
1650 9112 } elseif ( 'elementor' === $args['editor'] || ( 'wdkit' === $args['editor'] && ! empty( $file_type ) && 'elementor' === $file_type ) ) {
1651 9113 if ( did_action( 'elementor/loaded' ) ) {
1652 9114 if ( empty( $content ) ) {
@@ -1652,17 +9114,21 @@
1652 9114 if ( empty( $content ) ) {
1653 9115 wp_send_json(
1654 9116 array(
1655 9117 'template_id' => $template_id,
1656 - 'message' => 'Content is Empty.',
9118 + 'message' => __( 'Content is Empty.', 'wdesignkit' ),
1657 9119 )
1658 9120 );
1659 9121 wp_die();
1660 9122 } elseif ( ! empty( $content ) && ! empty( $file_type ) && 'elementor' === $file_type ) {
9123 +
9124 + $content = $this->wdkit_content_remover( $content );
9125 +
1661 9126 $post_attributes = array(
1662 9127 'post_title' => $post_title,
1663 9128 'post_type' => $post_type,
1664 9129 'post_status' => 'publish',
9130 + 'post_name' => $post_slug,
1665 9131 );
1666 9132
1667 9133 if ( 'elementor_library' === $post_type ) {
1668 9134 $el_type = ( isset( $post_content->el_type ) && ! empty( $post_content->el_type ) ) ? sanitize_text_field( $post_content->el_type ) : 'page';
@@ -1686,13 +9152,58 @@
1686 9152 );
1687 9153 wp_die();
1688 9154 }
1689 9155
9156 + $inserted_id = $new_document->get_main_id();
9157 +
9158 + if ( ! empty( $thumb_image ) && wdesignkit_validate_external_url( $thumb_image ) ) {
9159 + // $featured_image_url = esc_url_raw( $thumb_image );
9160 + $tmp = download_url( $thumb_image );
9161 + if ( is_wp_error( $tmp ) ) {
9162 + error_log( 'Image download failed: ' . esc_html( $tmp->get_error_message() ) );
9163 + } else {
9164 + $file_array = array(
9165 + 'name' => wp_basename( $thumb_image ),
9166 + 'tmp_name' => $tmp,
9167 + );
9168 +
9169 + $image_id = media_handle_sideload( $file_array, $inserted_id );
9170 +
9171 + if ( is_wp_error( $image_id ) ) {
9172 + error_log( 'Image sideload failed: ' . esc_html( $image_id->get_error_message() ) );
9173 + } else {
9174 + set_post_thumbnail( $inserted_id, $image_id );
9175 + }
9176 +
9177 + @unlink( $tmp );
9178 + }
9179 + }
9180 +
9181 + if ( ! empty( $category_list ) && is_array( $category_list ) ) {
9182 + $category_ids = array_map( 'intval', $category_list );
9183 + wp_set_post_terms( $inserted_id, $category_ids, 'category' );
9184 + }
9185 +
9186 + if ( ! empty( $tag_list ) && is_array( $tag_list ) ) {
9187 + $tag_ids = array_map( 'intval', $tag_list );
9188 + wp_set_post_terms( $inserted_id, $tag_ids, 'post_tag' );
9189 + }
9190 +
1690 9191 $settings = ( isset( $post_content->settings ) && ! empty( $post_content->settings ) ) ? json_decode( wp_json_encode( $post_content->settings ), true ) : array();
1691 9192
1692 9193 $content = wp_json_encode( $content );
1693 - $content = $this->wdkit_media_import( $content, $file_type );
9194 + $content = $this->wdkit_media_import( $content, $file_type, $defer_media );
1694 9195
9196 + // Same header-logo guarantee the bundle path applies, so a kit imported
9197 + // through either route ends up with the same clickable logo.
9198 + $content = self::wdkit_link_header_logo( $content, isset( $post_content->custom_meta ) ? $post_content->custom_meta : array() );
9199 +
9200 + $widgets_in = self::wdkit_widget_types_in( $content );
9201 + $unregistered = self::wdkit_unregistered_widget_types( $widgets_in );
9202 + if ( ! empty( $unregistered ) ) {
9203 + $this->wdkit_enable_widgets_data( $unregistered );
9204 + }
9205 +
1695 9206 $new_document->save(
1696 9207 array(
1697 9208 'elements' => $content,
1698 9209 'settings' => ! empty( $settings ) ? $settings : array(),
@@ -1700,14 +9211,18 @@
1700 9211 );
1701 9212
1702 9213 $inserted_id = $new_document->get_main_id();
1703 9214
9215 + if( $temp_type == 'navigation' ){
9216 + $temp_con = $content;
9217 + }
9218 +
1704 9219 if ( ! empty( $args['custom_meta'] ) && 'true' == $args['custom_meta'] ) {
1705 9220 $custom_meta = isset( $post_content->custom_meta ) ? json_decode( wp_json_encode( $post_content->custom_meta ), true ) : '';
1706 9221 if ( ! empty( $custom_meta ) ) {
1707 9222 foreach ( $custom_meta as $meta_key => $meta_val ) {
1708 9223 if ( ! empty( $meta_val[0] ) && is_serialized( $meta_val[0] ) ) {
1709 - $meta_val[0] = maybe_unserialize( $meta_val[0] );
9224 + $meta_val[0] = unserialize( $meta_val[0], array( 'allowed_classes' => false ) );
1710 9225 }
1711 9226 if ( '' === get_post_meta( $inserted_id, $meta_key, true ) ) {
1712 9227 add_post_meta( $inserted_id, $meta_key, $meta_val[0] );
1713 9228 }
@@ -1714,13 +9229,45 @@
1714 9229 }
1715 9230 }
1716 9231 }
1717 9232
1718 - return array(
9233 + $temp_detail = array(
1719 9234 'title' => get_the_title( $inserted_id ),
1720 9235 'edit_link' => get_edit_post_link( $inserted_id, 'internal' ),
1721 9236 'view' => get_permalink( $inserted_id ),
9237 + 'id' => $inserted_id,
1722 9238 );
9239 +
9240 + if ( ! empty( $template_id->id ) ) {
9241 + $temp_id = $template_id->id;
9242 + } elseif ( ! empty( $template_id ) ) {
9243 + $temp_id = $template_id;
9244 + } else {
9245 + $temp_id = '';
9246 + }
9247 +
9248 + \Elementor\Plugin::$instance->files_manager->clear_cache();
9249 +
9250 + // See the matching note in the Gutenberg branch above — this method never
9251 + // fired the tracking hook for either editor.
9252 + do_action( 'wdkit_template_imported', 'single', 'elementor', 1 );
9253 +
9254 + wp_send_json(
9255 + array(
9256 + $temp_id => $temp_detail,
9257 + 'content' => $temp_con,
9258 + 'description' => 'Yay! Your Section has been Successfully Imported.',
9259 + 'message' => __( 'Successfully Imported.', 'wdesignkit' ),
9260 + 'inserted_id' => $inserted_id,
9261 + // Same contract as the Gutenberg branch: the media this call
9262 + // chose to defer, for the caller to schedule.
9263 + 'image_urls' => $defer_media
9264 + ? array_map( 'esc_url_raw', Wdkit_Import_Images::get_and_clear_deferred_urls() )
9265 + : array(),
9266 + 'success' => true,
9267 + )
9268 + );
9269 + wp_die();
1723 9270 }
1724 9271 } else {
1725 9272 wp_send_json(
1726 9273 array(
@@ -1731,11 +9278,803 @@
1731 9278 wp_die();
1732 9279 }
1733 9280 }
1734 9281 }
9282 +
9283 + wp_send_json(
9284 + array(
9285 + 'success' => false,
9286 + 'message' => esc_html__( 'Something went wrong', 'wdesignkit' ),
9287 + )
9288 + );
9289 + wp_die();
1735 9290 }
1736 9291
1737 9292 /**
9293 + * Replace unicode glitch
9294 + *
9295 + * @since 2.0.0
9296 + */
9297 + /**
9298 + * Widened from private to public in 2.6.5 so the PHP page importer can apply the exact
9299 + * same unicode normalisation the AJAX page import applies. Pure string transform, no
9300 + * state, no capability implications.
9301 + */
9302 + public function replace_unicode_glitch( $content ) {
9303 +
9304 + // Fix escaped unicode like \u003c → <
9305 + $content = preg_replace_callback(
9306 + '/\\\\u([0-9a-fA-F]{4})/',
9307 + function ( $match ) {
9308 + return html_entity_decode(
9309 + mb_convert_encoding(
9310 + pack('H*', $match[1]),
9311 + 'UTF-8',
9312 + 'UCS-2BE'
9313 + ),
9314 + ENT_QUOTES,
9315 + 'UTF-8'
9316 + );
9317 + },
9318 + $content
9319 + );
9320 +
9321 + return $content;
9322 + }
9323 +
9324 +
9325 + /**
9326 + * change plugins setting for import kit
9327 + *
9328 + * @since 2.0.0
9329 + */
9330 + protected function update_plugin_setting() {
9331 + $temp_id = isset( $_POST['plugin_type'] ) ? sanitize_text_field( $_POST['plugin_type'] ) : '';
9332 +
9333 + $response = $this->wdkit_apply_plugin_settings_data( $temp_id );
9334 +
9335 + wp_send_json( $response );
9336 + wp_die();
9337 + }
9338 +
9339 + /**
9340 + * Apply the plugin-side settings an imported kit needs.
9341 + *
9342 + * Extracted from update_plugin_setting() so the PHP import runner can reach it without
9343 + * `$_POST`. The option list is a closed set written by this method alone — there is no
9344 + * path here for a caller to name an option, which is the point.
9345 + *
9346 + * The AJAX action above is now a thin adapter over this and its response is unchanged.
9347 + *
9348 + * @since 2.6.5
9349 + *
9350 + * @param string $temp_id Plugin identifier; only 'elementor' is recognised.
9351 + * @return array Response array, exactly as the AJAX action used to emit.
9352 + */
9353 + public function wdkit_apply_plugin_settings_data( $temp_id ) {
9354 + $temp_id = is_string( $temp_id ) ? $temp_id : '';
9355 +
9356 + if ( $temp_id == 'elementor' ) {
9357 + $unfiltered_files = get_option( 'elementor_unfiltered_files_upload', false );
9358 + $load_fa4 = get_option( 'elementor_load_fa4_shim', false );
9359 + $Inline_font_icons = get_option( 'elementor_experiment-e_font_icon_svg', false );
9360 + $container = get_option( 'elementor_experiment-container', false );
9361 +
9362 + if ( isset( $unfiltered_files ) ) {
9363 + update_option( 'elementor_unfiltered_files_upload', 1 );
9364 + } else {
9365 + add_option( 'elementor_unfiltered_files_upload', 1 );
9366 + }
9367 +
9368 + if ( isset( $load_fa4 ) ) {
9369 + update_option( 'elementor_load_fa4_shim', 'yes' );
9370 + } else {
9371 + add_option( 'elementor_load_fa4_shim', 'yes' );
9372 + }
9373 +
9374 + if ( isset( $container ) ) {
9375 + update_option( 'elementor_experiment-container', 'active' );
9376 + } else {
9377 + add_option( 'elementor_experiment-container', 'active' );
9378 + }
9379 +
9380 + if ( isset( $Inline_font_icons ) ) {
9381 + update_option( 'elementor_experiment-e_font_icon_svg', 'inactive' );
9382 + } else {
9383 + add_option( 'elementor_experiment-e_font_icon_svg', 'inactive' );
9384 + }
9385 +
9386 + $response = array(
9387 + 'message' => esc_html__( 'Plugin Setting updated', 'wdesignkit' ),
9388 + 'description' => esc_html__( 'Plugin Setting updated', 'wdesignkit' ),
9389 + 'success' => true,
9390 + );
9391 + } else {
9392 + $response = array(
9393 + 'message' => esc_html__( 'Plugin not found', 'wdesignkit' ),
9394 + 'description' => esc_html__( 'Plugin not found', 'wdesignkit' ),
9395 + 'success' => false,
9396 + );
9397 + }
9398 +
9399 + return $response;
9400 + }
9401 +
9402 + /**
9403 + * generate different color logo
9404 + *
9405 + * @since 2.0.0
9406 + */
9407 + protected function wkit_generate_site_logo() {
9408 +
9409 + if ( empty( $_POST['image_url'] ) ) {
9410 + wp_send_json_error( 'Image URL not provided.' );
9411 + }
9412 +
9413 + if ( isset( $_POST['colors'] ) ) {
9414 + $img_colors = ! empty( $_POST['colors'] ) ? json_decode( sanitize_text_field( wp_unslash( $_POST['colors'] ) ), true ) : array();
9415 + }
9416 +
9417 + if ( empty( $img_colors ) ) {
9418 + wp_send_json_error( 'Image color not provided.' );
9419 + }
9420 +
9421 + $image_url = esc_url_raw( $_POST['image_url'] );
9422 +
9423 + if ( ! wdesignkit_validate_external_url( $image_url ) ) {
9424 + wp_send_json_error( 'Image could not be downloaded.' );
9425 + }
9426 +
9427 + $tmp_file = download_url( $image_url );
9428 + if ( is_wp_error( $tmp_file ) ) {
9429 + wp_send_json_error( 'Image could not be downloaded.' );
9430 + }
9431 +
9432 + if ( mime_content_type( $tmp_file ) !== 'image/png' ) {
9433 + unlink( $tmp_file );
9434 + wp_send_json_error( 'Not a PNG file.' );
9435 + }
9436 +
9437 + $src = imagecreatefrompng( $tmp_file );
9438 + imagesavealpha( $src, true );
9439 +
9440 + $width = imagesx( $src );
9441 + $height = imagesy( $src );
9442 +
9443 + $hasTransparency = false;
9444 + for ( $x = 0; $x < $width; $x++ ) {
9445 + for ( $y = 0; $y < $height; $y++ ) {
9446 + $rgba = imagecolorat( $src, $x, $y );
9447 + $alpha = ( $rgba & 0x7F000000 ) >> 24;
9448 +
9449 + if ( $alpha > 0 ) {
9450 + $hasTransparency = true;
9451 + break 2;
9452 + }
9453 + }
9454 + }
9455 +
9456 + if ( ! $hasTransparency ) {
9457 + unlink( $tmp_file );
9458 + wp_send_json_error( 'PNG has no transparent pixels.' );
9459 + }
9460 +
9461 + $upload_dir = wp_upload_dir();
9462 + $result_urls = array();
9463 +
9464 + $colour_index = 0;
9465 + foreach ( $img_colors as $name => $rgb ) {
9466 + // $name is a key from the posted colours payload and went straight into the output
9467 + // filename, so traversal sequences in it steered imagepng() outside the upload
9468 + // directory (CWE-22, ClickUp 86d41ced6). sanitize_file_name() flattens it to one
9469 + // path segment; a key made only of dots/separators sanitizes to empty, so fall back
9470 + // to a positional index rather than writing to a bare "colored--<time>.png".
9471 + ++$colour_index;
9472 + $safe_name = sanitize_file_name( (string) $name );
9473 + if ( '' === $safe_name ) {
9474 + $safe_name = 'colour-' . $colour_index;
9475 + }
9476 +
9477 + $new = imagecreatetruecolor( $width, $height );
9478 + imagesavealpha( $new, true );
9479 + imagealphablending( $new, false );
9480 +
9481 + $transparent = imagecolorallocatealpha( $new, 0, 0, 0, 127 );
9482 + imagefill( $new, 0, 0, $transparent );
9483 +
9484 + for ( $x = 0; $x < $width; $x++ ) {
9485 + for ( $y = 0; $y < $height; $y++ ) {
9486 + $rgba = imagecolorat( $src, $x, $y );
9487 + $alpha = ( $rgba & 0x7F000000 ) >> 24;
9488 +
9489 + // Skip fully transparent pixels
9490 + if ( $alpha === 127 ) {
9491 + continue;
9492 + }
9493 +
9494 + // Replace pixel color directly
9495 + $new_r = $rgb[0];
9496 + $new_g = $rgb[1];
9497 + $new_b = $rgb[2];
9498 +
9499 + $color = imagecolorallocatealpha( $new, $new_r, $new_g, $new_b, $alpha );
9500 + imagesetpixel( $new, $x, $y, $color );
9501 + }
9502 + }
9503 +
9504 + $filename = 'colored-' . $safe_name . '-' . time() . '.png';
9505 + $filepath = $upload_dir['path'] . '/' . $filename;
9506 +
9507 + imagepng( $new, $filepath );
9508 + imagedestroy( $new );
9509 +
9510 + $attachment = array(
9511 + 'post_mime_type' => 'image/png',
9512 + 'post_title' => sanitize_file_name( $filename ),
9513 + 'post_content' => '',
9514 + 'post_status' => 'inherit',
9515 + );
9516 +
9517 + $attach_id = wp_insert_attachment( $attachment, $filepath );
9518 + require_once ABSPATH . 'wp-admin/includes/image.php';
9519 + $attach_data = wp_generate_attachment_metadata( $attach_id, $filepath );
9520 + wp_update_attachment_metadata( $attach_id, $attach_data );
9521 +
9522 + $result_urls[ $name ] = wp_get_attachment_url( $attach_id );
9523 + }
9524 +
9525 + imagedestroy( $src );
9526 + unlink( $tmp_file );
9527 +
9528 + wp_send_json_success( $result_urls );
9529 + }
9530 +
9531 + /**
9532 + * change theme setting for import kit
9533 + *
9534 + * @since 2.0.0
9535 + */
9536 + protected function update_theme_setting() {
9537 + $response = $this->wdkit_apply_theme_settings_data();
9538 +
9539 + wp_send_json( $response );
9540 + wp_die();
9541 + }
9542 +
9543 + /**
9544 + * Apply the Nexter theme container settings an imported kit expects.
9545 + *
9546 + * Extracted from update_theme_setting() so the PHP import runner can reach it without
9547 + * `$_POST`. Note that this method takes no input at all and never has: it writes a
9548 + * fixed set of `nxt-theme-options` sub-keys, which is why it is inherently idempotent
9549 + * and why there is nothing here a caller could steer.
9550 + *
9551 + * The AJAX action above is now a thin adapter over this and its response is unchanged.
9552 + *
9553 + * @since 2.6.5
9554 + *
9555 + * @return array Response array, exactly as the AJAX action used to emit.
9556 + */
9557 + public function wdkit_apply_theme_settings_data() {
9558 + $theme_db = get_option( 'nxt-theme-options', false );
9559 +
9560 + $container_type = 'container-fluid';
9561 + $fluid_spacing = array(
9562 + 'md' => array(
9563 + 'left' => '0',
9564 + 'right' => '0',
9565 + ),
9566 + 'sm' => array(
9567 + 'left' => '',
9568 + 'right' => '',
9569 + ),
9570 + 'xs' => array(
9571 + 'left' => '',
9572 + 'right' => '',
9573 + ),
9574 + 'md-unit' => 'px',
9575 + 'sm-unit' => 'px',
9576 + 'xs-unit' => 'px',
9577 + );
9578 +
9579 + if ( isset( $theme_db ) ) {
9580 + $nexter_setting = $theme_db;
9581 + $nexter_setting['site-header-container'] = $container_type;
9582 + $nexter_setting['site-footer-container'] = $container_type;
9583 + $nexter_setting['site-layout-container'] = $container_type;
9584 + $nexter_setting['site-page-container'] = $container_type;
9585 +
9586 + $nexter_setting['header-fluid-spacing'] = $fluid_spacing;
9587 + $nexter_setting['footer-fluid-spacing'] = $fluid_spacing;
9588 + $nexter_setting['site-fluid-spacing'] = $fluid_spacing;
9589 + $nexter_setting['page-fluid-spacing'] = $fluid_spacing;
9590 +
9591 + update_option( 'nxt-theme-options', $nexter_setting );
9592 + } else {
9593 + $nexter_setting = array(
9594 + 'site-header-container' => 'container-fluid',
9595 + 'header-fluid-spacing' => array(
9596 + 'md' => array(
9597 + 'left' => '0',
9598 + 'right' => '0',
9599 + ),
9600 + 'sm' => array(
9601 + 'left' => '',
9602 + 'right' => '',
9603 + ),
9604 + 'xs' => array(
9605 + 'left' => '',
9606 + 'right' => '',
9607 + ),
9608 + 'md-unit' => 'px',
9609 + 'sm-unit' => 'px',
9610 + 'xs-unit' => 'px',
9611 + ),
9612 + 'site-footer-container' => 'container-fluid',
9613 + 'footer-fluid-spacing' => array(
9614 + 'md' => array(
9615 + 'left' => '0',
9616 + 'right' => '0',
9617 + ),
9618 + 'sm' => array(
9619 + 'left' => '',
9620 + 'right' => '',
9621 + ),
9622 + 'xs' => array(
9623 + 'left' => '',
9624 + 'right' => '',
9625 + ),
9626 + 'md-unit' => 'px',
9627 + 'sm-unit' => 'px',
9628 + 'xs-unit' => 'px',
9629 + ),
9630 + 'site-layout-container' => 'container-fluid',
9631 + 'site-fluid-spacing' => array(
9632 + 'md' => array(
9633 + 'left' => '0',
9634 + 'right' => '0',
9635 + ),
9636 + 'sm' => array(
9637 + 'left' => '',
9638 + 'right' => '',
9639 + ),
9640 + 'xs' => array(
9641 + 'left' => '',
9642 + 'right' => '',
9643 + ),
9644 + 'md-unit' => 'px',
9645 + 'sm-unit' => 'px',
9646 + 'xs-unit' => 'px',
9647 + ),
9648 + 'site-page-container' => 'container-fluid',
9649 + 'page-fluid-spacing' => array(
9650 + 'md' => array(
9651 + 'left' => '0',
9652 + 'right' => '0',
9653 + ),
9654 + 'sm' => array(
9655 + 'left' => '',
9656 + 'right' => '',
9657 + ),
9658 + 'xs' => array(
9659 + 'left' => '',
9660 + 'right' => '',
9661 + ),
9662 + 'md-unit' => 'px',
9663 + 'sm-unit' => 'px',
9664 + 'xs-unit' => 'px',
9665 + ),
9666 + 'site-page-container' => '',
9667 + 'site-posts-container' => '',
9668 + 'site-archive-container' => '',
9669 + );
9670 +
9671 + add_option( 'nxt-theme-options', $nexter_setting );
9672 + }
9673 +
9674 + $response = array(
9675 + 'message' => esc_html__( 'Theme Setting updated', 'wdesignkit' ),
9676 + 'description' => esc_html__( 'Theme Setting updated', 'wdesignkit' ),
9677 + 'success' => true,
9678 + );
9679 +
9680 + return $response;
9681 + }
9682 +
9683 + /**
9684 + * change site setting for import kit
9685 + *
9686 + * @since 2.0.0
9687 + */
9688 + protected function update_site_setting() {
9689 +
9690 + // This is the import's "Finalizing Settings" request, and it is not the cheap
9691 + // bookkeeping call its name suggests: it generates the site logo/icon and then runs
9692 + // wdkit_sweep_attachment_ids() across every page that was just created, which resolves
9693 + // attachment ids and can pull WordPress into Imagick subsize generation. Every other
9694 + // heavy handler here already extends the limit (see the bundle import, the page-section
9695 + // import, the deferred-media cron); this one did not, so on a kit with a large image it
9696 + // fataled at PHP's default 30s. The client has no recovery path for that - the step
9697 + // simply stays on "Finalizing Settings" and polls forever, which is exactly the hang
9698 + // reported against Elementor kits. Harmless no-op where set_time_limit() is disabled.
9699 + if ( function_exists( 'set_time_limit' ) ) {
9700 + @set_time_limit( 300 );
9701 + }
9702 +
9703 + $temp_id = isset( $_POST['id'] ) ? sanitize_text_field( wp_unslash( $_POST['id'] ) ) : '';
9704 + $shop_id = isset( $_POST['shop_id'] ) ? sanitize_text_field( wp_unslash( $_POST['shop_id'] ) ) : '';
9705 + $temp_type = isset( $_POST['temp_type'] ) ? sanitize_text_field( wp_unslash( $_POST['temp_type'] ) ) : 'page';
9706 + $site_name = isset( $_POST['site_name'] ) ? sanitize_text_field( wp_unslash( $_POST['site_name'] ) ) : '';
9707 + $site_tagline = isset( $_POST['site_tagline'] ) ? sanitize_text_field( wp_unslash( $_POST['site_tagline'] ) ) : '';
9708 +
9709 + $page_information = isset( $_POST['page_information'] ) ? sanitize_text_field( wp_unslash( $_POST['page_information'] ) ) : '';
9710 + $page_information = json_decode( $page_information, true );
9711 +
9712 + $response = $this->wdkit_apply_site_settings_data(
9713 + array(
9714 + 'id' => $temp_id,
9715 + 'shop_id' => $shop_id,
9716 + 'temp_type' => $temp_type,
9717 + 'site_name' => $site_name,
9718 + 'site_tagline' => $site_tagline,
9719 + 'page_information' => $page_information,
9720 + )
9721 + );
9722 +
9723 + wp_send_json( $response );
9724 + wp_die();
9725 + }
9726 +
9727 + /**
9728 + * Apply a kit's site settings: front page, shop page, name and tagline.
9729 + *
9730 + * Extracted from update_site_setting() so the PHP import runner can reach it without
9731 + * `$_POST`. The set of options written is closed and hard-coded here —
9732 + * `show_on_front`, `page_on_front`, `woocommerce_shop_page_id`, `blogname`,
9733 + * `blogdescription`. There is deliberately no way for a caller to name an option.
9734 + *
9735 + * Theme-builder condition rewriting still runs through
9736 + * wdkit_nxt_thembuilder_update(), which now takes its page map as an argument.
9737 + *
9738 + * The AJAX action above is now a thin adapter over this and its response is unchanged.
9739 + *
9740 + * @since 2.6.5
9741 + *
9742 + * @param array $args {id, shop_id, temp_type, site_name, site_tagline, page_information,
9743 + * defer_attachment_downloads}.
9744 + * @return array Response array, exactly as the AJAX action used to emit.
9745 + */
9746 + public function wdkit_apply_site_settings_data( $args ) {
9747 + $args = is_array( $args ) ? $args : array();
9748 +
9749 + $temp_id = isset( $args['id'] ) ? sanitize_text_field( (string) $args['id'] ) : '';
9750 + $shop_id = isset( $args['shop_id'] ) ? sanitize_text_field( (string) $args['shop_id'] ) : '';
9751 + $temp_type = isset( $args['temp_type'] ) ? sanitize_text_field( (string) $args['temp_type'] ) : 'page';
9752 + $site_name = isset( $args['site_name'] ) ? sanitize_text_field( (string) $args['site_name'] ) : '';
9753 + $site_tagline = isset( $args['site_tagline'] ) ? sanitize_text_field( (string) $args['site_tagline'] ) : '';
9754 +
9755 + $this->wdkit_nxt_thembuilder_update(
9756 + isset( $args['page_information'] ) ? $args['page_information'] : null,
9757 + ! empty( $args['defer_attachment_downloads'] )
9758 + );
9759 +
9760 + if ( ! empty( $shop_id ) ) {
9761 + update_option( 'woocommerce_shop_page_id', $shop_id );
9762 + }
9763 +
9764 + if ( $temp_id ) {
9765 + update_option( 'show_on_front', $temp_type );
9766 + update_option( 'page_on_front', $temp_id );
9767 +
9768 + if ( ! empty( $site_name ) ) {
9769 + update_option( 'blogname', $site_name );
9770 + }
9771 +
9772 + if ( ! empty( $site_tagline ) ) {
9773 + update_option( 'blogdescription', $site_tagline );
9774 +
9775 + /* Remembered so a LATER import can tell a tagline this importer wrote from
9776 + * one the site's owner wrote. Only the former is ours to clear. */
9777 + update_option( 'wdkit_applied_tagline', $site_tagline );
9778 + } else {
9779 + /* Skipped the tagline question - which is not the same as "leave whatever is
9780 + * there". Importing a second kit onto a site that already carries the FIRST
9781 + * import's tagline left the new business sitting under the old one's slogan
9782 + * (a SaaS kit headed "Golden Crust Melts Inside"), because nothing ever
9783 + * cleared it.
9784 + *
9785 + * Cleared only when the current tagline is character-for-character the one a
9786 + * previous import wrote. A tagline the site's own owner typed is left
9787 + * completely alone - a kit import has no business wiping it. */
9788 + $applied = (string) get_option( 'wdkit_applied_tagline', '' );
9789 +
9790 + if ( '' !== $applied && $applied === (string) get_option( 'blogdescription', '' ) ) {
9791 + update_option( 'blogdescription', '' );
9792 + delete_option( 'wdkit_applied_tagline' );
9793 + }
9794 + }
9795 +
9796 + $response = array(
9797 + 'message' => esc_html__( 'Site link updated', 'wdesignkit' ),
9798 + 'description' => esc_html__( 'Site link updated', 'wdesignkit' ),
9799 + 'site_link' => get_site_url(),
9800 + 'success' => true,
9801 + );
9802 + } else {
9803 + $response = array(
9804 + 'message' => esc_html__( 'Site not found', 'wdesignkit' ),
9805 + 'description' => esc_html__( 'Site not found', 'wdesignkit' ),
9806 + 'success' => false,
9807 + );
9808 + }
9809 +
9810 + return $response;
9811 + }
9812 +
9813 + /**
9814 + * update theme builder
9815 + *
9816 + * @since 2.0.4
9817 + *
9818 + * @param array|null $page_information Page map; falls back to $_POST when null.
9819 + * @param bool $local_only Passed straight through to
9820 + * wdkit_sweep_attachment_ids() — see its docblock.
9821 + */
9822 + public function wdkit_nxt_thembuilder_update( $page_information = null, $local_only = false ) {
9823 +
9824 + /* Falls back to `$_POST` when called with no argument, so every existing caller —
9825 + * and the AJAX path — behaves exactly as before. The PHP runner passes the map in. */
9826 + if ( null === $page_information ) {
9827 + $page_information = isset( $_POST['page_information'] ) ? sanitize_text_field( wp_unslash( $_POST['page_information'] ) ) : '';
9828 + $page_information = json_decode( $page_information, true );
9829 + }
9830 +
9831 + $t_finalize = microtime( true );
9832 +
9833 + if ( ! empty( $page_information ) && is_array( $page_information ) ) {
9834 +
9835 + // Every page and attachment now exists, so resolve any image ID the per-page
9836 + // pass could not (siblings import concurrently and share icons).
9837 + $t_sweep = microtime( true );
9838 + $this->wdkit_sweep_attachment_ids( wp_list_pluck( $page_information, 'inserted_id' ), $local_only );
9839 +
9840 + // This walks every page's content looking for unresolved attachment ids, so it
9841 + // scales with kit size and was not covered by the per-template numbers.
9842 + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
9843 + error_log( sprintf(
9844 + 'WDKIT finalize: sweep_attachment_ids %dms over %d pages',
9845 + (int) round( ( microtime( true ) - $t_sweep ) * 1000 ),
9846 + count( $page_information )
9847 + ) );
9848 + }
9849 +
9850 + // Step 1: banavo mapping [ old_id => new_id ]
9851 + $id_mapping = array();
9852 + foreach ( $page_information as $page_info ) {
9853 + if ( ! empty( $page_info['old_page_id'] ) ) {
9854 + $id_mapping[ $page_info['old_page_id'] ] = $page_info['inserted_id'];
9855 + }
9856 + }
9857 +
9858 + // Step 2: loop karo and update exclude
9859 + foreach ( $page_information as $page_info ) {
9860 +
9861 + $post_id = $page_info['inserted_id'] ?? '';
9862 + $old_post_id = $page_info['old_page_id'] ?? '';
9863 + $post_type = $page_info['post_type'] ?? '';
9864 +
9865 + if ( empty( $old_post_id ) ) {
9866 + continue; // only update where old id exists
9867 + }
9868 +
9869 + if ( $post_type != 'nxt_builder' ) {
9870 + continue;
9871 + }
9872 +
9873 + $include_specific = get_post_meta( $post_id, 'nxt-hooks-layout-specific', true );
9874 + if ( ! empty( $include_specific ) && is_array( $include_specific ) ) {
9875 +
9876 + foreach ( $include_specific as $key => $val ) {
9877 +
9878 + // check karo ke koi old_id ka post match kare che ke nahi
9879 + foreach ( $id_mapping as $old_id => $new_id ) {
9880 + $search = 'post-' . $old_id;
9881 + $replace = 'post-' . $new_id;
9882 +
9883 + if ( $val === $search ) {
9884 + $include_specific[ $key ] = $replace;
9885 + }
9886 + }
9887 + }
9888 +
9889 + // save back updated array
9890 + update_post_meta( $post_id, 'nxt-hooks-layout-specific', $include_specific );
9891 + }
9892 +
9893 + // Get exclude meta
9894 + $exclude_specific = get_post_meta( $post_id, 'nxt-hooks-layout-exclude-specific', true );
9895 + if ( ! empty( $exclude_specific ) && is_array( $exclude_specific ) ) {
9896 +
9897 + foreach ( $exclude_specific as $key => $val ) {
9898 +
9899 + // check karo ke koi old_id ka post match kare che ke nahi
9900 + foreach ( $id_mapping as $old_id => $new_id ) {
9901 + $search = 'post-' . $old_id;
9902 + $replace = 'post-' . $new_id;
9903 +
9904 + if ( $val === $search ) {
9905 + $exclude_specific[ $key ] = $replace;
9906 + }
9907 + }
9908 + }
9909 +
9910 + // save back updated array
9911 + update_post_meta( $post_id, 'nxt-hooks-layout-exclude-specific', $exclude_specific );
9912 + }
9913 + }
9914 + }
9915 + }
9916 +
9917 + /**
9918 + *
9919 + * select team image for import kit
9920 + *
9921 + * @since 2.2.2
9922 + */
9923 + public function wdkit_check_user_credit() {
9924 + $token = isset( $_POST['token'] ) ? sanitize_text_field( $_POST['token'] ) : '';
9925 +
9926 + /* The caller sends the token from its own localStorage, which is per browser. The
9927 + * SITE's cloud session is not - it lives in a transient written at login and is what
9928 + * every import actually authenticates with. Reading only the browser copy meant a
9929 + * connected site told a second browser, an incognito window or another WP admin that
9930 + * it needed to log in, while an import from that same site would have worked
9931 + * (ClickUp 14ynqxywrg4). */
9932 + if ( '' === $token && function_exists( 'wdkit_kit_import_resolve_token' ) ) {
9933 + $token = (string) wdkit_kit_import_resolve_token();
9934 + }
9935 +
9936 + if ( '' === $token ) {
9937 + wp_send_json(
9938 + array(
9939 + 'success' => false,
9940 + 'reason' => 'no_session',
9941 + 'logged_in' => false,
9942 + 'message' => esc_html__( 'Not signed into WDesignKit on this site.', 'wdesignkit' ),
9943 + )
9944 + );
9945 + wp_die();
9946 + }
9947 +
9948 + $array_data = array( 'token' => $token );
9949 +
9950 + $response = $this->wkit_api_call( $array_data, 'ai/credits/get' );
9951 + $success = ! empty( $response['success'] ) ? $response['success'] : false;
9952 +
9953 + if ( empty( $success ) ) {
9954 + /* Why it failed, not just that it did. The caller used to receive one flat
9955 + * "Data Not Found" for every failure and read it as "no credits left", so a
9956 + * cloud request that merely timed out was reported to the user as "your site
9957 + * limit for today has been reached" - a limit that had not been reached. A
9958 + * transport failure says so through wkit_api_call()'s own message. */
9959 + $massage = isset( $response['massage'] ) ? (string) $response['massage'] : '';
9960 + $unreachable = ( '' !== $massage && false !== stripos( $massage, 'API request error' ) );
9961 +
9962 + wp_send_json(
9963 + array(
9964 + 'success' => false,
9965 + 'reason' => $unreachable ? 'unreachable' : 'rejected',
9966 +
9967 + /* A token existed and was sent, so whatever went wrong here is not the
9968 + * user being signed out. */
9969 + 'logged_in' => true,
9970 + 'message' => $unreachable
9971 + ? esc_html__( 'Could not reach WDesignKit.', 'wdesignkit' )
9972 + : esc_html__( 'Data Not Found', 'wdesignkit' ),
9973 + 'description' => $massage,
9974 + )
9975 + );
9976 + wp_die();
9977 + }
9978 +
9979 + $response = json_decode( wp_json_encode( $response['data'] ), true );
9980 +
9981 + if ( is_array( $response ) ) {
9982 + /* The cloud answered for this token, so the site is signed in - said plainly so
9983 + * the caller does not have to infer it from its own localStorage. */
9984 + $response['logged_in'] = true;
9985 + }
9986 +
9987 + $this->wdkit_cache_cloud_usage( $response );
9988 +
9989 + wp_send_json( $response );
9990 + wp_die();
9991 + }
9992 +
9993 + /**
9994 + * Caches the storage / credit figures this response carried.
9995 + *
9996 + * This handler is the ONLY place those numbers ever exist on the site: the cloud endpoint
9997 + * authenticates with a user token that only a logged-in dashboard request carries, so the
9998 + * analytics heartbeat — which runs on cron with no user at all — can never fetch them itself.
9999 + * Caching them here is what lets Posimyth_Tracker_WDK report them, and it reports the cache's
10000 + * age alongside so a stale reading is recognisable as one.
10001 + *
10002 + * Field names are probed rather than assumed: the cloud has renamed these before, and the
10003 + * licence ability already carries six spellings of its own key field for the same reason. An
10004 + * unrecognised shape simply caches nothing rather than storing a wrong number.
10005 + *
10006 + * Only the figures are kept. No token, no account id, no email — the analytics consent copy
10007 + * promises non-sensitive data only, and this is read by the payload builder.
10008 + *
10009 + * @since 2.6.4
10010 + *
10011 + * @param mixed $data Decoded `data` object from the credits endpoint.
10012 + * @return void
10013 + */
10014 + private function wdkit_cache_cloud_usage( $data ) {
10015 + if ( ! is_array( $data ) ) {
10016 + return;
10017 + }
10018 +
10019 + $pick = static function ( $source, array $fields ) {
10020 + foreach ( $fields as $field ) {
10021 + if ( isset( $source[ $field ] ) && is_numeric( $source[ $field ] ) ) {
10022 + return (float) $source[ $field ];
10023 + }
10024 + }
10025 + return null;
10026 + };
10027 +
10028 + $usage = array(
10029 + 'storage_used' => $pick( $data, array( 'used_storage', 'storage_used', 'used_space' ) ),
10030 + 'storage_total' => $pick( $data, array( 'total_storage', 'storage_total', 'storage', 'total_space' ) ),
10031 + 'credit_used' => $pick( $data, array( 'used_credit', 'credit_used', 'used_credits' ) ),
10032 + 'credit_total' => $pick( $data, array( 'total_credit', 'credit_total', 'credits', 'real_credit' ) ),
10033 + );
10034 +
10035 + $usage = array_filter(
10036 + $usage,
10037 + static function ( $value ) {
10038 + return null !== $value;
10039 + }
10040 + );
10041 +
10042 + if ( empty( $usage ) ) {
10043 + return;
10044 + }
10045 +
10046 + $usage['cached_at'] = gmdate( 'Y-m-d H:i:s' );
10047 +
10048 + // Not autoloaded: read once a week by the heartbeat, never on a front-end request.
10049 + update_option( 'wdkit_cloud_usage', $usage, false );
10050 + }
10051 +
10052 + public function wdkit_nxt_thembuilder_reset() {
10053 + $post_id = isset( $_POST['post_id'] ) ? sanitize_text_field( $_POST['post_id'] ) : '';
10054 + $sections_layout = get_post_meta( $post_id, 'nxt-hooks-layout-sections', true );
10055 +
10056 + if ( ( ! empty( $sections_layout ) && ( $sections_layout == 'header' || $sections_layout == 'footer' || $sections_layout == 'breadcrumb' || $sections_layout == 'hooks' ) ) ) {
10057 + if ( get_post_meta( $post_id, 'nxt-add-display-rule' ) ) {
10058 + delete_post_meta( $post_id, 'nxt-add-display-rule' );
10059 + }
10060 +
10061 + if ( get_post_meta( $post_id, 'nxt-hooks-layout-specific' ) ) {
10062 + update_post_meta( $post_id, 'nxt-hooks-layout-specific', '' );
10063 + }
10064 +
10065 + if ( get_post_meta( $post_id, 'nxt-exclude-display-rule' ) ) {
10066 + update_post_meta( $post_id, 'nxt-exclude-display-rule', '' );
10067 + }
10068 +
10069 + if ( get_post_meta( $post_id, 'nxt-hooks-layout-exclude-specific' ) ) {
10070 + update_post_meta( $post_id, 'nxt-hooks-layout-exclude-specific', '' );
10071 + }
10072 + }
10073 + }
10074 +
10075 +
10076 + /**
1738 10077 * Share with Me Template and widgets
1739 10078 *
1740 10079 * @since 1.0.0
1741 10080 */
@@ -1795,10 +10134,10 @@
1795 10134 *
1796 10135 * @since 1.0.0
1797 10136 */
1798 10137 protected function wdkit_manage_widget_workspace() {
1799 - $Workspace_info = isset( $_POST['workspace_info'] ) ? sanitize_text_field( wp_unslash( $_POST['workspace_info'] ) ) : array();
1800 - $data = isset( $Workspace_info ) ? json_decode( stripslashes( $Workspace_info ) ) : array();
10138 + $workspace_info = isset( $_POST['workspace_info'] ) ? sanitize_text_field( wp_unslash( $_POST['workspace_info'] ) ) : array();
10139 + $data = isset( $workspace_info ) ? json_decode( stripslashes( $workspace_info ) ) : array();
1801 10140
1802 10141 $array_data = array(
1803 10142 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '',
1804 10143 'wstype' => isset( $data->type ) ? sanitize_text_field( $data->type ) : '',
@@ -1822,9 +10161,10 @@
1822 10161 protected function wdkit_activate_key() {
1823 10162 $email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
1824 10163 $response = '';
1825 10164
1826 - if ( empty( $user_email ) ) {
10165 + // Bug C fix: variable was $user_email but only $email is set above — always triggered empty() guard.
10166 + if ( empty( $email ) ) {
1827 10167 $response = array(
1828 10168 'message' => $this->e_msg_login,
1829 10169 'description' => $this->e_desc_login,
1830 10170 'success' => false,
@@ -1858,13 +10198,13 @@
1858 10198 }
1859 10199
1860 10200 /**
1861 10201 *
1862 - * It is Use for get local widget list.
10202 + * Get list local Widget List
1863 10203 *
1864 10204 * @since 1.0.0
1865 10205 */
1866 - protected function wdkit_get_widget_list() {
10206 + protected function wdkit_get_local_widgets() {
1867 10207 $builder = array();
1868 10208 $a_c_s_d_s_c = array();
1869 10209 $j_s_o_n_array = array();
1870 10210
@@ -1879,8 +10219,12 @@
1879 10219 if ( Wdkit_Wdesignkit::wdkit_is_compatible( 'gutenberg', 'widget' ) ) {
1880 10220 array_push( $builder, 'gutenberg' );
1881 10221 }
1882 10222
10223 + if ( Wdkit_Wdesignkit::wdkit_is_compatible( 'gutenberg_core', 'widget' ) ) {
10224 + array_push( $builder, 'gutenberg_core' );
10225 + }
10226 +
1883 10227 foreach ( $builder as $key => $name ) {
1884 10228 $elementor_dir = WDKIT_BUILDER_PATH . '/' . $name;
1885 10229
1886 10230 if ( ! empty( $elementor_dir ) && is_dir( $elementor_dir ) ) {
@@ -1888,10 +10232,10 @@
1888 10232 $elementor_list = array_diff( $elementor_list, array( '.', '..' ) );
1889 10233
1890 10234 if ( ! empty( $elementor_list ) ) {
1891 10235 foreach ( $elementor_list as $key => $value ) {
1892 - $a_c_s_d_s_c[ filemtime( "{$elementor_dir}/{$value}" ) ]['data'] = $value;
1893 - $a_c_s_d_s_c[ filemtime( "{$elementor_dir}/{$value}" ) ]['builder'] = $name;
10236 + $a_c_s_d_s_c[ filemtime( "{$elementor_dir}/{$value}" ) . $key ]['data'] = $value;
10237 + $a_c_s_d_s_c[ filemtime( "{$elementor_dir}/{$value}" ) . $key ]['builder'] = $name;
1894 10238 }
1895 10239 }
1896 10240 }
1897 10241 }
@@ -1920,10 +10264,9 @@
1920 10264 }
1921 10265 }
1922 10266 }
1923 10267
1924 - wp_send_json( $j_s_o_n_array );
1925 - wp_die();
10268 + return $j_s_o_n_array;
1926 10269 }
1927 10270
1928 10271 /**
1929 10272 *
@@ -1959,521 +10302,133 @@
1959 10302 wp_send_json( get_option( 'wkit_builder' ) );
1960 10303 }
1961 10304
1962 10305 /**
10306 + * Get Workspace data
1963 10307 *
1964 - * Custom_upload_dir
1965 - *
1966 - * @since 1.0.0
1967 - *
1968 - * @param array $upload store data.
10308 + * @since 2.2.5
1969 10309 */
1970 - public function custom_upload_dir( $upload ) {
1971 - // Specify the path to your custom upload directory.
1972 - if ( isset( $this->widget_folder_u_r_l ) && ! empty( $this->widget_folder_u_r_l ) ) {
10310 + public function wdkit_get_workspace_data() {
1973 10311
1974 - // Set the custom directory as the upload path.
1975 - $upload['path'] = $this->widget_folder_u_r_l;
1976 - // Set the URL for the uploaded file.
1977 - $upload['url'] = $upload['baseurl'] . $upload['subdir'];
1978 - }
10312 + $wid = isset( $_POST['wid'] ) ? sanitize_text_field( $_POST['wid'] ) : '';
10313 + $token = isset( $_POST['token'] ) ? sanitize_text_field( $_POST['token'] ) : '';
1979 10314
1980 - return $upload;
1981 - }
1982 -
1983 - /**
1984 - *
1985 - * It is Use for create widget for local
1986 - *
1987 - * @since 1.0.0
1988 - */
1989 - protected function wdkit_create_widget() {
1990 - $image = '';
1991 - if ( isset( $_FILES ) && ! empty( $_FILES ) && isset( $_FILES['image'] ) && ! empty( $_FILES['image'] ) ) {
1992 - $image = Wdkit_Data_Hooks::get_super_global_value( $_FILES, 'image' );
1993 - }
1994 -
1995 - $icon = '';
1996 - if ( isset( $_FILES ) && ! empty( $_FILES ) && isset( $_FILES['icon'] ) && ! empty( $_FILES['icon'] ) ) {
1997 - $icon = Wdkit_Data_Hooks::get_super_global_value( $_FILES, 'icon' );
1998 - }
1999 -
2000 - $data = ! empty( $_POST['value'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'value', 'cr_widget' ) : '';
2001 - $data = ! empty( $data ) ? stripslashes( $data ) : '';
2002 - $return = ! empty( $data ) ? json_decode( $data ) : '';
2003 -
2004 - $all_val = ! empty( $return ) ? $return : '';
2005 - if ( empty( $all_val ) ) {
2006 -
2007 - $responce = array(
2008 - 'message' => esc_html__( 'Data Not Found', 'wdesignkit' ),
2009 - 'description' => esc_html__( 'something went wrong! please try again later.', 'wdesignkit' ),
10315 + if ( empty( $wid ) ) {
10316 + return array(
2010 10317 'success' => false,
10318 + 'message' => esc_html__( 'Workspace ID Not Found', 'wdesignkit' ),
10319 + 'description' => esc_html__( 'Workspace ID is required', 'wdesignkit' ),
2011 10320 );
2012 -
2013 - wp_send_json( $responce );
2014 - wp_die();
2015 10321 }
2016 10322
2017 - $file_name = ! empty( $all_val->file_name ) ? sanitize_text_field( $all_val->file_name ) : '';
2018 - $folder_name = ! empty( $all_val->folder_name ) ? sanitize_text_field( $all_val->folder_name ) : '';
2019 - $old_widget = ! empty( $all_val->old_folder ) ? sanitize_text_field( $all_val->old_folder ) : '';
2020 - $description = ! empty( $all_val->description ) ? sanitize_text_field( $all_val->description ) : '';
2021 - $json_file = ! empty( $all_val->json_file ) ? $all_val->json_file : '';
2022 - $function_call = ! empty( $all_val->call ) ? sanitize_text_field( $all_val->call ) : '';
2023 - $plugin = ! empty( $all_val->plugin ) ? $all_val->plugin : '';
2024 - $d_image = ! empty( $all_val->d_image ) ? $all_val->d_image : '';
2025 - $data = json_decode( $json_file );
2026 -
2027 - $elementor_php_file = ! empty( $all_val->elementor_php_file ) ? $all_val->elementor_php_file : '';
2028 - $elementor_js = ! empty( $all_val->elementor_js ) ? $all_val->elementor_js : '';
2029 - $elementor_css = ! empty( $all_val->elementor_css ) ? $all_val->elementor_css : '';
2030 -
2031 - $gutenberg_php_file = ! empty( $all_val->gutenberg_php_file ) ? $all_val->gutenberg_php_file : '';
2032 - $gutenberg_js = ! empty( $all_val->gutenberg_js ) ? $all_val->gutenberg_js : '';
2033 - $gutenberg_css = ! empty( $all_val->gutenberg_css ) ? $all_val->gutenberg_css : '';
2034 - $external_js_file = ! empty( $all_val->external_js_file ) ? $all_val->external_js_file : '';
2035 - $style_file = ! empty( $all_val->style_file ) ? $all_val->style_file : '';
2036 -
2037 - $bricks_php_file = ! empty( $all_val->bricks_php_file ) ? $all_val->bricks_php_file : '';
2038 - $bricks_js = ! empty( $all_val->bricks_js ) ? $all_val->bricks_js : '';
2039 - $bricks_css = ! empty( $all_val->bricks_css ) ? $all_val->bricks_css : '';
2040 -
2041 - $old_folder = ! empty( $old_widget ) ? str_replace( ' ', '-', $old_widget ) : '';
2042 - $widget_type = ! empty( $data->widget_data->widgetdata->type ) ? sanitize_text_field( $data->widget_data->widgetdata->type ) : '';
2043 -
2044 - if ( empty( $widget_type ) ) {
2045 - $responce = array(
2046 - 'message' => esc_html__( 'Builder Type not found', 'wdesignkit' ),
2047 - 'description' => esc_html__( 'something went wrong! please try again later.', 'wdesignkit' ),
10323 + if ( empty( $token ) ) {
10324 + return array(
2048 10325 'success' => false,
10326 + 'message' => esc_html__( 'Token Not Found', 'wdesignkit' ),
10327 + 'description' => esc_html__( 'Token is required', 'wdesignkit' ),
2049 10328 );
2050 -
2051 - wp_send_json( $responce );
2052 - wp_die();
2053 10329 }
2054 10330
2055 - $builder_type_path = trailingslashit( WDKIT_BUILDER_PATH ) . trailingslashit( $widget_type );
2056 - $widget_file_url = $builder_type_path . $folder_name;
10331 + $args = array(
10332 + 'token' => $token,
10333 + 'wid' => $wid,
10334 + );
2057 10335
2058 - if ( ! is_dir( $widget_file_url ) ) {
2059 - wp_mkdir_p( $widget_file_url );
2060 - }
10336 + $this->wdkit_api = $this->wdkit_api_v2;
2061 10337
2062 - include_once ABSPATH . 'wp-admin/includes/file.php';
2063 - \WP_Filesystem();
2064 - global $wp_filesystem;
2065 - $widget_folder_u_r_l = trailingslashit( $widget_file_url ) . $file_name;
2066 - $this->widget_folder_u_r_l = $widget_file_url;
10338 + $url = "workspace/{$wid}/get";
2067 10339
2068 - if ( 'elementor' === $plugin ) {
2069 - $widget_file_list = scandir( $widget_file_url );
2070 - $widget_file_list = array_diff( $widget_file_list, array( '.', '..' ) );
10340 + $response = $this->wkit_api_call( $args, $url );
2071 10341
2072 - foreach ( $widget_file_list as $sub_dir_value ) {
2073 - $file = new SplFileInfo( $sub_dir_value );
2074 - $check_ext = $file->getExtension();
2075 - $extiona = pathinfo( $sub_dir_value, PATHINFO_EXTENSION );
2076 -
2077 - if ( 'js' === $extiona || 'css' === $extiona || 'json' === $extiona || 'php' === $extiona ) {
2078 - $wp_filesystem->rmdir( "$widget_file_url/$sub_dir_value", true );
2079 - }
2080 - }
2081 -
2082 - if ( ! empty( $elementor_php_file ) ) {
2083 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.php", $elementor_php_file );
2084 - }
2085 - if ( ! empty( $json_file ) ) {
2086 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.json", $json_file );
2087 - }
2088 - if ( ! empty( $elementor_css ) ) {
2089 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.css", $elementor_css );
2090 - }
2091 - if ( ! empty( $elementor_js ) ) {
2092 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.js", $elementor_js );
2093 - }
2094 - } elseif ( 'bricks' === $plugin ) {
2095 -
2096 - $widget_file_list = scandir( $widget_file_url );
2097 - $widget_file_list = array_diff( $widget_file_list, array( '.', '..' ) );
2098 -
2099 - foreach ( $widget_file_list as $sub_dir_value ) {
2100 - $file = new SplFileInfo( $sub_dir_value );
2101 - $check_ext = $file->getExtension();
2102 - $extiona = pathinfo( $sub_dir_value, PATHINFO_EXTENSION );
2103 -
2104 - if ( 'js' === $extiona || 'css' === $extiona || 'json' === $extiona || 'php' === $extiona ) {
2105 - $wp_filesystem->rmdir( "$widget_file_url/$sub_dir_value", true );
2106 - }
2107 - }
2108 -
2109 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.php", $bricks_php_file );
2110 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.json", $json_file );
2111 -
2112 - if ( ! empty( $bricks_css ) ) {
2113 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.css", $bricks_css );
2114 - }
2115 -
2116 - if ( ! empty( $bricks_js ) ) {
2117 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.js", $bricks_js );
2118 - }
2119 - } elseif ( 'gutenberg' === $plugin ) {
2120 -
2121 - $widget_file_list = scandir( $widget_file_url );
2122 - $widget_file_list = array_diff( $widget_file_list, array( '.', '..' ) );
2123 -
2124 - foreach ( $widget_file_list as $sub_dir_value ) {
2125 - $file = new SplFileInfo( $sub_dir_value );
2126 - $check_ext = $file->getExtension();
2127 - $extiona = pathinfo( $sub_dir_value, PATHINFO_EXTENSION );
2128 -
2129 - if ( 'js' === $extiona || 'css' === $extiona || 'json' === $extiona || 'php' === $extiona ) {
2130 - $wp_filesystem->rmdir( "$widget_file_url/$sub_dir_value", true );
2131 - }
2132 - }
2133 -
2134 - if ( ! empty( $external_js_file ) ) {
2135 - $wp_filesystem->put_contents( "$widget_file_url/index.js", $external_js_file );
2136 - }
2137 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.php", $gutenberg_php_file );
2138 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.json", $json_file );
2139 - if ( ! empty( $gutenberg_css ) ) {
2140 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.css", $gutenberg_css );
2141 - }
2142 - $wp_filesystem->put_contents( "$widget_folder_u_r_l.js", $gutenberg_js );
2143 - }
2144 -
2145 - if ( ! empty( $image ) && ! empty( $image['tmp_name'] ) ) {
2146 -
2147 - $img_type = array( 'jpg', 'png' );
2148 -
2149 - foreach ( $img_type as $imgext ) {
2150 - $wp_filesystem->rmdir( "$widget_folder_u_r_l . $imgext", true );
2151 - }
2152 -
2153 - $ext = $image['type'];
2154 - $img_ext = '';
2155 - if ( strpos( $ext, 'jpeg' ) ) {
2156 - $img_ext = 'jpg';
2157 - } elseif ( strpos( $ext, 'png' ) ) {
2158 - $img_ext = 'png';
2159 - }
2160 - if ( ! empty( $img_ext ) ) {
2161 - add_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );
2162 -
2163 - $uploaded_file = wp_handle_upload( $image, array( 'test_form' => false ) );
2164 -
2165 - rename( $uploaded_file['file'], $widget_folder_u_r_l . '.' . $img_ext );
2166 -
2167 - remove_filter( 'upload_dir', array( $this, 'custom_upload_dir' ) );
2168 -
2169 - }
2170 - } elseif ( ! empty( $old_widget ) ) {
2171 - $img_url = $data->widget_data->widgetdata->w_image;
2172 - $img_ext = ! empty( pathinfo( $img_url )['extension'] ) ? pathinfo( $img_url )['extension'] : '';
2173 -
2174 - if ( ! empty( $img_ext ) ) {
2175 - $old_widget_folder = str_replace( ' ', '-', $old_widget );
2176 - $old_widget_file = str_replace( ' ', '_', $old_widget );
2177 - $img_path = "$builder_type_path$old_widget_folder/$old_widget_file.$img_ext";
2178 - $img_path = str_replace( '\\', '/', $img_path );
2179 -
2180 - if ( file_exists( $img_path ) ) {
2181 - $get_img = $img_path;
2182 - $put_img = "$widget_folder_u_r_l.$img_ext";
2183 -
2184 - if ( ! empty( $get_img ) && ! empty( $put_img ) ) {
2185 - rename( $get_img, $put_img );
2186 - }
2187 - }
2188 - }
2189 - }
2190 -
2191 - if ( ! empty( $d_image ) ) {
2192 - $d_image = str_replace( '\\', '', $d_image );
2193 - $d_img_url = $d_image;
2194 - $img_body = wp_remote_get( $d_img_url );
2195 - $img_ext = pathinfo( $d_img_url )['extension'];
2196 - $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$widget_type/$folder_name/$file_name.$img_ext", $img_body['body'] );
2197 - }
2198 -
2199 - if ( ! empty( $function_call ) && 'import' !== $function_call && ! empty( $old_folder ) && strtolower( $old_folder ) !== strtolower( $folder_name ) && is_dir( $builder_type_path . $old_folder ) ) {
2200 - require_once ABSPATH . 'wp-admin/includes/file.php';
2201 - global $wp_filesystem;
2202 - WP_Filesystem();
2203 - $wp_filesystem->rmdir( $builder_type_path . $old_folder, true );
2204 - } elseif ( $old_folder !== $folder_name ) {
2205 - rename( WDKIT_BUILDER_PATH . "/$widget_type/$old_folder", WDKIT_BUILDER_PATH . "/$widget_type/$folder_name" );
2206 - }
2207 -
2208 - $responce = array(
2209 - 'message' => esc_html__( 'Update Saved Successfully', 'wdesignkit' ),
2210 - 'description' => esc_html__( 'Success! Update Saved', 'wdesignkit' ),
2211 - 'success' => true,
2212 - );
2213 -
2214 - wp_send_json( $responce );
10342 + wp_send_json( $response['data'] );
2215 10343 wp_die();
2216 10344 }
2217 10345
2218 10346 /**
2219 10347 *
2220 - * It is Use for delete widget from server
10348 + * Custom_upload_dir
2221 10349 *
2222 10350 * @since 1.0.0
10351 + *
10352 + * @param array $upload store data.
2223 10353 */
2224 - protected function wdkit_import_widget() {
2225 - $filename = '';
2226 - if ( isset( $_FILES ) && ! empty( $_FILES ) && isset( $_FILES['zipName'] ) && ! empty( $_FILES['zipName'] ) ) {
2227 - $filename = ! empty( $_FILES['zipName']['name'] ) ? sanitize_file_name( $_FILES['zipName']['name'] ) : '';
2228 - }
10354 + public function custom_upload_dir( $upload ) {
10355 + // Specify the path to your custom upload directory.
10356 + if ( isset( $this->widget_folder_u_r_l ) && ! empty( $this->widget_folder_u_r_l ) ) {
2229 10357
2230 - $name = rtrim( $filename, '.zip' );
2231 - $ext = WDKIT_BUILDER_PATH . '/elementor/dump/';
2232 -
2233 - if ( ! is_dir( $ext ) ) {
2234 - wp_mkdir_p( $ext );
2235 - } else {
2236 - require_once ABSPATH . 'wp-admin/includes/file.php';
2237 - global $wp_filesystem;
2238 - WP_Filesystem();
2239 - $wp_filesystem->rmdir( $ext, true );
10358 + // Set the custom directory as the upload path.
10359 + $upload['path'] = $this->widget_folder_u_r_l;
10360 + // Set the URL for the uploaded file.
10361 + $upload['url'] = $upload['baseurl'] . $upload['subdir'];
2240 10362 }
2241 10363
2242 - $dir = WDKIT_BUILDER_PATH . '/elementor/dump';
2243 - $getall_json = array();
2244 - $zip = new ZipArchive();
2245 -
2246 - $zipname = '';
2247 - if ( ! empty( $_FILES['zipName']['tmp_name'] ) ) {
2248 - $zipname = $this->wdkit_file_sanitizer_bypass( $_FILES, 'zipName', 'name' );
2249 - }
2250 -
2251 - $res = $zip->open( $zipname );
2252 - if ( true === $res ) {
2253 - $zip->extractTo( $ext );
2254 - $zip->close();
2255 - $widget_name = $image = $json_file = '';
2256 - $list = scandir( $dir );
2257 - $list = array_diff( $list, array( '.', '..' ) );
2258 - foreach ( $list as $sub_dir_value ) {
2259 - $file = new SplFileInfo( $sub_dir_value );
2260 - $check_ext = $file->getExtension();
2261 - $extiona = pathinfo( $sub_dir_value, PATHINFO_EXTENSION );
2262 - if ( 'json' === $extiona ) {
2263 - $json_file = $sub_dir_value;
2264 - $u_r_l = wp_json_file_decode( $ext . $sub_dir_value );
2265 - if ( ! empty( $u_r_l->widget_data->widgetdata->name ) && ! empty( $u_r_l->widget_data->widgetdata->widget_id ) ) {
2266 - $widget_name = $u_r_l->widget_data->widgetdata->name;
2267 - $widget_id = $u_r_l->widget_data->widgetdata->widget_id;
2268 - $widget_type = ! empty( $u_r_l->widget_data->widgetdata->type ) ? $u_r_l->widget_data->widgetdata->type : '';
2269 - }
2270 - } elseif ( 'jpg' === $extiona || 'png' === $extiona || 'jpeg' === $extiona ) {
2271 - $img_ext = $extiona;
2272 - $image = $sub_dir_value;
2273 - }
2274 - }
2275 -
2276 - if ( ! empty( $widget_name ) && ! empty( $json_file ) ) {
2277 - $folder_name = str_replace( ' ', '-', $widget_name );
2278 - $file_name = str_replace( ' ', '_', $widget_name );
2279 - if ( ! is_dir( WDKIT_BUILDER_PATH . "/{$widget_type}" ) ) {
2280 - wp_mkdir_p( WDKIT_BUILDER_PATH . "/{$widget_type}" );
2281 - }
2282 - $file_path = WDKIT_BUILDER_PATH . "/{$widget_type}/{$folder_name}_{$widget_id}";
2283 - $dummy_path = WDKIT_BUILDER_PATH . '/elementor/dump';
2284 -
2285 - if ( is_dir( $dummy_path ) ) {
2286 - if ( ! rename( $dummy_path, $file_path ) ) {
2287 -
2288 - require_once ABSPATH . 'wp-admin/includes/file.php';
2289 - global $wp_filesystem;
2290 - WP_Filesystem();
2291 - $wp_filesystem->rmdir( $dummy_path, true );
2292 -
2293 - $responce = (object) array(
2294 - 'success' => false,
2295 - 'message' => esc_html__( 'Widget Not imported', 'wdesignkit' ),
2296 - 'description' => esc_html__( 'Widget alreday exist!', 'wdesignkit' ),
2297 - );
2298 -
2299 - wp_send_json( $responce );
2300 - wp_die();
2301 - }
2302 - }
2303 -
2304 - rename( "{$file_path}/{$json_file}", "{$file_path}/{$file_name}_{$widget_id}.json" );
2305 -
2306 - $get_img_file = "{$file_path}/{$image}";
2307 -
2308 - if ( file_exists( $get_img_file ) ) {
2309 - rename( $get_img_file, "{$file_path}/{$file_name}_{$widget_id}.{$img_ext}" );
2310 - }
2311 - }
2312 -
2313 - $responce = (object) array(
2314 - 'success' => true,
2315 - 'message' => esc_html__( 'Widget imported', 'wdesignkit' ),
2316 - 'description' => esc_html__( 'Widget imported successfully', 'wdesignkit' ),
2317 - 'json' => $u_r_l,
2318 - );
2319 -
2320 - wp_send_json( $responce );
2321 - wp_die();
2322 -
2323 - } else {
2324 - $responce = (object) array(
2325 - 'success' => false,
2326 - 'message' => esc_html__( 'Operation Fial!', 'wdesignkit' ),
2327 - 'description' => esc_html__( 'Widget can not imported', 'wdesignkit' ),
2328 - );
2329 -
2330 - wp_send_json( $responce );
2331 - wp_die();
2332 - }
10364 + return $upload;
2333 10365 }
2334 10366
2335 10367 /**
2336 10368 *
2337 - * Create Uniq name
2338 - *
2339 - * @since 1.0.0
2340 - */
2341 - protected function generate_unique_id() {
2342 - $now = new DateTime();
2343 - $unique_i_d = $now->format( 'YmdHis' );
2344 - $hashed_i_d = (int) $unique_i_d % 10000;
2345 - return str_pad( $hashed_i_d, 4, '0', STR_PAD_LEFT );
2346 - }
2347 -
2348 - /**
2349 - *
2350 10369 * It is Use for delete widget from server
2351 10370 *
2352 10371 * @since 1.0.0
2353 10372 */
2354 - protected function wdkit_export_widget() {
2355 - $data = isset( $_POST['info'] ) ? sanitize_text_field( wp_unslash( $_POST['info'] ) ) : '';
2356 - $data = json_decode( stripslashes( $data ) );
10373 + protected function wkit_widget_json() {
10374 + $widget_type = ! empty( $_POST['widget_type'] ) ? wp_unslash( $_POST['widget_type'] ) : '';
10375 + $folder_name = ! empty( $_POST['folder_name'] ) ? wp_unslash( $_POST['folder_name'] ) : '';
10376 + $file_name = ! empty( $_POST['file_name'] ) ? ( wp_unslash( $_POST['file_name'] ) ) : '';
2357 10377
2358 - $widget_name_temp = isset( $data->widget_name ) ? sanitize_text_field( $data->widget_name ) : '';
2359 - $widget_type = isset( $data->widget_type ) ? sanitize_text_field( $data->widget_type ) : '';
10378 + if ( empty( $widget_type ) || empty( $folder_name ) || empty( $file_name ) ) {
10379 + return array(
10380 + 'success' => false,
10381 + 'message' => esc_html__( 'Widget JSON not found', 'wdesignkit' ),
10382 + 'description' => esc_html__( 'widget JSON file not found.', 'wdesignkit' ),
10383 + );
10384 + }
2360 10385
2361 - $widget_name = str_replace( ' ', '_', $widget_name_temp );
2362 - $folder = str_replace( ' ', '-', $widget_name_temp );
2363 - $unique_version = $this->generate_unique_id();
10386 + // Read-side twin of the write and delete traversals fixed in 86d41cckh / 86d41ccz2: all
10387 + // three segments arrive from $_POST with only wp_unslash() applied — which strips
10388 + // nothing path-relevant — so "../" in any of them walked out of the builder directory
10389 + // and this handler returned the decoded contents of any .json file the web server user
10390 + // could read (CWE-22, ClickUp 86d41zaun).
10391 + $safe_path = wdesignkit_widget_path_guard( $widget_type, $folder_name, $file_name );
2364 10392
2365 - if ( empty( $widget_type ) ) {
2366 - $result = (object) array(
10393 + if ( false === $safe_path || '' === $safe_path['folder'] || '' === $safe_path['file'] ) {
10394 + return array(
2367 10395 'success' => false,
2368 - 'url' => '',
2369 - 'message' => esc_html__( 'Widget Type Fail', 'wdesignkit' ),
2370 - 'description' => esc_html__( 'Widget Type Not Exists', 'wdesignkit' ),
10396 + 'message' => esc_html__( 'Widget JSON not found', 'wdesignkit' ),
10397 + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ),
2371 10398 );
2372 -
2373 - wp_send_json( $result );
2374 - wp_die();
2375 10399 }
2376 10400
2377 - $downlod_path = WDKIT_BUILDER_PATH . "/{$widget_type}/";
2378 - $new_path = "{$downlod_path}/{$folder}/{$widget_name}";
10401 + $json_path = $safe_path['base'];
2379 10402
2380 - $download_url = WDKIT_SERVER_PATH . "/{$widget_type}/{$widget_name}.zip";
2381 - $zip = new ZipArchive();
2382 - $tmp_file = "{$downlod_path}{$widget_name}.zip";
10403 + // Re-check the resolved file: the component guard above cannot see a symlink. Returns
10404 + // false for a path that does not exist, which is the same answer we want anyway.
10405 + if ( ! wdesignkit_path_inside_builder_dir( "$json_path.json" ) ) {
10406 + return array(
10407 + 'success' => false,
10408 + 'message' => esc_html__( 'Widget JSON not found', 'wdesignkit' ),
10409 + 'description' => esc_html__( 'widget JSON file not found.', 'wdesignkit' ),
10410 + );
10411 + }
2383 10412
2384 - $json_data = wp_json_file_decode( "$new_path.json" );
2385 - $img_ext = $json_data->widget_data->widgetdata->img_ext;
2386 -
2387 - if ( true === $zip->open( $tmp_file, ZipArchive::CREATE ) ) {
2388 - $widget_wb = str_replace( '-', '_', $folder );
2389 - $zip->addFile( "$new_path.json", "$widget_wb.json" );
2390 - if ( ! empty( $img_ext ) ) {
2391 - $zip->addFile( "$new_path.$img_ext", "$widget_wb.$img_ext" );
2392 - }
2393 - $zip->close();
2394 -
10413 + $json_data = wp_json_file_decode( "$json_path.json" );
10414 + if ( ! empty( $json_data ) ) {
2395 10415 $result = (object) array(
2396 10416 'success' => true,
2397 - 'url' => $download_url,
2398 - 'message' => esc_html__( 'Widget Exported', 'wdesignkit' ),
2399 - 'description' => esc_html__( 'Widget Exported successfully', 'wdesignkit' ),
10417 + 'data' => $json_data,
10418 + 'message' => esc_html__( 'Widget get Successfully', 'wdesignkit' ),
10419 + 'description' => esc_html__( 'Widget JSON get Successfully', 'wdesignkit' ),
2400 10420 );
2401 -
2402 - wp_send_json( $result );
2403 - wp_die();
2404 10421 } else {
2405 10422 $result = (object) array(
2406 10423 'success' => false,
2407 - 'url' => '',
2408 - 'message' => esc_html__( 'Widget Exported Fail', 'wdesignkit' ),
2409 - 'description' => esc_html__( 'something went wrong! please try again later.', 'wdesignkit' ),
10424 + 'message' => esc_html__( 'Widget not get', 'wdesignkit' ),
10425 + 'description' => esc_html__( 'Widget JSON not get', 'wdesignkit' ),
2410 10426 );
2411 -
2412 - wp_send_json( $result );
2413 - wp_die();
2414 10427 }
2415 - }
2416 10428
2417 - /**
2418 - *
2419 - * It is Use for delete widget from server
2420 - *
2421 - * @since 1.0.0
2422 - */
2423 - protected function wdkit_delete_widget() {
2424 - $data = isset( $_POST['info'] ) ? sanitize_text_field( wp_unslash( $_POST['info'] ) ) : '';
2425 - $data = json_decode( stripslashes( $data ) );
2426 -
2427 - $delete_type = isset( $data->delete_type ) ? sanitize_text_field( $data->delete_type ) : '';
2428 -
2429 - if ( 'plugin_server' === $delete_type ) {
2430 - $array_data = array(
2431 - 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '',
2432 - 'type' => isset( $data->type ) ? sanitize_text_field( $data->type ) : '',
2433 - 'w_unique' => isset( $data->w_unique ) ? sanitize_text_field( $data->w_unique ) : '',
2434 - 'id' => isset( $data->id ) ? sanitize_text_field( $data->id ) : '',
2435 - );
2436 -
2437 - $response = $this->wkit_api_call( $array_data, 'save_widget' );
2438 - $success = ! empty( $response['success'] ) ? $response['success'] : false;
2439 -
2440 - if ( empty( $success ) ) {
2441 - $massage = ! empty( $response['massage'] ) ? $response['massage'] : esc_html__( 'server error', 'wdesignkit' );
2442 -
2443 - $result = (object) array(
2444 - 'success' => false,
2445 - 'message' => esc_html__( 'Widget Not Deleted', 'wdesignkit' ),
2446 - 'description' => esc_html__( 'Widget Not Deleted', 'wdesignkit' ),
2447 - );
2448 -
2449 - wp_send_json( $result );
2450 - wp_die();
2451 - }
2452 - }
2453 -
2454 - $dir_name = isset( $data->name ) ? sanitize_text_field( $data->name ) : '';
2455 - $widget_type = isset( $data->builder ) ? sanitize_text_field( $data->builder ) : '';
2456 - $dir = WDKIT_BUILDER_PATH . "/{$widget_type}/{$dir_name}";
2457 -
2458 - require_once ABSPATH . 'wp-admin/includes/file.php';
2459 - global $wp_filesystem;
2460 - WP_Filesystem();
2461 - $wp_filesystem->rmdir( $dir, true );
2462 -
2463 - if ( 'plugin_server' === $delete_type ) {
2464 - wp_send_json( $response['data'] );
2465 - wp_die();
2466 - } else {
2467 - $result = (object) array(
2468 - 'success' => true,
2469 - 'message' => esc_html__( 'widget deleted', 'wdesignkit' ),
2470 - 'description' => esc_html__( 'Widget deleted successfully', 'wdesignkit' ),
2471 - );
2472 -
2473 - wp_send_json( $result );
2474 - wp_die();
2475 - }
10429 + wp_send_json( $result );
10430 + wp_die();
2476 10431 }
2477 10432
2478 10433 /**
2479 10434 *
@@ -2481,9 +10436,9 @@
2481 10436 *
2482 10437 * @since 1.0.0
2483 10438 */
2484 10439 protected function wdkit_download_widget() {
2485 - $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'widget_info', 'none' ) : '';
10440 + $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_extract_post_field( $_POST, 'widget_info', 'none' ) : '';
2486 10441 $data = json_decode( stripslashes( $data ) );
2487 10442
2488 10443 $array_data = array(
2489 10444 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '',
@@ -2488,8 +10443,10 @@
2488 10443 $array_data = array(
2489 10444 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '',
2490 10445 'type' => isset( $data->type ) ? sanitize_text_field( $data->type ) : '',
2491 10446 'w_unique' => isset( $data->w_uniq ) ? sanitize_text_field( $data->w_uniq ) : '',
10447 + // Bug F fix: u_id (widget owner's user ID) was missing — cloud cannot locate the widget without it.
10448 + 'u_id' => isset( $data->u_id ) ? sanitize_text_field( $data->u_id ) : '',
2492 10449 );
2493 10450
2494 10451 $response = $this->wkit_api_call( $array_data, 'save_widget' );
2495 10452 $success = ! empty( $response['success'] ) ? $response['success'] : false;
@@ -2522,10 +10479,16 @@
2522 10479 wp_die();
2523 10480 }
2524 10481
2525 10482 $img_url = ! empty( $response['data']['image'] ) ? $response['data']['image'] : '';
2526 - $json_data = ! empty( $response['data']['json'] ) ? json_decode( $response['data']['json'] ) : '';
10483 + $json_data = ! empty( $response['data']['json'] ) ? json_decode( $response['data']['json'], true ) : '';
2527 10484
10485 + // Bug E fix (part 1): $responce was a typo of $response — sent undefined variable (null) to frontend.
10486 + if ( empty( $response['success'] ) ) {
10487 + wp_send_json( $response );
10488 + wp_die();
10489 + }
10490 +
2528 10491 if ( empty( $img_url ) && empty( $json_data ) ) {
2529 10492 $responce = (object) array(
2530 10493 'success' => false,
2531 10494 'message' => esc_html__( 'No Response Found', 'wdesignkit' ),
@@ -2539,14 +10502,38 @@
2539 10502 include_once ABSPATH . 'wp-admin/includes/file.php';
2540 10503 \WP_Filesystem();
2541 10504 global $wp_filesystem;
2542 10505
2543 - $title = ! empty( $json_data->widget_data->widgetdata->name ) ? sanitize_text_field( $json_data->widget_data->widgetdata->name ) : '';
2544 - $builder = ! empty( $json_data->widget_data->widgetdata->type ) ? sanitize_text_field( $json_data->widget_data->widgetdata->type ) : '';
2545 - $w_uniq = ! empty( $json_data->widget_data->widgetdata->widget_id ) ? sanitize_text_field( $json_data->widget_data->widgetdata->widget_id ) : '';
10506 + if ( ! is_array( $json_data ) ) {
10507 + $json_data = json_decode( $json_data, true );
10508 + }
2546 10509
2547 - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq;
2548 - $file_name = str_replace( ' ', '_', $title ) . '_' . $w_uniq;
10510 + // Sanitize as filenames before use in the widget path (CWE-22): sanitize_file_name()
10511 + // on name/id and sanitize_key() + allowlist on the builder strip path separators and
10512 + // dots so a crafted cloud response cannot escape WDKIT_BUILDER_PATH.
10513 + $title = ! empty( $json_data['widget_data']['widgetdata']['name'] ) ? sanitize_file_name( $json_data['widget_data']['widgetdata']['name'] ) : '';
10514 + $builder = ! empty( $json_data['widget_data']['widgetdata']['type'] ) ? sanitize_key( $json_data['widget_data']['widgetdata']['type'] ) : '';
10515 + $w_uniq = ! empty( $json_data['widget_data']['widgetdata']['widget_id'] ) ? sanitize_file_name( $json_data['widget_data']['widgetdata']['widget_id'] ) : '';
10516 +
10517 + $allowed_builders = array( 'elementor', 'gutenberg', 'gutenberg_core', 'bricks' );
10518 + if ( '' === $title || '' === $w_uniq || ! in_array( $builder, $allowed_builders, true ) ) {
10519 + $responce = (object) array(
10520 + 'success' => false,
10521 + 'message' => esc_html__( 'Operation Failed!', 'wdesignkit' ),
10522 + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ),
10523 + );
10524 +
10525 + wp_send_json( $responce );
10526 + wp_die();
10527 + }
10528 +
10529 + // Canonical helpers replace spaces BEFORE sanitize_file_name(). $title above is
10530 + // already sanitized, which collapsed spaces to hyphens and left the underscore pass
10531 + // with nothing to do — a multi-word title wrote "My-Widget_id.json" next to the
10532 + // "My_Widget_id.php" the builder's save path writes. The loader pairs the two by
10533 + // swapping .php for .json, so the widget was silently dropped (ClickUp 86d41cck5).
10534 + $folder_name = wdesignkit_widget_folder_name( $title, $w_uniq );
10535 + $file_name = wdesignkit_widget_file_name( $title, $w_uniq );
2549 10536 $builder_type_path = WDKIT_BUILDER_PATH . "/{$builder}/";
2550 10537
2551 10538 if ( ! is_dir( $builder_type_path ) ) {
2552 10539 wp_mkdir_p( $builder_type_path );
@@ -2556,17 +10543,30 @@
2556 10543 wp_mkdir_p( $builder_type_path . $folder_name );
2557 10544 }
2558 10545
2559 10546 if ( ! empty( $img_url ) ) {
2560 - $img_body = wp_remote_get( $img_url );
2561 - $img_ext = pathinfo( $img_url )['extension'];
10547 + // SSRF guard (CWE-918): validate the resolved host before fetching.
10548 + $img_body = wdesignkit_safe_remote_get( $img_url );
10549 + if ( ! is_wp_error( $img_body ) ) {
10550 + // The remote extension was written verbatim here, so a cloud response naming a
10551 + // ".php" image put executable PHP in the builder directory (CWE-434,
10552 + // ClickUp 86d41cczd). An empty return means the bytes are not an image.
10553 + $img_ext = wdesignkit_safe_image_extension( $img_url, $img_body['body'] );
2562 10554
2563 - $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] );
2564 - $json_data->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext";
10555 + if ( '' !== $img_ext ) {
10556 + $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] );
10557 + $json_data['widget_data']['widgetdata']['w_image'] = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext";
10558 + }
10559 + }
2565 10560 }
2566 10561
10562 + if ( function_exists( 'wdesignkit_invalidate_widget_registry' ) ) {
10563 + wdesignkit_invalidate_widget_registry( $builder );
10564 + }
10565 +
10566 + // Bug E fix (part 2): success was hardcoded false on the successful download path — always reported failure.
2567 10567 $result = (object) array(
2568 - 'success' => false,
10568 + 'success' => true,
2569 10569 'message' => ! empty( $response['message'] ) ? $response['message'] : esc_html__( 'no message', 'wdesignkit' ),
2570 10570 'description' => '',
2571 10571 'json' => wp_json_encode( $json_data ),
2572 10572 );
@@ -2576,94 +10576,16 @@
2576 10576 }
2577 10577
2578 10578 /**
2579 10579 *
2580 - * It is Use for download widget from browse page.
2581 - *
2582 - * @since 1.0.0
2583 - */
2584 - protected function wdkit_public_download_widget() {
2585 - $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'widget_info', 'none' ) : '';
2586 - $data = json_decode( stripslashes( $data ) );
2587 -
2588 - $array_data = array(
2589 - 'id' => isset( $data->w_uniq ) ? sanitize_text_field( $data->w_uniq ) : '',
2590 - 'u_id' => isset( $data->u_id ) ? sanitize_text_field( $data->u_id ) : '',
2591 - 'type' => isset( $data->d_type ) ? sanitize_text_field( $data->d_type ) : '',
2592 - );
2593 -
2594 - $response = $this->wkit_api_call( $array_data, 'widget/download' );
2595 - $success = ! empty( $response['success'] ) ? $response['success'] : false;
2596 -
2597 - if ( empty( $success ) ) {
2598 - $massage = ! empty( $response['massage'] ) ? $response['massage'] : esc_html__( 'server error', 'wdesignkit' );
2599 -
2600 - $result = (object) array(
2601 - 'success' => false,
2602 - 'message' => $massage,
2603 - 'description' => esc_html__( 'Widget not Downloaded', 'wdesignkit' ),
2604 - );
2605 -
2606 - wp_send_json( $result );
2607 - wp_die();
2608 - }
2609 -
2610 - $response = json_decode( wp_json_encode( $response['data'] ), true );
2611 - if ( ! empty( $response ) && ! empty( $response['data'] ) ) {
2612 - $img_url = ! empty( $response['data']['image'] ) ? esc_url_raw( $response['data']['image'] ) : '';
2613 - $json = ! empty( $response['data']['json'] ) ? wp_json_encode( $response['data']['json'] ) : '';
2614 -
2615 - if ( ! empty( $json ) ) {
2616 - include_once ABSPATH . 'wp-admin/includes/file.php';
2617 - \WP_Filesystem();
2618 - global $wp_filesystem;
2619 -
2620 - $json_data = json_decode( $json );
2621 - $json_data = json_decode( $json_data );
2622 - $title = ! empty( $json_data->widget_data->widgetdata->name ) ? sanitize_text_field( $json_data->widget_data->widgetdata->name ) : '';
2623 - $builder = ! empty( $json_data->widget_data->widgetdata->type ) ? sanitize_text_field( $json_data->widget_data->widgetdata->type ) : '';
2624 - $widget_id = ! empty( $json_data->widget_data->widgetdata->widget_id ) ? sanitize_text_field( $json_data->widget_data->widgetdata->widget_id ) : '';
2625 -
2626 - $folder_name = str_replace( ' ', '-', $title ) . '_' . $widget_id;
2627 - $file_name = str_replace( ' ', '_', $title ) . '_' . $widget_id;
2628 -
2629 - $builder_type_path = WDKIT_BUILDER_PATH . "/{$builder}/";
2630 -
2631 - if ( ! is_dir( $builder_type_path . $folder_name ) ) {
2632 - wp_mkdir_p( $builder_type_path . $folder_name );
2633 - }
2634 -
2635 - if ( ! empty( $img_url ) ) {
2636 - $img_body = wp_remote_get( $img_url );
2637 - $img_ext = pathinfo( $img_url )['extension'];
2638 - $wp_filesystem->put_contents( WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name.$img_ext", $img_body['body'] );
2639 -
2640 - $json_data->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext";
2641 - }
2642 -
2643 - $response = (object) array(
2644 - 'message' => ! empty( $response['message'] ) ? $response['message'] : '',
2645 - 'description' => ! empty( $response['description'] ) ? $response['description'] : '',
2646 - 'success' => ! empty( $response['success'] ) ? $response['success'] : false,
2647 - 'r_id' => ! empty( $response['data']['rid'] ) ? $response['data']['rid'] : 0,
2648 - 'json' => wp_json_encode( $json_data ),
2649 - );
2650 - }
2651 - }
2652 -
2653 - wp_send_json( $response );
2654 - wp_die();
2655 - }
2656 -
2657 - /**
2658 - *
2659 10580 * It is Use for sync widget to server
2660 10581 *
2661 10582 * @since 1.0.0
2662 10583 */
2663 10584 protected function wdkit_add_widget() {
2664 - $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_sanitizer_bypass( $_POST, 'widget_info', 'none' ) : '';
2665 - $data = json_decode( stripslashes( $data ) );
10585 + $data = ! empty( $_POST['widget_info'] ) ? $this->wdkit_extract_post_field( $_POST, 'widget_info', 'none' ) : '';
10586 + $data = base64_decode( $data );
10587 + $data = json_decode( $data );
2666 10588
2667 10589 $title = isset( $data->title ) ? sanitize_text_field( $data->title ) : '';
2668 10590 $builder = isset( $data->builder ) ? sanitize_text_field( $data->builder ) : '';
2669 10591 $w_uniq = isset( $data->w_uniq ) ? sanitize_text_field( $data->w_uniq ) : '';
@@ -2670,9 +10592,11 @@
2670 10592 $w_image = isset( $data->w_image ) ? esc_url_raw( $data->w_image ) : '';
2671 10593
2672 10594 if ( ! empty( $w_image ) ) {
2673 10595 $w_image = str_replace( '\\', '', $w_image );
2674 - $w_image = wp_remote_get( $w_image )['body'];
10596 + // SSRF guard (CWE-918): validate the resolved host before fetching.
10597 + $fetched = wdesignkit_safe_remote_get( $w_image );
10598 + $w_image = is_wp_error( $fetched ) ? '' : wp_remote_retrieve_body( $fetched );
2675 10599 }
2676 10600
2677 10601 $array_data = array(
2678 10602 'token' => isset( $data->token ) ? sanitize_text_field( $data->token ) : '',
@@ -2686,8 +10610,9 @@
2686 10610 'w_imgext' => isset( $data->w_imgext ) ? sanitize_text_field( $data->w_imgext ) : '',
2687 10611 'w_version' => isset( $data->w_version ) ? $data->w_version : '',
2688 10612 'w_updates' => ! empty( $data->w_updates ) ? serialize( $data->w_updates ) : serialize( array() ),
2689 10613 'r_id' => isset( $data->r_id ) ? $data->r_id : 0,
10614 + 'unique_id' => get_option( 'wdkit_unique_id' ) ?? '',
2690 10615 );
2691 10616
2692 10617 $response = $this->wkit_api_call( $array_data, 'save_widget' );
2693 10618 $success = ! empty( $response['success'] ) ? $response['success'] : false;
@@ -2710,22 +10635,52 @@
2710 10635 $response = json_decode( wp_json_encode( $res ), true );
2711 10636 $img_url = ! empty( $response['data']['imgurl'] ) ? $response['data']['imgurl'] : '';
2712 10637
2713 10638 if ( ! empty( $img_url ) && 'error' !== $res ) {
2714 - $img_body = wp_remote_get( $img_url );
2715 - $img_ext = pathinfo( $img_url )['extension'];
2716 - include_once ABSPATH . 'wp-admin/includes/file.php';
2717 - \WP_Filesystem();
2718 - global $wp_filesystem;
2719 - $folder_name = str_replace( ' ', '-', $title ) . '_' . $w_uniq;
2720 - $file_name = str_replace( ' ', '_', $title ) . '_' . $w_uniq;
2721 - $file_path = WDKIT_BUILDER_PATH . "/$builder/$folder_name/$file_name";
2722 10639
2723 - $u_r_l = wp_json_file_decode( "$file_path.json" );
2724 - $u_r_l->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext";
10640 + // SSRF guard (CWE-918): validate the resolved host before fetching.
10641 + $img_body = wdesignkit_safe_remote_get( $img_url );
10642 + if ( ! is_wp_error( $img_body ) ) {
10643 + // Verified against the payload rather than trusted from the URL (CWE-434,
10644 + // ClickUp 86d41cczd); '' means the bytes are not an image we accept.
10645 + $img_ext = wdesignkit_safe_image_extension( $img_url, $img_body['body'] );
10646 + include_once ABSPATH . 'wp-admin/includes/file.php';
10647 + \WP_Filesystem();
10648 + global $wp_filesystem;
10649 + // Canonical helpers, so the JSON read and the image write here address the same
10650 + // base name every other writer uses (ClickUp 86d41cck5). They also apply
10651 + // sanitize_file_name(), which $title and $w_uniq had not been through.
10652 + $folder_name = wdesignkit_widget_folder_name( $title, $w_uniq );
10653 + $file_name = wdesignkit_widget_file_name( $title, $w_uniq );
2725 10654
2726 - $wp_filesystem->put_contents( "$file_path.json", wp_json_encode( $u_r_l ) );
2727 - $wp_filesystem->put_contents( "$file_path.$img_ext", $img_body['body'] );
10655 + // $builder reaches here with only sanitize_text_field() applied and no
10656 + // allowlist, so it was a live traversal segment in this path (CWE-22,
10657 + // ClickUp 86d41cckh). Unlike the download handler earlier in this file, this
10658 + // one had neither the builder allowlist nor a containment check.
10659 + $safe_path = wdesignkit_widget_path_guard( $builder, $folder_name, $file_name );
10660 + if ( false === $safe_path || ! wdesignkit_path_inside_builder_dir( $safe_path['dir'] ) ) {
10661 + wp_send_json(
10662 + (object) array(
10663 + 'success' => false,
10664 + 'message' => esc_html__( 'Operation Failed!', 'wdesignkit' ),
10665 + 'description' => esc_html__( 'Invalid widget path.', 'wdesignkit' ),
10666 + )
10667 + );
10668 + wp_die();
10669 + }
10670 +
10671 + $builder = $safe_path['builder'];
10672 + $file_path = $safe_path['base'];
10673 +
10674 + $u_r_l = wp_json_file_decode( "$file_path.json" );
10675 +
10676 + if ( '' !== $img_ext ) {
10677 + $u_r_l->widget_data->widgetdata->w_image = WDKIT_SERVER_PATH . "/$builder/$folder_name/$file_name.$img_ext";
10678 + $wp_filesystem->put_contents( "$file_path.$img_ext", $img_body['body'] );
10679 + }
10680 +
10681 + $wp_filesystem->put_contents( "$file_path.json", wp_json_encode( $u_r_l ) );
10682 + }
2728 10683 }
2729 10684
2730 10685 wp_send_json( $response );
2731 10686 wp_die();
@@ -2792,24 +10747,135 @@
2792 10747 *
2793 10748 * @since 1.0.0
2794 10749 */
2795 10750 protected static function wkit_get_settings_panel() {
10751 + $new_version = '';
10752 + $current_version = WDKIT_VERSION;
10753 + $response = wp_remote_get( 'https://api.wordpress.org/plugins/info/1.0/wdesignkit.json' );
10754 +
10755 + if ( is_wp_error( $response ) ) {
10756 + return false;
10757 + }
10758 +
10759 + $body = wp_remote_retrieve_body( $response );
10760 + $data = json_decode( $body );
10761 +
10762 + if ( isset( $data->version ) ) {
10763 + $new_version = $data->version;
10764 + }
10765 +
10766 + $version_check = array();
10767 +
10768 + if ( $new_version && version_compare( $current_version, $new_version, '<' ) ) {
10769 + $version_check['success'] = true;
10770 + $version_check['version'] = $new_version;
10771 + } else {
10772 + $version_check['success'] = false;
10773 + $version_check['version'] = $new_version;
10774 + }
10775 +
2796 10776 $get_setting = get_option( 'wkit_settings_panel', false );
2797 10777
2798 - return array(
2799 - 'builder' => isset( $get_setting['builder'] ) ? $get_setting['builder'] : true,
2800 - 'template' => isset( $get_setting['template'] ) ? $get_setting['template'] : true,
2801 - 'gutenberg_builder' => isset( $get_setting['gutenberg_builder'] ) ? $get_setting['gutenberg_builder'] : true,
2802 - 'elementor_builder' => isset( $get_setting['elementor_builder'] ) ? $get_setting['elementor_builder'] : true,
2803 - 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : false,
2804 - 'debugger_mode' => isset( $get_setting['debugger_mode'] ) ? $get_setting['debugger_mode'] : false,
2805 - 'gutenberg_template' => isset( $get_setting['gutenberg_template'] ) ? $get_setting['gutenberg_template'] : true,
2806 - 'elementor_template' => isset( $get_setting['elementor_template'] ) ? $get_setting['elementor_template'] : true,
10778 + $setting_data = array(
10779 + 'builder' => isset( $get_setting['builder'] ) ? $get_setting['builder'] : true,
10780 + 'template' => isset( $get_setting['template'] ) ? $get_setting['template'] : true,
10781 + 'gutenberg_builder' => isset( $get_setting['gutenberg_builder'] ) ? $get_setting['gutenberg_builder'] : true,
10782 + 'gutenberg_core_builder' => isset( $get_setting['gutenberg_core_builder'] ) ? $get_setting['gutenberg_core_builder'] : false,
10783 + 'elementor_builder' => isset( $get_setting['elementor_builder'] ) ? $get_setting['elementor_builder'] : true,
10784 + 'bricks_builder' => isset( $get_setting['bricks_builder'] ) ? $get_setting['bricks_builder'] : true,
10785 + 'gutenberg_template' => isset( $get_setting['gutenberg_template'] ) ? $get_setting['gutenberg_template'] : true,
10786 + 'elementor_template' => isset( $get_setting['elementor_template'] ) ? $get_setting['elementor_template'] : true,
10787 + 'code_snippet' => isset( $get_setting['code_snippet'] ) ? $get_setting['code_snippet'] : true,
10788 + 'cross_copy_paste' => isset( $get_setting['cross_copy_paste'] ) ? $get_setting['cross_copy_paste'] : false,
10789 + 'cross_copy_paste_elementor' => isset( $get_setting['cross_copy_paste_elementor'] ) ? $get_setting['cross_copy_paste_elementor'] : false,
10790 + 'cross_copy_paste_gutenberg' => isset( $get_setting['cross_copy_paste_gutenberg'] ) ? $get_setting['cross_copy_paste_gutenberg'] : false,
10791 + 'cross_copy_paste_bricks' => isset( $get_setting['cross_copy_paste_bricks'] ) ? $get_setting['cross_copy_paste_bricks'] : false,
10792 + 'plugin_version' => $version_check,
2807 10793 );
10794 +
10795 + if ( isset( $get_setting['remove_db'] ) ) {
10796 + $setting_data['remove_db'] = $get_setting['remove_db'];
10797 + }
10798 +
10799 + if ( isset( $get_setting['debugger_mode'] ) ) {
10800 + $setting_data['debugger_mode'] = $get_setting['debugger_mode'];
10801 + }
10802 +
10803 + return $setting_data;
2808 10804 }
2809 10805
2810 10806 /**
10807 + * Updated White Label Data.
2811 10808 *
10809 + * @since 1.1.8
10810 + */
10811 + protected function wkit_white_label() {
10812 +
10813 + $get_wl_data = ! empty( $_POST['WhiteLabelData'] ) ? wp_unslash( $_POST['WhiteLabelData'] ) : array();
10814 +
10815 + if ( ! empty( $get_wl_data ) ) {
10816 + $white_label_data = json_decode( $get_wl_data, true );
10817 + $plugin_name = $white_label_data['plugin_name'];
10818 + } else {
10819 + $result = array(
10820 + 'success' => false,
10821 + 'message' => esc_html__( 'Data Not Found', 'wdesignkit' ),
10822 + );
10823 +
10824 + wp_send_json( $result );
10825 + wp_die();
10826 + }
10827 +
10828 + if ( ! empty( $plugin_name ) ) {
10829 + $get_white_label = get_option( 'wkit_white_label', false );
10830 + if ( ! empty( $get_white_label ) ) {
10831 + update_option( 'wkit_white_label', $white_label_data );
10832 + } else {
10833 + add_option( 'wkit_white_label', $white_label_data );
10834 + }
10835 + } else {
10836 + $result = array(
10837 + 'success' => false,
10838 + 'message' => esc_html__( 'Plugin Name Not Found', 'wdesignkit' ),
10839 + );
10840 +
10841 + wp_send_json( $result );
10842 + wp_die();
10843 + }
10844 +
10845 + $get_updated_data = get_option( 'wkit_white_label', false );
10846 + $response = array(
10847 + 'message' => __( 'Data Added successfully', 'wdesignkit' ),
10848 + 'success' => true,
10849 + 'data' => $get_updated_data,
10850 + );
10851 +
10852 + wp_send_json( $response );
10853 + }
10854 +
10855 + /**
10856 + * Reset White Label Data.
10857 + *
10858 + * @since 1.1.8
10859 + */
10860 + public function wkit_reset_wl() {
10861 + $wl_data = get_option( 'wkit_white_label' );
10862 +
10863 + if ( ! empty( $wl_data ) ) {
10864 + delete_option( 'wkit_white_label' );
10865 +
10866 + $result = array(
10867 + 'success' => true,
10868 + 'message' => esc_html__( 'Reset White Label Successfully', 'wdesignkit' ),
10869 + );
10870 +
10871 + wp_send_json( $result );
10872 + wp_die();
10873 + }
10874 + }
10875 +
10876 + /**
10877 + *
2812 10878 * Use for Add new licence key.
2813 10879 *
2814 10880 * @since 1.0.0
2815 10881 */
@@ -2822,9 +10888,38 @@
2822 10888 );
2823 10889
2824 10890 $response = $this->wkit_api_call( $args, 'wkit_activate_key' );
2825 10891
2826 - wp_send_json( $response['data'] );
10892 + if ( ! empty( $response['data'] ) ) {
10893 + $response = json_decode( wp_json_encode( $response['data'] ), true );
10894 +
10895 + if ( ! empty( $response['data']['tpae_licence'] ) && is_serialized( $response['data']['tpae_licence'] ) ) {
10896 + $response['data']['tpae_licence'] = unserialize( $response['data']['tpae_licence'], array( 'allowed_classes' => false ) );
10897 + }
10898 +
10899 + if ( ! empty( $response['data']['tpag_licence'] ) && is_serialized( $response['data']['tpag_licence'] ) ) {
10900 + $response['data']['tpag_licence'] = unserialize( $response['data']['tpag_licence'], array( 'allowed_classes' => false ) );
10901 + }
10902 +
10903 + if ( ! empty( $response['data']['uichemy_licence'] ) && is_serialized( $response['data']['uichemy_licence'] ) ) {
10904 + $response['data']['uichemy_licence'] = unserialize( $response['data']['uichemy_licence'], array( 'allowed_classes' => false ) );
10905 + }
10906 +
10907 + if ( ! empty( $response['data']['wdkit_licence'] ) && is_serialized( $response['data']['wdkit_licence'] ) ) {
10908 + $response['data']['wdkit_licence'] = unserialize( $response['data']['wdkit_licence'], array( 'allowed_classes' => false ) );
10909 +
10910 + // Store WDesignKit license status locally for quick access
10911 + if ( ! empty( $response['data']['wdkit_licence'] ) && is_array( $response['data']['wdkit_licence'] ) ) {
10912 + update_option( 'wdkit_licence_data', $response['data']['wdkit_licence'] );
10913 + }
10914 + }
10915 +
10916 + if ( ! empty( $response['data']['wdkit_licence_extra'] ) && is_serialized( $response['data']['wdkit_licence_extra'] ) ) {
10917 + $response['data']['wdkit_licence_extra'] = unserialize( $response['data']['wdkit_licence_extra'], array( 'allowed_classes' => false ) );
10918 + }
10919 + }
10920 +
10921 + wp_send_json( $response );
2827 10922 wp_die();
2828 10923 }
2829 10924
2830 10925 /**
@@ -2835,16 +10930,23 @@
2835 10930 */
2836 10931 protected function wdkit_delete_licence_key() {
2837 10932 $token = ! empty( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
2838 10933 $licencename = ! empty( $_POST['licencename'] ) ? sanitize_text_field( wp_unslash( $_POST['licencename'] ) ) : '';
10934 + $apikey = ! empty( $_POST['apikey'] ) ? sanitize_text_field( wp_unslash( $_POST['apikey'] ) ) : '';
2839 10935
2840 10936 $args = array(
2841 10937 'token' => $token,
2842 10938 'licencename' => $licencename,
10939 + 'apikey' => $apikey,
2843 10940 );
2844 10941
2845 10942 $response = $this->wkit_api_call( $args, 'licence_delete' );
2846 10943
10944 + // Remove local WDesignKit license data if deleting WDesignKit license
10945 + if ( 'wdkit' === $licencename ) {
10946 + delete_option( 'wdkit_licence_data' );
10947 + }
10948 +
2847 10949 wp_send_json( $response['data'] );
2848 10950 wp_die();
2849 10951 }
2850 10952
@@ -2856,12 +10958,16 @@
2856 10958 */
2857 10959 protected function wdkit_sync_licence_key() {
2858 10960 $token = ! empty( $_POST['token'] ) ? sanitize_text_field( wp_unslash( $_POST['token'] ) ) : '';
2859 10961 $licencename = ! empty( $_POST['licencename'] ) ? sanitize_text_field( wp_unslash( $_POST['licencename'] ) ) : '';
10962 + // Needed to identify which extra-credit key to sync (wdkit_extra / wdkit_ai_extra
10963 + // are arrays matched by the api key's last digits on the server).
10964 + $apikey = ! empty( $_POST['apikey'] ) ? sanitize_text_field( wp_unslash( $_POST['apikey'] ) ) : '';
2860 10965
2861 10966 $args = array(
2862 10967 'token' => $token,
2863 10968 'licencename' => $licencename,
10969 + 'apikey' => $apikey,
2864 10970 );
2865 10971
2866 10972 $response = $this->wkit_api_call( $args, 'licence_sync' );
2867 10973
@@ -2869,15 +10975,126 @@
2869 10975 wp_die();
2870 10976 }
2871 10977
2872 10978 /**
10979 + * Rollback to Previous Versions
2873 10980 *
10981 + * @since 1.1.0
10982 + */
10983 + protected function wdkit_prev_version() {
10984 +
10985 + require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
10986 +
10987 + $plugin_info = plugins_api(
10988 + 'plugin_information',
10989 + array(
10990 + 'slug' => 'wdesignkit',
10991 + )
10992 + );
10993 +
10994 + if ( empty( $plugin_info->versions ) || ! is_array( $plugin_info->versions ) ) {
10995 + return array();
10996 + }
10997 +
10998 + krsort( $plugin_info->versions );
10999 +
11000 + $versions_list = array();
11001 + $index = 0;
11002 +
11003 + foreach ( $plugin_info->versions as $version => $download_link ) {
11004 +
11005 + $lowercase_version = strtolower( $version );
11006 +
11007 + $is_valid_version = ! preg_match( '/(beta|rc|trunk|dev)/i', $lowercase_version );
11008 +
11009 + $is_valid_version = apply_filters( 'wdkit_check_rollback_version', $is_valid_version, $lowercase_version );
11010 +
11011 + if ( ! $is_valid_version || version_compare( $version, WDKIT_VERSION, '>=' ) ) {
11012 + continue;
11013 + }
11014 +
11015 + $versions_list[] = $version;
11016 + ++$index;
11017 + }
11018 +
11019 + // set_transient( 'wdkit_rollback_version_' . WDKIT_VERSION, $versions_list, WEEK_IN_SECONDS );
11020 +
11021 + return $versions_list;
11022 + }
11023 +
11024 + /**
11025 + * Rollback to Previous Versions
11026 + *
11027 + * @since 1.1.0
11028 + */
11029 + protected function wdkit_rollback_check() {
11030 +
11031 + $current_ver = isset( $_POST['version'] ) ? sanitize_text_field( wp_unslash( $_POST['version'] ) ) : '';
11032 + $rv = $this->wdkit_prev_version();
11033 +
11034 + if ( empty( $current_ver ) || ! in_array( $current_ver, $rv ) ) {
11035 + return array(
11036 + 'message' => esc_html__( 'Invalid Nonce or version not found', 'wdesignkit' ),
11037 + 'status' => 'error',
11038 + 'success' => false,
11039 + );
11040 + }
11041 +
11042 + $plugin_slug = basename( WDKIT_PBNAME, '.php' );
11043 +
11044 + $this_version = $current_ver;
11045 + $this_pluginname = WDKIT_PBNAME;
11046 + $this_plugin_u_r_l = sprintf( 'https://downloads.wordpress.org/plugin/%s.%s.zip', $plugin_slug, $this_version );
11047 +
11048 + $plugin_info = new \stdClass();
11049 + $plugin_info->new_version = $this_version;
11050 + $plugin_info->slug = $plugin_slug;
11051 + $plugin_info->package = $this_plugin_u_r_l;
11052 + $plugin_info->url = 'https://wdesignkit.com/';
11053 +
11054 + $update_plugins_data = get_site_transient( 'update_plugins' );
11055 +
11056 + if ( ! is_object( $update_plugins_data ) ) {
11057 + $update_plugins_data = new \stdClass();
11058 + }
11059 +
11060 + $update_plugins_data->response[ $this_pluginname ] = $plugin_info;
11061 +
11062 + set_site_transient( 'update_plugins', $update_plugins_data );
11063 +
11064 + require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
11065 +
11066 + $logo_url = WDKIT_URL . 'assets/images/jpg/Wdesignkit-logo.png';
11067 +
11068 + $args = array(
11069 + 'url' => 'update.php?action=upgrade-plugin&plugin=' . rawurlencode( $this_pluginname ),
11070 + 'plugin' => $this_pluginname,
11071 + 'nonce' => 'upgrade-plugin_' . $this_pluginname,
11072 + 'title' => '<img src="' . esc_url( $logo_url ) . '" alt="wdesignkit-logo"><div class="theplus-rb-subtitle">' . esc_html__( 'Rollback to Previous Version', 'wdesignkit' ) . '</div>',
11073 + );
11074 +
11075 + $upgrader_plugin = new \Plugin_Upgrader( new \Plugin_Upgrader_Skin( $args ) );
11076 + $upgrader_plugin->upgrade( $this_pluginname );
11077 +
11078 + activate_plugin( $this_pluginname );
11079 +
11080 + return array(
11081 + 'message' => esc_html__( 'Rollback Successful, Plugin Re-activated', 'wdesignkit' ),
11082 + 'status' => 'Success',
11083 + 'success' => true,
11084 + );
11085 + }
11086 +
11087 + /**
11088 + *
2874 11089 * It is Use for logout.
2875 11090 *
2876 11091 * @since 1.0.0
2877 11092 */
2878 11093 protected function wdkit_logout() {
2879 - $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false;
11094 + $email = isset( $_POST['email'] ) ? strtolower( sanitize_email( wp_unslash( $_POST['email'] ) ) ) : false;
11095 + $logout_type = isset( $_POST['logout_type'] ) ? strtolower( sanitize_text_field( wp_unslash( $_POST['logout_type'] ) ) ) : '';
11096 +
2880 11097 $response = '';
2881 11098
2882 11099 if ( ! empty( $email ) ) {
2883 11100 $token = $this->wdkit_login_user_token( $email );
@@ -2882,10 +11099,21 @@
2882 11099 if ( ! empty( $email ) ) {
2883 11100 $token = $this->wdkit_login_user_token( $email );
2884 11101 $args = array( 'token' => $token );
2885 11102
2886 - delete_transient( 'wdkit_auth_' . $email );
2887 - $response = WDesignKit_Data_Query::get_data( 'logout', $args );
11103 + if ( 'session' !== $logout_type ) {
11104 + delete_transient( 'wdkit_auth_' . wdesignkit_cloud_session_key( $email ) );
11105 +
11106 + /* Logged out means the site no longer acts for this account - see
11107 + * Wdkit_Import_Remote::forget_registration(). */
11108 + if ( class_exists( 'Wdkit_Import_Remote' ) ) {
11109 + Wdkit_Import_Remote::forget_registration();
11110 + }
11111 +
11112 + // Clear stored license data on logout so banner shows again
11113 + delete_option( 'wdkit_licence_data' );
11114 + $response = WDesignKit_Data_Query::get_data( 'logout', $args );
11115 + }
2888 11116 }
2889 11117
2890 11118 wp_send_json( $response );
2891 11119 wp_die();
@@ -2901,9 +11129,9 @@
2901 11129 */
2902 11130 protected function wdkit_login_user_token( $email = '' ) {
2903 11131
2904 11132 if ( ! empty( $email ) ) {
2905 - $user_key = strstr( $email, '@', true );
11133 + $user_key = wdesignkit_cloud_session_key( $email );
2906 11134 $get_login = get_transient( 'wdkit_auth_' . $user_key );
2907 11135
2908 11136 if ( ! empty( $get_login ) && ! empty( $get_login['token'] ) ) {
2909 11137 return $get_login['token'];
@@ -2921,9 +11149,9 @@
2921 11149 * @param string $data send all post data.
2922 11150 * @param string $type store text data.
2923 11151 * @param string $condition store text data.
2924 11152 */
2925 - protected function wdkit_sanitizer_bypass( $data, $type, $condition = 'none' ) {
11153 + protected function wdkit_extract_post_field( $data, $type, $condition = 'none' ) {
2926 11154
2927 11155 if ( 'none' === $condition ) {
2928 11156 return $data[ $type ];
2929 11157 } elseif ( 'cr_widget' === $condition ) {
@@ -2928,26 +11156,13 @@
2928 11156 return $data[ $type ];
2929 11157 } elseif ( 'cr_widget' === $condition ) {
2930 11158 return $data[ $type ];
2931 11159 }
11160 +
11161 + return null;
2932 11162 }
2933 11163
2934 - /**
2935 - * Parse args $_POST
2936 - *
2937 - * @since 1.0.0
2938 - *
2939 - * @param string $data send all post data.
2940 - * @param string $type store text data.
2941 - * @param string $condition store text data.
2942 - */
2943 - protected function wdkit_file_sanitizer_bypass( $data, $type, $condition = 'none' ) {
2944 11164
2945 - if ( 'name' === $condition ) {
2946 - return wp_normalize_path( $data[ $type ]['tmp_name'] );
2947 - }
2948 - }
2949 -
2950 11165 /**
2951 11166 * Parse args $_POST
2952 11167 *
2953 11168 * @since 1.0.0
@@ -3023,8 +11238,28 @@
3023 11238 if ( isset( $data['plugin'] ) ) {
3024 11239 $args['plugin'] = isset( $data['plugin'] ) ? wp_unslash( $data['plugin'] ) : array();
3025 11240 }
3026 11241
11242 + if ( isset( $data['plugin_exclude'] ) ) {
11243 + $args['plugin_exclude'] = isset( $data['plugin_exclude'] ) ? wp_unslash( $data['plugin_exclude'] ) : array();
11244 + }
11245 +
11246 + if ( isset( $data['ai_compatibility'] ) ) {
11247 + $args['ai_compatibility'] = isset( $data['ai_compatibility'] ) ? wp_unslash( $data['ai_compatibility'] ) : array();
11248 + }
11249 +
11250 + // if ( isset( $data['global_color'] ) ) {
11251 + // $args['global_color'] = isset( $data['global_color'] ) ? wp_unslash( $data['global_color'] ) : array();
11252 + // }
11253 +
11254 + // if ( isset( $data['global_font_family'] ) ) {
11255 + // $args['global_font_family'] = isset( $data['global_font_family'] ) ? wp_unslash( $data['global_font_family'] ) : array();
11256 + // }
11257 +
11258 + if ( isset( $data['global_data'] ) ) {
11259 + $args['global_data'] = isset( $data['global_data'] ) ? wp_unslash( $data['global_data'] ) : array();
11260 + }
11261 +
3027 11262 if ( isset( $data['tag'] ) ) {
3028 11263 $args['tag'] = isset( $data['tag'] ) ? wp_unslash( $data['tag'] ) : array();
3029 11264 }
3030 11265
@@ -3053,8 +11288,596 @@
3053 11288 }
3054 11289
3055 11290 return $args;
3056 11291 }
11292 +
11293 + /**
11294 + * Dark Mode
11295 + *
11296 + * @since 2.0.0
11297 + *
11298 + * @param string store darkmode value in database.
11299 + */
11300 + protected function wdkit_dark_mode() {
11301 + $dark_mode = ! empty( $_POST['dark_mode'] ) ? sanitize_text_field( $_POST['dark_mode'] ) : 'light';
11302 +
11303 + if ( get_option( 'wdkit_dark_mode' ) ) {
11304 + update_option( 'wdkit_dark_mode', $dark_mode );
11305 + } else {
11306 + add_option( 'wdkit_dark_mode', $dark_mode );
11307 + }
11308 +
11309 + $response = array(
11310 + 'message' => esc_html__( 'Dark Mode Updated', 'wdesignkit' ),
11311 + 'status' => 'Success',
11312 + 'success' => true,
11313 + );
11314 +
11315 + wp_send_json( $response );
11316 + wp_die();
11317 + }
11318 +
11319 + /**
11320 + * The Gutenberg global-options record a site starts from.
11321 + *
11322 + * Nexter has no defaults of its own until something writes `tpgb_global_options`, and
11323 + * this is what WDesignKit seeds it with: five base colours, the base type scale, the
11324 + * gradients, spacing and box-shadow entries, and an empty container width.
11325 + *
11326 + * Extracted from wdkit_get_global_val() so the PHP import runner can seed the same
11327 + * record. The runner appends the kit's palette to whatever the site already has, and on
11328 + * a site where nothing had written the option yet it was appending to NOTHING — a
11329 + * headless import came out with 13 colours instead of 18, 12 type entries instead of 19
11330 + * and no gradients, spacing or box shadows at all, because the baseline it should have
11331 + * built on did not exist. The wizard never saw it: the globals screen calls
11332 + * wdkit_get_global_val() first, which seeds the option as a side effect.
11333 + *
11334 + * @since 2.7.2
11335 + *
11336 + * @return array
11337 + */
11338 + public static function wdkit_default_gutenberg_globals() {
11339 + return array(
11340 + 'active' => 'preset1',
11341 + 'darkMode' => 'none',
11342 + 'presets' => array(
11343 + 'preset1' => array(
11344 + 'name' => 'Preset 1',
11345 + 'key' => 'preset1',
11346 + 'colors' => array(
11347 + array(
11348 + 'label' => 'Primary',
11349 + 'value' => '#8072FC',
11350 + ),
11351 + array(
11352 + 'label' => 'Secondary',
11353 + 'value' => '#6FC784',
11354 + ),
11355 + array(
11356 + 'label' => 'Tertiary',
11357 + 'value' => '#FF5A6E',
11358 + ),
11359 + array(
11360 + 'label' => 'Accent',
11361 + 'value' => '#F3F3F3',
11362 + ),
11363 + array(
11364 + 'label' => 'Background',
11365 + 'value' => '#888888',
11366 + ),
11367 + ),
11368 + 'gradient' => array(
11369 + array(
11370 + 'label' => 'Primary',
11371 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11372 + ),
11373 +
11374 + array(
11375 + 'label' => 'Secondary',
11376 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11377 + ),
11378 +
11379 + array(
11380 + 'label' => 'Tertiary',
11381 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11382 + ),
11383 +
11384 + array(
11385 + 'label' => 'Accent',
11386 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11387 + ),
11388 +
11389 + array(
11390 + 'label' => 'Background',
11391 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11392 + ),
11393 + ),
11394 + 'spacing' => array(
11395 + array(
11396 + 'label' => 'Large',
11397 + 'value' => array(
11398 + 'md' => 70,
11399 + 'unit' => 'px',
11400 + ),
11401 + ),
11402 + array(
11403 + 'label' => 'Medium',
11404 + 'value' => array(
11405 + 'md' => 40,
11406 + 'unit' => 'px',
11407 + ),
11408 + ),
11409 + array(
11410 + 'label' => 'Small',
11411 + 'value' => array(
11412 + 'md' => 20,
11413 + 'unit' => 'px',
11414 + ),
11415 +
11416 + ),
11417 + ),
11418 + 'typography' => array(
11419 + array(
11420 + 'label' => 'Display Text',
11421 + 'value' => array(
11422 + 'openTypography' => 1,
11423 + 'size' => array(
11424 + 'md' => 65,
11425 + 'unit' => 'px',
11426 + ),
11427 + 'height' => array(
11428 + 'md' => 75,
11429 + 'unit' => 'px',
11430 + ),
11431 + 'fontFamily' => array(
11432 + 'family' => 'Roboto',
11433 + 'type' => 'sans-serif',
11434 + 'fontWeight' => 700,
11435 + ),
11436 + 'spacing' => array(
11437 + 'md' => 0,
11438 + 'unit' => 'px',
11439 + ),
11440 + ),
11441 + ),
11442 + array(
11443 + 'label' => 'Headline',
11444 + 'value' => array(
11445 + 'openTypography' => 1,
11446 + 'size' => array(
11447 + 'md' => 45,
11448 + 'unit' => 'px',
11449 + ),
11450 + 'height' => array(
11451 + 'md' => 60,
11452 + 'unit' => 'px',
11453 + ),
11454 + 'fontFamily' => array(
11455 + 'family' => 'Roboto',
11456 + 'type' => 'sans-serif',
11457 + 'fontWeight' => 700,
11458 + ),
11459 + 'spacing' => array(
11460 + 'md' => 0,
11461 + 'unit' => 'px',
11462 + ),
11463 + ),
11464 + ),
11465 + array(
11466 + 'label' => 'Sub Headline',
11467 + 'value' => array(
11468 + 'openTypography' => 1,
11469 + 'size' => array(
11470 + 'md' => 38,
11471 + 'unit' => 'px',
11472 + ),
11473 + 'height' => array(
11474 + 'md' => 45,
11475 + 'unit' => 'px',
11476 + ),
11477 + 'fontFamily' => array(
11478 + 'family' => 'Roboto',
11479 + 'type' => 'sans-serif',
11480 + 'fontWeight' => 500,
11481 + ),
11482 + 'spacing' => array(
11483 + 'md' => 0,
11484 + 'unit' => 'px',
11485 + ),
11486 + ),
11487 + ),
11488 + array(
11489 + 'label' => 'Title 1',
11490 + 'value' => array(
11491 + 'openTypography' => 1,
11492 + 'size' => array(
11493 + 'md' => 30,
11494 + 'unit' => 'px',
11495 + ),
11496 + 'height' => array(
11497 + 'md' => 40,
11498 + 'unit' => 'px',
11499 + ),
11500 + 'fontFamily' => array(
11501 + 'family' => 'Roboto',
11502 + 'type' => 'sans-serif',
11503 + 'fontWeight' => 500,
11504 + ),
11505 + 'spacing' => array(
11506 + 'md' => 0,
11507 + 'unit' => 'px',
11508 + ),
11509 + ),
11510 + ),
11511 + array(
11512 + 'label' => 'Title 2',
11513 + 'value' => array(
11514 + 'openTypography' => 1,
11515 + 'size' => array(
11516 + 'md' => 25,
11517 + 'unit' => 'px',
11518 + ),
11519 + 'height' => array(
11520 + 'md' => 30,
11521 + 'unit' => 'px',
11522 + ),
11523 + 'fontFamily' => array(
11524 + 'family' => 'Roboto',
11525 + 'type' => 'sans-serif',
11526 + 'fontWeight' => 400,
11527 + ),
11528 + 'spacing' => array(
11529 + 'md' => 0,
11530 + 'unit' => 'px',
11531 + ),
11532 + ),
11533 + ),
11534 + array(
11535 + 'label' => 'Body',
11536 + 'value' => array(
11537 + 'openTypography' => 1,
11538 + 'size' => array(
11539 + 'md' => 17,
11540 + 'unit' => 'px',
11541 + ),
11542 + 'height' => array(
11543 + 'md' => 22,
11544 + 'unit' => 'px',
11545 + ),
11546 + 'fontFamily' => array(
11547 + 'family' => 'Roboto',
11548 + 'type' => 'sans-serif',
11549 + 'fontWeight' => 400,
11550 + ),
11551 + 'spacing' => array(
11552 + 'md' => 0,
11553 + 'unit' => 'px',
11554 + ),
11555 + ),
11556 + ),
11557 + array(
11558 + 'label' => 'Captions',
11559 + 'value' => array(
11560 + 'openTypography' => 1,
11561 + 'size' => array(
11562 + 'md' => 13,
11563 + 'unit' => 'px',
11564 + ),
11565 + 'height' => array(
11566 + 'md' => 16,
11567 + 'unit' => 'px',
11568 + ),
11569 + 'fontFamily' => array(
11570 + 'family' => 'Roboto',
11571 + 'type' => 'sans-serif',
11572 + 'fontWeight' => 400,
11573 + ),
11574 + 'spacing' => array(
11575 + 'md' => 0,
11576 + 'unit' => 'px',
11577 + ),
11578 + ),
11579 + ),
11580 + ),
11581 + 'boxshadow' => array(
11582 + array(
11583 + 'label' => 'Normal Shadow',
11584 + 'value' => array(
11585 + 'openShadow' => 1,
11586 + 'inset' => 0,
11587 + 'horizontal' => 2,
11588 + 'vertical' => 6,
11589 + 'blur' => 10,
11590 + 'spread' => 0,
11591 + 'color' => 'rgba(0,0,0,0.15)',
11592 + ),
11593 + ),
11594 + array(
11595 + 'label' => 'Hover Shadow',
11596 + 'value' => array(
11597 + 'openShadow' => 1,
11598 + 'inset' => 0,
11599 + 'horizontal' => 2,
11600 + 'vertical' => 5,
11601 + 'blur' => 14,
11602 + 'spread' => 3,
11603 + 'color' => 'rgba(0,0,0,0.2)',
11604 + ),
11605 + ),
11606 + ),
11607 + ),
11608 + 'preset2' => array(
11609 + 'name' => 'Preset 2',
11610 + 'key' => 'preset2',
11611 + 'colors' => array(
11612 + array(
11613 + 'label' => 'Primary',
11614 + 'value' => '#8072FC',
11615 + ),
11616 + array(
11617 + 'label' => 'Secondary',
11618 + 'value' => '#6FC784',
11619 + ),
11620 + array(
11621 + 'label' => 'Tertiary',
11622 + 'value' => '#FF5A6E',
11623 + ),
11624 + array(
11625 + 'label' => 'Accent',
11626 + 'value' => '#F3F3F3',
11627 + ),
11628 + array(
11629 + 'label' => 'Background',
11630 + 'value' => '#888888',
11631 + ),
11632 + ),
11633 + 'gradient' => array(
11634 + array(
11635 + 'label' => 'Primary',
11636 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11637 + ),
11638 +
11639 + array(
11640 + 'label' => 'Secondary',
11641 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11642 + ),
11643 +
11644 + array(
11645 + 'label' => 'Tertiary',
11646 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11647 + ),
11648 +
11649 + array(
11650 + 'label' => 'Accent',
11651 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11652 + ),
11653 +
11654 + array(
11655 + 'label' => 'Background',
11656 + 'value' => 'linear-gradient(135deg,rgb(8,148,229) 0%,rgb(155,81,224) 100%)',
11657 + ),
11658 + ),
11659 + 'spacing' => array(
11660 + array(
11661 + 'label' => 'Large',
11662 + 'value' => array(
11663 + 'md' => 70,
11664 + 'unit' => 'px',
11665 + ),
11666 + ),
11667 + array(
11668 + 'label' => 'Medium',
11669 + 'value' => array(
11670 + 'md' => 40,
11671 + 'unit' => 'px',
11672 + ),
11673 + ),
11674 + array(
11675 + 'label' => 'Small',
11676 + 'value' => array(
11677 + 'md' => 20,
11678 + 'unit' => 'px',
11679 + ),
11680 +
11681 + ),
11682 + ),
11683 + 'typography' => array(
11684 + array(
11685 + 'label' => 'Display Text',
11686 + 'value' => array(
11687 + 'openTypography' => 1,
11688 + 'size' => array(
11689 + 'md' => 65,
11690 + 'unit' => 'px',
11691 + ),
11692 + 'height' => array(
11693 + 'md' => 75,
11694 + 'unit' => 'px',
11695 + ),
11696 + 'fontFamily' => array(
11697 + 'family' => 'Roboto',
11698 + 'type' => 'sans-serif',
11699 + 'fontWeight' => 700,
11700 + ),
11701 + 'spacing' => array(
11702 + 'md' => 0,
11703 + 'unit' => 'px',
11704 + ),
11705 + ),
11706 + ),
11707 + array(
11708 + 'label' => 'Headline',
11709 + 'value' => array(
11710 + 'openTypography' => 1,
11711 + 'size' => array(
11712 + 'md' => 45,
11713 + 'unit' => 'px',
11714 + ),
11715 + 'height' => array(
11716 + 'md' => 60,
11717 + 'unit' => 'px',
11718 + ),
11719 + 'fontFamily' => array(
11720 + 'family' => 'Roboto',
11721 + 'type' => 'sans-serif',
11722 + 'fontWeight' => 700,
11723 + ),
11724 + 'spacing' => array(
11725 + 'md' => 0,
11726 + 'unit' => 'px',
11727 + ),
11728 + ),
11729 + ),
11730 + array(
11731 + 'label' => 'Sub Headline',
11732 + 'value' => array(
11733 + 'openTypography' => 1,
11734 + 'size' => array(
11735 + 'md' => 38,
11736 + 'unit' => 'px',
11737 + ),
11738 + 'height' => array(
11739 + 'md' => 45,
11740 + 'unit' => 'px',
11741 + ),
11742 + 'fontFamily' => array(
11743 + 'family' => 'Roboto',
11744 + 'type' => 'sans-serif',
11745 + 'fontWeight' => 500,
11746 + ),
11747 + 'spacing' => array(
11748 + 'md' => 0,
11749 + 'unit' => 'px',
11750 + ),
11751 + ),
11752 + ),
11753 + array(
11754 + 'label' => 'Title 1',
11755 + 'value' => array(
11756 + 'openTypography' => 1,
11757 + 'size' => array(
11758 + 'md' => 30,
11759 + 'unit' => 'px',
11760 + ),
11761 + 'height' => array(
11762 + 'md' => 40,
11763 + 'unit' => 'px',
11764 + ),
11765 + 'fontFamily' => array(
11766 + 'family' => 'Roboto',
11767 + 'type' => 'sans-serif',
11768 + 'fontWeight' => 500,
11769 + ),
11770 + 'spacing' => array(
11771 + 'md' => 0,
11772 + 'unit' => 'px',
11773 + ),
11774 + ),
11775 + ),
11776 + array(
11777 + 'label' => 'Title 2',
11778 + 'value' => array(
11779 + 'openTypography' => 1,
11780 + 'size' => array(
11781 + 'md' => 25,
11782 + 'unit' => 'px',
11783 + ),
11784 + 'height' => array(
11785 + 'md' => 30,
11786 + 'unit' => 'px',
11787 + ),
11788 + 'fontFamily' => array(
11789 + 'family' => 'Roboto',
11790 + 'type' => 'sans-serif',
11791 + 'fontWeight' => 400,
11792 + ),
11793 + 'spacing' => array(
11794 + 'md' => 0,
11795 + 'unit' => 'px',
11796 + ),
11797 + ),
11798 + ),
11799 + array(
11800 + 'label' => 'Body',
11801 + 'value' => array(
11802 + 'openTypography' => 1,
11803 + 'size' => array(
11804 + 'md' => 17,
11805 + 'unit' => 'px',
11806 + ),
11807 + 'height' => array(
11808 + 'md' => 22,
11809 + 'unit' => 'px',
11810 + ),
11811 + 'fontFamily' => array(
11812 + 'family' => 'Roboto',
11813 + 'type' => 'sans-serif',
11814 + 'fontWeight' => 400,
11815 + ),
11816 + 'spacing' => array(
11817 + 'md' => 0,
11818 + 'unit' => 'px',
11819 + ),
11820 + ),
11821 + ),
11822 + array(
11823 + 'label' => 'Captions',
11824 + 'value' => array(
11825 + 'openTypography' => 1,
11826 + 'size' => array(
11827 + 'md' => 13,
11828 + 'unit' => 'px',
11829 + ),
11830 + 'height' => array(
11831 + 'md' => 16,
11832 + 'unit' => 'px',
11833 + ),
11834 + 'fontFamily' => array(
11835 + 'family' => 'Roboto',
11836 + 'type' => 'sans-serif',
11837 + 'fontWeight' => 400,
11838 + ),
11839 + 'spacing' => array(
11840 + 'md' => 0,
11841 + 'unit' => 'px',
11842 + ),
11843 + ),
11844 + ),
11845 + ),
11846 + 'boxshadow' => array(
11847 + array(
11848 + 'label' => 'Normal Shadow',
11849 + 'value' => array(
11850 + 'openShadow' => 1,
11851 + 'inset' => 0,
11852 + 'horizontal' => 2,
11853 + 'vertical' => 6,
11854 + 'blur' => 10,
11855 + 'spread' => 0,
11856 + 'color' => 'rgba(0,0,0,0.15)',
11857 + ),
11858 + ),
11859 + array(
11860 + 'label' => 'Hover Shadow',
11861 + 'value' => array(
11862 + 'openShadow' => 1,
11863 + 'inset' => 0,
11864 + 'horizontal' => 2,
11865 + 'vertical' => 5,
11866 + 'blur' => 14,
11867 + 'spread' => 3,
11868 + 'color' => 'rgba(0,0,0,0.2)',
11869 + ),
11870 + ),
11871 + ),
11872 + ),
11873 + ),
11874 + 'globalContainer' => array(
11875 + 'md' => '',
11876 + 'unit' => 'px',
11877 + ),
11878 + );
11879 + }
3057 11880 }
3058 11881
3059 11882 Wdkit_Api_Call::get_instance();
3060 -}
11883 +}