PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.4.9
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.4.9
3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 3.1.10 All 111 releases
← All changes | includes/Core/Importer/FullSiteImport.php +96 -503 3.7.53.4.9 View file →
@@ -21,12 +21,9 @@
21 21 use Templately\Core\Importer\Exception\UnknownErrorException;
22 22 use Templately\Core\Importer\Runners\Finalizer;
23 23 use Templately\Core\Importer\Utils\LogHandler;
24 24 use Templately\Core\Importer\Utils\Utils;
25 -use Templately\Core\Importer\Utils\SessionData;
26 25 use Templately\Core\Importer\Utils\AIUtils;
27 -use Templately\Core\Importer\Utils\ElementorSettingsMerger;
28 -use Templately\Core\Importer\Utils\GutenbergSettingsMerger;
29 26 use Templately\Utils\Base;
30 27 use Templately\Utils\Helper;
31 28 use Templately\Utils\Installer;
32 29 use Templately\Utils\Options;
@@ -64,15 +61,13 @@
64 61 $this->add_ajax_action('import_status', $this);
65 62 $this->add_ajax_action('import', $this);
66 63 $this->add_ajax_action('import_revert', $this);
67 64 $this->add_ajax_action('import_info', $this);
68 - $this->add_ajax_action('import_global_settings', $this);
69 65 $this->add_ajax_action('import_close_feedback_modal', $this);
70 66 $this->add_ajax_action('feedback_form', $this);
71 67 $this->add_ajax_action('google_font', $this);
72 68 $this->add_ajax_action('ai_get_json', $this);
73 69 $this->add_ajax_action('ai_poll_template', $this);
74 - $this->add_ajax_action('merge_settings_with_template', $this);
75 70
76 71 add_action('admin_init', [$this, 'admin_init']);
77 72 // add_action('admin_notices', [$this, 'add_revert_button']);
78 73
@@ -123,14 +118,8 @@
123 118 }
124 119 }
125 120
126 121 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;
131 - }
132 -
133 122 $data = wp_unslash($_POST);
134 123
135 124 $upload_dir = wp_upload_dir();
136 125
@@ -141,21 +130,17 @@
141 130 if (is_wp_error($session_id)) {
142 131 wp_send_json_error(['message' => $session_id->get_error_message()]);
143 132 return;
144 133 }
145 - $session_data = SessionData::get_data($session_id);
134 + $session_data = Utils::get_session_data($session_id);
146 135 $data = array_merge($session_data, $data);
147 136 }
148 137 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();
138 + $session_id = uniqid();
154 139 }
155 140
156 - $tmp_dir = Helper::upload_dir('tmp');
157 - $prv_dir = Helper::upload_dir('preview');
141 + $tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
142 + $prv_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'preview' . DIRECTORY_SEPARATOR;
158 143
159 144 $this->session_id = $session_id;
160 145 $data['session_id'] = $session_id;
161 146
@@ -171,9 +156,9 @@
171 156 $data[ $key ] = $json !== null ? $json : $value;
172 157 }
173 158 }
174 159
175 - SessionData::save($session_id, $data);
160 + Utils::update_session_data($session_id, $data);
176 161
177 162
178 163 //clear previous revert backup
179 164 $options = Utils::get_backup_options();
@@ -200,10 +185,10 @@
200 185 wp_send_json_error(['message' => $session_id->get_error_message()]);
201 186 return;
202 187 }
203 188
204 - $tmp_dir = Helper::upload_dir('tmp');
205 - $prv_dir = Helper::upload_dir('preview');
189 + $tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
190 + $prv_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'preview' . DIRECTORY_SEPARATOR;
206 191
207 192 $this->session_id = $session_id;
208 193 $data['root_dir'] = $tmp_dir;
209 194 $data['prv_dir'] = $prv_dir;
@@ -221,9 +206,9 @@
221 206 $data[ $key ] = $json !== null ? $json : $value;
222 207 }
223 208 }
224 209
225 - SessionData::save($session_id, $data);
210 + Utils::update_session_data($session_id, $data);
226 211
227 212
228 213 return $data;
229 214 }
@@ -232,9 +217,9 @@
232 217 if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
233 218 exit;
234 219 }
235 220
236 - Utils::add_gd_editor_filter();
221 + add_filter( 'wp_image_editors', [ $this, 'wp_image_editors' ], 10, 1 );
237 222
238 223 define('TEMPLATELY_START_TIME', microtime(true));
239 224
240 225 register_shutdown_function( [ $this, 'register_shutdown' ] );
@@ -253,9 +238,12 @@
253 238 if(empty($progress['create_log_dir'])){
254 239 // Create Log Directory and if fail then chose option method
255 240 LogHandler::create_log_dir();
256 241
257 - SessionData::mark_step_complete($this->session_id, 'create_log_dir');
242 + $progress['create_log_dir'] = true;
243 + $this->update_session_data( [
244 + 'progress' => $progress,
245 + ] );
258 246 }
259 247
260 248 $_id = isset($this->request_params['id']) ? (int) $this->request_params['id'] : null;
261 249
@@ -272,9 +260,12 @@
272 260 * Download the zip
273 261 */
274 262 $this->download_zip( $_id, true );
275 263
276 - SessionData::mark_step_complete($this->session_id, 'download_zip');
264 + $progress['download_zip'] = true;
265 + $this->update_session_data( [
266 + 'progress' => $progress,
267 + ] );
277 268 }
278 269
279 270 /**
280 271 * Reading Manifest File
@@ -376,22 +367,16 @@
376 367
377 368 wp_send_json_success($result);
378 369 }
379 370
380 - // Modified get_session_data to use SessionData
371 + // Modified get_session_data to use the static version
381 372 public function get_session_data() {
382 - if ($session_id = SessionData::get_session_id()) {
383 - return SessionData::get_data($session_id);
384 - }
385 - return [];
373 + return Utils::get_session_data_by_id();
386 374 }
387 375
388 - // Modified update_session_data to use SessionData
376 + // Modified update_session_data to use the static version
389 377 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;
378 + return Utils::update_session_data_by_id($data);
394 379 }
395 380
396 381 public function initialize_props() {
397 382 $data = $this->get_session_data();
@@ -475,9 +460,9 @@
475 460 if ( ! $this->dev_mode && ! wp_doing_ajax() ) {
476 461 exit;
477 462 }
478 463
479 - Utils::add_gd_editor_filter();
464 + add_filter( 'wp_image_editors', [ $this, 'wp_image_editors' ], 10, 1 );
480 465
481 466
482 467 define('TEMPLATELY_START_TIME', microtime(true));
483 468
@@ -487,13 +472,9 @@
487 472
488 473 $this->finishRequestHeaders();
489 474
490 475 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 - }
495 -
476 + // TODO: Need to check if user is connected or not
496 477 if(!empty($_GET['session_id'])){
497 478 // Security: Sanitize session_id from user input
498 479 $session_id = AIUtils::sanitize_path_component(sanitize_text_field($_GET['session_id']), 'session_id');
499 480 if (is_wp_error($session_id)) {
@@ -513,13 +494,8 @@
513 494
514 495 // Trigger action hook for network admin multisite handling
515 496 do_action( 'templately_fsi_before_import', $this, $this->request_params );
516 497
517 - // Filter Child Type for Elementor Pro Promotion Widget
518 - if ( class_exists( '\Elementor\Plugin' ) ) {
519 - \Templately\Core\Platform\Elementor::register_child_type_filter();
520 - }
521 -
522 498 // Refresh progress after potential multisite creation
523 499 $progress = $this->request_params['progress'] ?? [];
524 500
525 501 if(empty($progress['create_log_dir'])){
@@ -525,9 +501,12 @@
525 501 if(empty($progress['create_log_dir'])){
526 502 // Create Log Directory and if fail then chose option method
527 503 LogHandler::create_log_dir();
528 504
529 - SessionData::mark_step_complete($this->session_id, 'create_log_dir');
505 + $progress['create_log_dir'] = true;
506 + $this->update_session_data( [
507 + 'progress' => $progress,
508 + ] );
530 509 $this->sse_message( [
531 510 'type' => 'eventLog',
532 511 'action' => 'eventLog',
533 512 'info' => 'create_log_dir',
@@ -552,9 +531,12 @@
552 531 * Check Writing Permission
553 532 */
554 533 $this->check_writing_permission();
555 534
556 - SessionData::mark_step_complete($this->session_id, 'check_writing_permission');
535 + $progress['check_writing_permission'] = true;
536 + $this->update_session_data( [
537 + 'progress' => $progress,
538 + ] );
557 539 }
558 540
559 541 if(empty($progress['download_zip'])){
560 542
@@ -562,9 +544,12 @@
562 544 * Download the zip
563 545 */
564 546 $this->download_zip( $_id );
565 547
566 - SessionData::mark_step_complete($this->session_id, 'download_zip');
548 + $progress['download_zip'] = true;
549 + $this->update_session_data( [
550 + 'progress' => $progress,
551 + ] );
567 552 $this->sse_message( [
568 553 'type' => 'continue',
569 554 'action' => 'continue',
570 555 'info' => 'download_zip',
@@ -632,8 +617,16 @@
632 617 // }
633 618 }
634 619
635 620
621 + public function wp_image_editors( $editors ) {
622 + // If GD is available, use only GD. Otherwise, fallback to all available editors.
623 + if ( is_callable( [ 'WP_Image_Editor_GD', 'test' ] ) && call_user_func( [ 'WP_Image_Editor_GD', 'test' ] ) ) {
624 + return [ 'WP_Image_Editor_GD' ];
625 + }
626 + return $editors;
627 + }
628 +
636 629 // Updated import_status method
637 630 public function import_status() {
638 631 $request_params = $this->get_session_data();
639 632
@@ -699,12 +692,9 @@
699 692 if (!is_writable($upload_dir['basedir'])) {
700 693 $this->throw(__('Upload directory is not writable.', 'templately'));
701 694 }
702 695
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');
696 + $this->tmp_dir = trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
707 697
708 698 if (!is_dir($this->tmp_dir)) {
709 699 wp_mkdir_p($this->tmp_dir);
710 700 }
@@ -737,10 +727,10 @@
737 727
738 728 // If the response body is JSON and it contains an error, throw an exception with the error message
739 729 if (isset($response_body['status']) && $response_body['status'] === 'error') {
740 730 $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');
731 + if(strpos($response_body['message'], 'https://wpdeveloper.com/support') === false){
732 + $support_message = sprintf(__(" Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://wpdeveloper.com/support');
743 733 }
744 734 $this->throw_non_retryable($response_body['message'] . $support_message);
745 735 }
746 736 }
@@ -748,9 +738,11 @@
748 738 }
749 739
750 740 $this->sse_log('download', __('Downloading Template Pack', 'templately'), 57);
751 741
752 - SessionData::set($this->session_id, 'download_key', $this->download_key);
742 + $this->update_session_data([
743 + 'download_key' => $this->download_key,
744 + ]);
753 745
754 746 // Security: Validate file path is within WordPress upload directory before writing
755 747 $validation = AIUtils::validate_file_path($this->filePath);
756 748 if (is_wp_error($validation)) {
@@ -776,22 +768,11 @@
776 768 $this->throw(__('WP_Filesystem cannot be initialized', 'templately'));
777 769 }
778 770 $unzip = unzip_file($this->filePath, $this->dir_path);
779 771 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 772 $unzip = $this->unzip_file($this->filePath, $this->dir_path);
787 773 }
788 774
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 775 $manifest_file = $this->dir_path . 'manifest.json';
795 776
796 777 // If manifest.json is missing, but any subdirectory contains manifest.json, move all its contents up and remove the subdirectory.
797 778 if ( ! file_exists( $manifest_file ) ) {
@@ -827,9 +808,9 @@
827 808 $error = $unzip->get_error_message();
828 809 if (empty($error)) {
829 810 // Generic error message
830 811 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');
812 + $error_message = sprintf(__("It seems we're experiencing technical difficulties. Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://wpdeveloper.com/support');
832 813 $this->throw($error_message);
833 814 } else {
834 815 $this->throw($unzip->get_error_message());
835 816 }
@@ -877,315 +858,34 @@
877 858
878 859
879 860
880 861 /**
881 - * Extensions a pack carries that WordPress does not accept as an upload.
882 - *
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.
903 - */
904 - const PACK_EXTENSIONS = [
905 - 'json', 'xml', 'ai',
906 - 'svg', 'svgz',
907 - 'woff', 'woff2', 'ttf', 'otf', 'eot',
908 - ];
909 -
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);
924 -
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;
929 - }
930 - }
931 -
932 - return $allowed;
933 - }
934 -
935 - /**
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.
948 - */
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();
983 - }
984 -
985 - $segments = explode('.', strtolower(wp_basename($name)));
986 -
987 - // No extension, or a leading-dot name whose only "segment" is empty.
988 - if (count($segments) < 2 || '' === $segments[0]) {
989 - return false;
990 - }
991 -
992 - $extension = array_pop($segments);
993 - if (!isset($allowed[$extension])) {
994 - return false;
995 - }
996 -
997 - $executable = array_fill_keys(self::EXECUTABLE_SEGMENTS, true);
998 -
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 - }
1006 -
1007 - return true;
1008 - }
1009 -
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 - }
1025 -
1026 - $removed = 0;
1027 - $allowed = self::allowed_pack_extensions();
1028 -
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;
1038 - }
1039 -
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');
1047 - }
1048 -
1049 - return $removed;
1050 - }
1051 -
1052 - /**
1053 862 * Unzip a specified ZIP file to a location on the Filesystem.
1054 863 *
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 864 * @param string $file Full path and filename of ZIP archive.
1070 865 * @param string $to Full path on the filesystem to extract archive to.
1071 866 * @return true|WP_Error True on success, WP_Error on failure.
1072 867 */
1073 868 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());
1079 - }
1080 -
1081 - // Close the archive handle on every exit path (success, mid-loop
1082 - // exception, or early return) so it is never leaked.
1083 869 try {
1084 - $to = trailingslashit($to);
870 + $zip = new \ZipArchive;
1085 871
1086 - $allowed_extensions = self::allowed_pack_extensions();
872 + $res = $zip->open($file);
873 + if ($res === TRUE) {
874 + $zip->extractTo($to);
875 + $zip->close();
1087 876
1088 - for ($i = 0; $i < $zip->numFiles; $i++) {
1089 - $name = $zip->getNameIndex($i);
1090 - if ($name === false) {
1091 - continue;
1092 - }
1093 -
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 - }
1100 -
1101 - // Skip the OS X-created __MACOSX directory.
1102 - if (strpos($name, '__MACOSX/') === 0) {
1103 - continue;
1104 - }
1105 -
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 - }
1115 -
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;
1123 - }
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
877 + return true;
1139 878 }
1140 -
1141 - return true;
1142 879 } catch (\Throwable $th) {
1143 880 return new \WP_Error('exception_caught', $th->getMessage());
1144 - } finally {
1145 - $zip->close();
1146 881 }
1147 - }
1148 882
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;
883 + if (isset($zip)) {
884 + return new \WP_Error('zip_error_' . $zip->status, $zip->getStatusString());
885 + } else {
886 + return new \WP_Error('unknown_error', '');
1168 887 }
1169 -
1170 - // A trailing slash, or a trailing "/." or bare ".", denotes a directory.
1171 - $is_directory = substr($name, -1) === '/' || substr($name, -2) === '/.' || '.' === $name;
1172 -
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;
1188 888 }
1189 889
1190 890 /**
1191 891 * @throws Exception
@@ -1255,16 +955,14 @@
1255 955 if(!empty($request_params['ai_page_ids']) && empty($normalized_data['ai_content']['processed']['credit_cost'])){
1256 956 $processed_pages = get_option("templately_ai_processed_pages", []);
1257 957 $updated_ids = $processed_pages[$request_params['process_id']] ?? [];
1258 958
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).
959 + // Use the static timeout-aware wait handler from AIUtils
1262 960 AIUtils::handle_sse_wait_with_timeout(
1263 961 $this->session_id,
1264 962 'ai_content_import_time',
1265 963 $updated_ids,
1266 - AIUtils::flatten_ai_page_ids($request_params['ai_page_ids']),
964 + $request_params['ai_page_ids'],
1267 965 [$this, 'sse_message'],
1268 966 [
1269 967 'name' => 'ai-content',
1270 968 'message' => __('Missing Credit Cost', 'templately'),
@@ -1273,14 +971,8 @@
1273 971 30
1274 972 );
1275 973 }
1276 974
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 975 $this->sse_message([
1284 976 'type' => 'complete',
1285 977 'action' => 'complete',
1286 978 'results' => $normalized_data,
@@ -1294,9 +986,9 @@
1294 986 update_user_meta(get_current_user_id(), 'templately_fsi_complete', true);
1295 987 }
1296 988
1297 989 do_action('templately_fsi_import_complete', $normalized_data);
1298 - $this->clear_data_file($request_params);
990 + // $this->clear_data_file($request_params);
1299 991 }
1300 992
1301 993 private function clear_data_file($request_params){
1302 994 if(defined('TEMPLATELY_DEV') && TEMPLATELY_DEV){
@@ -1316,19 +1008,13 @@
1316 1008 $prv_dir = $request_params['prv_dir'] ?? trailingslashit($upload_dir['basedir']) . 'templately' . DIRECTORY_SEPARATOR . 'preview';
1317 1009
1318 1010 $processed_data = AIUtils::get_ai_process_data_by_session_id($session_id);
1319 1011
1012 + // Clean up WordPress options data and corresponding directories
1320 1013 if (!empty($pack_id) && !empty($session_id)) {
1321 1014 // 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 - }
1015 + $removed_session_ids = Utils::clean_session_data_by_pack_id($pack_id, $session_id);
1325 1016
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 1017 // Clean AI process data - keep only current process, remove others with same pack_id
1332 1018 $current_process_id = !empty($processed_data['process_id']) ? $processed_data['process_id'] : null;
1333 1019 $removed_process_ids = AIUtils::clean_ai_process_data_by_pack_id($pack_id, $current_process_id);
1334 1020
@@ -1364,9 +1050,9 @@
1364 1050 }
1365 1051
1366 1052 try {
1367 1053 // Get all session data to check pack_id associations
1368 - $all_session_data = SessionData::get_all_data();
1054 + $all_session_data = Utils::get_all_session_data();
1369 1055
1370 1056 // Scan the actual directories in the filesystem
1371 1057 $directories = scandir($root_dir);
1372 1058 if ($directories === false) {
@@ -1577,9 +1263,9 @@
1577 1263 $sse_message = $lines[0];
1578 1264 $sse_message = str_replace(ABSPATH, 'ABSPATH/', $sse_message);
1579 1265 } else {
1580 1266 // 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');
1267 + $import_status_message = sprintf(__("It seems we're experiencing technical difficulties. Please try again or contact <a href='%s' target='_blank'>support</a>.", "templately"), 'https://wpdeveloper.com/support');
1582 1268 $sse_message = $import_status_message;
1583 1269 }
1584 1270
1585 1271 $this->handle_import_status('failed', $import_status_message);
@@ -1630,35 +1316,18 @@
1630 1316 $headers['x-templately-ai-requested-pages'] = implode(',', $ai_page_ids);
1631 1317 $headers['x-templately-ai-updated-pages'] = implode(',', array_keys($updated_pages));
1632 1318 $headers['x-templately-ai-missing-pages'] = implode(',', array_diff($ai_page_ids, array_keys($updated_pages)));
1633 1319 $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 1320 }
1645 1321
1646 1322
1647 1323 $extra_headers = $headers;
1648 1324
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 1325 if ($status === 'success') {
1657 - $body = ['type' => 'pack', 'customizer_idle' => $customizer_idle];
1326 + $body = ['type' => 'pack'];
1658 1327 $response = Helper::make_api_post_request('v1/import/success', $body, $extra_headers);
1659 1328 } elseif ($status === 'failed') {
1660 - $body = ['type' => 'pack', 'description' => $description ?: "Something Went wrong.....", 'customizer_idle' => $customizer_idle];
1329 + $body = ['type' => 'pack', 'description' => $description ?: "Something Went wrong....."];
1661 1330 $response = Helper::make_api_post_request('v1/import/failed', $body, $extra_headers);
1662 1331 }
1663 1332
1664 1333 Helper::log($response);
@@ -1667,9 +1336,11 @@
1667 1336 // Handle error
1668 1337 Helper::log($response->get_error_message());
1669 1338 } else {
1670 1339
1671 - SessionData::set($this->session_id, 'is_import_status_handled', $this->is_import_status_handled);
1340 + $this->update_session_data([
1341 + 'is_import_status_handled' => $this->is_import_status_handled,
1342 + ]);
1672 1343 // Handle success
1673 1344 $body = wp_remote_retrieve_body($response);
1674 1345 $data = json_decode($body, true);
1675 1346 // Do something with $body
@@ -1706,14 +1377,33 @@
1706 1377 $platform = isset($_GET['platform']) ? $_GET['platform'] : 'elementor';
1707 1378 $id = isset($_GET['id']) ? intval($_GET['id']) : 0;
1708 1379 $isAi = isset($_GET['isAi']) ? $_GET['isAi'] : false;
1709 1380
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());
1381 + $extra_headers = [
1382 + 'x-templately-is-ai' => $isAi,
1383 + ];
1384 + $response = Helper::make_api_get_request("v2/import/info/pack/$id", [], $extra_headers, 30);
1385 +
1386 + if (is_wp_error($response)) {
1387 + wp_send_json_error($response->get_error_message());
1713 1388 return;
1714 1389 }
1390 + // If the response code is not 200, return the error message
1391 + if (wp_remote_retrieve_response_code($response) != 200) {
1392 + $error_message = $this->extract_error_from_response($response);
1393 + wp_send_json_error($error_message, wp_remote_retrieve_response_code($response));
1394 + return;
1395 + }
1396 + // If the response body is JSON and it contains an error, return the error message
1397 + // Retrieve Data from Response Body.
1398 + $body = wp_remote_retrieve_body($response);
1399 + $data = json_decode($body, true);
1715 1400
1401 + if (isset($data['error'])) {
1402 + wp_send_json_error($data['error']);
1403 + return;
1404 + }
1405 +
1716 1406 $business_niches = get_option('templately_ai_business_niches', []);
1717 1407 $data['data']['business_niches'] = $business_niches;
1718 1408
1719 1409 if (isset($data['data']['manifest'])) {
@@ -1726,17 +1416,12 @@
1726 1416 if ($isAi) {
1727 1417 // Get the latest AI process for the current API key
1728 1418 $last_ai_process = AIUtils::get_latest_ai_process_by_api_key($id);
1729 1419 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 1420 $data['data']['ai_process'] = $last_ai_process;
1736 1421 }
1737 1422
1738 - if($last_ai_process && $id == ($last_ai_process['pack_id'] ?? null)){
1423 + if($last_ai_process && $id == $last_ai_process['pack_id']){
1739 1424 // Read AI preview content directly from files using the common function
1740 1425 $session_id = $last_ai_process['session_id'] ?? null;
1741 1426 $ai_page_ids = $last_ai_process['ai_page_ids'] ?? [];
1742 1427 $dir_path = null;
@@ -1742,9 +1427,9 @@
1742 1427 $dir_path = null;
1743 1428
1744 1429 // Get session data to retrieve dir_path
1745 1430 if ($session_id) {
1746 - $session_data = SessionData::get_data($session_id);
1431 + $session_data = Utils::get_session_data($session_id);
1747 1432 $dir_path = $session_data['dir_path'] ?? null;
1748 1433 }
1749 1434
1750 1435 // Use the common function to read AI template data if we have the required data
@@ -1759,51 +1444,8 @@
1759 1444 // Return the response body
1760 1445 wp_send_json($data);
1761 1446 }
1762 1447
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 - // 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);
1776 - }
1777 - // Retrieve Data from Response Body.
1778 - $body = wp_remote_retrieve_body($response);
1779 - $data = json_decode($body, true);
1780 -
1781 - 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());
1795 - return;
1796 - }
1797 -
1798 - $settings = [];
1799 - if (isset($data['data']['settings'])) {
1800 - $settings = json_decode($data['data']['settings'], true);
1801 - }
1802 -
1803 - wp_send_json_success(['settings' => $settings]);
1804 - }
1805 -
1806 1448 public function update_imported_list($type, $id) {
1807 1449 $imported_list = get_option('templately_fsi_imported_list', []);
1808 1450 if(!in_array($id, $imported_list[$type] ?? [])){
1809 1451 $imported_list[$type][] = $id;
@@ -2006,57 +1648,8 @@
2006 1648 set_transient('templately-google-fonts', $result, DAY_IN_SECONDS);
2007 1649 }
2008 1650
2009 1651 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 1652 }
2060 1653
2061 1654 public function ai_get_json() {
2062 1655 // read json data from post body