| 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 |
|