PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.4.2
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.4.2
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
templately / includes / Core / Importer / FullSiteImport.php

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

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