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

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