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