PluginProbe
WDesignKit – AI Templates, Widget Builder & MCP Workflow / 2.7.1
WDesignKit – AI Templates, Widget Builder & MCP Workflow v2.7.1
2.7.1 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 All 130 releases
← All changes | includes/admin/hooks/class-wdkit-kit-import-hook.php +77 -277 2.6.4 → 2.7.1 View file →
@@ -79,9 +79,9 @@
79 79 ];
80 80 }
81 81
82 82 $token = wdkit_kit_import_resolve_token();
83 - if ( $token === '' ) {
83 + if ( ! wdkit_kit_import_can_authenticate( $token ) ) {
84 84 return [
85 85 'success' => false,
86 86 'message' => __( 'Not logged in to WDesignKit cloud. Go to WP Admin → WDesignKit and click Login.', 'wdesignkit' ),
87 87 'description' => '',
@@ -110,15 +110,17 @@
110 110 }
111 111
112 112 // Mirrors $temp_args in wdkit_import_kit_template() exactly — only these
113 113 // five keys are sent to the cloud for a kit import.
114 - $cloud_args = [
115 - 'token' => $token,
116 - 'template_id' => $template_id,
117 - 'editor' => $editor,
118 - 'website_kit' => $website_kit,
119 - 'unique_id' => get_option( 'wdkit_unique_id', '' ),
120 - ];
114 + $cloud_args = wdkit_kit_import_with_site_identity(
115 + [
116 + 'token' => $token,
117 + 'template_id' => $template_id,
118 + 'editor' => $editor,
119 + 'website_kit' => $website_kit,
120 + 'unique_id' => get_option( 'wdkit_unique_id', '' ),
121 + ]
122 + );
121 123
122 124 /**
123 125 * Fires just before WDesignKit makes the cloud import request.
124 126 *
@@ -228,10 +230,25 @@
228 230 * @type string $site_name Optional site title.
229 231 * @type string $tagline Optional site tagline.
230 232 * @type string[] $skip Steps to skip: 'reset_site', 'plugin_settings',
231 233 * 'theme_settings', 'enable_widgets'.
234 + * @type string $session_id Optional. Pass a session id from a previous response to RESUME
235 + * that import: templates that already landed are skipped and only
236 + * the unfinished ones run. Omit to start a fresh import.
237 + *
238 + * Optional and additive — omitting all of these gives exactly the previous behaviour:
239 + * @type string $import_type 'normal_import' | 'ai_import'.
240 + * @type string $site_type Business/site type (required when import_type=ai_import).
241 + * @type string $site_description Site description (required when import_type=ai_import).
242 + * @type array $ai_document Pre-generated AI content: {pages, products, posts, taxonomy}.
243 + * @type array $images Chosen stock images: [{url,width,height}].
244 + * @type array $products Product records for WooCommerce.
245 + * @type bool $blog_post Import blog posts (default true).
246 + * @type array $plugin_catalogue Catalogue the templates' plugins_id resolve against.
247 + * @type array $site_global Site-level globals (container width, body background).
248 + * @type bool $allow_destructive_cleanup Default false. Gates every permanent deletion.
232 249 * }
233 - * @return array{success:bool,message:string,site_url:string,home_page_id:int,pages:array,steps:array,errors:array}
250 + * @return array{success:bool,message:string,site_url:string,home_page_id:int,shop_page_id:int,pages:array,steps:array,errors:array,session_id:string,status:string}
234 251 */
235 252 function wdkit_handle_create_full_site( array $output, array $args ): array {
236 253
237 254 // ── Guard ────────────────────────────────────────────────────────────────
@@ -243,289 +260,72 @@
243 260 return wdkit_site_error( 'Insufficient permissions.' );
244 261 }
245 262
246 263 $token = wdkit_kit_import_resolve_token();
247 - if ( $token === '' ) {
264 + if ( ! wdkit_kit_import_can_authenticate( $token ) ) {
248 265 return wdkit_site_error( 'Not logged in to WDesignKit cloud. Go to WP Admin → WDesignKit and click Login.' );
249 266 }
250 267
251 - // ── Sanitize args ────────────────────────────────────────────────────────
252 - $kit_id = sanitize_text_field( (string) ( $args['kit_id'] ?? '' ) );
253 - $editor = sanitize_text_field( (string) ( $args['editor'] ?? 'elementor' ) );
254 - $templates = is_array( $args['templates'] ?? null ) ? $args['templates'] : [];
255 - $site_name = sanitize_text_field( (string) ( $args['site_name'] ?? '' ) );
256 - $tagline = sanitize_text_field( (string) ( $args['tagline'] ?? '' ) );
257 - $skip = is_array( $args['skip'] ?? null ) ? array_map( 'sanitize_text_field', $args['skip'] ) : [];
258 -
259 - if ( $kit_id === '' ) {
260 - return wdkit_site_error( 'kit_id is required.' );
268 + if ( ! class_exists( 'Wdkit_Import_Bridge' ) ) {
269 + return wdkit_site_error( 'Import engine is not available.' );
261 270 }
262 271
263 - if ( empty( $templates ) ) {
264 - return wdkit_site_error( 'templates array is required and must not be empty.' );
265 - }
272 + // ── Delegate ─────────────────────────────────────────────────────────────
273 + // The three guards above are unchanged and still run first, in the same order, so an
274 + // unauthenticated or under-privileged caller gets the same refusal it always got.
275 + //
276 + // Everything after them used to be ~280 lines of inline sequence here. It is now
277 + // Wdkit_Import_Runner, reached through Wdkit_Import_Bridge, which preserves this
278 + // function's args, its `wdkit_site_step` events and its return shape. What the caller
279 + // gains is the work the inline version could not do: AI merge, image substitution and
280 + // sideload, taxonomy, WooCommerce products, blog posts, navigation rewriting, global
281 + // colours and typography, theme-builder conditions, and resume after a failure.
282 + //
283 + // Resume: the response now carries `session_id`. Passing it back as `session_id` on a
284 + // later call re-runs only the templates that did not finish.
285 + return Wdkit_Import_Bridge::create_full_site( $args );
286 +}
266 287
267 - // ── State ────────────────────────────────────────────────────────────────
268 - $steps = [];
269 - $errors = [];
270 - $pages = [];
271 - $home_page_id = 0;
272 - $shop_page_id = 0;
288 +// ════════════════════════════════════════════════════════════════════════════
289 +// Shared helpers
290 +// ════════════════════════════════════════════════════════════════════════════
273 291
274 - // ── Step 1: Reset site — draft all existing published pages ──────────────
275 - if ( ! in_array( 'reset_site', $skip, true ) ) {
276 - do_action( 'wdkit_site_step', 'reset_site', 'start', [] );
277 -
278 - do_action( 'nxt_update_builder_status', 'all' );
279 -
280 - $existing = get_posts( [
281 - 'post_type' => 'page',
282 - 'post_status' => 'publish',
283 - 'numberposts' => -1,
284 - 'fields' => 'ids',
285 - ] );
286 -
287 - foreach ( $existing as $pid ) {
288 - wp_update_post( [ 'ID' => $pid, 'post_status' => 'draft' ] );
289 - }
290 -
291 - $steps['reset_site'] = [ 'success' => true ];
292 - do_action( 'wdkit_site_step', 'reset_site', 'done', [ 'drafted' => count( $existing ) ] );
292 +/**
293 + * Add this site's own credential to a kit-content request.
294 + *
295 + * Kit content is normally authenticated with the signed-in account's JWT. A sandbox has no
296 + * signed-in account — it is created for the user, not by them — so the cloud accepts the site's
297 + * poll token instead, which already identifies both the site and the account that owns it.
298 + *
299 + * Sent alongside the JWT rather than in place of it: when both are present the cloud prefers the
300 + * JWT, so an ordinary logged-in site behaves exactly as before.
301 + *
302 + * @param array $args Cloud request arguments.
303 + * @return array The same arguments, with poll_token and site_url added when this site has one.
304 + */
305 +function wdkit_kit_import_with_site_identity( array $args ): array {
306 + if ( ! class_exists( 'Wdkit_Import_Remote' ) ) {
307 + return $args;
293 308 }
294 309
295 - // ── Step 2: Plugin settings — Elementor options ──────────────────────────
296 - if ( ! in_array( 'plugin_settings', $skip, true ) && $editor === 'elementor' ) {
297 - do_action( 'wdkit_site_step', 'plugin_settings', 'start', [] );
310 + return array_merge( $args, Wdkit_Import_Remote::cloud_identity() );
311 +}
298 312
299 - update_option( 'elementor_unfiltered_files_upload', 1 );
300 - update_option( 'elementor_load_fa4_shim', 'yes' );
301 - update_option( 'elementor_experiment-container', 'active' );
302 - update_option( 'elementor_experiment-e_font_icon_svg', 'inactive' );
303 -
304 - $steps['plugin_settings'] = [ 'success' => true ];
305 - do_action( 'wdkit_site_step', 'plugin_settings', 'done', [] );
313 +/**
314 + * Whether a kit-content request can be authenticated at all.
315 + *
316 + * Either credential is enough: an account JWT, or this site's own poll token.
317 + *
318 + * @param string $token Resolved cloud JWT, possibly ''.
319 + * @return bool
320 + */
321 +function wdkit_kit_import_can_authenticate( string $token ): bool {
322 + if ( '' !== $token ) {
323 + return true;
306 324 }
307 325
308 - // ── Step 3: Theme settings — Nexter fluid container ─────────────────────
309 - if ( ! in_array( 'theme_settings', $skip, true ) ) {
310 - do_action( 'wdkit_site_step', 'theme_settings', 'start', [] );
311 -
312 - $fluid_spacing = [
313 - 'md' => [ 'left' => '0', 'right' => '0' ],
314 - 'sm' => [ 'left' => '', 'right' => '' ],
315 - 'xs' => [ 'left' => '', 'right' => '' ],
316 - 'md-unit' => 'px',
317 - 'sm-unit' => 'px',
318 - 'xs-unit' => 'px',
319 - ];
320 -
321 - $theme_db = get_option( 'nxt-theme-options', [] );
322 - if ( ! is_array( $theme_db ) ) {
323 - $theme_db = [];
324 - }
325 -
326 - $theme_db['site-header-container'] = 'container-fluid';
327 - $theme_db['site-footer-container'] = 'container-fluid';
328 - $theme_db['site-layout-container'] = 'container-fluid';
329 - $theme_db['site-page-container'] = 'container-fluid';
330 - $theme_db['header-fluid-spacing'] = $fluid_spacing;
331 - $theme_db['footer-fluid-spacing'] = $fluid_spacing;
332 - $theme_db['site-fluid-spacing'] = $fluid_spacing;
333 - $theme_db['page-fluid-spacing'] = $fluid_spacing;
334 -
335 - update_option( 'nxt-theme-options', $theme_db );
336 -
337 - $steps['theme_settings'] = [ 'success' => true ];
338 - do_action( 'wdkit_site_step', 'theme_settings', 'done', [] );
339 - }
340 -
341 - // ── Step 4: Import each template and create WP pages ────────────────────
342 - do_action( 'wdkit_site_step', 'import_pages', 'start', [ 'total' => count( $templates ) ] );
343 -
344 - $widgets_to_enable = [];
345 -
346 - foreach ( $templates as $template ) {
347 - $tpl_id = sanitize_text_field( (string) ( $template['id'] ?? '' ) );
348 - $tpl_title_raw = (string) ( $template['title'] ?? '' );
349 - $tpl_title = sanitize_text_field( explode( '|', $tpl_title_raw )[0] );
350 - $wp_post_type = sanitize_text_field( (string) ( $template['wp_post_type'] ?? 'page' ) );
351 -
352 - if ( $tpl_id === '' ) {
353 - continue;
354 - }
355 -
356 - // 4a — Fetch content from WDesignKit cloud.
357 - $fetch = apply_filters( 'wdkit_import_kit_content', [], [
358 - 'template_id' => $tpl_id,
359 - 'editor' => $editor,
360 - 'website_kit' => $kit_id,
361 - // import_template is the working cloud route; import_kit_template has none (404).
362 - 'api_type' => 'import_template',
363 - 'custom_meta' => false,
364 - ] );
365 -
366 - if ( empty( $fetch['success'] ) || empty( $fetch['response']['content'] ) ) {
367 - $errors[] = [
368 - 'template_id' => $tpl_id,
369 - 'title' => $tpl_title,
370 - 'message' => $fetch['message'] ?? 'Failed to fetch template from cloud.',
371 - ];
372 - continue;
373 - }
374 -
375 - // 4b — Decode content JSON.
376 - $decoded = json_decode( $fetch['response']['content'], true );
377 - if ( ! is_array( $decoded ) ) {
378 - $errors[] = [
379 - 'template_id' => $tpl_id,
380 - 'title' => $tpl_title,
381 - 'message' => __( 'Cloud returned invalid JSON content.', 'wdesignkit' ),
382 - ];
383 - continue;
384 - }
385 -
386 - $content = $decoded['content'] ?? '';
387 -
388 - // 4c — Create the WordPress post.
389 - $post_data = [
390 - 'post_title' => $tpl_title !== '' ? $tpl_title : 'Imported Page',
391 - 'post_name' => sanitize_title( $tpl_title ),
392 - 'post_status' => 'publish',
393 - 'post_type' => $wp_post_type,
394 - 'post_content' => $editor === 'gutenberg' ? (string) $content : '',
395 - ];
396 -
397 - $inserted_id = wp_insert_post( $post_data, true );
398 -
399 - if ( is_wp_error( $inserted_id ) ) {
400 - $errors[] = [
401 - 'template_id' => $tpl_id,
402 - 'title' => $tpl_title,
403 - 'message' => $inserted_id->get_error_message(),
404 - ];
405 - continue;
406 - }
407 -
408 - // 4d — Save Elementor data when editor is Elementor.
409 - if ( $editor === 'elementor' ) {
410 - update_post_meta( $inserted_id, '_elementor_data', wp_slash( (string) $content ) );
411 - update_post_meta( $inserted_id, '_elementor_edit_mode', 'builder' );
412 - update_post_meta( $inserted_id, '_elementor_template_type', 'page' );
413 - }
414 -
415 - // 4e — Restore nxt-* custom meta (theme builder conditions, etc.).
416 - if ( ! empty( $decoded['custom_meta'] ) && is_array( $decoded['custom_meta'] ) ) {
417 - foreach ( $decoded['custom_meta'] as $meta_key => $meta_val ) {
418 - $value = $meta_val[0] ?? null;
419 - if ( is_string( $value ) && is_serialized( $value ) ) {
420 - // See the note on the identical restore loop earlier in this file: an options
421 - // array cannot be passed to maybe_unserialize(), so this uses unserialize()
422 - // with allowed_classes => false (CWE-502, ClickUp 86d41zauw).
423 - $value = unserialize( $value, array( 'allowed_classes' => false ) );
424 - }
425 - update_post_meta( $inserted_id, $meta_key, $value );
426 - }
427 - }
428 -
429 - // 4f — Track homepage and shop page.
430 - $title_lower = strtolower( $tpl_title );
431 - if ( $home_page_id === 0 && ( str_contains( $title_lower, 'home' ) || str_contains( $title_lower, 'landing' ) ) ) {
432 - $home_page_id = $inserted_id;
433 - }
434 - if ( $shop_page_id === 0 && ( str_contains( $title_lower, 'shop' ) || str_contains( $title_lower, 'store' ) ) ) {
435 - $shop_page_id = $inserted_id;
436 - }
437 -
438 - // 4g — Collect widgets used in this page for bulk enable later.
439 - if ( ! empty( $decoded['widget_list'] ) && is_array( $decoded['widget_list'] ) ) {
440 - $widgets_to_enable = array_unique( array_merge( $widgets_to_enable, $decoded['widget_list'] ) );
441 - }
442 -
443 - $page_entry = [
444 - 'template_id' => $tpl_id,
445 - 'post_id' => $inserted_id,
446 - 'title' => $tpl_title,
447 - 'url' => get_permalink( $inserted_id ),
448 - 'success' => true,
449 - ];
450 - $pages[] = $page_entry;
451 -
452 - do_action( 'wdkit_site_step', 'import_pages', 'progress', $page_entry );
453 - }
454 -
455 - $steps['import_pages'] = [
456 - 'success' => count( $pages ) > 0,
457 - 'count' => count( $pages ),
458 - 'errors' => count( $errors ),
459 - ];
460 -
461 - do_action( 'wdkit_site_step', 'import_pages', 'done', [
462 - 'pages' => $pages,
463 - 'errors' => $errors,
464 - ] );
465 -
466 - // ── Step 5: Enable required widgets ─────────────────────────────────────
467 - if ( ! in_array( 'enable_widgets', $skip, true ) && has_filter( 'tpae_enable_selected_widgets' ) ) {
468 - do_action( 'wdkit_site_step', 'enable_widgets', 'start', [] );
469 -
470 - if ( ! empty( $widgets_to_enable ) ) {
471 - apply_filters( 'tpae_enable_selected_widgets', [
472 - 'widgets' => $widgets_to_enable,
473 - 'extensions' => [],
474 - ] );
475 - }
476 -
477 - $steps['enable_widgets'] = [ 'success' => true ];
478 - do_action( 'wdkit_site_step', 'enable_widgets', 'done', [ 'widgets' => $widgets_to_enable ] );
479 - }
480 -
481 - // ── Step 6: Finalize — set homepage, site name, tagline ─────────────────
482 - do_action( 'wdkit_site_step', 'finalize', 'start', [] );
483 -
484 - $finalize_success = false;
485 -
486 - if ( $home_page_id > 0 ) {
487 - update_option( 'show_on_front', 'page' );
488 - update_option( 'page_on_front', $home_page_id );
489 - $finalize_success = true;
490 - }
491 -
492 - if ( $shop_page_id > 0 ) {
493 - update_option( 'woocommerce_shop_page_id', $shop_page_id );
494 - }
495 -
496 - if ( $site_name !== '' ) {
497 - update_option( 'blogname', $site_name );
498 - }
499 -
500 - if ( $tagline !== '' ) {
501 - update_option( 'blogdescription', $tagline );
502 - }
503 -
504 - $steps['finalize'] = [ 'success' => $finalize_success ];
505 -
506 - do_action( 'wdkit_site_step', 'finalize', 'done', [
507 - 'home_page_id' => $home_page_id,
508 - 'shop_page_id' => $shop_page_id,
509 - 'site_url' => get_site_url(),
510 - ] );
511 -
512 - // ── Return ───────────────────────────────────────────────────────────────
513 - return [
514 - 'success' => count( $pages ) > 0,
515 - 'message' => count( $pages ) > 0 ? 'Site created successfully.' : 'Site creation completed with errors.',
516 - 'site_url' => get_site_url(),
517 - 'home_page_id' => $home_page_id,
518 - 'shop_page_id' => $shop_page_id,
519 - 'pages' => $pages,
520 - 'steps' => $steps,
521 - 'errors' => $errors,
522 - ];
326 + return class_exists( 'Wdkit_Import_Remote' ) && array() !== Wdkit_Import_Remote::cloud_identity();
523 327 }
524 -
525 -// ════════════════════════════════════════════════════════════════════════════
526 -// Shared helpers
527 -// ════════════════════════════════════════════════════════════════════════════
528 328
529 329 /**
530 330 * Resolve an active WDesignKit cloud token without depending on the abilities system.
531 331 * Checks current WP user transient first, then scans all wdkit_auth_* transients.