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

class-wdkit-kit-import-hook.php in WDesignKit – AI Templates, Widget Builder & MCP Workflow 2.7.0, at includes/admin/hooks/class-wdkit-kit-import-hook.php

400 lines 18.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Programmatic hooks for external plugins to trigger WDesignKit's kit import
4 * and full site creation flows without duplicating any internal logic.
5 *
6 * ──────────────────────────────────────────────────────────────────────────
7 * HOOK 1 — Single template fetch (building block)
8 * ──────────────────────────────────────────────────────────────────────────
9 * $result = apply_filters( 'wdkit_import_kit_content', [], [
10 * 'template_id' => 'abc123', // required
11 * 'editor' => 'elementor', // 'elementor' | 'gutenberg'
12 * 'website_kit' => 'my-kit-slug', // optional
13 * 'api_type' => 'import_template', // cloud endpoint (note: import_kit_template has no cloud route)
14 * 'custom_meta' => false, // restore nxt-* post meta
15 * ] );
16 *
17 * ──────────────────────────────────────────────────────────────────────────
18 * HOOK 2 — Full site creation (master hook — use this from Sprout MCP)
19 * ──────────────────────────────────────────────────────────────────────────
20 * $result = apply_filters( 'wdkit_create_full_site', [], [
21 * 'kit_id' => 'my-kit-slug', // required — WDesignKit kit ID
22 * 'editor' => 'elementor', // 'elementor' | 'gutenberg'
23 * 'templates' => [ // required — list from cloud
24 * [
25 * 'id' => 'tpl-123',
26 * 'title' => 'Home|Landing',
27 * 'type' => 'page', // 'page' | 'section'
28 * 'wp_post_type' => 'page',
29 * ],
30 * // ... more templates
31 * ],
32 * 'site_name' => 'My Business', // optional
33 * 'tagline' => 'We build things', // optional
34 * 'skip' => ['reset_site'], // optional steps to skip
35 * ] );
36 *
37 * Progress lifecycle (fire-and-forget — Sprout listens, WDesignKit fires):
38 * add_action( 'wdkit_site_step', function( $step, $status, $data ) { }, 10, 3 );
39 * $step : 'reset_site' | 'plugin_settings' | 'theme_settings' |
40 * 'import_pages' | 'enable_widgets' | 'finalize'
41 * $status : 'start' | 'done' | 'fail'
42 * $data : context array for the step
43 *
44 * @package Wdesignkit
45 * @since 2.3.1
46 */
47
48 if ( ! defined( 'ABSPATH' ) ) {
49 exit;
50 }
51
52 // ─── Hook 1: single template fetch ──────────────────────────────────────────
53 add_filter( 'wdkit_import_kit_content', 'wdkit_handle_kit_import_hook', 10, 2 );
54
55 // ─── Hook 2: full site creation ─────────────────────────────────────────────
56 add_filter( 'wdkit_create_full_site', 'wdkit_handle_create_full_site', 10, 2 );
57
58 // ════════════════════════════════════════════════════════════════════════════
59 // HOOK 1 — Single template fetch
60 // ════════════════════════════════════════════════════════════════════════════
61
62 /**
63 * Handle the wdkit_import_kit_content filter.
64 * Fetches one template's JSON content from WDesignKit cloud.
65 *
66 * @param array $output Ignored — always overwritten.
67 * @param array $args Import arguments. Required: template_id.
68 * @return array{success:bool,message:string,description:string,id:string,response:array}
69 */
70 function wdkit_handle_kit_import_hook( array $output, array $args ): array {
71
72 if ( ! class_exists( 'WDesignKit_Data_Query' ) ) {
73 return [
74 'success' => false,
75 'message' => __( 'WDesignKit plugin is not loaded.', 'wdesignkit' ),
76 'description' => '',
77 'id' => '',
78 'response' => [],
79 ];
80 }
81
82 $token = wdkit_kit_import_resolve_token();
83 if ( ! wdkit_kit_import_can_authenticate( $token ) ) {
84 return [
85 'success' => false,
86 'message' => __( 'Not logged in to WDesignKit cloud. Go to WP Admin → WDesignKit and click Login.', 'wdesignkit' ),
87 'description' => '',
88 'id' => '',
89 'response' => [],
90 ];
91 }
92
93 $template_id = sanitize_text_field( (string) ( $args['template_id'] ?? '' ) );
94 $editor = sanitize_text_field( (string) ( $args['editor'] ?? 'elementor' ) );
95 $website_kit = sanitize_text_field( (string) ( $args['website_kit'] ?? '' ) );
96 // Default to 'import_template' — get_data() posts to api/wp/{api_type}, and there is NO
97 // api/wp/import_kit_template route on the cloud (it 404s → "Unexpected response"). The
98 // working endpoint is import_template, which is also what wdkit_import_kit_template() uses.
99 $api_type = sanitize_text_field( (string) ( $args['api_type'] ?? 'import_template' ) );
100 $custom_meta = ! empty( $args['custom_meta'] );
101
102 if ( $template_id === '' ) {
103 return [
104 'success' => false,
105 'message' => __( 'template_id is required.', 'wdesignkit' ),
106 'description' => '',
107 'id' => '',
108 'response' => [],
109 ];
110 }
111
112 // Mirrors $temp_args in wdkit_import_kit_template() exactly — only these
113 // five keys are sent to the cloud for a kit import.
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 );
123
124 /**
125 * Fires just before WDesignKit makes the cloud import request.
126 *
127 * @param string $template_id Template ID being imported.
128 * @param array $cloud_args Arguments sent to the cloud API.
129 */
130 do_action( 'wdkit_before_kit_import', $template_id, $cloud_args );
131
132 $response = WDesignKit_Data_Query::get_data( $api_type, $cloud_args );
133
134 if ( is_wp_error( $response ) ) {
135 return [
136 'success' => false,
137 'message' => $response->get_error_message(),
138 'description' => '',
139 'id' => $template_id,
140 'response' => [],
141 ];
142 }
143
144 if ( ! is_array( $response ) ) {
145 return [
146 'success' => false,
147 'message' => __( 'Unexpected response from WDesignKit cloud.', 'wdesignkit' ),
148 'description' => '',
149 'id' => $template_id,
150 'response' => [],
151 ];
152 }
153
154 // Cloud signals a soft failure via content === 'error' (line 2866 in class-api.php).
155 if ( isset( $response['content'] ) && 'error' === $response['content'] ) {
156 return [
157 'success' => false,
158 'message' => $response['message'] ?? 'Cloud returned a content error.',
159 'description' => $response['description'] ?? '',
160 'id' => $template_id,
161 'response' => $response,
162 ];
163 }
164
165 // Restore nxt-* post meta onto the current post when custom_meta is requested.
166 if ( $custom_meta && ! empty( $response['content'] ) ) {
167 $current_post_id = get_the_ID();
168 $decoded = json_decode( (string) $response['content'], true );
169
170 if ( $current_post_id && is_array( $decoded ) && ! empty( $decoded['custom_meta'] ) ) {
171 foreach ( $decoded['custom_meta'] as $meta_key => $meta_val ) {
172 $value = $meta_val[0] ?? null;
173 if ( is_string( $value ) && is_serialized( $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 ) );
185 }
186 if ( get_post_meta( $current_post_id, $meta_key, true ) === '' ) {
187 add_post_meta( $current_post_id, $meta_key, $value );
188 } else {
189 update_post_meta( $current_post_id, $meta_key, $value );
190 }
191 }
192 }
193 }
194
195 $result = [
196 'success' => (bool) ( $response['success'] ?? ! empty( $response['content'] ) ),
197 'message' => $response['message'] ?? '',
198 'description' => $response['description'] ?? '',
199 'id' => $template_id,
200 'response' => $response,
201 ];
202
203 /**
204 * Fires after the kit import attempt completes.
205 *
206 * @param array $result Import result.
207 * @param string $template_id Template ID that was imported.
208 */
209 do_action( 'wdkit_after_kit_import', $result, $template_id );
210
211 return $result;
212 }
213
214 // ════════════════════════════════════════════════════════════════════════════
215 // HOOK 2 — Full site creation
216 // ════════════════════════════════════════════════════════════════════════════
217
218 /**
219 * Handle the wdkit_create_full_site filter.
220 *
221 * Runs all 4 site-creation steps internally; fires wdkit_site_step actions
222 * at start/done/fail of each step so external plugins can track progress
223 * without knowing the internal sequence.
224 *
225 * @param array $output Ignored — always overwritten.
226 * @param array $args {
227 * @type string $kit_id WDesignKit kit ID (required).
228 * @type string $editor 'elementor' | 'gutenberg' (default: 'elementor').
229 * @type array $templates List of template objects from cloud (required).
230 * @type string $site_name Optional site title.
231 * @type string $tagline Optional site tagline.
232 * @type string[] $skip Steps to skip: 'reset_site', 'plugin_settings',
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.
249 * }
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}
251 */
252 function wdkit_handle_create_full_site( array $output, array $args ): array {
253
254 // ── Guard ────────────────────────────────────────────────────────────────
255 if ( ! class_exists( 'WDesignKit_Data_Query' ) ) {
256 return wdkit_site_error( 'WDesignKit plugin is not loaded.' );
257 }
258
259 if ( ! current_user_can( 'manage_options' ) ) {
260 return wdkit_site_error( 'Insufficient permissions.' );
261 }
262
263 $token = wdkit_kit_import_resolve_token();
264 if ( ! wdkit_kit_import_can_authenticate( $token ) ) {
265 return wdkit_site_error( 'Not logged in to WDesignKit cloud. Go to WP Admin → WDesignKit and click Login.' );
266 }
267
268 if ( ! class_exists( 'Wdkit_Import_Bridge' ) ) {
269 return wdkit_site_error( 'Import engine is not available.' );
270 }
271
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 }
287
288 // ════════════════════════════════════════════════════════════════════════════
289 // Shared helpers
290 // ════════════════════════════════════════════════════════════════════════════
291
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;
308 }
309
310 return array_merge( $args, Wdkit_Import_Remote::cloud_identity() );
311 }
312
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;
324 }
325
326 return class_exists( 'Wdkit_Import_Remote' ) && array() !== Wdkit_Import_Remote::cloud_identity();
327 }
328
329 /**
330 * Resolve an active WDesignKit cloud token without depending on the abilities system.
331 * Checks current WP user transient first, then scans all wdkit_auth_* transients.
332 *
333 * @return string Token string, or empty string if not logged in.
334 */
335 function wdkit_kit_import_resolve_token(): string {
336 // Shared with every cloud ability — see wdesignkit_mcp_find_auth_session() in
337 // includes/abilities/class-wdk-ability-main.php. It checks the account recorded at login
338 // first, so the session is found even though its transient is keyed off the CLOUD email
339 // rather than the WordPress user's. The local lookup below only tried the WP user's key and
340 // then an unordered LIMIT 10 scan, which reported "not logged in" straight after a
341 // successful login on any site that had accumulated a few stored sessions.
342 if ( function_exists( 'wdesignkit_mcp_find_auth_session' ) ) {
343 $session = wdesignkit_mcp_find_auth_session();
344
345 return ! empty( $session['found'] ) ? (string) $session['data']['token'] : '';
346 }
347
348 // Fallback for the (unexpected) case where the abilities loader has not run.
349 $current_user = wp_get_current_user();
350 if ( $current_user && $current_user->user_email ) {
351 $key = strstr( $current_user->user_email, '@', true );
352 $data = get_transient( 'wdkit_auth_' . $key );
353 if ( ! empty( $data['token'] ) ) {
354 return (string) $data['token'];
355 }
356 }
357
358 global $wpdb;
359 $rows = $wpdb->get_results(
360 $wpdb->prepare(
361 "SELECT option_name, option_value FROM {$wpdb->options} WHERE option_name LIKE %s",
362 $wpdb->esc_like( '_transient_wdkit_auth_' ) . '%'
363 ),
364 ARRAY_A
365 );
366
367 foreach ( ( $rows ?: [] ) as $row ) {
368 $key = str_replace( '_transient_', '', $row['option_name'] );
369 $timeout = get_option( '_transient_timeout_' . $key );
370 if ( $timeout && (int) $timeout < time() ) {
371 continue;
372 }
373 $data = @maybe_unserialize( $row['option_value'] );
374 if ( is_array( $data ) && ! empty( $data['token'] ) ) {
375 return (string) $data['token'];
376 }
377 }
378
379 return '';
380 }
381
382 /**
383 * Build a standard error return for wdkit_create_full_site.
384 *
385 * @param string $message Human-readable error.
386 * @return array
387 */
388 function wdkit_site_error( string $message ): array {
389 return [
390 'success' => false,
391 'message' => $message,
392 'site_url' => '',
393 'home_page_id' => 0,
394 'shop_page_id' => 0,
395 'pages' => [],
396 'steps' => [],
397 'errors' => [ [ 'message' => $message ] ],
398 ];
399 }
400