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/hooks/class-wdkit-kit-import-hook.php +88 -275 2.6.3 → 2.7.0 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 *
@@ -168,9 +170,19 @@
168 170 if ( $current_post_id && is_array( $decoded ) && ! empty( $decoded['custom_meta'] ) ) {
169 171 foreach ( $decoded['custom_meta'] as $meta_key => $meta_val ) {
170 172 $value = $meta_val[0] ?? null;
171 173 if ( is_string( $value ) && is_serialized( $value ) ) {
172 - $value = maybe_unserialize( $value );
174 + // allowed_classes => false: $value comes from the imported kit body, so a
175 + // serialized object here would be instantiated and could fire a POP gadget
176 + // chain in any loaded plugin or theme (CWE-502, ClickUp 86d41zauw).
177 + //
178 + // maybe_unserialize() CANNOT express this — WP core declares it as
179 + // maybe_unserialize( $data ) and calls @unserialize( trim( $data ) ) with no
180 + // options, so an options array passed to it is silently ignored and the object
181 + // is still built. The is_serialized() check above already gates this call, so
182 + // unserialize() direct is an exact drop-in. Legitimate values here are arrays
183 + // (theme-builder conditions and similar); anything object-shaped is not.
184 + $value = unserialize( $value, array( 'allowed_classes' => false ) );
173 185 }
174 186 if ( get_post_meta( $current_post_id, $meta_key, true ) === '' ) {
175 187 add_post_meta( $current_post_id, $meta_key, $value );
176 188 } else {
@@ -218,10 +230,25 @@
218 230 * @type string $site_name Optional site title.
219 231 * @type string $tagline Optional site tagline.
220 232 * @type string[] $skip Steps to skip: 'reset_site', 'plugin_settings',
221 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.
222 249 * }
223 - * @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}
224 251 */
225 252 function wdkit_handle_create_full_site( array $output, array $args ): array {
226 253
227 254 // ── Guard ────────────────────────────────────────────────────────────────
@@ -233,286 +260,72 @@
233 260 return wdkit_site_error( 'Insufficient permissions.' );
234 261 }
235 262
236 263 $token = wdkit_kit_import_resolve_token();
237 - if ( $token === '' ) {
264 + if ( ! wdkit_kit_import_can_authenticate( $token ) ) {
238 265 return wdkit_site_error( 'Not logged in to WDesignKit cloud. Go to WP Admin → WDesignKit and click Login.' );
239 266 }
240 267
241 - // ── Sanitize args ────────────────────────────────────────────────────────
242 - $kit_id = sanitize_text_field( (string) ( $args['kit_id'] ?? '' ) );
243 - $editor = sanitize_text_field( (string) ( $args['editor'] ?? 'elementor' ) );
244 - $templates = is_array( $args['templates'] ?? null ) ? $args['templates'] : [];
245 - $site_name = sanitize_text_field( (string) ( $args['site_name'] ?? '' ) );
246 - $tagline = sanitize_text_field( (string) ( $args['tagline'] ?? '' ) );
247 - $skip = is_array( $args['skip'] ?? null ) ? array_map( 'sanitize_text_field', $args['skip'] ) : [];
248 -
249 - if ( $kit_id === '' ) {
250 - 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.' );
251 270 }
252 271
253 - if ( empty( $templates ) ) {
254 - return wdkit_site_error( 'templates array is required and must not be empty.' );
255 - }
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 +}
256 287
257 - // ── State ────────────────────────────────────────────────────────────────
258 - $steps = [];
259 - $errors = [];
260 - $pages = [];
261 - $home_page_id = 0;
262 - $shop_page_id = 0;
288 +// ════════════════════════════════════════════════════════════════════════════
289 +// Shared helpers
290 +// ════════════════════════════════════════════════════════════════════════════
263 291
264 - // ── Step 1: Reset site — draft all existing published pages ──────────────
265 - if ( ! in_array( 'reset_site', $skip, true ) ) {
266 - do_action( 'wdkit_site_step', 'reset_site', 'start', [] );
267 -
268 - do_action( 'nxt_update_builder_status', 'all' );
269 -
270 - $existing = get_posts( [
271 - 'post_type' => 'page',
272 - 'post_status' => 'publish',
273 - 'numberposts' => -1,
274 - 'fields' => 'ids',
275 - ] );
276 -
277 - foreach ( $existing as $pid ) {
278 - wp_update_post( [ 'ID' => $pid, 'post_status' => 'draft' ] );
279 - }
280 -
281 - $steps['reset_site'] = [ 'success' => true ];
282 - 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;
283 308 }
284 309
285 - // ── Step 2: Plugin settings — Elementor options ──────────────────────────
286 - if ( ! in_array( 'plugin_settings', $skip, true ) && $editor === 'elementor' ) {
287 - do_action( 'wdkit_site_step', 'plugin_settings', 'start', [] );
310 + return array_merge( $args, Wdkit_Import_Remote::cloud_identity() );
311 +}
288 312
289 - update_option( 'elementor_unfiltered_files_upload', 1 );
290 - update_option( 'elementor_load_fa4_shim', 'yes' );
291 - update_option( 'elementor_experiment-container', 'active' );
292 - update_option( 'elementor_experiment-e_font_icon_svg', 'inactive' );
293 -
294 - $steps['plugin_settings'] = [ 'success' => true ];
295 - 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;
296 324 }
297 325
298 - // ── Step 3: Theme settings — Nexter fluid container ─────────────────────
299 - if ( ! in_array( 'theme_settings', $skip, true ) ) {
300 - do_action( 'wdkit_site_step', 'theme_settings', 'start', [] );
301 -
302 - $fluid_spacing = [
303 - 'md' => [ 'left' => '0', 'right' => '0' ],
304 - 'sm' => [ 'left' => '', 'right' => '' ],
305 - 'xs' => [ 'left' => '', 'right' => '' ],
306 - 'md-unit' => 'px',
307 - 'sm-unit' => 'px',
308 - 'xs-unit' => 'px',
309 - ];
310 -
311 - $theme_db = get_option( 'nxt-theme-options', [] );
312 - if ( ! is_array( $theme_db ) ) {
313 - $theme_db = [];
314 - }
315 -
316 - $theme_db['site-header-container'] = 'container-fluid';
317 - $theme_db['site-footer-container'] = 'container-fluid';
318 - $theme_db['site-layout-container'] = 'container-fluid';
319 - $theme_db['site-page-container'] = 'container-fluid';
320 - $theme_db['header-fluid-spacing'] = $fluid_spacing;
321 - $theme_db['footer-fluid-spacing'] = $fluid_spacing;
322 - $theme_db['site-fluid-spacing'] = $fluid_spacing;
323 - $theme_db['page-fluid-spacing'] = $fluid_spacing;
324 -
325 - update_option( 'nxt-theme-options', $theme_db );
326 -
327 - $steps['theme_settings'] = [ 'success' => true ];
328 - do_action( 'wdkit_site_step', 'theme_settings', 'done', [] );
329 - }
330 -
331 - // ── Step 4: Import each template and create WP pages ────────────────────
332 - do_action( 'wdkit_site_step', 'import_pages', 'start', [ 'total' => count( $templates ) ] );
333 -
334 - $widgets_to_enable = [];
335 -
336 - foreach ( $templates as $template ) {
337 - $tpl_id = sanitize_text_field( (string) ( $template['id'] ?? '' ) );
338 - $tpl_title_raw = (string) ( $template['title'] ?? '' );
339 - $tpl_title = sanitize_text_field( explode( '|', $tpl_title_raw )[0] );
340 - $wp_post_type = sanitize_text_field( (string) ( $template['wp_post_type'] ?? 'page' ) );
341 -
342 - if ( $tpl_id === '' ) {
343 - continue;
344 - }
345 -
346 - // 4a — Fetch content from WDesignKit cloud.
347 - $fetch = apply_filters( 'wdkit_import_kit_content', [], [
348 - 'template_id' => $tpl_id,
349 - 'editor' => $editor,
350 - 'website_kit' => $kit_id,
351 - // import_template is the working cloud route; import_kit_template has none (404).
352 - 'api_type' => 'import_template',
353 - 'custom_meta' => false,
354 - ] );
355 -
356 - if ( empty( $fetch['success'] ) || empty( $fetch['response']['content'] ) ) {
357 - $errors[] = [
358 - 'template_id' => $tpl_id,
359 - 'title' => $tpl_title,
360 - 'message' => $fetch['message'] ?? 'Failed to fetch template from cloud.',
361 - ];
362 - continue;
363 - }
364 -
365 - // 4b — Decode content JSON.
366 - $decoded = json_decode( $fetch['response']['content'], true );
367 - if ( ! is_array( $decoded ) ) {
368 - $errors[] = [
369 - 'template_id' => $tpl_id,
370 - 'title' => $tpl_title,
371 - 'message' => __( 'Cloud returned invalid JSON content.', 'wdesignkit' ),
372 - ];
373 - continue;
374 - }
375 -
376 - $content = $decoded['content'] ?? '';
377 -
378 - // 4c — Create the WordPress post.
379 - $post_data = [
380 - 'post_title' => $tpl_title !== '' ? $tpl_title : 'Imported Page',
381 - 'post_name' => sanitize_title( $tpl_title ),
382 - 'post_status' => 'publish',
383 - 'post_type' => $wp_post_type,
384 - 'post_content' => $editor === 'gutenberg' ? (string) $content : '',
385 - ];
386 -
387 - $inserted_id = wp_insert_post( $post_data, true );
388 -
389 - if ( is_wp_error( $inserted_id ) ) {
390 - $errors[] = [
391 - 'template_id' => $tpl_id,
392 - 'title' => $tpl_title,
393 - 'message' => $inserted_id->get_error_message(),
394 - ];
395 - continue;
396 - }
397 -
398 - // 4d — Save Elementor data when editor is Elementor.
399 - if ( $editor === 'elementor' ) {
400 - update_post_meta( $inserted_id, '_elementor_data', wp_slash( (string) $content ) );
401 - update_post_meta( $inserted_id, '_elementor_edit_mode', 'builder' );
402 - update_post_meta( $inserted_id, '_elementor_template_type', 'page' );
403 - }
404 -
405 - // 4e — Restore nxt-* custom meta (theme builder conditions, etc.).
406 - if ( ! empty( $decoded['custom_meta'] ) && is_array( $decoded['custom_meta'] ) ) {
407 - foreach ( $decoded['custom_meta'] as $meta_key => $meta_val ) {
408 - $value = $meta_val[0] ?? null;
409 - if ( is_string( $value ) && is_serialized( $value ) ) {
410 - $value = maybe_unserialize( $value );
411 - }
412 - update_post_meta( $inserted_id, $meta_key, $value );
413 - }
414 - }
415 -
416 - // 4f — Track homepage and shop page.
417 - $title_lower = strtolower( $tpl_title );
418 - if ( $home_page_id === 0 && ( str_contains( $title_lower, 'home' ) || str_contains( $title_lower, 'landing' ) ) ) {
419 - $home_page_id = $inserted_id;
420 - }
421 - if ( $shop_page_id === 0 && ( str_contains( $title_lower, 'shop' ) || str_contains( $title_lower, 'store' ) ) ) {
422 - $shop_page_id = $inserted_id;
423 - }
424 -
425 - // 4g — Collect widgets used in this page for bulk enable later.
426 - if ( ! empty( $decoded['widget_list'] ) && is_array( $decoded['widget_list'] ) ) {
427 - $widgets_to_enable = array_unique( array_merge( $widgets_to_enable, $decoded['widget_list'] ) );
428 - }
429 -
430 - $page_entry = [
431 - 'template_id' => $tpl_id,
432 - 'post_id' => $inserted_id,
433 - 'title' => $tpl_title,
434 - 'url' => get_permalink( $inserted_id ),
435 - 'success' => true,
436 - ];
437 - $pages[] = $page_entry;
438 -
439 - do_action( 'wdkit_site_step', 'import_pages', 'progress', $page_entry );
440 - }
441 -
442 - $steps['import_pages'] = [
443 - 'success' => count( $pages ) > 0,
444 - 'count' => count( $pages ),
445 - 'errors' => count( $errors ),
446 - ];
447 -
448 - do_action( 'wdkit_site_step', 'import_pages', 'done', [
449 - 'pages' => $pages,
450 - 'errors' => $errors,
451 - ] );
452 -
453 - // ── Step 5: Enable required widgets ─────────────────────────────────────
454 - if ( ! in_array( 'enable_widgets', $skip, true ) && has_filter( 'tpae_enable_selected_widgets' ) ) {
455 - do_action( 'wdkit_site_step', 'enable_widgets', 'start', [] );
456 -
457 - if ( ! empty( $widgets_to_enable ) ) {
458 - apply_filters( 'tpae_enable_selected_widgets', [
459 - 'widgets' => $widgets_to_enable,
460 - 'extensions' => [],
461 - ] );
462 - }
463 -
464 - $steps['enable_widgets'] = [ 'success' => true ];
465 - do_action( 'wdkit_site_step', 'enable_widgets', 'done', [ 'widgets' => $widgets_to_enable ] );
466 - }
467 -
468 - // ── Step 6: Finalize — set homepage, site name, tagline ─────────────────
469 - do_action( 'wdkit_site_step', 'finalize', 'start', [] );
470 -
471 - $finalize_success = false;
472 -
473 - if ( $home_page_id > 0 ) {
474 - update_option( 'show_on_front', 'page' );
475 - update_option( 'page_on_front', $home_page_id );
476 - $finalize_success = true;
477 - }
478 -
479 - if ( $shop_page_id > 0 ) {
480 - update_option( 'woocommerce_shop_page_id', $shop_page_id );
481 - }
482 -
483 - if ( $site_name !== '' ) {
484 - update_option( 'blogname', $site_name );
485 - }
486 -
487 - if ( $tagline !== '' ) {
488 - update_option( 'blogdescription', $tagline );
489 - }
490 -
491 - $steps['finalize'] = [ 'success' => $finalize_success ];
492 -
493 - do_action( 'wdkit_site_step', 'finalize', 'done', [
494 - 'home_page_id' => $home_page_id,
495 - 'shop_page_id' => $shop_page_id,
496 - 'site_url' => get_site_url(),
497 - ] );
498 -
499 - // ── Return ───────────────────────────────────────────────────────────────
500 - return [
501 - 'success' => count( $pages ) > 0,
502 - 'message' => count( $pages ) > 0 ? 'Site created successfully.' : 'Site creation completed with errors.',
503 - 'site_url' => get_site_url(),
504 - 'home_page_id' => $home_page_id,
505 - 'shop_page_id' => $shop_page_id,
506 - 'pages' => $pages,
507 - 'steps' => $steps,
508 - 'errors' => $errors,
509 - ];
326 + return class_exists( 'Wdkit_Import_Remote' ) && array() !== Wdkit_Import_Remote::cloud_identity();
510 327 }
511 -
512 -// ════════════════════════════════════════════════════════════════════════════
513 -// Shared helpers
514 -// ════════════════════════════════════════════════════════════════════════════
515 328
516 329 /**
517 330 * Resolve an active WDesignKit cloud token without depending on the abilities system.
518 331 * Checks current WP user transient first, then scans all wdkit_auth_* transients.