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-import-temp-ajax.php

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

1,236 lines 42.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The file that defines the core plugin class
4 *
5 * @link https://posimyth.com/
6 * @since 1.1.1
7 *
8 * @package Wdesignkit
9 * @subpackage Wdesignkit/includes
10 */
11
12 /**
13 * Exit if accessed directly.
14 * */
15 if ( ! defined( 'ABSPATH' ) ) {
16 exit;
17 }
18
19 use wdkit\Wdkit_Wdesignkit;
20 use wdkit\wdkit_datahooks\Wdkit_Data_Hooks;
21
22
23
24 if ( ! class_exists( 'Wdkit_Import_temp_Ajax' ) ) {
25
26 /**
27 * It is wdesignkit Main Class
28 *
29 * @since 2.0
30 */
31 class Wdkit_Import_temp_Ajax {
32
33 /**
34 * Member Variable
35 *
36 * @var instance
37 */
38 private static $instance;
39
40 /**
41 * Member Variable
42 *
43 * @var staring $wdkit_api
44 */
45 public $wdkit_api = WDKIT_SERVER_API_URL . 'api/wp/';
46 public $wdkit_front_api = WDKIT_SERVER_SITE_URL . 'next/api/v2/';
47
48 /**
49 * Initiator
50 */
51 public static function get_instance() {
52 if ( ! isset( self::$instance ) ) {
53 self::$instance = new self();
54 }
55
56 return self::$instance;
57 }
58
59 /**
60 * Define the core functionality of the plugin.
61 */
62 public function __construct() {
63 add_filter( 'wp_wdkit_import_temp_ajax', array( $this, 'wdkit_import_temp_ajax_call' ) );
64 }
65
66 /**
67 * Get Wdkit Api Call Ajax.
68 *
69 * @since 1.1.1
70 */
71 public function wdkit_import_temp_ajax_call( $type ) {
72
73 check_ajax_referer( 'wdkit_nonce', 'kit_nonce' );
74
75 if ( ! is_user_logged_in() || ! current_user_can( 'manage_options' ) ) {
76 wp_send_json_error( array( 'content' => __( 'Insufficient permissions.', 'wdesignkit' ) ) );
77 }
78
79 if ( ! $type ) {
80 $this->wdkit_error_msg( 'Something went wrong.' );
81 }
82
83 switch ( $type ) {
84 case 'select_team_img':
85 $response = $this->wdkit_select_team_img();
86 break;
87 case 'wkit_ai_desc_keyword':
88 $response = $this->wkit_ai_desc_keyword();
89 break;
90 case 'wkit_ai_credit_update':
91 $response = $this->wkit_ai_credit_update();
92 break;
93 case 'wkit_generate_post_data':
94 $response = $this->wkit_generate_post_data();
95 break;
96 case 'wkit_cteate_product':
97 $response = $this->wkit_cteate_product();
98 break;
99 case 'wkit_generate_product_data':
100 $response = $this->wkit_generate_product_data();
101 break;
102 case 'wkit_remove_dummy_post':
103 $response = $this->wkit_remove_dummy_post();
104 break;
105 case 'wkit_create_widget':
106 $response = $this->wkit_create_widget();
107 break;
108 case 'generate_ai_content':
109 $response = $this->wkit_generate_ai_content();
110 break;
111 case 'generate_ai_content_batch':
112 $response = $this->wkit_generate_ai_content_batch();
113 break;
114 case 'reset_site':
115 $response = $this->wkit_reset_site();
116 break;
117 case 'wdkit_remove_header_footer':
118 $response = $this->wdkit_remove_header_footer();
119 break;
120 case 'check_post_count':
121 $response = $this->wkit_check_post_count();
122 break;
123 case 'wkit_check_product_count':
124 $response = $this->wkit_check_product_count();
125 break;
126 }
127
128 wp_send_json( $response );
129 wp_die();
130 }
131
132 /**
133 *
134 * select team image for import kit
135 *
136 * @since 2.0.0
137 */
138 public function wdkit_select_team_img() {
139 $result = $this->wdkit_team_images_data(
140 isset($_POST['folder_id']) ? sanitize_text_field($_POST['folder_id']) : '',
141 isset($_POST['img_type']) ? sanitize_text_field($_POST['img_type']) : 'default',
142 isset($_POST['image_count']) ? intval($_POST['image_count']) : 5,
143 isset($_POST['token']) ? sanitize_text_field($_POST['token']) : ''
144 );
145
146 if ( empty( $result['success'] ) ) {
147 /* Emit exactly the three keys this action has always emitted on failure — the
148 * service's `data` key is internal and must not reach the AJAX response. */
149 wp_send_json(
150 array(
151 'success' => false,
152 'message' => $result['message'],
153 'description' => $result['description'],
154 )
155 );
156 wp_die();
157 }
158
159 return $result['data'];
160 }
161
162 /**
163 * Fetch team-library images for one collection.
164 *
165 * Extracted from wdkit_select_team_img() so the PHP import runner can source team
166 * photos without `$_POST`. The cloud call, endpoint and payload shape are unchanged.
167 *
168 * Returns a wrapper rather than the bare payload because the AJAX adapter has to be
169 * able to tell "no images" from "images", and the failure response it emits must stay
170 * byte-for-byte what it was.
171 *
172 * @since 2.6.5
173 *
174 * @param string $folder_id Team collection id.
175 * @param string $img_type Collection variant.
176 * @param int $count How many images are needed.
177 * @param string $token Cloud auth token.
178 * @return array{success:bool,data:array,message?:string,description?:string}
179 */
180 public function wdkit_team_images_data( $folder_id, $img_type = 'default', $count = 5, $token = '' ) {
181 $array_data = array(
182 'id' => sanitize_text_field( (string) $folder_id ),
183 'count' => (int) $count,
184 'type' => '' !== $img_type ? sanitize_text_field( (string) $img_type ) : 'default',
185 'token' => sanitize_text_field( (string) $token ),
186 );
187
188 $response = $this->wkit_api_call( $array_data, 'ai/team/image' );
189 $success = ! empty( $response['success'] ) ? $response['success'] : false;
190
191 if ( empty( $success ) ) {
192 return array(
193 'success' => false,
194 'message' => esc_html__( 'Data Not Found', 'wdesignkit' ),
195 'description' => esc_html__( 'Images not found', 'wdesignkit' ),
196 'data' => array(),
197 );
198 }
199
200 return array(
201 'success' => true,
202 'data' => json_decode( wp_json_encode( $response['data'] ), true ),
203 );
204 }
205
206 /**
207 *
208 * generate ai descrioption and image keyword from kit
209 *
210 * @since 2.0.0
211 */
212 protected function wkit_ai_desc_keyword (){
213 $array_data = array(
214 'site_name' => isset($_POST['site_type']) ? sanitize_text_field($_POST['site_type']) : '',
215 // intval() on a prose paragraph. The site description is what the classifier is
216 // meant to categorise from, and intval( 'Indulge in a cheesy pizza...' ) is 0 -
217 // so it has never received one, and every category pick has been made from the
218 // site name alone. sanitize_textarea_field() keeps the newlines the prompt reads.
219 'description' => isset($_POST['site_desc']) ? sanitize_textarea_field(wp_unslash($_POST['site_desc'])) : '',
220 'type' => isset($_POST['api_type']) ? sanitize_text_field($_POST['api_type']) : 'description',
221 'token' => isset($_POST['token']) ? sanitize_text_field($_POST['token']) : '',
222 );
223
224 $response = $this->wkit_api_call( $array_data, 'ai/metadata' );
225 $success = ! empty( $response['success'] ) ? $response['success'] : false;
226
227 if ( empty( $success ) ) {
228 $response = array(
229 'success' => false,
230 'message' => esc_html__( 'Data Not Found', 'wdesignkit' ),
231 'description' => esc_html__( 'Images not found', 'wdesignkit' ),
232 );
233
234 wp_send_json( $response );
235 wp_die();
236 }
237
238 $response = json_decode( wp_json_encode( $response['data'] ), true );
239
240 return $response;
241 }
242
243 /**
244 *
245 * update user credit as per page import
246 *
247 * @since 2.1.7
248 */
249 protected function wkit_ai_credit_update (){
250 $array_data = array(
251 'kit_id' => isset($_POST['kit_id']) ? sanitize_text_field($_POST['kit_id']) : '',
252 'site_url' => isset($_POST['site_url']) ? sanitize_text_field($_POST['site_url']) : '',
253 'used_credit' => isset($_POST['used_credit']) ? intval($_POST['used_credit']) : '',
254 'real_credit' => isset($_POST['real_credit']) ? intval($_POST['real_credit']) : '',
255 'ids_failed' => isset($_POST['ids_failed']) ? sanitize_text_field($_POST['ids_failed']) : '',
256 'ids_success' => isset($_POST['ids_success']) ? sanitize_text_field($_POST['ids_success']) : '',
257 'token' => isset($_POST['token']) ? sanitize_text_field($_POST['token']) : '',
258 );
259
260 $response = $this->wkit_api_call( $array_data, 'ai/history/set' );
261 $success = ! empty( $response['success'] ) ? $response['success'] : false;
262
263 if ( empty( $success ) ) {
264 $response = array(
265 'success' => false,
266 'message' => esc_html__( 'Something Wrong', 'wdesignkit' ),
267 'description' => esc_html__( 'Something Wrong', 'wdesignkit' ),
268 );
269
270 wp_send_json( $response );
271 wp_die();
272 }
273
274 $response = json_decode( wp_json_encode( $response['data'] ), true );
275
276 return $response;
277 }
278
279 /**
280 * Insert Remove Hello World post
281 *
282 * @since 2.2.7
283 */
284 protected function wkit_remove_dummy_post (){
285 $response = $this->wdkit_remove_dummy_post_data();
286
287 wp_send_json( $response );
288 wp_die();
289 }
290
291 /**
292 * Remove WordPress's default "Hello world!" post.
293 *
294 * Extracted from wkit_remove_dummy_post() so the PHP import runner can reach it without
295 * `$_POST`. Selection is unchanged and deliberately narrow: post_type `post`, exact
296 * title `Hello world!`, in the `uncategorized` category, one result. Capability check
297 * (`delete_posts`) is unchanged, and the delete still skips the trash.
298 *
299 * ⚠ The post it removes was created by WordPress, not by the importer, so ownership
300 * *by this plugin* cannot be proven — a site could in principle have a real post with
301 * that exact title in Uncategorized. The browser deletes it unconditionally during an
302 * import; the PHP runner requires an explicit opt-in instead. See Wdkit_Import_Cleanup.
303 *
304 * @since 2.6.5
305 *
306 * @return array Response array, exactly as the AJAX action used to emit.
307 */
308 public function wdkit_remove_dummy_post_data() {
309 // Security check (recommended)
310 if ( ! current_user_can( 'delete_posts' ) ) {
311 return [
312 'success' => false,
313 'message' => __( 'Permission denied', 'wdesignkit' ),
314 'description' => 'Permission denied',
315 ];
316 }
317
318 $args = array(
319 'post_type' => 'post',
320 'post_status' => 'any',
321 'posts_per_page' => 1,
322 'title' => 'Hello world!',
323 'tax_query' => array(
324 array(
325 'taxonomy' => 'category',
326 'field' => 'slug',
327 'terms' => 'uncategorized',
328 ),
329 ),
330 );
331
332 $query = new WP_Query( $args );
333
334 if ( $query->have_posts() ) {
335 while ( $query->have_posts() ) {
336 $query->the_post();
337
338 $post_id = get_the_ID();
339
340 // Force delete (skip trash)
341 wp_delete_post( $post_id, true );
342 }
343
344 wp_reset_postdata();
345
346 return [
347 'success' => true,
348 'message' => __( 'Post Removed Successfully !', 'wdesignkit' ),
349 'description' => 'Dummy post successfully removed',
350 ];
351 }
352
353 return [
354 'success' => false,
355 'message' => __( 'No matching post found', 'wdesignkit' ),
356 'description' => 'Dummy post not found',
357 ];
358 }
359
360 /**
361 * Insert dummy product data if not available
362 *
363 * @since 2.0.0
364 */
365 protected function wkit_generate_product_data (){
366
367 $array_data = array(
368 'site_type' => isset( $_POST['site_type'] ) ? sanitize_text_field( $_POST['site_type'] ) : '',
369 'site_desc' => isset( $_POST['site_desc'] ) ? sanitize_text_field( $_POST['site_desc'] ) : '',
370 'site_title' => isset( $_POST['site_title'] ) ? sanitize_text_field( $_POST['site_title'] ) : '',
371 'token' => isset($_POST['token']) ? sanitize_text_field($_POST['token']) : '',
372 );
373
374 $response = $this->wkit_api_call( $array_data, 'ai/template/product/generate' );
375 $data = !empty( $response['data'] ) ? $response['data'] : [];
376
377 $success = ! empty( $data->success ) ? $data->success : false;
378
379 if ( empty($success) ) {
380 wp_send_json([
381 'success' => false,
382 'message' => !empty( $data->message ) ? $data->message : 'API connection failed',
383 'description' => !empty( $data->description ) ? $data->description : 'API connection failed',
384 ]);
385 wp_die();
386 }
387
388 wp_send_json([
389 'success' => true,
390 'message' => !empty( $data->message ) ? $data->message : 'Product Data Generated Successfully',
391 'description' => !empty( $data->description ) ? $data->description : 'Product Data Generated Successfully',
392 'response' => $data->data,
393 ]);
394 wp_die();
395 }
396
397 /**
398 * Insert Products if not available
399 *
400 * @since 2.0.0
401 */
402 protected function wkit_cteate_product() {
403 $response = $this->wdkit_create_product_data(
404 array(
405 'product_title' => isset($_POST['product_title']) ? sanitize_text_field($_POST['product_title']) : '',
406 'product_image' => isset($_POST['product_image']) ? esc_url_raw($_POST['product_image']) : '',
407 // Placeholder attachment the browser sideloaded once for the whole batch.
408 'product_image_id' => isset($_POST['product_image_id']) ? absint($_POST['product_image_id']) : 0,
409 'product_desc' => isset($_POST['product_desc']) ? sanitize_textarea_field($_POST['product_desc']) : '',
410 'product_price' => isset($_POST['product_price']) ? floatval($_POST['product_price']) : 0,
411 'product_type' => isset($_POST['product_type']) ? sanitize_text_field($_POST['product_type']) : '',
412 'product_category' => isset($_POST['product_category']) ? json_decode(wp_unslash($_POST['product_category']), true) : [],
413 )
414 );
415
416 wp_send_json( $response );
417 wp_die();
418 }
419
420 /**
421 * Create one WooCommerce product.
422 *
423 * Extracted from wkit_cteate_product() so the PHP import runner can create products
424 * without `$_POST`. Every WooCommerce call, the hardcoded Red/Green/Blue variation set
425 * and the price offsets are unchanged — this is the same code, reachable twice.
426 *
427 * Note what it does NOT do: there is no duplicate check. Calling it twice with the same
428 * title creates two products, because the browser has no notion of a product source id.
429 * That behaviour is preserved here. Wdkit_Import_Products adds the idempotency on top
430 * by recording a source marker and skipping products it has already created.
431 *
432 * @since 2.6.5
433 *
434 * @param array $args {product_title, product_image, product_desc, product_price,
435 * product_type, product_category}.
436 * @return array Response array, exactly as the AJAX action used to emit.
437 */
438 public function wdkit_create_product_data( $args ) {
439
440 try {
441 $args = is_array( $args ) ? $args : array();
442
443 $product_title = isset( $args['product_title'] ) ? sanitize_text_field( (string) $args['product_title'] ) : '';
444 $product_image = isset( $args['product_image'] ) ? esc_url_raw( (string) $args['product_image'] ) : '';
445 // The dummy-product placeholder is the same file for every product in the batch;
446 // the caller sideloads it once and passes the resulting attachment back here so
447 // each of the 6 products reuses it instead of re-downloading/re-resizing it.
448 $product_image_id = isset( $args['product_image_id'] ) ? absint( $args['product_image_id'] ) : 0;
449 $product_desc = isset( $args['product_desc'] ) ? sanitize_textarea_field( (string) $args['product_desc'] ) : '';
450 $product_price = isset( $args['product_price'] ) ? floatval( $args['product_price'] ) : 0;
451 $product_type = isset( $args['product_type'] ) ? sanitize_text_field( (string) $args['product_type'] ) : '';
452 $product_category = isset( $args['product_category'] ) && is_array( $args['product_category'] ) ? $args['product_category'] : [];
453
454 if ( ! class_exists( 'WC_Product_Simple' ) ) {
455 throw new Exception( 'WooCommerce is not available' );
456 }
457
458 if (empty($product_title)) {
459 throw new Exception('Product title is required');
460 }
461
462 if ($product_price <= 0) {
463 throw new Exception('Invalid product price');
464 }
465
466 $category_ids = [];
467
468 if (!empty($product_category)) {
469 foreach ($product_category as $category) {
470
471 $term = term_exists($category, 'product_cat');
472
473 if (!$term) {
474 $term = wp_insert_term($category, 'product_cat');
475 }
476
477 if (is_wp_error($term)) {
478 throw new Exception('Category error: ' . $term->get_error_message());
479 }
480
481 $category_ids[] = is_array($term) ? $term['term_id'] : $term;
482 }
483 }
484
485 if ($product_type == 'variation') {
486
487 // Ensure attribute exists
488 if (!taxonomy_exists('pa_color')) {
489 register_taxonomy(
490 'pa_color',
491 'product',
492 [
493 'label' => 'Color',
494 'hierarchical' => false,
495 'show_ui' => false,
496 ]
497 );
498 }
499
500 $product = new WC_Product_Variable();
501 $product->set_name($product_title);
502 $product->set_status('publish');
503
504 if (!empty($category_ids)) {
505 $product->set_category_ids($category_ids);
506 }
507
508 /* Attribute */
509 $attribute = new WC_Product_Attribute();
510 $attribute->set_name('pa_color');
511 $attribute->set_options(['Red', 'Green', 'Blue']);
512 $attribute->set_visible(true);
513 $attribute->set_variation(true);
514
515 $product->set_attributes([$attribute]);
516
517 if ( $product_image_id ) {
518 $product->set_image_id( $product_image_id );
519 } elseif( !empty( $product_image )){
520 $image_id = $this->upload_image_from_url($product_image);
521
522 if ($image_id) {
523 $product->set_image_id($image_id); // main product image
524 }
525 }
526
527 $parent_id = $product->save();
528
529 if (!$parent_id) {
530 throw new Exception('Failed to create variable product');
531 }
532
533 /* Variations */
534 $variations = [
535 ['color' => 'Red', 'price' => $product_price - 1, 'stock' => 'instock'],
536 ['color' => 'Green', 'price' => $product_price + 1, 'stock' => 'instock'],
537 ['color' => 'Blue', 'price' => $product_price, 'stock' => 'outofstock'],
538 ];
539
540 foreach ($variations as $var) {
541 $variation = new WC_Product_Variation();
542 $variation->set_parent_id($parent_id);
543 $variation->set_attributes(['pa_color' => $var['color']]);
544 $variation->set_regular_price($var['price']);
545 $variation->set_stock_status($var['stock']);
546
547 $variation_id = $variation->save();
548
549 if (!$variation_id) {
550 throw new Exception('Failed to create variation: ' . $var['color']);
551 }
552 }
553
554 $product_id = $parent_id;
555
556 } else {
557 $product = new WC_Product_Simple();
558
559 $product->set_name($product_title);
560 $product->set_status('publish');
561 $product->set_description($product_desc);
562 $product->set_regular_price($product_price);
563
564 if (!empty($category_ids)) {
565 $product->set_category_ids($category_ids);
566 }
567
568 if ($product_type == 'discount') {
569 $sale_price = $product_price * 0.85;
570 $product->set_sale_price($sale_price);
571 }
572
573 if ($product_type == 'out_of_stock') {
574 $product->set_stock_status('outofstock');
575 }
576
577 if ( $product_image_id ) {
578 $product->set_image_id( $product_image_id );
579 } elseif( !empty( $product_image )){
580 $image_id = $this->upload_image_from_url($product_image);
581
582 if ($image_id) {
583 $product->set_image_id($image_id); // main product image
584 }
585 }
586
587 $product_id = $product->save();
588
589 if (!$product_id) {
590 throw new Exception('Failed to create product');
591 }
592 }
593
594 return [
595 'success' => true,
596 'product_id' => $product_id,
597 'message' => __( 'Product created successfully', 'wdesignkit' ),
598 ];
599
600 } catch (Exception $e) {
601 return [
602 'success' => false,
603 'message' => $e->getMessage(),
604 ];
605 }
606 }
607
608 /**
609 * Insert dummy post type if not available
610 *
611 * @since 2.0.0
612 */
613 protected function wkit_generate_post_data (){
614
615 $array_data = array(
616 'site_type' => isset( $_POST['site_type'] ) ? sanitize_text_field( $_POST['site_type'] ) : '',
617 'site_desc' => isset( $_POST['site_desc'] ) ? sanitize_text_field( $_POST['site_desc'] ) : '',
618 'site_title' => isset( $_POST['site_title'] ) ? sanitize_text_field( $_POST['site_title'] ) : '',
619 'builder' => isset( $_POST['builder'] ) ? sanitize_text_field( $_POST['builder'] ) : 'elementor',
620 'token' => isset($_POST['token']) ? sanitize_text_field($_POST['token']) : '',
621 );
622
623 $t_ai_call = microtime( true );
624 $response = $this->wkit_api_call( $array_data, 'ai/post/generate' );
625
626 // WP_DEBUG_LOG as well as WP_DEBUG: with WP_DEBUG alone this lands in the PHP
627 // error log, which on some shared hosts is served over HTTP.
628 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
629 error_log( '[WDKIT] AI content timing: ' . wp_json_encode( array( 'route' => 'post/generate', 'ms' => (int) round( ( microtime( true ) - $t_ai_call ) * 1000 ) ) ) );
630 }
631
632 $data = !empty( $response['data'] ) ? $response['data'] : [];
633
634 $success = ! empty( $data->success ) ? $data->success : false;
635
636 if ( empty($success) ) {
637 wp_send_json([
638 'success' => false,
639 'message' => !empty( $data->message ) ? $data->message : 'API connection failed',
640 'description' => !empty( $data->description ) ? $data->description : 'API connection failed',
641 ]);
642 wp_die();
643 }
644
645 wp_send_json([
646 'success' => true,
647 'message' => !empty( $data->message ) ? $data->message : 'Text content converted successfully',
648 'description' => !empty( $data->description ) ? $data->description : 'Text content converted successfully',
649 'response' => $data->response,
650 ]);
651 wp_die();
652 }
653
654 /**
655 *
656 * generate ai text
657 *
658 * @since 2.0.0
659 */
660 protected function wkit_generate_ai_content (){
661 $result = $this->wdkit_generate_ai_content_data(
662 array(
663 'text_array' => isset( $_POST['text_array'] ) ? json_decode( wp_unslash( $_POST['text_array'] ), true ) : '',
664 'type' => isset( $_POST['site_type'] ) ? sanitize_text_field( $_POST['site_type'] ) : '',
665 'title' => isset( $_POST['site_title'] ) ? sanitize_text_field( $_POST['site_title'] ) : '',
666 'language' => isset( $_POST['site_lang'] ) ? sanitize_text_field( $_POST['site_lang'] ) : 'english',
667 'agency' => isset( $_POST['site_agency'] ) ? sanitize_text_field( $_POST['site_agency'] ) : '',
668 'description' => isset( $_POST['site_desc'] ) ? sanitize_text_field( $_POST['site_desc'] ) : '',
669 'builder' => isset( $_POST['site_builder'] ) ? sanitize_text_field( $_POST['site_builder'] ) : '',
670 'token' => isset($_POST['token']) ? sanitize_text_field($_POST['token']) : '',
671 )
672 );
673
674 /* Emitted here so this action's output is byte-for-byte what it was: on failure the
675 * three-key error array and wp_die(), on success the decoded payload returned to
676 * the router exactly as before. The AI call itself - and its timing log - now live
677 * in wdkit_generate_ai_content_data(), so both callers are measured. */
678 if ( empty( $result['success'] ) && isset( $result['__ai_failed'] ) ) {
679 unset( $result['__ai_failed'] );
680
681 wp_send_json( $result );
682 wp_die();
683 }
684
685 return $result;
686 }
687
688 /**
689 * Ask the cloud to generate AI copy for one template's instruction set.
690 *
691 * Extracted from wkit_generate_ai_content() so the PHP import runner can generate
692 * without `$_POST`. Same endpoint (`ai/template_import`), same token, therefore the
693 * same credit accounting — this is not a second generator, it is the same one reachable
694 * twice.
695 *
696 * Returns the decoded payload on success. On failure it returns the identical
697 * three-key error array the action emits, plus a private `__ai_failed` marker so the
698 * adapter above knows to emit-and-die rather than return; the marker is removed before
699 * anything is sent.
700 *
701 * @since 2.6.5
702 *
703 * @param array $args {text_array, type, title, language, agency, description, builder, token}.
704 * @return array
705 */
706 public function wdkit_generate_ai_content_data( $args ) {
707 $args = is_array( $args ) ? $args : array();
708
709 $array_data = array(
710 'text_array' => isset( $args['text_array'] ) ? $args['text_array'] : '',
711 'type' => isset( $args['type'] ) ? sanitize_text_field( (string) $args['type'] ) : '',
712 'title' => isset( $args['title'] ) ? sanitize_text_field( (string) $args['title'] ) : '',
713 'language' => isset( $args['language'] ) ? sanitize_text_field( (string) $args['language'] ) : 'english',
714 'agency' => isset( $args['agency'] ) ? sanitize_text_field( (string) $args['agency'] ) : '',
715 'description' => isset( $args['description'] ) ? sanitize_text_field( (string) $args['description'] ) : '',
716 'builder' => isset( $args['builder'] ) ? sanitize_text_field( (string) $args['builder'] ) : '',
717 'token' => isset( $args['token'] ) ? sanitize_text_field( (string) $args['token'] ) : '',
718 );
719
720 /* A sandbox site has no account token - see wdkit_kit_import_with_site_identity()'s
721 * own note. Carried alongside token rather than instead of it, so a signed-in site's
722 * request is unchanged; the cloud route this hits does not yet read these two, that
723 * is the other half of this fix. */
724 if ( function_exists( 'wdkit_kit_import_with_site_identity' ) ) {
725 $identity = wdkit_kit_import_with_site_identity( array() );
726
727 if ( ! empty( $identity['poll_token'] ) ) {
728 $array_data['poll_token'] = (string) $identity['poll_token'];
729 }
730
731 if ( ! empty( $identity['site_url'] ) ) {
732 $array_data['site_url'] = (string) $identity['site_url'];
733 }
734 }
735
736 $t_ai_call = microtime( true );
737 $response = $this->wkit_api_call( wp_json_encode( $array_data ), 'ai/template_import', 'frontside' );
738
739 // WP_DEBUG_LOG as well as WP_DEBUG: with WP_DEBUG alone this lands in the PHP
740 // error log, which on some shared hosts is served over HTTP.
741 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
742 error_log( '[WDKIT] AI content timing: ' . wp_json_encode( array( 'route' => 'template_import', 'ms' => (int) round( ( microtime( true ) - $t_ai_call ) * 1000 ) ) ) );
743 }
744
745 /* A transport failure — a timeout, DNS, a refused connection — comes back from
746 * wkit_api_call() as a WP_Error, not an array. Indexing it on the next line is a
747 * fatal ("Cannot use object of type WP_Error as array"), and under the runner that
748 * fatal is not confined to one template: it aborts the whole content stage, so a
749 * single slow cloud response leaves the import stopped with four pages in. Observed
750 * exactly that on a 15-template run.
751 *
752 * Reported through the same `__ai_failed` shape the failure branch below already
753 * uses, so callers need no new case: the page keeps its template copy, the run
754 * carries on, and generate_payload() logs `cloud_call_failed` with the reason. */
755 if ( is_wp_error( $response ) ) {
756 return array(
757 'success' => false,
758 'message' => $response->get_error_message(),
759 'description' => esc_html__( 'Ai data not found', 'wdesignkit' ),
760 '__ai_failed' => true,
761 );
762 }
763
764 if ( ! is_array( $response ) ) {
765 return array(
766 'success' => false,
767 'message' => esc_html__( 'Unexpected response from the AI service.', 'wdesignkit' ),
768 'description' => esc_html__( 'Ai data not found', 'wdesignkit' ),
769 '__ai_failed' => true,
770 );
771 }
772
773 $success = ! empty( $response['success'] ) ? $response['success'] : false;
774
775 if ( empty( $success ) ) {
776 return array(
777 'success' => false,
778 'message' => ! empty( $response['massage'] ) ? $response['massage'] : esc_html__( 'Data Not Found', 'wdesignkit' ),
779 'description' => esc_html__( 'Ai data not found', 'wdesignkit' ),
780 '__ai_failed' => true,
781 );
782 }
783
784 return json_decode( wp_json_encode( $response['data'] ), true );
785 }
786
787 /**
788 * Generate AI text for an entire kit in a single request.
789 *
790 * Mirrors wkit_generate_ai_content() but accepts a `pages` array (one entry per
791 * template needing an AI rewrite) and forwards it to the batched frontend endpoint,
792 * which fans the per-page OpenAI calls out concurrently on the Node server. Collapses
793 * 12-13 browser round trips into one. The per-page contract is unchanged, so the caller
794 * can fall back to per-template generate_ai_content when this route is unavailable.
795 *
796 * @since 2.6.2
797 */
798 protected function wkit_generate_ai_content_batch() {
799 $pages = isset( $_POST['pages'] ) ? json_decode( wp_unslash( $_POST['pages'] ), true ) : array();
800
801 return $this->wdkit_generate_ai_content_batch_data(
802 array(
803 'pages' => $pages,
804 'type' => isset( $_POST['site_type'] ) ? sanitize_text_field( $_POST['site_type'] ) : '',
805 'title' => isset( $_POST['site_title'] ) ? sanitize_text_field( $_POST['site_title'] ) : '',
806 'language' => isset( $_POST['site_lang'] ) ? sanitize_text_field( $_POST['site_lang'] ) : 'english',
807 'agency' => isset( $_POST['site_agency'] ) ? sanitize_text_field( $_POST['site_agency'] ) : '',
808 'description' => isset( $_POST['site_desc'] ) ? sanitize_text_field( $_POST['site_desc'] ) : '',
809 'builder' => isset( $_POST['site_builder'] ) ? sanitize_text_field( $_POST['site_builder'] ) : '',
810 'token' => isset( $_POST['token'] ) ? sanitize_text_field( $_POST['token'] ) : '',
811 )
812 );
813 }
814
815 /**
816 * Ask the cloud to generate AI copy for several templates in one request.
817 *
818 * Extracted from wkit_generate_ai_content_batch() so the PHP import runner can generate
819 * without `$_POST`, exactly as wdkit_generate_ai_content_data() was extracted from
820 * wkit_generate_ai_content(). Same batched endpoint (`ai/template_import_batch`), which
821 * fans the per-page OpenAI calls out concurrently server-side, so the caller pays one
822 * network round trip (bounded by the slowest page) instead of N sequential ones.
823 *
824 * @since 2.7.3
825 *
826 * @param array $args {pages, type, title, language, agency, description, builder, token}.
827 * `pages` is a list of {id, text_array}.
828 * @return array {success, results[], message?} — `results` items carry {id, success,
829 * response|message, used_credits, real_credit}, keyed by the page's own
830 * `id` (not by array index).
831 */
832 public function wdkit_generate_ai_content_batch_data( $args ) {
833 $args = is_array( $args ) ? $args : array();
834 $pages = isset( $args['pages'] ) && is_array( $args['pages'] ) ? $args['pages'] : array();
835
836 if ( empty( $pages ) ) {
837 return array(
838 'success' => false,
839 'message' => esc_html__( 'No pages supplied for batch AI generation', 'wdesignkit' ),
840 'results' => array(),
841 );
842 }
843
844 $array_data = array(
845 'pages' => $pages,
846 'type' => isset( $args['type'] ) ? sanitize_text_field( (string) $args['type'] ) : '',
847 'title' => isset( $args['title'] ) ? sanitize_text_field( (string) $args['title'] ) : '',
848 'language' => isset( $args['language'] ) ? sanitize_text_field( (string) $args['language'] ) : 'english',
849 'agency' => isset( $args['agency'] ) ? sanitize_text_field( (string) $args['agency'] ) : '',
850 'description' => isset( $args['description'] ) ? sanitize_text_field( (string) $args['description'] ) : '',
851 'builder' => isset( $args['builder'] ) ? sanitize_text_field( (string) $args['builder'] ) : '',
852 'token' => isset( $args['token'] ) ? sanitize_text_field( (string) $args['token'] ) : '',
853 );
854
855 /* Same site-identity carry as wdkit_generate_ai_content_data() - see that one's
856 * comment. */
857 if ( function_exists( 'wdkit_kit_import_with_site_identity' ) ) {
858 $identity = wdkit_kit_import_with_site_identity( array() );
859
860 if ( ! empty( $identity['poll_token'] ) ) {
861 $array_data['poll_token'] = (string) $identity['poll_token'];
862 }
863
864 if ( ! empty( $identity['site_url'] ) ) {
865 $array_data['site_url'] = (string) $identity['site_url'];
866 }
867 }
868
869 $t_ai_call = microtime( true );
870 $response = $this->wkit_api_call( wp_json_encode( $array_data ), 'ai/template_import_batch', 'frontside' );
871
872 // WP_DEBUG_LOG as well as WP_DEBUG: with WP_DEBUG alone this lands in the PHP
873 // error log, which on some shared hosts is served over HTTP.
874 if ( defined( 'WP_DEBUG' ) && WP_DEBUG && defined( 'WP_DEBUG_LOG' ) && WP_DEBUG_LOG ) {
875 error_log( '[WDKIT] AI content timing: ' . wp_json_encode( array( 'route' => 'template_import_batch', 'page_count' => count( $pages ), 'ms' => (int) round( ( microtime( true ) - $t_ai_call ) * 1000 ) ) ) );
876 }
877
878 if ( is_wp_error( $response ) ) {
879 return array(
880 'success' => false,
881 'message' => $response->get_error_message(),
882 'results' => array(),
883 );
884 }
885
886 $success = ! empty( $response['success'] ) ? $response['success'] : false;
887
888 if ( empty( $success ) ) {
889 return array(
890 'success' => false,
891 'message' => ! empty( $response['massage'] ) ? $response['massage'] : esc_html__( 'Batch AI data not found', 'wdesignkit' ),
892 'results' => array(),
893 );
894 }
895
896 return json_decode( wp_json_encode( $response['data'] ), true );
897 }
898
899 /**
900 *
901 * darft all theme builder page and normal page
902 *
903 * @since 2.0.0
904 */
905 protected function wkit_reset_site(){
906 do_action( 'nxt_update_builder_status', 'all' );
907
908 if ( current_user_can('manage_options') ) {
909 $pages = get_posts([
910 'post_type' => 'page',
911 'post_status' => 'publish',
912 'numberposts' => -1
913 ]);
914 foreach ($pages as $page) {
915 wp_update_post([
916 'ID' => $page->ID,
917 'post_status' => 'draft'
918 ]);
919 }
920 }
921
922 $response = array(
923 'message' => esc_html__( 'site Setting updated', 'wdesignkit' ),
924 'description' => esc_html__( 'site Setting updated', 'wdesignkit' ),
925 'success' => true,
926 );
927
928 wp_send_json( $response );
929 wp_die();
930 }
931
932 /**
933 *
934 * remove old header and footer
935 *
936 * @since 2.0.7
937 */
938 protected function wdkit_remove_header_footer() {
939 $post_id = isset($_POST['post_id']) ? json_decode($_POST['post_id']) : [];
940
941 $response = $this->wdkit_delete_posts_data( $post_id );
942
943 wp_send_json( $response );
944 wp_die();
945 }
946
947 /**
948 * Force-delete a list of post ids.
949 *
950 * Extracted from wdkit_remove_header_footer() so the PHP import runner can reach it
951 * without `$_POST`. The behaviour is unchanged and the response is unchanged.
952 *
953 * ⚠ READ THIS BEFORE CALLING. Despite the handler's name, this deletes whatever ids it
954 * is given — any post type, published or not — with wp_delete_post( $id, true ), which
955 * skips the trash and is unrecoverable. It performs NO ownership check of its own.
956 *
957 * The browser is safe because of its caller, not because of this method: the wizard
958 * only ever passes `current_section.current`, which holds nothing but `inserted_id`
959 * values from posts the same import run just created (import_loader.js — pushed at the
960 * template-insert site, cleared straight after the retry pass). Ownership is the
961 * caller's responsibility.
962 *
963 * Wdkit_Import_Cleanup is the only PHP caller and it passes only ids recorded in the
964 * import session, behind an explicit opt-in. Do not call this with ids from anywhere
965 * else, and never with ids from a remote payload.
966 *
967 * @since 2.6.5
968 *
969 * @param array $post_id Post ids to delete.
970 * @return array Response array, exactly as the AJAX action used to emit.
971 */
972 public function wdkit_delete_posts_data( $post_id ) {
973 $post_id = is_array( $post_id ) ? $post_id : (array) $post_id;
974
975 if (empty($post_id)) {
976 $response = array(
977 'message' => esc_html__( 'Post id not found', 'wdesignkit' ),
978 'description' => esc_html__( 'Post id not found', 'wdesignkit' ),
979 'success' => true,
980 );
981
982 return $response;
983 }
984
985 foreach ($post_id as $post_id) {
986 $post_id = intval($post_id);
987 if ($post_id > 0) {
988 $deleted = wp_delete_post($post_id, true);
989
990 if ($deleted) {
991 $response = array(
992 'message' => esc_html__( 'Post Deleted Successfully', 'wdesignkit' ),
993 'description' => esc_html__( 'Post Deleted Successfully', 'wdesignkit' ),
994 'success' => true,
995 );
996 } else {
997 $response = array(
998 'message' => esc_html__( 'Post can not deleted', 'wdesignkit' ),
999 'description' => esc_html__( 'Post can not deleted', 'wdesignkit' ),
1000 'success' => true,
1001 );
1002 }
1003 }
1004 }
1005
1006 return $response;
1007 }
1008
1009 /**
1010 *
1011 * check how much product are in site
1012 *
1013 * @since 2.0.0
1014 */
1015 protected function wkit_check_product_count() {
1016 $args = [
1017 'post_type' => 'product',
1018 'post_status' => 'publish',
1019 'posts_per_page' => -1,
1020 'fields' => 'ids', // Only fetch IDs for performance
1021 ];
1022
1023 $product = get_posts($args);
1024
1025 $response = [
1026 'count' => count($product),
1027 'message' => esc_html__( 'Product count retrieved successfully', 'wdesignkit' ),
1028 'description' => esc_html__( 'Product count retrieved successfully', 'wdesignkit' ),
1029 'success' => true,
1030 ];
1031
1032 wp_send_json($response);
1033 wp_die();
1034 }
1035
1036 /**
1037 *
1038 * darft all theme builder page and normal page
1039 *
1040 * @since 2.0.0
1041 */
1042 protected function wkit_check_post_count() {
1043 $builder = isset($_POST['builder']) ? sanitize_text_field($_POST['builder']) : '';
1044
1045 $args = [
1046 'post_type' => 'any',
1047 'post_status' => 'publish',
1048 'posts_per_page' => -1,
1049 'fields' => 'all', // Full post objects so post_content comes back in this query — avoids a get_post_field() round-trip per post.
1050 ];
1051
1052 $posts = get_posts($args);
1053
1054 // Primes the meta cache for every post in one query, so the get_post_meta()
1055 // calls below hit the cache instead of issuing a query each — turns 1+2N
1056 // queries into 2 total, regardless of site size.
1057 update_meta_cache('post', wp_list_pluck($posts, 'ID'));
1058
1059 $elementor = 0;
1060 $gutenberg = 0;
1061 $classic = 0;
1062
1063 foreach ($posts as $post) {
1064 $is_elementor = get_post_meta($post->ID, '_elementor_edit_mode', true);
1065
1066 if ($is_elementor === 'builder') {
1067 $elementor++;
1068 } else {
1069 if (strpos($post->post_content, '<!-- wp:') !== false) {
1070 $gutenberg++;
1071 } else {
1072 $classic++;
1073 }
1074 }
1075 }
1076
1077 // Return only requested builder count if parameter is passed
1078 if ($builder === 'elementor') {
1079 $post_data = [ 'count' => $elementor ];
1080 } elseif ($builder === 'gutenberg') {
1081 $post_data = [ 'count' => $gutenberg ];
1082 } elseif ($builder === 'classic') {
1083 $post_data = [ 'count' => $classic ];
1084 } else {
1085 // Default → return all
1086 $post_data = [
1087 'elementor' => $elementor,
1088 'gutenberg' => $gutenberg,
1089 'classic' => $classic,
1090 ];
1091 }
1092
1093 $response = [
1094 'data' => $post_data,
1095 'message' => esc_html__( 'Post count retrieved successfully', 'wdesignkit' ),
1096 'description' => esc_html__( 'Post count retrieved successfully', 'wdesignkit' ),
1097 'success' => true,
1098 ];
1099
1100 wp_send_json($response);
1101 wp_die();
1102 }
1103
1104 /**
1105 *
1106 * This Function is used for upload image in media folder
1107 *
1108 * @since 2.2.10
1109 *
1110 * @param array $data give array.
1111 * @param array $name store data.
1112 */
1113 function upload_image_from_url($image_url) {
1114 // SSRF guard (CWE-918, ClickUp 86d41zav4). media_sideload_image() -> download_url() ->
1115 // wp_safe_remote_get() does apply core's wp_http_validate_url(), which on current WP
1116 // resolves the hostname and blocks 169.254.0.0/16 among others — so this is defence in
1117 // depth, not an open hole. What core does NOT cover, and this wrapper does:
1118 // - core resolves with gethostbyname(), i.e. the FIRST A record only; a host with one
1119 // public and one internal A record can pass. This wrapper uses gethostbynamel() and
1120 // rejects if ANY resolved address is private/reserved.
1121 // - core's range check is IPv4-only; this wrapper also resolves AAAA and applies the
1122 // same rules to IPv6 (::1, fc00::/7, fe80::/10).
1123 // - core skips validation entirely when the URL host matches the site's own host, and
1124 // its decision is overridable via the http_request_host_is_external filter.
1125 // Fails closed: if the helper is unavailable the image is skipped rather than fetched
1126 // unvalidated (it is loaded well before this hook, so that path is not expected).
1127 require_once(ABSPATH . 'wp-admin/includes/file.php');
1128 require_once(ABSPATH . 'wp-admin/includes/media.php');
1129 require_once(ABSPATH . 'wp-admin/includes/image.php');
1130
1131 if ( ! class_exists( 'Wdkit_Image_Guard' ) ) {
1132 require_once WDKIT_INCLUDES . 'admin/class-wdkit-image-guard.php';
1133 }
1134
1135 // Rewrite BEFORE validating, so the string that gets fetched is the string that was
1136 // checked. The other order validated the caller's URL and then fetched a different
1137 // one; harmless while the rewriter only edits the query, but it makes the guard
1138 // depend on that staying true, which is not a property worth relying on.
1139 $image_url = Wdkit_Image_Guard::cap_pexels_source( $image_url );
1140
1141 if ( ! function_exists( 'wdesignkit_validate_external_url' ) || ! wdesignkit_validate_external_url( $image_url ) ) {
1142 return false;
1143 }
1144
1145 $attachment_id = media_sideload_image($image_url, 0, null, 'id');
1146
1147 if (is_wp_error($attachment_id)) {
1148 return false;
1149 }
1150
1151 return $attachment_id;
1152 }
1153
1154
1155 /**
1156 *
1157 * This Function is used for API call
1158 *
1159 * @since 2.0.0
1160 *
1161 * @param array $data give array.
1162 * @param array $name store data.
1163 */
1164 public function wkit_api_call( $data, $name, $type = '' ) {
1165 $u_r_l = $this->wdkit_api;
1166
1167 if ( !empty($type) && $type == 'frontside' ){
1168 $u_r_l = $this->wdkit_front_api;
1169 }
1170
1171 if ( empty( $u_r_l ) ) {
1172 return array(
1173 'massage' => esc_html__( 'API Not Found', 'wdesignkit' ),
1174 'success' => false,
1175 );
1176 }
1177
1178 $args = array(
1179 'method' => 'POST',
1180 'body' => $data,
1181 'timeout' => 200,
1182 );
1183 $response = wp_remote_post( $u_r_l . $name, $args );
1184
1185 if ( is_wp_error( $response ) ) {
1186 $error_message = $response->get_error_message();
1187
1188 /* Translators: %s is a placeholder for the error message */
1189 $error_message = esc_html__( 'API request error: ', 'wdesignkit' ). esc_html( $error_message );
1190
1191 return array(
1192 'massage' => $error_message,
1193 'success' => false,
1194 );
1195 }
1196
1197 $status_code = wp_remote_retrieve_response_code( $response );
1198 if ( 200 === $status_code ) {
1199
1200 return array(
1201 'data' => json_decode( wp_remote_retrieve_body( $response ) ),
1202 'massage' => esc_html__( 'Success', 'wdesignkit' ),
1203 'status' => $status_code,
1204 'success' => true,
1205 );
1206 }
1207
1208 $body_data = json_decode( wp_remote_retrieve_body( $response ), true );
1209
1210 // isset(), because not every error body carries `details`. A bare
1211 // { "message": "Token not provided" } made this read an offset on null and emit two
1212 // PHP warnings per failed API call before falling through anyway.
1213 if ( isset( $body_data['details']['message'] ) ) {
1214 return array(
1215 'massage' => $body_data['details']['message'],
1216 'status' => $status_code,
1217 'success' => false,
1218 );
1219 }
1220
1221 $error_message = 'Server error: '. esc_html( $status_code );
1222
1223 if ( isset( $error_data->message ) ) {
1224 $error_message .= ' (' . $error_data->message . ')';
1225 }
1226
1227 return array(
1228 'massage' => $error_message,
1229 'status' => $status_code,
1230 'success' => false,
1231 );
1232 }
1233 }
1234
1235 Wdkit_Import_temp_Ajax::get_instance();
1236 }