PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.1.2
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.1.2
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
← All changes | includes/Core/Importer/FullSiteImport.php +430 -1863 3.7.33.1.2 View file →
@@ -1,32 +1,9 @@
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 -
14 3 namespace Templately\Core\Importer;
15 4
16 -use Elementor\Plugin;
17 -use Error;
18 5 use Exception;
19 -use Templately\Core\Importer\Exception\NonRetirableErrorException;
20 -use Templately\Core\Importer\Exception\RetryableErrorException;
21 -use Templately\Core\Importer\Exception\UnknownErrorException;
22 -use Templately\Core\Importer\Runners\Finalizer;
23 -use Templately\Core\Importer\Utils\LogHandler;
24 -use Templately\Core\Importer\Utils\Utils;
25 -use Templately\Core\Importer\Utils\SessionData;
26 -use Templately\Core\Importer\Utils\AIUtils;
27 -use Templately\Core\Importer\Utils\ElementorSettingsMerger;
28 -use Templately\Core\Importer\Utils\GutenbergSettingsMerger;
29 6 use Templately\Utils\Base;
30 7 use Templately\Utils\Helper;
31 8 use Templately\Utils\Installer;
32 9 use Templately\Utils\Options;
@@ -39,86 +16,46 @@
39 16 protected $export;
40 17
41 18 private $version = '1.0.0';
42 19
20 + public $session_id;
43 21 public $download_key;
22 + public $dir_path;
23 + protected $filePath;
24 + protected $tmp_dir = null;
44 25 protected $dev_mode = false;
45 26 protected $api_key = '';
46 - protected $session_id = '';
27 + public $request_params = [];
47 28 protected $documents_data = [];
29 + protected $dependency_data = [];
48 30 private $is_import_status_handled = false;
49 31
50 - public $dir_path;
51 - protected $filePath;
52 - protected $tmp_dir = null;
53 - public $request_params = [];
54 -
55 - // Polling-specific property for ai_poll_template()
56 - private $polling_is_last_part = null;
57 -
58 32 public function __construct() {
59 - $this->dev_mode = defined('TEMPLATELY_DEV') && TEMPLATELY_DEV;
60 - $this->api_key = Options::get_instance()->get('api_key');
33 + $this->dev_mode = defined( 'TEMPLATELY_DEV' ) && TEMPLATELY_DEV;
34 + $this->api_key = Options::get_instance()->get( 'api_key' );
61 35
62 - $this->add_ajax_action('import_settings', $this);
63 - $this->add_ajax_action('create_session_and_download', $this);
64 - $this->add_ajax_action('import_status', $this);
65 - $this->add_ajax_action('import', $this);
66 - $this->add_ajax_action('import_revert', $this);
67 - $this->add_ajax_action('import_info', $this);
68 - $this->add_ajax_action('import_global_settings', $this);
69 - $this->add_ajax_action('import_close_feedback_modal', $this);
70 - $this->add_ajax_action('feedback_form', $this);
71 - $this->add_ajax_action('google_font', $this);
72 - $this->add_ajax_action('ai_get_json', $this);
73 - $this->add_ajax_action('ai_poll_template', $this);
74 - $this->add_ajax_action('merge_settings_with_template', $this);
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' ] );
75 41
76 - add_action('admin_init', [$this, 'admin_init']);
77 - // add_action('admin_notices', [$this, 'add_revert_button']);
78 -
79 - if(isset($_GET['action']) && ($_GET['action'] == 'templately_pack_import' || $_GET['action'] == 'templately_pack_import_status')) {
42 + if(isset($_GET['action']) && $_GET['action'] == 'templately_pack_import') {
80 43 add_filter('wp_redirect', '__return_false', 999);
81 44 }
82 45
83 - if ($this->dev_mode) {
84 - add_filter('http_request_host_is_external', '__return_true');
85 - add_filter('http_request_args', function ($args) {
46 + if ( $this->dev_mode ) {
47 + add_filter( 'http_request_host_is_external', '__return_true' );
48 + add_filter( 'http_request_args', function ( $args ) {
86 49 $args['sslverify'] = false;
87 50
88 51 return $args;
89 - });
52 + } );
90 53 }
91 54 }
92 55
93 - public function add_ajax_action($action, $object) {
94 - add_action("wp_ajax_templately_pack_$action", function() use ($action, $object) {
95 - // Check nonce
96 - $nonce = null;
97 - if(isset($_POST['nonce'])){
98 - $nonce = $_POST['nonce'];
99 - }
100 - if(isset($_GET['nonce'])){
101 - $nonce = $_GET['nonce'];
102 - }
103 - if (!$nonce || !wp_verify_nonce($nonce, 'templately_nonce')) {
104 - wp_send_json_error(['message' => __('Invalid nonce', 'templately')]);
105 - wp_die();
106 - }
107 -
108 - // Check user capability
109 - if (!current_user_can('install_plugins') || !current_user_can('install_themes')) {
110 - wp_send_json_error(['message' => __('Insufficient permissions', 'templately')]);
111 - wp_die();
112 - }
113 -
114 - // Call the actual handler method
115 - call_user_func([$this, $action]);
116 - });
117 - }
118 -
119 - public function admin_init() {
120 - if (get_option('templately_flush_rewrite_rules', false)) {
56 + public function admin_init(){
57 + if(get_option('templately_flush_rewrite_rules', false)){
121 58 flush_rewrite_rules();
122 59 delete_option('templately_flush_rewrite_rules');
123 60 }
124 61 }
@@ -123,573 +60,224 @@
123 60 }
124 61 }
125 62
126 63 public function import_settings() {
127 - $user = Options::get_instance()->get('user');
128 - if (!empty($user['is_disconnected'])) {
129 - wp_send_json_error(['message' => __('Your site connection is disconnected. Please migrate your connection first.', 'templately')]);
130 - return;
64 + if( ! current_user_can( 'edit_posts' ) ) {
65 + // @todo show error in ui. Currently getting stack.
66 + wp_send_json_error( __( 'Unauthorized action.', 'templately' ) );
131 67 }
132 68
133 - $data = wp_unslash($_POST);
69 + $data = wp_unslash( $_POST );
134 70
135 - $upload_dir = wp_upload_dir();
71 + update_option( self::SESSION_OPTION_KEY, $data );
136 72
137 - if(!empty($data['session_id'])){
138 - $session_id = $data['session_id'];
139 - // Security: Sanitize session_id from user input
140 - $session_id = AIUtils::sanitize_path_component($data['session_id'], 'session_id');
141 - if (is_wp_error($session_id)) {
142 - wp_send_json_error(['message' => $session_id->get_error_message()]);
143 - return;
144 - }
145 - $session_data = SessionData::get_data($session_id);
146 - $data = array_merge($session_data, $data);
147 - }
148 - else {
149 - // Not uniqid(): that is a formatted microtime, so the path it keys in
150 - // web-served wp-uploads is enumerable by anyone who knows roughly when
151 - // an import ran. The chatbot flow already sends a client-side uuid4
152 - // here, so this only brings the fallback in line with it.
153 - $session_id = wp_generate_uuid4();
154 - }
73 + delete_transient('templately_fsi_log');
155 74
156 - $tmp_dir = Helper::upload_dir('tmp');
157 - $prv_dir = Helper::upload_dir('preview');
158 -
159 - $this->session_id = $session_id;
160 - $data['session_id'] = $session_id;
161 -
162 - $data['root_dir'] = $tmp_dir;
163 - $data['prv_dir'] = $prv_dir;
164 - $data['dir_path'] = $tmp_dir . $session_id . DIRECTORY_SEPARATOR;
165 - $data['zip_path'] = $tmp_dir . "{$session_id}.zip";
166 -
167 -
168 - if ( is_array( $data ) && ! empty( $data ) ) {
169 - foreach ( $data as $key => $value ) {
170 - $json = is_string($value) ? json_decode( $value, true ) : null;
171 - $data[ $key ] = $json !== null ? $json : $value;
172 - }
173 - }
174 -
175 - SessionData::save($session_id, $data);
176 -
177 -
178 - //clear previous revert backup
179 - $options = Utils::get_backup_options();
180 - foreach ($options as $key => $value) {
181 - delete_option("__templately_$key");
182 - }
183 - delete_option('templately_fsi_imported_list');
184 - delete_option('templately_fsi_log');
185 -
186 - wp_send_json_success([
75 + wp_send_json_success( [
187 76 'is_lightspeed' => !Helper::should_flush(),
188 - 'session_id' => $session_id,
189 - ]);
77 + ] );
190 78 }
191 79
192 - public function import_ai_settings() {
193 - $data = wp_unslash($_POST);
80 + private function update_session_data( $data ): bool {
81 + $old_data = get_option( self::SESSION_OPTION_KEY, [] );
194 82
195 - $upload_dir = wp_upload_dir();
83 + return update_option( self::SESSION_OPTION_KEY, wp_parse_args( $data, $old_data ) );
84 + }
196 85
197 - // Security: Sanitize session_id from user input
198 - $session_id = AIUtils::sanitize_path_component($data['session_id'], 'session_id');
199 - if (is_wp_error($session_id)) {
200 - wp_send_json_error(['message' => $session_id->get_error_message()]);
201 - return;
202 - }
86 + public function get_session_data(): array {
87 + $data = get_option( self::SESSION_OPTION_KEY, [] );
203 88
204 - $tmp_dir = Helper::upload_dir('tmp');
205 - $prv_dir = Helper::upload_dir('preview');
89 + $options = [];
206 90
207 - $this->session_id = $session_id;
208 - $data['root_dir'] = $tmp_dir;
209 - $data['prv_dir'] = $prv_dir;
210 - $data['dir_path'] = $tmp_dir . $session_id . DIRECTORY_SEPARATOR;
211 - $data['zip_path'] = $tmp_dir . "{$session_id}.zip";
212 -
213 - // Handle isLocalSite flag conversion
214 - if (isset($data['isLocalSite'])) {
215 - $data['isLocalSite'] = filter_var($data['isLocalSite'], FILTER_VALIDATE_BOOLEAN);
216 - }
217 -
218 91 if ( is_array( $data ) && ! empty( $data ) ) {
219 92 foreach ( $data as $key => $value ) {
220 - $json = is_string($value) ? json_decode( $value, true ) : null;
221 - $data[ $key ] = $json !== null ? $json : $value;
93 + $json = json_decode( $value, true );
94 + $options[ $key ] = $json !== null ? $json : $value;
222 95 }
223 96 }
224 97
225 - SessionData::save($session_id, $data);
226 -
227 -
228 - return $data;
98 + return $options;
229 99 }
230 100
231 - public function create_session_and_download() {
232 - if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
233 - exit;
234 - }
235 -
236 - Utils::add_gd_editor_filter();
237 -
238 - define('TEMPLATELY_START_TIME', microtime(true));
239 -
240 - register_shutdown_function( [ $this, 'register_shutdown' ] );
241 -
242 - // $this->finishRequestHeaders();
243 -
244 - try {
245 - // Get session data from AJAX request
246 - $session_data = $this->import_ai_settings();
247 -
248 - $this->request_params = $session_data;
249 - $this->initialize_props();
250 - $this->add_revert_hooks();
251 - $progress = $this->request_params['progress'] ?? [];
252 -
253 - if(empty($progress['create_log_dir'])){
254 - // Create Log Directory and if fail then chose option method
255 - LogHandler::create_log_dir();
256 -
257 - SessionData::mark_step_complete($this->session_id, 'create_log_dir');
258 - }
259 -
260 - $_id = isset($this->request_params['id']) ? (int) $this->request_params['id'] : null;
261 -
262 - if ($_id === null) {
263 - $this->throw(__('Invalid Pack ID.', 'templately'));
264 - }
265 -
266 - $this->check_writing_permission();
267 -
268 -
269 - if(empty($progress['download_zip'])){
270 -
271 - /**
272 - * Download the zip
273 - */
274 - $this->download_zip( $_id, true );
275 -
276 - SessionData::mark_step_complete($this->session_id, 'download_zip');
277 - }
278 -
279 - /**
280 - * Reading Manifest File
281 - */
282 - $this->manifest = $this->read_manifest($this->request_params['dir_path']);
283 -
284 - /**
285 - * Version Check
286 - */
287 - if ( ! empty( $this->manifest['version'] ) && version_compare( $this->manifest['version'], $this->version, '>' ) ) {
288 - $this->throw( __( 'Please update the templately plugin.', 'templately' ) );
289 - }
290 -
291 - $platform = $this->manifest['platform'] ?? '';
292 - if($platform === 'elementor') {
293 - Helper::enable_elementor_container();
294 - }
295 -
296 - update_option('templately_import_platform', $platform);
297 -
298 - // Return success response for AJAX
299 - wp_send_json_success([
300 - 'session_id' => $this->session_id,
301 - 'pack_downloaded' => true,
302 - 'platform' => $platform,
303 - 'message' => __('Session created and pack downloaded successfully', 'templately')
304 - ]);
305 -
306 - } catch ( Exception $e ) {
307 - $should_retry = $e instanceof RetryableErrorException;
308 -
309 - wp_send_json_error([
310 - 'message' => $e->getMessage(),
311 - 'should_retry' => $should_retry
312 - ]);
313 - }
314 - }
315 -
316 - public function import_close_feedback_modal() {
317 - $return = null;
318 - if(isset($_GET['closeAction']) && $_GET['closeAction']){
319 - $review_email = isset($_POST['review-email']) ? sanitize_email($_POST['review-email']) : '';
320 - $pack_id = get_user_meta(get_current_user_id(), 'templately_fsi_pack_id', true);
321 -
322 - // Prepare the body of the request
323 - $body = json_encode([
324 - 'action' => $_GET['closeAction'],
325 - 'email' => $review_email,
326 - 'pack_id' => (int) $pack_id,
327 - ]);
328 -
329 - // Send the request to the API
330 - $response = Helper::make_api_post_request('v2/feedback/close', json_decode($body, true), [], 30);
331 - $body = wp_remote_retrieve_body($response);
332 - $return = json_decode($body, true);
333 - }
334 - update_user_meta(get_current_user_id(), 'templately_fsi_complete', 'done');
335 - wp_send_json_success($return);
336 - }
337 - public function feedback_form() {
338 - // Get data from $_POST
339 - $review_description = isset($_POST['review-description']) ? sanitize_textarea_field($_POST['review-description']) : '';
340 - $review_email = isset($_POST['review-email']) ? sanitize_email($_POST['review-email']) : '';
341 - $rating = isset($_POST['rating']) ? sanitize_text_field($_POST['rating']) : '';
342 - $pack_id = get_user_meta(get_current_user_id(), 'templately_fsi_pack_id', true);
343 -
344 - // Prepare the body of the request
345 - $body = json_encode([
346 - 'description' => $review_description,
347 - 'email' => $review_email,
348 - 'rating' => (int) $rating,
349 - 'pack_id' => (int) $pack_id,
350 - ]);
351 -
352 - // Send the request to the API
353 - $response = Helper::make_api_post_request('v2/feedback/store', json_decode($body, true), [], 30);
354 -
355 - if (is_wp_error($response)) {
356 - wp_send_json_error($response->get_error_message());
357 - }
358 -
359 - if (wp_remote_retrieve_response_code($response) != 200) {
360 - $error_message = $this->extract_error_from_response($response);
361 - wp_send_json_error($error_message, wp_remote_retrieve_response_code($response));
362 - }
363 -
364 - $body = wp_remote_retrieve_body($response);
365 - $data = json_decode($body, true);
366 -
367 - if (!isset($data['status']) || $data['status'] !== 'success') {
368 - wp_send_json_error('API response indicates failure.');
369 - }
370 -
371 - if (!isset($data['message'])) {
372 - wp_send_json_error('API response missing data.');
373 - }
374 -
375 - $result = $data['message'];
376 -
377 - wp_send_json_success($result);
378 - }
379 -
380 - // Modified get_session_data to use SessionData
381 - public function get_session_data() {
382 - if ($session_id = SessionData::get_session_id()) {
383 - return SessionData::get_data($session_id);
384 - }
385 - return [];
386 - }
387 -
388 - // Modified update_session_data to use SessionData
389 - public function update_session_data($data) {
390 - if ($session_id = SessionData::get_session_id()) {
391 - return SessionData::save($session_id, array_merge($this->get_session_data(), $data));
392 - }
393 - return false;
394 - }
395 -
396 101 public function initialize_props() {
397 102 $data = $this->get_session_data();
398 - if (isset($data['session_id'])) {
103 + if(isset($data['session_id'])){
399 104 $this->session_id = $data['session_id'];
400 105 }
401 - if (isset($data['dir_path'])) {
106 + if(isset($data['dir_path'])){
402 107 $this->dir_path = $data['dir_path'];
403 108 }
404 - if (isset($data['zip_path'])) {
405 - $this->filePath = $data['zip_path'];
406 - }
407 - if (isset($data['download_key'])) {
109 + if(isset($data['download_key'])){
408 110 $this->download_key = $data['download_key'];
409 111 }
410 - if (isset($data['is_import_status_handled'])) {
411 - $this->is_import_status_handled = $data['is_import_status_handled'];
112 + if(isset($data['dependency_data'])){
113 + $this->dependency_data = $data['dependency_data'];
412 114 }
115 +
413 116 }
414 117
415 - public function clear_session_data(): bool {
416 - return delete_site_option(self::SESSION_OPTION_KEY);
118 + private function clear_session_data(): bool {
119 + return delete_site_option( self::SESSION_OPTION_KEY );
417 120 }
418 121
419 - private function finishRequestHeaders() {
420 - if(Helper::should_flush()) {
421 - // Disable output buffering and compression
422 - @ini_set('output_buffering', 'Off');
423 - @ini_set('zlib.output_compression', 'Off');
424 - @ini_set('implicit_flush', 1);
122 + public function import() {
123 + if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
124 + exit;
125 + }
425 126
426 - // Time to run the import! Set no limit
427 - set_time_limit(0);
127 + delete_transient( 'templately_fsi_log' );
428 128
429 -
430 - // Set headers to prevent caching and buffering
431 - header('Content-Type: text/event-stream, charset=UTF-8');
432 - header('Cache-Control: no-cache, must-revalidate');
433 - header('Expires: Sat, 26 Jul 1997 05:00:00 GMT');
434 - header('Connection: Keep-Alive');
435 - header('Pragma: no-cache');
436 -
437 - if (!empty($GLOBALS['is_nginx'])) {
438 - header('X-Accel-Buffering: no');
439 - header('Content-Encoding: none');
440 - }
441 -
442 - flush();
443 - ob_flush();
444 - wp_ob_end_flush_all();
445 - } else {
446 - header("Cache-Control: no-store, no-cache");
447 - // header( 'Content-Type: text/event-stream, charset=UTF-8' );
448 - // header( "Connection: Keep-Alive" );
449 -
450 - // Ignore user aborts and allow the script to run forever
451 - // (Use with caution, consider progress updates or timeouts)
452 - ignore_user_abort(true);
453 -
454 - // Time to run the import! Set no limit
455 - set_time_limit(0);
456 -
457 -
458 - if (!empty($GLOBALS['is_nginx'])) {
459 - header('X-Accel-Buffering: no');
460 - header('Content-Encoding: none');
461 - }
462 -
463 - // Send output as soon as possible during long-running process
464 - if (function_exists('fastcgi_finish_request')) {
465 - fastcgi_finish_request();
466 - } elseif (function_exists('litespeed_finish_request')) {
467 - litespeed_finish_request();
468 - } else {
469 - wp_ob_end_flush_all();
470 - }
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" );
471 133 }
472 - }
473 134
474 - public function import() {
475 - if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
476 - exit;
135 + if ( $GLOBALS['is_nginx'] ) {
136 + header( 'X-Accel-Buffering: no' );
137 + header( 'Content-Encoding: none' );
477 138 }
478 139
479 - Utils::add_gd_editor_filter();
140 + register_shutdown_function( [ $this, 'register_shutdown' ] );
480 141
142 + // Ignore user aborts and allow the script
143 + // to run forever
144 + ignore_user_abort(true);
481 145
482 - define('TEMPLATELY_START_TIME', microtime(true));
146 + // Time to run the import!
147 + set_time_limit( 0 );
483 148
484 - // delete_option( 'templately_fsi_log' );
485 149
486 - register_shutdown_function( [ $this, 'register_shutdown' ] );
487 150
488 - $this->finishRequestHeaders();
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 + }
489 161
490 162 try {
491 - $user = Options::get_instance()->get('user');
492 - if (!empty($user['is_disconnected'])) {
493 - $this->throw(__('Your site connection is disconnected. Please migrate your connection first.', 'templately'));
494 - }
163 + // TODO: Need to check if user is connected or not
495 164
496 - if(!empty($_GET['session_id'])){
497 - // Security: Sanitize session_id from user input
498 - $session_id = AIUtils::sanitize_path_component(sanitize_text_field($_GET['session_id']), 'session_id');
499 - if (is_wp_error($session_id)) {
500 - $this->throw($session_id->get_error_message());
501 - }
502 - $this->session_id = $session_id;
503 - }
504 - else {
505 - $this->throw(__('Invalid Session ID.', 'templately'));
506 - }
507 -
508 -
509 165 $this->request_params = $this->get_session_data();
510 - $this->initialize_props();
511 - $this->add_revert_hooks();
512 - $progress = $this->request_params['progress'] ?? [];
513 166
514 - // Trigger action hook for network admin multisite handling
515 - do_action( 'templately_fsi_before_import', $this, $this->request_params );
167 + $_id = isset( $this->request_params['id'] ) ? (int) $this->request_params['id'] : null;
516 168
517 - // Filter Child Type for Elementor Pro Promotion Widget
518 - if ( class_exists( '\Elementor\Plugin' ) ) {
519 - \Templately\Core\Platform\Elementor::register_child_type_filter();
169 + if ( $_id === null ) {
170 + $this->throw( __( 'Invalid Pack ID.', 'templately' ) );
520 171 }
521 172
522 - // Refresh progress after potential multisite creation
523 - $progress = $this->request_params['progress'] ?? [];
524 -
525 - if(empty($progress['create_log_dir'])){
526 - // Create Log Directory and if fail then chose option method
527 - LogHandler::create_log_dir();
528 -
529 - SessionData::mark_step_complete($this->session_id, 'create_log_dir');
530 - $this->sse_message( [
531 - 'type' => 'eventLog',
532 - 'action' => 'eventLog',
533 - 'info' => 'create_log_dir',
534 - 'results' => __METHOD__ . '::' . __LINE__,
535 - ] );
173 + if(!isset($_GET['part'])){
174 + $this->throw( __( 'Invalid request.', 'templately' ) );
536 175 }
537 176
538 - $_id = isset($this->request_params['id']) ? (int) $this->request_params['id'] : null;
177 + switch ($_GET['part']) {
178 + case 'download':
179 + /**
180 + * Check Writing Permission
181 + */
182 + $this->check_writing_permission();
539 183
540 - if ($_id === null) {
541 - $this->throw(__('Invalid Pack ID.', 'templately'));
542 - }
184 + /**
185 + * Download the zip
186 + */
187 + $this->download_zip( $_id );
543 188
544 - $this->sse_message( [
545 - 'type' => 'start',
546 - 'action' => 'eventLog',
547 - 'results' => __METHOD__ . '::' . __LINE__,
548 - ] );
189 + /**
190 + * Reading Manifest File
191 + */
192 + $this->read_manifest();
549 193
550 - if(empty($progress['check_writing_permission'])){
551 - /**
552 - * Check Writing Permission
553 - */
554 - $this->check_writing_permission();
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 + }
555 203
556 - SessionData::mark_step_complete($this->session_id, 'check_writing_permission');
557 - }
204 + /**
205 + * Checking & Installing Plugin Dependencies
206 + */
207 + $this->install_dependencies();
558 208
559 - if(empty($progress['download_zip'])){
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();
560 220
561 - /**
562 - * Download the zip
563 - */
564 - $this->download_zip( $_id );
565 221
566 - SessionData::mark_step_complete($this->session_id, 'download_zip');
567 - $this->sse_message( [
568 - 'type' => 'continue',
569 - 'action' => 'continue',
570 - 'info' => 'download_zip',
571 - 'results' => __METHOD__ . '::' . __LINE__,
572 - ] );
573 - exit;
574 - }
222 + $this->initialize_props();
575 223
224 + /**
225 + * Reading Manifest File
226 + */
227 + $this->read_manifest();
576 228
577 -
578 -
579 - /**
580 - * Reading Manifest File
581 - */
582 - $this->manifest = $this->read_manifest($this->request_params['dir_path']);
583 -
584 - /**
585 - * Version Check
586 - */
587 - if ( ! empty( $this->manifest['version'] ) && version_compare( $this->manifest['version'], $this->version, '>' ) ) {
588 - /**
589 - * FIXME: The message should be re-written (by content/support team).
590 - */
591 - $this->throw( __( 'Please update the templately plugin.', 'templately' ) );
229 + /**
230 + * Platform Based Templates Import
231 + */
232 + $this->start_content_import();
233 + break;
234 + default:
235 + $this->throw( __( 'Invalid request.', 'templately' ) );
236 + break;
592 237 }
593 238
594 - $platform = $this->manifest['platform'] ?? '';
595 - if($platform === 'elementor') {
596 - Helper::enable_elementor_container();
597 - }
598 -
599 -
600 -
601 - update_option('templately_import_platform', $platform);
602 -
603 -
604 - /**
605 - * Should Revert Old Data
606 - */
607 - // $this->revert();
608 -
609 - /**
610 - * Platform Based Templates Import
611 - */
612 - $this->start_content_import();
613 -
614 239 } catch ( Exception $e ) {
615 - $should_retry = $e instanceof RetryableErrorException;
616 240 $this->handle_import_status('failed', $e->getMessage());
617 -
618 - $this->sse_message([
619 - 'action' => 'error',
620 - 'status' => 'error',
621 - 'type' => "error",
622 - 'retry' => $should_retry,
623 - 'title' => __("Oops!", "templately"),
624 - 'message' => $e->getMessage(),
625 - 'trace' => $e->getTraceAsString(),
626 - ]);
241 + $this->sse_message( [
242 + 'action' => 'error',
243 + 'status' => 'error',
244 + 'type' => "error",
245 + 'title' => __("Oops!", "templately"),
246 + 'message' => $e->getMessage()
247 + ] );
627 248 }
628 249
629 - // if($_GET['part'] === 'import'){
250 + if($_GET['part'] === 'import'){
630 251 // TODO: cleanup
631 - // $this->clear_session_data();
632 - // }
252 + $this->clear_session_data();
253 + }
633 254 }
634 255
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);
635 261
636 - // Updated import_status method
637 - public function import_status() {
638 - $request_params = $this->get_session_data();
639 -
640 - if (isset($request_params['log_type']) && $request_params['log_type'] == 'file') {
641 - $log_index = isset($_GET['lastLogIndex']) ? (int) $_GET['lastLogIndex'] : 0;
642 - $log = LogHandler::read_log_file($log_index);
643 -
644 - wp_send_json(['count' => count($log), 'log' => $log]);
645 - } else {
646 - $log = get_option('templately_fsi_log');
647 -
648 - if (!empty($log) && is_array($log) && isset($_GET['lastLogIndex'])) {
649 - $lastLogIndex = (int) $_GET['lastLogIndex'];
650 - $log = array_slice($log, $lastLogIndex);
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' );
651 266 }
652 - wp_send_json(['count' => $log ? count($log) : 0, 'log' => $log]);
653 267 }
268 + wp_send_json( ['log' => $log] );
654 269 }
655 270
656 271 /**
657 272 * @throws Exception
658 273 */
659 - private function throw($message, $code = 0) {
660 - if ($this->dev_mode) {
661 - error_log(print_r($message, 1));
274 + private function throw( $message, $code = 0 ) {
275 + if ( $this->dev_mode ) {
276 + error_log( print_r( $message, 1 ) );
662 277 }
663 - throw new Exception($message);
278 + throw new Exception( $message );
664 279 }
665 - /**
666 - * @throws Exception
667 - */
668 - private function throw_non_retryable($message, $code = 0) {
669 - if ($this->dev_mode) {
670 - error_log(print_r($message, 1));
671 - }
672 - throw new NonRetirableErrorException($message);
673 - }
674 - /**
675 - * @throws Exception
676 - */
677 - private function throw_retryable($message, $code = 0) {
678 - if ($this->dev_mode) {
679 - error_log(print_r($message, 1));
680 - }
681 - throw new RetryableErrorException($message);
682 - }
683 - /**
684 - * @throws Exception
685 - */
686 - private function throw_unknown($message, $code = 0) {
687 - if ($this->dev_mode) {
688 - error_log(print_r($message, 1));
689 - }
690 - throw new UnknownErrorException($message);
691 - }
692 280
693 281 /**
694 282 * @throws Exception
695 283 */
@@ -695,77 +283,84 @@
695 283 */
696 284 private function check_writing_permission() {
697 285 $upload_dir = wp_upload_dir();
698 286
699 - if (!is_writable($upload_dir['basedir'])) {
700 - $this->throw(__('Upload directory is not writable.', 'templately'));
287 + if ( ! is_writable( $upload_dir['basedir'] ) ) {
288 + $this->throw( __( 'Upload directory is not writable.', 'templately' ) );
701 289 }
702 290
703 - // Goes through Helper so the wp-uploads/templately root gets its
704 - // index.php / .htaccess / web.config guards before anything is written
705 - // into it — the extracted pack lives here and uploads is web-served.
706 - $this->tmp_dir = Helper::upload_dir('tmp');
291 + $this->tmp_dir = trailingslashit( $upload_dir['basedir'] ) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
707 292
708 - if (!is_dir($this->tmp_dir)) {
709 - wp_mkdir_p($this->tmp_dir);
293 + if ( ! is_dir( $this->tmp_dir ) ) {
294 + wp_mkdir_p( $this->tmp_dir );
710 295 }
711 296
712 - $this->sse_log('writing_permission_check', __('Permission Passed', 'templately'), 100);
297 + $this->sse_log( 'writing_permission_check', __( 'Permission Passed', 'templately' ), 100 );
713 298 }
714 299
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 +
715 308 /**
716 309 * @throws Exception
717 310 */
718 - private function download_zip( $id, $is_ai = false ) {
719 - $this->sse_log( 'download', __( 'Downloading Template Pack', 'templately' ), 1 );
720 - $extra_headers = [
721 - 'x-templately-is-ai' => $is_ai,
722 - 'x-templately-session-id' => $this->session_id,
723 - 'x-templately-requested-platform' => $this->request_params["requested_platform"] ?? 'templately',
724 - ];
725 - $response = Helper::make_api_get_request("v2/import/pack/$id", [], $extra_headers, 90);
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 + ] );
726 323
727 - $response_code = wp_remote_retrieve_response_code($response);
728 - $content_type = wp_remote_retrieve_header($response, 'content-type');
729 - $this->download_key = wp_remote_retrieve_header($response, 'download-key');
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' );
730 327
731 - if (is_wp_error($response)) {
732 - $this->throw_retryable(__('Template pack download failed', 'templately') . $response->get_error_message());
733 - } else if ($response_code != 200) {
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) {
734 332 if (strpos($content_type, 'application/json') !== false) {
735 333 // Retrieve Data from Response Body.
736 - $response_body = json_decode(wp_remote_retrieve_body($response), true);
334 + $response_body = json_decode( wp_remote_retrieve_body( $response ), true );
737 335
738 336 // If the response body is JSON and it contains an error, throw an exception with the error message
739 337 if (isset($response_body['status']) && $response_body['status'] === 'error') {
740 - $support_message = '';
741 - if(strpos($response_body['message'], 'https://templately.com/?support=open') === false){
742 - $support_message = sprintf(__(" Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://templately.com/?support=open');
743 - }
744 - $this->throw_non_retryable($response_body['message'] . $support_message);
338 + $this->throw( $response_body['message'] );
745 339 }
746 340 }
747 - $this->throw_unknown(__('Template pack download failed with response code: ', 'templately') . $response_code);
341 + $this->throw( __( 'Template pack download failed with response code: ', 'templately' ) . $response_code );
748 342 }
749 343
750 - $this->sse_log('download', __('Downloading Template Pack', 'templately'), 57);
344 + $this->sse_log( 'download', __( 'Template is getting ready', 'templately' ), 57 );
751 345
752 - SessionData::set($this->session_id, 'download_key', $this->download_key);
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;
753 350
754 - // Security: Validate file path is within WordPress upload directory before writing
755 - $validation = AIUtils::validate_file_path($this->filePath);
756 - if (is_wp_error($validation)) {
757 - $this->throw($validation->get_error_message());
758 - }
351 + $this->update_session_data( [
352 + 'session_id' => $this->session_id,
353 + 'dir_path' => $this->dir_path,
354 + 'download_key' => $this->download_key,
355 + ] );
759 356
760 - wp_mkdir_p(dirname($this->filePath));
357 + if ( file_put_contents( $this->filePath, $response['body'] ) ) { // phpcs:ignore
358 + $this->sse_log( 'download', __( 'Template is getting ready', 'templately' ), 100 );
761 359
762 - if (file_put_contents($this->filePath, $response['body'])) { // phpcs:ignore
763 - $this->sse_log('download', __('Downloading Template Pack', 'templately'), 100);
764 -
765 360 $this->unzip();
766 361 } else {
767 - $this->throw_retryable(__('Downloading Failed. Please try again', 'templately'));
362 + $this->throw( __( 'Downloading Failed. Please try again', 'templately' ) );
768 363 }
769 364 }
770 365
771 366 /**
@@ -771,450 +366,205 @@
771 366 /**
772 367 * @throws Exception
773 368 */
774 369 protected function unzip() {
775 - if (!WP_Filesystem()) {
776 - $this->throw(__('WP_Filesystem cannot be initialized', 'templately'));
370 + if ( ! WP_Filesystem() ) {
371 + $this->throw( __( 'WP_Filesystem cannot be initialized', 'templately' ) );
777 372 }
778 - $unzip = unzip_file($this->filePath, $this->dir_path);
779 - if (is_wp_error($unzip)) {
780 - // Fall back to our own ZipArchive-based extractor. Some Templately
781 - // packs carry entries with a leading "./" (or embedded "/./") path
782 - // segment, which WordPress core's unzip_file() fails to extract.
783 - // self::unzip_file() extracts with native ZipArchive and normalizes
784 - // those "./" segments so the pack still imports. See
785 - // self::unzip_file() for the extraction and path-traversal guard.
786 - $unzip = $this->unzip_file($this->filePath, $this->dir_path);
787 - }
788 373
789 - // Core's unzip_file() extracts whatever the archive holds, and the pack
790 - // lands under web-served wp-uploads. Sweep before anything else touches
791 - // the tree, so the relocation below cannot copy an executable upward.
792 - $this->purge_disallowed_files($this->dir_path);
793 -
794 - $manifest_file = $this->dir_path . 'manifest.json';
795 -
796 - // If manifest.json is missing, but any subdirectory contains manifest.json, move all its contents up and remove the subdirectory.
797 - if ( ! file_exists( $manifest_file ) ) {
798 - $entries = array_diff( scandir( $this->dir_path ), [ '.', '..' ] );
799 - $dirs = array_filter( $entries, fn($e) => is_dir( $this->dir_path . $e ) );
800 - $files = array_filter( $entries, fn($e) => is_file( $this->dir_path . $e ) );
801 - foreach ($dirs as $subdir) {
802 - $subdir_path = $this->dir_path . $subdir . DIRECTORY_SEPARATOR;
803 - if ( file_exists( $subdir_path . 'manifest.json' ) ) {
804 - copy($subdir_path . 'manifest.json', $manifest_file);
805 -
806 - foreach ( array_diff( scandir( $subdir_path ), [ '.', '..' ] ) as $item ) {
807 - $src = $subdir_path . $item;
808 - $dst = $this->dir_path . $item;
809 - if (is_dir($src)) {
810 - if (!file_exists($dst)) {
811 - wp_mkdir_p($dst);
812 - }
813 - // Recursively copy directory
814 - $this->copyDirectory($src, $dst);
815 - } else {
816 - copy($src, $dst);
817 - }
818 - }
819 - // Remove the subdirectory and its contents
820 - $this->removeDirectory($subdir_path);
821 - break; // Only process the first subdir with manifest.json
822 - }
823 - }
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 );
824 377 }
825 378
826 - if (is_wp_error($unzip)) {
379 + if ( is_wp_error( $unzip ) ) {
827 380 $error = $unzip->get_error_message();
828 - if (empty($error)) {
381 + if(empty($error)){
829 382 // Generic error message
830 - Helper::log($unzip);
831 - $error_message = sprintf(__("It seems we're experiencing technical difficulties. Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://templately.com/?support=open');
832 - $this->throw($error_message);
833 - } else {
834 - $this->throw($unzip->get_error_message());
383 + Helper::log( $unzip );
384 + $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 );
835 386 }
836 - }
837 -
838 - if ($unzip) {
839 - unlink($this->filePath);
840 - }
841 - }
842 -
843 - /**
844 - * Recursively copy a directory
845 - */
846 - private function copyDirectory($src, $dst) {
847 - $dir = opendir($src);
848 - wp_mkdir_p($dst);
849 - while(false !== ($file = readdir($dir))) {
850 - if (($file != '.') && ($file != '..')) {
851 - if (is_dir($src . DIRECTORY_SEPARATOR . $file)) {
852 - $this->copyDirectory($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
853 - } else {
854 - copy($src . DIRECTORY_SEPARATOR . $file, $dst . DIRECTORY_SEPARATOR . $file);
855 - }
387 + else{
388 + $this->throw( $unzip->get_error_message() );
856 389 }
857 390 }
858 - closedir($dir);
859 - }
860 391
861 - /**
862 - * Recursively remove a directory
863 - */
864 - private function removeDirectory($dir) {
865 - if (!file_exists($dir)) return;
866 - $items = array_diff(scandir($dir), ['.', '..']);
867 - foreach ($items as $item) {
868 - $path = $dir . DIRECTORY_SEPARATOR . $item;
869 - if (is_dir($path)) {
870 - $this->removeDirectory($path);
871 - } else {
872 - unlink($path);
873 - }
392 + if ( $unzip ) {
393 + unlink( $this->filePath );
874 394 }
875 - rmdir($dir);
876 395 }
877 396
878 -
879 -
880 397 /**
881 - * Extensions a pack carries that WordPress does not accept as an upload.
398 + * Unzip a specified ZIP file to a location on the Filesystem.
882 399 *
883 - * Everything else comes from get_allowed_mime_types(), so this list only has
884 - * to name what the pack format adds on top of it:
885 - *
886 - * - `json` — every pack is built from JSON (manifest, templates, content),
887 - * and WordPress has never allowed .json uploads.
888 - * - `xml` — the WXR files under wp-content/. Also not an allowed upload:
889 - * core hands .xml to the importer plugin rather than the media library.
890 - * - `ai` — the marker segment in `{id}.ai.json`, an AI-generated page. Not
891 - * the Illustrator format.
892 - * - `svg` — packs ship SVG artwork. This plugin already allows SVG uploads
893 - * for the duration of an import (see allow_svg_upload()), but that filter is
894 - * only added in start_content_import(), which runs AFTER extraction — so at
895 - * sweep time get_allowed_mime_types() does not have it and the pack's SVGs
896 - * would be deleted. It has to be named here.
897 - * - fonts — a pack may ship webfonts, and none of these are upload types.
898 - * Passive assets; nothing here is executable by any server.
899 - *
900 - * Adding to this list is how a new pack file type gets through. It fails
901 - * closed and logs, so a missing entry shows up as a dropped file in the
902 - * import log — not as a silent hole.
400 + * @param string $file Full path and filename of ZIP archive.
401 + * @param string $to Full path on the filesystem to extract archive to.
402 + * @return true|WP_Error True on success, WP_Error on failure.
903 403 */
904 - const PACK_EXTENSIONS = [
905 - 'json', 'xml', 'ai',
906 - 'svg', 'svgz',
907 - 'woff', 'woff2', 'ttf', 'otf', 'eot',
908 - ];
404 + function unzip_file($file, $to) {
405 + try {
406 + $zip = new \ZipArchive;
909 407
910 - /**
911 - * Extensions an extracted pack member is permitted to use.
912 - *
913 - * Deliberately an allowlist, and deliberately WordPress's own: naming the
914 - * dangerous extensions instead means being exhaustive about `.phtml`, `.pht`,
915 - * `.phar`, `.cgi`, `.htaccess`, `.user.ini` and whatever a future server
916 - * config decides to execute — one omission and the guard is silent.
917 - * get_allowed_mime_types() already encodes what this site accepts, follows
918 - * the `upload_mimes` filter, and gains new formats as WordPress does.
919 - *
920 - * @return array Extension => true lookup.
921 - */
922 - protected static function allowed_pack_extensions() {
923 - $allowed = array_fill_keys(self::PACK_EXTENSIONS, true);
408 + $res = $zip->open($file);
409 + if ($res === TRUE) {
410 + $zip->extractTo($to);
411 + $zip->close();
924 412
925 - // Keys are alternation patterns: 'jpg|jpeg|jpe' => 'image/jpeg'.
926 - foreach (array_keys(get_allowed_mime_types()) as $pattern) {
927 - foreach (explode('|', $pattern) as $extension) {
928 - $allowed[$extension] = true;
413 + return true;
929 414 }
415 + } catch (\Throwable $th) {
416 + return new \WP_Error('exception_caught', $th->getMessage());
930 417 }
931 418
932 - return $allowed;
419 + if (isset($zip)) {
420 + return new \WP_Error('zip_error_' . $zip->status, $zip->getStatusString());
421 + } else {
422 + return new \WP_Error('unknown_error', '');
423 + }
933 424 }
934 425
935 426 /**
936 - * Segments that must never appear anywhere in a pack member's name.
937 - *
938 - * This one IS a denylist, and only because it is applied to the segments
939 - * *before* the real extension, which the allowlist has already vetted. A
940 - * permissive `AddHandler` runs `shell.php.gif` as PHP, so an inner segment
941 - * still has to be refused — but refusing every inner segment the allowlist
942 - * does not know would delete ordinary names like `style.min.css` or
943 - * `hero.2x.png`, where the inner segment is a word, not an extension.
944 - *
945 - * An omission here is far less serious than in the allowlist: it only
946 - * matters for a name whose final extension is already an accepted upload
947 - * type, on a server configured to hand an inner extension to a handler.
427 + * @throws Exception
948 428 */
949 - const EXECUTABLE_SEGMENTS = [
950 - 'php', 'php3', 'php4', 'php5', 'php6', 'php7', 'php8',
951 - 'phps', 'phtml', 'phtm', 'pht', 'phar', 'inc',
952 - 'cgi', 'fcgi', 'pl', 'py', 'rb', 'sh', 'bash', 'ksh', 'csh', 'zsh',
953 - 'asp', 'aspx', 'ascx', 'ashx', 'asmx', 'cfm', 'cfml',
954 - 'jsp', 'jspx', 'jar', 'war',
955 - 'shtml', 'shtm',
956 - 'exe', 'com', 'bat', 'cmd', 'dll', 'so',
957 - 'htaccess', 'htpasswd', 'ini', 'env', 'conf',
958 - ];
959 -
960 - /**
961 - * Whether an archive entry (or extracted file) is something a pack may hold.
962 - *
963 - * Two different rules, because a filename's dots do not all mean the same
964 - * thing. The LAST segment is the extension the server dispatches on, so it
965 - * is checked against the allowlist. The segments before it are usually just
966 - * part of the name — `style.min.css`, `hero.2x.png`, `logo.v2.png`,
967 - * `12.ai.json` — so they are only checked against EXECUTABLE_SEGMENTS,
968 - * which is what still refuses `shell.php.gif` under a permissive
969 - * `AddHandler`.
970 - *
971 - * A name with no extension at all is refused, which is what catches
972 - * `.htaccess`, `.user.ini` and `.env` — none of them have one.
973 - *
974 - * @param string $name Entry name or file path.
975 - * @param array $allowed Lookup from allowed_pack_extensions(). Built on
976 - * demand when omitted; pass it in when looping.
977 - *
978 - * @return bool
979 - */
980 - protected static function is_allowed_entry($name, $allowed = null) {
981 - if (null === $allowed) {
982 - $allowed = self::allowed_pack_extensions();
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' ) );
983 433 }
984 434
985 - $segments = explode('.', strtolower(wp_basename($name)));
435 + $this->manifest = json_decode( $manifest_content, true );
436 + $this->removeLog( 'temp' );
986 437
987 - // No extension, or a leading-dot name whose only "segment" is empty.
988 - if (count($segments) < 2 || '' === $segments[0]) {
989 - return false;
990 - }
438 + // TODO: Read & Broadcast the LOG for waiting list
439 + // $this->sse_log( 'plugin', 'Installing required plugins', '--', 'updateLog', 'processing' );
440 + // // $this->sse_log( 'extra-content', 'Import Extra Contents (i.e: Forms)', '--', 'updateLog', 'processing' );
441 + // $this->sse_log( 'templates', 'Import Templates (i.e: Header, Footer etc)', '--', 'updateLog', 'processing' );
442 + // // $this->sse_log( 'content', 'Import Pages, Posts etc', '--', 'updateLog', 'processing' );
443 + // $this->sse_log( 'wp-content', 'Importing Pages, Posts, Navigation, etc', '--', 'updateLog', 'processing' );
444 + // $this->sse_log( 'finalize', 'Finalizing Your Imports', '--', 'updateLog', 'processing' );
445 + }
991 446
992 - $extension = array_pop($segments);
993 - if (!isset($allowed[$extension])) {
994 - return false;
995 - }
447 + private function skipped_plugin(): bool {
448 + return empty( $this->request_params['plugins'] ) || ! is_array( $this->request_params['plugins'] );
449 + }
996 450
997 - $executable = array_fill_keys(self::EXECUTABLE_SEGMENTS, true);
451 + private function install_dependencies() {
452 + $allowed_themes = ['hello-elementor', 'twentytwentyfour'];
998 453
999 - // array_slice() drops the base name; only what sits between it and the
1000 - // extension is a masking risk.
1001 - foreach (array_slice($segments, 1) as $segment) {
1002 - if (isset($executable[$segment])) {
1003 - return false;
1004 - }
1005 - }
454 + if ( ! empty( $this->request_params['theme'] ) && is_array( $this->request_params['theme'] ) ) {
455 + // activate theme
456 + $theme = $this->request_params['theme'];
1006 457
1007 - return true;
1008 - }
458 + $this->before_install_hook();
1009 459
1010 - /**
1011 - * Deletes anything an extracted pack has no business containing.
1012 - *
1013 - * The fallback extractor below refuses these entries outright, but WordPress
1014 - * core's unzip_file() runs first and has no such filter, so the guard has to
1015 - * exist on the extracted tree as well.
1016 - *
1017 - * @param string $dir Extracted pack root.
1018 - *
1019 - * @return int Number of files removed.
1020 - */
1021 - protected function purge_disallowed_files($dir) {
1022 - if (empty($dir) || !is_dir($dir)) {
1023 - return 0;
1024 - }
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'] );
1025 464
1026 - $removed = 0;
1027 - $allowed = self::allowed_pack_extensions();
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
1028 481
1029 - try {
1030 - $files = new \RecursiveIteratorIterator(
1031 - new \RecursiveDirectoryIterator($dir, \RecursiveDirectoryIterator::SKIP_DOTS),
1032 - \RecursiveIteratorIterator::CHILD_FIRST
1033 - );
1034 -
1035 - foreach ($files as $fileinfo) {
1036 - if (!$fileinfo->isFile() || self::is_allowed_entry($fileinfo->getFilename(), $allowed)) {
1037 - continue;
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 + ];
1038 495 }
496 + }
1039 497
1040 - if (@unlink($fileinfo->getPathname())) {
1041 - $removed++;
1042 - Helper::log($fileinfo->getPathname(), 'unzip: removed disallowed file from extracted pack', 'warning');
1043 - }
1044 - }
1045 - } catch (\Exception $e) {
1046 - Helper::log($e->getMessage(), 'purge_disallowed_files', 'warning');
498 + $this->after_install_hook();
1047 499 }
1048 -
1049 - return $removed;
1050 - }
1051 -
1052 - /**
1053 - * Unzip a specified ZIP file to a location on the Filesystem.
1054 - *
1055 - * Why this custom extractor exists: some Templately packs carry entries with
1056 - * a leading "./" (or embedded "/./") path segment, which WordPress core's
1057 - * unzip_file() fails to extract. This method extracts with PHP's native
1058 - * ZipArchive and normalizes the redundant "./" segments (see
1059 - * normalize_zip_entry_name()) so the pack still imports. It is a fallback:
1060 - * self::unzip() only calls it after the core unzip_file() returns a WP_Error.
1061 - *
1062 - * Each archive member is validated before extraction rather than
1063 - * calling ZipArchive::extractTo() blindly: entries that resolve outside the
1064 - * destination (path traversal / "Zip Slip"), absolute paths, and Windows
1065 - * drive-letter paths are skipped via validate_file(), mirroring the guard
1066 - * WordPress core applies in _unzip_file_ziparchive(). Redundant "./" path
1067 - * segments are normalized first so members land at their intended location.
1068 - *
1069 - * @param string $file Full path and filename of ZIP archive.
1070 - * @param string $to Full path on the filesystem to extract archive to.
1071 - * @return true|WP_Error True on success, WP_Error on failure.
1072 - */
1073 - function unzip_file($file, $to) {
1074 - $zip = new \ZipArchive;
1075 -
1076 - $res = $zip->open($file);
1077 - if ($res !== TRUE) {
1078 - return new \WP_Error('zip_error_' . $zip->status, $zip->getStatusString());
500 + else {
501 + $this->removeLog( 'theme' );
1079 502 }
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'] );
1080 506
1081 - // Close the archive handle on every exit path (success, mid-loop
1082 - // exception, or early return) so it is never leaked.
1083 - try {
1084 - $to = trailingslashit($to);
507 + $total_plugin_installed = $total_plugin;
508 + $_installed_plugins = 0;
1085 509
1086 - $allowed_extensions = self::allowed_pack_extensions();
510 + $this->before_install_hook();
1087 511
1088 - for ($i = 0; $i < $zip->numFiles; $i++) {
1089 - $name = $zip->getNameIndex($i);
1090 - if ($name === false) {
1091 - continue;
1092 - }
512 + foreach ( $this->request_params['plugins'] as $dependency ) {
513 + $this->sse_log( 'plugin', 'Installing required plugins: ' . $dependency['name'], floor( ( 100 * $_installed_plugins / $total_plugin ) ) );
1093 514
1094 - // Normalize redundant "./" segments so the destination path
1095 - // is computed from a clean entry name.
1096 - $name = $this->normalize_zip_entry_name($name);
1097 - if ($name === '') {
1098 - continue; // Archive root (e.g. a "./" entry).
1099 - }
515 + $dependency['slug'] = $dependency['plugin_original_slug'];
516 + $plugin_status = Installer::get_instance()->install( $dependency );
1100 517
1101 - // Skip the OS X-created __MACOSX directory.
1102 - if (strpos($name, '__MACOSX/') === 0) {
1103 - continue;
1104 - }
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 + ] );
1105 527
1106 - // Don't extract invalid files: reject "../" traversal,
1107 - // absolute, and drive-letter paths so no member can be
1108 - // written outside $to. Log the skipped entry name so a
1109 - // hostile or corrupt pack leaves a forensic trail rather
1110 - // than silently extracting only part of its contents.
1111 - if (0 !== validate_file($name)) {
1112 - Helper::log($name, 'unzip_file: skipped unsafe archive entry', 'warning');
1113 - continue;
1114 - }
528 + if(isset($dependency['mustHave']) && $dependency['mustHave']){
529 + $this->removeLog( 'plugin' );
530 + $this->throw( 'Installation Failed: ' . $dependency['name'] . ' (' . ( $plugin_status['message'] ?? '' ) . ')' );
531 + }
1115 532
1116 - // validate_file() stops a member escaping $to; it says nothing
1117 - // about what the member *is*. $to lives under web-served
1118 - // wp-uploads, so an executable member would be directly
1119 - // requestable.
1120 - if (!self::is_allowed_entry($name, $allowed_extensions)) {
1121 - Helper::log($name, 'unzip_file: skipped disallowed archive entry', 'warning');
1122 - continue;
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--;
1123 542 }
1124 -
1125 - if (substr($name, -1) === '/') {
1126 - // Directory entry.
1127 - wp_mkdir_p($to . untrailingslashit($name));
1128 - continue;
1129 - }
1130 -
1131 - $contents = $zip->getFromIndex($i);
1132 - if ($contents === false) {
1133 - continue;
1134 - }
1135 -
1136 - $target = $to . $name;
1137 - wp_mkdir_p(dirname($target));
1138 - file_put_contents($target, $contents); // phpcs:ignore
1139 543 }
1140 544
1141 - return true;
1142 - } catch (\Throwable $th) {
1143 - return new \WP_Error('exception_caught', $th->getMessage());
1144 - } finally {
1145 - $zip->close();
1146 - }
1147 - }
545 + $this->after_install_hook();
1148 546
1149 - /**
1150 - * Removes redundant current-directory ("./") segments from a ZIP entry path.
1151 - *
1152 - * Some archive tools store entry names with a leading "./" or embedded "/./"
1153 - * segment (for example "./manifest.json" or "content/./page.json"). Parent
1154 - * ("..") segments are intentionally left untouched so validate_file() can
1155 - * still reject them.
1156 - *
1157 - * @param string $name A ZIP archive entry path.
1158 - * @return string The entry path with current-directory segments removed.
1159 - */
1160 - protected function normalize_zip_entry_name($name) {
1161 - // Fast path: bail when there is no current-directory segment to remove.
1162 - if ('.' !== $name
1163 - && strpos($name, './') !== 0
1164 - && strpos($name, '/./') === false
1165 - && substr($name, -2) !== '/.'
1166 - ) {
1167 - return $name;
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' );
1168 558 }
1169 559
1170 - // A trailing slash, or a trailing "/." or bare ".", denotes a directory.
1171 - $is_directory = substr($name, -1) === '/' || substr($name, -2) === '/.' || '.' === $name;
1172 560
1173 - $segments = array();
1174 - foreach (explode('/', $name) as $segment) {
1175 - if ('.' !== $segment) {
1176 - $segments[] = $segment;
1177 - }
1178 - }
1179 -
1180 - $name = implode('/', $segments);
1181 -
1182 - // Preserve the trailing slash that marks a directory entry.
1183 - if ($is_directory && '' !== $name && substr($name, -1) !== '/') {
1184 - $name .= '/';
1185 - }
1186 -
1187 - return $name;
561 + $this->update_session_data( [
562 + 'dependency_data' => json_encode($this->dependency_data),
563 + ] );
564 + // $this->sse_log( 'plugin', 'Skipped Installing Plugins', '--', 'updateLog', 'skipped' );
1188 565 }
1189 566
1190 - /**
1191 - * @throws Exception
1192 - */
1193 - private function read_manifest($dir_path) {
1194 - $manifest_content = file_get_contents($dir_path . 'manifest.json');
1195 - if (empty($manifest_content)) {
1196 - $this->throw(__('Cannot be imported, as the manifest file is corrupted', 'templately'));
1197 - }
1198 -
1199 - $manifest_content = json_decode($manifest_content, true);
1200 - $this->removeLog('temp');
1201 -
1202 - return $manifest_content;
1203 - // TODO: Read & Broadcast the LOG for waiting list
1204 - // $this->sse_log( 'plugin', 'Installing required plugins', '--', 'updateLog', 'processing' );
1205 - // // $this->sse_log( 'extra-content', 'Import Extra Contents (i.e: Forms)', '--', 'updateLog', 'processing' );
1206 - // $this->sse_log( 'templates', 'Import Templates (i.e: Header, Footer etc)', '--', 'updateLog', 'processing' );
1207 - // // $this->sse_log( 'content', 'Import Pages, Posts etc', '--', 'updateLog', 'processing' );
1208 - // $this->sse_log( 'wp-content', 'Importing Pages, Posts, Navigation, etc', '--', 'updateLog', 'processing' );
1209 - // $this->sse_log( 'finalize', 'Finalizing Your Imports', '--', 'updateLog', 'processing' );
1210 - }
1211 -
1212 - private function skipped_plugin(): bool {
1213 - return empty($this->request_params['plugins']) || !is_array($this->request_params['plugins']);
1214 - }
1215 -
1216 -
1217 567 private function before_install_hook() {
1218 568 // remove_all_actions( 'wp_loaded' );
1219 569 // remove_all_actions( 'after_setup_theme' );
1220 570 // remove_all_actions( 'plugins_loaded' );
@@ -1237,303 +587,55 @@
1237 587 private function start_content_import() {
1238 588 add_filter('upload_mimes', array($this, 'allow_svg_upload'));
1239 589 add_filter('elementor/files/allow_unfiltered_upload', '__return_true');
1240 590
1241 - $request_params = $this->get_session_data();
1242 -
1243 - $import = new Import(array_merge($request_params, [
1244 - 'origin' => $this,
1245 - 'manifest' => $this->manifest,
1246 - ]));
591 + $import = new Import( $this );
1247 592 $imported_data = $import->run();
1248 593
1249 - $import_status = $this->handle_import_status('success');
594 + $this->handle_import_status('success');
1250 595
1251 596 update_option('templately_flush_rewrite_rules', true, false);
1252 597
1253 - $normalized_data = $this->normalize_imported_data($imported_data);
1254 - // Use timeout-aware wait handler for AI content processing
1255 - if(!empty($request_params['ai_page_ids']) && empty($normalized_data['ai_content']['processed']['credit_cost'])){
1256 - $processed_pages = get_option("templately_ai_processed_pages", []);
1257 - $updated_ids = $processed_pages[$request_params['process_id']] ?? [];
1258 -
1259 - // Use the static timeout-aware wait handler from AIUtils.
1260 - // ai_page_ids must be FLATTENED — the handler counts it against the
1261 - // number of processed pages (see AIUtils::flatten_ai_page_ids).
1262 - AIUtils::handle_sse_wait_with_timeout(
1263 - $this->session_id,
1264 - 'ai_content_import_time',
1265 - $updated_ids,
1266 - AIUtils::flatten_ai_page_ids($request_params['ai_page_ids']),
1267 - [$this, 'sse_message'],
1268 - [
1269 - 'name' => 'ai-content',
1270 - 'message' => __('Missing Credit Cost', 'templately'),
1271 - ],
1272 - null, // No specific template ID for this context
1273 - 30
1274 - );
1275 - }
1276 -
1277 - // Get skipped items if skip feature was enabled
1278 - $skipped_items = SessionData::get_skipped_items($this->session_id);
1279 - if (!empty($skipped_items)) {
1280 - $normalized_data['skipped_items'] = $skipped_items;
1281 - }
1282 -
1283 - $this->sse_message([
598 + $this->sse_message( [
1284 599 'type' => 'complete',
1285 600 'action' => 'complete',
1286 - 'results' => $normalized_data,
1287 - ]);
1288 -
1289 - update_user_meta(get_current_user_id(), 'templately_fsi_pack_id', $request_params["id"]);
1290 - if(!empty($import_status['hasFeedback'])){
1291 - update_user_meta(get_current_user_id(), 'templately_fsi_complete', 'done');
1292 - }
1293 - else{
1294 - update_user_meta(get_current_user_id(), 'templately_fsi_complete', true);
1295 - }
1296 -
1297 - do_action('templately_fsi_import_complete', $normalized_data);
1298 - $this->clear_data_file($request_params);
601 + 'results' => $this->normalize_imported_data( $imported_data )
602 + ] );
1299 603 }
1300 604
1301 - private function clear_data_file($request_params){
1302 - if(defined('TEMPLATELY_DEV') && TEMPLATELY_DEV){
1303 - return;
1304 - }
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'] : [];
1305 608
1306 - // Handle directory cleanup
1307 - Utils::cleanup_directory($this->dir_path);
1308 - $upload_dir = wp_upload_dir();
1309 -
1310 - // Always save to preview directory for AI content workflow
1311 - $session_id = $request_params['session_id'] ?? '';
1312 - $pack_id = $request_params['id'] ?? '';
1313 -
1314 - // Set up directory paths for cleanup
1315 - $root_dir = $request_params['root_dir'] ?? trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp';
1316 - $prv_dir = $request_params['prv_dir'] ?? trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'preview';
1317 -
1318 - $processed_data = AIUtils::get_ai_process_data_by_session_id($session_id);
1319 -
1320 - if (!empty($pack_id) && !empty($session_id)) {
1321 - // Clean session data - keep only current session, remove others with same pack_id
1322 - // Modified to use SessionData directly
1323 - $removed_session_ids = SessionData::clean_by_pack_id($pack_id, $session_id);
1324 - }
1325 -
1326 - // Also run general expiration cleanup (7 days default)
1327 - // Modified to use SessionData directly
1328 - if (mt_rand(1, 100) <= 5) { // 5% chance to run on any import finish
1329 - $expired_cleanup = SessionData::cleanup_expired(7);
1330 -
1331 - // Clean AI process data - keep only current process, remove others with same pack_id
1332 - $current_process_id = !empty($processed_data['process_id']) ? $processed_data['process_id'] : null;
1333 - $removed_process_ids = AIUtils::clean_ai_process_data_by_pack_id($pack_id, $current_process_id);
1334 -
1335 - // Directory-based cleanup for session data directories
1336 - $this->cleanup_session_directories($root_dir, $pack_id, $session_id);
1337 -
1338 - // Directory-based cleanup for AI process data directories
1339 - $this->cleanup_ai_process_directories($prv_dir, $pack_id, $current_process_id);
1340 -
1341 - // Log cleanup results if in dev mode
1342 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1343 - if (!empty($removed_session_ids)) {
1344 - error_log('Templately: Cleaned up session IDs: ' . implode(', ', $removed_session_ids));
1345 - }
1346 - if (!empty($removed_process_ids)) {
1347 - error_log('Templately: Cleaned up process IDs: ' . implode(', ', $removed_process_ids));
1348 - }
1349 - }
1350 - }
1351 - }
1352 -
1353 - /**
1354 - * Directory-based cleanup for session data directories
1355 - * Scans the actual filesystem directories and removes directories that match cleanup criteria
1356 - *
1357 - * @param string $root_dir The root directory containing session directories
1358 - * @param string $pack_id The pack ID to match for cleanup
1359 - * @param string $current_session_id The current session ID to preserve
1360 - */
1361 - private function cleanup_session_directories($root_dir, $pack_id, $current_session_id) {
1362 - if (empty($root_dir) || !is_dir($root_dir) || empty($pack_id) || empty($current_session_id)) {
1363 - return;
1364 - }
1365 -
1366 - try {
1367 - // Get all session data to check pack_id associations
1368 - $all_session_data = SessionData::get_all_data();
1369 -
1370 - // Scan the actual directories in the filesystem
1371 - $directories = scandir($root_dir);
1372 - if ($directories === false) {
1373 - return;
1374 - }
1375 -
1376 - foreach ($directories as $dir_name) {
1377 - // Skip current directory, parent directory, and current session
1378 - if ($dir_name === '.' || $dir_name === '..' || $dir_name === $current_session_id) {
1379 - continue;
1380 - }
1381 -
1382 - $dir_path = trailingslashit($root_dir) . $dir_name;
1383 -
1384 - // Only process actual directories
1385 - if (!is_dir($dir_path)) {
1386 - continue;
1387 - }
1388 -
1389 - // Check if this directory should be cleaned up
1390 - $should_cleanup = false;
1391 -
1392 - // If we have session data for this directory, check if it matches the pack_id
1393 - if (isset($all_session_data[$dir_name]) &&
1394 - isset($all_session_data[$dir_name]['id']) &&
1395 - $all_session_data[$dir_name]['id'] === $pack_id) {
1396 - $should_cleanup = true;
1397 - } else if (!isset($all_session_data[$dir_name])) {
1398 - // This is an orphaned directory with no corresponding session data
1399 - $should_cleanup = true;
1400 - }
1401 -
1402 - if ($should_cleanup) {
1403 - Utils::cleanup_directory($dir_path);
1404 -
1405 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1406 - error_log('Templately: Cleaned up session directory: ' . $dir_name);
1407 - }
1408 - }
1409 - }
1410 - } catch (Exception $e) {
1411 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1412 - error_log('Templately: Error during session directory cleanup: ' . $e->getMessage());
1413 - }
1414 - }
1415 - }
1416 -
1417 - /**
1418 - * Directory-based cleanup for AI process data directories
1419 - * Scans the actual filesystem directories and removes directories that match cleanup criteria
1420 - *
1421 - * @param string $prv_dir The preview directory containing process directories
1422 - * @param string $pack_id The pack ID to match for cleanup
1423 - * @param string $current_process_id The current process ID to preserve (optional)
1424 - */
1425 - private function cleanup_ai_process_directories($prv_dir, $pack_id, $current_process_id = null) {
1426 - if (empty($prv_dir) || !is_dir($prv_dir) || empty($pack_id)) {
1427 - return;
1428 - }
1429 -
1430 - try {
1431 - // Get all AI process data to check pack_id associations
1432 - $ai_process_data = AIUtils::get_ai_process_data();
1433 -
1434 - // Scan the actual directories in the filesystem
1435 - $directories = scandir($prv_dir);
1436 - if ($directories === false) {
1437 - return;
1438 - }
1439 -
1440 - foreach ($directories as $dir_name) {
1441 - // Skip current directory, parent directory, and current process
1442 - if ($dir_name === '.' || $dir_name === '..' ||
1443 - (!empty($current_process_id) && $dir_name === $current_process_id)) {
1444 - continue;
1445 - }
1446 -
1447 - $dir_path = trailingslashit($prv_dir) . $dir_name;
1448 -
1449 - // Only process actual directories
1450 - if (!is_dir($dir_path)) {
1451 - continue;
1452 - }
1453 -
1454 - // Check if this directory should be cleaned up
1455 - $should_cleanup = false;
1456 -
1457 - // If we have process data for this directory, check if it matches the pack_id
1458 - if (isset($ai_process_data[$dir_name]) &&
1459 - is_array($ai_process_data[$dir_name]) &&
1460 - isset($ai_process_data[$dir_name]['pack_id']) &&
1461 - $ai_process_data[$dir_name]['pack_id'] === $pack_id) {
1462 - $should_cleanup = true;
1463 - } else if (!isset($ai_process_data[$dir_name])) {
1464 - // This is an orphaned directory with no corresponding process data
1465 - $should_cleanup = true;
1466 - }
1467 -
1468 - if ($should_cleanup) {
1469 - Utils::cleanup_directory($dir_path);
1470 -
1471 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1472 - error_log('Templately: Cleaned up AI process directory: ' . $dir_name);
1473 - }
1474 - }
1475 - }
1476 - } catch (Exception $e) {
1477 - if (defined('TEMPLATELY_DEV') && TEMPLATELY_DEV) {
1478 - error_log('Templately: Error during AI process directory cleanup: ' . $e->getMessage());
1479 - }
1480 - }
1481 - }
1482 -
1483 - private function normalize_imported_data($data) {
1484 - $request_params = $this->get_session_data();
1485 - $attachments = !empty($data['attachments']['succeed']) ? count($data['attachments']['succeed']) : 0;
1486 - $attachments_fail = !empty($data['attachments']['failed']) ? count($data['attachments']['failed']) : 0;
1487 - $attachments_errors = !empty($data['attachments_errors']) ? $data['attachments_errors'] : [];
1488 - $templates = !empty($data['templates']['succeed']) ? count($data['templates']['succeed']) : 0;
1489 - $template_types = !empty($data['templates']['template_types']) ? $data['templates']['template_types'] : [];
1490 - $dependency_data = !empty($data['dependency_data']) ? $data['dependency_data'] : [];
1491 -
1492 609 $post_types = [];
1493 610 $content_templates = [];
1494 - if (!empty($data['content']) && is_array($data['content'])) {
1495 - foreach ($data['content'] as $type => $type_data) {
1496 - $content_templates[$type] = !empty($type_data['succeed']) ? count($type_data['succeed']) : 0;
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;
1497 614 $post_types[] = $this->get_post_type_label_by_slug($type);
1498 615 }
1499 616 }
1500 617
1501 618 $contents = [];
1502 - if (!empty($data['wp-content']) && is_array($data['wp-content'])) {
1503 - foreach ($data['wp-content'] as $type => $type_data) {
1504 - $contents[$type] = !empty($type_data['succeed']) ? count($type_data['succeed']) : 0;
1505 - if (!in_array($type, ['wp_navigation', 'nav_menu_item'])) {
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'])){
1506 623 $post_types[] = $this->get_post_type_label_by_slug($type);
1507 624 }
1508 625 }
1509 626 }
1510 627
1511 - $_processed_pages = AIUtils::get_processed_pages_data($request_params['process_id']);
1512 - $ai_content = [
1513 - 'requested' => $request_params['ai_page_ids'] ?? [],
1514 - 'processed' => $_processed_pages,
1515 - ];
628 + Helper::log( $data );
1516 629
1517 -
1518 - $result = [
1519 - 'attachments' => $attachments,
1520 - 'attachments_fail' => $attachments_fail,
1521 - 'attachments_errors' => $attachments_errors,
1522 - 'templates' => $templates,
1523 - 'contents' => $content_templates,
1524 - 'wp-content' => $contents,
1525 - 'post_types' => $post_types,
1526 - 'template_types' => $template_types,
1527 - 'ai_content' => $ai_content,
1528 - 'dependency_data' => $dependency_data,
1529 - 'home_url' => home_url('/'),
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,
1530 637 ];
1531 -
1532 - Helper::log($data);
1533 - Helper::log($result);
1534 -
1535 - return $result;
1536 638 }
1537 639
1538 640 public function get_request_params() {
1539 641 return $this->request_params;
@@ -1545,12 +647,12 @@
1545 647 // // TODO: Implement the Revert Process.
1546 648 // }
1547 649 }
1548 650
1549 - public function redirect_for_archives($link, $post_id) {
1550 - $archive_settings = get_option('templately_post_archive');
1551 - if (!empty($archive_settings) && intval($archive_settings['post_id']) === intval($post_id)) {
1552 - $link = str_replace($post_id, $archive_settings['archive_id'], $link);
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 );
1553 655 }
1554 656
1555 657 return $link;
1556 658 }
@@ -1560,50 +662,46 @@
1560 662 $mimes['svg'] = 'image/svg+xml';
1561 663 return $mimes;
1562 664 }
1563 665
1564 - public function register_shutdown() {
666 + public function register_shutdown(){
1565 667 $status = connection_status();
1566 668 $last_error = error_get_last();
1567 - 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)) {
1568 - if (!empty($last_error['message'])) {
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'])) {
1569 671 $full_message = $last_error['message'];
1570 - $lines = explode("\n", $full_message);
1571 672
1572 - // For import status: first 5 lines
1573 - $import_status_message = implode("\n", array_slice($lines, 0, 5));
1574 - $import_status_message = str_replace(ABSPATH, 'ABSPATH/', $import_status_message);
673 + // Split the message at the first newline to remove the stack trace
674 + $message_parts = preg_split('/\n/', $full_message);
1575 675
1576 - // For SSE: first line only
1577 - $sse_message = $lines[0];
1578 - $sse_message = str_replace(ABSPATH, 'ABSPATH/', $sse_message);
676 + // The error message is the first part
677 + $error_message = $message_parts[0];
1579 678 } else {
1580 679 // Generic error message
1581 - $import_status_message = sprintf(__("It seems we're experiencing technical difficulties. Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://templately.com/?support=open');
1582 - $sse_message = $import_status_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');
1583 681 }
1584 682
1585 - $this->handle_import_status('failed', $import_status_message);
1586 - $this->sse_message([
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( [
1587 687 'action' => 'error',
1588 688 'status' => 'error',
1589 689 'type' => "error",
1590 - 'retry' => true,
1591 690 'title' => __("Oops!", "templately"),
1592 - 'message' => $sse_message,
1593 - 'error' => $last_error,
691 + 'message' => $error_message,
1594 692 // 'position' => 'plugin',
1595 693 // 'progress' => '--',
1596 - ]);
694 + ] );
1597 695 }
1598 696
1599 - $this->debug_log("Shutdown:.....");
1600 - $this->debug_log("connection_status: " . $this->getConnectionStatusText());
1601 - $this->debug_log($last_error);
697 + $this->debug_log( "Shutdown:....." );
698 + $this->debug_log( "connection_status: " . $this->getConnectionStatusText() );
699 + $this->debug_log( $last_error );
1602 700 }
1603 701
1604 702 public function handle_import_status($status, $description = '') {
1605 - if ($this->is_import_status_handled === $status) {
703 + if ($this->is_import_status_handled) {
1606 704 Helper::log("Import status already handled: $status");
1607 705 return null;
1608 706 }
1609 707 $this->is_import_status_handled = $status;
@@ -1610,56 +708,29 @@
1610 708
1611 709 $download_key = $this->download_key;
1612 710
1613 711 $headers = [
1614 - 'Content-Type' => 'application/json',
712 + 'Content-Type' => 'application/json',
1615 713 'Authorization' => 'Bearer ' . $this->api_key,
1616 714 'download_key' => $download_key,
1617 715 'download-key' => $download_key,
1618 716 'x-templately-ip' => Helper::get_ip(),
1619 - 'x-templately-url' => home_url('/'),
717 + 'x-templately-url' => home_url( '/' ),
1620 718 ];
1621 719
720 + $args = [
721 + 'headers' => $headers,
722 + ];
1622 723
1623 - $request_params = $this->get_session_data();
1624 - if(isset($request_params['process_id']) && !empty($request_params['ai_page_ids'])){
1625 - $updated_ids = AIUtils::get_processed_pages_data($request_params['process_id']);
1626 - $updated_pages = $updated_ids['pages'] ?? [];
1627 - $ai_page_ids = array_reduce($request_params['ai_page_ids'], 'array_merge', array());
1628 724
1629 - $headers['x-templately-ai-process-id'] = $request_params['process_id'];
1630 - $headers['x-templately-ai-requested-pages'] = implode(',', $ai_page_ids);
1631 - $headers['x-templately-ai-updated-pages'] = implode(',', array_keys($updated_pages));
1632 - $headers['x-templately-ai-missing-pages'] = implode(',', array_diff($ai_page_ids, array_keys($updated_pages)));
1633 - $headers['x-templately-ai-credit-cost'] = $updated_ids['credit_cost'] ?? null;
1634 -
1635 - // Phase-2 (chat-from-site): the backend keys the generated site by the
1636 - // conversation uuid — no download_key is minted for AI bundles. Forward
1637 - // it so the success/failed endpoints can flip the conversation status
1638 - // (delivered / error) and persist the failure. The uuid was stored under
1639 - // the process data when the import was prepared (chatbot-import-prepare).
1640 - $ai_process_data = AIUtils::get_ai_process_data_by_process_id($request_params['process_id']);
1641 - if (!empty($ai_process_data['chat_id'])) {
1642 - $headers['x-templately-ai-chat-uuid'] = $ai_process_data['chat_id'];
1643 - }
1644 - }
1645 -
1646 -
1647 - $extra_headers = $headers;
1648 -
1649 - // Seconds the user spent idling in the customizer step, accumulated by
1650 - // the React import wizard. Sent so the backend can exclude it from the
1651 - // import-time metric (a customizer left open for hours would otherwise
1652 - // inflate the count). Absent for older clients, which the backend
1653 - // treats as zero idle.
1654 - $customizer_idle = isset($request_params['customizer_idle']) ? (int) $request_params['customizer_idle'] : 0;
1655 -
1656 725 if ($status === 'success') {
1657 - $body = ['type' => 'pack', 'customizer_idle' => $customizer_idle];
1658 - $response = Helper::make_api_post_request('v1/import/success', $body, $extra_headers);
726 + $url = $this->get_api_url( "v1", 'import/success');
727 + $args['body'] = json_encode(['type' => 'pack']);
728 + $response = wp_remote_post($url, $args);
1659 729 } elseif ($status === 'failed') {
1660 - $body = ['type' => 'pack', 'description' => $description ?: "Something Went wrong.....", 'customizer_idle' => $customizer_idle];
1661 - $response = Helper::make_api_post_request('v1/import/failed', $body, $extra_headers);
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);
1662 733 }
1663 734
1664 735 Helper::log($response);
1665 736
@@ -1666,15 +737,12 @@
1666 737 if (is_wp_error($response)) {
1667 738 // Handle error
1668 739 Helper::log($response->get_error_message());
1669 740 } else {
1670 -
1671 - SessionData::set($this->session_id, 'is_import_status_handled', $this->is_import_status_handled);
1672 741 // Handle success
1673 742 $body = wp_remote_retrieve_body($response);
1674 - $data = json_decode($body, true);
1675 743 // Do something with $body
1676 - return $data;
744 + return $body;
1677 745 }
1678 746
1679 747 return null;
1680 748 }
@@ -1694,557 +762,56 @@
1694 762 }
1695 763
1696 764 protected function get_post_type_label_by_slug($slug) {
1697 765 $post_type_obj = get_post_type_object($slug);
1698 - if ($post_type_obj) {
766 + if($post_type_obj) {
1699 767 return $post_type_obj->label;
1700 768 }
1701 769 return null;
1702 770 }
1703 771
1704 - public function import_info() {
772 + public function import_info(){
1705 773
1706 774 $platform = isset($_GET['platform']) ? $_GET['platform'] : 'elementor';
1707 775 $id = isset($_GET['id']) ? intval($_GET['id']) : 0;
1708 - $isAi = isset($_GET['isAi']) ? $_GET['isAi'] : false;
1709 776
1710 - $data = $this->fetch_pack_info_from_api($id, $isAi);
1711 - if (is_wp_error($data)) {
1712 - wp_send_json_error($data->get_error_message());
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 + ] );
786 +
787 + if( is_wp_error( $response ) ){
788 + wp_send_json_error( $response->get_error_message() );
1713 789 return;
1714 790 }
1715 -
1716 - $business_niches = get_option('templately_ai_business_niches', []);
1717 - $data['data']['business_niches'] = $business_niches;
1718 -
1719 - if (isset($data['data']['manifest'])) {
1720 - $data['data']['manifest'] = json_decode($data['data']['manifest'], true);
1721 - }
1722 - if (isset($data['data']['settings'])) {
1723 - $data['data']['settings'] = json_decode($data['data']['settings'], true);
1724 - }
1725 -
1726 - if ($isAi) {
1727 - // Get the latest AI process for the current API key
1728 - $last_ai_process = AIUtils::get_latest_ai_process_by_api_key($id);
1729 - if ($last_ai_process) {
1730 - // The cloud API key is stored on the process for server-side matching
1731 - // only. It must never reach the browser — under a global login it is
1732 - // the admin's credential, not the requesting user's.
1733 - unset($last_ai_process['api_key']);
1734 -
1735 - $data['data']['ai_process'] = $last_ai_process;
1736 - }
1737 -
1738 - if($last_ai_process && $id == ($last_ai_process['pack_id'] ?? null)){
1739 - // Read AI preview content directly from files using the common function
1740 - $session_id = $last_ai_process['session_id'] ?? null;
1741 - $ai_page_ids = $last_ai_process['ai_page_ids'] ?? [];
1742 - $dir_path = null;
1743 -
1744 - // Get session data to retrieve dir_path
1745 - if ($session_id) {
1746 - $session_data = SessionData::get_data($session_id);
1747 - $dir_path = $session_data['dir_path'] ?? null;
1748 - }
1749 -
1750 - // Use the common function to read AI template data if we have the required data
1751 - if ($session_id && $ai_page_ids && $dir_path) {
1752 - $data['data']['ai_preview_content'] = AIUtils::read_ai_template_data($session_id, $ai_page_ids, $dir_path);
1753 - } else {
1754 - $data['data']['ai_preview_content'] = [];
1755 - }
1756 - }
1757 - }
1758 -
1759 - // Return the response body
1760 - wp_send_json($data);
1761 - }
1762 -
1763 - private function fetch_pack_info_from_api($id, $isAi) {
1764 - $extra_headers = [
1765 - 'x-templately-is-ai' => $isAi,
1766 - ];
1767 - $response = Helper::make_api_get_request("v2/import/info/pack/$id", [], $extra_headers, 30);
1768 -
1769 - if (is_wp_error($response)) {
1770 - return new \WP_Error('api_error', $response->get_error_message());
1771 - }
1772 791 // If the response code is not 200, return the error message
1773 - if (wp_remote_retrieve_response_code($response) != 200) {
1774 - $error_message = $this->extract_error_from_response($response);
1775 - return new \WP_Error('api_error', $error_message);
792 + if( wp_remote_retrieve_response_code( $response ) != 200 ){
793 + wp_send_json_error( json_decode(wp_remote_retrieve_body( $response )) );
794 + return;
1776 795 }
796 + // If the response body is JSON and it contains an error, return the error message
1777 797 // Retrieve Data from Response Body.
1778 - $body = wp_remote_retrieve_body($response);
798 + $body = wp_remote_retrieve_body( $response );
1779 799 $data = json_decode($body, true);
1780 800
1781 801 if (isset($data['error'])) {
1782 - return new \WP_Error('api_error', $data['error']);
1783 - }
1784 -
1785 - return $data;
1786 - }
1787 -
1788 - public function import_global_settings() {
1789 - $id = isset($_GET['id']) ? intval($_GET['id']) : 0;
1790 - $isAi = isset($_GET['isAi']) ? $_GET['isAi'] : false;
1791 -
1792 - $data = $this->fetch_pack_info_from_api($id, $isAi);
1793 - if (is_wp_error($data)) {
1794 - wp_send_json_error($data->get_error_message());
802 + wp_send_json_error($data['error']);
1795 803 return;
1796 804 }
1797 805
1798 - $settings = [];
1799 - if (isset($data['data']['settings'])) {
1800 - $settings = json_decode($data['data']['settings'], true);
806 + if(isset($data['data']['manifest'])){
807 + $data['data']['manifest'] = json_decode($data['data']['manifest'], true);
1801 808 }
1802 -
1803 - wp_send_json_success(['settings' => $settings]);
1804 - }
1805 -
1806 - public function update_imported_list($type, $id) {
1807 - $imported_list = get_option('templately_fsi_imported_list', []);
1808 - if(!in_array($id, $imported_list[$type] ?? [])){
1809 - $imported_list[$type][] = $id;
1810 - update_option('templately_fsi_imported_list', $imported_list, false);
809 + if(isset($data['data']['settings'])){
810 + $data['data']['settings'] = json_decode($data['data']['settings'], true);
1811 811 }
1812 - }
1813 812
1814 - /**
1815 - *
1816 - *
1817 - * @return void
1818 - */
1819 - protected function add_revert_hooks() {
1820 - add_action('wp_insert_post', function ($post_id) {
1821 - $this->update_imported_list('posts', $post_id);
1822 - });
1823 - add_action('add_attachment', function ($post_id) {
1824 - $this->update_imported_list('attachment', $post_id);
1825 - });
1826 - add_action('created_term', function ($term_id, $tt_id, $taxonomy, $args) {
1827 - $this->update_imported_list('term', [$term_id, $taxonomy]);
1828 - }, 10, 4);
1829 - add_action('registered_taxonomy', function ($taxonomy, $object_type, $taxonomy_object) {
1830 - $this->update_imported_list('taxonomy', $taxonomy);
1831 - }, 10, 3);
1832 - add_action('fluentform/form_imported', function ($formId){
1833 - $this->update_imported_list('fluentform', $formId);
1834 - }, 10, 1);
813 + // Return the response body
814 + wp_send_json($data);
1835 815 }
1836 816
1837 - public static function has_revert(){
1838 - $options = Utils::get_backup_options();
1839 - $imported_list = get_option('templately_fsi_imported_list', []);
1840 - if(!empty($options) || !empty($imported_list)){
1841 - return true;
1842 - }
1843 - return false;
1844 - }
1845 -
1846 - public function import_revert() {
1847 -
1848 - // // Get the nonce value from the request (usually from $_POST or $_GET)
1849 - // $received_nonce = isset($_REQUEST['_wpnonce']) ? $_REQUEST['_wpnonce'] : '';
1850 -
1851 - // // Verify the nonce using wp_verify_nonce()
1852 - // $verified = wp_verify_nonce($received_nonce, 'templately_pack_import_revert_nonce');
1853 -
1854 - // if (!$verified) {
1855 - // wp_send_json_error("Nonce not verified.");
1856 - // }
1857 -
1858 - delete_option('templately_import_platform');
1859 -
1860 - $option_active = null;
1861 - $options_deleted = false;
1862 - $imported_list_deleted = false;
1863 - $options = Utils::get_backup_options();
1864 - $status_args = [ 'post_type' => 'templately_library' ];
1865 - $all_post_url = add_query_arg( [
1866 - "page" => "templately_settings",
1867 - "path" => "settings/elementor/miscellaneous",
1868 - ], admin_url('admin.php' ));
1869 - // wp_send_json_success([$options]);
1870 -
1871 - if(class_exists('Elementor\Plugin')){
1872 - $kits_manager = Plugin::$instance->kits_manager;
1873 - $option_active = $kits_manager::OPTION_ACTIVE;
1874 - $kit = $kits_manager->get_active_kit();
1875 -
1876 - if ( ! $kit->get_id() ) {
1877 - $kit = $kits_manager->create_default();
1878 - update_option( $kits_manager::OPTION_ACTIVE, $kit );
1879 - }
1880 - }
1881 -
1882 -
1883 - if (!empty($options) && is_array($options)) {
1884 - foreach ($options as $key => $value) {
1885 - if ('stylesheet' === $key) {
1886 - if (get_option('stylesheet') !== $value) {
1887 - switch_theme($value);
1888 - }
1889 - } else if($option_active === $key && class_exists('Elementor\Plugin')) {
1890 - $kits_manager->revert( (int) $kits_manager->get_active_id(), (int) $value, 0 );
1891 - $kit = $kits_manager->get_active_kit();
1892 - $settings = $kit->get_data('settings');
1893 - if ( isset( $settings['site_logo'] ) ) {
1894 - set_theme_mod( 'custom_logo', $settings['site_logo']['id'] );
1895 - }
1896 - } else {
1897 - update_option($key, $value);
1898 - }
1899 - delete_option("__templately_$key");
1900 - $options_deleted = true;
1901 - }
1902 - }
1903 -
1904 - $imported_list = get_option('templately_fsi_imported_list', []);
1905 - if (!empty($imported_list) && is_array($imported_list)) {
1906 - $_GET['force_delete_kit'] = 1; // Fallback GET Ready!
1907 - foreach ($imported_list as $type => $list) {
1908 - if (empty($list) || !is_array($list)) {
1909 - continue;
1910 - }
1911 - // Loop through each item ID and delete it
1912 - foreach ($list as $key => $item_id) {
1913 - switch ($type) {
1914 - case 'posts':
1915 - // making sure default kit don't get deleted.
1916 - if($option_active && isset($options[$option_active]) && $options[$option_active] == $item_id){
1917 - break;
1918 - }
1919 - wp_delete_post($item_id, true); // Set true for permanent deletion
1920 - break;
1921 - case 'attachment':
1922 - wp_delete_attachment($item_id, true); // Set true for permanent deletion
1923 - break;
1924 - case 'term':
1925 - list($term_id, $taxonomy) = $item_id;
1926 - wp_delete_term($term_id, $taxonomy); // Use corresponding taxonomy
1927 - break;
1928 - case 'taxonomy':
1929 - // Taxonomies cannot be directly deleted. Consider de-registering it.
1930 - break;
1931 - case 'fluentform':
1932 - if(class_exists('\FluentForm\App\Models\Form')){
1933 - \FluentForm\App\Models\Form::remove($item_id);
1934 - }
1935 - break;
1936 - }
1937 - }
1938 - }
1939 -
1940 - $imported_list_deleted = true;
1941 - delete_option('templately_fsi_imported_list');
1942 - }
1943 -
1944 -
1945 - if($options_deleted || $imported_list_deleted){
1946 - sleep(5);
1947 - wp_send_json_success([ 'options' => $options_deleted, 'imported_list' => $imported_list_deleted, 'site_url' => home_url(), 'redirect' => $all_post_url ]);
1948 - }
1949 -
1950 - wp_send_json_error([ 'options' => $options_deleted, 'imported_list' => $imported_list_deleted, 'site_url' => home_url() ]);
1951 - }
1952 -
1953 - /**
1954 - * Extract error response from JSON response body
1955 - * Returns the entire decoded JSON response if Content-Type is JSON,
1956 - * otherwise returns a generic HTTP error message
1957 - *
1958 - * @param array $response The response from wp_remote_get or similar
1959 - * @return mixed Decoded JSON response or generic HTTP error string
1960 - */
1961 - private function extract_error_from_response($response) {
1962 - try {
1963 - $content_type = wp_remote_retrieve_header($response, 'content-type');
1964 - if (!empty($content_type) && strpos($content_type, 'application/json') !== false) {
1965 - $body = wp_remote_retrieve_body($response);
1966 - $error_data = json_decode($body, true);
1967 - if ($error_data !== null) {
1968 - return $error_data;
1969 - }
1970 - }
1971 - } catch (Exception $e) {
1972 - // If JSON parsing fails, fall through to generic error
1973 - }
1974 -
1975 - // Fallback to generic HTTP error message
1976 - return __('API request failed with response code ', 'templately') . wp_remote_retrieve_response_code($response);
1977 - }
1978 -
1979 - public function google_font() {
1980 - $result = get_transient('templately-google-fonts');
1981 -
1982 - if (false == $result) {
1983 - $response = Helper::make_api_get_request('v2/google-font', [], [], 30);
1984 -
1985 - if (is_wp_error($response)) {
1986 - wp_send_json_error($response->get_error_message());
1987 - }
1988 -
1989 - if (wp_remote_retrieve_response_code($response) != 200) {
1990 - $error_message = $this->extract_error_from_response($response);
1991 - wp_send_json_error($error_message, wp_remote_retrieve_response_code($response));
1992 - }
1993 -
1994 - $body = wp_remote_retrieve_body($response);
1995 - $data = json_decode($body, true);
1996 -
1997 - if (!isset($data['status']) || $data['status'] !== 'success') {
1998 - wp_send_json_error('API response indicates failure.');
1999 - }
2000 -
2001 - if (!isset($data['data'])) {
2002 - wp_send_json_error('API response missing data.');
2003 - }
2004 -
2005 - $result = $data['data'];
2006 - set_transient('templately-google-fonts', $result, DAY_IN_SECONDS);
2007 - }
2008 -
2009 - wp_send_json_success($result);
2010 - }
2011 -
2012 - /**
2013 - * AJAX handler: merge global kit settings into template content.
2014 - *
2015 - * Accepts (via POST JSON body):
2016 - * platform - 'elementor' | 'gutenberg'
2017 - * template - the template data array returned by /v1/insert
2018 - * settings - the kit settings array from import_info
2019 - *
2020 - * Returns the template data with globals resolved, or an error.
2021 - * The JS side should fall back to the unmerged template on any error.
2022 - */
2023 - public function merge_settings_with_template() {
2024 - $body = file_get_contents( 'php://input' );
2025 - $data = json_decode( $body, true );
2026 -
2027 - $platform = isset( $data['platform'] ) ? sanitize_text_field( $data['platform'] ) : 'elementor';
2028 - $template = $data['template'] ?? null;
2029 - $settings = $data['settings'] ?? null;
2030 -
2031 - if ( empty( $template ) || ! is_array( $template ) ) {
2032 - wp_send_json_error( 'Invalid template data.' );
2033 - return;
2034 - }
2035 -
2036 - if ( empty( $settings ) || ! is_array( $settings ) ) {
2037 - // No settings to merge – return template unchanged.
2038 - wp_send_json_success( $template );
2039 - return;
2040 - }
2041 -
2042 - try {
2043 - if ( $platform === 'elementor' ) {
2044 - if ( ! empty( $template['content'] ) && is_array( $template['content'] ) ) {
2045 - $template['content'] = ElementorSettingsMerger::merge( $template['content'], $settings );
2046 - }
2047 - } elseif ( $platform === 'gutenberg' ) {
2048 - if ( ! empty( $template['content'] ) && is_string( $template['content'] ) ) {
2049 - $template['content'] = GutenbergSettingsMerger::merge( $template['content'], $settings );
2050 - }
2051 - }
2052 -
2053 - wp_send_json_success( $template );
2054 - } catch ( \Exception $e ) {
2055 - Helper::log( 'merge_settings_with_template error: ' . $e->getMessage() );
2056 - // Return error so JS can fall back to unmerged template.
2057 - wp_send_json_error( 'Settings merge failed: ' . $e->getMessage() );
2058 - }
2059 - }
2060 -
2061 - public function ai_get_json() {
2062 - // read json data from post body
2063 - $body = file_get_contents('php://input');
2064 - $data = json_decode($body, true);
2065 -
2066 - if(empty($data['ai_page_ids'])){
2067 - wp_send_json_error('Invalid ai_page_ids');
2068 - return;
2069 - }
2070 -
2071 - if(!isset($_GET['session_id'])){
2072 - wp_send_json_error('Invalid session_id');
2073 - return;
2074 - }
2075 -
2076 - $session_id = isset($_GET['session_id']) ? sanitize_text_field($_GET['session_id']) : null;
2077 - $process_id = $data['process_id'] ?? null;
2078 - $ai_page_ids = $data['ai_page_ids'] ?? null;
2079 -
2080 - $this->request_params = $this->get_session_data();
2081 - try {
2082 - $this->manifest = $this->read_manifest($this->request_params['dir_path']);
2083 - } catch (\Exception $th) {
2084 - wp_send_json_error($th->getMessage());
2085 - }
2086 -
2087 - if(!empty($session_id) && empty($process_id)){
2088 - if ( !empty($this->request_params['process_id']) ){
2089 - $process_id = $this->request_params['process_id'] ?? null;
2090 - } else {
2091 - $process_id = AIUtils::get_ai_process_id_by_session_id($session_id);
2092 - }
2093 - }
2094 -
2095 - if(empty($process_id)){
2096 - wp_send_json_error('Invalid process_id');
2097 - return;
2098 - }
2099 -
2100 - $process_data = AIUtils::get_ai_process_data_by_process_id($process_id);
2101 - if (!empty($process_data['preview_error'])) {
2102 - wp_send_json_error($process_data['preview_error']);
2103 - }
2104 -
2105 - // Use the new common function to read AI template data directly
2106 - $result = AIUtils::read_ai_template_data($session_id, $ai_page_ids, $this->request_params['dir_path']);
2107 -
2108 - // Check if this is called from polling endpoint and include additional data
2109 - $response_data = ['process_id' => $process_id, 'templates' => $result];
2110 -
2111 - if (isset($this->polling_is_last_part)) {
2112 - $response_data['is_last_part'] = $this->polling_is_last_part;
2113 -
2114 - // Clean up the polling property
2115 - unset($this->polling_is_last_part);
2116 - }
2117 -
2118 - wp_send_json_success($response_data);
2119 - }
2120 -
2121 - /**
2122 - * AJAX handler for polling AI template generation status on local sites
2123 - * Makes GET request to API endpoint and returns data in same format as ai_get_json()
2124 - */
2125 - public function ai_poll_template() {
2126 - // Read JSON data from post body
2127 - $body = file_get_contents('php://input');
2128 - $data = json_decode($body, true);
2129 -
2130 - $process_id = $data['process_id'] ?? null;
2131 - $ai_page_ids = $data['ai_page_ids'] ?? null;
2132 -
2133 - if(empty($process_id)){
2134 - wp_send_json_error('Invalid process_id');
2135 - return;
2136 - }
2137 -
2138 - // Validate and get AI process data using centralized method
2139 - $process_data = AIUtils::validate_and_get_process_data($process_id);
2140 - if (is_wp_error($process_data)) {
2141 - $this->ai_get_json();
2142 - return;
2143 - }
2144 -
2145 - $session_id = $process_data['session_id'];
2146 - $ai_page_ids = $process_data['ai_page_ids'];
2147 -
2148 - // Use the common polling function to handle all template processing
2149 - $polling_result = AIUtils::poll_for_template($process_id, $session_id, $ai_page_ids);
2150 -
2151 - if (!$polling_result) {
2152 - // Polling failed, fallback to ai_get_json
2153 - $this->ai_get_json();
2154 - return;
2155 - }
2156 -
2157 - // After polling and processing templates, call ai_get_json() to return the data
2158 - // This reuses all the existing logic without duplication
2159 - $this->ai_get_json();
2160 - }
2161 -
2162 - /**
2163 - * Process AI preview content following the ai_get_json() pattern
2164 - *
2165 - * @param string $process_id The AI process ID
2166 - * @param array $ai_page_ids The AI page IDs data structure
2167 - * @param array $ai_preview_ids The AI preview IDs to process
2168 - * @return array Processed AI content data
2169 - */
2170 - private function process_ai_preview_content($process_id, $ai_page_ids, $ai_preview_ids) {
2171 - if (empty($process_id) || empty($ai_page_ids) || empty($ai_preview_ids)) {
2172 - return [];
2173 - }
2174 -
2175 - $all_ai_process_data = AIUtils::get_ai_process_data();
2176 - if (empty($all_ai_process_data[$process_id])) {
2177 - return [];
2178 - }
2179 - $ai_process_data = $all_ai_process_data[$process_id];
2180 - $_REQUEST['is_lightspeed'] = 'true';
2181 - $_REQUEST['session_id'] = $ai_process_data['session_id'] ?? null;
2182 - // Initialize session data and manifest following ai_get_json() pattern
2183 - $this->request_params = $this->get_session_data();
2184 - $this->manifest = $this->read_manifest($this->request_params['dir_path']);
2185 -
2186 - // Create Finalizer instance with the same configuration as ai_get_json()
2187 - $finalizer = new Finalizer(array_merge($this->request_params, [
2188 - 'origin' => $this,
2189 - 'manifest' => $this->manifest,
2190 - ]));
2191 - $finalizer->process_id = $process_id;
2192 - $finalizer->ai_page_ids = $ai_page_ids;
2193 -
2194 - $result = [];
2195 -
2196 - // Process each AI preview ID
2197 - foreach ($ai_preview_ids as $preview_id) {
2198 - // Extract type and sub_type metadata from ai_page_ids structure
2199 - $type_info = $this->extract_content_metadata($preview_id, $ai_page_ids);
2200 -
2201 - if ($type_info) {
2202 - $finalizer->type = $type_info['type'];
2203 - $finalizer->sub_type = $type_info['sub_type'];
2204 -
2205 - // Check if this is AI content before processing
2206 - if ($finalizer->isAiContent($preview_id)) {
2207 - // Process AI content using AIContentHelper trait
2208 - $ai_result = $finalizer->processAiContent($preview_id);
2209 - if ($ai_result['is_ai'] && !empty($ai_result['template_json'])) {
2210 - $template_json = $ai_result['template_json'];
2211 - $result[$preview_id] = $template_json;
2212 - } else if ($finalizer->isAiFileSkipped($preview_id)) {
2213 - // Handle skipped AI files
2214 - $result[$preview_id] = [];
2215 - }
2216 - }
2217 - }
2218 - }
2219 -
2220 - return $result;
2221 - }
2222 -
2223 - /**
2224 - * Extract content metadata (type and sub_type) from ai_page_ids structure
2225 - *
2226 - * @param string $preview_id The preview ID to find
2227 - * @param array $ai_page_ids The AI page IDs data structure
2228 - * @return array|null Array with 'type' and 'sub_type' keys, or null if not found
2229 - */
2230 - private function extract_content_metadata($preview_id, $ai_page_ids) {
2231 - if (empty($ai_page_ids) || !is_array($ai_page_ids)) {
2232 - return null;
2233 - }
2234 -
2235 - // Search through the ai_page_ids structure to find the preview_id
2236 - foreach ($ai_page_ids as $key => $ids) {
2237 - if (is_array($ids) && in_array($preview_id, $ids)) {
2238 - // Extract type and sub_type from the key (e.g., 'content/page' or 'templates')
2239 - $type_arr = explode('/', $key);
2240 - return [
2241 - 'type' => $type_arr[0],
2242 - 'sub_type' => isset($type_arr[1]) ? $type_arr[1] : ''
2243 - ];
2244 - }
2245 - }
2246 -
2247 - return null;
2248 - }
2249 -
2250 -}
817 +}