| @@ -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 |