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
wdesignkit / includes / admin / import / class-wdkit-import-bridge.php

class-wdkit-import-bridge.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.1, at includes/admin/import/class-wdkit-import-bridge.php

753 lines 29.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Adapter between the full-site entry point and the runner.
4 *
5 * `wdkit_handle_create_full_site()` had its own inline copy of the import sequence: reset,
6 * plugin settings, theme settings, a per-template fetch/insert loop, widget enabling, and a
7 * homepage/site-name finalise. That copy could not do AI merge, image substitution, taxonomy,
8 * products, blog posts, navigation, globals, or resume after a failure — the runner can, so
9 * this maps one onto the other.
10 *
11 * ── The contract this must not break ───────────────────────────────────────
12 *
13 * Callers of that filter (the Abilities / MCP surface, and anything hooking it) depend on:
14 *
15 * args kit_id, editor, templates, site_name, tagline, skip[]
16 * return { success, message, site_url, home_page_id, shop_page_id, pages, steps, errors }
17 * pages[] { template_id, post_id, title, url, success }
18 * events do_action( 'wdkit_site_step', <step>, 'start'|'progress'|'done', <data> )
19 *
20 * All of it is preserved. `steps` keeps the original key names — reset_site, plugin_settings,
21 * theme_settings, import_pages, enable_widgets, finalize — even though the runner groups its
22 * work differently, because those are the names a caller already reads.
23 *
24 * ── What a caller gains ────────────────────────────────────────────────────
25 *
26 * `session_id` is added to the return. It is additive, so nothing that ignores it breaks, and
27 * it is the handle for a resume: calling again with the same id re-runs only what did not
28 * finish. Passing it in via `session_id` is how a caller retries.
29 *
30 * @package Wdesignkit
31 * @since 2.6.5
32 */
33
34 if ( ! defined( 'ABSPATH' ) ) {
35 exit;
36 }
37
38 if ( ! class_exists( 'Wdkit_Import_Bridge' ) ) {
39
40 /**
41 * Full-site entry point adapter.
42 */
43 class Wdkit_Import_Bridge {
44
45 /**
46 * Steps a caller may skip, mapped to what skipping them means to the runner.
47 *
48 * `reset_site` is the only one that changes runner behaviour, because the runner
49 * defaults it to OFF while the entry point defaults it to ON — see build_context().
50 * The other three are reported as skipped but the runner still applies them, exactly
51 * as the inline version did for anything it had no switch for.
52 *
53 * @var string[]
54 */
55 private static $skippable = array( 'reset_site', 'reset_builders', 'plugin_settings', 'theme_settings', 'enable_widgets' );
56
57 /**
58 * Run a full-site import through the runner.
59 *
60 * @param array $args Entry-point args.
61 * @return array Entry-point response shape.
62 */
63 public static function create_full_site( $args ) {
64 $args = is_array( $args ) ? $args : array();
65
66 $kit_id = sanitize_text_field( (string) ( isset( $args['kit_id'] ) ? $args['kit_id'] : '' ) );
67 $templates = ( isset( $args['templates'] ) && is_array( $args['templates'] ) ) ? $args['templates'] : array();
68 $skip = ( isset( $args['skip'] ) && is_array( $args['skip'] ) ) ? array_map( 'sanitize_text_field', $args['skip'] ) : array();
69
70 if ( '' === $kit_id ) {
71 return self::error( 'kit_id is required.' );
72 }
73
74 /* A caller that knows only the kit id is the normal case for a site-side handoff: the
75 * website hands over "import kit 21603" and cannot reasonably enumerate its
76 * templates first. The wizard passes props.kit_template because it already fetched it
77 * for the preview; nothing else has. Resolve it from the same cloud endpoint the
78 * browser uses. */
79 if ( empty( $templates ) ) {
80 $named = isset( $args['builder'] ) ? (string) $args['builder'] : '';
81 $resolved = null;
82 $templates = self::fetch_kit_templates( $kit_id, $named, $resolved );
83
84 /* The named builder was wrong and the other one answered. Correct the args now,
85 * before build_context() freezes it onto the session — otherwise the run imports
86 * a Gutenberg kit while every builder-gated step still believes it is Elementor. */
87 if ( ! empty( $resolved ) && $resolved !== $named ) {
88 $args['builder'] = $resolved;
89 }
90 }
91
92 if ( empty( $templates ) ) {
93 return self::error( 'templates array is required and the kit returned none.' );
94 }
95
96 /* build_context() reads $args, not the local. Without this the resolved list was
97 * discarded and the context validator rejected the run with "At least one template is
98 * required" - having just fetched thirteen of them. */
99 $args['templates'] = $templates;
100
101 /* ── Preflight, BEFORE anything destructive ──────────────────────────────────
102 *
103 * Step 1 of this entry point resets the site: `Wdkit_Import_Reset` drafts every
104 * published page. Template CONTENT is not fetched until step 2, so a run whose
105 * content turns out to be unreachable had already emptied the site by the time it
106 * found out — observed end to end: 3 published pages drafted, 13 templates then
107 * failed `cloud_not_authenticated`, and the entry point still returned with the
108 * stages marked done and nothing imported. Resolving the template LIST is not
109 * enough to catch this: the list endpoint answered fine while the content endpoint
110 * refused, which is exactly what an expired token looks like.
111 *
112 * Priming the bundle here is the cheapest possible real check — it is the same
113 * fetch step 2 performs, it is idempotent, and its result is parked for the run, so
114 * the work is not repeated. A site that cannot fetch its kit is told so while its
115 * pages are still published. */
116 if ( ! in_array( 'reset_site', $skip, true ) && class_exists( 'Wdkit_Page_Importer' ) && method_exists( 'Wdkit_Page_Importer', 'prime_bundle' ) ) {
117 $preflight_builder = self::resolve_builder( $args );
118 $primed = Wdkit_Page_Importer::prime_bundle(
119 array( 'kit_id' => $kit_id ),
120 ( 'gutenberg' === $preflight_builder ) ? 'gutenberg' : 'elementor',
121 $templates
122 );
123
124 if ( ! $primed ) {
125 return self::error( 'The kit could not be downloaded, so nothing was changed on this site. Check that this site is still signed in to WDesignKit, then try again.' );
126 }
127 }
128
129 /* The kit's palette and its fonts.
130 *
131 * The wizard puts these on site_obj itself: get_global_data() in import_temp_main.js
132 * walks the kit's template list and hands the result to the globals screen, which
133 * carries it forward as `kit_global` / `font_family`. A site-side caller has neither
134 * that pass nor any way to compute it, so a headless import arrived with an empty
135 * palette - the pages were correct but every colour and font fell back to the site's
136 * stock five, which reads as "the import lost its styling".
137 *
138 * Derived from the same template list the browser walks, so the two paths agree. Only
139 * filled in when the caller supplied nothing: a caller that HAS made a choice on the
140 * globals screen must keep it. */
141 if ( empty( $args['kit_global'] ) ) {
142 $resolved = self::resolve_kit_global( $templates, self::resolve_builder( $args ) );
143
144 if ( ! empty( $resolved['kit_global']['color'] ) || ! empty( $resolved['kit_global']['typo'] ) ) {
145 $args['kit_global'] = $resolved['kit_global'];
146
147 if ( empty( $args['font_family'] ) ) {
148 $args['font_family'] = $resolved['font_family'];
149 }
150 }
151 }
152
153 $context = self::build_context( $args, $skip );
154 $session_id = isset( $args['session_id'] ) ? Wdkit_Import_Session::sanitize_id( (string) $args['session_id'] ) : '';
155
156 /* Republish runner stages as the `wdkit_site_step` events this entry point has
157 * always fired. Removed straight afterwards so a second call in the same request
158 * does not double-emit. */
159 $relay = function ( $stage, $state, $data ) use ( $skip ) {
160 $step = self::step_name( $stage );
161
162 if ( '' === $step ) {
163 return;
164 }
165
166 do_action( 'wdkit_site_step', $step, $state, is_array( $data ) ? $data : array() );
167 };
168
169 add_action( 'wdkit_import_runner_stage', $relay, 10, 3 );
170
171 try {
172 $runner = ( '' !== $session_id && null !== Wdkit_Import_Session::get( $session_id ) )
173 ? new Wdkit_Import_Runner( $session_id )
174 : Wdkit_Import_Runner::for_context( $context, $session_id );
175
176 $run = $runner->run();
177 } finally {
178 remove_action( 'wdkit_import_runner_stage', $relay, 10 );
179 }
180
181 return self::response( $run, $runner->get_session_id(), $skip );
182 }
183
184 /**
185 * The builder a caller named, or '' when it named none.
186 *
187 * `editor` is this entry point's original name for it, `builder` and `kit_builder` are
188 * what every other caller uses - Wdkit_Import_Context treats all three as the same
189 * field. Reading only `editor` meant a caller passing `builder: gutenberg` was silently
190 * given Elementor, and given it EXPLICITLY, so the content/builder guard refused all
191 * thirteen templates instead of resolving the builder from the content. A headless
192 * import of a Gutenberg kit imported nothing at all.
193 *
194 * '' is a meaningful answer, not a failure: it records the builder as not-explicit, and
195 * the importer then takes it from the template's own file_type.
196 *
197 * @param array $args Entry-point args.
198 * @return string 'gutenberg'|'elementor'|''.
199 */
200 private static function resolve_builder( $args ) {
201 foreach ( array( 'editor', 'builder', 'kit_builder' ) as $key ) {
202 if ( empty( $args[ $key ] ) || ! is_string( $args[ $key ] ) ) {
203 continue;
204 }
205
206 $named = sanitize_text_field( $args[ $key ] );
207
208 if ( 'gutenberg' === $named || 'elementor' === $named ) {
209 return $named;
210 }
211 }
212
213 return '';
214 }
215
216 /**
217 * Derive the kit's palette, typography and font list from its template list.
218 *
219 * PHP side of get_global_data() in import_temp_main.js. Wdkit_Import_Globals::collect()
220 * does the walk and the de-duplication; the one thing added here is the browser's
221 * rgba() -> hex conversion for Gutenberg, carried on `new_color` exactly as the browser
222 * carries it, because that is the key from_kit() reads the intended value off.
223 *
224 * Deliberately not folded into collect(): its other caller reads global_data off
225 * DECODED templates, where the browser never converted anything either, and that path
226 * is already verified working. Converting there too would change behaviour nothing
227 * asked to change.
228 *
229 * @param array $templates Kit template list, as the cloud returns it.
230 * @param string $builder 'gutenberg'|'elementor'|'' (unnamed).
231 * @return array{kit_global:array{color:array,typo:array},font_family:array}
232 */
233 private static function resolve_kit_global( $templates, $builder ) {
234 /* De-duplication keys on `id` for Gutenberg and `_id` for Elementor, so the builder
235 * decides whether collect() finds anything at all.
236 *
237 * An unnamed builder is therefore read off the data rather than guessed. Guessing
238 * Gutenberg — which this did — meant an Elementor kit was walked looking for `id`,
239 * found nothing, and produced an empty palette: the pages imported and then rendered
240 * in Elementor's factory blue and Roboto, because every var(--e-global-*) fell back
241 * to stock. Whichever key actually yields entries is the right one. */
242 $named = in_array( $builder, array( 'elementor', 'gutenberg' ), true ) ? $builder : '';
243
244 if ( '' !== $named ) {
245 $globals = Wdkit_Import_Globals::collect( $templates, $named );
246 $builder = $named;
247 } else {
248 $builder = 'gutenberg';
249 $globals = Wdkit_Import_Globals::collect( $templates, 'gutenberg' );
250
251 if ( empty( $globals['color'] ) && empty( $globals['typo'] ) ) {
252 $elementor = Wdkit_Import_Globals::collect( $templates, 'elementor' );
253
254 if ( ! empty( $elementor['color'] ) || ! empty( $elementor['typo'] ) ) {
255 $builder = 'elementor';
256 $globals = $elementor;
257 }
258 }
259 }
260
261 $colors = array();
262
263 foreach ( $globals['color'] as $entry ) {
264 if ( ! is_array( $entry ) ) {
265 continue;
266 }
267
268 if ( 'gutenberg' === $builder ) {
269 $hex = self::rgba_to_hex( isset( $entry['value'] ) ? $entry['value'] : '' );
270
271 if ( '' !== $hex ) {
272 $entry['new_color'] = $hex;
273 $entry['value'] = $hex;
274 }
275 }
276
277 $colors[] = $entry;
278 }
279
280 return array(
281 'kit_global' => array(
282 'color' => $colors,
283 'typo' => $globals['typo'],
284 ),
285 'font_family' => $globals['font_family'],
286 );
287 }
288
289 /**
290 * `rgb()` / `rgba()` to `#rrggbbaa`. '' when the value is not one of those.
291 *
292 * Mirrors rbg_to_hash() in import_temp_main.js, including its choice to always emit the
293 * alpha pair — 'ff' when none was given — so a value that round-trips through the
294 * browser and one that round-trips through here are byte-identical.
295 *
296 * @param mixed $value Colour value as the kit shipped it.
297 * @return string
298 */
299 private static function rgba_to_hex( $value ) {
300 if ( ! is_string( $value ) || ! preg_match( '/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*(?:,\s*([\d.]+)\s*)?\)/i', $value, $m ) ) {
301 return '';
302 }
303
304 $hex = '#';
305
306 foreach ( array( 1, 2, 3 ) as $i ) {
307 $hex .= str_pad( dechex( min( 255, max( 0, (int) $m[ $i ] ) ) ), 2, '0', STR_PAD_LEFT );
308 }
309
310 $alpha = isset( $m[4] ) && '' !== $m[4] ? (float) $m[4] : 1.0;
311
312 return $hex . str_pad( dechex( (int) round( min( 1.0, max( 0.0, $alpha ) ) * 255 ) ), 2, '0', STR_PAD_LEFT );
313 }
314
315 /**
316 * Map entry-point args onto a runner context.
317 *
318 * @param array $args Entry-point args.
319 * @param string[] $skip Steps the caller asked to skip.
320 * @return array
321 */
322 private static function build_context( $args, $skip ) {
323 $editor = self::resolve_builder( $args );
324
325 $context = array(
326 'kit_id' => sanitize_text_field( (string) $args['kit_id'] ),
327
328 /* Left unset when the caller named no builder, so the context records it as
329 * not-explicit and the importer can take it from the template's own file_type. */
330 'builder' => ( 'gutenberg' === $editor ) ? 'gutenberg' : ( 'elementor' === $editor ? 'elementor' : '' ),
331 'templates' => $args['templates'],
332 'site_info' => self::site_info( $args ),
333
334 /* This entry point has always reset the site unless told not to — that is its
335 * Step 1. The runner's own default is the opposite, so the default is restored
336 * here rather than in the context class, where it would change every other
337 * caller too. */
338 'reset_site' => ! in_array( 'reset_site', $skip, true ),
339
340 /* Disabling the site's existing header/footer/etc. templates is not destructive
341 * to page content, so — unlike reset_site above — it defaults to ON regardless
342 * of whether page-drafting was skipped. A caller that skips reset_site (the
343 * remote website-triggered job, to avoid drafting a real site's own pages) still
344 * wants its imported header/footer to be the ones that render, not stack on top
345 * of whatever the last import left active. A caller that genuinely wants the
346 * old templates left alone can opt out with skip => ['reset_builders']. */
347 'reset_builders' => ! in_array( 'reset_builders', $skip, true ),
348 );
349
350 /* Everything below is additive: the inline version had no concept of any of it, so
351 * a caller that passes nothing gets exactly the old behaviour. */
352 foreach ( array( 'import_type', 'site_type', 'site_description', 'site_lang', 'site_agency', 'site_category_id', 'blog_post', 'ai_blog', 'wirefram_import', 'products', 'ecommerce', 'features', 'ai_document', 'images', 'plugin_catalogue', 'site_global', 'allow_destructive_cleanup', 'update_existing_products', 'theme_setting', 'kit_global', 'font_family' ) as $key ) {
353 if ( isset( $args[ $key ] ) ) {
354 $context[ $key ] = $args[ $key ];
355 }
356 }
357
358 return $context;
359 }
360
361 /**
362 * The business details the templates fill their own fields from.
363 *
364 * This entry point has only ever taken `site_name` and `tagline`, so those two are
365 * still accepted at the top level and still win — nothing that calls it today changes.
366 * What is new is accepting a whole `site_info`, because the importer has always
367 * validated one: address, email, phone, logo and social links all have fields in the
368 * kits and all reach the AI content pass.
369 *
370 * Building only the two meant a site imported from the website came out with no
371 * contact details and no logo anywhere, while the same kit imported through the wizard
372 * had all of it. Wdkit_Import_Context::site_info() is what sanitises the result, so
373 * nothing unvalidated gets through by being passed here.
374 *
375 * @param array $args Entry-point args.
376 * @return array
377 */
378 private static function site_info( $args ) {
379 $info = ( isset( $args['site_info'] ) && is_array( $args['site_info'] ) ) ? $args['site_info'] : array();
380
381 foreach ( array( 'site_name', 'tagline' ) as $key ) {
382 if ( isset( $args[ $key ] ) && '' !== $args[ $key ] ) {
383 $info[ $key ] = $args[ $key ];
384 } elseif ( ! isset( $info[ $key ] ) ) {
385 $info[ $key ] = '';
386 }
387 }
388
389 return $info;
390 }
391
392 /**
393 * Runner stage name => the step name this entry point publishes.
394 *
395 * @param string $stage Runner stage.
396 * @return string Empty when the stage has no public equivalent.
397 */
398 private static function step_name( $stage ) {
399 $map = array(
400 Wdkit_Import_Runner::STAGE_DEPENDENCIES => 'reset_site',
401 Wdkit_Import_Runner::STAGE_CONTENT => 'import_pages',
402 Wdkit_Import_Runner::STAGE_SETUP => 'plugin_settings',
403 Wdkit_Import_Runner::STAGE_FINALIZE => 'finalize',
404 );
405
406 return isset( $map[ $stage ] ) ? $map[ $stage ] : '';
407 }
408
409 /**
410 * Build the entry point's response from a finished run.
411 *
412 * @param array $run Runner result.
413 * @param string $session_id Session id.
414 * @param string[] $skip Steps the caller asked to skip.
415 * @return array
416 */
417 private static function response( $run, $session_id, $skip ) {
418 $session = ( ! empty( $run['session'] ) && is_array( $run['session'] ) ) ? $run['session'] : array();
419 $results = ! empty( $session['results'] ) ? $session['results'] : array();
420 $failed = ! empty( $session['failed'] ) ? $session['failed'] : array();
421
422 $pages = array();
423 $errors = array();
424
425 foreach ( $results as $step => $record ) {
426 if ( ! is_array( $record ) || 0 !== strpos( (string) $step, 'page_' ) ) {
427 continue;
428 }
429
430 if ( empty( $record['post_id'] ) ) {
431 continue;
432 }
433
434 $pages[] = array(
435 'template_id' => isset( $record['template_id'] ) ? $record['template_id'] : substr( (string) $step, 5 ),
436 'post_id' => (int) $record['post_id'],
437 'title' => isset( $record['title'] ) ? $record['title'] : '',
438 'url' => isset( $record['url'] ) ? $record['url'] : get_permalink( (int) $record['post_id'] ),
439 'success' => true,
440 );
441 }
442
443 foreach ( $failed as $step => $record ) {
444 /* Stage-level "some items failed" markers are bookkeeping, not something a
445 * caller can act on — the per-item records below carry the real reasons. */
446 if ( ! is_array( $record ) || ( isset( $record['code'] ) && 'stage_incomplete' === $record['code'] ) ) {
447 continue;
448 }
449
450 $errors[] = array(
451 'step' => (string) $step,
452 'code' => isset( $record['code'] ) ? $record['code'] : '',
453 'message' => isset( $record['message'] ) ? $record['message'] : '',
454 );
455 }
456
457 $steps = self::step_summary( $results, $failed, $skip, $pages );
458
459 $home_page_id = 0;
460 $shop_page_id = 0;
461
462 if ( ! empty( $results['site_settings']['front_page_id'] ) ) {
463 $home_page_id = (int) $results['site_settings']['front_page_id'];
464 }
465
466 if ( ! empty( $results['site_settings']['shop_page_id'] ) ) {
467 $shop_page_id = (int) $results['site_settings']['shop_page_id'];
468 }
469
470 $success = count( $pages ) > 0;
471
472 return array(
473 'success' => $success,
474
475 /* The run stopped on purpose and has more to do — it activated a plugin and
476 * needs a fresh request before the rest of the import can use it. A caller that
477 * ignores this still sees `success`, so nothing that existed before changes;
478 * a caller that resumes should call again with `session_id`. */
479 'restart' => ! empty( $run['restart'] ),
480
481 'message' => ! empty( $run['restart'] )
482 ? 'Plugins installed. Resume to finish the import.'
483 : ( $success ? 'Site created successfully.' : 'Site creation completed with errors.' ),
484 'site_url' => get_site_url(),
485 'home_page_id' => $home_page_id,
486 'shop_page_id' => $shop_page_id,
487 'pages' => $pages,
488 'steps' => $steps,
489 'errors' => $errors,
490
491 /* Additive. The handle for a resume. */
492 'session_id' => $session_id,
493 'status' => isset( $session['status'] ) ? $session['status'] : '',
494
495 /* Additive, and the honest half of `success`.
496 *
497 * A headless run imports the content correctly and then cannot build
498 * plus-global.css, because only TPGB's editor JS writes that file and there is no
499 * browser here (see Wdkit_Import_Css::globals()). The pages are real and the
500 * content is right, but every design token is undefined until someone opens the
501 * site in a browser, so reporting a bare "Site created successfully." overstates
502 * what the caller got. A caller that ignores this key behaves exactly as before. */
503 'styling_pending' => self::styling_pending( $results ),
504 );
505 }
506
507 /**
508 * Did the run finish without the Gutenberg design-token file?
509 *
510 * @since 2.7.2
511 *
512 * @param array $results Stage results.
513 * @return bool
514 */
515 private static function styling_pending( $results ) {
516 $finalize = isset( $results[ Wdkit_Import_Runner::STAGE_FINALIZE ] ) ? $results[ Wdkit_Import_Runner::STAGE_FINALIZE ] : array();
517
518 if ( ! is_array( $finalize ) || empty( $finalize['global_css'] ) || ! is_array( $finalize['global_css'] ) ) {
519 return false;
520 }
521
522 return ( 'needs_editor_pass' === ( $finalize['global_css']['reason'] ?? '' ) );
523 }
524
525 /**
526 * The `steps` map, under the key names this entry point has always used.
527 *
528 * @param array $results Session results.
529 * @param array $failed Session failures.
530 * @param string[] $skip Skipped steps.
531 * @param array $pages Imported pages.
532 * @return array
533 */
534 private static function step_summary( $results, $failed, $skip, $pages ) {
535 $steps = array();
536
537 if ( ! in_array( 'reset_site', $skip, true ) ) {
538 $reset = isset( $results[ Wdkit_Import_Runner::STAGE_DEPENDENCIES ]['reset'] )
539 ? $results[ Wdkit_Import_Runner::STAGE_DEPENDENCIES ]['reset']
540 : ( isset( $results['reset_site'] ) ? $results['reset_site'] : array() );
541
542 $steps['reset_site'] = array(
543 'success' => ! empty( $reset['status'] ) && 'reset' === $reset['status'],
544 'drafted' => isset( $reset['drafted'] ) ? (int) $reset['drafted'] : 0,
545 );
546 }
547
548 foreach ( array( 'plugin_settings', 'theme_settings' ) as $key ) {
549 if ( in_array( $key, $skip, true ) ) {
550 continue;
551 }
552
553 $steps[ $key ] = array( 'success' => isset( $results[ $key ] ) && empty( $failed[ $key ] ) );
554 }
555
556 $steps['import_pages'] = array(
557 'success' => count( $pages ) > 0,
558 'count' => count( $pages ),
559 'errors' => count(
560 array_filter(
561 array_keys( $failed ),
562 static function ( $step ) {
563 return 0 === strpos( (string) $step, 'page_' );
564 }
565 )
566 ),
567 );
568
569 if ( ! in_array( 'enable_widgets', $skip, true ) ) {
570 $steps['enable_widgets'] = array( 'success' => ! empty( $results['widgets']['enabled'] ) );
571 }
572
573 $steps['finalize'] = array(
574 'success' => ! empty( $results[ Wdkit_Import_Runner::STAGE_FINALIZE ]['finalized'] ),
575 );
576
577 return $steps;
578 }
579
580 /**
581 * The entry point's error shape.
582 *
583 * @param string $message Message.
584 * @return array
585 */
586 private static function error( $message ) {
587 return array(
588 'success' => false,
589 'message' => $message,
590 'site_url' => get_site_url(),
591 'home_page_id' => 0,
592 'shop_page_id' => 0,
593 'pages' => array(),
594 'steps' => array(),
595 'errors' => array( array( 'step' => '', 'code' => 'invalid_args', 'message' => $message ) ),
596 'session_id' => '',
597 'status' => 'failed',
598 );
599 }
600 /**
601 * A kit's template list, straight from the cloud.
602 *
603 * Same request the browser makes on the preview screen - `kit_template` with the kit id -
604 * through WDesignKit_Data_Query, so no AJAX round trip and no browser.
605 *
606 * @since 2.6.5
607 *
608 * @param string $kit_id Kit id.
609 * @param string $builder 'elementor'|'gutenberg'|''.
610 * @return array[] Template records, or [] when the kit cannot be read.
611 */
612 private static function fetch_kit_templates( $kit_id, $builder = '', &$resolved = null ) {
613 if ( ! class_exists( 'WDesignKit_Data_Query' ) || '' === $kit_id ) {
614 return array();
615 }
616
617 /* A named builder is asked for on its own. An unnamed one tries both rather than
618 * assuming, because the kit list is fetched PER BUILDER — there is no content yet
619 * to infer a builder from, so assuming Gutenberg made an Elementor kit come back
620 * with nothing and fail as "the kit returned none". Order puts Gutenberg first
621 * only because it is the common case; whichever answers wins. */
622 /* A NAMED builder is a preference, not a promise.
623 *
624 * It used to be the only candidate, so naming the wrong one — which the website does
625 * whenever its record of a kit's builder is stale, and a caller does whenever it
626 * guesses — returned nothing and failed the whole import as "the kit returned none",
627 * even though the kit was right there under the other builder. Verified: kit 21603 is
628 * a Gutenberg kit, and `builder => elementor` killed the run outright.
629 *
630 * The named one is still tried FIRST and still wins when it answers, so a kit that
631 * genuinely ships both is unaffected. The other is only reached when the first came
632 * back empty, which is a request that would otherwise have been a hard failure. */
633 $candidates = in_array( $builder, array( 'elementor', 'gutenberg' ), true )
634 ? array( $builder, ( 'gutenberg' === $builder ) ? 'elementor' : 'gutenberg' )
635 : array( 'gutenberg', 'elementor' );
636
637 $response = array();
638
639 foreach ( $candidates as $candidate ) {
640 $attempt = WDesignKit_Data_Query::get_data(
641 'kit_template',
642 array(
643 'template_id' => $kit_id,
644 'builder' => $candidate,
645 'editor' => $candidate,
646 )
647 );
648
649 /* get_data() answers with a WP_Error when the request itself fails - a timeout, a
650 * rate limit, a DNS blip. Indexing that as an array is a fatal, and it took the
651 * whole import down from inside a cron run where nothing was watching. A failed
652 * candidate is simply a candidate that did not answer. */
653 if ( ! is_array( $attempt ) ) {
654 continue;
655 }
656
657 if ( ! empty( $attempt['success'] ) && ! empty( $attempt['template'] ) && is_array( $attempt['template'] ) ) {
658 $response = $attempt;
659
660 /* Which builder actually answered. The caller corrects its own context with
661 * this, because every later decision — the page importer's editor, the
662 * Gutenberg-only block CSS pass — reads the context builder, and leaving it
663 * on the name that returned nothing would import the kit as the wrong
664 * builder. */
665 $resolved = $candidate;
666
667 break;
668 }
669 }
670
671 if ( empty( $response['template'] ) || ! is_array( $response['template'] ) ) {
672 return array();
673 }
674
675 $out = array();
676
677 foreach ( $response['template'] as $template ) {
678 if ( ! is_array( $template ) || empty( $template['id'] ) ) {
679 continue;
680 }
681
682 /* Rebuilt rather than passed through, so a change in the cloud's response shape
683 * cannot introduce fields nothing validated. */
684 $out[] = array(
685 'id' => (string) $template['id'],
686 'title' => isset( $template['title'] ) ? (string) $template['title'] : '',
687 'type' => isset( $template['type'] ) ? (string) $template['type'] : 'page',
688 'wp_post_type' => isset( $template['wp_post_type'] ) ? (string) $template['wp_post_type'] : 'page',
689
690 /* Where this template lives on the demo site. Every internal link the kit
691 * ships is an absolute URL into that demo, so without this there is nothing
692 * to rewrite them against and the imported site's menu, buttons and footer
693 * all navigate away to gtemplates.wdesignkit.com. */
694 'post_url' => isset( $template['post_url'] ) ? (string) $template['post_url'] : '',
695
696 /* Catalogue ids for this template's plugin dependencies. Dropping them meant
697 * a headless import resolved no per-template dependencies at all, so the
698 * plugin list came out short of what the same kit installs in the browser. */
699 'plugins_id' => isset( $template['plugins_id'] ) ? $template['plugins_id'] : array(),
700
701 /* The kit's global palette and typography, as shipped with the template.
702 * resolve_kit_global() reads it off this list before the context validator
703 * strips it - the same list get_global_data() walks in the browser, and the
704 * only place the kit's colours and fonts are stated. */
705 'global_data' => self::sanitize_global_data( isset( $template['global_data'] ) ? $template['global_data'] : array() ),
706 );
707 }
708
709 return $out;
710 }
711
712 /**
713 * Keep only the two lists a kit's `global_data` is allowed to carry.
714 *
715 * Same reasoning as rebuilding the template entry itself: this comes off a cloud
716 * response, so it is reduced to the shape collect() reads - `color` and `typography`,
717 * each a list of arrays - rather than trusted wholesale. Entries themselves are left
718 * alone; from_kit() and apply() are what decide which of their keys mean anything.
719 *
720 * @param mixed $value Raw global_data.
721 * @return array
722 */
723 private static function sanitize_global_data( $value ) {
724 if ( ! is_array( $value ) ) {
725 return array();
726 }
727
728 $out = array();
729
730 foreach ( array( 'color', 'typography' ) as $key ) {
731 if ( empty( $value[ $key ] ) || ! is_array( $value[ $key ] ) ) {
732 continue;
733 }
734
735 $entries = array();
736
737 foreach ( $value[ $key ] as $entry ) {
738 if ( is_array( $entry ) ) {
739 $entries[] = $entry;
740 }
741 }
742
743 if ( ! empty( $entries ) ) {
744 $out[ $key ] = $entries;
745 }
746 }
747
748 return $out;
749 }
750
751 }
752 }
753