first commit

This commit is contained in:
2026-03-24 00:31:47 +01:00
commit 2506f6f9c7
3328 changed files with 1172155 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
# Contributing
A guide on how to get started contributing code to the Elementor plugin.
Before diving into this repository, make sure you have a basic understanding of Elementor and its architecture.
## Architecture
If you are interested in learning more about the architecture of Elementor, please refer to the documentation in the `docs/` directory.
## Repository structure
The repository is structured as follows:
```
@/elementor
├── app/
│ ├── admin-menu-items/
│ ├── assets/
│ │ ├── js/
│ │ └── styles/
│ ├── modules/
│ │ ├── import-export/
│ │ ├── kit-library/
│ │ ├── onboarding/
│ │ └── site-editor/
│ ├── app.php
│ └── view.php
├── core/
│ ├── admin/
│ ├── base/
│ ├── editor/
│ ├── frontend/
│ ├── settings/
│ ├── utils/
│ └── ...
├── includes/
│ ├── controls/
│ ├── widgets/
│ ├── managers/
│ └── ...
├── modules/ (Feature modules)
│ ├── ai/
│ ├── atomic-widgets/
│ ├── floating-buttons/
│ ├── global-classes/
│ ├── nested-elements/
│ └── ...
├── assets/ (Static assets)
│ ├── css/
│ ├── js/
│ ├── images/
│ └── lib/
├── packages/ (V4 packages)
│ ├── packages/
│ ├── tests/
│ └── package.json
├── tests/ (Test suites)
│ ├── playwright/
│ ├── phpunit/
│ ├── jest/
│ └── qunit/
├── docs/ (Documentation)
├── elementor.php (Main plugin file)
├── package.json
└── composer.json
```
## Development Setup
To get started with development:
1. Clone the repository
2. Install dependencies:
```bash
npm run prepare-environment
```
3. Start development:
```bash
npm run watch
```
This will start the development environment with file watching enabled.
## Test, Lint & Build
### Testing
To run PHP tests:
```bash
npm run test:php
```
To run JavaScript tests:
```bash
npm run test:jest
```
To run Playwright end-to-end tests:
```bash
npm run start-local-server
npm run test:playwright
or
npm run test:playwright:*
```
### Linting
You can run the linter by executing:
```bash
npm run lint
```
This command uses ESLint for JavaScript/TypeScript files and includes package linting.
### Building
To build the project for production:
```bash
npm run build
```
For development builds:
```bash
npm run start
```
To build packages:
```bash
npm run build:packages
```
## Development Commands
- `npm run start` - Full build and setup (dev mode)
- `npm run watch` - Start development with file watching
- `npm run scripts` - Build JavaScript assets
- `npm run scripts:watch` - Watch JavaScript files
- `npm run styles` - Build CSS assets
- `npm run styles:watch` - Watch CSS files
- `npm run build:packages` - Build frontend packages
- `npm run build:tools` - Build development tools
## Testing Environment Setup
To set up the testing environment:
```bash
npm run setup:testing
```
To restart the testing environment:
```bash
npm run restart:testing
```
## Commit message conventions
This repository uses [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/), so please make sure to follow this convention to keep consistency in the repository.
## Pull requests
Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface.
Ensure you choose an appropriate commit message, and exercise caution when dealing with changes that may disrupt existing functionality.
Additionally, remember to include tests for your modifications to ensure comprehensive coverage and maintain code quality.
## Working with Packages
The `packages/` directory contains frontend packages that can be developed separately:
1. Navigate to the packages directory:
```bash
cd packages
```
2. Install dependencies:
```bash
npm ci
```
3. Start development:
```bash
npm run dev
```
When working on the main plugin with packages, use:
```bash
npm run watch
```
This will automatically handle package building and watching.
## Code Quality
- Follow WordPress coding standards
- Use meaningful commit messages
- Write tests for new features
- Update documentation when needed
- Ensure backward compatibility when possible
## Getting Help
- Check the `docs/` directory for detailed documentation
- Review existing code for patterns and conventions
- Ask questions in pull requests for clarification

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\App\AdminMenuItems;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Theme_Builder_Menu_Item implements Admin_Menu_Item {
public function is_visible() {
return true;
}
public function get_parent_slug() {
return Source_Local::ADMIN_MENU_SLUG;
}
public function get_label() {
return esc_html__( 'Theme Builder', 'elementor' );
}
public function get_capability() {
return 'manage_options';
}
}

View File

@@ -0,0 +1,327 @@
<?php
namespace Elementor\App;
use Elementor\App\AdminMenuItems\Theme_Builder_Menu_Item;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Core\Experiments\Manager as ExperimentsManager;
use Elementor\Modules\WebCli\Module as WebCLIModule;
use Elementor\Core\Base\App as BaseApp;
use Elementor\Core\Settings\Manager as SettingsManager;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\User;
use Elementor\Utils;
use Elementor\Core\Utils\Promotions\Filtered_Promotions_Manager;
use Elementor\Core\Utils\Assets_Config_Provider;
use Elementor\Core\Utils\Collection;
use Elementor\App\Modules\ImportExport\Module as ImportExportModule;
use Elementor\App\Modules\KitLibrary\Module as KitLibraryModule;
use Elementor\App\Modules\ImportExportCustomization\Module as ImportExportCustomizationModule;
use Elementor\App\Modules\SiteEditor\Module as SiteEditorModule;
use Elementor\App\Modules\Onboarding\Module as OnboardingModule;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class App extends BaseApp {
const PAGE_ID = 'elementor-app';
/**
* Get module name.
*
* Retrieve the module name.
*
* @since 3.0.0
* @access public
*
* @return string Module name.
*/
public function get_name() {
return 'app';
}
public function get_base_url() {
return admin_url( 'admin.php?page=' . self::PAGE_ID . '&ver=' . ELEMENTOR_VERSION );
}
private function register_admin_menu( Admin_Menu_Manager $admin_menu ) {
$admin_menu->register( static::PAGE_ID, new Theme_Builder_Menu_Item() );
}
public function fix_submenu( $menu ) {
global $submenu;
if ( is_multisite() && is_network_admin() ) {
return $menu;
}
// Non admin role / custom wp menu.
if ( empty( $submenu[ Source_Local::ADMIN_MENU_SLUG ] ) ) {
return $menu;
}
// Hack to add a link to sub menu.
foreach ( $submenu[ Source_Local::ADMIN_MENU_SLUG ] as &$item ) {
if ( self::PAGE_ID === $item[2] ) {
$item[2] = $this->get_settings( 'menu_url' ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$item[4] = 'elementor-app-link'; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
}
}
return $menu;
}
public function is_current() {
return ( ! empty( $_GET['page'] ) && self::PAGE_ID === $_GET['page'] );
}
public function admin_init() {
do_action( 'elementor/app/init', $this );
// Add the introduction and user settings only when it is needed (when loading the app and not in the editor or admin pages)
$this->set_settings( 'user', [
'introduction' => (object) User::get_introduction_meta(),
'is_administrator' => current_user_can( 'manage_options' ),
'restrictions' => Plugin::$instance->role_manager->get_user_restrictions_array(),
] );
$this->enqueue_assets();
remove_action( 'wp_print_styles', 'print_emoji_styles' );
// Setup default heartbeat options
// TODO: Enable heartbeat.
add_filter( 'heartbeat_settings', function( $settings ) {
$settings['interval'] = 15;
return $settings;
} );
$this->render();
die;
}
protected function get_init_settings() {
$referer = wp_get_referer();
return [
'menu_url' => $this->get_base_url() . '#site-editor/promotion',
'assets_url' => ELEMENTOR_ASSETS_URL,
'pages_url' => admin_url( 'edit.php?post_type=page' ),
'return_url' => $referer ? $referer : admin_url(),
'hasPro' => Utils::has_pro(),
'admin_url' => admin_url(),
'login_url' => wp_login_url(),
'base_url' => $this->get_base_url(),
'home_url' => home_url(),
'promotion' => Filtered_Promotions_Manager::get_filtered_promotion_data(
[ 'upgrade_url' => 'https://go.elementor.com/go-pro-theme-builder/' ],
'elementor/site-editor/promotion',
'upgrade_url'
),
];
}
private function render() {
require __DIR__ . '/view.php';
}
/**
* Get Elementor editor theme color preference.
*
* Retrieve the user theme color preference as defined by editor preferences manager.
*
* @since 3.0.0
* @access private
*
* @return string Preferred editor theme.
*/
private function get_elementor_ui_theme_preference() {
$editor_preferences = SettingsManager::get_settings_managers( 'editorPreferences' );
return $editor_preferences->get_model()->get_settings( 'ui_theme' );
}
/**
* Enqueue dark theme detection script.
*
* Enqueues an inline script that detects user-agent settings for dark mode and adds a complimentary class to the body tag.
*
* @since 3.0.0
* @access private
*/
private function enqueue_dark_theme_detection_script() {
if ( 'auto' === $this->get_elementor_ui_theme_preference() ) {
wp_add_inline_script( 'elementor-app',
'if ( window.matchMedia && window.matchMedia( `(prefers-color-scheme: dark)` ).matches )
{ document.body.classList.add( `eps-theme-dark` ); }' );
}
}
private function register_packages() {
$assets_config_provider = ( new Assets_Config_Provider() )
->set_path_resolver( function ( $name ) {
return ELEMENTOR_ASSETS_PATH . "js/packages/{$name}/{$name}.asset.php";
} );
Collection::make( [ 'ui', 'icons' ] )
->each( function( $package ) use ( $assets_config_provider ) {
$suffix = Utils::is_script_debug() ? '' : '.min';
$config = $assets_config_provider->load( $package )->get( $package );
if ( ! $config ) {
return;
}
wp_register_script(
$config['handle'],
ELEMENTOR_ASSETS_URL . "js/packages/{$package}/{$package}{$suffix}.js",
$config['deps'],
ELEMENTOR_VERSION,
true
);
} );
}
private function enqueue_assets() {
Plugin::$instance->init_common();
$this->register_packages();
/** @var WebCLIModule $web_cli */
$web_cli = Plugin::$instance->modules_manager->get_modules( 'web-cli' );
$web_cli->register_scripts();
Plugin::$instance->common->register_scripts();
wp_register_style(
'select2',
$this->get_css_assets_url( 'e-select2', 'assets/lib/e-select2/css/' ),
[],
'4.0.6-rc.1'
);
Plugin::$instance->common->register_styles();
wp_register_style(
'select2',
ELEMENTOR_ASSETS_URL . 'lib/e-select2/css/e-select2.css',
[],
'4.0.6-rc.1'
);
wp_enqueue_style(
'elementor-app',
$this->get_css_assets_url( 'app', null, 'default', true ),
[
'select2',
'elementor-icons',
'elementor-common',
'select2',
],
ELEMENTOR_VERSION
);
wp_enqueue_script(
'elementor-app-packages',
$this->get_js_assets_url( 'app-packages' ),
[
'wp-i18n',
'react',
],
ELEMENTOR_VERSION,
true
);
wp_register_script(
'select2',
$this->get_js_assets_url( 'e-select2.full', 'assets/lib/e-select2/js/' ),
[
'jquery',
],
'4.0.6-rc.1',
true
);
wp_enqueue_script(
'elementor-app',
$this->get_js_assets_url( 'app' ),
[
'wp-url',
'wp-i18n',
'elementor-v2-ui',
'elementor-v2-icons',
'react',
'react-dom',
'select2',
],
ELEMENTOR_VERSION,
true
);
$this->enqueue_dark_theme_detection_script();
wp_set_script_translations( 'elementor-app-packages', 'elementor' );
wp_set_script_translations( 'elementor-app', 'elementor' );
$this->print_config();
}
public function enqueue_app_loader() {
wp_enqueue_script(
'elementor-app-loader',
$this->get_js_assets_url( 'app-loader' ),
[
'elementor-common',
],
ELEMENTOR_VERSION,
true
);
$this->print_config( 'elementor-app-loader' );
}
private function register_import_export_customization_experiment() {
Plugin::$instance->experiments->add_feature( [
'name' => 'import-export-customization',
'title' => esc_html__( 'Import/Export Customization', 'elementor' ),
'description' => esc_html__( 'Enhanced import/export for website templates. Selectively include site content, templates, and settings with advanced granular control.', 'elementor' ),
'release_status' => ExperimentsManager::RELEASE_STATUS_BETA,
'default' => ExperimentsManager::STATE_ACTIVE,
] );
}
public function __construct() {
$this->register_import_export_customization_experiment();
$this->add_component( 'site-editor', new SiteEditorModule() );
if ( current_user_can( 'manage_options' ) || Utils::is_wp_cli() ) {
$this->add_component( 'import-export', new ImportExportModule() );
if ( Plugin::$instance->experiments->is_feature_active( 'import-export-customization' ) ) {
$this->add_component( 'import-export-customization', new ImportExportCustomizationModule() );
}
// Kit library is depended on import-export
$this->add_component( 'kit-library', new KitLibraryModule() );
}
$this->add_component( 'onboarding', new OnboardingModule() );
add_action( 'elementor/admin/menu/register', function ( Admin_Menu_Manager $admin_menu ) {
$this->register_admin_menu( $admin_menu );
}, Source_Local::ADMIN_MENU_PRIORITY + 10 );
// Happens after WP plugin page validation.
add_filter( 'add_menu_classes', [ $this, 'fix_submenu' ] );
if ( $this->is_current() ) {
add_action( 'admin_init', [ $this, 'admin_init' ], 0 );
} else {
add_action( 'elementor/common/after_register_scripts', [ $this, 'enqueue_app_loader' ] );
}
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Compatibility;
use Elementor\App\Modules\ImportExportCustomization\Import;
use Elementor\Core\Base\Base_Object;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Adapter {
/**
* @param array $manifest_data
* @param array $meta
* @return false
*/
public static function is_compatibility_needed( array $manifest_data, array $meta ) {
return false;
}
public function adapt_manifest( array $manifest_data ) {
return $manifest_data;
}
public function adapt_site_settings( array $site_settings, array $manifest_data, $path ) {
return $site_settings;
}
public function adapt_template( array $template_data, array $template_settings ) {
return $template_data;
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Compatibility;
use Elementor\App\Modules\ImportExportCustomization\Module;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Handles conversion from manifest format v2.0 to v3.0
* Main change: site-settings changed from array of tab keys to object with boolean values
*/
class Customization extends Base_Adapter {
/**
* Check if compatibility is needed based on manifest version
*
* @param array $manifest_data
* @param array $meta
* @return bool
*/
public static function is_compatibility_needed( array $manifest_data, array $meta ) {
// Check if we have an old version (2.0 or lower)
$version = $manifest_data['version'] ?? '1.0';
return version_compare( $version, '3.0', '<' );
}
/**
* Adapt the manifest from old format to new format
*
* @param array $manifest_data
* @return array
*/
public function adapt_manifest( array $manifest_data ) {
// Check if site-settings needs adaptation
if ( isset( $manifest_data['site-settings'] ) && is_array( $manifest_data['site-settings'] ) ) {
// Old format: array of tab keys
// New format: object with boolean values for each setting type
$old_site_settings = $manifest_data['site-settings'];
// Initialize new format with all settings as false
$new_site_settings = [
'theme' => false,
'globalColors' => false,
'globalFonts' => false,
'themeStyleSettings' => false,
'generalSettings' => false,
'experiments' => false,
];
// Map old tab keys to new setting types
$tab_mapping = [
'settings-global-colors' => 'globalColors',
'settings-global-typography' => 'globalFonts',
'theme-style-typography' => 'themeStyleSettings',
'settings-general' => 'generalSettings',
];
// If we have tab keys, assume all were exported (true)
if ( ! empty( $old_site_settings ) ) {
// In the old format, if site-settings was included, all settings were exported
$new_site_settings = [
'theme' => true,
'globalColors' => true,
'globalFonts' => true,
'themeStyleSettings' => true,
'generalSettings' => true,
'experiments' => true,
];
}
$manifest_data['site-settings'] = $new_site_settings;
}
return $manifest_data;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Compatibility;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Envato extends Base_Adapter {
public static function is_compatibility_needed( array $manifest_data, array $meta ) {
return ! empty( $manifest_data['manifest_version'] );
}
public function adapt_manifest( array $manifest_data ) {
$templates = $manifest_data['templates'];
$manifest_data['templates'] = [];
foreach ( $templates as $template ) {
// Envato store their global kit styles as a 'global.json' template file.
// We need to be able to know the path to this specific 'global.json' since it functions as the site-settings.json
$is_global = ! empty( $template['metadata']['template_type'] ) && 'global-styles' === $template['metadata']['template_type'];
if ( $is_global ) {
// Adding the path of the 'global.json' template to the manifest which will be used in the future.
$manifest_data['path-to-envto-site-settings'] = $template['source'];
// Getting the site-settings because Envato stores them in one of the posts.
$kit = Plugin::$instance->kits_manager->get_active_kit();
$kit_tabs = $kit->get_tabs();
unset( $kit_tabs['settings-site-identity'] );
$manifest_data['site-settings'] = array_keys( $kit_tabs );
continue;
}
// Evanto uses "type" instead of "doc_type"
$template['doc_type'] = $template['type'];
// Evanto uses for "name" instead of "title"
$template['title'] = $template['name'];
// Envato specifying an exact path to the template rather than using its "ID" as an index.
// This extracts the "file name" part out of our exact source list and we treat that as an ID.
$file_name_without_extension = str_replace( '.json', '', basename( $template['source'] ) );
// Append the template to the global list:
$manifest_data['templates'][ $file_name_without_extension ] = $template;
}
$manifest_data['name'] = $manifest_data['title'];
return $manifest_data;
}
public function adapt_site_settings( array $site_settings, array $manifest_data, $path ) {
if ( empty( $manifest_data['path-to-envto-site-settings'] ) ) {
return $site_settings;
}
$global_file_path = $path . $manifest_data['path-to-envto-site-settings'];
$global_file_data = ImportExportUtils::read_json_file( $global_file_path );
return [
'settings' => $global_file_data['page_settings'],
];
}
public function adapt_template( array $template_data, array $template_settings ) {
if ( ! empty( $template_data['metadata']['elementor_pro_conditions'] ) ) {
foreach ( $template_data['metadata']['elementor_pro_conditions'] as $condition ) {
list ( $type, $name, $sub_name, $sub_id ) = array_pad( explode( '/', $condition ), 4, '' );
$template_data['import_settings']['conditions'][] = compact( 'type', 'name', 'sub_name', 'sub_id' );
}
}
return $template_data;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Compatibility;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Kit_Library extends Base_Adapter {
public static function is_compatibility_needed( array $manifest_data, array $meta ) {
return ! empty( $meta['referrer'] ) && 'kit-library' === $meta['referrer'];
}
public function adapt_manifest( array $manifest_data ) {
if ( ! empty( $manifest_data['content']['page'] ) ) {
foreach ( $manifest_data['content']['page'] as & $page ) {
$page['thumbnail'] = false;
}
}
if ( ! empty( $manifest_data['templates'] ) ) {
foreach ( $manifest_data['templates'] as & $template ) {
$template['thumbnail'] = false;
}
}
return $manifest_data;
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Data;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Export;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Upload;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Import;
use Elementor\App\Modules\ImportExportCustomization\Data\Routes\Import_Runner;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Controller {
const API_NAMESPACE = 'elementor/v1';
const API_BASE = 'import-export-customization';
public static function register_hooks() {
add_action( 'rest_api_init', fn() => self::register_routes() );
}
public static function get_base_url() {
return get_rest_url() . self::API_NAMESPACE . '/' . self::API_BASE;
}
private static function register_routes() {
( new Export() )->register_route( self::API_NAMESPACE, self::API_BASE );
( new Upload() )->register_route( self::API_NAMESPACE, self::API_BASE );
( new Import() )->register_route( self::API_NAMESPACE, self::API_BASE );
( new Import_Runner() )->register_route( self::API_NAMESPACE, self::API_BASE );
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Data;
class Response {
private array $data;
private array $meta;
public function __construct( array $data, array $meta = [] ) {
$this->data = $data;
$this->meta = $meta;
}
public static function success( array $data, array $meta = [] ): \WP_REST_Response {
$response = new self( $data, $meta );
return $response->to_wp_rest_response( 200 );
}
public static function error( string $code, string $message, array $meta = [] ): \WP_REST_Response {
$response = new self([
'code' => $code,
'message' => $message,
], $meta);
return $response->to_wp_rest_response( 500 );
}
private function to_array(): array {
return [
'data' => $this->data,
'meta' => $this->meta,
];
}
private function to_wp_rest_response( int $status_code = 200 ): \WP_REST_Response {
return new \WP_REST_Response( $this->to_array(), $status_code );
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;
abstract class Base_Route {
public function __construct() {}
public function register_route( $namespace, $base_route ): void {
register_rest_route( $namespace, '/' . $base_route . '/' . $this->get_route(), [
[
'methods' => $this->get_method(),
'callback' => fn( $request ) => $this->callback( $request ),
'permission_callback' => $this->permission_callback(),
'args' => $this->get_args(),
],
] );
}
abstract protected function get_route(): string;
abstract protected function get_method(): string;
abstract protected function callback( $request ): \WP_REST_Response;
protected function permission_callback(): callable {
return fn() => current_user_can( 'manage_options' );
}
abstract protected function get_args(): array;
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;
use Elementor\Utils as ElementorUtils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Export extends Base_Route {
protected function get_route(): string {
return 'export';
}
protected function get_method(): string {
return \WP_REST_Server::CREATABLE;
}
protected function callback( $request ): \WP_REST_Response {
try {
$settings = [
'include' => $request->get_param( 'include' ),
'kitInfo' => $request->get_param( 'kitInfo' ),
'screenShotBlob' => $request->get_param( 'screenShotBlob' ),
'customization' => $request->get_param( 'customization' ),
'plugins' => $request->get_param( 'plugins' ),
'selectedCustomPostTypes' => $request->get_param( 'selectedCustomPostTypes' ),
];
$settings = array_filter( $settings );
$source = $settings['kitInfo']['source'];
$module = Plugin::$instance->app->get_component( 'import-export-customization' );
$export = $module->export_kit( $settings );
$file_name = $export['file_name'];
$file = ElementorUtils::file_get_contents( $file_name );
if ( ! $file ) {
throw new \Error( 'Could not read the exported file.' );
}
Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $file_name ) );
$result = apply_filters(
'elementor/export/kit/export-result',
[
'manifest' => $export['manifest'],
'file' => base64_encode( $file ),
],
$source,
$export,
$settings,
$file,
);
if ( is_wp_error( $result ) ) {
throw new \Error( $result->get_error_message() );
}
return Response::success( $result );
} catch ( \Error | \Exception $e ) {
Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
'meta' => [
'trace' => $e->getTraceAsString(),
],
] );
return Response::error( 'export_error', $e->getMessage() );
}
}
protected function get_args(): array {
return [
'include' => [
'type' => 'array',
'description' => 'Content types to include in export',
'required' => false,
'default' => [ 'templates', 'content', 'settings', 'plugins' ],
],
'kitInfo' => [
'type' => 'object',
'description' => 'Kit information',
'required' => false,
'default' => [
'title' => 'Elementor Website Template',
'description' => '',
'source' => 'local',
],
],
'screenShotBlob' => [
'type' => [ 'string', 'null' ],
'description' => 'Base64 encoded screenshot for cloud exports',
'required' => false,
'default' => null,
],
'customization' => [
'type' => 'object',
'description' => 'Customization settings for selective export',
'required' => false,
'default' => null,
'properties' => [
'settings' => [
'type' => [ 'object', 'null' ],
'description' => 'Site settings customization',
],
'templates' => [
'type' => [ 'object', 'null' ],
'description' => 'Templates customization',
],
'content' => [
'type' => [ 'object', 'null' ],
'description' => 'Content customization',
],
'plugins' => [
'type' => [ 'object', 'null' ],
'description' => 'Plugins customization',
],
],
],
'plugins' => [
'type' => 'array',
'description' => 'Selected plugins to export',
'required' => false,
'default' => [],
],
'selectedCustomPostTypes' => [
'type' => 'array',
'description' => 'Selected custom post types',
'required' => false,
'default' => [],
],
];
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Import_Runner extends Base_Route {
protected function get_route(): string {
return 'import-runner';
}
protected function get_method(): string {
return \WP_REST_Server::CREATABLE;
}
protected function callback( $request ): \WP_REST_Response {
try {
$session_id = $request->get_param( 'session' );
$runner = $request->get_param( 'runner' );
$module = Plugin::$instance->app->get_component( 'import-export-customization' );
if ( empty( $session_id ) ) {
return Response::error( 'Session ID is required.', 'missing_session_id' );
}
if ( empty( $runner ) ) {
return Response::error( 'Runner name is required.', 'missing_runner_name' );
}
$import = $module->import_kit_by_runner( $session_id, $runner );
if ( ! empty( $import['status'] ) ) {
Plugin::$instance->logger->get_logger()->info(
sprintf( 'Import runner completed via REST API: %1$s %2$s',
$import['runner'] ?? $runner,
( 'success' === $import['status'] ? '✓' : '✗' )
)
);
}
do_action( 'elementor/import-export-customization/import-kit/runner/after-run', $import );
return Response::success( $import );
} catch ( \Error $e ) {
Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
'meta' => [
'trace' => $e->getTraceAsString(),
],
] );
return Response::error( $e->getMessage(), 'import_runner_error' );
}
}
protected function get_args(): array {
return [
'session' => [
'type' => 'string',
'description' => 'Session ID for import operations',
'required' => true,
],
'runner' => [
'type' => 'string',
'description' => 'Runner name for import_runner action',
'required' => true,
],
];
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Import extends Base_Route {
protected function get_route(): string {
return 'import';
}
protected function get_method(): string {
return \WP_REST_Server::CREATABLE;
}
protected function callback( $request ): \WP_REST_Response {
try {
$session = $request->get_param( 'session' );
if ( empty( $session ) ) {
return Response::error( 'Session ID is required.', 'missing_session_id' );
}
$module = Plugin::$instance->app->get_component( 'import-export-customization' );
$settings = [
'include' => $request->get_param( 'include' ),
'customization' => $request->get_param( 'customization' ),
];
$import = $module->import_kit( $session, $settings, true );
Plugin::$instance->logger->get_logger()->info(
sprintf( 'Selected import runners via REST API: %1$s',
implode( ', ', $import['runners'] ?? [] )
)
);
return Response::success( $import );
} catch ( \Error $e ) {
Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
'meta' => [
'trace' => $e->getTraceAsString(),
],
] );
return Response::error( $e->getMessage(), 'import_error' );
}
}
protected function get_args(): array {
return [
'session' => [
'type' => 'string',
'description' => 'Session ID for import operations',
'required' => true,
],
'settings' => [
'type' => 'object',
'description' => 'Import settings',
'required' => false,
'default' => [],
],
];
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Data\Routes;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Data\Response;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Upload extends Base_Route {
protected function get_route(): string {
return 'upload';
}
protected function get_method(): string {
return \WP_REST_Server::CREATABLE;
}
private function format_url( string $url ): string {
return wp_unslash( urldecode( $url ) );
}
/**
* @param $request \WP_REST_Request
* @return \WP_REST_Response
*/
protected function callback( $request ): \WP_REST_Response {
try {
$file_url = $request->get_param( 'file_url' );
$kit_id = $request->get_param( 'kit_id' );
$source = $request->get_param( 'source' );
$module = Plugin::$instance->app->get_component( 'import-export-customization' );
$is_import_from_library = ! empty( $file_url );
if ( $is_import_from_library ) {
$file_url = $this->format_url( $file_url );
}
if ( $is_import_from_library ) {
if ( ! filter_var( $file_url, FILTER_VALIDATE_URL ) || 0 !== strpos( $file_url, 'http' ) ) {
return Response::error( 'Invalid kit library URL.', 'invalid_kit_library_url' );
}
$import_result = apply_filters( 'elementor/import/kit/result', [ 'file_url' => $file_url ] );
} elseif ( ! empty( $source ) ) {
$import_result = apply_filters( 'elementor/import/kit/result/' . $source, [
'kit_id' => $kit_id,
'source' => $source,
] );
} else {
$files = $request->get_file_params();
$file = $files['e_import_file'] ?? null;
if ( empty( $file ) || empty( $file['tmp_name'] ) ) {
return Response::error( 'No file uploaded or upload error occurred.', 'no_file_uploaded' );
}
$import_result = [
'file_name' => $file['tmp_name'],
'referrer' => $module::REFERRER_LOCAL,
];
}
Plugin::$instance->logger->get_logger()->info( 'Uploading Kit via REST API: ', [
'meta' => [
'kit_id' => $kit_id,
'referrer' => $import_result['referrer'] ?? 'unknown',
],
] );
if ( is_wp_error( $import_result ) ) {
return Response::error( $import_result->get_error_message(), 'upload_error' );
}
$uploaded_kit = $module->upload_kit( $import_result['file_name'], $import_result['referrer'], $kit_id );
$result = [
'session' => $uploaded_kit['session'],
'manifest' => $uploaded_kit['manifest'],
];
if ( ! empty( $import_result['file_url'] ) ) {
$result['file_url'] = $import_result['file_url'];
}
if ( ! empty( $import_result['kit'] ) ) {
$result['uploaded_kit'] = $import_result['kit'];
}
if ( ! empty( $uploaded_kit['conflicts'] ) ) {
$result['conflicts'] = $uploaded_kit['conflicts'];
}
// Clean up temporary files
if ( $is_import_from_library || ! empty( $source ) ) {
Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $import_result['file_name'] ) );
}
return Response::success( $result );
} catch ( \Error $e ) {
Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
'meta' => [
'trace' => $e->getTraceAsString(),
],
] );
return Response::error( $e->getMessage(), 'upload_error' );
}
}
protected function get_args(): array {
return [
'file_url' => [
'type' => 'string',
'description' => 'File URL for upload action',
'required' => false,
'validate_callback' => function ( $value ) {
if ( empty( $value ) ) {
return true;
}
return filter_var( $this->format_url( $value ), FILTER_VALIDATE_URL );
},
],
'kit_id' => [
'type' => 'string',
'description' => 'Kit ID for upload action',
'required' => false,
],
'source' => [
'type' => 'string',
'description' => 'Source for upload action',
'required' => false,
],
];
}
}

View File

@@ -0,0 +1,757 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization;
use Elementor\App\Modules\ImportExportCustomization\Processes\Export;
use Elementor\App\Modules\ImportExportCustomization\Processes\Import;
use Elementor\App\Modules\ImportExportCustomization\Processes\Revert;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Files\Uploads_Manager;
use Elementor\Modules\CloudKitLibrary\Module as CloudKitLibrary;
use Elementor\Modules\System_Info\Reporters\Server;
use Elementor\Plugin;
use Elementor\Tools;
use Elementor\Utils as ElementorUtils;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\App\Modules\ImportExportCustomization\Data\Controller;
use Elementor\Core\Settings\Manager as SettingsManager;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Import Export Module
*
* Responsible for initializing Elementor App functionality
*/
class Module extends BaseModule {
const FORMAT_VERSION = '3.0';
const REFERRER_KIT_LIBRARY = 'kit-library';
const REFERRER_LOCAL = 'local';
const REFERRER_CLOUD = 'cloud';
const PLUGIN_PERMISSIONS_ERROR_KEY = 'plugin-installation-permissions-error';
const KIT_LIBRARY_ERROR_KEY = 'invalid-kit-library-zip-error';
const NO_WRITE_PERMISSIONS_KEY = 'no-write-permissions';
const THIRD_PARTY_ERROR = 'third-party-error';
const DOMDOCUMENT_MISSING = 'domdocument-missing';
const OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS = 'elementor_import_sessions';
const OPTION_KEY_ELEMENTOR_REVERT_SESSIONS = 'elementor_revert_sessions';
const META_KEY_ELEMENTOR_IMPORT_SESSION_ID = '_elementor_import_session_id';
const META_KEY_ELEMENTOR_EDIT_MODE = '_elementor_edit_mode';
const IMPORT_PLUGINS_ACTION = 'import-plugins';
const EXPORT_SOURCE_CLOUD = 'cloud';
const EXPORT_SOURCE_FILE = 'file';
/**
* Assigning the export process to a property, so we can use the process from outside the class.
*
* @var Export
*/
public $export;
/**
* Assigning the import process to a property, so we can use the process from outside the class.
*
* @var Import
*/
public $import;
/**
* Assigning the revert process to a property, so we can use the process from outside the class.
*
* @var Revert
*/
public $revert;
/**
* Get name.
*
* @access public
*
* @return string
*/
public function get_name() {
return 'import-export-customization';
}
public function __construct() {
$this->register_actions();
Controller::register_hooks();
if ( ElementorUtils::is_wp_cli() ) {
\WP_CLI::add_command( 'elementor kit', WP_CLI::class );
}
( new Usage() )->register();
$this->revert = new Revert();
}
public function get_init_settings() {
if ( ! Plugin::$instance->app->is_current() ) {
return [];
}
return $this->get_config_data();
}
/**
* Register the import/export tab in elementor tools.
*/
public function register_settings_tab( Tools $tools ) {
$tools->add_tab( 'import-export-kit', [
'label' => esc_html__( 'Website Templates', 'elementor' ),
'sections' => [
'intro' => [
'label' => esc_html__( 'Website Templates', 'elementor' ),
'callback' => function() {
$this->render_import_export_tab_content();
},
'fields' => [],
],
],
] );
}
/**
* Render the import/export tab content.
*/
private function render_import_export_tab_content() {
$is_cloud_kits_available = CloudKitLibrary::get_app()->check_eligibility()['is_eligible'];
$content_data = [
'export' => [
'title' => esc_html__( 'Export this website', 'elementor' ),
'button' => [
'url' => Plugin::$instance->app->get_base_url() . '#/export-customization',
'text' => esc_html__( 'Export', 'elementor' ),
'id' => 'elementor-import-export__export',
],
'description' => esc_html__( 'You can download this website as a .zip file, or upload it to the library.', 'elementor' ),
],
'import' => [
'title' => esc_html__( 'Apply a Website Template', 'elementor' ),
'button' => [
'url' => Plugin::$instance->app->get_base_url() . '#/import-customization',
'text' => $is_cloud_kits_available ? esc_html__( 'Upload .zip file', 'elementor' ) : esc_html__( 'Import', 'elementor' ),
'id' => 'elementor-import-export__import',
],
'description' => esc_html__( 'You can import design and settings from a .zip file or choose from the library.', 'elementor' ),
],
];
if ( $is_cloud_kits_available ) {
$content_data['import']['button_secondary'] = [
'url' => Plugin::$instance->app->get_base_url() . '#/kit-library/cloud',
'text' => esc_html__( 'Import from library', 'elementor' ),
'id' => 'elementor-import-export__import_from_library',
];
}
$last_imported_kit = $this->revert->get_last_import_session();
$penultimate_imported_kit = $this->revert->get_penultimate_import_session();
$user_date_format = get_option( 'date_format' );
$user_time_format = get_option( 'time_format' );
$date_format = $user_date_format . ' ' . $user_time_format;
$should_show_revert_section = ! empty( $last_imported_kit );
if ( $should_show_revert_section ) {
if ( ! empty( $penultimate_imported_kit ) ) {
$revert_text = sprintf(
/* translators: 1: kit title, 2: date, 3: line break, 4: kit title, 5: date. */
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' ),
! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
'<br>',
! empty( $penultimate_imported_kit['kit_title'] ) ? $penultimate_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
gmdate( $date_format, $penultimate_imported_kit['start_timestamp'] )
);
} else {
$revert_text = sprintf(
/* translators: 1: kit title, 2: date, 3: line break */
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' ),
! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
'<br>'
);
}
}
?>
<div class="tab-import-export-kit__content">
<p class="tab-import-export-kit__info">
<?php
printf(
'%1$s <a href="https://go.elementor.com/wp-dash-import-export-general/" target="_blank">%2$s</a>',
esc_html__( 'Heres 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' ),
esc_html__( 'Learn more', 'elementor' ),
);
?>
</p>
<div class="tab-import-export-kit__wrapper">
<?php foreach ( $content_data as $data ) {
$this->print_item_content( $data );
} ?>
</div>
<?php
if ( $should_show_revert_section ) {
$link_attributes = [
'href' => $this->get_revert_href(),
'id' => 'elementor-import-export__revert_kit',
'class' => 'button',
];
?>
<div class="tab-import-export-kit__revert">
<h2>
<?php echo esc_html__( 'Remove the most recent Website Template', 'elementor' ); ?>
</h2>
<p class="tab-import-export-kit__info">
<?php ElementorUtils::print_unescaped_internal_string( $revert_text ); ?>
</p>
<?php $this->render_last_kit_thumbnail( $last_imported_kit ); ?>
<a <?php ElementorUtils::print_html_attributes( $link_attributes ); ?> >
<?php echo esc_html__( 'Remove Website Template', 'elementor' ); ?>
</a>
</div>
<?php } ?>
</div>
<?php
}
private function print_item_content( $data ) {
?>
<div class="tab-import-export-kit__container">
<div class="tab-import-export-kit__box">
<h2><?php ElementorUtils::print_unescaped_internal_string( $data['title'] ); ?></h2>
</div>
<p class="description"><?php ElementorUtils::print_unescaped_internal_string( $data['description'] ); ?></p>
<?php if ( ! empty( $data['link'] ) ) : ?>
<a href="<?php ElementorUtils::print_unescaped_internal_string( $data['link']['url'] ); ?>" target="_blank"><?php ElementorUtils::print_unescaped_internal_string( $data['link']['text'] ); ?></a>
<?php endif; ?>
<div class="tab-import-export-kit__box action-buttons">
<?php if ( ! empty( $data['button_secondary'] ) ) : ?>
<a href="<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['url'] ); ?>" class="elementor-button e-btn-txt e-btn-txt-border">
<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['text'] ); ?>
</a>
<?php endif; ?>
<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">
<?php ElementorUtils::print_unescaped_internal_string( $data['button']['text'] ); ?>
</a>
</div>
</div>
<?php
}
private function get_revert_href(): string {
$admin_post_url = admin_url( 'admin-post.php?action=elementor_revert_kit' );
$nonced_admin_post_url = wp_nonce_url( $admin_post_url, 'elementor_revert_kit' );
return $this->maybe_add_referrer_param( $nonced_admin_post_url );
}
/**
* Checks if referred by a kit and adds the referrer ID to the href
*
* @param string $href
*
* @return string
*/
private function maybe_add_referrer_param( string $href ): string {
$param_name = 'referrer_kit';
if ( empty( $_GET[ $param_name ] ) ) {
return $href;
}
return add_query_arg( $param_name, sanitize_key( $_GET[ $param_name ] ), $href );
}
/**
* Render the last kit thumbnail if exists
*
* @param $last_imported_kit
*
* @return void
*/
private function render_last_kit_thumbnail( $last_imported_kit ) {
if ( empty( $last_imported_kit['kit_thumbnail'] ) ) {
return;
}
?>
<div class="tab-import-export-kit__kit-item-row">
<article class="tab-import-export-kit__kit-item">
<header>
<h3>
<?php echo esc_html( $last_imported_kit['kit_title'] ); ?>
</h3>
</header>
<img
src="<?php echo esc_url( $last_imported_kit['kit_thumbnail'] ); ?>"
alt="<?php echo esc_attr( $last_imported_kit['kit_title'] ); ?>"
loading="lazy"
>
</article>
</div>
<?php
}
/**
* Upload a kit zip file and get the kit data.
*
* Assigning the Import process to the 'import' property,
* so it will be available to use in different places such as: WP_Cli, Pro, etc.
*
* @param string $file Path to the file.
* @param string $referrer Referrer of the file 'local' or 'kit-library'.
* @param string $kit_id
* @return array
* @throws \Exception
*/
public function upload_kit( $file, $referrer, $kit_id = null ) {
$this->ensure_writing_permissions();
$this->import = new Import( $file, [
'referrer' => $referrer,
'id' => $kit_id,
] );
return [
'session' => $this->import->get_session_id(),
'manifest' => $this->import->get_manifest(),
'conflicts' => $this->import->get_settings_conflicts(),
];
}
/**
* Import a kit by session_id.
* Upload and import a kit by kit zip file.
*
* If the split_to_chunks flag is true, the process won't start
* It will initialize the import process and return the session_id and the runners.
*
* Assigning the Import process to the 'import' property,
* so it will be available to use in different places such as: WP_Cli, Pro, etc.
*
* @param string $path Path to the file or session_id.
* @param array $settings Settings the import use to determine which content to import.
* (e.g: include, selected_plugins, selected_cpt, selected_override_conditions, etc.)
* @param bool $split_to_chunks Determine if the import process should be split into chunks.
* @return array
* @throws \Exception
*/
public function import_kit( string $path, array $settings, bool $split_to_chunks = false ): array {
$this->ensure_writing_permissions();
$this->ensure_DOMDocument_exists();
$this->import = new Import( $path, $settings );
$this->import->register_default_runners();
remove_filter( 'elementor/document/save/data', [ Plugin::$instance->modules_manager->get_modules( 'content-sanitizer' ), 'sanitize_content' ] );
do_action( 'elementor/import-export-customization/import-kit', $this->import );
if ( $split_to_chunks ) {
$this->import->init_import_session( true );
return [
'session' => $this->import->get_session_id(),
'runners' => $this->import->get_runners_name(),
];
}
return $this->import->run();
}
/**
* Resuming import process by re-creating the import instance and running the specific runner.
*
* @param string $session_id The id off the import session.
* @param string $runner_name The specific runner that we want to run.
*
* @return array Two types of response.
* 1. The status and the runner name.
* 2. The imported data. (Only if the runner is the last one in the import process)
* @throws \Exception
*/
public function import_kit_by_runner( string $session_id, string $runner_name ): array {
// Check session_id
$this->import = Import::from_session( $session_id );
$runners = $this->import->get_runners_name();
$run = $this->import->run_runner( $runner_name );
if ( end( $runners ) === $run['runner'] ) {
return $this->import->get_imported_data();
}
return $run;
}
/**
* Export a kit.
*
* Assigning the Export process to the 'export' property,
* so it will be available to use in different places such as: WP_Cli, Pro, etc.
*
* @param array $settings Settings the export use to determine which content to export.
* (e.g: include, kit_info, selected_plugins, selected_cpt, etc.)
* @return array
* @throws \Exception
*/
public function export_kit( array $settings ) {
$this->ensure_writing_permissions();
$this->export = new Export( $settings );
$this->export->register_default_runners();
do_action( 'elementor/import-export-customization/export-kit', $this->export );
return $this->export->run();
}
/**
* Handle revert kit ajax request.
*/
public function revert_last_imported_kit() {
$this->revert = new Revert();
$this->revert->register_default_runners();
do_action( 'elementor/import-export-customization/revert-kit', $this->revert );
$this->revert->run();
}
/**
* Handle revert last imported kit ajax request.
*/
public function handle_revert_last_imported_kit() {
check_admin_referer( 'elementor_revert_kit' );
$this->revert_last_imported_kit();
wp_safe_redirect( admin_url( 'admin.php?page=' . Tools::PAGE_ID . '#tab-import-export-kit' ) );
die;
}
/**
* Register appropriate actions.
*/
private function register_actions() {
add_action( 'admin_post_elementor_revert_kit', [ $this, 'handle_revert_last_imported_kit' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
$page_id = Tools::PAGE_ID;
add_action( "elementor/admin/after_create_settings/{$page_id}", [ $this, 'register_settings_tab' ] );
// TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
if ( self::IMPORT_PLUGINS_ACTION === ElementorUtils::get_super_global_value( $_SERVER, 'HTTP_X_ELEMENTOR_ACTION' ) ) {
add_filter( 'woocommerce_create_pages', [ $this, 'empty_pages' ], 10, 0 );
}
// TODO ^^^
add_filter( 'elementor/import/kit/result', function( $result ) {
if ( ! empty( $result['file_url'] ) ) {
return [
'file_name' => $this->get_remote_kit_zip( $result['file_url'] ),
'referrer' => static::REFERRER_KIT_LIBRARY,
'file_url' => $result['file_url'],
];
}
return $result;
} );
}
/**
* Prevent the creation of the default WooCommerce pages (Cart, Checkout, etc.)
*
* TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
*
* @return array
*/
public function empty_pages(): array {
return [];
}
private function ensure_writing_permissions() {
$server = new Server();
$paths_to_check = [
Server::KEY_PATH_WP_CONTENT_DIR => $server->get_system_path( Server::KEY_PATH_WP_CONTENT_DIR ),
Server::KEY_PATH_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_UPLOADS_DIR ),
Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ),
];
$permissions = $server->get_paths_permissions( $paths_to_check );
// WP Content dir has to be exists and writable.
if ( ! $permissions[ Server::KEY_PATH_WP_CONTENT_DIR ]['write'] ) {
throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
// WP Uploads dir has to be exists and writable.
if ( ! $permissions[ Server::KEY_PATH_UPLOADS_DIR ]['write'] ) {
throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
// Elementor uploads dir permissions is divided to 2 cases:
// 1. If the dir exists, it has to be writable.
// 2. If the dir doesn't exist, the parent dir has to be writable (wp uploads dir), so we can create it.
if ( $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['exists'] && ! $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['write'] ) {
throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
}
private function ensure_DOMDocument_exists() {
if ( ! class_exists( 'DOMDocument' ) ) {
throw new \Error( self::DOMDOCUMENT_MISSING ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
}
/**
* Enqueue admin scripts
*/
public function enqueue_scripts() {
wp_enqueue_script(
'elementor-import-export-admin',
$this->get_js_assets_url( 'import-export-admin' ),
[ 'elementor-common' ],
ELEMENTOR_VERSION,
true
);
wp_localize_script(
'elementor-import-export-admin',
'elementorImportExport',
[
'lastImportedSession' => $this->revert->get_last_import_session(),
'appUrl' => Plugin::$instance->app->get_base_url() . '#/kit-library',
]
);
wp_enqueue_script(
'import-export-customization-admin',
$this->get_js_assets_url( 'import-export-customization-admin' ),
[ 'elementor-common' ],
ELEMENTOR_VERSION,
true
);
}
protected function get_remote_kit_zip( $url ) {
$remote_zip_request = wp_safe_remote_get( $url );
if ( is_wp_error( $remote_zip_request ) ) {
Plugin::$instance->logger->get_logger()->error( $remote_zip_request->get_error_message() );
throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
if ( 200 !== $remote_zip_request['response']['code'] ) {
Plugin::$instance->logger->get_logger()->error( $remote_zip_request['response']['message'] );
throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
return Plugin::$instance->uploads_manager->create_temp_file( $remote_zip_request['body'], 'kit.zip' );
}
/**
* Get config data that will be exposed to the frontend.
*/
private function get_config_data() {
$export_nonce = wp_create_nonce( 'elementor_export' );
$export_url = add_query_arg( [ '_nonce' => $export_nonce ], Plugin::$instance->app->get_base_url() );
return [
'exportURL' => $export_url,
'summaryTitles' => $this->get_summary_titles(),
'builtinWpPostTypes' => ImportExportUtils::get_builtin_wp_post_types(),
'elementorPostTypes' => ImportExportUtils::get_elementor_post_types(),
'isUnfilteredFilesEnabled' => Uploads_Manager::are_unfiltered_uploads_enabled(),
'elementorHomePageUrl' => $this->get_elementor_home_page_url(),
'recentlyEditedElementorPageUrl' => $this->get_recently_edited_elementor_page_url(),
'tools_url' => Tools::get_url(),
'importSessions' => Revert::get_import_sessions(),
'lastImportedSession' => $this->revert->get_last_import_session(),
'kitPreviewNonce' => wp_create_nonce( 'kit_thumbnail' ),
'restApiBaseUrl' => Controller::get_base_url(),
'uiTheme' => $this->get_elementor_ui_theme_preference(),
'exportGroups' => $this->get_export_groups(),
'manifestVersion' => self::FORMAT_VERSION,
'elementorVersion' => ELEMENTOR_VERSION,
'upgradeVersionUrl' => admin_url( 'plugins.php' ),
];
}
private function get_elementor_ui_theme_preference() {
$editor_preferences = SettingsManager::get_settings_managers( 'editorPreferences' );
return $editor_preferences->get_model()->get_settings( 'ui_theme' );
}
private function get_export_groups() {
$export_groups = [];
$document_types = Plugin::$instance->documents->get_document_types();
foreach ( $document_types as $name => $document_type ) {
$export_groups[ $name ] = defined( $document_type . '::EXPORT_GROUP' ) ? $document_type::EXPORT_GROUP : '';
}
return $export_groups;
}
/**
* Get labels of Elementor document types, Elementor Post types, WordPress Post types and Custom Post types.
*/
private function get_summary_titles() {
$summary_titles = [];
$document_types = Plugin::$instance->documents->get_document_types();
foreach ( $document_types as $name => $document_type ) {
$summary_titles['templates'][ $name ] = [
'single' => $document_type::get_title(),
'plural' => $document_type::get_plural_title(),
];
}
$elementor_post_types = ImportExportUtils::get_elementor_post_types();
$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
$post_types = array_merge( $elementor_post_types, $wp_builtin_post_types );
foreach ( $post_types as $post_type ) {
$post_type_object = get_post_type_object( $post_type );
$summary_titles['content'][ $post_type ] = [
'single' => $post_type_object->labels->singular_name ?? '',
'plural' => $post_type_object->label ?? '',
];
}
$custom_post_types = ImportExportUtils::get_registered_cpt_names();
if ( ! empty( $custom_post_types ) ) {
foreach ( $custom_post_types as $custom_post_type ) {
$custom_post_types_object = get_post_type_object( $custom_post_type );
// CPT data appears in two arrays:
// 1. content object: in order to show the export summary when completed in getLabel function
$summary_titles['content'][ $custom_post_type ] = [
'single' => $custom_post_types_object->labels->singular_name ?? '',
'plural' => $custom_post_types_object->label ?? '',
];
// 2. customPostTypes object: in order to actually export the data
$summary_titles['content']['customPostTypes'][ $custom_post_type ] = [
'single' => $custom_post_types_object->labels->singular_name ?? '',
'plural' => $custom_post_types_object->label ?? '',
];
}
}
return $summary_titles;
}
private function get_elementor_editor_home_page_url() {
if ( 'page' !== get_option( 'show_on_front' ) ) {
return '';
}
$frontpage_id = get_option( 'page_on_front' );
return $this->get_elementor_editor_page_url( $frontpage_id );
}
private function get_elementor_home_page_url() {
if ( 'page' !== get_option( 'show_on_front' ) ) {
return '';
}
$frontpage_id = get_option( 'page_on_front' );
return $this->get_elementor_page_url( $frontpage_id );
}
private function get_recently_edited_elementor_page_url() {
$query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );
if ( ! isset( $query->post ) ) {
return '';
}
return $this->get_elementor_page_url( $query->post->ID );
}
private function get_recently_edited_elementor_editor_page_url() {
$query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );
if ( ! isset( $query->post ) ) {
return '';
}
return $this->get_elementor_editor_page_url( $query->post->ID );
}
private function get_elementor_document( $page_id ) {
$document = Plugin::$instance->documents->get( $page_id );
if ( ! $document || ! $document->is_built_with_elementor() ) {
return false;
}
return $document;
}
private function get_elementor_page_url( $page_id ) {
$document = $this->get_elementor_document( $page_id );
return $document ? $document->get_preview_url() : '';
}
private function get_elementor_editor_page_url( $page_id ) {
$document = $this->get_elementor_document( $page_id );
return $document ? $document->get_edit_url() : '';
}
/**
* @param string $class
*
* @return bool
*/
public function is_third_party_class( $class ) {
$allowed_classes = [
'Elementor\\',
'ElementorPro\\',
'WP_',
'wp_',
];
foreach ( $allowed_classes as $allowed_class ) {
if ( str_starts_with( $class, $allowed_class ) ) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,368 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Processes;
use Elementor\App\Modules\ImportExportCustomization\Module;
use Elementor\App\Modules\ImportExportCustomization\Utils;
use Elementor\Core\Utils\Str;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Elementor_Content;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Export_Runner_Base;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Plugins;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Site_Settings;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Taxonomies;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Templates;
use Elementor\App\Modules\ImportExportCustomization\Runners\Export\Wp_Content;
class Export {
const ZIP_ARCHIVE_MODULE_MISSING = 'zip-archive-module-is-missing';
/**
* @var Export_Runner_Base[]
*/
protected $runners = [];
/**
* Selected content types to export.
*
* @var array
*/
private $settings_include;
/**
* The kit information. (e.g: title, description)
*
* @var array $export_data
*/
private $settings_kit_info;
/**
* Customization settings for selective export.
*
* @var array
*/
private $settings_customization;
/**
* Selected plugins to export.
* Contains the plugins essential data for export. (e.g: name, path, version, etc.)
*
* @var array
*/
private $settings_selected_plugins;
/**
* Selected custom post types to export.
*
* @var array
*/
private $settings_selected_custom_post_types;
/**
* The output data of the export process.
* Will be written into the manifest.json file.
*
* @var array
*/
private $manifest_data;
/**
* The zip archive object.
*
* @var \ZipArchive
*/
private $zip;
public function __construct( $settings = [] ) {
$this->settings_include = ! empty( $settings['include'] ) ? $settings['include'] : null;
$this->settings_kit_info = ! empty( $settings['kitInfo'] ) ? $settings['kitInfo'] : null;
$this->settings_customization = isset( $settings['customization'] ) ? $settings['customization'] : null;
$this->settings_selected_plugins = isset( $settings['plugins'] ) ? $settings['plugins'] : null;
$this->settings_selected_custom_post_types = isset( $settings['customization']['content']['customPostTypes'] ) ? $settings['customization']['content']['customPostTypes'] : null;
}
/**
* Register a runner.
*
* @param Export_Runner_Base $runner_instance
*/
public function register( Export_Runner_Base $runner_instance ) {
$this->runners[ $runner_instance::get_name() ] = $runner_instance;
}
public function register_default_runners() {
$this->register( new Site_Settings() );
$this->register( new Plugins() );
$this->register( new Templates() );
$this->register( new Taxonomies() );
$this->register( new Elementor_Content() );
$this->register( new Wp_Content() );
}
/**
* Execute the export process.
*
* @return array The export data output.
*
* @throws \Exception If no export runners have been specified.
*/
public function run() {
if ( empty( $this->runners ) ) {
throw new \Exception( 'Couldnt execute the export process because no export runners have been specified. Try again by specifying export runners.' );
}
$this->set_default_settings();
$this->init_zip_archive();
$this->init_manifest_data();
$data = [
'include' => $this->settings_include,
'customization' => $this->settings_customization,
'selected_plugins' => $this->settings_selected_plugins,
'selected_custom_post_types' => $this->settings_selected_custom_post_types,
];
foreach ( $this->runners as $runner ) {
if ( $runner->should_export( $data ) ) {
$export_result = $runner->export( $data );
$this->handle_export_result( $export_result );
}
}
$this->add_json_file( 'manifest', $this->manifest_data );
$zip_file_name = $this->zip->filename;
$this->zip->close();
return [
'manifest' => $this->manifest_data,
'file_name' => $zip_file_name,
];
}
/**
* Set default settings for the export.
*/
private function set_default_settings() {
if ( ! is_array( $this->get_settings_include() ) ) {
$this->settings_include( $this->get_default_settings_include() );
}
if ( ! is_array( $this->get_settings_kit_info() ) ) {
$this->settings_kit_info( $this->get_default_settings_kit_info() );
}
if ( ! is_array( $this->get_settings_selected_custom_post_types() ) && in_array( 'content', $this->settings_include, true ) ) {
$this->settings_selected_custom_post_types( $this->get_default_settings_custom_post_types() );
}
if ( ! is_array( $this->get_settings_selected_plugins() ) && in_array( 'plugins', $this->settings_include, true ) ) {
$this->settings_selected_plugins( $this->get_default_settings_selected_plugins() );
}
if ( ! is_array( $this->get_settings_customization() ) ) {
$this->settings_customization( $this->get_default_settings_customization() );
}
}
public function settings_include( $include ) {
$this->settings_include = $include;
}
public function get_settings_include() {
return $this->settings_include;
}
private function settings_kit_info( $kit_info ) {
$this->settings_kit_info = $kit_info;
}
private function get_settings_kit_info() {
return $this->settings_kit_info;
}
public function settings_customization( $customization ) {
$this->settings_customization = $customization;
}
public function get_settings_customization() {
return $this->settings_customization;
}
public function settings_selected_custom_post_types( $selected_custom_post_types ) {
$this->settings_selected_custom_post_types = $selected_custom_post_types;
}
public function get_settings_selected_custom_post_types() {
return $this->settings_selected_custom_post_types;
}
public function settings_selected_plugins( $plugins ) {
$this->settings_selected_plugins = $plugins;
}
public function get_settings_selected_plugins() {
return $this->settings_selected_plugins;
}
/**
* Get the default settings of which content types should be exported.
*
* @return array
*/
private function get_default_settings_include() {
return [ 'templates', 'content', 'settings', 'plugins' ];
}
/**
* Get the default settings of the kit info.
*
* @return array
*/
private function get_default_settings_kit_info() {
return [
'title' => 'kit',
'description' => '',
];
}
/**
* Get the default settings of the plugins that should be exported.
*
* @return array{name: string, plugin:string, pluginUri: string, version: string}
*/
private function get_default_settings_selected_plugins() {
$installed_plugins = Plugin::$instance->wp->get_plugins();
$result = [];
foreach ( $installed_plugins->all() as $key => $item ) {
$plugin_key = str_replace( '.php', '', $key ); // Inconsistency between get_plugins() and WP Rest API's key format.
$result[ $plugin_key ] = [
'name' => $item['Name'],
'plugin' => $plugin_key,
'pluginUri' => $item['PluginURI'],
'version' => $item['Version'],
];
}
return $result;
}
private function get_default_settings_customization() {
return [
'settings' => null,
'templates' => null,
'content' => null,
'plugins' => null,
];
}
/**
* Get the default settings of all the custom post types that should be exported.
* Should be all the custom post types that are not built in to WordPress and not part of Elementor.
*
* @return array
*/
private function get_default_settings_custom_post_types() {
return Utils::get_registered_cpt_names();
}
/**
* Init the zip archive.
*/
private function init_zip_archive() {
if ( ! class_exists( '\ZipArchive' ) ) {
throw new \Error( static::ZIP_ARCHIVE_MODULE_MISSING ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
$zip = new \ZipArchive();
$temp_dir = Plugin::$instance->uploads_manager->create_unique_dir();
$zip_file_name = $temp_dir . sanitize_title( $this->settings_kit_info['title'] ) . '.zip';
$zip->open( $zip_file_name, \ZipArchive::CREATE | \ZipArchive::OVERWRITE );
$this->zip = $zip;
}
/**
* Init the manifest data and add some basic info to it.
*/
private function init_manifest_data() {
$kit_post = Plugin::$instance->kits_manager->get_active_kit()->get_post();
$manifest_data = [
'name' => sanitize_title( $this->settings_kit_info['title'] ),
'title' => $this->settings_kit_info['title'],
'description' => $this->settings_kit_info['description'],
'author' => get_the_author_meta( 'display_name', $kit_post->post_author ),
'version' => Module::FORMAT_VERSION,
'elementor_version' => ELEMENTOR_VERSION,
'created' => gmdate( 'Y-m-d H:i:s' ),
'thumbnail' => get_the_post_thumbnail_url( $kit_post ),
'site' => get_site_url(),
];
$this->manifest_data = $manifest_data;
}
/**
* Handle the export process output.
* Add the manifest data from the runner to the manifest.json file.
* Create files according to the files array that should be exported by the runner.
*
* @param array $export_result
*/
private function handle_export_result( $export_result ) {
foreach ( $export_result['manifest'] as $data ) {
$this->manifest_data += $data;
}
if ( isset( $export_result['files']['path'] ) ) {
$export_result['files'] = [ $export_result['files'] ];
}
foreach ( $export_result['files'] as $file ) {
$file_extension = pathinfo( $file['path'], PATHINFO_EXTENSION );
if ( empty( $file_extension ) ) {
$this->add_json_file(
$file['path'],
$file['data']
);
} else {
$this->add_file(
$file['path'],
$file['data']
);
}
}
}
/**
* Add json file to the zip archive.
*
* @param string $path The relative path to the file.
* @param array $content The content of the file.
* @param int $json_flags
*/
private function add_json_file( $path, array $content, $json_flags = 0 ) {
if ( ! Str::ends_with( $path, '.json' ) ) {
$path .= '.json';
}
$this->add_file( $path, wp_json_encode( $content, $json_flags ) );
}
/**
* Add file to the zip archive.
*
* @param string $file
* @param string $content The content of the file.
*/
private function add_file( $file, $content ) {
$this->zip->addFromString( $file, $content );
}
}

View File

@@ -0,0 +1,840 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Processes;
use Elementor\App\Modules\ImportExportCustomization\Compatibility\Base_Adapter;
use Elementor\App\Modules\ImportExportCustomization\Compatibility\Envato;
use Elementor\App\Modules\ImportExportCustomization\Compatibility\Kit_Library;
use Elementor\App\Modules\ImportExportCustomization\Compatibility\Customization;
use Elementor\App\Modules\ImportExportCustomization\Utils;
use Elementor\Core\Base\Document;
use Elementor\Core\Kits\Documents\Kit;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Elementor_Content;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Import_Runner_Base;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Plugins;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Site_Settings;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Taxonomies;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Templates;
use Elementor\App\Modules\ImportExportCustomization\Runners\Import\Wp_Content;
use Elementor\App\Modules\ImportExportCustomization\Module;
class Import {
const MANIFEST_ERROR_KEY = 'manifest-error';
const ZIP_FILE_ERROR_KEY = 'invalid-zip-file';
const ZIP_ARCHIVE_ERROR_KEY = 'zip-archive-module-missing';
/**
* @var Import_Runner_Base[]
*/
protected $runners = [];
/**
* The session ID of the import process.
* This ID is uniquely generated for each import process (by the temp folder which contains the extracted kit files).
*
* @var string
*/
private $session_id;
/**
* The Kit ID.
*
* @var string
*/
private $kit_id;
/**
* Adapter for the kit compatibility.
*
* @var Base_Adapter[]
*/
private $adapters;
/**
* Document's data (elements and settings) that was imported during the process.
*
* @var array { [document_id] => { "elements": array , "settings": array } }
*/
private $documents_data = [];
/**
* Path to the extracted kit files.
*
* @var string
*/
private $extracted_directory_path;
/**
* Imported kit manifest.
*
* @var array
*/
private $manifest;
/**
* Imported kit site settings. (e.g: custom_colors, custom_typography, etc.)
*
* @var array
*/
private $site_settings;
/**
* Selected content types to import.
*
* @var array
*/
private $settings_include;
/**
* Referer of the import. (e.g: kit-library, local, etc.)
*
* @var string
*/
private $settings_referrer;
/**
* All the conflict between the exited templates and the kit templates.
*
* @var array
*/
private $settings_conflicts;
/**
* Selected elementor templates conditions to override.
*
* @var array
*/
private $settings_selected_override_conditions;
/**
* Selected custom post types to import.
*
* @var array
*/
private $settings_selected_custom_post_types;
/**
* Selected plugins to import.
*
* @var array
*/
private $settings_selected_plugins;
/**
* Customization settings for selective import.
*
* @var array
*/
private $settings_customization;
/**
* The imported data output.
*
* @var array
*/
private $imported_data = [];
/**
* The metadata output of the import runners.
* Will be saved in the import_session and will be used to revert the import process.
*
* @var array
*/
private $runners_import_metadata = [];
/**
* @param string $path session_id | zip_file_path
* @param array $settings Use to determine which content to import.
* (e.g: include, selected_plugins, selected_cpt, selected_override_conditions, etc.)
* @param array|null $old_instance An array of old instance parameters that will be used for creating new instance.
* We are using it for quick creation of the instance when the import process is being split into chunks.
* @throws \Exception If the import session does not exist.
*/
public function __construct( string $path, array $settings = [], array $old_instance = null ) {
if ( ! empty( $old_instance ) ) {
$this->set_import_object( $old_instance );
} else {
if ( is_file( $path ) ) {
$this->extracted_directory_path = $this->extract_zip( $path );
} else {
$elementor_tmp_directory = Plugin::$instance->uploads_manager->get_temp_dir();
$path = $elementor_tmp_directory . basename( $path );
if ( ! is_dir( $path ) ) {
throw new \Exception( 'Couldnt execute the import process because the import session does not exist.' );
}
$this->extracted_directory_path = $path . '/';
}
$this->session_id = basename( $this->extracted_directory_path );
$this->kit_id = $settings['id'] ?? '';
$this->settings_referrer = ! empty( $settings['referrer'] ) ? $settings['referrer'] : 'local';
$this->settings_include = ! empty( $settings['include'] ) ? $settings['include'] : null;
// Using isset and not empty is important since empty array is valid option.
$this->settings_selected_override_conditions = $settings['customization']['templates']['themeBuilder']['overrideConditions'] ?? null;
$this->settings_selected_custom_post_types = $settings['customization']['content']['customPostTypes'] ?? null;
$this->settings_selected_plugins = $settings['plugins'] ?? null;
$this->settings_customization = $settings['customization'] ?? null;
$this->manifest = $this->read_manifest_json();
$this->site_settings = $this->read_site_settings_json();
$this->set_default_settings();
}
add_filter( 'wp_php_error_args', function ( $args, $error ) {
return $this->filter_php_error_args( $args, $error );
}, 10, 2 );
}
/**
* Set the import object parameters.
*
* @param array $instance
* @return void
*/
private function set_import_object( array $instance ) {
$this->session_id = $instance['session_id'];
$instance_data = $instance['instance_data'];
$this->extracted_directory_path = $instance_data['extracted_directory_path'];
$this->runners = $instance_data['runners'];
$this->adapters = $instance_data['adapters'];
$this->manifest = $instance_data['manifest'];
$this->site_settings = $instance_data['site_settings'];
$this->settings_include = $instance_data['settings_include'];
$this->settings_referrer = $instance_data['settings_referrer'];
$this->settings_conflicts = $instance_data['settings_conflicts'];
$this->settings_selected_override_conditions = $instance_data['settings_selected_override_conditions'];
$this->settings_selected_custom_post_types = $instance_data['settings_selected_custom_post_types'];
$this->settings_selected_plugins = $instance_data['settings_selected_plugins'];
$this->settings_customization = $instance_data['settings_customization'];
$this->documents_data = $instance_data['documents_data'];
$this->imported_data = $instance_data['imported_data'];
$this->runners_import_metadata = $instance_data['runners_import_metadata'];
}
/**
* Creating a new instance of the import process by the id of the old import session.
*
* @param string $session_id
*
* @return Import
* @throws \Exception If the import session does not exist.
*/
public static function from_session( string $session_id ): Import {
$import_sessions = Utils::get_import_sessions();
if ( ! $import_sessions || ! isset( $import_sessions[ $session_id ] ) ) {
throw new \Exception( 'Couldnt execute the import process because the import session does not exist.' );
}
$import_session = $import_sessions[ $session_id ];
return new self( $session_id, [], $import_session );
}
/**
* Register a runner.
* Be aware that the runner will be executed in the order of registration, the order is crucial for the import process.
*
* @param Import_Runner_Base $runner_instance
*/
public function register( Import_Runner_Base $runner_instance ) {
$this->runners[ $runner_instance::get_name() ] = $runner_instance;
}
public function register_default_runners() {
$this->register( new Site_Settings() );
$this->register( new Plugins() );
$this->register( new Templates() );
$this->register( new Taxonomies() );
$this->register( new Elementor_Content() );
$this->register( new Wp_Content() );
}
/**
* Set default settings for the import.
*/
private function set_default_settings() {
if ( ! is_array( $this->get_settings_include() ) ) {
$this->settings_include( $this->get_default_settings_include() );
}
if ( ! is_array( $this->get_settings_conflicts() ) ) {
$this->settings_conflicts( $this->get_default_settings_conflicts() );
}
if ( ! is_array( $this->get_settings_selected_override_conditions() ) ) {
$this->settings_selected_override_conditions( $this->get_default_settings_override_conditions() );
}
if ( ! is_array( $this->get_settings_selected_custom_post_types() ) ) {
$this->settings_selected_custom_post_types( $this->get_default_settings_custom_post_types() );
}
if ( ! is_array( $this->get_settings_selected_plugins() ) ) {
$this->settings_selected_plugins( $this->get_default_settings_plugins() );
}
if ( ! is_array( $this->get_settings_customization() ) ) {
$this->settings_customization( $this->get_default_settings_customization() );
}
}
/**
* Execute the import process.
*
* @return array The imported data output.
*
* @throws \Exception If no import runners have been specified.
*/
public function run() {
if ( empty( $this->runners ) ) {
throw new \Exception( 'Couldnt execute the import process because no import runners have been specified. Try again by specifying import runners.' );
}
$data = [
'session_id' => $this->session_id,
'include' => $this->settings_include,
'manifest' => $this->manifest,
'site_settings' => $this->site_settings,
'selected_plugins' => $this->settings_selected_plugins,
'customization' => $this->settings_customization,
'extracted_directory_path' => $this->extracted_directory_path,
'selected_custom_post_types' => $this->settings_selected_custom_post_types,
];
$this->init_import_session();
remove_filter( 'elementor/document/save/data', [ Plugin::$instance->modules_manager->get_modules( 'content-sanitizer' ), 'sanitize_content' ] );
add_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10, 2 );
// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
foreach ( $this->runners as $runner ) {
if ( $runner->should_import( $data ) ) {
$import = $runner->import( $data, $this->imported_data );
$this->imported_data = array_merge_recursive( $this->imported_data, $import );
$this->runners_import_metadata[ $runner::get_name() ] = $runner->get_import_session_metadata();
}
}
// After the upload complete, set the elementor upload state back to false.
Plugin::$instance->uploads_manager->set_elementor_upload_state( false );
remove_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10 );
$this->finalize_import_session_option();
$this->save_elements_of_imported_posts();
Plugin::$instance->uploads_manager->remove_file_or_dir( $this->extracted_directory_path );
return $this->imported_data;
}
/**
* Run specific runner by runner_name
*
* @param string $runner_name
*
* @return array
*
* @throws \Exception If no export runners have been specified.
*/
public function run_runner( string $runner_name ): array {
if ( empty( $this->runners ) ) {
throw new \Exception( 'Couldnt execute the import process because no import runners have been specified. Try again by specifying import runners.' );
}
$data = [
'session_id' => $this->session_id,
'include' => $this->settings_include,
'manifest' => $this->manifest,
'site_settings' => $this->site_settings,
'selected_plugins' => $this->settings_selected_plugins,
'customization' => $this->settings_customization,
'extracted_directory_path' => $this->extracted_directory_path,
'selected_custom_post_types' => $this->settings_selected_custom_post_types,
];
add_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10, 2 );
// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
$runner = $this->runners[ $runner_name ];
if ( empty( $runner ) ) {
throw new \Exception( 'Couldnt execute the import process because the import runner was not found. Try again by specifying an import runner.' );
}
if ( $runner->should_import( $data ) ) {
$import = $runner->import( $data, $this->imported_data );
$this->imported_data = array_merge_recursive( $this->imported_data, $import );
$this->runners_import_metadata[ $runner::get_name() ] = $runner->get_import_session_metadata();
}
// After the upload complete, set the elementor upload state back to false.
Plugin::$instance->uploads_manager->set_elementor_upload_state( false );
remove_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10 );
$is_last_runner = key( array_slice( $this->runners, -1, 1, true ) ) === $runner_name;
if ( $is_last_runner ) {
$this->finalize_import_session_option();
$this->save_elements_of_imported_posts();
} else {
$this->update_instance_data_in_import_session_option();
}
return [
'status' => 'success',
'runner' => $runner_name,
'imported_data' => $this->imported_data,
];
}
/**
* Create and save all the instance data to the import sessions option.
*
* @return void
*/
public function init_import_session( $save_instance_data = false ) {
$import_sessions = Utils::get_import_sessions( true );
$import_sessions[ $this->session_id ] = [
'session_id' => $this->session_id,
'kit_title' => $this->manifest['title'] ?? '',
'kit_name' => $this->manifest['name'] ?? '',
'kit_thumbnail' => $this->get_kit_thumbnail(),
'kit_source' => $this->settings_referrer,
'user_id' => get_current_user_id(),
'start_timestamp' => current_time( 'timestamp' ),
];
if ( $save_instance_data ) {
$import_sessions[ $this->session_id ]['instance_data'] = [
'extracted_directory_path' => $this->extracted_directory_path,
'runners' => $this->runners,
'adapters' => $this->adapters,
'manifest' => $this->manifest,
'site_settings' => $this->site_settings,
'settings_include' => $this->settings_include,
'settings_referrer' => $this->settings_referrer,
'settings_conflicts' => $this->settings_conflicts,
'settings_selected_override_conditions' => $this->settings_selected_override_conditions,
'settings_selected_custom_post_types' => $this->settings_selected_custom_post_types,
'settings_selected_plugins' => $this->settings_selected_plugins,
'settings_customization' => $this->settings_customization,
'documents_data' => $this->documents_data,
'imported_data' => $this->imported_data,
'runners_import_metadata' => $this->runners_import_metadata,
];
}
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
}
/**
* Get the Kit thumbnail, goes to the home page thumbnail if main doesn't exist
*
* @return string
*/
private function get_kit_thumbnail(): string {
if ( ! empty( $this->manifest['thumbnail'] ) ) {
return $this->manifest['thumbnail'];
}
return apply_filters( 'elementor/import/kit_thumbnail', '', $this->kit_id, $this->settings_referrer );
}
public function get_runners_name(): array {
return array_keys( $this->runners );
}
public function get_manifest() {
return $this->manifest;
}
public function get_extracted_directory_path() {
return $this->extracted_directory_path;
}
public function get_session_id() {
return $this->session_id;
}
public function get_adapters() {
return $this->adapters;
}
public function get_imported_data() {
return $this->imported_data;
}
/**
* Get settings by key.
* Used for backward compatibility.
*
* @param string $key The key of the setting.
*/
public function get_settings( $key ) {
switch ( $key ) {
case 'include':
return $this->get_settings_include();
case 'overrideConditions':
return $this->get_settings_selected_override_conditions();
case 'selectedCustomPostTypes':
return $this->get_settings_selected_custom_post_types();
case 'plugins':
return $this->get_settings_selected_plugins();
case 'customization':
return $this->get_settings_customization();
default:
return [];
}
}
public function settings_include( array $settings_include ) {
$this->settings_include = $settings_include;
return $this;
}
public function get_settings_include() {
return $this->settings_include;
}
public function settings_referrer( $settings_referrer ) {
$this->settings_referrer = $settings_referrer;
return $this;
}
public function get_settings_referrer() {
return $this->settings_referrer;
}
public function settings_conflicts( array $settings_conflicts ) {
$this->settings_conflicts = $settings_conflicts;
return $this;
}
public function get_settings_conflicts() {
return $this->settings_conflicts;
}
public function settings_selected_override_conditions( array $settings_selected_override_conditions ) {
$this->settings_selected_override_conditions = $settings_selected_override_conditions;
return $this;
}
public function get_settings_selected_override_conditions() {
return $this->settings_selected_override_conditions;
}
public function settings_selected_custom_post_types( array $settings_selected_custom_post_types ) {
$this->settings_selected_custom_post_types = $settings_selected_custom_post_types;
return $this;
}
public function get_settings_selected_custom_post_types() {
return $this->settings_selected_custom_post_types;
}
public function settings_selected_plugins( array $settings_selected_plugins ) {
$this->settings_selected_plugins = $settings_selected_plugins;
return $this;
}
public function get_settings_selected_plugins() {
return $this->settings_selected_plugins;
}
/**
* Prevent saving elements on elementor post creation.
*
* @param array $data
* @param Document $document
*
* @return array
*/
public function prevent_saving_elements_on_post_creation( array $data, Document $document ) {
if ( isset( $data['elements'] ) ) {
$this->documents_data[ $document->get_main_id() ] = [ 'elements' => $data['elements'] ];
$data['elements'] = [];
}
if ( isset( $data['settings'] ) ) {
$this->documents_data[ $document->get_main_id() ]['settings'] = $data['settings'];
}
return $data;
}
/**
* Extract the zip file.
*
* @param string $zip_path The path to the zip file.
* @return string The extracted directory path.
*/
private function extract_zip( $zip_path ) {
$extraction_result = Plugin::$instance->uploads_manager->extract_and_validate_zip( $zip_path, [ 'json', 'xml' ] );
if ( is_wp_error( $extraction_result ) ) {
if ( isset( $extraction_result->errors['zip_error'] ) ) {
throw new \Error( static::ZIP_ARCHIVE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
throw new \Error( static::ZIP_FILE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
return $extraction_result['extraction_directory'];
}
/**
* Get the manifest file from the extracted directory and adapt it if needed.
*
* @return string The manifest file content.
*/
private function read_manifest_json() {
$manifest = Utils::read_json_file( $this->extracted_directory_path . 'manifest' );
if ( ! $manifest ) {
Plugin::$instance->logger->get_logger()->error( static::MANIFEST_ERROR_KEY );
throw new \Error( static::ZIP_FILE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
$this->init_adapters( $manifest );
foreach ( $this->adapters as $adapter ) {
$manifest = $adapter->adapt_manifest( $manifest );
}
return $manifest;
}
/**
* Init the adapters and determine which ones to use.
*
* @param array $manifest_data The manifest file content.
*/
private function init_adapters( array $manifest_data ) {
$this->adapters = [];
/** @var Base_Adapter[] $adapter_types */
$adapter_types = [ Customization::class, Envato::class, Kit_Library::class ];
foreach ( $adapter_types as $adapter_type ) {
if ( $adapter_type::is_compatibility_needed( $manifest_data, [ 'referrer' => $this->get_settings_referrer() ] ) ) {
$this->adapters[] = new $adapter_type( $this );
}
}
}
/**
* Get the site settings file from the extracted directory and adapt it if needed.
*
* @return string The site settings file content.
*/
private function read_site_settings_json() {
$site_settings = Utils::read_json_file( $this->extracted_directory_path . 'site-settings' );
foreach ( $this->adapters as $adapter ) {
$site_settings = $adapter->adapt_site_settings( $site_settings, $this->manifest, $this->extracted_directory_path );
}
return $site_settings;
}
/**
* Get all the custom post types in the kit.
*
* @return array Custom post types names.
*/
private function get_default_settings_custom_post_types() {
if ( empty( $this->manifest['custom-post-type-title'] ) ) {
return [];
}
$excluded = [ 'page', 'nav_menu_item' ];
if ( empty( $this->manifest['content']['post'] ?? [] ) && empty( $this->manifest['wp-content']['post'] ?? [] ) ) {
$excluded[] = 'post';
}
$manifest_post_types = array_keys( $this->manifest['custom-post-type-title'] );
return array_merge( $manifest_post_types, Utils::get_builtin_wp_post_types( $excluded ) );
}
/**
* Get the default settings of elementor templates conditions to override.
*
* @return array
*/
private function get_default_settings_conflicts() {
if ( empty( $this->manifest['templates'] ) ) {
return [];
}
return apply_filters( 'elementor/import/get_default_settings_conflicts', [], $this->manifest['templates'] );
}
/**
* Get the default settings of elementor templates conditions to override.
*
* @return array
*/
private function get_default_settings_override_conditions() {
if ( empty( $this->settings_conflicts ) ) {
return [];
}
return array_keys( $this->settings_conflicts );
}
/**
* Get the default settings of the plugins that should be imported.
*
* @return array
*/
private function get_default_settings_plugins() {
return ! empty( $this->manifest['plugins'] ) ? $this->manifest['plugins'] : [];
}
/**
* Get the default settings of which content types should be imported.
*
* @return array
*/
private function get_default_settings_include() {
return [ 'templates', 'plugins', 'content', 'settings' ];
}
public function settings_customization( $customization ) {
$this->settings_customization = $customization;
return $this;
}
public function get_settings_customization() {
return $this->settings_customization;
}
private function get_default_settings_customization() {
return [
'settings' => null,
'templates' => null,
'content' => null,
'plugins' => null,
];
}
/**
* Get the data that requires updating/replacement when imported.
*
* @return array{post_ids: array, term_ids: array}
*/
private function get_imported_data_replacements(): array {
return [
'post_ids' => Utils::map_old_new_post_ids( $this->imported_data ),
'term_ids' => Utils::map_old_new_term_ids( $this->imported_data ),
];
}
/**
* Save the prevented elements on elementor post creation elements.
* Handle the replacement of all the dynamic content of the elements that probably have been changed during the import.
*/
private function save_elements_of_imported_posts() {
$imported_data_replacements = $this->get_imported_data_replacements();
foreach ( $this->documents_data as $new_id => $data ) {
$document = Plugin::$instance->documents->get( $new_id );
if ( isset( $data['elements'] ) ) {
$data['elements'] = $document->on_import_update_dynamic_content( $data['elements'], $imported_data_replacements );
}
if ( isset( $data['settings'] ) ) {
if ( $document instanceof Kit ) {
// Without post_status certain tabs in the Kit will not save properly.
$data['settings']['post_status'] = get_post_status( $new_id );
}
$data['settings'] = $document->on_import_update_settings( $data['settings'], $imported_data_replacements );
}
$document->save( $data );
}
}
private function update_instance_data_in_import_session_option() {
$import_sessions = Utils::get_import_sessions();
$import_sessions[ $this->session_id ]['instance_data']['documents_data'] = $this->documents_data;
$import_sessions[ $this->session_id ]['instance_data']['imported_data'] = $this->imported_data;
$import_sessions[ $this->session_id ]['instance_data']['runners_import_metadata'] = $this->runners_import_metadata;
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
}
public function finalize_import_session_option() {
$import_sessions = Utils::get_import_sessions();
if ( ! isset( $import_sessions[ $this->session_id ] ) ) {
return;
}
unset( $import_sessions[ $this->session_id ]['instance_data'] );
$import_sessions[ $this->session_id ]['end_timestamp'] = current_time( 'timestamp' );
$import_sessions[ $this->session_id ]['runners'] = $this->runners_import_metadata;
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
}
/**
* Filter the php error args and return 408 status code if the error is a timeout.
*
* @param array $args
* @param array $error
* @return array
*/
private function filter_php_error_args( $args, $error ) {
if ( strpos( $error['message'], 'Maximum execution time' ) !== false ) {
$args['response'] = 408;
}
return $args;
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Processes;
use Elementor\App\Modules\ImportExportCustomization\Module;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Elementor_Content;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Revert_Runner_Base;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Plugins;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Site_Settings;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Taxonomies;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Templates;
use Elementor\App\Modules\ImportExportCustomization\Runners\Revert\Wp_Content;
use Elementor\App\Modules\ImportExportCustomization\Utils;
class Revert {
/**
* @var Revert_Runner_Base[]
*/
protected $runners = [];
private $import_sessions;
private $revert_sessions;
public function __construct() {
$this->import_sessions = self::get_import_sessions();
$this->revert_sessions = self::get_revert_sessions();
}
/**
* Register a runner.
*
* @param Revert_Runner_Base $runner_instance
*/
public function register( Revert_Runner_Base $runner_instance ) {
$this->runners[ $runner_instance::get_name() ] = $runner_instance;
}
public function register_default_runners() {
$this->register( new Site_Settings() );
$this->register( new Plugins() );
$this->register( new Templates() );
$this->register( new Taxonomies() );
$this->register( new Elementor_Content() );
$this->register( new Wp_Content() );
}
/**
* Execute the revert process.
*
* @throws \Exception If no revert runners have been specified.
*/
public function run() {
if ( empty( $this->runners ) ) {
throw new \Exception( 'Couldnt execute the revert process because no revert runners have been specified. Try again by specifying revert runners.' );
}
$import_session = $this->get_last_import_session();
if ( empty( $import_session ) ) {
throw new \Exception( 'Couldnt execute the revert process because there are no import sessions to revert.' );
}
// fallback if the import session failed and doesn't have the runners metadata
if ( ! isset( $import_session['runners'] ) && isset( $import_session['instance_data'] ) ) {
$import_session['runners'] = $import_session['instance_data']['runners_import_metadata'] ?? [];
}
foreach ( $this->runners as $runner ) {
if ( $runner->should_revert( $import_session ) ) {
$runner->revert( $import_session );
}
}
$this->revert_attachments( $import_session );
$this->delete_last_import_data();
}
public static function get_import_sessions() {
$import_sessions = Utils::get_import_sessions();
if ( ! $import_sessions ) {
return [];
}
usort( $import_sessions, function( $a, $b ) {
return strcmp( $a['start_timestamp'], $b['start_timestamp'] );
} );
return $import_sessions;
}
public static function get_revert_sessions() {
$revert_sessions = get_option( Module::OPTION_KEY_ELEMENTOR_REVERT_SESSIONS );
if ( ! $revert_sessions ) {
return [];
}
return $revert_sessions;
}
public function get_last_import_session() {
$import_sessions = $this->import_sessions;
if ( empty( $import_sessions ) ) {
return [];
}
return end( $import_sessions );
}
public function get_penultimate_import_session() {
$sessions_data = $this->import_sessions;
$penultimate_element_value = [];
if ( empty( $sessions_data ) ) {
return [];
}
end( $sessions_data );
prev( $sessions_data );
if ( ! is_null( key( $sessions_data ) ) ) {
$penultimate_element_value = current( $sessions_data );
}
return $penultimate_element_value;
}
private function delete_last_import_data() {
$import_sessions = $this->import_sessions;
$revert_sessions = $this->revert_sessions;
$reverted_session = array_pop( $import_sessions );
$revert_sessions[] = [
'session_id' => $reverted_session['session_id'],
'kit_title' => $reverted_session['kit_title'],
'kit_name' => $reverted_session['kit_name'],
'kit_thumbnail' => $reverted_session['kit_thumbnail'],
'source' => $reverted_session['kit_source'],
'user_id' => get_current_user_id(),
'import_timestamp' => $reverted_session['start_timestamp'],
'revert_timestamp' => current_time( 'timestamp' ),
];
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
update_option( Module::OPTION_KEY_ELEMENTOR_REVERT_SESSIONS, $revert_sessions, false );
$this->import_sessions = $import_sessions;
$this->revert_sessions = $revert_sessions;
}
private function revert_attachments( $data ) {
$query_args = [
'post_type' => 'attachment',
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => Module::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
];
$query = new \WP_Query( $query_args );
foreach ( $query->posts as $post ) {
wp_delete_attachment( $post->ID, true );
}
}
}

View File

@@ -0,0 +1,156 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;
class Elementor_Content extends Export_Runner_Base {
private $page_on_front_id;
public function __construct() {
$this->init_page_on_front_data();
}
public static function get_name(): string {
return 'elementor-content';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true )
);
}
public function export( array $data ) {
$customization = $data['customization']['content'] ?? null;
$selected_custom_post_types = $data['selected_custom_post_types'] ?? null;
$excluded_post_types = [];
if ( $selected_custom_post_types && ! in_array( 'post', $selected_custom_post_types, true ) ) {
$excluded_post_types[] = 'post';
}
$elementor_post_types = ImportExportUtils::get_elementor_post_types( $excluded_post_types );
$elementor_post_types = apply_filters( 'elementor/import-export-customization/elementor-content/post-types/customization', $elementor_post_types, $customization );
$files = [];
$manifest = [];
foreach ( $elementor_post_types as $post_type ) {
$export = $this->export_elementor_post_type( $post_type, $customization );
$files = array_merge( $files, $export['files'] );
$manifest[ $post_type ] = $export['manifest_data'];
}
$manifest_data['content'] = $manifest;
return [
'files' => $files,
'manifest' => [
$manifest_data,
],
];
}
private function export_elementor_post_type( $post_type, $customization ) {
$manifest_data = [];
$files = [];
$query_args = [
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
'compare' => 'EXISTS',
],
[
'key' => '_elementor_data',
'compare' => 'EXISTS',
],
[
'key' => '_elementor_data',
'compare' => '!=',
'value' => '[]',
],
],
];
$query_args = apply_filters( 'elementor/import-export-customization/export/elementor-content/query-args/customization', $query_args, $post_type, $customization );
$query = new \WP_Query( $query_args );
if ( empty( $query ) ) {
return [
'files' => [],
'manifest_data' => [],
];
}
$post_type_taxonomies = $this->get_post_type_taxonomies( $post_type );
foreach ( $query->posts as $post ) {
$document = Plugin::$instance->documents->get( $post->ID );
$terms = ! empty( $post_type_taxonomies ) ? $this->get_post_terms( $post->ID, $post_type_taxonomies ) : [];
$post_manifest_data = [
'title' => $post->post_title,
'excerpt' => $post->post_excerpt,
'doc_type' => $document->get_name(),
'thumbnail' => get_the_post_thumbnail_url( $post ),
'url' => get_permalink( $post ),
'terms' => $terms,
];
if ( $post->ID === $this->page_on_front_id ) {
$post_manifest_data['show_on_front'] = true;
}
$manifest_data[ $post->ID ] = $post_manifest_data;
$files[] = [
'path' => 'content/' . $post_type . '/' . $post->ID,
'data' => $document->get_export_data(),
];
}
return [
'files' => $files,
'manifest_data' => $manifest_data,
];
}
private function get_post_type_taxonomies( $post_type ) {
return get_object_taxonomies( $post_type );
}
private function get_post_terms( $post_id, array $taxonomies ) {
$terms = wp_get_object_terms( $post_id, $taxonomies );
$result = [];
foreach ( $terms as $term ) {
$result[] = [
'term_id' => $term->term_id,
'taxonomy' => $term->taxonomy,
'slug' => $term->slug,
];
}
return $result;
}
private function init_page_on_front_data() {
$show_page_on_front = 'page' === get_option( 'show_on_front' );
if ( $show_page_on_front ) {
$this->page_on_front_id = (int) get_option( 'page_on_front' );
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;
use Elementor\App\Modules\ImportExportCustomization\Runners\Runner_Interface;
abstract class Export_Runner_Base implements Runner_Interface {
/**
* By the passed data we should decide if we want to run the export function of the runner or not.
*
* @param array $data
*
* @return bool
*/
abstract public function should_export( array $data );
/**
* Main function of the runner export process.
*
* @param array $data Necessary data for the export process.
*
* @return array{files: array, manifest: array}
* The files that should be part of the kit and the relevant manifest data.
*/
abstract public function export( array $data );
}

View File

@@ -0,0 +1,43 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;
use Elementor\Core\Utils\Collection;
class Plugins extends Export_Runner_Base {
public static function get_name(): string {
return 'plugins';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'plugins', $data['include'], true ) &&
is_array( $data['selected_plugins'] )
);
}
public function export( array $data ) {
$customization = $data['customization']['plugins'] ?? null;
if ( $customization ) {
$enabled_plugin_keys = Collection::make( $customization )->filter()->keys();
$plugins = Collection::make( $data['selected_plugins'] )
->filter( function( $plugin_data, $plugin_key ) use ( $enabled_plugin_keys ) {
return $enabled_plugin_keys->contains( $plugin_key );
} )
->all();
} else {
$plugins = $data['selected_plugins'];
}
return [
'manifest' => [
[ 'plugins' => array_values( $plugins ) ],
],
'files' => [],
];
}
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;
use Elementor\Plugin;
class Site_Settings extends Export_Runner_Base {
const ALLOWED_SETTINGS = [
'theme',
'globalColors',
'globalFonts',
'themeStyleSettings',
'generalSettings',
'experiments',
];
public static function get_name(): string {
return 'site-settings';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'settings', $data['include'], true )
);
}
public function export( array $data ) {
$customization = $data['customization']['settings'] ?? null;
if ( $customization ) {
return $this->export_customization( $data, $customization );
}
return $this->export_all( $data );
}
private function export_all( $data, $include_theme = true ) {
$kit = Plugin::$instance->kits_manager->get_active_kit();
$kit_data = $kit->get_export_data();
$excluded_kit_settings_keys = [
'site_name',
'site_description',
'site_logo',
'site_favicon',
];
foreach ( $excluded_kit_settings_keys as $setting_key ) {
unset( $kit_data['settings'][ $setting_key ] );
}
if ( $include_theme ) {
$theme_data = $this->export_theme();
if ( $theme_data ) {
$kit_data['theme'] = $theme_data;
$manifest_data['theme'] = $theme_data;
}
}
$experiments_data = $this->export_experiments();
if ( $experiments_data ) {
$kit_data['experiments'] = $experiments_data;
$manifest_data['experiments'] = array_keys( $experiments_data );
}
$manifest_data['site-settings'] = array_fill_keys( self::ALLOWED_SETTINGS, true );
if ( ! $include_theme ) {
$manifest_data['site-settings']['theme'] = false;
}
return [
'files' => [
'path' => 'site-settings',
'data' => $kit_data,
],
'manifest' => [
$manifest_data,
],
];
}
private function export_customization( $data, $customization ) {
$result = apply_filters( 'elementor/import-export-customization/export/site-settings/customization', null, $data, $customization, $this );
if ( is_array( $result ) ) {
return $result;
}
return $this->export_all( $data, ! empty( $customization['theme'] ) );
}
public function export_theme() {
$theme = wp_get_theme();
if ( empty( $theme ) || empty( $theme->get( 'ThemeURI' ) ) ) {
return null;
}
$theme_data['name'] = $theme->get( 'Name' );
$theme_data['theme_uri'] = $theme->get( 'ThemeURI' );
$theme_data['version'] = $theme->get( 'Version' );
$theme_data['slug'] = $theme->get_stylesheet();
return $theme_data;
}
public function export_experiments() {
$features = Plugin::$instance->experiments->get_features();
if ( empty( $features ) ) {
return null;
}
$experiments_data = [];
foreach ( $features as $feature_name => $feature ) {
$experiments_data[ $feature_name ] = [
'name' => $feature_name,
'title' => $feature['title'],
'state' => $feature['state'],
'default' => $feature['default'],
'release_status' => $feature['release_status'],
];
}
return empty( $experiments_data ) ? null : $experiments_data;
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
class Taxonomies extends Export_Runner_Base {
public static function get_name(): string {
return 'taxonomies';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true )
);
}
public function export( array $data ) {
$customization = $data['customization']['content'] ?? null;
if ( $customization ) {
return $this->export_customization( $data, $customization );
}
return $this->export_all( $data );
}
public function export_customization( array $data, array $customization ) {
$result = apply_filters( 'elementor/import-export-customization/export/taxonomies/customization', null, $data, $customization, $this );
if ( is_array( $result ) ) {
return $result;
}
return $this->export_all( $data );
}
public function export_all( array $data ) {
$selected_custom_post_types = $data['selected_custom_post_types'] ?? null;
$exclude_post_types = [];
if ( is_array( $selected_custom_post_types ) && ! in_array( 'post', $selected_custom_post_types, true ) ) {
$exclude_post_types[] = 'post';
}
$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types( $exclude_post_types );
$post_types = is_array( $selected_custom_post_types )
? array_merge( $wp_builtin_post_types, $selected_custom_post_types )
: $wp_builtin_post_types;
$export = $this->export_taxonomies( $post_types );
$manifest_data['taxonomies'] = $export['manifest'];
return [
'files' => $export['files'],
'manifest' => [
$manifest_data,
],
];
}
private function export_taxonomies( array $post_types ) {
$files = [];
$manifest = [];
$taxonomies = get_taxonomies();
foreach ( $taxonomies as $taxonomy ) {
$taxonomy_obj = get_taxonomy( $taxonomy );
$taxonomy_post_types = $taxonomy_obj->object_type;
$intersected_post_types = array_intersect( $taxonomy_post_types, $post_types );
if ( empty( $intersected_post_types ) ) {
continue;
}
$data = $this->export_terms( $taxonomy );
if ( empty( $data ) ) {
continue;
}
foreach ( $intersected_post_types as $post_type ) {
$manifest[ $post_type ][] = [
'name' => $taxonomy,
'label' => $taxonomy_obj->label,
];
}
$files[] = [
'path' => 'taxonomies/' . $taxonomy,
'data' => $data,
];
}
return [
'files' => $files,
'manifest' => $manifest,
];
}
public function export_terms( $taxonomy ) {
$terms = get_terms( [
'taxonomy' => (array) $taxonomy,
'hide_empty' => true,
'get' => 'all',
] );
$ordered_terms = $this->order_terms( $terms );
if ( empty( $ordered_terms ) ) {
return [];
}
$data = [];
foreach ( $ordered_terms as $term ) {
$data[] = [
'term_id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'taxonomy' => $term->taxonomy,
'description' => $term->description,
'parent' => $term->parent,
];
}
return $data;
}
/**
* Put terms in order with no child going before its parent.
*/
private function order_terms( array $terms ) {
$ordered_terms = [];
while ( $term = array_shift( $terms ) ) {
$is_top_level = 0 === $term->parent;
$is_parent_exits = isset( $ordered_terms[ $term->parent ] );
if ( $is_top_level || $is_parent_exits ) {
$ordered_terms[ $term->term_id ] = $term;
} else {
$terms[] = $term;
}
}
return $ordered_terms;
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;
use Elementor\Core\Base\Document;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
use Elementor\Modules\Library\Documents\Library_Document;
class Templates extends Export_Runner_Base {
public static function get_name(): string {
return 'templates';
}
public function should_export( array $data ) {
return (
Utils::has_pro() &&
isset( $data['include'] ) &&
in_array( 'templates', $data['include'], true )
);
}
public function export( array $data ) {
$customization = $data['customization']['templates'] ?? null;
if ( $customization ) {
return $this->export_with_customization( $data, $customization );
}
return $this->export_all( $data );
}
private function export_with_customization( array $data, array $customization ) {
$result = apply_filters( 'elementor/import-export-customization/export/templates/customization', null, $data, $customization, $this );
if ( is_array( $result ) ) {
return $result;
}
return $this->export_all( $data );
}
private function export_all( array $data ) {
$template_types = array_values( Source_Local::get_template_types() );
return $this->export_templates_by_types( $template_types, $data );
}
public function export_templates_by_types( array $template_types, array $data ) {
$templates_manifest_data = [];
$files = [];
if ( ! empty( $template_types ) ) {
$query_args = [
'post_type' => Source_Local::CPT,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => Document::TYPE_META_KEY,
'value' => $template_types,
],
],
];
$templates_query = new \WP_Query( $query_args );
foreach ( $templates_query->posts as $template_post ) {
$template_id = $template_post->ID;
$template_document = Plugin::$instance->documents->get( $template_id );
$templates_manifest_data[ $template_id ] = $template_document->get_export_summary();
$files[] = [
'path' => 'templates/' . $template_id,
'data' => $template_document->get_export_data(),
];
}
}
$manifest_data['templates'] = $templates_manifest_data;
$export_data = [
'files' => $files,
'manifest' => [
$manifest_data,
],
];
/**
* Filter the templates export data to allow adding additional data.
*
* @param array $export_data The export data structure with 'files' and 'manifest' keys.
* @param array $data The full export data.
* @param array|null $customization The customization settings for templates.
*/
$customization = $data['customization']['templates'] ?? null;
$export_data = apply_filters( 'elementor/import-export-customization/export/templates_data', $export_data, $data, $customization );
return $export_data;
}
}

View File

@@ -0,0 +1,109 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Export;
use Elementor\App\Modules\ImportExportCustomization\Compatibility\Customization;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Core\Utils\ImportExport\WP_Exporter;
class Wp_Content extends Export_Runner_Base {
public static function get_name(): string {
return 'wp-content';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true )
);
}
public function export( array $data ) {
$customization = $data['customization']['content'] ?? null;
$exclude_post_types = [];
if ( isset( $customization['customPostTypes'] ) && ! in_array( 'post', $customization['customPostTypes'], true ) ) {
$exclude_post_types[] = 'post';
}
$post_types = ImportExportUtils::get_builtin_wp_post_types( $exclude_post_types );
$post_types = apply_filters( 'elementor/import-export-customization/wp-content/post-types/customization', $post_types, $data, $customization );
$custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];
$files = [];
$manifest_data = [];
foreach ( $post_types as $post_type ) {
$export = $this->export_wp_post_type( $post_type, $customization );
if ( ! empty( $export['file'] ) ) {
$files[] = $export['file'];
}
$manifest_data['wp-content'][ $post_type ] = $export['manifest_data'];
}
foreach ( $custom_post_types as $post_type ) {
$post_type_object = get_post_type_object( $post_type );
$manifest_data['custom-post-type-title'][ $post_type ] = [
'name' => $post_type_object->name,
'label' => $post_type_object->label,
];
// handled in the previous loop
if ( 'post' === $post_type ) {
continue;
}
$export = $this->export_wp_post_type( $post_type, $customization );
if ( ! empty( $export['file'] ) ) {
$files[] = $export['file'];
}
$manifest_data['wp-content'][ $post_type ] = $export['manifest_data'];
}
return [
'files' => $files,
'manifest' => [
$manifest_data,
],
];
}
private function export_wp_post_type( $post_type, $customization ) {
$exporter_args = [
'content' => $post_type,
'status' => 'publish',
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
'compare' => 'NOT EXISTS',
],
],
'include_post_featured_image_as_attachment' => true,
];
if ( 'pages' !== $post_type ) {
$exporter_args['limit'] = 20;
}
$exporter_args = apply_filters( 'elementor/import-export-customization/export/wp-content/query-args/customization', $exporter_args, $post_type, $customization );
$wp_exporter = new WP_Exporter( $exporter_args );
$export_result = $wp_exporter->run();
return [
'file' => [
'path' => 'wp-content/' . $post_type . '/' . $post_type . '.xml',
'data' => $export_result['xml'],
],
'manifest_data' => $export_result['posts'],
];
}
}

View File

@@ -0,0 +1,220 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;
class Elementor_Content extends Import_Runner_Base {
const IMPORT_STATUS_SUCCEEDED = 'succeed';
const IMPORT_STATUS_FAILED = 'failed';
private $show_page_on_front;
private $page_on_front_id;
private $import_session_id;
public function __construct() {
$this->init_page_on_front_data();
}
public static function get_name(): string {
return 'elementor-content';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true ) &&
! empty( $data['manifest']['content'] ) &&
! empty( $data['extracted_directory_path'] )
);
}
public function import( array $data, array $imported_data ) {
if ( ! function_exists( 'wp_set_post_terms' ) ) {
require_once ABSPATH . 'wp-admin/includes/taxonomy.php';
}
$result['content'] = [];
$this->import_session_id = $data['session_id'];
$customization = $data['customization']['content'] ?? null;
$selected_custom_post_types = $data['selected_custom_post_types'] ?? null;
$excluded_post_types = [];
if ( $selected_custom_post_types && ! in_array( 'post', $selected_custom_post_types, true ) ) {
$excluded_post_types[] = 'post';
}
$post_types = ImportExportUtils::get_elementor_post_types( $excluded_post_types );
$post_types = apply_filters( 'elementor/import-export-customization/elementor-content/post-types/customization', $post_types, $customization );
foreach ( $post_types as $post_type ) {
if ( empty( $data['manifest']['content'][ $post_type ] ) ) {
continue;
}
$posts_settings = $data['manifest']['content'][ $post_type ];
$path = $data['extracted_directory_path'] . 'content/' . $post_type . '/';
$imported_terms = ! empty( $imported_data['taxonomies'] )
? ImportExportUtils::map_old_new_term_ids( $imported_data )
: [];
$result['content'][ $post_type ] = $this->import_elementor_post_type(
$posts_settings,
$path,
$post_type,
$imported_terms,
$data['customization']['content'] ?? null,
);
}
return $result;
}
private function import_elementor_post_type( array $posts_settings, $path, $post_type, array $imported_terms, $customization ) {
$result = [
'succeed' => [],
'failed' => [],
];
foreach ( $posts_settings as $id => $post_settings ) {
try {
if ( 'page' === $post_type ) {
$data = [
'path' => $path,
'id' => $id,
'post_settings' => $post_settings,
'post_type' => $post_type,
'imported_terms' => $imported_terms,
];
$import_result = apply_filters( 'elementor/import-export-customization/import/elementor-content/customization', null, $data, [], $customization ?? [], $this );
if ( is_array( $import_result ) ) {
$result[ $import_result['status'] ][ $id ] = $import_result['result'];
continue;
}
}
$import_result = $this->read_and_import_post( $path, $id, $post_settings, $post_type, $imported_terms );
$result[ $import_result['status'] ][ $id ] = $import_result['result'];
} catch ( \Exception $error ) {
$result['failed'][ $id ] = $error->getMessage();
}
}
return $result;
}
public function read_and_import_post( $path, $id, $post_settings, $post_type, $imported_terms ) {
try {
$post_data = ImportExportUtils::read_json_file( $path . $id );
$import = $this->import_post( $post_settings, $post_data, $post_type, $imported_terms );
if ( is_wp_error( $import ) ) {
$result = [
'status' => static::IMPORT_STATUS_FAILED,
'result' => $import->get_error_message(),
];
} else {
$result = [
'status' => static::IMPORT_STATUS_SUCCEEDED,
'result' => $import,
];
}
} catch ( \Exception $error ) {
$result = [
'status' => static::IMPORT_STATUS_FAILED,
'result' => $error->getMessage(),
];
}
return $result;
}
private function import_post( array $post_settings, array $post_data, $post_type, array $imported_terms ) {
$post_attributes = [
'post_title' => $post_settings['title'],
'post_type' => $post_type,
'post_status' => 'publish',
];
if ( ! empty( $post_settings['excerpt'] ) ) {
$post_attributes['post_excerpt'] = $post_settings['excerpt'];
}
$new_document = Plugin::$instance->documents->create(
$post_settings['doc_type'],
$post_attributes
);
if ( is_wp_error( $new_document ) ) {
throw new \Exception( esc_html( $new_document->get_error_message() ) );
}
$post_data['import_settings'] = $post_settings;
$new_attachment_callback = function( $attachment_id ) {
$this->set_session_post_meta( $attachment_id, $this->import_session_id );
};
add_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$new_document->import( $post_data );
remove_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$new_post_id = $new_document->get_main_id();
if ( ! empty( $post_settings['terms'] ) ) {
$this->set_post_terms( $new_post_id, $post_settings['terms'], $imported_terms );
}
if ( ! empty( $post_settings['show_on_front'] ) ) {
$this->set_page_on_front( $new_post_id );
}
$this->set_session_post_meta( $new_post_id, $this->import_session_id );
return $new_post_id;
}
private function set_post_terms( $post_id, array $terms, array $imported_terms ) {
foreach ( $terms as $term ) {
if ( ! isset( $imported_terms[ $term['term_id'] ] ) ) {
continue;
}
wp_set_post_terms( $post_id, [ $imported_terms[ $term['term_id'] ] ], $term['taxonomy'], false );
}
}
private function init_page_on_front_data() {
$this->show_page_on_front = 'page' === get_option( 'show_on_front' );
if ( $this->show_page_on_front ) {
$this->page_on_front_id = (int) get_option( 'page_on_front' );
}
}
private function set_page_on_front( $page_id ) {
update_option( 'page_on_front', $page_id );
if ( ! $this->show_page_on_front ) {
update_option( 'show_on_front', 'page' );
}
}
public function get_import_session_metadata(): array {
return [
'page_on_front' => $this->page_on_front_id ?? 0,
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;
use Elementor\App\Modules\ImportExportCustomization\Runners\Runner_Interface;
abstract class Import_Runner_Base implements Runner_Interface {
/**
* By the passed data we should decide if we want to run the import function of the runner or not.
*
* @param array $data
*
* @return bool
*/
abstract public function should_import( array $data );
/**
* Main function of the runner import process.
*
* @param array $data Necessary data for the import process.
* @param array $imported_data Data that already imported by previously runners.
*
* @return array The result of the import process
*/
abstract public function import( array $data, array $imported_data );
public function get_import_session_metadata(): array {
return [];
}
public function set_session_post_meta( $post_id, $meta_value ) {
update_post_meta( $post_id, static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID, $meta_value );
}
public function set_session_term_meta( $term_id, $meta_value ) {
update_term_meta( $term_id, static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID, $meta_value );
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;
use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Plugins_Manager;
use Elementor\Core\Utils\Str;
class Plugins extends Import_Runner_Base {
/**
* @var Plugins_Manager
*/
private $plugins_manager;
public function __construct( $plugins_manager = null ) {
if ( $plugins_manager ) {
$this->plugins_manager = $plugins_manager;
} else {
$this->plugins_manager = new Plugins_Manager();
}
}
public static function get_name(): string {
return 'plugins';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'plugins', $data['include'], true ) &&
! empty( $data['manifest']['plugins'] ) &&
! empty( $data['selected_plugins'] )
);
}
public function import( array $data, array $imported_data ) {
$customization = $data['customization']['plugins'] ?? null;
if ( $customization ) {
$enabled_plugin_keys = Collection::make( $customization )->filter()->keys();
$plugins = Collection::make( $data['selected_plugins'] )
->filter( function( $plugin_data, $plugin_key ) use ( $enabled_plugin_keys ) {
return $enabled_plugin_keys->contains( $plugin_data['plugin'] );
} )
->values();
} else {
$plugins = $data['selected_plugins'];
}
$plugins_collection = ( new Collection( $plugins ) )
->map( function ( $item ) {
if ( ! Str::ends_with( $item['plugin'], '.php' ) ) {
$item['plugin'] .= '.php';
}
return $item;
} );
$slugs = $plugins_collection
->map( function ( $item ) {
return $item['plugin'];
} )
->all();
if ( ! function_exists( 'request_filesystem_credentials' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
$installed = $this->plugins_manager->install( $slugs );
$activated = $this->plugins_manager->activate( $installed['succeeded'] );
$ordered_activated_plugins = $plugins_collection
->filter( function ( $item ) use ( $activated ) {
return in_array( $item['plugin'], $activated['succeeded'], true );
} )
->map( function ( $item ) {
return $item['name'];
} )
->all();
$result['plugins'] = $ordered_activated_plugins;
return $result;
}
}

View File

@@ -0,0 +1,415 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;
use Elementor\Plugin;
use Elementor\Core\Settings\Page\Manager as PageManager;
use Elementor\App\Modules\ImportExportCustomization\Utils;
use Elementor\Core\Experiments\Manager as ExperimentsManager;
class Site_Settings extends Import_Runner_Base {
const ALLOWED_SETTINGS = [
'theme',
'globalColors',
'globalFonts',
'themeStyleSettings',
'generalSettings',
'experiments',
];
/**
* @var int
*/
private $previous_kit_id;
/**
* @var int
*/
private $active_kit_id;
/**
* @var int
*/
private $imported_kit_id;
/**
* @var string|null
*/
private ?string $installed_theme = null;
/**
* @var string|null
*/
private ?string $activated_theme = null;
/**
* @var array|null
*/
private ?array $previous_active_theme = null;
/**
* @var array
*/
private $previous_experiments = [];
/**
* @var array
*/
private $imported_experiments = [];
public function get_theme_upgrader(): \Theme_Upgrader {
if ( ! function_exists( 'request_filesystem_credentials' ) ) {
require_once ABSPATH . 'wp-admin/includes/file.php';
}
if ( ! class_exists( '\Theme_Upgrader' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
if ( ! class_exists( '\WP_Ajax_Upgrader_Skin' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
}
return new \Theme_Upgrader( new \WP_Ajax_Upgrader_Skin() );
}
public static function get_name(): string {
return 'site-settings';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'settings', $data['include'], true ) &&
! empty( $data['site_settings']['settings'] )
);
}
public function import( array $data, array $imported_data ) {
$customization = $data['customization']['settings'] ?? null;
if ( $customization ) {
return $this->import_with_customization( $data, $imported_data, $customization );
}
return $this->import_with_manifest( $data, $imported_data );
}
private function import_with_customization( array $data, array $imported_data, array $customization ) {
$result = apply_filters( 'elementor/import-export-customization/import/site-settings/customization', null, $data, $imported_data, $customization, $this );
if ( is_array( $result ) ) {
return $result;
}
return $this->import_with_manifest( $data, $imported_data, ! empty( $customization['theme'] ) );
}
private function import_with_manifest( array $data, array $imported_data, $include_theme = true ) {
$new_site_settings = $data['site_settings']['settings'];
$title = $data['manifest']['title'] ?? 'Imported Kit';
$manifest_settings = $data['manifest']['site-settings'] ?? [];
$active_kit = Plugin::$instance->kits_manager->get_active_kit();
$this->active_kit_id = (int) $active_kit->get_id();
$this->previous_kit_id = (int) Plugin::$instance->kits_manager->get_previous_id();
$result = [];
$old_settings = $active_kit->get_meta( PageManager::META_KEY );
if ( ! $old_settings ) {
$old_settings = [];
}
$new_site_settings = $this->filter_settings_by_manifest( $new_site_settings, $manifest_settings );
if ( ( $manifest_settings['globalColors'] ?? false ) && ! empty( $old_settings['custom_colors'] ) && ! empty( $new_site_settings['custom_colors'] ) ) {
$new_site_settings['custom_colors'] = array_merge( $old_settings['custom_colors'], $new_site_settings['custom_colors'] );
}
if ( ( $manifest_settings['globalFonts'] ?? false ) && ! empty( $old_settings['custom_typography'] ) && ! empty( $new_site_settings['custom_typography'] ) ) {
$new_site_settings['custom_typography'] = array_merge( $old_settings['custom_typography'], $new_site_settings['custom_typography'] );
}
if ( ( $manifest_settings['generalSettings'] ?? false ) && ! empty( $new_site_settings['space_between_widgets'] ) ) {
$new_site_settings['space_between_widgets'] = Utils::update_space_between_widgets_values( $new_site_settings['space_between_widgets'] );
}
$new_site_settings = array_replace_recursive( $old_settings, $new_site_settings );
$new_kit = Plugin::$instance->kits_manager->create_new_kit( $title, $new_site_settings );
$this->imported_kit_id = (int) $new_kit;
$result['site-settings']['imported_kit_id'] = $this->imported_kit_id;
foreach ( $new_site_settings as $key => $value ) {
$result['site-settings'][ $key ] = $value;
}
if ( ( $manifest_settings['theme'] ?? false ) && $include_theme ) {
$import_theme_result = $this->import_theme( $data );
if ( ! empty( $import_theme_result ) ) {
$result['theme'] = $import_theme_result;
}
}
if ( $manifest_settings['experiments'] ?? false ) {
$this->import_experiments( $data );
if ( ! empty( $this->imported_experiments ) ) {
$result['experiments'] = $this->imported_experiments;
}
}
return $result;
}
private function filter_settings_by_manifest( array $settings, array $manifest_settings ): array {
foreach ( self::ALLOWED_SETTINGS as $setting_key ) {
if ( ! ( $manifest_settings[ $setting_key ] ?? false ) ) {
$settings = $this->remove_setting_by_key( $settings, $setting_key );
}
}
return $settings;
}
private function remove_setting_by_key( array $settings, string $setting_key ): array {
switch ( $setting_key ) {
case 'globalColors':
$settings = $this->remove_global_colors( $settings );
break;
case 'globalFonts':
$settings = $this->remove_global_fonts( $settings );
break;
case 'themeStyleSettings':
$settings = $this->remove_theme_style( $settings );
break;
case 'generalSettings':
$settings = $this->remove_other_settings( $settings );
break;
}
return $settings;
}
private function remove_global_colors( array $settings ): array {
$color_keys = [ 'system_colors', 'custom_colors' ];
foreach ( $color_keys as $key ) {
if ( isset( $settings[ $key ] ) ) {
unset( $settings[ $key ] );
}
}
return $settings;
}
private function remove_global_fonts( array $settings ): array {
$typography_keys = [ 'system_typography', 'custom_typography', 'default_generic_fonts' ];
foreach ( $typography_keys as $key ) {
if ( isset( $settings[ $key ] ) ) {
unset( $settings[ $key ] );
}
}
return $settings;
}
private function remove_theme_style( array $settings ): array {
$theme_style_patterns = [
'/^body_/',
'/^h[1-6]_/',
'/^button_/',
'/^link_/',
'/^form_field_/',
];
foreach ( $settings as $key => $value ) {
foreach ( $theme_style_patterns as $pattern ) {
if ( preg_match( $pattern, $key ) ) {
unset( $settings[ $key ] );
break;
}
}
}
return $settings;
}
private function remove_other_settings( array $settings ): array {
$settings_keys = [
'template',
'container_width',
'container_padding',
'space_between_widgets',
'viewport_md',
'viewport_lg',
'page_title_selector',
'activeItemIndex',
];
foreach ( $settings_keys as $key ) {
if ( isset( $settings[ $key ] ) ) {
unset( $settings[ $key ] );
}
}
return $settings;
}
protected function install_theme( $slug, $version ) {
$download_url = "https://downloads.wordpress.org/theme/{$slug}.{$version}.zip";
return $this->get_theme_upgrader()->install( $download_url );
}
protected function activate_theme( $slug ) {
switch_theme( $slug );
}
public function import_theme( array $data ) {
if ( empty( $data['site_settings']['theme'] ) ) {
return null;
}
if ( ! function_exists( 'wp_get_theme' ) ) {
require_once ABSPATH . 'wp-admin/includes/theme.php';
}
$theme = $data['site_settings']['theme'];
$theme_slug = $theme['slug'];
$theme_name = $theme['name'];
$current_theme = wp_get_theme();
$this->previous_active_theme = [];
$this->previous_active_theme['slug'] = $current_theme->get_stylesheet();
$this->previous_active_theme['version'] = $current_theme->get( 'Version' );
if ( $current_theme->get_stylesheet() === $theme_slug ) {
$result['succeed'][ $theme_slug ] = sprintf(
/* translators: %s: Theme name. */
__( 'Theme: %s is already used', 'elementor' ),
$theme_name
);
return $result;
}
try {
if ( wp_get_theme( $theme_slug )->exists() ) {
$this->activate_theme( $theme_slug );
$this->activated_theme = $theme_slug;
$result['succeed'][ $theme_slug ] = sprintf(
/* translators: %s: Theme name. */
__( 'Theme: %s has already been installed and activated', 'elementor' ),
$theme_name
);
return $result;
}
$import = $this->install_theme( $theme_slug, $theme['version'] );
if ( is_wp_error( $import ) ) {
$result['failed'][ $theme_slug ] = sprintf(
/* translators: %s: Theme name. */
__( 'Failed to install theme: %s', 'elementor' ),
$theme_name
);
return $result;
}
$result['succeed'][ $theme_slug ] = sprintf(
/* translators: %s: Theme name. */
__( 'Theme: %s has been successfully installed', 'elementor' ),
$theme_name
);
$this->installed_theme = $theme_slug;
$this->activate_theme( $theme_slug );
} catch ( \Exception $error ) {
$result['failed'][ $theme_slug ] = $error->getMessage();
}
return $result;
}
public function import_experiments( array $data ) {
if ( empty( $data['site_settings']['experiments'] ) ) {
return null;
}
$experiments_data = $data['site_settings']['experiments'];
$experiments_manager = Plugin::$instance->experiments;
$current_features = $experiments_manager->get_features();
$this->save_previous_experiments_state( $current_features );
foreach ( $experiments_data as $feature_name => $feature_data ) {
if ( ! isset( $current_features[ $feature_name ] ) ) {
continue;
}
$current_feature = $current_features[ $feature_name ];
$current_feature_state = $current_feature['state'];
$new_state = $feature_data['state'];
if ( $current_feature_state === $new_state ) {
continue;
}
if ( ! in_array( $new_state, [ ExperimentsManager::STATE_DEFAULT, ExperimentsManager::STATE_ACTIVE, ExperimentsManager::STATE_ACTIVE ], true ) ) {
continue;
}
$option_key = $experiments_manager->get_feature_option_key( $feature_name );
if ( 'default' === $new_state ) {
delete_option( $option_key );
} else {
update_option( $option_key, $new_state );
}
$this->imported_experiments[ $feature_name ] = $feature_data;
}
}
private function save_previous_experiments_state( array $current_features ) {
$experiments_manager = Plugin::$instance->experiments;
foreach ( $current_features as $feature_name => $feature ) {
if ( ! $feature['mutable'] ) {
continue;
}
$option_key = $experiments_manager->get_feature_option_key( $feature_name );
$saved_state = get_option( $option_key );
$this->previous_experiments[ $feature_name ] = [
'name' => $feature_name,
'title' => $feature['title'],
'state' => empty( $saved_state ) ? 'default' : $saved_state,
'default' => $feature['default'],
'release_status' => $feature['release_status'],
];
}
}
public function get_import_session_metadata(): array {
return [
'previous_kit_id' => $this->previous_kit_id,
'active_kit_id' => $this->active_kit_id,
'imported_kit_id' => $this->imported_kit_id,
'installed_theme' => $this->installed_theme,
'activated_theme' => $this->activated_theme,
'previous_active_theme' => $this->previous_active_theme,
'previous_experiments' => $this->previous_experiments,
'imported_experiments' => $this->imported_experiments,
];
}
}

View File

@@ -0,0 +1,169 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
class Taxonomies extends Import_Runner_Base {
private $import_session_id;
public static function get_name(): string {
return 'taxonomies';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true ) &&
! empty( $data['extracted_directory_path'] ) &&
! empty( $data['manifest']['taxonomies'] )
);
}
public function import( array $data, array $imported_data ) {
if ( ! function_exists( 'wp_insert_term' ) ) {
require_once ABSPATH . 'wp-admin/includes/taxonomy.php';
}
$customization = $data['customization']['content'] ?? null;
if ( $customization ) {
return $this->import_with_customization( $data, $imported_data, $customization );
}
return $this->import_all( $data, $imported_data );
}
public function import_all( array $data, array $imported_data ) {
$path = $data['extracted_directory_path'] . 'taxonomies/';
$this->import_session_id = $data['session_id'];
$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
$selected_custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];
$post_types = array_merge( $wp_builtin_post_types, $selected_custom_post_types );
$result = [];
foreach ( $post_types as $post_type ) {
if ( empty( $data['manifest']['taxonomies'][ $post_type ] ) ) {
continue;
}
$result['taxonomies'][ $post_type ] = $this->import_taxonomies( $data['manifest']['taxonomies'][ $post_type ], $path );
}
return $result;
}
public function import_with_customization( array $data, array $imported_data, array $customization ) {
$result = apply_filters( 'elementor/import-export-customization/import/taxonomies/customization', null, $data, $imported_data, $customization, $this );
if ( is_array( $result ) ) {
return $result;
}
return $this->import_all( $data, $imported_data );
}
public function import_taxonomies( array $taxonomies, $path ) {
$result = [];
$imported_taxonomies = [];
foreach ( $taxonomies as $taxonomy_object ) {
$taxonomy = is_array( $taxonomy_object ) ? $taxonomy_object['name'] : $taxonomy_object;
if ( ! taxonomy_exists( $taxonomy ) ) {
continue;
}
if ( ! empty( $imported_taxonomies[ $taxonomy ] ) ) {
$result[ $taxonomy ] = $imported_taxonomies[ $taxonomy ];
continue;
}
$taxonomy_data = ImportExportUtils::read_json_file( $path . $taxonomy );
if ( empty( $taxonomy_data ) ) {
continue;
}
$import = $this->import_taxonomy( $taxonomy_data );
$result[ $taxonomy ] = $import;
$imported_taxonomies[ $taxonomy ] = $import;
}
return $result;
}
private function import_taxonomy( array $taxonomy_data ) {
$terms = [];
foreach ( $taxonomy_data as $term ) {
$old_slug = $term['slug'];
$existing_term = term_exists( $term['slug'], $term['taxonomy'] );
if ( $existing_term ) {
if ( 'nav_menu' === $term['taxonomy'] ) {
$term = $this->handle_duplicated_nav_menu_term( $term );
} else {
$terms[] = [
'old_id' => (int) $term['term_id'],
'new_id' => (int) $existing_term['term_id'],
'old_slug' => $old_slug,
'new_slug' => $term['slug'],
];
continue;
}
}
$parent = $this->get_term_parent( $term, $terms );
$args = [
'slug' => $term['slug'],
'description' => wp_slash( $term['description'] ),
'parent' => (int) $parent,
];
$new_term = wp_insert_term( wp_slash( $term['name'] ), $term['taxonomy'], $args );
if ( ! is_wp_error( $new_term ) ) {
$this->set_session_term_meta( (int) $new_term['term_id'], $this->import_session_id );
$terms[] = [
'old_id' => $term['term_id'],
'new_id' => (int) $new_term['term_id'],
'old_slug' => $old_slug,
'new_slug' => $term['slug'],
];
}
}
return $terms;
}
private function handle_duplicated_nav_menu_term( $term ) {
do {
$term['slug'] = $term['slug'] . '-duplicate';
$term['name'] = $term['name'] . ' duplicate';
} while ( term_exists( $term['slug'], 'nav_menu' ) );
return $term;
}
private function get_term_parent( $term, array $imported_terms ) {
$parent = $term['parent'];
if ( 0 !== $parent && ! empty( $imported_terms ) ) {
foreach ( $imported_terms as $imported_term ) {
if ( $parent === $imported_term['old_id'] ) {
$parent_term = term_exists( $imported_term['new_id'], $term['taxonomy'] );
break;
}
}
if ( isset( $parent_term['term_id'] ) ) {
return $parent_term['term_id'];
}
}
return 0;
}
}

View File

@@ -0,0 +1,145 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
use Elementor\Modules\Library\Documents\Library_Document;
class Templates extends Import_Runner_Base {
private $import_session_id;
private $import_session_metadata = [];
public static function get_name(): string {
return 'templates';
}
public function should_import( array $data ) {
return (
Utils::has_pro() &&
isset( $data['include'] ) &&
in_array( 'templates', $data['include'], true ) &&
! empty( $data['extracted_directory_path'] ) &&
! empty( $data['manifest']['templates'] )
);
}
public function import( array $data, array $imported_data ) {
$customization = $data['customization']['templates'] ?? null;
if ( $customization ) {
return $this->import_with_customization( $data, $imported_data, $customization );
}
return $this->import_all( $data, $imported_data );
}
private function import_with_customization( array $data, array $imported_data, array $customization ) {
$result = apply_filters( 'elementor/import-export-customization/import/templates/customization', null, $data, $imported_data, $customization, $this );
if ( is_array( $result ) ) {
return $result;
}
return $this->import_all( $data, $imported_data );
}
private function import_all( array $data, array $imported_data ) {
$template_types = array_keys( Plugin::$instance->documents->get_document_types( [
'is_editable' => true,
'show_in_library' => true,
'export_group' => Library_Document::EXPORT_GROUP,
] ) );
$result = $this->process_templates_import( $data, $template_types );
/**
* Filter the templates import result to allow 3rd parties to add their own imported templates.
*
* @param array $result The import result structure with 'templates' key containing succeed/failed/succeed_summary.
* @param array $data The full import data.
* @param array|null $customization The customization settings for templates.
* @param object $runner The runner instance.
*/
$customization = $data['customization']['templates'] ?? null;
$result = apply_filters( 'elementor/import-export-customization/import/templates_result', $result, $data, $customization, $this );
return $result;
}
public function process_templates_import( array $data, array $template_types ) {
$this->import_session_id = $data['session_id'];
$path = $data['extracted_directory_path'] . 'templates/';
$templates = $data['manifest']['templates'];
$result['templates'] = [
'succeed' => [],
'failed' => [],
'succeed_summary' => [],
];
foreach ( $templates as $id => $template_settings ) {
if ( ! empty( $template_types ) && ! in_array( $template_settings['doc_type'], $template_types, true ) ) {
continue;
}
try {
$template_data = ImportExportUtils::read_json_file( $path . $id );
$import = $this->import_template( $id, $template_settings, $template_data );
$result['templates']['succeed'][ $id ] = $import;
$result['templates']['succeed_summary'][ $template_settings['doc_type'] ] = ( $result['templates']['succeed_summary'][ $template_settings['doc_type'] ] ?? 0 ) + 1;
} catch ( \Exception $error ) {
$result['templates']['failed'][ $id ] = $error->getMessage();
}
}
return $result;
}
public function import_template( $id, array $template_settings, array $template_data ) {
$doc_type = $template_settings['doc_type'];
$new_document = Plugin::$instance->documents->create(
$doc_type,
[
'post_title' => $template_settings['title'],
'post_type' => Source_Local::CPT,
'post_status' => 'publish',
]
);
if ( is_wp_error( $new_document ) ) {
throw new \Exception( esc_html( $new_document->get_error_message() ) );
}
$template_data['import_settings'] = $template_settings;
$template_data['id'] = $id;
$new_attachment_callback = function( $attachment_id ) {
$this->set_session_post_meta( $attachment_id, $this->import_session_id );
};
add_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$new_document->import( $template_data );
remove_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$document_id = $new_document->get_main_id();
$this->set_session_post_meta( $document_id, $this->import_session_id );
return $document_id;
}
public function get_import_session_metadata(): array {
return $this->import_session_metadata;
}
public function add_import_session_metadata( $key, $metadata ) {
$this->import_session_metadata[ $key ] = $metadata;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Import;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Core\Utils\ImportExport\WP_Import;
class Wp_Content extends Import_Runner_Base {
private $import_session_id;
/**
* @var array
*/
private $selected_custom_post_types = [];
public static function get_name(): string {
return 'wp-content';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true ) &&
! empty( $data['extracted_directory_path'] ) &&
! empty( $data['manifest']['wp-content'] )
);
}
public function import( array $data, array $imported_data ) {
$this->import_session_id = $data['session_id'];
$path = $data['extracted_directory_path'] . 'wp-content/';
$post_types = $this->filter_post_types( $data );
$taxonomies = $imported_data['taxonomies'] ?? [];
$imported_terms = ImportExportUtils::map_old_new_term_ids( $imported_data );
$result['wp-content'] = [];
foreach ( $post_types as $post_type ) {
$import = $this->import_wp_post_type(
$path,
$post_type,
$imported_data,
$taxonomies,
$imported_terms,
$data['customization']['content'] ?? null
);
if ( empty( $import ) ) {
continue;
}
$result['wp-content'][ $post_type ] = $import;
$imported_data = array_merge( $imported_data, $result );
}
return $result;
}
private function import_wp_post_type( $path, $post_type, array $imported_data, array $taxonomies, array $imported_terms, $customization ) {
$args = [
'fetch_attachments' => true,
'posts' => ImportExportUtils::map_old_new_post_ids( $imported_data ),
'terms' => $imported_terms,
'taxonomies' => ! empty( $taxonomies[ $post_type ] ) ? $taxonomies[ $post_type ] : [],
'posts_meta' => [
static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID => $this->import_session_id,
],
'terms_meta' => [
static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID => $this->import_session_id,
],
'include' => 'page' === $post_type ? $customization['pages'] ?? null : null,
];
$args = apply_filters( 'elementor/import-export-customization/import/wp-content/query-args/customization', $args, $post_type, $customization );
$file = $path . $post_type . '/' . $post_type . '.xml';
if ( ! file_exists( $file ) ) {
return [];
}
$wp_importer = new WP_Import( $file, $args );
$result = $wp_importer->run();
return $result['summary']['posts'];
}
private function filter_post_types( $data ) {
$selected_custom_post_types = $data['selected_custom_post_types'];
$customization = $data['customization']['content'] ?? null;
$exclude = [];
if ( ! empty( $selected_custom_post_types ) && in_array( 'post', $selected_custom_post_types, true ) ) {
$exclude[] = 'post';
}
$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types( $exclude );
foreach ( $selected_custom_post_types as $custom_post_type ) {
if ( post_type_exists( $custom_post_type ) ) {
$this->selected_custom_post_types[] = $custom_post_type;
}
}
$post_types = array_merge( $wp_builtin_post_types, $this->selected_custom_post_types );
$post_types = apply_filters( 'elementor/import-export-customization/wp-content/post-types/customization', $post_types, $data, $customization );
$post_types = $this->force_element_to_be_last_by_value( $post_types, 'nav_menu_item' );
return $post_types;
}
public function get_import_session_metadata(): array {
return [
'custom_post_types' => $this->selected_custom_post_types,
];
}
/**
* @param $array array The array we want to relocate his element.
* @param $element mixed The value of the element in the array we want to shift to end of the array.
* @return mixed
*/
private function force_element_to_be_last_by_value( array $array, $element ) {
$index = array_search( $element, $array, true );
if ( false !== $index ) {
unset( $array[ $index ] );
$array[] = $element;
}
return $array;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
use Elementor\Plugin;
class Elementor_Content extends Revert_Runner_Base {
private $show_page_on_front;
private $page_on_front_id;
public function __construct() {
$this->init_page_on_front_data();
}
public static function get_name(): string {
return 'elementor-content';
}
public function should_revert( array $data ): bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
$elementor_post_types = ImportExportUtils::get_elementor_post_types();
$query_args = [
'post_type' => $elementor_post_types,
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
'compare' => 'EXISTS',
],
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
];
$query = new \WP_Query( $query_args );
foreach ( $query->posts as $post ) {
$post_type_document = Plugin::$instance->documents->get( $post->ID );
$post_type_document->delete();
// Deleting the post will reset the show_on_front option. We need to set it to false,
// so we can set it back to what it was.
if ( $post->ID === $this->page_on_front_id ) {
$this->show_page_on_front = false;
}
}
$this->restore_page_on_front( $data );
}
private function init_page_on_front_data() {
$this->show_page_on_front = 'page' === get_option( 'show_on_front' );
if ( $this->show_page_on_front ) {
$this->page_on_front_id = (int) get_option( 'page_on_front' );
}
}
private function restore_page_on_front( $data ) {
if ( empty( $data['runners'][ static::get_name() ]['page_on_front'] ) ) {
return;
}
$page_on_front = $data['runners'][ static::get_name() ]['page_on_front'];
$document = Plugin::$instance->documents->get( $page_on_front );
if ( ! $document ) {
return;
}
$this->set_page_on_front( $document->get_main_id() );
}
private function set_page_on_front( $page_id ) {
update_option( 'page_on_front', $page_id );
if ( ! $this->show_page_on_front ) {
update_option( 'show_on_front', 'page' );
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;
class Plugins extends Revert_Runner_Base {
public static function get_name(): string {
return 'plugins';
}
public function should_revert( array $data ): bool {
return false;
}
public function revert( array $data ) {}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;
use Elementor\App\Modules\ImportExportCustomization\Runners\Runner_Interface;
abstract class Revert_Runner_Base implements Runner_Interface {
/**
* By the passed data we should decide if we want to run the revert function of the runner or not.
*
* @param array $data
*
* @return bool
*/
abstract public function should_revert( array $data ): bool;
/**
* Main function of the runner revert process.
*
* @param array $data Necessary data for the revert process.
*/
abstract public function revert( array $data );
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;
use Elementor\Plugin;
use Elementor\Core\Experiments\Manager as ExperimentsManager;
class Site_Settings extends Revert_Runner_Base {
public static function get_name(): string {
return 'site-settings';
}
public function should_revert( array $data ): bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
Plugin::$instance->kits_manager->revert(
$data['runners'][ static::get_name() ]['imported_kit_id'],
$data['runners'][ static::get_name() ]['active_kit_id'],
$data['runners'][ static::get_name() ]['previous_kit_id']
);
$this->revert_theme( $data );
$this->revert_experiments( $data );
}
public function get_theme_upgrader(): \Theme_Upgrader {
if ( ! class_exists( '\Theme_Upgrader' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
if ( ! class_exists( '\WP_Ajax_Upgrader_Skin' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
}
return new \Theme_Upgrader( new \WP_Ajax_Upgrader_Skin() );
}
protected function revert_theme( $data ) {
$installed_theme = $data['runners'][ static::get_name() ]['installed_theme'];
$activated_theme = $data['runners'][ static::get_name() ]['activated_theme'];
$previous_active_theme = $data['runners'][ static::get_name() ]['previous_active_theme'];
if ( empty( $installed_theme ) && empty( $activated_theme ) ) {
// no need to remove a theme as it was used before import
return;
}
if ( ! empty( $activated_theme ) ) {
$previous_theme = wp_get_theme( $previous_active_theme['slug'] );
// no need to remove imported theme as it existed before import
$this->activate_previous_theme( $previous_active_theme );
return;
}
if ( ! empty( $installed_theme ) ) {
$this->activate_previous_theme( $previous_active_theme );
$this->delete_theme( $installed_theme );
}
}
protected function should_delete_theme( $theme_slug ): bool {
$current_theme = wp_get_theme();
return $theme_slug !== $current_theme->get_stylesheet() && wp_get_theme( $theme_slug )->exists();
}
protected function delete_theme( $theme_slug ): bool {
return delete_theme( $theme_slug );
}
protected function activate_previous_theme( $previous_active_theme ) {
if ( ! $previous_active_theme ) {
return;
}
$theme = wp_get_theme( $previous_active_theme['slug'] );
if ( $theme->exists() ) {
switch_theme( $theme->get_stylesheet() );
return;
}
$download_url = "https://downloads.wordpress.org/theme/{$previous_active_theme['slug']}.{$previous_active_theme['version']}.zip";
$install = $this->get_theme_upgrader()->install( $download_url );
if ( ! $install || is_wp_error( $install ) ) {
return;
}
switch_theme( $previous_active_theme['slug'] );
}
protected function revert_experiments( array $data ) {
$runner_data = $data['runners'][ static::get_name() ];
$previous_experiments = $runner_data['previous_experiments'] ?? [];
if ( empty( $previous_experiments ) ) {
return;
}
$experiments_manager = Plugin::$instance->experiments;
$current_features = $experiments_manager->get_features();
foreach ( $previous_experiments as $feature_name => $feature_data ) {
if ( ! isset( $current_features[ $feature_name ] ) ) {
continue;
}
if ( ! array_key_exists( $feature_name, $previous_experiments ) ) {
continue;
}
$option_key = $experiments_manager->get_feature_option_key( $feature_name );
$previous_state = $feature_data['state'];
if ( ExperimentsManager::STATE_DEFAULT === $previous_state ) {
delete_option( $option_key );
} else {
update_option( $option_key, $previous_state );
}
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;
class Taxonomies extends Revert_Runner_Base {
public static function get_name(): string {
return 'taxonomies';
}
public function should_revert( array $data ): bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
$taxonomies = get_taxonomies();
$terms = get_terms( [
'taxonomy' => $taxonomies,
'hide_empty' => false,
'get' => 'all',
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
] );
foreach ( $terms as $term ) {
wp_delete_term( $term->term_id, $term->taxonomy );
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Core\Base\Document;
class Templates extends Revert_Runner_Base {
public static function get_name(): string {
return 'templates';
}
public function should_revert( array $data ): bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
$template_types = array_values( Source_Local::get_template_types() );
$query_args = [
'post_type' => Source_Local::CPT,
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => Document::TYPE_META_KEY,
'value' => $template_types,
],
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
];
$templates_query = new \WP_Query( $query_args );
foreach ( $templates_query->posts as $template_post ) {
$template_document = Plugin::$instance->documents->get( $template_post->ID );
$template_document->delete();
}
do_action( 'elementor/import-export-customization/revert/templates', $data );
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners\Revert;
use Elementor\App\Modules\ImportExportCustomization\Utils as ImportExportUtils;
class Wp_Content extends Revert_Runner_Base {
public static function get_name(): string {
return 'wp-content';
}
public function should_revert( array $data ): bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
$builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
$custom_post_types = $data['runners']['wp-content']['custom_post_types'] ?? [];
$post_types = array_merge( $builtin_post_types, $custom_post_types );
$query_args = [
'post_type' => $post_types,
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
'compare' => 'NOT EXISTS',
],
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
];
$query = new \WP_Query( $query_args );
foreach ( $query->posts as $post ) {
wp_delete_post( $post->ID, true );
}
/**
* Revert the nav menu terms.
* BC: The nav menu in new kits will be imported as part of the taxonomies, but old kits
* importing the nav menu terms as part from the wp-content import.
*/
$this->revert_nav_menus( $data );
}
private function revert_nav_menus( array $data ) {
$terms = get_terms( [
'taxonomy' => 'nav_menu',
'hide_empty' => false,
'get' => 'all',
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
] );
foreach ( $terms as $term ) {
wp_delete_term( $term->term_id, $term->taxonomy );
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization\Runners;
use Elementor\App\Modules\ImportExportCustomization\Module;
interface Runner_Interface {
const META_KEY_ELEMENTOR_IMPORT_SESSION_ID = Module::META_KEY_ELEMENTOR_IMPORT_SESSION_ID;
const META_KEY_ELEMENTOR_EDIT_MODE = Module::META_KEY_ELEMENTOR_EDIT_MODE;
/**
* Get the name of the runners, used to identify the runner.
* The name should be unique, unless you want to run over existing runner.
*
* @return string
*/
public static function get_name(): string;
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization;
use Elementor\App\Modules\ImportExportCustomization\Processes\Revert;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Usage {
/**
* Register hooks.
*
* @return void
*/
public function register() {
add_filter( 'elementor/tracker/send_tracking_data_params', function ( array $params ) {
$params['usages']['import_export']['revert'] = $this->get_revert_usage_data();
return $params;
} );
}
/**
* Get the Revert usage data.
*
* @return array
*/
private function get_revert_usage_data() {
$revert_sessions = ( new Revert() )->get_revert_sessions();
$data = [];
foreach ( $revert_sessions as $revert_session ) {
$data[] = [
'kit_name' => $revert_session['kit_name'],
'source' => $revert_session['source'],
'revert_timestamp' => (int) $revert_session['revert_timestamp'],
'total_time' => ( (int) $revert_session['revert_timestamp'] - (int) $revert_session['import_timestamp'] ),
];
}
return $data;
}
}

View File

@@ -0,0 +1,154 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization;
use Elementor\Core\Utils\Str;
use Elementor\Modules\LandingPages\Module as Landing_Pages_Module;
use Elementor\Modules\FloatingButtons\Module as Floating_Buttons_Module;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils as ElementorUtils;
class Utils {
public static function read_json_file( $path ) {
if ( ! Str::ends_with( $path, '.json' ) ) {
$path .= '.json';
}
$file_content = ElementorUtils::file_get_contents( $path, true );
return $file_content ? json_decode( $file_content, true ) : [];
}
public static function map_old_new_post_ids( array $imported_data ) {
$result = [];
$result += $imported_data['templates']['succeed'] ?? [];
if ( isset( $imported_data['content'] ) ) {
foreach ( $imported_data['content'] as $post_type ) {
$result += $post_type['succeed'] ?? [];
}
}
if ( isset( $imported_data['wp-content'] ) ) {
foreach ( $imported_data['wp-content'] as $post_type ) {
$result += $post_type['succeed'] ?? [];
}
}
return $result;
}
public static function map_old_new_term_ids( array $imported_data ) {
$result = [];
if ( ! isset( $imported_data['taxonomies'] ) ) {
return $result;
}
foreach ( $imported_data['taxonomies'] as $post_type_taxonomies ) {
foreach ( $post_type_taxonomies as $taxonomy ) {
foreach ( $taxonomy as $term ) {
$result[ $term['old_id'] ] = $term['new_id'];
}
}
}
return $result;
}
public static function get_elementor_post_types( $exclude = [] ) {
$elementor_post_types = get_post_types_by_support( 'elementor' );
return array_filter( $elementor_post_types, function ( $value ) {
// Templates are handled in a separate process.
if ( 'elementor_library' === $value ) {
return false;
}
if ( ! empty( $exclude ) && in_array( $value, $exclude, true ) ) {
return false;
}
return 'elementor_library' !== $value;
} );
}
public static function get_builtin_wp_post_types( $exclude = [] ) {
$builtin_wp_post_types = [ 'post', 'page', 'nav_menu_item' ];
if ( ! empty( $exclude ) ) {
return array_diff( $builtin_wp_post_types, $exclude );
}
return $builtin_wp_post_types;
}
public static function get_registered_cpt_names() {
$post_types = get_post_types( [
'public' => true,
'can_export' => true,
'_builtin' => false,
] );
unset(
$post_types[ Landing_Pages_Module::CPT ],
$post_types[ Source_Local::CPT ],
$post_types[ Floating_Buttons_Module::CPT_FLOATING_BUTTONS ]
);
return array_keys( $post_types );
}
/**
* Transform a string name to title format.
*
* @param $name
*
* @return string
*/
public static function transform_name_to_title( $name ): string {
if ( empty( $name ) ) {
return '';
}
$title = str_replace( [ '-', '_' ], ' ', $name );
return ucwords( $title );
}
public static function get_import_sessions( $should_run_cleanup = false ) {
$import_sessions = get_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, [] );
if ( $should_run_cleanup ) {
foreach ( $import_sessions as $session_id => $import_session ) {
if ( ! isset( $import_session['runners'] ) && isset( $import_session['instance_data'] ) ) {
$import_sessions[ $session_id ]['runners'] = $import_session['instance_data']['runners_import_metadata'] ?? [];
unset( $import_sessions[ $session_id ]['instance_data'] );
}
}
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions );
}
return $import_sessions;
}
public static function update_space_between_widgets_values( $space_between_widgets ) {
$setting_exist = isset( $space_between_widgets['size'] );
$already_processed = isset( $space_between_widgets['column'] );
if ( ! $setting_exist || $already_processed ) {
return $space_between_widgets;
}
$size = strval( $space_between_widgets['size'] );
$space_between_widgets['column'] = $size;
$space_between_widgets['row'] = $size;
$space_between_widgets['isLinked'] = true;
return $space_between_widgets;
}
}

View File

@@ -0,0 +1,280 @@
<?php
namespace Elementor\App\Modules\ImportExportCustomization;
use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Plugins_Manager;
use Elementor\Plugin;
use Elementor\App\Modules\KitLibrary\Connect\Kit_Library;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Wp_Cli extends \WP_CLI_Command {
const AVAILABLE_SETTINGS = [ 'include', 'overrideConditions', 'selectedCustomPostTypes', 'plugins' ];
/**
* Export a Kit
*
* [--include]
* Which type of content to include. Possible values are 'content', 'templates', 'site-settings'.
* if this parameter won't be specified, All data types will be included.
*
* ## EXAMPLES
*
* 1. wp elementor kit export path/to/export-file-name.zip
* - This will export all site data to the specified file name.
*
* 2. wp elementor kit export path/to/export-file-name.zip --include=kit-settings,content
* - This will export only site settings and content.
*
* @param array $args
* @param array $assoc_args
*/
public function export( $args, $assoc_args ) {
if ( empty( $args[0] ) ) {
\WP_CLI::error( 'Please specify a file name' );
}
\WP_CLI::line( 'Kit export started.' );
$export_settings = [];
foreach ( $assoc_args as $key => $value ) {
if ( ! in_array( $key, static::AVAILABLE_SETTINGS, true ) ) {
continue;
}
$export_settings[ $key ] = explode( ',', $value );
}
try {
/**
* Running the export process through the import-export module so the export property in the module will be available to use.
*
* @type Module $import_export_module
*/
$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
$result = $import_export_module->export_kit( $export_settings );
rename( $result['file_name'], $args[0] );
} catch ( \Error $error ) {
\WP_CLI::error( $error->getMessage() );
}
\WP_CLI::success( 'Kit exported successfully.' );
}
/**
* Import a Kit
*
* [--include]
* Which type of content to include. Possible values are 'content', 'templates', 'site-settings'.
* if this parameter won't be specified, All data types will be included.
*
* [--overrideConditions]
* Templates ids to override conditions for.
*
* [--sourceType]
* Which source type is used in the current session. Available values are 'local', 'remote', 'library'.
* The default value is 'local'
*
* ## EXAMPLES
*
* 1. wp elementor kit import path/to/elementor-kit.zip
* - This will import the whole kit file content.
*
* 2. wp elementor kit import path/to/elementor-kit.zip --include=site-settings,content
* - This will import only site settings and content.
*
* 3. wp elementor kit import path/to/elementor-kit.zip --overrideConditions=3478,4520
* - This will import all content and will override conditions for the given template ids.
*
* 4. wp elementor kit import path/to/elementor-kit.zip --unfilteredFilesUpload=enable
* - This will allow the import process to import unfiltered files.
*
* @param array $args
* @param array $assoc_args
*/
public function import( array $args, array $assoc_args ) {
if ( ! current_user_can( 'manage_options' ) ) {
\WP_CLI::error( 'You must run this command as an admin user' );
}
if ( empty( $args[0] ) ) {
\WP_CLI::error( 'Please specify a file to import' );
}
\WP_CLI::line( 'Kit import started' );
$assoc_args = wp_parse_args( $assoc_args, [
'sourceType' => 'local',
] );
$url = null;
$file_path = $args[0];
$import_settings = [];
$import_settings['referrer'] = Module::REFERRER_LOCAL;
switch ( $assoc_args['sourceType'] ) {
case 'library':
$url = $this->get_url_from_library( $file_path );
$zip_path = $this->create_temp_file_from_url( $url );
$import_settings['referrer'] = Module::REFERRER_KIT_LIBRARY;
break;
case 'remote':
$zip_path = $this->create_temp_file_from_url( $file_path );
break;
case 'local':
$zip_path = $file_path;
break;
default:
\WP_CLI::error( 'Unknown source type.' );
break;
}
if ( 'enable' === $assoc_args['unfilteredFilesUpload'] ) {
Plugin::$instance->uploads_manager->enable_unfiltered_files_upload();
}
foreach ( $assoc_args as $key => $value ) {
if ( ! in_array( $key, static::AVAILABLE_SETTINGS, true ) ) {
continue;
}
$import_settings[ $key ] = explode( ',', $value );
}
try {
\WP_CLI::line( 'Importing data...' );
/**
* Running the import process through the import-export module so the import property in the module will be available to use.
*
* @type Module $import_export_module
*/
$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
if ( ! $import_export_module ) {
\WP_CLI::error( 'Import Export module is not available.' );
}
$import = $import_export_module->import_kit( $zip_path, $import_settings );
$manifest_data = $import_export_module->import->get_manifest();
/**
* Import Export Manifest Data
*
* Allows 3rd parties to read and edit the kit's manifest before it is used.
*
* @since 3.7.0
*
* @param array $manifest_data The Kit's Manifest data
*/
$manifest_data = apply_filters( 'elementor/import-export/wp-cli/manifest_data', $manifest_data );
\WP_CLI::line( 'Removing temp files...' );
// The file was created from remote or library request, it also should be removed.
if ( $url ) {
Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $zip_path ) );
}
\WP_CLI::success( 'Kit imported successfully' );
} catch ( \Error $error ) {
Plugin::$instance->logger->get_logger()->error( $error->getMessage(), [
'meta' => [
'trace' => $error->getTraceAsString(),
],
] );
if ( $url ) {
Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $zip_path ) );
}
\WP_CLI::error( $error->getMessage() );
}
}
/**
* Revert last imported kit.
*/
public function revert() {
\WP_CLI::line( 'Kit revert started.' );
try {
/**
* Running the revert process through the import-export module so the revert property in the module will be available to use.
*
* @type Module $import_export_module
*/
$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
$import_export_module->revert_last_imported_kit();
} catch ( \Error $error ) {
\WP_CLI::error( $error->getMessage() );
}
\WP_CLI::success( 'Kit reverted successfully.' );
}
/**
* Helper to get kit url by the kit id
* TODO: Maybe extract it.
*
* @param $kit_id
*
* @return string
*/
private function get_url_from_library( $kit_id ) {
/** @var Kit_Library $app */
$app = Plugin::$instance->common->get_component( 'connect' )->get_app( 'kit-library' );
if ( ! $app ) {
\WP_CLI::error( 'Kit library app not found' );
}
$response = $app->download_link( $kit_id );
if ( is_wp_error( $response ) ) {
\WP_CLI::error( "Library Response: {$response->get_error_message()}" );
}
return $response->download_link;
}
/**
* Helper to get kit zip file path by the kit url
* TODO: Maybe extract it.
*
* @param $url
*
* @return string
*/
private function create_temp_file_from_url( $url ) {
\WP_CLI::line( 'Extracting zip archive...' );
$response = wp_remote_get( $url );
if ( is_wp_error( $response ) ) {
\WP_CLI::error( "Download file url: {$response->get_error_message()}" );
}
if ( 200 !== $response['response']['code'] ) {
\WP_CLI::error( "Download file url: {$response['response']['message']}" );
}
// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
$file = Plugin::$instance->uploads_manager->create_temp_file( $response['body'], 'kit.zip' );
// After the upload complete, set the elementor upload state back to false.
Plugin::$instance->uploads_manager->set_elementor_upload_state( false );
return $file;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Elementor\App\Modules\ImportExport\Compatibility;
use Elementor\App\Modules\ImportExport\Import;
use Elementor\Core\Base\Base_Object;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Adapter {
/**
* @param array $manifest_data
* @param array $meta
* @return false
*/
public static function is_compatibility_needed( array $manifest_data, array $meta ) {
return false;
}
public function adapt_manifest( array $manifest_data ) {
return $manifest_data;
}
public function adapt_site_settings( array $site_settings, array $manifest_data, $path ) {
return $site_settings;
}
public function adapt_template( array $template_data, array $template_settings ) {
return $template_data;
}
}

View File

@@ -0,0 +1,82 @@
<?php
namespace Elementor\App\Modules\ImportExport\Compatibility;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Envato extends Base_Adapter {
public static function is_compatibility_needed( array $manifest_data, array $meta ) {
return ! empty( $manifest_data['manifest_version'] );
}
public function adapt_manifest( array $manifest_data ) {
$templates = $manifest_data['templates'];
$manifest_data['templates'] = [];
foreach ( $templates as $template ) {
// Envato store their global kit styles as a 'global.json' template file.
// We need to be able to know the path to this specific 'global.json' since it functions as the site-settings.json
$is_global = ! empty( $template['metadata']['template_type'] ) && 'global-styles' === $template['metadata']['template_type'];
if ( $is_global ) {
// Adding the path of the 'global.json' template to the manifest which will be used in the future.
$manifest_data['path-to-envto-site-settings'] = $template['source'];
// Getting the site-settings because Envato stores them in one of the posts.
$kit = Plugin::$instance->kits_manager->get_active_kit();
$kit_tabs = $kit->get_tabs();
unset( $kit_tabs['settings-site-identity'] );
$manifest_data['site-settings'] = array_keys( $kit_tabs );
continue;
}
// Evanto uses "type" instead of "doc_type"
$template['doc_type'] = $template['type'];
// Evanto uses for "name" instead of "title"
$template['title'] = $template['name'];
// Envato specifying an exact path to the template rather than using its "ID" as an index.
// This extracts the "file name" part out of our exact source list and we treat that as an ID.
$file_name_without_extension = str_replace( '.json', '', basename( $template['source'] ) );
// Append the template to the global list:
$manifest_data['templates'][ $file_name_without_extension ] = $template;
}
$manifest_data['name'] = $manifest_data['title'];
return $manifest_data;
}
public function adapt_site_settings( array $site_settings, array $manifest_data, $path ) {
if ( empty( $manifest_data['path-to-envto-site-settings'] ) ) {
return $site_settings;
}
$global_file_path = $path . $manifest_data['path-to-envto-site-settings'];
$global_file_data = ImportExportUtils::read_json_file( $global_file_path );
return [
'settings' => $global_file_data['page_settings'],
];
}
public function adapt_template( array $template_data, array $template_settings ) {
if ( ! empty( $template_data['metadata']['elementor_pro_conditions'] ) ) {
foreach ( $template_data['metadata']['elementor_pro_conditions'] as $condition ) {
list ( $type, $name, $sub_name, $sub_id ) = array_pad( explode( '/', $condition ), 4, '' );
$template_data['import_settings']['conditions'][] = compact( 'type', 'name', 'sub_name', 'sub_id' );
}
}
return $template_data;
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Elementor\App\Modules\ImportExport\Compatibility;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Kit_Library extends Base_Adapter {
public static function is_compatibility_needed( array $manifest_data, array $meta ) {
return ! empty( $meta['referrer'] ) && 'kit-library' === $meta['referrer'];
}
public function adapt_manifest( array $manifest_data ) {
if ( ! empty( $manifest_data['content']['page'] ) ) {
foreach ( $manifest_data['content']['page'] as & $page ) {
$page['thumbnail'] = false;
}
}
if ( ! empty( $manifest_data['templates'] ) ) {
foreach ( $manifest_data['templates'] as & $template ) {
$template['thumbnail'] = false;
}
}
return $manifest_data;
}
}

View File

@@ -0,0 +1,993 @@
<?php
namespace Elementor\App\Modules\ImportExport;
use Elementor\App\Modules\ImportExport\Processes\Export;
use Elementor\App\Modules\ImportExport\Processes\Import;
use Elementor\App\Modules\ImportExport\Processes\Revert;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Files\Uploads_Manager;
use Elementor\Modules\System_Info\Reporters\Server;
use Elementor\Plugin;
use Elementor\Tools;
use Elementor\Utils as ElementorUtils;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Modules\CloudKitLibrary\Module as CloudKitLibrary;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Import Export Module
*
* Responsible for initializing Elementor App functionality
*/
class Module extends BaseModule {
const FORMAT_VERSION = '2.0';
const EXPORT_TRIGGER_KEY = 'elementor_export_kit';
const UPLOAD_TRIGGER_KEY = 'elementor_upload_kit';
const IMPORT_TRIGGER_KEY = 'elementor_import_kit';
const IMPORT_RUNNER_TRIGGER_KEY = 'elementor_import_kit__runner';
const REFERRER_KIT_LIBRARY = 'kit-library';
const REFERRER_LOCAL = 'local';
const REFERRER_CLOUD = 'cloud';
const PLUGIN_PERMISSIONS_ERROR_KEY = 'plugin-installation-permissions-error';
const KIT_LIBRARY_ERROR_KEY = 'invalid-kit-library-zip-error';
const NO_WRITE_PERMISSIONS_KEY = 'no-write-permissions';
const THIRD_PARTY_ERROR = 'third-party-error';
const DOMDOCUMENT_MISSING = 'domdocument-missing';
const OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS = 'elementor_import_sessions';
const OPTION_KEY_ELEMENTOR_REVERT_SESSIONS = 'elementor_revert_sessions';
const META_KEY_ELEMENTOR_IMPORT_SESSION_ID = '_elementor_import_session_id';
const META_KEY_ELEMENTOR_EDIT_MODE = '_elementor_edit_mode';
const IMPORT_PLUGINS_ACTION = 'import-plugins';
const EXPORT_SOURCE_CLOUD = 'cloud';
const EXPORT_SOURCE_FILE = 'file';
/**
* Assigning the export process to a property, so we can use the process from outside the class.
*
* @var Export
*/
public $export;
/**
* Assigning the import process to a property, so we can use the process from outside the class.
*
* @var Import
*/
public $import;
/**
* Assigning the revert process to a property, so we can use the process from outside the class.
*
* @var Revert
*/
public $revert;
/**
* Get name.
*
* @access public
*
* @return string
*/
public function get_name() {
return 'import-export';
}
public function __construct() {
$this->register_actions();
if ( ElementorUtils::is_wp_cli() ) {
\WP_CLI::add_command( 'elementor kit', WP_CLI::class );
}
( new Usage() )->register();
$this->revert = new Revert();
}
public function get_init_settings() {
if ( ! Plugin::$instance->app->is_current() ) {
return [];
}
return $this->get_config_data();
}
/**
* Register the import/export tab in elementor tools.
*/
public function register_settings_tab( Tools $tools ) {
$tools->add_tab( 'import-export-kit', [
'label' => esc_html__( 'Website Templates', 'elementor' ),
'sections' => [
'intro' => [
'label' => esc_html__( 'Website Templates', 'elementor' ),
'callback' => function() {
$this->render_import_export_tab_content();
},
'fields' => [],
],
],
] );
}
/**
* Render the import/export tab content.
*/
private function render_import_export_tab_content() {
$is_cloud_kits_available = CloudKitLibrary::get_app()->check_eligibility()['is_eligible'];
$content_data = [
'export' => [
'title' => esc_html__( 'Export this website', 'elementor' ),
'button' => [
'url' => Plugin::$instance->app->get_base_url() . '#/export',
'text' => esc_html__( 'Export', 'elementor' ),
'id' => 'elementor-import-export__export',
],
'description' => esc_html__( 'You can download this website as a .zip file, or upload it to the library.', 'elementor' ),
],
'import' => [
'title' => esc_html__( 'Import website templates', 'elementor' ),
'button' => [
'url' => Plugin::$instance->app->get_base_url() . '#/import',
'text' => esc_html__( 'Import', 'elementor' ),
'id' => 'elementor-import-export__import',
],
'description' => esc_html__( 'You can import design and settings from a .zip file or choose from the library.', 'elementor' ),
],
];
if ( $is_cloud_kits_available ) {
$content_data['import']['button_secondary'] = [
'url' => Plugin::$instance->app->get_base_url() . '#/kit-library/cloud',
'text' => esc_html__( 'Import from library', 'elementor' ),
'id' => 'elementor-import-export__import_from_library',
];
}
$last_imported_kit = $this->revert->get_last_import_session();
$penultimate_imported_kit = $this->revert->get_penultimate_import_session();
$user_date_format = get_option( 'date_format' );
$user_time_format = get_option( 'time_format' );
$date_format = $user_date_format . ' ' . $user_time_format;
$should_show_revert_section = $this->should_show_revert_section( $last_imported_kit );
if ( $should_show_revert_section ) {
if ( ! empty( $penultimate_imported_kit ) ) {
$revert_text = sprintf(
/* 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. */
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' ),
! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
'<br>',
! empty( $penultimate_imported_kit['kit_title'] ) ? $penultimate_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
gmdate( $date_format, $penultimate_imported_kit['start_timestamp'] )
);
} else {
$revert_text = sprintf(
/* translators: 1: Last imported kit title, 2: Last imported kit date, 3: Line break <br>. */
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' ),
! empty( $last_imported_kit['kit_title'] ) ? $last_imported_kit['kit_title'] : esc_html__( 'imported kit', 'elementor' ),
gmdate( $date_format, $last_imported_kit['start_timestamp'] ),
'<br>'
);
}
}
?>
<div class="tab-import-export-kit__content">
<p class="tab-import-export-kit__info">
<?php
printf(
'%1$s <a href="https://go.elementor.com/wp-dash-import-export-general/" target="_blank">%2$s</a>',
esc_html__( 'Heres 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' ),
esc_html__( 'Learn more', 'elementor' ),
);
?>
</p>
<div class="tab-import-export-kit__wrapper">
<?php foreach ( $content_data as $data ) {
$this->print_item_content( $data );
} ?>
</div>
<?php
if ( $should_show_revert_section ) {
$link_attributes = [
'href' => $this->get_revert_href(),
'id' => 'elementor-import-export__revert_kit',
'class' => 'button',
];
?>
<div class="tab-import-export-kit__revert">
<h2>
<?php echo esc_html__( 'Remove the most recent Website Template', 'elementor' ); ?>
</h2>
<p class="tab-import-export-kit__info">
<?php ElementorUtils::print_unescaped_internal_string( $revert_text ); ?>
</p>
<?php $this->render_last_kit_thumbnail( $last_imported_kit ); ?>
<a <?php ElementorUtils::print_html_attributes( $link_attributes ); ?> >
<?php echo esc_html__( 'Remove Website Template', 'elementor' ); ?>
</a>
</div>
<?php } ?>
</div>
<?php
}
private function print_item_content( $data ) {
?>
<div class="tab-import-export-kit__container">
<div class="tab-import-export-kit__box">
<h2><?php ElementorUtils::print_unescaped_internal_string( $data['title'] ); ?></h2>
</div>
<p class="description"><?php ElementorUtils::print_unescaped_internal_string( $data['description'] ); ?></p>
<?php if ( ! empty( $data['link'] ) ) : ?>
<a href="<?php ElementorUtils::print_unescaped_internal_string( $data['link']['url'] ); ?>" target="_blank"><?php ElementorUtils::print_unescaped_internal_string( $data['link']['text'] ); ?></a>
<?php endif; ?>
<div class="tab-import-export-kit__box action-buttons">
<?php if ( ! empty( $data['button_secondary'] ) ) : ?>
<a href="<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['url'] ); ?>" class="elementor-button e-btn-txt e-btn-txt-border">
<?php ElementorUtils::print_unescaped_internal_string( $data['button_secondary']['text'] ); ?>
</a>
<?php endif; ?>
<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">
<?php ElementorUtils::print_unescaped_internal_string( $data['button']['text'] ); ?>
</a>
</div>
</div>
<?php
}
private function get_revert_href(): string {
$admin_post_url = admin_url( 'admin-post.php?action=elementor_revert_kit' );
$nonced_admin_post_url = wp_nonce_url( $admin_post_url, 'elementor_revert_kit' );
return $this->maybe_add_referrer_param( $nonced_admin_post_url );
}
/**
* Checks if referred by a kit and adds the referrer ID to the href
*
* @param string $href
*
* @return string
*/
private function maybe_add_referrer_param( string $href ): string {
$param_name = 'referrer_kit';
if ( empty( $_GET[ $param_name ] ) ) {
return $href;
}
return add_query_arg( $param_name, sanitize_key( $_GET[ $param_name ] ), $href );
}
/**
* Render the last kit thumbnail if exists
*
* @param $last_imported_kit
*
* @return void
*/
private function render_last_kit_thumbnail( $last_imported_kit ) {
if ( empty( $last_imported_kit['kit_thumbnail'] ) ) {
return;
}
?>
<div class="tab-import-export-kit__kit-item-row">
<article class="tab-import-export-kit__kit-item">
<header>
<h3>
<?php echo esc_html( $last_imported_kit['kit_title'] ); ?>
</h3>
</header>
<img
src="<?php echo esc_url( $last_imported_kit['kit_thumbnail'] ); ?>"
alt="<?php echo esc_attr( $last_imported_kit['kit_title'] ); ?>"
loading="lazy"
>
</article>
</div>
<?php
}
/**
* Upload a kit zip file and get the kit data.
*
* Assigning the Import process to the 'import' property,
* so it will be available to use in different places such as: WP_Cli, Pro, etc.
*
* @param string $file Path to the file.
* @param string $referrer Referrer of the file 'local' or 'kit-library'.
* @param string $kit_id
* @return array
* @throws \Exception
*/
public function upload_kit( $file, $referrer, $kit_id = null ) {
$this->ensure_writing_permissions();
$this->import = new Import( $file, [
'referrer' => $referrer,
'id' => $kit_id,
] );
return [
'session' => $this->import->get_session_id(),
'manifest' => $this->import->get_manifest(),
'conflicts' => $this->import->get_settings_conflicts(),
];
}
/**
* Import a kit by session_id.
* Upload and import a kit by kit zip file.
*
* If the split_to_chunks flag is true, the process won't start
* It will initialize the import process and return the session_id and the runners.
*
* Assigning the Import process to the 'import' property,
* so it will be available to use in different places such as: WP_Cli, Pro, etc.
*
* @param string $path Path to the file or session_id.
* @param array $settings Settings the import use to determine which content to import.
* (e.g: include, selected_plugins, selected_cpt, selected_override_conditions, etc.)
* @param bool $split_to_chunks Determine if the import process should be split into chunks.
* @return array
* @throws \Exception
*/
public function import_kit( string $path, array $settings, bool $split_to_chunks = false ): array {
$this->ensure_writing_permissions();
$this->ensure_DOMDocument_exists();
$this->import = new Import( $path, $settings );
$this->import->register_default_runners();
remove_filter( 'elementor/document/save/data', [ Plugin::$instance->modules_manager->get_modules( 'content-sanitizer' ), 'sanitize_content' ] );
do_action( 'elementor/import-export/import-kit', $this->import );
if ( $split_to_chunks ) {
$this->import->init_import_session( true );
return [
'session' => $this->import->get_session_id(),
'runners' => $this->import->get_runners_name(),
];
}
return $this->import->run();
}
/**
* Resuming import process by re-creating the import instance and running the specific runner.
*
* @param string $session_id The id off the import session.
* @param string $runner_name The specific runner that we want to run.
*
* @return array Two types of response.
* 1. The status and the runner name.
* 2. The imported data. (Only if the runner is the last one in the import process)
* @throws \Exception
*/
public function import_kit_by_runner( string $session_id, string $runner_name ): array {
// Check session_id
$this->import = Import::from_session( $session_id );
$runners = $this->import->get_runners_name();
$run = $this->import->run_runner( $runner_name );
if ( end( $runners ) === $run['runner'] ) {
return $this->import->get_imported_data();
}
return $run;
}
/**
* Export a kit.
*
* Assigning the Export process to the 'export' property,
* so it will be available to use in different places such as: WP_Cli, Pro, etc.
*
* @param array $settings Settings the export use to determine which content to export.
* (e.g: include, kit_info, selected_plugins, selected_cpt, etc.)
* @return array
* @throws \Exception
*/
public function export_kit( array $settings ) {
$this->ensure_writing_permissions();
$this->export = new Export( $settings );
$this->export->register_default_runners();
do_action( 'elementor/import-export/export-kit', $this->export );
return $this->export->run();
}
/**
* Handle revert kit ajax request.
*/
public function revert_last_imported_kit() {
$this->revert = new Revert();
$this->revert->register_default_runners();
do_action( 'elementor/import-export/revert-kit', $this->revert );
$this->revert->run();
}
/**
* Handle revert last imported kit ajax request.
*/
public function handle_revert_last_imported_kit() {
check_admin_referer( 'elementor_revert_kit' );
$this->revert_last_imported_kit();
wp_safe_redirect( admin_url( 'admin.php?page=' . Tools::PAGE_ID . '#tab-import-export-kit' ) );
die;
}
/**
* Register appropriate actions.
*/
private function register_actions() {
add_action( 'admin_init', function() {
if ( wp_doing_ajax() &&
isset( $_POST['action'] ) &&
wp_verify_nonce( ElementorUtils::get_super_global_value( $_POST, '_nonce' ), Ajax::NONCE_KEY ) &&
current_user_can( 'manage_options' )
) {
$this->maybe_handle_ajax();
}
} );
add_action( 'admin_post_elementor_revert_kit', [ $this, 'handle_revert_last_imported_kit' ] );
add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] );
if ( ! Plugin::$instance->experiments->is_feature_active( 'import-export-customization' ) ) {
$page_id = Tools::PAGE_ID;
add_action( "elementor/admin/after_create_settings/{$page_id}", [ $this, 'register_settings_tab' ] );
}
// TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
if ( self::IMPORT_PLUGINS_ACTION === ElementorUtils::get_super_global_value( $_SERVER, 'HTTP_X_ELEMENTOR_ACTION' ) ) {
add_filter( 'woocommerce_create_pages', [ $this, 'empty_pages' ], 10, 0 );
}
// TODO ^^^
add_filter( 'elementor/import/kit/result', function( $result ) {
if ( ! empty( $result['file_url'] ) ) {
return [
'file_name' => $this->get_remote_kit_zip( $result['file_url'] ),
'referrer' => static::REFERRER_KIT_LIBRARY,
'file_url' => $result['file_url'],
];
}
return $result;
} );
}
/**
* Prevent the creation of the default WooCommerce pages (Cart, Checkout, etc.)
*
* TODO 18/04/2023 : This needs to be moved to the runner itself after https://elementor.atlassian.net/browse/HTS-434 is done.
*
* @return array
*/
public function empty_pages(): array {
return [];
}
private function ensure_writing_permissions() {
$server = new Server();
$paths_to_check = [
Server::KEY_PATH_WP_CONTENT_DIR => $server->get_system_path( Server::KEY_PATH_WP_CONTENT_DIR ),
Server::KEY_PATH_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_UPLOADS_DIR ),
Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR => $server->get_system_path( Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ),
];
$permissions = $server->get_paths_permissions( $paths_to_check );
// WP Content dir has to be exists and writable.
if ( ! $permissions[ Server::KEY_PATH_WP_CONTENT_DIR ]['write'] ) {
throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
// WP Uploads dir has to be exists and writable.
if ( ! $permissions[ Server::KEY_PATH_UPLOADS_DIR ]['write'] ) {
throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
// Elementor uploads dir permissions is divided to 2 cases:
// 1. If the dir exists, it has to be writable.
// 2. If the dir doesn't exist, the parent dir has to be writable (wp uploads dir), so we can create it.
if ( $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['exists'] && ! $permissions[ Server::KEY_PATH_ELEMENTOR_UPLOADS_DIR ]['write'] ) {
throw new \Error( self::NO_WRITE_PERMISSIONS_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
}
private function ensure_DOMDocument_exists() {
if ( ! class_exists( 'DOMDocument' ) ) {
throw new \Error( self::DOMDOCUMENT_MISSING ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
}
/**
* Enqueue admin scripts
*/
public function enqueue_scripts() {
wp_enqueue_script(
'elementor-import-export-admin',
$this->get_js_assets_url( 'import-export-admin' ),
[ 'elementor-common' ],
ELEMENTOR_VERSION,
true
);
wp_localize_script(
'elementor-import-export-admin',
'elementorImportExport',
[
'lastImportedSession' => $this->revert->get_last_import_session(),
'appUrl' => Plugin::$instance->app->get_base_url() . '#/kit-library',
]
);
}
/**
* Assign each ajax action to a method.
*/
private function maybe_handle_ajax() {
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$action = ElementorUtils::get_super_global_value( $_POST, 'action' );
try {
switch ( $action ) {
case static::EXPORT_TRIGGER_KEY:
$this->handle_export_kit();
break;
case static::UPLOAD_TRIGGER_KEY:
$this->handle_upload_kit();
break;
case static::IMPORT_TRIGGER_KEY:
$this->handle_import_kit();
break;
case static::IMPORT_RUNNER_TRIGGER_KEY:
$this->handle_import_kit__runner();
break;
default:
break;
}
} catch ( \Error $e ) {
if ( isset( $this->import ) ) {
$this->import->finalize_import_session_option();
}
Plugin::$instance->logger->get_logger()->error( $e->getMessage(), [
'meta' => [
'trace' => $e->getTraceAsString(),
],
] );
if ( isset( $this->import ) && $this->is_third_party_class( $e->getTrace()[0]['class'] ) ) {
wp_send_json_error( self::THIRD_PARTY_ERROR, 500 );
}
wp_send_json_error( $e->getMessage(), 500 );
}
}
/**
* Handle upload kit ajax request.
*/
private function handle_upload_kit() {
// PHPCS - A URL that should contain special chars (auth headers information).
$file_url = isset( $_POST['e_import_file'] )
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
? wp_unslash( $_POST['e_import_file'] )
: '';
// PHPCS - Already validated in caller function
$kit_id = ElementorUtils::get_super_global_value( $_POST, 'kit_id' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
$source = ElementorUtils::get_super_global_value( $_POST, 'source' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
$is_import_from_library = ! empty( $file_url );
if ( $is_import_from_library ) {
if (
! wp_verify_nonce( ElementorUtils::get_super_global_value( $_POST, 'e_kit_library_nonce' ), 'kit-library-import' )
) {
throw new \Error( 'Invalid kit library nonce.' );
}
if ( ! filter_var( $file_url, FILTER_VALIDATE_URL ) || 0 !== strpos( $file_url, 'http' ) ) {
throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
$import_result = apply_filters( 'elementor/import/kit/result', [ 'file_url' => $file_url ] );
} else if ( ! empty( $source ) ) {
$import_result = apply_filters( 'elementor/import/kit/result/' . $source, [
'kit_id' => $kit_id,
'source' => $source,
] );
} else {
$import_result = [
'file_name' => ElementorUtils::get_super_global_value( $_FILES, 'e_import_file' )['tmp_name'],
'referrer' => static::REFERRER_LOCAL,
];
}
Plugin::$instance->logger->get_logger()->info( 'Uploading Kit: ', [
'meta' => [
'kit_id' => $kit_id,
'referrer' => $import_result['referrer'],
],
] );
if ( is_wp_error( $import_result ) ) {
wp_send_json_error( $import_result->get_error_message() );
}
$uploaded_kit = $this->upload_kit( $import_result['file_name'], $import_result['referrer'], $kit_id );
$session_dir = $uploaded_kit['session'];
$manifest = $uploaded_kit['manifest'];
$conflicts = $uploaded_kit['conflicts'];
if ( $is_import_from_library || ! empty( $source ) ) {
Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $import_result['file_name'] ) );
}
if ( isset( $manifest['plugins'] ) && ! current_user_can( 'install_plugins' ) ) {
throw new \Error( static::PLUGIN_PERMISSIONS_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
$result = [
'session' => $session_dir,
'manifest' => $manifest,
'file_url' => $import_result['file_url'],
];
if ( ! empty( $import_result['kit'] ) ) {
$result['uploaded_kit'] = $import_result['kit'];
}
if ( ! empty( $conflicts ) ) {
$result['conflicts'] = $conflicts;
} else {
// Moved into the IE process \Elementor\App\Modules\ImportExport\Processes\Import::get_default_settings_conflicts
// TODO: remove in 3.10.0
$result = apply_filters( 'elementor/import/stage_1/result', $result );
}
wp_send_json_success( $result );
}
protected function get_remote_kit_zip( $url ) {
$remote_zip_request = wp_safe_remote_get( $url );
if ( is_wp_error( $remote_zip_request ) ) {
Plugin::$instance->logger->get_logger()->error( $remote_zip_request->get_error_message() );
throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
if ( 200 !== $remote_zip_request['response']['code'] ) {
Plugin::$instance->logger->get_logger()->error( $remote_zip_request['response']['message'] );
throw new \Error( static::KIT_LIBRARY_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
return Plugin::$instance->uploads_manager->create_temp_file( $remote_zip_request['body'], 'kit.zip' );
}
/**
* Handle import kit ajax request.
*/
private function handle_import_kit() {
// PHPCS - Already validated in caller function
$settings = json_decode( ElementorUtils::get_super_global_value( $_POST, 'data' ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
$tmp_folder_id = $settings['session'];
$import = $this->import_kit( $tmp_folder_id, $settings, true );
// get_settings_config() added manually because the frontend Ajax request doesn't trigger the get_init_settings().
$import['configData'] = $this->get_config_data();
Plugin::$instance->logger->get_logger()->info(
sprintf( 'Selected import runners: %1$s',
implode( ', ', $import['runners'] )
)
);
wp_send_json_success( $import );
}
/**
* Handle ajax request for running specific runner in the import kit process.
*/
private function handle_import_kit__runner() {
// PHPCS - Already validated in caller function
$settings = json_decode( ElementorUtils::get_super_global_value( $_POST, 'data' ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
$session_id = $settings['session'];
$runner = $settings['runner'];
$import = $this->import_kit_by_runner( $session_id, $runner );
// get_settings_config() added manually because the frontend Ajax request doesn't trigger the get_init_settings().
$import['configData'] = $this->get_config_data();
if ( ! empty( $import['status'] ) ) {
Plugin::$instance->logger->get_logger()->info(
sprintf( 'Import runner completed: %1$s %2$s',
$import['runner'],
( 'success' === $import['status'] ? '✓' : '✗' )
)
);
}
do_action( 'elementor/import-export/import-kit/runner/after-run', $import );
wp_send_json_success( $import );
}
/**
* Handle export kit ajax request.
*/
private function handle_export_kit() {
// PHPCS - Already validated in caller function
$settings = json_decode( ElementorUtils::get_super_global_value( $_POST, 'data' ), true ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
$source = $settings['kitInfo']['source'];
$export = $this->export_kit( $settings );
$file_name = $export['file_name'];
$file = ElementorUtils::file_get_contents( $file_name );
if ( ! $file ) {
throw new \Error( 'Could not read the exported file.' );
}
Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $file_name ) );
$result = apply_filters(
'elementor/export/kit/export-result',
[
'manifest' => $export['manifest'],
'file' => base64_encode( $file ),
],
$source,
$export,
$settings,
$file,
);
if ( is_wp_error( $result ) ) {
wp_send_json_error( $result );
}
wp_send_json_success( $result );
}
/**
* Get config data that will be exposed to the frontend.
*/
private function get_config_data() {
$export_nonce = wp_create_nonce( 'elementor_export' );
$export_url = add_query_arg( [ '_nonce' => $export_nonce ], Plugin::$instance->app->get_base_url() );
return [
'exportURL' => $export_url,
'summaryTitles' => $this->get_summary_titles(),
'builtinWpPostTypes' => ImportExportUtils::get_builtin_wp_post_types(),
'elementorPostTypes' => ImportExportUtils::get_elementor_post_types(),
'isUnfilteredFilesEnabled' => Uploads_Manager::are_unfiltered_uploads_enabled(),
'elementorHomePageUrl' => $this->get_elementor_home_page_url(),
'recentlyEditedElementorPageUrl' => $this->get_recently_edited_elementor_page_url(),
'tools_url' => Tools::get_url(),
'importSessions' => Revert::get_import_sessions(),
'lastImportedSession' => $this->revert->get_last_import_session(),
'kitPreviewNonce' => wp_create_nonce( 'kit_thumbnail' ),
];
}
/**
* Get labels of Elementor document types, Elementor Post types, WordPress Post types and Custom Post types.
*/
private function get_summary_titles() {
$summary_titles = [];
$document_types = Plugin::$instance->documents->get_document_types();
foreach ( $document_types as $name => $document_type ) {
$summary_titles['templates'][ $name ] = [
'single' => $document_type::get_title(),
'plural' => $document_type::get_plural_title(),
];
}
$elementor_post_types = ImportExportUtils::get_elementor_post_types();
$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
$post_types = array_merge( $elementor_post_types, $wp_builtin_post_types );
foreach ( $post_types as $post_type ) {
$post_type_object = get_post_type_object( $post_type );
$summary_titles['content'][ $post_type ] = [
'single' => $post_type_object->labels->singular_name ?? '',
'plural' => $post_type_object->label ?? '',
];
}
$custom_post_types = ImportExportUtils::get_registered_cpt_names();
if ( ! empty( $custom_post_types ) ) {
foreach ( $custom_post_types as $custom_post_type ) {
$custom_post_types_object = get_post_type_object( $custom_post_type );
// CPT data appears in two arrays:
// 1. content object: in order to show the export summary when completed in getLabel function
$summary_titles['content'][ $custom_post_type ] = [
'single' => $custom_post_types_object->labels->singular_name ?? '',
'plural' => $custom_post_types_object->label ?? '',
];
// 2. customPostTypes object: in order to actually export the data
$summary_titles['content']['customPostTypes'][ $custom_post_type ] = [
'single' => $custom_post_types_object->labels->singular_name ?? '',
'plural' => $custom_post_types_object->label ?? '',
];
}
}
$active_kit = Plugin::$instance->kits_manager->get_active_kit();
foreach ( $active_kit->get_tabs() as $key => $tab ) {
$summary_titles['site-settings'][ $key ] = $tab->get_title();
}
return $summary_titles;
}
public function should_show_revert_section( $last_imported_kit ) {
if ( empty( $last_imported_kit ) ) {
return false;
}
// TODO: BC - remove in the future
// The 'templates' runner was in core and moved to the Pro plugin. (Part of it still exits in the Core for BC)
// The runner that is in the core version is missing the revert functionality,
// therefore we shouldn't display the revert section if the import process done with the core version.
$is_import_templates_ran = isset( $last_imported_kit['runners']['templates'] );
if ( $this->has_pro() && $is_import_templates_ran ) {
$has_imported_templates = ! empty( $last_imported_kit['runners']['templates'] );
return $has_imported_templates;
}
return true;
}
public function has_pro(): bool {
return ElementorUtils::has_pro();
}
private function get_elementor_editor_home_page_url() {
if ( 'page' !== get_option( 'show_on_front' ) ) {
return '';
}
$frontpage_id = get_option( 'page_on_front' );
return $this->get_elementor_editor_page_url( $frontpage_id );
}
private function get_elementor_home_page_url() {
if ( 'page' !== get_option( 'show_on_front' ) ) {
return '';
}
$frontpage_id = get_option( 'page_on_front' );
return $this->get_elementor_page_url( $frontpage_id );
}
private function get_recently_edited_elementor_page_url() {
$query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );
if ( ! isset( $query->post ) ) {
return '';
}
return $this->get_elementor_page_url( $query->post->ID );
}
private function get_recently_edited_elementor_editor_page_url() {
$query = ElementorUtils::get_recently_edited_posts_query( [ 'posts_per_page' => 1 ] );
if ( ! isset( $query->post ) ) {
return '';
}
return $this->get_elementor_editor_page_url( $query->post->ID );
}
private function get_elementor_document( $page_id ) {
$document = Plugin::$instance->documents->get( $page_id );
if ( ! $document || ! $document->is_built_with_elementor() ) {
return false;
}
return $document;
}
private function get_elementor_page_url( $page_id ) {
$document = $this->get_elementor_document( $page_id );
return $document ? $document->get_preview_url() : '';
}
private function get_elementor_editor_page_url( $page_id ) {
$document = $this->get_elementor_document( $page_id );
return $document ? $document->get_edit_url() : '';
}
/**
* @param string $class
*
* @return bool
*/
public function is_third_party_class( $class ) {
$allowed_classes = [
'Elementor\\',
'ElementorPro\\',
'WP_',
'wp_',
];
foreach ( $allowed_classes as $allowed_class ) {
if ( str_starts_with( $class, $allowed_class ) ) {
return false;
}
}
return true;
}
}

View File

@@ -0,0 +1,334 @@
<?php
namespace Elementor\App\Modules\ImportExport\Processes;
use Elementor\App\Modules\ImportExport\Module;
use Elementor\App\Modules\ImportExport\Utils;
use Elementor\Core\Utils\Str;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExport\Runners\Export\Elementor_Content;
use Elementor\App\Modules\ImportExport\Runners\Export\Export_Runner_Base;
use Elementor\App\Modules\ImportExport\Runners\Export\Plugins;
use Elementor\App\Modules\ImportExport\Runners\Export\Site_Settings;
use Elementor\App\Modules\ImportExport\Runners\Export\Taxonomies;
use Elementor\App\Modules\ImportExport\Runners\Export\Templates;
use Elementor\App\Modules\ImportExport\Runners\Export\Wp_Content;
class Export {
const ZIP_ARCHIVE_MODULE_MISSING = 'zip-archive-module-is-missing';
/**
* @var Export_Runner_Base[]
*/
protected $runners = [];
/**
* Selected content types to export.
*
* @var array
*/
private $settings_include;
/**
* The kit information. (e.g: title, description)
*
* @var array $export_data
*/
private $settings_kit_info;
/**
* Selected plugins to export.
* Contains the plugins essential data for export. (e.g: name, path, version, etc.)
*
* @var array
*/
private $settings_selected_plugins;
/**
* Selected custom post types to export.
*
* @var array
*/
private $settings_selected_custom_post_types;
/**
* The output data of the export process.
* Will be written into the manifest.json file.
*
* @var array
*/
private $manifest_data;
/**
* The zip archive object.
*
* @var \ZipArchive
*/
private $zip;
public function __construct( $settings = [] ) {
$this->settings_include = ! empty( $settings['include'] ) ? $settings['include'] : null;
$this->settings_kit_info = ! empty( $settings['kitInfo'] ) ? $settings['kitInfo'] : null;
$this->settings_selected_plugins = isset( $settings['plugins'] ) ? $settings['plugins'] : null;
$this->settings_selected_custom_post_types = isset( $settings['selectedCustomPostTypes'] ) ? $settings['selectedCustomPostTypes'] : null;
}
/**
* Register a runner.
*
* @param Export_Runner_Base $runner_instance
*/
public function register( Export_Runner_Base $runner_instance ) {
$this->runners[ $runner_instance::get_name() ] = $runner_instance;
}
public function register_default_runners() {
$this->register( new Site_Settings() );
$this->register( new Plugins() );
$this->register( new Templates() );
$this->register( new Taxonomies() );
$this->register( new Elementor_Content() );
$this->register( new Wp_Content() );
}
/**
* Execute the export process.
*
* @return array The export data output.
*
* @throws \Exception If no export runners have been specified.
*/
public function run() {
if ( empty( $this->runners ) ) {
throw new \Exception( 'Couldnt execute the export process because no export runners have been specified. Try again by specifying export runners.' );
}
$this->set_default_settings();
$this->init_zip_archive();
$this->init_manifest_data();
$data = [
'include' => $this->settings_include,
'selected_plugins' => $this->settings_selected_plugins,
'selected_custom_post_types' => $this->settings_selected_custom_post_types,
];
foreach ( $this->runners as $runner ) {
if ( $runner->should_export( $data ) ) {
$export_result = $runner->export( $data );
$this->handle_export_result( $export_result );
}
}
$this->add_json_file( 'manifest', $this->manifest_data );
$zip_file_name = $this->zip->filename;
$this->zip->close();
return [
'manifest' => $this->manifest_data,
'file_name' => $zip_file_name,
];
}
/**
* Set default settings for the export.
*/
private function set_default_settings() {
if ( ! is_array( $this->get_settings_include() ) ) {
$this->settings_include( $this->get_default_settings_include() );
}
if ( ! is_array( $this->get_settings_kit_info() ) ) {
$this->settings_kit_info( $this->get_default_settings_kit_info() );
}
if ( ! is_array( $this->get_settings_selected_custom_post_types() ) && in_array( 'content', $this->settings_include, true ) ) {
$this->settings_selected_custom_post_types( $this->get_default_settings_custom_post_types() );
}
if ( ! is_array( $this->get_settings_selected_plugins() ) && in_array( 'plugins', $this->settings_include, true ) ) {
$this->settings_selected_plugins( $this->get_default_settings_selected_plugins() );
}
}
public function settings_include( $include ) {
$this->settings_include = $include;
}
public function get_settings_include() {
return $this->settings_include;
}
private function settings_kit_info( $kit_info ) {
$this->settings_kit_info = $kit_info;
}
private function get_settings_kit_info() {
return $this->settings_kit_info;
}
public function settings_selected_custom_post_types( $selected_custom_post_types ) {
$this->settings_selected_custom_post_types = $selected_custom_post_types;
}
public function get_settings_selected_custom_post_types() {
return $this->settings_selected_custom_post_types;
}
public function settings_selected_plugins( $plugins ) {
$this->settings_selected_plugins = $plugins;
}
public function get_settings_selected_plugins() {
return $this->settings_selected_plugins;
}
/**
* Get the default settings of which content types should be exported.
*
* @return array
*/
private function get_default_settings_include() {
return [ 'templates', 'content', 'settings', 'plugins' ];
}
/**
* Get the default settings of the kit info.
*
* @return array
*/
private function get_default_settings_kit_info() {
return [
'title' => 'kit',
'description' => '',
];
}
/**
* Get the default settings of the plugins that should be exported.
*
* @return array{name: string, plugin:string, pluginUri: string, version: string}
*/
private function get_default_settings_selected_plugins() {
$installed_plugins = Plugin::$instance->wp->get_plugins();
return $installed_plugins->map( function ( $item, $key ) {
return [
'name' => $item['Name'],
'plugin' => $key,
'pluginUri' => $item['PluginURI'],
'version' => $item['Version'],
];
} )->all();
}
/**
* Get the default settings of all the custom post types that should be exported.
* Should be all the custom post types that are not built in to WordPress and not part of Elementor.
*
* @return array
*/
private function get_default_settings_custom_post_types() {
return Utils::get_registered_cpt_names();
}
/**
* Init the zip archive.
*/
private function init_zip_archive() {
if ( ! class_exists( '\ZipArchive' ) ) {
throw new \Error( static::ZIP_ARCHIVE_MODULE_MISSING ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
$zip = new \ZipArchive();
$temp_dir = Plugin::$instance->uploads_manager->create_unique_dir();
$zip_file_name = $temp_dir . sanitize_title( $this->settings_kit_info['title'] ) . '.zip';
$zip->open( $zip_file_name, \ZipArchive::CREATE | \ZipArchive::OVERWRITE );
$this->zip = $zip;
}
/**
* Init the manifest data and add some basic info to it.
*/
private function init_manifest_data() {
$kit_post = Plugin::$instance->kits_manager->get_active_kit()->get_post();
$manifest_data = [
'name' => sanitize_title( $this->settings_kit_info['title'] ),
'title' => $this->settings_kit_info['title'],
'description' => $this->settings_kit_info['description'],
'author' => get_the_author_meta( 'display_name', $kit_post->post_author ),
'version' => Module::FORMAT_VERSION,
'elementor_version' => ELEMENTOR_VERSION,
'created' => gmdate( 'Y-m-d H:i:s' ),
'thumbnail' => get_the_post_thumbnail_url( $kit_post ),
'site' => get_site_url(),
];
$this->manifest_data = $manifest_data;
}
/**
* Handle the export process output.
* Add the manifest data from the runner to the manifest.json file.
* Create files according to the files array that should be exported by the runner.
*
* @param array $export_result
*/
private function handle_export_result( $export_result ) {
foreach ( $export_result['manifest'] as $data ) {
$this->manifest_data += $data;
}
if ( isset( $export_result['files']['path'] ) ) {
$export_result['files'] = [ $export_result['files'] ];
}
foreach ( $export_result['files'] as $file ) {
$file_extension = pathinfo( $file['path'], PATHINFO_EXTENSION );
if ( empty( $file_extension ) ) {
$this->add_json_file(
$file['path'],
$file['data']
);
} else {
$this->add_file(
$file['path'],
$file['data']
);
}
}
}
/**
* Add json file to the zip archive.
*
* @param string $path The relative path to the file.
* @param array $content The content of the file.
* @param int $json_flags
*/
private function add_json_file( $path, array $content, $json_flags = 0 ) {
if ( ! Str::ends_with( $path, '.json' ) ) {
$path .= '.json';
}
$this->add_file( $path, wp_json_encode( $content, $json_flags ) );
}
/**
* Add file to the zip archive.
*
* @param string $file
* @param string $content The content of the file.
*/
private function add_file( $file, $content ) {
$this->zip->addFromString( $file, $content );
}
}

View File

@@ -0,0 +1,795 @@
<?php
namespace Elementor\App\Modules\ImportExport\Processes;
use Elementor\App\Modules\ImportExport\Compatibility\Base_Adapter;
use Elementor\App\Modules\ImportExport\Compatibility\Envato;
use Elementor\App\Modules\ImportExport\Compatibility\Kit_Library;
use Elementor\App\Modules\ImportExport\Utils;
use Elementor\Core\Base\Document;
use Elementor\Core\Kits\Documents\Kit;
use Elementor\Plugin;
use Elementor\App\Modules\ImportExport\Runners\Import\Elementor_Content;
use Elementor\App\Modules\ImportExport\Runners\Import\Import_Runner_Base;
use Elementor\App\Modules\ImportExport\Runners\Import\Plugins;
use Elementor\App\Modules\ImportExport\Runners\Import\Site_Settings;
use Elementor\App\Modules\ImportExport\Runners\Import\Taxonomies;
use Elementor\App\Modules\ImportExport\Runners\Import\Templates;
use Elementor\App\Modules\ImportExport\Runners\Import\Wp_Content;
use Elementor\App\Modules\ImportExport\Module;
class Import {
const MANIFEST_ERROR_KEY = 'manifest-error';
const ZIP_FILE_ERROR_KEY = 'invalid-zip-file';
const ZIP_ARCHIVE_ERROR_KEY = 'zip-archive-module-missing';
/**
* @var Import_Runner_Base[]
*/
protected $runners = [];
/**
* The session ID of the import process.
* This ID is uniquely generated for each import process (by the temp folder which contains the extracted kit files).
*
* @var string
*/
private $session_id;
/**
* The Kit ID.
*
* @var string
*/
private $kit_id;
/**
* Adapter for the kit compatibility.
*
* @var Base_Adapter[]
*/
private $adapters;
/**
* Document's data (elements and settings) that was imported during the process.
*
* @var array { [document_id] => { "elements": array , "settings": array } }
*/
private $documents_data = [];
/**
* Path to the extracted kit files.
*
* @var string
*/
private $extracted_directory_path;
/**
* Imported kit manifest.
*
* @var array
*/
private $manifest;
/**
* Imported kit site settings. (e.g: custom_colors, custom_typography, etc.)
*
* @var array
*/
private $site_settings;
/**
* Selected content types to import.
*
* @var array
*/
private $settings_include;
/**
* Referer of the import. (e.g: kit-library, local, etc.)
*
* @var string
*/
private $settings_referrer;
/**
* All the conflict between the exited templates and the kit templates.
*
* @var array
*/
private $settings_conflicts;
/**
* Selected elementor templates conditions to override.
*
* @var array
*/
private $settings_selected_override_conditions;
/**
* Selected custom post types to import.
*
* @var array
*/
private $settings_selected_custom_post_types;
/**
* Selected plugins to import.
*
* @var array
*/
private $settings_selected_plugins;
/**
* The imported data output.
*
* @var array
*/
private $imported_data = [];
/**
* The metadata output of the import runners.
* Will be saved in the import_session and will be used to revert the import process.
*
* @var array
*/
private $runners_import_metadata = [];
/**
* @param string $path session_id | zip_file_path
* @param array $settings Use to determine which content to import.
* (e.g: include, selected_plugins, selected_cpt, selected_override_conditions, etc.)
* @param array|null $old_instance An array of old instance parameters that will be used for creating new instance.
* We are using it for quick creation of the instance when the import process is being split into chunks.
* @throws \Exception If the import session does not exist.
*/
public function __construct( string $path, array $settings = [], array $old_instance = null ) {
if ( ! empty( $old_instance ) ) {
$this->set_import_object( $old_instance );
} else {
if ( is_file( $path ) ) {
$this->extracted_directory_path = $this->extract_zip( $path );
} else {
$elementor_tmp_directory = Plugin::$instance->uploads_manager->get_temp_dir();
$path = $elementor_tmp_directory . basename( $path );
if ( ! is_dir( $path ) ) {
throw new \Exception( 'Couldnt execute the import process because the import session does not exist.' );
}
$this->extracted_directory_path = $path . '/';
}
$this->session_id = basename( $this->extracted_directory_path );
$this->kit_id = $settings['id'] ?? '';
$this->settings_referrer = ! empty( $settings['referrer'] ) ? $settings['referrer'] : 'local';
$this->settings_include = ! empty( $settings['include'] ) ? $settings['include'] : null;
// Using isset and not empty is important since empty array is valid option.
$this->settings_selected_override_conditions = $settings['overrideConditions'] ?? null;
$this->settings_selected_custom_post_types = $settings['selectedCustomPostTypes'] ?? null;
$this->settings_selected_plugins = $settings['plugins'] ?? null;
$this->manifest = $this->read_manifest_json();
$this->site_settings = $this->read_site_settings_json();
$this->set_default_settings();
}
add_filter( 'wp_php_error_args', function ( $args, $error ) {
return $this->filter_php_error_args( $args, $error );
}, 10, 2 );
}
/**
* Set the import object parameters.
*
* @param array $instance
* @return void
*/
private function set_import_object( array $instance ) {
$this->session_id = $instance['session_id'];
$instance_data = $instance['instance_data'];
$this->extracted_directory_path = $instance_data['extracted_directory_path'];
$this->runners = $instance_data['runners'];
$this->adapters = $instance_data['adapters'];
$this->manifest = $instance_data['manifest'];
$this->site_settings = $instance_data['site_settings'];
$this->settings_include = $instance_data['settings_include'];
$this->settings_referrer = $instance_data['settings_referrer'];
$this->settings_conflicts = $instance_data['settings_conflicts'];
$this->settings_selected_override_conditions = $instance_data['settings_selected_override_conditions'];
$this->settings_selected_custom_post_types = $instance_data['settings_selected_custom_post_types'];
$this->settings_selected_plugins = $instance_data['settings_selected_plugins'];
$this->documents_data = $instance_data['documents_data'];
$this->imported_data = $instance_data['imported_data'];
$this->runners_import_metadata = $instance_data['runners_import_metadata'];
}
/**
* Creating a new instance of the import process by the id of the old import session.
*
* @param string $session_id
*
* @return Import
* @throws \Exception If the import session does not exist.
*/
public static function from_session( string $session_id ): Import {
$import_sessions = Utils::get_import_sessions();
if ( ! $import_sessions || ! isset( $import_sessions[ $session_id ] ) ) {
throw new \Exception( 'Couldnt execute the import process because the import session does not exist.' );
}
$import_session = $import_sessions[ $session_id ];
return new self( $session_id, [], $import_session );
}
/**
* Register a runner.
* Be aware that the runner will be executed in the order of registration, the order is crucial for the import process.
*
* @param Import_Runner_Base $runner_instance
*/
public function register( Import_Runner_Base $runner_instance ) {
$this->runners[ $runner_instance::get_name() ] = $runner_instance;
}
public function register_default_runners() {
$this->register( new Site_Settings() );
$this->register( new Plugins() );
$this->register( new Templates() );
$this->register( new Taxonomies() );
$this->register( new Elementor_Content() );
$this->register( new Wp_Content() );
}
/**
* Set default settings for the import.
*/
private function set_default_settings() {
if ( ! is_array( $this->get_settings_include() ) ) {
$this->settings_include( $this->get_default_settings_include() );
}
if ( ! is_array( $this->get_settings_conflicts() ) ) {
$this->settings_conflicts( $this->get_default_settings_conflicts() );
}
if ( ! is_array( $this->get_settings_selected_override_conditions() ) ) {
$this->settings_selected_override_conditions( $this->get_default_settings_override_conditions() );
}
if ( ! is_array( $this->get_settings_selected_custom_post_types() ) ) {
$this->settings_selected_custom_post_types( $this->get_default_settings_custom_post_types() );
}
if ( ! is_array( $this->get_settings_selected_plugins() ) ) {
$this->settings_selected_plugins( $this->get_default_settings_plugins() );
}
}
/**
* Execute the import process.
*
* @return array The imported data output.
*
* @throws \Exception If no import runners have been specified.
*/
public function run() {
if ( empty( $this->runners ) ) {
throw new \Exception( 'Couldnt execute the import process because no import runners have been specified. Try again by specifying import runners.' );
}
$data = [
'session_id' => $this->session_id,
'include' => $this->settings_include,
'manifest' => $this->manifest,
'site_settings' => $this->site_settings,
'selected_plugins' => $this->settings_selected_plugins,
'extracted_directory_path' => $this->extracted_directory_path,
'selected_custom_post_types' => $this->settings_selected_custom_post_types,
];
$this->init_import_session();
remove_filter( 'elementor/document/save/data', [ Plugin::$instance->modules_manager->get_modules( 'content-sanitizer' ), 'sanitize_content' ] );
add_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10, 2 );
// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
foreach ( $this->runners as $runner ) {
if ( $runner->should_import( $data ) ) {
$import = $runner->import( $data, $this->imported_data );
$this->imported_data = array_merge_recursive( $this->imported_data, $import );
$this->runners_import_metadata[ $runner::get_name() ] = $runner->get_import_session_metadata();
}
}
// After the upload complete, set the elementor upload state back to false.
Plugin::$instance->uploads_manager->set_elementor_upload_state( false );
remove_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10 );
$this->finalize_import_session_option();
$this->save_elements_of_imported_posts();
Plugin::$instance->uploads_manager->remove_file_or_dir( $this->extracted_directory_path );
return $this->imported_data;
}
/**
* Run specific runner by runner_name
*
* @param string $runner_name
*
* @return array
*
* @throws \Exception If no export runners have been specified.
*/
public function run_runner( string $runner_name ): array {
if ( empty( $this->runners ) ) {
throw new \Exception( 'Couldnt execute the import process because no import runners have been specified. Try again by specifying import runners.' );
}
$data = [
'session_id' => $this->session_id,
'include' => $this->settings_include,
'manifest' => $this->manifest,
'site_settings' => $this->site_settings,
'selected_plugins' => $this->settings_selected_plugins,
'extracted_directory_path' => $this->extracted_directory_path,
'selected_custom_post_types' => $this->settings_selected_custom_post_types,
];
add_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10, 2 );
// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
$runner = $this->runners[ $runner_name ];
if ( empty( $runner ) ) {
throw new \Exception( 'Couldnt execute the import process because the import runner was not found. Try again by specifying an import runner.' );
}
if ( $runner->should_import( $data ) ) {
$import = $runner->import( $data, $this->imported_data );
$this->imported_data = array_merge_recursive( $this->imported_data, $import );
$this->runners_import_metadata[ $runner::get_name() ] = $runner->get_import_session_metadata();
}
// After the upload complete, set the elementor upload state back to false.
Plugin::$instance->uploads_manager->set_elementor_upload_state( false );
remove_filter( 'elementor/document/save/data', [ $this, 'prevent_saving_elements_on_post_creation' ], 10 );
$is_last_runner = key( array_slice( $this->runners, -1, 1, true ) ) === $runner_name;
if ( $is_last_runner ) {
$this->finalize_import_session_option();
$this->save_elements_of_imported_posts();
} else {
$this->update_instance_data_in_import_session_option();
}
return [
'status' => 'success',
'runner' => $runner_name,
];
}
/**
* Create and save all the instance data to the import sessions option.
*
* @return void
*/
public function init_import_session( $save_instance_data = false ) {
$import_sessions = Utils::get_import_sessions( true );
$import_sessions[ $this->session_id ] = [
'session_id' => $this->session_id,
'kit_title' => $this->manifest['title'] ?? '',
'kit_name' => $this->manifest['name'] ?? '',
'kit_thumbnail' => $this->get_kit_thumbnail(),
'kit_source' => $this->settings_referrer,
'user_id' => get_current_user_id(),
'start_timestamp' => current_time( 'timestamp' ),
];
if ( $save_instance_data ) {
$import_sessions[ $this->session_id ]['instance_data'] = [
'extracted_directory_path' => $this->extracted_directory_path,
'runners' => $this->runners,
'adapters' => $this->adapters,
'manifest' => $this->manifest,
'site_settings' => $this->site_settings,
'settings_include' => $this->settings_include,
'settings_referrer' => $this->settings_referrer,
'settings_conflicts' => $this->settings_conflicts,
'settings_selected_override_conditions' => $this->settings_selected_override_conditions,
'settings_selected_custom_post_types' => $this->settings_selected_custom_post_types,
'settings_selected_plugins' => $this->settings_selected_plugins,
'documents_data' => $this->documents_data,
'imported_data' => $this->imported_data,
'runners_import_metadata' => $this->runners_import_metadata,
];
}
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
}
/**
* Get the Kit thumbnail, goes to the home page thumbnail if main doesn't exist
*
* @return string
*/
private function get_kit_thumbnail(): string {
if ( ! empty( $this->manifest['thumbnail'] ) ) {
return $this->manifest['thumbnail'];
}
return apply_filters( 'elementor/import/kit_thumbnail', '', $this->kit_id, $this->settings_referrer );
}
public function get_runners_name(): array {
return array_keys( $this->runners );
}
public function get_manifest() {
return $this->manifest;
}
public function get_extracted_directory_path() {
return $this->extracted_directory_path;
}
public function get_session_id() {
return $this->session_id;
}
public function get_adapters() {
return $this->adapters;
}
public function get_imported_data() {
return $this->imported_data;
}
/**
* Get settings by key.
* Used for backward compatibility.
*
* @param string $key The key of the setting.
*/
public function get_settings( $key ) {
switch ( $key ) {
case 'include':
return $this->get_settings_include();
case 'overrideConditions':
return $this->get_settings_selected_override_conditions();
case 'selectedCustomPostTypes':
return $this->get_settings_selected_custom_post_types();
case 'plugins':
return $this->get_settings_selected_plugins();
default:
return [];
}
}
public function settings_include( array $settings_include ) {
$this->settings_include = $settings_include;
return $this;
}
public function get_settings_include() {
return $this->settings_include;
}
public function settings_referrer( $settings_referrer ) {
$this->settings_referrer = $settings_referrer;
return $this;
}
public function get_settings_referrer() {
return $this->settings_referrer;
}
public function settings_conflicts( array $settings_conflicts ) {
$this->settings_conflicts = $settings_conflicts;
return $this;
}
public function get_settings_conflicts() {
return $this->settings_conflicts;
}
public function settings_selected_override_conditions( array $settings_selected_override_conditions ) {
$this->settings_selected_override_conditions = $settings_selected_override_conditions;
return $this;
}
public function get_settings_selected_override_conditions() {
return $this->settings_selected_override_conditions;
}
public function settings_selected_custom_post_types( array $settings_selected_custom_post_types ) {
$this->settings_selected_custom_post_types = $settings_selected_custom_post_types;
return $this;
}
public function get_settings_selected_custom_post_types() {
return $this->settings_selected_custom_post_types;
}
public function settings_selected_plugins( array $settings_selected_plugins ) {
$this->settings_selected_plugins = $settings_selected_plugins;
return $this;
}
public function get_settings_selected_plugins() {
return $this->settings_selected_plugins;
}
/**
* Prevent saving elements on elementor post creation.
*
* @param array $data
* @param Document $document
*
* @return array
*/
public function prevent_saving_elements_on_post_creation( array $data, Document $document ) {
if ( isset( $data['elements'] ) ) {
$this->documents_data[ $document->get_main_id() ] = [ 'elements' => $data['elements'] ];
$data['elements'] = [];
}
if ( isset( $data['settings'] ) ) {
$this->documents_data[ $document->get_main_id() ]['settings'] = $data['settings'];
}
return $data;
}
/**
* Extract the zip file.
*
* @param string $zip_path The path to the zip file.
* @return string The extracted directory path.
*/
private function extract_zip( $zip_path ) {
$extraction_result = Plugin::$instance->uploads_manager->extract_and_validate_zip( $zip_path, [ 'json', 'xml' ] );
if ( is_wp_error( $extraction_result ) ) {
if ( isset( $extraction_result->errors['zip_error'] ) ) {
throw new \Error( static::ZIP_ARCHIVE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
throw new \Error( static::ZIP_FILE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
return $extraction_result['extraction_directory'];
}
/**
* Get the manifest file from the extracted directory and adapt it if needed.
*
* @return string The manifest file content.
*/
private function read_manifest_json() {
$manifest = Utils::read_json_file( $this->extracted_directory_path . 'manifest' );
if ( ! $manifest ) {
Plugin::$instance->logger->get_logger()->error( static::MANIFEST_ERROR_KEY );
throw new \Error( static::ZIP_FILE_ERROR_KEY ); // phpcs:ignore WordPress.Security.EscapeOutput.ExceptionNotEscaped
}
$this->init_adapters( $manifest );
foreach ( $this->adapters as $adapter ) {
$manifest = $adapter->adapt_manifest( $manifest );
}
return $manifest;
}
/**
* Init the adapters and determine which ones to use.
*
* @param array $manifest_data The manifest file content.
*/
private function init_adapters( array $manifest_data ) {
$this->adapters = [];
/** @var Base_Adapter[] $adapter_types */
$adapter_types = [ Envato::class, Kit_Library::class ];
foreach ( $adapter_types as $adapter_type ) {
if ( $adapter_type::is_compatibility_needed( $manifest_data, [ 'referrer' => $this->get_settings_referrer() ] ) ) {
$this->adapters[] = new $adapter_type( $this );
}
}
}
/**
* Get the site settings file from the extracted directory and adapt it if needed.
*
* @return string The site settings file content.
*/
private function read_site_settings_json() {
$site_settings = Utils::read_json_file( $this->extracted_directory_path . 'site-settings' );
foreach ( $this->adapters as $adapter ) {
$site_settings = $adapter->adapt_site_settings( $site_settings, $this->manifest, $this->extracted_directory_path );
}
return $site_settings;
}
/**
* Get all the custom post types in the kit.
*
* @return array Custom post types names.
*/
private function get_default_settings_custom_post_types() {
if ( empty( $this->manifest['custom-post-type-title'] ) ) {
return [];
}
$manifest_post_types = array_keys( $this->manifest['custom-post-type-title'] );
return array_diff( $manifest_post_types, Utils::get_builtin_wp_post_types() );
}
/**
* Get the default settings of elementor templates conditions to override.
*
* @return array
*/
private function get_default_settings_conflicts() {
if ( empty( $this->manifest['templates'] ) ) {
return [];
}
return apply_filters( 'elementor/import/get_default_settings_conflicts', [], $this->manifest['templates'] );
}
/**
* Get the default settings of elementor templates conditions to override.
*
* @return array
*/
private function get_default_settings_override_conditions() {
if ( empty( $this->settings_conflicts ) ) {
return [];
}
return array_keys( $this->settings_conflicts );
}
/**
* Get the default settings of the plugins that should be imported.
*
* @return array
*/
private function get_default_settings_plugins() {
return ! empty( $this->manifest['plugins'] ) ? $this->manifest['plugins'] : [];
}
/**
* Get the default settings of which content types should be imported.
*
* @return array
*/
private function get_default_settings_include() {
return [ 'templates', 'plugins', 'content', 'settings' ];
}
/**
* Get the data that requires updating/replacement when imported.
*
* @return array{post_ids: array, term_ids: array}
*/
private function get_imported_data_replacements(): array {
return [
'post_ids' => Utils::map_old_new_post_ids( $this->imported_data ),
'term_ids' => Utils::map_old_new_term_ids( $this->imported_data ),
];
}
/**
* Save the prevented elements on elementor post creation elements.
* Handle the replacement of all the dynamic content of the elements that probably have been changed during the import.
*/
private function save_elements_of_imported_posts() {
$imported_data_replacements = $this->get_imported_data_replacements();
foreach ( $this->documents_data as $new_id => $data ) {
$document = Plugin::$instance->documents->get( $new_id );
if ( isset( $data['elements'] ) ) {
$data['elements'] = $document->on_import_update_dynamic_content( $data['elements'], $imported_data_replacements );
}
if ( isset( $data['settings'] ) ) {
if ( $document instanceof Kit ) {
// Without post_status certain tabs in the Kit will not save properly.
$data['settings']['post_status'] = get_post_status( $new_id );
}
$data['settings'] = $document->on_import_update_settings( $data['settings'], $imported_data_replacements );
}
$document->save( $data );
}
}
private function update_instance_data_in_import_session_option() {
$import_sessions = Utils::get_import_sessions();
$import_sessions[ $this->session_id ]['instance_data']['documents_data'] = $this->documents_data;
$import_sessions[ $this->session_id ]['instance_data']['imported_data'] = $this->imported_data;
$import_sessions[ $this->session_id ]['instance_data']['runners_import_metadata'] = $this->runners_import_metadata;
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
}
public function finalize_import_session_option() {
$import_sessions = Utils::get_import_sessions();
if ( ! isset( $import_sessions[ $this->session_id ] ) ) {
return;
}
unset( $import_sessions[ $this->session_id ]['instance_data'] );
$import_sessions[ $this->session_id ]['end_timestamp'] = current_time( 'timestamp' );
$import_sessions[ $this->session_id ]['runners'] = $this->runners_import_metadata;
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
}
/**
* Filter the php error args and return 408 status code if the error is a timeout.
*
* @param array $args
* @param array $error
* @return array
*/
private function filter_php_error_args( $args, $error ) {
if ( strpos( $error['message'], 'Maximum execution time' ) !== false ) {
$args['response'] = 408;
}
return $args;
}
}

View File

@@ -0,0 +1,176 @@
<?php
namespace Elementor\App\Modules\ImportExport\Processes;
use Elementor\App\Modules\ImportExport\Module;
use Elementor\App\Modules\ImportExport\Runners\Revert\Elementor_Content;
use Elementor\App\Modules\ImportExport\Runners\Revert\Revert_Runner_Base;
use Elementor\App\Modules\ImportExport\Runners\Revert\Plugins;
use Elementor\App\Modules\ImportExport\Runners\Revert\Site_Settings;
use Elementor\App\Modules\ImportExport\Runners\Revert\Taxonomies;
use Elementor\App\Modules\ImportExport\Runners\Revert\Templates;
use Elementor\App\Modules\ImportExport\Runners\Revert\Wp_Content;
use Elementor\App\Modules\ImportExport\Utils;
class Revert {
/**
* @var Revert_Runner_Base[]
*/
protected $runners = [];
private $import_sessions;
private $revert_sessions;
public function __construct() {
$this->import_sessions = self::get_import_sessions();
$this->revert_sessions = self::get_revert_sessions();
}
/**
* Register a runner.
*
* @param Revert_Runner_Base $runner_instance
*/
public function register( Revert_Runner_Base $runner_instance ) {
$this->runners[ $runner_instance::get_name() ] = $runner_instance;
}
public function register_default_runners() {
$this->register( new Site_Settings() );
$this->register( new Plugins() );
$this->register( new Templates() );
$this->register( new Taxonomies() );
$this->register( new Elementor_Content() );
$this->register( new Wp_Content() );
}
/**
* Execute the revert process.
*
* @throws \Exception If no revert runners have been specified.
*/
public function run() {
if ( empty( $this->runners ) ) {
throw new \Exception( 'Couldnt execute the revert process because no revert runners have been specified. Try again by specifying revert runners.' );
}
$import_session = $this->get_last_import_session();
if ( empty( $import_session ) ) {
throw new \Exception( 'Couldnt execute the revert process because there are no import sessions to revert.' );
}
// fallback if the import session failed and doesn't have the runners metadata
if ( ! isset( $import_session['runners'] ) && isset( $import_session['instance_data'] ) ) {
$import_session['runners'] = $import_session['instance_data']['runners_import_metadata'] ?? [];
}
foreach ( $this->runners as $runner ) {
if ( $runner->should_revert( $import_session ) ) {
$runner->revert( $import_session );
}
}
$this->revert_attachments( $import_session );
$this->delete_last_import_data();
}
public static function get_import_sessions() {
$import_sessions = Utils::get_import_sessions();
if ( ! $import_sessions ) {
return [];
}
usort( $import_sessions, function( $a, $b ) {
return strcmp( $a['start_timestamp'], $b['start_timestamp'] );
} );
return $import_sessions;
}
public static function get_revert_sessions() {
$revert_sessions = get_option( Module::OPTION_KEY_ELEMENTOR_REVERT_SESSIONS );
if ( ! $revert_sessions ) {
return [];
}
return $revert_sessions;
}
public function get_last_import_session() {
$import_sessions = $this->import_sessions;
if ( empty( $import_sessions ) ) {
return [];
}
return end( $import_sessions );
}
public function get_penultimate_import_session() {
$sessions_data = $this->import_sessions;
$penultimate_element_value = [];
if ( empty( $sessions_data ) ) {
return [];
}
end( $sessions_data );
prev( $sessions_data );
if ( ! is_null( key( $sessions_data ) ) ) {
$penultimate_element_value = current( $sessions_data );
}
return $penultimate_element_value;
}
private function delete_last_import_data() {
$import_sessions = $this->import_sessions;
$revert_sessions = $this->revert_sessions;
$reverted_session = array_pop( $import_sessions );
$revert_sessions[] = [
'session_id' => $reverted_session['session_id'],
'kit_title' => $reverted_session['kit_title'],
'kit_name' => $reverted_session['kit_name'],
'kit_thumbnail' => $reverted_session['kit_thumbnail'],
'source' => $reverted_session['kit_source'],
'user_id' => get_current_user_id(),
'import_timestamp' => $reverted_session['start_timestamp'],
'revert_timestamp' => current_time( 'timestamp' ),
];
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions, false );
update_option( Module::OPTION_KEY_ELEMENTOR_REVERT_SESSIONS, $revert_sessions, false );
$this->import_sessions = $import_sessions;
$this->revert_sessions = $revert_sessions;
}
private function revert_attachments( $data ) {
$query_args = [
'post_type' => 'attachment',
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => Module::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
];
$query = new \WP_Query( $query_args );
foreach ( $query->posts as $post ) {
wp_delete_attachment( $post->ID, true );
}
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Export;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;
class Elementor_Content extends Export_Runner_Base {
private $page_on_front_id;
public function __construct() {
$this->init_page_on_front_data();
}
public static function get_name(): string {
return 'elementor-content';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true )
);
}
public function export( array $data ) {
$elementor_post_types = ImportExportUtils::get_elementor_post_types();
$files = [];
$manifest = [];
foreach ( $elementor_post_types as $post_type ) {
$export = $this->export_elementor_post_type( $post_type );
$files = array_merge( $files, $export['files'] );
$manifest[ $post_type ] = $export['manifest_data'];
}
$manifest_data['content'] = $manifest;
return [
'files' => $files,
'manifest' => [
$manifest_data,
],
];
}
private function export_elementor_post_type( $post_type ) {
$query_args = [
'post_type' => $post_type,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
'compare' => 'EXISTS',
],
[
'key' => '_elementor_data',
'compare' => 'EXISTS',
],
[
'key' => '_elementor_data',
'compare' => '!=',
'value' => '[]',
],
],
];
$query = new \WP_Query( $query_args );
if ( empty( $query ) ) {
return [
'files' => [],
'manifest_data' => [],
];
}
$post_type_taxonomies = $this->get_post_type_taxonomies( $post_type );
$manifest_data = [];
$files = [];
foreach ( $query->posts as $post ) {
$document = Plugin::$instance->documents->get( $post->ID );
$terms = ! empty( $post_type_taxonomies ) ? $this->get_post_terms( $post->ID, $post_type_taxonomies ) : [];
$post_manifest_data = [
'title' => $post->post_title,
'excerpt' => $post->post_excerpt,
'doc_type' => $document->get_name(),
'thumbnail' => get_the_post_thumbnail_url( $post ),
'url' => get_permalink( $post ),
'terms' => $terms,
];
if ( $post->ID === $this->page_on_front_id ) {
$post_manifest_data['show_on_front'] = true;
}
$manifest_data[ $post->ID ] = $post_manifest_data;
$files[] = [
'path' => 'content/' . $post_type . '/' . $post->ID,
'data' => $document->get_export_data(),
];
}
return [
'files' => $files,
'manifest_data' => $manifest_data,
];
}
private function get_post_type_taxonomies( $post_type ) {
return get_object_taxonomies( $post_type );
}
private function get_post_terms( $post_id, array $taxonomies ) {
$terms = wp_get_object_terms( $post_id, $taxonomies );
$result = [];
foreach ( $terms as $term ) {
$result[] = [
'term_id' => $term->term_id,
'taxonomy' => $term->taxonomy,
'slug' => $term->slug,
];
}
return $result;
}
private function init_page_on_front_data() {
$show_page_on_front = 'page' === get_option( 'show_on_front' );
if ( $show_page_on_front ) {
$this->page_on_front_id = (int) get_option( 'page_on_front' );
}
}
}

View File

@@ -0,0 +1,27 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Export;
use Elementor\App\Modules\ImportExport\Runners\Runner_Interface;
abstract class Export_Runner_Base implements Runner_Interface {
/**
* By the passed data we should decide if we want to run the export function of the runner or not.
*
* @param array $data
*
* @return bool
*/
abstract public function should_export( array $data );
/**
* Main function of the runner export process.
*
* @param array $data Necessary data for the export process.
*
* @return array{files: array, manifest: array}
* The files that should be part of the kit and the relevant manifest data.
*/
abstract public function export( array $data );
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Export;
class Plugins extends Export_Runner_Base {
public static function get_name(): string {
return 'plugins';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'plugins', $data['include'], true ) &&
is_array( $data['selected_plugins'] )
);
}
public function export( array $data ) {
$manifest_data['plugins'] = $data['selected_plugins'];
return [
'manifest' => [
$manifest_data,
],
'files' => [],
];
}
}

View File

@@ -0,0 +1,102 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Export;
use Elementor\Plugin;
class Site_Settings extends Export_Runner_Base {
public static function get_name(): string {
return 'site-settings';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'settings', $data['include'], true )
);
}
public function export( array $data ) {
$kit = Plugin::$instance->kits_manager->get_active_kit();
$kit_data = $kit->get_export_data();
$kit_tabs = $kit->get_tabs();
$excluded_kit_settings_keys = [
'site_name',
'site_description',
'site_logo',
'site_favicon',
];
foreach ( $excluded_kit_settings_keys as $setting_key ) {
unset( $kit_data['settings'][ $setting_key ] );
}
unset( $kit_tabs['settings-site-identity'] );
$kit_tabs = array_keys( $kit_tabs );
$theme_data = $this->export_theme();
if ( $theme_data ) {
$kit_data['theme'] = $theme_data;
$manifest_data['theme'] = $theme_data;
}
$experiments_data = $this->export_experiments();
if ( $experiments_data ) {
$kit_data['experiments'] = $experiments_data;
$manifest_data['experiments'] = array_keys( $experiments_data );
}
$manifest_data['site-settings'] = $kit_tabs;
return [
'files' => [
'path' => 'site-settings',
'data' => $kit_data,
],
'manifest' => [
$manifest_data,
],
];
}
public function export_theme() {
$theme = wp_get_theme();
if ( empty( $theme ) || empty( $theme->get( 'ThemeURI' ) ) ) {
return null;
}
$theme_data['name'] = $theme->get( 'Name' );
$theme_data['theme_uri'] = $theme->get( 'ThemeURI' );
$theme_data['version'] = $theme->get( 'Version' );
$theme_data['slug'] = $theme->get_stylesheet();
return $theme_data;
}
private function export_experiments() {
$features = Plugin::$instance->experiments->get_features();
if ( empty( $features ) ) {
return null;
}
$experiments_data = [];
foreach ( $features as $feature_name => $feature ) {
$experiments_data[ $feature_name ] = [
'name' => $feature_name,
'title' => $feature['title'],
'state' => $feature['state'],
'default' => $feature['default'],
'release_status' => $feature['release_status'],
];
}
return empty( $experiments_data ) ? null : $experiments_data;
}
}

View File

@@ -0,0 +1,119 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Export;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
class Taxonomies extends Export_Runner_Base {
public static function get_name(): string {
return 'taxonomies';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true )
);
}
public function export( array $data ) {
$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
$selected_custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];
$post_types = array_merge( $wp_builtin_post_types, $selected_custom_post_types );
$export = $this->export_taxonomies( $post_types );
$manifest_data['taxonomies'] = $export['manifest'];
return [
'files' => $export['files'],
'manifest' => [
$manifest_data,
],
];
}
private function export_taxonomies( array $post_types ) {
$files = [];
$manifest = [];
$taxonomies = get_taxonomies();
foreach ( $taxonomies as $taxonomy ) {
$taxonomy_post_types = get_taxonomy( $taxonomy )->object_type;
$intersected_post_types = array_intersect( $taxonomy_post_types, $post_types );
if ( empty( $intersected_post_types ) ) {
continue;
}
$data = $this->export_terms( $taxonomy );
if ( empty( $data ) ) {
continue;
}
foreach ( $intersected_post_types as $post_type ) {
$manifest[ $post_type ][] = $taxonomy;
}
$files[] = [
'path' => 'taxonomies/' . $taxonomy,
'data' => $data,
];
}
return [
'files' => $files,
'manifest' => $manifest,
];
}
private function export_terms( $taxonomy ) {
$terms = get_terms( [
'taxonomy' => (array) $taxonomy,
'hide_empty' => true,
'get' => 'all',
] );
$ordered_terms = $this->order_terms( $terms );
if ( empty( $ordered_terms ) ) {
return [];
}
$data = [];
foreach ( $ordered_terms as $term ) {
$data[] = [
'term_id' => $term->term_id,
'name' => $term->name,
'slug' => $term->slug,
'taxonomy' => $term->taxonomy,
'description' => $term->description,
'parent' => $term->parent,
];
}
return $data;
}
/**
* Put terms in order with no child going before its parent.
*/
private function order_terms( array $terms ) {
$ordered_terms = [];
while ( $term = array_shift( $terms ) ) {
$is_top_level = 0 === $term->parent;
$is_parent_exits = isset( $ordered_terms[ $term->parent ] );
if ( $is_top_level || $is_parent_exits ) {
$ordered_terms[ $term->term_id ] = $term;
} else {
$terms[] = $term;
}
}
return $ordered_terms;
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Export;
use Elementor\Core\Base\Document;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
class Templates extends Export_Runner_Base {
public static function get_name(): string {
return 'templates';
}
public function should_export( array $data ) {
return (
Utils::has_pro() &&
isset( $data['include'] ) &&
in_array( 'templates', $data['include'], true )
);
}
public function export( array $data ) {
$template_types = array_values( Source_Local::get_template_types() );
$query_args = [
'post_type' => Source_Local::CPT,
'post_status' => 'publish',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => Document::TYPE_META_KEY,
'value' => $template_types,
],
],
];
$templates_query = new \WP_Query( $query_args );
$templates_manifest_data = [];
$files = [];
foreach ( $templates_query->posts as $template_post ) {
$template_id = $template_post->ID;
$template_document = Plugin::$instance->documents->get( $template_id );
$templates_manifest_data[ $template_id ] = $template_document->get_export_summary();
$files[] = [
'path' => 'templates/' . $template_id,
'data' => $template_document->get_export_data(),
];
}
$manifest_data['templates'] = $templates_manifest_data;
return [
'files' => $files,
'manifest' => [
$manifest_data,
],
];
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Export;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Core\Utils\ImportExport\WP_Exporter;
class Wp_Content extends Export_Runner_Base {
public static function get_name(): string {
return 'wp-content';
}
public function should_export( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true )
);
}
public function export( array $data ) {
$post_types = ImportExportUtils::get_builtin_wp_post_types();
$custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];
$files = [];
$manifest_data = [];
foreach ( $post_types as $post_type ) {
$export = $this->export_wp_post_type( $post_type );
$files[] = $export['file'];
$manifest_data['wp-content'][ $post_type ] = $export['manifest_data'];
}
foreach ( $custom_post_types as $post_type ) {
$export = $this->export_wp_post_type( $post_type );
$files[] = $export['file'];
$manifest_data['wp-content'][ $post_type ] = $export['manifest_data'];
$post_type_object = get_post_type_object( $post_type );
$manifest_data['custom-post-type-title'][ $post_type ] = [
'name' => $post_type_object->name,
'label' => $post_type_object->label,
];
}
return [
'files' => $files,
'manifest' => [
$manifest_data,
],
];
}
private function export_wp_post_type( $post_type ) {
$wp_exporter = new WP_Exporter( [
'content' => $post_type,
'status' => 'publish',
'limit' => 20,
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
'compare' => 'NOT EXISTS',
],
],
'include_post_featured_image_as_attachment' => true,
] );
$export_result = $wp_exporter->run();
return [
'file' => [
'path' => 'wp-content/' . $post_type . '/' . $post_type . '.xml',
'data' => $export_result['xml'],
],
'manifest_data' => $export_result['ids'],
];
}
}

View File

@@ -0,0 +1,158 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Import;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;
class Elementor_Content extends Import_Runner_Base {
private $show_page_on_front;
private $page_on_front_id;
private $import_session_id;
public function __construct() {
$this->init_page_on_front_data();
}
public static function get_name(): string {
return 'elementor-content';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true ) &&
! empty( $data['manifest']['content'] ) &&
! empty( $data['extracted_directory_path'] )
);
}
public function import( array $data, array $imported_data ) {
$result['content'] = [];
$this->import_session_id = $data['session_id'];
$elementor_post_types = ImportExportUtils::get_elementor_post_types();
foreach ( $elementor_post_types as $post_type ) {
if ( empty( $data['manifest']['content'][ $post_type ] ) ) {
continue;
}
$posts_settings = $data['manifest']['content'][ $post_type ];
$path = $data['extracted_directory_path'] . 'content/' . $post_type . '/';
$imported_terms = ! empty( $imported_data['taxonomies'] )
? ImportExportUtils::map_old_new_term_ids( $imported_data )
: [];
$result['content'][ $post_type ] = $this->import_elementor_post_type( $posts_settings, $path, $post_type, $imported_terms );
}
return $result;
}
private function import_elementor_post_type( array $posts_settings, $path, $post_type, array $imported_terms ) {
$result = [
'succeed' => [],
'failed' => [],
];
foreach ( $posts_settings as $id => $post_settings ) {
try {
$post_data = ImportExportUtils::read_json_file( $path . $id );
$import = $this->import_post( $post_settings, $post_data, $post_type, $imported_terms );
if ( is_wp_error( $import ) ) {
$result['failed'][ $id ] = $import->get_error_message();
continue;
}
$result['succeed'][ $id ] = $import;
} catch ( \Exception $error ) {
$result['failed'][ $id ] = $error->getMessage();
}
}
return $result;
}
private function import_post( array $post_settings, array $post_data, $post_type, array $imported_terms ) {
$post_attributes = [
'post_title' => $post_settings['title'],
'post_type' => $post_type,
'post_status' => 'publish',
];
if ( ! empty( $post_settings['excerpt'] ) ) {
$post_attributes['post_excerpt'] = $post_settings['excerpt'];
}
$new_document = Plugin::$instance->documents->create(
$post_settings['doc_type'],
$post_attributes
);
if ( is_wp_error( $new_document ) ) {
throw new \Exception( esc_html( $new_document->get_error_message() ) );
}
$post_data['import_settings'] = $post_settings;
$new_attachment_callback = function( $attachment_id ) {
$this->set_session_post_meta( $attachment_id, $this->import_session_id );
};
add_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$new_document->import( $post_data );
remove_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$new_post_id = $new_document->get_main_id();
if ( ! empty( $post_settings['terms'] ) ) {
$this->set_post_terms( $new_post_id, $post_settings['terms'], $imported_terms );
}
if ( ! empty( $post_settings['show_on_front'] ) ) {
$this->set_page_on_front( $new_post_id );
}
$this->set_session_post_meta( $new_post_id, $this->import_session_id );
return $new_post_id;
}
private function set_post_terms( $post_id, array $terms, array $imported_terms ) {
foreach ( $terms as $term ) {
if ( ! isset( $imported_terms[ $term['term_id'] ] ) ) {
continue;
}
wp_set_post_terms( $post_id, [ $imported_terms[ $term['term_id'] ] ], $term['taxonomy'], false );
}
}
private function init_page_on_front_data() {
$this->show_page_on_front = 'page' === get_option( 'show_on_front' );
if ( $this->show_page_on_front ) {
$this->page_on_front_id = (int) get_option( 'page_on_front' );
}
}
private function set_page_on_front( $page_id ) {
update_option( 'page_on_front', $page_id );
if ( ! $this->show_page_on_front ) {
update_option( 'show_on_front', 'page' );
}
}
public function get_import_session_metadata(): array {
return [
'page_on_front' => $this->page_on_front_id ?? 0,
];
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Import;
use Elementor\App\Modules\ImportExport\Runners\Runner_Interface;
abstract class Import_Runner_Base implements Runner_Interface {
/**
* By the passed data we should decide if we want to run the import function of the runner or not.
*
* @param array $data
*
* @return bool
*/
abstract public function should_import( array $data );
/**
* Main function of the runner import process.
*
* @param array $data Necessary data for the import process.
* @param array $imported_data Data that already imported by previously runners.
*
* @return array The result of the import process
*/
abstract public function import( array $data, array $imported_data );
public function get_import_session_metadata(): array {
return [];
}
public function set_session_post_meta( $post_id, $meta_value ) {
update_post_meta( $post_id, static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID, $meta_value );
}
public function set_session_term_meta( $term_id, $meta_value ) {
update_term_meta( $term_id, static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID, $meta_value );
}
}

View File

@@ -0,0 +1,70 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Import;
use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Plugins_Manager;
use Elementor\Core\Utils\Str;
class Plugins extends Import_Runner_Base {
/**
* @var Plugins_Manager
*/
private $plugins_manager;
public function __construct( $plugins_manager = null ) {
if ( $plugins_manager ) {
$this->plugins_manager = $plugins_manager;
} else {
$this->plugins_manager = new Plugins_Manager();
}
}
public static function get_name(): string {
return 'plugins';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'plugins', $data['include'], true ) &&
! empty( $data['manifest']['plugins'] ) &&
! empty( $data['selected_plugins'] )
);
}
public function import( array $data, array $imported_data ) {
$plugins = $data['selected_plugins'];
$plugins_collection = ( new Collection( $plugins ) )
->map( function ( $item ) {
if ( ! Str::ends_with( $item['plugin'], '.php' ) ) {
$item['plugin'] .= '.php';
}
return $item;
} );
$slugs = $plugins_collection
->map( function ( $item ) {
return $item['plugin'];
} )
->all();
$installed = $this->plugins_manager->install( $slugs );
$activated = $this->plugins_manager->activate( $installed['succeeded'] );
$ordered_activated_plugins = $plugins_collection
->filter( function ( $item ) use ( $activated ) {
return in_array( $item['plugin'], $activated['succeeded'], true );
} )
->map( function ( $item ) {
return $item['name'];
} )
->all();
$result['plugins'] = $ordered_activated_plugins;
return $result;
}
}

View File

@@ -0,0 +1,272 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Import;
use Elementor\Plugin;
use Elementor\Core\Settings\Page\Manager as PageManager;
use Elementor\App\Modules\ImportExport\Utils;
use Elementor\Core\Experiments\Manager as ExperimentsManager;
class Site_Settings extends Import_Runner_Base {
/**
* @var int
*/
private $previous_kit_id;
/**
* @var int
*/
private $active_kit_id;
/**
* @var int
*/
private $imported_kit_id;
/**
* @var string|null
*/
private ?string $installed_theme = null;
/**
* @var string|null
*/
private ?string $activated_theme = null;
/**
* @var array|null
*/
private ?array $previous_active_theme = null;
/**
* @var array
*/
private $previous_experiments = [];
/**
* @var array
*/
private $imported_experiments = [];
public function get_theme_upgrader(): \Theme_Upgrader {
if ( ! class_exists( '\Theme_Upgrader' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
if ( ! class_exists( '\WP_Ajax_Upgrader_Skin' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
}
return new \Theme_Upgrader( new \WP_Ajax_Upgrader_Skin() );
}
public static function get_name(): string {
return 'site-settings';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'settings', $data['include'], true ) &&
! empty( $data['site_settings']['settings'] )
);
}
public function import( array $data, array $imported_data ) {
$new_site_settings = $data['site_settings']['settings'];
$title = $data['manifest']['title'] ?? 'Imported Kit';
$active_kit = Plugin::$instance->kits_manager->get_active_kit();
$this->active_kit_id = (int) $active_kit->get_id();
$this->previous_kit_id = (int) Plugin::$instance->kits_manager->get_previous_id();
$result = [];
$old_settings = $active_kit->get_meta( PageManager::META_KEY );
if ( ! $old_settings ) {
$old_settings = [];
}
if ( ! empty( $old_settings['custom_colors'] ) ) {
$new_site_settings['custom_colors'] = array_merge( $old_settings['custom_colors'], $new_site_settings['custom_colors'] );
}
if ( ! empty( $old_settings['custom_typography'] ) ) {
$new_site_settings['custom_typography'] = array_merge( $old_settings['custom_typography'], $new_site_settings['custom_typography'] );
}
if ( ! empty( $new_site_settings['space_between_widgets'] ) ) {
$new_site_settings['space_between_widgets'] = Utils::update_space_between_widgets_values( $new_site_settings['space_between_widgets'] );
}
$new_site_settings = array_replace_recursive( $old_settings, $new_site_settings );
$new_kit = Plugin::$instance->kits_manager->create_new_kit( $title, $new_site_settings );
$this->imported_kit_id = (int) $new_kit;
$result['site-settings'] = (bool) $new_kit;
$import_theme_result = $this->import_theme( $data );
if ( ! empty( $import_theme_result ) ) {
$result['theme'] = $import_theme_result;
}
$this->import_experiments( $data );
if ( ! empty( $this->imported_experiments ) ) {
$result['experiments'] = $this->imported_experiments;
}
return $result;
}
protected function install_theme( $slug, $version ) {
$download_url = "https://downloads.wordpress.org/theme/{$slug}.{$version}.zip";
return $this->get_theme_upgrader()->install( $download_url );
}
protected function activate_theme( $slug ) {
switch_theme( $slug );
}
public function import_theme( array $data ) {
if ( empty( $data['site_settings']['theme'] ) ) {
return null;
}
$theme = $data['site_settings']['theme'];
$theme_slug = $theme['slug'];
$theme_name = $theme['name'];
$current_theme = wp_get_theme();
$this->previous_active_theme = [];
$this->previous_active_theme['slug'] = $current_theme->get_stylesheet();
$this->previous_active_theme['version'] = $current_theme->get( 'Version' );
if ( $current_theme->get_stylesheet() === $theme_slug ) {
$result['succeed'][ $theme_slug ] = sprintf(
/* translators: %s: Theme name. */
__( 'Theme: %1$s is already used', 'elementor' ),
$theme_name
);
return $result;
}
try {
if ( wp_get_theme( $theme_slug )->exists() ) {
$this->activate_theme( $theme_slug );
$this->activated_theme = $theme_slug;
$result['succeed'][ $theme_slug ] = sprintf(
/* translators: %s: Theme name. */
__( 'Theme: %1$s has already been installed and activated', 'elementor' ),
$theme_name
);
return $result;
}
$import = $this->install_theme( $theme_slug, $theme['version'] );
if ( is_wp_error( $import ) ) {
$result['failed'][ $theme_slug ] = sprintf(
/* translators: %s: Theme name. */
__( 'Failed to install theme: %1$s', 'elementor' ),
$theme_name
);
return $result;
}
$result['succeed'][ $theme_slug ] = sprintf(
/* translators: %s: Theme name. */
__( 'Theme: %1$s has been successfully installed', 'elementor' ),
$theme_name
);
$this->installed_theme = $theme_slug;
$this->activate_theme( $theme_slug );
} catch ( \Exception $error ) {
$result['failed'][ $theme_slug ] = $error->getMessage();
}
return $result;
}
private function import_experiments( array $data ) {
if ( empty( $data['site_settings']['experiments'] ) ) {
return null;
}
$experiments_data = $data['site_settings']['experiments'];
$experiments_manager = Plugin::$instance->experiments;
$current_features = $experiments_manager->get_features();
$this->save_previous_experiments_state( $current_features );
foreach ( $experiments_data as $feature_name => $feature_data ) {
if ( ! isset( $current_features[ $feature_name ] ) ) {
continue;
}
$current_feature = $current_features[ $feature_name ];
$current_feature_state = $current_feature['state'];
$new_state = $feature_data['state'];
if ( $current_feature_state === $new_state ) {
continue;
}
if ( ! in_array( $new_state, [ ExperimentsManager::STATE_DEFAULT, ExperimentsManager::STATE_ACTIVE, ExperimentsManager::STATE_ACTIVE ], true ) ) {
continue;
}
$option_key = $experiments_manager->get_feature_option_key( $feature_name );
if ( 'default' === $new_state ) {
delete_option( $option_key );
} else {
update_option( $option_key, $new_state );
}
$this->imported_experiments[ $feature_name ] = $feature_data;
}
}
private function save_previous_experiments_state( array $current_features ) {
$experiments_manager = Plugin::$instance->experiments;
foreach ( $current_features as $feature_name => $feature ) {
if ( ! $feature['mutable'] ) {
continue;
}
$option_key = $experiments_manager->get_feature_option_key( $feature_name );
$saved_state = get_option( $option_key );
$this->previous_experiments[ $feature_name ] = [
'name' => $feature_name,
'title' => $feature['title'],
'state' => empty( $saved_state ) ? 'default' : $saved_state,
'default' => $feature['default'],
'release_status' => $feature['release_status'],
];
}
}
public function get_import_session_metadata(): array {
return [
'previous_kit_id' => $this->previous_kit_id,
'active_kit_id' => $this->active_kit_id,
'imported_kit_id' => $this->imported_kit_id,
'installed_theme' => $this->installed_theme,
'activated_theme' => $this->activated_theme,
'previous_active_theme' => $this->previous_active_theme,
'previous_experiments' => $this->previous_experiments,
'imported_experiments' => $this->imported_experiments,
];
}
}

View File

@@ -0,0 +1,143 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Import;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
class Taxonomies extends Import_Runner_Base {
private $import_session_id;
public static function get_name(): string {
return 'taxonomies';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true ) &&
! empty( $data['extracted_directory_path'] ) &&
! empty( $data['manifest']['taxonomies'] )
);
}
public function import( array $data, array $imported_data ) {
$path = $data['extracted_directory_path'] . 'taxonomies/';
$this->import_session_id = $data['session_id'];
$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
$selected_custom_post_types = isset( $data['selected_custom_post_types'] ) ? $data['selected_custom_post_types'] : [];
$post_types = array_merge( $wp_builtin_post_types, $selected_custom_post_types );
$result = [];
foreach ( $post_types as $post_type ) {
if ( empty( $data['manifest']['taxonomies'][ $post_type ] ) ) {
continue;
}
$result['taxonomies'][ $post_type ] = $this->import_taxonomies( $data['manifest']['taxonomies'][ $post_type ], $path );
}
return $result;
}
private function import_taxonomies( array $taxonomies, $path ) {
$result = [];
$imported_taxonomies = [];
foreach ( $taxonomies as $taxonomy ) {
if ( ! taxonomy_exists( $taxonomy ) ) {
continue;
}
if ( ! empty( $imported_taxonomies[ $taxonomy ] ) ) {
$result[ $taxonomy ] = $imported_taxonomies[ $taxonomy ];
continue;
}
$taxonomy_data = ImportExportUtils::read_json_file( $path . $taxonomy );
if ( empty( $taxonomy_data ) ) {
continue;
}
$import = $this->import_taxonomy( $taxonomy_data );
$result[ $taxonomy ] = $import;
$imported_taxonomies[ $taxonomy ] = $import;
}
return $result;
}
private function import_taxonomy( array $taxonomy_data ) {
$terms = [];
foreach ( $taxonomy_data as $term ) {
$old_slug = $term['slug'];
$existing_term = term_exists( $term['slug'], $term['taxonomy'] );
if ( $existing_term ) {
if ( 'nav_menu' === $term['taxonomy'] ) {
$term = $this->handle_duplicated_nav_menu_term( $term );
} else {
$terms[] = [
'old_id' => (int) $term['term_id'],
'new_id' => (int) $existing_term['term_id'],
'old_slug' => $old_slug,
'new_slug' => $term['slug'],
];
continue;
}
}
$parent = $this->get_term_parent( $term, $terms );
$args = [
'slug' => $term['slug'],
'description' => wp_slash( $term['description'] ),
'parent' => (int) $parent,
];
$new_term = wp_insert_term( wp_slash( $term['name'] ), $term['taxonomy'], $args );
if ( ! is_wp_error( $new_term ) ) {
$this->set_session_term_meta( (int) $new_term['term_id'], $this->import_session_id );
$terms[] = [
'old_id' => $term['term_id'],
'new_id' => (int) $new_term['term_id'],
'old_slug' => $old_slug,
'new_slug' => $term['slug'],
];
}
}
return $terms;
}
private function handle_duplicated_nav_menu_term( $term ) {
do {
$term['slug'] = $term['slug'] . '-duplicate';
$term['name'] = $term['name'] . ' duplicate';
} while ( term_exists( $term['slug'], 'nav_menu' ) );
return $term;
}
private function get_term_parent( $term, array $imported_terms ) {
$parent = $term['parent'];
if ( 0 !== $parent && ! empty( $imported_terms ) ) {
foreach ( $imported_terms as $imported_term ) {
if ( $parent === $imported_term['old_id'] ) {
$parent_term = term_exists( $imported_term['new_id'], $term['taxonomy'] );
break;
}
}
if ( isset( $parent_term['term_id'] ) ) {
return $parent_term['term_id'];
}
}
return 0;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Import;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils;
class Templates extends Import_Runner_Base {
private $import_session_id;
public static function get_name(): string {
return 'templates';
}
public function should_import( array $data ) {
return (
Utils::has_pro() &&
isset( $data['include'] ) &&
in_array( 'templates', $data['include'], true ) &&
! empty( $data['extracted_directory_path'] ) &&
! empty( $data['manifest']['templates'] )
);
}
public function import( array $data, array $imported_data ) {
$this->import_session_id = $data['session_id'];
$path = $data['extracted_directory_path'] . 'templates/';
$templates = $data['manifest']['templates'];
$result['templates'] = [
'succeed' => [],
'failed' => [],
];
foreach ( $templates as $id => $template_settings ) {
try {
$template_data = ImportExportUtils::read_json_file( $path . $id );
$import = $this->import_template( $id, $template_settings, $template_data );
$result['templates']['succeed'][ $id ] = $import;
} catch ( \Exception $error ) {
$result['templates']['failed'][ $id ] = $error->getMessage();
}
}
return $result;
}
private function import_template( $id, array $template_settings, array $template_data ) {
$doc_type = $template_settings['doc_type'];
$new_document = Plugin::$instance->documents->create(
$doc_type,
[
'post_title' => $template_settings['title'],
'post_type' => Source_Local::CPT,
'post_status' => 'publish',
]
);
if ( is_wp_error( $new_document ) ) {
throw new \Exception( esc_html( $new_document->get_error_message() ) );
}
$template_data['import_settings'] = $template_settings;
$template_data['id'] = $id;
$new_attachment_callback = function( $attachment_id ) {
$this->set_session_post_meta( $attachment_id, $this->import_session_id );
};
add_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$new_document->import( $template_data );
remove_filter( 'elementor/template_library/import_images/new_attachment', $new_attachment_callback );
$document_id = $new_document->get_main_id();
$this->set_session_post_meta( $document_id, $this->import_session_id );
return $document_id;
}
}

View File

@@ -0,0 +1,124 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Import;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Core\Utils\ImportExport\WP_Import;
class Wp_Content extends Import_Runner_Base {
private $import_session_id;
/**
* @var array
*/
private $selected_custom_post_types = [];
public static function get_name(): string {
return 'wp-content';
}
public function should_import( array $data ) {
return (
isset( $data['include'] ) &&
in_array( 'content', $data['include'], true ) &&
! empty( $data['extracted_directory_path'] ) &&
! empty( $data['manifest']['wp-content'] )
);
}
public function import( array $data, array $imported_data ) {
$this->import_session_id = $data['session_id'];
$path = $data['extracted_directory_path'] . 'wp-content/';
$post_types = $this->filter_post_types( $data['selected_custom_post_types'] );
$taxonomies = $imported_data['taxonomies'] ?? [];
$imported_terms = ImportExportUtils::map_old_new_term_ids( $imported_data );
$result['wp-content'] = [];
foreach ( $post_types as $post_type ) {
$import = $this->import_wp_post_type(
$path,
$post_type,
$imported_data,
$taxonomies,
$imported_terms
);
if ( empty( $import ) ) {
continue;
}
$result['wp-content'][ $post_type ] = $import;
$imported_data = array_merge( $imported_data, $result );
}
return $result;
}
private function import_wp_post_type( $path, $post_type, array $imported_data, array $taxonomies, array $imported_terms ) {
$args = [
'fetch_attachments' => true,
'posts' => ImportExportUtils::map_old_new_post_ids( $imported_data ),
'terms' => $imported_terms,
'taxonomies' => ! empty( $taxonomies[ $post_type ] ) ? $taxonomies[ $post_type ] : [],
'posts_meta' => [
static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID => $this->import_session_id,
],
'terms_meta' => [
static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID => $this->import_session_id,
],
];
$file = $path . $post_type . '/' . $post_type . '.xml';
if ( ! file_exists( $file ) ) {
return [];
}
$wp_importer = new WP_Import( $file, $args );
$result = $wp_importer->run();
return $result['summary']['posts'];
}
private function filter_post_types( $selected_custom_post_types = [] ) {
$wp_builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
foreach ( $selected_custom_post_types as $custom_post_type ) {
if ( post_type_exists( $custom_post_type ) ) {
$this->selected_custom_post_types[] = $custom_post_type;
}
}
$post_types = array_merge( $wp_builtin_post_types, $this->selected_custom_post_types );
$post_types = $this->force_element_to_be_last_by_value( $post_types, 'nav_menu_item' );
return $post_types;
}
public function get_import_session_metadata(): array {
return [
'custom_post_types' => $this->selected_custom_post_types,
];
}
/**
* @param $array array The array we want to relocate his element.
* @param $element mixed The value of the element in the array we want to shift to end of the array.
* @return mixed
*/
private function force_element_to_be_last_by_value( array $array, $element ) {
$index = array_search( $element, $array, true );
if ( false !== $index ) {
unset( $array[ $index ] );
$array[] = $element;
}
return $array;
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Revert;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
use Elementor\Plugin;
class Elementor_Content extends Revert_Runner_Base {
private $show_page_on_front;
private $page_on_front_id;
public function __construct() {
$this->init_page_on_front_data();
}
public static function get_name(): string {
return 'elementor-content';
}
public function should_revert( array $data ): bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
$elementor_post_types = ImportExportUtils::get_elementor_post_types();
$query_args = [
'post_type' => $elementor_post_types,
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
'compare' => 'EXISTS',
],
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
];
$query = new \WP_Query( $query_args );
foreach ( $query->posts as $post ) {
$post_type_document = Plugin::$instance->documents->get( $post->ID );
$post_type_document->delete();
// Deleting the post will reset the show_on_front option. We need to set it to false,
// so we can set it back to what it was.
if ( $post->ID === $this->page_on_front_id ) {
$this->show_page_on_front = false;
}
}
$this->restore_page_on_front( $data );
}
private function init_page_on_front_data() {
$this->show_page_on_front = 'page' === get_option( 'show_on_front' );
if ( $this->show_page_on_front ) {
$this->page_on_front_id = (int) get_option( 'page_on_front' );
}
}
private function restore_page_on_front( $data ) {
if ( empty( $data['runners'][ static::get_name() ]['page_on_front'] ) ) {
return;
}
$page_on_front = $data['runners'][ static::get_name() ]['page_on_front'];
$document = Plugin::$instance->documents->get( $page_on_front );
if ( ! $document ) {
return;
}
$this->set_page_on_front( $document->get_main_id() );
}
private function set_page_on_front( $page_id ) {
update_option( 'page_on_front', $page_id );
if ( ! $this->show_page_on_front ) {
update_option( 'show_on_front', 'page' );
}
}
}

View File

@@ -0,0 +1,16 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Revert;
class Plugins extends Revert_Runner_Base {
public static function get_name(): string {
return 'plugins';
}
public function should_revert( array $data ): bool {
return false;
}
public function revert( array $data ) {}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Revert;
use Elementor\App\Modules\ImportExport\Runners\Runner_Interface;
abstract class Revert_Runner_Base implements Runner_Interface {
/**
* By the passed data we should decide if we want to run the revert function of the runner or not.
*
* @param array $data
*
* @return bool
*/
abstract public function should_revert( array $data ): bool;
/**
* Main function of the runner revert process.
*
* @param array $data Necessary data for the revert process.
*/
abstract public function revert( array $data );
}

View File

@@ -0,0 +1,130 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Revert;
use Elementor\Plugin;
use Elementor\Core\Experiments\Manager as ExperimentsManager;
class Site_Settings extends Revert_Runner_Base {
public static function get_name(): string {
return 'site-settings';
}
public function should_revert( array $data ): bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
Plugin::$instance->kits_manager->revert(
$data['runners'][ static::get_name() ]['imported_kit_id'],
$data['runners'][ static::get_name() ]['active_kit_id'],
$data['runners'][ static::get_name() ]['previous_kit_id']
);
$this->revert_theme( $data );
$this->revert_experiments( $data );
}
public function get_theme_upgrader(): \Theme_Upgrader {
if ( ! class_exists( '\Theme_Upgrader' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
if ( ! class_exists( '\WP_Ajax_Upgrader_Skin' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-ajax-upgrader-skin.php';
}
return new \Theme_Upgrader( new \WP_Ajax_Upgrader_Skin() );
}
protected function revert_theme( $data ) {
$installed_theme = $data['runners'][ static::get_name() ]['installed_theme'];
$activated_theme = $data['runners'][ static::get_name() ]['activated_theme'];
$previous_active_theme = $data['runners'][ static::get_name() ]['previous_active_theme'];
if ( empty( $installed_theme ) && empty( $activated_theme ) ) {
// no need to remove a theme as it was used before import
return;
}
if ( ! empty( $activated_theme ) ) {
$previous_theme = wp_get_theme( $previous_active_theme['slug'] );
// no need to remove imported theme as it existed before import
$this->activate_previous_theme( $previous_active_theme );
return;
}
if ( ! empty( $installed_theme ) ) {
$this->activate_previous_theme( $previous_active_theme );
$this->delete_theme( $installed_theme );
}
}
protected function should_delete_theme( $theme_slug ): bool {
$current_theme = wp_get_theme();
return $theme_slug !== $current_theme->get_stylesheet() && wp_get_theme( $theme_slug )->exists();
}
protected function delete_theme( $theme_slug ): bool {
return delete_theme( $theme_slug );
}
protected function activate_previous_theme( $previous_active_theme ) {
if ( ! $previous_active_theme ) {
return;
}
$theme = wp_get_theme( $previous_active_theme['slug'] );
if ( $theme->exists() ) {
switch_theme( $theme->get_stylesheet() );
return;
}
$download_url = "https://downloads.wordpress.org/theme/{$previous_active_theme['slug']}.{$previous_active_theme['version']}.zip";
$install = $this->get_theme_upgrader()->install( $download_url );
if ( ! $install || is_wp_error( $install ) ) {
return;
}
switch_theme( $previous_active_theme['slug'] );
}
protected function revert_experiments( array $data ) {
$runner_data = $data['runners'][ static::get_name() ];
$previous_experiments = $runner_data['previous_experiments'] ?? [];
if ( empty( $previous_experiments ) ) {
return;
}
$experiments_manager = Plugin::$instance->experiments;
$current_features = $experiments_manager->get_features();
foreach ( $previous_experiments as $feature_name => $feature_data ) {
if ( ! isset( $current_features[ $feature_name ] ) ) {
continue;
}
if ( ! array_key_exists( $feature_name, $previous_experiments ) ) {
continue;
}
$option_key = $experiments_manager->get_feature_option_key( $feature_name );
$previous_state = $feature_data['state'];
if ( ExperimentsManager::STATE_DEFAULT === $previous_state ) {
delete_option( $option_key );
} else {
update_option( $option_key, $previous_state );
}
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Revert;
class Taxonomies extends Revert_Runner_Base {
public static function get_name(): string {
return 'taxonomies';
}
public function should_revert( array $data ): bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
$taxonomies = get_taxonomies();
$terms = get_terms( [
'taxonomy' => $taxonomies,
'hide_empty' => false,
'get' => 'all',
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
] );
foreach ( $terms as $term ) {
wp_delete_term( $term->term_id, $term->taxonomy );
}
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Revert;
class Templates extends Revert_Runner_Base {
/**
* The implement of this runner is part of the Pro plugin.
*/
public static function get_name(): string {
return 'templates';
}
public function should_revert( array $data ): bool {
return false;
}
public function revert( array $data ) { }
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners\Revert;
use Elementor\App\Modules\ImportExport\Utils as ImportExportUtils;
class Wp_Content extends Revert_Runner_Base {
public static function get_name(): string {
return 'wp-content';
}
public function should_revert( array $data ): bool {
return (
isset( $data['runners'] ) &&
array_key_exists( static::get_name(), $data['runners'] )
);
}
public function revert( array $data ) {
$builtin_post_types = ImportExportUtils::get_builtin_wp_post_types();
$custom_post_types = $data['runners']['wp-content']['custom_post_types'] ?? [];
$post_types = array_merge( $builtin_post_types, $custom_post_types );
$query_args = [
'post_type' => $post_types,
'post_status' => 'any',
'posts_per_page' => -1,
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_EDIT_MODE,
'compare' => 'NOT EXISTS',
],
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
];
$query = new \WP_Query( $query_args );
foreach ( $query->posts as $post ) {
wp_delete_post( $post->ID, true );
}
/**
* Revert the nav menu terms.
* BC: The nav menu in new kits will be imported as part of the taxonomies, but old kits
* importing the nav menu terms as part from the wp-content import.
*/
$this->revert_nav_menus( $data );
}
private function revert_nav_menus( array $data ) {
$terms = get_terms( [
'taxonomy' => 'nav_menu',
'hide_empty' => false,
'get' => 'all',
'meta_query' => [
[
'key' => static::META_KEY_ELEMENTOR_IMPORT_SESSION_ID,
'value' => $data['session_id'],
],
],
] );
foreach ( $terms as $term ) {
wp_delete_term( $term->term_id, $term->taxonomy );
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Elementor\App\Modules\ImportExport\Runners;
use Elementor\App\Modules\ImportExport\Module;
interface Runner_Interface {
const META_KEY_ELEMENTOR_IMPORT_SESSION_ID = Module::META_KEY_ELEMENTOR_IMPORT_SESSION_ID;
const META_KEY_ELEMENTOR_EDIT_MODE = Module::META_KEY_ELEMENTOR_EDIT_MODE;
/**
* Get the name of the runners, used to identify the runner.
* The name should be unique, unless you want to run over existing runner.
*
* @return string
*/
public static function get_name(): string;
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Elementor\App\Modules\ImportExport;
use Elementor\App\Modules\ImportExport\Processes\Revert;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Usage {
/**
* Register hooks.
*
* @return void
*/
public function register() {
add_filter( 'elementor/tracker/send_tracking_data_params', function ( array $params ) {
$params['usages']['import_export']['revert'] = $this->get_revert_usage_data();
return $params;
} );
}
/**
* Get the Revert usage data.
*
* @return array
*/
private function get_revert_usage_data() {
$revert_sessions = ( new Revert() )->get_revert_sessions();
$data = [];
foreach ( $revert_sessions as $revert_session ) {
$data[] = [
'kit_name' => $revert_session['kit_name'],
'source' => $revert_session['source'],
'revert_timestamp' => (int) $revert_session['revert_timestamp'],
'total_time' => ( (int) $revert_session['revert_timestamp'] - (int) $revert_session['import_timestamp'] ),
];
}
return $data;
}
}

View File

@@ -0,0 +1,140 @@
<?php
namespace Elementor\App\Modules\ImportExport;
use Elementor\Core\Utils\Str;
use Elementor\Modules\LandingPages\Module as Landing_Pages_Module;
use Elementor\Modules\FloatingButtons\Module as Floating_Buttons_Module;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Utils as ElementorUtils;
class Utils {
public static function read_json_file( $path ) {
if ( ! Str::ends_with( $path, '.json' ) ) {
$path .= '.json';
}
$file_content = ElementorUtils::file_get_contents( $path, true );
return $file_content ? json_decode( $file_content, true ) : [];
}
public static function map_old_new_post_ids( array $imported_data ) {
$result = [];
$result += $imported_data['templates']['succeed'] ?? [];
if ( isset( $imported_data['content'] ) ) {
foreach ( $imported_data['content'] as $post_type ) {
$result += $post_type['succeed'] ?? [];
}
}
if ( isset( $imported_data['wp-content'] ) ) {
foreach ( $imported_data['wp-content'] as $post_type ) {
$result += $post_type['succeed'] ?? [];
}
}
return $result;
}
public static function map_old_new_term_ids( array $imported_data ) {
$result = [];
if ( ! isset( $imported_data['taxonomies'] ) ) {
return $result;
}
foreach ( $imported_data['taxonomies'] as $post_type_taxonomies ) {
foreach ( $post_type_taxonomies as $taxonomy ) {
foreach ( $taxonomy as $term ) {
$result[ $term['old_id'] ] = $term['new_id'];
}
}
}
return $result;
}
public static function get_elementor_post_types() {
$elementor_post_types = get_post_types_by_support( 'elementor' );
return array_filter( $elementor_post_types, function ( $value ) {
// Templates are handled in a separate process.
return 'elementor_library' !== $value;
} );
}
public static function get_builtin_wp_post_types() {
return [ 'post', 'page', 'nav_menu_item' ];
}
public static function get_registered_cpt_names() {
$post_types = get_post_types( [
'public' => true,
'can_export' => true,
'_builtin' => false,
] );
unset(
$post_types[ Landing_Pages_Module::CPT ],
$post_types[ Source_Local::CPT ],
$post_types[ Floating_Buttons_Module::CPT_FLOATING_BUTTONS ]
);
return array_keys( $post_types );
}
/**
* Transform a string name to title format.
*
* @param $name
*
* @return string
*/
public static function transform_name_to_title( $name ): string {
if ( empty( $name ) ) {
return '';
}
$title = str_replace( [ '-', '_' ], ' ', $name );
return ucwords( $title );
}
public static function get_import_sessions( $should_run_cleanup = false ) {
$import_sessions = get_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, [] );
if ( $should_run_cleanup ) {
foreach ( $import_sessions as $session_id => $import_session ) {
if ( ! isset( $import_session['runners'] ) && isset( $import_session['instance_data'] ) ) {
$import_sessions[ $session_id ]['runners'] = $import_session['instance_data']['runners_import_metadata'] ?? [];
unset( $import_sessions[ $session_id ]['instance_data'] );
}
}
update_option( Module::OPTION_KEY_ELEMENTOR_IMPORT_SESSIONS, $import_sessions );
}
return $import_sessions;
}
public static function update_space_between_widgets_values( $space_between_widgets ) {
$setting_exist = isset( $space_between_widgets['size'] );
$already_processed = isset( $space_between_widgets['column'] );
if ( ! $setting_exist || $already_processed ) {
return $space_between_widgets;
}
$size = strval( $space_between_widgets['size'] );
$space_between_widgets['column'] = $size;
$space_between_widgets['row'] = $size;
$space_between_widgets['isLinked'] = true;
return $space_between_widgets;
}
}

View File

@@ -0,0 +1,280 @@
<?php
namespace Elementor\App\Modules\ImportExport;
use Elementor\Core\Utils\Collection;
use Elementor\Core\Utils\Plugins_Manager;
use Elementor\Plugin;
use Elementor\App\Modules\KitLibrary\Connect\Kit_Library;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Wp_Cli extends \WP_CLI_Command {
const AVAILABLE_SETTINGS = [ 'include', 'overrideConditions', 'selectedCustomPostTypes', 'plugins' ];
/**
* Export a Kit
*
* [--include]
* Which type of content to include. Possible values are 'content', 'templates', 'site-settings'.
* if this parameter won't be specified, All data types will be included.
*
* ## EXAMPLES
*
* 1. wp elementor kit export path/to/export-file-name.zip
* - This will export all site data to the specified file name.
*
* 2. wp elementor kit export path/to/export-file-name.zip --include=kit-settings,content
* - This will export only site settings and content.
*
* @param array $args
* @param array $assoc_args
*/
public function export( $args, $assoc_args ) {
if ( empty( $args[0] ) ) {
\WP_CLI::error( 'Please specify a file name' );
}
\WP_CLI::line( 'Kit export started.' );
$export_settings = [];
foreach ( $assoc_args as $key => $value ) {
if ( ! in_array( $key, static::AVAILABLE_SETTINGS, true ) ) {
continue;
}
$export_settings[ $key ] = explode( ',', $value );
}
try {
/**
* Running the export process through the import-export module so the export property in the module will be available to use.
*
* @type Module $import_export_module
*/
$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
$result = $import_export_module->export_kit( $export_settings );
rename( $result['file_name'], $args[0] );
} catch ( \Error $error ) {
\WP_CLI::error( $error->getMessage() );
}
\WP_CLI::success( 'Kit exported successfully.' );
}
/**
* Import a Kit
*
* [--include]
* Which type of content to include. Possible values are 'content', 'templates', 'site-settings'.
* if this parameter won't be specified, All data types will be included.
*
* [--overrideConditions]
* Templates ids to override conditions for.
*
* [--sourceType]
* Which source type is used in the current session. Available values are 'local', 'remote', 'library'.
* The default value is 'local'
*
* ## EXAMPLES
*
* 1. wp elementor kit import path/to/elementor-kit.zip
* - This will import the whole kit file content.
*
* 2. wp elementor kit import path/to/elementor-kit.zip --include=site-settings,content
* - This will import only site settings and content.
*
* 3. wp elementor kit import path/to/elementor-kit.zip --overrideConditions=3478,4520
* - This will import all content and will override conditions for the given template ids.
*
* 4. wp elementor kit import path/to/elementor-kit.zip --unfilteredFilesUpload=enable
* - This will allow the import process to import unfiltered files.
*
* @param array $args
* @param array $assoc_args
*/
public function import( array $args, array $assoc_args ) {
if ( ! current_user_can( 'manage_options' ) ) {
\WP_CLI::error( 'You must run this command as an admin user' );
}
if ( empty( $args[0] ) ) {
\WP_CLI::error( 'Please specify a file to import' );
}
\WP_CLI::line( 'Kit import started' );
$assoc_args = wp_parse_args( $assoc_args, [
'sourceType' => 'local',
] );
$url = null;
$file_path = $args[0];
$import_settings = [];
$import_settings['referrer'] = Module::REFERRER_LOCAL;
switch ( $assoc_args['sourceType'] ) {
case 'library':
$url = $this->get_url_from_library( $file_path );
$zip_path = $this->create_temp_file_from_url( $url );
$import_settings['referrer'] = Module::REFERRER_KIT_LIBRARY;
break;
case 'remote':
$zip_path = $this->create_temp_file_from_url( $file_path );
break;
case 'local':
$zip_path = $file_path;
break;
default:
\WP_CLI::error( 'Unknown source type.' );
break;
}
if ( 'enable' === $assoc_args['unfilteredFilesUpload'] ) {
Plugin::$instance->uploads_manager->enable_unfiltered_files_upload();
}
foreach ( $assoc_args as $key => $value ) {
if ( ! in_array( $key, static::AVAILABLE_SETTINGS, true ) ) {
continue;
}
$import_settings[ $key ] = explode( ',', $value );
}
try {
\WP_CLI::line( 'Importing data...' );
/**
* Running the import process through the import-export module so the import property in the module will be available to use.
*
* @type Module $import_export_module
*/
$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
if ( ! $import_export_module ) {
\WP_CLI::error( 'Import Export module is not available.' );
}
$import = $import_export_module->import_kit( $zip_path, $import_settings );
$manifest_data = $import_export_module->import->get_manifest();
/**
* Import Export Manifest Data
*
* Allows 3rd parties to read and edit the kit's manifest before it is used.
*
* @since 3.7.0
*
* @param array $manifest_data The Kit's Manifest data
*/
$manifest_data = apply_filters( 'elementor/import-export/wp-cli/manifest_data', $manifest_data );
\WP_CLI::line( 'Removing temp files...' );
// The file was created from remote or library request, it also should be removed.
if ( $url ) {
Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $zip_path ) );
}
\WP_CLI::success( 'Kit imported successfully' );
} catch ( \Error $error ) {
Plugin::$instance->logger->get_logger()->error( $error->getMessage(), [
'meta' => [
'trace' => $error->getTraceAsString(),
],
] );
if ( $url ) {
Plugin::$instance->uploads_manager->remove_file_or_dir( dirname( $zip_path ) );
}
\WP_CLI::error( $error->getMessage() );
}
}
/**
* Revert last imported kit.
*/
public function revert() {
\WP_CLI::line( 'Kit revert started.' );
try {
/**
* Running the revert process through the import-export module so the revert property in the module will be available to use.
*
* @type Module $import_export_module
*/
$import_export_module = Plugin::$instance->app->get_component( 'import-export' );
$import_export_module->revert_last_imported_kit();
} catch ( \Error $error ) {
\WP_CLI::error( $error->getMessage() );
}
\WP_CLI::success( 'Kit reverted successfully.' );
}
/**
* Helper to get kit url by the kit id
* TODO: Maybe extract it.
*
* @param $kit_id
*
* @return string
*/
private function get_url_from_library( $kit_id ) {
/** @var Kit_Library $app */
$app = Plugin::$instance->common->get_component( 'connect' )->get_app( 'kit-library' );
if ( ! $app ) {
\WP_CLI::error( 'Kit library app not found' );
}
$response = $app->download_link( $kit_id );
if ( is_wp_error( $response ) ) {
\WP_CLI::error( "Library Response: {$response->get_error_message()}" );
}
return $response->download_link;
}
/**
* Helper to get kit zip file path by the kit url
* TODO: Maybe extract it.
*
* @param $url
*
* @return string
*/
private function create_temp_file_from_url( $url ) {
\WP_CLI::line( 'Extracting zip archive...' );
$response = wp_remote_get( $url );
if ( is_wp_error( $response ) ) {
\WP_CLI::error( "Download file url: {$response->get_error_message()}" );
}
if ( 200 !== $response['response']['code'] ) {
\WP_CLI::error( "Download file url: {$response['response']['message']}" );
}
// Set the Request's state as an Elementor upload request, in order to support unfiltered file uploads.
Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
$file = Plugin::$instance->uploads_manager->create_temp_file( $response['body'], 'kit.zip' );
// After the upload complete, set the elementor upload state back to false.
Plugin::$instance->uploads_manager->set_elementor_upload_state( false );
return $file;
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Elementor\App\Modules\KitLibrary\Connect;
use Elementor\Core\Common\Modules\Connect\Apps\Base_App;
use Elementor\Core\Common\Modules\Connect\Apps\Library;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Kit_Library extends Library {
const DEFAULT_BASE_ENDPOINT = 'https://my.elementor.com/api/v1/kits-library';
const FALLBACK_BASE_ENDPOINT = 'https://ms-8874.elementor.com/api/v1/kits-library';
public function get_title() {
return esc_html__( 'Kit Library', 'elementor' );
}
public function get_all( $args = [] ) {
return $this->http_request( 'GET', 'kits/plugin-version/' . ELEMENTOR_VERSION, $args );
}
public function get_by_id( $id ) {
return $this->http_request( 'GET', 'kits/' . $id );
}
public function get_taxonomies() {
return $this->http_request( 'GET', 'taxonomies' );
}
public function get_manifest( $id ) {
return $this->http_request( 'GET', "kits/{$id}/manifest" );
}
public function download_link( $id ) {
return $this->http_request( 'GET', "kits/{$id}/download-link" );
}
protected function get_api_url() {
return [
static::DEFAULT_BASE_ENDPOINT,
static::FALLBACK_BASE_ENDPOINT,
];
}
/**
* Get all the connect information
*
* @return array
*/
protected function get_connect_info() {
$connect_info = $this->get_base_connect_info();
$additional_info = [];
// BC Support.
$old_kit_library = new \Elementor\Core\App\Modules\KitLibrary\Connect\Kit_Library();
/**
* Additional connect info.
*
* Filters the connection information when connecting to Elementor servers.
* This hook can be used to add more information or add more data.
*
* @param array $additional_info Additional connecting information array.
* @param Base_App $this The base app instance.
*/
$additional_info = apply_filters( 'elementor/connect/additional-connect-info', $additional_info, $old_kit_library );
return array_merge( $connect_info, $additional_info );
}
protected function init() {
// Remove parent init actions.
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Elementor\App\Modules\KitLibrary\Data;
use Elementor\Plugin;
use Elementor\Data\V2\Base\Controller;
use Elementor\Core\Utils\Collection;
use Elementor\Modules\Library\User_Favorites;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
abstract class Base_Controller extends Controller {
/**
* @var Repository
*/
private $repository;
/**
* @return Repository
*/
public function get_repository() {
if ( ! $this->repository ) {
/** @var \Elementor\Core\Common\Modules\Connect\Module $connect */
$connect = Plugin::$instance->common->get_component( 'connect' );
$subscription_plans = ( new Collection( $connect->get_subscription_plans() ) )
->map( function ( $value ) {
return $value['label'];
} );
$this->repository = new Repository(
$connect->get_app( 'kit-library' ),
new User_Favorites( get_current_user_id() ),
$subscription_plans
);
}
return $this->repository;
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Elementor\App\Modules\KitLibrary\Data\Kits;
use Elementor\App\Modules\KitLibrary\Data\Base_Controller;
use Elementor\Data\V2\Base\Exceptions\Error_404;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Controller extends Base_Controller {
public function get_name() {
return 'kits';
}
public function get_items( $request ) {
$data = $this->get_repository()->get_all( $request->get_param( 'force' ) );
return [
'data' => $data->values(),
];
}
public function get_item( $request ) {
$data = $this->get_repository()->find( $request->get_param( 'id' ) );
if ( ! $data ) {
return new Error_404( esc_html__( 'Kit not exists.', 'elementor' ), 'kit_not_exists' );
}
return [
'data' => $data,
];
}
public function get_collection_params() {
return [
'force' => [
'description' => 'Force an API request and skip the cache.',
'required' => false,
'default' => false,
'type' => 'boolean',
],
];
}
public function register_endpoints() {
$this->index_endpoint->register_item_route( \WP_REST_Server::READABLE, [
'id' => [
'description' => 'Unique identifier for the object.',
'type' => 'string',
'required' => true,
],
'id_arg_type_regex' => '[\w]+',
] );
$this->register_endpoint( new Endpoints\Download_Link( $this ) );
$this->register_endpoint( new Endpoints\Favorites( $this ) );
}
public function get_permission_callback( $request ) {
return current_user_can( 'manage_options' );
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Elementor\App\Modules\KitLibrary\Data\Kits\Endpoints;
use Elementor\Data\V2\Base\Endpoint;
use Elementor\App\Modules\KitLibrary\Data\Kits\Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* @property Controller $controller
*/
class Download_Link extends Endpoint {
public function get_name() {
return 'download-link';
}
public function get_format() {
return 'kits/download-link/{id}';
}
protected function register() {
$this->register_item_route( \WP_REST_Server::READABLE, [
'id_arg_type_regex' => '[\w]+',
] );
}
public function get_item( $id, $request ) {
$repository = $this->controller->get_repository();
$data = $repository->get_download_link( $id );
return [
'data' => $data,
'meta' => [
'nonce' => wp_create_nonce( 'kit-library-import' ),
],
];
}
}

View File

@@ -0,0 +1,50 @@
<?php
namespace Elementor\App\Modules\KitLibrary\Data\Kits\Endpoints;
use Elementor\App\Modules\KitLibrary\Data\Kits\Controller;
use Elementor\Data\V2\Base\Endpoint;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* @property Controller $controller
*/
class Favorites extends Endpoint {
public function get_name() {
return 'favorites';
}
public function get_format() {
return 'kits/favorites/{id}';
}
protected function register() {
$args = [
'id_arg_type_regex' => '[\w]+',
];
$this->register_item_route( \WP_REST_Server::CREATABLE, $args );
$this->register_item_route( \WP_REST_Server::DELETABLE, $args );
}
public function create_item( $id, $request ) {
$repository = $this->controller->get_repository();
$kit = $repository->add_to_favorites( $id );
return [
'data' => $kit,
];
}
public function delete_item( $id, $request ) {
$repository = $this->controller->get_repository();
$kit = $repository->remove_from_favorites( $id );
return [
'data' => $kit,
];
}
}

View File

@@ -0,0 +1,338 @@
<?php
namespace Elementor\App\Modules\KitLibrary\Data;
use Elementor\Core\Common\Modules\Connect\Module as ConnectModule;
use Elementor\Core\Utils\Collection;
use Elementor\Data\V2\Base\Exceptions\Error_404;
use Elementor\Data\V2\Base\Exceptions\WP_Error_Exception;
use Elementor\Modules\Library\User_Favorites;
use Elementor\App\Modules\KitLibrary\Connect\Kit_Library;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Repository {
/**
* There is no label for subscription plan with access_level=0 + it should not
* be translated.
*/
const SUBSCRIPTION_PLAN_FREE_TAG = 'Free';
const TAXONOMIES_KEYS = [ 'tags', 'categories', 'main_category', 'third_category', 'features', 'types' ];
const KITS_CACHE_KEY = 'elementor_remote_kits';
const KITS_TAXONOMIES_CACHE_KEY = 'elementor_remote_kits_taxonomies';
const KITS_CACHE_TTL_HOURS = 12;
const KITS_TAXONOMIES_CACHE_TTL_HOURS = 12;
/**
* @var Kit_Library
*/
protected $api;
/**
* @var User_Favorites
*/
protected $user_favorites;
/**
* @var Collection
*/
protected $subscription_plans;
/**
* Get all kits.
*
* @param false $force_api_request
*
* @return Collection
*/
public function get_all( $force_api_request = false ) {
return $this->get_kits_data( $force_api_request )
->map( function ( $kit ) {
return $this->transform_kit_api_response( $kit );
} );
}
/**
* Get specific kit.
*
* @param $id
* @param array $options
*
* @return array|null
*/
public function find( $id, $options = [] ) {
$options = wp_parse_args( $options, [
'manifest_included' => true,
] );
$item = $this->get_kits_data()
->find( function ( $kit ) use ( $id ) {
return $kit->_id === $id;
} );
if ( ! $item ) {
return null;
}
$manifest = null;
if ( $options['manifest_included'] ) {
$manifest = $this->api->get_manifest( $id );
if ( is_wp_error( $manifest ) ) {
throw new WP_Error_Exception( esc_html( $manifest ) );
}
}
return $this->transform_kit_api_response( $item, $manifest );
}
/**
* @param false $force_api_request
*
* @return Collection
*/
public function get_taxonomies( $force_api_request = false ) {
return $this->get_taxonomies_data( $force_api_request )
->only( static::TAXONOMIES_KEYS )
->reduce( function ( Collection $carry, $taxonomies, $type ) {
return $carry->merge( array_map( function ( $taxonomy ) use ( $type ) {
return [
'text' => $taxonomy->name,
'type' => $type,
];
}, $taxonomies ) );
}, new Collection( [] ) )
->merge(
$this->subscription_plans->map( function ( $label ) {
return [
'text' => $label ? $label : self::SUBSCRIPTION_PLAN_FREE_TAG,
'type' => 'subscription_plans',
];
} )
)
->unique( [ 'text', 'type' ] );
}
/**
* @param $id
*
* @return array
*/
public function get_download_link( $id ) {
$response = $this->api->download_link( $id );
if ( is_wp_error( $response ) ) {
throw new WP_Error_Exception( esc_html( $response ) );
}
return [ 'download_link' => $response->download_link ];
}
/**
* @param $id
*
* @return array
* @throws \Exception
*/
public function add_to_favorites( $id ) {
$kit = $this->find( $id, [ 'manifest_included' => false ] );
if ( ! $kit ) {
throw new Error_404( esc_html__( 'Kit not found', 'elementor' ), 'kit_not_found' );
}
$this->user_favorites->add( 'elementor', 'kits', $kit['id'] );
$kit['is_favorite'] = true;
return $kit;
}
/**
* @param $id
*
* @return array
* @throws \Exception
*/
public function remove_from_favorites( $id ) {
$kit = $this->find( $id, [ 'manifest_included' => false ] );
if ( ! $kit ) {
throw new Error_404( esc_html__( 'Kit not found', 'elementor' ), 'kit_not_found' );
}
$this->user_favorites->remove( 'elementor', 'kits', $kit['id'] );
$kit['is_favorite'] = false;
return $kit;
}
/**
* @param bool $force_api_request
*
* @return Collection
*/
private function get_kits_data( $force_api_request = false ) {
$data = get_transient( static::KITS_CACHE_KEY );
$experiments_manager = Plugin::$instance->experiments;
$kits_editor_layout_type = $experiments_manager->is_feature_active( 'container' ) ? 'container_flexbox' : '';
if ( ! $data || $force_api_request ) {
$args = [
'body' => [
'editor_layout_type' => $kits_editor_layout_type,
],
];
/**
* Filters arguments for the request to the Kits API.
*
* @since 3.11.0
*
* @param array[] $args Array of http arguments.
*/
$args = apply_filters( 'elementor/kit-library/get-kits-data/args', $args );
$data = $this->api->get_all( $args );
if ( is_wp_error( $data ) ) {
throw new WP_Error_Exception( esc_html( $data ) );
}
set_transient( static::KITS_CACHE_KEY, $data, static::KITS_CACHE_TTL_HOURS * HOUR_IN_SECONDS );
}
return new Collection( $data );
}
/**
* @param bool $force_api_request
*
* @return Collection
*/
private function get_taxonomies_data( $force_api_request = false ) {
$data = get_transient( static::KITS_TAXONOMIES_CACHE_KEY );
if ( ! $data || $force_api_request ) {
$data = $this->api->get_taxonomies();
if ( is_wp_error( $data ) ) {
throw new WP_Error_Exception( esc_html( $data ) );
}
set_transient( static::KITS_TAXONOMIES_CACHE_KEY, $data, static::KITS_TAXONOMIES_CACHE_TTL_HOURS * HOUR_IN_SECONDS );
}
return new Collection( (array) $data );
}
/**
* @param $kit
* @param null $manifest
*
* @return array
*/
private function transform_kit_api_response( $kit, $manifest = null ) {
// BC: Support legacy APIs that don't have access tiers.
if ( isset( $kit->access_tier ) ) {
$access_tier = $kit->access_tier;
} else {
$access_tier = 0 === $kit->access_level
? ConnectModule::ACCESS_TIER_FREE
: ConnectModule::ACCESS_TIER_ESSENTIAL;
}
$subscription_plan_tag = $this->subscription_plans->get( $access_tier );
$taxonomies = ( new Collection( ( (array) $kit )['taxonomies'] ) )
->filter( function ( $taxonomy ) {
return in_array( $taxonomy->type, self::TAXONOMIES_KEYS );
} )
->flatten()
->pluck( 'name' )
->push( $subscription_plan_tag ? $subscription_plan_tag : self::SUBSCRIPTION_PLAN_FREE_TAG );
return array_merge(
[
'id' => $kit->_id,
'title' => $kit->title,
'thumbnail_url' => $kit->thumbnail,
'access_level' => $kit->access_level,
'access_tier' => $access_tier,
'keywords' => $kit->keywords,
'taxonomies' => $taxonomies->values(),
'is_favorite' => $this->user_favorites->exists( 'elementor', 'kits', $kit->_id ),
// TODO: Remove all the isset when the API stable.
'trend_index' => isset( $kit->trend_index ) ? $kit->trend_index : 0,
'featured_index' => isset( $kit->featured_index ) ? $kit->featured_index : 0,
'popularity_index' => isset( $kit->popularity_index ) ? $kit->popularity_index : 0,
'created_at' => isset( $kit->created_at ) ? $kit->created_at : null,
'updated_at' => isset( $kit->updated_at ) ? $kit->updated_at : null,
],
$manifest ? $this->transform_manifest_api_response( $manifest ) : []
);
}
/**
* @param $manifest
*
* @return array
*/
private function transform_manifest_api_response( $manifest ) {
$manifest_content = ( new Collection( (array) $manifest->content ) )
->reduce( function ( $carry, $content, $type ) {
$mapped_documents = array_map( function ( $document ) use ( $type ) {
// TODO: Fix it!
// Hack to override a bug when a document with type of 'wp-page' is declared as 'wp-post'.
if ( 'page' === $type ) {
$document->doc_type = 'wp-page';
}
return $document;
}, (array) $content );
return $carry + $mapped_documents;
}, [] );
$content = ( new Collection( (array) $manifest->templates ) )
->union( $manifest_content )
->map( function ( $manifest_item, $key ) {
return [
'id' => isset( $manifest_item->id ) ? $manifest_item->id : $key,
'title' => $manifest_item->title,
'doc_type' => $manifest_item->doc_type,
'thumbnail_url' => $manifest_item->thumbnail,
'preview_url' => isset( $manifest_item->url ) ? $manifest_item->url : null,
];
} );
return [
'description' => $manifest->description,
'preview_url' => isset( $manifest->site ) ? $manifest->site : '',
'documents' => $content->values(),
];
}
/**
* @param Kit_Library $kit_library
* @param User_Favorites $user_favorites
* @param Collection $subscription_plans
*/
public function __construct( Kit_Library $kit_library, User_Favorites $user_favorites, Collection $subscription_plans ) {
$this->api = $kit_library;
$this->user_favorites = $user_favorites;
$this->subscription_plans = $subscription_plans;
}
public static function clear_cache() {
delete_transient( static::KITS_CACHE_KEY );
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Elementor\App\Modules\KitLibrary\Data\Taxonomies;
use Elementor\App\Modules\KitLibrary\Data\Base_Controller;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Controller extends Base_Controller {
public function get_name() {
return 'kit-taxonomies';
}
public function get_collection_params() {
return [
'force' => [
'description' => 'Force an API request and skip the cache.',
'required' => false,
'default' => false,
'type' => 'boolean',
],
];
}
public function get_items( $request ) {
$data = $this->get_repository()->get_taxonomies( $request->get_param( 'force' ) );
return [
'data' => $data->values(),
];
}
public function get_permission_callback( $request ) {
return current_user_can( 'manage_options' );
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Elementor\App\Modules\KitLibrary;
use Elementor\Core\Admin\Menu\Interfaces\Admin_Menu_Item;
use Elementor\TemplateLibrary\Source_Local;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Kit_Library_Menu_Item implements Admin_Menu_Item {
public function is_visible() {
return true;
}
public function get_parent_slug() {
return Source_Local::ADMIN_MENU_SLUG;
}
public function get_label() {
return esc_html__( 'Website Templates', 'elementor' );
}
public function get_capability() {
return 'manage_options';
}
}

View File

@@ -0,0 +1,174 @@
<?php
namespace Elementor\App\Modules\KitLibrary;
use Elementor\App\Modules\KitLibrary\Data\Repository;
use Elementor\Core\Admin\Menu\Admin_Menu_Manager;
use Elementor\Core\Admin\Menu\Main as MainMenu;
use Elementor\Plugin;
use Elementor\TemplateLibrary\Source_Local;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\App\Modules\KitLibrary\Connect\Kit_Library;
use Elementor\Core\Common\Modules\Connect\Module as ConnectModule;
use Elementor\App\Modules\KitLibrary\Data\Kits\Controller as Kits_Controller;
use Elementor\App\Modules\KitLibrary\Data\Taxonomies\Controller as Taxonomies_Controller;
use Elementor\Core\Utils\Promotions\Filtered_Promotions_Manager;
use Elementor\Utils as ElementorUtils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Module extends BaseModule {
/**
* Get name.
*
* @access public
*
* @return string
*/
public function get_name() {
return 'kit-library';
}
private function register_admin_menu( MainMenu $menu ) {
$menu->add_submenu( [
'page_title' => esc_html__( 'Website Templates', 'elementor' ),
'menu_title' => '<span id="e-admin-menu__kit-library">' . esc_html__( 'Website Templates', 'elementor' ) . '</span>',
'menu_slug' => Plugin::$instance->app->get_base_url() . '#/kit-library',
'index' => 40,
] );
}
/**
* Register the admin menu the old way.
*/
private function register_admin_menu_legacy( Admin_Menu_Manager $admin_menu ) {
$admin_menu->register(
Plugin::$instance->app->get_base_url() . '#/kit-library',
new Kit_Library_Menu_Item()
);
}
private function set_kit_library_settings() {
if ( ! Plugin::$instance->common ) {
return;
}
/** @var ConnectModule $connect */
$connect = Plugin::$instance->common->get_component( 'connect' );
/** @var Kit_Library $kit_library */
$kit_library = $connect->get_app( 'kit-library' );
Plugin::$instance->app->set_settings( 'kit-library', [
'has_access_to_module' => current_user_can( 'manage_options' ),
'subscription_plans' => $this->apply_filter_subscription_plans( $connect->get_subscription_plans( 'kit-library' ) ),
'is_pro' => false,
'is_library_connected' => $kit_library->is_connected(),
'library_connect_url' => $kit_library->get_admin_url( 'authorize', [
'utm_source' => 'kit-library',
'utm_medium' => 'wp-dash',
'utm_campaign' => 'library-connect',
'utm_term' => '%%page%%', // Will be replaced in the frontend.
] ),
'access_level' => ConnectModule::ACCESS_LEVEL_CORE,
'access_tier' => ConnectModule::ACCESS_TIER_FREE,
'plan_type' => ConnectModule::ACCESS_TIER_FREE,
'app_url' => Plugin::$instance->app->get_base_url() . '#/' . $this->get_name(),
] );
}
private function apply_filter_subscription_plans( array $subscription_plans ): array {
foreach ( $subscription_plans as $key => $plan ) {
if ( null === $plan['promotion_url'] ) {
continue;
}
$subscription_plans[ $key ] = Filtered_Promotions_Manager::get_filtered_promotion_data(
$plan,
'elementor/kit_library/' . $key . '/promotion',
'promotion_url'
);
}
return $subscription_plans;
}
/**
* Module constructor.
*/
public function __construct() {
Plugin::$instance->data_manager_v2->register_controller( new Kits_Controller() );
Plugin::$instance->data_manager_v2->register_controller( new Taxonomies_Controller() );
$this->register_actions();
do_action( 'elementor/kit_library/registered', $this );
}
public function register_actions() {
// Assigning this action here since the repository is being loaded by demand.
add_action( 'elementor/experiments/feature-state-change/container', [ Repository::class, 'clear_cache' ], 10, 0 );
add_action( 'elementor/admin/menu/register', function( Admin_Menu_Manager $admin_menu ) {
$this->register_admin_menu_legacy( $admin_menu );
}, Source_Local::ADMIN_MENU_PRIORITY + 30 );
add_action( 'elementor/connect/apps/register', function ( ConnectModule $connect_module ) {
$connect_module->register_app( 'kit-library', Kit_Library::get_class_name() );
} );
add_action( 'elementor/init', function () {
$this->set_kit_library_settings();
}, 12 /** After the initiation of the connect kit library */ );
add_action( 'template_redirect', [ $this, 'handle_kit_screenshot_generation' ] );
}
public function handle_kit_screenshot_generation() {
$is_kit_preview = ElementorUtils::get_super_global_value( $_GET, 'kit_thumbnail' );
$nonce = ElementorUtils::get_super_global_value( $_GET, 'nonce' );
if ( $is_kit_preview ) {
if ( ! wp_verify_nonce( $nonce, 'kit_thumbnail' ) ) {
wp_die( esc_html__( 'Not Authorized', 'elementor' ), esc_html__( 'Error', 'elementor' ), 403 );
}
$suffix = ( ElementorUtils::is_script_debug() || ElementorUtils::is_elementor_tests() ) ? '' : '.min';
show_admin_bar( false );
wp_enqueue_script(
'dom-to-image',
ELEMENTOR_ASSETS_URL . "/lib/dom-to-image/js/dom-to-image{$suffix}.js",
[],
'2.6.0',
true
);
wp_enqueue_script(
'html2canvas',
ELEMENTOR_ASSETS_URL . "/lib/html2canvas/js/html2canvas{$suffix}.js",
[],
'1.4.1',
true
);
wp_enqueue_script(
'cloud-library-screenshot',
ELEMENTOR_ASSETS_URL . "/js/cloud-library-screenshot{$suffix}.js",
[ 'dom-to-image', 'html2canvas', 'elementor-common', 'elementor-common-modules' ],
ELEMENTOR_VERSION,
true
);
$config = [
'home_url' => home_url(),
'kit_id' => uniqid(),
'selector' => 'body',
];
wp_add_inline_script( 'cloud-library-screenshot', 'var ElementorScreenshotConfig = ' . wp_json_encode( $config ) . ';' );
}
}
}

View File

@@ -0,0 +1,49 @@
<?php
namespace Elementor\App\Modules\Onboarding;
use Elementor\Tracker;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
class Features_Usage {
const ONBOARDING_FEATURES_OPTION = '_elementor_onboarding_features';
public function register() {
if ( ! Tracker::is_allow_track() ) {
return;
}
add_filter( 'elementor/tracker/send_tracking_data_params', function ( array $params ) {
$params['usages']['onboarding_features'] = $this->get_usage_data();
return $params;
} );
}
public function save_onboarding_features( $raw_post_data ) {
if ( empty( $raw_post_data ) ) {
return;
}
$post_data = json_decode( $raw_post_data, true );
if ( empty( $post_data['features'] ) ) {
return;
}
update_option( static::ONBOARDING_FEATURES_OPTION, $post_data['features'] );
return [
'status' => 'success',
'payload' => [],
];
}
private function get_usage_data() {
return get_option( static::ONBOARDING_FEATURES_OPTION, [] );
}
}

View File

@@ -0,0 +1,500 @@
<?php
namespace Elementor\App\Modules\Onboarding;
use Automatic_Upgrader_Skin;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Core\Common\Modules\Ajax\Module as Ajax;
use Elementor\Core\Common\Modules\Connect\Apps\Library;
use Elementor\Core\Files\Uploads_Manager;
use Elementor\Plugin;
use Elementor\Utils;
use Plugin_Upgrader;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Onboarding Module
*
* Responsible for initializing Elementor App functionality
*
* @since 3.6.0
*/
class Module extends BaseModule {
const VERSION = '1.0.0';
const ONBOARDING_OPTION = 'elementor_onboarded';
/**
* Get name.
*
* @since 3.6.0
* @access public
*
* @return string
*/
public function get_name() {
return 'onboarding';
}
/**
* Set Onboarding Settings
*
* Creates an array of module settings that is localized into the JS App config.
*
* @since 3.6.0
*/
private function set_onboarding_settings() {
if ( ! Plugin::$instance->common ) {
return;
}
// Get the published pages and posts
$pages_and_posts = new \WP_Query( [
'post_type' => [ 'page', 'post' ],
'post_status' => 'publish',
'update_post_meta_cache' => false,
'update_post_term_cache' => false,
'no_found_rows' => true,
] );
$custom_site_logo_id = get_theme_mod( 'custom_logo' );
$custom_logo_src = wp_get_attachment_image_src( $custom_site_logo_id, 'full' );
$site_name = get_option( 'blogname', '' );
$hello_theme = wp_get_theme( 'hello-elementor' );
$hello_theme_errors = is_object( $hello_theme->errors() ) ? $hello_theme->errors()->errors : [];
/** @var Library $library */
$library = Plugin::$instance->common->get_component( 'connect' )->get_app( 'library' );
Plugin::$instance->app->set_settings( 'onboarding', [
'eventPlacement' => 'Onboarding wizard',
'onboardingAlreadyRan' => get_option( self::ONBOARDING_OPTION ),
'onboardingVersion' => self::VERSION,
'isLibraryConnected' => $library->is_connected(),
// Used to check if the Hello Elementor theme is installed but not activated.
'helloInstalled' => empty( $hello_theme_errors['theme_not_found'] ),
'helloActivated' => 'hello-elementor' === get_option( 'template' ),
// The "Use Hello theme on my site" checkbox should be checked by default only if this condition is met.
'helloOptOut' => count( $pages_and_posts->posts ) < 5,
'siteName' => esc_html( $site_name ),
'isUnfilteredFilesEnabled' => Uploads_Manager::are_unfiltered_uploads_enabled(),
'urls' => [
'kitLibrary' => Plugin::$instance->app->get_base_url() . '#/kit-library?order[direction]=desc&order[by]=featuredIndex',
'sitePlanner' => add_query_arg( [
'type' => 'editor',
'siteUrl' => esc_url( home_url() ),
'siteName' => esc_html( $site_name ),
'siteDescription' => esc_html( get_bloginfo( 'description' ) ),
'siteLanguage' => get_locale(),
], 'https://planner.elementor.com/onboarding.html' ),
'createNewPage' => Plugin::$instance->documents->get_create_new_post_url(),
'connect' => $library->get_admin_url( 'authorize', [
'utm_source' => 'onboarding-wizard',
'utm_campaign' => 'connect-account',
'utm_medium' => 'wp-dash',
'utm_term' => self::VERSION,
'source' => 'generic',
] ),
'upgrade' => 'https://go.elementor.com/go-pro-onboarding-wizard-upgrade/',
'signUp' => $library->get_admin_url( 'authorize', [
'utm_source' => 'onboarding-wizard',
'utm_campaign' => 'connect-account',
'utm_medium' => 'wp-dash',
'utm_term' => self::VERSION,
'source' => 'generic',
'screen_hint' => 'signup',
] ),
'uploadPro' => Plugin::$instance->app->get_base_url() . '#/onboarding/uploadAndInstallPro?mode=popup',
],
'siteLogo' => [
'id' => $custom_site_logo_id,
'url' => $custom_logo_src ? $custom_logo_src[0] : '',
],
'utms' => [
'connectTopBar' => '&utm_content=top-bar',
'connectCta' => '&utm_content=cta-button',
'connectCtaLink' => '&utm_content=cta-link',
'downloadPro' => '?utm_source=onboarding-wizard&utm_campaign=my-account-subscriptions&utm_medium=wp-dash&utm_content=import-pro-plugin&utm_term=' . self::VERSION,
],
'nonce' => wp_create_nonce( 'onboarding' ),
'experiment' => true,
] );
}
/**
* Get Permission Error Response
*
* Returns the response that is returned when the user's capabilities are not sufficient for performing an action.
*
* @since 3.6.4
*
* @return array
*/
private function get_permission_error_response() {
return [
'status' => 'error',
'payload' => [
'error_message' => esc_html__( 'You do not have permission to perform this action.', 'elementor' ),
],
];
}
/**
* Maybe Update Site Logo
*
* If a new name is provided, it will be updated as the Site Name.
*
* @since 3.6.0
*
* @return array
*/
private function maybe_update_site_name() {
$problem_error = [
'status' => 'error',
'payload' => [
'error_message' => esc_html__( 'There was a problem setting your site name.', 'elementor' ),
],
];
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( empty( $_POST['data'] ) ) {
return $problem_error;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$data = json_decode( Utils::get_super_global_value( $_POST, 'data' ), true );
if ( ! isset( $data['siteName'] ) ) {
return $problem_error;
}
/**
* Onboarding Site Name
*
* Filters the new site name passed by the user to update in Elementor's onboarding process.
* Elementor runs `esc_html()` on the Site Name passed by the user for security reasons. If a user wants to
* include special characters in their site name, they can use this filter to override it.
*
* @since 3.6.0
*
* @param string Escaped new site name
*/
$new_site_name = apply_filters( 'elementor/onboarding/site-name', $data['siteName'] );
// The site name is sanitized in `update_options()`
update_option( 'blogname', $new_site_name );
return [
'status' => 'success',
'payload' => [
'siteNameUpdated' => true,
],
];
}
/**
* Maybe Update Site Logo
*
* If an image attachment ID is provided, it will be updated as the Site Logo Theme Mod.
*
* @since 3.6.0
*
* @return array
*/
private function maybe_update_site_logo() {
if ( ! current_user_can( 'edit_theme_options' ) ) {
return $this->get_permission_error_response();
}
$data_error = [
'status' => 'error',
'payload' => [
'error_message' => esc_html__( 'There was a problem setting your site logo.', 'elementor' ),
],
];
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( empty( $_POST['data'] ) ) {
return $data_error;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$data = json_decode( Utils::get_super_global_value( $_POST, 'data' ), true );
// If there is no attachment ID passed or it is not a valid ID, exit here.
if ( empty( $data['attachmentId'] ) ) {
return $data_error;
}
$absint_attachment_id = absint( $data['attachmentId'] );
if ( 0 === $absint_attachment_id ) {
return $data_error;
}
$attachment_url = wp_get_attachment_url( $data['attachmentId'] );
// Check if the attachment exists. If it does not, exit here.
if ( ! $attachment_url ) {
return $data_error;
}
set_theme_mod( 'custom_logo', $absint_attachment_id );
return [
'status' => 'success',
'payload' => [
'siteLogoUpdated' => true,
],
];
}
/**
* Maybe Upload Logo Image
*
* If an image file upload is provided, and it passes validation, it will be uploaded to the site's Media Library.
*
* @since 3.6.0
*
* @return array
*/
private function maybe_upload_logo_image() {
$error_message = esc_html__( 'There was a problem uploading your file.', 'elementor' );
$file = Utils::get_super_global_value( $_FILES, 'fileToUpload' ); // phpcs:ignore WordPress.Security.NonceVerification.Missing
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( ! is_array( $file ) || empty( $file['type'] ) ) {
return [
'status' => 'error',
'payload' => [
'error_message' => $error_message,
],
];
}
// If the user has allowed it, set the Request's state as an "Elementor Upload" request, in order to add
// support for non-standard file uploads.
if ( 'image/svg+xml' === $file['type'] ) {
if ( Uploads_Manager::are_unfiltered_uploads_enabled() ) {
Plugin::$instance->uploads_manager->set_elementor_upload_state( true );
} else {
wp_send_json_error( 'To upload SVG files, you must allow uploading unfiltered files.' );
}
}
// If the image is an SVG file, sanitation is performed during the import (upload) process.
$image_attachment = Plugin::$instance->templates_manager->get_import_images_instance()->import( $file );
if ( 'image/svg+xml' === $file['type'] && Uploads_Manager::are_unfiltered_uploads_enabled() ) {
// Reset Upload state.
Plugin::$instance->uploads_manager->set_elementor_upload_state( false );
}
if ( $image_attachment && ! is_wp_error( $image_attachment ) ) {
$result = [
'status' => 'success',
'payload' => [
'imageAttachment' => $image_attachment,
],
];
} else {
$result = [
'status' => 'error',
'payload' => [
'error_message' => $error_message,
],
];
}
return $result;
}
/**
* Activate Hello Theme
*
* @since 3.6.0
*
* @return array
*/
private function maybe_activate_hello_theme() {
if ( ! current_user_can( 'switch_themes' ) ) {
return $this->get_permission_error_response();
}
switch_theme( 'hello-biz' );
return [
'status' => 'success',
'payload' => [
'helloThemeActivated' => true,
],
];
}
/**
* Upload and Install Elementor Pro
*
* @since 3.6.0
*
* @return array
*/
private function upload_and_install_pro() {
if ( ! current_user_can( 'install_plugins' ) || ! current_user_can( 'activate_plugins' ) ) {
return $this->get_permission_error_response();
}
$error_message = esc_html__( 'There was a problem uploading your file.', 'elementor' );
$file = Utils::get_super_global_value( $_FILES, 'fileToUpload' ) ?? []; // phpcs:ignore WordPress.Security.NonceVerification.Missing
// phpcs:ignore WordPress.Security.NonceVerification.Missing
if ( ! is_array( $file ) || empty( $file['type'] ) ) {
return [
'status' => 'error',
'payload' => [
'error_message' => $error_message,
],
];
}
$result = [];
if ( ! class_exists( 'Automatic_Upgrader_Skin' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
}
$skin = new Automatic_Upgrader_Skin();
$upgrader = new Plugin_Upgrader( $skin );
$upload_result = $upgrader->install( $file['tmp_name'], [ 'overwrite_package' => false ] );
if ( ! $upload_result || is_wp_error( $upload_result ) ) {
$result = [
'status' => 'error',
'payload' => [
'error_message' => $error_message,
],
];
} else {
$activated = activate_plugin( WP_PLUGIN_DIR . '/elementor-pro/elementor-pro.php', false, false, true );
if ( ! is_wp_error( $activated ) ) {
$result = [
'status' => 'success',
'payload' => [
'elementorProInstalled' => true,
],
];
} else {
$result = [
'status' => 'error',
'payload' => [
'error_message' => $error_message,
'elementorProInstalled' => false,
],
];
}
}
return $result;
}
private function maybe_update_onboarding_db_option() {
$db_option = get_option( self::ONBOARDING_OPTION );
if ( ! $db_option ) {
update_option( self::ONBOARDING_OPTION, true );
}
return [
'status' => 'success',
'payload' => 'onboarding DB',
];
}
/**
* Maybe Handle Ajax
*
* This method checks if there are any AJAX actions being
*
* @since 3.6.0
*/
private function maybe_handle_ajax() {
$result = [];
// phpcs:ignore WordPress.Security.NonceVerification.Missing
switch ( Utils::get_super_global_value( $_POST, 'action' ) ) {
case 'elementor_update_site_name':
// If no value is passed for any reason, no need to update the site name.
$result = $this->maybe_update_site_name();
break;
case 'elementor_update_site_logo':
$result = $this->maybe_update_site_logo();
break;
case 'elementor_upload_site_logo':
$result = $this->maybe_upload_logo_image();
break;
case 'elementor_activate_hello_theme':
$result = $this->maybe_activate_hello_theme();
break;
case 'elementor_upload_and_install_pro':
$result = $this->upload_and_install_pro();
break;
case 'elementor_update_onboarding_option':
$result = $this->maybe_update_onboarding_db_option();
break;
case 'elementor_save_onboarding_features':
// phpcs:ignore WordPress.Security.NonceVerification.Missing
$result = $this->get_component( 'features_usage' )->save_onboarding_features( Utils::get_super_global_value( $_POST, 'data' ) ?? [] );
}
if ( ! empty( $result ) ) {
if ( 'success' === $result['status'] ) {
wp_send_json_success( $result['payload'] );
} else {
wp_send_json_error( $result['payload'] );
}
}
}
public function __construct() {
$this->add_component( 'features_usage', new Features_Usage() );
add_action( 'elementor/init', function() {
// Only load when viewing the onboarding app.
if ( Plugin::$instance->app->is_current() ) {
$this->set_onboarding_settings();
// Needed for installing the Hello Elementor theme.
wp_enqueue_script( 'updates' );
// Needed for uploading Logo from WP Media Library.
wp_enqueue_media();
}
}, 12 );
// Needed for uploading Logo from WP Media Library. The 'admin_menu' hook is used because it runs before
// 'admin_init', and the App triggers printing footer scripts on 'admin_init' at priority 0.
add_action( 'admin_menu', function () {
add_action( 'wp_print_footer_scripts', function () {
if ( function_exists( 'wp_print_media_templates' ) ) {
wp_print_media_templates();
}
} );
} );
add_action( 'admin_init', function() {
if ( wp_doing_ajax() &&
isset( $_POST['action'] ) &&
isset( $_POST['_nonce'] ) &&
wp_verify_nonce( Utils::get_super_global_value( $_POST, '_nonce' ), Ajax::NONCE_KEY ) &&
current_user_can( 'manage_options' )
) {
$this->maybe_handle_ajax();
}
} );
$this->get_component( 'features_usage' )->register();
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Elementor\App\Modules\SiteEditor;
use Elementor\Core\Base\Module as BaseModule;
use Elementor\Plugin;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* Site Editor Module
*
* Responsible for initializing Elementor App functionality
*/
class Module extends BaseModule {
/**
* Get name.
*
* @access public
*
* @return string
*/
public function get_name() {
return 'site-editor';
}
public function add_menu_in_admin_bar( $admin_bar_config ) {
$admin_bar_config['elementor_edit_page']['children'][] = [
'id' => 'elementor_app_site_editor',
'title' => esc_html__( 'Theme Builder', 'elementor' ),
'sub_title' => esc_html__( 'Site', 'elementor' ),
'href' => Plugin::$instance->app->get_settings( 'menu_url' ),
'class' => 'elementor-app-link',
'parent_class' => 'elementor-second-section',
];
return $admin_bar_config;
}
public function __construct() {
add_filter( 'elementor/frontend/admin_bar/settings', [ $this, 'add_menu_in_admin_bar' ] ); // After kit (Site settings)
}
}

View File

@@ -0,0 +1,31 @@
<?php
namespace Elementor\App;
use Elementor\Utils;
if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}
/**
* @var App $this
*/
$theme_class = 'dark' === $this->get_elementor_ui_theme_preference() ? 'eps-theme-dark' : '';
?>
<!DOCTYPE html>
<html <?php language_attributes(); ?>>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title><?php echo esc_html__( 'Elementor', 'elementor' ) . ' ... '; ?></title>
<base target="_parent">
<?php wp_print_styles(); ?>
</head>
<body class="<?php Utils::print_unescaped_internal_string( $theme_class ); ?>">
<div id="e-app"></div>
<?php wp_print_footer_scripts(); ?>
</body>
</html>

View File

@@ -0,0 +1,35 @@
/*! elementor - v3.32.0 - 18-09-2025 */
#wp-admin-bar-elementor_edit_page > .ab-item::before {
content: "\e813";
font-family: eicons;
inset-block-start: 3px;
font-size: 18px;
}
#wp-admin-bar-elementor_edit_page .ab-submenu .ab-item {
display: flex;
width: 200px;
}
#wp-admin-bar-elementor_edit_page .elementor-edit-link-title {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
}
#wp-admin-bar-elementor_edit_page .elementor-edit-link-type {
background: #3f444b;
font-size: 11px;
line-height: 9px;
margin-block-start: 6px;
padding: 4px 8px;
border-radius: 3px;
}
#wp-admin-bar-elementor_inspector > .ab-item::before {
content: "\f348";
inset-block-start: 2px;
}
#wpadminbar * {
font-style: normal;
}
/*# sourceMappingURL=admin-bar.css.map */

View File

@@ -0,0 +1,2 @@
/*! elementor - v3.32.0 - 18-09-2025 */
#wp-admin-bar-elementor_edit_page>.ab-item:before{content:"\e813";font-family:eicons;font-size:18px;inset-block-start:3px}#wp-admin-bar-elementor_edit_page .ab-submenu .ab-item{display:flex;width:200px}#wp-admin-bar-elementor_edit_page .elementor-edit-link-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;width:100%}#wp-admin-bar-elementor_edit_page .elementor-edit-link-type{background:#3f444b;border-radius:3px;font-size:11px;line-height:9px;margin-block-start:6px;padding:4px 8px}#wp-admin-bar-elementor_inspector>.ab-item:before{content:"\f348";inset-block-start:2px}#wpadminbar *{font-style:normal}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,165 @@
/*! elementor - v3.32.0 - 18-09-2025 */
#e-dashboard-widget-admin-top-bar {
position: absolute;
opacity: 0;
pointer-events: none;
}
#e-admin-top-bar-root {
font-family: var(--e-a-font-family);
background: var(--e-a-bg-default);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.03);
display: none;
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
width: calc(100% - 160px);
z-index: 1;
}
body.folded #e-admin-top-bar-root {
width: calc(100% - 36px);
}
#e-admin-top-bar-root .e-admin-top-bar {
display: flex;
height: 50px;
justify-content: space-between;
padding: 0 16px;
}
#e-admin-top-bar-root .page-title-action {
font-size: 12px;
font-weight: 500;
line-height: 1.2;
text-transform: uppercase;
text-decoration: none;
padding: 8px 16px;
outline: none;
border: none;
border-radius: var(--e-a-border-radius);
background-color: var(--e-a-btn-bg);
color: var(--e-a-btn-color-invert);
transition: var(--e-a-transition-hover);
}
#e-admin-top-bar-root .page-title-action:hover {
background-color: var(--e-a-btn-bg-hover);
color: var(--e-a-btn-color-invert);
}
#e-admin-top-bar-root .e-admin-top-bar__heading {
display: inline-flex;
align-items: center;
justify-content: center;
margin-inline-end: 40px;
}
#e-admin-top-bar-root .e-admin-top-bar__main-area {
display: inline-flex;
align-items: center;
justify-content: center;
}
#e-admin-top-bar-root .e-admin-top-bar__main-area button {
margin: 0 4px;
}
#e-admin-top-bar-root .e-admin-top-bar__secondary-area {
display: inline-flex;
align-items: center;
justify-content: center;
}
#e-admin-top-bar-root .e-admin-top-bar__heading-title {
color: var(--e-a-color-txt);
font-size: 15px;
font-weight: 700;
padding: 0 8px;
line-height: normal;
}
#e-admin-top-bar-root .e-admin-top-bar__main-area-buttons {
display: inline-flex;
gap: 5px;
}
#e-admin-top-bar-root.e-admin-top-bar--active {
display: block;
}
#e-admin-top-bar-root.e-admin-top-bar--active ~ #wpbody #wpbody-content {
margin-block-start: 50px;
}
#e-admin-top-bar-root.e-admin-top-bar--active ~ #wpbody .wrap {
clear: both;
padding-block-start: 10px;
}
#e-admin-top-bar-root.e-admin-top-bar--active ~ #wpbody .wrap h1 {
display: none;
}
#e-admin-top-bar-root:not(.e-admin-top-bar--active) ~ #wpbody .wrap h1, #e-admin-top-bar-root:not(.e-admin-top-bar--active) ~ #wpbody .wrap .page-title-action {
display: inline-block;
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button {
align-items: center;
cursor: pointer;
display: inline-flex;
justify-content: center;
margin: 0 10px;
text-decoration: none;
color: var(--e-a-color-txt);
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent {
color: var(--e-a-color-accent);
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent:hover .e-admin-top-bar__bar-button-title,
#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent:hover .e-admin-top-bar__bar-button-icon {
color: var(--e-a-color-accent);
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button .crown-icon {
font-size: 14px;
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button .e-admin-top-bar__bar-button-icon {
margin: 0 4px;
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button:hover .e-admin-top-bar__bar-button-title,
#e-admin-top-bar-root .e-admin-top-bar__bar-button:hover .e-admin-top-bar__bar-button-icon {
color: var(--e-a-color-txt-hover);
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button-title {
font-size: 13px;
font-weight: 500;
margin: 0 4px;
line-height: normal;
}
#e-admin-top-bar-root ~ #wpbody .wrap h1, #e-admin-top-bar-root ~ #wpbody .wrap .page-title-action {
display: none;
}
@media screen and (max-width: 960px) {
#e-admin-top-bar-root {
width: calc(100% - 36px);
}
}
@media screen and (max-width: 782px) {
#e-admin-top-bar-root {
width: 100%;
}
}
@media screen and (max-width: 600px) {
#e-admin-top-bar-root {
top: 46px;
}
}
@media (max-width: 768px) {
#e-admin-top-bar-root {
display: inline-flex;
align-items: center;
justify-content: center;
}
#e-admin-top-bar-root .e-admin-top-bar__main-area-buttons {
position: absolute;
top: calc(100% + 10px);
}
#e-admin-top-bar-root .e-admin-top-bar__secondary-area .e-admin-top-bar__secondary-area-buttons {
display: none;
}
#e-admin-top-bar-root .e-admin-top-bar__secondary-area > .e-admin-top-bar__bar-button .e-admin-top-bar__bar-button-title {
display: none;
}
}
@media (min-width: 768px) {
#e-admin-top-bar-root .e-admin-top-bar__secondary-area .e-admin-top-bar__secondary-area-buttons {
display: flex;
}
}
/*# sourceMappingURL=admin-top-bar-rtl.css.map */

View File

@@ -0,0 +1,2 @@
/*! elementor - v3.32.0 - 18-09-2025 */
#e-dashboard-widget-admin-top-bar{opacity:0;pointer-events:none;position:absolute}#e-admin-top-bar-root{background:var(--e-a-bg-default);box-shadow:0 4px 6px rgba(0,0,0,.03);display:none;font-family:var(--e-a-font-family);inset-block-start:0;inset-inline-end:0;position:absolute;width:calc(100% - 160px);z-index:1}body.folded #e-admin-top-bar-root{width:calc(100% - 36px)}#e-admin-top-bar-root .e-admin-top-bar{display:flex;height:50px;justify-content:space-between;padding:0 16px}#e-admin-top-bar-root .page-title-action{background-color:var(--e-a-btn-bg);border:none;border-radius:var(--e-a-border-radius);color:var(--e-a-btn-color-invert);font-size:12px;font-weight:500;line-height:1.2;outline:none;padding:8px 16px;text-decoration:none;text-transform:uppercase;transition:var(--e-a-transition-hover)}#e-admin-top-bar-root .page-title-action:hover{background-color:var(--e-a-btn-bg-hover);color:var(--e-a-btn-color-invert)}#e-admin-top-bar-root .e-admin-top-bar__heading{margin-inline-end:40px}#e-admin-top-bar-root .e-admin-top-bar__heading,#e-admin-top-bar-root .e-admin-top-bar__main-area{align-items:center;display:inline-flex;justify-content:center}#e-admin-top-bar-root .e-admin-top-bar__main-area button{margin:0 4px}#e-admin-top-bar-root .e-admin-top-bar__secondary-area{align-items:center;display:inline-flex;justify-content:center}#e-admin-top-bar-root .e-admin-top-bar__heading-title{color:var(--e-a-color-txt);font-size:15px;font-weight:700;line-height:normal;padding:0 8px}#e-admin-top-bar-root .e-admin-top-bar__main-area-buttons{display:inline-flex;gap:5px}#e-admin-top-bar-root.e-admin-top-bar--active{display:block}#e-admin-top-bar-root.e-admin-top-bar--active~#wpbody #wpbody-content{margin-block-start:50px}#e-admin-top-bar-root.e-admin-top-bar--active~#wpbody .wrap{clear:both;padding-block-start:10px}#e-admin-top-bar-root.e-admin-top-bar--active~#wpbody .wrap h1{display:none}#e-admin-top-bar-root:not(.e-admin-top-bar--active)~#wpbody .wrap .page-title-action,#e-admin-top-bar-root:not(.e-admin-top-bar--active)~#wpbody .wrap h1{display:inline-block}#e-admin-top-bar-root .e-admin-top-bar__bar-button{align-items:center;color:var(--e-a-color-txt);cursor:pointer;display:inline-flex;justify-content:center;margin:0 10px;text-decoration:none}#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent,#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent:hover .e-admin-top-bar__bar-button-icon,#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent:hover .e-admin-top-bar__bar-button-title{color:var(--e-a-color-accent)}#e-admin-top-bar-root .e-admin-top-bar__bar-button .crown-icon{font-size:14px}#e-admin-top-bar-root .e-admin-top-bar__bar-button .e-admin-top-bar__bar-button-icon{margin:0 4px}#e-admin-top-bar-root .e-admin-top-bar__bar-button:hover .e-admin-top-bar__bar-button-icon,#e-admin-top-bar-root .e-admin-top-bar__bar-button:hover .e-admin-top-bar__bar-button-title{color:var(--e-a-color-txt-hover)}#e-admin-top-bar-root .e-admin-top-bar__bar-button-title{font-size:13px;font-weight:500;line-height:normal;margin:0 4px}#e-admin-top-bar-root~#wpbody .wrap .page-title-action,#e-admin-top-bar-root~#wpbody .wrap h1{display:none}@media screen and (max-width:960px){#e-admin-top-bar-root{width:calc(100% - 36px)}}@media screen and (max-width:782px){#e-admin-top-bar-root{width:100%}}@media screen and (max-width:600px){#e-admin-top-bar-root{top:46px}}@media (max-width:768px){#e-admin-top-bar-root{align-items:center;display:inline-flex;justify-content:center}#e-admin-top-bar-root .e-admin-top-bar__main-area-buttons{position:absolute;top:calc(100% + 10px)}#e-admin-top-bar-root .e-admin-top-bar__secondary-area .e-admin-top-bar__secondary-area-buttons,#e-admin-top-bar-root .e-admin-top-bar__secondary-area>.e-admin-top-bar__bar-button .e-admin-top-bar__bar-button-title{display:none}}@media (min-width:768px){#e-admin-top-bar-root .e-admin-top-bar__secondary-area .e-admin-top-bar__secondary-area-buttons{display:flex}}

View File

@@ -0,0 +1,165 @@
/*! elementor - v3.32.0 - 18-09-2025 */
#e-dashboard-widget-admin-top-bar {
position: absolute;
opacity: 0;
pointer-events: none;
}
#e-admin-top-bar-root {
font-family: var(--e-a-font-family);
background: var(--e-a-bg-default);
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.03);
display: none;
position: absolute;
inset-block-start: 0;
inset-inline-end: 0;
width: calc(100% - 160px);
z-index: 1;
}
body.folded #e-admin-top-bar-root {
width: calc(100% - 36px);
}
#e-admin-top-bar-root .e-admin-top-bar {
display: flex;
height: 50px;
justify-content: space-between;
padding: 0 16px;
}
#e-admin-top-bar-root .page-title-action {
font-size: 12px;
font-weight: 500;
line-height: 1.2;
text-transform: uppercase;
text-decoration: none;
padding: 8px 16px;
outline: none;
border: none;
border-radius: var(--e-a-border-radius);
background-color: var(--e-a-btn-bg);
color: var(--e-a-btn-color-invert);
transition: var(--e-a-transition-hover);
}
#e-admin-top-bar-root .page-title-action:hover {
background-color: var(--e-a-btn-bg-hover);
color: var(--e-a-btn-color-invert);
}
#e-admin-top-bar-root .e-admin-top-bar__heading {
display: inline-flex;
align-items: center;
justify-content: center;
margin-inline-end: 40px;
}
#e-admin-top-bar-root .e-admin-top-bar__main-area {
display: inline-flex;
align-items: center;
justify-content: center;
}
#e-admin-top-bar-root .e-admin-top-bar__main-area button {
margin: 0 4px;
}
#e-admin-top-bar-root .e-admin-top-bar__secondary-area {
display: inline-flex;
align-items: center;
justify-content: center;
}
#e-admin-top-bar-root .e-admin-top-bar__heading-title {
color: var(--e-a-color-txt);
font-size: 15px;
font-weight: 700;
padding: 0 8px;
line-height: normal;
}
#e-admin-top-bar-root .e-admin-top-bar__main-area-buttons {
display: inline-flex;
gap: 5px;
}
#e-admin-top-bar-root.e-admin-top-bar--active {
display: block;
}
#e-admin-top-bar-root.e-admin-top-bar--active ~ #wpbody #wpbody-content {
margin-block-start: 50px;
}
#e-admin-top-bar-root.e-admin-top-bar--active ~ #wpbody .wrap {
clear: both;
padding-block-start: 10px;
}
#e-admin-top-bar-root.e-admin-top-bar--active ~ #wpbody .wrap h1 {
display: none;
}
#e-admin-top-bar-root:not(.e-admin-top-bar--active) ~ #wpbody .wrap h1, #e-admin-top-bar-root:not(.e-admin-top-bar--active) ~ #wpbody .wrap .page-title-action {
display: inline-block;
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button {
align-items: center;
cursor: pointer;
display: inline-flex;
justify-content: center;
margin: 0 10px;
text-decoration: none;
color: var(--e-a-color-txt);
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent {
color: var(--e-a-color-accent);
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent:hover .e-admin-top-bar__bar-button-title,
#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent:hover .e-admin-top-bar__bar-button-icon {
color: var(--e-a-color-accent);
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button .crown-icon {
font-size: 14px;
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button .e-admin-top-bar__bar-button-icon {
margin: 0 4px;
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button:hover .e-admin-top-bar__bar-button-title,
#e-admin-top-bar-root .e-admin-top-bar__bar-button:hover .e-admin-top-bar__bar-button-icon {
color: var(--e-a-color-txt-hover);
}
#e-admin-top-bar-root .e-admin-top-bar__bar-button-title {
font-size: 13px;
font-weight: 500;
margin: 0 4px;
line-height: normal;
}
#e-admin-top-bar-root ~ #wpbody .wrap h1, #e-admin-top-bar-root ~ #wpbody .wrap .page-title-action {
display: none;
}
@media screen and (max-width: 960px) {
#e-admin-top-bar-root {
width: calc(100% - 36px);
}
}
@media screen and (max-width: 782px) {
#e-admin-top-bar-root {
width: 100%;
}
}
@media screen and (max-width: 600px) {
#e-admin-top-bar-root {
top: 46px;
}
}
@media (max-width: 768px) {
#e-admin-top-bar-root {
display: inline-flex;
align-items: center;
justify-content: center;
}
#e-admin-top-bar-root .e-admin-top-bar__main-area-buttons {
position: absolute;
top: calc(100% + 10px);
}
#e-admin-top-bar-root .e-admin-top-bar__secondary-area .e-admin-top-bar__secondary-area-buttons {
display: none;
}
#e-admin-top-bar-root .e-admin-top-bar__secondary-area > .e-admin-top-bar__bar-button .e-admin-top-bar__bar-button-title {
display: none;
}
}
@media (min-width: 768px) {
#e-admin-top-bar-root .e-admin-top-bar__secondary-area .e-admin-top-bar__secondary-area-buttons {
display: flex;
}
}
/*# sourceMappingURL=admin-top-bar.css.map */

View File

@@ -0,0 +1,2 @@
/*! elementor - v3.32.0 - 18-09-2025 */
#e-dashboard-widget-admin-top-bar{opacity:0;pointer-events:none;position:absolute}#e-admin-top-bar-root{background:var(--e-a-bg-default);box-shadow:0 4px 6px rgba(0,0,0,.03);display:none;font-family:var(--e-a-font-family);inset-block-start:0;inset-inline-end:0;position:absolute;width:calc(100% - 160px);z-index:1}body.folded #e-admin-top-bar-root{width:calc(100% - 36px)}#e-admin-top-bar-root .e-admin-top-bar{display:flex;height:50px;justify-content:space-between;padding:0 16px}#e-admin-top-bar-root .page-title-action{background-color:var(--e-a-btn-bg);border:none;border-radius:var(--e-a-border-radius);color:var(--e-a-btn-color-invert);font-size:12px;font-weight:500;line-height:1.2;outline:none;padding:8px 16px;text-decoration:none;text-transform:uppercase;transition:var(--e-a-transition-hover)}#e-admin-top-bar-root .page-title-action:hover{background-color:var(--e-a-btn-bg-hover);color:var(--e-a-btn-color-invert)}#e-admin-top-bar-root .e-admin-top-bar__heading{margin-inline-end:40px}#e-admin-top-bar-root .e-admin-top-bar__heading,#e-admin-top-bar-root .e-admin-top-bar__main-area{align-items:center;display:inline-flex;justify-content:center}#e-admin-top-bar-root .e-admin-top-bar__main-area button{margin:0 4px}#e-admin-top-bar-root .e-admin-top-bar__secondary-area{align-items:center;display:inline-flex;justify-content:center}#e-admin-top-bar-root .e-admin-top-bar__heading-title{color:var(--e-a-color-txt);font-size:15px;font-weight:700;line-height:normal;padding:0 8px}#e-admin-top-bar-root .e-admin-top-bar__main-area-buttons{display:inline-flex;gap:5px}#e-admin-top-bar-root.e-admin-top-bar--active{display:block}#e-admin-top-bar-root.e-admin-top-bar--active~#wpbody #wpbody-content{margin-block-start:50px}#e-admin-top-bar-root.e-admin-top-bar--active~#wpbody .wrap{clear:both;padding-block-start:10px}#e-admin-top-bar-root.e-admin-top-bar--active~#wpbody .wrap h1{display:none}#e-admin-top-bar-root:not(.e-admin-top-bar--active)~#wpbody .wrap .page-title-action,#e-admin-top-bar-root:not(.e-admin-top-bar--active)~#wpbody .wrap h1{display:inline-block}#e-admin-top-bar-root .e-admin-top-bar__bar-button{align-items:center;color:var(--e-a-color-txt);cursor:pointer;display:inline-flex;justify-content:center;margin:0 10px;text-decoration:none}#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent,#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent:hover .e-admin-top-bar__bar-button-icon,#e-admin-top-bar-root .e-admin-top-bar__bar-button.accent:hover .e-admin-top-bar__bar-button-title{color:var(--e-a-color-accent)}#e-admin-top-bar-root .e-admin-top-bar__bar-button .crown-icon{font-size:14px}#e-admin-top-bar-root .e-admin-top-bar__bar-button .e-admin-top-bar__bar-button-icon{margin:0 4px}#e-admin-top-bar-root .e-admin-top-bar__bar-button:hover .e-admin-top-bar__bar-button-icon,#e-admin-top-bar-root .e-admin-top-bar__bar-button:hover .e-admin-top-bar__bar-button-title{color:var(--e-a-color-txt-hover)}#e-admin-top-bar-root .e-admin-top-bar__bar-button-title{font-size:13px;font-weight:500;line-height:normal;margin:0 4px}#e-admin-top-bar-root~#wpbody .wrap .page-title-action,#e-admin-top-bar-root~#wpbody .wrap h1{display:none}@media screen and (max-width:960px){#e-admin-top-bar-root{width:calc(100% - 36px)}}@media screen and (max-width:782px){#e-admin-top-bar-root{width:100%}}@media screen and (max-width:600px){#e-admin-top-bar-root{top:46px}}@media (max-width:768px){#e-admin-top-bar-root{align-items:center;display:inline-flex;justify-content:center}#e-admin-top-bar-root .e-admin-top-bar__main-area-buttons{position:absolute;top:calc(100% + 10px)}#e-admin-top-bar-root .e-admin-top-bar__secondary-area .e-admin-top-bar__secondary-area-buttons,#e-admin-top-bar-root .e-admin-top-bar__secondary-area>.e-admin-top-bar__bar-button .e-admin-top-bar__bar-button-title{display:none}}@media (min-width:768px){#e-admin-top-bar-root .e-admin-top-bar__secondary-area .e-admin-top-bar__secondary-area-buttons{display:flex}}

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,107 @@
/*! elementor - v3.32.0 - 18-09-2025 */
:root {
--color-box-shadow-color: rgba(0, 0, 0, 0.05);
}
.eps-theme-dark {
--color-box-shadow-color: rgba(0, 0, 0, 0.1);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
@media screen and (min-width: 480px) {
.text-start-sm {
text-align: start;
}
}
@media screen and (min-width: 480px) {
.text-center-sm {
text-align: center;
}
}
@media screen and (min-width: 480px) {
.text-end-sm {
text-align: end;
}
}
@media screen and (min-width: 768px) {
.text-start-md {
text-align: start;
}
}
@media screen and (min-width: 768px) {
.text-center-md {
text-align: center;
}
}
@media screen and (min-width: 768px) {
.text-end-md {
text-align: end;
}
}
@media screen and (min-width: 1025px) {
.text-start-lg {
text-align: start;
}
}
@media screen and (min-width: 1025px) {
.text-center-lg {
text-align: center;
}
}
@media screen and (min-width: 1025px) {
.text-end-lg {
text-align: end;
}
}
@media screen and (min-width: 1440px) {
.text-start-xl {
text-align: start;
}
}
@media screen and (min-width: 1440px) {
.text-center-xl {
text-align: center;
}
}
@media screen and (min-width: 1440px) {
.text-end-xl {
text-align: end;
}
}
@media screen and (min-width: 1600px) {
.text-start-xxl {
text-align: start;
}
}
@media screen and (min-width: 1600px) {
.text-center-xxl {
text-align: center;
}
}
@media screen and (min-width: 1600px) {
.text-end-xxl {
text-align: end;
}
}
@keyframes eps-animation-pop {
from {
transform: scale(0.75);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
/*# sourceMappingURL=app-base-rtl.css.map */

View File

@@ -0,0 +1,2 @@
/*! elementor - v3.32.0 - 18-09-2025 */
:root{--color-box-shadow-color:rgba(0,0,0,.05)}.eps-theme-dark{--color-box-shadow-color:rgba(0,0,0,.1)}.sr-only{height:1px;margin:-1px;overflow:hidden;padding:0;position:absolute;width:1px;clip:rect(0,0,0,0);border:0;white-space:nowrap}@media screen and (min-width:480px){.text-start-sm{text-align:start}.text-center-sm{text-align:center}.text-end-sm{text-align:end}}@media screen and (min-width:768px){.text-start-md{text-align:start}.text-center-md{text-align:center}.text-end-md{text-align:end}}@media screen and (min-width:1025px){.text-start-lg{text-align:start}.text-center-lg{text-align:center}.text-end-lg{text-align:end}}@media screen and (min-width:1440px){.text-start-xl{text-align:start}.text-center-xl{text-align:center}.text-end-xl{text-align:end}}@media screen and (min-width:1600px){.text-start-xxl{text-align:start}.text-center-xxl{text-align:center}.text-end-xxl{text-align:end}}@keyframes eps-animation-pop{0%{opacity:0;transform:scale(.75)}to{opacity:1;transform:scale(1)}}

Some files were not shown because too many files have changed in this diff Show More