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

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