Diff: STRATO-apps/wordpress_03/app/wp-content/plugins/elementor/app/modules/import-export/module.php

Keine Baseline-Datei – Diff nur gegen leer.
Zur Liste
1 -
1 + <?php
2 + namespace Elementor\App\Modules\ImportExport;
3 +
4 + use Elementor\App\Modules\ImportExport\Processes\Export;
5 + use Elementor\App\Modules\ImportExport\Processes\Import;
6 + use Elementor\App\Modules\ImportExport\Processes\Revert;
7 + use Elementor\Core\Base\Module as BaseModule;
8 + use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
9 + use Elementor\Core\Files\Uploads_Manager;
10 + use Elementor\Modules\System_Info\Reporters\Server;
11 + use Elementor\Plugin;
12 + use Elementor\Tools;
13 + use Elementor\Utils as ElementorUtils;
14 + use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
15 + use Elementor\Modules\CloudKitLibrary\Module as CloudKitLibrary;
16 +
17 + if ( ! defined( 'ABSPATH' ) ) {
18 + exit; // Exit if accessed directly.
19 + }
20 +
21 + /**
22 + * Import Export Module
23 + *
24 + * Responsible for initializing Elementor App functionality
25 + */
26 + class Module extends BaseModule {
27 + const FORMAT_VERSION = '2.0';
28 +
29 + const EXPORT_TRIGGER_KEY = 'elementor_export_kit';
30 +
31 + const UPLOAD_TRIGGER_KEY = 'elementor_upload_kit';
32 +
33 + const IMPORT_TRIGGER_KEY = 'elementor_import_kit';
34 +
35 + const IMPORT_RUNNER_TRIGGER_KEY = 'elementor_import_kit__runner';
36 +
37 + const REFERRER_KIT_LIBRARY = 'kit-library';
38 +
39 + const REFERRER_LOCAL = 'local';
40 +
41 + const REFERRER_CLOUD = 'cloud';
42 +
43 + const PLUGIN_PERMISSIONS_ERROR_KEY = 'plugin-installation-permissions-error';
44 +
45 + const KIT_LIBRARY_ERROR_KEY = 'invalid-kit-library-zip-error';
46 +
47 + const NO_WRITE_PERMISSIONS_KEY = 'no-write-permissions';
48 +
49 + const THIRD_PARTY_ERROR = 'third-party-error';
50 +
51 + const DOMDOCUMENT_MISSING = 'domdocument-missing';
52 +
53 + const OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS = 'elementor_import_sessions';
54 +
55 + const OPTION_KEY_ELEMENTOR_REVERT_SESSIONS = 'elementor_revert_sessions';
56 +
57 + const META_KEY_ELEMENTOR_IMPORT_SESSION_ID = '_elementor_import_session_id';
58 +
59 + const META_KEY_ELEMENTOR_EDIT_MODE = '_elementor_edit_mode';
60 + const IMPORT_PLUGINS_ACTION = 'import-plugins';
61 + const EXPORT_SOURCE_CLOUD = 'cloud';
62 + const EXPORT_SOURCE_FILE = 'file';
63 +
64 + /**
65 + * Assigning the export process to a property, so we can use the process from outside the class.
66 + *
67 + * @var Export
68 + */
69 + public $export;
70 +
71 + /**
72 + * Assigning the import process to a property, so we can use the process from outside the class.
73 + *
74 + * @var Import
75 + */
76 + public $import;
77 +
78 + /**
79 + * Assigning the revert process to a property, so we can use the process from outside the class.
80 + *
81 + * @var Revert
82 + */
83 + public $revert;
84 +
85 + /**
86 + * Get name.
87 + *
88 + * @access public
89 + *
90 + * @return string
91 + */
92 + public function get_name() {
93 + return 'import-export';
94 + }
95 +
96 + public function __construct() {
97 + $this->register_actions();
98 +
99 + if ( ElementorUtils::is_wp_cli() ) {
100 + \WP_CLI::add_command( 'elementor kit', WP_CLI::class );
101 + }
102 +
103 + ( new Usage() )->register();
104 +
105 + $this->revert = new Revert();
106 + }
107 +
108 + public function get_init_settings() {
109 + if ( ! Plugin::$instance->app->is_current() ) {
110 + return [];
111 + }
112 +
113 + return $this->get_config_data();
114 + }
115 +
116 + /**
117 + * Register the import/export tab in elementor tools.
118 + */
119 + public function register_settings_tab( Tools $tools ) {
120 + $tools->add_tab( 'import-export-kit', [
121 + 'label' => esc_html__( 'Website Templates', 'elementor' ),
122 + 'sections' => [
123 + 'intro' => [
124 + 'label' => esc_html__( 'Website Templates', 'elementor' ),
125 + 'callback' => function() {
126 + $this->render_import_export_tab_content();
127 + },
128 + 'fields' => [],
129 + ],
130 + ],
131 + ] );
132 + }
133 +
134 + /**
135 + * Render the import/export tab content.
136 + */
137 + private function render_import_export_tab_content() {
138 + $is_cloud_kits_available = CloudKitLibrary::get_app()->check_eligibility()['is_eligible'];
139 +
140 + $content_data = [
141 + 'export' => [
142 + 'title' => esc_html__( 'Export this website', 'elementor' ),
143 + 'button' => [
144 + 'url' => Plugin::$instance->app->get_base_url() . '#/export',
145 + 'text' => esc_html__( 'Export', 'elementor' ),
146 + 'id' => 'elementor-import-export__export',
147 + ],
148 + 'description' => esc_html__( 'You can download this website as a .zip file, or upload it to the library.', 'elementor' ),
149 + ],
150 + 'import' => [
151 + 'title' => esc_html__( 'Import website templates', 'elementor' ),
152 + 'button' => [
153 + 'url' => Plugin::$instance->app->get_base_url() . '#/import',
154 + 'text' => esc_html__( 'Import', 'elementor' ),
155 + 'id' => 'elementor-import-export__import',
156 + ],
157 + 'description' => esc_html__( 'You can import design and settings from a .zip file or choose from the library.', 'elementor' ),
158 + ],
159 + ];
160 +
161 + if ( $is_cloud_kits_available ) {
162 + $content_data['import']['button_secondary'] = [
163 + 'url' => Plugin::$instance->app->get_base_url() . '#/kit-library/cloud',
164 + 'text' => esc_html__( 'Import from library', 'elementor' ),
165 + 'id' => 'elementor-import-export__import_from_library',
166 + ];
167 + }
168 +
169 + $last_imported_kit = $this->revert->get_last_import_session();
170 + $penultimate_imported_kit = $this->revert->get_penultimate_import_session();
171 +
172 + $user_date_format = get_option( 'date_format' );
173 + $user_time_format = get_option( 'time_format' );
174 + $date_format = $user_date_format . ' ' . $user_time_format;
175 +
176 + $should_show_revert_section = $this->should_show_revert_section( $last_imported_kit );
177 +
178 + if ( $should_show_revert_section ) {
179 + if ( ! empty( $penultimate_imported_kit ) ) {
180 + $revert_text = sprintf(
181 + /* translators: 1: Last imported kit title, 2: Last imported kit date, 3: Line break <br>, 4: Penultimate imported kit title, 5: Penultimate imported kit date. */
182 + esc_html__( 'Remove all the content and site settings that came with "%1$s" on %2$s %3$s and revert to the site setting that came with "%4$s" on %5$s.', 'elementor' ),
183 + ! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
184 + gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
185 + '<br>',
186 + ! empty( $penultimate_imported_kit['kit_title'] ) ? $penultimate_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
187 + gmdate( $date_format, $penultimate_imported_kit['start_timestamp'] )
188 + );
189 + } else {
190 + $revert_text = sprintf(
191 + /* translators: 1: Last imported kit title, 2: Last imported kit date, 3: Line break <br>. */
192 + esc_html__( 'Remove all the content and site settings that came with "%1$s" on %2$s.%3$s Your original site settings will be restored.', 'elementor' ),
193 + ! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
194 + gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
195 + '<br>'
196 + );
197 + }
198 + }
199 + ?>
200 +
201 + <div class="tab-import-export-kit__content">
202 + <p class="tab-import-export-kit__info">
203 + <?php
204 + printf(
205 + '%1$s <a href="https://go.elementor.com/wp-dash-import-export-general/" target="_blank">%2$s</a>',
206 + esc_html__( 'Here’s where you can export this website as a .zip file, upload it to the cloud, or start the process of applying an existing template to your site.', 'elementor' ),
207 + esc_html__( 'Learn more', 'elementor' ),
208 + );
209 + ?>
210 + </p>
211 +
212 + <div class="tab-import-export-kit__wrapper">
213 + <?php foreach ( $content_data as $data ) {
214 + $this->print_item_content( $data );
215 + } ?>
216 + </div>
217 +
218 + <?php
219 + if ( $should_show_revert_section ) {
220 +
221 + $link_attributes = [
222 + 'href' => $this->get_revert_href(),
223 + 'id' => 'elementor-import-export__revert_kit',
224 + 'class' => 'button',
225 + ];
226 + ?>
227 + <div class="tab-import-export-kit__revert">
228 + <h2>
229 + <?php echo esc_html__( 'Remove the most recent Website Template', 'elementor' ); ?>
230 + </h2>
231 + <p class="tab-import-export-kit__info">
232 + <?php ElementorUtils::print_unescaped_internal_string( $revert_text ); ?>
233 + </p>
234 + <?php $this->render_last_kit_thumbnail( $last_imported_kit ); ?>
235 + <a <?php ElementorUtils::print_html_attributes( $link_attributes ); ?> >
236 + <?php echo esc_html__( 'Remove Website Template', 'elementor' ); ?>
237 + </a>
238 + </div>
239 + <?php } ?>
240 + </div>
241 + <?php
242 + }
243 +
244 + private function print_item_content( $data ) {
245 + ?>
246 + <div class="tab-import-export-kit__container">
247 + <div class="tab-import-export-kit__box">
248 + <h2><?php ElementorUtils::print_unescaped_internal_string( $data['title'] ); ?></h2>
249 + </div>
250 + <p class="description"><?php ElementorUtils::print_unescaped_internal_string( $data['description'] ); ?></p>
251 +
252 + <?php if ( ! empty( $data['link'] ) ) : ?>
253 + <a href="<?php ElementorUtils::print_unescaped_internal_string( $data['link']['url'] ); ?>" target="_blank"><?php ElementorUtils::print_unescaped_internal_string( $data['link']['text'] ); ?></a>
254 + <?php endif; ?>
255 + <div class="tab-import-export-kit__box action-buttons">
256 + <?php if ( ! empty( $data['button_secondary'] ) ) : ?>
257 + <a href="<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['url'] ); ?>" class="elementor-button e-btn-txt e-btn-txt-border">
258 + <?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['text'] ); ?>
259 + </a>
260 + <?php endif; ?>
261 + <a <?php ElementorUtils::print_html_attributes( [ 'id' => $data['button']['id'] ] ); ?> href="<?php ElementorUtils::print_unescaped_internal_string( $data['button']['url'] ); ?>" class="elementor-button e-primary">
262 + <?php ElementorUtils::print_unescaped_internal_string( $data['button']['text'] ); ?>
263 + </a>
264 + </div>
265 + </div>
266 + <?php
267 + }
268 +
269 + private function get_revert_href(): string {
270 + $admin_post_url = admin_url( 'admin-post.php?action=elementor_revert_kit' );
271 + $nonced_admin_post_url = wp_nonce_url( $admin_post_url, 'elementor_revert_kit' );
272 + return $this->maybe_add_referrer_param( $nonced_admin_post_url );
273 + }
274 +
275 + /**
276 + * Checks if referred by a kit and adds the referrer ID to the href
277 + *
278 + * @param string $href
279 + *
280 + * @return string
281 + */
282 + private function maybe_add_referrer_param( string $href ): string {
283 + $param_name = 'referrer_kit';
284 +
285 + if ( empty( $_GET[ $param_name ] ) ) {
286 + return $href;
287 + }
288 +
289 + return add_query_arg( $param_name, sanitize_key( $_GET[ $param_name ] ), $href );
290 + }
291 +
292 + /**
293 + * Render the last kit thumbnail if exists
294 + *
295 + * @param $last_imported_kit
296 + *
297 + * @return void
298 + */
299 + private function render_last_kit_thumbnail( $last_imported_kit ) {
300 + if ( empty( $last_imported_kit['kit_thumbnail'] ) ) {
301 + return;
302 + }
303 +
304 + ?>
305 + <div class="tab-import-export-kit__kit-item-row">
306 + <article class="tab-import-export-kit__kit-item">
307 + <header>
308 + <h3>
309 + <?php echo esc_html( $last_imported_kit['kit_title'] ); ?>
310 + </h3>
311 + </header>
312 + <img
313 + src="<?php echo esc_url( $last_imported_kit['kit_thumbnail'] ); ?>"
314 + alt="<?php echo esc_attr( $last_imported_kit['kit_title'] ); ?>"
315 + loading="lazy"
316 + >
317 + </article>
318 + </div>
319 + <?php
320 + }
321 +
322 + /**
323 + * Upload a kit zip file and get the kit data.
324 + *
325 + * Assigning the Import process to the 'import' property,
326 + * so it will be available to use in different places such as: WP_Cli, Pro, etc.
327 + *
328 + * @param string $file Path to the file.
329 + * @param string $referrer Referrer of the file 'local' or 'kit-library'.
330 + * @param string $kit_id
331 + * @return array
332 + * @throws \Exception If export validation fails or processing errors occur.
333 + */
334 + public function upload_kit( $file, $referrer, $kit_id = null ) {
335 + $this->ensure_writing_permissions();
336 +
337 + $this->import = new Import( $file, [
338 + 'referrer' => $referrer,
339 + 'id' => $kit_id,
340 + ] );
341 +
342 + return [
343 + 'session' => $this->import->get_session_id(),
344 + 'manifest' => $this->import->get_manifest(),
345 + 'conflicts' => $this->import->get_settings_conflicts(),
346 + ];
347 + }
348 +
349 + /**
350 + * Import a kit by session_id.
351 + * Upload and import a kit by kit zip file.
352 + *
353 + * If the split_to_chunks flag is true, the process won't start
354 + * It will initialize the import process and return the session_id and the runners.
355 + *
356 + * Assigning the Import process to the 'import' property,
357 + * so it will be available to use in different places such as: WP_Cli, Pro, etc.
358 + *
359 + * @param string $path Path to the file or session_id.
360 + * @param array $settings Settings the import use to determine which content to import.
361 + * (e.g: include, selected_plugins, selected_cpt, selected_override_conditions, etc.)
362 + * @param bool $split_to_chunks Determine if the import process should be split into chunks.
363 + * @return array
364 + * @throws \Exception If export configuration is invalid or processing fails.
365 + */
366 + public function import_kit( string $path, array $settings, bool $split_to_chunks = false ): array {
367 + $this->ensure_writing_permissions();
368 + $this->ensure_DOMDocument_exists();
369 +
370 + $this->import = new Import( $path, $settings );
371 + $this->import->register_default_runners();
372 +
373 + remove_filter( 'elementor/document/save/data', [ Plugin::$instance->modules_manager->get_modules( 'content-sanitizer' ), 'sanitize_content' ] );
374 + do_action( 'elementor/import-export/import-kit', $this->import );
375 +
376 + if ( $split_to_chunks ) {
377 + $this->import->init_import_session( true );
378 +
379 + return [
380 + 'session' => $this->import->get_session_id(),
381 + 'runners' => $this->import->get_runners_name(),
382 + ];
383 + }
384 +
385 + return $this->import->run();
386 + }
387 +
388 + /**
389 + * Resuming import process by re-creating the import instance and running the specific runner.
390 + *
391 + * @param string $session_id The id off the import session.
392 + * @param string $runner_name The specific runner that we want to run.
393 + *
394 + * @return array Two types of response.
395 + * 1. The status and the runner name.
396 + * 2. The imported data. (Only if the runner is the last one in the import process)
397 + * @throws \Exception If import configuration is invalid or processing fails.
398 + */
399 + public function import_kit_by_runner( string $session_id, string $runner_name ): array {
400 + // Check session_id
401 + $this->import = Import::from_session( $session_id );
402 + $runners = $this->import->get_runners_name();
403 +
404 + $run = $this->import->run_runner( $runner_name );
405 +
406 + if ( end( $runners ) === $run['runner'] ) {
407 + return $this->import->get_imported_data();
408 + }
409 +
410 + return $run;
411 + }
412 +
413 + /**
414 + * Export a kit.
415 + *
416 + * Assigning the Export process to the 'export' property,
417 + * so it will be available to use in different places such as: WP_Cli, Pro, etc.
418 + *
419 + * @param array $settings Settings the export use to determine which content to export.
420 + * (e.g: include, kit_info, selected_plugins, selected_cpt, etc.)
421 + * @return array
422 + * @throws \Exception If import/export process fails or validation errors occur.
423 + */
424 + public function export_kit( array $settings ) {
425 + $this->ensure_writing_permissions();
426 +
427 + $this->export = new Export( $settings );
428 + $this->export->register_default_runners();
429 +
430 + do_action( 'elementor/import-export/export-kit', $this->export );
431 +
432 + return $this->export->run();
433 + }
434 +
435 + /**
436 + * Handle revert kit ajax request.
437 + */
438 + public function revert_last_imported_kit() {
439 + $this->revert = new Revert();
440 + $this->revert->register_default_runners();
441 +
442 + do_action( 'elementor/import-export/revert-kit', $this->revert );
443 +
444 + $this->revert->run();
445 + }
446 +
447 +
448 + /**
449 + * Handle revert last imported kit ajax request.
450 + */
451 + public function handle_revert_last_imported_kit() {
452 + check_admin_referer( 'elementor_revert_kit' );
453 +
454 + $this->revert_last_imported_kit();
455 +
456 + wp_safe_redirect( admin_url( 'admin.php?page=' . Tools::PAGE_ID . '#tab-import-export-kit' ) );
457 + die;
458 + }
459 +
460 + /**
461 + * Register appropriate actions.
462 + */
463 + private function register_actions() {
464 + add_action( 'admin_init', function() {
465 + if ( wp_doing_ajax() &&
466 + isset( $_POST['action'] ) &&
467 + wp_verify_nonce( ElementorUtils::get_super_global_value( $_POST, '_nonce' ), Ajax::NONCE_KEY ) &&
468 + current_user_can( 'manage_options' )
469 + ) {
470 + $this->maybe_handle_ajax();
471 + }
472 + } );
473 +
474 + add_action( 'admin_post_elementor_revert_kit', [ $this, 'handle_revert_last_imported_kit' ] );
475 +
476 + add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
477 +
478 + if ( ! Plugin::$instance->experiments->is_feature_active( 'import-export-customization' ) ) {
479 + $page_id = Tools::PAGE_ID;
480 +
481 + add_action( "elementor/admin/after_create_settings/{$page_id}", [ $this, 'register_settings_tab' ] );
482 + }
483 +
484 + // TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
485 + if ( self::IMPORT_PLUGINS_ACTION === ElementorUtils::get_super_global_value( $_SERVER, 'HTTP_X_ELEMENTOR_ACTION' ) ) {
486 + add_filter( 'woocommerce_create_pages', [ $this, 'empty_pages' ], 10, 0 );
487 + }
488 + // TODO ^^^
489 +
490 + add_filter( 'elementor/import/kit/result', function( $result ) {
491 + if ( ! empty( $result['file_url'] ) ) {
492 + return [
493 + 'file_name' => $this->get_remote_kit_zip( $result['file_url'] ),
494 + 'referrer' => static::REFERRER_KIT_LIBRARY,
495 + 'file_url' => $result['file_url'],
496 + ];
497 + }
498 +
499 + return $result;
500 + } );
501 + }
502 +
503 + /**
504 + * Prevent the creation of the default WooCommerce pages (Cart, Checkout, etc.)
505 + *
506 + * TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
507 + *
508 + * @return array
509 + */
510 + public function empty_pages(): array {
511 + return [];
512 + }
513 +
514 + private function ensure_writing_permissions() {
515 + $server = new Server();
516 +
517 + $paths_to_check = [
518 + Server::KEY_PATH_WP_CONTENT_DIR => $server->get_system_path( Server::KEY_PATH_WP_CONTENT_DIR ),
519 + Server::KEY_PATH_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_UPLOADS_DIR ),
520 + Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ),
521 + ];
522 +
523 + $permissions = $server->get_paths_permissions( $paths_to_check );
524 +
525 + // WP Content dir has to be exists and writable.
526 + if ( ! $permissions[ Server::KEY_PATH_WP_CONTENT_DIR ]['write'] ) {
527 + throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
528 + }
529 +
530 + // WP Uploads dir has to be exists and writable.
531 + if ( ! $permissions[ Server::KEY_PATH_UPLOADS_DIR ]['write'] ) {
532 + throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
533 + }
534 +
535 + // Elementor uploads dir permissions is divided to 2 cases:
536 + // 1. If the dir exists, it has to be writable.
537 + // 2. If the dir doesn't exist, the parent dir has to be writable (wp uploads dir), so we can create it.
538 + if ( $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['exists'] && ! $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['write'] ) {
539 + throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
540 + }
541 + }
542 +
543 + private function ensure_DOMDocument_exists() {
544 + if ( ! class_exists( 'DOMDocument' ) ) {
545 + throw new \Error( self::DOMDOCUMENT_MISSING ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
546 + }
547 + }
548 +
549 + /**
550 + * Enqueue admin scripts
551 + */
552 + public function enqueue_scripts() {
553 + wp_enqueue_script(
554 + 'elementor-import-export-admin',
555 + $this->get_js_assets_url( 'import-export-admin' ),
556 + [ 'elementor-common' ],
557 + ELEMENTOR_VERSION,
558 + true
559 + );
560 +
561 + wp_localize_script(
562 + 'elementor-import-export-admin',
563 + 'elementorImportExport',
564 + [
565 + 'lastImportedSession' => $this->revert->get_last_import_session(),
566 + 'appUrl' => Plugin::$instance->app->get_base_url() . '#/kit-library',
567 + ]
568 + );
569 + }
570 +
571 + /**
572 + * Assign each ajax action to a method.
573 + */
574 + private function maybe_handle_ajax() {
575 + // phpcs:ignore WordPress.Security.NonceVerification.Missing
576 + $action = ElementorUtils::get_super_global_value( $_POST, 'action' );
577 +
578 + try {
579 + switch ( $action ) {
580 + case static::EXPORT_TRIGGER_KEY:
581 + $this->handle_export_kit();
582 + break;
583 +
584 + case static::UPLOAD_TRIGGER_KEY:
585 + $this->handle_upload_kit();
586 + break;
587 +
588 + case static::IMPORT_TRIGGER_KEY:
589 + $this->handle_import_kit();
590 + break;
591 +
592 + case static::IMPORT_RUNNER_TRIGGER_KEY:
593 + $this->handle_import_kit__runner();
594 + break;
595 +
596 + default:
597 + break;
598 + }
599 + } catch ( \Error $e ) {
600 + if ( isset( $this->import ) ) {
601 + $this->import->finalize_import_session_option();
602 + }
603 +
604 + Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
605 + 'meta' => [
606 + 'trace' => $e->getTraceAsString(),
607 + ],
608 + ] );
609 +
610 + if ( isset( $this->import ) && $this->is_third_party_class( $e->getTrace()[0]['class'] ) ) {
611 + wp_send_json_error( self::THIRD_PARTY_ERROR, 500 );
612 + }
613 +
614 + wp_send_json_error( $e->getMessage(), 500 );
615 + }
616 + }
617 +
618 + /**
619 + * Handle upload kit ajax request.
620 + *
621 + * @throws \Error If operation validation fails or processing errors occur.
622 + */
623 + private function handle_upload_kit() {
624 + // PHPCS - A URL that should contain special chars (auth headers information).
625 + $file_url = isset( $_POST['e_import_file'] )
626 + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
627 + ? wp_unslash( $_POST['e_import_file'] )
628 + : '';
629 +
630 + // PHPCS - Already validated in caller function
631 + $kit_id = ElementorUtils::get_super_global_value( $_POST, 'kit_id' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
632 + $source = ElementorUtils::get_super_global_value( $_POST, 'source' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
633 +
634 + $is_import_from_library = ! empty( $file_url );
635 +
636 + if ( $is_import_from_library ) {
637 + if (
638 + ! wp_verify_nonce( ElementorUtils::get_super_global_value( $_POST, 'e_kit_library_nonce' ), 'kit-library-import' )
639 + ) {
640 + throw new \Error( 'Invalid kit library nonce.' );
641 + }
642 +
643 + if ( ! filter_var( $file_url, FILTER_VALIDATE_URL ) || 0 !== strpos( $file_url, 'http' ) ) {
644 + throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
645 + }
646 +
647 + $import_result = apply_filters( 'elementor/import/kit/result', [ 'file_url' => $file_url ] );
648 + } elseif ( ! empty( $source ) ) {
649 + $import_result = apply_filters( 'elementor/import/kit/result/' . $source, [
650 + 'kit_id' => $kit_id,
651 + 'source' => $source,
652 + ] );
653 + } else {
654 + $import_result = [
655 + 'file_name' => ElementorUtils::get_super_global_value( $_FILES, 'e_import_file' )['tmp_name'],
656 + 'referrer' => static::REFERRER_LOCAL,
657 + ];
658 + }
659 +
660 + Plugin::$instance->logger->get_logger()->info( 'Uploading Kit: ', [
661 + 'meta' => [
662 + 'kit_id' => $kit_id,
663 + 'referrer' => $import_result['referrer'],
664 + ],
665 + ] );
666 +
667 + if ( is_wp_error( $import_result ) ) {
668 + wp_send_json_error( $import_result->get_error_message() );
669 + }
670 +
671 + $uploaded_kit = $this->upload_kit( $import_result['file_name'], $import_result['referrer'], $kit_id );
672 +
673 + $session_dir = $uploaded_kit['session'];
674 + $manifest = $uploaded_kit['manifest'];
675 + $conflicts = $uploaded_kit['conflicts'];
676 +
677 + if ( $is_import_from_library || ! empty( $source ) ) {
678 + Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $import_result['file_name'] ) );
679 + }
680 +
681 + if ( isset( $manifest['plugins'] ) && ! current_user_can( 'install_plugins' ) ) {
682 + throw new \Error( static::PLUGIN_PERMISSIONS_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
683 + }
684 +
685 + $result = [
686 + 'session' => $session_dir,
687 + 'manifest' => $manifest,
688 + 'file_url' => $import_result['file_url'],
689 + ];
690 +
691 + if ( ! empty( $import_result['kit'] ) ) {
692 + $result['uploaded_kit'] = $import_result['kit'];
693 + }
694 +
695 + if ( ! empty( $conflicts ) ) {
696 + $result['conflicts'] = $conflicts;
697 + } else {
698 + // Moved into the IE process \Elementor\App\Modules\ImportExport\Processes\Import::get_default_settings_conflicts
699 + // TODO: remove in 3.10.0
700 + $result = apply_filters( 'elementor/import/stage_1/result', $result );
701 + }
702 +
703 + wp_send_json_success( $result );
704 + }
705 +
706 + protected function get_remote_kit_zip( $url ) {
707 + $remote_zip_request = wp_safe_remote_get( $url );
708 +
709 + if ( is_wp_error( $remote_zip_request ) ) {
710 + Plugin::$instance->logger->get_logger()->error( $remote_zip_request->get_error_message() );
711 + throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
712 + }
713 +
714 + if ( 200 !== $remote_zip_request['response']['code'] ) {
715 + Plugin::$instance->logger->get_logger()->error( $remote_zip_request['response']['message'] );
716 + throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
717 + }
718 +
719 + return Plugin::$instance->uploads_manager->create_temp_file( $remote_zip_request['body'], 'kit.zip' );
720 + }
721 +
722 + /**
723 + * Handle import kit ajax request.
724 + */
725 + private function handle_import_kit() {
726 + // PHPCS - Already validated in caller function
727 + $settings = json_decode( ElementorUtils::get_super_global_value( $_POST, 'data' ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
728 + $tmp_folder_id = $settings['session'];
729 +
730 + $import = $this->import_kit( $tmp_folder_id, $settings, true );
731 +
732 + // get_settings_config() added manually because the frontend Ajax request doesn't trigger the get_init_settings().
733 + $import['configData'] = $this->get_config_data();
734 +
735 + Plugin::$instance->logger->get_logger()->info(
736 + sprintf( 'Selected import runners: %1$s',
737 + implode( ', ', $import['runners'] )
738 + )
739 + );
740 +
741 + wp_send_json_success( $import );
742 + }
743 +
744 + /**
745 + * Handle ajax request for running specific runner in the import kit process.
746 + */
747 + private function handle_import_kit__runner() {
748 + // PHPCS - Already validated in caller function
749 + $settings = json_decode( ElementorUtils::get_super_global_value( $_POST, 'data' ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
750 + $session_id = $settings['session'];
751 + $runner = $settings['runner'];
752 +
753 + $import = $this->import_kit_by_runner( $session_id, $runner );
754 +
755 + // get_settings_config() added manually because the frontend Ajax request doesn't trigger the get_init_settings().
756 + $import['configData'] = $this->get_config_data();
757 +
758 + if ( ! empty( $import['status'] ) ) {
759 + Plugin::$instance->logger->get_logger()->info(
760 + sprintf( 'Import runner completed: %1$s %2$s',
761 + $import['runner'],
762 + ( 'success' === $import['status'] ? '✓' : '✗' )
763 + )
764 + );
765 + }
766 +
767 + do_action( 'elementor/import-export/import-kit/runner/after-run', $import );
768 +
769 + wp_send_json_success( $import );
770 + }
771 +
772 + /**
773 + * Handle export kit ajax request.
774 + *
775 + * @throws \Error If cleanup process fails or file system errors occur.
776 + */
777 + private function handle_export_kit() {
778 + // PHPCS - Already validated in caller function
779 + $settings = json_decode( ElementorUtils::get_super_global_value( $_POST, 'data' ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
780 + $source = $settings['kitInfo']['source'];
781 +
782 + $export = $this->export_kit( $settings );
783 +
784 + $file_name = $export['file_name'];
785 + $file_size = filesize( $file_name );
786 + $file = ElementorUtils::file_get_contents( $file_name );
787 +
788 + if ( ! $file ) {
789 + throw new \Error( 'Could not read the exported file.' );
790 + }
791 +
792 + Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $file_name ) );
793 +
794 + $result = apply_filters(
795 + 'elementor/export/kit/export-result',
796 + [
797 + 'manifest' => $export['manifest'],
798 + 'file' => base64_encode( $file ),
799 + ],
800 + $source,
801 + $export,
802 + $settings,
803 + $file,
804 + $file_size,
805 + );
806 +
807 + if ( is_wp_error( $result ) ) {
808 + wp_send_json_error( $result );
809 + }
810 +
811 + wp_send_json_success( $result );
812 + }
813 +
814 + /**
815 + * Get config data that will be exposed to the frontend.
816 + */
817 + private function get_config_data() {
818 + $export_nonce = wp_create_nonce( 'elementor_export' );
819 + $export_url = add_query_arg( [ '_nonce' => $export_nonce ], Plugin::$instance->app->get_base_url() );
820 +
821 + return [
822 + 'exportURL' => $export_url,
823 + 'summaryTitles' => $this->get_summary_titles(),
824 + 'builtinWpPostTypes' => ImportExportUtils::get_builtin_wp_post_types(),
825 + 'elementorPostTypes' => ImportExportUtils::get_elementor_post_types(),
826 + 'isUnfilteredFilesEnabled' => Uploads_Manager::are_unfiltered_uploads_enabled(),
827 + 'elementorHomePageUrl' => $this->get_elementor_home_page_url(),
828 + 'recentlyEditedElementorPageUrl' => $this->get_recently_edited_elementor_page_url(),
829 + 'tools_url' => Tools::get_url(),
830 + 'importSessions' => Revert::get_import_sessions(),
831 + 'lastImportedSession' => $this->revert->get_last_import_session(),
832 + 'kitPreviewNonce' => wp_create_nonce( 'kit_thumbnail' ),
833 + ];
834 + }
835 +
836 + /**
837 + * Get labels of Elementor document types, Elementor Post types, WordPress Post types and Custom Post types.
838 + */
839 + private function get_summary_titles() {
840 + $summary_titles = [];
841 +
842 + $document_types = Plugin::$instance->documents->get_document_types();
843 +
844 + foreach ( $document_types as $name => $document_type ) {
845 + $summary_titles['templates'][ $name ] = [
846 + 'single' => $document_type::get_title(),
847 + 'plural' => $document_type::get_plural_title(),
848 + ];
849 + }
850 +
851 + $elementor_post_types = ImportExportUtils::get_elementor_post_types();
852 + $wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
853 + $post_types = array_merge( $elementor_post_types, $wp_builtin_post_types );
854 +
855 + foreach ( $post_types as $post_type ) {
856 + $post_type_object = get_post_type_object( $post_type );
857 +
858 + $summary_titles['content'][ $post_type ] = [
859 + 'single' => $post_type_object->labels->singular_name ?? '',
860 + 'plural' => $post_type_object->label ?? '',
861 + ];
862 + }
863 +
864 + $custom_post_types = ImportExportUtils::get_registered_cpt_names();
865 + if ( ! empty( $custom_post_types ) ) {
866 + foreach ( $custom_post_types as $custom_post_type ) {
867 +
868 + $custom_post_types_object = get_post_type_object( $custom_post_type );
869 + // CPT data appears in two arrays:
870 + // 1. content object: in order to show the export summary when completed in getLabel function
871 + $summary_titles['content'][ $custom_post_type ] = [
872 + 'single' => $custom_post_types_object->labels->singular_name ?? '',
873 + 'plural' => $custom_post_types_object->label ?? '',
874 + ];
875 +
876 + // 2. customPostTypes object: in order to actually export the data
877 + $summary_titles['content']['customPostTypes'][ $custom_post_type ] = [
878 + 'single' => $custom_post_types_object->labels->singular_name ?? '',
879 + 'plural' => $custom_post_types_object->label ?? '',
880 + ];
881 + }
882 + }
883 +
884 + $active_kit = Plugin::$instance->kits_manager->get_active_kit();
885 +
886 + foreach ( $active_kit->get_tabs() as $key => $tab ) {
887 + $summary_titles['site-settings'][ $key ] = $tab->get_title();
888 + }
889 +
890 + return $summary_titles;
891 + }
892 +
893 + public function should_show_revert_section( $last_imported_kit ) {
894 + if ( empty( $last_imported_kit ) ) {
895 + return false;
896 + }
897 +
898 + // TODO: BC - remove in the future
899 + // The 'templates' runner was in core and moved to the Pro plugin. (Part of it still exits in the Core for BC)
900 + // The runner that is in the core version is missing the revert functionality,
901 + // therefore we shouldn't display the revert section if the import process done with the core version.
902 + $is_import_templates_ran = isset( $last_imported_kit['runners']['templates'] );
903 + if ( $this->has_pro() && $is_import_templates_ran ) {
904 + $has_imported_templates = ! empty( $last_imported_kit['runners']['templates'] );
905 +
906 + return $has_imported_templates;
907 + }
908 +
909 + return true;
910 + }
911 +
912 + public function has_pro(): bool {
913 + return ElementorUtils::has_pro();
914 + }
915 +
916 + private function get_elementor_editor_home_page_url() {
917 + if ( 'page' !== get_option( 'show_on_front' ) ) {
918 + return '';
919 + }
920 +
921 + $frontpage_id = get_option( 'page_on_front' );
922 +
923 + return $this->get_elementor_editor_page_url( $frontpage_id );
924 + }
925 +
926 + private function get_elementor_home_page_url() {
927 + if ( 'page' !== get_option( 'show_on_front' ) ) {
928 + return '';
929 + }
930 +
931 + $frontpage_id = get_option( 'page_on_front' );
932 +
933 + return $this->get_elementor_page_url( $frontpage_id );
934 + }
935 +
936 + private function get_recently_edited_elementor_page_url() {
937 + $query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );
938 +
939 + if ( ! isset( $query->post ) ) {
940 + return '';
941 + }
942 +
943 + return $this->get_elementor_page_url( $query->post->ID );
944 + }
945 +
946 + private function get_recently_edited_elementor_editor_page_url() {
947 + $query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );
948 +
949 + if ( ! isset( $query->post ) ) {
950 + return '';
951 + }
952 +
953 + return $this->get_elementor_editor_page_url( $query->post->ID );
954 + }
955 +
956 + private function get_elementor_document( $page_id ) {
957 + $document = Plugin::$instance->documents->get( $page_id );
958 +
959 + if ( ! $document || ! $document->is_built_with_elementor() ) {
960 + return false;
961 + }
962 +
963 + return $document;
964 + }
965 +
966 + private function get_elementor_page_url( $page_id ) {
967 + $document = $this->get_elementor_document( $page_id );
968 +
969 + return $document ? $document->get_preview_url() : '';
970 + }
971 +
972 + private function get_elementor_editor_page_url( $page_id ) {
973 + $document = $this->get_elementor_document( $page_id );
974 +
975 + return $document ? $document->get_edit_url() : '';
976 + }
977 +
978 + /**
979 + * @param string $class_name
980 + *
981 + * @return bool
982 + */
983 + public function is_third_party_class( $class_name ) {
984 + $allowed_classes = [
985 + 'Elementor\\',
986 + 'ElementorPro\\',
987 + 'WP_',
988 + 'wp_',
989 + ];
990 +
991 + foreach ( $allowed_classes as $allowed_class ) {
992 + if ( str_starts_with( $class_name, $allowed_class ) ) {
993 + return false;
994 + }
995 + }
996 +
997 + return true;
998 + }
999 + }
1000 +