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

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