PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.5.2
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.5.2
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
templately / includes / Core / Importer / FullSiteImport.php

FullSiteImport.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.5.2, at includes/Core/Importer/FullSiteImport.php

1,855 lines 58.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * remove 'action' => 'continue',
5 * way to retry
6 * way to skip if failed multiple times
7 *
8 * @todo: on runner check for timeout and retry
9 * @todo: use ErrorException on runner to skip item when error occurs: not useful
10 *
11 */
12
13
14 namespace Templately\Core\Importer;
15
16 use Elementor\Plugin;
17 use Error;
18 use Exception;
19 use Templately\Core\Importer\Exception\NonRetirableErrorException;
20 use Templately\Core\Importer\Exception\RetryableErrorException;
21 use Templately\Core\Importer\Exception\UnknownErrorException;
22 use Templately\Core\Importer\Runners\Finalizer;
23 use Templately\Core\Importer\Utils\LogHandler;
24 use Templately\Core\Importer\Utils\Utils;
25 use Templately\Core\Importer\Utils\SessionData;
26 use Templately\Core\Importer\Utils\AIUtils;
27 use Templately\Utils\Base;
28 use Templately\Utils\Helper;
29 use Templately\Utils\Installer;
30 use Templately\Utils\Options;
31
32 class FullSiteImport extends Base {
33 use LogHelper;
34
35 const SESSION_OPTION_KEY = 'templately_import_session';
36 public $manifest;
37 protected $export;
38
39 private $version = '1.0.0';
40
41 public $download_key;
42 protected $dev_mode = false;
43 protected $api_key = '';
44 protected $session_id = '';
45 protected $documents_data = [];
46 private $is_import_status_handled = false;
47
48 public $dir_path;
49 protected $filePath;
50 protected $tmp_dir = null;
51 public $request_params = [];
52
53 // Polling-specific property for ai_poll_template()
54 private $polling_is_last_part = null;
55
56 public function __construct() {
57 $this->dev_mode = defined('TEMPLATELY_DEV') && TEMPLATELY_DEV;
58 $this->api_key = Options::get_instance()->get('api_key');
59
60 $this->add_ajax_action('import_settings', $this);
61 $this->add_ajax_action('create_session_and_download', $this);
62 $this->add_ajax_action('import_status', $this);
63 $this->add_ajax_action('import', $this);
64 $this->add_ajax_action('import_revert', $this);
65 $this->add_ajax_action('import_info', $this);
66 $this->add_ajax_action('import_close_feedback_modal', $this);
67 $this->add_ajax_action('feedback_form', $this);
68 $this->add_ajax_action('google_font', $this);
69 $this->add_ajax_action('ai_get_json', $this);
70 $this->add_ajax_action('ai_poll_template', $this);
71
72 add_action('admin_init', [$this, 'admin_init']);
73 // add_action('admin_notices', [$this, 'add_revert_button']);
74
75 if(isset($_GET['action']) && ($_GET['action'] == 'templately_pack_import' || $_GET['action'] == 'templately_pack_import_status')) {
76 add_filter('wp_redirect', '__return_false', 999);
77 }
78
79 if ($this->dev_mode) {
80 add_filter('http_request_host_is_external', '__return_true');
81 add_filter('http_request_args', function ($args) {
82 $args['sslverify'] = false;
83
84 return $args;
85 });
86 }
87 }
88
89 public function add_ajax_action($action, $object) {
90 add_action("wp_ajax_templately_pack_$action", function() use ($action, $object) {
91 // Check nonce
92 $nonce = null;
93 if(isset($_POST['nonce'])){
94 $nonce = $_POST['nonce'];
95 }
96 if(isset($_GET['nonce'])){
97 $nonce = $_GET['nonce'];
98 }
99 if (!$nonce || !wp_verify_nonce($nonce, 'templately_nonce')) {
100 wp_send_json_error(['message' => __('Invalid nonce', 'templately')]);
101 wp_die();
102 }
103
104 // Check user capability
105 if (!current_user_can('install_plugins') || !current_user_can('install_themes')) {
106 wp_send_json_error(['message' => __('Insufficient permissions', 'templately')]);
107 wp_die();
108 }
109
110 // Call the actual handler method
111 call_user_func([$this, $action]);
112 });
113 }
114
115 public function admin_init() {
116 if (get_option('templately_flush_rewrite_rules', false)) {
117 flush_rewrite_rules();
118 delete_option('templately_flush_rewrite_rules');
119 }
120 }
121
122 public function import_settings() {
123 $data = wp_unslash($_POST);
124
125 $upload_dir = wp_upload_dir();
126
127 if(!empty($data['session_id'])){
128 $session_id = $data['session_id'];
129 // Security: Sanitize session_id from user input
130 $session_id = AIUtils::sanitize_path_component($data['session_id'], 'session_id');
131 if (is_wp_error($session_id)) {
132 wp_send_json_error(['message' => $session_id->get_error_message()]);
133 return;
134 }
135 $session_data = SessionData::get_data($session_id);
136 $data = array_merge($session_data, $data);
137 }
138 else {
139 $session_id = uniqid();
140 }
141
142 $tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
143 $prv_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'preview' . DIRECTORY_SEPARATOR;
144
145 $this->session_id = $session_id;
146 $data['session_id'] = $session_id;
147
148 $data['root_dir'] = $tmp_dir;
149 $data['prv_dir'] = $prv_dir;
150 $data['dir_path'] = $tmp_dir . $session_id . DIRECTORY_SEPARATOR;
151 $data['zip_path'] = $tmp_dir . "{$session_id}.zip";
152
153
154 if ( is_array( $data ) && ! empty( $data ) ) {
155 foreach ( $data as $key => $value ) {
156 $json = is_string($value) ? json_decode( $value, true ) : null;
157 $data[ $key ] = $json !== null ? $json : $value;
158 }
159 }
160
161 SessionData::save($session_id, $data);
162
163
164 //clear previous revert backup
165 $options = Utils::get_backup_options();
166 foreach ($options as $key => $value) {
167 delete_option("__templately_$key");
168 }
169 delete_option('templately_fsi_imported_list');
170 delete_option('templately_fsi_log');
171
172 wp_send_json_success([
173 'is_lightspeed' => !Helper::should_flush(),
174 'session_id' => $session_id,
175 ]);
176 }
177
178 public function import_ai_settings() {
179 $data = wp_unslash($_POST);
180
181 $upload_dir = wp_upload_dir();
182
183 // Security: Sanitize session_id from user input
184 $session_id = AIUtils::sanitize_path_component($data['session_id'], 'session_id');
185 if (is_wp_error($session_id)) {
186 wp_send_json_error(['message' => $session_id->get_error_message()]);
187 return;
188 }
189
190 $tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
191 $prv_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'preview' . DIRECTORY_SEPARATOR;
192
193 $this->session_id = $session_id;
194 $data['root_dir'] = $tmp_dir;
195 $data['prv_dir'] = $prv_dir;
196 $data['dir_path'] = $tmp_dir . $session_id . DIRECTORY_SEPARATOR;
197 $data['zip_path'] = $tmp_dir . "{$session_id}.zip";
198
199 // Handle isLocalSite flag conversion
200 if (isset($data['isLocalSite'])) {
201 $data['isLocalSite'] = filter_var($data['isLocalSite'], FILTER_VALIDATE_BOOLEAN);
202 }
203
204 if ( is_array( $data ) && ! empty( $data ) ) {
205 foreach ( $data as $key => $value ) {
206 $json = is_string($value) ? json_decode( $value, true ) : null;
207 $data[ $key ] = $json !== null ? $json : $value;
208 }
209 }
210
211 SessionData::save($session_id, $data);
212
213
214 return $data;
215 }
216
217 public function create_session_and_download() {
218 if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
219 exit;
220 }
221
222 add_filter( 'wp_image_editors', [ $this, 'wp_image_editors' ], 10, 1 );
223
224 define('TEMPLATELY_START_TIME', microtime(true));
225
226 register_shutdown_function( [ $this, 'register_shutdown' ] );
227
228 // $this->finishRequestHeaders();
229
230 try {
231 // Get session data from AJAX request
232 $session_data = $this->import_ai_settings();
233
234 $this->request_params = $session_data;
235 $this->initialize_props();
236 $this->add_revert_hooks();
237 $progress = $this->request_params['progress'] ?? [];
238
239 if(empty($progress['create_log_dir'])){
240 // Create Log Directory and if fail then chose option method
241 LogHandler::create_log_dir();
242
243 SessionData::mark_step_complete($this->session_id, 'create_log_dir');
244 }
245
246 $_id = isset($this->request_params['id']) ? (int) $this->request_params['id'] : null;
247
248 if ($_id === null) {
249 $this->throw(__('Invalid Pack ID.', 'templately'));
250 }
251
252 $this->check_writing_permission();
253
254
255 if(empty($progress['download_zip'])){
256
257 /**
258 * Download the zip
259 */
260 $this->download_zip( $_id, true );
261
262 SessionData::mark_step_complete($this->session_id, 'download_zip');
263 }
264
265 /**
266 * Reading Manifest File
267 */
268 $this->manifest = $this->read_manifest($this->request_params['dir_path']);
269
270 /**
271 * Version Check
272 */
273 if ( ! empty( $this->manifest['version'] ) && version_compare( $this->manifest['version'], $this->version, '>' ) ) {
274 $this->throw( __( 'Please update the templately plugin.', 'templately' ) );
275 }
276
277 $platform = $this->manifest['platform'] ?? '';
278 if($platform === 'elementor') {
279 Helper::enable_elementor_container();
280 }
281
282 update_option('templately_import_platform', $platform);
283
284 // Return success response for AJAX
285 wp_send_json_success([
286 'session_id' => $this->session_id,
287 'pack_downloaded' => true,
288 'platform' => $platform,
289 'message' => __('Session created and pack downloaded successfully', 'templately')
290 ]);
291
292 } catch ( Exception $e ) {
293 $should_retry = $e instanceof RetryableErrorException;
294
295 wp_send_json_error([
296 'message' => $e->getMessage(),
297 'should_retry' => $should_retry
298 ]);
299 }
300 }
301
302 public function import_close_feedback_modal() {
303 $return = null;
304 if(isset($_GET['closeAction']) && $_GET['closeAction']){
305 $review_email = isset($_POST['review-email']) ? sanitize_email($_POST['review-email']) : '';
306 $pack_id = get_user_meta(get_current_user_id(), 'templately_fsi_pack_id', true);
307
308 // Prepare the body of the request
309 $body = json_encode([
310 'action' => $_GET['closeAction'],
311 'email' => $review_email,
312 'pack_id' => (int) $pack_id,
313 ]);
314
315 // Send the request to the API
316 $response = Helper::make_api_post_request('v2/feedback/close', json_decode($body, true), [], 30);
317 $body = wp_remote_retrieve_body($response);
318 $return = json_decode($body, true);
319 }
320 update_user_meta(get_current_user_id(), 'templately_fsi_complete', 'done');
321 wp_send_json_success($return);
322 }
323 public function feedback_form() {
324 // Get data from $_POST
325 $review_description = isset($_POST['review-description']) ? sanitize_textarea_field($_POST['review-description']) : '';
326 $review_email = isset($_POST['review-email']) ? sanitize_email($_POST['review-email']) : '';
327 $rating = isset($_POST['rating']) ? sanitize_text_field($_POST['rating']) : '';
328 $pack_id = get_user_meta(get_current_user_id(), 'templately_fsi_pack_id', true);
329
330 // Prepare the body of the request
331 $body = json_encode([
332 'description' => $review_description,
333 'email' => $review_email,
334 'rating' => (int) $rating,
335 'pack_id' => (int) $pack_id,
336 ]);
337
338 // Send the request to the API
339 $response = Helper::make_api_post_request('v2/feedback/store', json_decode($body, true), [], 30);
340
341 if (is_wp_error($response)) {
342 wp_send_json_error($response->get_error_message());
343 }
344
345 if (wp_remote_retrieve_response_code($response) != 200) {
346 $error_message = $this->extract_error_from_response($response);
347 wp_send_json_error($error_message, wp_remote_retrieve_response_code($response));
348 }
349
350 $body = wp_remote_retrieve_body($response);
351 $data = json_decode($body, true);
352
353 if (!isset($data['status']) || $data['status'] !== 'success') {
354 wp_send_json_error('API response indicates failure.');
355 }
356
357 if (!isset($data['message'])) {
358 wp_send_json_error('API response missing data.');
359 }
360
361 $result = $data['message'];
362
363 wp_send_json_success($result);
364 }
365
366 // Modified get_session_data to use SessionData
367 public function get_session_data() {
368 if ($session_id = SessionData::get_session_id()) {
369 return SessionData::get_data($session_id);
370 }
371 return [];
372 }
373
374 // Modified update_session_data to use SessionData
375 public function update_session_data($data) {
376 if ($session_id = SessionData::get_session_id()) {
377 return SessionData::save($session_id, array_merge($this->get_session_data(), $data));
378 }
379 return false;
380 }
381
382 public function initialize_props() {
383 $data = $this->get_session_data();
384 if (isset($data['session_id'])) {
385 $this->session_id = $data['session_id'];
386 }
387 if (isset($data['dir_path'])) {
388 $this->dir_path = $data['dir_path'];
389 }
390 if (isset($data['zip_path'])) {
391 $this->filePath = $data['zip_path'];
392 }
393 if (isset($data['download_key'])) {
394 $this->download_key = $data['download_key'];
395 }
396 if (isset($data['is_import_status_handled'])) {
397 $this->is_import_status_handled = $data['is_import_status_handled'];
398 }
399 }
400
401 public function clear_session_data(): bool {
402 return delete_site_option(self::SESSION_OPTION_KEY);
403 }
404
405 private function finishRequestHeaders() {
406 if(Helper::should_flush()) {
407 // Disable output buffering and compression
408 @ini_set('output_buffering', 'Off');
409 @ini_set('zlib.output_compression', 'Off');
410 @ini_set('implicit_flush', 1);
411
412 // Time to run the import! Set no limit
413 set_time_limit(0);
414
415
416 // Set headers to prevent caching and buffering
417 header('Content-Type: text/event-stream, charset=UTF-8');
418 header('Cache-Control: no-cache, must-revalidate');
419 header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
420 header('Connection: Keep-Alive');
421 header('Pragma: no-cache');
422
423 if (!empty($GLOBALS['is_nginx'])) {
424 header('X-Accel-Buffering: no');
425 header('Content-Encoding: none');
426 }
427
428 flush();
429 ob_flush();
430 wp_ob_end_flush_all();
431 } else {
432 header("Cache-Control: no-store, no-cache");
433 // header( 'Content-Type: text/event-stream, charset=UTF-8' );
434 // header( "Connection: Keep-Alive" );
435
436 // Ignore user aborts and allow the script to run forever
437 // (Use with caution, consider progress updates or timeouts)
438 ignore_user_abort(true);
439
440 // Time to run the import! Set no limit
441 set_time_limit(0);
442
443
444 if (!empty($GLOBALS['is_nginx'])) {
445 header('X-Accel-Buffering: no');
446 header('Content-Encoding: none');
447 }
448
449 // Send output as soon as possible during long-running process
450 if (function_exists('fastcgi_finish_request')) {
451 fastcgi_finish_request();
452 } elseif (function_exists('litespeed_finish_request')) {
453 litespeed_finish_request();
454 } else {
455 wp_ob_end_flush_all();
456 }
457 }
458 }
459
460 public function import() {
461 if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
462 exit;
463 }
464
465 add_filter( 'wp_image_editors', [ $this, 'wp_image_editors' ], 10, 1 );
466
467
468 define('TEMPLATELY_START_TIME', microtime(true));
469
470 // delete_option( 'templately_fsi_log' );
471
472 register_shutdown_function( [ $this, 'register_shutdown' ] );
473
474 $this->finishRequestHeaders();
475
476 try {
477 // TODO: Need to check if user is connected or not
478 if(!empty($_GET['session_id'])){
479 // Security: Sanitize session_id from user input
480 $session_id = AIUtils::sanitize_path_component(sanitize_text_field($_GET['session_id']), 'session_id');
481 if (is_wp_error($session_id)) {
482 $this->throw($session_id->get_error_message());
483 }
484 $this->session_id = $session_id;
485 }
486 else {
487 $this->throw(__('Invalid Session ID.', 'templately'));
488 }
489
490
491 $this->request_params = $this->get_session_data();
492 $this->initialize_props();
493 $this->add_revert_hooks();
494 $progress = $this->request_params['progress'] ?? [];
495
496 // Trigger action hook for network admin multisite handling
497 do_action( 'templately_fsi_before_import', $this, $this->request_params );
498
499 // Filter Child Type for Elementor Pro Promotion Widget
500 if ( class_exists( '\Elementor\Plugin' ) ) {
501 add_filter( 'elementor/element/get_child_type', [ '\Templately\Core\Platform\Elementor', 'filter_child_type' ], 10, 3 );
502 }
503
504 // Refresh progress after potential multisite creation
505 $progress = $this->request_params['progress'] ?? [];
506
507 if(empty($progress['create_log_dir'])){
508 // Create Log Directory and if fail then chose option method
509 LogHandler::create_log_dir();
510
511 SessionData::mark_step_complete($this->session_id, 'create_log_dir');
512 $this->sse_message( [
513 'type' => 'eventLog',
514 'action' => 'eventLog',
515 'info' => 'create_log_dir',
516 'results' => __METHOD__ . '::' . __LINE__,
517 ] );
518 }
519
520 $_id = isset($this->request_params['id']) ? (int) $this->request_params['id'] : null;
521
522 if ($_id === null) {
523 $this->throw(__('Invalid Pack ID.', 'templately'));
524 }
525
526 $this->sse_message( [
527 'type' => 'start',
528 'action' => 'eventLog',
529 'results' => __METHOD__ . '::' . __LINE__,
530 ] );
531
532 if(empty($progress['check_writing_permission'])){
533 /**
534 * Check Writing Permission
535 */
536 $this->check_writing_permission();
537
538 SessionData::mark_step_complete($this->session_id, 'check_writing_permission');
539 }
540
541 if(empty($progress['download_zip'])){
542
543 /**
544 * Download the zip
545 */
546 $this->download_zip( $_id );
547
548 SessionData::mark_step_complete($this->session_id, 'download_zip');
549 $this->sse_message( [
550 'type' => 'continue',
551 'action' => 'continue',
552 'info' => 'download_zip',
553 'results' => __METHOD__ . '::' . __LINE__,
554 ] );
555 exit;
556 }
557
558
559
560
561 /**
562 * Reading Manifest File
563 */
564 $this->manifest = $this->read_manifest($this->request_params['dir_path']);
565
566 /**
567 * Version Check
568 */
569 if ( ! empty( $this->manifest['version'] ) && version_compare( $this->manifest['version'], $this->version, '>' ) ) {
570 /**
571 * FIXME: The message should be re-written (by content/support team).
572 */
573 $this->throw( __( 'Please update the templately plugin.', 'templately' ) );
574 }
575
576 $platform = $this->manifest['platform'] ?? '';
577 if($platform === 'elementor') {
578 Helper::enable_elementor_container();
579 }
580
581
582
583 update_option('templately_import_platform', $platform);
584
585
586 /**
587 * Should Revert Old Data
588 */
589 // $this->revert();
590
591 /**
592 * Platform Based Templates Import
593 */
594 $this->start_content_import();
595
596 } catch ( Exception $e ) {
597 $should_retry = $e instanceof RetryableErrorException;
598 $this->handle_import_status('failed', $e->getMessage());
599
600 $this->sse_message([
601 'action' => 'error',
602 'status' => 'error',
603 'type' => "error",
604 'retry' => $should_retry,
605 'title' => __("Oops!", "templately"),
606 'message' => $e->getMessage(),
607 'trace' => $e->getTraceAsString(),
608 ]);
609 }
610
611 // if($_GET['part'] === 'import'){
612 // TODO: cleanup
613 // $this->clear_session_data();
614 // }
615 }
616
617
618 public function wp_image_editors( $editors ) {
619 // If GD is available, use only GD. Otherwise, fallback to all available editors.
620 if ( is_callable( [ 'WP_Image_Editor_GD', 'test' ] ) && call_user_func( [ 'WP_Image_Editor_GD', 'test' ] ) ) {
621 return [ 'WP_Image_Editor_GD' ];
622 }
623 return $editors;
624 }
625
626 // Updated import_status method
627 public function import_status() {
628 $request_params = $this->get_session_data();
629
630 if (isset($request_params['log_type']) && $request_params['log_type'] == 'file') {
631 $log_index = isset($_GET['lastLogIndex']) ? (int) $_GET['lastLogIndex'] : 0;
632 $log = LogHandler::read_log_file($log_index);
633
634 wp_send_json(['count' => count($log), 'log' => $log]);
635 } else {
636 $log = get_option('templately_fsi_log');
637
638 if (!empty($log) && is_array($log) && isset($_GET['lastLogIndex'])) {
639 $lastLogIndex = (int) $_GET['lastLogIndex'];
640 $log = array_slice($log, $lastLogIndex);
641 }
642 wp_send_json(['count' => $log ? count($log) : 0, 'log' => $log]);
643 }
644 }
645
646 /**
647 * @throws Exception
648 */
649 private function throw($message, $code = 0) {
650 if ($this->dev_mode) {
651 error_log(print_r($message, 1));
652 }
653 throw new Exception($message);
654 }
655 /**
656 * @throws Exception
657 */
658 private function throw_non_retryable($message, $code = 0) {
659 if ($this->dev_mode) {
660 error_log(print_r($message, 1));
661 }
662 throw new NonRetirableErrorException($message);
663 }
664 /**
665 * @throws Exception
666 */
667 private function throw_retryable($message, $code = 0) {
668 if ($this->dev_mode) {
669 error_log(print_r($message, 1));
670 }
671 throw new RetryableErrorException($message);
672 }
673 /**
674 * @throws Exception
675 */
676 private function throw_unknown($message, $code = 0) {
677 if ($this->dev_mode) {
678 error_log(print_r($message, 1));
679 }
680 throw new UnknownErrorException($message);
681 }
682
683 /**
684 * @throws Exception
685 */
686 private function check_writing_permission() {
687 $upload_dir = wp_upload_dir();
688
689 if (!is_writable($upload_dir['basedir'])) {
690 $this->throw(__('Upload directory is not writable.', 'templately'));
691 }
692
693 $this->tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
694
695 if (!is_dir($this->tmp_dir)) {
696 wp_mkdir_p($this->tmp_dir);
697 }
698
699 $this->sse_log('writing_permission_check', __('Permission Passed', 'templately'), 100);
700 }
701
702 /**
703 * @throws Exception
704 */
705 private function download_zip( $id, $is_ai = false ) {
706 $this->sse_log( 'download', __( 'Downloading Template Pack', 'templately' ), 1 );
707 $extra_headers = [
708 'x-templately-is-ai' => $is_ai,
709 'x-templately-session-id' => $this->session_id,
710 'x-templately-requested-platform' => $this->request_params["requested_platform"] ?? 'templately',
711 ];
712 $response = Helper::make_api_get_request("v2/import/pack/$id", [], $extra_headers, 90);
713
714 $response_code = wp_remote_retrieve_response_code($response);
715 $content_type = wp_remote_retrieve_header($response, 'content-type');
716 $this->download_key = wp_remote_retrieve_header($response, 'download-key');
717
718 if (is_wp_error($response)) {
719 $this->throw_retryable(__('Template pack download failed', 'templately') . $response->get_error_message());
720 } else if ($response_code != 200) {
721 if (strpos($content_type, 'application/json') !== false) {
722 // Retrieve Data from Response Body.
723 $response_body = json_decode(wp_remote_retrieve_body($response), true);
724
725 // If the response body is JSON and it contains an error, throw an exception with the error message
726 if (isset($response_body['status']) && $response_body['status'] === 'error') {
727 $support_message = '';
728 if(strpos($response_body['message'], 'https://wpdeveloper.com/support') === false){
729 $support_message = sprintf(__(" Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://wpdeveloper.com/support');
730 }
731 $this->throw_non_retryable($response_body['message'] . $support_message);
732 }
733 }
734 $this->throw_unknown(__('Template pack download failed with response code: ', 'templately') . $response_code);
735 }
736
737 $this->sse_log('download', __('Downloading Template Pack', 'templately'), 57);
738
739 SessionData::set($this->session_id, 'download_key', $this->download_key);
740
741 // Security: Validate file path is within WordPress upload directory before writing
742 $validation = AIUtils::validate_file_path($this->filePath);
743 if (is_wp_error($validation)) {
744 $this->throw($validation->get_error_message());
745 }
746
747 // Security: Validate file path is within WordPress upload directory before writing
748 $validation = AIUtils::validate_file_path($this->filePath);
749 if (is_wp_error($validation)) {
750 $this->throw($validation->get_error_message());
751 }
752
753 wp_mkdir_p(dirname($this->filePath));
754
755 if (file_put_contents($this->filePath, $response['body'])) { // phpcs:ignore
756 $this->sse_log('download', __('Downloading Template Pack', 'templately'), 100);
757
758 $this->unzip();
759 } else {
760 $this->throw_retryable(__('Downloading Failed. Please try again', 'templately'));
761 }
762 }
763
764 /**
765 * @throws Exception
766 */
767 protected function unzip() {
768 if (!WP_Filesystem()) {
769 $this->throw(__('WP_Filesystem cannot be initialized', 'templately'));
770 }
771 $unzip = unzip_file($this->filePath, $this->dir_path);
772 if (is_wp_error($unzip)) {
773 $unzip = $this->unzip_file($this->filePath, $this->dir_path);
774 }
775
776 $manifest_file = $this->dir_path . 'manifest.json';
777
778 // If manifest.json is missing, but any subdirectory contains manifest.json, move all its contents up and remove the subdirectory.
779 if ( ! file_exists( $manifest_file ) ) {
780 $entries = array_diff( scandir( $this->dir_path ), [ '.', '..' ] );
781 $dirs = array_filter( $entries, fn($e) => is_dir( $this->dir_path . $e ) );
782 $files = array_filter( $entries, fn($e) => is_file( $this->dir_path . $e ) );
783 foreach ($dirs as $subdir) {
784 $subdir_path = $this->dir_path . $subdir . DIRECTORY_SEPARATOR;
785 if ( file_exists( $subdir_path . 'manifest.json' ) ) {
786 copy($subdir_path . 'manifest.json', $manifest_file);
787
788 foreach ( array_diff( scandir( $subdir_path ), [ '.', '..' ] ) as $item ) {
789 $src = $subdir_path . $item;
790 $dst = $this->dir_path . $item;
791 if (is_dir($src)) {
792 if (!file_exists($dst)) {
793 wp_mkdir_p($dst);
794 }
795 // Recursively copy directory
796 $this->copyDirectory($src, $dst);
797 } else {
798 copy($src, $dst);
799 }
800 }
801 // Remove the subdirectory and its contents
802 $this->removeDirectory($subdir_path);
803 break; // Only process the first subdir with manifest.json
804 }
805 }
806 }
807
808 if (is_wp_error($unzip)) {
809 $error = $unzip->get_error_message();
810 if (empty($error)) {
811 // Generic error message
812 Helper::log($unzip);
813 $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');
814 $this->throw($error_message);
815 } else {
816 $this->throw($unzip->get_error_message());
817 }
818 }
819
820 if ($unzip) {
821 unlink($this->filePath);
822 }
823 }
824
825 /**
826 * Recursively copy a directory
827 */
828 private function copyDirectory($src, $dst) {
829 $dir = opendir($src);
830 wp_mkdir_p($dst);
831 while(false !== ($file = readdir($dir))) {
832 if (($file != '.') && ($file != '..')) {
833 if (is_dir($src . DIRECTORY_SEPARATOR . $file)) {
834 $this->copyDirectory($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
835 } else {
836 copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
837 }
838 }
839 }
840 closedir($dir);
841 }
842
843 /**
844 * Recursively remove a directory
845 */
846 private function removeDirectory($dir) {
847 if (!file_exists($dir)) return;
848 $items = array_diff(scandir($dir), ['.', '..']);
849 foreach ($items as $item) {
850 $path = $dir . DIRECTORY_SEPARATOR . $item;
851 if (is_dir($path)) {
852 $this->removeDirectory($path);
853 } else {
854 unlink($path);
855 }
856 }
857 rmdir($dir);
858 }
859
860
861
862 /**
863 * Unzip a specified ZIP file to a location on the Filesystem.
864 *
865 * @param string $file Full path and filename of ZIP archive.
866 * @param string $to Full path on the filesystem to extract archive to.
867 * @return true|WP_Error True on success, WP_Error on failure.
868 */
869 function unzip_file($file, $to) {
870 try {
871 $zip = new \ZipArchive;
872
873 $res = $zip->open($file);
874 if ($res === TRUE) {
875 $zip->extractTo($to);
876 $zip->close();
877
878 return true;
879 }
880 } catch (\Throwable $th) {
881 return new \WP_Error('exception_caught', $th->getMessage());
882 }
883
884 if (isset($zip)) {
885 return new \WP_Error('zip_error_' . $zip->status, $zip->getStatusString());
886 } else {
887 return new \WP_Error('unknown_error', '');
888 }
889 }
890
891 /**
892 * @throws Exception
893 */
894 private function read_manifest($dir_path) {
895 $manifest_content = file_get_contents($dir_path . 'manifest.json');
896 if (empty($manifest_content)) {
897 $this->throw(__('Cannot be imported, as the manifest file is corrupted', 'templately'));
898 }
899
900 $manifest_content = json_decode($manifest_content, true);
901 $this->removeLog('temp');
902
903 return $manifest_content;
904 // TODO: Read & Broadcast the LOG for waiting list
905 // $this->sse_log( 'plugin', 'Installing required plugins', '--', 'updateLog', 'processing' );
906 // // $this->sse_log( 'extra-content', 'Import Extra Contents (i.e: Forms)', '--', 'updateLog', 'processing' );
907 // $this->sse_log( 'templates', 'Import Templates (i.e: Header, Footer etc)', '--', 'updateLog', 'processing' );
908 // // $this->sse_log( 'content', 'Import Pages, Posts etc', '--', 'updateLog', 'processing' );
909 // $this->sse_log( 'wp-content', 'Importing Pages, Posts, Navigation, etc', '--', 'updateLog', 'processing' );
910 // $this->sse_log( 'finalize', 'Finalizing Your Imports', '--', 'updateLog', 'processing' );
911 }
912
913 private function skipped_plugin(): bool {
914 return empty($this->request_params['plugins']) || !is_array($this->request_params['plugins']);
915 }
916
917
918 private function before_install_hook() {
919 // remove_all_actions( 'wp_loaded' );
920 // remove_all_actions( 'after_setup_theme' );
921 // remove_all_actions( 'plugins_loaded' );
922 // remove_all_actions( 'init' );
923
924 // making sure so that no redirection happens during plugin installation and hooks triggered bellow.
925 add_filter('wp_redirect', '__return_false', 999);
926 }
927
928 private function after_install_hook() {
929 // do_action( 'wp_loaded' );
930 // do_action( 'after_setup_theme' );
931 // do_action( 'plugins_loaded' );
932 // do_action( 'init' );
933 }
934
935 /**
936 * @throws Exception
937 */
938 private function start_content_import() {
939 add_filter('upload_mimes', array($this, 'allow_svg_upload'));
940 add_filter('elementor/files/allow_unfiltered_upload', '__return_true');
941
942 $request_params = $this->get_session_data();
943
944 $import = new Import(array_merge($request_params, [
945 'origin' => $this,
946 'manifest' => $this->manifest,
947 ]));
948 $imported_data = $import->run();
949
950 $import_status = $this->handle_import_status('success');
951
952 update_option('templately_flush_rewrite_rules', true, false);
953
954 $normalized_data = $this->normalize_imported_data($imported_data);
955 // Use timeout-aware wait handler for AI content processing
956 if(!empty($request_params['ai_page_ids']) && empty($normalized_data['ai_content']['processed']['credit_cost'])){
957 $processed_pages = get_option("templately_ai_processed_pages", []);
958 $updated_ids = $processed_pages[$request_params['process_id']] ?? [];
959
960 // Use the static timeout-aware wait handler from AIUtils
961 AIUtils::handle_sse_wait_with_timeout(
962 $this->session_id,
963 'ai_content_import_time',
964 $updated_ids,
965 $request_params['ai_page_ids'],
966 [$this, 'sse_message'],
967 [
968 'name' => 'ai-content',
969 'message' => __('Missing Credit Cost', 'templately'),
970 ],
971 null, // No specific template ID for this context
972 30
973 );
974 }
975
976 // Get skipped items if skip feature was enabled
977 $skipped_items = SessionData::get_skipped_items($this->session_id);
978 if (!empty($skipped_items)) {
979 $normalized_data['skipped_items'] = $skipped_items;
980 }
981
982 $this->sse_message([
983 'type' => 'complete',
984 'action' => 'complete',
985 'results' => $normalized_data,
986 ]);
987
988 update_user_meta(get_current_user_id(), 'templately_fsi_pack_id', $request_params["id"]);
989 if(!empty($import_status['hasFeedback'])){
990 update_user_meta(get_current_user_id(), 'templately_fsi_complete', 'done');
991 }
992 else{
993 update_user_meta(get_current_user_id(), 'templately_fsi_complete', true);
994 }
995
996 do_action('templately_fsi_import_complete', $normalized_data);
997 $this->clear_data_file($request_params);
998 }
999
1000 private function clear_data_file($request_params){
1001 if(defined('TEMPLATELY_DEV') && TEMPLATELY_DEV){
1002 return;
1003 }
1004
1005 // Handle directory cleanup
1006 Utils::cleanup_directory($this->dir_path);
1007 $upload_dir = wp_upload_dir();
1008
1009 // Always save to preview directory for AI content workflow
1010 $session_id = $request_params['session_id'] ?? '';
1011 $pack_id = $request_params['id'] ?? '';
1012
1013 // Set up directory paths for cleanup
1014 $root_dir = $request_params['root_dir'] ?? trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp';
1015 $prv_dir = $request_params['prv_dir'] ?? trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'preview';
1016
1017 $processed_data = AIUtils::get_ai_process_data_by_session_id($session_id);
1018
1019 if (!empty($pack_id) && !empty($session_id)) {
1020 // Clean session data - keep only current session, remove others with same pack_id
1021 // Modified to use SessionData directly
1022 $removed_session_ids = SessionData::clean_by_pack_id($pack_id, $session_id);
1023 }
1024
1025 // Also run general expiration cleanup (7 days default)
1026 // Modified to use SessionData directly
1027 if (mt_rand(1, 100) <= 5) { // 5% chance to run on any import finish
1028 $expired_cleanup = SessionData::cleanup_expired(7);
1029
1030 // Clean AI process data - keep only current process, remove others with same pack_id
1031 $current_process_id = !empty($processed_data['process_id']) ? $processed_data['process_id'] : null;
1032 $removed_process_ids = AIUtils::clean_ai_process_data_by_pack_id($pack_id, $current_process_id);
1033
1034 // Directory-based cleanup for session data directories
1035 $this->cleanup_session_directories($root_dir, $pack_id, $session_id);
1036
1037 // Directory-based cleanup for AI process data directories
1038 $this->cleanup_ai_process_directories($prv_dir, $pack_id, $current_process_id);
1039
1040 // Log cleanup results if in dev mode
1041 if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1042 if (!empty($removed_session_ids)) {
1043 error_log('Templately: Cleaned up session IDs: ' . implode(', ', $removed_session_ids));
1044 }
1045 if (!empty($removed_process_ids)) {
1046 error_log('Templately: Cleaned up process IDs: ' . implode(', ', $removed_process_ids));
1047 }
1048 }
1049 }
1050 }
1051
1052 /**
1053 * Directory-based cleanup for session data directories
1054 * Scans the actual filesystem directories and removes directories that match cleanup criteria
1055 *
1056 * @param string $root_dir The root directory containing session directories
1057 * @param string $pack_id The pack ID to match for cleanup
1058 * @param string $current_session_id The current session ID to preserve
1059 */
1060 private function cleanup_session_directories($root_dir, $pack_id, $current_session_id) {
1061 if (empty($root_dir) || !is_dir($root_dir) || empty($pack_id) || empty($current_session_id)) {
1062 return;
1063 }
1064
1065 try {
1066 // Get all session data to check pack_id associations
1067 $all_session_data = SessionData::get_all_data();
1068
1069 // Scan the actual directories in the filesystem
1070 $directories = scandir($root_dir);
1071 if ($directories === false) {
1072 return;
1073 }
1074
1075 foreach ($directories as $dir_name) {
1076 // Skip current directory, parent directory, and current session
1077 if ($dir_name === '.' || $dir_name === '..' || $dir_name === $current_session_id) {
1078 continue;
1079 }
1080
1081 $dir_path = trailingslashit($root_dir) . $dir_name;
1082
1083 // Only process actual directories
1084 if (!is_dir($dir_path)) {
1085 continue;
1086 }
1087
1088 // Check if this directory should be cleaned up
1089 $should_cleanup = false;
1090
1091 // If we have session data for this directory, check if it matches the pack_id
1092 if (isset($all_session_data[$dir_name]) &&
1093 isset($all_session_data[$dir_name]['id']) &&
1094 $all_session_data[$dir_name]['id'] === $pack_id) {
1095 $should_cleanup = true;
1096 } else if (!isset($all_session_data[$dir_name])) {
1097 // This is an orphaned directory with no corresponding session data
1098 $should_cleanup = true;
1099 }
1100
1101 if ($should_cleanup) {
1102 Utils::cleanup_directory($dir_path);
1103
1104 if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1105 error_log('Templately: Cleaned up session directory: ' . $dir_name);
1106 }
1107 }
1108 }
1109 } catch (Exception $e) {
1110 if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1111 error_log('Templately: Error during session directory cleanup: ' . $e->getMessage());
1112 }
1113 }
1114 }
1115
1116 /**
1117 * Directory-based cleanup for AI process data directories
1118 * Scans the actual filesystem directories and removes directories that match cleanup criteria
1119 *
1120 * @param string $prv_dir The preview directory containing process directories
1121 * @param string $pack_id The pack ID to match for cleanup
1122 * @param string $current_process_id The current process ID to preserve (optional)
1123 */
1124 private function cleanup_ai_process_directories($prv_dir, $pack_id, $current_process_id = null) {
1125 if (empty($prv_dir) || !is_dir($prv_dir) || empty($pack_id)) {
1126 return;
1127 }
1128
1129 try {
1130 // Get all AI process data to check pack_id associations
1131 $ai_process_data = AIUtils::get_ai_process_data();
1132
1133 // Scan the actual directories in the filesystem
1134 $directories = scandir($prv_dir);
1135 if ($directories === false) {
1136 return;
1137 }
1138
1139 foreach ($directories as $dir_name) {
1140 // Skip current directory, parent directory, and current process
1141 if ($dir_name === '.' || $dir_name === '..' ||
1142 (!empty($current_process_id) && $dir_name === $current_process_id)) {
1143 continue;
1144 }
1145
1146 $dir_path = trailingslashit($prv_dir) . $dir_name;
1147
1148 // Only process actual directories
1149 if (!is_dir($dir_path)) {
1150 continue;
1151 }
1152
1153 // Check if this directory should be cleaned up
1154 $should_cleanup = false;
1155
1156 // If we have process data for this directory, check if it matches the pack_id
1157 if (isset($ai_process_data[$dir_name]) &&
1158 is_array($ai_process_data[$dir_name]) &&
1159 isset($ai_process_data[$dir_name]['pack_id']) &&
1160 $ai_process_data[$dir_name]['pack_id'] === $pack_id) {
1161 $should_cleanup = true;
1162 } else if (!isset($ai_process_data[$dir_name])) {
1163 // This is an orphaned directory with no corresponding process data
1164 $should_cleanup = true;
1165 }
1166
1167 if ($should_cleanup) {
1168 Utils::cleanup_directory($dir_path);
1169
1170 if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1171 error_log('Templately: Cleaned up AI process directory: ' . $dir_name);
1172 }
1173 }
1174 }
1175 } catch (Exception $e) {
1176 if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1177 error_log('Templately: Error during AI process directory cleanup: ' . $e->getMessage());
1178 }
1179 }
1180 }
1181
1182 private function normalize_imported_data($data) {
1183 $request_params = $this->get_session_data();
1184 $attachments = !empty($data['attachments']['succeed']) ? count($data['attachments']['succeed']) : 0;
1185 $attachments_fail = !empty($data['attachments']['failed']) ? count($data['attachments']['failed']) : 0;
1186 $attachments_errors = !empty($data['attachments_errors']) ? $data['attachments_errors'] : [];
1187 $templates = !empty($data['templates']['succeed']) ? count($data['templates']['succeed']) : 0;
1188 $template_types = !empty($data['templates']['template_types']) ? $data['templates']['template_types'] : [];
1189 $dependency_data = !empty($data['dependency_data']) ? $data['dependency_data'] : [];
1190
1191 $post_types = [];
1192 $content_templates = [];
1193 if (!empty($data['content']) && is_array($data['content'])) {
1194 foreach ($data['content'] as $type => $type_data) {
1195 $content_templates[$type] = !empty($type_data['succeed']) ? count($type_data['succeed']) : 0;
1196 $post_types[] = $this->get_post_type_label_by_slug($type);
1197 }
1198 }
1199
1200 $contents = [];
1201 if (!empty($data['wp-content']) && is_array($data['wp-content'])) {
1202 foreach ($data['wp-content'] as $type => $type_data) {
1203 $contents[$type] = !empty($type_data['succeed']) ? count($type_data['succeed']) : 0;
1204 if (!in_array($type, ['wp_navigation', 'nav_menu_item'])) {
1205 $post_types[] = $this->get_post_type_label_by_slug($type);
1206 }
1207 }
1208 }
1209
1210 $_processed_pages = AIUtils::get_processed_pages_data($request_params['process_id']);
1211 $ai_content = [
1212 'requested' => $request_params['ai_page_ids'] ?? [],
1213 'processed' => $_processed_pages,
1214 ];
1215
1216
1217 $result = [
1218 'attachments' => $attachments,
1219 'attachments_fail' => $attachments_fail,
1220 'attachments_errors' => $attachments_errors,
1221 'templates' => $templates,
1222 'contents' => $content_templates,
1223 'wp-content' => $contents,
1224 'post_types' => $post_types,
1225 'template_types' => $template_types,
1226 'ai_content' => $ai_content,
1227 'dependency_data' => $dependency_data,
1228 'home_url' => home_url('/'),
1229 ];
1230
1231 Helper::log($data);
1232 Helper::log($result);
1233
1234 return $result;
1235 }
1236
1237 public function get_request_params() {
1238 return $this->request_params;
1239 }
1240
1241 private function revert() {
1242 // $request = $this->get_request_params();
1243 // if ( isset( $request['revert'] ) && $request['revert'] ) {
1244 // // TODO: Implement the Revert Process.
1245 // }
1246 }
1247
1248 public function redirect_for_archives($link, $post_id) {
1249 $archive_settings = get_option('templately_post_archive');
1250 if (!empty($archive_settings) && intval($archive_settings['post_id']) === intval($post_id)) {
1251 $link = str_replace($post_id, $archive_settings['archive_id'], $link);
1252 }
1253
1254 return $link;
1255 }
1256
1257 public function allow_svg_upload($mimes) {
1258 // Allow SVG
1259 $mimes['svg'] = 'image/svg+xml';
1260 return $mimes;
1261 }
1262
1263 public function register_shutdown() {
1264 $status = connection_status();
1265 $last_error = error_get_last();
1266 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)) {
1267 if (!empty($last_error['message'])) {
1268 $full_message = $last_error['message'];
1269 $lines = explode("\n", $full_message);
1270
1271 // For import status: first 5 lines
1272 $import_status_message = implode("\n", array_slice($lines, 0, 5));
1273 $import_status_message = str_replace(ABSPATH, 'ABSPATH/', $import_status_message);
1274
1275 // For SSE: first line only
1276 $sse_message = $lines[0];
1277 $sse_message = str_replace(ABSPATH, 'ABSPATH/', $sse_message);
1278 } else {
1279 // Generic error message
1280 $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://wpdeveloper.com/support');
1281 $sse_message = $import_status_message;
1282 }
1283
1284 $this->handle_import_status('failed', $import_status_message);
1285 $this->sse_message([
1286 'action' => 'error',
1287 'status' => 'error',
1288 'type' => "error",
1289 'retry' => true,
1290 'title' => __("Oops!", "templately"),
1291 'message' => $sse_message,
1292 'error' => $last_error,
1293 // 'position' => 'plugin',
1294 // 'progress' => '--',
1295 ]);
1296 }
1297
1298 $this->debug_log("Shutdown:.....");
1299 $this->debug_log("connection_status: " . $this->getConnectionStatusText());
1300 $this->debug_log($last_error);
1301 }
1302
1303 public function handle_import_status($status, $description = '') {
1304 if ($this->is_import_status_handled === $status) {
1305 Helper::log("Import status already handled: $status");
1306 return null;
1307 }
1308 $this->is_import_status_handled = $status;
1309
1310 $download_key = $this->download_key;
1311
1312 $headers = [
1313 'Content-Type' => 'application/json',
1314 'Authorization' => 'Bearer ' . $this->api_key,
1315 'download_key' => $download_key,
1316 'download-key' => $download_key,
1317 'x-templately-ip' => Helper::get_ip(),
1318 'x-templately-url' => home_url('/'),
1319 ];
1320
1321
1322 $request_params = $this->get_session_data();
1323 if(isset($request_params['process_id']) && !empty($request_params['ai_page_ids'])){
1324 $updated_ids = AIUtils::get_processed_pages_data($request_params['process_id']);
1325 $updated_pages = $updated_ids['pages'] ?? [];
1326 $ai_page_ids = array_reduce($request_params['ai_page_ids'], 'array_merge', array());
1327
1328 $headers['x-templately-ai-process-id'] = $request_params['process_id'];
1329 $headers['x-templately-ai-requested-pages'] = implode(',', $ai_page_ids);
1330 $headers['x-templately-ai-updated-pages'] = implode(',', array_keys($updated_pages));
1331 $headers['x-templately-ai-missing-pages'] = implode(',', array_diff($ai_page_ids, array_keys($updated_pages)));
1332 $headers['x-templately-ai-credit-cost'] = $updated_ids['credit_cost'] ?? null;
1333 }
1334
1335
1336 $extra_headers = $headers;
1337
1338 if ($status === 'success') {
1339 $body = ['type' => 'pack'];
1340 $response = Helper::make_api_post_request('v1/import/success', $body, $extra_headers);
1341 } elseif ($status === 'failed') {
1342 $body = ['type' => 'pack', 'description' => $description ?: "Something Went wrong....."];
1343 $response = Helper::make_api_post_request('v1/import/failed', $body, $extra_headers);
1344 }
1345
1346 Helper::log($response);
1347
1348 if (is_wp_error($response)) {
1349 // Handle error
1350 Helper::log($response->get_error_message());
1351 } else {
1352
1353 SessionData::set($this->session_id, 'is_import_status_handled', $this->is_import_status_handled);
1354 // Handle success
1355 $body = wp_remote_retrieve_body($response);
1356 $data = json_decode($body, true);
1357 // Do something with $body
1358 return $data;
1359 }
1360
1361 return null;
1362 }
1363
1364 protected function getConnectionStatusText() {
1365 $status = connection_status();
1366 switch ($status) {
1367 case CONNECTION_NORMAL:
1368 return "Normal";
1369 case CONNECTION_ABORTED:
1370 return "Aborted";
1371 case CONNECTION_TIMEOUT:
1372 return "Timeout";
1373 default:
1374 return "Unknown";
1375 }
1376 }
1377
1378 protected function get_post_type_label_by_slug($slug) {
1379 $post_type_obj = get_post_type_object($slug);
1380 if ($post_type_obj) {
1381 return $post_type_obj->label;
1382 }
1383 return null;
1384 }
1385
1386 public function import_info() {
1387
1388 $platform = isset($_GET['platform']) ? $_GET['platform'] : 'elementor';
1389 $id = isset($_GET['id']) ? intval($_GET['id']) : 0;
1390 $isAi = isset($_GET['isAi']) ? $_GET['isAi'] : false;
1391
1392 $extra_headers = [
1393 'x-templately-is-ai' => $isAi,
1394 ];
1395 $response = Helper::make_api_get_request("v2/import/info/pack/$id", [], $extra_headers, 30);
1396
1397 if (is_wp_error($response)) {
1398 wp_send_json_error($response->get_error_message());
1399 return;
1400 }
1401 // If the response code is not 200, return the error message
1402 if (wp_remote_retrieve_response_code($response) != 200) {
1403 $error_message = $this->extract_error_from_response($response);
1404 wp_send_json_error($error_message, wp_remote_retrieve_response_code($response));
1405 return;
1406 }
1407 // If the response body is JSON and it contains an error, return the error message
1408 // Retrieve Data from Response Body.
1409 $body = wp_remote_retrieve_body($response);
1410 $data = json_decode($body, true);
1411
1412 if (isset($data['error'])) {
1413 wp_send_json_error($data['error']);
1414 return;
1415 }
1416
1417 $business_niches = get_option('templately_ai_business_niches', []);
1418 $data['data']['business_niches'] = $business_niches;
1419
1420 if (isset($data['data']['manifest'])) {
1421 $data['data']['manifest'] = json_decode($data['data']['manifest'], true);
1422 }
1423 if (isset($data['data']['settings'])) {
1424 $data['data']['settings'] = json_decode($data['data']['settings'], true);
1425 }
1426
1427 if ($isAi) {
1428 // Get the latest AI process for the current API key
1429 $last_ai_process = AIUtils::get_latest_ai_process_by_api_key($id);
1430 if ($last_ai_process) {
1431 $data['data']['ai_process'] = $last_ai_process;
1432 }
1433
1434 if($last_ai_process && $id == $last_ai_process['pack_id']){
1435 // Read AI preview content directly from files using the common function
1436 $session_id = $last_ai_process['session_id'] ?? null;
1437 $ai_page_ids = $last_ai_process['ai_page_ids'] ?? [];
1438 $dir_path = null;
1439
1440 // Get session data to retrieve dir_path
1441 if ($session_id) {
1442 $session_data = SessionData::get_data($session_id);
1443 $dir_path = $session_data['dir_path'] ?? null;
1444 }
1445
1446 // Use the common function to read AI template data if we have the required data
1447 if ($session_id && $ai_page_ids && $dir_path) {
1448 $data['data']['ai_preview_content'] = AIUtils::read_ai_template_data($session_id, $ai_page_ids, $dir_path);
1449 } else {
1450 $data['data']['ai_preview_content'] = [];
1451 }
1452 }
1453 }
1454
1455 // Return the response body
1456 wp_send_json($data);
1457 }
1458
1459 public function update_imported_list($type, $id) {
1460 $imported_list = get_option('templately_fsi_imported_list', []);
1461 if(!in_array($id, $imported_list[$type] ?? [])){
1462 $imported_list[$type][] = $id;
1463 update_option('templately_fsi_imported_list', $imported_list, false);
1464 }
1465 }
1466
1467 /**
1468 *
1469 *
1470 * @return void
1471 */
1472 protected function add_revert_hooks() {
1473 add_action('wp_insert_post', function ($post_id) {
1474 $this->update_imported_list('posts', $post_id);
1475 });
1476 add_action('add_attachment', function ($post_id) {
1477 $this->update_imported_list('attachment', $post_id);
1478 });
1479 add_action('created_term', function ($term_id, $tt_id, $taxonomy, $args) {
1480 $this->update_imported_list('term', [$term_id, $taxonomy]);
1481 }, 10, 4);
1482 add_action('registered_taxonomy', function ($taxonomy, $object_type, $taxonomy_object) {
1483 $this->update_imported_list('taxonomy', $taxonomy);
1484 }, 10, 3);
1485 add_action('fluentform/form_imported', function ($formId){
1486 $this->update_imported_list('fluentform', $formId);
1487 }, 10, 1);
1488 }
1489
1490 public static function has_revert(){
1491 $options = Utils::get_backup_options();
1492 $imported_list = get_option('templately_fsi_imported_list', []);
1493 if(!empty($options) || !empty($imported_list)){
1494 return true;
1495 }
1496 return false;
1497 }
1498
1499 public function import_revert() {
1500
1501 // // Get the nonce value from the request (usually from $_POST or $_GET)
1502 // $received_nonce = isset($_REQUEST['_wpnonce']) ? $_REQUEST['_wpnonce'] : '';
1503
1504 // // Verify the nonce using wp_verify_nonce()
1505 // $verified = wp_verify_nonce($received_nonce, 'templately_pack_import_revert_nonce');
1506
1507 // if (!$verified) {
1508 // wp_send_json_error("Nonce not verified.");
1509 // }
1510
1511 delete_option('templately_import_platform');
1512
1513 $option_active = null;
1514 $options_deleted = false;
1515 $imported_list_deleted = false;
1516 $options = Utils::get_backup_options();
1517 $status_args = [ 'post_type' => 'templately_library' ];
1518 $all_post_url = add_query_arg( [
1519 "page" => "templately_settings",
1520 "path" => "settings/elementor/miscellaneous",
1521 ], admin_url('admin.php' ));
1522 // wp_send_json_success([$options]);
1523
1524 if(class_exists('Elementor\Plugin')){
1525 $kits_manager = Plugin::$instance->kits_manager;
1526 $option_active = $kits_manager::OPTION_ACTIVE;
1527 $kit = $kits_manager->get_active_kit();
1528
1529 if ( ! $kit->get_id() ) {
1530 $kit = $kits_manager->create_default();
1531 update_option( $kits_manager::OPTION_ACTIVE, $kit );
1532 }
1533 }
1534
1535
1536 if (!empty($options) && is_array($options)) {
1537 foreach ($options as $key => $value) {
1538 if ('stylesheet' === $key) {
1539 if (get_option('stylesheet') !== $value) {
1540 switch_theme($value);
1541 }
1542 } else if($option_active === $key && class_exists('Elementor\Plugin')) {
1543 $kits_manager->revert( (int) $kits_manager->get_active_id(), (int) $value, 0 );
1544 $kit = $kits_manager->get_active_kit();
1545 $settings = $kit->get_data('settings');
1546 if ( isset( $settings['site_logo'] ) ) {
1547 set_theme_mod( 'custom_logo', $settings['site_logo']['id'] );
1548 }
1549 } else {
1550 update_option($key, $value);
1551 }
1552 delete_option("__templately_$key");
1553 $options_deleted = true;
1554 }
1555 }
1556
1557 $imported_list = get_option('templately_fsi_imported_list', []);
1558 if (!empty($imported_list) && is_array($imported_list)) {
1559 $_GET['force_delete_kit'] = 1; // Fallback GET Ready!
1560 foreach ($imported_list as $type => $list) {
1561 if (empty($list) || !is_array($list)) {
1562 continue;
1563 }
1564 // Loop through each item ID and delete it
1565 foreach ($list as $key => $item_id) {
1566 switch ($type) {
1567 case 'posts':
1568 // making sure default kit don't get deleted.
1569 if($option_active && isset($options[$option_active]) && $options[$option_active] == $item_id){
1570 break;
1571 }
1572 wp_delete_post($item_id, true); // Set true for permanent deletion
1573 break;
1574 case 'attachment':
1575 wp_delete_attachment($item_id, true); // Set true for permanent deletion
1576 break;
1577 case 'term':
1578 list($term_id, $taxonomy) = $item_id;
1579 wp_delete_term($term_id, $taxonomy); // Use corresponding taxonomy
1580 break;
1581 case 'taxonomy':
1582 // Taxonomies cannot be directly deleted. Consider de-registering it.
1583 break;
1584 case 'fluentform':
1585 if(class_exists('\FluentForm\App\Models\Form')){
1586 \FluentForm\App\Models\Form::remove($item_id);
1587 }
1588 break;
1589 }
1590 }
1591 }
1592
1593 $imported_list_deleted = true;
1594 delete_option('templately_fsi_imported_list');
1595 }
1596
1597
1598 if($options_deleted || $imported_list_deleted){
1599 sleep(5);
1600 wp_send_json_success([ 'options' => $options_deleted, 'imported_list' => $imported_list_deleted, 'site_url' => home_url(), 'redirect' => $all_post_url ]);
1601 }
1602
1603 wp_send_json_error([ 'options' => $options_deleted, 'imported_list' => $imported_list_deleted, 'site_url' => home_url() ]);
1604 }
1605
1606 /**
1607 * Extract error response from JSON response body
1608 * Returns the entire decoded JSON response if Content-Type is JSON,
1609 * otherwise returns a generic HTTP error message
1610 *
1611 * @param array $response The response from wp_remote_get or similar
1612 * @return mixed Decoded JSON response or generic HTTP error string
1613 */
1614 private function extract_error_from_response($response) {
1615 try {
1616 $content_type = wp_remote_retrieve_header($response, 'content-type');
1617 if (!empty($content_type) && strpos($content_type, 'application/json') !== false) {
1618 $body = wp_remote_retrieve_body($response);
1619 $error_data = json_decode($body, true);
1620 if ($error_data !== null) {
1621 return $error_data;
1622 }
1623 }
1624 } catch (Exception $e) {
1625 // If JSON parsing fails, fall through to generic error
1626 }
1627
1628 // Fallback to generic HTTP error message
1629 return __('API request failed with response code ', 'templately') . wp_remote_retrieve_response_code($response);
1630 }
1631
1632 public function google_font() {
1633 $result = get_transient('templately-google-fonts');
1634
1635 if (false == $result) {
1636 $response = Helper::make_api_get_request('v2/google-font', [], [], 30);
1637
1638 if (is_wp_error($response)) {
1639 wp_send_json_error($response->get_error_message());
1640 }
1641
1642 if (wp_remote_retrieve_response_code($response) != 200) {
1643 $error_message = $this->extract_error_from_response($response);
1644 wp_send_json_error($error_message, wp_remote_retrieve_response_code($response));
1645 }
1646
1647 $body = wp_remote_retrieve_body($response);
1648 $data = json_decode($body, true);
1649
1650 if (!isset($data['status']) || $data['status'] !== 'success') {
1651 wp_send_json_error('API response indicates failure.');
1652 }
1653
1654 if (!isset($data['data'])) {
1655 wp_send_json_error('API response missing data.');
1656 }
1657
1658 $result = $data['data'];
1659 set_transient('templately-google-fonts', $result, DAY_IN_SECONDS);
1660 }
1661
1662 wp_send_json_success($result);
1663 }
1664
1665 public function ai_get_json() {
1666 // read json data from post body
1667 $body = file_get_contents('php://input');
1668 $data = json_decode($body, true);
1669
1670 if(empty($data['ai_page_ids'])){
1671 wp_send_json_error('Invalid ai_page_ids');
1672 return;
1673 }
1674
1675 if(!isset($_GET['session_id'])){
1676 wp_send_json_error('Invalid session_id');
1677 return;
1678 }
1679
1680 $session_id = isset($_GET['session_id']) ? sanitize_text_field($_GET['session_id']) : null;
1681 $process_id = $data['process_id'] ?? null;
1682 $ai_page_ids = $data['ai_page_ids'] ?? null;
1683
1684 $this->request_params = $this->get_session_data();
1685 try {
1686 $this->manifest = $this->read_manifest($this->request_params['dir_path']);
1687 } catch (\Exception $th) {
1688 wp_send_json_error($th->getMessage());
1689 }
1690
1691 if(!empty($session_id) && empty($process_id)){
1692 if ( !empty($this->request_params['process_id']) ){
1693 $process_id = $this->request_params['process_id'] ?? null;
1694 } else {
1695 $process_id = AIUtils::get_ai_process_id_by_session_id($session_id);
1696 }
1697 }
1698
1699 if(empty($process_id)){
1700 wp_send_json_error('Invalid process_id');
1701 return;
1702 }
1703
1704 $process_data = AIUtils::get_ai_process_data_by_process_id($process_id);
1705 if (!empty($process_data['preview_error'])) {
1706 wp_send_json_error($process_data['preview_error']);
1707 }
1708
1709 // Use the new common function to read AI template data directly
1710 $result = AIUtils::read_ai_template_data($session_id, $ai_page_ids, $this->request_params['dir_path']);
1711
1712 // Check if this is called from polling endpoint and include additional data
1713 $response_data = ['process_id' => $process_id, 'templates' => $result];
1714
1715 if (isset($this->polling_is_last_part)) {
1716 $response_data['is_last_part'] = $this->polling_is_last_part;
1717
1718 // Clean up the polling property
1719 unset($this->polling_is_last_part);
1720 }
1721
1722 wp_send_json_success($response_data);
1723 }
1724
1725 /**
1726 * AJAX handler for polling AI template generation status on local sites
1727 * Makes GET request to API endpoint and returns data in same format as ai_get_json()
1728 */
1729 public function ai_poll_template() {
1730 // Read JSON data from post body
1731 $body = file_get_contents('php://input');
1732 $data = json_decode($body, true);
1733
1734 $process_id = $data['process_id'] ?? null;
1735 $ai_page_ids = $data['ai_page_ids'] ?? null;
1736
1737 if(empty($process_id)){
1738 wp_send_json_error('Invalid process_id');
1739 return;
1740 }
1741
1742 // Validate and get AI process data using centralized method
1743 $process_data = AIUtils::validate_and_get_process_data($process_id);
1744 if (is_wp_error($process_data)) {
1745 $this->ai_get_json();
1746 return;
1747 }
1748
1749 $session_id = $process_data['session_id'];
1750 $ai_page_ids = $process_data['ai_page_ids'];
1751
1752 // Use the common polling function to handle all template processing
1753 $polling_result = AIUtils::poll_for_template($process_id, $session_id, $ai_page_ids);
1754
1755 if (!$polling_result) {
1756 // Polling failed, fallback to ai_get_json
1757 $this->ai_get_json();
1758 return;
1759 }
1760
1761 // After polling and processing templates, call ai_get_json() to return the data
1762 // This reuses all the existing logic without duplication
1763 $this->ai_get_json();
1764 }
1765
1766 /**
1767 * Process AI preview content following the ai_get_json() pattern
1768 *
1769 * @param string $process_id The AI process ID
1770 * @param array $ai_page_ids The AI page IDs data structure
1771 * @param array $ai_preview_ids The AI preview IDs to process
1772 * @return array Processed AI content data
1773 */
1774 private function process_ai_preview_content($process_id, $ai_page_ids, $ai_preview_ids) {
1775 if (empty($process_id) || empty($ai_page_ids) || empty($ai_preview_ids)) {
1776 return [];
1777 }
1778
1779 $all_ai_process_data = AIUtils::get_ai_process_data();
1780 if (empty($all_ai_process_data[$process_id])) {
1781 return [];
1782 }
1783 $ai_process_data = $all_ai_process_data[$process_id];
1784 $_REQUEST['is_lightspeed'] = 'true';
1785 $_REQUEST['session_id'] = $ai_process_data['session_id'] ?? null;
1786 // Initialize session data and manifest following ai_get_json() pattern
1787 $this->request_params = $this->get_session_data();
1788 $this->manifest = $this->read_manifest($this->request_params['dir_path']);
1789
1790 // Create Finalizer instance with the same configuration as ai_get_json()
1791 $finalizer = new Finalizer(array_merge($this->request_params, [
1792 'origin' => $this,
1793 'manifest' => $this->manifest,
1794 ]));
1795 $finalizer->process_id = $process_id;
1796 $finalizer->ai_page_ids = $ai_page_ids;
1797
1798 $result = [];
1799
1800 // Process each AI preview ID
1801 foreach ($ai_preview_ids as $preview_id) {
1802 // Extract type and sub_type metadata from ai_page_ids structure
1803 $type_info = $this->extract_content_metadata($preview_id, $ai_page_ids);
1804
1805 if ($type_info) {
1806 $finalizer->type = $type_info['type'];
1807 $finalizer->sub_type = $type_info['sub_type'];
1808
1809 // Check if this is AI content before processing
1810 if ($finalizer->isAiContent($preview_id)) {
1811 // Process AI content using AIContentHelper trait
1812 $ai_result = $finalizer->processAiContent($preview_id);
1813 if ($ai_result['is_ai'] && !empty($ai_result['template_json'])) {
1814 $template_json = $ai_result['template_json'];
1815 $result[$preview_id] = $template_json;
1816 } else if ($finalizer->isAiFileSkipped($preview_id)) {
1817 // Handle skipped AI files
1818 $result[$preview_id] = [];
1819 }
1820 }
1821 }
1822 }
1823
1824 return $result;
1825 }
1826
1827 /**
1828 * Extract content metadata (type and sub_type) from ai_page_ids structure
1829 *
1830 * @param string $preview_id The preview ID to find
1831 * @param array $ai_page_ids The AI page IDs data structure
1832 * @return array|null Array with 'type' and 'sub_type' keys, or null if not found
1833 */
1834 private function extract_content_metadata($preview_id, $ai_page_ids) {
1835 if (empty($ai_page_ids) || !is_array($ai_page_ids)) {
1836 return null;
1837 }
1838
1839 // Search through the ai_page_ids structure to find the preview_id
1840 foreach ($ai_page_ids as $key => $ids) {
1841 if (is_array($ids) && in_array($preview_id, $ids)) {
1842 // Extract type and sub_type from the key (e.g., 'content/page' or 'templates')
1843 $type_arr = explode('/', $key);
1844 return [
1845 'type' => $type_arr[0],
1846 'sub_type' => isset($type_arr[1]) ? $type_arr[1] : ''
1847 ];
1848 }
1849 }
1850
1851 return null;
1852 }
1853
1854 }
1855