PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.2.7
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.2.7
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
← All changes | includes/Core/Importer/FullSiteImport.php +162 -1251 3.7.53.2.7 View file →
@@ -15,18 +15,13 @@
15 15
16 16 use Elementor\Plugin;
17 17 use Error;
18 18 use Exception;
19 -use Templately\Core\Importer\Exception\NonRetirableErrorException;
19 +use Templately\Core\Importer\Exception\NonRetryableErrorException;
20 20 use Templately\Core\Importer\Exception\RetryableErrorException;
21 21 use Templately\Core\Importer\Exception\UnknownErrorException;
22 -use Templately\Core\Importer\Runners\Finalizer;
23 22 use Templately\Core\Importer\Utils\LogHandler;
24 23 use Templately\Core\Importer\Utils\Utils;
25 -use Templately\Core\Importer\Utils\SessionData;
26 -use Templately\Core\Importer\Utils\AIUtils;
27 -use Templately\Core\Importer\Utils\ElementorSettingsMerger;
28 -use Templately\Core\Importer\Utils\GutenbergSettingsMerger;
29 24 use Templately\Utils\Base;
30 25 use Templately\Utils\Helper;
31 26 use Templately\Utils\Installer;
32 27 use Templately\Utils\Options;
@@ -44,8 +39,9 @@
44 39 protected $dev_mode = false;
45 40 protected $api_key = '';
46 41 protected $session_id = '';
47 42 protected $documents_data = [];
43 + protected $dependency_data = [];
48 44 private $is_import_status_handled = false;
49 45
50 46 public $dir_path;
51 47 protected $filePath;
@@ -51,28 +47,20 @@
51 47 protected $filePath;
52 48 protected $tmp_dir = null;
53 49 public $request_params = [];
54 50
55 - // Polling-specific property for ai_poll_template()
56 - private $polling_is_last_part = null;
57 -
58 51 public function __construct() {
59 52 $this->dev_mode = defined('TEMPLATELY_DEV') && TEMPLATELY_DEV;
60 53 $this->api_key = Options::get_instance()->get('api_key');
61 54
62 55 $this->add_ajax_action('import_settings', $this);
63 - $this->add_ajax_action('create_session_and_download', $this);
64 56 $this->add_ajax_action('import_status', $this);
65 57 $this->add_ajax_action('import', $this);
66 58 $this->add_ajax_action('import_revert', $this);
67 59 $this->add_ajax_action('import_info', $this);
68 - $this->add_ajax_action('import_global_settings', $this);
69 60 $this->add_ajax_action('import_close_feedback_modal', $this);
70 61 $this->add_ajax_action('feedback_form', $this);
71 62 $this->add_ajax_action('google_font', $this);
72 - $this->add_ajax_action('ai_get_json', $this);
73 - $this->add_ajax_action('ai_poll_template', $this);
74 - $this->add_ajax_action('merge_settings_with_template', $this);
75 63
76 64 add_action('admin_init', [$this, 'admin_init']);
77 65 // add_action('admin_notices', [$this, 'add_revert_button']);
78 66
@@ -111,9 +99,9 @@
111 99 wp_die();
112 100 }
113 101
114 102 // Call the actual handler method
115 - call_user_func([$this, $action]);
103 + call_user_func([$object, $action]);
116 104 });
117 105 }
118 106
119 107 public function admin_init() {
@@ -123,49 +111,21 @@
123 111 }
124 112 }
125 113
126 114 public function import_settings() {
127 - $user = Options::get_instance()->get('user');
128 - if (!empty($user['is_disconnected'])) {
129 - wp_send_json_error(['message' => __('Your site connection is disconnected. Please migrate your connection first.', 'templately')]);
130 - return;
131 - }
132 -
133 115 $data = wp_unslash($_POST);
134 116
135 - $upload_dir = wp_upload_dir();
117 + $upload_dir = wp_upload_dir();
118 + $session_id = uniqid();
119 + $tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
136 120
137 - if(!empty($data['session_id'])){
138 - $session_id = $data['session_id'];
139 - // Security: Sanitize session_id from user input
140 - $session_id = AIUtils::sanitize_path_component($data['session_id'], 'session_id');
141 - if (is_wp_error($session_id)) {
142 - wp_send_json_error(['message' => $session_id->get_error_message()]);
143 - return;
144 - }
145 - $session_data = SessionData::get_data($session_id);
146 - $data = array_merge($session_data, $data);
147 - }
148 - else {
149 - // Not uniqid(): that is a formatted microtime, so the path it keys in
150 - // web-served wp-uploads is enumerable by anyone who knows roughly when
151 - // an import ran. The chatbot flow already sends a client-side uuid4
152 - // here, so this only brings the fallback in line with it.
153 - $session_id = wp_generate_uuid4();
154 - }
155 -
156 - $tmp_dir = Helper::upload_dir('tmp');
157 - $prv_dir = Helper::upload_dir('preview');
158 -
159 - $this->session_id = $session_id;
121 + $this->session_id = $session_id;
160 122 $data['session_id'] = $session_id;
123 + $data['root_dir'] = $tmp_dir;
124 + $data['dir_path'] = $tmp_dir . $session_id . DIRECTORY_SEPARATOR;
125 + $data['zip_path'] = $tmp_dir . "{$session_id}.zip";
161 126
162 - $data['root_dir'] = $tmp_dir;
163 - $data['prv_dir'] = $prv_dir;
164 - $data['dir_path'] = $tmp_dir . $session_id . DIRECTORY_SEPARATOR;
165 - $data['zip_path'] = $tmp_dir . "{$session_id}.zip";
166 127
167 -
168 128 if ( is_array( $data ) && ! empty( $data ) ) {
169 129 foreach ( $data as $key => $value ) {
170 130 $json = is_string($value) ? json_decode( $value, true ) : null;
171 131 $data[ $key ] = $json !== null ? $json : $value;
@@ -171,9 +131,9 @@
171 131 $data[ $key ] = $json !== null ? $json : $value;
172 132 }
173 133 }
174 134
175 - SessionData::save($session_id, $data);
135 + Utils::update_session_data($session_id, $data);
176 136
177 137
178 138 //clear previous revert backup
179 139 $options = Utils::get_backup_options();
@@ -188,132 +148,8 @@
188 148 'session_id' => $session_id,
189 149 ]);
190 150 }
191 151
192 - public function import_ai_settings() {
193 - $data = wp_unslash($_POST);
194 -
195 - $upload_dir = wp_upload_dir();
196 -
197 - // Security: Sanitize session_id from user input
198 - $session_id = AIUtils::sanitize_path_component($data['session_id'], 'session_id');
199 - if (is_wp_error($session_id)) {
200 - wp_send_json_error(['message' => $session_id->get_error_message()]);
201 - return;
202 - }
203 -
204 - $tmp_dir = Helper::upload_dir('tmp');
205 - $prv_dir = Helper::upload_dir('preview');
206 -
207 - $this->session_id = $session_id;
208 - $data['root_dir'] = $tmp_dir;
209 - $data['prv_dir'] = $prv_dir;
210 - $data['dir_path'] = $tmp_dir . $session_id . DIRECTORY_SEPARATOR;
211 - $data['zip_path'] = $tmp_dir . "{$session_id}.zip";
212 -
213 - // Handle isLocalSite flag conversion
214 - if (isset($data['isLocalSite'])) {
215 - $data['isLocalSite'] = filter_var($data['isLocalSite'], FILTER_VALIDATE_BOOLEAN);
216 - }
217 -
218 - if ( is_array( $data ) && ! empty( $data ) ) {
219 - foreach ( $data as $key => $value ) {
220 - $json = is_string($value) ? json_decode( $value, true ) : null;
221 - $data[ $key ] = $json !== null ? $json : $value;
222 - }
223 - }
224 -
225 - SessionData::save($session_id, $data);
226 -
227 -
228 - return $data;
229 - }
230 -
231 - public function create_session_and_download() {
232 - if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
233 - exit;
234 - }
235 -
236 - Utils::add_gd_editor_filter();
237 -
238 - define('TEMPLATELY_START_TIME', microtime(true));
239 -
240 - register_shutdown_function( [ $this, 'register_shutdown' ] );
241 -
242 - // $this->finishRequestHeaders();
243 -
244 - try {
245 - // Get session data from AJAX request
246 - $session_data = $this->import_ai_settings();
247 -
248 - $this->request_params = $session_data;
249 - $this->initialize_props();
250 - $this->add_revert_hooks();
251 - $progress = $this->request_params['progress'] ?? [];
252 -
253 - if(empty($progress['create_log_dir'])){
254 - // Create Log Directory and if fail then chose option method
255 - LogHandler::create_log_dir();
256 -
257 - SessionData::mark_step_complete($this->session_id, 'create_log_dir');
258 - }
259 -
260 - $_id = isset($this->request_params['id']) ? (int) $this->request_params['id'] : null;
261 -
262 - if ($_id === null) {
263 - $this->throw(__('Invalid Pack ID.', 'templately'));
264 - }
265 -
266 - $this->check_writing_permission();
267 -
268 -
269 - if(empty($progress['download_zip'])){
270 -
271 - /**
272 - * Download the zip
273 - */
274 - $this->download_zip( $_id, true );
275 -
276 - SessionData::mark_step_complete($this->session_id, 'download_zip');
277 - }
278 -
279 - /**
280 - * Reading Manifest File
281 - */
282 - $this->manifest = $this->read_manifest($this->request_params['dir_path']);
283 -
284 - /**
285 - * Version Check
286 - */
287 - if ( ! empty( $this->manifest['version'] ) && version_compare( $this->manifest['version'], $this->version, '>' ) ) {
288 - $this->throw( __( 'Please update the templately plugin.', 'templately' ) );
289 - }
290 -
291 - $platform = $this->manifest['platform'] ?? '';
292 - if($platform === 'elementor') {
293 - Helper::enable_elementor_container();
294 - }
295 -
296 - update_option('templately_import_platform', $platform);
297 -
298 - // Return success response for AJAX
299 - wp_send_json_success([
300 - 'session_id' => $this->session_id,
301 - 'pack_downloaded' => true,
302 - 'platform' => $platform,
303 - 'message' => __('Session created and pack downloaded successfully', 'templately')
304 - ]);
305 -
306 - } catch ( Exception $e ) {
307 - $should_retry = $e instanceof RetryableErrorException;
308 -
309 - wp_send_json_error([
310 - 'message' => $e->getMessage(),
311 - 'should_retry' => $should_retry
312 - ]);
313 - }
314 - }
315 -
316 152 public function import_close_feedback_modal() {
317 153 $return = null;
318 154 if(isset($_GET['closeAction']) && $_GET['closeAction']){
319 155 $review_email = isset($_POST['review-email']) ? sanitize_email($_POST['review-email']) : '';
@@ -326,9 +162,19 @@
326 162 'pack_id' => (int) $pack_id,
327 163 ]);
328 164
329 165 // Send the request to the API
330 - $response = Helper::make_api_post_request('v2/feedback/close', json_decode($body, true), [], 30);
166 + $response = wp_remote_post($this->get_api_url('v2', 'feedback/close'), [
167 + 'timeout' => 30,
168 + 'headers' => [
169 + 'Content-Type' => 'application/json',
170 + 'Authorization' => 'Bearer ' . $this->api_key,
171 + 'x-templately-ip' => Helper::get_ip(),
172 + 'x-templately-url' => home_url('/'),
173 + 'x-templately-version' => TEMPLATELY_VERSION,
174 + ],
175 + 'body' => $body,
176 + ]);
331 177 $body = wp_remote_retrieve_body($response);
332 178 $return = json_decode($body, true);
333 179 }
334 180 update_user_meta(get_current_user_id(), 'templately_fsi_complete', 'done');
@@ -349,17 +195,26 @@
349 195 'pack_id' => (int) $pack_id,
350 196 ]);
351 197
352 198 // Send the request to the API
353 - $response = Helper::make_api_post_request('v2/feedback/store', json_decode($body, true), [], 30);
199 + $response = wp_remote_post($this->get_api_url('v2', 'feedback/store'), [
200 + 'timeout' => 30,
201 + 'headers' => [
202 + 'Content-Type' => 'application/json',
203 + 'Authorization' => 'Bearer ' . $this->api_key,
204 + 'x-templately-ip' => Helper::get_ip(),
205 + 'x-templately-url' => home_url('/'),
206 + 'x-templately-version' => TEMPLATELY_VERSION,
207 + ],
208 + 'body' => $body,
209 + ]);
354 210
355 211 if (is_wp_error($response)) {
356 212 wp_send_json_error($response->get_error_message());
357 213 }
358 214
359 - if (wp_remote_retrieve_response_code($response) != 200) {
360 - $error_message = $this->extract_error_from_response($response);
361 - wp_send_json_error($error_message, wp_remote_retrieve_response_code($response));
215 + if (wp_remote_retrieve_response_code($response) != 200 && wp_remote_retrieve_response_code($response) != 201) {
216 + wp_send_json_error('API request failed with response code ' . wp_remote_retrieve_response_code($response), wp_remote_retrieve_response_code($response));
362 217 }
363 218
364 219 $body = wp_remote_retrieve_body($response);
365 220 $data = json_decode($body, true);
@@ -376,22 +231,16 @@
376 231
377 232 wp_send_json_success($result);
378 233 }
379 234
380 - // Modified get_session_data to use SessionData
381 - public function get_session_data() {
382 - if ($session_id = SessionData::get_session_id()) {
383 - return SessionData::get_data($session_id);
384 - }
385 - return [];
235 + // Modified get_session_data to use the static version
236 + protected function get_session_data() {
237 + return Utils::get_session_data_by_id();
386 238 }
387 239
388 - // Modified update_session_data to use SessionData
389 - public function update_session_data($data) {
390 - if ($session_id = SessionData::get_session_id()) {
391 - return SessionData::save($session_id, array_merge($this->get_session_data(), $data));
392 - }
393 - return false;
240 + // Modified update_session_data to use the static version
241 + protected function update_session_data($data) {
242 + return Utils::update_session_data_by_id($data);
394 243 }
395 244
396 245 public function initialize_props() {
397 246 $data = $this->get_session_data();
@@ -406,14 +255,17 @@
406 255 }
407 256 if (isset($data['download_key'])) {
408 257 $this->download_key = $data['download_key'];
409 258 }
259 + if (isset($data['dependency_data'])) {
260 + $this->dependency_data = $data['dependency_data'];
261 + }
410 262 if (isset($data['is_import_status_handled'])) {
411 263 $this->is_import_status_handled = $data['is_import_status_handled'];
412 264 }
413 265 }
414 266
415 - public function clear_session_data(): bool {
267 + private function clear_session_data(): bool {
416 268 return delete_site_option(self::SESSION_OPTION_KEY);
417 269 }
418 270
419 271 private function finishRequestHeaders() {
@@ -475,9 +327,9 @@
475 327 if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
476 328 exit;
477 329 }
478 330
479 - Utils::add_gd_editor_filter();
331 + add_filter( 'wp_image_editors', [ $this, 'wp_image_editors' ], 10, 1 );
480 332
481 333
482 334 define('TEMPLATELY_START_TIME', microtime(true));
483 335
@@ -487,20 +339,11 @@
487 339
488 340 $this->finishRequestHeaders();
489 341
490 342 try {
491 - $user = Options::get_instance()->get('user');
492 - if (!empty($user['is_disconnected'])) {
493 - $this->throw(__('Your site connection is disconnected. Please migrate your connection first.', 'templately'));
494 - }
495 -
343 + // TODO: Need to check if user is connected or not
496 344 if(!empty($_GET['session_id'])){
497 - // Security: Sanitize session_id from user input
498 - $session_id = AIUtils::sanitize_path_component(sanitize_text_field($_GET['session_id']), 'session_id');
499 - if (is_wp_error($session_id)) {
500 - $this->throw($session_id->get_error_message());
501 - }
502 - $this->session_id = $session_id;
345 + $this->session_id = sanitize_text_field($_GET['session_id']);
503 346 }
504 347 else {
505 348 $this->throw(__('Invalid Session ID.', 'templately'));
506 349 }
@@ -506,28 +349,20 @@
506 349 }
507 350
508 351
509 352 $this->request_params = $this->get_session_data();
510 - $this->initialize_props();
353 + $this->initialize_props();
511 354 $this->add_revert_hooks();
512 355 $progress = $this->request_params['progress'] ?? [];
513 356
514 - // Trigger action hook for network admin multisite handling
515 - do_action( 'templately_fsi_before_import', $this, $this->request_params );
516 -
517 - // Filter Child Type for Elementor Pro Promotion Widget
518 - if ( class_exists( '\Elementor\Plugin' ) ) {
519 - \Templately\Core\Platform\Elementor::register_child_type_filter();
520 - }
521 -
522 - // Refresh progress after potential multisite creation
523 - $progress = $this->request_params['progress'] ?? [];
524 -
525 357 if(empty($progress['create_log_dir'])){
526 358 // Create Log Directory and if fail then chose option method
527 359 LogHandler::create_log_dir();
528 360
529 - SessionData::mark_step_complete($this->session_id, 'create_log_dir');
361 + $progress['create_log_dir'] = true;
362 + $this->update_session_data( [
363 + 'progress' => $progress,
364 + ] );
530 365 $this->sse_message( [
531 366 'type' => 'eventLog',
532 367 'action' => 'eventLog',
533 368 'info' => 'create_log_dir',
@@ -546,25 +381,23 @@
546 381 'action' => 'eventLog',
547 382 'results' => __METHOD__ . '::' . __LINE__,
548 383 ] );
549 384
550 - if(empty($progress['check_writing_permission'])){
385 + if(empty($progress['download_zip'])){
551 386 /**
552 387 * Check Writing Permission
553 388 */
554 389 $this->check_writing_permission();
555 390
556 - SessionData::mark_step_complete($this->session_id, 'check_writing_permission');
557 - }
558 -
559 - if(empty($progress['download_zip'])){
560 -
561 391 /**
562 392 * Download the zip
563 393 */
564 394 $this->download_zip( $_id );
565 395
566 - SessionData::mark_step_complete($this->session_id, 'download_zip');
396 + $progress['download_zip'] = true;
397 + $this->update_session_data( [
398 + 'progress' => $progress,
399 + ] );
567 400 $this->sse_message( [
568 401 'type' => 'continue',
569 402 'action' => 'continue',
570 403 'info' => 'download_zip',
@@ -632,8 +465,16 @@
632 465 // }
633 466 }
634 467
635 468
469 + public function wp_image_editors( $editors ) {
470 + // If GD is available, use only GD. Otherwise, fallback to all available editors.
471 + if ( is_callable( [ 'WP_Image_Editor_GD', 'test' ] ) && call_user_func( [ 'WP_Image_Editor_GD', 'test' ] ) ) {
472 + return [ 'WP_Image_Editor_GD' ];
473 + }
474 + return $editors;
475 + }
476 +
636 477 // Updated import_status method
637 478 public function import_status() {
638 479 $request_params = $this->get_session_data();
639 480
@@ -668,9 +509,9 @@
668 509 private function throw_non_retryable($message, $code = 0) {
669 510 if ($this->dev_mode) {
670 511 error_log(print_r($message, 1));
671 512 }
672 - throw new NonRetirableErrorException($message);
513 + throw new NonRetryableErrorException($message);
673 514 }
674 515 /**
675 516 * @throws Exception
676 517 */
@@ -699,12 +540,9 @@
699 540 if (!is_writable($upload_dir['basedir'])) {
700 541 $this->throw(__('Upload directory is not writable.', 'templately'));
701 542 }
702 543
703 - // Goes through Helper so the wp-uploads/templately root gets its
704 - // index.php / .htaccess / web.config guards before anything is written
705 - // into it — the extracted pack lives here and uploads is web-served.
706 - $this->tmp_dir = Helper::upload_dir('tmp');
544 + $this->tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
707 545
708 546 if (!is_dir($this->tmp_dir)) {
709 547 wp_mkdir_p($this->tmp_dir);
710 548 }
@@ -711,19 +549,31 @@
711 549
712 550 $this->sse_log('writing_permission_check', __('Permission Passed', 'templately'), 100);
713 551 }
714 552
553 + private function get_api_url($version, $end_point): string {
554 + return $this->dev_mode ? "https://app.templately.dev/api/$version/" . $end_point : "https://app.templately.com/api/$version/" . $end_point;
555 + }
556 +
557 + private function info_get_api_url($id): string {
558 + return $this->dev_mode ? 'https://app.templately.dev/api/v1/import/info/pack/' . $id : 'https://app.templately.com/api/v1/import/info/pack/' . $id;
559 + }
560 +
715 561 /**
716 562 * @throws Exception
717 563 */
718 - private function download_zip( $id, $is_ai = false ) {
564 + private function download_zip( $id ) {
719 565 $this->sse_log( 'download', __( 'Downloading Template Pack', 'templately' ), 1 );
720 - $extra_headers = [
721 - 'x-templately-is-ai' => $is_ai,
722 - 'x-templately-session-id' => $this->session_id,
723 - 'x-templately-requested-platform' => $this->request_params["requested_platform"] ?? 'templately',
724 - ];
725 - $response = Helper::make_api_get_request("v2/import/pack/$id", [], $extra_headers, 90);
566 + $response = wp_remote_get( $this->get_api_url( "v2", "import/pack/$id" ), [
567 + 'timeout' => 30,
568 + 'headers' => [
569 + 'Content-Type' => 'application/json',
570 + 'Authorization' => 'Bearer ' . $this->api_key,
571 + 'x-templately-ip' => Helper::get_ip(),
572 + 'x-templately-url' => home_url('/'),
573 + 'x-templately-version' => TEMPLATELY_VERSION,
574 + ]
575 + ]);
726 576
727 577 $response_code = wp_remote_retrieve_response_code($response);
728 578 $content_type = wp_remote_retrieve_header($response, 'content-type');
729 579 $this->download_key = wp_remote_retrieve_header($response, 'download-key');
@@ -737,10 +587,10 @@
737 587
738 588 // If the response body is JSON and it contains an error, throw an exception with the error message
739 589 if (isset($response_body['status']) && $response_body['status'] === 'error') {
740 590 $support_message = '';
741 - if(strpos($response_body['message'], 'https://templately.com/?support=open') === false){
742 - $support_message = sprintf(__(" Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://templately.com/?support=open');
591 + if(strpos($response_body['message'], 'https://wpdeveloper.com/support') === false){
592 + $support_message = sprintf(__(" Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://wpdeveloper.com/support');
743 593 }
744 594 $this->throw_non_retryable($response_body['message'] . $support_message);
745 595 }
746 596 }
@@ -748,18 +598,12 @@
748 598 }
749 599
750 600 $this->sse_log('download', __('Downloading Template Pack', 'templately'), 57);
751 601
752 - SessionData::set($this->session_id, 'download_key', $this->download_key);
602 + $this->update_session_data([
603 + 'download_key' => $this->download_key,
604 + ]);
753 605
754 - // Security: Validate file path is within WordPress upload directory before writing
755 - $validation = AIUtils::validate_file_path($this->filePath);
756 - if (is_wp_error($validation)) {
757 - $this->throw($validation->get_error_message());
758 - }
759 -
760 - wp_mkdir_p(dirname($this->filePath));
761 -
762 606 if (file_put_contents($this->filePath, $response['body'])) { // phpcs:ignore
763 607 $this->sse_log('download', __('Downloading Template Pack', 'templately'), 100);
764 608
765 609 $this->unzip();
@@ -774,62 +618,20 @@
774 618 protected function unzip() {
775 619 if (!WP_Filesystem()) {
776 620 $this->throw(__('WP_Filesystem cannot be initialized', 'templately'));
777 621 }
622 +
778 623 $unzip = unzip_file($this->filePath, $this->dir_path);
779 624 if (is_wp_error($unzip)) {
780 - // Fall back to our own ZipArchive-based extractor. Some Templately
781 - // packs carry entries with a leading "./" (or embedded "/./") path
782 - // segment, which WordPress core's unzip_file() fails to extract.
783 - // self::unzip_file() extracts with native ZipArchive and normalizes
784 - // those "./" segments so the pack still imports. See
785 - // self::unzip_file() for the extraction and path-traversal guard.
786 625 $unzip = $this->unzip_file($this->filePath, $this->dir_path);
787 626 }
788 627
789 - // Core's unzip_file() extracts whatever the archive holds, and the pack
790 - // lands under web-served wp-uploads. Sweep before anything else touches
791 - // the tree, so the relocation below cannot copy an executable upward.
792 - $this->purge_disallowed_files($this->dir_path);
793 -
794 - $manifest_file = $this->dir_path . 'manifest.json';
795 -
796 - // If manifest.json is missing, but any subdirectory contains manifest.json, move all its contents up and remove the subdirectory.
797 - if ( ! file_exists( $manifest_file ) ) {
798 - $entries = array_diff( scandir( $this->dir_path ), [ '.', '..' ] );
799 - $dirs = array_filter( $entries, fn($e) => is_dir( $this->dir_path . $e ) );
800 - $files = array_filter( $entries, fn($e) => is_file( $this->dir_path . $e ) );
801 - foreach ($dirs as $subdir) {
802 - $subdir_path = $this->dir_path . $subdir . DIRECTORY_SEPARATOR;
803 - if ( file_exists( $subdir_path . 'manifest.json' ) ) {
804 - copy($subdir_path . 'manifest.json', $manifest_file);
805 -
806 - foreach ( array_diff( scandir( $subdir_path ), [ '.', '..' ] ) as $item ) {
807 - $src = $subdir_path . $item;
808 - $dst = $this->dir_path . $item;
809 - if (is_dir($src)) {
810 - if (!file_exists($dst)) {
811 - wp_mkdir_p($dst);
812 - }
813 - // Recursively copy directory
814 - $this->copyDirectory($src, $dst);
815 - } else {
816 - copy($src, $dst);
817 - }
818 - }
819 - // Remove the subdirectory and its contents
820 - $this->removeDirectory($subdir_path);
821 - break; // Only process the first subdir with manifest.json
822 - }
823 - }
824 - }
825 -
826 628 if (is_wp_error($unzip)) {
827 629 $error = $unzip->get_error_message();
828 630 if (empty($error)) {
829 631 // Generic error message
830 632 Helper::log($unzip);
831 - $error_message = sprintf(__("It seems we're experiencing technical difficulties. Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://templately.com/?support=open');
633 + $error_message = sprintf(__("It seems we're experiencing technical difficulties. Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://wpdeveloper.com/support');
832 634 $this->throw($error_message);
833 635 } else {
834 636 $this->throw($unzip->get_error_message());
835 637 }
@@ -840,352 +642,34 @@
840 642 }
841 643 }
842 644
843 645 /**
844 - * Recursively copy a directory
845 - */
846 - private function copyDirectory($src, $dst) {
847 - $dir = opendir($src);
848 - wp_mkdir_p($dst);
849 - while(false !== ($file = readdir($dir))) {
850 - if (($file != '.') && ($file != '..')) {
851 - if (is_dir($src . DIRECTORY_SEPARATOR . $file)) {
852 - $this->copyDirectory($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
853 - } else {
854 - copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
855 - }
856 - }
857 - }
858 - closedir($dir);
859 - }
860 -
861 - /**
862 - * Recursively remove a directory
863 - */
864 - private function removeDirectory($dir) {
865 - if (!file_exists($dir)) return;
866 - $items = array_diff(scandir($dir), ['.', '..']);
867 - foreach ($items as $item) {
868 - $path = $dir . DIRECTORY_SEPARATOR . $item;
869 - if (is_dir($path)) {
870 - $this->removeDirectory($path);
871 - } else {
872 - unlink($path);
873 - }
874 - }
875 - rmdir($dir);
876 - }
877 -
878 -
879 -
880 - /**
881 - * Extensions a pack carries that WordPress does not accept as an upload.
882 - *
883 - * Everything else comes from get_allowed_mime_types(), so this list only has
884 - * to name what the pack format adds on top of it:
885 - *
886 - * - `json` — every pack is built from JSON (manifest, templates, content),
887 - * and WordPress has never allowed .json uploads.
888 - * - `xml` — the WXR files under wp-content/. Also not an allowed upload:
889 - * core hands .xml to the importer plugin rather than the media library.
890 - * - `ai` — the marker segment in `{id}.ai.json`, an AI-generated page. Not
891 - * the Illustrator format.
892 - * - `svg` — packs ship SVG artwork. This plugin already allows SVG uploads
893 - * for the duration of an import (see allow_svg_upload()), but that filter is
894 - * only added in start_content_import(), which runs AFTER extraction — so at
895 - * sweep time get_allowed_mime_types() does not have it and the pack's SVGs
896 - * would be deleted. It has to be named here.
897 - * - fonts — a pack may ship webfonts, and none of these are upload types.
898 - * Passive assets; nothing here is executable by any server.
899 - *
900 - * Adding to this list is how a new pack file type gets through. It fails
901 - * closed and logs, so a missing entry shows up as a dropped file in the
902 - * import log — not as a silent hole.
903 - */
904 - const PACK_EXTENSIONS = [
905 - 'json', 'xml', 'ai',
906 - 'svg', 'svgz',
907 - 'woff', 'woff2', 'ttf', 'otf', 'eot',
908 - ];
909 -
910 - /**
911 - * Extensions an extracted pack member is permitted to use.
912 - *
913 - * Deliberately an allowlist, and deliberately WordPress's own: naming the
914 - * dangerous extensions instead means being exhaustive about `.phtml`, `.pht`,
915 - * `.phar`, `.cgi`, `.htaccess`, `.user.ini` and whatever a future server
916 - * config decides to execute — one omission and the guard is silent.
917 - * get_allowed_mime_types() already encodes what this site accepts, follows
918 - * the `upload_mimes` filter, and gains new formats as WordPress does.
919 - *
920 - * @return array Extension => true lookup.
921 - */
922 - protected static function allowed_pack_extensions() {
923 - $allowed = array_fill_keys(self::PACK_EXTENSIONS, true);
924 -
925 - // Keys are alternation patterns: 'jpg|jpeg|jpe' => 'image/jpeg'.
926 - foreach (array_keys(get_allowed_mime_types()) as $pattern) {
927 - foreach (explode('|', $pattern) as $extension) {
928 - $allowed[$extension] = true;
929 - }
930 - }
931 -
932 - return $allowed;
933 - }
934 -
935 - /**
936 - * Segments that must never appear anywhere in a pack member's name.
937 - *
938 - * This one IS a denylist, and only because it is applied to the segments
939 - * *before* the real extension, which the allowlist has already vetted. A
940 - * permissive `AddHandler` runs `shell.php.gif` as PHP, so an inner segment
941 - * still has to be refused — but refusing every inner segment the allowlist
942 - * does not know would delete ordinary names like `style.min.css` or
943 - * `hero.2x.png`, where the inner segment is a word, not an extension.
944 - *
945 - * An omission here is far less serious than in the allowlist: it only
946 - * matters for a name whose final extension is already an accepted upload
947 - * type, on a server configured to hand an inner extension to a handler.
948 - */
949 - const EXECUTABLE_SEGMENTS = [
950 - 'php', 'php3', 'php4', 'php5', 'php6', 'php7', 'php8',
951 - 'phps', 'phtml', 'phtm', 'pht', 'phar', 'inc',
952 - 'cgi', 'fcgi', 'pl', 'py', 'rb', 'sh', 'bash', 'ksh', 'csh', 'zsh',
953 - 'asp', 'aspx', 'ascx', 'ashx', 'asmx', 'cfm', 'cfml',
954 - 'jsp', 'jspx', 'jar', 'war',
955 - 'shtml', 'shtm',
956 - 'exe', 'com', 'bat', 'cmd', 'dll', 'so',
957 - 'htaccess', 'htpasswd', 'ini', 'env', 'conf',
958 - ];
959 -
960 - /**
961 - * Whether an archive entry (or extracted file) is something a pack may hold.
962 - *
963 - * Two different rules, because a filename's dots do not all mean the same
964 - * thing. The LAST segment is the extension the server dispatches on, so it
965 - * is checked against the allowlist. The segments before it are usually just
966 - * part of the name — `style.min.css`, `hero.2x.png`, `logo.v2.png`,
967 - * `12.ai.json` — so they are only checked against EXECUTABLE_SEGMENTS,
968 - * which is what still refuses `shell.php.gif` under a permissive
969 - * `AddHandler`.
970 - *
971 - * A name with no extension at all is refused, which is what catches
972 - * `.htaccess`, `.user.ini` and `.env` — none of them have one.
973 - *
974 - * @param string $name Entry name or file path.
975 - * @param array $allowed Lookup from allowed_pack_extensions(). Built on
976 - * demand when omitted; pass it in when looping.
977 - *
978 - * @return bool
979 - */
980 - protected static function is_allowed_entry($name, $allowed = null) {
981 - if (null === $allowed) {
982 - $allowed = self::allowed_pack_extensions();
983 - }
984 -
985 - $segments = explode('.', strtolower(wp_basename($name)));
986 -
987 - // No extension, or a leading-dot name whose only "segment" is empty.
988 - if (count($segments) < 2 || '' === $segments[0]) {
989 - return false;
990 - }
991 -
992 - $extension = array_pop($segments);
993 - if (!isset($allowed[$extension])) {
994 - return false;
995 - }
996 -
997 - $executable = array_fill_keys(self::EXECUTABLE_SEGMENTS, true);
998 -
999 - // array_slice() drops the base name; only what sits between it and the
1000 - // extension is a masking risk.
1001 - foreach (array_slice($segments, 1) as $segment) {
1002 - if (isset($executable[$segment])) {
1003 - return false;
1004 - }
1005 - }
1006 -
1007 - return true;
1008 - }
1009 -
1010 - /**
1011 - * Deletes anything an extracted pack has no business containing.
1012 - *
1013 - * The fallback extractor below refuses these entries outright, but WordPress
1014 - * core's unzip_file() runs first and has no such filter, so the guard has to
1015 - * exist on the extracted tree as well.
1016 - *
1017 - * @param string $dir Extracted pack root.
1018 - *
1019 - * @return int Number of files removed.
1020 - */
1021 - protected function purge_disallowed_files($dir) {
1022 - if (empty($dir) || !is_dir($dir)) {
1023 - return 0;
1024 - }
1025 -
1026 - $removed = 0;
1027 - $allowed = self::allowed_pack_extensions();
1028 -
1029 - try {
1030 - $files = new \RecursiveIteratorIterator(
1031 - new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
1032 - \RecursiveIteratorIterator::CHILD_FIRST
1033 - );
1034 -
1035 - foreach ($files as $fileinfo) {
1036 - if (!$fileinfo->isFile() || self::is_allowed_entry($fileinfo->getFilename(), $allowed)) {
1037 - continue;
1038 - }
1039 -
1040 - if (@unlink($fileinfo->getPathname())) {
1041 - $removed++;
1042 - Helper::log($fileinfo->getPathname(), 'unzip: removed disallowed file from extracted pack', 'warning');
1043 - }
1044 - }
1045 - } catch (\Exception $e) {
1046 - Helper::log($e->getMessage(), 'purge_disallowed_files', 'warning');
1047 - }
1048 -
1049 - return $removed;
1050 - }
1051 -
1052 - /**
1053 646 * Unzip a specified ZIP file to a location on the Filesystem.
1054 647 *
1055 - * Why this custom extractor exists: some Templately packs carry entries with
1056 - * a leading "./" (or embedded "/./") path segment, which WordPress core's
1057 - * unzip_file() fails to extract. This method extracts with PHP's native
1058 - * ZipArchive and normalizes the redundant "./" segments (see
1059 - * normalize_zip_entry_name()) so the pack still imports. It is a fallback:
1060 - * self::unzip() only calls it after the core unzip_file() returns a WP_Error.
1061 - *
1062 - * Each archive member is validated before extraction rather than
1063 - * calling ZipArchive::extractTo() blindly: entries that resolve outside the
1064 - * destination (path traversal / "Zip Slip"), absolute paths, and Windows
1065 - * drive-letter paths are skipped via validate_file(), mirroring the guard
1066 - * WordPress core applies in _unzip_file_ziparchive(). Redundant "./" path
1067 - * segments are normalized first so members land at their intended location.
1068 - *
1069 648 * @param string $file Full path and filename of ZIP archive.
1070 649 * @param string $to Full path on the filesystem to extract archive to.
1071 650 * @return true|WP_Error True on success, WP_Error on failure.
1072 651 */
1073 652 function unzip_file($file, $to) {
1074 - $zip = new \ZipArchive;
1075 -
1076 - $res = $zip->open($file);
1077 - if ($res !== TRUE) {
1078 - return new \WP_Error('zip_error_' . $zip->status, $zip->getStatusString());
1079 - }
1080 -
1081 - // Close the archive handle on every exit path (success, mid-loop
1082 - // exception, or early return) so it is never leaked.
1083 653 try {
1084 - $to = trailingslashit($to);
654 + $zip = new \ZipArchive;
1085 655
1086 - $allowed_extensions = self::allowed_pack_extensions();
656 + $res = $zip->open($file);
657 + if ($res === TRUE) {
658 + $zip->extractTo($to);
659 + $zip->close();
1087 660
1088 - for ($i = 0; $i < $zip->numFiles; $i++) {
1089 - $name = $zip->getNameIndex($i);
1090 - if ($name === false) {
1091 - continue;
1092 - }
1093 -
1094 - // Normalize redundant "./" segments so the destination path
1095 - // is computed from a clean entry name.
1096 - $name = $this->normalize_zip_entry_name($name);
1097 - if ($name === '') {
1098 - continue; // Archive root (e.g. a "./" entry).
1099 - }
1100 -
1101 - // Skip the OS X-created __MACOSX directory.
1102 - if (strpos($name, '__MACOSX/') === 0) {
1103 - continue;
1104 - }
1105 -
1106 - // Don't extract invalid files: reject "../" traversal,
1107 - // absolute, and drive-letter paths so no member can be
1108 - // written outside $to. Log the skipped entry name so a
1109 - // hostile or corrupt pack leaves a forensic trail rather
1110 - // than silently extracting only part of its contents.
1111 - if (0 !== validate_file($name)) {
1112 - Helper::log($name, 'unzip_file: skipped unsafe archive entry', 'warning');
1113 - continue;
1114 - }
1115 -
1116 - // validate_file() stops a member escaping $to; it says nothing
1117 - // about what the member *is*. $to lives under web-served
1118 - // wp-uploads, so an executable member would be directly
1119 - // requestable.
1120 - if (!self::is_allowed_entry($name, $allowed_extensions)) {
1121 - Helper::log($name, 'unzip_file: skipped disallowed archive entry', 'warning');
1122 - continue;
1123 - }
1124 -
1125 - if (substr($name, -1) === '/') {
1126 - // Directory entry.
1127 - wp_mkdir_p($to . untrailingslashit($name));
1128 - continue;
1129 - }
1130 -
1131 - $contents = $zip->getFromIndex($i);
1132 - if ($contents === false) {
1133 - continue;
1134 - }
1135 -
1136 - $target = $to . $name;
1137 - wp_mkdir_p(dirname($target));
1138 - file_put_contents($target, $contents); // phpcs:ignore
661 + return true;
1139 662 }
1140 -
1141 - return true;
1142 663 } catch (\Throwable $th) {
1143 664 return new \WP_Error('exception_caught', $th->getMessage());
1144 - } finally {
1145 - $zip->close();
1146 665 }
1147 - }
1148 666
1149 - /**
1150 - * Removes redundant current-directory ("./") segments from a ZIP entry path.
1151 - *
1152 - * Some archive tools store entry names with a leading "./" or embedded "/./"
1153 - * segment (for example "./manifest.json" or "content/./page.json"). Parent
1154 - * ("..") segments are intentionally left untouched so validate_file() can
1155 - * still reject them.
1156 - *
1157 - * @param string $name A ZIP archive entry path.
1158 - * @return string The entry path with current-directory segments removed.
1159 - */
1160 - protected function normalize_zip_entry_name($name) {
1161 - // Fast path: bail when there is no current-directory segment to remove.
1162 - if ('.' !== $name
1163 - && strpos($name, './') !== 0
1164 - && strpos($name, '/./') === false
1165 - && substr($name, -2) !== '/.'
1166 - ) {
1167 - return $name;
667 + if (isset($zip)) {
668 + return new \WP_Error('zip_error_' . $zip->status, $zip->getStatusString());
669 + } else {
670 + return new \WP_Error('unknown_error', '');
1168 671 }
1169 -
1170 - // A trailing slash, or a trailing "/." or bare ".", denotes a directory.
1171 - $is_directory = substr($name, -1) === '/' || substr($name, -2) === '/.' || '.' === $name;
1172 -
1173 - $segments = array();
1174 - foreach (explode('/', $name) as $segment) {
1175 - if ('.' !== $segment) {
1176 - $segments[] = $segment;
1177 - }
1178 - }
1179 -
1180 - $name = implode('/', $segments);
1181 -
1182 - // Preserve the trailing slash that marks a directory entry.
1183 - if ($is_directory && '' !== $name && substr($name, -1) !== '/') {
1184 - $name .= '/';
1185 - }
1186 -
1187 - return $name;
1188 672 }
1189 673
1190 674 /**
1191 675 * @throws Exception
@@ -1249,42 +733,12 @@
1249 733 $import_status = $this->handle_import_status('success');
1250 734
1251 735 update_option('templately_flush_rewrite_rules', true, false);
1252 736
1253 - $normalized_data = $this->normalize_imported_data($imported_data);
1254 - // Use timeout-aware wait handler for AI content processing
1255 - if(!empty($request_params['ai_page_ids']) && empty($normalized_data['ai_content']['processed']['credit_cost'])){
1256 - $processed_pages = get_option("templately_ai_processed_pages", []);
1257 - $updated_ids = $processed_pages[$request_params['process_id']] ?? [];
1258 -
1259 - // Use the static timeout-aware wait handler from AIUtils.
1260 - // ai_page_ids must be FLATTENED — the handler counts it against the
1261 - // number of processed pages (see AIUtils::flatten_ai_page_ids).
1262 - AIUtils::handle_sse_wait_with_timeout(
1263 - $this->session_id,
1264 - 'ai_content_import_time',
1265 - $updated_ids,
1266 - AIUtils::flatten_ai_page_ids($request_params['ai_page_ids']),
1267 - [$this, 'sse_message'],
1268 - [
1269 - 'name' => 'ai-content',
1270 - 'message' => __('Missing Credit Cost', 'templately'),
1271 - ],
1272 - null, // No specific template ID for this context
1273 - 30
1274 - );
1275 - }
1276 -
1277 - // Get skipped items if skip feature was enabled
1278 - $skipped_items = SessionData::get_skipped_items($this->session_id);
1279 - if (!empty($skipped_items)) {
1280 - $normalized_data['skipped_items'] = $skipped_items;
1281 - }
1282 -
1283 737 $this->sse_message([
1284 738 'type' => 'complete',
1285 739 'action' => 'complete',
1286 - 'results' => $normalized_data,
740 + 'results' => $this->normalize_imported_data($imported_data)
1287 741 ]);
1288 742
1289 743 update_user_meta(get_current_user_id(), 'templately_fsi_pack_id', $request_params["id"]);
1290 744 if(!empty($import_status['hasFeedback'])){
@@ -1292,203 +746,16 @@
1292 746 }
1293 747 else{
1294 748 update_user_meta(get_current_user_id(), 'templately_fsi_complete', true);
1295 749 }
1296 -
1297 - do_action('templately_fsi_import_complete', $normalized_data);
1298 - $this->clear_data_file($request_params);
1299 750 }
1300 751
1301 - private function clear_data_file($request_params){
1302 - if(defined('TEMPLATELY_DEV') && TEMPLATELY_DEV){
1303 - return;
1304 - }
1305 -
1306 - // Handle directory cleanup
1307 - Utils::cleanup_directory($this->dir_path);
1308 - $upload_dir = wp_upload_dir();
1309 -
1310 - // Always save to preview directory for AI content workflow
1311 - $session_id = $request_params['session_id'] ?? '';
1312 - $pack_id = $request_params['id'] ?? '';
1313 -
1314 - // Set up directory paths for cleanup
1315 - $root_dir = $request_params['root_dir'] ?? trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp';
1316 - $prv_dir = $request_params['prv_dir'] ?? trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'preview';
1317 -
1318 - $processed_data = AIUtils::get_ai_process_data_by_session_id($session_id);
1319 -
1320 - if (!empty($pack_id) && !empty($session_id)) {
1321 - // Clean session data - keep only current session, remove others with same pack_id
1322 - // Modified to use SessionData directly
1323 - $removed_session_ids = SessionData::clean_by_pack_id($pack_id, $session_id);
1324 - }
1325 -
1326 - // Also run general expiration cleanup (7 days default)
1327 - // Modified to use SessionData directly
1328 - if (mt_rand(1, 100) <= 5) { // 5% chance to run on any import finish
1329 - $expired_cleanup = SessionData::cleanup_expired(7);
1330 -
1331 - // Clean AI process data - keep only current process, remove others with same pack_id
1332 - $current_process_id = !empty($processed_data['process_id']) ? $processed_data['process_id'] : null;
1333 - $removed_process_ids = AIUtils::clean_ai_process_data_by_pack_id($pack_id, $current_process_id);
1334 -
1335 - // Directory-based cleanup for session data directories
1336 - $this->cleanup_session_directories($root_dir, $pack_id, $session_id);
1337 -
1338 - // Directory-based cleanup for AI process data directories
1339 - $this->cleanup_ai_process_directories($prv_dir, $pack_id, $current_process_id);
1340 -
1341 - // Log cleanup results if in dev mode
1342 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1343 - if (!empty($removed_session_ids)) {
1344 - error_log('Templately: Cleaned up session IDs: ' . implode(', ', $removed_session_ids));
1345 - }
1346 - if (!empty($removed_process_ids)) {
1347 - error_log('Templately: Cleaned up process IDs: ' . implode(', ', $removed_process_ids));
1348 - }
1349 - }
1350 - }
1351 - }
1352 -
1353 - /**
1354 - * Directory-based cleanup for session data directories
1355 - * Scans the actual filesystem directories and removes directories that match cleanup criteria
1356 - *
1357 - * @param string $root_dir The root directory containing session directories
1358 - * @param string $pack_id The pack ID to match for cleanup
1359 - * @param string $current_session_id The current session ID to preserve
1360 - */
1361 - private function cleanup_session_directories($root_dir, $pack_id, $current_session_id) {
1362 - if (empty($root_dir) || !is_dir($root_dir) || empty($pack_id) || empty($current_session_id)) {
1363 - return;
1364 - }
1365 -
1366 - try {
1367 - // Get all session data to check pack_id associations
1368 - $all_session_data = SessionData::get_all_data();
1369 -
1370 - // Scan the actual directories in the filesystem
1371 - $directories = scandir($root_dir);
1372 - if ($directories === false) {
1373 - return;
1374 - }
1375 -
1376 - foreach ($directories as $dir_name) {
1377 - // Skip current directory, parent directory, and current session
1378 - if ($dir_name === '.' || $dir_name === '..' || $dir_name === $current_session_id) {
1379 - continue;
1380 - }
1381 -
1382 - $dir_path = trailingslashit($root_dir) . $dir_name;
1383 -
1384 - // Only process actual directories
1385 - if (!is_dir($dir_path)) {
1386 - continue;
1387 - }
1388 -
1389 - // Check if this directory should be cleaned up
1390 - $should_cleanup = false;
1391 -
1392 - // If we have session data for this directory, check if it matches the pack_id
1393 - if (isset($all_session_data[$dir_name]) &&
1394 - isset($all_session_data[$dir_name]['id']) &&
1395 - $all_session_data[$dir_name]['id'] === $pack_id) {
1396 - $should_cleanup = true;
1397 - } else if (!isset($all_session_data[$dir_name])) {
1398 - // This is an orphaned directory with no corresponding session data
1399 - $should_cleanup = true;
1400 - }
1401 -
1402 - if ($should_cleanup) {
1403 - Utils::cleanup_directory($dir_path);
1404 -
1405 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1406 - error_log('Templately: Cleaned up session directory: ' . $dir_name);
1407 - }
1408 - }
1409 - }
1410 - } catch (Exception $e) {
1411 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1412 - error_log('Templately: Error during session directory cleanup: ' . $e->getMessage());
1413 - }
1414 - }
1415 - }
1416 -
1417 - /**
1418 - * Directory-based cleanup for AI process data directories
1419 - * Scans the actual filesystem directories and removes directories that match cleanup criteria
1420 - *
1421 - * @param string $prv_dir The preview directory containing process directories
1422 - * @param string $pack_id The pack ID to match for cleanup
1423 - * @param string $current_process_id The current process ID to preserve (optional)
1424 - */
1425 - private function cleanup_ai_process_directories($prv_dir, $pack_id, $current_process_id = null) {
1426 - if (empty($prv_dir) || !is_dir($prv_dir) || empty($pack_id)) {
1427 - return;
1428 - }
1429 -
1430 - try {
1431 - // Get all AI process data to check pack_id associations
1432 - $ai_process_data = AIUtils::get_ai_process_data();
1433 -
1434 - // Scan the actual directories in the filesystem
1435 - $directories = scandir($prv_dir);
1436 - if ($directories === false) {
1437 - return;
1438 - }
1439 -
1440 - foreach ($directories as $dir_name) {
1441 - // Skip current directory, parent directory, and current process
1442 - if ($dir_name === '.' || $dir_name === '..' ||
1443 - (!empty($current_process_id) && $dir_name === $current_process_id)) {
1444 - continue;
1445 - }
1446 -
1447 - $dir_path = trailingslashit($prv_dir) . $dir_name;
1448 -
1449 - // Only process actual directories
1450 - if (!is_dir($dir_path)) {
1451 - continue;
1452 - }
1453 -
1454 - // Check if this directory should be cleaned up
1455 - $should_cleanup = false;
1456 -
1457 - // If we have process data for this directory, check if it matches the pack_id
1458 - if (isset($ai_process_data[$dir_name]) &&
1459 - is_array($ai_process_data[$dir_name]) &&
1460 - isset($ai_process_data[$dir_name]['pack_id']) &&
1461 - $ai_process_data[$dir_name]['pack_id'] === $pack_id) {
1462 - $should_cleanup = true;
1463 - } else if (!isset($ai_process_data[$dir_name])) {
1464 - // This is an orphaned directory with no corresponding process data
1465 - $should_cleanup = true;
1466 - }
1467 -
1468 - if ($should_cleanup) {
1469 - Utils::cleanup_directory($dir_path);
1470 -
1471 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1472 - error_log('Templately: Cleaned up AI process directory: ' . $dir_name);
1473 - }
1474 - }
1475 - }
1476 - } catch (Exception $e) {
1477 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1478 - error_log('Templately: Error during AI process directory cleanup: ' . $e->getMessage());
1479 - }
1480 - }
1481 - }
1482 -
1483 752 private function normalize_imported_data($data) {
1484 - $request_params = $this->get_session_data();
1485 753 $attachments = !empty($data['attachments']['succeed']) ? count($data['attachments']['succeed']) : 0;
1486 754 $attachments_fail = !empty($data['attachments']['failed']) ? count($data['attachments']['failed']) : 0;
1487 755 $attachments_errors = !empty($data['attachments_errors']) ? $data['attachments_errors'] : [];
1488 756 $templates = !empty($data['templates']['succeed']) ? count($data['templates']['succeed']) : 0;
1489 757 $template_types = !empty($data['templates']['template_types']) ? $data['templates']['template_types'] : [];
1490 - $dependency_data = !empty($data['dependency_data']) ? $data['dependency_data'] : [];
1491 758
1492 759 $post_types = [];
1493 760 $content_templates = [];
1494 761 if (!empty($data['content']) && is_array($data['content'])) {
@@ -1507,16 +774,11 @@
1507 774 }
1508 775 }
1509 776 }
1510 777
1511 - $_processed_pages = AIUtils::get_processed_pages_data($request_params['process_id']);
1512 - $ai_content = [
1513 - 'requested' => $request_params['ai_page_ids'] ?? [],
1514 - 'processed' => $_processed_pages,
1515 - ];
778 + Helper::log($data);
1516 779
1517 -
1518 - $result = [
780 + return [
1519 781 'attachments' => $attachments,
1520 782 'attachments_fail' => $attachments_fail,
1521 783 'attachments_errors' => $attachments_errors,
1522 784 'templates' => $templates,
@@ -1523,17 +785,10 @@
1523 785 'contents' => $content_templates,
1524 786 'wp-content' => $contents,
1525 787 'post_types' => $post_types,
1526 788 'template_types' => $template_types,
1527 - 'ai_content' => $ai_content,
1528 - 'dependency_data' => $dependency_data,
1529 - 'home_url' => home_url('/'),
789 + 'dependency_data' => $this->dependency_data,
1530 790 ];
1531 -
1532 - Helper::log($data);
1533 - Helper::log($result);
1534 -
1535 - return $result;
1536 791 }
1537 792
1538 793 public function get_request_params() {
1539 794 return $this->request_params;
@@ -1566,24 +821,21 @@
1566 821 $last_error = error_get_last();
1567 822 if ($last_error && ($last_error['type'] === E_ERROR || $last_error['type'] === E_CORE_ERROR || $last_error['type'] === E_COMPILE_ERROR || $last_error['type'] === E_USER_ERROR)) {
1568 823 if (!empty($last_error['message'])) {
1569 824 $full_message = $last_error['message'];
1570 - $lines = explode("\n", $full_message);
825 + // Extract the first line from the error message
826 + $firstLine = strtok($full_message, "\n");
1571 827
1572 - // For import status: first 5 lines
1573 - $import_status_message = implode("\n", array_slice($lines, 0, 5));
1574 - $import_status_message = str_replace(ABSPATH, 'ABSPATH/', $import_status_message);
1575 -
1576 - // For SSE: first line only
1577 - $sse_message = $lines[0];
1578 - $sse_message = str_replace(ABSPATH, 'ABSPATH/', $sse_message);
828 + // Remove absolute paths by replacing the WordPress directory path with a placeholder
829 + $error_message = str_replace(ABSPATH, 'ABSPATH/', $firstLine);
1579 830 } else {
1580 831 // Generic error message
1581 - $import_status_message = sprintf(__("It seems we're experiencing technical difficulties. Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://templately.com/?support=open');
1582 - $sse_message = $import_status_message;
832 + $error_message = sprintf(__("It seems we're experiencing technical difficulties. Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://wpdeveloper.com/support');
1583 833 }
1584 834
1585 - $this->handle_import_status('failed', $import_status_message);
835 +
836 + $this->handle_import_status('failed', $error_message);
837 + // Handle the error, e.g. log it or display a message to the user
1586 838 $this->sse_message([
1587 839 'action' => 'error',
1588 840 'status' => 'error',
1589 841 'type' => "error",
@@ -1588,10 +840,9 @@
1588 840 'status' => 'error',
1589 841 'type' => "error",
1590 842 'retry' => true,
1591 843 'title' => __("Oops!", "templately"),
1592 - 'message' => $sse_message,
1593 - 'error' => $last_error,
844 + 'message' => $error_message,
1594 845 // 'position' => 'plugin',
1595 846 // 'progress' => '--',
1596 847 ]);
1597 848 }
@@ -1618,48 +869,21 @@
1618 869 'x-templately-ip' => Helper::get_ip(),
1619 870 'x-templately-url' => home_url('/'),
1620 871 ];
1621 872
873 + $args = [
874 + 'headers' => $headers,
875 + ];
1622 876
1623 - $request_params = $this->get_session_data();
1624 - if(isset($request_params['process_id']) && !empty($request_params['ai_page_ids'])){
1625 - $updated_ids = AIUtils::get_processed_pages_data($request_params['process_id']);
1626 - $updated_pages = $updated_ids['pages'] ?? [];
1627 - $ai_page_ids = array_reduce($request_params['ai_page_ids'], 'array_merge', array());
1628 877
1629 - $headers['x-templately-ai-process-id'] = $request_params['process_id'];
1630 - $headers['x-templately-ai-requested-pages'] = implode(',', $ai_page_ids);
1631 - $headers['x-templately-ai-updated-pages'] = implode(',', array_keys($updated_pages));
1632 - $headers['x-templately-ai-missing-pages'] = implode(',', array_diff($ai_page_ids, array_keys($updated_pages)));
1633 - $headers['x-templately-ai-credit-cost'] = $updated_ids['credit_cost'] ?? null;
1634 -
1635 - // Phase-2 (chat-from-site): the backend keys the generated site by the
1636 - // conversation uuid — no download_key is minted for AI bundles. Forward
1637 - // it so the success/failed endpoints can flip the conversation status
1638 - // (delivered / error) and persist the failure. The uuid was stored under
1639 - // the process data when the import was prepared (chatbot-import-prepare).
1640 - $ai_process_data = AIUtils::get_ai_process_data_by_process_id($request_params['process_id']);
1641 - if (!empty($ai_process_data['chat_id'])) {
1642 - $headers['x-templately-ai-chat-uuid'] = $ai_process_data['chat_id'];
1643 - }
1644 - }
1645 -
1646 -
1647 - $extra_headers = $headers;
1648 -
1649 - // Seconds the user spent idling in the customizer step, accumulated by
1650 - // the React import wizard. Sent so the backend can exclude it from the
1651 - // import-time metric (a customizer left open for hours would otherwise
1652 - // inflate the count). Absent for older clients, which the backend
1653 - // treats as zero idle.
1654 - $customizer_idle = isset($request_params['customizer_idle']) ? (int) $request_params['customizer_idle'] : 0;
1655 -
1656 878 if ($status === 'success') {
1657 - $body = ['type' => 'pack', 'customizer_idle' => $customizer_idle];
1658 - $response = Helper::make_api_post_request('v1/import/success', $body, $extra_headers);
879 + $url = $this->get_api_url("v1", 'import/success');
880 + $args['body'] = json_encode(['type' => 'pack']);
881 + $response = wp_remote_post($url, $args);
1659 882 } elseif ($status === 'failed') {
1660 - $body = ['type' => 'pack', 'description' => $description ?: "Something Went wrong.....", 'customizer_idle' => $customizer_idle];
1661 - $response = Helper::make_api_post_request('v1/import/failed', $body, $extra_headers);
883 + $url = $this->get_api_url("v1", 'import/failed');
884 + $args['body'] = json_encode(['type' => 'pack', 'description' => $description ?: "Something Went wrong....."]);
885 + $response = wp_remote_post($url, $args);
1662 886 }
1663 887
1664 888 Helper::log($response);
1665 889
@@ -1667,9 +891,11 @@
1667 891 // Handle error
1668 892 Helper::log($response->get_error_message());
1669 893 } else {
1670 894
1671 - SessionData::set($this->session_id, 'is_import_status_handled', $this->is_import_status_handled);
895 + $this->update_session_data([
896 + 'is_import_status_handled' => $this->is_import_status_handled,
897 + ]);
1672 898 // Handle success
1673 899 $body = wp_remote_retrieve_body($response);
1674 900 $data = json_decode($body, true);
1675 901 // Do something with $body
@@ -1704,112 +930,53 @@
1704 930 public function import_info() {
1705 931
1706 932 $platform = isset($_GET['platform']) ? $_GET['platform'] : 'elementor';
1707 933 $id = isset($_GET['id']) ? intval($_GET['id']) : 0;
1708 - $isAi = isset($_GET['isAi']) ? $_GET['isAi'] : false;
1709 934
1710 - $data = $this->fetch_pack_info_from_api($id, $isAi);
1711 - if (is_wp_error($data)) {
1712 - wp_send_json_error($data->get_error_message());
1713 - return;
1714 - }
935 + $response = wp_remote_get($this->info_get_api_url($id), [
936 + 'timeout' => 30,
937 + 'headers' => [
938 + 'Authorization' => 'Bearer ' . $this->api_key,
939 + 'x-templately-ip' => Helper::get_ip(),
940 + 'x-templately-url' => home_url('/'),
941 + 'x-templately-version' => TEMPLATELY_VERSION,
942 + ]
943 + ]);
1715 944
1716 - $business_niches = get_option('templately_ai_business_niches', []);
1717 - $data['data']['business_niches'] = $business_niches;
1718 -
1719 - if (isset($data['data']['manifest'])) {
1720 - $data['data']['manifest'] = json_decode($data['data']['manifest'], true);
1721 - }
1722 - if (isset($data['data']['settings'])) {
1723 - $data['data']['settings'] = json_decode($data['data']['settings'], true);
1724 - }
1725 -
1726 - if ($isAi) {
1727 - // Get the latest AI process for the current API key
1728 - $last_ai_process = AIUtils::get_latest_ai_process_by_api_key($id);
1729 - if ($last_ai_process) {
1730 - // The cloud API key is stored on the process for server-side matching
1731 - // only. It must never reach the browser — under a global login it is
1732 - // the admin's credential, not the requesting user's.
1733 - unset($last_ai_process['api_key']);
1734 -
1735 - $data['data']['ai_process'] = $last_ai_process;
1736 - }
1737 -
1738 - if($last_ai_process && $id == ($last_ai_process['pack_id'] ?? null)){
1739 - // Read AI preview content directly from files using the common function
1740 - $session_id = $last_ai_process['session_id'] ?? null;
1741 - $ai_page_ids = $last_ai_process['ai_page_ids'] ?? [];
1742 - $dir_path = null;
1743 -
1744 - // Get session data to retrieve dir_path
1745 - if ($session_id) {
1746 - $session_data = SessionData::get_data($session_id);
1747 - $dir_path = $session_data['dir_path'] ?? null;
1748 - }
1749 -
1750 - // Use the common function to read AI template data if we have the required data
1751 - if ($session_id && $ai_page_ids && $dir_path) {
1752 - $data['data']['ai_preview_content'] = AIUtils::read_ai_template_data($session_id, $ai_page_ids, $dir_path);
1753 - } else {
1754 - $data['data']['ai_preview_content'] = [];
1755 - }
1756 - }
1757 - }
1758 -
1759 - // Return the response body
1760 - wp_send_json($data);
1761 - }
1762 -
1763 - private function fetch_pack_info_from_api($id, $isAi) {
1764 - $extra_headers = [
1765 - 'x-templately-is-ai' => $isAi,
1766 - ];
1767 - $response = Helper::make_api_get_request("v2/import/info/pack/$id", [], $extra_headers, 30);
1768 -
1769 945 if (is_wp_error($response)) {
1770 - return new \WP_Error('api_error', $response->get_error_message());
946 + wp_send_json_error($response->get_error_message());
947 + return;
1771 948 }
1772 949 // If the response code is not 200, return the error message
1773 950 if (wp_remote_retrieve_response_code($response) != 200) {
1774 - $error_message = $this->extract_error_from_response($response);
1775 - return new \WP_Error('api_error', $error_message);
951 + wp_send_json_error(json_decode(wp_remote_retrieve_body($response)), wp_remote_retrieve_response_code($response));
952 + return;
1776 953 }
954 + // If the response body is JSON and it contains an error, return the error message
1777 955 // Retrieve Data from Response Body.
1778 956 $body = wp_remote_retrieve_body($response);
1779 957 $data = json_decode($body, true);
1780 958
1781 959 if (isset($data['error'])) {
1782 - return new \WP_Error('api_error', $data['error']);
960 + wp_send_json_error($data['error']);
961 + return;
1783 962 }
1784 963
1785 - return $data;
1786 - }
1787 -
1788 - public function import_global_settings() {
1789 - $id = isset($_GET['id']) ? intval($_GET['id']) : 0;
1790 - $isAi = isset($_GET['isAi']) ? $_GET['isAi'] : false;
1791 -
1792 - $data = $this->fetch_pack_info_from_api($id, $isAi);
1793 - if (is_wp_error($data)) {
1794 - wp_send_json_error($data->get_error_message());
1795 - return;
964 + if (isset($data['data']['manifest'])) {
965 + $data['data']['manifest'] = json_decode($data['data']['manifest'], true);
1796 966 }
1797 -
1798 - $settings = [];
1799 967 if (isset($data['data']['settings'])) {
1800 - $settings = json_decode($data['data']['settings'], true);
968 + $data['data']['settings'] = json_decode($data['data']['settings'], true);
1801 969 }
1802 970
1803 - wp_send_json_success(['settings' => $settings]);
971 + // Return the response body
972 + wp_send_json($data);
1804 973 }
1805 974
1806 975 public function update_imported_list($type, $id) {
1807 976 $imported_list = get_option('templately_fsi_imported_list', []);
1808 - if(!in_array($id, $imported_list[$type] ?? [])){
1809 - $imported_list[$type][] = $id;
1810 - update_option('templately_fsi_imported_list', $imported_list, false);
1811 - }
977 + $imported_list[$type][] = $id;
978 + update_option('templately_fsi_imported_list', $imported_list);
1812 979 }
1813 980
1814 981 /**
1815 982 *
@@ -1949,39 +1116,21 @@
1949 1116
1950 1117 wp_send_json_error([ 'options' => $options_deleted, 'imported_list' => $imported_list_deleted, 'site_url' => home_url() ]);
1951 1118 }
1952 1119
1953 - /**
1954 - * Extract error response from JSON response body
1955 - * Returns the entire decoded JSON response if Content-Type is JSON,
1956 - * otherwise returns a generic HTTP error message
1957 - *
1958 - * @param array $response The response from wp_remote_get or similar
1959 - * @return mixed Decoded JSON response or generic HTTP error string
1960 - */
1961 - private function extract_error_from_response($response) {
1962 - try {
1963 - $content_type = wp_remote_retrieve_header($response, 'content-type');
1964 - if (!empty($content_type) && strpos($content_type, 'application/json') !== false) {
1965 - $body = wp_remote_retrieve_body($response);
1966 - $error_data = json_decode($body, true);
1967 - if ($error_data !== null) {
1968 - return $error_data;
1969 - }
1970 - }
1971 - } catch (Exception $e) {
1972 - // If JSON parsing fails, fall through to generic error
1973 - }
1974 -
1975 - // Fallback to generic HTTP error message
1976 - return __('API request failed with response code ', 'templately') . wp_remote_retrieve_response_code($response);
1977 - }
1978 -
1979 1120 public function google_font() {
1980 1121 $result = get_transient('templately-google-fonts');
1981 1122
1982 1123 if (false == $result) {
1983 - $response = Helper::make_api_get_request('v2/google-font', [], [], 30);
1124 + $response = wp_remote_get($this->get_api_url('v2', 'google-font'), [
1125 + 'timeout' => 30,
1126 + 'headers' => [
1127 + 'Authorization' => 'Bearer ' . $this->api_key,
1128 + 'x-templately-ip' => Helper::get_ip(),
1129 + 'x-templately-url' => home_url( '/' ),
1130 + 'x-templately-version' => TEMPLATELY_VERSION,
1131 + ]
1132 + ]);
1984 1133
1985 1134 if (is_wp_error($response)) {
1986 1135 wp_send_json_error($response->get_error_message());
1987 1136 }
@@ -1986,10 +1135,9 @@
1986 1135 wp_send_json_error($response->get_error_message());
1987 1136 }
1988 1137
1989 1138 if (wp_remote_retrieve_response_code($response) != 200) {
1990 - $error_message = $this->extract_error_from_response($response);
1991 - wp_send_json_error($error_message, wp_remote_retrieve_response_code($response));
1139 + wp_send_json_error('API request failed with response code ' . wp_remote_retrieve_response_code($response), wp_remote_retrieve_response_code($response));
1992 1140 }
1993 1141
1994 1142 $body = wp_remote_retrieve_body($response);
1995 1143 $data = json_decode($body, true);
@@ -2008,243 +1156,6 @@
2008 1156
2009 1157 wp_send_json_success($result);
2010 1158 }
2011 1159
2012 - /**
2013 - * AJAX handler: merge global kit settings into template content.
2014 - *
2015 - * Accepts (via POST JSON body):
2016 - * platform - 'elementor' | 'gutenberg'
2017 - * template - the template data array returned by /v1/insert
2018 - * settings - the kit settings array from import_info
2019 - *
2020 - * Returns the template data with globals resolved, or an error.
2021 - * The JS side should fall back to the unmerged template on any error.
2022 - */
2023 - public function merge_settings_with_template() {
2024 - $body = file_get_contents( 'php://input' );
2025 - $data = json_decode( $body, true );
2026 -
2027 - $platform = isset( $data['platform'] ) ? sanitize_text_field( $data['platform'] ) : 'elementor';
2028 - $template = $data['template'] ?? null;
2029 - $settings = $data['settings'] ?? null;
2030 -
2031 - if ( empty( $template ) || ! is_array( $template ) ) {
2032 - wp_send_json_error( 'Invalid template data.' );
2033 - return;
2034 - }
2035 -
2036 - if ( empty( $settings ) || ! is_array( $settings ) ) {
2037 - // No settings to merge – return template unchanged.
2038 - wp_send_json_success( $template );
2039 - return;
2040 - }
2041 -
2042 - try {
2043 - if ( $platform === 'elementor' ) {
2044 - if ( ! empty( $template['content'] ) && is_array( $template['content'] ) ) {
2045 - $template['content'] = ElementorSettingsMerger::merge( $template['content'], $settings );
2046 - }
2047 - } elseif ( $platform === 'gutenberg' ) {
2048 - if ( ! empty( $template['content'] ) && is_string( $template['content'] ) ) {
2049 - $template['content'] = GutenbergSettingsMerger::merge( $template['content'], $settings );
2050 - }
2051 - }
2052 -
2053 - wp_send_json_success( $template );
2054 - } catch ( \Exception $e ) {
2055 - Helper::log( 'merge_settings_with_template error: ' . $e->getMessage() );
2056 - // Return error so JS can fall back to unmerged template.
2057 - wp_send_json_error( 'Settings merge failed: ' . $e->getMessage() );
2058 - }
2059 - }
2060 -
2061 - public function ai_get_json() {
2062 - // read json data from post body
2063 - $body = file_get_contents('php://input');
2064 - $data = json_decode($body, true);
2065 -
2066 - if(empty($data['ai_page_ids'])){
2067 - wp_send_json_error('Invalid ai_page_ids');
2068 - return;
2069 - }
2070 -
2071 - if(!isset($_GET['session_id'])){
2072 - wp_send_json_error('Invalid session_id');
2073 - return;
2074 - }
2075 -
2076 - $session_id = isset($_GET['session_id']) ? sanitize_text_field($_GET['session_id']) : null;
2077 - $process_id = $data['process_id'] ?? null;
2078 - $ai_page_ids = $data['ai_page_ids'] ?? null;
2079 -
2080 - $this->request_params = $this->get_session_data();
2081 - try {
2082 - $this->manifest = $this->read_manifest($this->request_params['dir_path']);
2083 - } catch (\Exception $th) {
2084 - wp_send_json_error($th->getMessage());
2085 - }
2086 -
2087 - if(!empty($session_id) && empty($process_id)){
2088 - if ( !empty($this->request_params['process_id']) ){
2089 - $process_id = $this->request_params['process_id'] ?? null;
2090 - } else {
2091 - $process_id = AIUtils::get_ai_process_id_by_session_id($session_id);
2092 - }
2093 - }
2094 -
2095 - if(empty($process_id)){
2096 - wp_send_json_error('Invalid process_id');
2097 - return;
2098 - }
2099 -
2100 - $process_data = AIUtils::get_ai_process_data_by_process_id($process_id);
2101 - if (!empty($process_data['preview_error'])) {
2102 - wp_send_json_error($process_data['preview_error']);
2103 - }
2104 -
2105 - // Use the new common function to read AI template data directly
2106 - $result = AIUtils::read_ai_template_data($session_id, $ai_page_ids, $this->request_params['dir_path']);
2107 -
2108 - // Check if this is called from polling endpoint and include additional data
2109 - $response_data = ['process_id' => $process_id, 'templates' => $result];
2110 -
2111 - if (isset($this->polling_is_last_part)) {
2112 - $response_data['is_last_part'] = $this->polling_is_last_part;
2113 -
2114 - // Clean up the polling property
2115 - unset($this->polling_is_last_part);
2116 - }
2117 -
2118 - wp_send_json_success($response_data);
2119 - }
2120 -
2121 - /**
2122 - * AJAX handler for polling AI template generation status on local sites
2123 - * Makes GET request to API endpoint and returns data in same format as ai_get_json()
2124 - */
2125 - public function ai_poll_template() {
2126 - // Read JSON data from post body
2127 - $body = file_get_contents('php://input');
2128 - $data = json_decode($body, true);
2129 -
2130 - $process_id = $data['process_id'] ?? null;
2131 - $ai_page_ids = $data['ai_page_ids'] ?? null;
2132 -
2133 - if(empty($process_id)){
2134 - wp_send_json_error('Invalid process_id');
2135 - return;
2136 - }
2137 -
2138 - // Validate and get AI process data using centralized method
2139 - $process_data = AIUtils::validate_and_get_process_data($process_id);
2140 - if (is_wp_error($process_data)) {
2141 - $this->ai_get_json();
2142 - return;
2143 - }
2144 -
2145 - $session_id = $process_data['session_id'];
2146 - $ai_page_ids = $process_data['ai_page_ids'];
2147 -
2148 - // Use the common polling function to handle all template processing
2149 - $polling_result = AIUtils::poll_for_template($process_id, $session_id, $ai_page_ids);
2150 -
2151 - if (!$polling_result) {
2152 - // Polling failed, fallback to ai_get_json
2153 - $this->ai_get_json();
2154 - return;
2155 - }
2156 -
2157 - // After polling and processing templates, call ai_get_json() to return the data
2158 - // This reuses all the existing logic without duplication
2159 - $this->ai_get_json();
2160 - }
2161 -
2162 - /**
2163 - * Process AI preview content following the ai_get_json() pattern
2164 - *
2165 - * @param string $process_id The AI process ID
2166 - * @param array $ai_page_ids The AI page IDs data structure
2167 - * @param array $ai_preview_ids The AI preview IDs to process
2168 - * @return array Processed AI content data
2169 - */
2170 - private function process_ai_preview_content($process_id, $ai_page_ids, $ai_preview_ids) {
2171 - if (empty($process_id) || empty($ai_page_ids) || empty($ai_preview_ids)) {
2172 - return [];
2173 - }
2174 -
2175 - $all_ai_process_data = AIUtils::get_ai_process_data();
2176 - if (empty($all_ai_process_data[$process_id])) {
2177 - return [];
2178 - }
2179 - $ai_process_data = $all_ai_process_data[$process_id];
2180 - $_REQUEST['is_lightspeed'] = 'true';
2181 - $_REQUEST['session_id'] = $ai_process_data['session_id'] ?? null;
2182 - // Initialize session data and manifest following ai_get_json() pattern
2183 - $this->request_params = $this->get_session_data();
2184 - $this->manifest = $this->read_manifest($this->request_params['dir_path']);
2185 -
2186 - // Create Finalizer instance with the same configuration as ai_get_json()
2187 - $finalizer = new Finalizer(array_merge($this->request_params, [
2188 - 'origin' => $this,
2189 - 'manifest' => $this->manifest,
2190 - ]));
2191 - $finalizer->process_id = $process_id;
2192 - $finalizer->ai_page_ids = $ai_page_ids;
2193 -
2194 - $result = [];
2195 -
2196 - // Process each AI preview ID
2197 - foreach ($ai_preview_ids as $preview_id) {
2198 - // Extract type and sub_type metadata from ai_page_ids structure
2199 - $type_info = $this->extract_content_metadata($preview_id, $ai_page_ids);
2200 -
2201 - if ($type_info) {
2202 - $finalizer->type = $type_info['type'];
2203 - $finalizer->sub_type = $type_info['sub_type'];
2204 -
2205 - // Check if this is AI content before processing
2206 - if ($finalizer->isAiContent($preview_id)) {
2207 - // Process AI content using AIContentHelper trait
2208 - $ai_result = $finalizer->processAiContent($preview_id);
2209 - if ($ai_result['is_ai'] && !empty($ai_result['template_json'])) {
2210 - $template_json = $ai_result['template_json'];
2211 - $result[$preview_id] = $template_json;
2212 - } else if ($finalizer->isAiFileSkipped($preview_id)) {
2213 - // Handle skipped AI files
2214 - $result[$preview_id] = [];
2215 - }
2216 - }
2217 - }
2218 - }
2219 -
2220 - return $result;
2221 - }
2222 -
2223 - /**
2224 - * Extract content metadata (type and sub_type) from ai_page_ids structure
2225 - *
2226 - * @param string $preview_id The preview ID to find
2227 - * @param array $ai_page_ids The AI page IDs data structure
2228 - * @return array|null Array with 'type' and 'sub_type' keys, or null if not found
2229 - */
2230 - private function extract_content_metadata($preview_id, $ai_page_ids) {
2231 - if (empty($ai_page_ids) || !is_array($ai_page_ids)) {
2232 - return null;
2233 - }
2234 -
2235 - // Search through the ai_page_ids structure to find the preview_id
2236 - foreach ($ai_page_ids as $key => $ids) {
2237 - if (is_array($ids) && in_array($preview_id, $ids)) {
2238 - // Extract type and sub_type from the key (e.g., 'content/page' or 'templates')
2239 - $type_arr = explode('/', $key);
2240 - return [
2241 - 'type' => $type_arr[0],
2242 - 'sub_type' => isset($type_arr[1]) ? $type_arr[1] : ''
2243 - ];
2244 - }
2245 - }
2246 -
2247 - return null;
2248 - }
2249 1160
2250 1161 }